primecli 0.5.4__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.4 → primecli-0.5.6}/PKG-INFO +2 -2
- {primecli-0.5.4 → primecli-0.5.6}/README.md +1 -1
- primecli-0.5.6/primecli/__init__.py +59 -0
- {primecli-0.5.4 → primecli-0.5.6}/primecli/arbprime.py +111 -38
- {primecli-0.5.4 → primecli-0.5.6}/primecli/degenprime.py +94 -24
- {primecli-0.5.4 → primecli-0.5.6}/primecli/deltaprime.py +90 -26
- {primecli-0.5.4 → primecli-0.5.6}/primecli/health_monitor.py +1 -1
- {primecli-0.5.4 → primecli-0.5.6}/primecli.egg-info/PKG-INFO +2 -2
- {primecli-0.5.4 → primecli-0.5.6}/pyproject.toml +1 -1
- {primecli-0.5.4 → primecli-0.5.6}/tests/test_gas_pricing.py +46 -15
- {primecli-0.5.4 → primecli-0.5.6}/tests/test_health_monitor.py +8 -8
- primecli-0.5.4/primecli/__init__.py +0 -8
- {primecli-0.5.4 → primecli-0.5.6}/LICENSE +0 -0
- {primecli-0.5.4 → primecli-0.5.6}/primecli.egg-info/SOURCES.txt +0 -0
- {primecli-0.5.4 → primecli-0.5.6}/primecli.egg-info/dependency_links.txt +0 -0
- {primecli-0.5.4 → primecli-0.5.6}/primecli.egg-info/entry_points.txt +0 -0
- {primecli-0.5.4 → primecli-0.5.6}/primecli.egg-info/requires.txt +0 -0
- {primecli-0.5.4 → primecli-0.5.6}/primecli.egg-info/top_level.txt +0 -0
- {primecli-0.5.4 → primecli-0.5.6}/setup.cfg +0 -0
- {primecli-0.5.4 → primecli-0.5.6}/tests/test_cross_file_identity.py +0 -0
- {primecli-0.5.4 → primecli-0.5.6}/tests/test_paraswap_validator.py +0 -0
- {primecli-0.5.4 → primecli-0.5.6}/tests/test_redstone_encoding.py +0 -0
- {primecli-0.5.4 → 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
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""primecli - command-line tools for DeltaPrime (Avalanche + Arbitrum) and DegenPrime (Base)."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import urllib.request
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
__version__ = _pkg_version("primecli")
|
|
11
|
+
except PackageNotFoundError: # running from source without an install
|
|
12
|
+
__version__ = "0.0.0+unknown"
|
|
13
|
+
|
|
14
|
+
_VERSION_CHECK_URL = "https://pypi.org/pypi/primecli/json"
|
|
15
|
+
_VERSION_TIMEOUT = 3 # seconds
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _parse_version(v: str) -> tuple:
|
|
19
|
+
"""Parse 'X.Y.Z' into a sortable tuple. Non-numeric suffixes become -inf."""
|
|
20
|
+
parts = v.split(".")
|
|
21
|
+
nums = []
|
|
22
|
+
for p in parts:
|
|
23
|
+
try:
|
|
24
|
+
nums.append(int(p))
|
|
25
|
+
except ValueError:
|
|
26
|
+
nums.append(-1)
|
|
27
|
+
# Pad to 3 elements
|
|
28
|
+
while len(nums) < 3:
|
|
29
|
+
nums.append(0)
|
|
30
|
+
return tuple(nums[:3])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def check_version(suppress_flag: bool = False) -> None:
|
|
34
|
+
"""Print a one-line upgrade hint to stderr when the installed version is behind
|
|
35
|
+
the latest on PyPI. Suppress with the env var PRIMECLI_NO_VERSION_CHECK=1,
|
|
36
|
+
the CLI flag --no-version-check, or pass suppress_flag=True."""
|
|
37
|
+
if suppress_flag or os.environ.get("PRIMECLI_NO_VERSION_CHECK") == "1":
|
|
38
|
+
return
|
|
39
|
+
if "--no-version-check" in sys.argv:
|
|
40
|
+
return
|
|
41
|
+
if __version__ in ("0.0.0+unknown",):
|
|
42
|
+
return
|
|
43
|
+
try:
|
|
44
|
+
req = urllib.request.Request(_VERSION_CHECK_URL)
|
|
45
|
+
with urllib.request.urlopen(req, timeout=_VERSION_TIMEOUT) as resp:
|
|
46
|
+
data = json.loads(resp.read().decode())
|
|
47
|
+
latest = data.get("info", {}).get("version", "")
|
|
48
|
+
if not latest:
|
|
49
|
+
return
|
|
50
|
+
installed = _parse_version(__version__)
|
|
51
|
+
latest_v = _parse_version(latest)
|
|
52
|
+
if installed < latest_v:
|
|
53
|
+
print(
|
|
54
|
+
f"⚠️ primecli {__version__} is outdated. Latest is {latest}. "
|
|
55
|
+
f"Upgrade: pip install --upgrade primecli",
|
|
56
|
+
file=sys.stderr,
|
|
57
|
+
)
|
|
58
|
+
except Exception:
|
|
59
|
+
pass # network failure or parse error → silent
|
|
@@ -228,6 +228,12 @@ if _hm is None:
|
|
|
228
228
|
_spec.loader.exec_module(_hm)
|
|
229
229
|
health_monitor = _hm
|
|
230
230
|
|
|
231
|
+
# Version check (silent on network failure or old install)
|
|
232
|
+
try:
|
|
233
|
+
from primecli import check_version
|
|
234
|
+
except ImportError:
|
|
235
|
+
def check_version(*a, **kw): pass
|
|
236
|
+
|
|
231
237
|
# Arbitrum One RPC. Override with ARBPRIME_RPC for a paid Alchemy/Infura endpoint.
|
|
232
238
|
ARBITRUM_RPC = os.environ.get("ARBPRIME_RPC", "https://arb1.arbitrum.io/rpc")
|
|
233
239
|
EXPLORER = "https://arbiscan.io" # display/links only — never used for ABI fetch
|
|
@@ -288,16 +294,16 @@ PARASWAP_AUGUSTUS = "0x6A000F20005980200259B80c5102003040001068"
|
|
|
288
294
|
PARASWAP_SUPPORTED_SELECTORS = {"0xe3ead59e", "0x876a02f6"}
|
|
289
295
|
# Executors the facet whitelists (ParaSwapHelper._checkExecutorAddress). Lowercased.
|
|
290
296
|
PARASWAP_EXECUTORS = {
|
|
291
|
-
#
|
|
292
|
-
#
|
|
293
|
-
#
|
|
294
|
-
#
|
|
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.
|
|
295
302
|
"0xdef171fe48cf0115b1d80b88dc8eab59176fee57",
|
|
296
303
|
"0x6a000f20005980200259b80c5102003040001068",
|
|
297
304
|
"0x000010036c0190e009a000d0fc3541100a07380a",
|
|
298
305
|
"0x00c600b30fb0400701010f4b080409018b9006e0",
|
|
299
306
|
"0xa0f408a000017007015e0f00320e470d00090a5b",
|
|
300
|
-
# 0x8faa... (Velora) returned by API but NOT whitelisted on-chain — confirmed 2026-05-28 & 2026-05-29
|
|
301
307
|
}
|
|
302
308
|
|
|
303
309
|
# RedStone on-demand oracle config for DeltaPrime on Arbitrum — IDENTICAL to Avalanche
|
|
@@ -665,33 +671,42 @@ def _tx_gas_price(w3) -> int:
|
|
|
665
671
|
|
|
666
672
|
def _set_gas_price(w3, tx_dict):
|
|
667
673
|
"""Set appropriate gas price fields for the chain, replacing the legacy gasPrice approach.
|
|
668
|
-
On EIP-1559 chains (Arbitrum, Base): sets maxFeePerGas +
|
|
669
|
-
base-fee hedge (base + prio + 1 gwei buffer).
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
|
674
685
|
tx_dict.pop("gasPrice", None)
|
|
675
|
-
|
|
686
|
+
try:
|
|
676
687
|
base = w3.eth.gas_price
|
|
677
688
|
prio = w3.eth.max_priority_fee
|
|
678
689
|
tx_dict["maxFeePerGas"] = max(int(base * 2), base + prio + 10**9)
|
|
679
690
|
tx_dict["maxPriorityFeePerGas"] = prio
|
|
680
|
-
|
|
691
|
+
except Exception:
|
|
692
|
+
# Legacy chain — use gasPrice instead
|
|
681
693
|
tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2), 1 * 10**9)
|
|
682
694
|
|
|
683
695
|
def _set_gas_price_for(chain_id, w3, tx_dict):
|
|
684
696
|
"""Set gas fields for an EXPLICIT chain_id rather than the module CHAIN_ID. Needed by
|
|
685
697
|
cross-chain flows (prime-bridge) where a tx may target Avalanche or Arbitrum regardless
|
|
686
|
-
of which tool built it.
|
|
687
|
-
|
|
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
|
|
688
703
|
tx_dict.pop("gasPrice", None)
|
|
689
|
-
|
|
704
|
+
try:
|
|
690
705
|
base = w3.eth.gas_price
|
|
691
706
|
prio = w3.eth.max_priority_fee
|
|
692
707
|
tx_dict["maxFeePerGas"] = max(int(base * 2), base + prio + 10**9)
|
|
693
708
|
tx_dict["maxPriorityFeePerGas"] = prio
|
|
694
|
-
|
|
709
|
+
except Exception:
|
|
695
710
|
tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2), 1 * 10**9)
|
|
696
711
|
|
|
697
712
|
def _read_env_var(path, var):
|
|
@@ -2320,14 +2335,40 @@ def _swap_via_paraswap(w3, acct, pa_cs, account, from_sym, to_sym, from_cfg, to_
|
|
|
2320
2335
|
selector_hex, data_bytes = "0x" + full[:4].hex(), full[4:]
|
|
2321
2336
|
_exec, _src, _dest, from_amt, min_out = _paraswap_decode_and_check(
|
|
2322
2337
|
selector_hex, data_bytes, from_cfg["token"], to_cfg["token"], amount_in, pa_cs)
|
|
2323
|
-
# 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.
|
|
2324
2341
|
_PARASWAP_FALLBACK_EXECUTOR = "0x000010036C0190E009a000d0fc3541100A07380A"
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
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}")
|
|
2331
2372
|
|
|
2332
2373
|
print(f"Swap {amount} {from_sym} -> {to_sym} on Prime Account {pa_cs} (via ParaSwap/Velora)")
|
|
2333
2374
|
print(f" Router method: {price_route['contractMethod']} ({selector_hex})")
|
|
@@ -2342,10 +2383,12 @@ def _swap_via_paraswap(w3, acct, pa_cs, account, from_sym, to_sym, from_cfg, to_
|
|
|
2342
2383
|
print("Run with --execute to broadcast (appends a fresh RedStone price payload).")
|
|
2343
2384
|
return
|
|
2344
2385
|
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
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).
|
|
2349
2392
|
payload = build_redstone_payload(feeds)
|
|
2350
2393
|
base_calldata = account.encode_abi("paraSwapV6", args=[full[:4], data_bytes])
|
|
2351
2394
|
data = base_calldata + payload.hex()
|
|
@@ -2538,7 +2581,8 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
|
|
|
2538
2581
|
|
|
2539
2582
|
Default (one-tx): SwapDebtFacet.swapDebtParaSwap — borrows _borrowAmount of _toAsset,
|
|
2540
2583
|
ParaSwaps it into _fromAsset, and repays _repayAmount of _fromAsset debt in a single tx.
|
|
2541
|
-
|
|
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.)
|
|
2542
2586
|
|
|
2543
2587
|
--fallback (manual 3-tx via YieldYak):
|
|
2544
2588
|
1. borrow to_sym — borrow the new debt asset into the account
|
|
@@ -2760,16 +2804,40 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
|
|
|
2760
2804
|
_exec, _src, _dest, swap_from_amt, swap_min_out = _paraswap_decode_and_check(
|
|
2761
2805
|
selector_hex, data_bytes, to_cfg["token"], from_cfg["token"], borrow_amount, pa_cs)
|
|
2762
2806
|
|
|
2763
|
-
#
|
|
2764
|
-
#
|
|
2765
|
-
#
|
|
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.
|
|
2766
2813
|
_PARASWAP_FALLBACK_EXECUTOR = "0x000010036C0190E009a000d0fc3541100A07380A"
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
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}")
|
|
2773
2841
|
|
|
2774
2842
|
from_pool, _, _ = get_pool_contract(_SYMBOL_TO_POOL[from_sym])
|
|
2775
2843
|
borrowed = from_pool.functions.getBorrowed(pa_cs).call()
|
|
@@ -2800,6 +2868,10 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
|
|
|
2800
2868
|
print("Run with --execute to broadcast (appends a fresh RedStone price payload).")
|
|
2801
2869
|
return
|
|
2802
2870
|
|
|
2871
|
+
if not sim_ok:
|
|
2872
|
+
print("✗ Refusing to broadcast: simulation reverted for both executor variants.")
|
|
2873
|
+
return
|
|
2874
|
+
|
|
2803
2875
|
base_calldata = account.encode_abi("swapDebtParaSwap", args=[
|
|
2804
2876
|
asset_b32(from_sym), asset_b32(to_sym), repay_amount, borrow_amount,
|
|
2805
2877
|
full_data[:4], data_bytes,
|
|
@@ -5360,6 +5432,7 @@ def cmd_prime_bridge(from_chain: str = "avax", amount: float = None, execute: bo
|
|
|
5360
5432
|
print(f" Tx: {src_cfg['explorer']}/{tx_hash.hex()}")
|
|
5361
5433
|
|
|
5362
5434
|
def main():
|
|
5435
|
+
check_version()
|
|
5363
5436
|
args = sys.argv[1:] if len(sys.argv) > 1 else []
|
|
5364
5437
|
# Global wallet selector: --as <agent>, stripped before command dispatch.
|
|
5365
5438
|
global _SELECTED_AGENT, _CLI_KEY
|
|
@@ -104,6 +104,12 @@ if _hm is None:
|
|
|
104
104
|
_spec.loader.exec_module(_hm)
|
|
105
105
|
health_monitor = _hm
|
|
106
106
|
|
|
107
|
+
# Version check (silent on network failure or old install)
|
|
108
|
+
try:
|
|
109
|
+
from primecli import check_version
|
|
110
|
+
except ImportError:
|
|
111
|
+
def check_version(*a, **kw): pass
|
|
112
|
+
|
|
107
113
|
# Default Base RPC. mainnet.base.org rate-limits hard (429 within a few calls); the
|
|
108
114
|
# publicnode endpoint is fronted by a load balancer with much higher anonymous limits
|
|
109
115
|
# and has been the most reliable free option for this tool's traffic pattern (lots of
|
|
@@ -236,19 +242,25 @@ def _tx_gas_price(w3) -> int:
|
|
|
236
242
|
|
|
237
243
|
def _set_gas_price(w3, tx_dict):
|
|
238
244
|
"""Set appropriate gas price fields for the chain, replacing the legacy gasPrice approach.
|
|
239
|
-
On EIP-1559 chains (Arbitrum, Base): sets maxFeePerGas +
|
|
240
|
-
base-fee hedge (base + prio + 1 gwei buffer).
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
|
245
256
|
tx_dict.pop("gasPrice", None)
|
|
246
|
-
|
|
257
|
+
try:
|
|
247
258
|
base = w3.eth.gas_price
|
|
248
259
|
prio = w3.eth.max_priority_fee
|
|
249
260
|
tx_dict["maxFeePerGas"] = max(int(base * 2), base + prio + 10**9)
|
|
250
261
|
tx_dict["maxPriorityFeePerGas"] = prio
|
|
251
|
-
|
|
262
|
+
except Exception:
|
|
263
|
+
# Legacy chain — use gasPrice instead
|
|
252
264
|
tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2), 1 * 10**9)
|
|
253
265
|
def resolve_private_key():
|
|
254
266
|
"""Resolve the signing key per the documented precedence:
|
|
@@ -1977,12 +1989,39 @@ def cmd_swap(from_sym: str, to_sym: str, amount: float, slippage_pct: float = 1.
|
|
|
1977
1989
|
selector_hex, data_bytes = "0x" + full[:4].hex(), full[4:]
|
|
1978
1990
|
_exec, _src, _dest, _from_amt, min_out = _paraswap_decode_and_check(
|
|
1979
1991
|
selector_hex, data_bytes, from_cfg["token"], to_cfg["token"], amount_in, pa_cs)
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
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}")
|
|
1986
2025
|
|
|
1987
2026
|
print(f"Swap {amount} {from_sym} -> {to_sym} on Degen Account {pa_cs} (via ParaSwap/Velora)")
|
|
1988
2027
|
print(f" Router method: {price_route['contractMethod']} ({selector_hex})")
|
|
@@ -1997,10 +2036,12 @@ def cmd_swap(from_sym: str, to_sym: str, amount: float, slippage_pct: float = 1.
|
|
|
1997
2036
|
print("Run with --execute to broadcast (appends a fresh RedStone price payload).")
|
|
1998
2037
|
return
|
|
1999
2038
|
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
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).
|
|
2004
2045
|
payload = build_redstone_payload(feeds)
|
|
2005
2046
|
base_calldata = account.encode_abi("paraSwapV6", args=[full[:4], data_bytes])
|
|
2006
2047
|
data = base_calldata + payload.hex()
|
|
@@ -2106,12 +2147,36 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
|
|
|
2106
2147
|
selector_hex, data_bytes = "0x" + full[:4].hex(), full[4:]
|
|
2107
2148
|
_exec, _src, _dest, _swap_from_amt, swap_min_out = _paraswap_decode_and_check(
|
|
2108
2149
|
selector_hex, data_bytes, to_cfg["token"], from_cfg["token"], borrow_amount, pa_cs)
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
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}")
|
|
2115
2180
|
|
|
2116
2181
|
print(f"Swap debt on Degen Account {pa}")
|
|
2117
2182
|
print(f" Refinance: {from_sym} debt -> {to_sym} debt")
|
|
@@ -2139,6 +2204,10 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
|
|
|
2139
2204
|
print("Run with --execute to broadcast (appends a fresh RedStone price payload).")
|
|
2140
2205
|
return
|
|
2141
2206
|
|
|
2207
|
+
if not sim_ok:
|
|
2208
|
+
print("✗ Refusing to broadcast: simulation reverted for both executor variants.")
|
|
2209
|
+
return
|
|
2210
|
+
|
|
2142
2211
|
base_calldata = account.encode_abi("swapDebtParaSwap", args=[
|
|
2143
2212
|
asset_b32(from_sym), asset_b32(to_sym), repay_amount, borrow_amount,
|
|
2144
2213
|
full[:4], data_bytes,
|
|
@@ -2425,6 +2494,7 @@ def cmd_aerodrome_positions():
|
|
|
2425
2494
|
print(" v1 lists tokenIds only. Composition + write paths deferred to v2.")
|
|
2426
2495
|
|
|
2427
2496
|
def main():
|
|
2497
|
+
check_version()
|
|
2428
2498
|
try:
|
|
2429
2499
|
_dispatch()
|
|
2430
2500
|
except RuntimeError as e:
|
|
@@ -237,6 +237,12 @@ if _hm is None:
|
|
|
237
237
|
_spec.loader.exec_module(_hm)
|
|
238
238
|
health_monitor = _hm
|
|
239
239
|
|
|
240
|
+
# Version check (silent on network failure or old install)
|
|
241
|
+
try:
|
|
242
|
+
from primecli import check_version
|
|
243
|
+
except ImportError:
|
|
244
|
+
def check_version(*a, **kw): pass
|
|
245
|
+
|
|
240
246
|
AVALANCHE_RPC = os.environ.get("DELTAPRIME_RPC", "https://api.avax.network/ext/bc/C/rpc")
|
|
241
247
|
EXPLORER = "https://snowtrace.io"
|
|
242
248
|
CHAIN_ID = 43114
|
|
@@ -293,16 +299,16 @@ PARASWAP_AUGUSTUS = "0x6A000F20005980200259B80c5102003040001068"
|
|
|
293
299
|
PARASWAP_SUPPORTED_SELECTORS = {"0xe3ead59e", "0x876a02f6"}
|
|
294
300
|
# Executors the facet whitelists (ParaSwapHelper._checkExecutorAddress). Lowercased.
|
|
295
301
|
PARASWAP_EXECUTORS = {
|
|
296
|
-
#
|
|
297
|
-
#
|
|
298
|
-
#
|
|
299
|
-
#
|
|
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.
|
|
300
307
|
"0xdef171fe48cf0115b1d80b88dc8eab59176fee57",
|
|
301
308
|
"0x6a000f20005980200259b80c5102003040001068",
|
|
302
309
|
"0x000010036c0190e009a000d0fc3541100a07380a",
|
|
303
310
|
"0x00c600b30fb0400701010f4b080409018b9006e0",
|
|
304
311
|
"0xa0f408a000017007015e0f00320e470d00090a5b",
|
|
305
|
-
# 0x8faa... (Velora) returned by API but NOT whitelisted on-chain — confirmed 2026-05-28 & 2026-05-29
|
|
306
312
|
}
|
|
307
313
|
|
|
308
314
|
# RedStone on-demand oracle config for DeltaPrime on Avalanche. The Prime Account's
|
|
@@ -2346,14 +2352,40 @@ def _swap_via_paraswap(w3, acct, pa_cs, account, from_sym, to_sym, from_cfg, to_
|
|
|
2346
2352
|
selector_hex, data_bytes = "0x" + full[:4].hex(), full[4:]
|
|
2347
2353
|
_exec, _src, _dest, from_amt, min_out = _paraswap_decode_and_check(
|
|
2348
2354
|
selector_hex, data_bytes, from_cfg["token"], to_cfg["token"], amount_in, pa_cs)
|
|
2349
|
-
# 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.
|
|
2350
2358
|
_PARASWAP_FALLBACK_EXECUTOR = "0x000010036C0190E009a000d0fc3541100A07380A"
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
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}")
|
|
2357
2389
|
|
|
2358
2390
|
print(f"Swap {amount} {from_sym} -> {to_sym} on Prime Account {pa_cs} (via ParaSwap/Velora)")
|
|
2359
2391
|
print(f" Router method: {price_route['contractMethod']} ({selector_hex})")
|
|
@@ -2368,10 +2400,12 @@ def _swap_via_paraswap(w3, acct, pa_cs, account, from_sym, to_sym, from_cfg, to_
|
|
|
2368
2400
|
print("Run with --execute to broadcast (appends a fresh RedStone price payload).")
|
|
2369
2401
|
return
|
|
2370
2402
|
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
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).
|
|
2375
2409
|
payload = build_redstone_payload(feeds)
|
|
2376
2410
|
base_calldata = account.encode_abi("paraSwapV6", args=[full[:4], data_bytes])
|
|
2377
2411
|
data = base_calldata + payload.hex()
|
|
@@ -2564,7 +2598,8 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
|
|
|
2564
2598
|
|
|
2565
2599
|
Default (one-tx): SwapDebtFacet.swapDebtParaSwap — borrows _borrowAmount of _toAsset,
|
|
2566
2600
|
ParaSwaps it into _fromAsset, and repays _repayAmount of _fromAsset debt in a single tx.
|
|
2567
|
-
|
|
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.)
|
|
2568
2603
|
|
|
2569
2604
|
--fallback (manual 3-tx via YieldYak):
|
|
2570
2605
|
1. borrow to_sym — borrow the new debt asset into the account
|
|
@@ -2786,16 +2821,40 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
|
|
|
2786
2821
|
_exec, _src, _dest, swap_from_amt, swap_min_out = _paraswap_decode_and_check(
|
|
2787
2822
|
selector_hex, data_bytes, to_cfg["token"], from_cfg["token"], borrow_amount, pa_cs)
|
|
2788
2823
|
|
|
2789
|
-
#
|
|
2790
|
-
#
|
|
2791
|
-
#
|
|
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.
|
|
2792
2830
|
_PARASWAP_FALLBACK_EXECUTOR = "0x000010036C0190E009a000d0fc3541100A07380A"
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
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}")
|
|
2799
2858
|
|
|
2800
2859
|
from_pool, _, _ = get_pool_contract(_SYMBOL_TO_POOL[from_sym])
|
|
2801
2860
|
borrowed = from_pool.functions.getBorrowed(pa_cs).call()
|
|
@@ -2826,6 +2885,10 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
|
|
|
2826
2885
|
print("Run with --execute to broadcast (appends a fresh RedStone price payload).")
|
|
2827
2886
|
return
|
|
2828
2887
|
|
|
2888
|
+
if not sim_ok:
|
|
2889
|
+
print("✗ Refusing to broadcast: simulation reverted for both executor variants.")
|
|
2890
|
+
return
|
|
2891
|
+
|
|
2829
2892
|
base_calldata = account.encode_abi("swapDebtParaSwap", args=[
|
|
2830
2893
|
asset_b32(from_sym), asset_b32(to_sym), repay_amount, borrow_amount,
|
|
2831
2894
|
full_data[:4], data_bytes,
|
|
@@ -5145,6 +5208,7 @@ def cmd_defi(as_json: bool = True):
|
|
|
5145
5208
|
print(json.dumps(_trim_defi_json(data), indent=2))
|
|
5146
5209
|
|
|
5147
5210
|
def main():
|
|
5211
|
+
check_version()
|
|
5148
5212
|
args = sys.argv[1:] if len(sys.argv) > 1 else []
|
|
5149
5213
|
# Global wallet selector: --as <agent>, stripped before command dispatch.
|
|
5150
5214
|
global _SELECTED_AGENT, _CLI_KEY
|
|
@@ -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():
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
"""primecli - command-line tools for DeltaPrime (Avalanche + Arbitrum) and DegenPrime (Base)."""
|
|
2
|
-
|
|
3
|
-
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
4
|
-
|
|
5
|
-
try:
|
|
6
|
-
__version__ = _pkg_version("primecli")
|
|
7
|
-
except PackageNotFoundError: # running from source without an install
|
|
8
|
-
__version__ = "0.0.0+unknown"
|
|
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
|