Compare commits

...
2 Commits
4 changed files with 149 additions and 22 deletions

No files matched your search

+2 -2
View File
@@ -7,7 +7,7 @@ Two tools scan the Vexanium blockchain voters table for accounts whose only vote
- Node reference script: `node get_voters.js` (no deps, Node 18+).
- Python tool (production): `./venv/bin/python get_voters.py`. venv is Python 3.12, deps `requests` + `flask` + `python-dotenv` + `gunicorn` + `pymysql` (mysql backend only) + `pyntelope` (distribute signing) (`requirements.txt`). Install with `./venv/bin/pip install -r requirements.txt`.
- Daily payout (production): `./venv/bin/python distribute.py` — reads the current voters snapshot **restricted to the reward window** (not stale AND account mature, `last_vote > now−28d` AND `first_seen_at ≤ now−3d`, §V40/V52: new voters (`first_seen` recent/NULL) and stale voters get nothing — returning re-voters with mature first_seen are paid regardless of vote age), fetches the BP account's liquid VEX, splits it pro-rata by stake (floor 4-dec, dust stays in the account), and pushes one signed `vex.token::transfer` per voter with memo `DATABISNISID PROFIT SHARE YYYY-MM-DD`. Preview the plan without signing/writing: `./venv/bin/python distribute.py --dry-run`. Requires `VEX_BP_PRIVATE_KEY` (BP active key) in env/`.env`; without it `--dry-run` still works, a real run raises. A real run is a no-op (exit 0) unless `DISTRIBUTE_ENABLED=true` — default is off (kill-switch, V35); `--dry-run` always works. Failed rows are recorded `failed` and rejoined by the next day's run — no manual cleanup needed.
- Daily reward claim (production): `./venv/bin/python claim_loop.py` — the `claim` service scheduler. `claim.py` computes the claim window as `last_claim_time + 24h` (from the `vexcore` `producers` table) in **UTC-naive time** (`_utcnow()`, matching the chain's UTC `last_claim_time`; the container's local TZ must not shift the window — V49/B13), sleeps until near it, then polls `vexcore::claimrewards` every `CLAIM_RETRY_SECONDS` (default 60) until the chain accepts; if 24h have already elapsed since the last claim the window clamps to now (missed-window recovery, no permanent spin-timeout). A claim is only treated as "landed" if Hyperion says `executed` OR `last_claim_time` actually advanced past its pre-send value (`_claim_time_advanced`) — on **both** the send-success and send-exception paths (`_confirm_landed`), because pyntelope's `send()` does NOT raise on a chain rejection (HTTP 500 comes back as a JSON body, V46/B11), and Hyperion returning `executed: false` for a just-landed-but-not-yet-indexed tx must NOT count as "didn't land" (V50/B14). Unconfirmed ⇒ silent `retry` (no `claim_runs` row, no success notify) — self-heals when Hyperion indexes the tx on the next poll. `next_window` raises if all producer reads fail (never clamps to now prematurely). Reward is measured **from the claim tx itself** (sum of `vex.bpay`+`vex.vpay` → BP transfers via Hyperion, `reward_from_tx`), falling back to the liquid-balance delta if Hyperion is down; if neither can be read the claim is recorded with fee `pending` and an internal `KLAIM MENDARAT · REWARD TAK TERUKUR` alert is sent (never a misleading 0-reward success). 10% (`VEX_BP_FEE_PERCENT`) is transferred to `VEX_BP_FEE_WALLET` (default `bpdbsjasprod`) with memo `BP FEE YYYY-MM-DD`, reusing `distribute.build_signed_transfer`. A claim cycle is complete only when the claim is recorded in `claim_runs` AND the fee is sent; an unsent fee (`pending`/`failed`) is retried each cycle and resumed on restart (crash-safe). No mutex with `distribute.py` — distribution freezes the balance at run start, so a mid-run claim is deferred to the next run.
- Daily reward claim (production): `./venv/bin/python claim_loop.py` — the `claim` service scheduler. `claim.py` computes the claim window as `last_claim_time + 24h` (from the `vexcore` `producers` table) in **UTC-naive time** (`_utcnow()`, matching the chain's UTC `last_claim_time`; the container's local TZ must not shift the window — V49/B13), sleeps until near it, then polls `vexcore::claimrewards` every `CLAIM_RETRY_SECONDS` (default 60) until the chain accepts; if 24h have already elapsed since the last claim the window clamps to now (missed-window recovery, no permanent spin-timeout). A claim is only treated as "landed" if Hyperion says `executed` OR `last_claim_time` actually advanced past its pre-send value (`_claim_time_advanced`) — on **both** the send-success and send-exception paths (`_confirm_landed`), because pyntelope's `send()` does NOT raise on a chain rejection (HTTP 500 comes back as a JSON body, V46/B11), and Hyperion returning `executed: false` for a just-landed-but-not-yet-indexed tx must NOT count as "didn't land" (V50/B14). Unconfirmed ⇒ silent `retry` (no `claim_runs` row, no success notify) — self-heals when Hyperion indexes the tx on the next poll. The landing baseline (`last_claim_time` before send) is captured **once per poll window** in `poll_claim` and passed to every `_try_claim_once` — it must NOT be re-read per attempt, or a landed-but-stale-first-read claim falls into a permanent retry loop (V54/B16); `_confirm_landed` also retries the post-send `last_claim_time` read so a lagging RPC node doesn't cause a false negative. `next_window` raises if all producer reads fail (never clamps to now prematurely). Reward is measured **from the claim tx itself** (sum of `vex.bpay`+`vex.vpay` → BP transfers via Hyperion, `reward_from_tx`), **retried up to 4× at `SETTLE_SECONDS` (30s)** so a just-landed tx not yet indexed by Hyperion doesn't read as `None` (V55/B17); if Hyperion is down it falls back to the liquid-balance delta, but **only a positive delta (`after > before`) counts as measured** — a 0 delta is ambiguous (stale node balance) and is treated as unmeasured, never as a genuine zero; if neither can be read the claim is recorded with fee `pending` and an internal `KLAIM MENDARAT · REWARD TAK TERUKUR` alert is sent (never a misleading 0-reward success — a real 0-reward claim must be evidenced by `reward_from_tx` returning `Decimal('0')` from an executed tx). 10% (`VEX_BP_FEE_PERCENT`) is transferred to `VEX_BP_FEE_WALLET` (default `bpdbsjasprod`) with memo `BP FEE YYYY-MM-DD`, reusing `distribute.build_signed_transfer`. A claim cycle is complete only when the claim is recorded in `claim_runs` AND the fee is sent; an unsent fee (`pending`/`failed`) is retried each cycle and resumed on restart (crash-safe). No mutex with `distribute.py` — distribution freezes the balance at run start, so a mid-run claim is deferred to the next run.
- Storage backend: `db.py` abstracts it. Default `sqlite` (`VEX_DB_PATH`, stdlib `sqlite3`, WAL). Optional `mysql` (`VEX_DB_BACKEND=mysql` + `VEX_DB_HOST/PORT/USER/PASS/NAME`, PyMySQL). Oracle tests stay on sqlite; `test_mariadb.py` is opt-in (skips unless `VEX_DB_BACKEND=mysql`). Query SQL is written once with `%s` placeholders (translated to `?` for sqlite); `db.query` always returns a list.
- Test MariaDB/MySQL via docker: `docker compose -f docker-compose.dev.yml up -d` (mariadb:11 container `databisnisid-mariadb`, localhost-only `127.0.0.1:3306`, db/user/pass `databisnisid`/`databisnis`/`databisnis`, named volume, healthcheck). Stop/remove with `docker compose -f docker-compose.dev.yml down`; wipe data with `docker compose -f docker-compose.dev.yml down -v`. Verify with `docker compose -f docker-compose.dev.yml exec mariadb mariadb -u databisnis -pdatabisnis databisnisid -e 'SELECT 1'`. Smoke against the container:
`VEX_DB_BACKEND=mysql VEX_DB_HOST=127.0.0.1 VEX_DB_PORT=3306 VEX_DB_USER=databisnis VEX_DB_PASS=databisnis VEX_DB_NAME=databisnisid ./venv/bin/python test_mariadb.py`
@@ -41,7 +41,7 @@ Two tools scan the Vexanium blockchain voters table for accounts whose only vote
- `db.py` — storage abstraction (sqlite default | mysql via PyMySQL); `connect/query/queryone/replace_snapshot`; `%s` → `?` for sqlite; `query` returns list. Juga tabel distribusi: `distribute_runs` + `distribute_payments` (append-only) + helper `ensure_distribute_schema/record_run/record_payment/update_payment_status/update_run_status/list_runs/list_payments`. Juga tabel klaim: `claim_runs` + helper `ensure_claim_schema/record_claim/update_claim_fee/pending_claim_fee`. Juga tabel V41 `voter_first_seen` (owner PK + `first_seen_at`, append-only) + helper `ensure_first_seen_schema/record_first_seen` — dipakai dashboard membedakan PEMILIH BARU vs REVOTE.
- `get_voters.js` — reference implementation only.
- `distribute.py` — pembayaran harian: fetch liquid balance → baca pemilih **band reward V40/V52** (`last_vote > now−28d AND first_seen_at ≤ now−3d` via LEFT JOIN `voter_first_seen` — pemilih baru (`first_seen` baru/NULL) & basi ⊥ dibayar; akun yang sudah matang (`first_seen ≤ now−3d`) dibayar langsung, ⊖ peduli umur vote, V52) → `compute_shares` (Decimal floor 4-des, sisa di akun) → sign `vex.token::transfer` via pyntelope (`trx.link` ambil ABI+TAPOS dari node, `sign` dengan `VEX_BP_PRIVATE_KEY`, `.send()`) → catat `sent/failed` per voter; verifikasi txid via Hyperion sebelum kirim ulang; `--dry-run` = rencana ⊥ tanda tangan; notifikasi Telegram mulai/selesai/gagal via `telegram.py` (best-effort, ⊥ dry-run/no-op). `test_distribute.py` — oracle (mock chain+pyntelope, temp DB).
- `claim.py` — klaim reward BP harian: baca `last_claim_time` (tabel `producers`, di-retry V46) → `next_window` = +24 jam, math UTC-naive via `_utcnow()` (V49/B13 — ⊥ `datetime.now()` lokal yang geser +TZ → poll prematur) → (≥24 jam lewat → clamp ke now, recovery B8); 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; Hyperion `executed: false` utk tx baru-mendarat-belum-terindeks juga dicek lewat `_claim_time_advanced`, V50/B14); 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, math UTC-naive via `_utcnow()` (V49/B13 — ⊥ `datetime.now()` lokal yang geser +TZ → poll prematur) → (≥24 jam lewat → clamp ke now, recovery B8); 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; Hyperion `executed: false` utk tx baru-mendarat-belum-terindeks juga dicek lewat `_claim_time_advanced`, V50/B14); tak terkonfirmasi → retry senyap; reward = isi tx via Hyperion (bpay+vpay → BP, `reward_from_tx`) prioritas — di-RETRY 4× dgn `SETTLE_SECONDS` (30) beri waktu Hyperion indeks tx baru (V55/B17); selisih saldo fallback HANYA delta positif (`after > before`), delta 0 ambigu → ⊥ dianggap reward 0; reward ⊥ terukur → fee `pending` + notif `KLAIM MENDARAT · REWARD TAK TERUKUR` (V33/B10/V55), ⊖ pernah `KLAIM REWARD SUKSES 0.0000` palsu; 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.
- `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/V52): the voters list is split into BARU (top, `PEMILIH BARU · BELUM MATANG`, new accounts by `first_seen_at` within 3 days) 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); account names link to `https://vexascan.com/account/{owner}` in the index list, `/history` pay-list, and live search (`.owner-link`, V51); a promo card (`BY DATABISNISID · VEXWALLET`) links the VexWallet Android app on the index page via the IDRS logo (`static/idrs.png`) + `UNDUH DI PLAY STORE` (V53); BARU rows show a `MATURITY PERIOD` countdown column (`VALID DALAM N HARI`) instead of TOTAL REWARD. `/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.
+4
View File
@@ -119,6 +119,8 @@ V49: math jendela klaim memakai `_utcnow()` (UTC-naive, `datetime.now(timezone.u
V50: `_confirm_landed` ⊥ langsung menyerah saat Hyperion `executed: false` (verify_txid → False): tx yang BARU SAJA mendarat bisa belum terindeks Hyperion (lag, response `executed: false` utk id yg belum diindeks) — menganggap False sbg "tak mendarat" membuat klaim sukses masuk retry-loop permanen (B14). Landing = Hyperion `executed` ATAU `last_claim_time` maju dari baseline; utk verified False MAUPUN None tetap cek `_claim_time_advanced`; hanya bila itu juga False → retry (klaim ditolak ⊥ memajukan `last_claim_time`). Oracle test_claim: send-normal+verify-False+last-maju → claimed + reward tercatat (V50); send-normal+verify-False+last-tak-maju → retry + ⊥ baris (V46)
V52: maturity pemilih baru diukur dari `first_seen_at` (stempel scan presisi detik, tabel `voter_first_seen`) BUKAN `last_vote` — `last_vote` terkuantisasi PEKAN oleh stake2vote (B12) sehingga voter baru yang vote Senin–Kamis tampak 4–6 hari ⇒ lolos maturity 3-hari tanpa menunggu (B15). Aturan (dashboard + distribusi): BARU = `last_vote > now−STALE` DAN (`first_seen_at` NULL atau `> now−MATURITY`); VALID = `last_vote > now−STALE` DAN `first_seen_at ≤ now−MATURITY` (akun matang, ⊖ perlu vote tua). `_edge_rows`/`_db_rows`/`_count`/`_stats`/`_search_rows` + distribusi memakai ambang `first_seen_at`; `_stats` partisi `n_new + n_valid + n_expired == total_voters`; BARU band kolom MATURITY PERIOD = countdown `VALID DALAM N HARI` (`first_seen + MATURITY`, filter `countdown`; ⊖ TOTAL REWARD yang selalu 0 utk pemilih baru); oracle test_distribute (make_voters_db seed first_seen matang default, make_first_seen REPLACE utk BARU/REVOTE) + test_dashboard (acct### seed first_seen matang; band check MATURITY PERIOD) | V52,V40,V42,V41
V53: kartu promo VEXWALLET di index (display-only): section `.news` tag `BY DATABISNISID`, judul `VEXWALLET · ANDROID`, link logo `static/idrs.png` (`.promo-logo`) + label `UNDUH DI PLAY STORE` → `https://play.google.com/store/apps/details?id=kriptoteknologi.io.vexwallet&hl=id` (`target=_blank rel=noopener`); oracle test_dashboard (logo idrs.png + link + tag + label ada di `/`); history ⊖ berubah
V54: baseline landing klaim STABIL per window (B16): `poll_claim` baca `last_claim_time` SEKALI di awal window dan mengoper ke SETIAP `_try_claim_once(last_before)` (⊥ tiap attempt baca ulang — baseline ikut maju setelah klaim mendarat, membuat `_claim_time_advanced` tak pernah True → retry-loop permanen walau klaim sukses); `_confirm_landed` baca `last_after` dgn retry singkat (node RPC bisa lag mencerminkan `last_claim_time` baru); attempt 1 yg baca basi → retry, attempt 2 (baseline orisinil vs `last_after` maju) → claimed. Oracle test_claim: stale-lalu-maju → poll_claim claimed + reward tercatat | V54,V50,V46
V55: reward klaim TAK PERNAH tercatat/notif sbg `0.0000` sukses bila tak terukur (B17): `_measure_reward` (1) RETRY `reward_from_tx` 4× dgn jeda `SETTLE_SECONDS` (default 30) — tx klaim baru mendarat bisa belum terindeks Hyperion sesaat → None palsu; (2) fallback selisih saldo 3× HANYA mengakui delta POSITIF (`after > before`) sbg terukur — delta 0 ambigu (node balik saldo lama / saldo belum ter-update) → tetap None; keduanya gagal → fee `pending` + alert `KLAIM MENDARAT · REWARD TAK TERUKUR` (jalur V45), ⊖ `KLAIM REWARD SUKSES 0.0000` menyesatkan. Reward 0 ASLI (tx executed tanpa transfer bpay/vpay) tetap terbaca via isi tx (`reward_from_tx` → Decimal('0')) → fee `skipped` (V33). Oracle test_claim: V33 reward-0 via isi tx (bukan delta 0); B17-a reward_from_tx None + delta 0 → claimed + fee pending + alert ⊖ sukses-0; B17-b reward_from_tx None lalu berhasil → reward asli tercatat | V55,V45,V33
V35: kill-switch distribusi `DISTRIBUTE_ENABLED` default false → run nyata (bukan dry-run) no-op: exit 0, ⊥ baca saldo, ⊥ tanda tangan, ⊥ tulis DB, ⊥ notif; `--dry-run` tetap menampilkan rencana (read-only); nilai `true`/`1`/`yes` → normal
V36: notifikasi klaim reward (sukses `notify_claim` & gagal `notify_claim_failure`) → kanal INTERNAL saja, ⊥ pernah ke KOMUNITAS (V30); fee gagal ⊥ spam — satu notif per klaim (saat transisi ke `failed`)
V37: jadwal distribusi MINGGUAN (⊥ harian): `distribute_loop` menunggu `DISTRIBUTE_WEEKDAY` — SATU hari (`sat`) atau CSV beberapa hari (`wed,sat`), nama `mon`..`sun` atau 0-6 (V44: multi-hari, event terdekat di antara hari-hari terdaftar) — jam `DISTRIBUTE_HOUR` (lokal TZ); plus satu one-off `DISTRIBUTE_FIRST_RUN` (`YYYY-MM-DD`; default baked `2026-08-17`) yang diambil bila lebih dekat dari mingguan; setelah lewat (atau ⊥ diset) → hanya mingguan; batas dihitung ulang tiap iterasi loop (setelah run → jadwal berikutnya); payout di-tunda bila `DISTRIBUTE_ENABLED=false` (V35) — jadwal tetap maju, run jadi no-op; `distribute_loop.main` baca jadwal via `config` (⊥ `os.getenv` sendiri) agar konsisten dgn dashboard V38
@@ -194,3 +196,5 @@ B12|2026-08-07|`derive_last_vote` pakai `365.25 hari/tahun` tapi Vexanium (`stak
B13|2026-08-07|claim service loop `[Peringatan] klaim terkirim tapi tak terkonfirmasi mendarat — retry` tanpa henti ~7 jam sebelum jendela: `step()` memakai `datetime.now()` (TZ container Asia/Jakarta, UTC+7) sedangkan `last_claim_time` chain UTC-naive → `next_window` menganggap jendela 24 jam sudah buka ~7 jam lebih awal → poll tiap 60s → `claimrewards` ditolak chain (HTTP 500 sbg body, pyntelope ⊥ raise) → `_confirm_landed` ⊥ terkonfirmasi → retry; V46 bekerja benar (⊥ sukses-0) tapi tak pernah ada tx mendarat utk dicatat|window math pakai `_utcnow()` (UTC-naive) di `step()`/`poll_claim`/`_try_claim_once` — konsisten dgn `last_claim_time` chain (V49); oracle `_utcnow` tzinfo None + step before-window → sleep
B14|2026-08-07|setelah fix TZ (V49) klaim BENAR-BENAR mendarat (tx `0e61fcb5…`, bpay 466.7587 + vpay 1215.4164 = 1682.1751 VEX) tapi app tetap retry-loop & ⊥ catat reward/fee: `_confirm_landed` menyerah saat `verify_txid` → False (`executed: false`) — Hyperion merespons false utk tx yang baru saja mendarat tapi belum terindeks (lag), padahal `last_claim_time` SUDAH maju; retry berikutnya baca `last_before` yg sudah maju → `_claim_time_advanced` tak pernah deteksi → retry permanen, fee 168.22 VEX utk siklus itu tak terkirim|`_confirm_landed`: verified False MAUPUN None → tetap cek `_claim_time_advanced`; hanya bila itu False → retry (V50); oracle send-normal+verify-False+last-maju → claimed
B15|2026-08-08|maturity 3-hari pemilih BARU ⊥ berfungsi utk vote Senin–Kamis: `last_vote` diturunkan dari bobot yang terkuantisasi PEKAN (stake2vote) → voter baru yg vote tengah pekan tampak 4–6 hari ⇒ lolos `last_vote ≤ now−MATURITY` di hari pertama (anti-gaming tembus); kejadian nyata: eosauthority vote 5 Agt (Rabu) → `last_vote` terderivasi 1 Agt (Sabtu)|maturity diukur dari `first_seen_at` (presisi detik) di dashboard + distribusi: BARU = first_seen baru/NULL; VALID = first_seen matang (V52); oracle test_distribute/test_dashboard seed first_seen matang
B16|2026-08-08|klaim mendarat di chain (tx `48c2066f…`, bpay 463.9816 + vpay 1219.3825 = 1683.3641 VEX) tapi app tetep `[Peringatan] klaim terkirim tapi tak terkonfirmasi mendarat — retry` tanpa henti & ⊖ notif/fee (sama utk klaim 7 Agt `0e61fcb5…`): `_try_claim_once` baca `last_before` ULANG tiap attempt — attempt 1 mendarat tapi baca `last_after` basi (node RPC belum mencerminkan `last_claim_time` baru) → retry; attempt 2 `last_before` sudah = nilai maju → `_claim_time_advanced(maju, maju)` ⊖ pernah True → retry-loop permanen walau V50/V14 sudah menangani Hyperion-False|`poll_claim` baca baseline SEKALI per window & oper ke tiap `_try_claim_once`; `_confirm_landed` retry baca `last_after` dgn jeda singkat (V54); oracle stale-lalu-maju → claimed
B17|2026-08-08|notif Telegram `KLAIM REWARD SUKSES / Reward 0.0000 / Fee BP 0.0000 → bpdbsjasprod` (22:47 WIB, ~1 mnt stlh klaim mendarat 15:46:06.5 tx `48c2066f…` reward asli 1683.3641) — `_measure_reward` balikin `Decimal('0')` BUKAN None: `reward_from_tx` → None (Hyperion belum indeks tx yg baru mendarat) lalu fallback selisih saldo baca `after == before` (node balik saldo lama) → `max(δ,0)=0`; guard `if reward is None` (V45) ⊖ pernah memicu (0 ⊖ None) → fee 0 `skipped` + `notify_claim(0,0)` sukses-0 palsu; fee 168.3364 VEX utk siklus itu tak terkirim (sama utk klaim 9 Agt `4b9ec0be…` reward 1683.0126 yg jalan di image V54: delta-0 → sukses-0 → fee 168.3012 tak terkirim)|`_measure_reward` retry `reward_from_tx` 4× dgn `SETTLE_SECONDS` (30) beri waktu Hyperion mengejar indeks; fallback selisih saldo HANYA delta POSITIF (`after > before`); delta 0 / keduanya gagal → None → fee `pending` + alert `KLAIM MENDARAT · REWARD TAK TERUKUR`, ⊖ sukses-0 (V55); reward 0 asli dibuktikan dari isi tx; oracle B17-a/B17-b
+51 -18
View File
@@ -34,7 +34,7 @@ from config import (API_NODE, BP_FEE_PERCENT, BP_FEE_WALLET, BP_PRIVATE_KEY,
TOKEN_CONTRACT)
VEX_PREC = Decimal('0.0001')
SETTLE_SECONDS = 10 # tunggu finalitas sebelum baca saldo setelah klaim
SETTLE_SECONDS = 30 # tunggu finalitas sebelum baca saldo setelah klaim
POLL_SPAN_SECONDS = 7200 # batas poll sekali jendela (2 jam)
@@ -183,16 +183,24 @@ def reward_from_tx(txid):
def _measure_reward(txid, before):
"""Reward klaim: prioritas isi tx (V45) → fallback selisih saldo.
"""Reward klaim: prioritas isi tx (V45) → fallback selisih saldo (V55).
Return `Decimal` | None bila keduanya gagal (reward tak terukur)."""
reward = reward_from_tx(txid)
if reward is not None:
return reward
Return `Decimal` | None bila keduanya gagal (reward tak terukur).
V55/B17: `reward_from_tx` di-RETRY dgn jeda settle — tx klaim baru mendarat
bisa belum terindeks Hyperion sesaat (None palsu) → beri waktu mengejar.
Fallback selisih saldo HANYA mengakui delta POSITIF (after > before) sbg
terukur; delta 0 ambigu (node balik saldo lama / saldo belum ter-update) →
tetap None → jalur tak-terukur (fee pending + alert), ⊖ sukses-0 palsu.
Reward nol asli (tx executed tanpa bpay/vpay) tetap terbaca via isi tx."""
for _ in range(4):
reward = reward_from_tx(txid)
if reward is not None:
return reward
time.sleep(SETTLE_SECONDS)
for _ in range(3):
time.sleep(SETTLE_SECONDS)
after = dist.fetch_balance()
if after is not None:
if after is not None and after > before:
return max(after - before, Decimal('0'))
return None
@@ -209,18 +217,27 @@ def _confirm_landed(txid, last_before):
tapi belum terindeks (lag) — padahal klaim sukses. Jadi utk verified False
maupun None, tetap cek `_claim_time_advanced`; hanya bila itu pun False →
retry senyap (⊥ catat baris / sukses-0). Klaim yang ditolak ⊥ memajukan
`last_claim_time` → tetap retry (V50/B14)."""
`last_claim_time` → tetap retry (V50/B14).
V54/B16: baca `last_claim_time` sesudah kirim di-RETRY beberapa kali dgn
jeda singkat — node RPC bisa terlambat mencerminkan `last_claim_time` yang
baru (lag sesaat); baca stale sekali ⊥ langsung dianggap tak mendarat."""
verified = dist.verify_txid(txid)
if verified is True:
return True
try:
last_after = fetch_producer_last_claim()
except Exception:
last_after = None
last_after = None
for _ in range(3):
try:
last_after = fetch_producer_last_claim()
except Exception:
last_after = None
if last_after is not None:
break
time.sleep(2)
return _claim_time_advanced(last_before, last_after)
def _try_claim_once():
def _try_claim_once(last_before=None):
"""Satu percobaan klaim: klaim → konfirmasi mendarat → ukur reward →
catat `claim_runs`.
@@ -230,14 +247,22 @@ def _try_claim_once():
senyap. Reward diukur dari isi tx (transfer bpay/vpay) — prioritas (V45);
selisih saldo fallback; keduanya gagal → fee `pending` + notif cek manual
(⊥ sukses-0 yang menyesatkan).
V54/B16: `last_before` (baseline `last_claim_time` sebelum kirim) dipakai
TETAP dari awal window (`poll_claim` menghitung sekali) — retry berikutnya
membandingkan baseline ORISINIL thd `last_after` yang sudah maju, jadi klaim
yang mendarat tapi baca-pertama-nya basi tetap terdeteksi (⊥ baseline ikut
maju tiap attempt → retry-loop permanen). Bila None (pemanggil langsung),
baca sendiri sbg fallback.
"""
before = dist.fetch_balance()
if before is None:
return 'retry'
try:
last_before = fetch_producer_last_claim()
except Exception:
last_before = None
if last_before is None:
try:
last_before = fetch_producer_last_claim()
except Exception:
last_before = None
txid = None
landed = False
try:
@@ -281,9 +306,17 @@ def poll_claim(deadline):
"""Poll `claimrewards` tiap `CLAIM_RETRY_SECONDS` sampai sukses / deadline.
Return `claimed` | `timeout` (deadline lewat — jadwal dihitung ulang).
V54/B16: baseline `last_claim_time` dihitung SEKALI di awal window dan
dipakai utk SEMUA attempt — klaim yang mendarat tapi baca-pertama-nya basi
terdeteksi di attempt berikutnya (baseline orisinil vs `last_after` maju),
⊥ baseline ikut maju tiap attempt → retry-loop permanen.
"""
try:
last_before = fetch_producer_last_claim()
except Exception:
last_before = None
while _utcnow() < deadline:
status = _try_claim_once()
status = _try_claim_once(last_before)
if status == 'claimed':
return 'claimed'
time.sleep(CLAIM_RETRY_SECONDS)
+92 -2
View File
@@ -184,14 +184,17 @@ def main():
check('V34 fee akhirnya sent', row[5] == 'sent' and row[6] == 'txok')
# ————— V33: reward 0 → fee skipped; step lanjut ke jadwal —————
# Reward 0 asli dibuktikan dari ISI TX (executed tanpa transfer bpay/vpay
# → Decimal('0')); selisih saldo 0 TIDAK dianggap reward 0 (B17) — delta
# 0 ambigu → jalur tak-terukur.
fresh_db('zero')
claim.fetch_producer_last_claim = lambda: (
claim._utcnow() - timedelta(hours=25)
).strftime('%Y-%m-%dT%H:%M:%S')
balances = iter([Decimal('100.0000'), Decimal('100.0000')])
dist.fetch_balance = lambda: next(balances)
dist.fetch_balance = lambda: Decimal('100.0000') # ⊥ dipakai (tx prioritas)
claim.build_claim_action = lambda: FakeSigned('txzero')
dist.verify_txid = lambda txid: True
claim.reward_from_tx = lambda txid: Decimal('0') # genuine zero
check('V33 klaim reward 0 → claimed', claim.step() == 'claimed')
row = last_claim_row()
check('V33 fee 0 → skipped', row[4] == 0.0 and row[5] == 'skipped')
@@ -200,6 +203,7 @@ def main():
claim._utcnow() - timedelta(hours=20)
).strftime('%Y-%m-%dT%H:%M:%S')
check('V34 ⊥ fee pending → jadwal (sleep)', claim.step() == 'sleep')
claim.reward_from_tx = lambda txid: None
# ————— V34: klaim timeout tapi mendarat (verify True) → diukur —————
fresh_db('landed')
@@ -321,6 +325,92 @@ def main():
check('V50 reward terukur + tercatat',
last_claim_row()[3] == 5.0 and last_claim_row()[2] == 'txR4')
# ————— V54/B16: baca last_after basi di attempt 1, maju di attempt 2 —————
# poll_claim menghitung baseline SEKALI; attempt 1 mendarat tapi node
# masih balikin last_claim_time lama → retry; attempt 2 (baseline sama)
# baca last_claim_time yg sudah maju → claimed (⊥ baseline ikut maju).
fresh_db('staleretry')
st = {'n': 0}
def stale_then_advance():
st['n'] += 1
if st['n'] == 1: # baseline (poll_claim awal window)
return (claim._utcnow() - timedelta(hours=2)
).strftime('%Y-%m-%dT%H:%M:%S')
if st['n'] == 2: # attempt 1: baca masih basi (belum ter-catch-up)
return (claim._utcnow() - timedelta(hours=2)
).strftime('%Y-%m-%dT%H:%M:%S')
return (claim._utcnow() - timedelta(hours=1)
).strftime('%Y-%m-%dT%H:%M:%S') # attempt 2: maju
claim.fetch_producer_last_claim = stale_then_advance
balances = iter([Decimal('30.0000'), Decimal('35.0000'),
Decimal('40.0000')])
dist.fetch_balance = lambda: next(balances)
sends = {'n': 0}
def send_once_then_reject():
sends['n'] += 1
if sends['n'] == 1:
return FakeSigned('txR5') # attempt 1: mendarat
return FakeSigned('txR5b', exc=RuntimeError('already claimed'))
claim.build_claim_action = send_once_then_reject
dist.verify_txid = lambda txid: False # Hyperion lag di kedua attempt
deadline = claim._utcnow() + timedelta(seconds=3600)
check('V54 stale lalu maju → poll_claim claimed',
claim.poll_claim(deadline) == 'claimed')
check('V54 reward terukur + tercatat',
last_claim_row()[3] == 5.0 and last_claim_row()[2] == 'txR5b')
# ————— V55/B17: reward_from_tx None + delta 0 → TAK TERUKUR (⊥ sukses-0) —————
# Delta saldo 0 (node belum up-date / saldo lama) ⊥ dianggap reward 0;
# keduanya gagal mengukur → fee pending + alert KLAIM MENDARAT, ⊖ notif
# sukses-0 palsu (kasus Telegram 2026-08-08: klaim mendarat, reward asli
# 1683.3641, tapi app notif `KLAIM REWARD SUKSES 0.0000`).
fresh_db('zerodelta')
claim.fetch_producer_last_claim = lambda: (
claim._utcnow() - timedelta(hours=25)
).strftime('%Y-%m-%dT%H:%M:%S')
claim.reward_from_tx = lambda txid: None # Hyperion belum indeks
bal_seq = iter([Decimal('100.0000'), Decimal('100.0000'),
Decimal('100.0000'), Decimal('100.0000')])
dist.fetch_balance = lambda: next(bal_seq) # delta selalu 0
claim.build_claim_action = lambda: FakeSigned('txB17')
dist.verify_txid = lambda txid: True
notifs = []
_orig_nc2 = telegram.notify_claim
_orig_ncu2 = telegram.notify_claim_unmeasured
telegram.notify_claim_unmeasured = lambda t: notifs.append('unmeasured')
telegram.notify_claim = lambda r, f, w: notifs.append('claimed')
check('V55 delta 0 → claimed (baris dicatat, ⊖ sukses-0)',
claim._try_claim_once() == 'claimed')
row = last_claim_row()
check('V55 fee pending utk cek manual (⊖ skipped)',
row[4] == 0.0 and row[5] == 'pending')
check('V55 alert cek manual, ⊖ notif sukses-0',
'unmeasured' in notifs and 'claimed' not in notifs)
telegram.notify_claim = _orig_nc2
telegram.notify_claim_unmeasured = _orig_ncu2
# ————— V55/B17: reward_from_tx catch-up (None lalu berhasil) —————
# Hyperion mengejar indeks tx → reward asli terbaca → fee tercatat
# (jalur normal yang menyelamatkan klaim seperti 2026-08-09).
fresh_db('catchup')
claim.fetch_producer_last_claim = lambda: (
claim._utcnow() - timedelta(hours=25)
).strftime('%Y-%m-%dT%H:%M:%S')
seq = iter([None, None, Decimal('1683.0126')])
claim.reward_from_tx = lambda txid: next(seq)
dist.fetch_balance = lambda: Decimal('100.0000') # ⊥ dipakai
claim.build_claim_action = lambda: FakeSigned('txC')
dist.verify_txid = lambda txid: True
check('V55 catch-up reward → claimed', claim._try_claim_once() == 'claimed')
row = last_claim_row()
check('V55 reward asli 1683.0126 + fee 168.3012 pending',
row[3] == 1683.0126 and row[4] == 168.3012
and row[5] == 'pending' and row[2] == 'txC')
# ————— V45: reward dari isi tx (bpay+vpay) → prioritas —————
fresh_db('txreward')
claim.fetch_producer_last_claim = lambda: (