diff --git a/AGENTS.md b/AGENTS.md index e4f304d..a9d48d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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** (mature + fresh, `now−28d < last_vote ≤ now−3d`, plus REVOTE immediately, §V40/V42: only new voters under `VEX_MATURITY_DAYS` and stale voters get nothing — returning re-voters skip maturity), 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), 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`) — never the old "within 24h ⇒ success" heuristic (B9, V45). 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), 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). 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. - 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` @@ -30,7 +30,7 @@ Two tools scan the Vexanium blockchain voters table for accounts whose only vote - 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. - 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=` 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. -- Claim (spec §V32–V34): the BP reward is claimed once per 24h window via `vexcore::claimrewards` (`owner` = BP). The `claim` service polls every `CLAIM_RETRY_SECONDS` only near the window (`last_claim_time` from the `vexcore` `producers` table + 24h) — no all-day tx spam; a window already ≥24h past clamps to now so a missed claim is caught up, never spin-timeout (B8). `get_table_rows` responses are always `{"rows":[...]}` — parse `data.get('rows')`, never `data[0]` (B7, V39). Landed detection = Hyperion `executed` OR `last_claim_time` advanced past pre-send value (V45, B9); `next_window` retries the producer read so a flaky node never triggers a premature claim. Reward = sum of `vex.bpay`+`vex.vpay` → BP transfers read from the claim tx via Hyperion (`reward_from_tx`), balance-delta as fallback (V33); if neither can be read → fee `pending` + internal `KLAIM MENDARAT · REWARD TAK TERUKUR` alert, never a misleading 0-reward success (B10). 10% fee floored at 4-dec goes to `bpdbsjasprod` (`VEX_BP_FEE_WALLET`), memo `BP FEE YYYY-MM-DD`, txid verified via Hyperion before any resend. A cycle completes only when the claim row (`claim_runs`) is recorded AND the fee is `sent`; an unsent fee is retried each cycle and resumed on restart. No mutex with distribution — the daily payout freezes the balance at run start, so a claim landing mid-run is simply paid out the next run. +- Claim (spec §V32–V34): the BP reward is claimed once per 24h window via `vexcore::claimrewards` (`owner` = BP). The `claim` service polls every `CLAIM_RETRY_SECONDS` only near the window (`last_claim_time` from the `vexcore` `producers` table + 24h) — no all-day tx spam; a window already ≥24h past clamps to now so a missed claim is caught up, never spin-timeout (B8). `get_table_rows` responses are always `{"rows":[...]}` — parse `data.get('rows')`, never `data[0]` (B7, V39). Landed detection = Hyperion `executed` OR `last_claim_time` advanced past pre-send value, checked on BOTH the send-success and send-exception paths (`_confirm_landed`) because pyntelope's `send()` returns HTTP 500 as a body instead of raising on a chain rejection (V46, B11); `next_window` raises if all producer reads fail so a flaky node never triggers a premature claim. Reward = sum of `vex.bpay`+`vex.vpay` → BP transfers read from the claim tx via Hyperion (`reward_from_tx`), balance-delta as fallback (V33); if neither can be read → fee `pending` + internal `KLAIM MENDARAT · REWARD TAK TERUKUR` alert, never a misleading 0-reward success (B10). 10% fee floored at 4-dec goes to `bpdbsjasprod` (`VEX_BP_FEE_WALLET`), memo `BP FEE YYYY-MM-DD`, txid verified via Hyperion before any resend. A cycle completes only when the claim row (`claim_runs`) is recorded AND the fee is `sent`; an unsent fee is retried each cycle and resumed on restart. No mutex with distribution — the daily payout freezes the balance at run start, so a claim landing mid-run is simply paid out the next run. - Dashboard layout (spec §V28): desktop gives AKUN/STAKE/REWARD/VOTE equal width with AKUN left and the other three centered; tablet keeps the fixed STAKE column (RANK 56 / AKUN 1fr / STAKE 160px / REWARD 1fr / VOTE 1fr); `/history` uses a six-column run grid and a four-column payment grid (AKUN/JUMLAH/STATUS/TXID) that shows only the latest run with its date in the title, and links each TXID to `https://vexascan.com/transaction/{txid}` — no TANGGAL or MEMO column (memo is on-chain only, not stored); mobile turns history rows into labeled cards. HTML responses use `Cache-Control: no-store`; stylesheet URL is versioned with `?v=` for deploy cache-busting. - Liquid balance (spec §V16): a 4th stat cell "SALDO LIQUID" shows the BP account's liquid VEX (`account.core_liquid_balance`) fetched from `GET {DATABISNIS_API}/v2/state/get_account?account=`. The dashboard fetches it live but caches per-worker in memory for `DASH_LIQUID_TTL` (default 60s); a failed fetch keeps the last value (or renders `—` if none ever succeeded) and the failure is also cooled-down so the API isn't hammered. The dashboard still never writes to the DB. @@ -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/V42** (`last_vote > now−28d AND (≤ now−3d OR REVOTE)` via LEFT JOIN `voter_first_seen` — pemilih baru < `VEX_MATURITY_DAYS` & basi ⊥ dibayar, tapi pemilih lama yang REVOTE dibayar langsung) → `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 V45) → `next_window` = +24 jam (≥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 = Hyperion `executed` ATAU `last_claim_time` maju dari baseline (V45/B9, ⊥ heuristik 24 jam); 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. - `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. diff --git a/SPEC.md b/SPEC.md index 9e752ee..555b2c8 100644 --- a/SPEC.md +++ b/SPEC.md @@ -114,6 +114,7 @@ V43: tag & badge display (display-only, ⊥ ubah eligibilitas/payout V40/V42): t V33: reward klaim diukur PRIORITAS dari isi tx via Hyperion `get_transaction` — jumlah transfer `vex.bpay` + `vex.vpay` → TARGET_BP (`reward_from_tx`; ⊥ selisih saldo yang flaky/stale saat node baca saldo di belakang); fallback selisih saldo (setelah − sebelum, settle delay, retry baca) bila Hyperion tak terjangkau; reward ⊥ terukur keduanya → baris dicatat fee `pending` + notif internal `KLAIM MENDARAT · REWARD TAK TERUKUR` (⊥ sukses-0 menyesatkan, V45); fee = floor(`BP_FEE_PERCENT` × reward) 4 desimal → `VEX_BP_FEE_WALLET` (default `bpdbsjasprod`), memo `BP FEE YYYY-MM-DD`, verifikasi txid Hyperion sebelum resend (pola V22); reward ≤ 0 → fee `skipped`; Σfee ≤ reward V34: siklus klaim selesai ⊥ bila klaim tercatat (`claim_runs`) DAN fee terkirim; fee `pending`/`failed` diulang tiap siklus + dilanjutkan saat restart (⊥ fee hilang); klaim saat distribusi berjalan didefer ke run berikutnya (distribute beku saldo di awal run, V20) — ⊥ mutex; fee txid ⊥ dikirim ulang bila ⊥ bisa diverifikasi V45: anti false-success klaim: klaim dianggap "mendarat" ⊥ cukup karena `last_claim_time` dalam 24 jam terakhir (heuristik lama — klaim yang DITOLAK sebelum jendela bisa salah dianggap sukses bila ada klaim lama <24 jam); deteksi = Hyperion `executed` ATAU `last_claim_time` benar-benar MAJU dari nilai sebelum kirim (`_claim_time_advanced`, `after > before`; baseline tak terbaca → ⊥ dianggap maju, retry); `next_window` baca `last_claim_time` di-retry (node flaky ⊥ langsung dianggap "belum pernah klaim" → clamp now prematur → percobaan ditolak); reward tak terukur → fee `pending` + notif cek manual, ⊥ `skipped` diam-diam (V33); oracle test_claim (last maju vs tak maju, reward_from_tx parse bpay+vpay, unmeasured → pending+notif) +V46: ⊥ pernah percaya `signed.send()` semata utk deteksi mendarat — pyntelope TIDAK raise saat chain menolak (`Net._request` mengembalikan body HTTP 500 sbg JSON, ⊥ exception), jadi "sukses kirim" bisa berarti DITOLAK (mis. claimrewards sebelum jendela 24 jam) → false success reward 0. Landing dikonfirmasi di KEDUA jalur (send sukses maupun exception) lewat `_confirm_landed` = Hyperion `executed` ATAU `last_claim_time` maju dari baseline; tak terkonfirmasi → `retry` senyap (⊥ catat `claim_runs`, ⊥ notif sukses) — klaim sah yang belum terindeks Hyperion → retry 60s berikutnya terverifikasi (self-healing dalam 2 jam poll). `next_window`: semua pembacaan producer gagal → raise (loop catch-and-sleep), ⊥ clamp now prematur; `now` hanya utk pembacaan sukses tanpa klaim sebelumnya (BP baru). Oracle test_claim: send-normal+verify-False → retry + ⊥ baris + ⊥ notif; send-normal+last-maju → claimed; baca-producer-gagal → raise 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 @@ -184,3 +185,4 @@ B7|2026-08-06|claim service di swarm mati total — log `[Error] Siklus klaim ga B8|2026-08-06|setelah jendela klaim lewat (poll 2 jam habis tanpa sukses) `next_window` tetap balikin `last+24h` masa lalu → `poll_claim` spin-timeout selamanya, klaim tak pernah bisa catch-up (deadlock) walau ≥24 jam sudah lewat|`next_window` = `max(last+24h, now)` — ≥24 jam → jendela = now, klaim dikejar segera (V32) 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) +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) diff --git a/claim.py b/claim.py index f2df2d3..a256fe4 100644 --- a/claim.py +++ b/claim.py @@ -4,10 +4,13 @@ Setiap siklus (V32..V34): tentukan jendela klaim = `last_claim_time` (tabel `producers`) + 24 jam, tidur sampai mendekatinya, lalu poll `claimrewards` tiap `CLAIM_RETRY_SECONDS` sampai diterima chain. Jendela yang sudah lewat (≥24 jam sejak last) → klaim dikejar segera (recovery, ⊥ spin-timeout -selamanya). Reward diukur PRIORITAS dari isi tx klaim via Hyperion (jumlah -transfer `vex.bpay`/`vex.vpay` → TARGET_BP; V45) — ⊥ selisih saldo yang -flaky/stale; selisih saldo jadi fallback; keduanya gagal → fee `pending` + -notif cek manual. Fee = floor(`VEX_BP_FEE_PERCENT` × reward) 4 desimal → +selamanya). Landing dikonfirmasi via Hyperion `executed` ATAU `last_claim_time` +maju dari baseline — `send()` pyntelope TIDAK raise saat chain menolak (HTTP +500 dikembalikan sbg body), jadi ⊥ pernah percaya send() semata (V46/B11). +Reward diukur PRIORITAS dari isi tx klaim via Hyperion (jumlah transfer +`vex.bpay`/`vex.vpay` → TARGET_BP; V45) — ⊥ selisih saldo yang flaky/stale; +selisih saldo jadi fallback; keduanya gagal → fee `pending` + notif cek +manual. Fee = floor(`VEX_BP_FEE_PERCENT` × reward) 4 desimal → `VEX_BP_FEE_WALLET` (default `bpdbsjasprod`), memo `BP FEE YYYY-MM-DD` (V33). Siklus selesai ⊥ bila klaim tercatat di `claim_runs` DAN fee terkirim; fee @@ -82,10 +85,10 @@ def next_window(now): ⊥ jadi spin-timeout selamanya; klaim dikejar segera, lalu jendela dihitung ulang dari last_claim_time yang baru. - Pembacaan `last_claim_time` node flaky di-retry: respons tak terbaca ⊥ - langsung dianggap "belum pernah klaim" (yang akan clamp ke now prematur - → percobaan klaim sebelum jendela → ditolak chain); hanya bila pembacaan - tetap gagal total maka `now` dipakai.""" + Pembacaan `last_claim_time` node flaky di-retry: bila SEMUA pembacaan + gagal/kosong → raise (loop catch-and-sleep, ⊥ clamp ke now prematur yang + memicu percobaan klaim sebelum jendela → ditolak chain). `now` hanya utk + pembacaan yang BENAR-BENAR berhasil tanpa klaim sebelumnya (BP baru).""" raw = None for _ in range(3): try: @@ -93,8 +96,10 @@ def next_window(now): if raw: break except Exception: - pass + raw = None time.sleep(2) + if not raw: + raise RuntimeError('tak bisa membaca last_claim_time producer') last = parse_claim_time(raw) if last is None: return now @@ -180,14 +185,35 @@ def _measure_reward(txid, before): return None +def _confirm_landed(txid, last_before): + """Konfirmasi tx klaim benar-benar mendarat. → True | False (⊥ None). + + `send()` pyntelope TIDAK raise saat chain menolak (HTTP 500 dikembalikan + sbg body) — jadi "sukses kirim" ⊥ cukup (V46/B11). Mendarat hanya bila + Hyperion `executed` ATAU `last_claim_time` MAJU dari baseline sebelum kirim. + Hyperion tak terjangkau (None) → cek `_claim_time_advanced`; False → retry + senyap (⊥ catat baris / sukses-0).""" + verified = dist.verify_txid(txid) + if verified is True: + return True + if verified is False: + return False + try: + last_after = fetch_producer_last_claim() + except Exception: + last_after = None + return _claim_time_advanced(last_before, last_after) + + def _try_claim_once(): - """Satu percobaan klaim: klaim → ukur reward → catat `claim_runs`. + """Satu percobaan klaim: klaim → konfirmasi mendarat → ukur reward → + catat `claim_runs`. Return `claimed` (baris tercatat, fee pending/skipped) | `retry`. - Klaim yang sudah mendarat walau timeout (verifikasi txid / `last_claim_time` - maju) tetap diukur — reward tak hilang (V34). - V45: reward diukur dari isi tx (transfer bpay/vpay) — prioritas; selisih - saldo jadi fallback; keduanya gagal → fee `pending` + notif cek manual + Landing dikonfirmasi di KEDUA jalur (send sukses maupun exception) — `send()` + pyntelope ⊥ raise saat chain menolak (V46/B11); tak terkonfirmasi → retry + 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). """ before = dist.fetch_balance() @@ -203,23 +229,17 @@ def _try_claim_once(): signed = build_claim_action() txid = signed.id() signed.send() - landed = True + landed = _confirm_landed(txid, last_before) except Exception as exc: if txid is not None: - verified = dist.verify_txid(txid) - if verified is True: - landed = True - elif verified is None: - # tak bisa dipastikan via Hyperion — cek apakah last_claim_time - # benar-benar MAJU dari baseline (⊥ heuristik "dalam 24 jam") - try: - last_after = fetch_producer_last_claim() - except Exception: - last_after = None - landed = _claim_time_advanced(last_before, last_after) + landed = _confirm_landed(txid, last_before) if not landed: print(f'[Peringatan] klaim gagal: {exc}', flush=True) return 'retry' + if not landed: + print('[Peringatan] klaim terkirim tapi tak terkonfirmasi mendarat ' + '— retry (⊥ catat sukses)', flush=True) + return 'retry' reward = _measure_reward(txid, before) now = datetime.now() now_iso = now.isoformat(timespec='seconds') diff --git a/test_claim.py b/test_claim.py index ed560a4..5d66015 100644 --- a/test_claim.py +++ b/test_claim.py @@ -110,7 +110,16 @@ def main(): claim.next_window(datetime(2026, 8, 6, 12, 0, 0)) == datetime(2026, 8, 6, 12, 0, 0)) claim.fetch_producer_last_claim = lambda: None - check('V32 tanpa last → now (langsung coba)', + try: + claim.next_window(datetime(2026, 8, 6, 12, 0, 0)) + raise AssertionError('GAGAL: baca producer gagal harus raise') + except RuntimeError: + pass + check('V46 baca producer gagal → raise (⊥ clamp prematur)', + True) + # BP baru (row ada, last_claim_time epoch/dulu) → jendela lewat → now + claim.fetch_producer_last_claim = lambda: '2000-01-01T00:00:00.000' + check('V32 last epoch (BP baru) → now', claim.next_window(datetime(2026, 8, 6, 12, 0, 0)) == datetime(2026, 8, 6, 12, 0, 0)) @@ -233,6 +242,49 @@ def main(): check('V45 ⊥ catat baris utk yg tak mendarat', db.queryone('SELECT COUNT(*) FROM claim_runs')[0] == 0) + # ————— V46: send() "sukses" tapi chain MENOLAK (HTTP 500 ⊥ raise) ————— + # pyntelope ⊥ raise saat chain reject — send kembali normal dgn body + # error; landing harus diverifikasi ulang → tak mendarat → retry. + fresh_db('rejectsilent') + claim.fetch_producer_last_claim = lambda: ( + datetime.now() - timedelta(hours=25) + ).strftime('%Y-%m-%dT%H:%M:%S') + dist.fetch_balance = lambda: Decimal('100.0000') + claim.build_claim_action = lambda: FakeSigned('txR2') # send ⊥ raise + dist.verify_txid = lambda txid: False # tak mendarat + notifs = [] + _orig_nc2 = telegram.notify_claim + telegram.notify_claim = lambda r, f, w: notifs.append('claimed') + check('V46 send normal + verify False → retry (⊥ sukses-0)', + claim._try_claim_once() == 'retry') + db.ensure_claim_schema() + check('V46 ⊥ catat baris + ⊥ notif sukses', + db.queryone('SELECT COUNT(*) FROM claim_runs')[0] == 0 + and notifs == []) + telegram.notify_claim = _orig_nc2 + + # ————— V46: send() "sukses" + last MAJU → benar-benar mendarat ————— + fresh_db('sendsuccess') + last_s = {'n': 0} + + def advancing_success(): + last_s['n'] += 1 + if last_s['n'] == 1: + return (datetime.now() - timedelta(hours=2) + ).strftime('%Y-%m-%dT%H:%M:%S') + return (datetime.now() - timedelta(hours=1) + ).strftime('%Y-%m-%dT%H:%M:%S') + + claim.fetch_producer_last_claim = advancing_success + balances = iter([Decimal('10.0000'), Decimal('12.0000')]) + dist.fetch_balance = lambda: next(balances) + claim.build_claim_action = lambda: FakeSigned('txR3') # send ⊥ raise + dist.verify_txid = lambda txid: None # Hyperion tak terjangkau + check('V46 send normal + last MAJU → claimed', + claim._try_claim_once() == 'claimed') + check('V46 reward terukur', + last_claim_row()[3] == 2.0) + # ————— V45: reward dari isi tx (bpay+vpay) → prioritas ————— fresh_db('txreward') claim.fetch_producer_last_claim = lambda: (