Compare commits
2
Commits
a6db3be436
...
e9df89aaac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9df89aaac | ||
|
|
cc9df71d9e |
No files matched your search
@@ -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). Reward is measured as the liquid-balance delta around the claim; 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=<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.
|
||||
- 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). Reward = liquid-balance delta (after − before the claim, settle delay); 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=<BP>`. 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`) → `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`; reward = selisih saldo (setelah−sebelum, settle); 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.
|
||||
|
||||
@@ -111,8 +111,10 @@ V40: band umur vote (dashboard + distribusi, UTC-naive konsisten stempel scan):
|
||||
V41: pembagian pemilih belum-matang menjadi PEMILIH BARU (new) vs REVOTE (pemilih lama yang revote/perpanjang): `first_seen_at` (tabel `voter_first_seen`, V7) dibandingkan dgn `new_cut = now − MATURITY` — `first_seen > new_cut` (atau NULL/DB lama) → BARU; `first_seen ≤ new_cut` → REVOTE; klasifikasi tampilan/search (V15); cold-start: skan pertama mencatat semua owner sbg first-seen-now → semua BARU tampak new satu siklus, klasifikasi benar mulai siklus revote berikutnya
|
||||
V42: REVOTE dianggap VALID LANGSUNG (dashboard + distribusi, V41): pemilih dgn `first_seen_at ≤ now − MATURITY` ⊖ perlu masa matang — memenuhi syarat reward begitu `last_vote > now − STALE`; hanya pemilih BARU (first_seen NULL atau `> now − MATURITY`) yang harus lewat masa matang (`last_vote ≤ now − MATURITY`); jendela VALID dashboard (`_db_rows`/`_count`/rank) = jendela eligibilitas distribusi = `last_vote > now−STALE AND (last_vote ≤ now−MATURITY OR REVOTE)` — konsisten; baris REVOTE menyatu di daftar utama dgn tag mint `REVOTE` (⊥ bagian sendiri, V15); `_stats` partisi `n_new + n_revote_window + n_expired + n_valid == total_voters` (`n_revote_window` = REVOTE jendela VALID; `n_revote` yang dirender = subset tag); distribusi LEFT JOIN `voter_first_seen` (`ensure_first_seen_schema`); oracle test_distribute (rev7 umur-1hr first_seen-60hr → dibayar; new1 umur-1hr → ⊖) + test_dashboard (revoter1 di #rows bertag)
|
||||
V43: tag & badge display (display-only, ⊥ ubah eligibilitas/payout V40/V42): tag mint `REVOTE` HANYA utk pemilih lama yang revote dalam `VEX_REVOTE_TAG_DAYS` (default 1) terakhir (`last_vote > revote_cut = now − VEX_REVOTE_TAG_DAYS` AND `first_seen ≤ now − MATURITY`) — setelah itu tampil VALID polos (tetap dibayar langsung); badge amber `VOTE ULANG SEGERA` (outline dashed, elemen ke-9) utk baris VALID dgn `expired_cut < last_vote ≤ warn_cut = now − (VEX_STALE_DAYS − VEX_WARN_DAYS)` (default 3 → umur 25..28 hr, overlay band VALID, ⊖ pin/tinting); `_freshness_cutoffs` → 4-tuple `(new_cut, warn_cut, exp_cut, hid_cut)`; `_search_rows` tag `revote` HANYA bila `last_vote > revote_cut` (revoter lebih lama → `valid`), + field `expiring` (main list, `?q=`, `/api/search`); legenda mint `TAG MINT = REVOTE KURANG DARI 1 HARI · VALID LANGSUNG · N AKUN` + legenda amber `TAG AMBER = KADALUARSA DALAM {WARN} HARI · N PEMILIH`; `_stats` 9-tuple (+`n_expiring`, `n_revote` = tag-1-hari utk legenda); oracle test_dashboard (revoter1 `_iso(0)` tetap bertag; expire1 `_iso(26)` staked-1290 → valid + expiring; TOTAL 129; config defaults)
|
||||
V33: reward = max(saldo liquid setelah − sebelum, 0) saat klaim (settle delay ≥ 10s); 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
|
||||
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
|
||||
@@ -181,3 +183,6 @@ B5|2026-08-06|`DISTRIBUTE_ENABLED` pakai `not in ('1','true','yes')` → default
|
||||
B6|2026-08-06|mirror `_parse_weekday` di dashboard memakai kunci nama hari id-ID (`sabtu`) tapi config default `sat` (singkatan EN) → ValueError 'sat'; drift-guard oracle tangkap|kunci parser = singkatan EN `mon..sun` (sama dgn distribute_loop.WEEKDAYS), V38 + drift-guard
|
||||
B7|2026-08-06|claim service di swarm mati total — log `[Error] Siklus klaim gagal: 0` tiap siklus: `fetch_producer_last_claim` parse respons `get_table_rows` sbg list (`data[0]`) padahal node balikin dict `{rows:[...]}` → `KeyError: 0` (str `0`); oracle `test_claim` mock `fetch_producer_last_claim` langsung → parse tak pernah diuji|parse `data.get('rows')` bentuk asli, bentuk tak dikenal → None (⊥ crash); oracle mock `_post_json` dgn bentuk respons asli (V39)
|
||||
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)
|
||||
@@ -4,8 +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 = selisih saldo liquid (setelah − sebelum klaim,
|
||||
settle delay); 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
|
||||
@@ -18,13 +23,14 @@ Modul logika (⊥ CLI) — `claim_loop.py` yang menjalankan loop tak-berhenti.
|
||||
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal, ROUND_FLOOR
|
||||
from decimal import Decimal, ROUND_FLOOR, InvalidOperation
|
||||
|
||||
import db
|
||||
import telegram
|
||||
import distribute as dist
|
||||
from config import (API_NODE, BP_FEE_PERCENT, BP_FEE_WALLET, BP_PRIVATE_KEY,
|
||||
CLAIM_RETRY_SECONDS, TARGET_BP)
|
||||
CLAIM_RETRY_SECONDS, DATABISNIS_API, TARGET_BP,
|
||||
TOKEN_CONTRACT)
|
||||
|
||||
VEX_PREC = Decimal('0.0001')
|
||||
SETTLE_SECONDS = 10 # tunggu finalitas sebelum baca saldo setelah klaim
|
||||
@@ -77,8 +83,24 @@ def next_window(now):
|
||||
Recovery: bila ≥24 jam sudah lewat sejak klaim terakhir, jendela = `now`
|
||||
(klaim dibolehkan) — jendela yang terlewat (container down / poll habis)
|
||||
⊥ jadi spin-timeout selamanya; klaim dikejar segera, lalu jendela dihitung
|
||||
ulang dari last_claim_time yang baru."""
|
||||
last = parse_claim_time(fetch_producer_last_claim())
|
||||
ulang dari last_claim_time yang baru.
|
||||
|
||||
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:
|
||||
raw = fetch_producer_last_claim()
|
||||
if raw:
|
||||
break
|
||||
except Exception:
|
||||
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
|
||||
window = last + timedelta(hours=24)
|
||||
@@ -105,50 +127,135 @@ def build_claim_action():
|
||||
return linked.sign(key=BP_PRIVATE_KEY)
|
||||
|
||||
|
||||
def _claim_time_advanced(before_raw, after_raw):
|
||||
"""True bila `last_claim_time` benar-benar MAJU dari nilai sebelum kirim.
|
||||
|
||||
Heuristik pengganti "dalam 24 jam terakhir" (yang memicu false-success:
|
||||
klaim lama yang masih <24 jam membuat percobaan yang ditolak dianggap
|
||||
mendarat). Hanya pergerakan nyata (`after > before`) yang membuktikan tx
|
||||
diterima chain; baseline tak terbaca → ⊥ dianggap maju (aman, retry)."""
|
||||
before = parse_claim_time(before_raw)
|
||||
after = parse_claim_time(after_raw)
|
||||
return before is not None and after is not None and after > before
|
||||
|
||||
|
||||
def reward_from_tx(txid):
|
||||
"""Reward BP dari isi tx klaim via Hyperion: jumlah transfer `vex.bpay`/
|
||||
`vex.vpay` → TARGET_BP. → Decimal | None (Hyperion tak terjangkau / tx tak
|
||||
ada / bentuk tak dikenal). Prioritas pengukuran (V45) — ⊥ selisih saldo
|
||||
yang flaky/stale. Tx yang tak `executed` → None."""
|
||||
if not txid:
|
||||
return None
|
||||
try:
|
||||
resp = dist.requests.get(f'{DATABISNIS_API}/v2/history/get_transaction',
|
||||
params={'id': txid}, timeout=10)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
except Exception:
|
||||
return None
|
||||
if not (data or {}).get('executed'):
|
||||
return None
|
||||
total = Decimal('0')
|
||||
for act in (data.get('actions') or []):
|
||||
a = act.get('act') or {}
|
||||
if a.get('account') != TOKEN_CONTRACT or a.get('name') != 'transfer':
|
||||
continue
|
||||
d = a.get('data') or {}
|
||||
if (d.get('to') == TARGET_BP
|
||||
and d.get('from') in ('vex.bpay', 'vex.vpay')):
|
||||
try:
|
||||
total += Decimal(str(d.get('quantity', '')).split()[0])
|
||||
except (ValueError, IndexError, InvalidOperation):
|
||||
continue
|
||||
return total
|
||||
|
||||
|
||||
def _measure_reward(txid, before):
|
||||
"""Reward klaim: prioritas isi tx (V45) → fallback selisih saldo.
|
||||
|
||||
Return `Decimal` | None bila keduanya gagal (reward tak terukur)."""
|
||||
reward = reward_from_tx(txid)
|
||||
if reward is not None:
|
||||
return reward
|
||||
for _ in range(3):
|
||||
time.sleep(SETTLE_SECONDS)
|
||||
after = dist.fetch_balance()
|
||||
if after is not None:
|
||||
return max(after - before, Decimal('0'))
|
||||
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 selisih saldo — reward tak hilang (V34).
|
||||
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()
|
||||
if before is None:
|
||||
return 'retry'
|
||||
try:
|
||||
last_before = fetch_producer_last_claim()
|
||||
except Exception:
|
||||
last_before = None
|
||||
txid = None
|
||||
landed = False
|
||||
try:
|
||||
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 — klaim dlm 24 jam terakhir? → mendarat
|
||||
last = parse_claim_time(fetch_producer_last_claim())
|
||||
if last is not None and datetime.now() - last < timedelta(hours=24):
|
||||
landed = True
|
||||
landed = _confirm_landed(txid, last_before)
|
||||
if not landed:
|
||||
print(f'[Peringatan] klaim gagal: {exc}', flush=True)
|
||||
return 'retry'
|
||||
time.sleep(SETTLE_SECONDS)
|
||||
after = dist.fetch_balance()
|
||||
if after is None:
|
||||
print('[Peringatan] saldo setelah klaim tak terbaca — reward tak '
|
||||
'diukur (cek manual)', flush=True)
|
||||
after = before
|
||||
reward = max(after - before, Decimal('0'))
|
||||
fee = (reward * BP_FEE_PERCENT).quantize(VEX_PREC, rounding=ROUND_FLOOR)
|
||||
fee_status = 'pending' if fee > 0 else 'skipped'
|
||||
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')
|
||||
txid_label = txid[:16] + '...' if txid else '?'
|
||||
if reward is None:
|
||||
# klaim mendarat tapi reward tak terukur → fee pending utk cek manual
|
||||
db.record_claim(now.strftime('%Y-%m-%d'), txid, 0.0, 0.0, 'pending',
|
||||
now_iso)
|
||||
print(f'[Peringatan] klaim mendarat (txid {txid_label}) tapi reward '
|
||||
f'tak terukur — fee pending utk cek manual', flush=True)
|
||||
telegram.notify_claim_unmeasured(txid_label)
|
||||
return 'claimed'
|
||||
fee = (reward * BP_FEE_PERCENT).quantize(VEX_PREC, rounding=ROUND_FLOOR)
|
||||
fee_status = 'pending' if fee > 0 else 'skipped'
|
||||
db.record_claim(now.strftime('%Y-%m-%d'), txid, float(reward), float(fee),
|
||||
fee_status, now_iso)
|
||||
txid_label = txid[:16] + '...' if txid else '?'
|
||||
print(f'[OK] klaim reward {reward} VEX (txid {txid_label}) | '
|
||||
f'fee {fee} VEX → {BP_FEE_WALLET}', flush=True)
|
||||
telegram.notify_claim(reward, fee, BP_FEE_WALLET)
|
||||
|
||||
@@ -84,6 +84,14 @@ def notify_claim(reward, fee, wallet):
|
||||
)
|
||||
|
||||
|
||||
def notify_claim_unmeasured(txid_label):
|
||||
"""Klaim mendarat tapi reward tak terukur (V45) — internal, ⊥ "sukses 0"."""
|
||||
send_internal(
|
||||
f'KLAIM MENDARAT · REWARD TAK TERUKUR\n'
|
||||
f'Periksa reward & fee BP manual (txid {txid_label})'
|
||||
)
|
||||
|
||||
|
||||
def notify_claim_failure(exc):
|
||||
"""Kegagalan siklus klaim — kanal internal saja (V36)."""
|
||||
send_internal(
|
||||
|
||||
+164
-7
@@ -69,9 +69,13 @@ def main():
|
||||
_tg_comm = telegram.TELEGRAM_COMMUNITY_CHAT_IDS
|
||||
_tg_int = telegram.TELEGRAM_INTERNAL_CHAT_IDS
|
||||
_tg_post = telegram.requests.post
|
||||
_req_get = dist.requests.get
|
||||
telegram.TELEGRAM_BOT_TOKEN = ''
|
||||
telegram.TELEGRAM_COMMUNITY_CHAT_IDS = []
|
||||
telegram.TELEGRAM_INTERNAL_CHAT_IDS = []
|
||||
# Default: pengukuran reward via selisih saldo (tx-based diuji terpisah).
|
||||
_orig_reward_from_tx = claim.reward_from_tx
|
||||
claim.reward_from_tx = lambda txid: None
|
||||
try:
|
||||
# ————— V32: parse_claim_time — format chain —————
|
||||
check('V32 parse format %f',
|
||||
@@ -106,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))
|
||||
|
||||
@@ -191,21 +204,164 @@ def main():
|
||||
row = last_claim_row()
|
||||
check('V34 reward terukur', row[3] == 10.0 and row[2] == 'txL')
|
||||
|
||||
# ————— V34: verify None + last_claim_time maju → dianggap mendarat —————
|
||||
# ————— V34/V45: verify None + last_claim_time benar2 MAJU → mendarat —————
|
||||
fresh_db('reconcile')
|
||||
claim.fetch_producer_last_claim = lambda: (
|
||||
datetime.now() - timedelta(hours=1)
|
||||
).strftime('%Y-%m-%dT%H:%M:%S')
|
||||
last_state = {'n': 0}
|
||||
|
||||
def advancing_last():
|
||||
last_state['n'] += 1
|
||||
if last_state['n'] == 1: # baseline sebelum kirim
|
||||
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') # maju → mendarat
|
||||
|
||||
claim.fetch_producer_last_claim = advancing_last
|
||||
balances = iter([Decimal('10.0000'), Decimal('12.0000')])
|
||||
dist.fetch_balance = lambda: next(balances)
|
||||
claim.build_claim_action = lambda: FakeSigned(
|
||||
'txN', exc=RuntimeError('timeout'))
|
||||
dist.verify_txid = lambda txid: None
|
||||
check('V34 verify None + last maju → claimed',
|
||||
check('V45 verify None + last MAJU → claimed',
|
||||
claim._try_claim_once() == 'claimed')
|
||||
check('V34 reward tetap diukur',
|
||||
check('V45 reward tetap diukur',
|
||||
last_claim_row()[3] == 2.0)
|
||||
|
||||
# ————— V45: verify None + last TIDAK maju → retry (⊥ false success) —————
|
||||
fresh_db('stale')
|
||||
claim.fetch_producer_last_claim = lambda: (
|
||||
datetime.now() - timedelta(hours=1)
|
||||
).strftime('%Y-%m-%dT%H:%M:%S')
|
||||
dist.fetch_balance = lambda: Decimal('100.0000')
|
||||
claim.build_claim_action = lambda: FakeSigned(
|
||||
'txS', exc=RuntimeError('timeout'))
|
||||
dist.verify_txid = lambda txid: None
|
||||
check('V45 last tak maju → retry (⊥ sukses-0)',
|
||||
claim._try_claim_once() == 'retry')
|
||||
db.ensure_claim_schema()
|
||||
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: (
|
||||
datetime.now() - timedelta(hours=25)
|
||||
).strftime('%Y-%m-%dT%H:%M:%S')
|
||||
dist.fetch_balance = lambda: Decimal('100.0000') # ⊥ dipakai
|
||||
claim.build_claim_action = lambda: FakeSigned('txR')
|
||||
dist.verify_txid = lambda txid: True
|
||||
|
||||
def fake_hyperion(url, params=None, timeout=None):
|
||||
return type('R', (), {
|
||||
'ok': True,
|
||||
'raise_for_status': lambda self: None,
|
||||
'json': lambda self: {
|
||||
'executed': True,
|
||||
'actions': [
|
||||
{'act': {'account': 'vexcore', 'name': 'claimrewards',
|
||||
'data': {'owner': 'databisnisid'}}},
|
||||
{'act': {'account': 'vex.token', 'name': 'issue',
|
||||
'data': {'to': 'vexcore',
|
||||
'quantity': '82744.9200 VEX'}}},
|
||||
{'act': {'account': 'vex.token', 'name': 'transfer',
|
||||
'data': {'from': 'vex.bpay', 'to': 'databisnisid',
|
||||
'quantity': '638.9244 VEX'}}},
|
||||
{'act': {'account': 'vex.token', 'name': 'transfer',
|
||||
'data': {'from': 'vex.vpay', 'to': 'databisnisid',
|
||||
'quantity': '1200.3217 VEX'}}},
|
||||
{'act': {'account': 'vex.token', 'name': 'transfer',
|
||||
'data': {'from': 'databisnisid', 'to': 'someone',
|
||||
'quantity': '5.0000 VEX'}}},
|
||||
],
|
||||
},
|
||||
})()
|
||||
|
||||
dist.requests.get = fake_hyperion
|
||||
claim.reward_from_tx = _orig_reward_from_tx
|
||||
check('V45 reward_from_tx jumlah bpay+vpay',
|
||||
claim.reward_from_tx('txR') == Decimal('1839.2461'))
|
||||
dist.requests.get = fake_hyperion
|
||||
check('V45 siklus: reward dari tx → claimed',
|
||||
claim._try_claim_once() == 'claimed')
|
||||
row = last_claim_row()
|
||||
check('V45 reward tx 1839.2461 + fee 183.9246 pending',
|
||||
row[3] == 1839.2461 and row[4] == 183.9246
|
||||
and row[5] == 'pending' and row[2] == 'txR')
|
||||
dist.requests.get = _req_get
|
||||
claim.reward_from_tx = lambda txid: None
|
||||
|
||||
# ————— V45: reward tak terukur (tx gagal + delta gagal) → fee pending —————
|
||||
fresh_db('unmeasured')
|
||||
claim.fetch_producer_last_claim = lambda: (
|
||||
datetime.now() - timedelta(hours=25)
|
||||
).strftime('%Y-%m-%dT%H:%M:%S')
|
||||
claim.reward_from_tx = lambda txid: None
|
||||
bal_seq = iter([Decimal('100.0000'), None, None, None]) # before ok, after ⊥
|
||||
dist.fetch_balance = lambda: next(bal_seq)
|
||||
claim.build_claim_action = lambda: FakeSigned('txU')
|
||||
dist.verify_txid = lambda txid: True
|
||||
notifs = []
|
||||
_orig_nc = telegram.notify_claim
|
||||
_orig_ncu = telegram.notify_claim_unmeasured
|
||||
|
||||
def capture_ni(label):
|
||||
notifs.append('unmeasured')
|
||||
|
||||
telegram.notify_claim_unmeasured = capture_ni
|
||||
telegram.notify_claim = lambda r, f, w: notifs.append('claimed')
|
||||
check('V45 reward tak terukur → claimed (baris dicatat)',
|
||||
claim._try_claim_once() == 'claimed')
|
||||
row = last_claim_row()
|
||||
check('V45 fee pending utk cek manual (⊥ skipped)',
|
||||
row[4] == 0.0 and row[5] == 'pending')
|
||||
check('V45 notif cek manual (⊥ sukses-0)',
|
||||
'unmeasured' in notifs and 'claimed' not in notifs)
|
||||
telegram.notify_claim = _orig_nc
|
||||
telegram.notify_claim_unmeasured = _orig_ncu
|
||||
|
||||
# ————— V32: klaim ditolak → retry; poll deadline lewat → timeout —————
|
||||
fresh_db('timeout')
|
||||
claim.build_claim_action = lambda: FakeSigned(
|
||||
@@ -255,6 +411,7 @@ def main():
|
||||
telegram.TELEGRAM_COMMUNITY_CHAT_IDS = _tg_comm
|
||||
telegram.TELEGRAM_INTERNAL_CHAT_IDS = _tg_int
|
||||
telegram.requests.post = _tg_post
|
||||
dist.requests.get = _req_get
|
||||
|
||||
print('\nSEMUA UJI PASS')
|
||||
|
||||
|
||||
Reference in new issue
Block a user