primecli 0.5.2__tar.gz → 0.5.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {primecli-0.5.2 → primecli-0.5.4}/PKG-INFO +2 -3
- {primecli-0.5.2 → primecli-0.5.4}/primecli/arbprime.py +73 -4
- {primecli-0.5.2 → primecli-0.5.4}/primecli/degenprime.py +70 -5
- {primecli-0.5.2 → primecli-0.5.4}/primecli/deltaprime.py +94 -16
- {primecli-0.5.2 → primecli-0.5.4}/primecli/health_monitor.py +47 -14
- {primecli-0.5.2 → primecli-0.5.4}/primecli.egg-info/PKG-INFO +2 -3
- {primecli-0.5.2 → primecli-0.5.4}/pyproject.toml +2 -2
- {primecli-0.5.2 → primecli-0.5.4}/LICENSE +0 -0
- {primecli-0.5.2 → primecli-0.5.4}/README.md +0 -0
- {primecli-0.5.2 → primecli-0.5.4}/primecli/__init__.py +0 -0
- {primecli-0.5.2 → primecli-0.5.4}/primecli.egg-info/SOURCES.txt +0 -0
- {primecli-0.5.2 → primecli-0.5.4}/primecli.egg-info/dependency_links.txt +0 -0
- {primecli-0.5.2 → primecli-0.5.4}/primecli.egg-info/entry_points.txt +0 -0
- {primecli-0.5.2 → primecli-0.5.4}/primecli.egg-info/requires.txt +0 -0
- {primecli-0.5.2 → primecli-0.5.4}/primecli.egg-info/top_level.txt +0 -0
- {primecli-0.5.2 → primecli-0.5.4}/setup.cfg +0 -0
- {primecli-0.5.2 → primecli-0.5.4}/tests/test_cross_file_identity.py +0 -0
- {primecli-0.5.2 → primecli-0.5.4}/tests/test_gas_pricing.py +0 -0
- {primecli-0.5.2 → primecli-0.5.4}/tests/test_health_monitor.py +0 -0
- {primecli-0.5.2 → primecli-0.5.4}/tests/test_paraswap_validator.py +0 -0
- {primecli-0.5.2 → primecli-0.5.4}/tests/test_redstone_encoding.py +0 -0
- {primecli-0.5.2 → primecli-0.5.4}/tests/test_to_wei_units.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: primecli
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.4
|
|
4
4
|
Summary: Agent-friendly CLI tools for the DeltaPrime (Avalanche + Arbitrum) and DegenPrime (Base) lending and leverage protocols. Preview-by-default; no Etherscan key required.
|
|
5
5
|
Author: Mnemosyne-quest contributors
|
|
6
6
|
License: MIT
|
|
@@ -28,7 +28,6 @@ Requires-Dist: eth-account>=0.13
|
|
|
28
28
|
Requires-Dist: eth-keys>=0.5
|
|
29
29
|
Requires-Dist: eth-abi<7,>=5.0
|
|
30
30
|
Requires-Dist: requests>=2.31
|
|
31
|
-
Dynamic: license-file
|
|
32
31
|
|
|
33
32
|
# primecli
|
|
34
33
|
|
|
@@ -57,6 +57,13 @@ prime-summary reports live solvency (health ratio, total value, debt, solvent fl
|
|
|
57
57
|
SolvencyFacetProdArbitrum, read via eth_call with a RedStone price payload appended (falls
|
|
58
58
|
back to balances-only if the gateway is unreachable).
|
|
59
59
|
|
|
60
|
+
NOTE: prime-summary shows TWO health metrics — don't confuse them:
|
|
61
|
+
- "Health ratio (chain)": on-chain getHealthRatio. 1.0 = liquidation, >1.0 = solvent.
|
|
62
|
+
- "Health (0-100%)": equity-based, frontend-style. 0% = liquidation, 50% = half
|
|
63
|
+
borrowing power used, 100% = no debt.
|
|
64
|
+
Formula for the latter: equity=supplied-debt, max_debt=equity*(tier-1),
|
|
65
|
+
pct=(max_debt-debt)/max_debt*100. tier=5 (BASIC) or 10 (PREMIUM).
|
|
66
|
+
|
|
60
67
|
Collateral withdrawal is a two-step, time-delayed flow on the Prime Account (there is NO
|
|
61
68
|
instant withdraw of in-account collateral). The savings-pool `withdraw` above is a separate
|
|
62
69
|
two-step intent flow on the pool itself, not the Prime Account. Step 1: `withdraw --pool X
|
|
@@ -227,7 +234,7 @@ EXPLORER = "https://arbiscan.io" # display/links only — never used for ABI fe
|
|
|
227
234
|
CHAIN_ID = 42161
|
|
228
235
|
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
|
|
229
236
|
# ── Agent / wallet selection ─────────────────────────────────────────────────
|
|
230
|
-
# Agent-agnostic: any agent (Parakletos, Paraklaudios, or another
|
|
237
|
+
# Agent-agnostic: any agent (Parakletos, Paraklaudios, or another authorized agent) runs
|
|
231
238
|
# this same tool with its OWN wallet. The Prime Account is derived on-chain from the
|
|
232
239
|
# wallet owner (getLoanForOwner), so each agent automatically operates on its own
|
|
233
240
|
# Prime Account — no per-agent addresses are hardcoded. The same EVM key works on
|
|
@@ -310,8 +317,8 @@ REDSTONE_GATEWAYS = [
|
|
|
310
317
|
]
|
|
311
318
|
|
|
312
319
|
# Active pool proxies — LIVE Arbitrum deployment, on-chain verified 2026-06-03.
|
|
313
|
-
# getAllPoolAssets live = [USDC, DAI, BTC, ARB, ETH]; the DAI pool is DROPPED
|
|
314
|
-
#
|
|
320
|
+
# getAllPoolAssets live = [USDC, DAI, BTC, ARB, ETH]; the DAI pool is DROPPED,
|
|
321
|
+
# leaving 4. The native-wrapped pool is `eth` (underlying WETH, account symbol
|
|
315
322
|
# "ETH"): its native deposit path uses depositNativeToken() (wraps ETH -> WETH).
|
|
316
323
|
POOLS = {
|
|
317
324
|
"eth": {
|
|
@@ -1961,6 +1968,49 @@ def gather_lending(w3, account):
|
|
|
1961
1968
|
r["usd"] = None
|
|
1962
1969
|
return out
|
|
1963
1970
|
|
|
1971
|
+
def _compute_health_pct(data: dict, tier_code: int = 0) -> dict:
|
|
1972
|
+
"""Compute equity-based health (0-100%) from gather_lending data + tier.
|
|
1973
|
+
|
|
1974
|
+
DeltaPrime has *two* health metrics that agents must not confuse:
|
|
1975
|
+
|
|
1976
|
+
1. health_ratio (on-chain, getHealthRatio): 1.0 = liquidation, >1.0 = solvent.
|
|
1977
|
+
This is the raw weighted-collateral / debt ratio from the SolvencyFacet.
|
|
1978
|
+
|
|
1979
|
+
2. health_pct (equity-based, 0-100%): the scale used in the DeltaPrime frontend
|
|
1980
|
+
and the account-health-monitor cron. 0% = liquidation, 100% = no debt.
|
|
1981
|
+
Formula:
|
|
1982
|
+
equity = supplied_usd - debt_usd
|
|
1983
|
+
max_mult = 10 if PREMIUM tier else 5 if BASIC
|
|
1984
|
+
max_debt = equity * (max_mult - 1)
|
|
1985
|
+
health_pct = (max_debt - debt_usd) / max_debt * 100
|
|
1986
|
+
|
|
1987
|
+
Returns dict with keys: health_pct, supplied_usd, debt_usd, equity, max_debt,
|
|
1988
|
+
tier_label, or error.
|
|
1989
|
+
"""
|
|
1990
|
+
supplied_usd = sum(r.get("usd", 0) or 0 for r in data.get("supplied", []))
|
|
1991
|
+
debt_usd = sum(r.get("usd", 0) or 0 for r in data.get("borrowed", []))
|
|
1992
|
+
equity = supplied_usd - debt_usd
|
|
1993
|
+
tier_labels = {0: "BASIC", 1: "PREMIUM", 2: "_NON_EXISTENT"}
|
|
1994
|
+
tier_label = tier_labels.get(tier_code, str(tier_code))
|
|
1995
|
+
max_mult = {0: 5, 1: 10}.get(tier_code, 5)
|
|
1996
|
+
|
|
1997
|
+
if equity <= 0.01:
|
|
1998
|
+
return {"health_pct": 0.0, "supplied_usd": round(supplied_usd, 2),
|
|
1999
|
+
"debt_usd": round(debt_usd, 2), "equity": round(equity, 2),
|
|
2000
|
+
"max_debt": 0.0, "tier": tier_label, "error": "equity near zero"}
|
|
2001
|
+
|
|
2002
|
+
max_debt = equity * (max_mult - 1)
|
|
2003
|
+
if max_debt > 0 and debt_usd >= 0:
|
|
2004
|
+
health_pct = (max_debt - min(debt_usd, max_debt)) / max_debt * 100
|
|
2005
|
+
health_pct = max(0.0, min(100.0, health_pct))
|
|
2006
|
+
else:
|
|
2007
|
+
health_pct = 100.0
|
|
2008
|
+
|
|
2009
|
+
return {"health_pct": round(health_pct, 1), "supplied_usd": round(supplied_usd, 2),
|
|
2010
|
+
"debt_usd": round(debt_usd, 2), "equity": round(equity, 2),
|
|
2011
|
+
"max_debt": round(max_debt, 2), "tier": tier_label}
|
|
2012
|
+
|
|
2013
|
+
|
|
1964
2014
|
def cmd_prime_summary():
|
|
1965
2015
|
w3 = get_w3()
|
|
1966
2016
|
acct = get_account()
|
|
@@ -2004,7 +2054,23 @@ def cmd_prime_summary():
|
|
|
2004
2054
|
# gather_lending nulls the ratio when debt is negligible (the raw value is
|
|
2005
2055
|
# astronomically large there); render that as ">1000" rather than a junk number.
|
|
2006
2056
|
ratio_str = ">1000.00 (negligible debt)" if ratio is None else f"{ratio:.4f}"
|
|
2007
|
-
print(f" Health ratio:
|
|
2057
|
+
print(f" Health ratio (chain): {ratio_str} (>1.0 = solvent, 1.0 = liquidation)")
|
|
2058
|
+
# ─── Equity-based health (0-100%) ───
|
|
2059
|
+
# Different from health_ratio! See _compute_health_pct docstring.
|
|
2060
|
+
# Get tier from the Prime Account (oracle-free view)
|
|
2061
|
+
try:
|
|
2062
|
+
tier_info = gather_prime_tier(w3, acct, account)
|
|
2063
|
+
tier_code = tier_info.get("tier_code", 0)
|
|
2064
|
+
except Exception:
|
|
2065
|
+
tier_code = 0
|
|
2066
|
+
hp = _compute_health_pct(data, tier_code)
|
|
2067
|
+
if "error" not in hp:
|
|
2068
|
+
print(f" Health (0-100%): {hp['health_pct']:.1f}%")
|
|
2069
|
+
print(f" (supplied=${hp['supplied_usd']:.2f}, debt=${hp['debt_usd']:.2f},"
|
|
2070
|
+
f" equity=${hp['equity']:.2f}, max_debt=${hp['max_debt']:.2f}, {hp['tier']})")
|
|
2071
|
+
print(f" 0%=liquidation 50%=half borrowing power used 100%=no debt")
|
|
2072
|
+
else:
|
|
2073
|
+
print(f" Health (0-100%): N/A ({hp['error']})")
|
|
2008
2074
|
print(f" Solvent: {'yes' if data['solvent'] else 'NO — liquidatable'}")
|
|
2009
2075
|
else:
|
|
2010
2076
|
print(f" Health/solvency: RedStone fetch/call failed ({data.get('solvency_error', 'error')}); "
|
|
@@ -5005,9 +5071,12 @@ def gather_defi() -> dict:
|
|
|
5005
5071
|
result["total_usd"] = lending["total_value_usd"]
|
|
5006
5072
|
result["health_ratio"] = lending["health_ratio"]
|
|
5007
5073
|
result["solvent"] = lending["solvent"]
|
|
5074
|
+
# Compute equity-based health (0-100%) from lending data + tier
|
|
5075
|
+
result["health_pct"] = _compute_health_pct(lending, tier.get("tier_code", 0)).get("health_pct")
|
|
5008
5076
|
if lending["supplied"] or lending["borrowed"]:
|
|
5009
5077
|
result["groups"].append({
|
|
5010
5078
|
"type": "Lending / Leverage", "health_ratio": lending["health_ratio"],
|
|
5079
|
+
"health_pct": result["health_pct"],
|
|
5011
5080
|
"supplied": [{"symbol": r["symbol"], "balance": r["balance"], "usd": r.get("usd")}
|
|
5012
5081
|
for r in lending["supplied"]],
|
|
5013
5082
|
"borrowed": [{"symbol": r["symbol"], "balance": r["balance"], "usd": r.get("usd")}
|
|
@@ -112,6 +112,7 @@ health_monitor = _hm
|
|
|
112
112
|
BASE_RPC = os.environ.get("DEGENPRIME_RPC", "https://base.publicnode.com")
|
|
113
113
|
EXPLORER = "https://basescan.org"
|
|
114
114
|
CHAIN_ID = 8453
|
|
115
|
+
DEGEN_MAX_MULT = 5 # Fixed max leverage multiplier (no tier system on DegenPrime)
|
|
115
116
|
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
|
|
116
117
|
|
|
117
118
|
# ── Signing key resolution ──────────────────────────────────────────────────
|
|
@@ -570,6 +571,41 @@ def _asset_meta(w3, symbol: str):
|
|
|
570
571
|
# gateway once instead of per-feed-symbol. Cleared implicitly when the process exits.
|
|
571
572
|
_redstone_gateway_cache = None
|
|
572
573
|
|
|
574
|
+
def _compute_health_pct(supplied: list, borrowed: list, max_mult: int = None) -> dict:
|
|
575
|
+
"""Compute equity-based health (0-100%) from per-asset supplied/borrowed data.
|
|
576
|
+
|
|
577
|
+
0% = liquidation, 50% = half borrowing power used, 100% = no debt.
|
|
578
|
+
Formula:
|
|
579
|
+
equity = supplied_usd - debt_usd
|
|
580
|
+
max_mult = 5 (fixed for DegenPrime; no tier system)
|
|
581
|
+
max_debt = equity * (max_mult - 1)
|
|
582
|
+
health_pct = (max_debt - debt_usd) / max_debt * 100
|
|
583
|
+
|
|
584
|
+
Returns dict with keys: health_pct, supplied_usd, debt_usd, equity, max_debt, or error.
|
|
585
|
+
"""
|
|
586
|
+
if max_mult is None:
|
|
587
|
+
max_mult = DEGEN_MAX_MULT
|
|
588
|
+
supplied_usd = sum(r.get("usd", 0) or 0 for r in supplied)
|
|
589
|
+
debt_usd = sum(r.get("usd", 0) or 0 for r in borrowed)
|
|
590
|
+
equity = supplied_usd - debt_usd
|
|
591
|
+
|
|
592
|
+
if equity <= 0.01:
|
|
593
|
+
return {"health_pct": 0.0, "supplied_usd": round(supplied_usd, 2),
|
|
594
|
+
"debt_usd": round(debt_usd, 2), "equity": round(equity, 2),
|
|
595
|
+
"max_debt": 0.0, "error": "equity near zero"}
|
|
596
|
+
|
|
597
|
+
max_debt = equity * (max_mult - 1)
|
|
598
|
+
if max_debt > 0 and debt_usd >= 0:
|
|
599
|
+
health_pct = (max_debt - min(debt_usd, max_debt)) / max_debt * 100
|
|
600
|
+
health_pct = max(0.0, min(100.0, health_pct))
|
|
601
|
+
else:
|
|
602
|
+
health_pct = 100.0
|
|
603
|
+
|
|
604
|
+
return {"health_pct": round(health_pct, 1), "supplied_usd": round(supplied_usd, 2),
|
|
605
|
+
"debt_usd": round(debt_usd, 2), "equity": round(equity, 2),
|
|
606
|
+
"max_debt": round(max_debt, 2), "tier": "FIXED_5X"}
|
|
607
|
+
|
|
608
|
+
|
|
573
609
|
def _redstone_fetch_packages(use_cache: bool = True) -> dict:
|
|
574
610
|
"""Fetch the latest signed price packages from the RedStone gateway. Returns the
|
|
575
611
|
per-feed map: {feedSymbol: [package, ...]} with one package per signer. The per-run
|
|
@@ -1458,11 +1494,19 @@ def gather_defi() -> dict:
|
|
|
1458
1494
|
row["usd"] = round(amt * usd, 2)
|
|
1459
1495
|
return row
|
|
1460
1496
|
|
|
1497
|
+
_rows_supplied = [_row(r) for r in supplied]
|
|
1498
|
+
_rows_borrowed = [_row(r) for r in borrowed]
|
|
1499
|
+
|
|
1500
|
+
# Compute equity-based health (0-100%)
|
|
1501
|
+
_hp = _compute_health_pct(_rows_supplied, _rows_borrowed)
|
|
1502
|
+
result["health_pct"] = _hp.get("health_pct")
|
|
1503
|
+
|
|
1461
1504
|
if supplied or borrowed:
|
|
1462
1505
|
result["groups"].append({
|
|
1463
1506
|
"type": "Lending / Leverage", "health_ratio": solvency["ratio"],
|
|
1464
|
-
"
|
|
1465
|
-
"
|
|
1507
|
+
"health_pct": result["health_pct"],
|
|
1508
|
+
"supplied": _rows_supplied,
|
|
1509
|
+
"borrowed": _rows_borrowed,
|
|
1466
1510
|
})
|
|
1467
1511
|
|
|
1468
1512
|
# Savings: the EOA's own pool deposits ("Diamond Hands"), independent of the Degen
|
|
@@ -1578,16 +1622,22 @@ def cmd_summary(as_json: bool = False):
|
|
|
1578
1622
|
row["usd"] = round(row["amount"] * usd, 2)
|
|
1579
1623
|
return row
|
|
1580
1624
|
|
|
1625
|
+
# Compute equity-based health (0-100%)
|
|
1626
|
+
_hp_rows = [_asset_row(r) for r in supplied]
|
|
1627
|
+
_hp_borrowed = [_asset_row(r) for r in borrowed]
|
|
1628
|
+
_hp = _compute_health_pct(_hp_rows, _hp_borrowed)
|
|
1629
|
+
|
|
1581
1630
|
out = {
|
|
1582
1631
|
"wallet": acct.address,
|
|
1583
1632
|
"account": pa,
|
|
1584
1633
|
"nativeBalance": pa_eth if pa_eth >= 1e-9 else None,
|
|
1585
|
-
"supplied":
|
|
1586
|
-
"borrowed":
|
|
1634
|
+
"supplied": _hp_rows,
|
|
1635
|
+
"borrowed": _hp_borrowed,
|
|
1587
1636
|
"poolDeposits": [_asset_row(r) for r in pool_deposits],
|
|
1588
1637
|
"totalValueUsd": solvency["total"],
|
|
1589
1638
|
"debtUsd": solvency["debt"],
|
|
1590
1639
|
"healthRatio": solvency["ratio"],
|
|
1640
|
+
"healthPct": _hp.get("health_pct"),
|
|
1591
1641
|
"solvent": solvency["solvent"],
|
|
1592
1642
|
"solvencyError": solvency["error"],
|
|
1593
1643
|
}
|
|
@@ -1626,7 +1676,22 @@ def cmd_summary(as_json: bool = False):
|
|
|
1626
1676
|
print(f" Total value: ${solvency['total']:,.2f}")
|
|
1627
1677
|
print(f" Debt: ${solvency['debt']:,.2f}")
|
|
1628
1678
|
ratio_str = ">1000.00 (negligible debt)" if solvency["ratio"] is None else f"{solvency['ratio']:.4f}"
|
|
1629
|
-
print(f" Health ratio:
|
|
1679
|
+
print(f" Health ratio (chain): {ratio_str} (>1.0 = solvent, 1.0 = liquidation)")
|
|
1680
|
+
# ─── Equity-based health (0-100%) ───
|
|
1681
|
+
# Different from health_ratio!
|
|
1682
|
+
hp = _compute_health_pct(
|
|
1683
|
+
[{"symbol": r["symbol"], "balance": "0", "usd": (r["raw"] / 10**r["decimals"]) * solvency["prices"].get(r["symbol"], 0)}
|
|
1684
|
+
for r in supplied if solvency["prices"].get(r["symbol"])],
|
|
1685
|
+
[{"symbol": r["symbol"], "balance": "0", "usd": (r["raw"] / 10**r["decimals"]) * solvency["prices"].get(r["symbol"], 0)}
|
|
1686
|
+
for r in borrowed if solvency["prices"].get(r["symbol"])],
|
|
1687
|
+
)
|
|
1688
|
+
if "error" not in hp:
|
|
1689
|
+
print(f" Health (0-100%): {hp['health_pct']:.1f}%")
|
|
1690
|
+
print(f" (supplied=${hp['supplied_usd']:.2f}, debt=${hp['debt_usd']:.2f},"
|
|
1691
|
+
f" equity=${hp['equity']:.2f}, max_debt=${hp['max_debt']:.2f}, {hp.get('tier','')})")
|
|
1692
|
+
print(f" 0%=liquidation 50%=half borrowing power used 100%=no debt")
|
|
1693
|
+
else:
|
|
1694
|
+
print(f" Health (0-100%): N/A ({hp['error']})")
|
|
1630
1695
|
print(f" Solvent: {'yes' if solvency['solvent'] else 'NO - liquidatable'}")
|
|
1631
1696
|
else:
|
|
1632
1697
|
print(f" Health/solvency: RedStone fetch/call failed ({solvency['error']}); showing balances only")
|
|
@@ -50,6 +50,13 @@ prime-summary reports live solvency (health ratio, total value, debt, solvent fl
|
|
|
50
50
|
SolvencyFacetProdAvalanche, read via eth_call with a RedStone price payload appended (falls
|
|
51
51
|
back to balances-only if the gateway is unreachable).
|
|
52
52
|
|
|
53
|
+
NOTE: prime-summary shows TWO health metrics — don't confuse them:
|
|
54
|
+
- "Health ratio (chain)": on-chain getHealthRatio. 1.0 = liquidation, >1.0 = solvent.
|
|
55
|
+
- "Health (0-100%)": equity-based, frontend-style. 0% = liquidation, 50% = half
|
|
56
|
+
borrowing power used, 100% = no debt.
|
|
57
|
+
Formula for the latter: equity=supplied-debt, max_debt=equity*(tier-1),
|
|
58
|
+
pct=(max_debt-debt)/max_debt*100. tier=5 (BASIC) or 10 (PREMIUM).
|
|
59
|
+
|
|
53
60
|
Collateral withdrawal is a two-step, time-delayed flow on the Prime Account (there is NO
|
|
54
61
|
instant withdraw of in-account collateral). The savings-pool `withdraw` above is a separate
|
|
55
62
|
two-step intent flow on the pool itself, not the Prime Account. Step 1: `withdraw --pool X
|
|
@@ -235,7 +242,7 @@ EXPLORER = "https://snowtrace.io"
|
|
|
235
242
|
CHAIN_ID = 43114
|
|
236
243
|
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
|
|
237
244
|
# ── Agent / wallet selection ─────────────────────────────────────────────────
|
|
238
|
-
# Agent-agnostic: any agent (Parakletos, Paraklaudios, or another
|
|
245
|
+
# Agent-agnostic: any agent (Parakletos, Paraklaudios, or another authorized agent) runs
|
|
239
246
|
# this same tool with its OWN wallet. The Prime Account is derived on-chain from the
|
|
240
247
|
# wallet owner (getLoanForOwner), so each agent automatically operates on its own
|
|
241
248
|
# Prime Account — no per-agent addresses are hardcoded.
|
|
@@ -434,12 +441,12 @@ TJ_ROUTER_V21 = "0xb4315e873dBcf96Ffd0acd8EA43f689D8c20fB30"
|
|
|
434
441
|
TJ_ROUTER_V22 = "0x18556DA13313f3532c54711497A8FedAC273220E"
|
|
435
442
|
TJ_MAX_BINS = 80
|
|
436
443
|
|
|
437
|
-
# Whitelisted LB pairs exposed as tool keys, matching
|
|
444
|
+
# Whitelisted LB pairs exposed as tool keys, matching the live frontend (bin step in
|
|
438
445
|
# the key suffix where a pair exists at two steps). For each: the LBPair address, the
|
|
439
446
|
# router version the pair belongs to, the canonical (tokenX, tokenY) order read on-chain,
|
|
440
447
|
# and the binStep. tokenX/tokenY carry the ERC20 address, the account bytes32 symbol (for
|
|
441
448
|
# the in-account balance read + RedStone feed), and decimals. The 13 source-whitelisted
|
|
442
|
-
# pairs include 4 aUSD pairs not on
|
|
449
|
+
# pairs include 4 aUSD pairs not on the live frontend; those are omitted (no clean
|
|
443
450
|
# symbol/decimals + out of scope).
|
|
444
451
|
_WAVAX = {"addr": "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", "symbol": "AVAX", "decimals": 18}
|
|
445
452
|
_USDC = {"addr": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", "symbol": "USDC", "decimals": 6}
|
|
@@ -658,33 +665,42 @@ def _tx_gas_price(w3) -> int:
|
|
|
658
665
|
|
|
659
666
|
def _set_gas_price(w3, tx_dict):
|
|
660
667
|
"""Set appropriate gas price fields for the chain, replacing the legacy gasPrice approach.
|
|
661
|
-
On EIP-1559 chains (Arbitrum, Base): sets maxFeePerGas +
|
|
662
|
-
base-fee hedge (base + prio + 1 gwei buffer).
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
668
|
+
On EIP-1559 chains (Arbitrum, Base, Avalanche post-Etna): sets maxFeePerGas +
|
|
669
|
+
maxPriorityFeePerGas with a 2x base-fee hedge (base + prio + 1 gwei buffer).
|
|
670
|
+
Falls back to legacy gasPrice only if the tx dict already lacks EIP-1559 fields
|
|
671
|
+
and the chain doesn't support max_priority_fee.
|
|
672
|
+
(25 gwei was the pre-Etna C-chain minimum; ACP-125 (Dec 2024) lowered the min base
|
|
673
|
+
fee to 1 nAVAX — base now sits at ~0.01 nAVAX, so a 25 gwei floor overpaid ~2500x
|
|
674
|
+
and inflated the upfront balance requirement past small EOAs.)"""
|
|
675
|
+
# If build_transaction already set EIP-1559 fields, don't touch them
|
|
676
|
+
if "maxFeePerGas" in tx_dict or "maxPriorityFeePerGas" in tx_dict:
|
|
677
|
+
tx_dict.pop("gasPrice", None)
|
|
678
|
+
return
|
|
667
679
|
tx_dict.pop("gasPrice", None)
|
|
668
|
-
|
|
680
|
+
try:
|
|
669
681
|
base = w3.eth.gas_price
|
|
670
682
|
prio = w3.eth.max_priority_fee
|
|
671
683
|
tx_dict["maxFeePerGas"] = max(int(base * 2), base + prio + 10**9)
|
|
672
684
|
tx_dict["maxPriorityFeePerGas"] = prio
|
|
673
|
-
|
|
685
|
+
except Exception:
|
|
686
|
+
# Legacy chain — use gasPrice instead
|
|
674
687
|
tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2), 1 * 10**9)
|
|
675
688
|
|
|
676
689
|
def _set_gas_price_for(chain_id, w3, tx_dict):
|
|
677
690
|
"""Set gas fields for an EXPLICIT chain_id rather than the module CHAIN_ID. Needed by
|
|
678
691
|
cross-chain flows (prime-bridge) where a tx may target Avalanche or Arbitrum regardless
|
|
679
|
-
of which tool built it.
|
|
680
|
-
|
|
692
|
+
of which tool built it."""
|
|
693
|
+
# If build_transaction already set EIP-1559 fields, don't touch them
|
|
694
|
+
if "maxFeePerGas" in tx_dict or "maxPriorityFeePerGas" in tx_dict:
|
|
695
|
+
tx_dict.pop("gasPrice", None)
|
|
696
|
+
return
|
|
681
697
|
tx_dict.pop("gasPrice", None)
|
|
682
|
-
|
|
698
|
+
try:
|
|
683
699
|
base = w3.eth.gas_price
|
|
684
700
|
prio = w3.eth.max_priority_fee
|
|
685
701
|
tx_dict["maxFeePerGas"] = max(int(base * 2), base + prio + 10**9)
|
|
686
702
|
tx_dict["maxPriorityFeePerGas"] = prio
|
|
687
|
-
|
|
703
|
+
except Exception:
|
|
688
704
|
tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2), 1 * 10**9)
|
|
689
705
|
|
|
690
706
|
def _read_env_var(path, var):
|
|
@@ -1979,6 +1995,49 @@ def gather_lending(w3, account):
|
|
|
1979
1995
|
r["usd"] = None
|
|
1980
1996
|
return out
|
|
1981
1997
|
|
|
1998
|
+
def _compute_health_pct(data: dict, tier_code: int = 0) -> dict:
|
|
1999
|
+
"""Compute equity-based health (0-100%) from gather_lending data + tier.
|
|
2000
|
+
|
|
2001
|
+
DeltaPrime has *two* health metrics that agents must not confuse:
|
|
2002
|
+
|
|
2003
|
+
1. health_ratio (on-chain, getHealthRatio): 1.0 = liquidation, >1.0 = solvent.
|
|
2004
|
+
This is the raw weighted-collateral / debt ratio from the SolvencyFacet.
|
|
2005
|
+
|
|
2006
|
+
2. health_pct (equity-based, 0-100%): the scale used in the DeltaPrime frontend
|
|
2007
|
+
and the account-health-monitor cron. 0% = liquidation, 100% = no debt.
|
|
2008
|
+
Formula:
|
|
2009
|
+
equity = supplied_usd - debt_usd
|
|
2010
|
+
max_mult = 10 if PREMIUM tier else 5 if BASIC
|
|
2011
|
+
max_debt = equity * (max_mult - 1)
|
|
2012
|
+
health_pct = (max_debt - debt_usd) / max_debt * 100
|
|
2013
|
+
|
|
2014
|
+
Returns dict with keys: health_pct, supplied_usd, debt_usd, equity, max_debt,
|
|
2015
|
+
tier_label, or error.
|
|
2016
|
+
"""
|
|
2017
|
+
supplied_usd = sum(r.get("usd", 0) or 0 for r in data.get("supplied", []))
|
|
2018
|
+
debt_usd = sum(r.get("usd", 0) or 0 for r in data.get("borrowed", []))
|
|
2019
|
+
equity = supplied_usd - debt_usd
|
|
2020
|
+
tier_labels = {0: "BASIC", 1: "PREMIUM", 2: "_NON_EXISTENT"}
|
|
2021
|
+
tier_label = tier_labels.get(tier_code, str(tier_code))
|
|
2022
|
+
max_mult = {0: 5, 1: 10}.get(tier_code, 5)
|
|
2023
|
+
|
|
2024
|
+
if equity <= 0.01:
|
|
2025
|
+
return {"health_pct": 0.0, "supplied_usd": round(supplied_usd, 2),
|
|
2026
|
+
"debt_usd": round(debt_usd, 2), "equity": round(equity, 2),
|
|
2027
|
+
"max_debt": 0.0, "tier": tier_label, "error": "equity near zero"}
|
|
2028
|
+
|
|
2029
|
+
max_debt = equity * (max_mult - 1)
|
|
2030
|
+
if max_debt > 0 and debt_usd >= 0:
|
|
2031
|
+
health_pct = (max_debt - min(debt_usd, max_debt)) / max_debt * 100
|
|
2032
|
+
health_pct = max(0.0, min(100.0, health_pct))
|
|
2033
|
+
else:
|
|
2034
|
+
health_pct = 100.0
|
|
2035
|
+
|
|
2036
|
+
return {"health_pct": round(health_pct, 1), "supplied_usd": round(supplied_usd, 2),
|
|
2037
|
+
"debt_usd": round(debt_usd, 2), "equity": round(equity, 2),
|
|
2038
|
+
"max_debt": round(max_debt, 2), "tier": tier_label}
|
|
2039
|
+
|
|
2040
|
+
|
|
1982
2041
|
def cmd_prime_summary():
|
|
1983
2042
|
w3 = get_w3()
|
|
1984
2043
|
acct = get_account()
|
|
@@ -2022,7 +2081,23 @@ def cmd_prime_summary():
|
|
|
2022
2081
|
# gather_lending nulls the ratio when debt is negligible (the raw value is
|
|
2023
2082
|
# astronomically large there); render that as ">1000" rather than a junk number.
|
|
2024
2083
|
ratio_str = ">1000.00 (negligible debt)" if ratio is None else f"{ratio:.4f}"
|
|
2025
|
-
print(f" Health ratio:
|
|
2084
|
+
print(f" Health ratio (chain): {ratio_str} (>1.0 = solvent, 1.0 = liquidation)")
|
|
2085
|
+
# ─── Equity-based health (0-100%) ───
|
|
2086
|
+
# Different from health_ratio! See _compute_health_pct docstring.
|
|
2087
|
+
# Get tier from the Prime Account (oracle-free view)
|
|
2088
|
+
try:
|
|
2089
|
+
tier_info = gather_prime_tier(w3, acct, account)
|
|
2090
|
+
tier_code = tier_info.get("tier_code", 0)
|
|
2091
|
+
except Exception:
|
|
2092
|
+
tier_code = 0
|
|
2093
|
+
hp = _compute_health_pct(data, tier_code)
|
|
2094
|
+
if "error" not in hp:
|
|
2095
|
+
print(f" Health (0-100%): {hp['health_pct']:.1f}%")
|
|
2096
|
+
print(f" (supplied=${hp['supplied_usd']:.2f}, debt=${hp['debt_usd']:.2f},"
|
|
2097
|
+
f" equity=${hp['equity']:.2f}, max_debt=${hp['max_debt']:.2f}, {hp['tier']})")
|
|
2098
|
+
print(f" 0%=liquidation 50%=half borrowing power used 100%=no debt")
|
|
2099
|
+
else:
|
|
2100
|
+
print(f" Health (0-100%): N/A ({hp['error']})")
|
|
2026
2101
|
print(f" Solvent: {'yes' if data['solvent'] else 'NO — liquidatable'}")
|
|
2027
2102
|
else:
|
|
2028
2103
|
print(f" Health/solvency: RedStone fetch/call failed ({data.get('solvency_error', 'error')}); "
|
|
@@ -4928,9 +5003,12 @@ def gather_defi() -> dict:
|
|
|
4928
5003
|
result["total_usd"] = lending["total_value_usd"]
|
|
4929
5004
|
result["health_ratio"] = lending["health_ratio"]
|
|
4930
5005
|
result["solvent"] = lending["solvent"]
|
|
5006
|
+
# Compute equity-based health (0-100%) from lending data + tier
|
|
5007
|
+
result["health_pct"] = _compute_health_pct(lending, tier.get("tier_code", 0)).get("health_pct")
|
|
4931
5008
|
if lending["supplied"] or lending["borrowed"]:
|
|
4932
5009
|
result["groups"].append({
|
|
4933
5010
|
"type": "Lending / Leverage", "health_ratio": lending["health_ratio"],
|
|
5011
|
+
"health_pct": result["health_pct"],
|
|
4934
5012
|
"supplied": [{"symbol": r["symbol"], "balance": r["balance"], "usd": r.get("usd")}
|
|
4935
5013
|
for r in lending["supplied"]],
|
|
4936
5014
|
"borrowed": [{"symbol": r["symbol"], "balance": r["balance"], "usd": r.get("usd")}
|
|
@@ -37,13 +37,21 @@ TIER_MAX = {"basic": 5, "premium": 10}
|
|
|
37
37
|
# ════════════════════════════════════════════════════════════════════
|
|
38
38
|
|
|
39
39
|
def compute_health(defi_data: dict, max_mult: int = 10) -> dict:
|
|
40
|
-
"""Compute
|
|
40
|
+
"""Compute equity-based health (0-100%) from defi --json data.
|
|
41
|
+
|
|
42
|
+
PREFERS the precomputed ``health_pct`` from defi --json (which primecli >= 0.5.4
|
|
43
|
+
includes), falling back to manual calculation when absent.
|
|
41
44
|
|
|
42
45
|
Formula:
|
|
43
46
|
equity = total_supplied_usd - total_debt_usd
|
|
44
47
|
max_debt = equity * (tier - 1) # PREMIUM=10, BASIC=5
|
|
45
48
|
health% = (max_debt - debt) / max_debt * 100
|
|
46
49
|
|
|
50
|
+
This is DIFFERENT from getHealthRatio (the on-chain ratio where 1.0 = liquidation).
|
|
51
|
+
Do NOT convert between the two. The ``health_ratio`` metric is the on-chain value
|
|
52
|
+
(1.0=liquidation, >1.0=solvent). The ``health_pct`` is the equity-based frontend
|
|
53
|
+
measurement (0%=liquidation, 50%=half borrowing power used, 100%=no debt).
|
|
54
|
+
|
|
47
55
|
Returns dict with health metrics or error.
|
|
48
56
|
"""
|
|
49
57
|
# Parse groups (DeltaPrime format) or flat format (DegenPrime)
|
|
@@ -53,6 +61,31 @@ def compute_health(defi_data: dict, max_mult: int = 10) -> dict:
|
|
|
53
61
|
supplied = g.get("supplied", [])
|
|
54
62
|
borrowed = g.get("borrowed", [])
|
|
55
63
|
health_ratio = g.get("health_ratio", 0) or 0
|
|
64
|
+
# Use precomputed health_pct from defi --json if available (primecli >= 0.5.4)
|
|
65
|
+
precomputed = g.get("health_pct") or g.get("bruno_pct") # bruno_pct for backward compat
|
|
66
|
+
if precomputed is not None:
|
|
67
|
+
# Early return: precomputed value exists, enrich with detail fields
|
|
68
|
+
supplied_usd = sum(s.get("usd", 0) or 0 for s in supplied)
|
|
69
|
+
debt_usd = sum(b.get("usd", 0) or 0 for b in borrowed)
|
|
70
|
+
equity = supplied_usd - debt_usd
|
|
71
|
+
raw_usdc = sum(s.get("usd", 0) for s in supplied if s.get("symbol") == "USDC")
|
|
72
|
+
symbols = [s.get("symbol", "") for s in supplied]
|
|
73
|
+
has_gmx = any("GM_" in sym for sym in symbols)
|
|
74
|
+
has_lb = any(sym in ("LB_AVAX_USDC", "LB_WAVAX_USDC", "JOE") or "TRADERJOE" in sym.upper() for sym in symbols)
|
|
75
|
+
has_aero = any("AERO" in sym.upper() or "CL_POSITION" in sym.upper() for sym in symbols)
|
|
76
|
+
return {
|
|
77
|
+
"health_pct": float(precomputed),
|
|
78
|
+
"health_ratio": round(health_ratio, 4),
|
|
79
|
+
"supplied_usd": round(supplied_usd, 2),
|
|
80
|
+
"debt_usd": round(debt_usd, 2),
|
|
81
|
+
"equity": round(equity, 2),
|
|
82
|
+
"max_debt": round(max(0, equity * (max_mult - 1)), 2),
|
|
83
|
+
"raw_usdc": round(raw_usdc, 2),
|
|
84
|
+
"has_gmx": has_gmx,
|
|
85
|
+
"has_lb": has_lb,
|
|
86
|
+
"has_aero": has_aero,
|
|
87
|
+
"action": "computed from defi --json health_pct",
|
|
88
|
+
}
|
|
56
89
|
else:
|
|
57
90
|
supplied = defi_data.get("supplied", [])
|
|
58
91
|
borrowed = defi_data.get("borrowed", [])
|
|
@@ -64,7 +97,7 @@ def compute_health(defi_data: dict, max_mult: int = 10) -> dict:
|
|
|
64
97
|
|
|
65
98
|
if equity <= 0.01:
|
|
66
99
|
return {
|
|
67
|
-
"
|
|
100
|
+
"health_pct": 0.0,
|
|
68
101
|
"health_ratio": round(health_ratio, 4),
|
|
69
102
|
"supplied_usd": round(supplied_usd, 2),
|
|
70
103
|
"debt_usd": round(debt_usd, 2),
|
|
@@ -88,15 +121,15 @@ def compute_health(defi_data: dict, max_mult: int = 10) -> dict:
|
|
|
88
121
|
has_aero = any("AERO" in sym.upper() or "CL_POSITION" in sym.upper() for sym in symbols)
|
|
89
122
|
|
|
90
123
|
if max_debt > 0 and debt_usd >= 0:
|
|
91
|
-
|
|
92
|
-
|
|
124
|
+
health_pct = (max_debt - min(debt_usd, max_debt)) / max_debt * 100
|
|
125
|
+
health_pct = max(0.0, min(100.0, health_pct))
|
|
93
126
|
else:
|
|
94
|
-
|
|
127
|
+
health_pct = 100.0
|
|
95
128
|
|
|
96
129
|
delta_debt = (max_debt * 0.5) - debt_usd # center target = 50%
|
|
97
130
|
|
|
98
131
|
return {
|
|
99
|
-
"
|
|
132
|
+
"health_pct": round(health_pct, 1),
|
|
100
133
|
"health_ratio": round(health_ratio, 4),
|
|
101
134
|
"supplied_usd": round(supplied_usd, 2),
|
|
102
135
|
"debt_usd": round(debt_usd, 2),
|
|
@@ -287,7 +320,7 @@ def run_tick(
|
|
|
287
320
|
"reason": "equity_near_zero",
|
|
288
321
|
"equity": health["equity"],
|
|
289
322
|
"debt": health["debt_usd"],
|
|
290
|
-
"health_pct": health["
|
|
323
|
+
"health_pct": health["health_pct"],
|
|
291
324
|
"label": label,
|
|
292
325
|
})
|
|
293
326
|
result.update(health)
|
|
@@ -303,23 +336,23 @@ def run_tick(
|
|
|
303
336
|
|
|
304
337
|
# 5. Health swing detection (always)
|
|
305
338
|
last_pct = load_last_health(state_dir)
|
|
306
|
-
if last_pct is not None and health["
|
|
307
|
-
diff = abs(health["
|
|
339
|
+
if last_pct is not None and health["health_pct"] is not None:
|
|
340
|
+
diff = abs(health["health_pct"] - last_pct)
|
|
308
341
|
if diff > 10:
|
|
309
342
|
write_escalation(state_dir, "health-swing", {
|
|
310
343
|
"reason": "health_swing",
|
|
311
344
|
"from_pct": last_pct,
|
|
312
|
-
"to_pct": health["
|
|
345
|
+
"to_pct": health["health_pct"],
|
|
313
346
|
"delta": diff,
|
|
314
347
|
"label": label,
|
|
315
348
|
})
|
|
316
349
|
result["escalation"] = "health_swing"
|
|
317
|
-
save_last_health(state_dir, health["
|
|
350
|
+
save_last_health(state_dir, health["health_pct"] or 0.0)
|
|
318
351
|
|
|
319
352
|
# 6. Append to history
|
|
320
353
|
entry = {
|
|
321
354
|
"ts": now_iso, "mode": mode,
|
|
322
|
-
"pct": health["
|
|
355
|
+
"pct": health["health_pct"],
|
|
323
356
|
"equity": health["equity"],
|
|
324
357
|
"debt": health["debt_usd"],
|
|
325
358
|
"hr": health["health_ratio"],
|
|
@@ -336,7 +369,7 @@ def run_tick(
|
|
|
336
369
|
write_escalation(state_dir, "incomplete-valuation", {
|
|
337
370
|
"reason": "incomplete_valuation",
|
|
338
371
|
"detail": val_reason,
|
|
339
|
-
"health_pct": health["
|
|
372
|
+
"health_pct": health["health_pct"],
|
|
340
373
|
"equity": health["equity"],
|
|
341
374
|
"debt": health["debt_usd"],
|
|
342
375
|
"label": label,
|
|
@@ -354,7 +387,7 @@ def run_tick(
|
|
|
354
387
|
side = strategy.get("side", "short")
|
|
355
388
|
low, high = target_range[0], target_range[1]
|
|
356
389
|
|
|
357
|
-
pct = health["
|
|
390
|
+
pct = health["health_pct"]
|
|
358
391
|
equity = health["equity"]
|
|
359
392
|
debt = health["debt_usd"]
|
|
360
393
|
raw_usdc = health["raw_usdc"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: primecli
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.4
|
|
4
4
|
Summary: Agent-friendly CLI tools for the DeltaPrime (Avalanche + Arbitrum) and DegenPrime (Base) lending and leverage protocols. Preview-by-default; no Etherscan key required.
|
|
5
5
|
Author: Mnemosyne-quest contributors
|
|
6
6
|
License: MIT
|
|
@@ -28,7 +28,6 @@ Requires-Dist: eth-account>=0.13
|
|
|
28
28
|
Requires-Dist: eth-keys>=0.5
|
|
29
29
|
Requires-Dist: eth-abi<7,>=5.0
|
|
30
30
|
Requires-Dist: requests>=2.31
|
|
31
|
-
Dynamic: license-file
|
|
32
31
|
|
|
33
32
|
# primecli
|
|
34
33
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools>=68", "wheel"]
|
|
2
|
+
requires = ["setuptools>=68,<73", "wheel"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "primecli"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.4"
|
|
8
8
|
description = "Agent-friendly CLI tools for the DeltaPrime (Avalanche + Arbitrum) and DegenPrime (Base) lending and leverage protocols. Preview-by-default; no Etherscan key required."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|