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.
- {primecli-0.10.2 → primecli-0.10.4}/PKG-INFO +2 -2
- {primecli-0.10.2 → primecli-0.10.4}/README.md +1 -1
- {primecli-0.10.2 → primecli-0.10.4}/primecli/arbprime.py +8 -0
- {primecli-0.10.2 → primecli-0.10.4}/primecli/degenprime.py +93 -81
- {primecli-0.10.2 → primecli-0.10.4}/primecli/deltaprime.py +8 -0
- {primecli-0.10.2 → primecli-0.10.4}/primecli.egg-info/PKG-INFO +2 -2
- {primecli-0.10.2 → primecli-0.10.4}/primecli.egg-info/SOURCES.txt +1 -0
- {primecli-0.10.2 → primecli-0.10.4}/pyproject.toml +1 -1
- primecli-0.10.4/tests/test_paraswap_requote.py +143 -0
- {primecli-0.10.2 → primecli-0.10.4}/LICENSE +0 -0
- {primecli-0.10.2 → primecli-0.10.4}/primecli/__init__.py +0 -0
- {primecli-0.10.2 → primecli-0.10.4}/primecli/_flowledger.py +0 -0
- {primecli-0.10.2 → primecli-0.10.4}/primecli/_wallets.py +0 -0
- {primecli-0.10.2 → primecli-0.10.4}/primecli/bridge.py +0 -0
- {primecli-0.10.2 → primecli-0.10.4}/primecli/health_monitor.py +0 -0
- {primecli-0.10.2 → primecli-0.10.4}/primecli.egg-info/dependency_links.txt +0 -0
- {primecli-0.10.2 → primecli-0.10.4}/primecli.egg-info/entry_points.txt +0 -0
- {primecli-0.10.2 → primecli-0.10.4}/primecli.egg-info/requires.txt +0 -0
- {primecli-0.10.2 → primecli-0.10.4}/primecli.egg-info/top_level.txt +0 -0
- {primecli-0.10.2 → primecli-0.10.4}/setup.cfg +0 -0
- {primecli-0.10.2 → primecli-0.10.4}/tests/test_aero_range_and_swap_fallback.py +0 -0
- {primecli-0.10.2 → primecli-0.10.4}/tests/test_aero_rebalance.py +0 -0
- {primecli-0.10.2 → primecli-0.10.4}/tests/test_bridge.py +0 -0
- {primecli-0.10.2 → primecli-0.10.4}/tests/test_cross_file_identity.py +0 -0
- {primecli-0.10.2 → primecli-0.10.4}/tests/test_flowledger_transferred_amount.py +0 -0
- {primecli-0.10.2 → primecli-0.10.4}/tests/test_gas_limit.py +0 -0
- {primecli-0.10.2 → primecli-0.10.4}/tests/test_gas_pricing.py +0 -0
- {primecli-0.10.2 → primecli-0.10.4}/tests/test_health_meter.py +0 -0
- {primecli-0.10.2 → primecli-0.10.4}/tests/test_health_monitor.py +0 -0
- {primecli-0.10.2 → primecli-0.10.4}/tests/test_paraswap_validator.py +0 -0
- {primecli-0.10.2 → primecli-0.10.4}/tests/test_redstone_encoding.py +0 -0
- {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.
|
|
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
|
|
|
@@ -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
|
|
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
|
|
@@ -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
|
-
#
|
|
3153
|
-
#
|
|
3154
|
-
#
|
|
3155
|
-
|
|
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
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
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=[
|
|
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
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
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:
|
|
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
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
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:
|
|
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.
|
|
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
|