primecli 0.6.0__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.0 → primecli-0.7.0}/PKG-INFO +2 -2
- {primecli-0.6.0 → primecli-0.7.0}/README.md +1 -1
- {primecli-0.6.0 → primecli-0.7.0}/primecli/arbprime.py +255 -38
- {primecli-0.6.0 → primecli-0.7.0}/primecli/degenprime.py +251 -41
- {primecli-0.6.0 → primecli-0.7.0}/primecli/deltaprime.py +254 -37
- {primecli-0.6.0 → primecli-0.7.0}/primecli.egg-info/PKG-INFO +2 -2
- {primecli-0.6.0 → primecli-0.7.0}/primecli.egg-info/SOURCES.txt +1 -0
- {primecli-0.6.0 → primecli-0.7.0}/pyproject.toml +1 -1
- {primecli-0.6.0 → 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.0 → primecli-0.7.0}/tests/test_health_monitor.py +13 -10
- {primecli-0.6.0 → primecli-0.7.0}/LICENSE +0 -0
- {primecli-0.6.0 → primecli-0.7.0}/primecli/__init__.py +0 -0
- {primecli-0.6.0 → primecli-0.7.0}/primecli/health_monitor.py +0 -0
- {primecli-0.6.0 → primecli-0.7.0}/primecli.egg-info/dependency_links.txt +0 -0
- {primecli-0.6.0 → primecli-0.7.0}/primecli.egg-info/entry_points.txt +0 -0
- {primecli-0.6.0 → primecli-0.7.0}/primecli.egg-info/requires.txt +0 -0
- {primecli-0.6.0 → primecli-0.7.0}/primecli.egg-info/top_level.txt +0 -0
- {primecli-0.6.0 → primecli-0.7.0}/setup.cfg +0 -0
- {primecli-0.6.0 → primecli-0.7.0}/tests/test_gas_pricing.py +0 -0
- {primecli-0.6.0 → primecli-0.7.0}/tests/test_paraswap_validator.py +0 -0
- {primecli-0.6.0 → primecli-0.7.0}/tests/test_redstone_encoding.py +0 -0
- {primecli-0.6.0 → 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
|
|
@@ -1921,8 +1921,21 @@ def gather_lending(w3, account):
|
|
|
1921
1921
|
sym = n.rstrip(b"\x00").decode(errors="replace")
|
|
1922
1922
|
if sym and sym not in feeds:
|
|
1923
1923
|
feeds.append(sym)
|
|
1924
|
-
out = {"supplied": supplied, "borrowed": borrowed,
|
|
1924
|
+
out = {"supplied": supplied, "borrowed": borrowed, "w3": w3,
|
|
1925
1925
|
"total_value_usd": None, "debt_usd": None, "health_ratio": None, "solvent": None}
|
|
1926
|
+
# Resolve the account's PRIME tier (oracle-free) and per-asset on-chain debtCoverage,
|
|
1927
|
+
# then stamp dc onto every row so _compute_health_pct can run getHealthMeter exactly.
|
|
1928
|
+
try:
|
|
1929
|
+
tier_code = account.functions.getLeverageTierFullInfo().call()[0]
|
|
1930
|
+
except Exception:
|
|
1931
|
+
tier_code = 0
|
|
1932
|
+
out["tier_code"] = tier_code
|
|
1933
|
+
try:
|
|
1934
|
+
dc_map = _resolve_debt_coverages(w3, [r["symbol"] for r in supplied + borrowed], tier_code)
|
|
1935
|
+
for r in supplied + borrowed:
|
|
1936
|
+
r["dc"] = dc_map.get(r["symbol"], 0.0)
|
|
1937
|
+
except Exception:
|
|
1938
|
+
pass
|
|
1926
1939
|
try:
|
|
1927
1940
|
payload = build_redstone_payload(feeds)
|
|
1928
1941
|
payload_hex = payload.hex()
|
|
@@ -1983,46 +1996,188 @@ def gather_lending(w3, account):
|
|
|
1983
1996
|
r["usd"] = None
|
|
1984
1997
|
return out
|
|
1985
1998
|
|
|
1999
|
+
def _health_meter_pct(assets: list) -> dict:
|
|
2000
|
+
"""getHealthMeter() exactly as the on-chain HealthMeterFacetProd renders it.
|
|
2001
|
+
|
|
2002
|
+
The frontend health meter is NOT equity*(mult-1); it is a per-asset, debtCoverage-
|
|
2003
|
+
weighted formula. For each asset i with USD-valued long balance and borrow, and its
|
|
2004
|
+
live debtCoverage dc_i:
|
|
2005
|
+
|
|
2006
|
+
net_i = supplied_usd_i - borrowed_usd_i
|
|
2007
|
+
weightedCollateralPlus = Σ dc_i·net_i for net_i > 0 (net-long legs)
|
|
2008
|
+
weightedCollateralMinus = Σ dc_i·(-net_i) for net_i < 0 (net-short legs)
|
|
2009
|
+
weightedCollateral = weightedCollateralPlus - weightedCollateralMinus
|
|
2010
|
+
weightedBorrowed = Σ dc_i·borrowed_usd_i
|
|
2011
|
+
borrowed = Σ borrowed_usd_i (UNWEIGHTED)
|
|
2012
|
+
|
|
2013
|
+
borrowed == 0 -> 100
|
|
2014
|
+
weightedCollateral > 0 and
|
|
2015
|
+
weightedCollateral + weightedBorrowed > borrowed
|
|
2016
|
+
-> (weightedCollateral + weightedBorrowed - borrowed) / weightedCollateral · 100
|
|
2017
|
+
else -> 0
|
|
2018
|
+
|
|
2019
|
+
Result clamped to [0, 100]. `assets` is a list of
|
|
2020
|
+
{"symbol", "dc", "supplied_usd", "borrowed_usd"}; missing usd legs count as 0.
|
|
2021
|
+
For a uniform-dc single-collateral position this reduces to the familiar
|
|
2022
|
+
(max_debt - debt)/max_debt·100 with max_debt = equity·dc/(1-dc).
|
|
2023
|
+
"""
|
|
2024
|
+
wc_plus = 0.0
|
|
2025
|
+
wc_minus = 0.0
|
|
2026
|
+
weighted_borrowed = 0.0
|
|
2027
|
+
borrowed = 0.0
|
|
2028
|
+
supplied_usd = 0.0
|
|
2029
|
+
debt_usd = 0.0
|
|
2030
|
+
for a in assets:
|
|
2031
|
+
dc = a.get("dc", 0.0) or 0.0
|
|
2032
|
+
sup = a.get("supplied_usd", 0.0) or 0.0
|
|
2033
|
+
bor = a.get("borrowed_usd", 0.0) or 0.0
|
|
2034
|
+
supplied_usd += sup
|
|
2035
|
+
debt_usd += bor
|
|
2036
|
+
net = sup - bor
|
|
2037
|
+
if net > 0:
|
|
2038
|
+
wc_plus += dc * net
|
|
2039
|
+
elif net < 0:
|
|
2040
|
+
wc_minus += dc * (-net)
|
|
2041
|
+
weighted_borrowed += dc * bor
|
|
2042
|
+
borrowed += bor
|
|
2043
|
+
weighted_collateral = wc_plus - wc_minus
|
|
2044
|
+
equity = supplied_usd - debt_usd
|
|
2045
|
+
if borrowed <= 0:
|
|
2046
|
+
health_pct = 100.0
|
|
2047
|
+
elif weighted_collateral > 0 and (weighted_collateral + weighted_borrowed) > borrowed:
|
|
2048
|
+
health_pct = (weighted_collateral + weighted_borrowed - borrowed) / weighted_collateral * 100.0
|
|
2049
|
+
health_pct = max(0.0, min(100.0, health_pct))
|
|
2050
|
+
else:
|
|
2051
|
+
health_pct = 0.0
|
|
2052
|
+
return {"health_pct": round(health_pct, 1), "supplied_usd": round(supplied_usd, 2),
|
|
2053
|
+
"debt_usd": round(debt_usd, 2), "equity": round(equity, 2),
|
|
2054
|
+
"weighted_collateral": round(weighted_collateral, 2),
|
|
2055
|
+
"weighted_borrowed": round(weighted_borrowed, 2)}
|
|
2056
|
+
|
|
2057
|
+
|
|
2058
|
+
_dc_cache = {}
|
|
2059
|
+
|
|
2060
|
+
|
|
2061
|
+
def _resolve_debt_coverages(w3, symbols: list, tier_code: int = 0) -> dict:
|
|
2062
|
+
"""Per-asset debtCoverage read LIVE on-chain from the TokenManager, keyed by symbol.
|
|
2063
|
+
|
|
2064
|
+
Resolves each symbol to its token address via getAssetAddress(bytes32,true), then reads
|
|
2065
|
+
the account's effective coverage: tieredDebtCoverage(tier, token) on Avalanche/Arbitrum
|
|
2066
|
+
(the contract's getHealthMeter uses getPrimeLeverageTier() for exactly this), falling
|
|
2067
|
+
back to the un-tiered debtCoverage(token). Cached per run keyed by (symbol, tier_code).
|
|
2068
|
+
Batched through multicall so N assets cost ~2 eth_calls, not 2N. Symbols that don't
|
|
2069
|
+
resolve get dc=0 (they contribute nothing — same as the contract skipping an unpriced
|
|
2070
|
+
leg)."""
|
|
2071
|
+
want = [s for s in dict.fromkeys(symbols) if s]
|
|
2072
|
+
out = {}
|
|
2073
|
+
missing = []
|
|
2074
|
+
for s in want:
|
|
2075
|
+
ck = (s, tier_code)
|
|
2076
|
+
if ck in _dc_cache:
|
|
2077
|
+
out[s] = _dc_cache[ck]
|
|
2078
|
+
else:
|
|
2079
|
+
missing.append(s)
|
|
2080
|
+
if not missing:
|
|
2081
|
+
return out
|
|
2082
|
+
tm_abi = json.loads(
|
|
2083
|
+
'[{"inputs":[{"name":"_asset","type":"bytes32"},{"name":"_active","type":"bool"}],'
|
|
2084
|
+
'"name":"getAssetAddress","outputs":[{"type":"address"}],"stateMutability":"view","type":"function"},'
|
|
2085
|
+
'{"inputs":[{"name":"a","type":"address"}],"name":"debtCoverage","outputs":[{"type":"uint256"}],'
|
|
2086
|
+
'"stateMutability":"view","type":"function"},'
|
|
2087
|
+
'{"inputs":[{"name":"t","type":"uint8"},{"name":"a","type":"address"}],"name":"tieredDebtCoverage",'
|
|
2088
|
+
'"outputs":[{"type":"uint256"}],"stateMutability":"view","type":"function"}]')
|
|
2089
|
+
tm = w3.eth.contract(address=Web3.to_checksum_address(TOKEN_MANAGER), abi=tm_abi)
|
|
2090
|
+
addr_legs = [(TOKEN_MANAGER, bytes.fromhex(tm.encode_abi("getAssetAddress", args=[asset_b32(s), True])[2:]))
|
|
2091
|
+
for s in missing]
|
|
2092
|
+
addr_res = multicall(w3, addr_legs)
|
|
2093
|
+
addrs = {}
|
|
2094
|
+
for s, (ok, rd) in zip(missing, addr_res):
|
|
2095
|
+
try:
|
|
2096
|
+
a = w3.codec.decode(["address"], rd)[0] if ok and rd else None
|
|
2097
|
+
except Exception:
|
|
2098
|
+
a = None
|
|
2099
|
+
addrs[s] = a if a and int(a, 16) != 0 else None
|
|
2100
|
+
resolvable = [s for s in missing if addrs[s]]
|
|
2101
|
+
# Try tiered coverage first (Avalanche/Arbitrum); fall back per-asset to un-tiered.
|
|
2102
|
+
tiered_legs = [(TOKEN_MANAGER, bytes.fromhex(
|
|
2103
|
+
tm.encode_abi("tieredDebtCoverage", args=[tier_code, Web3.to_checksum_address(addrs[s])])[2:]))
|
|
2104
|
+
for s in resolvable]
|
|
2105
|
+
untiered_legs = [(TOKEN_MANAGER, bytes.fromhex(
|
|
2106
|
+
tm.encode_abi("debtCoverage", args=[Web3.to_checksum_address(addrs[s])])[2:]))
|
|
2107
|
+
for s in resolvable]
|
|
2108
|
+
tiered_res = multicall(w3, tiered_legs) if tiered_legs else []
|
|
2109
|
+
untiered_res = multicall(w3, untiered_legs) if untiered_legs else []
|
|
2110
|
+
for i, s in enumerate(resolvable):
|
|
2111
|
+
dc = 0.0
|
|
2112
|
+
ok_t, rd_t = tiered_res[i]
|
|
2113
|
+
if ok_t and rd_t:
|
|
2114
|
+
try:
|
|
2115
|
+
dc = w3.codec.decode(["uint256"], rd_t)[0] / 1e18
|
|
2116
|
+
except Exception:
|
|
2117
|
+
dc = 0.0
|
|
2118
|
+
if dc <= 0:
|
|
2119
|
+
ok_u, rd_u = untiered_res[i]
|
|
2120
|
+
if ok_u and rd_u:
|
|
2121
|
+
try:
|
|
2122
|
+
dc = w3.codec.decode(["uint256"], rd_u)[0] / 1e18
|
|
2123
|
+
except Exception:
|
|
2124
|
+
dc = 0.0
|
|
2125
|
+
_dc_cache[(s, tier_code)] = dc
|
|
2126
|
+
out[s] = dc
|
|
2127
|
+
for s in missing:
|
|
2128
|
+
if s not in out:
|
|
2129
|
+
_dc_cache[(s, tier_code)] = 0.0
|
|
2130
|
+
out[s] = 0.0
|
|
2131
|
+
return out
|
|
2132
|
+
|
|
2133
|
+
|
|
1986
2134
|
def _compute_health_pct(data: dict, tier_code: int = 0) -> dict:
|
|
1987
|
-
"""
|
|
2135
|
+
"""Frontend-exact health (0-100%) for a Prime Account — wraps _health_meter_pct with
|
|
2136
|
+
on-chain debtCoverage resolution and the tier label.
|
|
1988
2137
|
|
|
1989
2138
|
DeltaPrime has *two* health metrics that agents must not confuse:
|
|
1990
2139
|
|
|
1991
2140
|
1. health_ratio (on-chain, getHealthRatio): 1.0 = liquidation, >1.0 = solvent.
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
2. health_pct (
|
|
1995
|
-
and the account-health-monitor cron. 0% = liquidation, 100% = no debt.
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
tier_label, or error.
|
|
2141
|
+
The raw weighted-collateral / debt ratio from the SolvencyFacet.
|
|
2142
|
+
|
|
2143
|
+
2. health_pct (0-100%, getHealthMeter): the scale the DeltaPrime frontend renders
|
|
2144
|
+
and the account-health-monitor cron acts on. 0% = liquidation, 100% = no debt.
|
|
2145
|
+
Computed by _health_meter_pct with per-asset dc from tieredDebtCoverage at the
|
|
2146
|
+
account's PRIME tier — NOT the old equity*(mult-1) approximation.
|
|
2147
|
+
|
|
2148
|
+
Per-asset USD comes from gather_lending (rows carry `dc` once resolved); if a row has
|
|
2149
|
+
no `dc` key yet it is resolved here from `data["w3"]` when present. Returns
|
|
2150
|
+
health_pct, supplied_usd, debt_usd, equity, max_debt (display zero-crossing debt),
|
|
2151
|
+
tier, or error.
|
|
2004
2152
|
"""
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
equity = supplied_usd - debt_usd
|
|
2153
|
+
supplied = data.get("supplied", [])
|
|
2154
|
+
borrowed = data.get("borrowed", [])
|
|
2008
2155
|
tier_labels = {0: "BASIC", 1: "PREMIUM", 2: "_NON_EXISTENT"}
|
|
2009
2156
|
tier_label = tier_labels.get(tier_code, str(tier_code))
|
|
2010
|
-
|
|
2011
|
-
|
|
2157
|
+
# Per-symbol long/short USD, merging an asset that is both supplied and borrowed.
|
|
2158
|
+
syms = list(dict.fromkeys([r["symbol"] for r in supplied + borrowed if r.get("symbol")]))
|
|
2159
|
+
dc_map = {r["symbol"]: r["dc"] for r in supplied + borrowed if r.get("dc") is not None}
|
|
2160
|
+
need = [s for s in syms if s not in dc_map]
|
|
2161
|
+
if need and data.get("w3") is not None:
|
|
2162
|
+
dc_map.update(_resolve_debt_coverages(data["w3"], need, tier_code))
|
|
2163
|
+
assets = []
|
|
2164
|
+
for s in syms:
|
|
2165
|
+
sup = sum(r.get("usd", 0) or 0 for r in supplied if r.get("symbol") == s)
|
|
2166
|
+
bor = sum(r.get("usd", 0) or 0 for r in borrowed if r.get("symbol") == s)
|
|
2167
|
+
assets.append({"symbol": s, "dc": dc_map.get(s, 0.0), "supplied_usd": sup, "borrowed_usd": bor})
|
|
2168
|
+
res = _health_meter_pct(assets)
|
|
2169
|
+
equity = res["equity"]
|
|
2012
2170
|
if equity <= 0.01:
|
|
2013
|
-
return {"health_pct": 0.0, "supplied_usd":
|
|
2014
|
-
"debt_usd":
|
|
2171
|
+
return {"health_pct": 0.0, "supplied_usd": res["supplied_usd"],
|
|
2172
|
+
"debt_usd": res["debt_usd"], "equity": equity,
|
|
2015
2173
|
"max_debt": 0.0, "tier": tier_label, "error": "equity near zero"}
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
return {"health_pct": round(health_pct, 1), "supplied_usd": round(supplied_usd, 2),
|
|
2025
|
-
"debt_usd": round(debt_usd, 2), "equity": round(equity, 2),
|
|
2174
|
+
# Display-only zero-crossing debt: the unweighted borrow at which health hits 0,
|
|
2175
|
+
# equity·dc_eff/(1-dc_eff) for the position's value-weighted collateral dc.
|
|
2176
|
+
coll_usd = sum(a["supplied_usd"] for a in assets) or 0.0
|
|
2177
|
+
dc_eff = (sum(a["dc"] * a["supplied_usd"] for a in assets) / coll_usd) if coll_usd > 0 else 0.0
|
|
2178
|
+
max_debt = equity * dc_eff / (1.0 - dc_eff) if 0 < dc_eff < 1 else 0.0
|
|
2179
|
+
return {"health_pct": res["health_pct"], "supplied_usd": res["supplied_usd"],
|
|
2180
|
+
"debt_usd": res["debt_usd"], "equity": equity,
|
|
2026
2181
|
"max_debt": round(max_debt, 2), "tier": tier_label}
|
|
2027
2182
|
|
|
2028
2183
|
|
|
@@ -2148,21 +2303,32 @@ def cmd_repay(pool_name: str, amount: float, execute: bool = False):
|
|
|
2148
2303
|
# The facet's repay reverts if amount > debt OR amount > in-account balance.
|
|
2149
2304
|
# Cap to min(requested, debt, in_account) so callers don't need to know either
|
|
2150
2305
|
# exact figure — pass an overshoot like 9999 and it clips cleanly.
|
|
2306
|
+
# ALSO: the contract's _getAvailableBalance() subtracts pending withdrawal intents,
|
|
2307
|
+
# so we subtract total_intent from the raw balance to get the true cap.
|
|
2151
2308
|
requested_wei = to_wei_units(amount, cfg["decimals"])
|
|
2152
2309
|
debt_wei = pool.functions.getBorrowed(pa_cs).call()
|
|
2153
2310
|
in_acct_wei = account.functions.getBalance(asset_b32(symbol)).call()
|
|
2311
|
+
try:
|
|
2312
|
+
total_intent_wei = account.functions.getTotalIntentAmount(asset_b32(symbol)).call()
|
|
2313
|
+
except Exception:
|
|
2314
|
+
total_intent_wei = 0
|
|
2315
|
+
available_wei = in_acct_wei - total_intent_wei if in_acct_wei > total_intent_wei else 0
|
|
2154
2316
|
if debt_wei == 0:
|
|
2155
2317
|
print(f"No {symbol} debt to repay on Prime Account {pa}.")
|
|
2156
2318
|
return
|
|
2157
|
-
amount_wei = min(requested_wei, debt_wei,
|
|
2319
|
+
amount_wei = min(requested_wei, debt_wei, available_wei)
|
|
2158
2320
|
if amount_wei == 0:
|
|
2159
|
-
print(f"Repay {amount} {symbol}:
|
|
2160
|
-
f"
|
|
2321
|
+
print(f"Repay {amount} {symbol}: available {symbol} balance is 0 "
|
|
2322
|
+
f"(total {in_acct_wei / 10**cfg['decimals']:.6f} minus "
|
|
2323
|
+
f"{total_intent_wei / 10**cfg['decimals']:.6f} pending withdrawal intent) — "
|
|
2324
|
+
f"swap into {symbol} first or wait for intents to mature.")
|
|
2161
2325
|
return
|
|
2162
2326
|
cap_notes = []
|
|
2163
2327
|
if amount_wei < requested_wei:
|
|
2164
|
-
if
|
|
2165
|
-
cap_notes.append(f"
|
|
2328
|
+
if available_wei < min(requested_wei, debt_wei):
|
|
2329
|
+
cap_notes.append(f"available {symbol} only {available_wei / 10**cfg['decimals']:.6f} "
|
|
2330
|
+
f"(total {in_acct_wei / 10**cfg['decimals']:.6f} minus "
|
|
2331
|
+
f"{total_intent_wei / 10**cfg['decimals']:.6f} pending intent)")
|
|
2166
2332
|
if debt_wei < requested_wei:
|
|
2167
2333
|
cap_notes.append(f"debt only {debt_wei / 10**cfg['decimals']:.6f} {symbol}")
|
|
2168
2334
|
|
|
@@ -2172,11 +2338,15 @@ def cmd_repay(pool_name: str, amount: float, execute: bool = False):
|
|
|
2172
2338
|
print(f" Capped from requested {amount}: {'; '.join(cap_notes)}")
|
|
2173
2339
|
print(f" Calls repay(bytes32 '{symbol}', {amount_wei}) on the Prime Account")
|
|
2174
2340
|
print(f" Current debt: {debt_wei / 10**cfg['decimals']:.6f} {symbol} | "
|
|
2175
|
-
f"in-account: {in_acct_wei / 10**cfg['decimals']:.6f} {symbol}"
|
|
2341
|
+
f"in-account: {in_acct_wei / 10**cfg['decimals']:.6f} {symbol} | "
|
|
2342
|
+
f"available: {available_wei / 10**cfg['decimals']:.6f} {symbol}")
|
|
2176
2343
|
if in_acct_wei < debt_wei:
|
|
2177
2344
|
shortfall = (debt_wei - in_acct_wei) / 10**cfg['decimals']
|
|
2178
2345
|
print(f" Note: in-account < debt by {shortfall:.6f} {symbol} — "
|
|
2179
2346
|
f"swap into {symbol} first to close the position fully.")
|
|
2347
|
+
if total_intent_wei > 0:
|
|
2348
|
+
print(f" Note: {total_intent_wei / 10**cfg['decimals']:.6f} {symbol} is locked in pending "
|
|
2349
|
+
f"withdrawal intent(s) and not available for repay.")
|
|
2180
2350
|
print("Run with --execute to broadcast")
|
|
2181
2351
|
return
|
|
2182
2352
|
|
|
@@ -2203,6 +2373,53 @@ def cmd_repay(pool_name: str, amount: float, execute: bool = False):
|
|
|
2203
2373
|
repaid = amount_wei / 10**cfg['decimals']
|
|
2204
2374
|
print(f"{'✓' if ok else '✗'} Repay {repaid:.6f} {symbol} {'confirmed' if ok else 'failed'}")
|
|
2205
2375
|
print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
|
|
2376
|
+
if not ok:
|
|
2377
|
+
_print_revert_reason(w3, tx, receipt)
|
|
2378
|
+
|
|
2379
|
+
def _print_revert_reason(w3, tx, receipt):
|
|
2380
|
+
"""Try to decode and print the revert reason from a failed tx."""
|
|
2381
|
+
try:
|
|
2382
|
+
result = w3.eth.call({
|
|
2383
|
+
"from": tx["from"], "to": tx["to"], "data": tx["input"],
|
|
2384
|
+
"gas": receipt["gasUsed"],
|
|
2385
|
+
"maxFeePerGas": tx.get("maxFeePerGas", tx.get("gasPrice", 0)),
|
|
2386
|
+
"maxPriorityFeePerGas": tx.get("maxPriorityFeePerGas", 0),
|
|
2387
|
+
}, receipt["blockNumber"])
|
|
2388
|
+
except Exception as e:
|
|
2389
|
+
err = str(e)
|
|
2390
|
+
data = err
|
|
2391
|
+
if isinstance(e.args, (list, tuple)):
|
|
2392
|
+
for arg in e.args:
|
|
2393
|
+
if isinstance(arg, str) and arg.startswith("0x"):
|
|
2394
|
+
data = arg
|
|
2395
|
+
break
|
|
2396
|
+
elif isinstance(arg, dict) and "data" in arg:
|
|
2397
|
+
data = arg["data"]
|
|
2398
|
+
break
|
|
2399
|
+
if isinstance(data, str) and data.startswith("0x") and len(data) >= 10:
|
|
2400
|
+
sel = data[:10]
|
|
2401
|
+
_known_errors = {
|
|
2402
|
+
"0x567fe27a": "Unknown error from Prime Account facet",
|
|
2403
|
+
"0xf4d678b8": "Execution rejected (may be intent-locked balance check)",
|
|
2404
|
+
"0x441a702e": "InsufficientBalance()",
|
|
2405
|
+
"0xfd36fde3": "SignerNotAuthorised (RedStone signer mismatch)",
|
|
2406
|
+
"0x92ba160c": "RedstoneConsensus()",
|
|
2407
|
+
"0xc2c286b7": "SignerNotAuthorised(address)",
|
|
2408
|
+
"0x08c379a0": "require() revert: see message below",
|
|
2409
|
+
}
|
|
2410
|
+
label = _known_errors.get(sel, f"Unknown custom error 0x{sel}")
|
|
2411
|
+
print(f" Revert: {label}")
|
|
2412
|
+
if sel == "0x08c379a0" and len(data) >= 138:
|
|
2413
|
+
try:
|
|
2414
|
+
from eth_abi import decode
|
|
2415
|
+
msg_bytes = bytes.fromhex(data[10:])
|
|
2416
|
+
decoded = decode(["string"], msg_bytes)
|
|
2417
|
+
print(f' Require message: "{decoded[0]}"')
|
|
2418
|
+
except Exception:
|
|
2419
|
+
pass
|
|
2420
|
+
else:
|
|
2421
|
+
print(f" Revert reason: {err[:200]}")
|
|
2422
|
+
|
|
2206
2423
|
|
|
2207
2424
|
def _decode_formatted_offer(raw: bytes):
|
|
2208
2425
|
"""Manually decode YieldYak's FormattedOffer struct
|