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.
Files changed (22) hide show
  1. {primecli-0.5.2 → primecli-0.5.4}/PKG-INFO +2 -3
  2. {primecli-0.5.2 → primecli-0.5.4}/primecli/arbprime.py +73 -4
  3. {primecli-0.5.2 → primecli-0.5.4}/primecli/degenprime.py +70 -5
  4. {primecli-0.5.2 → primecli-0.5.4}/primecli/deltaprime.py +94 -16
  5. {primecli-0.5.2 → primecli-0.5.4}/primecli/health_monitor.py +47 -14
  6. {primecli-0.5.2 → primecli-0.5.4}/primecli.egg-info/PKG-INFO +2 -3
  7. {primecli-0.5.2 → primecli-0.5.4}/pyproject.toml +2 -2
  8. {primecli-0.5.2 → primecli-0.5.4}/LICENSE +0 -0
  9. {primecli-0.5.2 → primecli-0.5.4}/README.md +0 -0
  10. {primecli-0.5.2 → primecli-0.5.4}/primecli/__init__.py +0 -0
  11. {primecli-0.5.2 → primecli-0.5.4}/primecli.egg-info/SOURCES.txt +0 -0
  12. {primecli-0.5.2 → primecli-0.5.4}/primecli.egg-info/dependency_links.txt +0 -0
  13. {primecli-0.5.2 → primecli-0.5.4}/primecli.egg-info/entry_points.txt +0 -0
  14. {primecli-0.5.2 → primecli-0.5.4}/primecli.egg-info/requires.txt +0 -0
  15. {primecli-0.5.2 → primecli-0.5.4}/primecli.egg-info/top_level.txt +0 -0
  16. {primecli-0.5.2 → primecli-0.5.4}/setup.cfg +0 -0
  17. {primecli-0.5.2 → primecli-0.5.4}/tests/test_cross_file_identity.py +0 -0
  18. {primecli-0.5.2 → primecli-0.5.4}/tests/test_gas_pricing.py +0 -0
  19. {primecli-0.5.2 → primecli-0.5.4}/tests/test_health_monitor.py +0 -0
  20. {primecli-0.5.2 → primecli-0.5.4}/tests/test_paraswap_validator.py +0 -0
  21. {primecli-0.5.2 → primecli-0.5.4}/tests/test_redstone_encoding.py +0 -0
  22. {primecli-0.5.2 → primecli-0.5.4}/tests/test_to_wei_units.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.1
2
2
  Name: primecli
3
- Version: 0.5.2
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 of Bruno's) runs
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 per
314
- # Bruno, leaving 4. The native-wrapped pool is `eth` (underlying WETH, account symbol
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: {ratio_str} (>1.0 = solvent)")
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
- "supplied": [_row(r) for r in supplied],
1465
- "borrowed": [_row(r) for r in borrowed],
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": [_asset_row(r) for r in supplied],
1586
- "borrowed": [_asset_row(r) for r in 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: {ratio_str} (>1.0 = solvent)")
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 of Bruno's) runs
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 Bruno's live frontend (bin step in
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 Bruno's frontend; those are omitted (no clean
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 + maxPriorityFeePerGas with a 2x
662
- base-fee hedge (base + prio + 1 gwei buffer). On Avalanche (legacy): sets gasPrice at
663
- 2x base fee with a 1 gwei floor. (25 gwei was the pre-Etna C-chain minimum;
664
- ACP-125 (Dec 2024) lowered the min base fee to 1 nAVAX — base now sits at ~0.01
665
- nAVAX, so a 25 gwei floor overpaid ~2500x and inflated the upfront balance
666
- requirement past small EOAs.)"""
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
- if CHAIN_ID in (42161, 8453): # Arbitrum, Base — EIP-1559
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
- else: # Avalanche (43114) — legacy gasPrice
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. Arbitrum/Base (EIP-1559): maxFeePerGas + maxPriorityFeePerGas;
680
- Avalanche (legacy): gasPrice with a 1 gwei floor (post-Etna; see _set_gas_price)."""
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
- if chain_id in (42161, 8453): # Arbitrum, Base — EIP-1559
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
- else: # Avalanche (43114) — legacy gasPrice
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: {ratio_str} (>1.0 = solvent)")
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 Bruno's 0-100% health scale from defi --json data.
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
- "bruno_pct": 0.0,
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
- bruno_pct = (max_debt - min(debt_usd, max_debt)) / max_debt * 100
92
- bruno_pct = max(0.0, min(100.0, bruno_pct))
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
- bruno_pct = 100.0
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
- "bruno_pct": round(bruno_pct, 1),
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["bruno_pct"],
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["bruno_pct"] is not None:
307
- diff = abs(health["bruno_pct"] - last_pct)
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["bruno_pct"],
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["bruno_pct"] or 0.0)
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["bruno_pct"],
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["bruno_pct"],
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["bruno_pct"]
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.4
1
+ Metadata-Version: 2.1
2
2
  Name: primecli
3
- Version: 0.5.2
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.2"
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