primecli 0.5.9__tar.gz → 0.6.0__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.5.9 → primecli-0.6.0}/PKG-INFO +1 -1
- {primecli-0.5.9 → primecli-0.6.0}/primecli/degenprime.py +676 -25
- {primecli-0.5.9 → primecli-0.6.0}/primecli.egg-info/PKG-INFO +1 -1
- {primecli-0.5.9 → primecli-0.6.0}/pyproject.toml +1 -1
- {primecli-0.5.9 → primecli-0.6.0}/LICENSE +0 -0
- {primecli-0.5.9 → primecli-0.6.0}/README.md +0 -0
- {primecli-0.5.9 → primecli-0.6.0}/primecli/__init__.py +0 -0
- {primecli-0.5.9 → primecli-0.6.0}/primecli/arbprime.py +0 -0
- {primecli-0.5.9 → primecli-0.6.0}/primecli/deltaprime.py +0 -0
- {primecli-0.5.9 → primecli-0.6.0}/primecli/health_monitor.py +0 -0
- {primecli-0.5.9 → primecli-0.6.0}/primecli.egg-info/SOURCES.txt +0 -0
- {primecli-0.5.9 → primecli-0.6.0}/primecli.egg-info/dependency_links.txt +0 -0
- {primecli-0.5.9 → primecli-0.6.0}/primecli.egg-info/entry_points.txt +0 -0
- {primecli-0.5.9 → primecli-0.6.0}/primecli.egg-info/requires.txt +0 -0
- {primecli-0.5.9 → primecli-0.6.0}/primecli.egg-info/top_level.txt +0 -0
- {primecli-0.5.9 → primecli-0.6.0}/setup.cfg +0 -0
- {primecli-0.5.9 → primecli-0.6.0}/tests/test_cross_file_identity.py +0 -0
- {primecli-0.5.9 → primecli-0.6.0}/tests/test_gas_pricing.py +0 -0
- {primecli-0.5.9 → primecli-0.6.0}/tests/test_health_monitor.py +0 -0
- {primecli-0.5.9 → primecli-0.6.0}/tests/test_paraswap_validator.py +0 -0
- {primecli-0.5.9 → primecli-0.6.0}/tests/test_redstone_encoding.py +0 -0
- {primecli-0.5.9 → primecli-0.6.0}/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.
|
|
3
|
+
Version: 0.6.0
|
|
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
|
|
@@ -26,6 +26,9 @@ Usage:
|
|
|
26
26
|
degenprime execute-withdrawal --pool usdc [--index N] [--execute]
|
|
27
27
|
degenprime cancel-withdrawal --pool usdc --index N [--execute]
|
|
28
28
|
degenprime aerodrome-positions
|
|
29
|
+
degenprime aero-add-liquidity --pool weth-usdc-100 --amount-weth 0.05 --amount-usdc 100 [--slippage 1] [--execute]
|
|
30
|
+
degenprime aero-remove-liquidity --token-id N [--percentage 100] [--execute]
|
|
31
|
+
degenprime aero-collect-fees --token-id N [--execute]
|
|
29
32
|
|
|
30
33
|
Configuration (env vars):
|
|
31
34
|
DEGENPRIME_PRIVATE_KEY Raw 0x... private key for the signer. Falls back to
|
|
@@ -72,10 +75,15 @@ it borrows --amount of the NEW debt asset (--to), ParaSwaps it into the OLD debt
|
|
|
72
75
|
asset (--from), and repays the old debt. --from is the existing debt being
|
|
73
76
|
refinanced; --to is the new debt taken on. RedStone-gated on execute.
|
|
74
77
|
|
|
75
|
-
aerodrome-positions is read-only: lists
|
|
76
|
-
owns/
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
aerodrome-positions is read-only: lists each Aerodrome Slipstream (CL) NFT position the
|
|
79
|
+
Degen Account owns/stakes, showing token0/token1/tick range/liquidity via the diamond's
|
|
80
|
+
getOwnedStakedAerodromeTokenIds + getPositionCompositionSimplified views.
|
|
81
|
+
|
|
82
|
+
aero-add-liquidity / aero-remove-liquidity / aero-collect-fees provide write paths to
|
|
83
|
+
the Aerodrome Slipstream NonfungiblePositionManager through the Degen Account's
|
|
84
|
+
AerodromeFacet wrapper functions. The facet selectors were determined via on-chain
|
|
85
|
+
probing (Diamond Loupe) of the smart-loan diamond; function names are inferred from
|
|
86
|
+
their parameter layouts and revert signatures.
|
|
79
87
|
"""
|
|
80
88
|
|
|
81
89
|
import json, os, sys, time, base64
|
|
@@ -185,6 +193,63 @@ TOKEN_MANAGER = "0x97e74e0A3D2713D87E3fBf6d18F869042F0d0116"
|
|
|
185
193
|
# Base native wrapped (used by the weth pool's native ETH path).
|
|
186
194
|
WETH = "0x4200000000000000000000000000000000000006"
|
|
187
195
|
|
|
196
|
+
# ── Aerodrome Slipstream (CL) ────────────────────────────────────────────────
|
|
197
|
+
# Verified on Base: DeployCL-Base.json output from aerodrome-finance/slipstream.
|
|
198
|
+
# The NonfungiblePositionManager wraps Uniswap V3-style concentrated liquidity
|
|
199
|
+
# positions as ERC-721 NFTs. The Degen Account's AerodromeFacet (diamond facet
|
|
200
|
+
# #14 at 0x3c0ddb23) passes calldata through with remainsSolvent checks.
|
|
201
|
+
AERODROME_NPM = "0x827922686190790b37229fd06084350E74485b72"
|
|
202
|
+
|
|
203
|
+
# Selectors extracted from the diamond via DiamondLoupe.facets() on the
|
|
204
|
+
# smart-loan diamond beacon. Function names are inferred from parmeter layouts
|
|
205
|
+
# and revert signatures (on-chain probing 2026-06-05).
|
|
206
|
+
#
|
|
207
|
+
# Known facet #14 selectors:
|
|
208
|
+
# 6f2845cd getOwnedStakedAerodromeTokenIds()
|
|
209
|
+
# b6626971 getPositionCompositionSimplified(uint256) -> (address,address,uint256,uint256)
|
|
210
|
+
# 121350b3 (view, takes uint256 — detailed position info, unknown return)
|
|
211
|
+
# 27bed82e (write, onlyOwner, takes MintParams-like tuple — inferred mint/add)
|
|
212
|
+
# 2c710777 (write, onlyOwner, takes IncreaseLiquidityParams-like tuple — inferred increase)
|
|
213
|
+
# ca15558b (write, takes DecreaseLiquidityParams tuple 5-field — matches decreaseLiquidity)
|
|
214
|
+
# 92b5a47e (write, takes uint256 tokenId, checks position exists — burn/collect)
|
|
215
|
+
# 46daca2c (write, onlyOwnerOrLiquidator — emergency withdrawal)
|
|
216
|
+
AERODROME_SEL_MINT = bytes.fromhex("27bed82e") # inferred: mint/add
|
|
217
|
+
AERODROME_SEL_INCREASE = bytes.fromhex("2c710777") # inferred: increaseLiquidity
|
|
218
|
+
AERODROME_SEL_DECREASE = bytes.fromhex("ca15558b") # inferred: decreaseLiquidity
|
|
219
|
+
AERODROME_SEL_BURN = bytes.fromhex("92b5a47e") # inferred: burn
|
|
220
|
+
AERODROME_SEL_COLLECT = bytes.fromhex("92b5a47e") # same as burn (inferred)
|
|
221
|
+
|
|
222
|
+
# Whitelisted Aerodrome CL pools exposed as tool keys. Each key carries the known
|
|
223
|
+
# token pair, fee tier (tickSpacing in bps), and the expected token addresses.
|
|
224
|
+
# tickSpacing maps: 1 = 0.01%, 5 = 0.05%, 10 = 0.1%, 30 = 0.3%, 100 = 1%.
|
|
225
|
+
# Most pools use the canonical (WETH/stablecoin) ordering from the CL Factory.
|
|
226
|
+
AERODROME_POOLS = {
|
|
227
|
+
"weth-usdc-100": {"token0": "0x4200000000000000000000000000000000000006",
|
|
228
|
+
"token1": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
229
|
+
"tickSpacing": 100, "symbol0": "ETH", "symbol1": "USDC",
|
|
230
|
+
"decimals0": 18, "decimals1": 6},
|
|
231
|
+
"weth-usdc-5": {"token0": "0x4200000000000000000000000000000000000006",
|
|
232
|
+
"token1": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
233
|
+
"tickSpacing": 5, "symbol0": "ETH", "symbol1": "USDC",
|
|
234
|
+
"decimals0": 18, "decimals1": 6},
|
|
235
|
+
"weth-cbbtc-100": {"token0": "0x4200000000000000000000000000000000000006",
|
|
236
|
+
"token1": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
|
|
237
|
+
"tickSpacing": 100, "symbol0": "ETH", "symbol1": "cbBTC",
|
|
238
|
+
"decimals0": 18, "decimals1": 8},
|
|
239
|
+
"weth-cbbtc-30": {"token0": "0x4200000000000000000000000000000000000006",
|
|
240
|
+
"token1": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
|
|
241
|
+
"tickSpacing": 30, "symbol0": "ETH", "symbol1": "cbBTC",
|
|
242
|
+
"decimals0": 18, "decimals1": 8},
|
|
243
|
+
"weth-aero-200": {"token0": "0x940181a94A35A4569E4529A3CDfB74e38FD98631",
|
|
244
|
+
"token1": "0x4200000000000000000000000000000000000006",
|
|
245
|
+
"tickSpacing": 200, "symbol0": "AERO", "symbol1": "ETH",
|
|
246
|
+
"decimals0": 18, "decimals1": 18},
|
|
247
|
+
"aero-usdc-100": {"token0": "0x940181a94A35A4569E4529A3CDfB74e38FD98631",
|
|
248
|
+
"token1": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
249
|
+
"tickSpacing": 100, "symbol0": "AERO", "symbol1": "USDC",
|
|
250
|
+
"decimals0": 18, "decimals1": 6},
|
|
251
|
+
}
|
|
252
|
+
|
|
188
253
|
# ParaSwap v6 / Velora aggregator on Base. The Degen Account's ParaSwapFacet.paraSwapV6
|
|
189
254
|
# and SwapDebtFacet.swapDebtParaSwap call this Augustus router with API-built calldata.
|
|
190
255
|
# The router address is shared across chains (v6 unified). The facet only decodes two
|
|
@@ -248,8 +313,8 @@ POOLS = {
|
|
|
248
313
|
# symbols from BaseOracle TWAP internally, so we filter the payload to only the symbols
|
|
249
314
|
# the gateway actually has feeds for. Probed against the gateway 2026-05-29.
|
|
250
315
|
REDSTONE_AVAILABLE_FEEDS = {
|
|
251
|
-
"USDC", "ETH", "
|
|
252
|
-
"
|
|
316
|
+
"USDC", "ETH", "BTC", "AERO", "BRETT", "KAITO", "DEGEN",
|
|
317
|
+
"EUROC", "USDT", "LBTC", "LTC", "DOGE", "XRP",
|
|
253
318
|
}
|
|
254
319
|
|
|
255
320
|
_abi_cache = {}
|
|
@@ -515,11 +580,72 @@ PRIME_ACCOUNT_ABI = [
|
|
|
515
580
|
"outputs": [{"type": "uint256"}], "stateMutability": "view", "type": "function"},
|
|
516
581
|
{"inputs": [{"name": "_asset", "type": "bytes32"}], "name": "getTotalIntentAmount",
|
|
517
582
|
"outputs": [{"type": "uint256"}], "stateMutability": "view", "type": "function"},
|
|
518
|
-
# Aerodrome
|
|
519
|
-
#
|
|
520
|
-
#
|
|
583
|
+
# Aerodrome facet (Facet 14 @ 0x3c0ddb23). Selectors extracted via DiamondLoupe
|
|
584
|
+
# on-chain probing 2026-06-05; function names inferred from parameter layouts and
|
|
585
|
+
# revert signatures. All write paths carry remainsSolvent or onlyOwner —
|
|
586
|
+
# RedStone-gated on --execute.
|
|
521
587
|
{"inputs": [], "name": "getOwnedStakedAerodromeTokenIds",
|
|
522
588
|
"outputs": [{"type": "uint256[]"}], "stateMutability": "view", "type": "function"},
|
|
589
|
+
# getPositionCompositionSimplified returns (address token0, address token1,
|
|
590
|
+
# uint256 tickData, uint256 liquidity) — tickData packs tickLower & tickUpper.
|
|
591
|
+
# Return type was verified via raw eth_call decode 2026-06-05.
|
|
592
|
+
{"inputs": [{"name": "tokenId", "type": "uint256"}],
|
|
593
|
+
"name": "getPositionCompositionSimplified",
|
|
594
|
+
"outputs": [{"name": "token0", "type": "address"},
|
|
595
|
+
{"name": "token1", "type": "address"},
|
|
596
|
+
{"name": "tickData", "type": "uint256"},
|
|
597
|
+
{"name": "liquidity", "type": "uint256"}],
|
|
598
|
+
"stateMutability": "view", "type": "function"},
|
|
599
|
+
# Write functions — using raw selectors with inferred parameter layouts.
|
|
600
|
+
# These take the same struct types as the Aerodrome NonfungiblePositionManager
|
|
601
|
+
# but are wrapped by the facet for solvency checks and owner validation.
|
|
602
|
+
#
|
|
603
|
+
# mintAerodrome / addLiquidityAerodrome: wraps NPM.mint(MintParams).
|
|
604
|
+
# MintParams = (address token0, address token1, int24 tickSpacing,
|
|
605
|
+
# int24 tickLower, int24 tickUpper, uint256 amount0Desired,
|
|
606
|
+
# uint256 amount1Desired, uint256 amount0Min, uint256 amount1Min,
|
|
607
|
+
# address recipient, uint256 deadline, uint160 sqrtPriceX96).
|
|
608
|
+
# Selector: 0x27bed82e (probed — onlyOwner, accepts MintParams-like encoding).
|
|
609
|
+
{"inputs": [{"name": "params", "type": "tuple", "components": [
|
|
610
|
+
{"name": "token0", "type": "address"},
|
|
611
|
+
{"name": "token1", "type": "address"},
|
|
612
|
+
{"name": "tickSpacing", "type": "int24"},
|
|
613
|
+
{"name": "tickLower", "type": "int24"},
|
|
614
|
+
{"name": "tickUpper", "type": "int24"},
|
|
615
|
+
{"name": "amount0Desired", "type": "uint256"},
|
|
616
|
+
{"name": "amount1Desired", "type": "uint256"},
|
|
617
|
+
{"name": "amount0Min", "type": "uint256"},
|
|
618
|
+
{"name": "amount1Min", "type": "uint256"},
|
|
619
|
+
{"name": "recipient", "type": "address"},
|
|
620
|
+
{"name": "deadline", "type": "uint256"},
|
|
621
|
+
{"name": "sqrtPriceX96", "type": "uint160"}
|
|
622
|
+
]}],
|
|
623
|
+
"name": "mintAerodrome", "outputs": [],
|
|
624
|
+
"stateMutability": "nonpayable", "type": "function"},
|
|
625
|
+
# decreaseAerodromeLiquidity: wraps NPM.decreaseLiquidity(DecreaseLiquidityParams).
|
|
626
|
+
# DecreaseLiquidityParams = (uint256 tokenId, uint128 liquidity,
|
|
627
|
+
# uint256 amount0Min, uint256 amount1Min, uint256 deadline).
|
|
628
|
+
# Selector: 0xca15558b (probed — accepts 5-field tuple matching decrease layout).
|
|
629
|
+
{"inputs": [{"name": "params", "type": "tuple", "components": [
|
|
630
|
+
{"name": "tokenId", "type": "uint256"},
|
|
631
|
+
{"name": "liquidity", "type": "uint128"},
|
|
632
|
+
{"name": "amount0Min", "type": "uint256"},
|
|
633
|
+
{"name": "amount1Min", "type": "uint256"},
|
|
634
|
+
{"name": "deadline", "type": "uint256"}
|
|
635
|
+
]}],
|
|
636
|
+
"name": "decreaseAerodromeLiquidity", "outputs": [],
|
|
637
|
+
"stateMutability": "nonpayable", "type": "function"},
|
|
638
|
+
# burnAerodromePosition: wraps NPM.burn(uint256). tokenId must have 0 liquidity
|
|
639
|
+
# and all fees collected.
|
|
640
|
+
# Selector: 0x92b5a47e (probed — takes uint256, checks position exists via NPM.positions).
|
|
641
|
+
{"inputs": [{"name": "tokenId", "type": "uint256"}],
|
|
642
|
+
"name": "burnAerodromePosition", "outputs": [],
|
|
643
|
+
"stateMutability": "nonpayable", "type": "function"},
|
|
644
|
+
# collectAerodromeFees: also uses selector 0x92b5a47e — same as burn when the
|
|
645
|
+
# facet distinguishes by internal logic. For the tool we expose a dedicated path.
|
|
646
|
+
{"inputs": [{"name": "tokenId", "type": "uint256"}],
|
|
647
|
+
"name": "collectAerodromeFees", "outputs": [],
|
|
648
|
+
"stateMutability": "nonpayable", "type": "function"},
|
|
523
649
|
]
|
|
524
650
|
|
|
525
651
|
# TokenManager ABI - minimal subset for symbol/decimals lookups + supported tokens
|
|
@@ -544,6 +670,84 @@ ERC20_ABI = json.loads(
|
|
|
544
670
|
'{"constant":true,"inputs":[],"name":"decimals","outputs":[{"type":"uint8"}],"stateMutability":"view","type":"function"}]'
|
|
545
671
|
)
|
|
546
672
|
|
|
673
|
+
# Aerodrome Slipstream NonfungiblePositionManager ABI (Uniswap V3 compatible).
|
|
674
|
+
# Source: aerodrome-finance/slipstream INonfungiblePositionManager.sol.
|
|
675
|
+
# The facet functions on the Degen Account diamond wrap these NPM calls with
|
|
676
|
+
# solvency checks (remainsSolvent) and owner validation.
|
|
677
|
+
AERODROME_NPM_ABI = [
|
|
678
|
+
# ── Read ──
|
|
679
|
+
{"inputs": [{"name": "tokenId", "type": "uint256"}], "name": "positions",
|
|
680
|
+
"outputs": [
|
|
681
|
+
{"name": "nonce", "type": "uint96"},
|
|
682
|
+
{"name": "operator", "type": "address"},
|
|
683
|
+
{"name": "token0", "type": "address"},
|
|
684
|
+
{"name": "token1", "type": "address"},
|
|
685
|
+
{"name": "tickSpacing", "type": "int24"},
|
|
686
|
+
{"name": "tickLower", "type": "int24"},
|
|
687
|
+
{"name": "tickUpper", "type": "int24"},
|
|
688
|
+
{"name": "liquidity", "type": "uint128"},
|
|
689
|
+
{"name": "feeGrowthInside0LastX128", "type": "uint256"},
|
|
690
|
+
{"name": "feeGrowthInside1LastX128", "type": "uint256"},
|
|
691
|
+
{"name": "tokensOwed0", "type": "uint128"},
|
|
692
|
+
{"name": "tokensOwed1", "type": "uint128"}
|
|
693
|
+
], "stateMutability": "view", "type": "function"},
|
|
694
|
+
{"inputs": [{"name": "tokenId", "type": "uint256"}], "name": "ownerOf",
|
|
695
|
+
"outputs": [{"name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
|
|
696
|
+
# ── Write ──
|
|
697
|
+
{"inputs": [{"name": "params", "type": "tuple", "components": [
|
|
698
|
+
{"name": "token0", "type": "address"},
|
|
699
|
+
{"name": "token1", "type": "address"},
|
|
700
|
+
{"name": "tickSpacing", "type": "int24"},
|
|
701
|
+
{"name": "tickLower", "type": "int24"},
|
|
702
|
+
{"name": "tickUpper", "type": "int24"},
|
|
703
|
+
{"name": "amount0Desired", "type": "uint256"},
|
|
704
|
+
{"name": "amount1Desired", "type": "uint256"},
|
|
705
|
+
{"name": "amount0Min", "type": "uint256"},
|
|
706
|
+
{"name": "amount1Min", "type": "uint256"},
|
|
707
|
+
{"name": "recipient", "type": "address"},
|
|
708
|
+
{"name": "deadline", "type": "uint256"},
|
|
709
|
+
{"name": "sqrtPriceX96", "type": "uint160"}
|
|
710
|
+
]}],
|
|
711
|
+
"name": "mint", "outputs": [{"name": "tokenId", "type": "uint256"},
|
|
712
|
+
{"name": "liquidity", "type": "uint128"},
|
|
713
|
+
{"name": "amount0", "type": "uint256"},
|
|
714
|
+
{"name": "amount1", "type": "uint256"}],
|
|
715
|
+
"stateMutability": "payable", "type": "function"},
|
|
716
|
+
{"inputs": [{"name": "params", "type": "tuple", "components": [
|
|
717
|
+
{"name": "tokenId", "type": "uint256"},
|
|
718
|
+
{"name": "amount0Desired", "type": "uint256"},
|
|
719
|
+
{"name": "amount1Desired", "type": "uint256"},
|
|
720
|
+
{"name": "amount0Min", "type": "uint256"},
|
|
721
|
+
{"name": "amount1Min", "type": "uint256"},
|
|
722
|
+
{"name": "deadline", "type": "uint256"}
|
|
723
|
+
]}],
|
|
724
|
+
"name": "increaseLiquidity", "outputs": [{"name": "liquidity", "type": "uint128"},
|
|
725
|
+
{"name": "amount0", "type": "uint256"},
|
|
726
|
+
{"name": "amount1", "type": "uint256"}],
|
|
727
|
+
"stateMutability": "payable", "type": "function"},
|
|
728
|
+
{"inputs": [{"name": "params", "type": "tuple", "components": [
|
|
729
|
+
{"name": "tokenId", "type": "uint256"},
|
|
730
|
+
{"name": "liquidity", "type": "uint128"},
|
|
731
|
+
{"name": "amount0Min", "type": "uint256"},
|
|
732
|
+
{"name": "amount1Min", "type": "uint256"},
|
|
733
|
+
{"name": "deadline", "type": "uint256"}
|
|
734
|
+
]}],
|
|
735
|
+
"name": "decreaseLiquidity", "outputs": [{"name": "amount0", "type": "uint256"},
|
|
736
|
+
{"name": "amount1", "type": "uint256"}],
|
|
737
|
+
"stateMutability": "payable", "type": "function"},
|
|
738
|
+
{"inputs": [{"name": "params", "type": "tuple", "components": [
|
|
739
|
+
{"name": "tokenId", "type": "uint256"},
|
|
740
|
+
{"name": "recipient", "type": "address"},
|
|
741
|
+
{"name": "amount0Max", "type": "uint128"},
|
|
742
|
+
{"name": "amount1Max", "type": "uint128"}
|
|
743
|
+
]}],
|
|
744
|
+
"name": "collect", "outputs": [{"name": "amount0", "type": "uint256"},
|
|
745
|
+
{"name": "amount1", "type": "uint256"}],
|
|
746
|
+
"stateMutability": "payable", "type": "function"},
|
|
747
|
+
{"inputs": [{"name": "tokenId", "type": "uint256"}], "name": "burn",
|
|
748
|
+
"outputs": [], "stateMutability": "payable", "type": "function"},
|
|
749
|
+
]
|
|
750
|
+
|
|
547
751
|
def get_factory_contract(w3):
|
|
548
752
|
"""SmartLoansFactory - hand-curated ABI (same shape as DeltaPrime's factory)."""
|
|
549
753
|
return w3.eth.contract(address=Web3.to_checksum_address(FACTORY_PROXY), abi=FACTORY_ABI)
|
|
@@ -756,6 +960,26 @@ def build_redstone_payload(symbols: list) -> bytes:
|
|
|
756
960
|
payload = b"".join(packages)
|
|
757
961
|
payload += len(packages).to_bytes(2, "big")
|
|
758
962
|
payload += (0).to_bytes(3, "big")
|
|
963
|
+
# RedStone v0.9 format: signed metadata (timestamp, version, dataServiceId)
|
|
964
|
+
# Format from Bruno's working tx: threshold byte is the first digit of the timestamp,
|
|
965
|
+
# then the rest of the metadata follows without the initial digit.
|
|
966
|
+
ts_ms = 0
|
|
967
|
+
for sym in symbols:
|
|
968
|
+
mapped = _redstone_data_feed_id(sym)
|
|
969
|
+
feed_packages = gateway.get(mapped)
|
|
970
|
+
if feed_packages:
|
|
971
|
+
ts_ms = feed_packages[0].get("timestampMilliseconds", 0)
|
|
972
|
+
if ts_ms:
|
|
973
|
+
break
|
|
974
|
+
if not ts_ms:
|
|
975
|
+
ts_ms = int(time.time() * 1000)
|
|
976
|
+
ts_str = str(ts_ms)
|
|
977
|
+
# Threshold byte = first digit of timestamp (as ASCII)
|
|
978
|
+
payload += bytes([ord(ts_str[0])])
|
|
979
|
+
# Metadata = rest of timestamp + version + data service ID + null terminator
|
|
980
|
+
signed_metadata = f"{ts_str[1:]}#0.9.0#{REDSTONE_DATA_SERVICE}\0".encode()
|
|
981
|
+
payload += signed_metadata
|
|
982
|
+
payload += len(signed_metadata).to_bytes(2, "big")
|
|
759
983
|
payload += REDSTONE_MARKER
|
|
760
984
|
return payload
|
|
761
985
|
|
|
@@ -2329,10 +2553,18 @@ def cmd_withdraw_collateral(pool_name: str, amount: float, execute: bool = False
|
|
|
2329
2553
|
print("Run with --execute to broadcast (registers the intent on-chain).")
|
|
2330
2554
|
return
|
|
2331
2555
|
|
|
2332
|
-
|
|
2333
|
-
|
|
2556
|
+
# createWithdrawalIntent on Base is RedStone-gated (on-chain solvency at create time).
|
|
2557
|
+
# The solvency check prices EVERY registered collateral type, not just owned assets.
|
|
2558
|
+
# Include all available feeds — same as the DegenPrime UI on mainnet.
|
|
2559
|
+
feeds = sorted(REDSTONE_AVAILABLE_FEEDS)
|
|
2560
|
+
payload = build_redstone_payload(feeds)
|
|
2561
|
+
base_calldata = account.encode_abi("createWithdrawalIntent", args=[asset_b32(symbol), amount_wei])
|
|
2562
|
+
data = base_calldata + payload.hex()
|
|
2563
|
+
tx = {
|
|
2564
|
+
"from": acct.address, "to": pa_cs, "data": data,
|
|
2565
|
+
"nonce": w3.eth.get_transaction_count(acct.address),
|
|
2334
2566
|
"gas": 1000000, "chainId": CHAIN_ID,
|
|
2335
|
-
}
|
|
2567
|
+
}
|
|
2336
2568
|
_set_gas_price(w3, tx)
|
|
2337
2569
|
signed = acct.sign_transaction(tx)
|
|
2338
2570
|
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
@@ -2496,10 +2728,17 @@ def cmd_cancel_withdrawal(pool_name: str, index: int, execute: bool = False):
|
|
|
2496
2728
|
print("Run with --execute to broadcast.")
|
|
2497
2729
|
return
|
|
2498
2730
|
|
|
2499
|
-
|
|
2500
|
-
|
|
2731
|
+
# cancelWithdrawalIntent on Base is also RedStone-gated (DegenPrime diamond requires
|
|
2732
|
+
# solvency payload on all state-changing facet calls). Include all available feeds.
|
|
2733
|
+
feeds = sorted(REDSTONE_AVAILABLE_FEEDS)
|
|
2734
|
+
payload = build_redstone_payload(feeds)
|
|
2735
|
+
base_calldata = account.encode_abi("cancelWithdrawalIntent", args=[asset_b32(symbol), index])
|
|
2736
|
+
data = base_calldata + payload.hex()
|
|
2737
|
+
tx = {
|
|
2738
|
+
"from": acct.address, "to": pa_cs, "data": data,
|
|
2739
|
+
"nonce": w3.eth.get_transaction_count(acct.address),
|
|
2501
2740
|
"gas": 1000000, "chainId": CHAIN_ID,
|
|
2502
|
-
}
|
|
2741
|
+
}
|
|
2503
2742
|
_set_gas_price(w3, tx)
|
|
2504
2743
|
signed = acct.sign_transaction(tx)
|
|
2505
2744
|
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
@@ -2508,15 +2747,12 @@ def cmd_cancel_withdrawal(pool_name: str, index: int, execute: bool = False):
|
|
|
2508
2747
|
print(f"{'✓' if ok else '✗'} Cancel withdrawal intent [{index}] {'confirmed' if ok else 'failed'}")
|
|
2509
2748
|
print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
|
|
2510
2749
|
|
|
2511
|
-
# ─── Aerodrome (
|
|
2750
|
+
# ─── Aerodrome (Slipstream CL) ──────────────────────────────────────────────
|
|
2512
2751
|
|
|
2513
2752
|
def cmd_aerodrome_positions():
|
|
2514
|
-
"""Read-only: list
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
version and need per-market probing before broadcasting. Position composition
|
|
2518
|
-
(per-token amounts) needs the getPositionCompositionSimplified return shape, which
|
|
2519
|
-
we don't decode in v1; just listing IDs keeps this safe and useful."""
|
|
2753
|
+
"""Read-only: list every Aerodrome Slipstream NFT position the Degen Account
|
|
2754
|
+
owns/stakes, showing token pair, tick range, and liquidity from the diamond's
|
|
2755
|
+
getOwnedStakedAerodromeTokenIds + getPositionCompositionSimplified views."""
|
|
2520
2756
|
w3 = get_w3()
|
|
2521
2757
|
acct = get_account()
|
|
2522
2758
|
print(f"Wallet: {acct.address}")
|
|
@@ -2534,10 +2770,386 @@ def cmd_aerodrome_positions():
|
|
|
2534
2770
|
if not ids:
|
|
2535
2771
|
print(" No Aerodrome positions (owned/staked tokenIds).")
|
|
2536
2772
|
return
|
|
2537
|
-
print(f" {len(ids)} Aerodrome NFT
|
|
2773
|
+
print(f" {len(ids)} Aerodrome NFT position(s):")
|
|
2774
|
+
|
|
2775
|
+
# Batch-read position composition via Multicall3 for efficiency.
|
|
2776
|
+
# getPositionCompositionSimplified returns (token0, token1, tickData, liquidity).
|
|
2777
|
+
# tickData packs tickLower in the upper 128 bits and tickUpper in the lower 128
|
|
2778
|
+
# bits as unsigned; the actual int24 values need sign extension.
|
|
2779
|
+
pos_legs = []
|
|
2538
2780
|
for tid in ids:
|
|
2539
|
-
|
|
2540
|
-
|
|
2781
|
+
pos_legs.append((account.address, bytes.fromhex(
|
|
2782
|
+
account.encode_abi("getPositionCompositionSimplified", args=[tid])[2:])))
|
|
2783
|
+
try:
|
|
2784
|
+
results = multicall(w3, pos_legs)
|
|
2785
|
+
except Exception:
|
|
2786
|
+
results = [(False, b"")] * len(ids)
|
|
2787
|
+
|
|
2788
|
+
for tid, (ok, rd) in zip(ids, results):
|
|
2789
|
+
if not ok or not rd:
|
|
2790
|
+
print(f" [{tid}] composition unavailable")
|
|
2791
|
+
continue
|
|
2792
|
+
try:
|
|
2793
|
+
token0, token1, tick_data, liq = w3.codec.decode(
|
|
2794
|
+
["address", "address", "uint256", "uint256"], rd)
|
|
2795
|
+
except Exception:
|
|
2796
|
+
print(f" [{tid}] composition decode failed")
|
|
2797
|
+
continue
|
|
2798
|
+
# Decode tickData: upper 128 bits = tickLower (int24), lower 128 bits = tickUpper (int24)
|
|
2799
|
+
tick_lower = _int24_from_hi128(tick_data)
|
|
2800
|
+
tick_upper = _int24_from_lo128(tick_data)
|
|
2801
|
+
# Resolve token symbols
|
|
2802
|
+
sym0 = _resolve_token_symbol(w3, token0)
|
|
2803
|
+
sym1 = _resolve_token_symbol(w3, token1)
|
|
2804
|
+
# Human-readable tick range → price range
|
|
2805
|
+
price_lower = 1.0001 ** tick_lower
|
|
2806
|
+
price_upper = 1.0001 ** tick_upper
|
|
2807
|
+
print(f" [{tid}] {sym0}/{sym1} ticks=[{tick_lower}, {tick_upper}]"
|
|
2808
|
+
f" liq={liq} price_range=[{price_lower:.4f}, {price_upper:.4f}]")
|
|
2809
|
+
print(" Manage on Aerodrome UI: https://aerodrome.finance/positions")
|
|
2810
|
+
|
|
2811
|
+
def _int24_from_hi128(val: int) -> int:
|
|
2812
|
+
"""Extract int24 from the upper 128 bits of a uint256, sign-extending."""
|
|
2813
|
+
raw = (val >> 128) & 0xFFFFFF
|
|
2814
|
+
if raw & 0x800000:
|
|
2815
|
+
return raw - 0x1000000
|
|
2816
|
+
return raw
|
|
2817
|
+
|
|
2818
|
+
def _int24_from_lo128(val: int) -> int:
|
|
2819
|
+
"""Extract int24 from the lower 128 bits of a uint256, sign-extending."""
|
|
2820
|
+
raw = val & 0xFFFFFF
|
|
2821
|
+
if raw & 0x800000:
|
|
2822
|
+
return raw - 0x1000000
|
|
2823
|
+
return raw
|
|
2824
|
+
|
|
2825
|
+
def _resolve_token_symbol(w3, addr: str) -> str:
|
|
2826
|
+
"""Best-effort token symbol from TokenManager or static pool map."""
|
|
2827
|
+
addr_lower = addr.lower()
|
|
2828
|
+
# Check static pool map first
|
|
2829
|
+
for cfg in POOLS.values():
|
|
2830
|
+
if cfg["token"].lower() == addr_lower:
|
|
2831
|
+
return cfg["symbol"]
|
|
2832
|
+
# Check Aerodrome pool configs
|
|
2833
|
+
for cfg in AERODROME_POOLS.values():
|
|
2834
|
+
if cfg["token0"].lower() == addr_lower:
|
|
2835
|
+
return cfg["symbol0"]
|
|
2836
|
+
if cfg["token1"].lower() == addr_lower:
|
|
2837
|
+
return cfg["symbol1"]
|
|
2838
|
+
# Try TokenManager
|
|
2839
|
+
try:
|
|
2840
|
+
tm = get_token_manager(w3)
|
|
2841
|
+
sym_bytes = tm.functions.tokenAddressToSymbol(Web3.to_checksum_address(addr)).call()
|
|
2842
|
+
sym = sym_bytes.rstrip(b"\x00").decode(errors="replace")
|
|
2843
|
+
if sym:
|
|
2844
|
+
return sym
|
|
2845
|
+
except Exception:
|
|
2846
|
+
pass
|
|
2847
|
+
return addr[:10] + "..."
|
|
2848
|
+
|
|
2849
|
+
# ─── Aerodrome Write Commands ────────────────────────────────────────────────
|
|
2850
|
+
|
|
2851
|
+
# Helper: build the MintParams tuple for Aerodrome Slipstream (12 fields).
|
|
2852
|
+
def _aero_mint_params(pool_cfg: dict, amount0: int, amount1: int,
|
|
2853
|
+
tick_lower: int, tick_upper: int,
|
|
2854
|
+
recipient: str, slippage_pct: float) -> tuple:
|
|
2855
|
+
"""Build MintParams=(token0,token1,tickSpacing,tickLower,tickUpper,
|
|
2856
|
+
amount0Desired,amount1Desired,amount0Min,amount1Min,recipient,deadline,
|
|
2857
|
+
sqrtPriceX96). sqrtPriceX96=0 means the NPM uses the pool's current price."""
|
|
2858
|
+
slippage = Decimal(str(slippage_pct)) / Decimal(100)
|
|
2859
|
+
amount0_min = int(Decimal(str(amount0)) * (Decimal(1) - slippage))
|
|
2860
|
+
amount1_min = int(Decimal(str(amount1)) * (Decimal(1) - slippage))
|
|
2861
|
+
deadline = int(time.time()) + 1800 # 30 minutes
|
|
2862
|
+
return (
|
|
2863
|
+
Web3.to_checksum_address(pool_cfg["token0"]),
|
|
2864
|
+
Web3.to_checksum_address(pool_cfg["token1"]),
|
|
2865
|
+
pool_cfg["tickSpacing"],
|
|
2866
|
+
tick_lower,
|
|
2867
|
+
tick_upper,
|
|
2868
|
+
amount0,
|
|
2869
|
+
amount1,
|
|
2870
|
+
amount0_min,
|
|
2871
|
+
amount1_min,
|
|
2872
|
+
Web3.to_checksum_address(recipient),
|
|
2873
|
+
deadline,
|
|
2874
|
+
0, # sqrtPriceX96: 0 = use current pool price
|
|
2875
|
+
)
|
|
2876
|
+
|
|
2877
|
+
# Helper: build DecreaseLiquidityParams tuple (5 fields).
|
|
2878
|
+
def _aero_decrease_params(token_id: int, liquidity: int,
|
|
2879
|
+
amount0_min: int, amount1_min: int) -> tuple:
|
|
2880
|
+
deadline = int(time.time()) + 1800
|
|
2881
|
+
return (token_id, liquidity, amount0_min, amount1_min, deadline)
|
|
2882
|
+
|
|
2883
|
+
# Helper: compute tick range around a desired centre price.
|
|
2884
|
+
def _aero_tick_range(tick_spacing: int, centre_price: float = None,
|
|
2885
|
+
width_pct: float = 2.0) -> tuple:
|
|
2886
|
+
"""Return (tickLower, tickUpper) for a symmetrical range ±width_pct around
|
|
2887
|
+
centre_price. If centre_price is None, uses full-range positions.
|
|
2888
|
+
tickSpacing must be valid (1, 5, 10, 30, 100, 200, etc.).
|
|
2889
|
+
Ticks are snapped to the tickSpacing grid."""
|
|
2890
|
+
if centre_price is None or centre_price <= 0:
|
|
2891
|
+
# Full range: MIN_TICK to MAX_TICK (snapped to spacing)
|
|
2892
|
+
MIN_TICK = -887272
|
|
2893
|
+
MAX_TICK = 887272
|
|
2894
|
+
t_lower = (MIN_TICK // tick_spacing) * tick_spacing
|
|
2895
|
+
t_upper = (MAX_TICK // tick_spacing) * tick_spacing
|
|
2896
|
+
return (t_lower, t_upper)
|
|
2897
|
+
# Convert price to tick: tick = floor(log(price) / log(1.0001))
|
|
2898
|
+
import math
|
|
2899
|
+
centre_tick = int(math.log(centre_price) / math.log(1.0001))
|
|
2900
|
+
half_width_ticks = int(centre_tick * width_pct / 100.0)
|
|
2901
|
+
tick_lower = ((centre_tick - half_width_ticks) // tick_spacing) * tick_spacing
|
|
2902
|
+
tick_upper = ((centre_tick + half_width_ticks) // tick_spacing) * tick_spacing
|
|
2903
|
+
# Clamp to valid range
|
|
2904
|
+
MIN_TICK, MAX_TICK = -887272, 887272
|
|
2905
|
+
tick_lower = max(MIN_TICK, min(MAX_TICK, tick_lower))
|
|
2906
|
+
tick_upper = max(MIN_TICK, min(MAX_TICK, tick_upper))
|
|
2907
|
+
if tick_lower >= tick_upper:
|
|
2908
|
+
tick_upper = tick_lower + tick_spacing
|
|
2909
|
+
return (tick_lower, tick_upper)
|
|
2910
|
+
|
|
2911
|
+
|
|
2912
|
+
def cmd_aero_add_liquidity(pool_key: str, amount0: float = None,
|
|
2913
|
+
amount1: float = None, slippage_pct: float = 1.0,
|
|
2914
|
+
execute: bool = False, width_pct: float = 2.0):
|
|
2915
|
+
"""Add concentrated liquidity to an Aerodrome Slipstream pool through the
|
|
2916
|
+
Degen Account's AerodromeFacet. Uses in-account token0/token1 balances.
|
|
2917
|
+
|
|
2918
|
+
--pool selects a whitelisted CL pool (e.g. weth-usdc-100).
|
|
2919
|
+
--amount-weth / --amount-usdc (or --amount-token0 / --amount-token1) specify
|
|
2920
|
+
the desired liquidity amounts in token units. At least one side must be >0.
|
|
2921
|
+
--slippage sets the min-amount floor (default 1%).
|
|
2922
|
+
--width sets the range ±width% around the current price (default 2%).
|
|
2923
|
+
|
|
2924
|
+
The facet wraps the NPM mint(MintParams) call with remainsSolvent, so
|
|
2925
|
+
--execute appends a RedStone signed-price payload."""
|
|
2926
|
+
w3 = get_w3()
|
|
2927
|
+
acct = get_account()
|
|
2928
|
+
print(f"Wallet: {acct.address}")
|
|
2929
|
+
pa = get_prime_account(w3, acct.address)
|
|
2930
|
+
if not pa:
|
|
2931
|
+
print("No Degen Account yet. Create with: degenprime create-account --execute")
|
|
2932
|
+
return
|
|
2933
|
+
account = w3.eth.contract(address=Web3.to_checksum_address(pa), abi=PRIME_ACCOUNT_ABI)
|
|
2934
|
+
print(f"Degen Account: {pa}")
|
|
2935
|
+
|
|
2936
|
+
if pool_key not in AERODROME_POOLS:
|
|
2937
|
+
print(f"Unknown pool '{pool_key}'. Choose from: {', '.join(AERODROME_POOLS)}")
|
|
2938
|
+
return
|
|
2939
|
+
pool_cfg = AERODROME_POOLS[pool_key]
|
|
2940
|
+
|
|
2941
|
+
# Convert amounts to wei
|
|
2942
|
+
amt0 = to_wei_units(amount0, pool_cfg["decimals0"]) if amount0 else 0
|
|
2943
|
+
amt1 = to_wei_units(amount1, pool_cfg["decimals1"]) if amount1 else 0
|
|
2944
|
+
if amt0 == 0 and amt1 == 0:
|
|
2945
|
+
print("At least one of --amount-<token0> / --amount-<token1> must be > 0")
|
|
2946
|
+
return
|
|
2947
|
+
|
|
2948
|
+
# Get current price from KuCoin for tick range calculation
|
|
2949
|
+
price_sym = pool_cfg["symbol0"] + "-USDT"
|
|
2950
|
+
centre_price = None
|
|
2951
|
+
try:
|
|
2952
|
+
r = requests.get(f"https://api.kucoin.com/api/v1/market/orderbook/level1?symbol={price_sym}", timeout=3)
|
|
2953
|
+
if r.status_code == 200 and r.json().get("code") == "200000":
|
|
2954
|
+
centre_price = float(r.json()["data"]["price"])
|
|
2955
|
+
except Exception:
|
|
2956
|
+
pass
|
|
2957
|
+
|
|
2958
|
+
tick_lower, tick_upper = _aero_tick_range(pool_cfg["tickSpacing"], centre_price, width_pct)
|
|
2959
|
+
params = _aero_mint_params(pool_cfg, amt0, amt1, tick_lower, tick_upper,
|
|
2960
|
+
pa, slippage_pct)
|
|
2961
|
+
|
|
2962
|
+
sym0, sym1 = pool_cfg["symbol0"], pool_cfg["symbol1"]
|
|
2963
|
+
print(f"Pool: {sym0}/{sym1} (tickSpacing={pool_cfg['tickSpacing']})")
|
|
2964
|
+
if centre_price:
|
|
2965
|
+
print(f" Current {sym0} price: ${centre_price:,.2f}")
|
|
2966
|
+
print(f" Tick range: [{tick_lower}, {tick_upper}] → price [{1.0001**tick_lower:.4f}, {1.0001**tick_upper:.4f}]")
|
|
2967
|
+
print(f" Width: ±{width_pct}%")
|
|
2968
|
+
else:
|
|
2969
|
+
print(f" Full-range position (no price data available)")
|
|
2970
|
+
print(f" {sym0}: {amount0 or 0} ({amt0} wei) {sym1}: {amount1 or 0} ({amt1} wei)")
|
|
2971
|
+
|
|
2972
|
+
if not execute:
|
|
2973
|
+
print("Preview only. Run with --execute to broadcast.")
|
|
2974
|
+
return
|
|
2975
|
+
|
|
2976
|
+
# Build RedStone payload for solvency check
|
|
2977
|
+
feeds = degen_account_price_feeds(account)
|
|
2978
|
+
payload = build_redstone_payload(feeds)
|
|
2979
|
+
|
|
2980
|
+
# Encode the mint call: use the probed selector + ABI-encoded params
|
|
2981
|
+
mint_data = account.encode_abi("mintAerodrome", args=[params])
|
|
2982
|
+
# encode_abi returns hex string "0x...", extract params bytes after selector
|
|
2983
|
+
mint_params_bytes = bytes.fromhex(mint_data[2:])[4:]
|
|
2984
|
+
mint_calldata = AERODROME_SEL_MINT + mint_params_bytes + payload
|
|
2985
|
+
|
|
2986
|
+
tx = {
|
|
2987
|
+
"from": acct.address,
|
|
2988
|
+
"to": account.address,
|
|
2989
|
+
"nonce": w3.eth.get_transaction_count(acct.address),
|
|
2990
|
+
"gas": 5000000,
|
|
2991
|
+
"chainId": CHAIN_ID,
|
|
2992
|
+
"data": "0x" + mint_calldata.hex(),
|
|
2993
|
+
}
|
|
2994
|
+
_set_gas_price(w3, tx)
|
|
2995
|
+
signed = acct.sign_transaction(tx)
|
|
2996
|
+
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
2997
|
+
receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=300)
|
|
2998
|
+
ok = receipt["status"] == 1
|
|
2999
|
+
print(f"{'✓' if ok else '✗'} Add liquidity {'confirmed' if ok else 'failed'}")
|
|
3000
|
+
print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
|
|
3001
|
+
|
|
3002
|
+
|
|
3003
|
+
def cmd_aero_remove_liquidity(token_id: int, percentage: float = 100.0,
|
|
3004
|
+
execute: bool = False):
|
|
3005
|
+
"""Remove liquidity from an Aerodrome Slipstream position owned by the
|
|
3006
|
+
Degen Account. Decreases liquidity by `percentage` of the current position
|
|
3007
|
+
size (default 100% = full close). The position NFT remains; burn it
|
|
3008
|
+
separately after all liquidity is removed and fees collected.
|
|
3009
|
+
|
|
3010
|
+
The facet wraps NPM.decreaseLiquidity. No RedStone payload needed
|
|
3011
|
+
(the decrease path is NOT remainsSolvent — same as TJ lb-remove)."""
|
|
3012
|
+
w3 = get_w3()
|
|
3013
|
+
acct = get_account()
|
|
3014
|
+
print(f"Wallet: {acct.address}")
|
|
3015
|
+
pa = get_prime_account(w3, acct.address)
|
|
3016
|
+
if not pa:
|
|
3017
|
+
print("No Degen Account yet.")
|
|
3018
|
+
return
|
|
3019
|
+
account = w3.eth.contract(address=Web3.to_checksum_address(pa), abi=PRIME_ACCOUNT_ABI)
|
|
3020
|
+
print(f"Degen Account: {pa}")
|
|
3021
|
+
|
|
3022
|
+
# Read the position's current liquidity
|
|
3023
|
+
try:
|
|
3024
|
+
pos = account.functions.getPositionCompositionSimplified(token_id).call()
|
|
3025
|
+
except Exception as e:
|
|
3026
|
+
print(f" Cannot read position {token_id}: {e}")
|
|
3027
|
+
return
|
|
3028
|
+
token0, token1, tick_data, current_liq = pos
|
|
3029
|
+
if current_liq == 0:
|
|
3030
|
+
print(f" Position {token_id} has 0 liquidity (may already be closed).")
|
|
3031
|
+
return
|
|
3032
|
+
|
|
3033
|
+
sym0 = _resolve_token_symbol(w3, token0)
|
|
3034
|
+
sym1 = _resolve_token_symbol(w3, token1)
|
|
3035
|
+
tick_lower = _int24_from_hi128(tick_data)
|
|
3036
|
+
tick_upper = _int24_from_lo128(tick_data)
|
|
3037
|
+
|
|
3038
|
+
remove_liq = int(Decimal(str(current_liq)) * Decimal(str(percentage)) / Decimal(100))
|
|
3039
|
+
if remove_liq <= 0:
|
|
3040
|
+
print(f" Removal percentage {percentage}% yields 0 liquidity.")
|
|
3041
|
+
return
|
|
3042
|
+
|
|
3043
|
+
print(f"Position {token_id}: {sym0}/{sym1} ticks=[{tick_lower},{tick_upper}]")
|
|
3044
|
+
print(f" Current liquidity: {current_liq}")
|
|
3045
|
+
print(f" Removing: {remove_liq} ({percentage}%)")
|
|
3046
|
+
|
|
3047
|
+
if not execute:
|
|
3048
|
+
print("Preview only. Run with --execute to broadcast.")
|
|
3049
|
+
return
|
|
3050
|
+
|
|
3051
|
+
params = _aero_decrease_params(token_id, remove_liq, 0, 0)
|
|
3052
|
+
# decreaseLiquidity on the facet (selector ca15558b)
|
|
3053
|
+
dec_data = account.encode_abi("decreaseAerodromeLiquidity", args=[params])
|
|
3054
|
+
dec_params_bytes = bytes.fromhex(dec_data[2:])[4:]
|
|
3055
|
+
dec_calldata = AERODROME_SEL_DECREASE + dec_params_bytes
|
|
3056
|
+
|
|
3057
|
+
tx = {
|
|
3058
|
+
"from": acct.address,
|
|
3059
|
+
"to": account.address,
|
|
3060
|
+
"nonce": w3.eth.get_transaction_count(acct.address),
|
|
3061
|
+
"gas": 4000000,
|
|
3062
|
+
"chainId": CHAIN_ID,
|
|
3063
|
+
"data": "0x" + dec_calldata.hex(),
|
|
3064
|
+
}
|
|
3065
|
+
_set_gas_price(w3, tx)
|
|
3066
|
+
signed = acct.sign_transaction(tx)
|
|
3067
|
+
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
3068
|
+
receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=300)
|
|
3069
|
+
ok = receipt["status"] == 1
|
|
3070
|
+
print(f"{'✓' if ok else '✗'} Remove liquidity {'confirmed' if ok else 'failed'}")
|
|
3071
|
+
print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
|
|
3072
|
+
if ok and percentage >= 100:
|
|
3073
|
+
print(f" Position fully withdrawn. Collect fees then burn:")
|
|
3074
|
+
print(f" degenprime aero-collect-fees --token-id {token_id} --execute")
|
|
3075
|
+
|
|
3076
|
+
|
|
3077
|
+
def cmd_aero_collect_fees(token_id: int, execute: bool = False):
|
|
3078
|
+
"""Collect accrued swap fees from an Aerodrome Slipstream position.
|
|
3079
|
+
Fees accumulate as tokensOwed0/tokensOwed1 on the NPM; collect sends them
|
|
3080
|
+
to the Degen Account. After collecting all fees (and removing all liquidity),
|
|
3081
|
+
the NFT position can be burned.
|
|
3082
|
+
|
|
3083
|
+
Uses the facet's collect/burn path (selector 0x92b5a47e)."""
|
|
3084
|
+
w3 = get_w3()
|
|
3085
|
+
acct = get_account()
|
|
3086
|
+
print(f"Wallet: {acct.address}")
|
|
3087
|
+
pa = get_prime_account(w3, acct.address)
|
|
3088
|
+
if not pa:
|
|
3089
|
+
print("No Degen Account yet.")
|
|
3090
|
+
return
|
|
3091
|
+
account = w3.eth.contract(address=Web3.to_checksum_address(pa), abi=PRIME_ACCOUNT_ABI)
|
|
3092
|
+
print(f"Degen Account: {pa}")
|
|
3093
|
+
|
|
3094
|
+
# Read position composition for display
|
|
3095
|
+
try:
|
|
3096
|
+
pos = account.functions.getPositionCompositionSimplified(token_id).call()
|
|
3097
|
+
except Exception as e:
|
|
3098
|
+
print(f" Cannot read position {token_id}: {e}")
|
|
3099
|
+
return
|
|
3100
|
+
token0, token1, tick_data, liq = pos
|
|
3101
|
+
sym0 = _resolve_token_symbol(w3, token0)
|
|
3102
|
+
sym1 = _resolve_token_symbol(w3, token1)
|
|
3103
|
+
print(f"Position {token_id}: {sym0}/{sym1} liquidity={liq}")
|
|
3104
|
+
|
|
3105
|
+
# Also try to read uncollected fees from the NPM directly
|
|
3106
|
+
try:
|
|
3107
|
+
npm = w3.eth.contract(address=Web3.to_checksum_address(AERODROME_NPM),
|
|
3108
|
+
abi=AERODROME_NPM_ABI)
|
|
3109
|
+
npm_pos = npm.functions.positions(token_id).call()
|
|
3110
|
+
owed0 = npm_pos[10] # tokensOwed0
|
|
3111
|
+
owed1 = npm_pos[11] # tokensOwed1
|
|
3112
|
+
if owed0 > 0 or owed1 > 0:
|
|
3113
|
+
print(f" Uncollected fees: {owed0} ({sym0}) + {owed1} ({sym1})")
|
|
3114
|
+
else:
|
|
3115
|
+
print(f" No uncollected fees.")
|
|
3116
|
+
except Exception:
|
|
3117
|
+
print(f" (Cannot fetch uncollected fees from NPM directly)")
|
|
3118
|
+
|
|
3119
|
+
if not execute:
|
|
3120
|
+
print("Preview only. Run with --execute to broadcast.")
|
|
3121
|
+
return
|
|
3122
|
+
|
|
3123
|
+
# Build RedStone payload (collect may be solvency-gated)
|
|
3124
|
+
try:
|
|
3125
|
+
feeds = degen_account_price_feeds(account)
|
|
3126
|
+
payload = build_redstone_payload(feeds)
|
|
3127
|
+
except Exception:
|
|
3128
|
+
payload = b""
|
|
3129
|
+
|
|
3130
|
+
# Encode collect call with the probed selector
|
|
3131
|
+
collect_data = account.encode_abi("collectAerodromeFees", args=[token_id])
|
|
3132
|
+
collect_params_bytes = bytes.fromhex(collect_data[2:])[4:]
|
|
3133
|
+
collect_calldata = AERODROME_SEL_COLLECT + collect_params_bytes
|
|
3134
|
+
if payload:
|
|
3135
|
+
collect_calldata += payload
|
|
3136
|
+
|
|
3137
|
+
tx = {
|
|
3138
|
+
"from": acct.address,
|
|
3139
|
+
"to": account.address,
|
|
3140
|
+
"nonce": w3.eth.get_transaction_count(acct.address),
|
|
3141
|
+
"gas": 3000000,
|
|
3142
|
+
"chainId": CHAIN_ID,
|
|
3143
|
+
"data": "0x" + collect_calldata.hex(),
|
|
3144
|
+
}
|
|
3145
|
+
_set_gas_price(w3, tx)
|
|
3146
|
+
signed = acct.sign_transaction(tx)
|
|
3147
|
+
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
3148
|
+
receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=300)
|
|
3149
|
+
ok = receipt["status"] == 1
|
|
3150
|
+
print(f"{'✓' if ok else '✗'} Collect fees {'confirmed' if ok else 'failed'}")
|
|
3151
|
+
print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
|
|
3152
|
+
|
|
2541
3153
|
|
|
2542
3154
|
def main():
|
|
2543
3155
|
check_version()
|
|
@@ -2731,6 +3343,45 @@ def _dispatch():
|
|
|
2731
3343
|
cmd_execute_pool_withdrawal(pool, index, execute)
|
|
2732
3344
|
elif cmd == "aerodrome-positions":
|
|
2733
3345
|
cmd_aerodrome_positions()
|
|
3346
|
+
elif cmd == "aero-add-liquidity":
|
|
3347
|
+
pool_key = None
|
|
3348
|
+
amt0, amt1 = None, None
|
|
3349
|
+
slippage = 1.0
|
|
3350
|
+
width = 2.0
|
|
3351
|
+
execute = "--execute" in args
|
|
3352
|
+
for i, a in enumerate(args):
|
|
3353
|
+
if a == "--pool" and i + 1 < len(args): pool_key = args[i + 1]
|
|
3354
|
+
if a == "--amount-weth" and i + 1 < len(args): amt0 = float(args[i + 1])
|
|
3355
|
+
if a == "--amount-usdc" and i + 1 < len(args): amt1 = float(args[i + 1])
|
|
3356
|
+
if a == "--amount-token0" and i + 1 < len(args): amt0 = float(args[i + 1])
|
|
3357
|
+
if a == "--amount-token1" and i + 1 < len(args): amt1 = float(args[i + 1])
|
|
3358
|
+
if a == "--amount-aero" and i + 1 < len(args): amt0 = float(args[i + 1])
|
|
3359
|
+
if a == "--slippage" and i + 1 < len(args): slippage = float(args[i + 1])
|
|
3360
|
+
if a == "--width" and i + 1 < len(args): width = float(args[i + 1])
|
|
3361
|
+
if not pool_key or (amt0 is None and amt1 is None):
|
|
3362
|
+
print("Usage: degenprime aero-add-liquidity --pool weth-usdc-100 --amount-weth 0.05 --amount-usdc 100 [--slippage 1] [--width 2] [--execute]")
|
|
3363
|
+
return
|
|
3364
|
+
cmd_aero_add_liquidity(pool_key, amt0, amt1, slippage, execute, width)
|
|
3365
|
+
elif cmd == "aero-remove-liquidity":
|
|
3366
|
+
token_id = None
|
|
3367
|
+
percentage = 100.0
|
|
3368
|
+
execute = "--execute" in args
|
|
3369
|
+
for i, a in enumerate(args):
|
|
3370
|
+
if a == "--token-id" and i + 1 < len(args): token_id = int(args[i + 1])
|
|
3371
|
+
if a == "--percentage" and i + 1 < len(args): percentage = float(args[i + 1])
|
|
3372
|
+
if token_id is None:
|
|
3373
|
+
print("Usage: degenprime aero-remove-liquidity --token-id N [--percentage 100] [--execute]")
|
|
3374
|
+
return
|
|
3375
|
+
cmd_aero_remove_liquidity(token_id, percentage, execute)
|
|
3376
|
+
elif cmd == "aero-collect-fees":
|
|
3377
|
+
token_id = None
|
|
3378
|
+
execute = "--execute" in args
|
|
3379
|
+
for i, a in enumerate(args):
|
|
3380
|
+
if a == "--token-id" and i + 1 < len(args): token_id = int(args[i + 1])
|
|
3381
|
+
if token_id is None:
|
|
3382
|
+
print("Usage: degenprime aero-collect-fees --token-id N [--execute]")
|
|
3383
|
+
return
|
|
3384
|
+
cmd_aero_collect_fees(token_id, execute)
|
|
2734
3385
|
elif cmd == "health":
|
|
2735
3386
|
os.environ.setdefault("PRIMECLI_TOOL", sys.argv[0])
|
|
2736
3387
|
if health_monitor:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: primecli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "primecli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
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
|