primecli 0.5.0__tar.gz → 0.5.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {primecli-0.5.0 → primecli-0.5.2}/PKG-INFO +2 -2
- {primecli-0.5.0 → primecli-0.5.2}/README.md +1 -1
- {primecli-0.5.0 → primecli-0.5.2}/primecli/arbprime.py +7 -4
- {primecli-0.5.0 → primecli-0.5.2}/primecli/degenprime.py +178 -53
- {primecli-0.5.0 → primecli-0.5.2}/primecli/deltaprime.py +7 -4
- {primecli-0.5.0 → primecli-0.5.2}/primecli.egg-info/PKG-INFO +2 -2
- {primecli-0.5.0 → primecli-0.5.2}/pyproject.toml +1 -1
- {primecli-0.5.0 → primecli-0.5.2}/tests/test_gas_pricing.py +7 -7
- {primecli-0.5.0 → primecli-0.5.2}/LICENSE +0 -0
- {primecli-0.5.0 → primecli-0.5.2}/primecli/__init__.py +0 -0
- {primecli-0.5.0 → primecli-0.5.2}/primecli/health_monitor.py +0 -0
- {primecli-0.5.0 → primecli-0.5.2}/primecli.egg-info/SOURCES.txt +0 -0
- {primecli-0.5.0 → primecli-0.5.2}/primecli.egg-info/dependency_links.txt +0 -0
- {primecli-0.5.0 → primecli-0.5.2}/primecli.egg-info/entry_points.txt +0 -0
- {primecli-0.5.0 → primecli-0.5.2}/primecli.egg-info/requires.txt +0 -0
- {primecli-0.5.0 → primecli-0.5.2}/primecli.egg-info/top_level.txt +0 -0
- {primecli-0.5.0 → primecli-0.5.2}/setup.cfg +0 -0
- {primecli-0.5.0 → primecli-0.5.2}/tests/test_cross_file_identity.py +0 -0
- {primecli-0.5.0 → primecli-0.5.2}/tests/test_health_monitor.py +0 -0
- {primecli-0.5.0 → primecli-0.5.2}/tests/test_paraswap_validator.py +0 -0
- {primecli-0.5.0 → primecli-0.5.2}/tests/test_redstone_encoding.py +0 -0
- {primecli-0.5.0 → primecli-0.5.2}/tests/test_to_wei_units.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: primecli
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
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
|
|
@@ -48,7 +48,7 @@ Built for agent use:
|
|
|
48
48
|
- RedStone-signed solvency math handled internally, with a regression test pinning the half-boundary `toFixed(8)` encoding.
|
|
49
49
|
- ParaSwap calldata validated client-side against the on-chain executor allowlist before broadcast.
|
|
50
50
|
|
|
51
|
-
**Current version:** 0.5.
|
|
51
|
+
**Current version:** 0.5.2 The 0.x line is pre-1.0, so breaking changes are possible. See [Releases](https://github.com/Mnemosyne-quest/primecli/releases).
|
|
52
52
|
|
|
53
53
|
> **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)).
|
|
54
54
|
|
|
@@ -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.5.
|
|
19
|
+
**Current version:** 0.5.2 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
|
|
|
@@ -660,7 +660,10 @@ def _set_gas_price(w3, tx_dict):
|
|
|
660
660
|
"""Set appropriate gas price fields for the chain, replacing the legacy gasPrice approach.
|
|
661
661
|
On EIP-1559 chains (Arbitrum, Base): sets maxFeePerGas + maxPriorityFeePerGas with a 2x
|
|
662
662
|
base-fee hedge (base + prio + 1 gwei buffer). On Avalanche (legacy): sets gasPrice at
|
|
663
|
-
2x base fee with a
|
|
663
|
+
2x base fee with a 1 gwei floor. (25 gwei was the pre-Etna C-chain minimum;
|
|
664
|
+
ACP-125 (Dec 2024) lowered the min base fee to 1 nAVAX — base now sits at ~0.01
|
|
665
|
+
nAVAX, so a 25 gwei floor overpaid ~2500x and inflated the upfront balance
|
|
666
|
+
requirement past small EOAs.)"""
|
|
664
667
|
tx_dict.pop("gasPrice", None)
|
|
665
668
|
if CHAIN_ID in (42161, 8453): # Arbitrum, Base — EIP-1559
|
|
666
669
|
base = w3.eth.gas_price
|
|
@@ -668,13 +671,13 @@ def _set_gas_price(w3, tx_dict):
|
|
|
668
671
|
tx_dict["maxFeePerGas"] = max(int(base * 2), base + prio + 10**9)
|
|
669
672
|
tx_dict["maxPriorityFeePerGas"] = prio
|
|
670
673
|
else: # Avalanche (43114) — legacy gasPrice
|
|
671
|
-
tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2),
|
|
674
|
+
tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2), 1 * 10**9)
|
|
672
675
|
|
|
673
676
|
def _set_gas_price_for(chain_id, w3, tx_dict):
|
|
674
677
|
"""Set gas fields for an EXPLICIT chain_id rather than the module CHAIN_ID. Needed by
|
|
675
678
|
cross-chain flows (prime-bridge) where a tx may target Avalanche or Arbitrum regardless
|
|
676
679
|
of which tool built it. Arbitrum/Base (EIP-1559): maxFeePerGas + maxPriorityFeePerGas;
|
|
677
|
-
Avalanche (legacy): gasPrice with a
|
|
680
|
+
Avalanche (legacy): gasPrice with a 1 gwei floor (post-Etna; see _set_gas_price)."""
|
|
678
681
|
tx_dict.pop("gasPrice", None)
|
|
679
682
|
if chain_id in (42161, 8453): # Arbitrum, Base — EIP-1559
|
|
680
683
|
base = w3.eth.gas_price
|
|
@@ -682,7 +685,7 @@ def _set_gas_price_for(chain_id, w3, tx_dict):
|
|
|
682
685
|
tx_dict["maxFeePerGas"] = max(int(base * 2), base + prio + 10**9)
|
|
683
686
|
tx_dict["maxPriorityFeePerGas"] = prio
|
|
684
687
|
else: # Avalanche (43114) — legacy gasPrice
|
|
685
|
-
tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2),
|
|
688
|
+
tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2), 1 * 10**9)
|
|
686
689
|
|
|
687
690
|
def _read_env_var(path, var):
|
|
688
691
|
"""Return the value of `var` from a KEY=VALUE env file, or None if absent."""
|
|
@@ -15,6 +15,7 @@ Usage:
|
|
|
15
15
|
degenprime create-account [--execute]
|
|
16
16
|
degenprime create-account --fund-pool usdc --fund-amount 100 [--execute]
|
|
17
17
|
degenprime summary
|
|
18
|
+
degenprime defi --json (aggregate ALL positions as DeBank-style JSON; read-only)
|
|
18
19
|
degenprime fund --pool usdc --amount 100 [--execute]
|
|
19
20
|
degenprime borrow --pool usdc --amount 100 [--execute]
|
|
20
21
|
degenprime repay --pool usdc --amount 100 [--execute]
|
|
@@ -236,7 +237,10 @@ def _set_gas_price(w3, tx_dict):
|
|
|
236
237
|
"""Set appropriate gas price fields for the chain, replacing the legacy gasPrice approach.
|
|
237
238
|
On EIP-1559 chains (Arbitrum, Base): sets maxFeePerGas + maxPriorityFeePerGas with a 2x
|
|
238
239
|
base-fee hedge (base + prio + 1 gwei buffer). On Avalanche (legacy): sets gasPrice at
|
|
239
|
-
2x base fee with a
|
|
240
|
+
2x base fee with a 1 gwei floor. (25 gwei was the pre-Etna C-chain minimum;
|
|
241
|
+
ACP-125 (Dec 2024) lowered the min base fee to 1 nAVAX — base now sits at ~0.01
|
|
242
|
+
nAVAX, so a 25 gwei floor overpaid ~2500x and inflated the upfront balance
|
|
243
|
+
requirement past small EOAs.)"""
|
|
240
244
|
tx_dict.pop("gasPrice", None)
|
|
241
245
|
if CHAIN_ID in (42161, 8453): # Arbitrum, Base — EIP-1559
|
|
242
246
|
base = w3.eth.gas_price
|
|
@@ -244,7 +248,7 @@ def _set_gas_price(w3, tx_dict):
|
|
|
244
248
|
tx_dict["maxFeePerGas"] = max(int(base * 2), base + prio + 10**9)
|
|
245
249
|
tx_dict["maxPriorityFeePerGas"] = prio
|
|
246
250
|
else: # Avalanche (43114) — legacy gasPrice
|
|
247
|
-
tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2),
|
|
251
|
+
tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2), 1 * 10**9)
|
|
248
252
|
def resolve_private_key():
|
|
249
253
|
"""Resolve the signing key per the documented precedence:
|
|
250
254
|
1. --key <0xhex> CLI flag
|
|
@@ -1290,34 +1294,10 @@ def _prices_usd(w3, account, symbols: list, payload: bytes) -> dict:
|
|
|
1290
1294
|
except Exception:
|
|
1291
1295
|
return {}
|
|
1292
1296
|
|
|
1293
|
-
def
|
|
1294
|
-
"""
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
Note: per-asset USD is best-effort - only symbols with a RedStone primary-prod
|
|
1298
|
-
feed are priced here. Symbols sourced on-chain from BaseOracle TWAP show as
|
|
1299
|
-
balance-only (the SolvencyFacet still values them for the total/debt figures).
|
|
1300
|
-
|
|
1301
|
-
With --json: emits a single JSON object covering wallet, account, native
|
|
1302
|
-
balance, per-asset supplied/borrowed with optional USD, poolDeposits (the EOA's
|
|
1303
|
-
'Diamond Hands' lending-pool balances, emitted even with no Degen Account),
|
|
1304
|
-
total/debt/health-ratio/solvent flags. Null fields, empty lists, and empty dicts are dropped (same
|
|
1305
|
-
trim contract as `deltaprime defi --json`). Numeric 0 and boolean false are
|
|
1306
|
-
preserved.
|
|
1307
|
-
|
|
1308
|
-
Multicall: stage A batches getAllOwnedAssets + getDebts (2 -> 1 RPC). Stage B
|
|
1309
|
-
batches one getBalance per owned asset (N -> 1 RPC). Stage C batches the four
|
|
1310
|
-
RedStone-gated solvency views + getPrices (4-5 -> 1 RPC), each leg carrying the
|
|
1311
|
-
same RedStone payload appended."""
|
|
1312
|
-
w3 = get_w3()
|
|
1313
|
-
acct = get_account()
|
|
1314
|
-
pa = get_prime_account(w3, acct.address)
|
|
1315
|
-
if not as_json:
|
|
1316
|
-
print(f"Wallet: {acct.address}")
|
|
1317
|
-
|
|
1318
|
-
# Pool deposits ("Diamond Hands") are EOA balances independent of the Degen Account,
|
|
1319
|
-
# so read them up front via one Multicall3 — they must surface even for a wallet with
|
|
1320
|
-
# no Degen Account (e.g. a deposit made before creating one).
|
|
1297
|
+
def _gather_pool_deposits(w3, owner: str) -> list:
|
|
1298
|
+
"""The EOA's 'Diamond Hands' lending-pool balances, read independently of the Degen
|
|
1299
|
+
Account via one Multicall3 (one balanceOf per pool). Surfaces even for a wallet with
|
|
1300
|
+
no Degen Account. Returns [{symbol, raw, decimals}, ...] for non-zero balances."""
|
|
1321
1301
|
pool_deposits = []
|
|
1322
1302
|
dep_legs, dep_meta = [], []
|
|
1323
1303
|
for _pname, _pcfg in POOLS.items():
|
|
@@ -1326,7 +1306,7 @@ def cmd_summary(as_json: bool = False):
|
|
|
1326
1306
|
except Exception:
|
|
1327
1307
|
continue
|
|
1328
1308
|
_proxy_cs = Web3.to_checksum_address(_pcfg["proxy"])
|
|
1329
|
-
dep_legs.append((_proxy_cs, bytes.fromhex(_pc.encode_abi("balanceOf", args=[
|
|
1309
|
+
dep_legs.append((_proxy_cs, bytes.fromhex(_pc.encode_abi("balanceOf", args=[owner])[2:])))
|
|
1330
1310
|
dep_meta.append(_pcfg)
|
|
1331
1311
|
if dep_legs:
|
|
1332
1312
|
try:
|
|
@@ -1337,32 +1317,21 @@ def cmd_summary(as_json: bool = False):
|
|
|
1337
1317
|
_bal = w3.codec.decode(["uint256"], _rd)[0] if _ok and _rd else 0
|
|
1338
1318
|
if _bal > 0:
|
|
1339
1319
|
pool_deposits.append({"symbol": _pcfg["symbol"], "raw": _bal, "decimals": _pcfg["decimals"]})
|
|
1320
|
+
return pool_deposits
|
|
1340
1321
|
|
|
1341
|
-
if not pa:
|
|
1342
|
-
# No Degen Account: still surface Diamond Hands deposits (balance-only — the
|
|
1343
|
-
# RedStone getPrices view lives on the Degen Account, absent here).
|
|
1344
|
-
if as_json:
|
|
1345
|
-
out = {"wallet": acct.address, "account": None}
|
|
1346
|
-
if pool_deposits:
|
|
1347
|
-
out["poolDeposits"] = [{"symbol": r["symbol"], "amount": r["raw"] / 10**r["decimals"]}
|
|
1348
|
-
for r in pool_deposits]
|
|
1349
|
-
print(json.dumps(out, indent=2))
|
|
1350
|
-
else:
|
|
1351
|
-
print("No Degen Account yet. Create one with: degenprime create-account --execute")
|
|
1352
|
-
if pool_deposits:
|
|
1353
|
-
print(" Pool Deposits (Diamond Hands):")
|
|
1354
|
-
for r in pool_deposits:
|
|
1355
|
-
print(f" {r['symbol']:<8} {r['raw'] / 10**r['decimals']:,.6f}")
|
|
1356
|
-
return
|
|
1357
1322
|
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1323
|
+
def _gather_account_state(w3, account, pool_deposits: list):
|
|
1324
|
+
"""Read-only collateral / debt / RedStone-gated solvency for an existing Degen Account.
|
|
1325
|
+
Shared by `summary` and `defi`. Returns (pa_eth, supplied, borrowed, solvency) where
|
|
1326
|
+
supplied/borrowed are [{symbol, raw, decimals}, ...] and solvency carries
|
|
1327
|
+
total/debt/ratio/solvent/error/prices. pool_deposits is taken so their feeds get folded
|
|
1328
|
+
into the RedStone payload (else getPrices reverts on a deposit-only symbol).
|
|
1363
1329
|
|
|
1364
|
-
|
|
1330
|
+
Multicall: stage A batches getAllOwnedAssets + getDebts (2 -> 1 RPC). Stage B batches
|
|
1331
|
+
one getBalance per owned asset (N -> 1 RPC). Stage C batches the four RedStone-gated
|
|
1332
|
+
solvency views + getPrices (4-5 -> 1 RPC), each leg carrying the same payload appended."""
|
|
1365
1333
|
pa_cs = account.address
|
|
1334
|
+
pa_eth = w3.eth.get_balance(pa_cs) / 1e18
|
|
1366
1335
|
stage_a_legs = [
|
|
1367
1336
|
("getAllOwnedAssets", ["bytes32[]"], account.encode_abi("getAllOwnedAssets", args=[])),
|
|
1368
1337
|
("getDebts", ["(bytes32,uint256)[]"], account.encode_abi("getDebts", args=[])),
|
|
@@ -1446,6 +1415,160 @@ def cmd_summary(as_json: bool = False):
|
|
|
1446
1415
|
solvency["prices"] = prices
|
|
1447
1416
|
except Exception as e:
|
|
1448
1417
|
solvency["error"] = type(e).__name__
|
|
1418
|
+
return pa_eth, supplied, borrowed, solvency
|
|
1419
|
+
|
|
1420
|
+
|
|
1421
|
+
def gather_defi() -> dict:
|
|
1422
|
+
"""Aggregate ALL DegenPrime positions for the selected wallet into one DeBank-style dict,
|
|
1423
|
+
matching the cross-tool shape `deltaprime defi --json` emits. Read-only: reuses the same
|
|
1424
|
+
gather helpers as `summary` (lending/solvency via the RedStone-gated views, plus the EOA's
|
|
1425
|
+
own pool deposits surfaced as a Savings group). Empty groups are omitted. total_usd /
|
|
1426
|
+
health_ratio / solvent come from the RedStone-gated solvency views; per-asset USD is
|
|
1427
|
+
best-effort (omitted where a RedStone feed is missing). Never broadcasts."""
|
|
1428
|
+
w3 = get_w3()
|
|
1429
|
+
acct = get_account()
|
|
1430
|
+
pa = get_prime_account(w3, acct.address)
|
|
1431
|
+
result = {
|
|
1432
|
+
"protocol": "DegenPrime", "url": "https://degenprime.io", "chain": "base",
|
|
1433
|
+
"wallet": acct.address, "prime_account": pa,
|
|
1434
|
+
"total_usd": None, "health_ratio": None, "solvent": None,
|
|
1435
|
+
"groups": [], "status": "ok",
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
pool_deposits = _gather_pool_deposits(w3, acct.address)
|
|
1439
|
+
# _gather_account_state folds pool-deposit symbols into getPrices, so this map covers
|
|
1440
|
+
# both in-account assets and Diamond-Hands deposits. Empty with no Degen Account.
|
|
1441
|
+
prices = {}
|
|
1442
|
+
|
|
1443
|
+
if pa:
|
|
1444
|
+
account = w3.eth.contract(address=Web3.to_checksum_address(pa), abi=PRIME_ACCOUNT_ABI)
|
|
1445
|
+
_pa_eth, supplied, borrowed, solvency = _gather_account_state(w3, account, pool_deposits)
|
|
1446
|
+
prices = solvency["prices"]
|
|
1447
|
+
result["total_usd"] = solvency["total"]
|
|
1448
|
+
result["health_ratio"] = solvency["ratio"]
|
|
1449
|
+
result["solvent"] = solvency["solvent"]
|
|
1450
|
+
if solvency["error"]:
|
|
1451
|
+
result["solvency_error"] = solvency["error"]
|
|
1452
|
+
|
|
1453
|
+
def _row(r):
|
|
1454
|
+
amt = r["raw"] / 10**r["decimals"]
|
|
1455
|
+
row = {"symbol": r["symbol"], "balance": f"{amt:.6f}"}
|
|
1456
|
+
usd = prices.get(r["symbol"])
|
|
1457
|
+
if usd is not None:
|
|
1458
|
+
row["usd"] = round(amt * usd, 2)
|
|
1459
|
+
return row
|
|
1460
|
+
|
|
1461
|
+
if supplied or borrowed:
|
|
1462
|
+
result["groups"].append({
|
|
1463
|
+
"type": "Lending / Leverage", "health_ratio": solvency["ratio"],
|
|
1464
|
+
"supplied": [_row(r) for r in supplied],
|
|
1465
|
+
"borrowed": [_row(r) for r in borrowed],
|
|
1466
|
+
})
|
|
1467
|
+
|
|
1468
|
+
# Savings: the EOA's own pool deposits ("Diamond Hands"), independent of the Degen
|
|
1469
|
+
# Account (so NOT in getTotalValue) — surfaced as their own group and added on top.
|
|
1470
|
+
# Priced from the same RedStone read used for the account (no extra RPC).
|
|
1471
|
+
if pool_deposits:
|
|
1472
|
+
sav_rows, sav_usd_total = [], 0.0
|
|
1473
|
+
for r in pool_deposits:
|
|
1474
|
+
amt = r["raw"] / 10**r["decimals"]
|
|
1475
|
+
row = {"symbol": r["symbol"], "balance": f"{amt:.6f}"}
|
|
1476
|
+
usd = prices.get(r["symbol"])
|
|
1477
|
+
if usd is not None:
|
|
1478
|
+
row["usd"] = round(amt * usd, 2)
|
|
1479
|
+
sav_usd_total += amt * usd
|
|
1480
|
+
sav_rows.append(row)
|
|
1481
|
+
result["groups"].append({"type": "Savings", "label": "Savings", "supplied": sav_rows})
|
|
1482
|
+
if sav_usd_total:
|
|
1483
|
+
result["total_usd"] = (result["total_usd"] or 0) + sav_usd_total
|
|
1484
|
+
|
|
1485
|
+
return result
|
|
1486
|
+
|
|
1487
|
+
|
|
1488
|
+
_DEFI_DECORATIVE_KEYS = {"url"}
|
|
1489
|
+
|
|
1490
|
+
|
|
1491
|
+
def _trim_defi_json(value):
|
|
1492
|
+
"""Recursively strip noise from `defi --json` output so an LLM consumer doesn't pay
|
|
1493
|
+
context for fields that carry no information: drops dict keys whose value is exactly
|
|
1494
|
+
None, drops keys whose value is an empty list or empty dict, drops the decorative
|
|
1495
|
+
top-level `url` key, but PRESERVES numeric 0 and boolean False (zero balance,
|
|
1496
|
+
explicitly-not-solvent, etc.) and keeps the top-level structure so a consumer can tell
|
|
1497
|
+
what's missing from what shape the response took. Same contract as `deltaprime`'s."""
|
|
1498
|
+
if isinstance(value, dict):
|
|
1499
|
+
out = {}
|
|
1500
|
+
for k, v in value.items():
|
|
1501
|
+
if k in _DEFI_DECORATIVE_KEYS:
|
|
1502
|
+
continue
|
|
1503
|
+
trimmed = _trim_defi_json(v)
|
|
1504
|
+
if trimmed is None:
|
|
1505
|
+
continue
|
|
1506
|
+
if isinstance(trimmed, (list, dict)) and len(trimmed) == 0:
|
|
1507
|
+
continue
|
|
1508
|
+
out[k] = trimmed
|
|
1509
|
+
return out
|
|
1510
|
+
if isinstance(value, list):
|
|
1511
|
+
return [_trim_defi_json(v) for v in value]
|
|
1512
|
+
return value
|
|
1513
|
+
|
|
1514
|
+
|
|
1515
|
+
def cmd_defi(as_json: bool = True):
|
|
1516
|
+
"""Aggregate all DegenPrime positions for the wallet. Default output is the DeBank-style
|
|
1517
|
+
JSON (the cross-tool shape the health monitor consumes). On error, emits
|
|
1518
|
+
{"status":"error", ...} rather than raising, so the caller always gets parseable JSON."""
|
|
1519
|
+
try:
|
|
1520
|
+
data = gather_defi()
|
|
1521
|
+
except Exception as e:
|
|
1522
|
+
data = {"protocol": "DegenPrime", "chain": "base",
|
|
1523
|
+
"status": "error", "error": f"{type(e).__name__}: {e}"}
|
|
1524
|
+
print(json.dumps(_trim_defi_json(data), indent=2))
|
|
1525
|
+
|
|
1526
|
+
|
|
1527
|
+
def cmd_summary(as_json: bool = False):
|
|
1528
|
+
"""Read-only Degen Account view: in-account collateral, debts, and live
|
|
1529
|
+
RedStone-gated solvency (getTotalValue/getDebt/getHealthRatio/isSolvent). Falls
|
|
1530
|
+
back to balances-only if the RedStone gateway is unreachable or a view reverts.
|
|
1531
|
+
Note: per-asset USD is best-effort - only symbols with a RedStone primary-prod
|
|
1532
|
+
feed are priced here. Symbols sourced on-chain from BaseOracle TWAP show as
|
|
1533
|
+
balance-only (the SolvencyFacet still values them for the total/debt figures).
|
|
1534
|
+
|
|
1535
|
+
With --json: emits a single JSON object covering wallet, account, native
|
|
1536
|
+
balance, per-asset supplied/borrowed with optional USD, poolDeposits (the EOA's
|
|
1537
|
+
'Diamond Hands' lending-pool balances, emitted even with no Degen Account),
|
|
1538
|
+
total/debt/health-ratio/solvent flags. Null fields, empty lists, and empty dicts are dropped (same
|
|
1539
|
+
trim contract as `deltaprime defi --json`). Numeric 0 and boolean false are
|
|
1540
|
+
preserved."""
|
|
1541
|
+
w3 = get_w3()
|
|
1542
|
+
acct = get_account()
|
|
1543
|
+
pa = get_prime_account(w3, acct.address)
|
|
1544
|
+
if not as_json:
|
|
1545
|
+
print(f"Wallet: {acct.address}")
|
|
1546
|
+
|
|
1547
|
+
pool_deposits = _gather_pool_deposits(w3, acct.address)
|
|
1548
|
+
|
|
1549
|
+
if not pa:
|
|
1550
|
+
# No Degen Account: still surface Diamond Hands deposits (balance-only — the
|
|
1551
|
+
# RedStone getPrices view lives on the Degen Account, absent here).
|
|
1552
|
+
if as_json:
|
|
1553
|
+
out = {"wallet": acct.address, "account": None}
|
|
1554
|
+
if pool_deposits:
|
|
1555
|
+
out["poolDeposits"] = [{"symbol": r["symbol"], "amount": r["raw"] / 10**r["decimals"]}
|
|
1556
|
+
for r in pool_deposits]
|
|
1557
|
+
print(json.dumps(out, indent=2))
|
|
1558
|
+
else:
|
|
1559
|
+
print("No Degen Account yet. Create one with: degenprime create-account --execute")
|
|
1560
|
+
if pool_deposits:
|
|
1561
|
+
print(" Pool Deposits (Diamond Hands):")
|
|
1562
|
+
for r in pool_deposits:
|
|
1563
|
+
print(f" {r['symbol']:<8} {r['raw'] / 10**r['decimals']:,.6f}")
|
|
1564
|
+
return
|
|
1565
|
+
|
|
1566
|
+
account = w3.eth.contract(address=Web3.to_checksum_address(pa), abi=PRIME_ACCOUNT_ABI)
|
|
1567
|
+
pa_eth, supplied, borrowed, solvency = _gather_account_state(w3, account, pool_deposits)
|
|
1568
|
+
if not as_json:
|
|
1569
|
+
print(f"Degen Account: {pa}")
|
|
1570
|
+
if pa_eth >= 1e-9:
|
|
1571
|
+
print(f" Native ETH (gas): {pa_eth:.6f}")
|
|
1449
1572
|
|
|
1450
1573
|
if as_json:
|
|
1451
1574
|
def _asset_row(r):
|
|
@@ -2312,6 +2435,8 @@ def _dispatch():
|
|
|
2312
2435
|
cmd_create_account("--execute" in args, fund_pool, fund_amount)
|
|
2313
2436
|
elif cmd == "summary":
|
|
2314
2437
|
cmd_summary(as_json="--json" in args)
|
|
2438
|
+
elif cmd == "defi":
|
|
2439
|
+
cmd_defi("--json" in args)
|
|
2315
2440
|
elif cmd == "fund":
|
|
2316
2441
|
pool, amount = None, None
|
|
2317
2442
|
execute = "--execute" in args
|
|
@@ -660,7 +660,10 @@ def _set_gas_price(w3, tx_dict):
|
|
|
660
660
|
"""Set appropriate gas price fields for the chain, replacing the legacy gasPrice approach.
|
|
661
661
|
On EIP-1559 chains (Arbitrum, Base): sets maxFeePerGas + maxPriorityFeePerGas with a 2x
|
|
662
662
|
base-fee hedge (base + prio + 1 gwei buffer). On Avalanche (legacy): sets gasPrice at
|
|
663
|
-
2x base fee with a
|
|
663
|
+
2x base fee with a 1 gwei floor. (25 gwei was the pre-Etna C-chain minimum;
|
|
664
|
+
ACP-125 (Dec 2024) lowered the min base fee to 1 nAVAX — base now sits at ~0.01
|
|
665
|
+
nAVAX, so a 25 gwei floor overpaid ~2500x and inflated the upfront balance
|
|
666
|
+
requirement past small EOAs.)"""
|
|
664
667
|
tx_dict.pop("gasPrice", None)
|
|
665
668
|
if CHAIN_ID in (42161, 8453): # Arbitrum, Base — EIP-1559
|
|
666
669
|
base = w3.eth.gas_price
|
|
@@ -668,13 +671,13 @@ def _set_gas_price(w3, tx_dict):
|
|
|
668
671
|
tx_dict["maxFeePerGas"] = max(int(base * 2), base + prio + 10**9)
|
|
669
672
|
tx_dict["maxPriorityFeePerGas"] = prio
|
|
670
673
|
else: # Avalanche (43114) — legacy gasPrice
|
|
671
|
-
tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2),
|
|
674
|
+
tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2), 1 * 10**9)
|
|
672
675
|
|
|
673
676
|
def _set_gas_price_for(chain_id, w3, tx_dict):
|
|
674
677
|
"""Set gas fields for an EXPLICIT chain_id rather than the module CHAIN_ID. Needed by
|
|
675
678
|
cross-chain flows (prime-bridge) where a tx may target Avalanche or Arbitrum regardless
|
|
676
679
|
of which tool built it. Arbitrum/Base (EIP-1559): maxFeePerGas + maxPriorityFeePerGas;
|
|
677
|
-
Avalanche (legacy): gasPrice with a
|
|
680
|
+
Avalanche (legacy): gasPrice with a 1 gwei floor (post-Etna; see _set_gas_price)."""
|
|
678
681
|
tx_dict.pop("gasPrice", None)
|
|
679
682
|
if chain_id in (42161, 8453): # Arbitrum, Base — EIP-1559
|
|
680
683
|
base = w3.eth.gas_price
|
|
@@ -682,7 +685,7 @@ def _set_gas_price_for(chain_id, w3, tx_dict):
|
|
|
682
685
|
tx_dict["maxFeePerGas"] = max(int(base * 2), base + prio + 10**9)
|
|
683
686
|
tx_dict["maxPriorityFeePerGas"] = prio
|
|
684
687
|
else: # Avalanche (43114) — legacy gasPrice
|
|
685
|
-
tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2),
|
|
688
|
+
tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2), 1 * 10**9)
|
|
686
689
|
|
|
687
690
|
def _read_env_var(path, var):
|
|
688
691
|
"""Return the value of `var` from a KEY=VALUE env file, or None if absent."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: primecli
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
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
|
|
@@ -48,7 +48,7 @@ Built for agent use:
|
|
|
48
48
|
- RedStone-signed solvency math handled internally, with a regression test pinning the half-boundary `toFixed(8)` encoding.
|
|
49
49
|
- ParaSwap calldata validated client-side against the on-chain executor allowlist before broadcast.
|
|
50
50
|
|
|
51
|
-
**Current version:** 0.5.
|
|
51
|
+
**Current version:** 0.5.2 The 0.x line is pre-1.0, so breaking changes are possible. See [Releases](https://github.com/Mnemosyne-quest/primecli/releases).
|
|
52
52
|
|
|
53
53
|
> **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)).
|
|
54
54
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "primecli"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.2"
|
|
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"
|
|
@@ -4,7 +4,7 @@ This helper sets gas fields for an explicit chain id (used by cross-chain flows
|
|
|
4
4
|
like prime-bridge), so it must pick the right fee model per chain:
|
|
5
5
|
* Arbitrum (42161) / Base (8453): EIP-1559 — maxFeePerGas + maxPriorityFeePerGas,
|
|
6
6
|
and NO legacy gasPrice.
|
|
7
|
-
* Avalanche (43114): legacy gasPrice with a
|
|
7
|
+
* Avalanche (43114): legacy gasPrice with a 1 gwei floor (post-Etna), and NO EIP-1559 fields.
|
|
8
8
|
|
|
9
9
|
No RPC is made: we feed a stub w3 whose `eth.gas_price` / `eth.max_priority_fee`
|
|
10
10
|
return canned values. The helper is duplicated in both modules, so both are tested.
|
|
@@ -82,11 +82,11 @@ def test_base_sets_eip1559_no_gasprice(mod):
|
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
85
|
-
# Avalanche (43114) — legacy gasPrice with
|
|
85
|
+
# Avalanche (43114) — legacy gasPrice with 1 gwei floor (post-Etna ACP-125)
|
|
86
86
|
|
|
87
87
|
|
|
88
88
|
def test_avalanche_sets_legacy_gasprice_no_eip1559(mod):
|
|
89
|
-
# gas_price*2 (60 gwei) >
|
|
89
|
+
# gas_price*2 (60 gwei) > 1 gwei floor → uses doubled value
|
|
90
90
|
w3 = _StubW3(gas_price=30 * GWEI, max_priority_fee=1 * GWEI)
|
|
91
91
|
tx = {}
|
|
92
92
|
mod._set_gas_price_for(43114, w3, tx)
|
|
@@ -95,11 +95,11 @@ def test_avalanche_sets_legacy_gasprice_no_eip1559(mod):
|
|
|
95
95
|
assert "maxPriorityFeePerGas" not in tx
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
def
|
|
99
|
-
# gas_price*2 (
|
|
100
|
-
w3 = _StubW3(gas_price=
|
|
98
|
+
def test_avalanche_applies_1_gwei_floor(mod):
|
|
99
|
+
# gas_price*2 (0.02 gwei, realistic post-Etna base) < 1 gwei floor → floor wins
|
|
100
|
+
w3 = _StubW3(gas_price=GWEI // 100, max_priority_fee=1 * GWEI)
|
|
101
101
|
tx = {"gasPrice": 1} # stale value replaced, not added-to
|
|
102
102
|
mod._set_gas_price_for(43114, w3, tx)
|
|
103
|
-
assert tx["gasPrice"] ==
|
|
103
|
+
assert tx["gasPrice"] == 1 * GWEI
|
|
104
104
|
assert "maxFeePerGas" not in tx
|
|
105
105
|
assert "maxPriorityFeePerGas" not in tx
|
|
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
|