"""Uji verifikasi klaim reward BP (oracle §V32..V34). `./venv/bin/python test_claim.py` harus exit 0. Memakai DB sementara + mock jaringan/pyntelope — ⊥ klaim nyata. Verifikasi: jendela klaim (V32), parse respons `get_table_rows` bentuk asli `{rows:[...]}` + recovery window lewat, math fee (V33), siklus penuh klaim→fee, retry fee, resume, reward 0 → skipped, klaim yang mendarat walau timeout (V34). """ import os import sqlite3 import sys import tempfile from datetime import datetime, timedelta from decimal import Decimal import db import claim import telegram import distribute as dist def check(tag, cond): if not cond: raise AssertionError(f'GAGAL: {tag}') print(f'ok: {tag}') class FakeSigned: """Objek SignedTransaction tiruan: id deterministik + send terkontrol.""" def __init__(self, txid, exc=None): self._txid = txid self._exc = exc def id(self): return self._txid def send(self): if self._exc is not None: raise self._exc # V61: bentuk sukses nodeos asli (whitelist `_send_rejected` di # `_send_fee` butuh `transaction_id` + receipt `executed`). return {'transaction_id': self._txid, 'processed': {'receipt': {'status': 'executed'}}} def fresh_db(tag): """DB sqlite sementara baru per bagian uji.""" tmp = tempfile.mkdtemp() db.DB_PATH = os.path.join(tmp, f'claim-{tag}.db') return db.DB_PATH def last_claim_row(): row = db.queryone( 'SELECT claim_id, run_date, claim_txid, reward, fee_amount, ' ' fee_status, fee_txid FROM claim_runs ORDER BY claim_id DESC ' 'LIMIT 1') return row def fake_post(data): return type('R', (), {'raise_for_status': lambda self: None, 'json': lambda self: data})() def main(): # Matikan sleep nyata (settle/poll) + Telegram selama uji. claim.time.sleep = lambda s: None _tg_token = telegram.TELEGRAM_BOT_TOKEN _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', claim.parse_claim_time('2026-08-05T07:01:44.500') == datetime(2026, 8, 5, 7, 1, 44, 500000)) check('V32 parse format detik', claim.parse_claim_time('2026-08-05T07:01:44') == datetime(2026, 8, 5, 7, 1, 44)) check('V32 parse kosong → None', claim.parse_claim_time(None) is None) check('V32 parse sampah → None', claim.parse_claim_time('x') is None) # ————— V32: fetch_producer_last_claim — bentuk asli get_table_rows ————— claim._post_json = lambda url, payload: { 'rows': [{'last_claim_time': '2026-08-05T07:01:44.500'}], 'more': False, 'next_key': ''} check('V32 parse respons asli {rows:[...]} → waktu', claim.fetch_producer_last_claim() == '2026-08-05T07:01:44.500') claim._post_json = lambda url, payload: {'rows': [], 'more': False, 'next_key': ''} check('V32 rows kosong → None', claim.fetch_producer_last_claim() is None) claim._post_json = lambda url, payload: {} check('V32 respons tak dikenal → None (⊥ crash)', claim.fetch_producer_last_claim() is None) # ————— V32: next_window = last + 24 jam; lewat → klaim segera ————— claim.fetch_producer_last_claim = lambda: '2026-08-05T07:01:44.500' check('V32 jendela = last + 24 jam', claim.next_window(datetime(2026, 8, 6, 5, 0, 0)) == datetime(2026, 8, 6, 7, 1, 44, 500000)) check('V32 window lewat (≥24 jam) → now (recovery, ⊥ deadlock)', claim.next_window(datetime(2026, 8, 6, 12, 0, 0)) == datetime(2026, 8, 6, 12, 0, 0)) claim.fetch_producer_last_claim = lambda: None 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)) # ————— V49: window math UTC-naive (konsisten dgn last_claim_time chain) ————— now_utc = claim._utcnow() check('V49 _utcnow UTC-naive (⊥ tzinfo lokal)', now_utc.tzinfo is None) # step dengan now jauh sebelum last+24h → sleep (⊥ poll prematur) fresh_db('prewindow') claim.fetch_producer_last_claim = lambda: ( claim._utcnow() - timedelta(hours=12) ).strftime('%Y-%m-%dT%H:%M:%S') check('V49 before window → sleep (⊥ poll prematur)', claim.step() == 'sleep') # ————— V32/V33: siklus penuh — klaim → catat → fee terkirim ————— fresh_db('cycle') claim.fetch_producer_last_claim = lambda: ( claim._utcnow() - timedelta(hours=25) ).strftime('%Y-%m-%dT%H:%M:%S') balances = iter([Decimal('100.0000'), Decimal('110.0004')]) dist.fetch_balance = lambda: next(balances) claim.build_claim_action = lambda: FakeSigned('txclaim') dist.verify_txid = lambda txid: True r1 = claim.step() check('V32 step klaim sukses → claimed', r1 == 'claimed') row = last_claim_row() check('V32 klaim tercatat (reward 10.0004, fee 1.0000 floor)', row is not None and row[3] == 10.0004 and row[4] == 1.0 and row[2] == 'txclaim' and row[5] == 'pending') memos = [] def fake_fee_sign(to, amount, memo): memos.append((to, amount, memo)) return FakeSigned('txfee') dist.build_signed_transfer = fake_fee_sign r2 = claim.step() check('V34 fee terkirim → fee_sent', r2 == 'fee_sent') check('V33 fee ke wallet bpdbsjasprod', memos[-1][0] == 'bpdbsjasprod') check('V33 memo BP FEE + tanggal', memos[-1][2] == f'BP FEE {claim._utcnow().strftime("%Y-%m-%d")}') row = last_claim_row() check('V34 fee ditandai sent + txid', row[5] == 'sent' and row[6] == 'txfee') # ————— V33/V34: fee gagal → failed → diulang sampai terkirim ————— fresh_db('retry') db.record_claim('2026-08-05', 'txold', 10.0, 1.0, 'pending', '2026-08-05T08:00:00') dist.build_signed_transfer = lambda to, amount, memo: FakeSigned( 'txfail', exc=RuntimeError('net down')) dist.verify_txid = lambda txid: False check('V34 fee gagal → fee_failed', claim.step() == 'fee_failed') check('V34 status failed', last_claim_row()[5] == 'failed') dist.build_signed_transfer = lambda to, amount, memo: FakeSigned('txok') check('V34 fee diulang → fee_sent', claim.step() == 'fee_sent') row = last_claim_row() 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') 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') # pending_claim_fee ⊥ memuat fee 0 → step lanjut ke jadwal (sleep) claim.fetch_producer_last_claim = lambda: ( 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') claim.fetch_producer_last_claim = lambda: ( claim._utcnow() - timedelta(hours=25) ).strftime('%Y-%m-%dT%H:%M:%S') balances = iter([Decimal('50.0000'), Decimal('60.0000')]) dist.fetch_balance = lambda: next(balances) claim.build_claim_action = lambda: FakeSigned( 'txL', exc=RuntimeError('broadcast timeout')) dist.verify_txid = lambda txid: True check('V34 timeout tapi mendarat → claimed', claim._try_claim_once() == 'claimed') row = last_claim_row() check('V34 reward terukur', row[3] == 10.0 and row[2] == 'txL') # ————— V34/V45: verify None + last_claim_time benar2 MAJU → mendarat ————— fresh_db('reconcile') last_state = {'n': 0} def advancing_last(): last_state['n'] += 1 if last_state['n'] == 1: # baseline sebelum kirim 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') # 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('V45 verify None + last MAJU → claimed', claim._try_claim_once() == 'claimed') 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: ( claim._utcnow() - 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: ( claim._utcnow() - 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 (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') 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) # ————— V50: Hyperion `False` (lag indeks) tapi last_claim_time MAJU ————— # tx BARU SAJA mendarat belum terindeks Hyperion → verify False (⊥ None); # landing tetap dideteksi via kemajuan `last_claim_time` (⊥ retry-loop). fresh_db('lagindex') last_l = {'n': 0} def advancing_lag(): last_l['n'] += 1 if last_l['n'] == 1: 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') claim.fetch_producer_last_claim = advancing_lag balances = iter([Decimal('20.0000'), Decimal('25.0000')]) dist.fetch_balance = lambda: next(balances) claim.build_claim_action = lambda: FakeSigned('txR4') # send ⊥ raise dist.verify_txid = lambda txid: False # Hyperion belum indeks (lag) check('V50 verify False + last MAJU → claimed (⊥ retry-loop)', claim._try_claim_once() == 'claimed') 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') # ————— V56/B18: klaim mendarat attempt 1, attempt 2 ditolak (already # claimed) — before_balance STABIL per window → reward tetap terukur ————— # Kasus 2026-08-10: attempt 1 (bfa9ab02) mendarat tapi confirm baca # basi → retry; attempt 2 (b0a8b2f9) ditolak chain tapi last_claim_time # SUDAH maju → dianggap mendarat; before_balance dibaca SEKALI di awal # window (belum termasuk reward) → delta fallback mengukur reward asli. # Tanpa baseline stabil, `before` attempt 2 sudah post-credit → delta 0 # → alert KLAIM MENDARAT palsu. fresh_db('b18land') b18 = {'n': 0} def b18_confirm(): b18['n'] += 1 if b18['n'] <= 4: # baseline + 3 baca attempt 1 → basi (stale) 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 = b18_confirm claim.reward_from_tx = lambda txid: None # tx ditolak → ⊥ terbaca bal_seq = iter([Decimal('1000.0000'), Decimal('2679.3365')]) dist.fetch_balance = lambda: next(bal_seq) # 1000 = baseline window sends = {'n': 0} def send_land_then_reject(): sends['n'] += 1 if sends['n'] == 1: return FakeSigned('txA1') # attempt 1: mendarat return FakeSigned('txA2', exc=RuntimeError('already claimed')) claim.build_claim_action = send_land_then_reject dist.verify_txid = lambda txid: False # Hyperion lag kedua kali notifs = [] _orig_ncA = telegram.notify_claim _orig_ncuA = telegram.notify_claim_unmeasured telegram.notify_claim_unmeasured = lambda t: notifs.append('unmeasured') telegram.notify_claim = lambda r, f, w: notifs.append('claimed') deadline = claim._utcnow() + timedelta(seconds=3600) check('V56 klaim attempt-1-mendarat → poll_claim claimed', claim.poll_claim(deadline) == 'claimed') row = last_claim_row() check('V56 reward asli terukur via delta (before_balance stabil)', row[3] == 1679.3365 and row[4] == 167.9336 and row[5] == 'pending' and row[2] == 'txA2') check('V56 ⊖ alert tak-terukur, notif sukses dipakai', 'unmeasured' not in notifs and 'claimed' in notifs) telegram.notify_claim = _orig_ncA telegram.notify_claim_unmeasured = _orig_ncuA # ————— V45: reward dari isi tx (bpay+vpay) → prioritas ————— fresh_db('txreward') claim.fetch_producer_last_claim = lambda: ( claim._utcnow() - 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 # ————— V60: reward_from_tx failover pool Hyperion ————— _orig_hynodes = list(dist.HYPERION_NODES) claim.reward_from_tx = _orig_reward_from_tx # fungsi asli (bukan stub) def fake_hyperion_failover(url, params=None, timeout=None): if url.startswith('http://hyperion-dead'): raise dist.requests.RequestException('hyperion mati') return type('R', (), { 'ok': True, 'raise_for_status': lambda self: None, 'json': lambda self: { 'executed': True, 'actions': [ {'act': {'account': 'vex.token', 'name': 'transfer', 'data': {'from': 'vex.bpay', 'to': 'databisnisid', 'quantity': '10.0000 VEX'}}}, ], }, })() dist.HYPERION_NODES = ['http://hyperion-dead', 'http://hyperion-alive'] dist.requests.get = fake_hyperion_failover check('V60 reward_from_tx failover → host cadangan', claim.reward_from_tx('txF') == Decimal('10.0000')) dist.requests.get = _req_get dist.HYPERION_NODES = _orig_hynodes # ————— V45: reward tak terukur (tx gagal + delta gagal) → fee pending ————— fresh_db('unmeasured') 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 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( 'txT', exc=RuntimeError('rejected')) dist.verify_txid = lambda txid: False dist.fetch_balance = lambda: Decimal('100.0000') check('V32 klaim ditolak → retry', claim._try_claim_once() == 'retry') check('V32 deadline lewat → timeout', claim.poll_claim(claim._utcnow() - timedelta(seconds=1)) == 'timeout') # ————— resume: fee failed dari DB → step mengirimnya ————— fresh_db('resume') db.record_claim('2026-08-05', 'txr', 10.0, 1.0, 'failed', '2026-08-05T08:00:00') dist.build_signed_transfer = lambda to, amount, memo: FakeSigned('txr1') dist.verify_txid = lambda txid: True check('V34 resume fee dari DB → fee_sent', claim.step() == 'fee_sent') check('V34 resume → sent', last_claim_row()[5] == 'sent' and last_claim_row()[6] == 'txr1') # ————— V61: fee ditolak chain (HTTP-500 body, V46/B11) → ⊖ 'sent' ————— # `_send_fee` ⊖ boleh tandai 'sent' utk fee yang chain TOLAK — pyntelope # mengembalikan 500 sbg body, ⊖ raise (V61-F1). Fee harus tersisa # 'failed' utk diulang, ⊖ pernah 'sent' dgn txid yg ⊥ mendarat. fresh_db('feereject') db.record_claim('2026-08-05', 'txz', 10.0, 1.0, 'failed', '2026-08-05T08:00:00') class FakeFeeReject: def id(self): return 'txfeej' def send(self): return {'code': 500, 'error': {'what': 'duplicate transaction'}} dist.build_signed_transfer = (lambda to, amount, memo: FakeFeeReject()) dist.verify_txid = lambda txid: False # txid penolakan ⊥ mendarat check('V61 fee rejection-500 → fee_failed (⊥ sent)', claim.step() == 'fee_failed') row = last_claim_row() check('V61 fee rejection → status failed (diulang), ⊖ sent', row[5] == 'failed') dist.build_signed_transfer = lambda to, amount, memo: FakeSigned('txr1') # ————— V36: klaim reward → kanal INTERNAL saja, ⊥ komunitas ————— tg = {'posts': []} def tg_post(url, json=None, timeout=None): tg['posts'].append((url, json['chat_id'], json['text'])) return fake_post({}) telegram.TELEGRAM_BOT_TOKEN = '123:TEST' telegram.TELEGRAM_COMMUNITY_CHAT_IDS = ['-1001'] telegram.TELEGRAM_INTERNAL_CHAT_IDS = ['-1002'] telegram.requests.post = tg_post telegram.notify_claim('10.0000', '1.0000', 'bpdbsjasprod') telegram.notify_claim_failure(RuntimeError('net down')) check('V36 klaim → internal saja', len(tg['posts']) == 2 and all(p[1] == '-1002' for p in tg['posts'])) check('V36 ⊥ komunitas utk klaim', not any(p[1] == '-1001' for p in tg['posts'])) check('V36 isi notif klaim', 'KLAIM REWARD SUKSES' in tg['posts'][0][2] and 'KLAIM GAGAL' in tg['posts'][1][2]) finally: claim.time.sleep = time.sleep telegram.TELEGRAM_BOT_TOKEN = _tg_token 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') if __name__ == '__main__': import time sys.exit(main())