dashboard: hapus TOTAL VEX + perbaiki wrap mobile (V48) ; catatan stats-note dipendekkan (buang perbandingan vexascan) ; formula last_vote 52-pekan (V13/B12) ; image web rebuild + push
This commit is contained in:
1 parent
e9df89aaac
commit
0e83137b5a
8 files changed
+70
-34
No files matched your search
@@ -26,7 +26,7 @@ Two tools scan the Vexanium blockchain voters table for accounts whose only vote
|
|||||||
- The node sometimes returns `staked` as a string instead of a number — normalize coerces to float (spec §V8).
|
- The node sometimes returns `staked` as a string instead of a number — normalize coerces to float (spec §V8).
|
||||||
- The voters table has a sentinel first row (`owner="...........q"`, uint64 0) that must be skipped.
|
- The voters table has a sentinel first row (`owner="...........q"`, uint64 0) that must be skipped.
|
||||||
- Authoritative vote weight is `last_vote_weight` (high-precision decimal string) — stored verbatim as TEXT, never float-converted (spec §V6).
|
- Authoritative vote weight is `last_vote_weight` (high-precision decimal string) — stored verbatim as TEXT, never float-converted (spec §V6).
|
||||||
- Freshness filter (spec §V13/V40): `last_vote` (estimated last re-vote date) is derived at scan time from the Vexanium weight formula `last_vote_weight = staked_raw × 2^(years since 2000)` → `log2(weight / (staked×10000))`. Since V40 `normalize()` stores **every** BP voter with a derivable `last_vote` — any age; only unverifiable votes (zero/empty weight) are dropped. Banding (BARU/VALID/KADALUARSA), display and payout are computed from `last_vote` age at query time (dashboard cuts 3/28/31 days; distribution pays only `now−28d < last_vote ≤ now−3d`, plus REVOTE immediately, V42). The heuristic over-reads when a voter unstaked without re-voting (weight ÷ smaller stake ⇒ future date), so `normalize()` clamps any `last_vote` past scan time down to `now`. Since V41, `voter_first_seen.first_seen_at` separates PEMILIH BARU (new, first-seen within the 3-day maturity) from REVOTE (returning re-voters); since V42 REVOTE are VALID immediately (paid + shown in the main list, mint tag), only new voters wait out maturity; since V43 the mint tag lasts only `VEX_REVOTE_TAG_DAYS` (1) and VALID rows nearing the KADALUARSA cutoff (within `VEX_WARN_DAYS`, 3) get a `VOTE ULANG SEGERA` badge — display-only.
|
- Freshness filter (spec §V13/V40): `last_vote` (estimated last re-vote date) is derived at scan time from the Vexanium weight formula `last_vote_weight = staked_raw × 2^(weeks since 2000 / 52)` → `last_vote = 2000-01-01 + round(52 × log2(weight / (staked×10000))) weeks` — the chain (`stake2vote`) quantizes the exponent to whole weeks (`int64((now−2000-01-01)/(86400×7))/52.0`), so derived dates land on 7-day boundaries (B12; earlier 365.25-day divisor drifted ~33 days forward for 2026 votes). Since V40 `normalize()` stores **every** BP voter with a derivable `last_vote` — any age; only unverifiable votes (zero/empty weight) are dropped. Banding (BARU/VALID/KADALUARSA), display and payout are computed from `last_vote` age at query time (dashboard cuts 3/28/31 days; distribution pays only `now−28d < last_vote ≤ now−3d`, plus REVOTE immediately, V42). The heuristic over-reads when a voter unstaked without re-voting (weight ÷ smaller stake ⇒ future date), so `normalize()` clamps any `last_vote` past scan time down to `now`. Since V41, `voter_first_seen.first_seen_at` separates PEMILIH BARU (new, first-seen within the 3-day maturity) from REVOTE (returning re-voters); since V42 REVOTE are VALID immediately (paid + shown in the main list, mint tag), only new voters wait out maturity; since V43 the mint tag lasts only `VEX_REVOTE_TAG_DAYS` (1) and VALID rows nearing the KADALUARSA cutoff (within `VEX_WARN_DAYS`, 3) get a `VOTE ULANG SEGERA` badge — display-only.
|
||||||
- The web list shows RANK / AKUN / STAKE (VEX) / TOTAL REWARD (VEX) / VOTE TERAKHIR columns (3 stats cells). TOTAL REWARD = all-time sum of `distribute_payments.amount` where `status='sent'` (0,0000 for never-paid), joined per page via a LEFT JOIN subquery. Vote weight stays in the DB and `/api/search` JSON but is not rendered as a column.
|
- The web list shows RANK / AKUN / STAKE (VEX) / TOTAL REWARD (VEX) / VOTE TERAKHIR columns (3 stats cells). TOTAL REWARD = all-time sum of `distribute_payments.amount` where `status='sent'` (0,0000 for never-paid), joined per page via a LEFT JOIN subquery. Vote weight stays in the DB and `/api/search` JSON but is not rendered as a column.
|
||||||
- Token contract is `vex.token` (NOT `eosio.token` — that name doesn't exist on Vexanium), 4-decimal VEX, chain_id `f9f432b1851b5c179d2091a96f593aaed50ec7466b74f89301f957a83e56ce1f`. Distribution signs `vex.token::transfer` with the BP `active` key.
|
- Token contract is `vex.token` (NOT `eosio.token` — that name doesn't exist on Vexanium), 4-decimal VEX, chain_id `f9f432b1851b5c179d2091a96f593aaed50ec7466b74f89301f957a83e56ce1f`. Distribution signs `vex.token::transfer` with the BP `active` key.
|
||||||
- Distribution (spec §V19–V27): each run pays the whole liquid balance pro-rata by stored `staked`, shares floored at 4 decimals with dust left in the account, one transfer per voter. A txid that can't be confirmed (`GET {DATABISNIS_API}/v2/history/get_transaction?id=<txid>` returns no `executed`) is never re-sent the same run — that payment stays `failed` and rejoins the next run. No-op (exit 0, no writes) when the voters table is empty or balance < 0.0001. The dashboard's `/history` view renders `distribute_runs` + `distribute_payments` read-only.
|
- Distribution (spec §V19–V27): each run pays the whole liquid balance pro-rata by stored `staked`, shares floored at 4 decimals with dust left in the account, one transfer per voter. A txid that can't be confirmed (`GET {DATABISNIS_API}/v2/history/get_transaction?id=<txid>` returns no `executed`) is never re-sent the same run — that payment stays `failed` and rejoins the next run. No-op (exit 0, no writes) when the voters table is empty or balance < 0.0001. The dashboard's `/history` view renders `distribute_runs` + `distribute_payments` read-only.
|
||||||
@@ -44,7 +44,7 @@ Two tools scan the Vexanium blockchain voters table for accounts whose only vote
|
|||||||
- `claim.py` — klaim reward BP harian: baca `last_claim_time` (tabel `producers`, di-retry V46) → `next_window` = +24 jam (≥24 jam lewat → clamp ke now, recovery B8; semua pembacaan gagal → raise, ⊥ clamp prematur); tidur sampai mendekat, lalu poll `vexcore::claimrewards` (sign pyntelope `VEX_BP_PRIVATE_KEY`) tiap `CLAIM_RETRY_SECONDS`; deteksi mendarat via `_confirm_landed` di KEDUA jalur (send sukses & exception) = Hyperion `executed` ATAU `last_claim_time` maju dari baseline (V46/B11 — `send()` pyntelope ⊥ raise saat chain menolak, HTTP 500 sbg body); tak terkonfirmasi → retry senyap; reward = isi tx via Hyperion (bpay+vpay → BP, `reward_from_tx`) prioritas, selisih saldo fallback; reward ⊥ terukur → fee `pending` + notif `KLAIM MENDARAT · REWARD TAK TERUKUR` (V33/B10); fee 10% (`VEX_BP_FEE_PERCENT`) floor 4-des → `VEX_BP_FEE_WALLET` via `distribute.build_signed_transfer`, memo `BP FEE YYYY-MM-DD`; parse `get_table_rows` pakai `data.get('rows')` (bentuk asli dict, ⊥ `data[0]` — B7/V39); `step()` = state machine (resume fee → jadwal → poll), source of truth `claim_runs` (fee pending/failed diulang + resume saat restart). Modul logika (⊥ CLI). `test_claim.py` — oracle (mock chain+pyntelope, bentuk respons asli, temp DB).
|
- `claim.py` — klaim reward BP harian: baca `last_claim_time` (tabel `producers`, di-retry V46) → `next_window` = +24 jam (≥24 jam lewat → clamp ke now, recovery B8; semua pembacaan gagal → raise, ⊥ clamp prematur); tidur sampai mendekat, lalu poll `vexcore::claimrewards` (sign pyntelope `VEX_BP_PRIVATE_KEY`) tiap `CLAIM_RETRY_SECONDS`; deteksi mendarat via `_confirm_landed` di KEDUA jalur (send sukses & exception) = Hyperion `executed` ATAU `last_claim_time` maju dari baseline (V46/B11 — `send()` pyntelope ⊥ raise saat chain menolak, HTTP 500 sbg body); tak terkonfirmasi → retry senyap; reward = isi tx via Hyperion (bpay+vpay → BP, `reward_from_tx`) prioritas, selisih saldo fallback; reward ⊥ terukur → fee `pending` + notif `KLAIM MENDARAT · REWARD TAK TERUKUR` (V33/B10); fee 10% (`VEX_BP_FEE_PERCENT`) floor 4-des → `VEX_BP_FEE_WALLET` via `distribute.build_signed_transfer`, memo `BP FEE YYYY-MM-DD`; parse `get_table_rows` pakai `data.get('rows')` (bentuk asli dict, ⊥ `data[0]` — B7/V39); `step()` = state machine (resume fee → jadwal → poll), source of truth `claim_runs` (fee pending/failed diulang + resume saat restart). Modul logika (⊥ CLI). `test_claim.py` — oracle (mock chain+pyntelope, bentuk respons asli, temp DB).
|
||||||
- `claim_loop.py` — scheduler harian dalam container stack (service `claim`): `_wait_db` (duplikat scan_loop) lalu `claim.step()` terus, loop tak pernah keluar.
|
- `claim_loop.py` — scheduler harian dalam container stack (service `claim`): `_wait_db` (duplikat scan_loop) lalu `claim.step()` terus, loop tak pernah keluar.
|
||||||
- `telegram.py` — kirim notifikasi status distribusi + klaim via Telegram Bot API (best-effort; ⊥ token/chat id → kanal no-op senyap; gagal kirim → log saja). Dua kanal: `TELEGRAM_COMMUNITY_CHAT_IDS` (CSV) = brief mulai/selesai distribusi saja; `TELEGRAM_INTERNAL_CHAT_IDS` (CSV) = detail distribusi + semua kegagalan + klaim reward (⊥ komunitas, V30/V36). `send_text(text, chat_ids=None)` = komunitas, `send_internal(text)` = internal; dari env/`.env`/NFS `config.env`.
|
- `telegram.py` — kirim notifikasi status distribusi + klaim via Telegram Bot API (best-effort; ⊥ token/chat id → kanal no-op senyap; gagal kirim → log saja). Dua kanal: `TELEGRAM_COMMUNITY_CHAT_IDS` (CSV) = brief mulai/selesai distribusi saja; `TELEGRAM_INTERNAL_CHAT_IDS` (CSV) = detail distribusi + semua kegagalan + klaim reward (⊥ komunitas, V30/V36). `send_text(text, chat_ids=None)` = komunitas, `send_internal(text)` = internal; dari env/`.env`/NFS `config.env`.
|
||||||
- `dashboard.py` + `templates/index.html` + `templates/history.html` + `static/style.css` + `static/app.js` — Flask web dashboard; reads the store via `db.py`, styled per `DESIGN.md`; `app.js` = debounced live owner search (fetch `/api/search`), degrades to the server-side `?q=` GET form if JS is off. Three vote-age bands (V40): the voters list is split into BARU (top, `PEMILIH BARU · BELUM MATANG`) and the main list (`voter-list` section, `.reward` cells, V29) where KADALUARSA rows are pinned at the top with amber styling + `VOTE ULANG` badge + legend before the paged VALID rows, which LEFT-joins a `status='sent'` payments aggregate for the TOTAL REWARD column; since V42 the VALID list also holds REVOTE (returning re-voters, `voter_first_seen.first_seen_at ≤ now−3d`) tagged with a mint `REVOTE` tag (legend `TAG MINT = REVOTE KURANG DARI 1 HARI · VALID LANGSUNG`, shows only while `last_vote > now−VEX_REVOTE_TAG_DAYS`) — no separate band, no maturity wait; only genuinely new voters sit in BARU tagged `BARU`; since V43 VALID rows within `VEX_WARN_DAYS` of the KADALUARSA cutoff get an amber dashed `VOTE ULANG SEGERA` badge + legend `TAG AMBER = KADALUARSA DALAM 3 HARI · N PEMILIH` (display-only); search (`?q=` and `/api/search`) covers all bands and tags each result (`group` = `baru|revote|valid|kadaluarsa` + `expired` + `expiring`). Banner BERITA (V38) shows the next distribution run from `DISTRIBUTE_HOUR`/`DISTRIBUTE_WEEKDAY`/`DISTRIBUTE_FIRST_RUN` + `TZ` (mirror of `distribute_loop.next_boundary`, ⊥ impor lintas-image; drift guarded in test_dashboard); the `SETIAP ... · HH:00` recurrence line renders only for regular weekly rounds — hidden while the next event is the one-off first-run (`_is_first_run_next`, date ≠ weekly day). `/history` = riwayat distribusi read-only; `run-list` = six-column desktop/tablet grid, `pay-list` = four-column grid (AKUN/JUMLAH/STATUS/TXID) showing only the latest run with its date in the title, and labeled mobile cards.
|
- `dashboard.py` + `templates/index.html` + `templates/history.html` + `static/style.css` + `static/app.js` — Flask web dashboard; reads the store via `db.py`, styled per `DESIGN.md`; `app.js` = debounced live owner search (fetch `/api/search`), degrades to the server-side `?q=` GET form if JS is off. Three vote-age bands (V40): the voters list is split into BARU (top, `PEMILIH BARU · BELUM MATANG`) and the main list (`voter-list` section, `.reward` cells, V29) where KADALUARSA rows are pinned at the top with amber styling + `VOTE ULANG` badge + legend before the paged VALID rows, which LEFT-joins a `status='sent'` payments aggregate for the TOTAL REWARD column; since V42 the VALID list also holds REVOTE (returning re-voters, `voter_first_seen.first_seen_at ≤ now−3d`) tagged with a mint `REVOTE` tag (legend `TAG MINT = REVOTE KURANG DARI 1 HARI · VALID LANGSUNG`, shows only while `last_vote > now−VEX_REVOTE_TAG_DAYS`) — no separate band, no maturity wait; only genuinely new voters sit in BARU tagged `BARU`; since V43 VALID rows within `VEX_WARN_DAYS` of the KADALUARSA cutoff get an amber dashed `VOTE ULANG SEGERA` badge + legend `TAG AMBER = KADALUARSA DALAM 3 HARI · N PEMILIH` (display-only); search (`?q=` and `/api/search`) covers all bands and tags each result (`group` = `baru|revote|valid|kadaluarsa` + `expired` + `expiring`). Banner BERITA (V38) shows the next distribution run from `DISTRIBUTE_HOUR`/`DISTRIBUTE_WEEKDAY`/`DISTRIBUTE_FIRST_RUN` + `TZ` (mirror of `distribute_loop.next_boundary`, ⊥ impor lintas-image; drift guarded in test_dashboard); the `SETIAP ... · HH:00` recurrence line renders only for regular weekly rounds — hidden while the next event is the one-off first-run (`_is_first_run_next`, date ≠ weekly day). A `stats-note` caption states the list is single-vote-to-BP accounts ≥ min stake (V47). `/history` = riwayat distribusi read-only; `run-list` = six-column desktop/tablet grid, `pay-list` = four-column grid (AKUN/JUMLAH/STATUS/TXID) showing only the latest run with its date in the title, and labeled mobile cards.
|
||||||
- `gunicorn.conf.py` — gunicorn production config (bind/workers from `config`, sync worker). `run.sh` — launcher: `./run.sh` = `./venv/bin/gunicorn -c gunicorn.conf.py dashboard:app` from any cwd.
|
- `gunicorn.conf.py` — gunicorn production config (bind/workers from `config`, sync worker). `run.sh` — launcher: `./run.sh` = `./venv/bin/gunicorn -c gunicorn.conf.py dashboard:app` from any cwd.
|
||||||
- `config.py` — loads env/`.env` (python-dotenv) → `TARGET_BP`, `API_NODE`, `DB_PATH`, `DB_BACKEND`, `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASS`, `DB_NAME`, `MIN_STAKED_VEX`, `PAGE_SIZE`, `DASH_HOST`, `DASH_PORT`, `DASH_WORKERS`, `VEX_STALE_DAYS`, `VEX_MATURITY_DAYS`, `VEX_EXPIRED_DAYS`, `DATABISNIS_API`, `DASH_LIQUID_TTL`, `BP_PRIVATE_KEY`, `DISTRIBUTE_HOUR`, `DISTRIBUTE_MAX_ATTEMPTS`, `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_IDS`, `CLAIM_RETRY_SECONDS`, `BP_FEE_WALLET`, `BP_FEE_PERCENT`; shared by get_voters & dashboard.
|
- `config.py` — loads env/`.env` (python-dotenv) → `TARGET_BP`, `API_NODE`, `DB_PATH`, `DB_BACKEND`, `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASS`, `DB_NAME`, `MIN_STAKED_VEX`, `PAGE_SIZE`, `DASH_HOST`, `DASH_PORT`, `DASH_WORKERS`, `VEX_STALE_DAYS`, `VEX_MATURITY_DAYS`, `VEX_EXPIRED_DAYS`, `DATABISNIS_API`, `DASH_LIQUID_TTL`, `BP_PRIVATE_KEY`, `DISTRIBUTE_HOUR`, `DISTRIBUTE_MAX_ATTEMPTS`, `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_IDS`, `CLAIM_RETRY_SECONDS`, `BP_FEE_WALLET`, `BP_FEE_PERCENT`; shared by get_voters & dashboard.
|
||||||
- `scan_loop.py` — scheduler dalam container stack: menunggu batas `SCAN_HOURS` (lokal via `TZ`), panggil `get_voters.main()`; skan-awal `SCAN_RUN_ON_START` + tunggu DB siap; loop tak pernah keluar.
|
- `scan_loop.py` — scheduler dalam container stack: menunggu batas `SCAN_HOURS` (lokal via `TZ`), panggil `get_voters.main()`; skan-awal `SCAN_RUN_ON_START` + tunggu DB siap; loop tak pernah keluar.
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ V9: normalize drops voters where `staked` < 1000 VEX (setelah ÷10000)
|
|||||||
V10: dashboard baca `voters.db` → rows sort `staked` DESC (terbesar dulu); ⊥ mutasi DB
|
V10: dashboard baca `voters.db` → rows sort `staked` DESC (terbesar dulu); ⊥ mutasi DB
|
||||||
V11: search `q` → filter owner substring (LIKE, wildcard-escape, case-insensitive) di SEMUA band tampil (V40); urut+paging terjaga; `?q=` (daftar datar ber-tag) & `/api/search` read-only; tiap hasil membawa `group` (`baru`|`valid`|`kadaluarsa`) + `expired`; rank = ordinal DALAM kelompok (⊥ rank global lintas band, V40)
|
V11: search `q` → filter owner substring (LIKE, wildcard-escape, case-insensitive) di SEMUA band tampil (V40); urut+paging terjaga; `?q=` (daftar datar ber-tag) & `/api/search` read-only; tiap hasil membawa `group` (`baru`|`valid`|`kadaluarsa`) + `expired`; rank = ordinal DALAM kelompok (⊥ rank global lintas band, V40)
|
||||||
V12: produksi web disajikan gunicorn (`dashboard:app`, `gunicorn.conf.py`); `DASH_WORKERS` default 2; `.env` ikut termuat via import `config`
|
V12: produksi web disajikan gunicorn (`dashboard:app`, `gunicorn.conf.py`); `DASH_WORKERS` default 2; `.env` ikut termuat via import `config`
|
||||||
V13: `last_vote` = `2000-01-01 + log2(last_vote_weight / (staked×10000)) × 365.25 hari` (None bila bobot/stake nol); estimasi yang melewati `now` (unstake tanpa revote) diklamp ke `now`; voter dgn `last_vote=None` dibuang di `normalize` (bobot tak bisa diverifikasi → tanggal tak diketahui, ⊥ bisa diklasifikasi); V40: vote basi (> VEX_STALE_DAYS) TIDAK lagi dibuang di scan — SEMUA umur disimpan; banding/tampil/bayar menghitung umur saat query (⊥ peran scan)
|
V13: `last_vote` = epoch 2000-01-01 + `round(52 × log2(last_vote_weight / (staked×10000)))` PEKAN (Vexanium/EOSIO `stake2vote`: eksponen = `int64((now − epoch)/(86400×7)) / 52.0` → bobot dikuantisasi pekan utuh; jadi tanggal kelipatan 7 hari, 00:00:00; regresi oracle baris nyata `..tg` 1291 pekan → 2024-09-28, `1.crownz` 1314 pekan → 2025-03-08); None bila bobot/stake nol; estimasi yang melewati `now` (unstake tanpa revote) diklamp ke `now`; voter dgn `last_vote=None` dibuang di `normalize` (bobot tak bisa diverifikasi → tanggal tak diketahui, ⊥ bisa diklasifikasi); V40: vote basi (> VEX_STALE_DAYS) TIDAK lagi dibuang di scan — SEMUA umur disimpan; banding/tampil/bayar menghitung umur saat query (⊥ peran scan)
|
||||||
V15: web list = kolom RANK/AKUN/STAKE (VEX)/TOTAL REWARD (VEX)/VOTE TERAKHIR — ⊥ BOBOT SUARA; stats = 3 sel (TOTAL PEMILIH/TOTAL VEX/STAKE TERTINGGI) atas semua baris TAMPIL (V40: ⊖ yang tersembunyi > 31 hari); V40/V42/V43: BARU (`PEMILIH BARU · BELUM MATANG`, new saja) + daftar utama — kadaluarsa menyatu DI ATAS baris VALID (dipin, sorot amber + badge `VOTE ULANG` + legenda `BARIS AMBER = VOTE LEWAT …`), lalu VALID (pager) = matang-baru + REVOTE (tag mint `REVOTE` + legenda `TAG MINT = REVOTE KURANG DARI 1 HARI · VALID LANGSUNG`, V43) + badge amber dashed `VOTE ULANG SEGERA` (V43) pada baris `exp_cut < last_vote ≤ warn_cut` + legenda `TAG AMBER = KADALUARSA DALAM {WARN} HARI · N PEMILIH`; REVOTE ⊖ lagi bagian sendiri (V42); `/api/search` hasil `{owner,staked,weight,rank,last_vote,total_reward,group,expired,expiring}` — `group` = `baru|revote|valid|kadaluarsa` (weight disimpan & di-API, ⊥ dirender)
|
V15: web list = kolom RANK/AKUN/STAKE (VEX)/TOTAL REWARD (VEX)/VOTE TERAKHIR — ⊥ BOBOT SUARA; stats = SALDO LIQUID/TOTAL PEMILIH/STAKE TERTINGGI atas semua baris TAMPIL (V40: ⊖ yang tersembunyi > 31 hari); TOTAL VEX dihapus (V48: raw single-vote stake mudah disalahartikan); V40/V42/V43: BARU (`PEMILIH BARU · BELUM MATANG`, new saja) + daftar utama — kadaluarsa menyatu DI ATAS baris VALID (dipin, sorot amber + badge `VOTE ULANG` + legenda `BARIS AMBER = VOTE LEWAT …`), lalu VALID (pager) = matang-baru + REVOTE (tag mint `REVOTE` + legenda `TAG MINT = REVOTE KURANG DARI 1 HARI · VALID LANGSUNG`, V43) + badge amber dashed `VOTE ULANG SEGERA` (V43) pada baris `exp_cut < last_vote ≤ warn_cut` + legenda `TAG AMBER = KADALUARSA DALAM {WARN} HARI · N PEMILIH`; REVOTE ⊖ lagi bagian sendiri (V42); `/api/search` hasil `{owner,staked,weight,rank,last_vote,total_reward,group,expired,expiring}` — `group` = `baru|revote|valid|kadaluarsa` (weight disimpan & di-API, ⊥ dirender); V47: catatan `stats-note` menyatakan TOTAL PEMILIH = akun suara tunggal ke BP stake ≥ `MIN_STAKED_VEX`.
|
||||||
V16: saldo liquid akun `TARGET_BP` dari `GET {DATABISNIS_API}/v2/state/get_account` (parse `account.core_liquid_balance`) → sel stat SALDO LIQUID; cache in-memory per worker TTL `DASH_LIQUID_TTL`; gagal fetch → nilai lama (atau `—` bila belum pernah sukses) + `at` ikut diset (cooldown, ⊥ pukulan berulang); read-only, ⊥ tulis DB
|
V16: saldo liquid akun `TARGET_BP` dari `GET {DATABISNIS_API}/v2/state/get_account` (parse `account.core_liquid_balance`) → sel stat SALDO LIQUID; cache in-memory per worker TTL `DASH_LIQUID_TTL`; gagal fetch → nilai lama (atau `—` bila belum pernah sukses) + `at` ikut diset (cooldown, ⊥ pukulan berulang); read-only, ⊥ tulis DB
|
||||||
V17: penyimpanan lewat `db.py` (backend `VEX_DB_BACKEND` = `sqlite` default | `mysql`); query berbagi sintaks pakai placeholder `%s` (diterjemahkan `?` utk sqlite); LIKE escape pakai `ESCAPE '!'` — backslash memutus literal string MySQL (B2); `db.query` → list (fetchall sqlite=list / PyMySQL=tuple diseragamkan, B3); replace snapshot mysql = CREATE IF NOT EXISTS + DELETE + INSERT satu transaksi (MVCC → pembaca dapat snapshot konsisten saat ganti harian, tak kena torn read); oracle utama (test_get_voters/test_dashboard) tetap sqlite hermetik; `test_mariadb.py` opt-in (skip exit 0 bila `VEX_DB_BACKEND != mysql`)
|
V17: penyimpanan lewat `db.py` (backend `VEX_DB_BACKEND` = `sqlite` default | `mysql`); query berbagi sintaks pakai placeholder `%s` (diterjemahkan `?` utk sqlite); LIKE escape pakai `ESCAPE '!'` — backslash memutus literal string MySQL (B2); `db.query` → list (fetchall sqlite=list / PyMySQL=tuple diseragamkan, B3); replace snapshot mysql = CREATE IF NOT EXISTS + DELETE + INSERT satu transaksi (MVCC → pembaca dapat snapshot konsisten saat ganti harian, tak kena torn read); oracle utama (test_get_voters/test_dashboard) tetap sqlite hermetik; `test_mariadb.py` opt-in (skip exit 0 bila `VEX_DB_BACKEND != mysql`)
|
||||||
V18: stack produksi = Docker Swarm (`docker-compose.yml`, `docker stack deploy -c docker-compose.yml databisnisid`): mariadb bind mount `/data/db/mariadb/databisnisid/data` pd node `server5.saltis.id` + placement constraint `node.hostname == server5.saltis.id` (data node-lokal → mariadb harus selalu di node itu; di swarm yg ⊥ punya node itu task-nya Pending, bukan bug); web (gunicorn, publik `:5000`) + scan (`scan_loop.py`) + distribute (`distribute_loop.py`, image `databisnisid-dist`) di node mana pun, terhubung via overlay `appnet`; image dari registry `git.proit.id/proitlab/databisnisid-web` & `-scan` & `-dist` (⊥ `build:`); scan berjalan pd jam `SCAN_HOURS` (default 0,8,16, lokal `TZ` default Asia/Jakarta via `datetime.now()`) + skan-awal `SCAN_RUN_ON_START` (setelah DB siap) → tabel voters langsung ada (dashboard ⊥ 500); distribute loop harian `DISTRIBUTE_HOUR` (default 10, lokal) — ⊥ distribusi-awal saat start (snapshot bisa basi); key `VEX_BP_PRIVATE_KEY` dari bind `/mnt/nfs/server5.saltis.id/data/databisnisid/app/config.env:/app/.env:ro` dibaca `config.py` `load_dotenv` (⊥ interpolasi env compose); loop tak pernah keluar; `stack deploy` ⊥ `env_file` (env inline via `${VAR}` interpolasi `.env`); image web set `DASH_HOST=0.0.0.0` agar ingress menjangkau; healthcheck web = cek socket TCP (⊥ ketergantungan isi DB)
|
V18: stack produksi = Docker Swarm (`docker-compose.yml`, `docker stack deploy -c docker-compose.yml databisnisid`): mariadb bind mount `/data/db/mariadb/databisnisid/data` pd node `server5.saltis.id` + placement constraint `node.hostname == server5.saltis.id` (data node-lokal → mariadb harus selalu di node itu; di swarm yg ⊥ punya node itu task-nya Pending, bukan bug); web (gunicorn, publik `:5000`) + scan (`scan_loop.py`) + distribute (`distribute_loop.py`, image `databisnisid-dist`) di node mana pun, terhubung via overlay `appnet`; image dari registry `git.proit.id/proitlab/databisnisid-web` & `-scan` & `-dist` (⊥ `build:`); scan berjalan pd jam `SCAN_HOURS` (default 0,8,16, lokal `TZ` default Asia/Jakarta via `datetime.now()`) + skan-awal `SCAN_RUN_ON_START` (setelah DB siap) → tabel voters langsung ada (dashboard ⊥ 500); distribute loop harian `DISTRIBUTE_HOUR` (default 10, lokal) — ⊥ distribusi-awal saat start (snapshot bisa basi); key `VEX_BP_PRIVATE_KEY` dari bind `/mnt/nfs/server5.saltis.id/data/databisnisid/app/config.env:/app/.env:ro` dibaca `config.py` `load_dotenv` (⊥ interpolasi env compose); loop tak pernah keluar; `stack deploy` ⊥ `env_file` (env inline via `${VAR}` interpolasi `.env`); image web set `DASH_HOST=0.0.0.0` agar ingress menjangkau; healthcheck web = cek socket TCP (⊥ ketergantungan isi DB)
|
||||||
@@ -186,3 +186,4 @@ B8|2026-08-06|setelah jendela klaim lewat (poll 2 jam habis tanpa sukses) `next_
|
|||||||
B9|2026-08-07|notif `KLAIM REWARD SUKSES / Reward 0.0000` walau tx klaim TIDAK mendarat: `signed.send()` timeout + Hyperion sempat tak terjangkau → heuristik "last_claim_time dalam 24 jam" (klaim lama masih <24h) menganggap mendarat → reward diukur 0 (δ saldo ⊥ berubah) → fee `skipped` diam-diam + sukses-0|deteksi mendarat = Hyperion `executed` ATAU `last_claim_time` MAJU dari baseline (V45); reward ⊥ terukur → fee `pending` + notif cek manual (V33); `next_window` baca di-retry (⊥ clamp prematur)
|
B9|2026-08-07|notif `KLAIM REWARD SUKSES / Reward 0.0000` walau tx klaim TIDAK mendarat: `signed.send()` timeout + Hyperion sempat tak terjangkau → heuristik "last_claim_time dalam 24 jam" (klaim lama masih <24h) menganggap mendarat → reward diukur 0 (δ saldo ⊥ berubah) → fee `skipped` diam-diam + sukses-0|deteksi mendarat = Hyperion `executed` ATAU `last_claim_time` MAJU dari baseline (V45); reward ⊥ terukur → fee `pending` + notif cek manual (V33); `next_window` baca di-retry (⊥ clamp prematur)
|
||||||
B10|2026-08-07|reward klaim diukur dari selisih saldo liquid (setelah−sebelum): saat node baca saldo di belakang/fetch gagal, reward nyata (transfer bpay/vpay) ⊥ tercatat → fee ⊥ dikirim utk siklus itu (fee `skipped` permanen, V34 ⊥ retry skipped)|ukur reward dari isi tx klaim via Hyperion (bpay+vpay → BP) prioritas; selisih saldo jadi fallback; keduanya gagal → fee `pending` + notif (V33/V45)
|
B10|2026-08-07|reward klaim diukur dari selisih saldo liquid (setelah−sebelum): saat node baca saldo di belakang/fetch gagal, reward nyata (transfer bpay/vpay) ⊥ tercatat → fee ⊥ dikirim utk siklus itu (fee `skipped` permanen, V34 ⊥ retry skipped)|ukur reward dari isi tx klaim via Hyperion (bpay+vpay → BP) prioritas; selisih saldo jadi fallback; keduanya gagal → fee `pending` + notif (V33/V45)
|
||||||
B11|2026-08-07|notif `[OK] klaim reward 0.0000` STILL terbit walau tx klaim tak mendarat — V45 hanya mengerasankan jalur exception, tapi `signed.send()` pyntelope TIDAK raise saat chain menolak (`Net._request` kembalikan body HTTP 500 sbg JSON): klaim sebelum jendela → HTTP 500 → `send()` normal → `landed=True` di jalur sukses → reward 0 → `[OK]`/`KLAIM REWARD SUKSES` palsu|landing dikonfirmasi di KEDUA jalur via `_confirm_landed` (Hyperion executed ATAU last_claim_time maju dari baseline); tak terkonfirmasi → retry senyap, ⊥ catat/notif (V46); `next_window` baca producer gagal → raise (⊥ clamp prematur)
|
B11|2026-08-07|notif `[OK] klaim reward 0.0000` STILL terbit walau tx klaim tak mendarat — V45 hanya mengerasankan jalur exception, tapi `signed.send()` pyntelope TIDAK raise saat chain menolak (`Net._request` kembalikan body HTTP 500 sbg JSON): klaim sebelum jendela → HTTP 500 → `send()` normal → `landed=True` di jalur sukses → reward 0 → `[OK]`/`KLAIM REWARD SUKSES` palsu|landing dikonfirmasi di KEDUA jalur via `_confirm_landed` (Hyperion executed ATAU last_claim_time maju dari baseline); tak terkonfirmasi → retry senyap, ⊥ catat/notif (V46); `next_window` baca producer gagal → raise (⊥ clamp prematur)
|
||||||
|
B12|2026-08-07|`derive_last_vote` pakai `365.25 hari/tahun` tapi Vexanium (`stake2vote`) mengkuantisasi bobot ke PEKAN UTUH (`int64((now−2000)/(86400×7))/52.0`) — drift ~33 hari ke depan utk vote 2026 (365.25 vs 364 hari/eksponen); jendela maturity 3/28/31 hari jadi miring ~sebulan|rekonstruksi = `epoch + round(52×log2(ratio)) pekan` (kelipatan 7 hari); oracle `_fresh_weight` + 2^26 + regresi baris nyata (..tg, 1.crownz) (V13)
|
||||||
+4
-5
@@ -408,9 +408,9 @@ def _stats():
|
|||||||
new_cut, warn_cut, exp_cut, hid_cut = _freshness_cutoffs()
|
new_cut, warn_cut, exp_cut, hid_cut = _freshness_cutoffs()
|
||||||
revote_cut = _revote_cut()
|
revote_cut = _revote_cut()
|
||||||
row = db.queryone(
|
row = db.queryone(
|
||||||
'SELECT COUNT(*), COALESCE(SUM(staked), 0), COALESCE(MAX(staked), 0) '
|
'SELECT COUNT(*), COALESCE(MAX(staked), 0) '
|
||||||
'FROM voters WHERE last_vote > %s', [hid_cut])
|
'FROM voters WHERE last_vote > %s', [hid_cut])
|
||||||
total_voters, total_staked, top_staked = row
|
total_voters, top_staked = row
|
||||||
counts = db.queryone(
|
counts = db.queryone(
|
||||||
'SELECT '
|
'SELECT '
|
||||||
' COALESCE(SUM(CASE WHEN v.last_vote > %s '
|
' COALESCE(SUM(CASE WHEN v.last_vote > %s '
|
||||||
@@ -438,7 +438,7 @@ def _stats():
|
|||||||
n_new, n_revote, n_expired, n_valid, n_expiring = counts
|
n_new, n_revote, n_expired, n_valid, n_expiring = counts
|
||||||
scanned = db.queryone(
|
scanned = db.queryone(
|
||||||
'SELECT COALESCE(MAX(scanned_at), \'\') FROM voters')[0]
|
'SELECT COALESCE(MAX(scanned_at), \'\') FROM voters')[0]
|
||||||
return (total_voters, total_staked, top_staked, scanned,
|
return (total_voters, top_staked, scanned,
|
||||||
n_new, n_revote, n_expired, n_valid, n_expiring)
|
n_new, n_revote, n_expired, n_valid, n_expiring)
|
||||||
|
|
||||||
|
|
||||||
@@ -510,7 +510,7 @@ def index():
|
|||||||
q = request.args.get('q', '').strip()[:MAX_Q_LEN]
|
q = request.args.get('q', '').strip()[:MAX_Q_LEN]
|
||||||
_ensure_dist_schema()
|
_ensure_dist_schema()
|
||||||
_ensure_first_seen_schema()
|
_ensure_first_seen_schema()
|
||||||
(total_voters, total_staked, top_staked, scanned,
|
(total_voters, top_staked, scanned,
|
||||||
n_new, n_revote, n_expired, n_valid, n_expiring) = _stats()
|
n_new, n_revote, n_expired, n_valid, n_expiring) = _stats()
|
||||||
liquid_vex = _get_liquid_vex()
|
liquid_vex = _get_liquid_vex()
|
||||||
|
|
||||||
@@ -539,7 +539,6 @@ def index():
|
|||||||
page=page,
|
page=page,
|
||||||
total_pages=total_pages,
|
total_pages=total_pages,
|
||||||
total_voters=total_voters,
|
total_voters=total_voters,
|
||||||
total_staked=total_staked,
|
|
||||||
top_staked=top_staked,
|
top_staked=top_staked,
|
||||||
liquid_vex=liquid_vex,
|
liquid_vex=liquid_vex,
|
||||||
scanned=scanned,
|
scanned=scanned,
|
||||||
|
|||||||
+9
-5
@@ -28,7 +28,8 @@ LIMIT = 500
|
|||||||
# Ambang-batas nama akun "0" (kodifikasi uint64 0) pada tabel voters
|
# Ambang-batas nama akun "0" (kodifikasi uint64 0) pada tabel voters
|
||||||
ZERO_OWNER = '...........q'
|
ZERO_OWNER = '...........q'
|
||||||
|
|
||||||
# Skala stake (×10000) dan epoch bobot suara Vexanium (1 Jan 2000)
|
# Skala stake (×10000) dan epoch bobot suara Vexanium (1 Jan 2000,
|
||||||
|
# block_timestamp_epoch). Eksponen bobot = PEKAN utuh sejak epoch ÷ 52.
|
||||||
VEX_SCALE = 10000
|
VEX_SCALE = 10000
|
||||||
WEIGHT_EPOCH = datetime(2000, 1, 1)
|
WEIGHT_EPOCH = datetime(2000, 1, 1)
|
||||||
|
|
||||||
@@ -36,8 +37,11 @@ WEIGHT_EPOCH = datetime(2000, 1, 1)
|
|||||||
def derive_last_vote(weight, staked_vex):
|
def derive_last_vote(weight, staked_vex):
|
||||||
"""V13: perkiraan waktu revote terakhir dari rasio bobot/stake.
|
"""V13: perkiraan waktu revote terakhir dari rasio bobot/stake.
|
||||||
|
|
||||||
Vexanium: last_vote_weight = staked_raw × 2^(tahun sejak 2000).
|
Vexanium (sistem EOSIO): `last_vote_weight = staked_raw × 2^(pekan/52)`
|
||||||
Maka t = log2(last_vote_weight / staked_raw) → tanggal revote.
|
— eksponen = `int64((now − 2000-01-01) / (86400×7)) / 52.0`, jadi bobot
|
||||||
|
dikuantisasi ke PEKAN UTUH sejak epoch 2000 (stake2vote). Maka
|
||||||
|
`t_pekan = round(52 × log2(last_vote_weight / staked_raw))` → tanggal
|
||||||
|
revote = epoch + t_pekan pekan (kelipatan 7 hari, 00:00:00).
|
||||||
Kembalikan None bila tak bisa dihitung (bobot/stake nol atau tak valid).
|
Kembalikan None bila tak bisa dihitung (bobot/stake nol atau tak valid).
|
||||||
"""
|
"""
|
||||||
staked_raw = staked_vex * VEX_SCALE
|
staked_raw = staked_vex * VEX_SCALE
|
||||||
@@ -49,8 +53,8 @@ def derive_last_vote(weight, staked_vex):
|
|||||||
return None
|
return None
|
||||||
if ratio <= 0:
|
if ratio <= 0:
|
||||||
return None
|
return None
|
||||||
t_years = math.log(ratio) / math.log(2)
|
t_weeks = round(52 * math.log(ratio) / math.log(2))
|
||||||
return (WEIGHT_EPOCH + timedelta(days=t_years * 365.25)).isoformat(timespec='seconds')
|
return (WEIGHT_EPOCH + timedelta(weeks=t_weeks)).isoformat(timespec='seconds')
|
||||||
|
|
||||||
|
|
||||||
def fetch_with_retry(payload, max_retries=3, timeout=30):
|
def fetch_with_retry(payload, max_retries=3, timeout=30):
|
||||||
|
|||||||
+14
-4
@@ -147,11 +147,17 @@ body {
|
|||||||
/* ————— stats band ————— */
|
/* ————— stats band ————— */
|
||||||
.stats {
|
.stats {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
border-top: 1px solid var(--hairline);
|
border-top: 1px solid var(--hairline);
|
||||||
border-bottom: 1px solid var(--hairline);
|
border-bottom: 1px solid var(--hairline);
|
||||||
margin: 0 var(--sp-lg) var(--sp-xl);
|
margin: 0 var(--sp-lg) var(--sp-xl);
|
||||||
}
|
}
|
||||||
|
.stats-note {
|
||||||
|
margin: calc(-1 * var(--sp-md)) var(--sp-lg) var(--sp-xl);
|
||||||
|
color: var(--muted-soft);
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
}
|
||||||
.spec-cell {
|
.spec-cell {
|
||||||
padding: var(--sp-lg) var(--sp-md);
|
padding: var(--sp-lg) var(--sp-md);
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -248,6 +254,7 @@ button.pill {
|
|||||||
padding-top: var(--sp-md);
|
padding-top: var(--sp-md);
|
||||||
}
|
}
|
||||||
.list-row:nth-child(odd) { background: var(--surface-soft); }
|
.list-row:nth-child(odd) { background: var(--surface-soft); }
|
||||||
|
.list-row > * { min-width: 0; }
|
||||||
.rank,
|
.rank,
|
||||||
.vote-date {
|
.vote-date {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
@@ -260,7 +267,7 @@ button.pill {
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
color: var(--on-dark);
|
color: var(--on-dark);
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ————— band BARU / KADALUARSA (V40) ————— */
|
/* ————— band BARU / KADALUARSA (V40) ————— */
|
||||||
@@ -439,13 +446,16 @@ button.pill {
|
|||||||
.search .pill { justify-content: center; width: 100%; }
|
.search .pill { justify-content: center; width: 100%; }
|
||||||
|
|
||||||
.list-header { display: none; }
|
.list-header { display: none; }
|
||||||
.list-row {
|
.voter-list .list-row,
|
||||||
grid-template-columns: 48px 1fr;
|
.band-list .list-row {
|
||||||
|
grid-template-columns: 48px minmax(0, 1fr);
|
||||||
gap: var(--sp-xxs) var(--sp-sm);
|
gap: var(--sp-xxs) var(--sp-sm);
|
||||||
padding: var(--sp-lg) var(--sp-sm);
|
padding: var(--sp-lg) var(--sp-sm);
|
||||||
}
|
}
|
||||||
.list-row .rank { grid-row: 1 / span 3; grid-column: 1; align-self: start; padding-top: 6px; }
|
.list-row .rank { grid-row: 1 / span 3; grid-column: 1; align-self: start; padding-top: 6px; }
|
||||||
.list-row .owner { grid-column: 2; font-size: 18px; }
|
.list-row .owner { grid-column: 2; font-size: 18px; }
|
||||||
|
.list-row > * { min-width: 0; }
|
||||||
|
.list-row .owner { overflow-wrap: break-word; }
|
||||||
.list-row .stake,
|
.list-row .stake,
|
||||||
.list-row .vote-date,
|
.list-row .vote-date,
|
||||||
.list-row .txid,
|
.list-row .txid,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Saira+Condensed:wght@400&family=EB+Garamond:ital,wght@0,400;1,400&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Saira+Condensed:wght@400&family=EB+Garamond:ital,wght@0,400;1,400&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css', v=14) }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css', v=15) }}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="top-nav">
|
<header class="top-nav">
|
||||||
@@ -41,16 +41,14 @@
|
|||||||
<span class="cell-value">{{ total_voters }}</span>
|
<span class="cell-value">{{ total_voters }}</span>
|
||||||
<span class="cell-label">TOTAL PEMILIH</span>
|
<span class="cell-label">TOTAL PEMILIH</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="spec-cell">
|
|
||||||
<span class="cell-value">{{ total_staked|vex }}</span>
|
|
||||||
<span class="cell-label">TOTAL VEX</span>
|
|
||||||
</div>
|
|
||||||
<div class="spec-cell">
|
<div class="spec-cell">
|
||||||
<span class="cell-value">{{ top_staked|vex }}</span>
|
<span class="cell-value">{{ top_staked|vex }}</span>
|
||||||
<span class="cell-label">STAKE TERTINGGI</span>
|
<span class="cell-label">STAKE TERTINGGI</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<p class="caption stats-note">DAFTAR INI = AKUN SUARA TUNGGAL KE {{ bp }} · STAKE ≥ 1.000 VEX</p>
|
||||||
|
|
||||||
<form class="search" method="get" action="{{ url_for('index') }}">
|
<form class="search" method="get" action="{{ url_for('index') }}">
|
||||||
<input class="search-input" type="search" name="q" id="search-input"
|
<input class="search-input" type="search" name="q" id="search-input"
|
||||||
value="{{ q }}" placeholder="CARI AKUN..." autocomplete="off">
|
value="{{ q }}" placeholder="CARI AKUN..." autocomplete="off">
|
||||||
|
|||||||
+7
-2
@@ -110,12 +110,17 @@ def main():
|
|||||||
check('V15 kolom BOBOT SUARA tak tampil', 'BOBOT SUARA' not in html)
|
check('V15 kolom BOBOT SUARA tak tampil', 'BOBOT SUARA' not in html)
|
||||||
check('V15 tak ada sel .weight', 'class="weight"' not in html)
|
check('V15 tak ada sel .weight', 'class="weight"' not in html)
|
||||||
check('V15 tak ada penanda basi', 'stale-mark' not in html and 'BASI' not in html)
|
check('V15 tak ada penanda basi', 'stale-mark' not in html and 'BASI' not in html)
|
||||||
check('V16 4 sel stats (3 pemilih + saldo liquid)',
|
check('V47 3 sel stats (2 pemilih + saldo liquid)',
|
||||||
html.count('spec-cell') == 4)
|
html.count('spec-cell') == 3)
|
||||||
|
check('V47 TOTAL VEX dihapus dari dashboard',
|
||||||
|
'TOTAL VEX' not in html)
|
||||||
check('V16 sel SALDO LIQUID', 'SALDO LIQUID' in html)
|
check('V16 sel SALDO LIQUID', 'SALDO LIQUID' in html)
|
||||||
check('V16 SALDO LIQUID paling kiri (sblm TOTAL PEMILIH)',
|
check('V16 SALDO LIQUID paling kiri (sblm TOTAL PEMILIH)',
|
||||||
html.index('SALDO LIQUID') < html.index('TOTAL PEMILIH'))
|
html.index('SALDO LIQUID') < html.index('TOTAL PEMILIH'))
|
||||||
check('V16 nilai saldo diformat id-ID', '50.691,4676' in html)
|
check('V16 nilai saldo diformat id-ID', '50.691,4676' in html)
|
||||||
|
check('V47 catatan definisi total vs vexascan',
|
||||||
|
'SUARA TUNGGAL KE' in html
|
||||||
|
and 'BUKAN TOTAL VOTE-WEIGHT' not in html)
|
||||||
check('V40 TOTAL PEMILIH = baris tampil (129)',
|
check('V40 TOTAL PEMILIH = baris tampil (129)',
|
||||||
'<span class="cell-value">129</span>' in html)
|
'<span class="cell-value">129</span>' in html)
|
||||||
|
|
||||||
|
|||||||
+28
-9
@@ -19,9 +19,10 @@ import get_voters as gv
|
|||||||
|
|
||||||
|
|
||||||
def _fresh_weight(staked_raw, days_ago=5):
|
def _fresh_weight(staked_raw, days_ago=5):
|
||||||
"""Bobot untuk revote ~sekarang: staked_raw × 2^(tahun sejak 2000)."""
|
"""Bobot utk revote ~sekarang: staked_raw × 2^(pekan/52) — kuantisasi
|
||||||
t = ((datetime.now() - gv.WEIGHT_EPOCH).days - days_ago) / 365.25
|
pekan utuh sejak epoch 2000 (formula stake2vote Vexanium)."""
|
||||||
return f'{staked_raw * 2 ** t:.17f}'
|
weeks = ((datetime.now() - gv.WEIGHT_EPOCH).days - days_ago) // 7
|
||||||
|
return f'{staked_raw * 2 ** (weeks / 52.0):.17f}'
|
||||||
|
|
||||||
|
|
||||||
class FakePayload:
|
class FakePayload:
|
||||||
@@ -126,11 +127,17 @@ def main():
|
|||||||
(datetime.now() - datetime.fromisoformat(by['zzz9']['last_vote'])).days
|
(datetime.now() - datetime.fromisoformat(by['zzz9']['last_vote'])).days
|
||||||
> 800)
|
> 800)
|
||||||
|
|
||||||
# V13: formula — staked_raw × 2^26 → ~2026-01-01 (±2 hari, toleransi 365.25 hari/tahun)
|
# V13: formula — staked_raw × 2^(pekan/52), kuantisasi pekan utuh sejak 2000
|
||||||
lv = gv.derive_last_vote(f'{12000000 * 2 ** 26}.00000000000000000', 1200.0)
|
# 2^1 → 52 pekan setelah 2000-01-01 = 2000-12-30 (52×7 = 364 hari)
|
||||||
|
lv = gv.derive_last_vote(f'{12000000 * 2 ** 1:.17f}', 1200.0)
|
||||||
lv_dt = datetime.fromisoformat(lv) if lv else None
|
lv_dt = datetime.fromisoformat(lv) if lv else None
|
||||||
check('V13 formula 2^26 → ~2026-01-01',
|
check('V13 formula 2^1 (52 pekan) → 2000-12-30',
|
||||||
lv_dt is not None and abs((lv_dt - datetime(2026, 1, 1)).days) <= 2)
|
lv_dt is not None and lv_dt == datetime(2000, 12, 30))
|
||||||
|
# 2^26 → 26×52 pekan = 1352 pekan setelah 2000 = 2025-11-29
|
||||||
|
lv = gv.derive_last_vote(f'{12000000 * 2 ** 26:.17f}', 1200.0)
|
||||||
|
lv_dt = datetime.fromisoformat(lv) if lv else None
|
||||||
|
check('V13 formula 2^26 (1352 pekan) → 2025-11-29',
|
||||||
|
lv_dt is not None and lv_dt == datetime(2025, 11, 29))
|
||||||
check('V13 rasio 1.0 → epoch 2000',
|
check('V13 rasio 1.0 → epoch 2000',
|
||||||
gv.derive_last_vote('12000000.00000000000000000', 1200.0) == '2000-01-01T00:00:00')
|
gv.derive_last_vote('12000000.00000000000000000', 1200.0) == '2000-01-01T00:00:00')
|
||||||
check('V13 bobot nol → None', gv.derive_last_vote('0', 1200.0) is None)
|
check('V13 bobot nol → None', gv.derive_last_vote('0', 1200.0) is None)
|
||||||
@@ -138,6 +145,17 @@ def main():
|
|||||||
check('V13 bobot tak valid → None', gv.derive_last_vote('abc', 1200.0) is None)
|
check('V13 bobot tak valid → None', gv.derive_last_vote('abc', 1200.0) is None)
|
||||||
check('V13 stake nol → None', gv.derive_last_vote('1.0', 0.0) is None)
|
check('V13 stake nol → None', gv.derive_last_vote('1.0', 0.0) is None)
|
||||||
|
|
||||||
|
# V13/B12: regresi formula pekan utuh — baris nyata dari chain
|
||||||
|
# ..tg: staked_raw 9000, weight → t×52 = 1291 pekan → 2024-09-28
|
||||||
|
lv = gv.derive_last_vote('267849632990.26596069335937500', 9000 / 10000)
|
||||||
|
check('V13 regresi ..tg (1291 pekan) → 2024-09-28',
|
||||||
|
lv == '2024-09-28T00:00:00')
|
||||||
|
# 1.crownz: staked_raw 17115095, t×52 = 1314 pekan → 2025-03-08
|
||||||
|
lv = gv.derive_last_vote(
|
||||||
|
f'{17115095 * 2 ** (1314 / 52.0):.17f}', 17115095 / 10000)
|
||||||
|
check('V13 regresi 1.crownz (1314 pekan) → 2025-03-08',
|
||||||
|
lv == '2025-03-08T00:00:00')
|
||||||
|
|
||||||
# V13/V40: normalize menyimpan basi, membuang yang tak dapat diverifikasi
|
# V13/V40: normalize menyimpan basi, membuang yang tak dapat diverifikasi
|
||||||
old = gv.normalize({'owner': 'old8', 'producers': [gv.TARGET_BP],
|
old = gv.normalize({'owner': 'old8', 'producers': [gv.TARGET_BP],
|
||||||
'staked': 20000000,
|
'staked': 20000000,
|
||||||
@@ -154,10 +172,11 @@ def main():
|
|||||||
|
|
||||||
# V13: estimasi yang melewati now (unstake tanpa revote) diklamp ke now
|
# V13: estimasi yang melewati now (unstake tanpa revote) diklamp ke now
|
||||||
fixed_now = datetime(2026, 8, 5, 12, 0, 0)
|
fixed_now = datetime(2026, 8, 5, 12, 0, 0)
|
||||||
t_future = ((fixed_now - gv.WEIGHT_EPOCH).days + 5) / 365.25
|
weeks_future = ((fixed_now - gv.WEIGHT_EPOCH).days + 14) // 7 # 2 pekan ke depan
|
||||||
fut = gv.normalize({'owner': 'fut9', 'producers': [gv.TARGET_BP],
|
fut = gv.normalize({'owner': 'fut9', 'producers': [gv.TARGET_BP],
|
||||||
'staked': 20000000,
|
'staked': 20000000,
|
||||||
'last_vote_weight': f'{20000000 * 2 ** t_future:.17f}'},
|
'last_vote_weight':
|
||||||
|
f'{20000000 * 2 ** (weeks_future / 52.0):.17f}'},
|
||||||
now=fixed_now)
|
now=fixed_now)
|
||||||
check('V13 last_vote masa depan diklamp ke now',
|
check('V13 last_vote masa depan diklamp ke now',
|
||||||
fut is not None
|
fut is not None
|
||||||
|
|||||||
Reference in new issue
Block a user