primecli 0.10.2__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.2 → primecli-0.10.4}/PKG-INFO +2 -2
  2. {primecli-0.10.2 → primecli-0.10.4}/README.md +1 -1
  3. {primecli-0.10.2 → primecli-0.10.4}/primecli/arbprime.py +8 -0
  4. {primecli-0.10.2 → primecli-0.10.4}/primecli/degenprime.py +93 -81
  5. {primecli-0.10.2 → primecli-0.10.4}/primecli/deltaprime.py +8 -0
  6. {primecli-0.10.2 → primecli-0.10.4}/primecli.egg-info/PKG-INFO +2 -2
  7. {primecli-0.10.2 → primecli-0.10.4}/primecli.egg-info/SOURCES.txt +1 -0
  8. {primecli-0.10.2 → primecli-0.10.4}/pyproject.toml +1 -1
  9. primecli-0.10.4/tests/test_paraswap_requote.py +143 -0
  10. {primecli-0.10.2 → primecli-0.10.4}/LICENSE +0 -0
  11. {primecli-0.10.2 → primecli-0.10.4}/primecli/__init__.py +0 -0
  12. {primecli-0.10.2 → primecli-0.10.4}/primecli/_flowledger.py +0 -0
  13. {primecli-0.10.2 → primecli-0.10.4}/primecli/_wallets.py +0 -0
  14. {primecli-0.10.2 → primecli-0.10.4}/primecli/bridge.py +0 -0
  15. {primecli-0.10.2 → primecli-0.10.4}/primecli/health_monitor.py +0 -0
  16. {primecli-0.10.2 → primecli-0.10.4}/primecli.egg-info/dependency_links.txt +0 -0
  17. {primecli-0.10.2 → primecli-0.10.4}/primecli.egg-info/entry_points.txt +0 -0
  18. {primecli-0.10.2 → primecli-0.10.4}/primecli.egg-info/requires.txt +0 -0
  19. {primecli-0.10.2 → primecli-0.10.4}/primecli.egg-info/top_level.txt +0 -0
  20. {primecli-0.10.2 → primecli-0.10.4}/setup.cfg +0 -0
  21. {primecli-0.10.2 → primecli-0.10.4}/tests/test_aero_range_and_swap_fallback.py +0 -0
  22. {primecli-0.10.2 → primecli-0.10.4}/tests/test_aero_rebalance.py +0 -0
  23. {primecli-0.10.2 → primecli-0.10.4}/tests/test_bridge.py +0 -0
  24. {primecli-0.10.2 → primecli-0.10.4}/tests/test_cross_file_identity.py +0 -0
  25. {primecli-0.10.2 → primecli-0.10.4}/tests/test_flowledger_transferred_amount.py +0 -0
  26. {primecli-0.10.2 → primecli-0.10.4}/tests/test_gas_limit.py +0 -0
  27. {primecli-0.10.2 → primecli-0.10.4}/tests/test_gas_pricing.py +0 -0
  28. {primecli-0.10.2 → primecli-0.10.4}/tests/test_health_meter.py +0 -0
  29. {primecli-0.10.2 → primecli-0.10.4}/tests/test_health_monitor.py +0 -0
  30. {primecli-0.10.2 → primecli-0.10.4}/tests/test_paraswap_validator.py +0 -0
  31. {primecli-0.10.2 → primecli-0.10.4}/tests/test_redstone_encoding.py +0 -0
  32. {primecli-0.10.2 → 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.2
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.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.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.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.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
 
@@ -1904,6 +1904,14 @@ def cmd_create_prime_account(execute: bool = False, fund_pool: str = None, fund_
1904
1904
  time.sleep(2)
1905
1905
  if pa:
1906
1906
  print(f" Prime Account: {pa}")
1907
+ if funding:
1908
+ # Seed deposit: createAndFundLoan does transferFrom(EOA, factory, amount)
1909
+ # — the FULL amount or it reverts (can't be partial), so the requested
1910
+ # amount is exact. Log it live so the PnL basis is captured without a
1911
+ # later rescan. token_addr left None (the ERC20 Transfer routes
1912
+ # EOA->factory->account, not directly EOA->account), so the exact
1913
+ # requested amount is logged; the backfill dedupes on (tx, asset, type).
1914
+ _log_fund_flow(pa, symbol, fund_amount, receipt)
1907
1915
  else:
1908
1916
  print(" Prime Account: created — getLoanForOwner not propagated yet, run 'my-positions' shortly.")
1909
1917
 
@@ -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
@@ -1941,6 +1939,14 @@ def cmd_create_account(execute: bool = False, fund_pool: str = None, fund_amount
1941
1939
  time.sleep(2)
1942
1940
  if pa:
1943
1941
  print(f" Degen Account: {pa}")
1942
+ if funding:
1943
+ # Seed deposit: createAndFundLoan does transferFrom(EOA, factory, amount)
1944
+ # — the FULL amount or it reverts (can't be partial), so the requested
1945
+ # amount is exact. Log it live so the PnL basis is captured without a
1946
+ # later rescan. token_addr left None (the ERC20 Transfer routes
1947
+ # EOA->factory->account, not directly EOA->account), so the exact
1948
+ # requested amount is logged; the backfill dedupes on (tx, asset, type).
1949
+ _log_fund_flow(pa, symbol, fund_amount, receipt)
1944
1950
  else:
1945
1951
  print(" Degen Account: created - getLoansForOwner not propagated yet, run 'my-positions' shortly.")
1946
1952
 
@@ -3070,13 +3076,17 @@ def _paraswap_price_route(src_token, src_dec, dest_token, dest_dec, amount_in_we
3070
3076
  """ParaSwap /prices on Base (network=8453, v6.2). Returns the priceRoute dict for a
3071
3077
  SELL of amount_in_wei src->dest. The priceRoute is passed verbatim to /transactions.
3072
3078
  excludeContractMethods is hard-coded to keep ParaSwap from picking a router method
3073
- 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."""
3074
3083
  params = {
3075
3084
  "srcToken": src_token, "srcDecimals": src_dec,
3076
3085
  "destToken": dest_token, "destDecimals": dest_dec,
3077
3086
  "amount": str(amount_in_wei), "side": "SELL",
3078
3087
  "network": CHAIN_ID, "version": "6.2", "userAddress": user_addr,
3079
3088
  "excludeContractMethods": "multiSwap,megaSwap,protectedMultiSwap,protectedMegaSwap,protectedSimpleSwap,simpleSwap,swapExactAmountInOnCurveV1",
3089
+ "excludeDEXS": "AugustusRFQ,ParaSwapLimitOrders,Hashflow,Bebop,Swaap,SwaapV2,DexalotRFQ,NativeV1,Clipper,Metric,MetricRFQ",
3080
3090
  }
3081
3091
  r = requests.get(f"{PARASWAP_API}/prices", params=params,
3082
3092
  headers={"Accept": "application/json"}, timeout=20)
@@ -3149,10 +3159,52 @@ def _paraswap_decode_and_check(selector_hex, data_bytes, src_token, dest_token,
3149
3159
  # UniV3 variant: selector + length sanity only.
3150
3160
  return None, src_token, dest_token, expected_from, None
3151
3161
 
3152
- # Executor fallback - mirrors DeltaPrime's swap-debt path. If the ParaSwap API returns a
3153
- # new executor not on the whitelist, patch in this one (the canonical legacy executor
3154
- # whose calldata format is compatible with the current API output).
3155
- _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
3156
3208
 
3157
3209
  def cmd_swap(from_sym: str, to_sym: str, amount: float, slippage_pct: float = 1.0,
3158
3210
  execute: bool = False):
@@ -3207,46 +3259,25 @@ def cmd_swap(from_sym: str, to_sym: str, amount: float, slippage_pct: float = 1.
3207
3259
  print("Fund or borrow more of the asset into the account first.")
3208
3260
  return
3209
3261
 
3210
- price_route = _paraswap_price_route(from_cfg["token"], from_cfg["decimals"],
3211
- to_cfg["token"], to_cfg["decimals"], amount_in, pa_cs)
3212
- quoted_out = int(price_route["destAmount"])
3213
- tx_built = _paraswap_build_tx(price_route, from_cfg["token"], from_cfg["decimals"],
3214
- to_cfg["token"], to_cfg["decimals"], amount_in,
3215
- slippage_pct, pa_cs)
3216
- full = bytes.fromhex(tx_built["data"][2:])
3217
- selector_hex, data_bytes = "0x" + full[:4].hex(), full[4:]
3218
- _exec, _src, _dest, _from_amt, min_out = _paraswap_decode_and_check(
3219
- selector_hex, data_bytes, from_cfg["token"], to_cfg["token"], amount_in, pa_cs)
3220
- # Simulate-first executor handling (see cmd_swap_debt rationale): keep the API
3221
- # executor when the exact tx simulates clean; only fall back to the legacy
3222
- # 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).
3223
3265
  feeds = sorted(REDSTONE_AVAILABLE_FEEDS)
3224
3266
  payload = build_redstone_payload(feeds)
3225
- def _sim_paraswap(db):
3226
- base = account.encode_abi("paraSwapV6", args=[full[:4], db])
3267
+ def _sim_paraswap(selector4, db):
3268
+ base = account.encode_abi("paraSwapV6", args=[selector4, db])
3227
3269
  try:
3228
3270
  w3.eth.call({"from": acct.address, "to": pa_cs,
3229
3271
  "data": base + payload.hex(), "gas": 8000000})
3230
3272
  return True, None
3231
3273
  except Exception as e:
3232
3274
  return False, str(e)
3233
- sim_ok, sim_err = _sim_paraswap(data_bytes)
3234
- if sim_ok:
3235
- if _exec is not None and _exec.lower() not in PARASWAP_EXECUTORS:
3236
- print(f" ✓ Executor {_exec} not in the static whitelist, but the full tx "
3237
- f"simulates clean using the API calldata as-is.")
3238
- else:
3239
- print(f" ✗ Simulation with API executor {_exec} reverted: {sim_err}")
3240
- patched = bytes(12) + bytes.fromhex(_PARASWAP_FALLBACK_EXECUTOR[2:]) + data_bytes[32:]
3241
- sim_ok, err2 = _sim_paraswap(patched)
3242
- if sim_ok:
3243
- print(f" ⚠ Falling back to legacy executor {_PARASWAP_FALLBACK_EXECUTOR} "
3244
- f"(simulates clean).")
3245
- data_bytes = patched
3246
- _paraswap_decode_and_check(selector_hex, data_bytes, from_cfg["token"],
3247
- to_cfg["token"], amount_in, pa_cs)
3248
- else:
3249
- 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"]
3250
3281
 
3251
3282
  print(f"Swap {amount} {from_asset_sym} -> {to_asset_sym} on Degen Account {pa_cs} (via ParaSwap/Velora)")
3252
3283
  print(f" Router method: {price_route['contractMethod']} ({selector_hex})")
@@ -3262,7 +3293,8 @@ def cmd_swap(from_sym: str, to_sym: str, amount: float, slippage_pct: float = 1.
3262
3293
  return
3263
3294
 
3264
3295
  if not sim_ok:
3265
- 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.")
3266
3298
  return
3267
3299
 
3268
3300
  # Rebuild the payload fresh for broadcast (the sim payload may be near the
@@ -3365,46 +3397,25 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
3365
3397
  borrow_usd = price_to * borrow_amount / 10**to_cfg["decimals"] / 1e18
3366
3398
  diff_bps = (abs(repay_usd - borrow_usd) / max(repay_usd, borrow_usd)) * 10000 if max(repay_usd, borrow_usd) else 0
3367
3399
 
3368
- price_route = _paraswap_price_route(to_cfg["token"], to_cfg["decimals"],
3369
- from_cfg["token"], from_cfg["decimals"], borrow_amount, pa_cs)
3370
- quoted_out = int(price_route["destAmount"])
3371
- tx_built = _paraswap_build_tx(price_route, to_cfg["token"], to_cfg["decimals"],
3372
- from_cfg["token"], from_cfg["decimals"], borrow_amount,
3373
- slippage_pct, pa_cs)
3374
- full = bytes.fromhex(tx_built["data"][2:])
3375
- selector_hex, data_bytes = "0x" + full[:4].hex(), full[4:]
3376
- _exec, _src, _dest, _swap_from_amt, swap_min_out = _paraswap_decode_and_check(
3377
- selector_hex, data_bytes, to_cfg["token"], from_cfg["token"], borrow_amount, pa_cs)
3378
- # Simulate-first executor handling (protocol-level facet fix confirmed 2026-06-04;
3379
- # Velora rotates executors per quote): keep the API executor when the exact tx
3380
- # simulates clean; only fall back to the legacy executor if it reverts.
3381
- 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):
3382
3404
  base = account.encode_abi("swapDebtParaSwap", args=[
3383
3405
  asset_b32(from_sym), asset_b32(to_sym), repay_amount, borrow_amount,
3384
- full[:4], db])
3406
+ selector4, db])
3385
3407
  try:
3386
3408
  w3.eth.call({"from": acct.address, "to": pa_cs,
3387
3409
  "data": base + payload.hex(), "gas": 8000000})
3388
3410
  return True, None
3389
3411
  except Exception as e:
3390
3412
  return False, str(e)
3391
- sim_ok, sim_err = _sim_swap_debt(data_bytes)
3392
- if sim_ok:
3393
- if _exec is not None and _exec.lower() not in PARASWAP_EXECUTORS:
3394
- print(f" ✓ Executor {_exec} not in the static whitelist, but the full tx "
3395
- f"simulates clean using the API calldata as-is.")
3396
- else:
3397
- print(f" ✗ Simulation with API executor {_exec} reverted: {sim_err}")
3398
- patched = bytes(12) + bytes.fromhex(_PARASWAP_FALLBACK_EXECUTOR[2:]) + data_bytes[32:]
3399
- sim_ok, err2 = _sim_swap_debt(patched)
3400
- if sim_ok:
3401
- print(f" ⚠ Falling back to legacy executor {_PARASWAP_FALLBACK_EXECUTOR} "
3402
- f"(simulates clean).")
3403
- data_bytes = patched
3404
- _paraswap_decode_and_check(selector_hex, data_bytes, to_cfg["token"],
3405
- from_cfg["token"], borrow_amount, pa_cs)
3406
- else:
3407
- 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"]
3408
3419
 
3409
3420
  print(f"Swap debt on Degen Account {pa}")
3410
3421
  print(f" Refinance: {from_sym} debt -> {to_sym} debt")
@@ -3433,7 +3444,8 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
3433
3444
  return
3434
3445
 
3435
3446
  if not sim_ok:
3436
- 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.")
3437
3449
  return
3438
3450
 
3439
3451
  base_calldata = account.encode_abi("swapDebtParaSwap", args=[
@@ -1923,6 +1923,14 @@ def cmd_create_prime_account(execute: bool = False, fund_pool: str = None, fund_
1923
1923
  time.sleep(2)
1924
1924
  if pa:
1925
1925
  print(f" Prime Account: {pa}")
1926
+ if funding:
1927
+ # Seed deposit: createAndFundLoan does transferFrom(EOA, factory, amount)
1928
+ # — the FULL amount or it reverts (can't be partial), so the requested
1929
+ # amount is exact. Log it live so the PnL basis is captured without a
1930
+ # later rescan. token_addr left None (the ERC20 Transfer routes
1931
+ # EOA->factory->account, not directly EOA->account), so the exact
1932
+ # requested amount is logged; the backfill dedupes on (tx, asset, type).
1933
+ _log_fund_flow(pa, symbol, fund_amount, receipt)
1926
1934
  else:
1927
1935
  print(" Prime Account: created — getLoanForOwner not propagated yet, run 'my-positions' shortly.")
1928
1936
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: primecli
3
- Version: 0.10.2
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.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.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.2"
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