primecli 0.5.5__tar.gz → 0.5.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. {primecli-0.5.5 → primecli-0.5.6}/PKG-INFO +2 -2
  2. {primecli-0.5.5 → primecli-0.5.6}/README.md +1 -1
  3. {primecli-0.5.5 → primecli-0.5.6}/primecli/arbprime.py +104 -38
  4. {primecli-0.5.5 → primecli-0.5.6}/primecli/degenprime.py +87 -24
  5. {primecli-0.5.5 → primecli-0.5.6}/primecli/deltaprime.py +83 -26
  6. {primecli-0.5.5 → primecli-0.5.6}/primecli/health_monitor.py +1 -1
  7. {primecli-0.5.5 → primecli-0.5.6}/primecli.egg-info/PKG-INFO +2 -2
  8. {primecli-0.5.5 → primecli-0.5.6}/pyproject.toml +1 -1
  9. {primecli-0.5.5 → primecli-0.5.6}/tests/test_gas_pricing.py +46 -15
  10. {primecli-0.5.5 → primecli-0.5.6}/tests/test_health_monitor.py +8 -8
  11. {primecli-0.5.5 → primecli-0.5.6}/LICENSE +0 -0
  12. {primecli-0.5.5 → primecli-0.5.6}/primecli/__init__.py +0 -0
  13. {primecli-0.5.5 → primecli-0.5.6}/primecli.egg-info/SOURCES.txt +0 -0
  14. {primecli-0.5.5 → primecli-0.5.6}/primecli.egg-info/dependency_links.txt +0 -0
  15. {primecli-0.5.5 → primecli-0.5.6}/primecli.egg-info/entry_points.txt +0 -0
  16. {primecli-0.5.5 → primecli-0.5.6}/primecli.egg-info/requires.txt +0 -0
  17. {primecli-0.5.5 → primecli-0.5.6}/primecli.egg-info/top_level.txt +0 -0
  18. {primecli-0.5.5 → primecli-0.5.6}/setup.cfg +0 -0
  19. {primecli-0.5.5 → primecli-0.5.6}/tests/test_cross_file_identity.py +0 -0
  20. {primecli-0.5.5 → primecli-0.5.6}/tests/test_paraswap_validator.py +0 -0
  21. {primecli-0.5.5 → primecli-0.5.6}/tests/test_redstone_encoding.py +0 -0
  22. {primecli-0.5.5 → primecli-0.5.6}/tests/test_to_wei_units.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: primecli
3
- Version: 0.5.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.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
 
@@ -294,16 +294,16 @@ PARASWAP_AUGUSTUS = "0x6A000F20005980200259B80c5102003040001068"
294
294
  PARASWAP_SUPPORTED_SELECTORS = {"0xe3ead59e", "0x876a02f6"}
295
295
  # Executors the facet whitelists (ParaSwapHelper._checkExecutorAddress). Lowercased.
296
296
  PARASWAP_EXECUTORS = {
297
- # Must match the ParaSwap executor whitelist on DeltaPrime's ParaSwapFacet and
298
- # SwapDebtFacet. The ParaSwap API can return new executors that aren't whitelisted
299
- # yet those cause on-chain InvalidExecutor() reverts. Only add executors verified
300
- # 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.
301
302
  "0xdef171fe48cf0115b1d80b88dc8eab59176fee57",
302
303
  "0x6a000f20005980200259b80c5102003040001068",
303
304
  "0x000010036c0190e009a000d0fc3541100a07380a",
304
305
  "0x00c600b30fb0400701010f4b080409018b9006e0",
305
306
  "0xa0f408a000017007015e0f00320e470d00090a5b",
306
- # 0x8faa... (Velora) returned by API but NOT whitelisted on-chain — confirmed 2026-05-28 & 2026-05-29
307
307
  }
308
308
 
309
309
  # RedStone on-demand oracle config for DeltaPrime on Arbitrum — IDENTICAL to Avalanche
@@ -671,33 +671,42 @@ def _tx_gas_price(w3) -> int:
671
671
 
672
672
  def _set_gas_price(w3, tx_dict):
673
673
  """Set appropriate gas price fields for the chain, replacing the legacy gasPrice approach.
674
- On EIP-1559 chains (Arbitrum, Base): sets maxFeePerGas + maxPriorityFeePerGas with a 2x
675
- base-fee hedge (base + prio + 1 gwei buffer). On Avalanche (legacy): sets gasPrice at
676
- 2x base fee with a 1 gwei floor. (25 gwei was the pre-Etna C-chain minimum;
677
- ACP-125 (Dec 2024) lowered the min base fee to 1 nAVAX — base now sits at ~0.01
678
- nAVAX, so a 25 gwei floor overpaid ~2500x and inflated the upfront balance
679
- 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
680
685
  tx_dict.pop("gasPrice", None)
681
- if CHAIN_ID in (42161, 8453): # Arbitrum, Base — EIP-1559
686
+ try:
682
687
  base = w3.eth.gas_price
683
688
  prio = w3.eth.max_priority_fee
684
689
  tx_dict["maxFeePerGas"] = max(int(base * 2), base + prio + 10**9)
685
690
  tx_dict["maxPriorityFeePerGas"] = prio
686
- else: # Avalanche (43114) — legacy gasPrice
691
+ except Exception:
692
+ # Legacy chain — use gasPrice instead
687
693
  tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2), 1 * 10**9)
688
694
 
689
695
  def _set_gas_price_for(chain_id, w3, tx_dict):
690
696
  """Set gas fields for an EXPLICIT chain_id rather than the module CHAIN_ID. Needed by
691
697
  cross-chain flows (prime-bridge) where a tx may target Avalanche or Arbitrum regardless
692
- of which tool built it. Arbitrum/Base (EIP-1559): maxFeePerGas + maxPriorityFeePerGas;
693
- 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
694
703
  tx_dict.pop("gasPrice", None)
695
- if chain_id in (42161, 8453): # Arbitrum, Base — EIP-1559
704
+ try:
696
705
  base = w3.eth.gas_price
697
706
  prio = w3.eth.max_priority_fee
698
707
  tx_dict["maxFeePerGas"] = max(int(base * 2), base + prio + 10**9)
699
708
  tx_dict["maxPriorityFeePerGas"] = prio
700
- else: # Avalanche (43114) — legacy gasPrice
709
+ except Exception:
701
710
  tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2), 1 * 10**9)
702
711
 
703
712
  def _read_env_var(path, var):
@@ -2326,14 +2335,40 @@ def _swap_via_paraswap(w3, acct, pa_cs, account, from_sym, to_sym, from_cfg, to_
2326
2335
  selector_hex, data_bytes = "0x" + full[:4].hex(), full[4:]
2327
2336
  _exec, _src, _dest, from_amt, min_out = _paraswap_decode_and_check(
2328
2337
  selector_hex, data_bytes, from_cfg["token"], to_cfg["token"], amount_in, pa_cs)
2329
- # Same 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.
2330
2341
  _PARASWAP_FALLBACK_EXECUTOR = "0x000010036C0190E009a000d0fc3541100A07380A"
2331
- if _exec is not None and _exec.lower() not in PARASWAP_EXECUTORS:
2332
- fallback_bytes = bytes(12) + bytes.fromhex(_PARASWAP_FALLBACK_EXECUTOR[2:])
2333
- data_bytes = fallback_bytes + data_bytes[32:]
2334
- print(f" ⚠ Executor {_exec} not whitelisted; patching to {_PARASWAP_FALLBACK_EXECUTOR}")
2335
- _paraswap_decode_and_check(selector_hex, data_bytes, from_cfg["token"], to_cfg["token"],
2336
- 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}")
2337
2372
 
2338
2373
  print(f"Swap {amount} {from_sym} -> {to_sym} on Prime Account {pa_cs} (via ParaSwap/Velora)")
2339
2374
  print(f" Router method: {price_route['contractMethod']} ({selector_hex})")
@@ -2348,10 +2383,12 @@ def _swap_via_paraswap(w3, acct, pa_cs, account, from_sym, to_sym, from_cfg, to_
2348
2383
  print("Run with --execute to broadcast (appends a fresh RedStone price payload).")
2349
2384
  return
2350
2385
 
2351
- feeds = prime_account_price_feeds(account)
2352
- for s in (from_sym, to_sym):
2353
- if s not in feeds:
2354
- 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).
2355
2392
  payload = build_redstone_payload(feeds)
2356
2393
  base_calldata = account.encode_abi("paraSwapV6", args=[full[:4], data_bytes])
2357
2394
  data = base_calldata + payload.hex()
@@ -2544,7 +2581,8 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
2544
2581
 
2545
2582
  Default (one-tx): SwapDebtFacet.swapDebtParaSwap — borrows _borrowAmount of _toAsset,
2546
2583
  ParaSwaps it into _fromAsset, and repays _repayAmount of _fromAsset debt in a single tx.
2547
- 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.)
2548
2586
 
2549
2587
  --fallback (manual 3-tx via YieldYak):
2550
2588
  1. borrow to_sym — borrow the new debt asset into the account
@@ -2766,16 +2804,40 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
2766
2804
  _exec, _src, _dest, swap_from_amt, swap_min_out = _paraswap_decode_and_check(
2767
2805
  selector_hex, data_bytes, to_cfg["token"], from_cfg["token"], borrow_amount, pa_cs)
2768
2806
 
2769
- # If the ParaSwap API returned a new executor not on the DeltaPrime whitelist, patch
2770
- # it to EXECUTOR_3 (0x00001003…A07380A) the only legacy executor whose calldata
2771
- # 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.
2772
2813
  _PARASWAP_FALLBACK_EXECUTOR = "0x000010036C0190E009a000d0fc3541100A07380A"
2773
- if _exec is not None and _exec.lower() not in PARASWAP_EXECUTORS:
2774
- fallback_bytes = bytes(12) + bytes.fromhex(_PARASWAP_FALLBACK_EXECUTOR[2:])
2775
- data_bytes = fallback_bytes + data_bytes[32:]
2776
- print(f" ⚠ Executor {_exec} not whitelisted; patching to {_PARASWAP_FALLBACK_EXECUTOR}")
2777
- _paraswap_decode_and_check(selector_hex, data_bytes, to_cfg["token"], from_cfg["token"],
2778
- 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}")
2779
2841
 
2780
2842
  from_pool, _, _ = get_pool_contract(_SYMBOL_TO_POOL[from_sym])
2781
2843
  borrowed = from_pool.functions.getBorrowed(pa_cs).call()
@@ -2806,6 +2868,10 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
2806
2868
  print("Run with --execute to broadcast (appends a fresh RedStone price payload).")
2807
2869
  return
2808
2870
 
2871
+ if not sim_ok:
2872
+ print("✗ Refusing to broadcast: simulation reverted for both executor variants.")
2873
+ return
2874
+
2809
2875
  base_calldata = account.encode_abi("swapDebtParaSwap", args=[
2810
2876
  asset_b32(from_sym), asset_b32(to_sym), repay_amount, borrow_amount,
2811
2877
  full_data[:4], data_bytes,
@@ -242,19 +242,25 @@ def _tx_gas_price(w3) -> int:
242
242
 
243
243
  def _set_gas_price(w3, tx_dict):
244
244
  """Set appropriate gas price fields for the chain, replacing the legacy gasPrice approach.
245
- On EIP-1559 chains (Arbitrum, Base): sets maxFeePerGas + maxPriorityFeePerGas with a 2x
246
- base-fee hedge (base + prio + 1 gwei buffer). On Avalanche (legacy): sets gasPrice at
247
- 2x base fee with a 1 gwei floor. (25 gwei was the pre-Etna C-chain minimum;
248
- ACP-125 (Dec 2024) lowered the min base fee to 1 nAVAX — base now sits at ~0.01
249
- nAVAX, so a 25 gwei floor overpaid ~2500x and inflated the upfront balance
250
- 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
251
256
  tx_dict.pop("gasPrice", None)
252
- if CHAIN_ID in (42161, 8453): # Arbitrum, Base — EIP-1559
257
+ try:
253
258
  base = w3.eth.gas_price
254
259
  prio = w3.eth.max_priority_fee
255
260
  tx_dict["maxFeePerGas"] = max(int(base * 2), base + prio + 10**9)
256
261
  tx_dict["maxPriorityFeePerGas"] = prio
257
- else: # Avalanche (43114) — legacy gasPrice
262
+ except Exception:
263
+ # Legacy chain — use gasPrice instead
258
264
  tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2), 1 * 10**9)
259
265
  def resolve_private_key():
260
266
  """Resolve the signing key per the documented precedence:
@@ -1983,12 +1989,39 @@ def cmd_swap(from_sym: str, to_sym: str, amount: float, slippage_pct: float = 1.
1983
1989
  selector_hex, data_bytes = "0x" + full[:4].hex(), full[4:]
1984
1990
  _exec, _src, _dest, _from_amt, min_out = _paraswap_decode_and_check(
1985
1991
  selector_hex, data_bytes, from_cfg["token"], to_cfg["token"], amount_in, pa_cs)
1986
- if _exec is not None and _exec.lower() not in PARASWAP_EXECUTORS:
1987
- fallback_bytes = bytes(12) + bytes.fromhex(_PARASWAP_FALLBACK_EXECUTOR[2:])
1988
- data_bytes = fallback_bytes + data_bytes[32:]
1989
- print(f" ⚠ Executor {_exec} not whitelisted; patching to {_PARASWAP_FALLBACK_EXECUTOR}")
1990
- _paraswap_decode_and_check(selector_hex, data_bytes, from_cfg["token"], to_cfg["token"],
1991
- 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}")
1992
2025
 
1993
2026
  print(f"Swap {amount} {from_sym} -> {to_sym} on Degen Account {pa_cs} (via ParaSwap/Velora)")
1994
2027
  print(f" Router method: {price_route['contractMethod']} ({selector_hex})")
@@ -2003,10 +2036,12 @@ def cmd_swap(from_sym: str, to_sym: str, amount: float, slippage_pct: float = 1.
2003
2036
  print("Run with --execute to broadcast (appends a fresh RedStone price payload).")
2004
2037
  return
2005
2038
 
2006
- feeds = degen_account_price_feeds(account)
2007
- for s in (from_sym, to_sym):
2008
- if s in REDSTONE_AVAILABLE_FEEDS and s not in feeds:
2009
- 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).
2010
2045
  payload = build_redstone_payload(feeds)
2011
2046
  base_calldata = account.encode_abi("paraSwapV6", args=[full[:4], data_bytes])
2012
2047
  data = base_calldata + payload.hex()
@@ -2112,12 +2147,36 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
2112
2147
  selector_hex, data_bytes = "0x" + full[:4].hex(), full[4:]
2113
2148
  _exec, _src, _dest, _swap_from_amt, swap_min_out = _paraswap_decode_and_check(
2114
2149
  selector_hex, data_bytes, to_cfg["token"], from_cfg["token"], borrow_amount, pa_cs)
2115
- if _exec is not None and _exec.lower() not in PARASWAP_EXECUTORS:
2116
- fallback_bytes = bytes(12) + bytes.fromhex(_PARASWAP_FALLBACK_EXECUTOR[2:])
2117
- data_bytes = fallback_bytes + data_bytes[32:]
2118
- print(f" ⚠ Executor {_exec} not whitelisted; patching to {_PARASWAP_FALLBACK_EXECUTOR}")
2119
- _paraswap_decode_and_check(selector_hex, data_bytes, to_cfg["token"], from_cfg["token"],
2120
- 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}")
2121
2180
 
2122
2181
  print(f"Swap debt on Degen Account {pa}")
2123
2182
  print(f" Refinance: {from_sym} debt -> {to_sym} debt")
@@ -2145,6 +2204,10 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
2145
2204
  print("Run with --execute to broadcast (appends a fresh RedStone price payload).")
2146
2205
  return
2147
2206
 
2207
+ if not sim_ok:
2208
+ print("✗ Refusing to broadcast: simulation reverted for both executor variants.")
2209
+ return
2210
+
2148
2211
  base_calldata = account.encode_abi("swapDebtParaSwap", args=[
2149
2212
  asset_b32(from_sym), asset_b32(to_sym), repay_amount, borrow_amount,
2150
2213
  full[:4], data_bytes,
@@ -299,16 +299,16 @@ PARASWAP_AUGUSTUS = "0x6A000F20005980200259B80c5102003040001068"
299
299
  PARASWAP_SUPPORTED_SELECTORS = {"0xe3ead59e", "0x876a02f6"}
300
300
  # Executors the facet whitelists (ParaSwapHelper._checkExecutorAddress). Lowercased.
301
301
  PARASWAP_EXECUTORS = {
302
- # Must match the ParaSwap executor whitelist on DeltaPrime's ParaSwapFacet and
303
- # SwapDebtFacet. The ParaSwap API can return new executors that aren't whitelisted
304
- # yet those cause on-chain InvalidExecutor() reverts. Only add executors verified
305
- # 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.
306
307
  "0xdef171fe48cf0115b1d80b88dc8eab59176fee57",
307
308
  "0x6a000f20005980200259b80c5102003040001068",
308
309
  "0x000010036c0190e009a000d0fc3541100a07380a",
309
310
  "0x00c600b30fb0400701010f4b080409018b9006e0",
310
311
  "0xa0f408a000017007015e0f00320e470d00090a5b",
311
- # 0x8faa... (Velora) returned by API but NOT whitelisted on-chain — confirmed 2026-05-28 & 2026-05-29
312
312
  }
313
313
 
314
314
  # RedStone on-demand oracle config for DeltaPrime on Avalanche. The Prime Account's
@@ -2352,14 +2352,40 @@ def _swap_via_paraswap(w3, acct, pa_cs, account, from_sym, to_sym, from_cfg, to_
2352
2352
  selector_hex, data_bytes = "0x" + full[:4].hex(), full[4:]
2353
2353
  _exec, _src, _dest, from_amt, min_out = _paraswap_decode_and_check(
2354
2354
  selector_hex, data_bytes, from_cfg["token"], to_cfg["token"], amount_in, pa_cs)
2355
- # Same 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.
2356
2358
  _PARASWAP_FALLBACK_EXECUTOR = "0x000010036C0190E009a000d0fc3541100A07380A"
2357
- if _exec is not None and _exec.lower() not in PARASWAP_EXECUTORS:
2358
- fallback_bytes = bytes(12) + bytes.fromhex(_PARASWAP_FALLBACK_EXECUTOR[2:])
2359
- data_bytes = fallback_bytes + data_bytes[32:]
2360
- print(f" ⚠ Executor {_exec} not whitelisted; patching to {_PARASWAP_FALLBACK_EXECUTOR}")
2361
- _paraswap_decode_and_check(selector_hex, data_bytes, from_cfg["token"], to_cfg["token"],
2362
- 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}")
2363
2389
 
2364
2390
  print(f"Swap {amount} {from_sym} -> {to_sym} on Prime Account {pa_cs} (via ParaSwap/Velora)")
2365
2391
  print(f" Router method: {price_route['contractMethod']} ({selector_hex})")
@@ -2374,10 +2400,12 @@ def _swap_via_paraswap(w3, acct, pa_cs, account, from_sym, to_sym, from_cfg, to_
2374
2400
  print("Run with --execute to broadcast (appends a fresh RedStone price payload).")
2375
2401
  return
2376
2402
 
2377
- feeds = prime_account_price_feeds(account)
2378
- for s in (from_sym, to_sym):
2379
- if s not in feeds:
2380
- 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).
2381
2409
  payload = build_redstone_payload(feeds)
2382
2410
  base_calldata = account.encode_abi("paraSwapV6", args=[full[:4], data_bytes])
2383
2411
  data = base_calldata + payload.hex()
@@ -2570,7 +2598,8 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
2570
2598
 
2571
2599
  Default (one-tx): SwapDebtFacet.swapDebtParaSwap — borrows _borrowAmount of _toAsset,
2572
2600
  ParaSwaps it into _fromAsset, and repays _repayAmount of _fromAsset debt in a single tx.
2573
- 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.)
2574
2603
 
2575
2604
  --fallback (manual 3-tx via YieldYak):
2576
2605
  1. borrow to_sym — borrow the new debt asset into the account
@@ -2792,16 +2821,40 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
2792
2821
  _exec, _src, _dest, swap_from_amt, swap_min_out = _paraswap_decode_and_check(
2793
2822
  selector_hex, data_bytes, to_cfg["token"], from_cfg["token"], borrow_amount, pa_cs)
2794
2823
 
2795
- # If the ParaSwap API returned a new executor not on the DeltaPrime whitelist, patch
2796
- # it to EXECUTOR_3 (0x00001003…A07380A) the only legacy executor whose calldata
2797
- # 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.
2798
2830
  _PARASWAP_FALLBACK_EXECUTOR = "0x000010036C0190E009a000d0fc3541100A07380A"
2799
- if _exec is not None and _exec.lower() not in PARASWAP_EXECUTORS:
2800
- fallback_bytes = bytes(12) + bytes.fromhex(_PARASWAP_FALLBACK_EXECUTOR[2:])
2801
- data_bytes = fallback_bytes + data_bytes[32:]
2802
- print(f" ⚠ Executor {_exec} not whitelisted; patching to {_PARASWAP_FALLBACK_EXECUTOR}")
2803
- _paraswap_decode_and_check(selector_hex, data_bytes, to_cfg["token"], from_cfg["token"],
2804
- 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}")
2805
2858
 
2806
2859
  from_pool, _, _ = get_pool_contract(_SYMBOL_TO_POOL[from_sym])
2807
2860
  borrowed = from_pool.functions.getBorrowed(pa_cs).call()
@@ -2832,6 +2885,10 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
2832
2885
  print("Run with --execute to broadcast (appends a fresh RedStone price payload).")
2833
2886
  return
2834
2887
 
2888
+ if not sim_ok:
2889
+ print("✗ Refusing to broadcast: simulation reverted for both executor variants.")
2890
+ return
2891
+
2835
2892
  base_calldata = account.encode_abi("swapDebtParaSwap", args=[
2836
2893
  asset_b32(from_sym), asset_b32(to_sym), repay_amount, borrow_amount,
2837
2894
  full_data[:4], data_bytes,
@@ -62,7 +62,7 @@ def compute_health(defi_data: dict, max_mult: int = 10) -> dict:
62
62
  borrowed = g.get("borrowed", [])
63
63
  health_ratio = g.get("health_ratio", 0) or 0
64
64
  # Use precomputed health_pct from defi --json if available (primecli >= 0.5.4)
65
- precomputed = g.get("health_pct") 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.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.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.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 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():
File without changes
File without changes
File without changes