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.
Files changed (23) hide show
  1. {primecli-0.5.4 → primecli-0.5.6}/PKG-INFO +2 -2
  2. {primecli-0.5.4 → primecli-0.5.6}/README.md +1 -1
  3. primecli-0.5.6/primecli/__init__.py +59 -0
  4. {primecli-0.5.4 → primecli-0.5.6}/primecli/arbprime.py +111 -38
  5. {primecli-0.5.4 → primecli-0.5.6}/primecli/degenprime.py +94 -24
  6. {primecli-0.5.4 → primecli-0.5.6}/primecli/deltaprime.py +90 -26
  7. {primecli-0.5.4 → primecli-0.5.6}/primecli/health_monitor.py +1 -1
  8. {primecli-0.5.4 → primecli-0.5.6}/primecli.egg-info/PKG-INFO +2 -2
  9. {primecli-0.5.4 → primecli-0.5.6}/pyproject.toml +1 -1
  10. {primecli-0.5.4 → primecli-0.5.6}/tests/test_gas_pricing.py +46 -15
  11. {primecli-0.5.4 → primecli-0.5.6}/tests/test_health_monitor.py +8 -8
  12. primecli-0.5.4/primecli/__init__.py +0 -8
  13. {primecli-0.5.4 → primecli-0.5.6}/LICENSE +0 -0
  14. {primecli-0.5.4 → primecli-0.5.6}/primecli.egg-info/SOURCES.txt +0 -0
  15. {primecli-0.5.4 → primecli-0.5.6}/primecli.egg-info/dependency_links.txt +0 -0
  16. {primecli-0.5.4 → primecli-0.5.6}/primecli.egg-info/entry_points.txt +0 -0
  17. {primecli-0.5.4 → primecli-0.5.6}/primecli.egg-info/requires.txt +0 -0
  18. {primecli-0.5.4 → primecli-0.5.6}/primecli.egg-info/top_level.txt +0 -0
  19. {primecli-0.5.4 → primecli-0.5.6}/setup.cfg +0 -0
  20. {primecli-0.5.4 → primecli-0.5.6}/tests/test_cross_file_identity.py +0 -0
  21. {primecli-0.5.4 → primecli-0.5.6}/tests/test_paraswap_validator.py +0 -0
  22. {primecli-0.5.4 → primecli-0.5.6}/tests/test_redstone_encoding.py +0 -0
  23. {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.4
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.2 The 0.x line is pre-1.0, so breaking changes are possible. See [Releases](https://github.com/Mnemosyne-quest/primecli/releases).
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.2 The 0.x line is pre-1.0, so breaking changes are possible. See [Releases](https://github.com/Mnemosyne-quest/primecli/releases).
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
- # Must match the ParaSwap executor whitelist on DeltaPrime's ParaSwapFacet and
292
- # SwapDebtFacet. The ParaSwap API can return new executors that aren't whitelisted
293
- # yet those cause on-chain InvalidExecutor() reverts. Only add executors verified
294
- # to be whitelisted on-chain.
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 + maxPriorityFeePerGas with a 2x
669
- base-fee hedge (base + prio + 1 gwei buffer). On Avalanche (legacy): sets gasPrice at
670
- 2x base fee with a 1 gwei floor. (25 gwei was the pre-Etna C-chain minimum;
671
- ACP-125 (Dec 2024) lowered the min base fee to 1 nAVAX — base now sits at ~0.01
672
- nAVAX, so a 25 gwei floor overpaid ~2500x and inflated the upfront balance
673
- requirement past small EOAs.)"""
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
- if CHAIN_ID in (42161, 8453): # Arbitrum, Base — EIP-1559
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
- else: # Avalanche (43114) — legacy gasPrice
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. Arbitrum/Base (EIP-1559): maxFeePerGas + maxPriorityFeePerGas;
687
- Avalanche (legacy): gasPrice with a 1 gwei floor (post-Etna; see _set_gas_price)."""
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
- if chain_id in (42161, 8453): # Arbitrum, Base — EIP-1559
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
- else: # Avalanche (43114) — legacy gasPrice
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 executor-patching as swap-debt (see cmd_swap_debt for full rationale).
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
- if _exec is not None and _exec.lower() not in PARASWAP_EXECUTORS:
2326
- fallback_bytes = bytes(12) + bytes.fromhex(_PARASWAP_FALLBACK_EXECUTOR[2:])
2327
- data_bytes = fallback_bytes + data_bytes[32:]
2328
- print(f" ⚠ Executor {_exec} not whitelisted; patching to {_PARASWAP_FALLBACK_EXECUTOR}")
2329
- _paraswap_decode_and_check(selector_hex, data_bytes, from_cfg["token"], to_cfg["token"],
2330
- amount_in, pa_cs)
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
- feeds = prime_account_price_feeds(account)
2346
- for s in (from_sym, to_sym):
2347
- if s not in feeds:
2348
- feeds.append(s)
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
- Broken on-chain due to a protocol-level bug on the Velora/ParaSwap facet (as of 2026-05-30).
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
- # If the ParaSwap API returned a new executor not on the DeltaPrime whitelist, patch
2764
- # it to EXECUTOR_3 (0x00001003…A07380A) the only legacy executor whose calldata
2765
- # format is compatible with the current API's output (tested on-chain 2026-05-28).
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
- if _exec is not None and _exec.lower() not in PARASWAP_EXECUTORS:
2768
- fallback_bytes = bytes(12) + bytes.fromhex(_PARASWAP_FALLBACK_EXECUTOR[2:])
2769
- data_bytes = fallback_bytes + data_bytes[32:]
2770
- print(f" ⚠ Executor {_exec} not whitelisted; patching to {_PARASWAP_FALLBACK_EXECUTOR}")
2771
- _paraswap_decode_and_check(selector_hex, data_bytes, to_cfg["token"], from_cfg["token"],
2772
- borrow_amount, pa_cs)
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 + maxPriorityFeePerGas with a 2x
240
- base-fee hedge (base + prio + 1 gwei buffer). On Avalanche (legacy): sets gasPrice at
241
- 2x base fee with a 1 gwei floor. (25 gwei was the pre-Etna C-chain minimum;
242
- ACP-125 (Dec 2024) lowered the min base fee to 1 nAVAX — base now sits at ~0.01
243
- nAVAX, so a 25 gwei floor overpaid ~2500x and inflated the upfront balance
244
- requirement past small EOAs.)"""
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
- if CHAIN_ID in (42161, 8453): # Arbitrum, Base — EIP-1559
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
- else: # Avalanche (43114) — legacy gasPrice
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
- if _exec is not None and _exec.lower() not in PARASWAP_EXECUTORS:
1981
- fallback_bytes = bytes(12) + bytes.fromhex(_PARASWAP_FALLBACK_EXECUTOR[2:])
1982
- data_bytes = fallback_bytes + data_bytes[32:]
1983
- print(f" ⚠ Executor {_exec} not whitelisted; patching to {_PARASWAP_FALLBACK_EXECUTOR}")
1984
- _paraswap_decode_and_check(selector_hex, data_bytes, from_cfg["token"], to_cfg["token"],
1985
- amount_in, pa_cs)
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
- feeds = degen_account_price_feeds(account)
2001
- for s in (from_sym, to_sym):
2002
- if s in REDSTONE_AVAILABLE_FEEDS and s not in feeds:
2003
- feeds.append(s)
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
- if _exec is not None and _exec.lower() not in PARASWAP_EXECUTORS:
2110
- fallback_bytes = bytes(12) + bytes.fromhex(_PARASWAP_FALLBACK_EXECUTOR[2:])
2111
- data_bytes = fallback_bytes + data_bytes[32:]
2112
- print(f" ⚠ Executor {_exec} not whitelisted; patching to {_PARASWAP_FALLBACK_EXECUTOR}")
2113
- _paraswap_decode_and_check(selector_hex, data_bytes, to_cfg["token"], from_cfg["token"],
2114
- borrow_amount, pa_cs)
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
- # Must match the ParaSwap executor whitelist on DeltaPrime's ParaSwapFacet and
297
- # SwapDebtFacet. The ParaSwap API can return new executors that aren't whitelisted
298
- # yet those cause on-chain InvalidExecutor() reverts. Only add executors verified
299
- # to be whitelisted on-chain.
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 executor-patching as swap-debt (see cmd_swap_debt for full rationale).
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
- if _exec is not None and _exec.lower() not in PARASWAP_EXECUTORS:
2352
- fallback_bytes = bytes(12) + bytes.fromhex(_PARASWAP_FALLBACK_EXECUTOR[2:])
2353
- data_bytes = fallback_bytes + data_bytes[32:]
2354
- print(f" ⚠ Executor {_exec} not whitelisted; patching to {_PARASWAP_FALLBACK_EXECUTOR}")
2355
- _paraswap_decode_and_check(selector_hex, data_bytes, from_cfg["token"], to_cfg["token"],
2356
- amount_in, pa_cs)
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
- feeds = prime_account_price_feeds(account)
2372
- for s in (from_sym, to_sym):
2373
- if s not in feeds:
2374
- feeds.append(s)
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
- Broken on-chain due to a protocol-level bug on the Velora/ParaSwap facet (as of 2026-05-30).
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
- # If the ParaSwap API returned a new executor not on the DeltaPrime whitelist, patch
2790
- # it to EXECUTOR_3 (0x00001003…A07380A) the only legacy executor whose calldata
2791
- # format is compatible with the current API's output (tested on-chain 2026-05-28).
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
- if _exec is not None and _exec.lower() not in PARASWAP_EXECUTORS:
2794
- fallback_bytes = bytes(12) + bytes.fromhex(_PARASWAP_FALLBACK_EXECUTOR[2:])
2795
- data_bytes = fallback_bytes + data_bytes[32:]
2796
- print(f" ⚠ Executor {_exec} not whitelisted; patching to {_PARASWAP_FALLBACK_EXECUTOR}")
2797
- _paraswap_decode_and_check(selector_hex, data_bytes, to_cfg["token"], from_cfg["token"],
2798
- borrow_amount, pa_cs)
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") or g.get("bruno_pct") # bruno_pct for backward compat
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.4
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.2 The 0.x line is pre-1.0, so breaking changes are possible. See [Releases](https://github.com/Mnemosyne-quest/primecli/releases).
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.4"
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 an explicit chain id (used by cross-chain flows
4
- like prime-bridge), so it must pick the right fee model per chain:
5
- * Arbitrum (42161) / Base (8453): EIP-1559 maxFeePerGas + maxPriorityFeePerGas,
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
- * Avalanche (43114): legacy gasPrice with a 1 gwei floor (post-Etna), and NO EIP-1559 fields.
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.max_priority_fee = max_priority_fee
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) — legacy gasPrice with 1 gwei floor (post-Etna ACP-125)
94
+ # Avalanche (43114) — EIP-1559 post-Etna (ACP-125); legacy gasPrice only as fallback
86
95
 
87
96
 
88
- def test_avalanche_sets_legacy_gasprice_no_eip1559(mod):
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 tx["gasPrice"] == 60 * GWEI
94
- assert "maxFeePerGas" not in tx
95
- assert "maxPriorityFeePerGas" not in tx
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 test_avalanche_applies_1_gwei_floor(mod):
99
- # gas_price*2 (0.02 gwei, realistic post-Etna base) < 1 gwei floor → floor wins
100
- w3 = _StubW3(gas_price=GWEI // 100, max_priority_fee=1 * GWEI)
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["bruno_pct"] == 85.0
61
- assert h["bruno_pct"] > 70 # lever territory
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["bruno_pct"] == 50.0
75
- assert 30 <= h["bruno_pct"] <= 70
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["bruno_pct"] == 10.0
89
- assert h["bruno_pct"] < 30 # de-lever territory
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["bruno_pct"] == 0.0
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["bruno_pct"] < premium["bruno_pct"]
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