primecli 0.10.3__tar.gz → 0.10.4__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 (32) hide show
  1. {primecli-0.10.3 → primecli-0.10.4}/PKG-INFO +2 -2
  2. {primecli-0.10.3 → primecli-0.10.4}/README.md +1 -1
  3. {primecli-0.10.3 → primecli-0.10.4}/primecli/degenprime.py +85 -81
  4. {primecli-0.10.3 → primecli-0.10.4}/primecli.egg-info/PKG-INFO +2 -2
  5. {primecli-0.10.3 → primecli-0.10.4}/primecli.egg-info/SOURCES.txt +1 -0
  6. {primecli-0.10.3 → primecli-0.10.4}/pyproject.toml +1 -1
  7. primecli-0.10.4/tests/test_paraswap_requote.py +143 -0
  8. {primecli-0.10.3 → primecli-0.10.4}/LICENSE +0 -0
  9. {primecli-0.10.3 → primecli-0.10.4}/primecli/__init__.py +0 -0
  10. {primecli-0.10.3 → primecli-0.10.4}/primecli/_flowledger.py +0 -0
  11. {primecli-0.10.3 → primecli-0.10.4}/primecli/_wallets.py +0 -0
  12. {primecli-0.10.3 → primecli-0.10.4}/primecli/arbprime.py +0 -0
  13. {primecli-0.10.3 → primecli-0.10.4}/primecli/bridge.py +0 -0
  14. {primecli-0.10.3 → primecli-0.10.4}/primecli/deltaprime.py +0 -0
  15. {primecli-0.10.3 → primecli-0.10.4}/primecli/health_monitor.py +0 -0
  16. {primecli-0.10.3 → primecli-0.10.4}/primecli.egg-info/dependency_links.txt +0 -0
  17. {primecli-0.10.3 → primecli-0.10.4}/primecli.egg-info/entry_points.txt +0 -0
  18. {primecli-0.10.3 → primecli-0.10.4}/primecli.egg-info/requires.txt +0 -0
  19. {primecli-0.10.3 → primecli-0.10.4}/primecli.egg-info/top_level.txt +0 -0
  20. {primecli-0.10.3 → primecli-0.10.4}/setup.cfg +0 -0
  21. {primecli-0.10.3 → primecli-0.10.4}/tests/test_aero_range_and_swap_fallback.py +0 -0
  22. {primecli-0.10.3 → primecli-0.10.4}/tests/test_aero_rebalance.py +0 -0
  23. {primecli-0.10.3 → primecli-0.10.4}/tests/test_bridge.py +0 -0
  24. {primecli-0.10.3 → primecli-0.10.4}/tests/test_cross_file_identity.py +0 -0
  25. {primecli-0.10.3 → primecli-0.10.4}/tests/test_flowledger_transferred_amount.py +0 -0
  26. {primecli-0.10.3 → primecli-0.10.4}/tests/test_gas_limit.py +0 -0
  27. {primecli-0.10.3 → primecli-0.10.4}/tests/test_gas_pricing.py +0 -0
  28. {primecli-0.10.3 → primecli-0.10.4}/tests/test_health_meter.py +0 -0
  29. {primecli-0.10.3 → primecli-0.10.4}/tests/test_health_monitor.py +0 -0
  30. {primecli-0.10.3 → primecli-0.10.4}/tests/test_paraswap_validator.py +0 -0
  31. {primecli-0.10.3 → primecli-0.10.4}/tests/test_redstone_encoding.py +0 -0
  32. {primecli-0.10.3 → primecli-0.10.4}/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.10.3
3
+ Version: 0.10.4
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.10.3 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.10.4 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.10.3 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.10.4 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
 
@@ -397,17 +397,15 @@ AERODROME_POOLS = {
397
397
  PARASWAP_API = "https://apiv5.paraswap.io"
398
398
  PARASWAP_AUGUSTUS = "0x6A000F20005980200259B80c5102003040001068"
399
399
  PARASWAP_SUPPORTED_SELECTORS = {"0xe3ead59e", "0x876a02f6"}
400
- # Executors the facet whitelists. Lowercased. Starting set mirrors DeltaPrime's - the
401
- # v6 router is shared, so the same executors are plausible candidates. Real txs will
402
- # reveal missing ones with InvalidExecutor reverts; add as they show up.
400
+ # Executors the facet actually accepts on Base, lowercased. Pruned 2026-06-24 after
401
+ # on-chain verification: only the v6.2 API executor below works through the DegenPrime
402
+ # ParaSwapFacet. The others Velora rotates into quotes hard-revert: 0x6a000f20 (Augustus)
403
+ # and 0x6f053800 (Velora v1) -> SwapFailed(); 0x000010036c, 0x00c600b3, 0xa0f408a0,
404
+ # 0xdef171fe -> InvalidExecutor(). So a route built around any of them is dead - the fix
405
+ # is to re-quote (Velora returns a different route each call) rather than patch the
406
+ # executor word. See _paraswap_requote_until_clean.
403
407
  PARASWAP_EXECUTORS = {
404
- "0xdef171fe48cf0115b1d80b88dc8eab59176fee57",
405
- "0x6a000f20005980200259b80c5102003040001068",
406
- "0x000010036c0190e009a000d0fc3541100a07380a",
407
- "0x00c600b30fb0400701010f4b080409018b9006e0",
408
- "0xa0f408a000017007015e0f00320e470d00090a5b",
409
- "0x8faa0000c10015610005ca010ee000d006e0e820",
410
- "0x6f0538001f90d0a5f0000060d01d34c002030900", # Velora v1
408
+ "0x8faa0000c10015610005ca010ee000d006e0e820", # v6.2 swapExactAmountIn / UniV3 executor
411
409
  }
412
410
 
413
411
  # RedStone on-demand oracle config for DegenPrime on Base. Verified identical to
@@ -3078,13 +3076,17 @@ def _paraswap_price_route(src_token, src_dec, dest_token, dest_dec, amount_in_we
3078
3076
  """ParaSwap /prices on Base (network=8453, v6.2). Returns the priceRoute dict for a
3079
3077
  SELL of amount_in_wei src->dest. The priceRoute is passed verbatim to /transactions.
3080
3078
  excludeContractMethods is hard-coded to keep ParaSwap from picking a router method
3081
- the facet can't decode (multiSwap/megaSwap/protected* etc.)."""
3079
+ the facet can't decode (multiSwap/megaSwap/protected* etc.). excludeDEXS drops the
3080
+ RFQ/maker sources (AugustusRFQ, Hashflow, etc.) up front: those won't fill for a
3081
+ contract caller and are the prime SwapFailed() culprits - cutting them at the quote
3082
+ keeps the re-quote loop from churning through routes that can never simulate clean."""
3082
3083
  params = {
3083
3084
  "srcToken": src_token, "srcDecimals": src_dec,
3084
3085
  "destToken": dest_token, "destDecimals": dest_dec,
3085
3086
  "amount": str(amount_in_wei), "side": "SELL",
3086
3087
  "network": CHAIN_ID, "version": "6.2", "userAddress": user_addr,
3087
3088
  "excludeContractMethods": "multiSwap,megaSwap,protectedMultiSwap,protectedMegaSwap,protectedSimpleSwap,simpleSwap,swapExactAmountInOnCurveV1",
3089
+ "excludeDEXS": "AugustusRFQ,ParaSwapLimitOrders,Hashflow,Bebop,Swaap,SwaapV2,DexalotRFQ,NativeV1,Clipper,Metric,MetricRFQ",
3088
3090
  }
3089
3091
  r = requests.get(f"{PARASWAP_API}/prices", params=params,
3090
3092
  headers={"Accept": "application/json"}, timeout=20)
@@ -3157,10 +3159,52 @@ def _paraswap_decode_and_check(selector_hex, data_bytes, src_token, dest_token,
3157
3159
  # UniV3 variant: selector + length sanity only.
3158
3160
  return None, src_token, dest_token, expected_from, None
3159
3161
 
3160
- # Executor fallback - mirrors DeltaPrime's swap-debt path. If the ParaSwap API returns a
3161
- # new executor not on the whitelist, patch in this one (the canonical legacy executor
3162
- # whose calldata format is compatible with the current API output).
3163
- _PARASWAP_FALLBACK_EXECUTOR = "0x000010036C0190E009a000d0fc3541100A07380A"
3162
+ # How many times to re-quote ParaSwap when the built route's facet simulation reverts.
3163
+ # Velora's /prices is non-deterministic per call: a SwapFailed() route (dead-but-
3164
+ # whitelisted executor, or an RFQ/maker leg that excludeDEXS missed) usually clears on a
3165
+ # fresh quote. We keep re-quoting and take the FIRST route that simulates clean for a
3166
+ # known-good executor. Patching the executor word in place no longer works (every
3167
+ # alternate executor reverts - see PARASWAP_EXECUTORS), so re-quoting is the only fix.
3168
+ _PARASWAP_REQUOTE_ATTEMPTS = 5
3169
+
3170
+ def _paraswap_requote_until_clean(src_token, src_dec, dest_token, dest_dec, amount_in_wei,
3171
+ slippage_pct, pa_cs, sim_fn):
3172
+ """Quote -> build -> decode -> simulate, re-quoting up to _PARASWAP_REQUOTE_ATTEMPTS
3173
+ times until the facet simulation passes, and return the first clean route.
3174
+
3175
+ `sim_fn(selector4, data_bytes) -> (ok: bool, err: str|None)` wraps the ParaSwap
3176
+ calldata in the caller's facet method (paraSwapV6 / swapDebtParaSwap) and eth_call's
3177
+ it. Returns a dict with the chosen route's artifacts (price_route, full, selector_hex,
3178
+ data_bytes, quoted_out, min_out, executor), plus sim_ok/last_err describing the final
3179
+ attempt. On all-fail it returns the LAST attempt's artifacts with sim_ok=False so the
3180
+ caller can still print a preview and refuse to broadcast."""
3181
+ last = None
3182
+ for attempt in range(1, _PARASWAP_REQUOTE_ATTEMPTS + 1):
3183
+ price_route = _paraswap_price_route(src_token, src_dec, dest_token, dest_dec,
3184
+ amount_in_wei, pa_cs)
3185
+ quoted_out = int(price_route["destAmount"])
3186
+ tx_built = _paraswap_build_tx(price_route, src_token, src_dec, dest_token, dest_dec,
3187
+ amount_in_wei, slippage_pct, pa_cs)
3188
+ full = bytes.fromhex(tx_built["data"][2:])
3189
+ selector_hex, data_bytes = "0x" + full[:4].hex(), full[4:]
3190
+ executor, _src, _dest, _from_amt, min_out = _paraswap_decode_and_check(
3191
+ selector_hex, data_bytes, src_token, dest_token, amount_in_wei, pa_cs)
3192
+ sim_ok, sim_err = sim_fn(full[:4], data_bytes)
3193
+ last = {
3194
+ "price_route": price_route, "tx_built": tx_built, "full": full,
3195
+ "selector_hex": selector_hex, "data_bytes": data_bytes,
3196
+ "quoted_out": quoted_out, "min_out": min_out, "executor": executor,
3197
+ "sim_ok": sim_ok, "last_err": sim_err,
3198
+ }
3199
+ if sim_ok:
3200
+ if attempt > 1:
3201
+ print(f" ✓ Route simulates clean on re-quote attempt {attempt}/"
3202
+ f"{_PARASWAP_REQUOTE_ATTEMPTS} (executor {executor}).")
3203
+ return last
3204
+ print(f" ✗ Route from quote attempt {attempt}/{_PARASWAP_REQUOTE_ATTEMPTS} "
3205
+ f"(executor {executor}) reverted in simulation: {sim_err}")
3206
+ print(f" ✗ All {_PARASWAP_REQUOTE_ATTEMPTS} ParaSwap quotes reverted in simulation.")
3207
+ return last
3164
3208
 
3165
3209
  def cmd_swap(from_sym: str, to_sym: str, amount: float, slippage_pct: float = 1.0,
3166
3210
  execute: bool = False):
@@ -3215,46 +3259,25 @@ def cmd_swap(from_sym: str, to_sym: str, amount: float, slippage_pct: float = 1.
3215
3259
  print("Fund or borrow more of the asset into the account first.")
3216
3260
  return
3217
3261
 
3218
- price_route = _paraswap_price_route(from_cfg["token"], from_cfg["decimals"],
3219
- to_cfg["token"], to_cfg["decimals"], amount_in, pa_cs)
3220
- quoted_out = int(price_route["destAmount"])
3221
- tx_built = _paraswap_build_tx(price_route, from_cfg["token"], from_cfg["decimals"],
3222
- to_cfg["token"], to_cfg["decimals"], amount_in,
3223
- slippage_pct, pa_cs)
3224
- full = bytes.fromhex(tx_built["data"][2:])
3225
- selector_hex, data_bytes = "0x" + full[:4].hex(), full[4:]
3226
- _exec, _src, _dest, _from_amt, min_out = _paraswap_decode_and_check(
3227
- selector_hex, data_bytes, from_cfg["token"], to_cfg["token"], amount_in, pa_cs)
3228
- # Simulate-first executor handling (see cmd_swap_debt rationale): keep the API
3229
- # executor when the exact tx simulates clean; only fall back to the legacy
3230
- # executor if the unpatched calldata reverts.
3262
+ # Simulate-first with a re-quote loop: Velora rotates executors/routes per quote, so
3263
+ # a SwapFailed() route usually clears on a fresh quote. Keep re-quoting and take the
3264
+ # first route that simulates clean (see _paraswap_requote_until_clean).
3231
3265
  feeds = sorted(REDSTONE_AVAILABLE_FEEDS)
3232
3266
  payload = build_redstone_payload(feeds)
3233
- def _sim_paraswap(db):
3234
- base = account.encode_abi("paraSwapV6", args=[full[:4], db])
3267
+ def _sim_paraswap(selector4, db):
3268
+ base = account.encode_abi("paraSwapV6", args=[selector4, db])
3235
3269
  try:
3236
3270
  w3.eth.call({"from": acct.address, "to": pa_cs,
3237
3271
  "data": base + payload.hex(), "gas": 8000000})
3238
3272
  return True, None
3239
3273
  except Exception as e:
3240
3274
  return False, str(e)
3241
- sim_ok, sim_err = _sim_paraswap(data_bytes)
3242
- if sim_ok:
3243
- if _exec is not None and _exec.lower() not in PARASWAP_EXECUTORS:
3244
- print(f" ✓ Executor {_exec} not in the static whitelist, but the full tx "
3245
- f"simulates clean using the API calldata as-is.")
3246
- else:
3247
- print(f" ✗ Simulation with API executor {_exec} reverted: {sim_err}")
3248
- patched = bytes(12) + bytes.fromhex(_PARASWAP_FALLBACK_EXECUTOR[2:]) + data_bytes[32:]
3249
- sim_ok, err2 = _sim_paraswap(patched)
3250
- if sim_ok:
3251
- print(f" ⚠ Falling back to legacy executor {_PARASWAP_FALLBACK_EXECUTOR} "
3252
- f"(simulates clean).")
3253
- data_bytes = patched
3254
- _paraswap_decode_and_check(selector_hex, data_bytes, from_cfg["token"],
3255
- to_cfg["token"], amount_in, pa_cs)
3256
- else:
3257
- print(f" ✗ Legacy-executor fallback also reverted: {err2}")
3275
+ route = _paraswap_requote_until_clean(
3276
+ from_cfg["token"], from_cfg["decimals"], to_cfg["token"], to_cfg["decimals"],
3277
+ amount_in, slippage_pct, pa_cs, _sim_paraswap)
3278
+ price_route, tx_built, full = route["price_route"], route["tx_built"], route["full"]
3279
+ selector_hex, data_bytes = route["selector_hex"], route["data_bytes"]
3280
+ quoted_out, min_out, sim_ok = route["quoted_out"], route["min_out"], route["sim_ok"]
3258
3281
 
3259
3282
  print(f"Swap {amount} {from_asset_sym} -> {to_asset_sym} on Degen Account {pa_cs} (via ParaSwap/Velora)")
3260
3283
  print(f" Router method: {price_route['contractMethod']} ({selector_hex})")
@@ -3270,7 +3293,8 @@ def cmd_swap(from_sym: str, to_sym: str, amount: float, slippage_pct: float = 1.
3270
3293
  return
3271
3294
 
3272
3295
  if not sim_ok:
3273
- print("✗ Refusing to broadcast: simulation reverted for both executor variants.")
3296
+ print(f"✗ Refusing to broadcast: every ParaSwap quote reverted in simulation "
3297
+ f"({_PARASWAP_REQUOTE_ATTEMPTS} attempts). Try again shortly.")
3274
3298
  return
3275
3299
 
3276
3300
  # Rebuild the payload fresh for broadcast (the sim payload may be near the
@@ -3373,46 +3397,25 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
3373
3397
  borrow_usd = price_to * borrow_amount / 10**to_cfg["decimals"] / 1e18
3374
3398
  diff_bps = (abs(repay_usd - borrow_usd) / max(repay_usd, borrow_usd)) * 10000 if max(repay_usd, borrow_usd) else 0
3375
3399
 
3376
- price_route = _paraswap_price_route(to_cfg["token"], to_cfg["decimals"],
3377
- from_cfg["token"], from_cfg["decimals"], borrow_amount, pa_cs)
3378
- quoted_out = int(price_route["destAmount"])
3379
- tx_built = _paraswap_build_tx(price_route, to_cfg["token"], to_cfg["decimals"],
3380
- from_cfg["token"], from_cfg["decimals"], borrow_amount,
3381
- slippage_pct, pa_cs)
3382
- full = bytes.fromhex(tx_built["data"][2:])
3383
- selector_hex, data_bytes = "0x" + full[:4].hex(), full[4:]
3384
- _exec, _src, _dest, _swap_from_amt, swap_min_out = _paraswap_decode_and_check(
3385
- selector_hex, data_bytes, to_cfg["token"], from_cfg["token"], borrow_amount, pa_cs)
3386
- # Simulate-first executor handling (protocol-level facet fix confirmed 2026-06-04;
3387
- # Velora rotates executors per quote): keep the API executor when the exact tx
3388
- # simulates clean; only fall back to the legacy executor if it reverts.
3389
- def _sim_swap_debt(db):
3400
+ # Simulate-first with a re-quote loop (Velora rotates executors/routes per quote, and
3401
+ # an RFQ/maker leg won't fill for a contract caller): re-quote until the swapDebt tx
3402
+ # simulates clean, taking the first good route (see _paraswap_requote_until_clean).
3403
+ def _sim_swap_debt(selector4, db):
3390
3404
  base = account.encode_abi("swapDebtParaSwap", args=[
3391
3405
  asset_b32(from_sym), asset_b32(to_sym), repay_amount, borrow_amount,
3392
- full[:4], db])
3406
+ selector4, db])
3393
3407
  try:
3394
3408
  w3.eth.call({"from": acct.address, "to": pa_cs,
3395
3409
  "data": base + payload.hex(), "gas": 8000000})
3396
3410
  return True, None
3397
3411
  except Exception as e:
3398
3412
  return False, str(e)
3399
- sim_ok, sim_err = _sim_swap_debt(data_bytes)
3400
- if sim_ok:
3401
- if _exec is not None and _exec.lower() not in PARASWAP_EXECUTORS:
3402
- print(f" ✓ Executor {_exec} not in the static whitelist, but the full tx "
3403
- f"simulates clean using the API calldata as-is.")
3404
- else:
3405
- print(f" ✗ Simulation with API executor {_exec} reverted: {sim_err}")
3406
- patched = bytes(12) + bytes.fromhex(_PARASWAP_FALLBACK_EXECUTOR[2:]) + data_bytes[32:]
3407
- sim_ok, err2 = _sim_swap_debt(patched)
3408
- if sim_ok:
3409
- print(f" ⚠ Falling back to legacy executor {_PARASWAP_FALLBACK_EXECUTOR} "
3410
- f"(simulates clean).")
3411
- data_bytes = patched
3412
- _paraswap_decode_and_check(selector_hex, data_bytes, to_cfg["token"],
3413
- from_cfg["token"], borrow_amount, pa_cs)
3414
- else:
3415
- print(f" ✗ Legacy-executor fallback also reverted: {err2}")
3413
+ route = _paraswap_requote_until_clean(
3414
+ to_cfg["token"], to_cfg["decimals"], from_cfg["token"], from_cfg["decimals"],
3415
+ borrow_amount, slippage_pct, pa_cs, _sim_swap_debt)
3416
+ price_route, tx_built, full = route["price_route"], route["tx_built"], route["full"]
3417
+ selector_hex, data_bytes = route["selector_hex"], route["data_bytes"]
3418
+ quoted_out, swap_min_out, sim_ok = route["quoted_out"], route["min_out"], route["sim_ok"]
3416
3419
 
3417
3420
  print(f"Swap debt on Degen Account {pa}")
3418
3421
  print(f" Refinance: {from_sym} debt -> {to_sym} debt")
@@ -3441,7 +3444,8 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
3441
3444
  return
3442
3445
 
3443
3446
  if not sim_ok:
3444
- print("✗ Refusing to broadcast: simulation reverted for both executor variants.")
3447
+ print(f"✗ Refusing to broadcast: every ParaSwap quote reverted in simulation "
3448
+ f"({_PARASWAP_REQUOTE_ATTEMPTS} attempts). Try again shortly.")
3445
3449
  return
3446
3450
 
3447
3451
  base_calldata = account.encode_abi("swapDebtParaSwap", args=[
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: primecli
3
- Version: 0.10.3
3
+ Version: 0.10.4
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.10.3 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.10.4 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
 
@@ -24,6 +24,7 @@ tests/test_gas_limit.py
24
24
  tests/test_gas_pricing.py
25
25
  tests/test_health_meter.py
26
26
  tests/test_health_monitor.py
27
+ tests/test_paraswap_requote.py
27
28
  tests/test_paraswap_validator.py
28
29
  tests/test_redstone_encoding.py
29
30
  tests/test_to_wei_units.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "primecli"
7
- version = "0.10.3"
7
+ version = "0.10.4"
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"
@@ -0,0 +1,143 @@
1
+ """Offline tests for the ParaSwap re-quote loop in degenprime.
2
+
3
+ `_paraswap_requote_until_clean` is the resilience fix for transient SwapFailed()
4
+ reverts: Velora's /prices is non-deterministic per call, so a route whose executor
5
+ is dead-but-whitelisted (or an RFQ/maker leg) usually clears on a fresh quote. The
6
+ loop re-quotes up to `_PARASWAP_REQUOTE_ATTEMPTS` times and returns the FIRST route
7
+ that simulates clean for the caller's facet method.
8
+
9
+ Pure/offline: the three ParaSwap HTTP helpers (`_paraswap_price_route`,
10
+ `_paraswap_build_tx`, `_paraswap_decode_and_check`) and the simulate callback are
11
+ all stubbed. No network, no signing, no RPC.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import importlib
17
+
18
+ import pytest
19
+
20
+ dp = importlib.import_module("primecli.degenprime")
21
+
22
+ SRC = "0x" + "11" * 20
23
+ DEST = "0x" + "22" * 20
24
+ PA = "0x" + "33" * 20
25
+ AMOUNT = 1000
26
+
27
+
28
+ def _patch_quote_chain(monkeypatch, *, n_routes):
29
+ """Stub the price-route / build-tx / decode chain so each loop iteration produces a
30
+ distinct, identifiable route. Route k carries destAmount=1000+k and selector word
31
+ bytes that encode k, so we can assert WHICH route the loop returned."""
32
+ calls = {"price": 0, "build": 0, "decode": 0}
33
+
34
+ def fake_price(src_token, src_dec, dest_token, dest_dec, amount_in_wei, user_addr):
35
+ calls["price"] += 1
36
+ k = calls["price"]
37
+ return {"destAmount": str(1000 + k), "contractMethod": "swapExactAmountIn",
38
+ "_k": k}
39
+
40
+ def fake_build(price_route, *a, **kw):
41
+ calls["build"] += 1
42
+ k = price_route["_k"]
43
+ # 4-byte selector + a body byte that records k, so data_bytes differs per route.
44
+ data_hex = "0xe3ead59e" + f"{k:064x}"
45
+ return {"data": data_hex, "to": "0xaugustus"}
46
+
47
+ def fake_decode(selector_hex, data_bytes, src_token, dest_token, expected_from, pa_cs):
48
+ calls["decode"] += 1
49
+ k = int.from_bytes(data_bytes, "big") # body == k
50
+ # (executor, src, dest, from_amt, to_amt)
51
+ return ("0xexec", src_token, dest_token, expected_from, 990 + k)
52
+
53
+ monkeypatch.setattr(dp, "_paraswap_price_route", fake_price)
54
+ monkeypatch.setattr(dp, "_paraswap_build_tx", fake_build)
55
+ monkeypatch.setattr(dp, "_paraswap_decode_and_check", fake_decode)
56
+ return calls
57
+
58
+
59
+ def _run(sim_fn):
60
+ return dp._paraswap_requote_until_clean(
61
+ SRC, 18, DEST, 6, AMOUNT, 1.0, PA, sim_fn)
62
+
63
+
64
+ def test_first_route_clean_no_requote(monkeypatch):
65
+ """Happy path: the first quote simulates clean -> exactly one quote, returned as-is."""
66
+ calls = _patch_quote_chain(monkeypatch, n_routes=5)
67
+ sim_calls = []
68
+
69
+ def sim_ok(selector4, db):
70
+ sim_calls.append(db)
71
+ return True, None
72
+
73
+ route = _run(sim_ok)
74
+ assert route["sim_ok"] is True
75
+ assert calls["price"] == 1 # no re-quote
76
+ assert len(sim_calls) == 1
77
+ assert route["quoted_out"] == 1001 # route k=1
78
+ assert route["min_out"] == 991
79
+
80
+
81
+ def test_bad_then_good_requotes_and_picks_good(monkeypatch):
82
+ """The first two routes revert in simulation; the third clears. The loop must
83
+ re-quote and return the THIRD route (its artifacts), not the first."""
84
+ calls = _patch_quote_chain(monkeypatch, n_routes=5)
85
+ attempts = {"n": 0}
86
+
87
+ def sim_bad_then_good(selector4, db):
88
+ attempts["n"] += 1
89
+ if attempts["n"] < 3:
90
+ return False, "SwapFailed()"
91
+ return True, None
92
+
93
+ route = _run(sim_bad_then_good)
94
+ assert route["sim_ok"] is True
95
+ assert calls["price"] == 3 # re-quoted twice before the clean route
96
+ assert route["quoted_out"] == 1003 # route k=3 won
97
+ assert route["min_out"] == 993
98
+ # data_bytes belongs to the winning (3rd) route.
99
+ assert int.from_bytes(route["data_bytes"], "big") == 3
100
+
101
+
102
+ def test_all_bad_returns_last_with_sim_ok_false(monkeypatch):
103
+ """If every quote reverts, the loop exhausts its attempts and returns the LAST
104
+ route's artifacts with sim_ok=False so the caller can preview and refuse."""
105
+ calls = _patch_quote_chain(monkeypatch, n_routes=10)
106
+
107
+ def sim_always_bad(selector4, db):
108
+ return False, "SwapFailed()"
109
+
110
+ route = _run(sim_always_bad)
111
+ assert route["sim_ok"] is False
112
+ assert route["last_err"] == "SwapFailed()"
113
+ assert calls["price"] == dp._PARASWAP_REQUOTE_ATTEMPTS # tried the full budget
114
+ # Returned artifacts are from the final attempt.
115
+ assert int.from_bytes(route["data_bytes"], "big") == dp._PARASWAP_REQUOTE_ATTEMPTS
116
+
117
+
118
+ def test_executors_pruned_to_verified_good():
119
+ """The static whitelist is pruned to the single verified-good v6.2 executor; the
120
+ dead legacy executors (and the useless fallback constant) are gone."""
121
+ assert dp.PARASWAP_EXECUTORS == {"0x8faa0000c10015610005ca010ee000d006e0e820"}
122
+ assert not hasattr(dp, "_PARASWAP_FALLBACK_EXECUTOR")
123
+
124
+
125
+ def test_price_route_excludes_rfq_dexs(monkeypatch):
126
+ """`_paraswap_price_route` must send excludeDEXS dropping the RFQ/maker sources so
127
+ they never reach the build/simulate stage."""
128
+ captured = {}
129
+
130
+ class _Resp:
131
+ @staticmethod
132
+ def json():
133
+ return {"priceRoute": {"destAmount": "1"}}
134
+
135
+ def fake_get(url, params=None, headers=None, timeout=None):
136
+ captured["params"] = params
137
+ return _Resp()
138
+
139
+ monkeypatch.setattr(dp.requests, "get", fake_get)
140
+ dp._paraswap_price_route(SRC, 18, DEST, 6, AMOUNT, PA)
141
+ excluded = captured["params"]["excludeDEXS"]
142
+ assert "AugustusRFQ" in excluded
143
+ assert "ParaSwapLimitOrders" in excluded
File without changes
File without changes
File without changes