653 lines
31 KiB
Python
653 lines
31 KiB
Python
"""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())
|