primecli 0.11.2__tar.gz → 0.11.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.11.2 → primecli-0.11.4}/PKG-INFO +2 -2
- {primecli-0.11.2 → primecli-0.11.4}/README.md +1 -1
- {primecli-0.11.2 → primecli-0.11.4}/primecli/arbprime.py +40 -8
- {primecli-0.11.2 → primecli-0.11.4}/primecli/degenprime.py +235 -77
- {primecli-0.11.2 → primecli-0.11.4}/primecli/deltaprime.py +58 -13
- {primecli-0.11.2 → primecli-0.11.4}/primecli.egg-info/PKG-INFO +2 -2
- {primecli-0.11.2 → primecli-0.11.4}/pyproject.toml +1 -1
- {primecli-0.11.2 → primecli-0.11.4}/LICENSE +0 -0
- {primecli-0.11.2 → primecli-0.11.4}/primecli/__init__.py +0 -0
- {primecli-0.11.2 → primecli-0.11.4}/primecli/_flowledger.py +0 -0
- {primecli-0.11.2 → primecli-0.11.4}/primecli/_wallets.py +0 -0
- {primecli-0.11.2 → primecli-0.11.4}/primecli/bridge.py +0 -0
- {primecli-0.11.2 → primecli-0.11.4}/primecli/health_monitor.py +0 -0
- {primecli-0.11.2 → primecli-0.11.4}/primecli.egg-info/SOURCES.txt +0 -0
- {primecli-0.11.2 → primecli-0.11.4}/primecli.egg-info/dependency_links.txt +0 -0
- {primecli-0.11.2 → primecli-0.11.4}/primecli.egg-info/entry_points.txt +0 -0
- {primecli-0.11.2 → primecli-0.11.4}/primecli.egg-info/requires.txt +0 -0
- {primecli-0.11.2 → primecli-0.11.4}/primecli.egg-info/top_level.txt +0 -0
- {primecli-0.11.2 → primecli-0.11.4}/setup.cfg +0 -0
- {primecli-0.11.2 → primecli-0.11.4}/tests/test_aero_range_and_swap_fallback.py +0 -0
- {primecli-0.11.2 → primecli-0.11.4}/tests/test_aero_rebalance.py +0 -0
- {primecli-0.11.2 → primecli-0.11.4}/tests/test_aero_v3_collision_fixes.py +0 -0
- {primecli-0.11.2 → primecli-0.11.4}/tests/test_bridge.py +0 -0
- {primecli-0.11.2 → primecli-0.11.4}/tests/test_cross_file_identity.py +0 -0
- {primecli-0.11.2 → primecli-0.11.4}/tests/test_flowledger_transferred_amount.py +0 -0
- {primecli-0.11.2 → primecli-0.11.4}/tests/test_gas_limit.py +0 -0
- {primecli-0.11.2 → primecli-0.11.4}/tests/test_gas_pricing.py +0 -0
- {primecli-0.11.2 → primecli-0.11.4}/tests/test_health_meter.py +0 -0
- {primecli-0.11.2 → primecli-0.11.4}/tests/test_health_monitor.py +0 -0
- {primecli-0.11.2 → primecli-0.11.4}/tests/test_paraswap_requote.py +0 -0
- {primecli-0.11.2 → primecli-0.11.4}/tests/test_paraswap_validator.py +0 -0
- {primecli-0.11.2 → primecli-0.11.4}/tests/test_redstone_encoding.py +0 -0
- {primecli-0.11.2 → primecli-0.11.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.11.
|
|
3
|
+
Version: 0.11.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.11.
|
|
50
|
+
**Current version:** 0.11.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.11.
|
|
19
|
+
**Current version:** 0.11.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
|
|
|
@@ -3470,12 +3470,24 @@ def cmd_withdrawal_intents():
|
|
|
3470
3470
|
return
|
|
3471
3471
|
|
|
3472
3472
|
any_pending = False
|
|
3473
|
+
# Batch the per-asset getAvailableBalance + getTotalIntentAmount + getUserIntents reads
|
|
3474
|
+
# (was 3 sequential eth_calls per owned asset) into one Multicall3 round-trip.
|
|
3475
|
+
pa_cs = account.address
|
|
3476
|
+
legs = []
|
|
3473
3477
|
for a in owned:
|
|
3478
|
+
legs.append((pa_cs, bytes.fromhex(account.encode_abi("getAvailableBalance", args=[a])[2:])))
|
|
3479
|
+
legs.append((pa_cs, bytes.fromhex(account.encode_abi("getTotalIntentAmount", args=[a])[2:])))
|
|
3480
|
+
legs.append((pa_cs, bytes.fromhex(account.encode_abi("getUserIntents", args=[a])[2:])))
|
|
3481
|
+
results = multicall(w3, legs)
|
|
3482
|
+
for i, a in enumerate(owned):
|
|
3474
3483
|
sym = a.rstrip(b"\x00").decode(errors="replace")
|
|
3475
3484
|
dec = _asset_decimals(sym)
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3485
|
+
av_ok, av_rd = results[3 * i]
|
|
3486
|
+
ti_ok, ti_rd = results[3 * i + 1]
|
|
3487
|
+
ui_ok, ui_rd = results[3 * i + 2]
|
|
3488
|
+
available = w3.codec.decode(["uint256"], av_rd)[0] if av_ok and av_rd else 0
|
|
3489
|
+
total_intent = w3.codec.decode(["uint256"], ti_rd)[0] if ti_ok and ti_rd else 0
|
|
3490
|
+
intents = w3.codec.decode(["(uint256,uint256,uint256,bool,bool,bool)[]"], ui_rd)[0] if ui_ok and ui_rd else []
|
|
3479
3491
|
print(f" {sym}: available {available / 10**dec:,.6f}, "
|
|
3480
3492
|
f"pending intents {total_intent / 10**dec:,.6f}")
|
|
3481
3493
|
for idx, (amt, actionable_at, expires_at, is_pending, is_actionable, is_expired) in enumerate(intents):
|
|
@@ -4823,13 +4835,33 @@ def cmd_lb_remove(pair_key: str, slippage_pct: float = 1.0, execute: bool = Fals
|
|
|
4823
4835
|
return
|
|
4824
4836
|
|
|
4825
4837
|
pair_c = _lb_pair_contract(w3, pair_cs)
|
|
4826
|
-
|
|
4838
|
+
# Batch getActiveId + per-bin (balanceOf + totalSupply + getBin) into one Multicall3
|
|
4839
|
+
# round-trip (was 1 + 3N sequential eth_calls). This is a WRITE path — the decoded
|
|
4840
|
+
# amounts feed removeLiquidity and the slippage floors — so a failed read aborts rather
|
|
4841
|
+
# than silently defaulting to 0 (matches the prior abort-on-error behaviour).
|
|
4842
|
+
legs = [(pair_cs, bytes.fromhex(pair_c.encode_abi("getActiveId", args=[])[2:]))]
|
|
4843
|
+
for binid in ids:
|
|
4844
|
+
legs.append((pair_cs, bytes.fromhex(pair_c.encode_abi("balanceOf", args=[pa_cs, binid])[2:])))
|
|
4845
|
+
legs.append((pair_cs, bytes.fromhex(pair_c.encode_abi("totalSupply", args=[binid])[2:])))
|
|
4846
|
+
legs.append((pair_cs, bytes.fromhex(pair_c.encode_abi("getBin", args=[binid])[2:])))
|
|
4847
|
+
results = multicall(w3, legs)
|
|
4848
|
+
a_ok, a_rd = results[0]
|
|
4849
|
+
if not (a_ok and a_rd):
|
|
4850
|
+
print(" Could not read the LB pair's active bin — refusing to remove.")
|
|
4851
|
+
return
|
|
4852
|
+
active_id = w3.codec.decode(["uint24"], a_rd)[0]
|
|
4827
4853
|
amounts = []
|
|
4828
4854
|
est_x = est_y = 0.0
|
|
4829
|
-
for binid in ids:
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4855
|
+
for k, binid in enumerate(ids):
|
|
4856
|
+
b_ok, b_rd = results[1 + 3 * k]
|
|
4857
|
+
t_ok, t_rd = results[1 + 3 * k + 1]
|
|
4858
|
+
g_ok, g_rd = results[1 + 3 * k + 2]
|
|
4859
|
+
if not (b_ok and b_rd and t_ok and t_rd and g_ok and g_rd):
|
|
4860
|
+
print(f" Could not read bin {binid} on [{pair_key}] — refusing to remove (incomplete data).")
|
|
4861
|
+
return
|
|
4862
|
+
bal = w3.codec.decode(["uint256"], b_rd)[0]
|
|
4863
|
+
ts = w3.codec.decode(["uint256"], t_rd)[0]
|
|
4864
|
+
rx, ry = w3.codec.decode(["uint128", "uint128"], g_rd)
|
|
4833
4865
|
amounts.append(bal)
|
|
4834
4866
|
share = (bal / ts) if ts else 0
|
|
4835
4867
|
est_x += rx * share / 10**x_cfg["decimals"]
|
|
@@ -1492,12 +1492,21 @@ def build_redstone_payload(symbols: list) -> bytes:
|
|
|
1492
1492
|
payload += REDSTONE_MARKER
|
|
1493
1493
|
return payload
|
|
1494
1494
|
|
|
1495
|
-
def degen_account_price_feeds(account) -> list:
|
|
1495
|
+
def degen_account_price_feeds(account, w3=None) -> list:
|
|
1496
1496
|
"""RedStone feed symbols a solvency check on this account needs: ETH (Base's native
|
|
1497
1497
|
base-asset reference, the BaseOracle anchor), every owned asset that has a RedStone
|
|
1498
1498
|
feed, and every debt-registry asset that has one. Symbols without a RedStone feed
|
|
1499
1499
|
are skipped - the SolvencyFacet sources them on-chain from BaseOracle TWAP and the
|
|
1500
|
-
payload doesn't need to cover them. Deduped, ETH first.
|
|
1500
|
+
payload doesn't need to cover them. Deduped, ETH first.
|
|
1501
|
+
|
|
1502
|
+
Staked Aerodrome LP legs need the same treatment: getAllOwnedAssets() and
|
|
1503
|
+
getDebts() never see tokens locked inside a staked LP NFT (per
|
|
1504
|
+
_aero_position_legs's own docstring), so an account whose ONLY exposure to a
|
|
1505
|
+
RedStone-priced asset is via a staked LP (no raw balance, no debt in it) would
|
|
1506
|
+
otherwise never request that feed - and every solvency-gated call
|
|
1507
|
+
(getTotalValue/getHealthRatio/isSolvent/repay/shouldRebalance) reverts for lack
|
|
1508
|
+
of a price instead of pricing the position correctly. Pass w3 to also enumerate
|
|
1509
|
+
those legs; omit it to keep the old (narrower) behavior."""
|
|
1501
1510
|
feeds = ["ETH"]
|
|
1502
1511
|
for a in account.functions.getAllOwnedAssets().call():
|
|
1503
1512
|
sym = a.rstrip(b"\x00").decode(errors="replace")
|
|
@@ -1507,6 +1516,18 @@ def degen_account_price_feeds(account) -> list:
|
|
|
1507
1516
|
sym = name.rstrip(b"\x00").decode(errors="replace")
|
|
1508
1517
|
if sym and sym in REDSTONE_AVAILABLE_FEEDS:
|
|
1509
1518
|
feeds.append(sym)
|
|
1519
|
+
if w3 is not None:
|
|
1520
|
+
for lg in _aero_position_legs(w3, account):
|
|
1521
|
+
for raw_sym in (lg.get("sym0"), lg.get("sym1")):
|
|
1522
|
+
if not raw_sym:
|
|
1523
|
+
continue
|
|
1524
|
+
# LP legs are read straight off the ERC20 (_resolve_token_symbol),
|
|
1525
|
+
# which returns the real on-chain symbol (e.g. "EURC"). DegenPrime's
|
|
1526
|
+
# TokenManager - and RedStone's feed - use the aliased name ("EUROC"),
|
|
1527
|
+
# same alias _account_asset_symbol already applies for raw holdings.
|
|
1528
|
+
sym = _account_asset_symbol(raw_sym)
|
|
1529
|
+
if sym in REDSTONE_AVAILABLE_FEEDS:
|
|
1530
|
+
feeds.append(sym)
|
|
1510
1531
|
return list(dict.fromkeys(feeds))
|
|
1511
1532
|
|
|
1512
1533
|
def redstone_view_call(w3, account, fn_name: str, payload: bytes, args: list = None):
|
|
@@ -2221,7 +2242,7 @@ def _gather_account_state(w3, account, pool_deposits: list):
|
|
|
2221
2242
|
# but correct: the SolvencyFacet parses the payload from the calldata tail per leg.
|
|
2222
2243
|
solvency = {"total": None, "debt": None, "ratio": None, "solvent": None, "error": None, "prices": {}}
|
|
2223
2244
|
try:
|
|
2224
|
-
feeds = degen_account_price_feeds(account)
|
|
2245
|
+
feeds = degen_account_price_feeds(account, w3=w3)
|
|
2225
2246
|
# Pool-deposit assets need their feeds in the payload too, else getPrices reverts
|
|
2226
2247
|
# on any deposit symbol whose feed the Degen Account doesn't already carry.
|
|
2227
2248
|
for _dr in pool_deposits:
|
|
@@ -2229,8 +2250,12 @@ def _gather_account_state(w3, account, pool_deposits: list):
|
|
|
2229
2250
|
feeds.append(_dr["symbol"])
|
|
2230
2251
|
payload = build_redstone_payload(feeds)
|
|
2231
2252
|
payload_hex = payload.hex()
|
|
2232
|
-
|
|
2233
|
-
|
|
2253
|
+
# Reuse `feeds` (already ETH + owned + debts + pool_deposits + staked LP legs,
|
|
2254
|
+
# alias-corrected, filtered to REDSTONE_AVAILABLE_FEEDS) instead of re-deriving
|
|
2255
|
+
# from supplied/borrowed/pool_deposits alone - that narrower set is exactly what
|
|
2256
|
+
# left LP-locked-only assets (e.g. EURC/EUROC) unpriced for the display/health
|
|
2257
|
+
# computation below even after the solvency payload itself carried the feed.
|
|
2258
|
+
price_syms = list(dict.fromkeys(feeds))
|
|
2234
2259
|
solv_legs = [
|
|
2235
2260
|
("getTotalValue", ["uint256"], account.encode_abi("getTotalValue", args=[])),
|
|
2236
2261
|
("getDebt", ["uint256"], account.encode_abi("getDebt", args=[])),
|
|
@@ -2275,6 +2300,56 @@ def _gather_account_state(w3, account, pool_deposits: list):
|
|
|
2275
2300
|
return pa_eth, supplied, borrowed, solvency
|
|
2276
2301
|
|
|
2277
2302
|
|
|
2303
|
+
def _aero_resolve_positions_batched(w3, token_ids, pa):
|
|
2304
|
+
"""Resolve {tokenId: (npm_contract, deployment, positions_struct)} for a list of staked
|
|
2305
|
+
Aerodrome tokenIds, batching the positions() probes on the V2 and V3 NPMs into ONE
|
|
2306
|
+
Multicall3 round-trip (previously 2 sequential eth_calls per id inside
|
|
2307
|
+
_aero_npm_for_token). aggregate3 allowFailure=True mirrors that resolver's try/except:
|
|
2308
|
+
a positions() revert means the id is not live on that deployment. For an id live on BOTH
|
|
2309
|
+
deployments (overlapping id ranges) the security-sensitive "which deployment does pa
|
|
2310
|
+
actually own" disambiguation is delegated unchanged to per-id _aero_npm_for_token. Dead
|
|
2311
|
+
ids are omitted. Each (contract, deployment, struct) returned is identical to what
|
|
2312
|
+
_aero_npm_for_token(w3, id, pa) would return."""
|
|
2313
|
+
if not token_ids:
|
|
2314
|
+
return {}
|
|
2315
|
+
npms = {
|
|
2316
|
+
"v2": w3.eth.contract(address=Web3.to_checksum_address(AERODROME_NPM_V2), abi=AERODROME_NPM_ABI),
|
|
2317
|
+
"v3": w3.eth.contract(address=Web3.to_checksum_address(AERODROME_NPM_V3), abi=AERODROME_NPM_ABI),
|
|
2318
|
+
}
|
|
2319
|
+
pos_out_types = [o["type"] for o in next(
|
|
2320
|
+
f for f in AERODROME_NPM_ABI if f.get("name") == "positions")["outputs"]]
|
|
2321
|
+
order = ("v2", "v3")
|
|
2322
|
+
legs = []
|
|
2323
|
+
for tid in token_ids:
|
|
2324
|
+
for ver in order:
|
|
2325
|
+
legs.append((npms[ver].address,
|
|
2326
|
+
bytes.fromhex(npms[ver].encode_abi("positions", args=[tid])[2:])))
|
|
2327
|
+
try:
|
|
2328
|
+
results = multicall(w3, legs)
|
|
2329
|
+
except Exception:
|
|
2330
|
+
results = [(False, b"")] * len(legs)
|
|
2331
|
+
resolved = {}
|
|
2332
|
+
for i, tid in enumerate(token_ids):
|
|
2333
|
+
live = []
|
|
2334
|
+
for j, ver in enumerate(order):
|
|
2335
|
+
ok, rd = results[2 * i + j]
|
|
2336
|
+
if ok and rd:
|
|
2337
|
+
try:
|
|
2338
|
+
live.append((ver, w3.codec.decode(pos_out_types, rd)))
|
|
2339
|
+
except Exception:
|
|
2340
|
+
continue
|
|
2341
|
+
if not live:
|
|
2342
|
+
continue
|
|
2343
|
+
if len(live) > 1 and pa is not None:
|
|
2344
|
+
npm, ver, pos = _aero_npm_for_token(w3, tid, pa)
|
|
2345
|
+
if npm is not None:
|
|
2346
|
+
resolved[tid] = (npm, ver, pos)
|
|
2347
|
+
else:
|
|
2348
|
+
ver, pos = live[0]
|
|
2349
|
+
resolved[tid] = (npms[ver], ver, pos)
|
|
2350
|
+
return resolved
|
|
2351
|
+
|
|
2352
|
+
|
|
2278
2353
|
def _aero_gauge_earned(w3, degen_account: 'Web3.eth.Contract', token_id: int) -> float:
|
|
2279
2354
|
"""Query unclaimed AERO rewards from the Aerodrome gauge for a staked LP.
|
|
2280
2355
|
|
|
@@ -2331,11 +2406,16 @@ def _aero_position_legs(w3, account):
|
|
|
2331
2406
|
return []
|
|
2332
2407
|
slot0_abi = json.loads(SLOT0_ABI)
|
|
2333
2408
|
q96 = Decimal(2) ** 96
|
|
2409
|
+
# Resolve V2/V3 per tokenId in ONE batched positions() round-trip (was 2 sequential
|
|
2410
|
+
# eth_calls per id). _aero_resolve_positions_batched returns exactly what per-id
|
|
2411
|
+
# _aero_npm_for_token would, ownership disambiguation for both-live ids included.
|
|
2412
|
+
resolved = _aero_resolve_positions_batched(w3, ids, account.address)
|
|
2334
2413
|
legs = []
|
|
2335
2414
|
for tid in ids:
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2415
|
+
entry = resolved.get(tid)
|
|
2416
|
+
if entry is None:
|
|
2417
|
+
continue
|
|
2418
|
+
_npm, ver, p = entry
|
|
2339
2419
|
if p is None:
|
|
2340
2420
|
continue
|
|
2341
2421
|
# positions(): nonce, operator, token0, token1, tickSpacing, tickLower,
|
|
@@ -2348,8 +2428,12 @@ def _aero_position_legs(w3, account):
|
|
|
2348
2428
|
dec1 = _resolve_token_decimals(w3, token1)
|
|
2349
2429
|
if dec0 is None or dec1 is None:
|
|
2350
2430
|
continue
|
|
2351
|
-
|
|
2352
|
-
|
|
2431
|
+
# Aliased (_account_asset_symbol) so a leg's symbol matches how DegenPrime's
|
|
2432
|
+
# TokenManager / RedStone feeds name it (e.g. "EURC" on-chain -> "EUROC"),
|
|
2433
|
+
# not the raw ERC20 symbol - callers price these legs against REDSTONE_
|
|
2434
|
+
# AVAILABLE_FEEDS / solvency["prices"], both keyed by the aliased name.
|
|
2435
|
+
sym0 = _account_asset_symbol(_resolve_token_symbol(w3, token0))
|
|
2436
|
+
sym1 = _account_asset_symbol(_resolve_token_symbol(w3, token1))
|
|
2353
2437
|
# Current sqrtPriceX96 from the pool's slot0 (exact); fall back to the
|
|
2354
2438
|
# geometric mean of the range bounds if the pool read fails.
|
|
2355
2439
|
sqrt_p = None
|
|
@@ -2817,9 +2901,38 @@ def _aero_unclaimed_usd(w3, account, solvency_prices: dict) -> float:
|
|
|
2817
2901
|
token_ids = []
|
|
2818
2902
|
if not token_ids:
|
|
2819
2903
|
return 0.0
|
|
2904
|
+
# Batch the gauge-reward reads: resolve each staked NFT's NPM (one batched positions()
|
|
2905
|
+
# round-trip), then batch ownerOf() to find each gauge, then batch earned() across all
|
|
2906
|
+
# gauges — was ~4 sequential eth_calls per id via _aero_gauge_earned. Fail-closed per
|
|
2907
|
+
# position exactly like _aero_gauge_earned: any failed leg contributes 0 AERO.
|
|
2820
2908
|
total_aero = 0.0
|
|
2821
|
-
|
|
2822
|
-
|
|
2909
|
+
resolved = _aero_resolve_positions_batched(w3, token_ids, account.address)
|
|
2910
|
+
staked = [(tid, resolved[tid][0]) for tid in token_ids if tid in resolved]
|
|
2911
|
+
if staked:
|
|
2912
|
+
owner_legs = [(npm.address, bytes.fromhex(npm.encode_abi("ownerOf", args=[tid])[2:]))
|
|
2913
|
+
for tid, npm in staked]
|
|
2914
|
+
try:
|
|
2915
|
+
owner_res = multicall(w3, owner_legs)
|
|
2916
|
+
except Exception:
|
|
2917
|
+
owner_res = [(False, b"")] * len(owner_legs)
|
|
2918
|
+
_gauge_abi = json.loads('[{"inputs":[{"name":"","type":"address"},{"name":"","type":"uint256"}],"name":"earned","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]')
|
|
2919
|
+
earned_legs = []
|
|
2920
|
+
for (tid, _npm), (ok, rd) in zip(staked, owner_res):
|
|
2921
|
+
if not (ok and rd):
|
|
2922
|
+
continue
|
|
2923
|
+
gauge_addr = w3.codec.decode(["address"], rd)[0]
|
|
2924
|
+
if not gauge_addr or int(gauge_addr, 16) == 0:
|
|
2925
|
+
continue
|
|
2926
|
+
gc = w3.eth.contract(address=Web3.to_checksum_address(gauge_addr), abi=_gauge_abi)
|
|
2927
|
+
earned_legs.append((Web3.to_checksum_address(gauge_addr),
|
|
2928
|
+
bytes.fromhex(gc.encode_abi("earned", args=[account.address, tid])[2:])))
|
|
2929
|
+
try:
|
|
2930
|
+
earned_res = multicall(w3, earned_legs)
|
|
2931
|
+
except Exception:
|
|
2932
|
+
earned_res = [(False, b"")] * len(earned_legs)
|
|
2933
|
+
for ok, rd in earned_res:
|
|
2934
|
+
if ok and rd:
|
|
2935
|
+
total_aero += w3.codec.decode(["uint256"], rd)[0] / 10 ** 18
|
|
2823
2936
|
if total_aero <= 0:
|
|
2824
2937
|
return 0.0
|
|
2825
2938
|
aero_price = (solvency_prices or {}).get("AERO")
|
|
@@ -3060,7 +3173,7 @@ def cmd_repay(pool_name: str, amount: float, execute: bool = False):
|
|
|
3060
3173
|
print(f" Capped requested {amount} {symbol} to {amount_wei / 10**cfg['decimals']:.6f} "
|
|
3061
3174
|
f"({'; '.join(cap_notes)}).")
|
|
3062
3175
|
# repay's internal _isSolvent uses proxyDelegateCalldata -> needs RedStone payload
|
|
3063
|
-
feeds = degen_account_price_feeds(account)
|
|
3176
|
+
feeds = degen_account_price_feeds(account, w3=w3)
|
|
3064
3177
|
if symbol not in feeds and symbol in REDSTONE_AVAILABLE_FEEDS:
|
|
3065
3178
|
feeds.append(symbol)
|
|
3066
3179
|
payload = build_redstone_payload(feeds)
|
|
@@ -3285,6 +3398,43 @@ def _paraswap_requote_until_clean(src_token, src_dec, dest_token, dest_dec, amou
|
|
|
3285
3398
|
print(f" ✗ All {_PARASWAP_REQUOTE_ATTEMPTS} ParaSwap quotes reverted in simulation.")
|
|
3286
3399
|
return last
|
|
3287
3400
|
|
|
3401
|
+
# Bounded slippage escalation: re-quoting at a FIXED slippage_pct only helps when
|
|
3402
|
+
# ParaSwap rotates to a different (whitelisted) executor/route. It does nothing when
|
|
3403
|
+
# ParaSwap's own off-chain quote is itself measurably rich vs the pool's live price -
|
|
3404
|
+
# confirmed on a WETH/EURC Aerodrome Slipstream pool where the quote sat ~3.2% above
|
|
3405
|
+
# the pool's own slot0 price, so every requote at 1% slippage reverted identically with
|
|
3406
|
+
# SwapFailed() (0x81ceff30, the facet's wrapper around ParaSwap's own
|
|
3407
|
+
# InsufficientReturnAmount()). Widening slippage (not re-quoting) is what actually
|
|
3408
|
+
# clears that case. Steps up only on that exact failure signature - a different error
|
|
3409
|
+
# (e.g. insufficient balance) won't be fixed by more slippage room, so we stop
|
|
3410
|
+
# immediately rather than burning extra RPC round-trips.
|
|
3411
|
+
_SLIPPAGE_ESCALATION_STEP_PCT = 1.5
|
|
3412
|
+
_SLIPPAGE_ESCALATION_MAX_PCT = 4.5 # stays clear of the facet's hard 5% ceiling
|
|
3413
|
+
|
|
3414
|
+
def _paraswap_swap_with_escalation(src_token, src_dec, dest_token, dest_dec, amount_in_wei,
|
|
3415
|
+
slippage_pct, pa_cs, sim_fn):
|
|
3416
|
+
"""Wraps _paraswap_requote_until_clean with bounded slippage escalation (see above).
|
|
3417
|
+
Starts at the caller's slippage_pct (unchanged behavior if that already clears).
|
|
3418
|
+
On an all-attempts-failed SwapFailed() result, steps slippage_pct up by
|
|
3419
|
+
_SLIPPAGE_ESCALATION_STEP_PCT and re-runs the full requote loop, capped at
|
|
3420
|
+
_SLIPPAGE_ESCALATION_MAX_PCT. Returns the same shape as
|
|
3421
|
+
_paraswap_requote_until_clean (the last attempted route, sim_ok reflecting whether
|
|
3422
|
+
it ultimately cleared)."""
|
|
3423
|
+
slip = slippage_pct
|
|
3424
|
+
while True:
|
|
3425
|
+
route = _paraswap_requote_until_clean(src_token, src_dec, dest_token, dest_dec,
|
|
3426
|
+
amount_in_wei, slip, pa_cs, sim_fn)
|
|
3427
|
+
route["slippage_pct_used"] = slip
|
|
3428
|
+
if route["sim_ok"] or slip >= _SLIPPAGE_ESCALATION_MAX_PCT:
|
|
3429
|
+
return route
|
|
3430
|
+
if not route["last_err"] or "81ceff30" not in str(route["last_err"]):
|
|
3431
|
+
return route # different failure mode - more slippage room won't fix it
|
|
3432
|
+
next_slip = min(slip + _SLIPPAGE_ESCALATION_STEP_PCT, _SLIPPAGE_ESCALATION_MAX_PCT)
|
|
3433
|
+
print(f" All quotes failed with SwapFailed() at {slip:.2f}% slippage - the "
|
|
3434
|
+
f"ParaSwap quote may be stale vs the live pool price. Retrying at "
|
|
3435
|
+
f"{next_slip:.2f}%...")
|
|
3436
|
+
slip = next_slip
|
|
3437
|
+
|
|
3288
3438
|
def cmd_swap(from_sym: str, to_sym: str, amount: float, slippage_pct: float = 1.0,
|
|
3289
3439
|
execute: bool = False):
|
|
3290
3440
|
"""Swap one in-account asset for another via the Degen Account on ParaSwap v6.
|
|
@@ -3351,19 +3501,22 @@ def cmd_swap(from_sym: str, to_sym: str, amount: float, slippage_pct: float = 1.
|
|
|
3351
3501
|
return True, None
|
|
3352
3502
|
except Exception as e:
|
|
3353
3503
|
return False, str(e)
|
|
3354
|
-
route =
|
|
3504
|
+
route = _paraswap_swap_with_escalation(
|
|
3355
3505
|
from_cfg["token"], from_cfg["decimals"], to_cfg["token"], to_cfg["decimals"],
|
|
3356
3506
|
amount_in, slippage_pct, pa_cs, _sim_paraswap)
|
|
3357
3507
|
price_route, tx_built, full = route["price_route"], route["tx_built"], route["full"]
|
|
3358
3508
|
selector_hex, data_bytes = route["selector_hex"], route["data_bytes"]
|
|
3359
3509
|
quoted_out, min_out, sim_ok = route["quoted_out"], route["min_out"], route["sim_ok"]
|
|
3510
|
+
slippage_used = route.get("slippage_pct_used", slippage_pct)
|
|
3360
3511
|
|
|
3361
3512
|
print(f"Swap {amount} {from_asset_sym} -> {to_asset_sym} on Degen Account {pa_cs} (via ParaSwap/Velora)")
|
|
3362
3513
|
print(f" Router method: {price_route['contractMethod']} ({selector_hex})")
|
|
3363
3514
|
print(f" Augustus router: {tx_built['to']}")
|
|
3364
3515
|
print(f" Expected out: {quoted_out / 10**to_cfg['decimals']:.6f} {to_asset_sym}")
|
|
3365
3516
|
if min_out is not None:
|
|
3366
|
-
print(f" Min out (@{
|
|
3517
|
+
print(f" Min out (@{slippage_used:.2f}% slippage): {min_out / 10**to_cfg['decimals']:.6f} {to_asset_sym}")
|
|
3518
|
+
if slippage_used != slippage_pct:
|
|
3519
|
+
print(f" (requested {slippage_pct}% was not routable - escalated to {slippage_used:.2f}% to clear SwapFailed())")
|
|
3367
3520
|
print(f" ParaSwap srcUSD ${price_route.get('srcUSD','?')} -> destUSD ${price_route.get('destUSD','?')}")
|
|
3368
3521
|
print(" Facet enforces a 5% hard slippage cap (RedStone-priced) on top of this.")
|
|
3369
3522
|
|
|
@@ -3372,8 +3525,9 @@ def cmd_swap(from_sym: str, to_sym: str, amount: float, slippage_pct: float = 1.
|
|
|
3372
3525
|
return
|
|
3373
3526
|
|
|
3374
3527
|
if not sim_ok:
|
|
3375
|
-
print(f"✗ Refusing to broadcast: every ParaSwap quote reverted in simulation "
|
|
3376
|
-
f"
|
|
3528
|
+
print(f"✗ Refusing to broadcast: every ParaSwap quote reverted in simulation, "
|
|
3529
|
+
f"even after escalating slippage tolerance up to {slippage_used:.2f}%. "
|
|
3530
|
+
"Try again shortly.")
|
|
3377
3531
|
return
|
|
3378
3532
|
|
|
3379
3533
|
# Rebuild the payload fresh for broadcast (the sim payload may be near the
|
|
@@ -3636,12 +3790,24 @@ def cmd_withdrawal_intents():
|
|
|
3636
3790
|
return
|
|
3637
3791
|
|
|
3638
3792
|
any_pending = False
|
|
3793
|
+
# Batch the per-asset getAvailableBalance + getTotalIntentAmount + getUserIntents reads
|
|
3794
|
+
# (was 3 sequential eth_calls per owned asset) into one Multicall3 round-trip.
|
|
3795
|
+
pa_cs = account.address
|
|
3796
|
+
legs = []
|
|
3639
3797
|
for a in owned:
|
|
3798
|
+
legs.append((pa_cs, bytes.fromhex(account.encode_abi("getAvailableBalance", args=[a])[2:])))
|
|
3799
|
+
legs.append((pa_cs, bytes.fromhex(account.encode_abi("getTotalIntentAmount", args=[a])[2:])))
|
|
3800
|
+
legs.append((pa_cs, bytes.fromhex(account.encode_abi("getUserIntents", args=[a])[2:])))
|
|
3801
|
+
results = multicall(w3, legs)
|
|
3802
|
+
for i, a in enumerate(owned):
|
|
3640
3803
|
sym = a.rstrip(b"\x00").decode(errors="replace")
|
|
3641
3804
|
dec = _asset_decimals(w3, sym)
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3805
|
+
av_ok, av_rd = results[3 * i]
|
|
3806
|
+
ti_ok, ti_rd = results[3 * i + 1]
|
|
3807
|
+
ui_ok, ui_rd = results[3 * i + 2]
|
|
3808
|
+
available = w3.codec.decode(["uint256"], av_rd)[0] if av_ok and av_rd else 0
|
|
3809
|
+
total_intent = w3.codec.decode(["uint256"], ti_rd)[0] if ti_ok and ti_rd else 0
|
|
3810
|
+
intents = w3.codec.decode(["(uint256,uint256,uint256,bool,bool,bool)[]"], ui_rd)[0] if ui_ok and ui_rd else []
|
|
3645
3811
|
print(f" {sym}: available {available / 10**dec:,.6f}, "
|
|
3646
3812
|
f"pending intents {total_intent / 10**dec:,.6f}")
|
|
3647
3813
|
for idx, (amt, actionable_at, expires_at, is_pending, is_actionable, is_expired) in enumerate(intents):
|
|
@@ -4369,6 +4535,48 @@ def _aero_separate_pool_and_sweeps(valuable: dict, sym0: str, sym1: str):
|
|
|
4369
4535
|
return pool0_bal_wei, pool1_bal_wei, sweeps
|
|
4370
4536
|
|
|
4371
4537
|
|
|
4538
|
+
def _aero_inventory_available(w3, account, pool_cfg):
|
|
4539
|
+
"""Inventory every REDSTONE-fed asset + both pool tokens held in the Degen Account, net
|
|
4540
|
+
of locked (withdrawal-intent) amounts. Batches the per-symbol getBalance +
|
|
4541
|
+
getTotalIntentAmount reads into ONE Multicall3 round-trip (was 2 sequential eth_calls per
|
|
4542
|
+
symbol — the documented cause of Base-converge RPC 429s). aggregate3 allowFailure=True
|
|
4543
|
+
preserves the old per-call tolerance: a reverting/failed leg reads 0, exactly as the
|
|
4544
|
+
previous per-call try/except did. Returns {sym: [avail_wei, decimals, 0.0]} for symbols
|
|
4545
|
+
with a positive available balance and resolvable decimals (empty dict if none)."""
|
|
4546
|
+
sym0, sym1 = pool_cfg["symbol0"], pool_cfg["symbol1"]
|
|
4547
|
+
candidates = sorted(set(REDSTONE_AVAILABLE_FEEDS) | {sym0, sym1})
|
|
4548
|
+
legs = []
|
|
4549
|
+
for sym in candidates:
|
|
4550
|
+
ab = asset_b32(_account_asset_symbol(sym))
|
|
4551
|
+
legs.append((account.address, bytes.fromhex(account.encode_abi("getBalance", args=[ab])[2:])))
|
|
4552
|
+
legs.append((account.address, bytes.fromhex(account.encode_abi("getTotalIntentAmount", args=[ab])[2:])))
|
|
4553
|
+
try:
|
|
4554
|
+
results = multicall(w3, legs)
|
|
4555
|
+
except Exception:
|
|
4556
|
+
results = [(False, b"")] * len(legs)
|
|
4557
|
+
inventory = {}
|
|
4558
|
+
for i, sym in enumerate(candidates):
|
|
4559
|
+
b_ok, b_rd = results[2 * i]
|
|
4560
|
+
l_ok, l_rd = results[2 * i + 1]
|
|
4561
|
+
bal = w3.codec.decode(["uint256"], b_rd)[0] if b_ok and b_rd else 0
|
|
4562
|
+
if bal <= 0:
|
|
4563
|
+
continue
|
|
4564
|
+
locked = w3.codec.decode(["uint256"], l_rd)[0] if l_ok and l_rd else 0
|
|
4565
|
+
avail = bal - locked if bal > locked else 0
|
|
4566
|
+
if avail <= 0:
|
|
4567
|
+
continue
|
|
4568
|
+
dec = pool_cfg["decimals0"] if sym == sym0 else (
|
|
4569
|
+
pool_cfg["decimals1"] if sym == sym1 else None)
|
|
4570
|
+
if dec is None:
|
|
4571
|
+
meta = _swap_asset_meta(w3, sym)
|
|
4572
|
+
if meta:
|
|
4573
|
+
dec = meta.get("decimals")
|
|
4574
|
+
if dec is None:
|
|
4575
|
+
continue
|
|
4576
|
+
inventory[sym] = [avail, dec, 0.0]
|
|
4577
|
+
return inventory
|
|
4578
|
+
|
|
4579
|
+
|
|
4372
4580
|
def _aero_use_all_available(
|
|
4373
4581
|
w3, acct, account, pa_cs, pool_cfg,
|
|
4374
4582
|
width_pct=2.0, slippage_pct=1.0, execute=False,
|
|
@@ -4390,34 +4598,9 @@ def _aero_use_all_available(
|
|
|
4390
4598
|
dec0, dec1 = pool_cfg["decimals0"], pool_cfg["decimals1"]
|
|
4391
4599
|
MIN_USD_VALUE = 5.0
|
|
4392
4600
|
|
|
4393
|
-
# 1. Inventory: check RedStone-fed assets + both pool tokens for balance
|
|
4394
|
-
|
|
4395
|
-
inventory =
|
|
4396
|
-
for sym in sorted(all_candidates):
|
|
4397
|
-
account_sym = _account_asset_symbol(sym)
|
|
4398
|
-
try:
|
|
4399
|
-
bal = account.functions.getBalance(asset_b32(account_sym)).call()
|
|
4400
|
-
except Exception:
|
|
4401
|
-
bal = 0
|
|
4402
|
-
if bal <= 0:
|
|
4403
|
-
continue
|
|
4404
|
-
try:
|
|
4405
|
-
locked = account.functions.getTotalIntentAmount(asset_b32(account_sym)).call()
|
|
4406
|
-
except Exception:
|
|
4407
|
-
locked = 0
|
|
4408
|
-
avail = bal - locked if bal > locked else 0
|
|
4409
|
-
if avail <= 0:
|
|
4410
|
-
continue
|
|
4411
|
-
dec = pool_cfg["decimals0"] if sym == sym0 else (
|
|
4412
|
-
pool_cfg["decimals1"] if sym == sym1 else None)
|
|
4413
|
-
if dec is None:
|
|
4414
|
-
meta = _swap_asset_meta(w3, sym)
|
|
4415
|
-
if meta:
|
|
4416
|
-
dec = meta.get("decimals")
|
|
4417
|
-
if dec is None:
|
|
4418
|
-
continue
|
|
4419
|
-
inventory[sym] = [avail, dec, 0.0]
|
|
4420
|
-
|
|
4601
|
+
# 1. Inventory: check RedStone-fed assets + both pool tokens for balance, batched into
|
|
4602
|
+
# one Multicall3 round-trip (see _aero_inventory_available).
|
|
4603
|
+
inventory = _aero_inventory_available(w3, account, pool_cfg)
|
|
4421
4604
|
if not inventory:
|
|
4422
4605
|
print(" No in-account assets found with non-zero balance.")
|
|
4423
4606
|
return False
|
|
@@ -5761,7 +5944,7 @@ def cmd_aero_rebalance_status(token_id: int = None, check: bool = False,
|
|
|
5761
5944
|
d["position"] = None
|
|
5762
5945
|
if check:
|
|
5763
5946
|
try:
|
|
5764
|
-
payload = build_redstone_payload(degen_account_price_feeds(account))
|
|
5947
|
+
payload = build_redstone_payload(degen_account_price_feeds(account, w3=w3))
|
|
5765
5948
|
d["shouldRebalance"] = bool(redstone_view_call(
|
|
5766
5949
|
w3, account, "shouldRebalance", payload, args=[tid])[0])
|
|
5767
5950
|
except Exception as e:
|
|
@@ -5926,34 +6109,9 @@ def _aero_rebuild_sweep(w3, acct, account, pa_cs, pool_key, pool_cfg=None, execu
|
|
|
5926
6109
|
sym0, sym1 = pool_cfg["symbol0"], pool_cfg["symbol1"]
|
|
5927
6110
|
MIN_USD_VALUE = 5.0
|
|
5928
6111
|
|
|
5929
|
-
# Inventory RedStone-priced assets
|
|
5930
|
-
|
|
5931
|
-
inventory =
|
|
5932
|
-
for sym in sorted(candidates):
|
|
5933
|
-
account_sym = _account_asset_symbol(sym)
|
|
5934
|
-
try:
|
|
5935
|
-
bal = account.functions.getBalance(asset_b32(account_sym)).call()
|
|
5936
|
-
except Exception:
|
|
5937
|
-
bal = 0
|
|
5938
|
-
if bal <= 0:
|
|
5939
|
-
continue
|
|
5940
|
-
try:
|
|
5941
|
-
locked = account.functions.getTotalIntentAmount(asset_b32(account_sym)).call()
|
|
5942
|
-
except Exception:
|
|
5943
|
-
locked = 0
|
|
5944
|
-
avail = bal - locked if bal > locked else 0
|
|
5945
|
-
if avail <= 0:
|
|
5946
|
-
continue
|
|
5947
|
-
dec = pool_cfg["decimals0"] if sym == sym0 else (
|
|
5948
|
-
pool_cfg["decimals1"] if sym == sym1 else None)
|
|
5949
|
-
if dec is None:
|
|
5950
|
-
meta = _swap_asset_meta(w3, sym)
|
|
5951
|
-
if meta:
|
|
5952
|
-
dec = meta.get("decimals")
|
|
5953
|
-
if dec is None:
|
|
5954
|
-
continue
|
|
5955
|
-
inventory[sym] = [avail, dec, 0.0]
|
|
5956
|
-
|
|
6112
|
+
# Inventory RedStone-priced assets + both pool tokens, batched into one Multicall3
|
|
6113
|
+
# round-trip (see _aero_inventory_available).
|
|
6114
|
+
inventory = _aero_inventory_available(w3, account, pool_cfg)
|
|
5957
6115
|
if not inventory:
|
|
5958
6116
|
return
|
|
5959
6117
|
|
|
@@ -1726,11 +1726,24 @@ def cmd_withdrawal_requests():
|
|
|
1726
1726
|
acct = get_account()
|
|
1727
1727
|
print(f"Wallet: {acct.address}")
|
|
1728
1728
|
any_pending = False
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1729
|
+
# Batch the per-pool balanceOf + getTotalIntentAmount + getUserIntents reads (was 3
|
|
1730
|
+
# sequential eth_calls per pool) into one Multicall3 round-trip.
|
|
1731
|
+
pool_meta = list(POOLS.items())
|
|
1732
|
+
legs = []
|
|
1733
|
+
for _pool_name, cfg in pool_meta:
|
|
1734
|
+
proxy_cs = Web3.to_checksum_address(cfg["proxy"])
|
|
1735
|
+
contract = w3.eth.contract(address=proxy_cs, abi=POOL_ABI)
|
|
1736
|
+
legs.append((proxy_cs, bytes.fromhex(contract.encode_abi("balanceOf", args=[acct.address])[2:])))
|
|
1737
|
+
legs.append((proxy_cs, bytes.fromhex(contract.encode_abi("getTotalIntentAmount", args=[acct.address])[2:])))
|
|
1738
|
+
legs.append((proxy_cs, bytes.fromhex(contract.encode_abi("getUserIntents", args=[acct.address])[2:])))
|
|
1739
|
+
results = multicall(w3, legs)
|
|
1740
|
+
for i, (pool_name, cfg) in enumerate(pool_meta):
|
|
1741
|
+
b_ok, b_rd = results[3 * i]
|
|
1742
|
+
t_ok, t_rd = results[3 * i + 1]
|
|
1743
|
+
u_ok, u_rd = results[3 * i + 2]
|
|
1744
|
+
balance = w3.codec.decode(["uint256"], b_rd)[0] if b_ok and b_rd else 0
|
|
1745
|
+
total_intent = w3.codec.decode(["uint256"], t_rd)[0] if t_ok and t_rd else 0
|
|
1746
|
+
intents = w3.codec.decode(["(uint256,uint256,uint256,bool,bool,bool)[]"], u_rd)[0] if u_ok and u_rd else []
|
|
1734
1747
|
if balance == 0 and not intents:
|
|
1735
1748
|
continue
|
|
1736
1749
|
dec = cfg["decimals"]
|
|
@@ -3492,12 +3505,24 @@ def cmd_withdrawal_intents():
|
|
|
3492
3505
|
return
|
|
3493
3506
|
|
|
3494
3507
|
any_pending = False
|
|
3508
|
+
# Batch the per-asset getAvailableBalance + getTotalIntentAmount + getUserIntents reads
|
|
3509
|
+
# (was 3 sequential eth_calls per owned asset) into one Multicall3 round-trip.
|
|
3510
|
+
pa_cs = account.address
|
|
3511
|
+
legs = []
|
|
3495
3512
|
for a in owned:
|
|
3513
|
+
legs.append((pa_cs, bytes.fromhex(account.encode_abi("getAvailableBalance", args=[a])[2:])))
|
|
3514
|
+
legs.append((pa_cs, bytes.fromhex(account.encode_abi("getTotalIntentAmount", args=[a])[2:])))
|
|
3515
|
+
legs.append((pa_cs, bytes.fromhex(account.encode_abi("getUserIntents", args=[a])[2:])))
|
|
3516
|
+
results = multicall(w3, legs)
|
|
3517
|
+
for i, a in enumerate(owned):
|
|
3496
3518
|
sym = a.rstrip(b"\x00").decode(errors="replace")
|
|
3497
3519
|
dec = _asset_decimals(sym)
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3520
|
+
av_ok, av_rd = results[3 * i]
|
|
3521
|
+
ti_ok, ti_rd = results[3 * i + 1]
|
|
3522
|
+
ui_ok, ui_rd = results[3 * i + 2]
|
|
3523
|
+
available = w3.codec.decode(["uint256"], av_rd)[0] if av_ok and av_rd else 0
|
|
3524
|
+
total_intent = w3.codec.decode(["uint256"], ti_rd)[0] if ti_ok and ti_rd else 0
|
|
3525
|
+
intents = w3.codec.decode(["(uint256,uint256,uint256,bool,bool,bool)[]"], ui_rd)[0] if ui_ok and ui_rd else []
|
|
3501
3526
|
print(f" {sym}: available {available / 10**dec:,.6f}, "
|
|
3502
3527
|
f"pending intents {total_intent / 10**dec:,.6f}")
|
|
3503
3528
|
for idx, (amt, actionable_at, expires_at, is_pending, is_actionable, is_expired) in enumerate(intents):
|
|
@@ -4473,13 +4498,33 @@ def cmd_lb_remove(pair_key: str, slippage_pct: float = 1.0, execute: bool = Fals
|
|
|
4473
4498
|
return
|
|
4474
4499
|
|
|
4475
4500
|
pair_c = _lb_pair_contract(w3, pair_cs)
|
|
4476
|
-
|
|
4501
|
+
# Batch getActiveId + per-bin (balanceOf + totalSupply + getBin) into one Multicall3
|
|
4502
|
+
# round-trip (was 1 + 3N sequential eth_calls). This is a WRITE path — the decoded
|
|
4503
|
+
# amounts feed removeLiquidity and the slippage floors — so a failed read aborts rather
|
|
4504
|
+
# than silently defaulting to 0 (matches the prior abort-on-error behaviour).
|
|
4505
|
+
legs = [(pair_cs, bytes.fromhex(pair_c.encode_abi("getActiveId", args=[])[2:]))]
|
|
4506
|
+
for binid in ids:
|
|
4507
|
+
legs.append((pair_cs, bytes.fromhex(pair_c.encode_abi("balanceOf", args=[pa_cs, binid])[2:])))
|
|
4508
|
+
legs.append((pair_cs, bytes.fromhex(pair_c.encode_abi("totalSupply", args=[binid])[2:])))
|
|
4509
|
+
legs.append((pair_cs, bytes.fromhex(pair_c.encode_abi("getBin", args=[binid])[2:])))
|
|
4510
|
+
results = multicall(w3, legs)
|
|
4511
|
+
a_ok, a_rd = results[0]
|
|
4512
|
+
if not (a_ok and a_rd):
|
|
4513
|
+
print(" Could not read the LB pair's active bin — refusing to remove.")
|
|
4514
|
+
return
|
|
4515
|
+
active_id = w3.codec.decode(["uint24"], a_rd)[0]
|
|
4477
4516
|
amounts = []
|
|
4478
4517
|
est_x = est_y = 0.0
|
|
4479
|
-
for binid in ids:
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4518
|
+
for k, binid in enumerate(ids):
|
|
4519
|
+
b_ok, b_rd = results[1 + 3 * k]
|
|
4520
|
+
t_ok, t_rd = results[1 + 3 * k + 1]
|
|
4521
|
+
g_ok, g_rd = results[1 + 3 * k + 2]
|
|
4522
|
+
if not (b_ok and b_rd and t_ok and t_rd and g_ok and g_rd):
|
|
4523
|
+
print(f" Could not read bin {binid} on [{pair_key}] — refusing to remove (incomplete data).")
|
|
4524
|
+
return
|
|
4525
|
+
bal = w3.codec.decode(["uint256"], b_rd)[0]
|
|
4526
|
+
ts = w3.codec.decode(["uint256"], t_rd)[0]
|
|
4527
|
+
rx, ry = w3.codec.decode(["uint128", "uint128"], g_rd)
|
|
4483
4528
|
amounts.append(bal)
|
|
4484
4529
|
share = (bal / ts) if ts else 0
|
|
4485
4530
|
est_x += rx * share / 10**x_cfg["decimals"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: primecli
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.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.11.
|
|
50
|
+
**Current version:** 0.11.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.11.
|
|
7
|
+
version = "0.11.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"
|
|
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
|
|
File without changes
|