primecli 0.5.5__tar.gz → 0.5.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {primecli-0.5.5 → primecli-0.5.6}/PKG-INFO +2 -2
- {primecli-0.5.5 → primecli-0.5.6}/README.md +1 -1
- {primecli-0.5.5 → primecli-0.5.6}/primecli/arbprime.py +104 -38
- {primecli-0.5.5 → primecli-0.5.6}/primecli/degenprime.py +87 -24
- {primecli-0.5.5 → primecli-0.5.6}/primecli/deltaprime.py +83 -26
- {primecli-0.5.5 → primecli-0.5.6}/primecli/health_monitor.py +1 -1
- {primecli-0.5.5 → primecli-0.5.6}/primecli.egg-info/PKG-INFO +2 -2
- {primecli-0.5.5 → primecli-0.5.6}/pyproject.toml +1 -1
- {primecli-0.5.5 → primecli-0.5.6}/tests/test_gas_pricing.py +46 -15
- {primecli-0.5.5 → primecli-0.5.6}/tests/test_health_monitor.py +8 -8
- {primecli-0.5.5 → primecli-0.5.6}/LICENSE +0 -0
- {primecli-0.5.5 → primecli-0.5.6}/primecli/__init__.py +0 -0
- {primecli-0.5.5 → primecli-0.5.6}/primecli.egg-info/SOURCES.txt +0 -0
- {primecli-0.5.5 → primecli-0.5.6}/primecli.egg-info/dependency_links.txt +0 -0
- {primecli-0.5.5 → primecli-0.5.6}/primecli.egg-info/entry_points.txt +0 -0
- {primecli-0.5.5 → primecli-0.5.6}/primecli.egg-info/requires.txt +0 -0
- {primecli-0.5.5 → primecli-0.5.6}/primecli.egg-info/top_level.txt +0 -0
- {primecli-0.5.5 → primecli-0.5.6}/setup.cfg +0 -0
- {primecli-0.5.5 → primecli-0.5.6}/tests/test_cross_file_identity.py +0 -0
- {primecli-0.5.5 → primecli-0.5.6}/tests/test_paraswap_validator.py +0 -0
- {primecli-0.5.5 → primecli-0.5.6}/tests/test_redstone_encoding.py +0 -0
- {primecli-0.5.5 → primecli-0.5.6}/tests/test_to_wei_units.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: primecli
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.6
|
|
4
4
|
Summary: Agent-friendly CLI tools for the DeltaPrime (Avalanche + Arbitrum) and DegenPrime (Base) lending and leverage protocols. Preview-by-default; no Etherscan key required.
|
|
5
5
|
Author: Mnemosyne-quest contributors
|
|
6
6
|
License: MIT
|
|
@@ -47,7 +47,7 @@ Built for agent use:
|
|
|
47
47
|
- RedStone-signed solvency math handled internally, with a regression test pinning the half-boundary `toFixed(8)` encoding.
|
|
48
48
|
- ParaSwap calldata validated client-side against the on-chain executor allowlist before broadcast.
|
|
49
49
|
|
|
50
|
-
**Current version:** 0.5.
|
|
50
|
+
**Current version:** 0.5.6 The 0.x line is pre-1.0, so breaking changes are possible. See [Releases](https://github.com/Mnemosyne-quest/primecli/releases).
|
|
51
51
|
|
|
52
52
|
> **Breaking change in 0.5.0:** there is no longer a default signing key. Earlier versions silently fell back to a baked-in agent when no key was configured; that fallback has been removed. With no key configured, every command now fails closed with `No signing key found...`. Set a key explicitly (see [Configuration](#configuration)).
|
|
53
53
|
|
|
@@ -16,7 +16,7 @@ Built for agent use:
|
|
|
16
16
|
- RedStone-signed solvency math handled internally, with a regression test pinning the half-boundary `toFixed(8)` encoding.
|
|
17
17
|
- ParaSwap calldata validated client-side against the on-chain executor allowlist before broadcast.
|
|
18
18
|
|
|
19
|
-
**Current version:** 0.5.
|
|
19
|
+
**Current version:** 0.5.6 The 0.x line is pre-1.0, so breaking changes are possible. See [Releases](https://github.com/Mnemosyne-quest/primecli/releases).
|
|
20
20
|
|
|
21
21
|
> **Breaking change in 0.5.0:** there is no longer a default signing key. Earlier versions silently fell back to a baked-in agent when no key was configured; that fallback has been removed. With no key configured, every command now fails closed with `No signing key found...`. Set a key explicitly (see [Configuration](#configuration)).
|
|
22
22
|
|
|
@@ -294,16 +294,16 @@ PARASWAP_AUGUSTUS = "0x6A000F20005980200259B80c5102003040001068"
|
|
|
294
294
|
PARASWAP_SUPPORTED_SELECTORS = {"0xe3ead59e", "0x876a02f6"}
|
|
295
295
|
# Executors the facet whitelists (ParaSwapHelper._checkExecutorAddress). Lowercased.
|
|
296
296
|
PARASWAP_EXECUTORS = {
|
|
297
|
-
#
|
|
298
|
-
#
|
|
299
|
-
#
|
|
300
|
-
#
|
|
297
|
+
# Historical static whitelist. Since the protocol-level facet fix (confirmed
|
|
298
|
+
# 2026-06-04), API-returned executors outside this set can be VALID — Velora
|
|
299
|
+
# rotates executors per quote (seen: 0x8faa…e820, 0x6f05…0900). The swap paths
|
|
300
|
+
# now decide by eth_call simulation of the exact tx, not by this set; it's kept
|
|
301
|
+
# only to label "known" vs "new" executors in output.
|
|
301
302
|
"0xdef171fe48cf0115b1d80b88dc8eab59176fee57",
|
|
302
303
|
"0x6a000f20005980200259b80c5102003040001068",
|
|
303
304
|
"0x000010036c0190e009a000d0fc3541100a07380a",
|
|
304
305
|
"0x00c600b30fb0400701010f4b080409018b9006e0",
|
|
305
306
|
"0xa0f408a000017007015e0f00320e470d00090a5b",
|
|
306
|
-
# 0x8faa... (Velora) returned by API but NOT whitelisted on-chain — confirmed 2026-05-28 & 2026-05-29
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
# RedStone on-demand oracle config for DeltaPrime on Arbitrum — IDENTICAL to Avalanche
|
|
@@ -671,33 +671,42 @@ def _tx_gas_price(w3) -> int:
|
|
|
671
671
|
|
|
672
672
|
def _set_gas_price(w3, tx_dict):
|
|
673
673
|
"""Set appropriate gas price fields for the chain, replacing the legacy gasPrice approach.
|
|
674
|
-
On EIP-1559 chains (Arbitrum, Base): sets maxFeePerGas +
|
|
675
|
-
base-fee hedge (base + prio + 1 gwei buffer).
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
674
|
+
On EIP-1559 chains (Arbitrum, Base, Avalanche post-Etna): sets maxFeePerGas +
|
|
675
|
+
maxPriorityFeePerGas with a 2x base-fee hedge (base + prio + 1 gwei buffer).
|
|
676
|
+
Falls back to legacy gasPrice only if the tx dict already lacks EIP-1559 fields
|
|
677
|
+
and the chain doesn't support max_priority_fee.
|
|
678
|
+
(25 gwei was the pre-Etna C-chain minimum; ACP-125 (Dec 2024) lowered the min base
|
|
679
|
+
fee to 1 nAVAX — base now sits at ~0.01 nAVAX, so a 25 gwei floor overpaid ~2500x
|
|
680
|
+
and inflated the upfront balance requirement past small EOAs.)"""
|
|
681
|
+
# If build_transaction already set EIP-1559 fields, don't touch them
|
|
682
|
+
if "maxFeePerGas" in tx_dict or "maxPriorityFeePerGas" in tx_dict:
|
|
683
|
+
tx_dict.pop("gasPrice", None)
|
|
684
|
+
return
|
|
680
685
|
tx_dict.pop("gasPrice", None)
|
|
681
|
-
|
|
686
|
+
try:
|
|
682
687
|
base = w3.eth.gas_price
|
|
683
688
|
prio = w3.eth.max_priority_fee
|
|
684
689
|
tx_dict["maxFeePerGas"] = max(int(base * 2), base + prio + 10**9)
|
|
685
690
|
tx_dict["maxPriorityFeePerGas"] = prio
|
|
686
|
-
|
|
691
|
+
except Exception:
|
|
692
|
+
# Legacy chain — use gasPrice instead
|
|
687
693
|
tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2), 1 * 10**9)
|
|
688
694
|
|
|
689
695
|
def _set_gas_price_for(chain_id, w3, tx_dict):
|
|
690
696
|
"""Set gas fields for an EXPLICIT chain_id rather than the module CHAIN_ID. Needed by
|
|
691
697
|
cross-chain flows (prime-bridge) where a tx may target Avalanche or Arbitrum regardless
|
|
692
|
-
of which tool built it.
|
|
693
|
-
|
|
698
|
+
of which tool built it."""
|
|
699
|
+
# If build_transaction already set EIP-1559 fields, don't touch them
|
|
700
|
+
if "maxFeePerGas" in tx_dict or "maxPriorityFeePerGas" in tx_dict:
|
|
701
|
+
tx_dict.pop("gasPrice", None)
|
|
702
|
+
return
|
|
694
703
|
tx_dict.pop("gasPrice", None)
|
|
695
|
-
|
|
704
|
+
try:
|
|
696
705
|
base = w3.eth.gas_price
|
|
697
706
|
prio = w3.eth.max_priority_fee
|
|
698
707
|
tx_dict["maxFeePerGas"] = max(int(base * 2), base + prio + 10**9)
|
|
699
708
|
tx_dict["maxPriorityFeePerGas"] = prio
|
|
700
|
-
|
|
709
|
+
except Exception:
|
|
701
710
|
tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2), 1 * 10**9)
|
|
702
711
|
|
|
703
712
|
def _read_env_var(path, var):
|
|
@@ -2326,14 +2335,40 @@ def _swap_via_paraswap(w3, acct, pa_cs, account, from_sym, to_sym, from_cfg, to_
|
|
|
2326
2335
|
selector_hex, data_bytes = "0x" + full[:4].hex(), full[4:]
|
|
2327
2336
|
_exec, _src, _dest, from_amt, min_out = _paraswap_decode_and_check(
|
|
2328
2337
|
selector_hex, data_bytes, from_cfg["token"], to_cfg["token"], amount_in, pa_cs)
|
|
2329
|
-
# Same
|
|
2338
|
+
# Same simulate-first executor handling as swap-debt (see cmd_swap_debt for full
|
|
2339
|
+
# rationale): keep the API executor when the exact tx simulates clean; only fall
|
|
2340
|
+
# back to the legacy executor if the unpatched calldata reverts.
|
|
2330
2341
|
_PARASWAP_FALLBACK_EXECUTOR = "0x000010036C0190E009a000d0fc3541100A07380A"
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2342
|
+
feeds = prime_account_price_feeds(account)
|
|
2343
|
+
for s in (from_sym, to_sym):
|
|
2344
|
+
if s not in feeds:
|
|
2345
|
+
feeds.append(s)
|
|
2346
|
+
payload = build_redstone_payload(feeds)
|
|
2347
|
+
def _sim_paraswap(db):
|
|
2348
|
+
base = account.encode_abi("paraSwapV6", args=[full[:4], db])
|
|
2349
|
+
try:
|
|
2350
|
+
w3.eth.call({"from": acct.address, "to": pa_cs,
|
|
2351
|
+
"data": base + payload.hex(), "gas": 8000000})
|
|
2352
|
+
return True, None
|
|
2353
|
+
except Exception as e:
|
|
2354
|
+
return False, str(e)
|
|
2355
|
+
sim_ok, sim_err = _sim_paraswap(data_bytes)
|
|
2356
|
+
if sim_ok:
|
|
2357
|
+
if _exec is not None and _exec.lower() not in PARASWAP_EXECUTORS:
|
|
2358
|
+
print(f" ✓ Executor {_exec} not in the static whitelist, but the full tx "
|
|
2359
|
+
f"simulates clean — using the API calldata as-is.")
|
|
2360
|
+
else:
|
|
2361
|
+
print(f" ✗ Simulation with API executor {_exec} reverted: {sim_err}")
|
|
2362
|
+
patched = bytes(12) + bytes.fromhex(_PARASWAP_FALLBACK_EXECUTOR[2:]) + data_bytes[32:]
|
|
2363
|
+
sim_ok, err2 = _sim_paraswap(patched)
|
|
2364
|
+
if sim_ok:
|
|
2365
|
+
print(f" ⚠ Falling back to legacy executor {_PARASWAP_FALLBACK_EXECUTOR} "
|
|
2366
|
+
f"(simulates clean).")
|
|
2367
|
+
data_bytes = patched
|
|
2368
|
+
_paraswap_decode_and_check(selector_hex, data_bytes, from_cfg["token"],
|
|
2369
|
+
to_cfg["token"], amount_in, pa_cs)
|
|
2370
|
+
else:
|
|
2371
|
+
print(f" ✗ Legacy-executor fallback also reverted: {err2}")
|
|
2337
2372
|
|
|
2338
2373
|
print(f"Swap {amount} {from_sym} -> {to_sym} on Prime Account {pa_cs} (via ParaSwap/Velora)")
|
|
2339
2374
|
print(f" Router method: {price_route['contractMethod']} ({selector_hex})")
|
|
@@ -2348,10 +2383,12 @@ def _swap_via_paraswap(w3, acct, pa_cs, account, from_sym, to_sym, from_cfg, to_
|
|
|
2348
2383
|
print("Run with --execute to broadcast (appends a fresh RedStone price payload).")
|
|
2349
2384
|
return
|
|
2350
2385
|
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2386
|
+
if not sim_ok:
|
|
2387
|
+
print("✗ Refusing to broadcast: simulation reverted for both executor variants.")
|
|
2388
|
+
return
|
|
2389
|
+
|
|
2390
|
+
# Rebuild the payload fresh for broadcast (the sim payload may be near the
|
|
2391
|
+
# RedStone staleness window by now).
|
|
2355
2392
|
payload = build_redstone_payload(feeds)
|
|
2356
2393
|
base_calldata = account.encode_abi("paraSwapV6", args=[full[:4], data_bytes])
|
|
2357
2394
|
data = base_calldata + payload.hex()
|
|
@@ -2544,7 +2581,8 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
|
|
|
2544
2581
|
|
|
2545
2582
|
Default (one-tx): SwapDebtFacet.swapDebtParaSwap — borrows _borrowAmount of _toAsset,
|
|
2546
2583
|
ParaSwaps it into _fromAsset, and repays _repayAmount of _fromAsset debt in a single tx.
|
|
2547
|
-
|
|
2584
|
+
(Was broken on-chain 2026-05-30 by a protocol-level Velora/ParaSwap facet bug; the
|
|
2585
|
+
DeltaPrime team fixed it — re-verified working via eth_call + live tx 2026-06-04.)
|
|
2548
2586
|
|
|
2549
2587
|
--fallback (manual 3-tx via YieldYak):
|
|
2550
2588
|
1. borrow to_sym — borrow the new debt asset into the account
|
|
@@ -2766,16 +2804,40 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
|
|
|
2766
2804
|
_exec, _src, _dest, swap_from_amt, swap_min_out = _paraswap_decode_and_check(
|
|
2767
2805
|
selector_hex, data_bytes, to_cfg["token"], from_cfg["token"], borrow_amount, pa_cs)
|
|
2768
2806
|
|
|
2769
|
-
#
|
|
2770
|
-
#
|
|
2771
|
-
#
|
|
2807
|
+
# Velora/ParaSwap executors rotate per quote and the facet's on-chain executor
|
|
2808
|
+
# check was fixed at the protocol level (DeltaPrime team, confirmed by eth_call
|
|
2809
|
+
# 2026-06-04) — API-built calldata now passes with its own executor, while the old
|
|
2810
|
+
# hard-patch to the legacy executor REVERTS (executor-specific calldata mismatch).
|
|
2811
|
+
# So: simulate the exact tx first and keep the API executor when it passes; only
|
|
2812
|
+
# fall back to the legacy executor if the unpatched calldata reverts.
|
|
2772
2813
|
_PARASWAP_FALLBACK_EXECUTOR = "0x000010036C0190E009a000d0fc3541100A07380A"
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2814
|
+
def _sim_swap_debt(db):
|
|
2815
|
+
base = account.encode_abi("swapDebtParaSwap", args=[
|
|
2816
|
+
asset_b32(from_sym), asset_b32(to_sym), repay_amount, borrow_amount,
|
|
2817
|
+
full_data[:4], db])
|
|
2818
|
+
try:
|
|
2819
|
+
w3.eth.call({"from": acct.address, "to": pa_cs,
|
|
2820
|
+
"data": base + payload.hex(), "gas": 8000000})
|
|
2821
|
+
return True, None
|
|
2822
|
+
except Exception as e:
|
|
2823
|
+
return False, str(e)
|
|
2824
|
+
sim_ok, sim_err = _sim_swap_debt(data_bytes)
|
|
2825
|
+
if sim_ok:
|
|
2826
|
+
if _exec is not None and _exec.lower() not in PARASWAP_EXECUTORS:
|
|
2827
|
+
print(f" ✓ Executor {_exec} not in the static whitelist, but the full tx "
|
|
2828
|
+
f"simulates clean — using the API calldata as-is.")
|
|
2829
|
+
else:
|
|
2830
|
+
print(f" ✗ Simulation with API executor {_exec} reverted: {sim_err}")
|
|
2831
|
+
patched = bytes(12) + bytes.fromhex(_PARASWAP_FALLBACK_EXECUTOR[2:]) + data_bytes[32:]
|
|
2832
|
+
sim_ok, err2 = _sim_swap_debt(patched)
|
|
2833
|
+
if sim_ok:
|
|
2834
|
+
print(f" ⚠ Falling back to legacy executor {_PARASWAP_FALLBACK_EXECUTOR} "
|
|
2835
|
+
f"(simulates clean).")
|
|
2836
|
+
data_bytes = patched
|
|
2837
|
+
_paraswap_decode_and_check(selector_hex, data_bytes, to_cfg["token"],
|
|
2838
|
+
from_cfg["token"], borrow_amount, pa_cs)
|
|
2839
|
+
else:
|
|
2840
|
+
print(f" ✗ Legacy-executor fallback also reverted: {err2}")
|
|
2779
2841
|
|
|
2780
2842
|
from_pool, _, _ = get_pool_contract(_SYMBOL_TO_POOL[from_sym])
|
|
2781
2843
|
borrowed = from_pool.functions.getBorrowed(pa_cs).call()
|
|
@@ -2806,6 +2868,10 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
|
|
|
2806
2868
|
print("Run with --execute to broadcast (appends a fresh RedStone price payload).")
|
|
2807
2869
|
return
|
|
2808
2870
|
|
|
2871
|
+
if not sim_ok:
|
|
2872
|
+
print("✗ Refusing to broadcast: simulation reverted for both executor variants.")
|
|
2873
|
+
return
|
|
2874
|
+
|
|
2809
2875
|
base_calldata = account.encode_abi("swapDebtParaSwap", args=[
|
|
2810
2876
|
asset_b32(from_sym), asset_b32(to_sym), repay_amount, borrow_amount,
|
|
2811
2877
|
full_data[:4], data_bytes,
|
|
@@ -242,19 +242,25 @@ def _tx_gas_price(w3) -> int:
|
|
|
242
242
|
|
|
243
243
|
def _set_gas_price(w3, tx_dict):
|
|
244
244
|
"""Set appropriate gas price fields for the chain, replacing the legacy gasPrice approach.
|
|
245
|
-
On EIP-1559 chains (Arbitrum, Base): sets maxFeePerGas +
|
|
246
|
-
base-fee hedge (base + prio + 1 gwei buffer).
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
245
|
+
On EIP-1559 chains (Arbitrum, Base, Avalanche post-Etna): sets maxFeePerGas +
|
|
246
|
+
maxPriorityFeePerGas with a 2x base-fee hedge (base + prio + 1 gwei buffer).
|
|
247
|
+
Falls back to legacy gasPrice only if the tx dict already lacks EIP-1559 fields
|
|
248
|
+
and the chain doesn't support max_priority_fee.
|
|
249
|
+
(25 gwei was the pre-Etna C-chain minimum; ACP-125 (Dec 2024) lowered the min base
|
|
250
|
+
fee to 1 nAVAX — base now sits at ~0.01 nAVAX, so a 25 gwei floor overpaid ~2500x
|
|
251
|
+
and inflated the upfront balance requirement past small EOAs.)"""
|
|
252
|
+
# If build_transaction already set EIP-1559 fields, don't touch them
|
|
253
|
+
if "maxFeePerGas" in tx_dict or "maxPriorityFeePerGas" in tx_dict:
|
|
254
|
+
tx_dict.pop("gasPrice", None)
|
|
255
|
+
return
|
|
251
256
|
tx_dict.pop("gasPrice", None)
|
|
252
|
-
|
|
257
|
+
try:
|
|
253
258
|
base = w3.eth.gas_price
|
|
254
259
|
prio = w3.eth.max_priority_fee
|
|
255
260
|
tx_dict["maxFeePerGas"] = max(int(base * 2), base + prio + 10**9)
|
|
256
261
|
tx_dict["maxPriorityFeePerGas"] = prio
|
|
257
|
-
|
|
262
|
+
except Exception:
|
|
263
|
+
# Legacy chain — use gasPrice instead
|
|
258
264
|
tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2), 1 * 10**9)
|
|
259
265
|
def resolve_private_key():
|
|
260
266
|
"""Resolve the signing key per the documented precedence:
|
|
@@ -1983,12 +1989,39 @@ def cmd_swap(from_sym: str, to_sym: str, amount: float, slippage_pct: float = 1.
|
|
|
1983
1989
|
selector_hex, data_bytes = "0x" + full[:4].hex(), full[4:]
|
|
1984
1990
|
_exec, _src, _dest, _from_amt, min_out = _paraswap_decode_and_check(
|
|
1985
1991
|
selector_hex, data_bytes, from_cfg["token"], to_cfg["token"], amount_in, pa_cs)
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
+
# Simulate-first executor handling (see cmd_swap_debt rationale): keep the API
|
|
1993
|
+
# executor when the exact tx simulates clean; only fall back to the legacy
|
|
1994
|
+
# executor if the unpatched calldata reverts.
|
|
1995
|
+
feeds = degen_account_price_feeds(account)
|
|
1996
|
+
for s in (from_sym, to_sym):
|
|
1997
|
+
if s in REDSTONE_AVAILABLE_FEEDS and s not in feeds:
|
|
1998
|
+
feeds.append(s)
|
|
1999
|
+
payload = build_redstone_payload(feeds)
|
|
2000
|
+
def _sim_paraswap(db):
|
|
2001
|
+
base = account.encode_abi("paraSwapV6", args=[full[:4], db])
|
|
2002
|
+
try:
|
|
2003
|
+
w3.eth.call({"from": acct.address, "to": pa_cs,
|
|
2004
|
+
"data": base + payload.hex(), "gas": 8000000})
|
|
2005
|
+
return True, None
|
|
2006
|
+
except Exception as e:
|
|
2007
|
+
return False, str(e)
|
|
2008
|
+
sim_ok, sim_err = _sim_paraswap(data_bytes)
|
|
2009
|
+
if sim_ok:
|
|
2010
|
+
if _exec is not None and _exec.lower() not in PARASWAP_EXECUTORS:
|
|
2011
|
+
print(f" ✓ Executor {_exec} not in the static whitelist, but the full tx "
|
|
2012
|
+
f"simulates clean — using the API calldata as-is.")
|
|
2013
|
+
else:
|
|
2014
|
+
print(f" ✗ Simulation with API executor {_exec} reverted: {sim_err}")
|
|
2015
|
+
patched = bytes(12) + bytes.fromhex(_PARASWAP_FALLBACK_EXECUTOR[2:]) + data_bytes[32:]
|
|
2016
|
+
sim_ok, err2 = _sim_paraswap(patched)
|
|
2017
|
+
if sim_ok:
|
|
2018
|
+
print(f" ⚠ Falling back to legacy executor {_PARASWAP_FALLBACK_EXECUTOR} "
|
|
2019
|
+
f"(simulates clean).")
|
|
2020
|
+
data_bytes = patched
|
|
2021
|
+
_paraswap_decode_and_check(selector_hex, data_bytes, from_cfg["token"],
|
|
2022
|
+
to_cfg["token"], amount_in, pa_cs)
|
|
2023
|
+
else:
|
|
2024
|
+
print(f" ✗ Legacy-executor fallback also reverted: {err2}")
|
|
1992
2025
|
|
|
1993
2026
|
print(f"Swap {amount} {from_sym} -> {to_sym} on Degen Account {pa_cs} (via ParaSwap/Velora)")
|
|
1994
2027
|
print(f" Router method: {price_route['contractMethod']} ({selector_hex})")
|
|
@@ -2003,10 +2036,12 @@ def cmd_swap(from_sym: str, to_sym: str, amount: float, slippage_pct: float = 1.
|
|
|
2003
2036
|
print("Run with --execute to broadcast (appends a fresh RedStone price payload).")
|
|
2004
2037
|
return
|
|
2005
2038
|
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2039
|
+
if not sim_ok:
|
|
2040
|
+
print("✗ Refusing to broadcast: simulation reverted for both executor variants.")
|
|
2041
|
+
return
|
|
2042
|
+
|
|
2043
|
+
# Rebuild the payload fresh for broadcast (the sim payload may be near the
|
|
2044
|
+
# RedStone staleness window by now).
|
|
2010
2045
|
payload = build_redstone_payload(feeds)
|
|
2011
2046
|
base_calldata = account.encode_abi("paraSwapV6", args=[full[:4], data_bytes])
|
|
2012
2047
|
data = base_calldata + payload.hex()
|
|
@@ -2112,12 +2147,36 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
|
|
|
2112
2147
|
selector_hex, data_bytes = "0x" + full[:4].hex(), full[4:]
|
|
2113
2148
|
_exec, _src, _dest, _swap_from_amt, swap_min_out = _paraswap_decode_and_check(
|
|
2114
2149
|
selector_hex, data_bytes, to_cfg["token"], from_cfg["token"], borrow_amount, pa_cs)
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2150
|
+
# Simulate-first executor handling (protocol-level facet fix confirmed 2026-06-04;
|
|
2151
|
+
# Velora rotates executors per quote): keep the API executor when the exact tx
|
|
2152
|
+
# simulates clean; only fall back to the legacy executor if it reverts.
|
|
2153
|
+
def _sim_swap_debt(db):
|
|
2154
|
+
base = account.encode_abi("swapDebtParaSwap", args=[
|
|
2155
|
+
asset_b32(from_sym), asset_b32(to_sym), repay_amount, borrow_amount,
|
|
2156
|
+
full[:4], db])
|
|
2157
|
+
try:
|
|
2158
|
+
w3.eth.call({"from": acct.address, "to": pa_cs,
|
|
2159
|
+
"data": base + payload.hex(), "gas": 8000000})
|
|
2160
|
+
return True, None
|
|
2161
|
+
except Exception as e:
|
|
2162
|
+
return False, str(e)
|
|
2163
|
+
sim_ok, sim_err = _sim_swap_debt(data_bytes)
|
|
2164
|
+
if sim_ok:
|
|
2165
|
+
if _exec is not None and _exec.lower() not in PARASWAP_EXECUTORS:
|
|
2166
|
+
print(f" ✓ Executor {_exec} not in the static whitelist, but the full tx "
|
|
2167
|
+
f"simulates clean — using the API calldata as-is.")
|
|
2168
|
+
else:
|
|
2169
|
+
print(f" ✗ Simulation with API executor {_exec} reverted: {sim_err}")
|
|
2170
|
+
patched = bytes(12) + bytes.fromhex(_PARASWAP_FALLBACK_EXECUTOR[2:]) + data_bytes[32:]
|
|
2171
|
+
sim_ok, err2 = _sim_swap_debt(patched)
|
|
2172
|
+
if sim_ok:
|
|
2173
|
+
print(f" ⚠ Falling back to legacy executor {_PARASWAP_FALLBACK_EXECUTOR} "
|
|
2174
|
+
f"(simulates clean).")
|
|
2175
|
+
data_bytes = patched
|
|
2176
|
+
_paraswap_decode_and_check(selector_hex, data_bytes, to_cfg["token"],
|
|
2177
|
+
from_cfg["token"], borrow_amount, pa_cs)
|
|
2178
|
+
else:
|
|
2179
|
+
print(f" ✗ Legacy-executor fallback also reverted: {err2}")
|
|
2121
2180
|
|
|
2122
2181
|
print(f"Swap debt on Degen Account {pa}")
|
|
2123
2182
|
print(f" Refinance: {from_sym} debt -> {to_sym} debt")
|
|
@@ -2145,6 +2204,10 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
|
|
|
2145
2204
|
print("Run with --execute to broadcast (appends a fresh RedStone price payload).")
|
|
2146
2205
|
return
|
|
2147
2206
|
|
|
2207
|
+
if not sim_ok:
|
|
2208
|
+
print("✗ Refusing to broadcast: simulation reverted for both executor variants.")
|
|
2209
|
+
return
|
|
2210
|
+
|
|
2148
2211
|
base_calldata = account.encode_abi("swapDebtParaSwap", args=[
|
|
2149
2212
|
asset_b32(from_sym), asset_b32(to_sym), repay_amount, borrow_amount,
|
|
2150
2213
|
full[:4], data_bytes,
|
|
@@ -299,16 +299,16 @@ PARASWAP_AUGUSTUS = "0x6A000F20005980200259B80c5102003040001068"
|
|
|
299
299
|
PARASWAP_SUPPORTED_SELECTORS = {"0xe3ead59e", "0x876a02f6"}
|
|
300
300
|
# Executors the facet whitelists (ParaSwapHelper._checkExecutorAddress). Lowercased.
|
|
301
301
|
PARASWAP_EXECUTORS = {
|
|
302
|
-
#
|
|
303
|
-
#
|
|
304
|
-
#
|
|
305
|
-
#
|
|
302
|
+
# Historical static whitelist. Since the protocol-level facet fix (confirmed
|
|
303
|
+
# 2026-06-04), API-returned executors outside this set can be VALID — Velora
|
|
304
|
+
# rotates executors per quote (seen: 0x8faa…e820, 0x6f05…0900). The swap paths
|
|
305
|
+
# now decide by eth_call simulation of the exact tx, not by this set; it's kept
|
|
306
|
+
# only to label "known" vs "new" executors in output.
|
|
306
307
|
"0xdef171fe48cf0115b1d80b88dc8eab59176fee57",
|
|
307
308
|
"0x6a000f20005980200259b80c5102003040001068",
|
|
308
309
|
"0x000010036c0190e009a000d0fc3541100a07380a",
|
|
309
310
|
"0x00c600b30fb0400701010f4b080409018b9006e0",
|
|
310
311
|
"0xa0f408a000017007015e0f00320e470d00090a5b",
|
|
311
|
-
# 0x8faa... (Velora) returned by API but NOT whitelisted on-chain — confirmed 2026-05-28 & 2026-05-29
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
# RedStone on-demand oracle config for DeltaPrime on Avalanche. The Prime Account's
|
|
@@ -2352,14 +2352,40 @@ def _swap_via_paraswap(w3, acct, pa_cs, account, from_sym, to_sym, from_cfg, to_
|
|
|
2352
2352
|
selector_hex, data_bytes = "0x" + full[:4].hex(), full[4:]
|
|
2353
2353
|
_exec, _src, _dest, from_amt, min_out = _paraswap_decode_and_check(
|
|
2354
2354
|
selector_hex, data_bytes, from_cfg["token"], to_cfg["token"], amount_in, pa_cs)
|
|
2355
|
-
# Same
|
|
2355
|
+
# Same simulate-first executor handling as swap-debt (see cmd_swap_debt for full
|
|
2356
|
+
# rationale): keep the API executor when the exact tx simulates clean; only fall
|
|
2357
|
+
# back to the legacy executor if the unpatched calldata reverts.
|
|
2356
2358
|
_PARASWAP_FALLBACK_EXECUTOR = "0x000010036C0190E009a000d0fc3541100A07380A"
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2359
|
+
feeds = prime_account_price_feeds(account)
|
|
2360
|
+
for s in (from_sym, to_sym):
|
|
2361
|
+
if s not in feeds:
|
|
2362
|
+
feeds.append(s)
|
|
2363
|
+
payload = build_redstone_payload(feeds)
|
|
2364
|
+
def _sim_paraswap(db):
|
|
2365
|
+
base = account.encode_abi("paraSwapV6", args=[full[:4], db])
|
|
2366
|
+
try:
|
|
2367
|
+
w3.eth.call({"from": acct.address, "to": pa_cs,
|
|
2368
|
+
"data": base + payload.hex(), "gas": 8000000})
|
|
2369
|
+
return True, None
|
|
2370
|
+
except Exception as e:
|
|
2371
|
+
return False, str(e)
|
|
2372
|
+
sim_ok, sim_err = _sim_paraswap(data_bytes)
|
|
2373
|
+
if sim_ok:
|
|
2374
|
+
if _exec is not None and _exec.lower() not in PARASWAP_EXECUTORS:
|
|
2375
|
+
print(f" ✓ Executor {_exec} not in the static whitelist, but the full tx "
|
|
2376
|
+
f"simulates clean — using the API calldata as-is.")
|
|
2377
|
+
else:
|
|
2378
|
+
print(f" ✗ Simulation with API executor {_exec} reverted: {sim_err}")
|
|
2379
|
+
patched = bytes(12) + bytes.fromhex(_PARASWAP_FALLBACK_EXECUTOR[2:]) + data_bytes[32:]
|
|
2380
|
+
sim_ok, err2 = _sim_paraswap(patched)
|
|
2381
|
+
if sim_ok:
|
|
2382
|
+
print(f" ⚠ Falling back to legacy executor {_PARASWAP_FALLBACK_EXECUTOR} "
|
|
2383
|
+
f"(simulates clean).")
|
|
2384
|
+
data_bytes = patched
|
|
2385
|
+
_paraswap_decode_and_check(selector_hex, data_bytes, from_cfg["token"],
|
|
2386
|
+
to_cfg["token"], amount_in, pa_cs)
|
|
2387
|
+
else:
|
|
2388
|
+
print(f" ✗ Legacy-executor fallback also reverted: {err2}")
|
|
2363
2389
|
|
|
2364
2390
|
print(f"Swap {amount} {from_sym} -> {to_sym} on Prime Account {pa_cs} (via ParaSwap/Velora)")
|
|
2365
2391
|
print(f" Router method: {price_route['contractMethod']} ({selector_hex})")
|
|
@@ -2374,10 +2400,12 @@ def _swap_via_paraswap(w3, acct, pa_cs, account, from_sym, to_sym, from_cfg, to_
|
|
|
2374
2400
|
print("Run with --execute to broadcast (appends a fresh RedStone price payload).")
|
|
2375
2401
|
return
|
|
2376
2402
|
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2403
|
+
if not sim_ok:
|
|
2404
|
+
print("✗ Refusing to broadcast: simulation reverted for both executor variants.")
|
|
2405
|
+
return
|
|
2406
|
+
|
|
2407
|
+
# Rebuild the payload fresh for broadcast (the sim payload may be near the
|
|
2408
|
+
# RedStone staleness window by now).
|
|
2381
2409
|
payload = build_redstone_payload(feeds)
|
|
2382
2410
|
base_calldata = account.encode_abi("paraSwapV6", args=[full[:4], data_bytes])
|
|
2383
2411
|
data = base_calldata + payload.hex()
|
|
@@ -2570,7 +2598,8 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
|
|
|
2570
2598
|
|
|
2571
2599
|
Default (one-tx): SwapDebtFacet.swapDebtParaSwap — borrows _borrowAmount of _toAsset,
|
|
2572
2600
|
ParaSwaps it into _fromAsset, and repays _repayAmount of _fromAsset debt in a single tx.
|
|
2573
|
-
|
|
2601
|
+
(Was broken on-chain 2026-05-30 by a protocol-level Velora/ParaSwap facet bug; the
|
|
2602
|
+
DeltaPrime team fixed it — re-verified working via eth_call + live tx 2026-06-04.)
|
|
2574
2603
|
|
|
2575
2604
|
--fallback (manual 3-tx via YieldYak):
|
|
2576
2605
|
1. borrow to_sym — borrow the new debt asset into the account
|
|
@@ -2792,16 +2821,40 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
|
|
|
2792
2821
|
_exec, _src, _dest, swap_from_amt, swap_min_out = _paraswap_decode_and_check(
|
|
2793
2822
|
selector_hex, data_bytes, to_cfg["token"], from_cfg["token"], borrow_amount, pa_cs)
|
|
2794
2823
|
|
|
2795
|
-
#
|
|
2796
|
-
#
|
|
2797
|
-
#
|
|
2824
|
+
# Velora/ParaSwap executors rotate per quote and the facet's on-chain executor
|
|
2825
|
+
# check was fixed at the protocol level (DeltaPrime team, confirmed by eth_call
|
|
2826
|
+
# 2026-06-04) — API-built calldata now passes with its own executor, while the old
|
|
2827
|
+
# hard-patch to the legacy executor REVERTS (executor-specific calldata mismatch).
|
|
2828
|
+
# So: simulate the exact tx first and keep the API executor when it passes; only
|
|
2829
|
+
# fall back to the legacy executor if the unpatched calldata reverts.
|
|
2798
2830
|
_PARASWAP_FALLBACK_EXECUTOR = "0x000010036C0190E009a000d0fc3541100A07380A"
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2831
|
+
def _sim_swap_debt(db):
|
|
2832
|
+
base = account.encode_abi("swapDebtParaSwap", args=[
|
|
2833
|
+
asset_b32(from_sym), asset_b32(to_sym), repay_amount, borrow_amount,
|
|
2834
|
+
full_data[:4], db])
|
|
2835
|
+
try:
|
|
2836
|
+
w3.eth.call({"from": acct.address, "to": pa_cs,
|
|
2837
|
+
"data": base + payload.hex(), "gas": 8000000})
|
|
2838
|
+
return True, None
|
|
2839
|
+
except Exception as e:
|
|
2840
|
+
return False, str(e)
|
|
2841
|
+
sim_ok, sim_err = _sim_swap_debt(data_bytes)
|
|
2842
|
+
if sim_ok:
|
|
2843
|
+
if _exec is not None and _exec.lower() not in PARASWAP_EXECUTORS:
|
|
2844
|
+
print(f" ✓ Executor {_exec} not in the static whitelist, but the full tx "
|
|
2845
|
+
f"simulates clean — using the API calldata as-is.")
|
|
2846
|
+
else:
|
|
2847
|
+
print(f" ✗ Simulation with API executor {_exec} reverted: {sim_err}")
|
|
2848
|
+
patched = bytes(12) + bytes.fromhex(_PARASWAP_FALLBACK_EXECUTOR[2:]) + data_bytes[32:]
|
|
2849
|
+
sim_ok, err2 = _sim_swap_debt(patched)
|
|
2850
|
+
if sim_ok:
|
|
2851
|
+
print(f" ⚠ Falling back to legacy executor {_PARASWAP_FALLBACK_EXECUTOR} "
|
|
2852
|
+
f"(simulates clean).")
|
|
2853
|
+
data_bytes = patched
|
|
2854
|
+
_paraswap_decode_and_check(selector_hex, data_bytes, to_cfg["token"],
|
|
2855
|
+
from_cfg["token"], borrow_amount, pa_cs)
|
|
2856
|
+
else:
|
|
2857
|
+
print(f" ✗ Legacy-executor fallback also reverted: {err2}")
|
|
2805
2858
|
|
|
2806
2859
|
from_pool, _, _ = get_pool_contract(_SYMBOL_TO_POOL[from_sym])
|
|
2807
2860
|
borrowed = from_pool.functions.getBorrowed(pa_cs).call()
|
|
@@ -2832,6 +2885,10 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
|
|
|
2832
2885
|
print("Run with --execute to broadcast (appends a fresh RedStone price payload).")
|
|
2833
2886
|
return
|
|
2834
2887
|
|
|
2888
|
+
if not sim_ok:
|
|
2889
|
+
print("✗ Refusing to broadcast: simulation reverted for both executor variants.")
|
|
2890
|
+
return
|
|
2891
|
+
|
|
2835
2892
|
base_calldata = account.encode_abi("swapDebtParaSwap", args=[
|
|
2836
2893
|
asset_b32(from_sym), asset_b32(to_sym), repay_amount, borrow_amount,
|
|
2837
2894
|
full_data[:4], data_bytes,
|
|
@@ -62,7 +62,7 @@ def compute_health(defi_data: dict, max_mult: int = 10) -> dict:
|
|
|
62
62
|
borrowed = g.get("borrowed", [])
|
|
63
63
|
health_ratio = g.get("health_ratio", 0) or 0
|
|
64
64
|
# Use precomputed health_pct from defi --json if available (primecli >= 0.5.4)
|
|
65
|
-
precomputed = g.get("health_pct")
|
|
65
|
+
precomputed = g.get("health_pct")
|
|
66
66
|
if precomputed is not None:
|
|
67
67
|
# Early return: precomputed value exists, enrich with detail fields
|
|
68
68
|
supplied_usd = sum(s.get("usd", 0) or 0 for s in supplied)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: primecli
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.6
|
|
4
4
|
Summary: Agent-friendly CLI tools for the DeltaPrime (Avalanche + Arbitrum) and DegenPrime (Base) lending and leverage protocols. Preview-by-default; no Etherscan key required.
|
|
5
5
|
Author: Mnemosyne-quest contributors
|
|
6
6
|
License: MIT
|
|
@@ -47,7 +47,7 @@ Built for agent use:
|
|
|
47
47
|
- RedStone-signed solvency math handled internally, with a regression test pinning the half-boundary `toFixed(8)` encoding.
|
|
48
48
|
- ParaSwap calldata validated client-side against the on-chain executor allowlist before broadcast.
|
|
49
49
|
|
|
50
|
-
**Current version:** 0.5.
|
|
50
|
+
**Current version:** 0.5.6 The 0.x line is pre-1.0, so breaking changes are possible. See [Releases](https://github.com/Mnemosyne-quest/primecli/releases).
|
|
51
51
|
|
|
52
52
|
> **Breaking change in 0.5.0:** there is no longer a default signing key. Earlier versions silently fell back to a baked-in agent when no key was configured; that fallback has been removed. With no key configured, every command now fails closed with `No signing key found...`. Set a key explicitly (see [Configuration](#configuration)).
|
|
53
53
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "primecli"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.6"
|
|
8
8
|
description = "Agent-friendly CLI tools for the DeltaPrime (Avalanche + Arbitrum) and DegenPrime (Base) lending and leverage protocols. Preview-by-default; no Etherscan key required."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
"""Offline tests for `_set_gas_price_for(chain_id, w3, tx)` in deltaprime and arbprime.
|
|
2
2
|
|
|
3
|
-
This helper sets gas fields for
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
This helper sets gas fields for cross-chain flows (like prime-bridge). All three
|
|
4
|
+
supported chains (Arbitrum 42161, Base 8453, Avalanche 43114 post-Etna) speak
|
|
5
|
+
EIP-1559, so the helper now tries EIP-1559 first regardless of chain id:
|
|
6
|
+
* `eth.max_priority_fee` works → maxFeePerGas + maxPriorityFeePerGas,
|
|
6
7
|
and NO legacy gasPrice.
|
|
7
|
-
*
|
|
8
|
+
* `eth.max_priority_fee` raises (legacy-only chain/RPC) → legacy gasPrice with
|
|
9
|
+
a 1 gwei floor, and NO EIP-1559 fields.
|
|
10
|
+
* EIP-1559 fields already present on the tx → left untouched (stale gasPrice dropped).
|
|
8
11
|
|
|
9
12
|
No RPC is made: we feed a stub w3 whose `eth.gas_price` / `eth.max_priority_fee`
|
|
10
13
|
return canned values. The helper is duplicated in both modules, so both are tested.
|
|
@@ -24,7 +27,13 @@ MODULES = ["primecli.deltaprime", "primecli.arbprime"]
|
|
|
24
27
|
class _StubEth:
|
|
25
28
|
def __init__(self, gas_price, max_priority_fee):
|
|
26
29
|
self.gas_price = gas_price
|
|
27
|
-
self.
|
|
30
|
+
self._max_priority_fee = max_priority_fee
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def max_priority_fee(self):
|
|
34
|
+
if isinstance(self._max_priority_fee, Exception):
|
|
35
|
+
raise self._max_priority_fee
|
|
36
|
+
return self._max_priority_fee
|
|
28
37
|
|
|
29
38
|
|
|
30
39
|
class _StubW3:
|
|
@@ -82,24 +91,46 @@ def test_base_sets_eip1559_no_gasprice(mod):
|
|
|
82
91
|
|
|
83
92
|
|
|
84
93
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
85
|
-
# Avalanche (43114) —
|
|
94
|
+
# Avalanche (43114) — EIP-1559 post-Etna (ACP-125); legacy gasPrice only as fallback
|
|
86
95
|
|
|
87
96
|
|
|
88
|
-
def
|
|
89
|
-
# gas_price*2 (60 gwei) > 1 gwei floor → uses doubled value
|
|
97
|
+
def test_avalanche_uses_eip1559_when_priority_fee_available(mod):
|
|
90
98
|
w3 = _StubW3(gas_price=30 * GWEI, max_priority_fee=1 * GWEI)
|
|
91
|
-
tx = {}
|
|
99
|
+
tx = {"gasPrice": 1} # stale value dropped, not added-to
|
|
92
100
|
mod._set_gas_price_for(43114, w3, tx)
|
|
93
|
-
assert
|
|
94
|
-
|
|
95
|
-
assert "
|
|
101
|
+
assert "gasPrice" not in tx
|
|
102
|
+
# max(base*2, base + prio + 1gwei) = max(60, 32) = 60 gwei
|
|
103
|
+
assert tx["maxFeePerGas"] == 60 * GWEI
|
|
104
|
+
assert tx["maxPriorityFeePerGas"] == 1 * GWEI
|
|
96
105
|
|
|
97
106
|
|
|
98
|
-
def
|
|
99
|
-
#
|
|
100
|
-
|
|
107
|
+
def test_legacy_fallback_applies_1_gwei_floor(mod):
|
|
108
|
+
# max_priority_fee unsupported (raises) → legacy gasPrice path with the 1 gwei
|
|
109
|
+
# floor: gas_price*2 (0.02 gwei, realistic post-Etna base) < 1 gwei → floor wins
|
|
110
|
+
w3 = _StubW3(gas_price=GWEI // 100, max_priority_fee=ValueError("no eip-1559"))
|
|
101
111
|
tx = {"gasPrice": 1} # stale value replaced, not added-to
|
|
102
112
|
mod._set_gas_price_for(43114, w3, tx)
|
|
103
113
|
assert tx["gasPrice"] == 1 * GWEI
|
|
104
114
|
assert "maxFeePerGas" not in tx
|
|
105
115
|
assert "maxPriorityFeePerGas" not in tx
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_legacy_fallback_doubles_above_floor(mod):
|
|
119
|
+
# max_priority_fee unsupported → legacy path: gas_price*2 (60 gwei) > 1 gwei floor
|
|
120
|
+
w3 = _StubW3(gas_price=30 * GWEI, max_priority_fee=ValueError("no eip-1559"))
|
|
121
|
+
tx = {}
|
|
122
|
+
mod._set_gas_price_for(43114, w3, tx)
|
|
123
|
+
assert tx["gasPrice"] == 60 * GWEI
|
|
124
|
+
assert "maxFeePerGas" not in tx
|
|
125
|
+
assert "maxPriorityFeePerGas" not in tx
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_preset_eip1559_fields_left_untouched(mod):
|
|
129
|
+
# build_transaction already set the fee fields → helper must not override them,
|
|
130
|
+
# but must still drop a stale legacy gasPrice
|
|
131
|
+
w3 = _StubW3(gas_price=30 * GWEI, max_priority_fee=1 * GWEI)
|
|
132
|
+
tx = {"maxFeePerGas": 5 * GWEI, "maxPriorityFeePerGas": 2 * GWEI, "gasPrice": 1}
|
|
133
|
+
mod._set_gas_price_for(43114, w3, tx)
|
|
134
|
+
assert tx["maxFeePerGas"] == 5 * GWEI
|
|
135
|
+
assert tx["maxPriorityFeePerGas"] == 2 * GWEI
|
|
136
|
+
assert "gasPrice" not in tx
|
|
@@ -57,8 +57,8 @@ def test_compute_health_lever_branch_high_pct():
|
|
|
57
57
|
max_mult=10,
|
|
58
58
|
)
|
|
59
59
|
assert h["equity"] == 2000
|
|
60
|
-
assert h["
|
|
61
|
-
assert h["
|
|
60
|
+
assert h["health_pct"] == 85.0
|
|
61
|
+
assert h["health_pct"] > 70 # lever territory
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
def test_compute_health_in_range_branch():
|
|
@@ -71,8 +71,8 @@ def test_compute_health_in_range_branch():
|
|
|
71
71
|
max_mult=10,
|
|
72
72
|
)
|
|
73
73
|
assert h["equity"] == 2000
|
|
74
|
-
assert h["
|
|
75
|
-
assert 30 <= h["
|
|
74
|
+
assert h["health_pct"] == 50.0
|
|
75
|
+
assert 30 <= h["health_pct"] <= 70
|
|
76
76
|
|
|
77
77
|
|
|
78
78
|
def test_compute_health_delever_branch_low_pct():
|
|
@@ -85,8 +85,8 @@ def test_compute_health_delever_branch_low_pct():
|
|
|
85
85
|
max_mult=10,
|
|
86
86
|
)
|
|
87
87
|
assert h["equity"] == 2000
|
|
88
|
-
assert h["
|
|
89
|
-
assert h["
|
|
88
|
+
assert h["health_pct"] == 10.0
|
|
89
|
+
assert h["health_pct"] < 30 # de-lever territory
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
def test_compute_health_equity_near_zero_errors():
|
|
@@ -99,7 +99,7 @@ def test_compute_health_equity_near_zero_errors():
|
|
|
99
99
|
max_mult=10,
|
|
100
100
|
)
|
|
101
101
|
assert h["error"] == "equity near zero"
|
|
102
|
-
assert h["
|
|
102
|
+
assert h["health_pct"] == 0.0
|
|
103
103
|
|
|
104
104
|
|
|
105
105
|
def test_compute_health_basic_tier_lower_ceiling():
|
|
@@ -112,7 +112,7 @@ def test_compute_health_basic_tier_lower_ceiling():
|
|
|
112
112
|
assert basic["equity"] == premium["equity"] == 2000
|
|
113
113
|
assert basic["max_debt"] == 2000 * 4
|
|
114
114
|
assert premium["max_debt"] == 2000 * 9
|
|
115
|
-
assert basic["
|
|
115
|
+
assert basic["health_pct"] < premium["health_pct"]
|
|
116
116
|
|
|
117
117
|
|
|
118
118
|
def test_compute_health_flat_format_and_position_detection():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|