primecli 0.7.4__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.
- {primecli-0.7.4 → primecli-0.7.5}/PKG-INFO +2 -2
- {primecli-0.7.4 → primecli-0.7.5}/README.md +1 -1
- {primecli-0.7.4 → primecli-0.7.5}/primecli/arbprime.py +26 -1
- {primecli-0.7.4 → primecli-0.7.5}/primecli/deltaprime.py +26 -1
- {primecli-0.7.4 → primecli-0.7.5}/primecli/health_monitor.py +234 -101
- {primecli-0.7.4 → primecli-0.7.5}/primecli.egg-info/PKG-INFO +2 -2
- {primecli-0.7.4 → primecli-0.7.5}/pyproject.toml +1 -1
- {primecli-0.7.4 → primecli-0.7.5}/LICENSE +0 -0
- {primecli-0.7.4 → primecli-0.7.5}/primecli/__init__.py +0 -0
- {primecli-0.7.4 → primecli-0.7.5}/primecli/degenprime.py +0 -0
- {primecli-0.7.4 → primecli-0.7.5}/primecli.egg-info/SOURCES.txt +0 -0
- {primecli-0.7.4 → primecli-0.7.5}/primecli.egg-info/dependency_links.txt +0 -0
- {primecli-0.7.4 → primecli-0.7.5}/primecli.egg-info/entry_points.txt +0 -0
- {primecli-0.7.4 → primecli-0.7.5}/primecli.egg-info/requires.txt +0 -0
- {primecli-0.7.4 → primecli-0.7.5}/primecli.egg-info/top_level.txt +0 -0
- {primecli-0.7.4 → primecli-0.7.5}/setup.cfg +0 -0
- {primecli-0.7.4 → primecli-0.7.5}/tests/test_cross_file_identity.py +0 -0
- {primecli-0.7.4 → primecli-0.7.5}/tests/test_gas_limit.py +0 -0
- {primecli-0.7.4 → primecli-0.7.5}/tests/test_gas_pricing.py +0 -0
- {primecli-0.7.4 → primecli-0.7.5}/tests/test_health_meter.py +0 -0
- {primecli-0.7.4 → primecli-0.7.5}/tests/test_health_monitor.py +0 -0
- {primecli-0.7.4 → primecli-0.7.5}/tests/test_paraswap_validator.py +0 -0
- {primecli-0.7.4 → primecli-0.7.5}/tests/test_redstone_encoding.py +0 -0
- {primecli-0.7.4 → 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
|
+
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.
|
|
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.
|
|
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
|
|
@@ -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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
#
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
#
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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, "
|
|
594
|
-
"--
|
|
595
|
-
"--
|
|
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"
|
|
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"
|
|
699
|
+
result["error"] = f"gmx deposit error: {e}"
|
|
609
700
|
deployed_fail += 1
|
|
610
701
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
+
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.
|
|
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.
|
|
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
|
|
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
|