Files
databisnisid/claim.py
T

222 lines
8.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Klaim reward BP + transfer fee harian: `vexcore::claimrewards` utk TARGET_BP.
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 →
`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
`pending`/`failed` diulang tiap siklus + dilanjutkan saat restart (crash-safe,
V34). ⊥ mutex dgn distribute.py — distribute beku saldo di awal run (V20),
klaim yang jatuh di tengah run didefer ke run berikutnya.
Modul logika (⊥ CLI) — `claim_loop.py` yang menjalankan loop tak-berhenti.
"""
import time
from datetime import datetime, timedelta
from decimal import Decimal, ROUND_FLOOR
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)
VEX_PREC = Decimal('0.0001')
SETTLE_SECONDS = 10 # tunggu finalitas sebelum baca saldo setelah klaim
POLL_SPAN_SECONDS = 7200 # batas poll sekali jendela (2 jam)
def _post_json(url, payload, max_retries=3, timeout=30):
"""POST JSON dengan retry (node publik flaky) — pola `get_voters`."""
for attempt in range(max_retries):
try:
resp = dist.requests.post(url, json=payload, timeout=timeout)
resp.raise_for_status()
return resp.json()
except Exception:
if attempt == max_retries - 1:
raise
print(f'[Peringatan] Gagal menghubungi node, mencoba lagi... '
f'({attempt + 1}/{max_retries})', flush=True)
time.sleep(2)
def fetch_producer_last_claim():
"""`last_claim_time` baris producers utk TARGET_BP dari chain (| None).
`get_table_rows` mengembalikan dict `{"rows":[...]}` (bukan list) — parse
bentuk asli; bentuk tak dikenal → None (⊥ crash → siklus tak mati)."""
data = _post_json(f'{API_NODE}/v1/chain/get_table_rows',
{'json': True, 'code': 'vexcore', 'scope': 'vexcore',
'table': 'producers', 'limit': 1,
'lower_bound': TARGET_BP, 'upper_bound': TARGET_BP})
rows = (data or {}).get('rows') or []
return (rows[0] or {}).get('last_claim_time') if rows else None
def parse_claim_time(raw):
"""ISO `last_claim_time` chain → datetime; format tak dikenal → None."""
if not raw:
return None
for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
try:
return datetime.strptime(raw, fmt)
except ValueError:
continue
return None
def next_window(now):
"""Jendela klaim berikutnya = `last_claim_time` + 24 jam; ⊥ last → 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())
if last is None:
return now
window = last + timedelta(hours=24)
return window if now < window else now
def build_claim_action():
"""Bangun + tanda tangani aksi `vexcore::claimrewards` via pyntelope.
Kembalikan objek SignedTransaction (⊥ broadcast). ABI + TAPOS dari node
saat `link()` — pola `distribute.build_signed_transfer`.
"""
from pyntelope import Action, Authorization, Data, Net, Transaction, types
trx = Transaction(actions=[
Action(
account='vexcore',
name='claimrewards',
authorization=[Authorization(actor=TARGET_BP, permission='active')],
data=[Data(name='owner', value=types.Name(TARGET_BP))],
)
])
linked = trx.link(net=Net(host=API_NODE))
return linked.sign(key=BP_PRIVATE_KEY)
def _try_claim_once():
"""Satu percobaan klaim: klaim → 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).
"""
before = dist.fetch_balance()
if before is None:
return 'retry'
txid = None
landed = False
try:
signed = build_claim_action()
txid = signed.id()
signed.send()
landed = True
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
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'
now = datetime.now()
now_iso = now.isoformat(timespec='seconds')
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)
return 'claimed'
def poll_claim(deadline):
"""Poll `claimrewards` tiap `CLAIM_RETRY_SECONDS` sampai sukses / deadline.
Return `claimed` | `timeout` (deadline lewat — jadwal dihitung ulang).
"""
while datetime.now() < deadline:
status = _try_claim_once()
if status == 'claimed':
return 'claimed'
time.sleep(CLAIM_RETRY_SECONDS)
print(f'[Peringatan] jendela klaim habis tanpa sukses '
f'({deadline.isoformat(timespec="seconds")})', flush=True)
return 'timeout'
def _send_fee(claim):
"""Kirim fee satu baris `claim_runs` (retry-until-sent, verifikasi V22).
`claim` = baris dari `db.pending_claim_fee()`. Return True bila terkirim
atau fee 0 (skipped); False → diulang siklus berikutnya.
"""
claim_id, run_date = claim[0], claim[1]
fee = Decimal(str(claim[4]))
if fee <= 0:
db.update_claim_fee(claim_id, 'skipped')
return True
first_fail = claim[5] != 'failed' # notif gagal ⊥ sekali per klaim
memo = f'BP FEE {run_date}'
txid = None
try:
signed = dist.build_signed_transfer(BP_FEE_WALLET, fee, memo)
txid = signed.id()
signed.send()
except Exception as exc:
if txid is not None and dist.verify_txid(txid) is True:
pass # mendarat walau timeout — lanjut tandai sent
else:
db.update_claim_fee(claim_id, 'failed', fee_txid=txid)
print(f'[GAGAL] fee {fee} VEX → {BP_FEE_WALLET}: {exc}', flush=True)
if first_fail:
telegram.notify_claim_failure(exc)
return False
db.update_claim_fee(claim_id, 'sent', fee_txid=txid)
print(f'[OK] fee {fee} VEX → {BP_FEE_WALLET} ({txid[:16]}...)', flush=True)
return True
def step():
"""Satu iterasi loop klaim. Return aksi utk uji: `fee_sent` | `fee_failed`
| `sleep` | `claimed` | `timeout`."""
claim = db.pending_claim_fee()
if claim is not None:
return 'fee_sent' if _send_fee(claim) else 'fee_failed'
now = datetime.now()
window = next_window(now)
margin = timedelta(seconds=CLAIM_RETRY_SECONDS)
if now < window - margin:
wait = (window - margin - now).total_seconds()
print(f'{now.isoformat(timespec="seconds")} tidur {wait / 3600:.2f} jam '
f'sampai {window.isoformat(timespec="seconds")}', flush=True)
time.sleep(max(wait, 0))
return 'sleep'
deadline = window + timedelta(seconds=POLL_SPAN_SECONDS)
return poll_claim(deadline)