primecli 0.11.2__tar.gz → 0.11.3__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.3}/PKG-INFO +2 -2
- {primecli-0.11.2 → primecli-0.11.3}/README.md +1 -1
- {primecli-0.11.2 → primecli-0.11.3}/primecli/arbprime.py +40 -8
- {primecli-0.11.2 → primecli-0.11.3}/primecli/degenprime.py +190 -73
- {primecli-0.11.2 → primecli-0.11.3}/primecli/deltaprime.py +58 -13
- {primecli-0.11.2 → primecli-0.11.3}/primecli.egg-info/PKG-INFO +2 -2
- {primecli-0.11.2 → primecli-0.11.3}/pyproject.toml +1 -1
- {primecli-0.11.2 → primecli-0.11.3}/LICENSE +0 -0
- {primecli-0.11.2 → primecli-0.11.3}/primecli/__init__.py +0 -0
- {primecli-0.11.2 → primecli-0.11.3}/primecli/_flowledger.py +0 -0
- {primecli-0.11.2 → primecli-0.11.3}/primecli/_wallets.py +0 -0
- {primecli-0.11.2 → primecli-0.11.3}/primecli/bridge.py +0 -0
- {primecli-0.11.2 → primecli-0.11.3}/primecli/health_monitor.py +0 -0
- {primecli-0.11.2 → primecli-0.11.3}/primecli.egg-info/SOURCES.txt +0 -0
- {primecli-0.11.2 → primecli-0.11.3}/primecli.egg-info/dependency_links.txt +0 -0
- {primecli-0.11.2 → primecli-0.11.3}/primecli.egg-info/entry_points.txt +0 -0
- {primecli-0.11.2 → primecli-0.11.3}/primecli.egg-info/requires.txt +0 -0
- {primecli-0.11.2 → primecli-0.11.3}/primecli.egg-info/top_level.txt +0 -0
- {primecli-0.11.2 → primecli-0.11.3}/setup.cfg +0 -0
- {primecli-0.11.2 → primecli-0.11.3}/tests/test_aero_range_and_swap_fallback.py +0 -0
- {primecli-0.11.2 → primecli-0.11.3}/tests/test_aero_rebalance.py +0 -0
- {primecli-0.11.2 → primecli-0.11.3}/tests/test_aero_v3_collision_fixes.py +0 -0
- {primecli-0.11.2 → primecli-0.11.3}/tests/test_bridge.py +0 -0
- {primecli-0.11.2 → primecli-0.11.3}/tests/test_cross_file_identity.py +0 -0
- {primecli-0.11.2 → primecli-0.11.3}/tests/test_flowledger_transferred_amount.py +0 -0
- {primecli-0.11.2 → primecli-0.11.3}/tests/test_gas_limit.py +0 -0
- {primecli-0.11.2 → primecli-0.11.3}/tests/test_gas_pricing.py +0 -0
- {primecli-0.11.2 → primecli-0.11.3}/tests/test_health_meter.py +0 -0
- {primecli-0.11.2 → primecli-0.11.3}/tests/test_health_monitor.py +0 -0
- {primecli-0.11.2 → primecli-0.11.3}/tests/test_paraswap_requote.py +0 -0
- {primecli-0.11.2 → primecli-0.11.3}/tests/test_paraswap_validator.py +0 -0
- {primecli-0.11.2 → primecli-0.11.3}/tests/test_redstone_encoding.py +0 -0
- {primecli-0.11.2 → primecli-0.11.3}/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.3
|
|
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.3 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.3 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)
|
|
@@ -3636,12 +3749,24 @@ def cmd_withdrawal_intents():
|
|
|
3636
3749
|
return
|
|
3637
3750
|
|
|
3638
3751
|
any_pending = False
|
|
3752
|
+
# Batch the per-asset getAvailableBalance + getTotalIntentAmount + getUserIntents reads
|
|
3753
|
+
# (was 3 sequential eth_calls per owned asset) into one Multicall3 round-trip.
|
|
3754
|
+
pa_cs = account.address
|
|
3755
|
+
legs = []
|
|
3639
3756
|
for a in owned:
|
|
3757
|
+
legs.append((pa_cs, bytes.fromhex(account.encode_abi("getAvailableBalance", args=[a])[2:])))
|
|
3758
|
+
legs.append((pa_cs, bytes.fromhex(account.encode_abi("getTotalIntentAmount", args=[a])[2:])))
|
|
3759
|
+
legs.append((pa_cs, bytes.fromhex(account.encode_abi("getUserIntents", args=[a])[2:])))
|
|
3760
|
+
results = multicall(w3, legs)
|
|
3761
|
+
for i, a in enumerate(owned):
|
|
3640
3762
|
sym = a.rstrip(b"\x00").decode(errors="replace")
|
|
3641
3763
|
dec = _asset_decimals(w3, sym)
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3764
|
+
av_ok, av_rd = results[3 * i]
|
|
3765
|
+
ti_ok, ti_rd = results[3 * i + 1]
|
|
3766
|
+
ui_ok, ui_rd = results[3 * i + 2]
|
|
3767
|
+
available = w3.codec.decode(["uint256"], av_rd)[0] if av_ok and av_rd else 0
|
|
3768
|
+
total_intent = w3.codec.decode(["uint256"], ti_rd)[0] if ti_ok and ti_rd else 0
|
|
3769
|
+
intents = w3.codec.decode(["(uint256,uint256,uint256,bool,bool,bool)[]"], ui_rd)[0] if ui_ok and ui_rd else []
|
|
3645
3770
|
print(f" {sym}: available {available / 10**dec:,.6f}, "
|
|
3646
3771
|
f"pending intents {total_intent / 10**dec:,.6f}")
|
|
3647
3772
|
for idx, (amt, actionable_at, expires_at, is_pending, is_actionable, is_expired) in enumerate(intents):
|
|
@@ -4369,6 +4494,48 @@ def _aero_separate_pool_and_sweeps(valuable: dict, sym0: str, sym1: str):
|
|
|
4369
4494
|
return pool0_bal_wei, pool1_bal_wei, sweeps
|
|
4370
4495
|
|
|
4371
4496
|
|
|
4497
|
+
def _aero_inventory_available(w3, account, pool_cfg):
|
|
4498
|
+
"""Inventory every REDSTONE-fed asset + both pool tokens held in the Degen Account, net
|
|
4499
|
+
of locked (withdrawal-intent) amounts. Batches the per-symbol getBalance +
|
|
4500
|
+
getTotalIntentAmount reads into ONE Multicall3 round-trip (was 2 sequential eth_calls per
|
|
4501
|
+
symbol — the documented cause of Base-converge RPC 429s). aggregate3 allowFailure=True
|
|
4502
|
+
preserves the old per-call tolerance: a reverting/failed leg reads 0, exactly as the
|
|
4503
|
+
previous per-call try/except did. Returns {sym: [avail_wei, decimals, 0.0]} for symbols
|
|
4504
|
+
with a positive available balance and resolvable decimals (empty dict if none)."""
|
|
4505
|
+
sym0, sym1 = pool_cfg["symbol0"], pool_cfg["symbol1"]
|
|
4506
|
+
candidates = sorted(set(REDSTONE_AVAILABLE_FEEDS) | {sym0, sym1})
|
|
4507
|
+
legs = []
|
|
4508
|
+
for sym in candidates:
|
|
4509
|
+
ab = asset_b32(_account_asset_symbol(sym))
|
|
4510
|
+
legs.append((account.address, bytes.fromhex(account.encode_abi("getBalance", args=[ab])[2:])))
|
|
4511
|
+
legs.append((account.address, bytes.fromhex(account.encode_abi("getTotalIntentAmount", args=[ab])[2:])))
|
|
4512
|
+
try:
|
|
4513
|
+
results = multicall(w3, legs)
|
|
4514
|
+
except Exception:
|
|
4515
|
+
results = [(False, b"")] * len(legs)
|
|
4516
|
+
inventory = {}
|
|
4517
|
+
for i, sym in enumerate(candidates):
|
|
4518
|
+
b_ok, b_rd = results[2 * i]
|
|
4519
|
+
l_ok, l_rd = results[2 * i + 1]
|
|
4520
|
+
bal = w3.codec.decode(["uint256"], b_rd)[0] if b_ok and b_rd else 0
|
|
4521
|
+
if bal <= 0:
|
|
4522
|
+
continue
|
|
4523
|
+
locked = w3.codec.decode(["uint256"], l_rd)[0] if l_ok and l_rd else 0
|
|
4524
|
+
avail = bal - locked if bal > locked else 0
|
|
4525
|
+
if avail <= 0:
|
|
4526
|
+
continue
|
|
4527
|
+
dec = pool_cfg["decimals0"] if sym == sym0 else (
|
|
4528
|
+
pool_cfg["decimals1"] if sym == sym1 else None)
|
|
4529
|
+
if dec is None:
|
|
4530
|
+
meta = _swap_asset_meta(w3, sym)
|
|
4531
|
+
if meta:
|
|
4532
|
+
dec = meta.get("decimals")
|
|
4533
|
+
if dec is None:
|
|
4534
|
+
continue
|
|
4535
|
+
inventory[sym] = [avail, dec, 0.0]
|
|
4536
|
+
return inventory
|
|
4537
|
+
|
|
4538
|
+
|
|
4372
4539
|
def _aero_use_all_available(
|
|
4373
4540
|
w3, acct, account, pa_cs, pool_cfg,
|
|
4374
4541
|
width_pct=2.0, slippage_pct=1.0, execute=False,
|
|
@@ -4390,34 +4557,9 @@ def _aero_use_all_available(
|
|
|
4390
4557
|
dec0, dec1 = pool_cfg["decimals0"], pool_cfg["decimals1"]
|
|
4391
4558
|
MIN_USD_VALUE = 5.0
|
|
4392
4559
|
|
|
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
|
-
|
|
4560
|
+
# 1. Inventory: check RedStone-fed assets + both pool tokens for balance, batched into
|
|
4561
|
+
# one Multicall3 round-trip (see _aero_inventory_available).
|
|
4562
|
+
inventory = _aero_inventory_available(w3, account, pool_cfg)
|
|
4421
4563
|
if not inventory:
|
|
4422
4564
|
print(" No in-account assets found with non-zero balance.")
|
|
4423
4565
|
return False
|
|
@@ -5761,7 +5903,7 @@ def cmd_aero_rebalance_status(token_id: int = None, check: bool = False,
|
|
|
5761
5903
|
d["position"] = None
|
|
5762
5904
|
if check:
|
|
5763
5905
|
try:
|
|
5764
|
-
payload = build_redstone_payload(degen_account_price_feeds(account))
|
|
5906
|
+
payload = build_redstone_payload(degen_account_price_feeds(account, w3=w3))
|
|
5765
5907
|
d["shouldRebalance"] = bool(redstone_view_call(
|
|
5766
5908
|
w3, account, "shouldRebalance", payload, args=[tid])[0])
|
|
5767
5909
|
except Exception as e:
|
|
@@ -5926,34 +6068,9 @@ def _aero_rebuild_sweep(w3, acct, account, pa_cs, pool_key, pool_cfg=None, execu
|
|
|
5926
6068
|
sym0, sym1 = pool_cfg["symbol0"], pool_cfg["symbol1"]
|
|
5927
6069
|
MIN_USD_VALUE = 5.0
|
|
5928
6070
|
|
|
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
|
-
|
|
6071
|
+
# Inventory RedStone-priced assets + both pool tokens, batched into one Multicall3
|
|
6072
|
+
# round-trip (see _aero_inventory_available).
|
|
6073
|
+
inventory = _aero_inventory_available(w3, account, pool_cfg)
|
|
5957
6074
|
if not inventory:
|
|
5958
6075
|
return
|
|
5959
6076
|
|
|
@@ -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.3
|
|
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.3 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.3"
|
|
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
|