primecli 0.7.3__tar.gz → 0.7.5__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.
Files changed (24) hide show
  1. {primecli-0.7.3 → primecli-0.7.5}/PKG-INFO +2 -2
  2. {primecli-0.7.3 → primecli-0.7.5}/README.md +1 -1
  3. {primecli-0.7.3 → primecli-0.7.5}/primecli/arbprime.py +26 -1
  4. {primecli-0.7.3 → primecli-0.7.5}/primecli/degenprime.py +134 -28
  5. {primecli-0.7.3 → primecli-0.7.5}/primecli/deltaprime.py +26 -1
  6. {primecli-0.7.3 → primecli-0.7.5}/primecli/health_monitor.py +234 -101
  7. {primecli-0.7.3 → primecli-0.7.5}/primecli.egg-info/PKG-INFO +2 -2
  8. {primecli-0.7.3 → primecli-0.7.5}/pyproject.toml +1 -1
  9. {primecli-0.7.3 → primecli-0.7.5}/LICENSE +0 -0
  10. {primecli-0.7.3 → primecli-0.7.5}/primecli/__init__.py +0 -0
  11. {primecli-0.7.3 → primecli-0.7.5}/primecli.egg-info/SOURCES.txt +0 -0
  12. {primecli-0.7.3 → primecli-0.7.5}/primecli.egg-info/dependency_links.txt +0 -0
  13. {primecli-0.7.3 → primecli-0.7.5}/primecli.egg-info/entry_points.txt +0 -0
  14. {primecli-0.7.3 → primecli-0.7.5}/primecli.egg-info/requires.txt +0 -0
  15. {primecli-0.7.3 → primecli-0.7.5}/primecli.egg-info/top_level.txt +0 -0
  16. {primecli-0.7.3 → primecli-0.7.5}/setup.cfg +0 -0
  17. {primecli-0.7.3 → primecli-0.7.5}/tests/test_cross_file_identity.py +0 -0
  18. {primecli-0.7.3 → primecli-0.7.5}/tests/test_gas_limit.py +0 -0
  19. {primecli-0.7.3 → primecli-0.7.5}/tests/test_gas_pricing.py +0 -0
  20. {primecli-0.7.3 → primecli-0.7.5}/tests/test_health_meter.py +0 -0
  21. {primecli-0.7.3 → primecli-0.7.5}/tests/test_health_monitor.py +0 -0
  22. {primecli-0.7.3 → primecli-0.7.5}/tests/test_paraswap_validator.py +0 -0
  23. {primecli-0.7.3 → primecli-0.7.5}/tests/test_redstone_encoding.py +0 -0
  24. {primecli-0.7.3 → primecli-0.7.5}/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.7.3
3
+ Version: 0.7.5
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.7.3 The 0.x line is pre-1.0, so breaking changes are possible. See [Releases](https://github.com/Mnemosyne-quest/primecli/releases).
50
+ **Current version:** 0.7.5 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.7.3 The 0.x line is pre-1.0, so breaking changes are possible. See [Releases](https://github.com/Mnemosyne-quest/primecli/releases).
19
+ **Current version:** 0.7.5 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
 
@@ -262,6 +262,7 @@ AGENTS = {
262
262
  }
263
263
  _SELECTED_AGENT = None # set by the --as CLI flag in main()
264
264
  _CLI_KEY = None # set by the --key CLI flag in main()
265
+ _OWNER_ADDRESS = None # set by --owner for keyless read-only commands (main())
265
266
  # Core protocol addresses — the LIVE Arbitrum deployment (DeploymentConstants.sol),
266
267
  # on-chain verified 2026-06-03. The stale *TUP.json deployment (factory 0x97f4C81…)
267
268
  # has only ETH+USDC pools — NOT used here.
@@ -847,6 +848,14 @@ def resolve_private_key():
847
848
  )
848
849
 
849
850
  def get_account() -> Account:
851
+ # --owner provides a keyless read-only account (address only, cannot sign) for
852
+ # monitoring/sim reads that need the wallet owner (e.g. to locate a Prime Account)
853
+ # but never broadcast. Write paths are blocked in main() when --owner is set.
854
+ if _OWNER_ADDRESS:
855
+ class _ReadOnlyAccount:
856
+ def __init__(self, address):
857
+ self.address = Web3.to_checksum_address(address)
858
+ return _ReadOnlyAccount(_OWNER_ADDRESS)
850
859
  return Account.from_key(resolve_private_key())
851
860
 
852
861
  def to_wei_units(amount, decimals):
@@ -5587,7 +5596,7 @@ def main():
5587
5596
  check_version()
5588
5597
  args = sys.argv[1:] if len(sys.argv) > 1 else []
5589
5598
  # Global wallet selector: --as <agent>, stripped before command dispatch.
5590
- global _SELECTED_AGENT, _CLI_KEY
5599
+ global _SELECTED_AGENT, _CLI_KEY, _OWNER_ADDRESS
5591
5600
  if "--as" in args:
5592
5601
  i = args.index("--as")
5593
5602
  if i + 1 >= len(args):
@@ -5603,11 +5612,27 @@ def main():
5603
5612
  return
5604
5613
  _CLI_KEY = args[i + 1]
5605
5614
  del args[i:i + 2]
5615
+ # Public owner-address selector for read-only commands. Lets monitoring/sim jobs
5616
+ # inspect a wallet's Prime Account / positions without resolving or loading a key.
5617
+ if "--owner" in args:
5618
+ i = args.index("--owner")
5619
+ if i + 1 >= len(args):
5620
+ print("--owner requires an EVM address. Example: --owner 0xabc...")
5621
+ return
5622
+ try:
5623
+ _OWNER_ADDRESS = Web3.to_checksum_address(args[i + 1])
5624
+ except Exception:
5625
+ print(f"Invalid --owner address: {args[i + 1]}")
5626
+ return
5627
+ del args[i:i + 2]
5606
5628
  if not args or args[0] in ("-h", "--help"):
5607
5629
  print(__doc__)
5608
5630
  return
5609
5631
 
5610
5632
  cmd = args[0]
5633
+ if _OWNER_ADDRESS and cmd not in {"defi", "lb-positions"}:
5634
+ print("--owner is only supported for read-only commands: defi, lb-positions")
5635
+ return
5611
5636
  if cmd == "pool-info":
5612
5637
  # First positional after `pool-info` is the pool name; --json is an opt-in flag
5613
5638
  # that switches output from human tables to a compact JSON shape (one object for
@@ -218,35 +218,141 @@ AERODROME_SEL_DECREASE = bytes.fromhex("ca15558b") # inferred: decreaseLiqu
218
218
  AERODROME_SEL_BURN = bytes.fromhex("92b5a47e") # inferred: burn
219
219
  AERODROME_SEL_COLLECT = bytes.fromhex("92b5a47e") # same as burn (inferred)
220
220
 
221
- # Whitelisted Aerodrome CL pools exposed as tool keys. Each key carries the known
222
- # token pair, fee tier (tickSpacing in bps), and the expected token addresses.
223
- # tickSpacing maps: 1 = 0.01%, 5 = 0.05%, 10 = 0.1%, 30 = 0.3%, 100 = 1%.
224
- # Most pools use the canonical (WETH/stablecoin) ordering from the CL Factory.
221
+ # Whitelisted Aerodrome CL pools exposed as tool keys the authoritative set of
222
+ # ~31 DegenPrime-supported SlipStream pools. Every entry was verified on-chain
223
+ # (2026-06-13): token0/token1/tickSpacing read from the pool, decimals/symbol from
224
+ # each token, and the pool address cross-checked against the SlipStream factory's
225
+ # getPool(token0, token1, tickSpacing) (0x5e7BB104d84c7CB9B682AaC2F3d509f5F406809A).
226
+ # token0/token1 follow the pool's canonical on-chain ordering; the key is a curated
227
+ # label (symbol0 stores WETH as "ETH" to match the lending-pool convention, so WETH
228
+ # pairs are keyed "weth-*").
229
+ # tickSpacing tiers in use: 1, 50, 100, 200, 2000. gauge_alive=False marks pools
230
+ # whose Aerodrome gauge is dead (no AERO emissions; still tradeable/LP-able).
225
231
  AERODROME_POOLS = {
226
- "weth-usdc-100": {"token0": "0x4200000000000000000000000000000000000006",
227
- "token1": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
228
- "tickSpacing": 100, "symbol0": "ETH", "symbol1": "USDC",
229
- "decimals0": 18, "decimals1": 6},
230
- "weth-usdc-5": {"token0": "0x4200000000000000000000000000000000000006",
231
- "token1": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
232
- "tickSpacing": 5, "symbol0": "ETH", "symbol1": "USDC",
233
- "decimals0": 18, "decimals1": 6},
234
- "weth-cbbtc-100": {"token0": "0x4200000000000000000000000000000000000006",
235
- "token1": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
236
- "tickSpacing": 100, "symbol0": "ETH", "symbol1": "cbBTC",
237
- "decimals0": 18, "decimals1": 8},
238
- "weth-cbbtc-30": {"token0": "0x4200000000000000000000000000000000000006",
239
- "token1": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
240
- "tickSpacing": 30, "symbol0": "ETH", "symbol1": "cbBTC",
241
- "decimals0": 18, "decimals1": 8},
242
- "weth-aero-200": {"token0": "0x940181a94A35A4569E4529A3CDfB74e38FD98631",
243
- "token1": "0x4200000000000000000000000000000000000006",
244
- "tickSpacing": 200, "symbol0": "AERO", "symbol1": "ETH",
245
- "decimals0": 18, "decimals1": 18},
246
- "aero-usdc-100": {"token0": "0x940181a94A35A4569E4529A3CDfB74e38FD98631",
247
- "token1": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
248
- "tickSpacing": 100, "symbol0": "AERO", "symbol1": "USDC",
249
- "decimals0": 18, "decimals1": 6},
232
+ "weth-usdc-100": {"token0": "0x4200000000000000000000000000000000000006",
233
+ "token1": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
234
+ "tickSpacing": 100, "symbol0": "ETH", "symbol1": "USDC",
235
+ "decimals0": 18, "decimals1": 6, "gauge_alive": True},
236
+ "weth-cbbtc-100": {"token0": "0x4200000000000000000000000000000000000006",
237
+ "token1": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
238
+ "tickSpacing": 100, "symbol0": "ETH", "symbol1": "cbBTC",
239
+ "decimals0": 18, "decimals1": 8, "gauge_alive": True},
240
+ "weth-aero-200": {"token0": "0x4200000000000000000000000000000000000006",
241
+ "token1": "0x940181a94A35A4569E4529A3CDfB74e38FD98631",
242
+ "tickSpacing": 200, "symbol0": "ETH", "symbol1": "AERO",
243
+ "decimals0": 18, "decimals1": 18, "gauge_alive": False},
244
+ "aero-usdc-2000": {"token0": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
245
+ "token1": "0x940181a94A35A4569E4529A3CDfB74e38FD98631",
246
+ "tickSpacing": 2000, "symbol0": "USDC", "symbol1": "AERO",
247
+ "decimals0": 6, "decimals1": 18, "gauge_alive": True},
248
+ "aero-cbbtc-200": {"token0": "0x940181a94A35A4569E4529A3CDfB74e38FD98631",
249
+ "token1": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
250
+ "tickSpacing": 200, "symbol0": "AERO", "symbol1": "cbBTC",
251
+ "decimals0": 18, "decimals1": 8, "gauge_alive": True},
252
+ "avnt-usdc-1": {"token0": "0x696F9436B67233384889472Cd7cD58A6fB5DF4f1",
253
+ "token1": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
254
+ "tickSpacing": 1, "symbol0": "AVNT", "symbol1": "USDC",
255
+ "decimals0": 18, "decimals1": 6, "gauge_alive": True},
256
+ "avnt-usdc-200": {"token0": "0x696F9436B67233384889472Cd7cD58A6fB5DF4f1",
257
+ "token1": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
258
+ "tickSpacing": 200, "symbol0": "AVNT", "symbol1": "USDC",
259
+ "decimals0": 18, "decimals1": 6, "gauge_alive": True},
260
+ "cbbtc-lbtc-1": {"token0": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
261
+ "token1": "0xecAc9C5F704e954931349Da37F60E39f515c11c1",
262
+ "tickSpacing": 1, "symbol0": "cbBTC", "symbol1": "LBTC",
263
+ "decimals0": 8, "decimals1": 8, "gauge_alive": True},
264
+ "cbbtc-cbdoge-100": {"token0": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
265
+ "token1": "0xcbD06E5A2B0C65597161de254AA074E489dEb510",
266
+ "tickSpacing": 100, "symbol0": "cbBTC", "symbol1": "cbDOGE",
267
+ "decimals0": 8, "decimals1": 8, "gauge_alive": False},
268
+ "clanker-weth-200": {"token0": "0x1bc0c42215582d5A085795f4baDbaC3ff36d1Bcb",
269
+ "token1": "0x4200000000000000000000000000000000000006",
270
+ "tickSpacing": 200, "symbol0": "CLANKER", "symbol1": "ETH",
271
+ "decimals0": 18, "decimals1": 18, "gauge_alive": True},
272
+ "weth-aixbt-200": {"token0": "0x4200000000000000000000000000000000000006",
273
+ "token1": "0x4F9Fd6Be4a90f2620860d680c0d4d5Fb53d1A825",
274
+ "tickSpacing": 200, "symbol0": "ETH", "symbol1": "AIXBT",
275
+ "decimals0": 18, "decimals1": 18, "gauge_alive": True},
276
+ "weth-brett-200": {"token0": "0x4200000000000000000000000000000000000006",
277
+ "token1": "0x532f27101965dd16442E59d40670FaF5eBB142E4",
278
+ "tickSpacing": 200, "symbol0": "ETH", "symbol1": "BRETT",
279
+ "decimals0": 18, "decimals1": 18, "gauge_alive": True},
280
+ "weth-degen-200": {"token0": "0x4200000000000000000000000000000000000006",
281
+ "token1": "0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed",
282
+ "tickSpacing": 200, "symbol0": "ETH", "symbol1": "DEGEN",
283
+ "decimals0": 18, "decimals1": 18, "gauge_alive": True},
284
+ "weth-euroc-100": {"token0": "0x4200000000000000000000000000000000000006",
285
+ "token1": "0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42",
286
+ "tickSpacing": 100, "symbol0": "ETH", "symbol1": "EURC",
287
+ "decimals0": 18, "decimals1": 6, "gauge_alive": False},
288
+ "weth-kaito-100": {"token0": "0x4200000000000000000000000000000000000006",
289
+ "token1": "0x98d0baa52b2D063E780DE12F615f963Fe8537553",
290
+ "tickSpacing": 100, "symbol0": "ETH", "symbol1": "KAITO",
291
+ "decimals0": 18, "decimals1": 18, "gauge_alive": True},
292
+ "weth-spx-200": {"token0": "0x4200000000000000000000000000000000000006",
293
+ "token1": "0x50dA645f148798F68EF2d7dB7C1CB22A6819bb2C",
294
+ "tickSpacing": 200, "symbol0": "ETH", "symbol1": "SPX",
295
+ "decimals0": 18, "decimals1": 8, "gauge_alive": True},
296
+ "weth-toshi-200": {"token0": "0x4200000000000000000000000000000000000006",
297
+ "token1": "0xAC1Bd2486aAf3B5C0fc3Fd868558b082a531B2B4",
298
+ "tickSpacing": 200, "symbol0": "ETH", "symbol1": "TOSHI",
299
+ "decimals0": 18, "decimals1": 18, "gauge_alive": True},
300
+ "weth-cbdoge-2000": {"token0": "0x4200000000000000000000000000000000000006",
301
+ "token1": "0xcbD06E5A2B0C65597161de254AA074E489dEb510",
302
+ "tickSpacing": 2000, "symbol0": "ETH", "symbol1": "cbDOGE",
303
+ "decimals0": 18, "decimals1": 8, "gauge_alive": True},
304
+ "weth-cbltc-200": {"token0": "0x4200000000000000000000000000000000000006",
305
+ "token1": "0xcb17C9Db87B595717C857a08468793f5bAb6445F",
306
+ "tickSpacing": 200, "symbol0": "ETH", "symbol1": "cbLTC",
307
+ "decimals0": 18, "decimals1": 8, "gauge_alive": True},
308
+ "weth-cbxrp-2000": {"token0": "0x4200000000000000000000000000000000000006",
309
+ "token1": "0xcb585250f852C6c6bf90434AB21A00f02833a4af",
310
+ "tickSpacing": 2000, "symbol0": "ETH", "symbol1": "cbXRP",
311
+ "decimals0": 18, "decimals1": 6, "gauge_alive": False},
312
+ "euroc-usdc-1": {"token0": "0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42",
313
+ "token1": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
314
+ "tickSpacing": 1, "symbol0": "EURC", "symbol1": "USDC",
315
+ "decimals0": 6, "decimals1": 6, "gauge_alive": False},
316
+ "euroc-usdc-50": {"token0": "0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42",
317
+ "token1": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
318
+ "tickSpacing": 50, "symbol0": "EURC", "symbol1": "USDC",
319
+ "decimals0": 6, "decimals1": 6, "gauge_alive": True},
320
+ "usdc-cbbtc-100": {"token0": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
321
+ "token1": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
322
+ "tickSpacing": 100, "symbol0": "USDC", "symbol1": "cbBTC",
323
+ "decimals0": 6, "decimals1": 8, "gauge_alive": True},
324
+ "usdc-usdt-1": {"token0": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
325
+ "token1": "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2",
326
+ "tickSpacing": 1, "symbol0": "USDC", "symbol1": "USDT",
327
+ "decimals0": 6, "decimals1": 6, "gauge_alive": True},
328
+ "virtual-weth-100": {"token0": "0x0b3e328455c4059EEb9e3f84b5543F74E24e7E1b",
329
+ "token1": "0x4200000000000000000000000000000000000006",
330
+ "tickSpacing": 100, "symbol0": "VIRTUAL", "symbol1": "ETH",
331
+ "decimals0": 18, "decimals1": 18, "gauge_alive": True},
332
+ "virtual-weth-200": {"token0": "0x0b3e328455c4059EEb9e3f84b5543F74E24e7E1b",
333
+ "token1": "0x4200000000000000000000000000000000000006",
334
+ "tickSpacing": 200, "symbol0": "VIRTUAL", "symbol1": "ETH",
335
+ "decimals0": 18, "decimals1": 18, "gauge_alive": True},
336
+ "zora-usdc-100": {"token0": "0x1111111111166b7FE7bd91427724B487980aFc69",
337
+ "token1": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
338
+ "tickSpacing": 100, "symbol0": "ZORA", "symbol1": "USDC",
339
+ "decimals0": 18, "decimals1": 6, "gauge_alive": True},
340
+ "cbltc-cbbtc-100": {"token0": "0xcb17C9Db87B595717C857a08468793f5bAb6445F",
341
+ "token1": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
342
+ "tickSpacing": 100, "symbol0": "cbLTC", "symbol1": "cbBTC",
343
+ "decimals0": 8, "decimals1": 8, "gauge_alive": True},
344
+ "cbxrp-cbbtc-100": {"token0": "0xcb585250f852C6c6bf90434AB21A00f02833a4af",
345
+ "token1": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
346
+ "tickSpacing": 100, "symbol0": "cbXRP", "symbol1": "cbBTC",
347
+ "decimals0": 6, "decimals1": 8, "gauge_alive": False},
348
+ "weeth-weth-1": {"token0": "0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A",
349
+ "token1": "0x4200000000000000000000000000000000000006",
350
+ "tickSpacing": 1, "symbol0": "weETH", "symbol1": "ETH",
351
+ "decimals0": 18, "decimals1": 18, "gauge_alive": True},
352
+ "ezeth-weth-1": {"token0": "0x2416092f143378750bb29b79eD961ab195CcEea5",
353
+ "token1": "0x4200000000000000000000000000000000000006",
354
+ "tickSpacing": 1, "symbol0": "ezETH", "symbol1": "ETH",
355
+ "decimals0": 18, "decimals1": 18, "gauge_alive": True},
250
356
  }
251
357
 
252
358
  # ParaSwap v6 / Velora aggregator on Base. The Degen Account's ParaSwapFacet.paraSwapV6
@@ -269,6 +269,7 @@ AGENTS = {
269
269
  }
270
270
  _SELECTED_AGENT = None # set by the --as CLI flag in main()
271
271
  _CLI_KEY = None # set by the --key CLI flag in main()
272
+ _OWNER_ADDRESS = None # set by --owner for keyless read-only commands (main())
272
273
  SNOWTRACE = "https://api.snowtrace.io/api"
273
274
  FACTORY_PROXY = "0x3Ea9D480295A73fd2aF95b4D96c2afF88b21B03D"
274
275
  # On-chain registry of active pools. getPoolAddress(bytes32 asset) is the source
@@ -842,6 +843,14 @@ def resolve_private_key():
842
843
  )
843
844
 
844
845
  def get_account() -> Account:
846
+ # --owner provides a keyless read-only account (address only, cannot sign) for
847
+ # monitoring/sim reads that need the wallet owner (e.g. to locate a Prime Account)
848
+ # but never broadcast. Write paths are blocked in main() when --owner is set.
849
+ if _OWNER_ADDRESS:
850
+ class _ReadOnlyAccount:
851
+ def __init__(self, address):
852
+ self.address = Web3.to_checksum_address(address)
853
+ return _ReadOnlyAccount(_OWNER_ADDRESS)
845
854
  return Account.from_key(resolve_private_key())
846
855
 
847
856
  def to_wei_units(amount, decimals):
@@ -5358,7 +5367,7 @@ def main():
5358
5367
  check_version()
5359
5368
  args = sys.argv[1:] if len(sys.argv) > 1 else []
5360
5369
  # Global wallet selector: --as <agent>, stripped before command dispatch.
5361
- global _SELECTED_AGENT, _CLI_KEY
5370
+ global _SELECTED_AGENT, _CLI_KEY, _OWNER_ADDRESS
5362
5371
  if "--as" in args:
5363
5372
  i = args.index("--as")
5364
5373
  if i + 1 >= len(args):
@@ -5374,11 +5383,27 @@ def main():
5374
5383
  return
5375
5384
  _CLI_KEY = args[i + 1]
5376
5385
  del args[i:i + 2]
5386
+ # Public owner-address selector for read-only commands. Lets monitoring/sim jobs
5387
+ # inspect a wallet's Prime Account / positions without resolving or loading a key.
5388
+ if "--owner" in args:
5389
+ i = args.index("--owner")
5390
+ if i + 1 >= len(args):
5391
+ print("--owner requires an EVM address. Example: --owner 0xabc...")
5392
+ return
5393
+ try:
5394
+ _OWNER_ADDRESS = Web3.to_checksum_address(args[i + 1])
5395
+ except Exception:
5396
+ print(f"Invalid --owner address: {args[i + 1]}")
5397
+ return
5398
+ del args[i:i + 2]
5377
5399
  if not args or args[0] in ("-h", "--help"):
5378
5400
  print(__doc__)
5379
5401
  return
5380
5402
 
5381
5403
  cmd = args[0]
5404
+ if _OWNER_ADDRESS and cmd not in {"defi", "lb-positions"}:
5405
+ print("--owner is only supported for read-only commands: defi, lb-positions")
5406
+ return
5382
5407
  if cmd == "pool-info":
5383
5408
  # First positional after `pool-info` is the pool name; --json is an opt-in flag
5384
5409
  # that switches output from human tables to a compact JSON shape (one object for
@@ -214,6 +214,22 @@ def append_history(state_dir: str, entry: dict):
214
214
  path.write_text("\n".join(lines[-1000:]) + "\n")
215
215
 
216
216
 
217
+ NOTIFY_SCRIPT = os.path.expanduser("/root/.openclaw/workspace/scripts/notify.sh")
218
+
219
+
220
+ def _notify(text: str):
221
+ """Send a Telegram notification via the notify.sh script."""
222
+ if not os.path.exists(NOTIFY_SCRIPT):
223
+ return
224
+ try:
225
+ subprocess.run(
226
+ ["bash", NOTIFY_SCRIPT, text],
227
+ capture_output=True, timeout=30,
228
+ )
229
+ except Exception:
230
+ pass
231
+
232
+
217
233
  def load_baseline_equity(state_dir: str) -> float | None:
218
234
  """Load baseline equity for stop-loss tracking."""
219
235
  path = Path(state_dir) / "baseline-equity"
@@ -325,7 +341,46 @@ def run_tick(
325
341
  # 3. Compute health
326
342
  health = compute_health(defi_data, max_mult)
327
343
  health["tier"] = tier
344
+
345
+ # 4. Load strategy (do this early so rebalance mode is known before equity checks)
346
+ strategy = load_strategy(strategy_path)
347
+ mode = strategy.get("mode", "observer")
348
+ health["mode"] = mode
349
+ result.update(health)
350
+ result["mode"] = mode
351
+
352
+ # 5. Check for unfunded or unpriced accounts
328
353
  if health.get("error") == "equity near zero":
354
+ # Check if there are actual token balances without USD prices (RedStone off?)
355
+ has_balances = False
356
+ groups = defi_data.get("groups", [])
357
+ if groups:
358
+ for g in groups:
359
+ for s in g.get("supplied", []):
360
+ bal = s.get("balance", 0)
361
+ try:
362
+ if float(bal) > 0:
363
+ has_balances = True
364
+ break
365
+ except (ValueError, TypeError):
366
+ pass
367
+ if has_balances:
368
+ break
369
+ else:
370
+ for s in defi_data.get("supplied", []):
371
+ bal = s.get("balance", 0)
372
+ try:
373
+ if float(bal) > 0:
374
+ has_balances = True
375
+ break
376
+ except (ValueError, TypeError):
377
+ pass
378
+
379
+ if has_balances:
380
+ # Positions exist but USD prices unavailable — skip this tick
381
+ result["action"] = "skip (unpriced positions)"
382
+ return result
383
+
329
384
  # Only escalate if there's actual debt — an empty unfunded wallet is not an emergency
330
385
  if health.get("debt_usd", 0) and health["debt_usd"] > 0.5:
331
386
  write_escalation(state_dir, "equity-near-zero", {
@@ -338,17 +393,9 @@ def run_tick(
338
393
  result["mode"] = "escalated"
339
394
  else:
340
395
  result["action"] = "none (unfunded account)"
341
- result.update(health)
342
396
  return result
343
397
 
344
- # 4. Load strategy (position/market/side are optional hints now — auto-detected from defi_data)
345
- strategy = load_strategy(strategy_path)
346
- mode = strategy.get("mode", "observer")
347
- health["mode"] = mode
348
- result.update(health)
349
- result["mode"] = mode
350
-
351
- # 5. Health swing detection (always)
398
+ # 6. Health swing detection (always)
352
399
  last_pct = load_last_health(state_dir)
353
400
  if last_pct is not None and health["health_pct"] is not None:
354
401
  diff = abs(health["health_pct"] - last_pct)
@@ -363,7 +410,7 @@ def run_tick(
363
410
  result["escalation"] = "health_swing"
364
411
  save_last_health(state_dir, health["health_pct"] or 0.0)
365
412
 
366
- # 6. Append to history
413
+ # 7. Append to history
367
414
  entry = {
368
415
  "ts": now_iso, "mode": mode,
369
416
  "pct": health["health_pct"],
@@ -373,7 +420,7 @@ def run_tick(
373
420
  }
374
421
  append_history(state_dir, entry)
375
422
 
376
- # 7. Rebalance mode logic
423
+ # 8. Rebalance mode logic
377
424
  if mode == "rebalance":
378
425
  # Valuation gate: never auto-lever/de-lever on incomplete or untrustworthy data
379
426
  # (missing RedStone feed → unpriced position → wrong equity/debt/health). Escalate
@@ -399,6 +446,7 @@ def run_tick(
399
446
  position_type = strategy.get("position", "")
400
447
  market = strategy.get("market", "avax-usdc")
401
448
  side = strategy.get("side", "short")
449
+ swap_target = strategy.get("swap_target", "")
402
450
  low, high = target_range[0], target_range[1]
403
451
 
404
452
  pct = health["health_pct"]
@@ -465,16 +513,73 @@ def run_tick(
465
513
  return result
466
514
 
467
515
  if repay_amt > raw_usdc:
468
- # Need to withdraw from position first escalate
469
- write_escalation(state_dir, "repay-no-usdc", {
470
- "reason": "repay_needs_position_close",
471
- "repay_needed": repay_amt,
472
- "raw_usdc": raw_usdc,
473
- "health_pct": pct,
474
- "label": label,
475
- })
476
- result["action"] = "escalate (need close)"
477
- return result
516
+ # Build supply_rows from defi_data for potential swap source
517
+ supply_rows = []
518
+ groups = defi_data.get("groups", [])
519
+ if groups:
520
+ for g in groups:
521
+ supply_rows.extend(g.get("supplied", []))
522
+ else:
523
+ supply_rows.extend(defi_data.get("supplied", []))
524
+
525
+ # Need more USDC — try to swap from swap_target if configured
526
+ if swap_target:
527
+ for s in list(supply_rows):
528
+ sym = s.get("symbol", "")
529
+ usd_val = s.get("usd", 0) or 0
530
+ if sym.upper() == swap_target.upper() and usd_val > 1:
531
+ raw_amt = s.get("amount", 0)
532
+ swap_token_amt = raw_amt * 0.95
533
+ if swap_token_amt < 0.001:
534
+ break
535
+ try:
536
+ sr = subprocess.run(
537
+ [sys.executable, tool_path, "swap",
538
+ "--from", swap_target,
539
+ "--to", "USDC",
540
+ "--amount", f"{swap_token_amt:.6f}",
541
+ "--slippage", "1.0",
542
+ "--execute"],
543
+ capture_output=True, text=True, timeout=180,
544
+ )
545
+ if sr.returncode == 0:
546
+ result["swap"] = f"swapped {swap_token_amt:.4f} {swap_target} -> USDC"
547
+ else:
548
+ write_escalation(state_dir, "repay-swap-failed", {
549
+ "reason": "repay_swap_failed",
550
+ "swap_source": swap_target,
551
+ "swap_amount": swap_token_amt,
552
+ "stderr": sr.stderr[:200],
553
+ "health_pct": pct,
554
+ "label": label,
555
+ })
556
+ result["error"] = f"swap failed: {sr.stderr[:200]}"
557
+ result["action"] = "escalate (swap failed)"
558
+ return result
559
+ except Exception as e:
560
+ result["error"] = f"swap error: {e}"
561
+ return result
562
+ break
563
+ else:
564
+ write_escalation(state_dir, "repay-no-usdc", {
565
+ "reason": "repay_needs_position_close",
566
+ "repay_needed": repay_amt,
567
+ "raw_usdc": raw_usdc,
568
+ "health_pct": pct,
569
+ "label": label,
570
+ })
571
+ result["action"] = "escalate (need close)"
572
+ return result
573
+ else:
574
+ write_escalation(state_dir, "repay-no-usdc", {
575
+ "reason": "repay_needs_position_close",
576
+ "repay_needed": repay_amt,
577
+ "raw_usdc": raw_usdc,
578
+ "health_pct": pct,
579
+ "label": label,
580
+ })
581
+ result["action"] = "escalate (need close)"
582
+ return result
478
583
 
479
584
  if dry_run:
480
585
  result["action"] = f"would repay ${repay_amt:.2f} USDC"
@@ -490,6 +595,7 @@ def run_tick(
490
595
  if r.returncode == 0:
491
596
  cooldown_file.write_text(str(int(time.time())))
492
597
  result["action"] = f"repaid ${repay_amt:.2f}"
598
+ _notify(f"🔄 Rebalance: {label} repaid ${repay_amt:.2f} USDC (health was {pct}%)")
493
599
  else:
494
600
  result["error"] = f"repay failed: {r.stderr[:200]}"
495
601
  except Exception as e:
@@ -527,99 +633,126 @@ def run_tick(
527
633
  result["error"] = f"borrow error: {e}"
528
634
  return result
529
635
 
530
- # Deploy into whatever positions are open (detected dynamically from defi_data)
531
- has_gmx = health.get("has_gmx", False)
532
- has_lb = health.get("has_lb", False)
533
- has_aero = health.get("has_aero", False)
534
- open_positions = []
535
- if has_gmx: open_positions.append("gmx")
536
- if has_lb: open_positions.append("lb")
537
- if has_aero: open_positions.append("aero")
538
-
539
- if not open_positions:
540
- # No open positions — just borrow and leave as USDC (or deploy to default)
541
- result["action"] = f"borrowed ${borrow_amt:.2f} (no positions to deploy into)"
542
- cooldown_file.write_text(str(int(time.time())))
636
+ # If swap_target is set, deploy by swapping borrowed USDC to that token
637
+ if swap_target and swap_target.upper() != "USDC":
638
+ split_amt = borrow_amt
639
+ try:
640
+ sr = subprocess.run(
641
+ [sys.executable, tool_path, "swap",
642
+ "--from", "USDC",
643
+ "--to", swap_target,
644
+ "--amount", f"{split_amt:.2f}",
645
+ "--slippage", "1.0",
646
+ "--execute"],
647
+ capture_output=True, text=True, timeout=180,
648
+ )
649
+ if sr.returncode == 0:
650
+ cooldown_file.write_text(str(int(time.time())))
651
+ result["action"] = f"borrowed ${borrow_amt:.2f}, swapped {split_amt:.2f} USDC -> {swap_target}"
652
+ _notify(f"🔄 Rebalance: {label} borrowed ${borrow_amt:.2f} USDC \u2192 swapped to {swap_target} (health was {pct}%)")
653
+ else:
654
+ result["warning"] = f"swap to {swap_target} failed after borrow: {sr.stderr[:200]}"
655
+ cooldown_file.write_text(str(int(time.time())))
656
+ except Exception as e:
657
+ result["error"] = f"borrow+swap error: {e}"
658
+ cooldown_file.write_text(str(int(time.time())))
659
+
543
660
  else:
544
- # Split borrow amount proportionally across open positions
545
- split_amt = borrow_amt / len(open_positions)
546
- deployed_ok = 0
547
- deployed_fail = 0
548
-
549
- for pos_type in open_positions:
550
- if pos_type == "gmx":
551
- # Use market/side from strategy as hint, fall back to sensible defaults
552
- mkt = strategy.get("market", "avax-usdc") if tool_path else "avax-usdc"
553
- sd = strategy.get("side", "long") if tool_path else "long"
554
- try:
555
- r = subprocess.run(
556
- [sys.executable, tool_path, "gmx-deposit",
557
- "--market", mkt, "--amount", f"{split_amt:.2f}",
558
- "--side", sd, "--fee-buffer", "1.5", "--execute"],
559
- capture_output=True, text=True, timeout=120,
560
- )
561
- if r.returncode == 0:
562
- deployed_ok += 1
563
- else:
564
- result["warning"] = f"gmx deposit failed: {r.stderr[:200]}"
565
- deployed_fail += 1
566
- except Exception as e:
567
- result["error"] = f"gmx deposit error: {e}"
568
- deployed_fail += 1
569
-
570
- elif pos_type == "lb":
571
- # Detect LB pair from defi data (look for "TraderJoe V2 LB" group)
572
- lb_pairs = []
573
- for g in defi_data.get("groups", []):
574
- if g.get("type") == "TraderJoe V2 LB":
575
- for item in g.get("items", []):
576
- label = item.get("label", "")
577
- m = re.match(r'\[([^\]]+)\]', label)
578
- if m:
579
- lb_pairs.append(m.group(1))
580
-
581
- # Skip if tool doesn't support lb-add (degenprime)
582
- tool_bn = os.path.basename(tool_path) if tool_path else ""
583
- if "degenprime" in tool_bn:
584
- result["action"] = f"lb-add not available on degenprime — leaving ${split_amt:.2f} as USDC"
585
- deployed_fail += 1
586
- elif not lb_pairs:
587
- result["warning"] = f"has_lb=True but no LB pair found in defi data — leaving ${split_amt:.2f} as USDC"
588
- deployed_fail += 1
589
- else:
590
- pair_key = lb_pairs[0]
661
+ # Deploy into whatever positions are open (detected dynamically from defi_data)
662
+ has_gmx = health.get("has_gmx", False)
663
+ has_lb = health.get("has_lb", False)
664
+ has_aero = health.get("has_aero", False)
665
+ open_positions = []
666
+ if has_gmx: open_positions.append("gmx")
667
+ if has_lb: open_positions.append("lb")
668
+ if has_aero: open_positions.append("aero")
669
+
670
+ if not open_positions:
671
+ # No open positions — just borrow and leave as USDC (or deploy to default)
672
+ result["action"] = f"borrowed ${borrow_amt:.2f} (no positions to deploy into)"
673
+ cooldown_file.write_text(str(int(time.time())))
674
+ _notify(f"🔄 Rebalance: {label} borrowed ${borrow_amt:.2f} USDC (no positions to deploy, health was {pct}%)")
675
+ else:
676
+ # Split borrow amount proportionally across open positions
677
+ split_amt = borrow_amt / len(open_positions)
678
+ deployed_ok = 0
679
+ deployed_fail = 0
680
+
681
+ for pos_type in open_positions:
682
+ if pos_type == "gmx":
683
+ # Use market/side from strategy as hint, fall back to sensible defaults
684
+ mkt = strategy.get("market", "avax-usdc") if tool_path else "avax-usdc"
685
+ sd = strategy.get("side", "long") if tool_path else "long"
591
686
  try:
592
687
  r = subprocess.run(
593
- [sys.executable, tool_path, "lb-add",
594
- "--pair", pair_key,
595
- "--amount-x", "0",
596
- "--amount-y", f"{split_amt:.2f}",
597
- "--shape", "spot",
598
- "--range", "15",
599
- "--execute"],
688
+ [sys.executable, tool_path, "gmx-deposit",
689
+ "--market", mkt, "--amount", f"{split_amt:.2f}",
690
+ "--side", sd, "--fee-buffer", "1.5", "--execute"],
600
691
  capture_output=True, text=True, timeout=120,
601
692
  )
602
693
  if r.returncode == 0:
603
694
  deployed_ok += 1
604
695
  else:
605
- result["warning"] = f"lb-add failed: {r.stderr[:200]}"
696
+ result["warning"] = f"gmx deposit failed: {r.stderr[:200]}"
606
697
  deployed_fail += 1
607
698
  except Exception as e:
608
- result["error"] = f"lb-add error: {e}"
699
+ result["error"] = f"gmx deposit error: {e}"
609
700
  deployed_fail += 1
610
701
 
611
- elif pos_type == "aero":
612
- # Aerodrome CL: degenprime has read-only aerodrome-positions,
613
- # but no deposit/withdraw commands yet (write paths deferred to
614
- # v2 on-chain signatures vary by Aerodrome version).
615
- # Use `degenprime aerodrome-positions` to list your NFT tokenIds.
616
- result["action"] = f"aero deposit not yet supported (read-only via aerodrome-positions, writes deferred to v2) — leaving ${split_amt:.2f} as USDC"
702
+ elif pos_type == "lb":
703
+ # Detect LB pair from defi data (look for "TraderJoe V2 LB" group)
704
+ lb_pairs = []
705
+ for g in defi_data.get("groups", []):
706
+ if g.get("type") == "TraderJoe V2 LB":
707
+ for item in g.get("items", []):
708
+ label = item.get("label", "")
709
+ m = re.match(r'\[([^\]]+)\]', label)
710
+ if m:
711
+ lb_pairs.append(m.group(1))
712
+
713
+ # Skip if tool doesn't support lb-add (degenprime)
714
+ tool_bn = os.path.basename(tool_path) if tool_path else ""
715
+ if "degenprime" in tool_bn:
716
+ result["action"] = f"lb-add not available on degenprime — leaving ${split_amt:.2f} as USDC"
717
+ deployed_fail += 1
718
+ elif not lb_pairs:
719
+ result["warning"] = f"has_lb=True but no LB pair found in defi data — leaving ${split_amt:.2f} as USDC"
720
+ deployed_fail += 1
721
+ else:
722
+ pair_key = lb_pairs[0]
723
+ try:
724
+ r = subprocess.run(
725
+ [sys.executable, tool_path, "lb-add",
726
+ "--pair", pair_key,
727
+ "--amount-x", "0",
728
+ "--amount-y", f"{split_amt:.2f}",
729
+ "--shape", "spot",
730
+ "--range", "15",
731
+ "--execute"],
732
+ capture_output=True, text=True, timeout=120,
733
+ )
734
+ if r.returncode == 0:
735
+ deployed_ok += 1
736
+ else:
737
+ result["warning"] = f"lb-add failed: {r.stderr[:200]}"
738
+ deployed_fail += 1
739
+ except Exception as e:
740
+ result["error"] = f"lb-add error: {e}"
741
+ deployed_fail += 1
617
742
 
618
- if deployed_ok > 0:
619
- cooldown_file.write_text(str(int(time.time())))
620
- result["action"] = f"borrowed ${borrow_amt:.2f}, deployed ${split_amt:.2f} to {deployed_ok} position(s)"
621
- else:
622
- result["warning"] = f"borrow ok but all deposits failed"
743
+ elif pos_type == "aero":
744
+ # Aerodrome CL: degenprime has read-only aerodrome-positions,
745
+ # but no deposit/withdraw commands yet (write paths deferred to
746
+ # v2 — on-chain signatures vary by Aerodrome version).
747
+ # Use `degenprime aerodrome-positions` to list your NFT tokenIds.
748
+ result["action"] = f"aero deposit not yet supported (read-only via aerodrome-positions, writes deferred to v2) — leaving ${split_amt:.2f} as USDC"
749
+
750
+ if deployed_ok > 0:
751
+ cooldown_file.write_text(str(int(time.time())))
752
+ result["action"] = f"borrowed ${borrow_amt:.2f}, deployed ${split_amt:.2f} to {deployed_ok} position(s)"
753
+ _notify(f"🔄 Rebalance: {label} borrowed ${borrow_amt:.2f} USDC, deployed {split_amt:.2f} to {deployed_ok} position(s) (health was {pct}%)")
754
+ else:
755
+ result["warning"] = f"borrow ok but all deposits failed"
623
756
 
624
757
  return result
625
758
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: primecli
3
- Version: 0.7.3
3
+ Version: 0.7.5
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.7.3 The 0.x line is pre-1.0, so breaking changes are possible. See [Releases](https://github.com/Mnemosyne-quest/primecli/releases).
50
+ **Current version:** 0.7.5 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.7.3"
7
+ version = "0.7.5"
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