primecli 0.5.3__tar.gz → 0.5.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.5.3 → primecli-0.5.5}/PKG-INFO +1 -1
- primecli-0.5.5/primecli/__init__.py +59 -0
- {primecli-0.5.3 → primecli-0.5.5}/primecli/arbprime.py +32 -25
- {primecli-0.5.3 → primecli-0.5.5}/primecli/degenprime.py +77 -5
- {primecli-0.5.3 → primecli-0.5.5}/primecli/deltaprime.py +32 -25
- {primecli-0.5.3 → primecli-0.5.5}/primecli/health_monitor.py +21 -21
- {primecli-0.5.3 → primecli-0.5.5}/primecli.egg-info/PKG-INFO +1 -1
- {primecli-0.5.3 → primecli-0.5.5}/pyproject.toml +1 -1
- primecli-0.5.3/primecli/__init__.py +0 -8
- {primecli-0.5.3 → primecli-0.5.5}/LICENSE +0 -0
- {primecli-0.5.3 → primecli-0.5.5}/README.md +0 -0
- {primecli-0.5.3 → primecli-0.5.5}/primecli.egg-info/SOURCES.txt +0 -0
- {primecli-0.5.3 → primecli-0.5.5}/primecli.egg-info/dependency_links.txt +0 -0
- {primecli-0.5.3 → primecli-0.5.5}/primecli.egg-info/entry_points.txt +0 -0
- {primecli-0.5.3 → primecli-0.5.5}/primecli.egg-info/requires.txt +0 -0
- {primecli-0.5.3 → primecli-0.5.5}/primecli.egg-info/top_level.txt +0 -0
- {primecli-0.5.3 → primecli-0.5.5}/setup.cfg +0 -0
- {primecli-0.5.3 → primecli-0.5.5}/tests/test_cross_file_identity.py +0 -0
- {primecli-0.5.3 → primecli-0.5.5}/tests/test_gas_pricing.py +0 -0
- {primecli-0.5.3 → primecli-0.5.5}/tests/test_health_monitor.py +0 -0
- {primecli-0.5.3 → primecli-0.5.5}/tests/test_paraswap_validator.py +0 -0
- {primecli-0.5.3 → primecli-0.5.5}/tests/test_redstone_encoding.py +0 -0
- {primecli-0.5.3 → primecli-0.5.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.5.
|
|
3
|
+
Version: 0.5.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
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""primecli - command-line tools for DeltaPrime (Avalanche + Arbitrum) and DegenPrime (Base)."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import urllib.request
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
__version__ = _pkg_version("primecli")
|
|
11
|
+
except PackageNotFoundError: # running from source without an install
|
|
12
|
+
__version__ = "0.0.0+unknown"
|
|
13
|
+
|
|
14
|
+
_VERSION_CHECK_URL = "https://pypi.org/pypi/primecli/json"
|
|
15
|
+
_VERSION_TIMEOUT = 3 # seconds
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _parse_version(v: str) -> tuple:
|
|
19
|
+
"""Parse 'X.Y.Z' into a sortable tuple. Non-numeric suffixes become -inf."""
|
|
20
|
+
parts = v.split(".")
|
|
21
|
+
nums = []
|
|
22
|
+
for p in parts:
|
|
23
|
+
try:
|
|
24
|
+
nums.append(int(p))
|
|
25
|
+
except ValueError:
|
|
26
|
+
nums.append(-1)
|
|
27
|
+
# Pad to 3 elements
|
|
28
|
+
while len(nums) < 3:
|
|
29
|
+
nums.append(0)
|
|
30
|
+
return tuple(nums[:3])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def check_version(suppress_flag: bool = False) -> None:
|
|
34
|
+
"""Print a one-line upgrade hint to stderr when the installed version is behind
|
|
35
|
+
the latest on PyPI. Suppress with the env var PRIMECLI_NO_VERSION_CHECK=1,
|
|
36
|
+
the CLI flag --no-version-check, or pass suppress_flag=True."""
|
|
37
|
+
if suppress_flag or os.environ.get("PRIMECLI_NO_VERSION_CHECK") == "1":
|
|
38
|
+
return
|
|
39
|
+
if "--no-version-check" in sys.argv:
|
|
40
|
+
return
|
|
41
|
+
if __version__ in ("0.0.0+unknown",):
|
|
42
|
+
return
|
|
43
|
+
try:
|
|
44
|
+
req = urllib.request.Request(_VERSION_CHECK_URL)
|
|
45
|
+
with urllib.request.urlopen(req, timeout=_VERSION_TIMEOUT) as resp:
|
|
46
|
+
data = json.loads(resp.read().decode())
|
|
47
|
+
latest = data.get("info", {}).get("version", "")
|
|
48
|
+
if not latest:
|
|
49
|
+
return
|
|
50
|
+
installed = _parse_version(__version__)
|
|
51
|
+
latest_v = _parse_version(latest)
|
|
52
|
+
if installed < latest_v:
|
|
53
|
+
print(
|
|
54
|
+
f"⚠️ primecli {__version__} is outdated. Latest is {latest}. "
|
|
55
|
+
f"Upgrade: pip install --upgrade primecli",
|
|
56
|
+
file=sys.stderr,
|
|
57
|
+
)
|
|
58
|
+
except Exception:
|
|
59
|
+
pass # network failure or parse error → silent
|
|
@@ -59,7 +59,7 @@ back to balances-only if the gateway is unreachable).
|
|
|
59
59
|
|
|
60
60
|
NOTE: prime-summary shows TWO health metrics — don't confuse them:
|
|
61
61
|
- "Health ratio (chain)": on-chain getHealthRatio. 1.0 = liquidation, >1.0 = solvent.
|
|
62
|
-
- "Health (
|
|
62
|
+
- "Health (0-100%)": equity-based, frontend-style. 0% = liquidation, 50% = half
|
|
63
63
|
borrowing power used, 100% = no debt.
|
|
64
64
|
Formula for the latter: equity=supplied-debt, max_debt=equity*(tier-1),
|
|
65
65
|
pct=(max_debt-debt)/max_debt*100. tier=5 (BASIC) or 10 (PREMIUM).
|
|
@@ -228,13 +228,19 @@ if _hm is None:
|
|
|
228
228
|
_spec.loader.exec_module(_hm)
|
|
229
229
|
health_monitor = _hm
|
|
230
230
|
|
|
231
|
+
# Version check (silent on network failure or old install)
|
|
232
|
+
try:
|
|
233
|
+
from primecli import check_version
|
|
234
|
+
except ImportError:
|
|
235
|
+
def check_version(*a, **kw): pass
|
|
236
|
+
|
|
231
237
|
# Arbitrum One RPC. Override with ARBPRIME_RPC for a paid Alchemy/Infura endpoint.
|
|
232
238
|
ARBITRUM_RPC = os.environ.get("ARBPRIME_RPC", "https://arb1.arbitrum.io/rpc")
|
|
233
239
|
EXPLORER = "https://arbiscan.io" # display/links only — never used for ABI fetch
|
|
234
240
|
CHAIN_ID = 42161
|
|
235
241
|
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
|
|
236
242
|
# ── Agent / wallet selection ─────────────────────────────────────────────────
|
|
237
|
-
# Agent-agnostic: any agent (Parakletos, Paraklaudios, or another
|
|
243
|
+
# Agent-agnostic: any agent (Parakletos, Paraklaudios, or another authorized agent) runs
|
|
238
244
|
# this same tool with its OWN wallet. The Prime Account is derived on-chain from the
|
|
239
245
|
# wallet owner (getLoanForOwner), so each agent automatically operates on its own
|
|
240
246
|
# Prime Account — no per-agent addresses are hardcoded. The same EVM key works on
|
|
@@ -317,8 +323,8 @@ REDSTONE_GATEWAYS = [
|
|
|
317
323
|
]
|
|
318
324
|
|
|
319
325
|
# Active pool proxies — LIVE Arbitrum deployment, on-chain verified 2026-06-03.
|
|
320
|
-
# getAllPoolAssets live = [USDC, DAI, BTC, ARB, ETH]; the DAI pool is DROPPED
|
|
321
|
-
#
|
|
326
|
+
# getAllPoolAssets live = [USDC, DAI, BTC, ARB, ETH]; the DAI pool is DROPPED,
|
|
327
|
+
# leaving 4. The native-wrapped pool is `eth` (underlying WETH, account symbol
|
|
322
328
|
# "ETH"): its native deposit path uses depositNativeToken() (wraps ETH -> WETH).
|
|
323
329
|
POOLS = {
|
|
324
330
|
"eth": {
|
|
@@ -1968,23 +1974,23 @@ def gather_lending(w3, account):
|
|
|
1968
1974
|
r["usd"] = None
|
|
1969
1975
|
return out
|
|
1970
1976
|
|
|
1971
|
-
def
|
|
1972
|
-
"""Compute
|
|
1977
|
+
def _compute_health_pct(data: dict, tier_code: int = 0) -> dict:
|
|
1978
|
+
"""Compute equity-based health (0-100%) from gather_lending data + tier.
|
|
1973
1979
|
|
|
1974
1980
|
DeltaPrime has *two* health metrics that agents must not confuse:
|
|
1975
1981
|
|
|
1976
1982
|
1. health_ratio (on-chain, getHealthRatio): 1.0 = liquidation, >1.0 = solvent.
|
|
1977
1983
|
This is the raw weighted-collateral / debt ratio from the SolvencyFacet.
|
|
1978
1984
|
|
|
1979
|
-
2.
|
|
1985
|
+
2. health_pct (equity-based, 0-100%): the scale used in the DeltaPrime frontend
|
|
1980
1986
|
and the account-health-monitor cron. 0% = liquidation, 100% = no debt.
|
|
1981
1987
|
Formula:
|
|
1982
1988
|
equity = supplied_usd - debt_usd
|
|
1983
1989
|
max_mult = 10 if PREMIUM tier else 5 if BASIC
|
|
1984
1990
|
max_debt = equity * (max_mult - 1)
|
|
1985
|
-
|
|
1991
|
+
health_pct = (max_debt - debt_usd) / max_debt * 100
|
|
1986
1992
|
|
|
1987
|
-
Returns dict with keys:
|
|
1993
|
+
Returns dict with keys: health_pct, supplied_usd, debt_usd, equity, max_debt,
|
|
1988
1994
|
tier_label, or error.
|
|
1989
1995
|
"""
|
|
1990
1996
|
supplied_usd = sum(r.get("usd", 0) or 0 for r in data.get("supplied", []))
|
|
@@ -1995,18 +2001,18 @@ def _compute_bruno_health(data: dict, tier_code: int = 0) -> dict:
|
|
|
1995
2001
|
max_mult = {0: 5, 1: 10}.get(tier_code, 5)
|
|
1996
2002
|
|
|
1997
2003
|
if equity <= 0.01:
|
|
1998
|
-
return {"
|
|
2004
|
+
return {"health_pct": 0.0, "supplied_usd": round(supplied_usd, 2),
|
|
1999
2005
|
"debt_usd": round(debt_usd, 2), "equity": round(equity, 2),
|
|
2000
2006
|
"max_debt": 0.0, "tier": tier_label, "error": "equity near zero"}
|
|
2001
2007
|
|
|
2002
2008
|
max_debt = equity * (max_mult - 1)
|
|
2003
2009
|
if max_debt > 0 and debt_usd >= 0:
|
|
2004
|
-
|
|
2005
|
-
|
|
2010
|
+
health_pct = (max_debt - min(debt_usd, max_debt)) / max_debt * 100
|
|
2011
|
+
health_pct = max(0.0, min(100.0, health_pct))
|
|
2006
2012
|
else:
|
|
2007
|
-
|
|
2013
|
+
health_pct = 100.0
|
|
2008
2014
|
|
|
2009
|
-
return {"
|
|
2015
|
+
return {"health_pct": round(health_pct, 1), "supplied_usd": round(supplied_usd, 2),
|
|
2010
2016
|
"debt_usd": round(debt_usd, 2), "equity": round(equity, 2),
|
|
2011
2017
|
"max_debt": round(max_debt, 2), "tier": tier_label}
|
|
2012
2018
|
|
|
@@ -2055,22 +2061,22 @@ def cmd_prime_summary():
|
|
|
2055
2061
|
# astronomically large there); render that as ">1000" rather than a junk number.
|
|
2056
2062
|
ratio_str = ">1000.00 (negligible debt)" if ratio is None else f"{ratio:.4f}"
|
|
2057
2063
|
print(f" Health ratio (chain): {ratio_str} (>1.0 = solvent, 1.0 = liquidation)")
|
|
2058
|
-
# ───
|
|
2059
|
-
# Different from health_ratio! See
|
|
2064
|
+
# ─── Equity-based health (0-100%) ───
|
|
2065
|
+
# Different from health_ratio! See _compute_health_pct docstring.
|
|
2060
2066
|
# Get tier from the Prime Account (oracle-free view)
|
|
2061
2067
|
try:
|
|
2062
2068
|
tier_info = gather_prime_tier(w3, acct, account)
|
|
2063
2069
|
tier_code = tier_info.get("tier_code", 0)
|
|
2064
2070
|
except Exception:
|
|
2065
2071
|
tier_code = 0
|
|
2066
|
-
|
|
2067
|
-
if "error" not in
|
|
2068
|
-
print(f" Health (
|
|
2069
|
-
print(f" (supplied=${
|
|
2070
|
-
f" equity=${
|
|
2072
|
+
hp = _compute_health_pct(data, tier_code)
|
|
2073
|
+
if "error" not in hp:
|
|
2074
|
+
print(f" Health (0-100%): {hp['health_pct']:.1f}%")
|
|
2075
|
+
print(f" (supplied=${hp['supplied_usd']:.2f}, debt=${hp['debt_usd']:.2f},"
|
|
2076
|
+
f" equity=${hp['equity']:.2f}, max_debt=${hp['max_debt']:.2f}, {hp['tier']})")
|
|
2071
2077
|
print(f" 0%=liquidation 50%=half borrowing power used 100%=no debt")
|
|
2072
2078
|
else:
|
|
2073
|
-
print(f" Health (
|
|
2079
|
+
print(f" Health (0-100%): N/A ({hp['error']})")
|
|
2074
2080
|
print(f" Solvent: {'yes' if data['solvent'] else 'NO — liquidatable'}")
|
|
2075
2081
|
else:
|
|
2076
2082
|
print(f" Health/solvency: RedStone fetch/call failed ({data.get('solvency_error', 'error')}); "
|
|
@@ -5071,12 +5077,12 @@ def gather_defi() -> dict:
|
|
|
5071
5077
|
result["total_usd"] = lending["total_value_usd"]
|
|
5072
5078
|
result["health_ratio"] = lending["health_ratio"]
|
|
5073
5079
|
result["solvent"] = lending["solvent"]
|
|
5074
|
-
# Compute
|
|
5075
|
-
result["
|
|
5080
|
+
# Compute equity-based health (0-100%) from lending data + tier
|
|
5081
|
+
result["health_pct"] = _compute_health_pct(lending, tier.get("tier_code", 0)).get("health_pct")
|
|
5076
5082
|
if lending["supplied"] or lending["borrowed"]:
|
|
5077
5083
|
result["groups"].append({
|
|
5078
5084
|
"type": "Lending / Leverage", "health_ratio": lending["health_ratio"],
|
|
5079
|
-
"
|
|
5085
|
+
"health_pct": result["health_pct"],
|
|
5080
5086
|
"supplied": [{"symbol": r["symbol"], "balance": r["balance"], "usd": r.get("usd")}
|
|
5081
5087
|
for r in lending["supplied"]],
|
|
5082
5088
|
"borrowed": [{"symbol": r["symbol"], "balance": r["balance"], "usd": r.get("usd")}
|
|
@@ -5360,6 +5366,7 @@ def cmd_prime_bridge(from_chain: str = "avax", amount: float = None, execute: bo
|
|
|
5360
5366
|
print(f" Tx: {src_cfg['explorer']}/{tx_hash.hex()}")
|
|
5361
5367
|
|
|
5362
5368
|
def main():
|
|
5369
|
+
check_version()
|
|
5363
5370
|
args = sys.argv[1:] if len(sys.argv) > 1 else []
|
|
5364
5371
|
# Global wallet selector: --as <agent>, stripped before command dispatch.
|
|
5365
5372
|
global _SELECTED_AGENT, _CLI_KEY
|
|
@@ -104,6 +104,12 @@ if _hm is None:
|
|
|
104
104
|
_spec.loader.exec_module(_hm)
|
|
105
105
|
health_monitor = _hm
|
|
106
106
|
|
|
107
|
+
# Version check (silent on network failure or old install)
|
|
108
|
+
try:
|
|
109
|
+
from primecli import check_version
|
|
110
|
+
except ImportError:
|
|
111
|
+
def check_version(*a, **kw): pass
|
|
112
|
+
|
|
107
113
|
# Default Base RPC. mainnet.base.org rate-limits hard (429 within a few calls); the
|
|
108
114
|
# publicnode endpoint is fronted by a load balancer with much higher anonymous limits
|
|
109
115
|
# and has been the most reliable free option for this tool's traffic pattern (lots of
|
|
@@ -112,6 +118,7 @@ health_monitor = _hm
|
|
|
112
118
|
BASE_RPC = os.environ.get("DEGENPRIME_RPC", "https://base.publicnode.com")
|
|
113
119
|
EXPLORER = "https://basescan.org"
|
|
114
120
|
CHAIN_ID = 8453
|
|
121
|
+
DEGEN_MAX_MULT = 5 # Fixed max leverage multiplier (no tier system on DegenPrime)
|
|
115
122
|
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
|
|
116
123
|
|
|
117
124
|
# ── Signing key resolution ──────────────────────────────────────────────────
|
|
@@ -570,6 +577,41 @@ def _asset_meta(w3, symbol: str):
|
|
|
570
577
|
# gateway once instead of per-feed-symbol. Cleared implicitly when the process exits.
|
|
571
578
|
_redstone_gateway_cache = None
|
|
572
579
|
|
|
580
|
+
def _compute_health_pct(supplied: list, borrowed: list, max_mult: int = None) -> dict:
|
|
581
|
+
"""Compute equity-based health (0-100%) from per-asset supplied/borrowed data.
|
|
582
|
+
|
|
583
|
+
0% = liquidation, 50% = half borrowing power used, 100% = no debt.
|
|
584
|
+
Formula:
|
|
585
|
+
equity = supplied_usd - debt_usd
|
|
586
|
+
max_mult = 5 (fixed for DegenPrime; no tier system)
|
|
587
|
+
max_debt = equity * (max_mult - 1)
|
|
588
|
+
health_pct = (max_debt - debt_usd) / max_debt * 100
|
|
589
|
+
|
|
590
|
+
Returns dict with keys: health_pct, supplied_usd, debt_usd, equity, max_debt, or error.
|
|
591
|
+
"""
|
|
592
|
+
if max_mult is None:
|
|
593
|
+
max_mult = DEGEN_MAX_MULT
|
|
594
|
+
supplied_usd = sum(r.get("usd", 0) or 0 for r in supplied)
|
|
595
|
+
debt_usd = sum(r.get("usd", 0) or 0 for r in borrowed)
|
|
596
|
+
equity = supplied_usd - debt_usd
|
|
597
|
+
|
|
598
|
+
if equity <= 0.01:
|
|
599
|
+
return {"health_pct": 0.0, "supplied_usd": round(supplied_usd, 2),
|
|
600
|
+
"debt_usd": round(debt_usd, 2), "equity": round(equity, 2),
|
|
601
|
+
"max_debt": 0.0, "error": "equity near zero"}
|
|
602
|
+
|
|
603
|
+
max_debt = equity * (max_mult - 1)
|
|
604
|
+
if max_debt > 0 and debt_usd >= 0:
|
|
605
|
+
health_pct = (max_debt - min(debt_usd, max_debt)) / max_debt * 100
|
|
606
|
+
health_pct = max(0.0, min(100.0, health_pct))
|
|
607
|
+
else:
|
|
608
|
+
health_pct = 100.0
|
|
609
|
+
|
|
610
|
+
return {"health_pct": round(health_pct, 1), "supplied_usd": round(supplied_usd, 2),
|
|
611
|
+
"debt_usd": round(debt_usd, 2), "equity": round(equity, 2),
|
|
612
|
+
"max_debt": round(max_debt, 2), "tier": "FIXED_5X"}
|
|
613
|
+
|
|
614
|
+
|
|
573
615
|
def _redstone_fetch_packages(use_cache: bool = True) -> dict:
|
|
574
616
|
"""Fetch the latest signed price packages from the RedStone gateway. Returns the
|
|
575
617
|
per-feed map: {feedSymbol: [package, ...]} with one package per signer. The per-run
|
|
@@ -1458,11 +1500,19 @@ def gather_defi() -> dict:
|
|
|
1458
1500
|
row["usd"] = round(amt * usd, 2)
|
|
1459
1501
|
return row
|
|
1460
1502
|
|
|
1503
|
+
_rows_supplied = [_row(r) for r in supplied]
|
|
1504
|
+
_rows_borrowed = [_row(r) for r in borrowed]
|
|
1505
|
+
|
|
1506
|
+
# Compute equity-based health (0-100%)
|
|
1507
|
+
_hp = _compute_health_pct(_rows_supplied, _rows_borrowed)
|
|
1508
|
+
result["health_pct"] = _hp.get("health_pct")
|
|
1509
|
+
|
|
1461
1510
|
if supplied or borrowed:
|
|
1462
1511
|
result["groups"].append({
|
|
1463
1512
|
"type": "Lending / Leverage", "health_ratio": solvency["ratio"],
|
|
1464
|
-
"
|
|
1465
|
-
"
|
|
1513
|
+
"health_pct": result["health_pct"],
|
|
1514
|
+
"supplied": _rows_supplied,
|
|
1515
|
+
"borrowed": _rows_borrowed,
|
|
1466
1516
|
})
|
|
1467
1517
|
|
|
1468
1518
|
# Savings: the EOA's own pool deposits ("Diamond Hands"), independent of the Degen
|
|
@@ -1578,16 +1628,22 @@ def cmd_summary(as_json: bool = False):
|
|
|
1578
1628
|
row["usd"] = round(row["amount"] * usd, 2)
|
|
1579
1629
|
return row
|
|
1580
1630
|
|
|
1631
|
+
# Compute equity-based health (0-100%)
|
|
1632
|
+
_hp_rows = [_asset_row(r) for r in supplied]
|
|
1633
|
+
_hp_borrowed = [_asset_row(r) for r in borrowed]
|
|
1634
|
+
_hp = _compute_health_pct(_hp_rows, _hp_borrowed)
|
|
1635
|
+
|
|
1581
1636
|
out = {
|
|
1582
1637
|
"wallet": acct.address,
|
|
1583
1638
|
"account": pa,
|
|
1584
1639
|
"nativeBalance": pa_eth if pa_eth >= 1e-9 else None,
|
|
1585
|
-
"supplied":
|
|
1586
|
-
"borrowed":
|
|
1640
|
+
"supplied": _hp_rows,
|
|
1641
|
+
"borrowed": _hp_borrowed,
|
|
1587
1642
|
"poolDeposits": [_asset_row(r) for r in pool_deposits],
|
|
1588
1643
|
"totalValueUsd": solvency["total"],
|
|
1589
1644
|
"debtUsd": solvency["debt"],
|
|
1590
1645
|
"healthRatio": solvency["ratio"],
|
|
1646
|
+
"healthPct": _hp.get("health_pct"),
|
|
1591
1647
|
"solvent": solvency["solvent"],
|
|
1592
1648
|
"solvencyError": solvency["error"],
|
|
1593
1649
|
}
|
|
@@ -1626,7 +1682,22 @@ def cmd_summary(as_json: bool = False):
|
|
|
1626
1682
|
print(f" Total value: ${solvency['total']:,.2f}")
|
|
1627
1683
|
print(f" Debt: ${solvency['debt']:,.2f}")
|
|
1628
1684
|
ratio_str = ">1000.00 (negligible debt)" if solvency["ratio"] is None else f"{solvency['ratio']:.4f}"
|
|
1629
|
-
print(f" Health ratio:
|
|
1685
|
+
print(f" Health ratio (chain): {ratio_str} (>1.0 = solvent, 1.0 = liquidation)")
|
|
1686
|
+
# ─── Equity-based health (0-100%) ───
|
|
1687
|
+
# Different from health_ratio!
|
|
1688
|
+
hp = _compute_health_pct(
|
|
1689
|
+
[{"symbol": r["symbol"], "balance": "0", "usd": (r["raw"] / 10**r["decimals"]) * solvency["prices"].get(r["symbol"], 0)}
|
|
1690
|
+
for r in supplied if solvency["prices"].get(r["symbol"])],
|
|
1691
|
+
[{"symbol": r["symbol"], "balance": "0", "usd": (r["raw"] / 10**r["decimals"]) * solvency["prices"].get(r["symbol"], 0)}
|
|
1692
|
+
for r in borrowed if solvency["prices"].get(r["symbol"])],
|
|
1693
|
+
)
|
|
1694
|
+
if "error" not in hp:
|
|
1695
|
+
print(f" Health (0-100%): {hp['health_pct']:.1f}%")
|
|
1696
|
+
print(f" (supplied=${hp['supplied_usd']:.2f}, debt=${hp['debt_usd']:.2f},"
|
|
1697
|
+
f" equity=${hp['equity']:.2f}, max_debt=${hp['max_debt']:.2f}, {hp.get('tier','')})")
|
|
1698
|
+
print(f" 0%=liquidation 50%=half borrowing power used 100%=no debt")
|
|
1699
|
+
else:
|
|
1700
|
+
print(f" Health (0-100%): N/A ({hp['error']})")
|
|
1630
1701
|
print(f" Solvent: {'yes' if solvency['solvent'] else 'NO - liquidatable'}")
|
|
1631
1702
|
else:
|
|
1632
1703
|
print(f" Health/solvency: RedStone fetch/call failed ({solvency['error']}); showing balances only")
|
|
@@ -2360,6 +2431,7 @@ def cmd_aerodrome_positions():
|
|
|
2360
2431
|
print(" v1 lists tokenIds only. Composition + write paths deferred to v2.")
|
|
2361
2432
|
|
|
2362
2433
|
def main():
|
|
2434
|
+
check_version()
|
|
2363
2435
|
try:
|
|
2364
2436
|
_dispatch()
|
|
2365
2437
|
except RuntimeError as e:
|
|
@@ -52,7 +52,7 @@ back to balances-only if the gateway is unreachable).
|
|
|
52
52
|
|
|
53
53
|
NOTE: prime-summary shows TWO health metrics — don't confuse them:
|
|
54
54
|
- "Health ratio (chain)": on-chain getHealthRatio. 1.0 = liquidation, >1.0 = solvent.
|
|
55
|
-
- "Health (
|
|
55
|
+
- "Health (0-100%)": equity-based, frontend-style. 0% = liquidation, 50% = half
|
|
56
56
|
borrowing power used, 100% = no debt.
|
|
57
57
|
Formula for the latter: equity=supplied-debt, max_debt=equity*(tier-1),
|
|
58
58
|
pct=(max_debt-debt)/max_debt*100. tier=5 (BASIC) or 10 (PREMIUM).
|
|
@@ -237,12 +237,18 @@ if _hm is None:
|
|
|
237
237
|
_spec.loader.exec_module(_hm)
|
|
238
238
|
health_monitor = _hm
|
|
239
239
|
|
|
240
|
+
# Version check (silent on network failure or old install)
|
|
241
|
+
try:
|
|
242
|
+
from primecli import check_version
|
|
243
|
+
except ImportError:
|
|
244
|
+
def check_version(*a, **kw): pass
|
|
245
|
+
|
|
240
246
|
AVALANCHE_RPC = os.environ.get("DELTAPRIME_RPC", "https://api.avax.network/ext/bc/C/rpc")
|
|
241
247
|
EXPLORER = "https://snowtrace.io"
|
|
242
248
|
CHAIN_ID = 43114
|
|
243
249
|
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
|
|
244
250
|
# ── Agent / wallet selection ─────────────────────────────────────────────────
|
|
245
|
-
# Agent-agnostic: any agent (Parakletos, Paraklaudios, or another
|
|
251
|
+
# Agent-agnostic: any agent (Parakletos, Paraklaudios, or another authorized agent) runs
|
|
246
252
|
# this same tool with its OWN wallet. The Prime Account is derived on-chain from the
|
|
247
253
|
# wallet owner (getLoanForOwner), so each agent automatically operates on its own
|
|
248
254
|
# Prime Account — no per-agent addresses are hardcoded.
|
|
@@ -441,12 +447,12 @@ TJ_ROUTER_V21 = "0xb4315e873dBcf96Ffd0acd8EA43f689D8c20fB30"
|
|
|
441
447
|
TJ_ROUTER_V22 = "0x18556DA13313f3532c54711497A8FedAC273220E"
|
|
442
448
|
TJ_MAX_BINS = 80
|
|
443
449
|
|
|
444
|
-
# Whitelisted LB pairs exposed as tool keys, matching
|
|
450
|
+
# Whitelisted LB pairs exposed as tool keys, matching the live frontend (bin step in
|
|
445
451
|
# the key suffix where a pair exists at two steps). For each: the LBPair address, the
|
|
446
452
|
# router version the pair belongs to, the canonical (tokenX, tokenY) order read on-chain,
|
|
447
453
|
# and the binStep. tokenX/tokenY carry the ERC20 address, the account bytes32 symbol (for
|
|
448
454
|
# the in-account balance read + RedStone feed), and decimals. The 13 source-whitelisted
|
|
449
|
-
# pairs include 4 aUSD pairs not on
|
|
455
|
+
# pairs include 4 aUSD pairs not on the live frontend; those are omitted (no clean
|
|
450
456
|
# symbol/decimals + out of scope).
|
|
451
457
|
_WAVAX = {"addr": "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", "symbol": "AVAX", "decimals": 18}
|
|
452
458
|
_USDC = {"addr": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", "symbol": "USDC", "decimals": 6}
|
|
@@ -1995,23 +2001,23 @@ def gather_lending(w3, account):
|
|
|
1995
2001
|
r["usd"] = None
|
|
1996
2002
|
return out
|
|
1997
2003
|
|
|
1998
|
-
def
|
|
1999
|
-
"""Compute
|
|
2004
|
+
def _compute_health_pct(data: dict, tier_code: int = 0) -> dict:
|
|
2005
|
+
"""Compute equity-based health (0-100%) from gather_lending data + tier.
|
|
2000
2006
|
|
|
2001
2007
|
DeltaPrime has *two* health metrics that agents must not confuse:
|
|
2002
2008
|
|
|
2003
2009
|
1. health_ratio (on-chain, getHealthRatio): 1.0 = liquidation, >1.0 = solvent.
|
|
2004
2010
|
This is the raw weighted-collateral / debt ratio from the SolvencyFacet.
|
|
2005
2011
|
|
|
2006
|
-
2.
|
|
2012
|
+
2. health_pct (equity-based, 0-100%): the scale used in the DeltaPrime frontend
|
|
2007
2013
|
and the account-health-monitor cron. 0% = liquidation, 100% = no debt.
|
|
2008
2014
|
Formula:
|
|
2009
2015
|
equity = supplied_usd - debt_usd
|
|
2010
2016
|
max_mult = 10 if PREMIUM tier else 5 if BASIC
|
|
2011
2017
|
max_debt = equity * (max_mult - 1)
|
|
2012
|
-
|
|
2018
|
+
health_pct = (max_debt - debt_usd) / max_debt * 100
|
|
2013
2019
|
|
|
2014
|
-
Returns dict with keys:
|
|
2020
|
+
Returns dict with keys: health_pct, supplied_usd, debt_usd, equity, max_debt,
|
|
2015
2021
|
tier_label, or error.
|
|
2016
2022
|
"""
|
|
2017
2023
|
supplied_usd = sum(r.get("usd", 0) or 0 for r in data.get("supplied", []))
|
|
@@ -2022,18 +2028,18 @@ def _compute_bruno_health(data: dict, tier_code: int = 0) -> dict:
|
|
|
2022
2028
|
max_mult = {0: 5, 1: 10}.get(tier_code, 5)
|
|
2023
2029
|
|
|
2024
2030
|
if equity <= 0.01:
|
|
2025
|
-
return {"
|
|
2031
|
+
return {"health_pct": 0.0, "supplied_usd": round(supplied_usd, 2),
|
|
2026
2032
|
"debt_usd": round(debt_usd, 2), "equity": round(equity, 2),
|
|
2027
2033
|
"max_debt": 0.0, "tier": tier_label, "error": "equity near zero"}
|
|
2028
2034
|
|
|
2029
2035
|
max_debt = equity * (max_mult - 1)
|
|
2030
2036
|
if max_debt > 0 and debt_usd >= 0:
|
|
2031
|
-
|
|
2032
|
-
|
|
2037
|
+
health_pct = (max_debt - min(debt_usd, max_debt)) / max_debt * 100
|
|
2038
|
+
health_pct = max(0.0, min(100.0, health_pct))
|
|
2033
2039
|
else:
|
|
2034
|
-
|
|
2040
|
+
health_pct = 100.0
|
|
2035
2041
|
|
|
2036
|
-
return {"
|
|
2042
|
+
return {"health_pct": round(health_pct, 1), "supplied_usd": round(supplied_usd, 2),
|
|
2037
2043
|
"debt_usd": round(debt_usd, 2), "equity": round(equity, 2),
|
|
2038
2044
|
"max_debt": round(max_debt, 2), "tier": tier_label}
|
|
2039
2045
|
|
|
@@ -2082,22 +2088,22 @@ def cmd_prime_summary():
|
|
|
2082
2088
|
# astronomically large there); render that as ">1000" rather than a junk number.
|
|
2083
2089
|
ratio_str = ">1000.00 (negligible debt)" if ratio is None else f"{ratio:.4f}"
|
|
2084
2090
|
print(f" Health ratio (chain): {ratio_str} (>1.0 = solvent, 1.0 = liquidation)")
|
|
2085
|
-
# ───
|
|
2086
|
-
# Different from health_ratio! See
|
|
2091
|
+
# ─── Equity-based health (0-100%) ───
|
|
2092
|
+
# Different from health_ratio! See _compute_health_pct docstring.
|
|
2087
2093
|
# Get tier from the Prime Account (oracle-free view)
|
|
2088
2094
|
try:
|
|
2089
2095
|
tier_info = gather_prime_tier(w3, acct, account)
|
|
2090
2096
|
tier_code = tier_info.get("tier_code", 0)
|
|
2091
2097
|
except Exception:
|
|
2092
2098
|
tier_code = 0
|
|
2093
|
-
|
|
2094
|
-
if "error" not in
|
|
2095
|
-
print(f" Health (
|
|
2096
|
-
print(f" (supplied=${
|
|
2097
|
-
f" equity=${
|
|
2099
|
+
hp = _compute_health_pct(data, tier_code)
|
|
2100
|
+
if "error" not in hp:
|
|
2101
|
+
print(f" Health (0-100%): {hp['health_pct']:.1f}%")
|
|
2102
|
+
print(f" (supplied=${hp['supplied_usd']:.2f}, debt=${hp['debt_usd']:.2f},"
|
|
2103
|
+
f" equity=${hp['equity']:.2f}, max_debt=${hp['max_debt']:.2f}, {hp['tier']})")
|
|
2098
2104
|
print(f" 0%=liquidation 50%=half borrowing power used 100%=no debt")
|
|
2099
2105
|
else:
|
|
2100
|
-
print(f" Health (
|
|
2106
|
+
print(f" Health (0-100%): N/A ({hp['error']})")
|
|
2101
2107
|
print(f" Solvent: {'yes' if data['solvent'] else 'NO — liquidatable'}")
|
|
2102
2108
|
else:
|
|
2103
2109
|
print(f" Health/solvency: RedStone fetch/call failed ({data.get('solvency_error', 'error')}); "
|
|
@@ -5003,12 +5009,12 @@ def gather_defi() -> dict:
|
|
|
5003
5009
|
result["total_usd"] = lending["total_value_usd"]
|
|
5004
5010
|
result["health_ratio"] = lending["health_ratio"]
|
|
5005
5011
|
result["solvent"] = lending["solvent"]
|
|
5006
|
-
# Compute
|
|
5007
|
-
result["
|
|
5012
|
+
# Compute equity-based health (0-100%) from lending data + tier
|
|
5013
|
+
result["health_pct"] = _compute_health_pct(lending, tier.get("tier_code", 0)).get("health_pct")
|
|
5008
5014
|
if lending["supplied"] or lending["borrowed"]:
|
|
5009
5015
|
result["groups"].append({
|
|
5010
5016
|
"type": "Lending / Leverage", "health_ratio": lending["health_ratio"],
|
|
5011
|
-
"
|
|
5017
|
+
"health_pct": result["health_pct"],
|
|
5012
5018
|
"supplied": [{"symbol": r["symbol"], "balance": r["balance"], "usd": r.get("usd")}
|
|
5013
5019
|
for r in lending["supplied"]],
|
|
5014
5020
|
"borrowed": [{"symbol": r["symbol"], "balance": r["balance"], "usd": r.get("usd")}
|
|
@@ -5145,6 +5151,7 @@ def cmd_defi(as_json: bool = True):
|
|
|
5145
5151
|
print(json.dumps(_trim_defi_json(data), indent=2))
|
|
5146
5152
|
|
|
5147
5153
|
def main():
|
|
5154
|
+
check_version()
|
|
5148
5155
|
args = sys.argv[1:] if len(sys.argv) > 1 else []
|
|
5149
5156
|
# Global wallet selector: --as <agent>, stripped before command dispatch.
|
|
5150
5157
|
global _SELECTED_AGENT, _CLI_KEY
|
|
@@ -37,10 +37,10 @@ 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
41
|
|
|
42
|
-
PREFERS the precomputed ``
|
|
43
|
-
includes
|
|
42
|
+
PREFERS the precomputed ``health_pct`` from defi --json (which primecli >= 0.5.4
|
|
43
|
+
includes), falling back to manual calculation when absent.
|
|
44
44
|
|
|
45
45
|
Formula:
|
|
46
46
|
equity = total_supplied_usd - total_debt_usd
|
|
@@ -49,7 +49,7 @@ def compute_health(defi_data: dict, max_mult: int = 10) -> dict:
|
|
|
49
49
|
|
|
50
50
|
This is DIFFERENT from getHealthRatio (the on-chain ratio where 1.0 = liquidation).
|
|
51
51
|
Do NOT convert between the two. The ``health_ratio`` metric is the on-chain value
|
|
52
|
-
(1.0=liquidation, >1.0=solvent). The ``
|
|
52
|
+
(1.0=liquidation, >1.0=solvent). The ``health_pct`` is the equity-based frontend
|
|
53
53
|
measurement (0%=liquidation, 50%=half borrowing power used, 100%=no debt).
|
|
54
54
|
|
|
55
55
|
Returns dict with health metrics or error.
|
|
@@ -61,8 +61,8 @@ def compute_health(defi_data: dict, max_mult: int = 10) -> dict:
|
|
|
61
61
|
supplied = g.get("supplied", [])
|
|
62
62
|
borrowed = g.get("borrowed", [])
|
|
63
63
|
health_ratio = g.get("health_ratio", 0) or 0
|
|
64
|
-
# Use precomputed
|
|
65
|
-
precomputed = g.get("bruno_pct")
|
|
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
66
|
if precomputed is not None:
|
|
67
67
|
# Early return: precomputed value exists, enrich with detail fields
|
|
68
68
|
supplied_usd = sum(s.get("usd", 0) or 0 for s in supplied)
|
|
@@ -74,7 +74,7 @@ def compute_health(defi_data: dict, max_mult: int = 10) -> dict:
|
|
|
74
74
|
has_lb = any(sym in ("LB_AVAX_USDC", "LB_WAVAX_USDC", "JOE") or "TRADERJOE" in sym.upper() for sym in symbols)
|
|
75
75
|
has_aero = any("AERO" in sym.upper() or "CL_POSITION" in sym.upper() for sym in symbols)
|
|
76
76
|
return {
|
|
77
|
-
"
|
|
77
|
+
"health_pct": float(precomputed),
|
|
78
78
|
"health_ratio": round(health_ratio, 4),
|
|
79
79
|
"supplied_usd": round(supplied_usd, 2),
|
|
80
80
|
"debt_usd": round(debt_usd, 2),
|
|
@@ -84,7 +84,7 @@ def compute_health(defi_data: dict, max_mult: int = 10) -> dict:
|
|
|
84
84
|
"has_gmx": has_gmx,
|
|
85
85
|
"has_lb": has_lb,
|
|
86
86
|
"has_aero": has_aero,
|
|
87
|
-
"action": "computed from defi --json
|
|
87
|
+
"action": "computed from defi --json health_pct",
|
|
88
88
|
}
|
|
89
89
|
else:
|
|
90
90
|
supplied = defi_data.get("supplied", [])
|
|
@@ -97,7 +97,7 @@ def compute_health(defi_data: dict, max_mult: int = 10) -> dict:
|
|
|
97
97
|
|
|
98
98
|
if equity <= 0.01:
|
|
99
99
|
return {
|
|
100
|
-
"
|
|
100
|
+
"health_pct": 0.0,
|
|
101
101
|
"health_ratio": round(health_ratio, 4),
|
|
102
102
|
"supplied_usd": round(supplied_usd, 2),
|
|
103
103
|
"debt_usd": round(debt_usd, 2),
|
|
@@ -121,15 +121,15 @@ def compute_health(defi_data: dict, max_mult: int = 10) -> dict:
|
|
|
121
121
|
has_aero = any("AERO" in sym.upper() or "CL_POSITION" in sym.upper() for sym in symbols)
|
|
122
122
|
|
|
123
123
|
if max_debt > 0 and debt_usd >= 0:
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
health_pct = (max_debt - min(debt_usd, max_debt)) / max_debt * 100
|
|
125
|
+
health_pct = max(0.0, min(100.0, health_pct))
|
|
126
126
|
else:
|
|
127
|
-
|
|
127
|
+
health_pct = 100.0
|
|
128
128
|
|
|
129
129
|
delta_debt = (max_debt * 0.5) - debt_usd # center target = 50%
|
|
130
130
|
|
|
131
131
|
return {
|
|
132
|
-
"
|
|
132
|
+
"health_pct": round(health_pct, 1),
|
|
133
133
|
"health_ratio": round(health_ratio, 4),
|
|
134
134
|
"supplied_usd": round(supplied_usd, 2),
|
|
135
135
|
"debt_usd": round(debt_usd, 2),
|
|
@@ -320,7 +320,7 @@ def run_tick(
|
|
|
320
320
|
"reason": "equity_near_zero",
|
|
321
321
|
"equity": health["equity"],
|
|
322
322
|
"debt": health["debt_usd"],
|
|
323
|
-
"health_pct": health["
|
|
323
|
+
"health_pct": health["health_pct"],
|
|
324
324
|
"label": label,
|
|
325
325
|
})
|
|
326
326
|
result.update(health)
|
|
@@ -336,23 +336,23 @@ def run_tick(
|
|
|
336
336
|
|
|
337
337
|
# 5. Health swing detection (always)
|
|
338
338
|
last_pct = load_last_health(state_dir)
|
|
339
|
-
if last_pct is not None and health["
|
|
340
|
-
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)
|
|
341
341
|
if diff > 10:
|
|
342
342
|
write_escalation(state_dir, "health-swing", {
|
|
343
343
|
"reason": "health_swing",
|
|
344
344
|
"from_pct": last_pct,
|
|
345
|
-
"to_pct": health["
|
|
345
|
+
"to_pct": health["health_pct"],
|
|
346
346
|
"delta": diff,
|
|
347
347
|
"label": label,
|
|
348
348
|
})
|
|
349
349
|
result["escalation"] = "health_swing"
|
|
350
|
-
save_last_health(state_dir, health["
|
|
350
|
+
save_last_health(state_dir, health["health_pct"] or 0.0)
|
|
351
351
|
|
|
352
352
|
# 6. Append to history
|
|
353
353
|
entry = {
|
|
354
354
|
"ts": now_iso, "mode": mode,
|
|
355
|
-
"pct": health["
|
|
355
|
+
"pct": health["health_pct"],
|
|
356
356
|
"equity": health["equity"],
|
|
357
357
|
"debt": health["debt_usd"],
|
|
358
358
|
"hr": health["health_ratio"],
|
|
@@ -369,7 +369,7 @@ def run_tick(
|
|
|
369
369
|
write_escalation(state_dir, "incomplete-valuation", {
|
|
370
370
|
"reason": "incomplete_valuation",
|
|
371
371
|
"detail": val_reason,
|
|
372
|
-
"health_pct": health["
|
|
372
|
+
"health_pct": health["health_pct"],
|
|
373
373
|
"equity": health["equity"],
|
|
374
374
|
"debt": health["debt_usd"],
|
|
375
375
|
"label": label,
|
|
@@ -387,7 +387,7 @@ def run_tick(
|
|
|
387
387
|
side = strategy.get("side", "short")
|
|
388
388
|
low, high = target_range[0], target_range[1]
|
|
389
389
|
|
|
390
|
-
pct = health["
|
|
390
|
+
pct = health["health_pct"]
|
|
391
391
|
equity = health["equity"]
|
|
392
392
|
debt = health["debt_usd"]
|
|
393
393
|
raw_usdc = health["raw_usdc"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: primecli
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "primecli"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.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"
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
"""primecli - command-line tools for DeltaPrime (Avalanche + Arbitrum) and DegenPrime (Base)."""
|
|
2
|
-
|
|
3
|
-
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
4
|
-
|
|
5
|
-
try:
|
|
6
|
-
__version__ = _pkg_version("primecli")
|
|
7
|
-
except PackageNotFoundError: # running from source without an install
|
|
8
|
-
__version__ = "0.0.0+unknown"
|
|
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
|