primecli 0.6.0__tar.gz → 0.6.1__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.6.1}/PKG-INFO +1 -1
- {primecli-0.6.0 → primecli-0.6.1}/primecli/arbprime.py +254 -37
- {primecli-0.6.0 → primecli-0.6.1}/primecli/degenprime.py +230 -34
- {primecli-0.6.0 → primecli-0.6.1}/primecli/deltaprime.py +254 -37
- {primecli-0.6.0 → primecli-0.6.1}/primecli.egg-info/PKG-INFO +1 -1
- {primecli-0.6.0 → primecli-0.6.1}/pyproject.toml +1 -1
- {primecli-0.6.0 → primecli-0.6.1}/LICENSE +0 -0
- {primecli-0.6.0 → primecli-0.6.1}/README.md +0 -0
- {primecli-0.6.0 → primecli-0.6.1}/primecli/__init__.py +0 -0
- {primecli-0.6.0 → primecli-0.6.1}/primecli/health_monitor.py +0 -0
- {primecli-0.6.0 → primecli-0.6.1}/primecli.egg-info/SOURCES.txt +0 -0
- {primecli-0.6.0 → primecli-0.6.1}/primecli.egg-info/dependency_links.txt +0 -0
- {primecli-0.6.0 → primecli-0.6.1}/primecli.egg-info/entry_points.txt +0 -0
- {primecli-0.6.0 → primecli-0.6.1}/primecli.egg-info/requires.txt +0 -0
- {primecli-0.6.0 → primecli-0.6.1}/primecli.egg-info/top_level.txt +0 -0
- {primecli-0.6.0 → primecli-0.6.1}/setup.cfg +0 -0
- {primecli-0.6.0 → primecli-0.6.1}/tests/test_cross_file_identity.py +0 -0
- {primecli-0.6.0 → primecli-0.6.1}/tests/test_gas_pricing.py +0 -0
- {primecli-0.6.0 → primecli-0.6.1}/tests/test_health_monitor.py +0 -0
- {primecli-0.6.0 → primecli-0.6.1}/tests/test_paraswap_validator.py +0 -0
- {primecli-0.6.0 → primecli-0.6.1}/tests/test_redstone_encoding.py +0 -0
- {primecli-0.6.0 → primecli-0.6.1}/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.6.
|
|
3
|
+
Version: 0.6.1
|
|
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
|
|
@@ -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
|
|
@@ -833,39 +833,175 @@ def _asset_meta(w3, symbol: str):
|
|
|
833
833
|
# gateway once instead of per-feed-symbol. Cleared implicitly when the process exits.
|
|
834
834
|
_redstone_gateway_cache = None
|
|
835
835
|
|
|
836
|
-
def
|
|
837
|
-
"""
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
836
|
+
def _health_meter_pct(assets: list) -> dict:
|
|
837
|
+
"""getHealthMeter() exactly as the on-chain HealthMeterFacetProd renders it.
|
|
838
|
+
|
|
839
|
+
The frontend health meter is NOT equity*(mult-1); it is a per-asset, debtCoverage-
|
|
840
|
+
weighted formula. For each asset i with USD-valued long balance and borrow, and its
|
|
841
|
+
live debtCoverage dc_i:
|
|
842
|
+
|
|
843
|
+
net_i = supplied_usd_i - borrowed_usd_i
|
|
844
|
+
weightedCollateralPlus = Σ dc_i·net_i for net_i > 0 (net-long legs)
|
|
845
|
+
weightedCollateralMinus = Σ dc_i·(-net_i) for net_i < 0 (net-short legs)
|
|
846
|
+
weightedCollateral = weightedCollateralPlus - weightedCollateralMinus
|
|
847
|
+
weightedBorrowed = Σ dc_i·borrowed_usd_i
|
|
848
|
+
borrowed = Σ borrowed_usd_i (UNWEIGHTED)
|
|
849
|
+
|
|
850
|
+
borrowed == 0 -> 100
|
|
851
|
+
weightedCollateral > 0 and
|
|
852
|
+
weightedCollateral + weightedBorrowed > borrowed
|
|
853
|
+
-> (weightedCollateral + weightedBorrowed - borrowed) / weightedCollateral · 100
|
|
854
|
+
else -> 0
|
|
855
|
+
|
|
856
|
+
Result clamped to [0, 100]. `assets` is a list of
|
|
857
|
+
{"symbol", "dc", "supplied_usd", "borrowed_usd"}; missing usd legs count as 0.
|
|
858
|
+
For a uniform-dc single-collateral position this reduces to the familiar
|
|
859
|
+
(max_debt - debt)/max_debt·100 with max_debt = equity·dc/(1-dc).
|
|
847
860
|
"""
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
861
|
+
wc_plus = 0.0
|
|
862
|
+
wc_minus = 0.0
|
|
863
|
+
weighted_borrowed = 0.0
|
|
864
|
+
borrowed = 0.0
|
|
865
|
+
supplied_usd = 0.0
|
|
866
|
+
debt_usd = 0.0
|
|
867
|
+
for a in assets:
|
|
868
|
+
dc = a.get("dc", 0.0) or 0.0
|
|
869
|
+
sup = a.get("supplied_usd", 0.0) or 0.0
|
|
870
|
+
bor = a.get("borrowed_usd", 0.0) or 0.0
|
|
871
|
+
supplied_usd += sup
|
|
872
|
+
debt_usd += bor
|
|
873
|
+
net = sup - bor
|
|
874
|
+
if net > 0:
|
|
875
|
+
wc_plus += dc * net
|
|
876
|
+
elif net < 0:
|
|
877
|
+
wc_minus += dc * (-net)
|
|
878
|
+
weighted_borrowed += dc * bor
|
|
879
|
+
borrowed += bor
|
|
880
|
+
weighted_collateral = wc_plus - wc_minus
|
|
852
881
|
equity = supplied_usd - debt_usd
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
"max_debt": 0.0, "error": "equity near zero"}
|
|
858
|
-
|
|
859
|
-
max_debt = equity * (max_mult - 1)
|
|
860
|
-
if max_debt > 0 and debt_usd >= 0:
|
|
861
|
-
health_pct = (max_debt - min(debt_usd, max_debt)) / max_debt * 100
|
|
882
|
+
if borrowed <= 0:
|
|
883
|
+
health_pct = 100.0
|
|
884
|
+
elif weighted_collateral > 0 and (weighted_collateral + weighted_borrowed) > borrowed:
|
|
885
|
+
health_pct = (weighted_collateral + weighted_borrowed - borrowed) / weighted_collateral * 100.0
|
|
862
886
|
health_pct = max(0.0, min(100.0, health_pct))
|
|
863
887
|
else:
|
|
864
|
-
health_pct =
|
|
865
|
-
|
|
888
|
+
health_pct = 0.0
|
|
866
889
|
return {"health_pct": round(health_pct, 1), "supplied_usd": round(supplied_usd, 2),
|
|
867
890
|
"debt_usd": round(debt_usd, 2), "equity": round(equity, 2),
|
|
868
|
-
"
|
|
891
|
+
"weighted_collateral": round(weighted_collateral, 2),
|
|
892
|
+
"weighted_borrowed": round(weighted_borrowed, 2)}
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
_dc_cache = {}
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
def _resolve_debt_coverages(w3, symbols: list, tier_code: int = 0) -> dict:
|
|
899
|
+
"""Per-asset debtCoverage read LIVE on-chain from the TokenManager, keyed by symbol.
|
|
900
|
+
|
|
901
|
+
Resolves each symbol to its token address via getAssetAddress(bytes32,true), then reads
|
|
902
|
+
the account's effective coverage: tieredDebtCoverage(tier, token) on Avalanche/Arbitrum
|
|
903
|
+
(the contract's getHealthMeter uses getPrimeLeverageTier() for exactly this), falling
|
|
904
|
+
back to the un-tiered debtCoverage(token). Cached per run keyed by (symbol, tier_code).
|
|
905
|
+
Batched through multicall so N assets cost ~2 eth_calls, not 2N. Symbols that don't
|
|
906
|
+
resolve get dc=0 (they contribute nothing — same as the contract skipping an unpriced
|
|
907
|
+
leg)."""
|
|
908
|
+
want = [s for s in dict.fromkeys(symbols) if s]
|
|
909
|
+
out = {}
|
|
910
|
+
missing = []
|
|
911
|
+
for s in want:
|
|
912
|
+
ck = (s, tier_code)
|
|
913
|
+
if ck in _dc_cache:
|
|
914
|
+
out[s] = _dc_cache[ck]
|
|
915
|
+
else:
|
|
916
|
+
missing.append(s)
|
|
917
|
+
if not missing:
|
|
918
|
+
return out
|
|
919
|
+
tm_abi = json.loads(
|
|
920
|
+
'[{"inputs":[{"name":"_asset","type":"bytes32"},{"name":"_active","type":"bool"}],'
|
|
921
|
+
'"name":"getAssetAddress","outputs":[{"type":"address"}],"stateMutability":"view","type":"function"},'
|
|
922
|
+
'{"inputs":[{"name":"a","type":"address"}],"name":"debtCoverage","outputs":[{"type":"uint256"}],'
|
|
923
|
+
'"stateMutability":"view","type":"function"},'
|
|
924
|
+
'{"inputs":[{"name":"t","type":"uint8"},{"name":"a","type":"address"}],"name":"tieredDebtCoverage",'
|
|
925
|
+
'"outputs":[{"type":"uint256"}],"stateMutability":"view","type":"function"}]')
|
|
926
|
+
tm = w3.eth.contract(address=Web3.to_checksum_address(TOKEN_MANAGER), abi=tm_abi)
|
|
927
|
+
addr_legs = [(TOKEN_MANAGER, bytes.fromhex(tm.encode_abi("getAssetAddress", args=[asset_b32(s), True])[2:]))
|
|
928
|
+
for s in missing]
|
|
929
|
+
addr_res = multicall(w3, addr_legs)
|
|
930
|
+
addrs = {}
|
|
931
|
+
for s, (ok, rd) in zip(missing, addr_res):
|
|
932
|
+
try:
|
|
933
|
+
a = w3.codec.decode(["address"], rd)[0] if ok and rd else None
|
|
934
|
+
except Exception:
|
|
935
|
+
a = None
|
|
936
|
+
addrs[s] = a if a and int(a, 16) != 0 else None
|
|
937
|
+
resolvable = [s for s in missing if addrs[s]]
|
|
938
|
+
# Try tiered coverage first (Avalanche/Arbitrum); fall back per-asset to un-tiered.
|
|
939
|
+
tiered_legs = [(TOKEN_MANAGER, bytes.fromhex(
|
|
940
|
+
tm.encode_abi("tieredDebtCoverage", args=[tier_code, Web3.to_checksum_address(addrs[s])])[2:]))
|
|
941
|
+
for s in resolvable]
|
|
942
|
+
untiered_legs = [(TOKEN_MANAGER, bytes.fromhex(
|
|
943
|
+
tm.encode_abi("debtCoverage", args=[Web3.to_checksum_address(addrs[s])])[2:]))
|
|
944
|
+
for s in resolvable]
|
|
945
|
+
tiered_res = multicall(w3, tiered_legs) if tiered_legs else []
|
|
946
|
+
untiered_res = multicall(w3, untiered_legs) if untiered_legs else []
|
|
947
|
+
for i, s in enumerate(resolvable):
|
|
948
|
+
dc = 0.0
|
|
949
|
+
ok_t, rd_t = tiered_res[i]
|
|
950
|
+
if ok_t and rd_t:
|
|
951
|
+
try:
|
|
952
|
+
dc = w3.codec.decode(["uint256"], rd_t)[0] / 1e18
|
|
953
|
+
except Exception:
|
|
954
|
+
dc = 0.0
|
|
955
|
+
if dc <= 0:
|
|
956
|
+
ok_u, rd_u = untiered_res[i]
|
|
957
|
+
if ok_u and rd_u:
|
|
958
|
+
try:
|
|
959
|
+
dc = w3.codec.decode(["uint256"], rd_u)[0] / 1e18
|
|
960
|
+
except Exception:
|
|
961
|
+
dc = 0.0
|
|
962
|
+
_dc_cache[(s, tier_code)] = dc
|
|
963
|
+
out[s] = dc
|
|
964
|
+
for s in missing:
|
|
965
|
+
if s not in out:
|
|
966
|
+
_dc_cache[(s, tier_code)] = 0.0
|
|
967
|
+
out[s] = 0.0
|
|
968
|
+
return out
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def _compute_health_pct(supplied: list, borrowed: list, w3=None, tier_code: int = 0) -> dict:
|
|
972
|
+
"""Frontend-exact health (0-100%) for a Degen Account — wraps _health_meter_pct with
|
|
973
|
+
per-asset on-chain debtCoverage. 0% = liquidation, 100% = no debt.
|
|
974
|
+
|
|
975
|
+
DegenPrime renders getHealthMeter (HealthMeterFacetProd) just like DeltaPrime, but Base
|
|
976
|
+
has no PRIME leverage tier, so debtCoverage is the un-tiered TokenManager value (the
|
|
977
|
+
tiered getter reverts and _resolve_debt_coverages falls back to it automatically).
|
|
978
|
+
|
|
979
|
+
supplied/borrowed are rows carrying `usd` (and optionally a pre-resolved `dc`); pass `w3`
|
|
980
|
+
so dc can be read live when a row lacks it. Returns health_pct, supplied_usd, debt_usd,
|
|
981
|
+
equity, max_debt (display zero-crossing debt), tier, or error.
|
|
982
|
+
"""
|
|
983
|
+
syms = list(dict.fromkeys([r["symbol"] for r in supplied + borrowed if r.get("symbol")]))
|
|
984
|
+
dc_map = {r["symbol"]: r["dc"] for r in supplied + borrowed if r.get("dc") is not None}
|
|
985
|
+
need = [s for s in syms if s not in dc_map]
|
|
986
|
+
if need and w3 is not None:
|
|
987
|
+
dc_map.update(_resolve_debt_coverages(w3, need, tier_code))
|
|
988
|
+
assets = []
|
|
989
|
+
for s in syms:
|
|
990
|
+
sup = sum(r.get("usd", 0) or 0 for r in supplied if r.get("symbol") == s)
|
|
991
|
+
bor = sum(r.get("usd", 0) or 0 for r in borrowed if r.get("symbol") == s)
|
|
992
|
+
assets.append({"symbol": s, "dc": dc_map.get(s, 0.0), "supplied_usd": sup, "borrowed_usd": bor})
|
|
993
|
+
res = _health_meter_pct(assets)
|
|
994
|
+
equity = res["equity"]
|
|
995
|
+
if equity <= 0.01:
|
|
996
|
+
return {"health_pct": 0.0, "supplied_usd": res["supplied_usd"],
|
|
997
|
+
"debt_usd": res["debt_usd"], "equity": equity,
|
|
998
|
+
"max_debt": 0.0, "tier": "FIXED", "error": "equity near zero"}
|
|
999
|
+
coll_usd = sum(a["supplied_usd"] for a in assets) or 0.0
|
|
1000
|
+
dc_eff = (sum(a["dc"] * a["supplied_usd"] for a in assets) / coll_usd) if coll_usd > 0 else 0.0
|
|
1001
|
+
max_debt = equity * dc_eff / (1.0 - dc_eff) if 0 < dc_eff < 1 else 0.0
|
|
1002
|
+
return {"health_pct": res["health_pct"], "supplied_usd": res["supplied_usd"],
|
|
1003
|
+
"debt_usd": res["debt_usd"], "equity": equity,
|
|
1004
|
+
"max_debt": round(max_debt, 2), "tier": "FIXED"}
|
|
869
1005
|
|
|
870
1006
|
|
|
871
1007
|
def _redstone_fetch_packages(use_cache: bool = True) -> dict:
|
|
@@ -2046,18 +2182,27 @@ def cmd_repay(pool_name: str, amount: float, execute: bool = False):
|
|
|
2046
2182
|
requested_wei = to_wei_units(amount, cfg["decimals"])
|
|
2047
2183
|
debt_wei = pool.functions.getBorrowed(pa_cs).call()
|
|
2048
2184
|
in_acct_wei = account.functions.getBalance(asset_b32(symbol)).call()
|
|
2185
|
+
try:
|
|
2186
|
+
total_intent_wei = account.functions.getTotalIntentAmount(asset_b32(symbol)).call()
|
|
2187
|
+
except Exception:
|
|
2188
|
+
total_intent_wei = 0
|
|
2189
|
+
available_wei = in_acct_wei - total_intent_wei if in_acct_wei > total_intent_wei else 0
|
|
2049
2190
|
if debt_wei == 0:
|
|
2050
2191
|
print(f"No {symbol} debt to repay on Degen Account {pa}.")
|
|
2051
2192
|
return
|
|
2052
|
-
amount_wei = min(requested_wei, debt_wei,
|
|
2193
|
+
amount_wei = min(requested_wei, debt_wei, available_wei)
|
|
2053
2194
|
if amount_wei == 0:
|
|
2054
|
-
print(f"Repay {amount} {symbol}:
|
|
2055
|
-
f"
|
|
2195
|
+
print(f"Repay {amount} {symbol}: available {symbol} balance is 0 "
|
|
2196
|
+
f"(total {in_acct_wei / 10**cfg['decimals']:.6f} minus "
|
|
2197
|
+
f"{total_intent_wei / 10**cfg['decimals']:.6f} pending withdrawal intent) — "
|
|
2198
|
+
f"swap into {symbol} first or wait for intents to mature.")
|
|
2056
2199
|
return
|
|
2057
2200
|
cap_notes = []
|
|
2058
2201
|
if amount_wei < requested_wei:
|
|
2059
|
-
if
|
|
2060
|
-
cap_notes.append(f"
|
|
2202
|
+
if available_wei < min(requested_wei, debt_wei):
|
|
2203
|
+
cap_notes.append(f"available {symbol} only {available_wei / 10**cfg['decimals']:.6f} "
|
|
2204
|
+
f"(total {in_acct_wei / 10**cfg['decimals']:.6f} minus "
|
|
2205
|
+
f"{total_intent_wei / 10**cfg['decimals']:.6f} pending intent)")
|
|
2061
2206
|
if debt_wei < requested_wei:
|
|
2062
2207
|
cap_notes.append(f"debt only {debt_wei / 10**cfg['decimals']:.6f} {symbol}")
|
|
2063
2208
|
|
|
@@ -2067,11 +2212,15 @@ def cmd_repay(pool_name: str, amount: float, execute: bool = False):
|
|
|
2067
2212
|
print(f" Capped from requested {amount}: {'; '.join(cap_notes)}")
|
|
2068
2213
|
print(f" Calls repay(bytes32 '{symbol}', {amount_wei}) on the Degen Account")
|
|
2069
2214
|
print(f" Current debt: {debt_wei / 10**cfg['decimals']:.6f} {symbol} | "
|
|
2070
|
-
f"in-account: {in_acct_wei / 10**cfg['decimals']:.6f} {symbol}"
|
|
2215
|
+
f"in-account: {in_acct_wei / 10**cfg['decimals']:.6f} {symbol} | "
|
|
2216
|
+
f"available: {available_wei / 10**cfg['decimals']:.6f} {symbol}")
|
|
2071
2217
|
if in_acct_wei < debt_wei:
|
|
2072
2218
|
shortfall = (debt_wei - in_acct_wei) / 10**cfg['decimals']
|
|
2073
|
-
print(f" Note: in-account < debt by {shortfall:.6f} {symbol}
|
|
2219
|
+
print(f" Note: in-account < debt by {shortfall:.6f} {symbol} — "
|
|
2074
2220
|
f"swap into {symbol} first to close the position fully.")
|
|
2221
|
+
if total_intent_wei > 0:
|
|
2222
|
+
print(f" Note: {total_intent_wei / 10**cfg['decimals']:.6f} {symbol} is locked in pending "
|
|
2223
|
+
f"withdrawal intent(s) and not available for repay.")
|
|
2075
2224
|
print("Run with --execute to broadcast")
|
|
2076
2225
|
return
|
|
2077
2226
|
|
|
@@ -2098,6 +2247,53 @@ def cmd_repay(pool_name: str, amount: float, execute: bool = False):
|
|
|
2098
2247
|
repaid = amount_wei / 10**cfg['decimals']
|
|
2099
2248
|
print(f"{'✓' if ok else '✗'} Repay {repaid:.6f} {symbol} {'confirmed' if ok else 'failed'}")
|
|
2100
2249
|
print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
|
|
2250
|
+
if not ok:
|
|
2251
|
+
_print_revert_reason(w3, tx, receipt)
|
|
2252
|
+
|
|
2253
|
+
def _print_revert_reason(w3, tx, receipt):
|
|
2254
|
+
"""Try to decode and print the revert reason from a failed tx."""
|
|
2255
|
+
try:
|
|
2256
|
+
result = w3.eth.call({
|
|
2257
|
+
"from": tx["from"], "to": tx["to"], "data": tx["input"],
|
|
2258
|
+
"gas": receipt["gasUsed"],
|
|
2259
|
+
"maxFeePerGas": tx.get("maxFeePerGas", tx.get("gasPrice", 0)),
|
|
2260
|
+
"maxPriorityFeePerGas": tx.get("maxPriorityFeePerGas", 0),
|
|
2261
|
+
}, receipt["blockNumber"])
|
|
2262
|
+
except Exception as e:
|
|
2263
|
+
err = str(e)
|
|
2264
|
+
data = err
|
|
2265
|
+
if isinstance(e.args, (list, tuple)):
|
|
2266
|
+
for arg in e.args:
|
|
2267
|
+
if isinstance(arg, str) and arg.startswith("0x"):
|
|
2268
|
+
data = arg
|
|
2269
|
+
break
|
|
2270
|
+
elif isinstance(arg, dict) and "data" in arg:
|
|
2271
|
+
data = arg["data"]
|
|
2272
|
+
break
|
|
2273
|
+
if isinstance(data, str) and data.startswith("0x") and len(data) >= 10:
|
|
2274
|
+
sel = data[:10]
|
|
2275
|
+
_known_errors = {
|
|
2276
|
+
"0x567fe27a": "Unknown error from Prime Account facet",
|
|
2277
|
+
"0xf4d678b8": "Execution rejected (may be intent-locked balance check)",
|
|
2278
|
+
"0x441a702e": "InsufficientBalance()",
|
|
2279
|
+
"0xfd36fde3": "SignerNotAuthorised (RedStone signer mismatch)",
|
|
2280
|
+
"0x92ba160c": "RedstoneConsensus()",
|
|
2281
|
+
"0xc2c286b7": "SignerNotAuthorised(address)",
|
|
2282
|
+
"0x08c379a0": "require() revert: see message below",
|
|
2283
|
+
}
|
|
2284
|
+
label = _known_errors.get(sel, f"Unknown custom error 0x{sel}")
|
|
2285
|
+
print(f" Revert: {label}")
|
|
2286
|
+
if sel == "0x08c379a0" and len(data) >= 138:
|
|
2287
|
+
try:
|
|
2288
|
+
from eth_abi import decode
|
|
2289
|
+
msg_bytes = bytes.fromhex(data[10:])
|
|
2290
|
+
decoded = decode(["string"], msg_bytes)
|
|
2291
|
+
print(f' Require message: "{decoded[0]}"')
|
|
2292
|
+
except Exception:
|
|
2293
|
+
pass
|
|
2294
|
+
else:
|
|
2295
|
+
print(f" Revert reason: {err[:200]}")
|
|
2296
|
+
|
|
2101
2297
|
|
|
2102
2298
|
# ─── ParaSwap / Velora route ─────────────────────────────────────────────────
|
|
2103
2299
|
# The Degen Account already holds the funds, so the facet (not the EOA) approves the
|
|
@@ -1939,8 +1939,21 @@ def gather_lending(w3, account):
|
|
|
1939
1939
|
sym = n.rstrip(b"\x00").decode(errors="replace")
|
|
1940
1940
|
if sym and sym not in feeds:
|
|
1941
1941
|
feeds.append(sym)
|
|
1942
|
-
out = {"supplied": supplied, "borrowed": borrowed,
|
|
1942
|
+
out = {"supplied": supplied, "borrowed": borrowed, "w3": w3,
|
|
1943
1943
|
"total_value_usd": None, "debt_usd": None, "health_ratio": None, "solvent": None}
|
|
1944
|
+
# Resolve the account's PRIME tier (oracle-free) and per-asset on-chain debtCoverage,
|
|
1945
|
+
# then stamp dc onto every row so _compute_health_pct can run getHealthMeter exactly.
|
|
1946
|
+
try:
|
|
1947
|
+
tier_code = account.functions.getLeverageTierFullInfo().call()[0]
|
|
1948
|
+
except Exception:
|
|
1949
|
+
tier_code = 0
|
|
1950
|
+
out["tier_code"] = tier_code
|
|
1951
|
+
try:
|
|
1952
|
+
dc_map = _resolve_debt_coverages(w3, [r["symbol"] for r in supplied + borrowed], tier_code)
|
|
1953
|
+
for r in supplied + borrowed:
|
|
1954
|
+
r["dc"] = dc_map.get(r["symbol"], 0.0)
|
|
1955
|
+
except Exception:
|
|
1956
|
+
pass
|
|
1944
1957
|
try:
|
|
1945
1958
|
payload = build_redstone_payload(feeds)
|
|
1946
1959
|
payload_hex = payload.hex()
|
|
@@ -2001,46 +2014,188 @@ def gather_lending(w3, account):
|
|
|
2001
2014
|
r["usd"] = None
|
|
2002
2015
|
return out
|
|
2003
2016
|
|
|
2017
|
+
def _health_meter_pct(assets: list) -> dict:
|
|
2018
|
+
"""getHealthMeter() exactly as the on-chain HealthMeterFacetProd renders it.
|
|
2019
|
+
|
|
2020
|
+
The frontend health meter is NOT equity*(mult-1); it is a per-asset, debtCoverage-
|
|
2021
|
+
weighted formula. For each asset i with USD-valued long balance and borrow, and its
|
|
2022
|
+
live debtCoverage dc_i:
|
|
2023
|
+
|
|
2024
|
+
net_i = supplied_usd_i - borrowed_usd_i
|
|
2025
|
+
weightedCollateralPlus = Σ dc_i·net_i for net_i > 0 (net-long legs)
|
|
2026
|
+
weightedCollateralMinus = Σ dc_i·(-net_i) for net_i < 0 (net-short legs)
|
|
2027
|
+
weightedCollateral = weightedCollateralPlus - weightedCollateralMinus
|
|
2028
|
+
weightedBorrowed = Σ dc_i·borrowed_usd_i
|
|
2029
|
+
borrowed = Σ borrowed_usd_i (UNWEIGHTED)
|
|
2030
|
+
|
|
2031
|
+
borrowed == 0 -> 100
|
|
2032
|
+
weightedCollateral > 0 and
|
|
2033
|
+
weightedCollateral + weightedBorrowed > borrowed
|
|
2034
|
+
-> (weightedCollateral + weightedBorrowed - borrowed) / weightedCollateral · 100
|
|
2035
|
+
else -> 0
|
|
2036
|
+
|
|
2037
|
+
Result clamped to [0, 100]. `assets` is a list of
|
|
2038
|
+
{"symbol", "dc", "supplied_usd", "borrowed_usd"}; missing usd legs count as 0.
|
|
2039
|
+
For a uniform-dc single-collateral position this reduces to the familiar
|
|
2040
|
+
(max_debt - debt)/max_debt·100 with max_debt = equity·dc/(1-dc).
|
|
2041
|
+
"""
|
|
2042
|
+
wc_plus = 0.0
|
|
2043
|
+
wc_minus = 0.0
|
|
2044
|
+
weighted_borrowed = 0.0
|
|
2045
|
+
borrowed = 0.0
|
|
2046
|
+
supplied_usd = 0.0
|
|
2047
|
+
debt_usd = 0.0
|
|
2048
|
+
for a in assets:
|
|
2049
|
+
dc = a.get("dc", 0.0) or 0.0
|
|
2050
|
+
sup = a.get("supplied_usd", 0.0) or 0.0
|
|
2051
|
+
bor = a.get("borrowed_usd", 0.0) or 0.0
|
|
2052
|
+
supplied_usd += sup
|
|
2053
|
+
debt_usd += bor
|
|
2054
|
+
net = sup - bor
|
|
2055
|
+
if net > 0:
|
|
2056
|
+
wc_plus += dc * net
|
|
2057
|
+
elif net < 0:
|
|
2058
|
+
wc_minus += dc * (-net)
|
|
2059
|
+
weighted_borrowed += dc * bor
|
|
2060
|
+
borrowed += bor
|
|
2061
|
+
weighted_collateral = wc_plus - wc_minus
|
|
2062
|
+
equity = supplied_usd - debt_usd
|
|
2063
|
+
if borrowed <= 0:
|
|
2064
|
+
health_pct = 100.0
|
|
2065
|
+
elif weighted_collateral > 0 and (weighted_collateral + weighted_borrowed) > borrowed:
|
|
2066
|
+
health_pct = (weighted_collateral + weighted_borrowed - borrowed) / weighted_collateral * 100.0
|
|
2067
|
+
health_pct = max(0.0, min(100.0, health_pct))
|
|
2068
|
+
else:
|
|
2069
|
+
health_pct = 0.0
|
|
2070
|
+
return {"health_pct": round(health_pct, 1), "supplied_usd": round(supplied_usd, 2),
|
|
2071
|
+
"debt_usd": round(debt_usd, 2), "equity": round(equity, 2),
|
|
2072
|
+
"weighted_collateral": round(weighted_collateral, 2),
|
|
2073
|
+
"weighted_borrowed": round(weighted_borrowed, 2)}
|
|
2074
|
+
|
|
2075
|
+
|
|
2076
|
+
_dc_cache = {}
|
|
2077
|
+
|
|
2078
|
+
|
|
2079
|
+
def _resolve_debt_coverages(w3, symbols: list, tier_code: int = 0) -> dict:
|
|
2080
|
+
"""Per-asset debtCoverage read LIVE on-chain from the TokenManager, keyed by symbol.
|
|
2081
|
+
|
|
2082
|
+
Resolves each symbol to its token address via getAssetAddress(bytes32,true), then reads
|
|
2083
|
+
the account's effective coverage: tieredDebtCoverage(tier, token) on Avalanche/Arbitrum
|
|
2084
|
+
(the contract's getHealthMeter uses getPrimeLeverageTier() for exactly this), falling
|
|
2085
|
+
back to the un-tiered debtCoverage(token). Cached per run keyed by (symbol, tier_code).
|
|
2086
|
+
Batched through multicall so N assets cost ~2 eth_calls, not 2N. Symbols that don't
|
|
2087
|
+
resolve get dc=0 (they contribute nothing — same as the contract skipping an unpriced
|
|
2088
|
+
leg)."""
|
|
2089
|
+
want = [s for s in dict.fromkeys(symbols) if s]
|
|
2090
|
+
out = {}
|
|
2091
|
+
missing = []
|
|
2092
|
+
for s in want:
|
|
2093
|
+
ck = (s, tier_code)
|
|
2094
|
+
if ck in _dc_cache:
|
|
2095
|
+
out[s] = _dc_cache[ck]
|
|
2096
|
+
else:
|
|
2097
|
+
missing.append(s)
|
|
2098
|
+
if not missing:
|
|
2099
|
+
return out
|
|
2100
|
+
tm_abi = json.loads(
|
|
2101
|
+
'[{"inputs":[{"name":"_asset","type":"bytes32"},{"name":"_active","type":"bool"}],'
|
|
2102
|
+
'"name":"getAssetAddress","outputs":[{"type":"address"}],"stateMutability":"view","type":"function"},'
|
|
2103
|
+
'{"inputs":[{"name":"a","type":"address"}],"name":"debtCoverage","outputs":[{"type":"uint256"}],'
|
|
2104
|
+
'"stateMutability":"view","type":"function"},'
|
|
2105
|
+
'{"inputs":[{"name":"t","type":"uint8"},{"name":"a","type":"address"}],"name":"tieredDebtCoverage",'
|
|
2106
|
+
'"outputs":[{"type":"uint256"}],"stateMutability":"view","type":"function"}]')
|
|
2107
|
+
tm = w3.eth.contract(address=Web3.to_checksum_address(TOKEN_MANAGER), abi=tm_abi)
|
|
2108
|
+
addr_legs = [(TOKEN_MANAGER, bytes.fromhex(tm.encode_abi("getAssetAddress", args=[asset_b32(s), True])[2:]))
|
|
2109
|
+
for s in missing]
|
|
2110
|
+
addr_res = multicall(w3, addr_legs)
|
|
2111
|
+
addrs = {}
|
|
2112
|
+
for s, (ok, rd) in zip(missing, addr_res):
|
|
2113
|
+
try:
|
|
2114
|
+
a = w3.codec.decode(["address"], rd)[0] if ok and rd else None
|
|
2115
|
+
except Exception:
|
|
2116
|
+
a = None
|
|
2117
|
+
addrs[s] = a if a and int(a, 16) != 0 else None
|
|
2118
|
+
resolvable = [s for s in missing if addrs[s]]
|
|
2119
|
+
# Try tiered coverage first (Avalanche/Arbitrum); fall back per-asset to un-tiered.
|
|
2120
|
+
tiered_legs = [(TOKEN_MANAGER, bytes.fromhex(
|
|
2121
|
+
tm.encode_abi("tieredDebtCoverage", args=[tier_code, Web3.to_checksum_address(addrs[s])])[2:]))
|
|
2122
|
+
for s in resolvable]
|
|
2123
|
+
untiered_legs = [(TOKEN_MANAGER, bytes.fromhex(
|
|
2124
|
+
tm.encode_abi("debtCoverage", args=[Web3.to_checksum_address(addrs[s])])[2:]))
|
|
2125
|
+
for s in resolvable]
|
|
2126
|
+
tiered_res = multicall(w3, tiered_legs) if tiered_legs else []
|
|
2127
|
+
untiered_res = multicall(w3, untiered_legs) if untiered_legs else []
|
|
2128
|
+
for i, s in enumerate(resolvable):
|
|
2129
|
+
dc = 0.0
|
|
2130
|
+
ok_t, rd_t = tiered_res[i]
|
|
2131
|
+
if ok_t and rd_t:
|
|
2132
|
+
try:
|
|
2133
|
+
dc = w3.codec.decode(["uint256"], rd_t)[0] / 1e18
|
|
2134
|
+
except Exception:
|
|
2135
|
+
dc = 0.0
|
|
2136
|
+
if dc <= 0:
|
|
2137
|
+
ok_u, rd_u = untiered_res[i]
|
|
2138
|
+
if ok_u and rd_u:
|
|
2139
|
+
try:
|
|
2140
|
+
dc = w3.codec.decode(["uint256"], rd_u)[0] / 1e18
|
|
2141
|
+
except Exception:
|
|
2142
|
+
dc = 0.0
|
|
2143
|
+
_dc_cache[(s, tier_code)] = dc
|
|
2144
|
+
out[s] = dc
|
|
2145
|
+
for s in missing:
|
|
2146
|
+
if s not in out:
|
|
2147
|
+
_dc_cache[(s, tier_code)] = 0.0
|
|
2148
|
+
out[s] = 0.0
|
|
2149
|
+
return out
|
|
2150
|
+
|
|
2151
|
+
|
|
2004
2152
|
def _compute_health_pct(data: dict, tier_code: int = 0) -> dict:
|
|
2005
|
-
"""
|
|
2153
|
+
"""Frontend-exact health (0-100%) for a Prime Account — wraps _health_meter_pct with
|
|
2154
|
+
on-chain debtCoverage resolution and the tier label.
|
|
2006
2155
|
|
|
2007
2156
|
DeltaPrime has *two* health metrics that agents must not confuse:
|
|
2008
2157
|
|
|
2009
2158
|
1. health_ratio (on-chain, getHealthRatio): 1.0 = liquidation, >1.0 = solvent.
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
2. health_pct (
|
|
2013
|
-
and the account-health-monitor cron. 0% = liquidation, 100% = no debt.
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
tier_label, or error.
|
|
2159
|
+
The raw weighted-collateral / debt ratio from the SolvencyFacet.
|
|
2160
|
+
|
|
2161
|
+
2. health_pct (0-100%, getHealthMeter): the scale the DeltaPrime frontend renders
|
|
2162
|
+
and the account-health-monitor cron acts on. 0% = liquidation, 100% = no debt.
|
|
2163
|
+
Computed by _health_meter_pct with per-asset dc from tieredDebtCoverage at the
|
|
2164
|
+
account's PRIME tier — NOT the old equity*(mult-1) approximation.
|
|
2165
|
+
|
|
2166
|
+
Per-asset USD comes from gather_lending (rows carry `dc` once resolved); if a row has
|
|
2167
|
+
no `dc` key yet it is resolved here from `data["w3"]` when present. Returns
|
|
2168
|
+
health_pct, supplied_usd, debt_usd, equity, max_debt (display zero-crossing debt),
|
|
2169
|
+
tier, or error.
|
|
2022
2170
|
"""
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
equity = supplied_usd - debt_usd
|
|
2171
|
+
supplied = data.get("supplied", [])
|
|
2172
|
+
borrowed = data.get("borrowed", [])
|
|
2026
2173
|
tier_labels = {0: "BASIC", 1: "PREMIUM", 2: "_NON_EXISTENT"}
|
|
2027
2174
|
tier_label = tier_labels.get(tier_code, str(tier_code))
|
|
2028
|
-
|
|
2029
|
-
|
|
2175
|
+
# Per-symbol long/short USD, merging an asset that is both supplied and borrowed.
|
|
2176
|
+
syms = list(dict.fromkeys([r["symbol"] for r in supplied + borrowed if r.get("symbol")]))
|
|
2177
|
+
dc_map = {r["symbol"]: r["dc"] for r in supplied + borrowed if r.get("dc") is not None}
|
|
2178
|
+
need = [s for s in syms if s not in dc_map]
|
|
2179
|
+
if need and data.get("w3") is not None:
|
|
2180
|
+
dc_map.update(_resolve_debt_coverages(data["w3"], need, tier_code))
|
|
2181
|
+
assets = []
|
|
2182
|
+
for s in syms:
|
|
2183
|
+
sup = sum(r.get("usd", 0) or 0 for r in supplied if r.get("symbol") == s)
|
|
2184
|
+
bor = sum(r.get("usd", 0) or 0 for r in borrowed if r.get("symbol") == s)
|
|
2185
|
+
assets.append({"symbol": s, "dc": dc_map.get(s, 0.0), "supplied_usd": sup, "borrowed_usd": bor})
|
|
2186
|
+
res = _health_meter_pct(assets)
|
|
2187
|
+
equity = res["equity"]
|
|
2030
2188
|
if equity <= 0.01:
|
|
2031
|
-
return {"health_pct": 0.0, "supplied_usd":
|
|
2032
|
-
"debt_usd":
|
|
2189
|
+
return {"health_pct": 0.0, "supplied_usd": res["supplied_usd"],
|
|
2190
|
+
"debt_usd": res["debt_usd"], "equity": equity,
|
|
2033
2191
|
"max_debt": 0.0, "tier": tier_label, "error": "equity near zero"}
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
return {"health_pct": round(health_pct, 1), "supplied_usd": round(supplied_usd, 2),
|
|
2043
|
-
"debt_usd": round(debt_usd, 2), "equity": round(equity, 2),
|
|
2192
|
+
# Display-only zero-crossing debt: the unweighted borrow at which health hits 0,
|
|
2193
|
+
# equity·dc_eff/(1-dc_eff) for the position's value-weighted collateral dc.
|
|
2194
|
+
coll_usd = sum(a["supplied_usd"] for a in assets) or 0.0
|
|
2195
|
+
dc_eff = (sum(a["dc"] * a["supplied_usd"] for a in assets) / coll_usd) if coll_usd > 0 else 0.0
|
|
2196
|
+
max_debt = equity * dc_eff / (1.0 - dc_eff) if 0 < dc_eff < 1 else 0.0
|
|
2197
|
+
return {"health_pct": res["health_pct"], "supplied_usd": res["supplied_usd"],
|
|
2198
|
+
"debt_usd": res["debt_usd"], "equity": equity,
|
|
2044
2199
|
"max_debt": round(max_debt, 2), "tier": tier_label}
|
|
2045
2200
|
|
|
2046
2201
|
|
|
@@ -2166,21 +2321,32 @@ def cmd_repay(pool_name: str, amount: float, execute: bool = False):
|
|
|
2166
2321
|
# The facet's repay reverts if amount > debt OR amount > in-account balance.
|
|
2167
2322
|
# Cap to min(requested, debt, in_account) so callers don't need to know either
|
|
2168
2323
|
# exact figure — pass an overshoot like 9999 and it clips cleanly.
|
|
2324
|
+
# ALSO: the contract's _getAvailableBalance() subtracts pending withdrawal intents,
|
|
2325
|
+
# so we subtract total_intent from the raw balance to get the true cap.
|
|
2169
2326
|
requested_wei = to_wei_units(amount, cfg["decimals"])
|
|
2170
2327
|
debt_wei = pool.functions.getBorrowed(pa_cs).call()
|
|
2171
2328
|
in_acct_wei = account.functions.getBalance(asset_b32(symbol)).call()
|
|
2329
|
+
try:
|
|
2330
|
+
total_intent_wei = account.functions.getTotalIntentAmount(asset_b32(symbol)).call()
|
|
2331
|
+
except Exception:
|
|
2332
|
+
total_intent_wei = 0
|
|
2333
|
+
available_wei = in_acct_wei - total_intent_wei if in_acct_wei > total_intent_wei else 0
|
|
2172
2334
|
if debt_wei == 0:
|
|
2173
2335
|
print(f"No {symbol} debt to repay on Prime Account {pa}.")
|
|
2174
2336
|
return
|
|
2175
|
-
amount_wei = min(requested_wei, debt_wei,
|
|
2337
|
+
amount_wei = min(requested_wei, debt_wei, available_wei)
|
|
2176
2338
|
if amount_wei == 0:
|
|
2177
|
-
print(f"Repay {amount} {symbol}:
|
|
2178
|
-
f"
|
|
2339
|
+
print(f"Repay {amount} {symbol}: available {symbol} balance is 0 "
|
|
2340
|
+
f"(total {in_acct_wei / 10**cfg['decimals']:.6f} minus "
|
|
2341
|
+
f"{total_intent_wei / 10**cfg['decimals']:.6f} pending withdrawal intent) — "
|
|
2342
|
+
f"swap into {symbol} first or wait for intents to mature.")
|
|
2179
2343
|
return
|
|
2180
2344
|
cap_notes = []
|
|
2181
2345
|
if amount_wei < requested_wei:
|
|
2182
|
-
if
|
|
2183
|
-
cap_notes.append(f"
|
|
2346
|
+
if available_wei < min(requested_wei, debt_wei):
|
|
2347
|
+
cap_notes.append(f"available {symbol} only {available_wei / 10**cfg['decimals']:.6f} "
|
|
2348
|
+
f"(total {in_acct_wei / 10**cfg['decimals']:.6f} minus "
|
|
2349
|
+
f"{total_intent_wei / 10**cfg['decimals']:.6f} pending intent)")
|
|
2184
2350
|
if debt_wei < requested_wei:
|
|
2185
2351
|
cap_notes.append(f"debt only {debt_wei / 10**cfg['decimals']:.6f} {symbol}")
|
|
2186
2352
|
|
|
@@ -2190,11 +2356,15 @@ def cmd_repay(pool_name: str, amount: float, execute: bool = False):
|
|
|
2190
2356
|
print(f" Capped from requested {amount}: {'; '.join(cap_notes)}")
|
|
2191
2357
|
print(f" Calls repay(bytes32 '{symbol}', {amount_wei}) on the Prime Account")
|
|
2192
2358
|
print(f" Current debt: {debt_wei / 10**cfg['decimals']:.6f} {symbol} | "
|
|
2193
|
-
f"in-account: {in_acct_wei / 10**cfg['decimals']:.6f} {symbol}"
|
|
2359
|
+
f"in-account: {in_acct_wei / 10**cfg['decimals']:.6f} {symbol} | "
|
|
2360
|
+
f"available: {available_wei / 10**cfg['decimals']:.6f} {symbol}")
|
|
2194
2361
|
if in_acct_wei < debt_wei:
|
|
2195
2362
|
shortfall = (debt_wei - in_acct_wei) / 10**cfg['decimals']
|
|
2196
2363
|
print(f" Note: in-account < debt by {shortfall:.6f} {symbol} — "
|
|
2197
2364
|
f"swap into {symbol} first to close the position fully.")
|
|
2365
|
+
if total_intent_wei > 0:
|
|
2366
|
+
print(f" Note: {total_intent_wei / 10**cfg['decimals']:.6f} {symbol} is locked in pending "
|
|
2367
|
+
f"withdrawal intent(s) and not available for repay.")
|
|
2198
2368
|
print("Run with --execute to broadcast")
|
|
2199
2369
|
return
|
|
2200
2370
|
|
|
@@ -2221,6 +2391,53 @@ def cmd_repay(pool_name: str, amount: float, execute: bool = False):
|
|
|
2221
2391
|
repaid = amount_wei / 10**cfg['decimals']
|
|
2222
2392
|
print(f"{'✓' if ok else '✗'} Repay {repaid:.6f} {symbol} {'confirmed' if ok else 'failed'}")
|
|
2223
2393
|
print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
|
|
2394
|
+
if not ok:
|
|
2395
|
+
_print_revert_reason(w3, tx, receipt)
|
|
2396
|
+
|
|
2397
|
+
def _print_revert_reason(w3, tx, receipt):
|
|
2398
|
+
"""Try to decode and print the revert reason from a failed tx."""
|
|
2399
|
+
try:
|
|
2400
|
+
result = w3.eth.call({
|
|
2401
|
+
"from": tx["from"], "to": tx["to"], "data": tx["input"],
|
|
2402
|
+
"gas": receipt["gasUsed"],
|
|
2403
|
+
"maxFeePerGas": tx.get("maxFeePerGas", tx.get("gasPrice", 0)),
|
|
2404
|
+
"maxPriorityFeePerGas": tx.get("maxPriorityFeePerGas", 0),
|
|
2405
|
+
}, receipt["blockNumber"])
|
|
2406
|
+
except Exception as e:
|
|
2407
|
+
err = str(e)
|
|
2408
|
+
data = err
|
|
2409
|
+
if isinstance(e.args, (list, tuple)):
|
|
2410
|
+
for arg in e.args:
|
|
2411
|
+
if isinstance(arg, str) and arg.startswith("0x"):
|
|
2412
|
+
data = arg
|
|
2413
|
+
break
|
|
2414
|
+
elif isinstance(arg, dict) and "data" in arg:
|
|
2415
|
+
data = arg["data"]
|
|
2416
|
+
break
|
|
2417
|
+
if isinstance(data, str) and data.startswith("0x") and len(data) >= 10:
|
|
2418
|
+
sel = data[:10]
|
|
2419
|
+
_known_errors = {
|
|
2420
|
+
"0x567fe27a": "Unknown error from Prime Account facet",
|
|
2421
|
+
"0xf4d678b8": "Execution rejected (may be intent-locked balance check)",
|
|
2422
|
+
"0x441a702e": "InsufficientBalance()",
|
|
2423
|
+
"0xfd36fde3": "SignerNotAuthorised (RedStone signer mismatch)",
|
|
2424
|
+
"0x92ba160c": "RedstoneConsensus()",
|
|
2425
|
+
"0xc2c286b7": "SignerNotAuthorised(address)",
|
|
2426
|
+
"0x08c379a0": "require() revert: see message below",
|
|
2427
|
+
}
|
|
2428
|
+
label = _known_errors.get(sel, f"Unknown custom error 0x{sel}")
|
|
2429
|
+
print(f" Revert: {label}")
|
|
2430
|
+
if sel == "0x08c379a0" and len(data) >= 138:
|
|
2431
|
+
try:
|
|
2432
|
+
from eth_abi import decode
|
|
2433
|
+
msg_bytes = bytes.fromhex(data[10:])
|
|
2434
|
+
decoded = decode(["string"], msg_bytes)
|
|
2435
|
+
print(f' Require message: "{decoded[0]}"')
|
|
2436
|
+
except Exception:
|
|
2437
|
+
pass
|
|
2438
|
+
else:
|
|
2439
|
+
print(f" Revert reason: {err[:200]}")
|
|
2440
|
+
|
|
2224
2441
|
|
|
2225
2442
|
def _decode_formatted_offer(raw: bytes):
|
|
2226
2443
|
"""Manually decode YieldYak's FormattedOffer struct
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: primecli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.1
|
|
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.6.
|
|
7
|
+
version = "0.6.1"
|
|
8
8
|
description = "Agent-friendly CLI tools for the DeltaPrime (Avalanche + Arbitrum) and DegenPrime (Base) lending and leverage protocols. Preview-by-default; no Etherscan key required."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|