primecli 0.6.1__tar.gz → 0.7.0__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.6.1 → primecli-0.7.0}/PKG-INFO +2 -2
- {primecli-0.6.1 → primecli-0.7.0}/README.md +1 -1
- {primecli-0.6.1 → primecli-0.7.0}/primecli/arbprime.py +1 -1
- {primecli-0.6.1 → primecli-0.7.0}/primecli/degenprime.py +21 -7
- {primecli-0.6.1 → primecli-0.7.0}/primecli.egg-info/PKG-INFO +2 -2
- {primecli-0.6.1 → primecli-0.7.0}/primecli.egg-info/SOURCES.txt +1 -0
- {primecli-0.6.1 → primecli-0.7.0}/pyproject.toml +1 -1
- {primecli-0.6.1 → primecli-0.7.0}/tests/test_cross_file_identity.py +5 -0
- primecli-0.7.0/tests/test_health_meter.py +340 -0
- {primecli-0.6.1 → primecli-0.7.0}/tests/test_health_monitor.py +13 -10
- {primecli-0.6.1 → primecli-0.7.0}/LICENSE +0 -0
- {primecli-0.6.1 → primecli-0.7.0}/primecli/__init__.py +0 -0
- {primecli-0.6.1 → primecli-0.7.0}/primecli/deltaprime.py +0 -0
- {primecli-0.6.1 → primecli-0.7.0}/primecli/health_monitor.py +0 -0
- {primecli-0.6.1 → primecli-0.7.0}/primecli.egg-info/dependency_links.txt +0 -0
- {primecli-0.6.1 → primecli-0.7.0}/primecli.egg-info/entry_points.txt +0 -0
- {primecli-0.6.1 → primecli-0.7.0}/primecli.egg-info/requires.txt +0 -0
- {primecli-0.6.1 → primecli-0.7.0}/primecli.egg-info/top_level.txt +0 -0
- {primecli-0.6.1 → primecli-0.7.0}/setup.cfg +0 -0
- {primecli-0.6.1 → primecli-0.7.0}/tests/test_gas_pricing.py +0 -0
- {primecli-0.6.1 → primecli-0.7.0}/tests/test_paraswap_validator.py +0 -0
- {primecli-0.6.1 → primecli-0.7.0}/tests/test_redstone_encoding.py +0 -0
- {primecli-0.6.1 → primecli-0.7.0}/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.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Agent-friendly CLI tools for the DeltaPrime (Avalanche + Arbitrum) and DegenPrime (Base) lending and leverage protocols. Preview-by-default; no Etherscan key required.
|
|
5
5
|
Author: Mnemosyne-quest contributors
|
|
6
6
|
License: MIT
|
|
@@ -47,7 +47,7 @@ Built for agent use:
|
|
|
47
47
|
- RedStone-signed solvency math handled internally, with a regression test pinning the half-boundary `toFixed(8)` encoding.
|
|
48
48
|
- ParaSwap calldata validated client-side against the on-chain executor allowlist before broadcast.
|
|
49
49
|
|
|
50
|
-
**Current version:** 0.
|
|
50
|
+
**Current version:** 0.7.0 The 0.x line is pre-1.0, so breaking changes are possible. See [Releases](https://github.com/Mnemosyne-quest/primecli/releases).
|
|
51
51
|
|
|
52
52
|
> **Breaking change in 0.5.0:** there is no longer a default signing key. Earlier versions silently fell back to a baked-in agent when no key was configured; that fallback has been removed. With no key configured, every command now fails closed with `No signing key found...`. Set a key explicitly (see [Configuration](#configuration)).
|
|
53
53
|
|
|
@@ -16,7 +16,7 @@ Built for agent use:
|
|
|
16
16
|
- RedStone-signed solvency math handled internally, with a regression test pinning the half-boundary `toFixed(8)` encoding.
|
|
17
17
|
- ParaSwap calldata validated client-side against the on-chain executor allowlist before broadcast.
|
|
18
18
|
|
|
19
|
-
**Current version:** 0.
|
|
19
|
+
**Current version:** 0.7.0 The 0.x line is pre-1.0, so breaking changes are possible. See [Releases](https://github.com/Mnemosyne-quest/primecli/releases).
|
|
20
20
|
|
|
21
21
|
> **Breaking change in 0.5.0:** there is no longer a default signing key. Earlier versions silently fell back to a baked-in agent when no key was configured; that fallback has been removed. With no key configured, every command now fails closed with `No signing key found...`. Set a key explicitly (see [Configuration](#configuration)).
|
|
22
22
|
|
|
@@ -503,7 +503,7 @@ _T_WEETH = {"addr": "0x35751007a407ca6FEFfE80b3cB397736D2cf4dbe", "symbol": "we
|
|
|
503
503
|
TJ_LB_PAIRS = {
|
|
504
504
|
"eth-usdc": {"pair": "0x69f1216cB2905bf0852f74624D5Fa7b5FC4dA710", "router": TJ_ROUTER_V21, "binStep": 15, "tokenX": _T_WETH, "tokenY": _T_USDC},
|
|
505
505
|
"eth-usdc-10": {"pair": "0xb7236B927e03542AC3bE0A054F2bEa8868AF9508", "router": TJ_ROUTER_V22, "binStep": 10, "tokenX": _T_WETH, "tokenY": _T_USDC},
|
|
506
|
-
"eth-usdt": {"pair": "0xd387c40a72703B38A5181573724bcaF2Ce6038a5", "router": TJ_ROUTER_V21, "binStep": 15, "tokenX": _T_WETH, "tokenY":
|
|
506
|
+
"eth-usdt": {"pair": "0xd387c40a72703B38A5181573724bcaF2Ce6038a5", "router": TJ_ROUTER_V21, "binStep": 15, "tokenX": _T_WETH, "tokenY": _T_USDT},
|
|
507
507
|
}
|
|
508
508
|
|
|
509
509
|
# LB pair (ILBPair) reads used for previews + position views. getActiveId is the current
|
|
@@ -126,7 +126,6 @@ except ImportError:
|
|
|
126
126
|
BASE_RPC = os.environ.get("DEGENPRIME_RPC", "https://base.publicnode.com")
|
|
127
127
|
EXPLORER = "https://basescan.org"
|
|
128
128
|
CHAIN_ID = 8453
|
|
129
|
-
DEGEN_MAX_MULT = 5 # Fixed max leverage multiplier (no tier system on DegenPrime)
|
|
130
129
|
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
|
|
131
130
|
|
|
132
131
|
# ── Signing key resolution ──────────────────────────────────────────────────
|
|
@@ -1809,6 +1808,14 @@ def _gather_account_state(w3, account, pool_deposits: list):
|
|
|
1809
1808
|
sym = n.rstrip(b"\x00").decode(errors="replace")
|
|
1810
1809
|
if v > 0:
|
|
1811
1810
|
borrowed.append({"symbol": sym, "raw": v, "decimals": _asset_decimals(w3, sym)})
|
|
1811
|
+
# Per-asset on-chain debtCoverage (Base: un-tiered) stamped onto every row so the
|
|
1812
|
+
# frontend-exact getHealthMeter computation has its dc inputs.
|
|
1813
|
+
try:
|
|
1814
|
+
dc_map = _resolve_debt_coverages(w3, [r["symbol"] for r in supplied + borrowed])
|
|
1815
|
+
for r in supplied + borrowed:
|
|
1816
|
+
r["dc"] = dc_map.get(r["symbol"], 0.0)
|
|
1817
|
+
except Exception:
|
|
1818
|
+
pass
|
|
1812
1819
|
|
|
1813
1820
|
# Solvency views (SolvencyFacet) are RedStone-gated: they revert (0xe7764c9e)
|
|
1814
1821
|
# without signed price calldata appended. Fetch a fresh RedStone payload covering
|
|
@@ -1916,7 +1923,7 @@ def gather_defi() -> dict:
|
|
|
1916
1923
|
_rows_borrowed = [_row(r) for r in borrowed]
|
|
1917
1924
|
|
|
1918
1925
|
# Compute equity-based health (0-100%)
|
|
1919
|
-
_hp = _compute_health_pct(_rows_supplied, _rows_borrowed)
|
|
1926
|
+
_hp = _compute_health_pct(_rows_supplied, _rows_borrowed, w3=w3)
|
|
1920
1927
|
result["health_pct"] = _hp.get("health_pct")
|
|
1921
1928
|
|
|
1922
1929
|
if supplied or borrowed:
|
|
@@ -2043,7 +2050,7 @@ def cmd_summary(as_json: bool = False):
|
|
|
2043
2050
|
# Compute equity-based health (0-100%)
|
|
2044
2051
|
_hp_rows = [_asset_row(r) for r in supplied]
|
|
2045
2052
|
_hp_borrowed = [_asset_row(r) for r in borrowed]
|
|
2046
|
-
_hp = _compute_health_pct(_hp_rows, _hp_borrowed)
|
|
2053
|
+
_hp = _compute_health_pct(_hp_rows, _hp_borrowed, w3=w3)
|
|
2047
2054
|
|
|
2048
2055
|
out = {
|
|
2049
2056
|
"wallet": acct.address,
|
|
@@ -2091,17 +2098,24 @@ def cmd_summary(as_json: bool = False):
|
|
|
2091
2098
|
print(f" {r['symbol']:<8} {r['raw'] / 10**r['decimals']:,.6f}{usd_str}")
|
|
2092
2099
|
|
|
2093
2100
|
if solvency["error"] is None:
|
|
2094
|
-
|
|
2095
|
-
|
|
2101
|
+
# A solvency view can come back None even with no error (e.g. a multicall leg that
|
|
2102
|
+
# decoded empty); guard the currency format so summary never crashes on it.
|
|
2103
|
+
tv_str = f"${solvency['total']:,.2f}" if solvency["total"] is not None else "n/a"
|
|
2104
|
+
debt_str = f"${solvency['debt']:,.2f}" if solvency["debt"] is not None else "n/a"
|
|
2105
|
+
print(f" Total value: {tv_str}")
|
|
2106
|
+
print(f" Debt: {debt_str}")
|
|
2096
2107
|
ratio_str = ">1000.00 (negligible debt)" if solvency["ratio"] is None else f"{solvency['ratio']:.4f}"
|
|
2097
2108
|
print(f" Health ratio (chain): {ratio_str} (>1.0 = solvent, 1.0 = liquidation)")
|
|
2098
2109
|
# ─── Equity-based health (0-100%) ───
|
|
2099
2110
|
# Different from health_ratio!
|
|
2100
2111
|
hp = _compute_health_pct(
|
|
2101
|
-
[{"symbol": r["symbol"], "balance": "0", "
|
|
2112
|
+
[{"symbol": r["symbol"], "balance": "0", "dc": r.get("dc", 0.0),
|
|
2113
|
+
"usd": (r["raw"] / 10**r["decimals"]) * solvency["prices"].get(r["symbol"], 0)}
|
|
2102
2114
|
for r in supplied if solvency["prices"].get(r["symbol"])],
|
|
2103
|
-
[{"symbol": r["symbol"], "balance": "0", "
|
|
2115
|
+
[{"symbol": r["symbol"], "balance": "0", "dc": r.get("dc", 0.0),
|
|
2116
|
+
"usd": (r["raw"] / 10**r["decimals"]) * solvency["prices"].get(r["symbol"], 0)}
|
|
2104
2117
|
for r in borrowed if solvency["prices"].get(r["symbol"])],
|
|
2118
|
+
w3=w3,
|
|
2105
2119
|
)
|
|
2106
2120
|
if "error" not in hp:
|
|
2107
2121
|
print(f" Health (0-100%): {hp['health_pct']:.1f}%")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: primecli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Agent-friendly CLI tools for the DeltaPrime (Avalanche + Arbitrum) and DegenPrime (Base) lending and leverage protocols. Preview-by-default; no Etherscan key required.
|
|
5
5
|
Author: Mnemosyne-quest contributors
|
|
6
6
|
License: MIT
|
|
@@ -47,7 +47,7 @@ Built for agent use:
|
|
|
47
47
|
- RedStone-signed solvency math handled internally, with a regression test pinning the half-boundary `toFixed(8)` encoding.
|
|
48
48
|
- ParaSwap calldata validated client-side against the on-chain executor allowlist before broadcast.
|
|
49
49
|
|
|
50
|
-
**Current version:** 0.
|
|
50
|
+
**Current version:** 0.7.0 The 0.x line is pre-1.0, so breaking changes are possible. See [Releases](https://github.com/Mnemosyne-quest/primecli/releases).
|
|
51
51
|
|
|
52
52
|
> **Breaking change in 0.5.0:** there is no longer a default signing key. Earlier versions silently fell back to a baked-in agent when no key was configured; that fallback has been removed. With no key configured, every command now fails closed with `No signing key found...`. Set a key explicitly (see [Configuration](#configuration)).
|
|
53
53
|
|
|
@@ -14,6 +14,7 @@ primecli.egg-info/requires.txt
|
|
|
14
14
|
primecli.egg-info/top_level.txt
|
|
15
15
|
tests/test_cross_file_identity.py
|
|
16
16
|
tests/test_gas_pricing.py
|
|
17
|
+
tests/test_health_meter.py
|
|
17
18
|
tests/test_health_monitor.py
|
|
18
19
|
tests/test_paraswap_validator.py
|
|
19
20
|
tests/test_redstone_encoding.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "primecli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.0"
|
|
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"
|
|
@@ -43,6 +43,11 @@ SHARED_FUNCS = [
|
|
|
43
43
|
"_redstone_package_signer",
|
|
44
44
|
"build_redstone_payload",
|
|
45
45
|
"to_wei_units",
|
|
46
|
+
# Frontend-exact getHealthMeter core + its on-chain debtCoverage resolver. The math
|
|
47
|
+
# and the TokenManager wiring must not drift — a wrong dc or formula misreports every
|
|
48
|
+
# account's health identically wrongly on whichever chain drifted.
|
|
49
|
+
"_health_meter_pct",
|
|
50
|
+
"_resolve_debt_coverages",
|
|
46
51
|
]
|
|
47
52
|
|
|
48
53
|
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""Tests for the frontend-exact getHealthMeter health computation.
|
|
2
|
+
|
|
3
|
+
`_health_meter_pct` is the shared, byte-identical core across deltaprime / arbprime /
|
|
4
|
+
degenprime (the cross-file identity test pins that parity). It is a pure function over a
|
|
5
|
+
per-asset list of {symbol, dc, supplied_usd, borrowed_usd}, so these tests are fully
|
|
6
|
+
offline. `_compute_health_pct` is exercised with `_resolve_debt_coverages` monkeypatched
|
|
7
|
+
so no chain is ever touched (the dc read is the only network seam).
|
|
8
|
+
|
|
9
|
+
The reference formula (HealthMeterFacetProd.getHealthMeter):
|
|
10
|
+
net_i = supplied_usd_i - borrowed_usd_i
|
|
11
|
+
weightedCollateral = Σ dc_i·net_i (net-long legs) - Σ dc_i·(-net_i) (net-short legs)
|
|
12
|
+
weightedBorrowed = Σ dc_i·borrowed_usd_i
|
|
13
|
+
borrowed = Σ borrowed_usd_i (UNWEIGHTED)
|
|
14
|
+
borrowed == 0 -> 100
|
|
15
|
+
wc > 0 and wc + weightedBorrowed > borrowed
|
|
16
|
+
-> (wc + weightedBorrowed - borrowed) / wc · 100 (clamped 0..100)
|
|
17
|
+
else -> 0
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import importlib
|
|
23
|
+
import random
|
|
24
|
+
|
|
25
|
+
import pytest
|
|
26
|
+
|
|
27
|
+
DELTA = importlib.import_module("primecli.deltaprime")
|
|
28
|
+
ARB = importlib.import_module("primecli.arbprime")
|
|
29
|
+
DEGEN = importlib.import_module("primecli.degenprime")
|
|
30
|
+
|
|
31
|
+
DC_MAJOR = 10.0 / 11.0 # 0.909090909… (10x class)
|
|
32
|
+
DC_RISKY = 5.0 / 6.0 # 0.833333333… (5x class)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _leveraged(dc: float, equity: float, debt: float) -> list:
|
|
36
|
+
"""Model a leveraged-farm position: one collateral asset holding (equity+debt) long,
|
|
37
|
+
one debt asset borrowed `debt`. equity = supplied - borrowed across the two legs."""
|
|
38
|
+
return [
|
|
39
|
+
{"symbol": "COLL", "dc": dc, "supplied_usd": equity + debt, "borrowed_usd": 0.0},
|
|
40
|
+
{"symbol": "DEBT", "dc": dc, "supplied_usd": 0.0, "borrowed_usd": debt},
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
# Reference anchors from the brief (queried on-chain): dc 0.909091 and 0.833333
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_uniform_major_dc_zero_half_full():
|
|
49
|
+
"""dc = 0.909091 (10x class), equity = 100: D=0 → 100%, D=500 → 50%, D=1000 → 0%."""
|
|
50
|
+
f = DELTA._health_meter_pct
|
|
51
|
+
assert f(_leveraged(DC_MAJOR, 100, 0))["health_pct"] == 100.0
|
|
52
|
+
assert f(_leveraged(DC_MAJOR, 100, 500))["health_pct"] == 50.0
|
|
53
|
+
assert f(_leveraged(DC_MAJOR, 100, 1000))["health_pct"] == 0.0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_uniform_risky_dc_max_debt_500():
|
|
57
|
+
"""dc = 0.833333 (5x class), equity = 100: health hits exactly 0 at D = 500 (the 5x
|
|
58
|
+
max-debt line). Just under is barely positive, at/over is 0."""
|
|
59
|
+
f = DELTA._health_meter_pct
|
|
60
|
+
assert f(_leveraged(DC_RISKY, 100, 499))["health_pct"] > 0.0
|
|
61
|
+
assert f(_leveraged(DC_RISKY, 100, 500))["health_pct"] == 0.0
|
|
62
|
+
assert f(_leveraged(DC_RISKY, 100, 501))["health_pct"] == 0.0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_no_debt_is_full_health():
|
|
66
|
+
f = DELTA._health_meter_pct
|
|
67
|
+
assert f([{"symbol": "A", "dc": DC_MAJOR, "supplied_usd": 1234.5, "borrowed_usd": 0.0}])["health_pct"] == 100.0
|
|
68
|
+
# No assets at all (empty account) → no debt → 100.
|
|
69
|
+
assert f([])["health_pct"] == 100.0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_net_short_leg_subtracts_from_collateral():
|
|
73
|
+
"""An asset borrowed MORE than its free balance is a net-short leg: it subtracts a
|
|
74
|
+
dc-weighted amount from weightedCollateral (it does not count as collateral)."""
|
|
75
|
+
f = DELTA._health_meter_pct
|
|
76
|
+
# COLL long 1000; X borrowed 600 with only 100 free balance → net short 500 on X.
|
|
77
|
+
assets = [
|
|
78
|
+
{"symbol": "COLL", "dc": DC_MAJOR, "supplied_usd": 1000.0, "borrowed_usd": 0.0},
|
|
79
|
+
{"symbol": "X", "dc": DC_MAJOR, "supplied_usd": 100.0, "borrowed_usd": 600.0},
|
|
80
|
+
]
|
|
81
|
+
wc_plus = DC_MAJOR * 1000.0
|
|
82
|
+
wc_minus = DC_MAJOR * 500.0
|
|
83
|
+
wc = wc_plus - wc_minus
|
|
84
|
+
wb = DC_MAJOR * 600.0
|
|
85
|
+
borrowed = 600.0
|
|
86
|
+
expected = max(0.0, min(100.0, (wc + wb - borrowed) / wc * 100.0))
|
|
87
|
+
assert f(assets)["health_pct"] == pytest.approx(round(expected, 1), abs=0.05)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
91
|
+
# Mixed-dc: sweep vs an independent inline re-implementation of the contract formula
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _reference_health(assets: list) -> float:
|
|
95
|
+
"""Independent re-implementation of getHealthMeter — deliberately NOT sharing code
|
|
96
|
+
with _health_meter_pct, so the sweep cross-checks the real implementation."""
|
|
97
|
+
wcp = wcm = wb = bor = 0.0
|
|
98
|
+
for a in assets:
|
|
99
|
+
dc = a["dc"]
|
|
100
|
+
net = a["supplied_usd"] - a["borrowed_usd"]
|
|
101
|
+
if net > 0:
|
|
102
|
+
wcp += dc * net
|
|
103
|
+
elif net < 0:
|
|
104
|
+
wcm += dc * (-net)
|
|
105
|
+
wb += dc * a["borrowed_usd"]
|
|
106
|
+
bor += a["borrowed_usd"]
|
|
107
|
+
wc = wcp - wcm
|
|
108
|
+
if bor <= 0:
|
|
109
|
+
return 100.0
|
|
110
|
+
if wc > 0 and wc + wb > bor:
|
|
111
|
+
return max(0.0, min(100.0, (wc + wb - bor) / wc * 100.0))
|
|
112
|
+
return 0.0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_mixed_dc_sweep_matches_reference():
|
|
116
|
+
f = DELTA._health_meter_pct
|
|
117
|
+
rng = random.Random(20260606)
|
|
118
|
+
dcs = [DC_MAJOR, DC_RISKY, 0.9, 0.8, 0.75, 0.5]
|
|
119
|
+
for _ in range(20000):
|
|
120
|
+
n = rng.randint(1, 5)
|
|
121
|
+
assets = [
|
|
122
|
+
{
|
|
123
|
+
"symbol": f"S{i}",
|
|
124
|
+
"dc": rng.choice(dcs),
|
|
125
|
+
"supplied_usd": rng.uniform(0, 5000),
|
|
126
|
+
"borrowed_usd": rng.uniform(0, 5000),
|
|
127
|
+
}
|
|
128
|
+
for i in range(n)
|
|
129
|
+
]
|
|
130
|
+
got = f(assets)["health_pct"]
|
|
131
|
+
exp = round(_reference_health(assets), 1)
|
|
132
|
+
# Both round to one decimal; the only gap is half-ULP rounding, well under 0.05.
|
|
133
|
+
assert abs(got - exp) <= 0.05, (assets, got, exp)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_core_is_identical_object_across_siblings():
|
|
137
|
+
"""Defensive: the three siblings expose the same callable behaviour (the cross-file
|
|
138
|
+
identity test pins the source; this pins runtime behaviour on a shared input)."""
|
|
139
|
+
assets = _leveraged(DC_MAJOR, 250, 1000)
|
|
140
|
+
r_delta = DELTA._health_meter_pct(assets)
|
|
141
|
+
r_arb = ARB._health_meter_pct(assets)
|
|
142
|
+
r_degen = DEGEN._health_meter_pct(assets)
|
|
143
|
+
assert r_delta == r_arb == r_degen
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
147
|
+
# _compute_health_pct — dc resolution mocked (no network), tier label + max_debt
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_compute_health_pct_delta_mocks_dc(monkeypatch):
|
|
151
|
+
"""deltaprime._compute_health_pct resolves dc via _resolve_debt_coverages (mocked here),
|
|
152
|
+
merges per-symbol supplied/borrowed, and reports the contract health plus a display
|
|
153
|
+
max_debt and the tier label. PREMIUM (tier 1) → dc 0.909091 → 50% at the half-debt line."""
|
|
154
|
+
monkeypatch.setattr(
|
|
155
|
+
DELTA, "_resolve_debt_coverages",
|
|
156
|
+
lambda w3, syms, tier_code=0: {s: DC_MAJOR for s in syms},
|
|
157
|
+
)
|
|
158
|
+
data = {
|
|
159
|
+
"w3": object(), # never used — dc is mocked
|
|
160
|
+
"supplied": [{"symbol": "USDC", "usd": 600.0}],
|
|
161
|
+
"borrowed": [{"symbol": "AVAX", "usd": 500.0}],
|
|
162
|
+
}
|
|
163
|
+
hp = DELTA._compute_health_pct(data, tier_code=1)
|
|
164
|
+
assert hp["equity"] == 100.0
|
|
165
|
+
assert hp["health_pct"] == 50.0
|
|
166
|
+
assert hp["tier"] == "PREMIUM"
|
|
167
|
+
assert hp["max_debt"] == pytest.approx(1000.0, abs=0.5) # equity·dc/(1-dc) = 100·10 = 1000
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_compute_health_pct_delta_uses_row_dc_without_w3(monkeypatch):
|
|
171
|
+
"""When rows already carry `dc` (stamped by gather_lending), no resolver call is needed
|
|
172
|
+
even with w3 absent."""
|
|
173
|
+
called = {"n": 0}
|
|
174
|
+
|
|
175
|
+
def _boom(*a, **k):
|
|
176
|
+
called["n"] += 1
|
|
177
|
+
raise AssertionError("resolver must not be called when rows carry dc")
|
|
178
|
+
|
|
179
|
+
monkeypatch.setattr(DELTA, "_resolve_debt_coverages", _boom)
|
|
180
|
+
data = {
|
|
181
|
+
"supplied": [{"symbol": "USDC", "usd": 1500.0, "dc": DC_RISKY}],
|
|
182
|
+
"borrowed": [{"symbol": "BTC", "usd": 500.0, "dc": DC_RISKY}],
|
|
183
|
+
}
|
|
184
|
+
hp = DELTA._compute_health_pct(data, tier_code=0)
|
|
185
|
+
assert called["n"] == 0
|
|
186
|
+
assert hp["equity"] == 1000.0
|
|
187
|
+
# equity 1000, dc 0.833333 → max_debt 5000 → debt 500 → health 90%.
|
|
188
|
+
assert hp["health_pct"] == 90.0
|
|
189
|
+
assert hp["tier"] == "BASIC"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_compute_health_pct_degen_list_signature(monkeypatch):
|
|
193
|
+
"""degenprime._compute_health_pct takes (supplied, borrowed) lists and resolves dc with
|
|
194
|
+
the un-tiered Base coverage (mocked). Equity near zero → error branch."""
|
|
195
|
+
monkeypatch.setattr(
|
|
196
|
+
DEGEN, "_resolve_debt_coverages",
|
|
197
|
+
lambda w3, syms, tier_code=0: {s: DC_MAJOR for s in syms},
|
|
198
|
+
)
|
|
199
|
+
supplied = [{"symbol": "USDC", "usd": 1000.0}]
|
|
200
|
+
borrowed = [{"symbol": "USDC", "usd": 1000.0}]
|
|
201
|
+
hp = DEGEN._compute_health_pct(supplied, borrowed, w3=object())
|
|
202
|
+
assert hp["error"] == "equity near zero"
|
|
203
|
+
assert hp["health_pct"] == 0.0
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_compute_health_pct_equity_near_zero_delta(monkeypatch):
|
|
207
|
+
monkeypatch.setattr(
|
|
208
|
+
DELTA, "_resolve_debt_coverages",
|
|
209
|
+
lambda w3, syms, tier_code=0: {s: DC_MAJOR for s in syms},
|
|
210
|
+
)
|
|
211
|
+
data = {
|
|
212
|
+
"w3": object(),
|
|
213
|
+
"supplied": [{"symbol": "USDC", "usd": 1000.0}],
|
|
214
|
+
"borrowed": [{"symbol": "USDC", "usd": 1000.0}],
|
|
215
|
+
}
|
|
216
|
+
hp = DELTA._compute_health_pct(data, tier_code=0)
|
|
217
|
+
assert hp["error"] == "equity near zero"
|
|
218
|
+
assert hp["health_pct"] == 0.0
|
|
219
|
+
assert hp["max_debt"] == 0.0
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
223
|
+
# _resolve_debt_coverages — the on-chain dc read wiring, multicall mocked
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class _FakeCodec:
|
|
227
|
+
def encode(self, types, values):
|
|
228
|
+
from eth_abi import encode as _enc
|
|
229
|
+
return _enc(types, values)
|
|
230
|
+
|
|
231
|
+
def decode(self, types, data):
|
|
232
|
+
from eth_abi import decode as _dec
|
|
233
|
+
return _dec(types, data)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class _FakeW3:
|
|
237
|
+
"""Just enough of a w3 for _resolve_debt_coverages: a codec and a contract whose
|
|
238
|
+
encode_abi produces real selectors+args. multicall is monkeypatched, so no RPC."""
|
|
239
|
+
|
|
240
|
+
def __init__(self):
|
|
241
|
+
self.codec = _FakeCodec()
|
|
242
|
+
self.eth = self
|
|
243
|
+
|
|
244
|
+
def contract(self, address=None, abi=None):
|
|
245
|
+
import web3
|
|
246
|
+
return web3.Web3().eth.contract(address=address, abi=abi)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _enc_addr(addr):
|
|
250
|
+
from eth_abi import encode
|
|
251
|
+
from web3 import Web3
|
|
252
|
+
return encode(["address"], [Web3.to_checksum_address(addr)])
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _enc_dc(dc):
|
|
256
|
+
from eth_abi import encode
|
|
257
|
+
return encode(["uint256"], [int(round(dc * 1e18))])
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _patch_dc_cache(mod, monkeypatch):
|
|
261
|
+
"""Give the module a fresh dc cache so cases don't bleed into each other."""
|
|
262
|
+
monkeypatch.setattr(mod, "_dc_cache", {})
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def test_resolve_dc_tiered_success_avalanche(monkeypatch):
|
|
266
|
+
"""Avalanche/Arbitrum path: getAssetAddress resolves, tieredDebtCoverage(tier) returns a
|
|
267
|
+
nonzero value, so the un-tiered fallback is never consulted."""
|
|
268
|
+
_patch_dc_cache(DELTA, monkeypatch)
|
|
269
|
+
addr = "0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed"
|
|
270
|
+
calls = {"batches": 0}
|
|
271
|
+
|
|
272
|
+
def fake_multicall(w3, legs):
|
|
273
|
+
calls["batches"] += 1
|
|
274
|
+
if calls["batches"] == 1: # getAssetAddress batch
|
|
275
|
+
return [(True, _enc_addr(addr)) for _ in legs]
|
|
276
|
+
if calls["batches"] == 2: # tieredDebtCoverage batch — succeeds
|
|
277
|
+
return [(True, _enc_dc(DC_MAJOR)) for _ in legs]
|
|
278
|
+
# untiered batch — should not be needed, but return something harmless
|
|
279
|
+
return [(True, _enc_dc(DC_RISKY)) for _ in legs]
|
|
280
|
+
|
|
281
|
+
monkeypatch.setattr(DELTA, "multicall", fake_multicall)
|
|
282
|
+
out = DELTA._resolve_debt_coverages(_FakeW3(), ["USDC", "ETH"], tier_code=1)
|
|
283
|
+
assert out == {"USDC": pytest.approx(DC_MAJOR), "ETH": pytest.approx(DC_MAJOR)}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def test_resolve_dc_tiered_reverts_falls_back_untiered(monkeypatch):
|
|
287
|
+
"""Base path: tieredDebtCoverage reverts (success=False), so the resolver falls back to
|
|
288
|
+
the un-tiered debtCoverage value per asset."""
|
|
289
|
+
_patch_dc_cache(DEGEN, monkeypatch)
|
|
290
|
+
addr = "0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed"
|
|
291
|
+
calls = {"batches": 0}
|
|
292
|
+
|
|
293
|
+
def fake_multicall(w3, legs):
|
|
294
|
+
calls["batches"] += 1
|
|
295
|
+
if calls["batches"] == 1: # getAssetAddress
|
|
296
|
+
return [(True, _enc_addr(addr)) for _ in legs]
|
|
297
|
+
if calls["batches"] == 2: # tieredDebtCoverage — reverts on Base
|
|
298
|
+
return [(False, b"") for _ in legs]
|
|
299
|
+
return [(True, _enc_dc(DC_RISKY)) for _ in legs] # untiered debtCoverage
|
|
300
|
+
|
|
301
|
+
monkeypatch.setattr(DEGEN, "multicall", fake_multicall)
|
|
302
|
+
out = DEGEN._resolve_debt_coverages(_FakeW3(), ["DEGEN"], tier_code=0)
|
|
303
|
+
assert out == {"DEGEN": pytest.approx(DC_RISKY)}
|
|
304
|
+
assert calls["batches"] == 3 # addr + tiered(revert) + untiered
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def test_resolve_dc_unresolvable_symbol_is_zero(monkeypatch):
|
|
308
|
+
"""A symbol whose getAssetAddress returns the zero address (not listed) gets dc=0 — it
|
|
309
|
+
then contributes nothing to the health meter, matching the contract skipping it."""
|
|
310
|
+
_patch_dc_cache(DELTA, monkeypatch)
|
|
311
|
+
|
|
312
|
+
def fake_multicall(w3, legs):
|
|
313
|
+
# getAssetAddress returns zero address for the lone symbol; no further batches.
|
|
314
|
+
return [(True, _enc_addr("0x0000000000000000000000000000000000000000")) for _ in legs]
|
|
315
|
+
|
|
316
|
+
monkeypatch.setattr(DELTA, "multicall", fake_multicall)
|
|
317
|
+
out = DELTA._resolve_debt_coverages(_FakeW3(), ["MYSTERY"], tier_code=0)
|
|
318
|
+
assert out == {"MYSTERY": 0.0}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def test_resolve_dc_is_cached(monkeypatch):
|
|
322
|
+
"""Second lookup of the same (symbol, tier) is served from cache — multicall not re-hit."""
|
|
323
|
+
_patch_dc_cache(DELTA, monkeypatch)
|
|
324
|
+
addr = "0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed"
|
|
325
|
+
calls = {"n": 0}
|
|
326
|
+
|
|
327
|
+
def fake_multicall(w3, legs):
|
|
328
|
+
calls["n"] += 1
|
|
329
|
+
b = calls["n"]
|
|
330
|
+
if b == 1:
|
|
331
|
+
return [(True, _enc_addr(addr)) for _ in legs]
|
|
332
|
+
return [(True, _enc_dc(DC_MAJOR)) for _ in legs]
|
|
333
|
+
|
|
334
|
+
monkeypatch.setattr(DELTA, "multicall", fake_multicall)
|
|
335
|
+
w3 = _FakeW3()
|
|
336
|
+
first = DELTA._resolve_debt_coverages(w3, ["USDC"], tier_code=1)
|
|
337
|
+
n_after_first = calls["n"]
|
|
338
|
+
second = DELTA._resolve_debt_coverages(w3, ["USDC"], tier_code=1)
|
|
339
|
+
assert first == second
|
|
340
|
+
assert calls["n"] == n_after_first # no new batches on the cached call
|
|
@@ -7,12 +7,15 @@ subprocess.run, so we monkeypatch that single seam to feed canned `defi --json`
|
|
|
7
7
|
and `prime-tier` output and to record any borrow/repay/gmx-deposit invocation —
|
|
8
8
|
no real process is ever spawned, no chain is touched.
|
|
9
9
|
|
|
10
|
-
Health pct formula (with tier multiplier `max_mult`):
|
|
10
|
+
Health pct formula (uniform-power fallback, with tier multiplier `max_mult`):
|
|
11
11
|
equity = sum(supplied.usd) - sum(borrowed.usd)
|
|
12
|
-
max_debt =
|
|
13
|
-
pct = (
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
max_debt = max_mult * equity
|
|
13
|
+
pct = 100 * (1 - debt / max_debt)
|
|
14
|
+
This matches the on-chain getHealthMeter zero-crossing (debt == max_mult·equity →
|
|
15
|
+
health 0), where max_mult = dc/(1-dc) is the asset's borrowing power (10 for the
|
|
16
|
+
0.909091 class, 5 for 0.833333). For premium (max_mult=10) with equity=2000 →
|
|
17
|
+
max_debt=20000, the debt levels below land on 86.5% (lever), 55% (in range) and
|
|
18
|
+
19% (de-lever) for a 30-70 range.
|
|
16
19
|
"""
|
|
17
20
|
|
|
18
21
|
from __future__ import annotations
|
|
@@ -57,7 +60,7 @@ def test_compute_health_lever_branch_high_pct():
|
|
|
57
60
|
max_mult=10,
|
|
58
61
|
)
|
|
59
62
|
assert h["equity"] == 2000
|
|
60
|
-
assert h["health_pct"] ==
|
|
63
|
+
assert h["health_pct"] == 86.5
|
|
61
64
|
assert h["health_pct"] > 70 # lever territory
|
|
62
65
|
|
|
63
66
|
|
|
@@ -71,7 +74,7 @@ def test_compute_health_in_range_branch():
|
|
|
71
74
|
max_mult=10,
|
|
72
75
|
)
|
|
73
76
|
assert h["equity"] == 2000
|
|
74
|
-
assert h["health_pct"] ==
|
|
77
|
+
assert h["health_pct"] == 55.0
|
|
75
78
|
assert 30 <= h["health_pct"] <= 70
|
|
76
79
|
|
|
77
80
|
|
|
@@ -85,7 +88,7 @@ def test_compute_health_delever_branch_low_pct():
|
|
|
85
88
|
max_mult=10,
|
|
86
89
|
)
|
|
87
90
|
assert h["equity"] == 2000
|
|
88
|
-
assert h["health_pct"] ==
|
|
91
|
+
assert h["health_pct"] == 19.0
|
|
89
92
|
assert h["health_pct"] < 30 # de-lever territory
|
|
90
93
|
|
|
91
94
|
|
|
@@ -110,8 +113,8 @@ def test_compute_health_basic_tier_lower_ceiling():
|
|
|
110
113
|
basic = hm.compute_health(_grouped(supplied, borrowed), max_mult=5)
|
|
111
114
|
premium = hm.compute_health(_grouped(supplied, borrowed), max_mult=10)
|
|
112
115
|
assert basic["equity"] == premium["equity"] == 2000
|
|
113
|
-
assert basic["max_debt"] == 2000 *
|
|
114
|
-
assert premium["max_debt"] == 2000 *
|
|
116
|
+
assert basic["max_debt"] == 2000 * 5
|
|
117
|
+
assert premium["max_debt"] == 2000 * 10
|
|
115
118
|
assert basic["health_pct"] < premium["health_pct"]
|
|
116
119
|
|
|
117
120
|
|
|
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
|