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.
- {primecli-0.10.3 → primecli-0.10.4}/PKG-INFO +2 -2
- {primecli-0.10.3 → primecli-0.10.4}/README.md +1 -1
- {primecli-0.10.3 → primecli-0.10.4}/primecli/degenprime.py +85 -81
- {primecli-0.10.3 → primecli-0.10.4}/primecli.egg-info/PKG-INFO +2 -2
- {primecli-0.10.3 → primecli-0.10.4}/primecli.egg-info/SOURCES.txt +1 -0
- {primecli-0.10.3 → primecli-0.10.4}/pyproject.toml +1 -1
- primecli-0.10.4/tests/test_paraswap_requote.py +143 -0
- {primecli-0.10.3 → primecli-0.10.4}/LICENSE +0 -0
- {primecli-0.10.3 → primecli-0.10.4}/primecli/__init__.py +0 -0
- {primecli-0.10.3 → primecli-0.10.4}/primecli/_flowledger.py +0 -0
- {primecli-0.10.3 → primecli-0.10.4}/primecli/_wallets.py +0 -0
- {primecli-0.10.3 → primecli-0.10.4}/primecli/arbprime.py +0 -0
- {primecli-0.10.3 → primecli-0.10.4}/primecli/bridge.py +0 -0
- {primecli-0.10.3 → primecli-0.10.4}/primecli/deltaprime.py +0 -0
- {primecli-0.10.3 → primecli-0.10.4}/primecli/health_monitor.py +0 -0
- {primecli-0.10.3 → primecli-0.10.4}/primecli.egg-info/dependency_links.txt +0 -0
- {primecli-0.10.3 → primecli-0.10.4}/primecli.egg-info/entry_points.txt +0 -0
- {primecli-0.10.3 → primecli-0.10.4}/primecli.egg-info/requires.txt +0 -0
- {primecli-0.10.3 → primecli-0.10.4}/primecli.egg-info/top_level.txt +0 -0
- {primecli-0.10.3 → primecli-0.10.4}/setup.cfg +0 -0
- {primecli-0.10.3 → primecli-0.10.4}/tests/test_aero_range_and_swap_fallback.py +0 -0
- {primecli-0.10.3 → primecli-0.10.4}/tests/test_aero_rebalance.py +0 -0
- {primecli-0.10.3 → primecli-0.10.4}/tests/test_bridge.py +0 -0
- {primecli-0.10.3 → primecli-0.10.4}/tests/test_cross_file_identity.py +0 -0
- {primecli-0.10.3 → primecli-0.10.4}/tests/test_flowledger_transferred_amount.py +0 -0
- {primecli-0.10.3 → primecli-0.10.4}/tests/test_gas_limit.py +0 -0
- {primecli-0.10.3 → primecli-0.10.4}/tests/test_gas_pricing.py +0 -0
- {primecli-0.10.3 → primecli-0.10.4}/tests/test_health_meter.py +0 -0
- {primecli-0.10.3 → primecli-0.10.4}/tests/test_health_monitor.py +0 -0
- {primecli-0.10.3 → primecli-0.10.4}/tests/test_paraswap_validator.py +0 -0
- {primecli-0.10.3 → primecli-0.10.4}/tests/test_redstone_encoding.py +0 -0
- {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
|
+
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.
|
|
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.
|
|
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
|
|
401
|
-
#
|
|
402
|
-
#
|
|
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
|
-
"
|
|
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
|
-
#
|
|
3161
|
-
#
|
|
3162
|
-
#
|
|
3163
|
-
|
|
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
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
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=[
|
|
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
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
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:
|
|
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
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
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:
|
|
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
|
+
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.
|
|
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
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "primecli"
|
|
7
|
-
version = "0.10.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|