distribusi hardened vs node mati + send ⊖ pernah menyesatkan (V58/V59)
V58 failover node pool: config += API_NODES (CSV VEX_API_NODES, default v2.vexascan.com:2096 + https://api.databisnis.id, mainnet terverifikasi) ; distribute _post_json coba SEMUA node per putaran (semua mati → RuntimeError), build_signed_transfer coba tiap node utk link+sign (net ter-bind → send ikut node sama), fetch_balance_with_retry backoff [30..180]s ~10 mnt real-run / dry-run satu attempt ; scan/claim tetap API_NODE tunggal, fee klaim mewarisi failover gratis ; oracle failover saldo + semua-mati raise + backoff + mock Net V59 send aman per pemilih (B19): (1) verify_txid → None (Hyperion down) = TAHAN sungguhan — break + failed di attempt 1, ⊖ resend dgn tapos/expiration baru → txid baru → double-send (kode lama cuma print ditahan lalu fallthrough ke retry, oracle lolos karena set MAX_ATTEMPTS=1) ; (2) _send_rejected(resp) deteksi body HTTP-500 pyntelope ⊖ raise (pola V46/B11, duplicate/ insufficient-balance) → raise → jalur verify/retry, ⊖ pernah sent utk tx yang ⊥ mendarat ; oracle hold-attempt-1 (3 attempt default, build sekali per pemilih) + rejection-500 → failed ; docs §V58/V59 + §B/B19 + T51/T52 + AGENTS image dist + claim rebuild + push (claim impor distribute)
This commit is contained in:
1 parent
8e80653df9
commit
fb16eeafaa
6 files changed
+285
-47
No files matched your search
+107
-38
@@ -20,34 +20,48 @@ import requests
|
||||
|
||||
import db
|
||||
import telegram
|
||||
from config import (API_NODE, BP_PRIVATE_KEY, DATABISNIS_API,
|
||||
from config import (API_NODES, BP_PRIVATE_KEY, DATABISNIS_API,
|
||||
DISTRIBUTE_ENABLED, DISTRIBUTE_MAX_ATTEMPTS, TARGET_BP,
|
||||
TOKEN_CONTRACT, VEX_SYMBOL)
|
||||
|
||||
VEX_PREC = Decimal('0.0001')
|
||||
|
||||
# V58: jendela retry saldo sebelum menyerah ke jadwal berikutnya (~10 menit).
|
||||
# Dipakai real-run saja; dry-run gagal cepat (single attempt).
|
||||
BALANCE_RETRY_DELAYS = [30, 60, 90, 120, 150, 180]
|
||||
|
||||
|
||||
def _post_json(url, payload, max_retries=3, timeout=30):
|
||||
"""POST JSON dengan retry (node publik flaky) — pola `get_voters`."""
|
||||
"""POST JSON dengan retry (node publik flaky) — pola `get_voters`.
|
||||
|
||||
V58: mencoba SEMUA node di `API_NODES` per putaran — gagal di node satu
|
||||
lanjut node berikutnya; baru menyerah setelah `max_retries` putaran penuh
|
||||
tanpa satu pun node berhasil.
|
||||
"""
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
resp = requests.post(url, json=payload, timeout=timeout)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
except requests.RequestException:
|
||||
if attempt == max_retries - 1:
|
||||
raise
|
||||
print(f'[Peringatan] Gagal menghubungi node, mencoba lagi... '
|
||||
f'({attempt + 1}/{max_retries})', flush=True)
|
||||
time.sleep(2)
|
||||
for node in API_NODES:
|
||||
try:
|
||||
resp = requests.post(f'{node}{url}', json=payload, timeout=timeout)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
except requests.RequestException:
|
||||
continue
|
||||
if attempt == max_retries - 1:
|
||||
raise RuntimeError(
|
||||
f'Gagal menghubungi semua node RPC {API_NODES} '
|
||||
f'({max_retries}×)')
|
||||
print(f'[Peringatan] Semua node gagal, mencoba lagi... '
|
||||
f'({attempt + 1}/{max_retries})', flush=True)
|
||||
time.sleep(2)
|
||||
|
||||
|
||||
def fetch_balance():
|
||||
"""V20: saldo liquid VEX akun `TARGET_BP` dari chain get_currency_balance.
|
||||
|
||||
Kembalikan Decimal (`0` bila kosong). Gagal total → None.
|
||||
V58: jalan lewat `_post_json` failover node pool — bila semua node gagal
|
||||
→ raise (bukan None). Kembalikan Decimal (`0` bila kosong).
|
||||
"""
|
||||
data = _post_json(f'{API_NODE}/v1/chain/get_currency_balance',
|
||||
data = _post_json('/v1/chain/get_currency_balance',
|
||||
{'code': TOKEN_CONTRACT, 'account': TARGET_BP,
|
||||
'symbol': VEX_SYMBOL})
|
||||
raw = (data or [''])[0] or ''
|
||||
@@ -57,6 +71,26 @@ def fetch_balance():
|
||||
return Decimal('0')
|
||||
|
||||
|
||||
def fetch_balance_with_retry(dry_run=False):
|
||||
"""V58: fetch saldo dgn retry backoff (~10 menit) di real-run.
|
||||
|
||||
Bila SEMUA node di `API_NODES` gagal (via `_post_json`), tidur
|
||||
`BALANCE_RETRY_DELAYS[i]` lalu coba lagi — jendela total ~10 menit sebelum
|
||||
menyerah (raise → main → notify_failure → jadwal berikutnya). Dry-run:
|
||||
satu attempt saja, gagal cepat (preview tak boleh tertahan lama).
|
||||
"""
|
||||
delays = [] if dry_run else BALANCE_RETRY_DELAYS
|
||||
for i, delay in enumerate(delays + [0]):
|
||||
try:
|
||||
return fetch_balance()
|
||||
except Exception:
|
||||
if i == len(delays):
|
||||
raise
|
||||
print(f'[Peringatan] Semua node gagal saat baca saldo — coba '
|
||||
f'lagi dalam {delay}s ({i + 1}/{len(delays)})', flush=True)
|
||||
time.sleep(delay)
|
||||
|
||||
|
||||
def compute_shares(balance, voters):
|
||||
"""V20: porsi per pemilih = floor(balance × stake ÷ total_stake), 4 desimal.
|
||||
|
||||
@@ -90,32 +124,53 @@ def verify_txid(txid):
|
||||
return None
|
||||
|
||||
|
||||
def _send_rejected(resp):
|
||||
"""V59: deteksi penolakan chain dari respons `send()`.
|
||||
|
||||
pyntelope ⊥ raise pada HTTP 500 — chain rejection datang sbg body JSON
|
||||
(pola V46/B11 di klaim): `{"code":500,"message":...,"error":{...}}` vs
|
||||
sukses `{"transaction_id":..., "processed":...}`. → True bila bentuk
|
||||
penolakan (ada `error`), False bila sukses/bentuk tak dikenal.
|
||||
"""
|
||||
return isinstance(resp, dict) and resp.get('error') is not None
|
||||
|
||||
|
||||
def build_signed_transfer(to, amount, memo):
|
||||
"""Bangun + tanda tangani tx `vex.token::transfer` via pyntelope.
|
||||
|
||||
Kembalikan objek SignedTransaction (⊥ broadcast). `amount` = Decimal.
|
||||
Mengambil ABI + TAPOS dari node saat `link()` (jaringan, sesuai pola
|
||||
retry di tingkat panggil).
|
||||
Mengambil ABI + TAPOS dari node saat `link()` — V58: coba SETIAP node di
|
||||
`API_NODES` (net di-bind ke node itu, jadi broadcast `send()` ikut ke node
|
||||
yang sama); baru raise bila semua node gagal.
|
||||
"""
|
||||
from pyntelope import Action, Authorization, Data, Net, Transaction, types
|
||||
|
||||
net = Net(host=API_NODE)
|
||||
trx = Transaction(actions=[
|
||||
Action(
|
||||
account=TOKEN_CONTRACT,
|
||||
name='transfer',
|
||||
authorization=[Authorization(actor=TARGET_BP, permission='active')],
|
||||
data=[
|
||||
Data(name='from', value=types.Name(TARGET_BP)),
|
||||
Data(name='to', value=types.Name(to)),
|
||||
Data(name='quantity',
|
||||
value=types.Asset(f'{amount:.4f} {VEX_SYMBOL}')),
|
||||
Data(name='memo', value=types.String(memo)),
|
||||
],
|
||||
)
|
||||
])
|
||||
linked = trx.link(net=net)
|
||||
return linked.sign(key=BP_PRIVATE_KEY)
|
||||
last_err = None
|
||||
for node in API_NODES:
|
||||
try:
|
||||
net = Net(host=node)
|
||||
trx = Transaction(actions=[
|
||||
Action(
|
||||
account=TOKEN_CONTRACT,
|
||||
name='transfer',
|
||||
authorization=[Authorization(actor=TARGET_BP,
|
||||
permission='active')],
|
||||
data=[
|
||||
Data(name='from', value=types.Name(TARGET_BP)),
|
||||
Data(name='to', value=types.Name(to)),
|
||||
Data(name='quantity',
|
||||
value=types.Asset(f'{amount:.4f} {VEX_SYMBOL}')),
|
||||
Data(name='memo', value=types.String(memo)),
|
||||
],
|
||||
)
|
||||
])
|
||||
linked = trx.link(net=net)
|
||||
return linked.sign(key=BP_PRIVATE_KEY)
|
||||
except Exception as exc: # node mati/link gagal → coba node cadangan
|
||||
last_err = exc
|
||||
raise RuntimeError(
|
||||
f'Gagal membangun transfer {to} di semua node RPC {API_NODES}: '
|
||||
f'{last_err}')
|
||||
|
||||
|
||||
def run_distribution(dry_run=False):
|
||||
@@ -134,7 +189,7 @@ def run_distribution(dry_run=False):
|
||||
if not voters:
|
||||
print('Tidak ada pemilih matang & segar tersimpan — no-op.', flush=True)
|
||||
return 0
|
||||
balance = fetch_balance()
|
||||
balance = fetch_balance_with_retry(dry_run=dry_run)
|
||||
if balance is None or balance < VEX_PREC:
|
||||
print('Saldo liquid kosong / di bawah 0.0001 VEX — no-op.', flush=True)
|
||||
return 0
|
||||
@@ -183,16 +238,24 @@ def run_distribution(dry_run=False):
|
||||
try:
|
||||
signed = build_signed_transfer(owner, share, memo)
|
||||
txid = signed.id() # deterministik, sebelum broadcast
|
||||
signed.send()
|
||||
resp = signed.send()
|
||||
# V59: pyntelope ⊥ raise pada HTTP 500 — penolakan chain datang
|
||||
# sbg body JSON (pola V46/B11). Deteksi → perlakukan sbg gagal,
|
||||
# ⊖ tandai 'sent' untuk tx yang ⊥ pernah mendarat.
|
||||
if _send_rejected(resp):
|
||||
raise RuntimeError(
|
||||
f'chain menolak transfer: '
|
||||
f"{resp.get('error', {}).get('what') or resp}")
|
||||
db.update_payment_status(pid, 'sent', txid=txid)
|
||||
sent_amount += float(share)
|
||||
print(f'[OK] {owner}: {share} VEX ({txid[:16]}...)',
|
||||
flush=True)
|
||||
break
|
||||
except Exception as exc:
|
||||
# Timeout setelah broadcast → tx mungkin mendarat. Verifikasi
|
||||
# sebelum resend agar ⊥ ganda (V22). Bila Hyperion tak bisa
|
||||
# dipastikan → tahan (tandai failed, jangan resend).
|
||||
# Timeout/penolakan setelah broadcast → tx mungkin mendarat.
|
||||
# Verifikasi sebelum resend agar ⊥ ganda (V22). Bila Hyperion
|
||||
# tak bisa dipastikan → TAHAN (tandai failed, jangan resend) —
|
||||
# V59: hold sungguhan (⊥ break hanya di attempt terakhir).
|
||||
if txid is not None:
|
||||
landed = verify_txid(txid)
|
||||
if landed is True:
|
||||
@@ -205,6 +268,12 @@ def run_distribution(dry_run=False):
|
||||
print(f'[Peringatan] Verifikasi txid tak tersedia '
|
||||
f'untuk {owner} — ditahan (⊥ resend)',
|
||||
flush=True)
|
||||
db.update_payment_status(pid, 'failed', txid=txid,
|
||||
error=str(exc))
|
||||
failed.append((owner, exc))
|
||||
print(f'[DITAHAN] {owner}: {share} VEX — {exc}',
|
||||
flush=True)
|
||||
break
|
||||
if attempt == DISTRIBUTE_MAX_ATTEMPTS:
|
||||
db.update_payment_status(pid, 'failed', txid=txid,
|
||||
error=str(exc))
|
||||
|
||||
Reference in new issue
Block a user