primecli 0.1.3__tar.gz → 0.2.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.1.3 → primecli-0.2.0}/PKG-INFO +5 -5
- {primecli-0.1.3 → primecli-0.2.0}/README.md +4 -4
- {primecli-0.1.3 → primecli-0.2.0}/primecli/degenprime.py +277 -58
- {primecli-0.1.3 → primecli-0.2.0}/primecli/deltaprime.py +430 -86
- {primecli-0.1.3 → primecli-0.2.0}/primecli.egg-info/PKG-INFO +5 -5
- {primecli-0.1.3 → primecli-0.2.0}/pyproject.toml +1 -1
- {primecli-0.1.3 → primecli-0.2.0}/LICENSE +0 -0
- {primecli-0.1.3 → primecli-0.2.0}/primecli/__init__.py +0 -0
- {primecli-0.1.3 → primecli-0.2.0}/primecli.egg-info/SOURCES.txt +0 -0
- {primecli-0.1.3 → primecli-0.2.0}/primecli.egg-info/dependency_links.txt +0 -0
- {primecli-0.1.3 → primecli-0.2.0}/primecli.egg-info/entry_points.txt +0 -0
- {primecli-0.1.3 → primecli-0.2.0}/primecli.egg-info/requires.txt +0 -0
- {primecli-0.1.3 → primecli-0.2.0}/primecli.egg-info/top_level.txt +0 -0
- {primecli-0.1.3 → primecli-0.2.0}/setup.cfg +0 -0
- {primecli-0.1.3 → primecli-0.2.0}/tests/test_redstone_encoding.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: primecli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Agent-friendly CLI tools for the DeltaPrime (Avalanche) and DegenPrime (Base) lending and leverage protocols. Preview-by-default; no Etherscan key required.
|
|
5
5
|
Author: Mnemosyne-quest contributors
|
|
6
6
|
License: MIT
|
|
@@ -118,7 +118,7 @@ State-changing commands preview by default. Add `--execute` to broadcast.
|
|
|
118
118
|
|
|
119
119
|
| Group | Commands |
|
|
120
120
|
|-------|----------|
|
|
121
|
-
| Lending core | `pool-info`, `my-positions`, `deposit`, `withdraw`, `borrow`, `repay`, `fund` |
|
|
121
|
+
| Lending core | `pool-info [--json]`, `my-positions`, `deposit`, `withdraw`, `borrow`, `repay`, `fund` |
|
|
122
122
|
| Prime Account | `create-prime-account` (alias `create-account`), `prime-summary`, `defi --json`, `withdraw-collateral`, `withdrawal-intents`, `execute-withdrawal` |
|
|
123
123
|
| Swaps | `swap --from S --to S --amount N [--via yak\|paraswap] [--slippage P]`, `swap-debt --from S --to S --amount N [--slippage P]` |
|
|
124
124
|
| GMX V2 LP (async, keeper-executed) | `gmx-positions`, `gmx-deposit --market M --amount N [--side long\|short]`, `gmx-withdraw --market M --amount N` |
|
|
@@ -129,7 +129,7 @@ State-changing commands preview by default. Add `--execute` to broadcast.
|
|
|
129
129
|
|
|
130
130
|
Pools: `usdc`, `wavax`, `weth`, `btc`, `usdt`. GM markets: `avax-usdc`, `btc-usdc`, `eth-usdc` (two-sided GM); `avax+`, `btc+`, `eth+` (single-sided GM+). LB pairs: `avax-usdc`, `avax-usdc-20`, `btc-usdc`, `eth-avax`, `btc-avax`, `avax-btc`, `eurc-usdc`, `usdt-usdc`, `joe-avax`.
|
|
131
131
|
|
|
132
|
-
`defi --json` emits a full positions snapshot in a DeBank-like shape. `zap` composes `fund` → `borrow` → optional `swap` → `gmx-deposit` into one preview-and-execute flow, stopping on the first failure.
|
|
132
|
+
`defi --json` emits a full positions snapshot in a DeBank-like shape — `null`, empty-list, and empty-dict fields are stripped, and the decorative `url` key is dropped (numeric `0` and boolean `false` are preserved, since they carry real information). `pool-info --json` emits a single JSON object for a named pool, or a `{name: {...}}` dict for `all`. `zap` composes `fund` → `borrow` → optional `swap` → `gmx-deposit` into one preview-and-execute flow, stopping on the first failure.
|
|
133
133
|
|
|
134
134
|
Full per-command reference: [docs/deltaprime-reference.md](docs/deltaprime-reference.md). Per-capability build spec: [docs/deltaprime-capabilities.md](docs/deltaprime-capabilities.md).
|
|
135
135
|
|
|
@@ -137,7 +137,7 @@ Full per-command reference: [docs/deltaprime-reference.md](docs/deltaprime-refer
|
|
|
137
137
|
|
|
138
138
|
| Group | Commands |
|
|
139
139
|
|-------|----------|
|
|
140
|
-
| Lending core | `pool-info`, `my-positions`, `deposit`, `withdraw`, `borrow`, `repay`, `fund` |
|
|
140
|
+
| Lending core | `pool-info [--json]`, `my-positions`, `deposit`, `withdraw`, `borrow`, `repay`, `fund` |
|
|
141
141
|
| Degen Account | `create-account`, `summary`, `withdraw-collateral`, `withdrawal-intents`, `execute-withdrawal`, `cancel-withdrawal` |
|
|
142
142
|
| Swaps | `swap --from S --to S --amount N [--slippage P]` (ParaSwap v6), `swap-debt --from S --to S --amount N` |
|
|
143
143
|
| Aerodrome (read-only in v1) | `aerodrome-positions` |
|
|
@@ -210,7 +210,7 @@ A copy-paste template is at [examples/env.example](examples/env.example).
|
|
|
210
210
|
`primecli` is built for autonomous and semi-autonomous use. Three properties matter:
|
|
211
211
|
|
|
212
212
|
1. **Preview by default.** Every state-changing command prints a structured preview and stops unless `--execute` is passed. An agent can call any command speculatively, parse the preview, decide whether to broadcast, then re-run with `--execute`.
|
|
213
|
-
2. **Predictable, parseable stdout.** Read-only commands (`pool-info
|
|
213
|
+
2. **Predictable, parseable stdout.** Read-only commands (`pool-info` (also `--json`), `my-positions`, `prime-summary`, `summary`, `withdrawal-intents`, `lb-positions`, `gmx-positions`, `aerodrome-positions`, `sjoe-position`, `prime-tier`, `defi --json`) emit fixed-format tables or JSON. `defi --json` is a one-shot full positions snapshot.
|
|
214
214
|
3. **Clean failure modes.** Configuration errors do not print stack traces. A missing key prints `deltaprime: No signing key found. Set DELTAPRIME_PRIVATE_KEY ...` to stderr and exits 1.
|
|
215
215
|
|
|
216
216
|
Full agent integration guide (Claude Code skill template, MCP notes, recommended guardrails): [docs/agent-integration.md](docs/agent-integration.md).
|
|
@@ -86,7 +86,7 @@ State-changing commands preview by default. Add `--execute` to broadcast.
|
|
|
86
86
|
|
|
87
87
|
| Group | Commands |
|
|
88
88
|
|-------|----------|
|
|
89
|
-
| Lending core | `pool-info`, `my-positions`, `deposit`, `withdraw`, `borrow`, `repay`, `fund` |
|
|
89
|
+
| Lending core | `pool-info [--json]`, `my-positions`, `deposit`, `withdraw`, `borrow`, `repay`, `fund` |
|
|
90
90
|
| Prime Account | `create-prime-account` (alias `create-account`), `prime-summary`, `defi --json`, `withdraw-collateral`, `withdrawal-intents`, `execute-withdrawal` |
|
|
91
91
|
| Swaps | `swap --from S --to S --amount N [--via yak\|paraswap] [--slippage P]`, `swap-debt --from S --to S --amount N [--slippage P]` |
|
|
92
92
|
| GMX V2 LP (async, keeper-executed) | `gmx-positions`, `gmx-deposit --market M --amount N [--side long\|short]`, `gmx-withdraw --market M --amount N` |
|
|
@@ -97,7 +97,7 @@ State-changing commands preview by default. Add `--execute` to broadcast.
|
|
|
97
97
|
|
|
98
98
|
Pools: `usdc`, `wavax`, `weth`, `btc`, `usdt`. GM markets: `avax-usdc`, `btc-usdc`, `eth-usdc` (two-sided GM); `avax+`, `btc+`, `eth+` (single-sided GM+). LB pairs: `avax-usdc`, `avax-usdc-20`, `btc-usdc`, `eth-avax`, `btc-avax`, `avax-btc`, `eurc-usdc`, `usdt-usdc`, `joe-avax`.
|
|
99
99
|
|
|
100
|
-
`defi --json` emits a full positions snapshot in a DeBank-like shape. `zap` composes `fund` → `borrow` → optional `swap` → `gmx-deposit` into one preview-and-execute flow, stopping on the first failure.
|
|
100
|
+
`defi --json` emits a full positions snapshot in a DeBank-like shape — `null`, empty-list, and empty-dict fields are stripped, and the decorative `url` key is dropped (numeric `0` and boolean `false` are preserved, since they carry real information). `pool-info --json` emits a single JSON object for a named pool, or a `{name: {...}}` dict for `all`. `zap` composes `fund` → `borrow` → optional `swap` → `gmx-deposit` into one preview-and-execute flow, stopping on the first failure.
|
|
101
101
|
|
|
102
102
|
Full per-command reference: [docs/deltaprime-reference.md](docs/deltaprime-reference.md). Per-capability build spec: [docs/deltaprime-capabilities.md](docs/deltaprime-capabilities.md).
|
|
103
103
|
|
|
@@ -105,7 +105,7 @@ Full per-command reference: [docs/deltaprime-reference.md](docs/deltaprime-refer
|
|
|
105
105
|
|
|
106
106
|
| Group | Commands |
|
|
107
107
|
|-------|----------|
|
|
108
|
-
| Lending core | `pool-info`, `my-positions`, `deposit`, `withdraw`, `borrow`, `repay`, `fund` |
|
|
108
|
+
| Lending core | `pool-info [--json]`, `my-positions`, `deposit`, `withdraw`, `borrow`, `repay`, `fund` |
|
|
109
109
|
| Degen Account | `create-account`, `summary`, `withdraw-collateral`, `withdrawal-intents`, `execute-withdrawal`, `cancel-withdrawal` |
|
|
110
110
|
| Swaps | `swap --from S --to S --amount N [--slippage P]` (ParaSwap v6), `swap-debt --from S --to S --amount N` |
|
|
111
111
|
| Aerodrome (read-only in v1) | `aerodrome-positions` |
|
|
@@ -178,7 +178,7 @@ A copy-paste template is at [examples/env.example](examples/env.example).
|
|
|
178
178
|
`primecli` is built for autonomous and semi-autonomous use. Three properties matter:
|
|
179
179
|
|
|
180
180
|
1. **Preview by default.** Every state-changing command prints a structured preview and stops unless `--execute` is passed. An agent can call any command speculatively, parse the preview, decide whether to broadcast, then re-run with `--execute`.
|
|
181
|
-
2. **Predictable, parseable stdout.** Read-only commands (`pool-info
|
|
181
|
+
2. **Predictable, parseable stdout.** Read-only commands (`pool-info` (also `--json`), `my-positions`, `prime-summary`, `summary`, `withdrawal-intents`, `lb-positions`, `gmx-positions`, `aerodrome-positions`, `sjoe-position`, `prime-tier`, `defi --json`) emit fixed-format tables or JSON. `defi --json` is a one-shot full positions snapshot.
|
|
182
182
|
3. **Clean failure modes.** Configuration errors do not print stack traces. A missing key prints `deltaprime: No signing key found. Set DELTAPRIME_PRIVATE_KEY ...` to stderr and exits 1.
|
|
183
183
|
|
|
184
184
|
Full agent integration guide (Claude Code skill template, MCP notes, recommended guardrails): [docs/agent-integration.md](docs/agent-integration.md).
|
|
@@ -8,7 +8,7 @@ a per-user SmartLoan (diamond proxy) created via the SmartLoansFactory. The EOA
|
|
|
8
8
|
it; borrow/repay/fund run on the Degen Account, which itself talks to the pools.
|
|
9
9
|
|
|
10
10
|
Usage:
|
|
11
|
-
degenprime pool-info [usdc|weth|cbbtc|aero|brett|kaito|cbdoge|cbxrp|all]
|
|
11
|
+
degenprime pool-info [usdc|weth|cbbtc|aero|brett|kaito|cbdoge|cbxrp|all] [--json]
|
|
12
12
|
degenprime my-positions
|
|
13
13
|
degenprime deposit --pool usdc --amount 100 [--execute]
|
|
14
14
|
degenprime withdraw --pool usdc --amount 100 [--execute]
|
|
@@ -298,6 +298,45 @@ def get_pool_contract(pool_name: str):
|
|
|
298
298
|
w3 = get_w3()
|
|
299
299
|
return w3.eth.contract(address=proxy, abi=POOL_ABI), cfg, w3
|
|
300
300
|
|
|
301
|
+
# Multicall3 — deterministic deployment at the same address on every EVM chain
|
|
302
|
+
# (Avalanche C-chain and Base both have it). aggregate3(Call3[]) batches read-only
|
|
303
|
+
# calls into one eth_call; allowFailure=true per-call so a single revert returns
|
|
304
|
+
# success=false for that leg instead of blowing up the whole batch. Used to collapse
|
|
305
|
+
# per-pool / per-asset fan-out loops (cmd_pool_info("all"), cmd_summary, my-positions)
|
|
306
|
+
# from N RPCs into 1. Same address as DeltaPrime's; verified on chainlist.org and
|
|
307
|
+
# tested against the Base RPC.
|
|
308
|
+
MULTICALL3 = "0xcA11bde05977b3631167028862bE2a173976CA11"
|
|
309
|
+
MULTICALL3_ABI = [
|
|
310
|
+
{"inputs": [{"components": [
|
|
311
|
+
{"name": "target", "type": "address"},
|
|
312
|
+
{"name": "allowFailure", "type": "bool"},
|
|
313
|
+
{"name": "callData", "type": "bytes"}], "type": "tuple[]", "name": "calls"}],
|
|
314
|
+
"name": "aggregate3",
|
|
315
|
+
"outputs": [{"components": [
|
|
316
|
+
{"name": "success", "type": "bool"},
|
|
317
|
+
{"name": "returnData", "type": "bytes"}], "type": "tuple[]", "name": "returnData"}],
|
|
318
|
+
"stateMutability": "view", "type": "function"},
|
|
319
|
+
]
|
|
320
|
+
|
|
321
|
+
def multicall(w3, calls):
|
|
322
|
+
"""Batch read-only calls via Multicall3.aggregate3. Each call is a (target_address,
|
|
323
|
+
calldata_bytes) tuple. Returns a list of (success, return_bytes) tuples in input
|
|
324
|
+
order — the caller is responsible for decoding return_bytes against the original
|
|
325
|
+
function's output types and treating success=False as a missing/reverted value. Empty
|
|
326
|
+
input returns []. The whole batch round-trips in one eth_call; gas is paid by the
|
|
327
|
+
simulated caller (zero address by default) so no key is required.
|
|
328
|
+
|
|
329
|
+
For RedStone-gated views: append the RedStone payload to each leg's calldata before
|
|
330
|
+
putting it in `calls`. Multicall3 only delegate-calls the target with the bytes you
|
|
331
|
+
provide; the on-chain solvency parser still reads the payload from the calldata tail
|
|
332
|
+
per leg."""
|
|
333
|
+
if not calls:
|
|
334
|
+
return []
|
|
335
|
+
mc = w3.eth.contract(address=Web3.to_checksum_address(MULTICALL3), abi=MULTICALL3_ABI)
|
|
336
|
+
args = [(Web3.to_checksum_address(t), True, d) for t, d in calls]
|
|
337
|
+
raw = mc.functions.aggregate3(args).call()
|
|
338
|
+
return [(bool(ok), bytes(rd)) for ok, rd in raw]
|
|
339
|
+
|
|
301
340
|
# Minimal Degen Account ABI: only the facet functions this tool calls. The diamond
|
|
302
341
|
# beacon's own ABI is beacon-management only, so the borrow/repay/fund and view
|
|
303
342
|
# selectors live in facets - we hand-pick the verified signatures here rather than
|
|
@@ -595,85 +634,205 @@ def redstone_view_call(w3, account, fn_name: str, payload: bytes, args: list = N
|
|
|
595
634
|
|
|
596
635
|
# ─── Commands ──────────────────────────────────────────────────────────────
|
|
597
636
|
|
|
598
|
-
def
|
|
637
|
+
def _pool_info_data(pool_name: str) -> dict:
|
|
638
|
+
"""Read every pool-info field for one pool in a SINGLE Multicall3 eth_call:
|
|
639
|
+
totalSupply, totalBorrowed, getDepositRate, getBorrowingRate, and (when a signer is
|
|
640
|
+
configured) the EOA's pool balance. Returns the raw + decoded values plus the
|
|
641
|
+
off-chain KuCoin USD price. Shared by the human-facing print path and the --json
|
|
642
|
+
path. cb-prefixed pool symbols fall back to their bare ticker for the KuCoin probe
|
|
643
|
+
(cbBTC -> BTC, cbDOGE -> DOGE, etc.)."""
|
|
644
|
+
contract, cfg, w3 = get_pool_contract(pool_name)
|
|
645
|
+
proxy_cs = Web3.to_checksum_address(cfg["proxy"])
|
|
646
|
+
legs = [
|
|
647
|
+
("totalSupply", contract.encode_abi("totalSupply", args=[])),
|
|
648
|
+
("totalBorrowed", contract.encode_abi("totalBorrowed", args=[])),
|
|
649
|
+
("getDepositRate", contract.encode_abi("getDepositRate", args=[])),
|
|
650
|
+
("getBorrowingRate", contract.encode_abi("getBorrowingRate", args=[])),
|
|
651
|
+
]
|
|
652
|
+
try:
|
|
653
|
+
acct = get_account()
|
|
654
|
+
signer_addr = acct.address
|
|
655
|
+
legs.append(("balanceOf", contract.encode_abi("balanceOf", args=[signer_addr])))
|
|
656
|
+
except RuntimeError:
|
|
657
|
+
signer_addr = None
|
|
658
|
+
results = multicall(w3, [(proxy_cs, bytes.fromhex(d[2:]) if d.startswith("0x") else bytes.fromhex(d))
|
|
659
|
+
for _, d in legs])
|
|
660
|
+
out_types = {"totalSupply": ["uint256"], "totalBorrowed": ["uint256"],
|
|
661
|
+
"getDepositRate": ["uint256"], "getBorrowingRate": ["uint256"],
|
|
662
|
+
"balanceOf": ["uint256"]}
|
|
663
|
+
decoded = {}
|
|
664
|
+
for (name, _data), (ok, rd) in zip(legs, results):
|
|
665
|
+
if not ok or not rd:
|
|
666
|
+
decoded[name] = None
|
|
667
|
+
continue
|
|
668
|
+
try:
|
|
669
|
+
decoded[name] = w3.codec.decode(out_types[name], rd)[0]
|
|
670
|
+
except Exception:
|
|
671
|
+
decoded[name] = None
|
|
672
|
+
price_sym = cfg["symbol"].replace("cb", "") if cfg["symbol"].startswith("cb") else cfg["symbol"]
|
|
673
|
+
price = token_price(price_sym)
|
|
674
|
+
return {"name": pool_name, "cfg": cfg, "signer": signer_addr,
|
|
675
|
+
"raw": decoded, "price": price}
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def _compact_num(value: float, places: int = 2) -> float:
|
|
679
|
+
"""Round to `places` decimal places, defaulting to 2. Compact enough for an LLM
|
|
680
|
+
consumer without lying about the underlying number. Used for amount/USD fields in
|
|
681
|
+
pool-info --json."""
|
|
682
|
+
if value is None:
|
|
683
|
+
return None
|
|
684
|
+
return round(float(value), places)
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _pool_json_shape(data: dict) -> dict:
|
|
688
|
+
"""Per-pool JSON object for `pool-info --json`. Same key names as deltaprime's
|
|
689
|
+
_pool_json_shape so an agent can consume both chains uniformly. Numbers are floats
|
|
690
|
+
rounded to 2 dp (amounts/USD/rates/utilization); proxy/token are full checksum
|
|
691
|
+
strings. Null-ish fields are omitted (no tokenPrice/tvl when KuCoin lookup fails,
|
|
692
|
+
no myDeposit without a key or with zero balance)."""
|
|
693
|
+
cfg, raw, price = data["cfg"], data["raw"], data["price"]
|
|
694
|
+
d = cfg["decimals"]
|
|
695
|
+
ts_raw, tb_raw = raw.get("totalSupply"), raw.get("totalBorrowed")
|
|
696
|
+
dr_raw, br_raw = raw.get("getDepositRate"), raw.get("getBorrowingRate")
|
|
697
|
+
ts = (ts_raw or 0) / 10**d
|
|
698
|
+
tb = (tb_raw or 0) / 10**d
|
|
699
|
+
util = (tb / ts * 100) if ts > 0 else 0.0
|
|
700
|
+
out = {
|
|
701
|
+
"symbol": cfg["symbol"],
|
|
702
|
+
"proxy": cfg["proxy"],
|
|
703
|
+
"token": cfg["token"],
|
|
704
|
+
"decimals": d,
|
|
705
|
+
"totalSupply": _compact_num(ts),
|
|
706
|
+
"totalBorrowed": _compact_num(tb),
|
|
707
|
+
"utilization": _compact_num(util),
|
|
708
|
+
}
|
|
709
|
+
if dr_raw is not None:
|
|
710
|
+
out["depositRate"] = _compact_num(dr_raw / 1e18 * 100)
|
|
711
|
+
if br_raw is not None:
|
|
712
|
+
out["borrowingRate"] = _compact_num(br_raw / 1e18 * 100)
|
|
713
|
+
if price:
|
|
714
|
+
out["tokenPrice"] = _compact_num(price, places=4)
|
|
715
|
+
out["tvl"] = _compact_num(ts * price)
|
|
716
|
+
my_bal = raw.get("balanceOf")
|
|
717
|
+
if my_bal is not None and my_bal > 0:
|
|
718
|
+
out["myDeposit"] = _compact_num(my_bal / 10**d, places=6)
|
|
719
|
+
return out
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def cmd_pool_info(pool_name: str, as_json: bool = False):
|
|
723
|
+
"""Print pool supply / borrow / utilization / APR / TVL for one pool or all.
|
|
724
|
+
|
|
725
|
+
Human-facing output (default) is unchanged. With --json: emits a single JSON object
|
|
726
|
+
for a named pool, or a {pool_name: {...}} dict for `all`. JSON shape matches
|
|
727
|
+
deltaprime so an agent can consume both chains uniformly. Numbers are floats (no
|
|
728
|
+
decoration), `tokenPrice` / `tvl` are omitted when KuCoin lookup fails, `myDeposit`
|
|
729
|
+
is omitted when no key is configured or the balance is zero."""
|
|
599
730
|
if pool_name == "all":
|
|
731
|
+
if as_json:
|
|
732
|
+
out = {name: _pool_json_shape(_pool_info_data(name)) for name in POOLS}
|
|
733
|
+
print(json.dumps(out, indent=2))
|
|
734
|
+
return
|
|
600
735
|
for name in POOLS:
|
|
601
736
|
cmd_pool_info(name)
|
|
602
737
|
print()
|
|
603
738
|
return
|
|
604
739
|
|
|
605
|
-
|
|
740
|
+
data = _pool_info_data(pool_name)
|
|
741
|
+
if as_json:
|
|
742
|
+
print(json.dumps(_pool_json_shape(data), indent=2))
|
|
743
|
+
return
|
|
744
|
+
|
|
745
|
+
cfg, raw, price = data["cfg"], data["raw"], data["price"]
|
|
606
746
|
p = cfg["proxy"][:12]
|
|
607
747
|
d = cfg["decimals"]
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
tb = contract.functions.totalBorrowed().call()
|
|
748
|
+
ts = raw.get("totalSupply") or 0
|
|
749
|
+
tb = raw.get("totalBorrowed") or 0
|
|
611
750
|
print(f"=== {cfg['symbol']} Pool ({p}...) ===")
|
|
612
751
|
print(f" Total Supply: {ts / 10**d:>14,.2f} {cfg['symbol']}")
|
|
613
752
|
print(f" Total Borrowed: {tb / 10**d:>14,.2f} {cfg['symbol']}")
|
|
614
753
|
util = tb / ts * 100 if ts > 0 else 0
|
|
615
754
|
print(f" Utilization: {util:>14.2f}%")
|
|
616
755
|
# getDepositRate / getBorrowingRate are 1e18-scaled annualised rates.
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
756
|
+
dr_raw, br_raw = raw.get("getDepositRate"), raw.get("getBorrowingRate")
|
|
757
|
+
if dr_raw is not None and br_raw is not None:
|
|
758
|
+
dr = dr_raw / 1e18 * 100
|
|
759
|
+
br = br_raw / 1e18 * 100
|
|
620
760
|
print(f" Deposit APR: {dr:>14.2f}%")
|
|
621
761
|
print(f" Borrow APR: {br:>14.2f}%")
|
|
622
|
-
except Exception:
|
|
623
|
-
pass
|
|
624
|
-
# KuCoin doesn't trade cbBTC/cbDOGE/cbXRP directly - the cb-prefixed variants are
|
|
625
|
-
# Coinbase wrapped versions; fall back to the underlying ticker for the price probe.
|
|
626
|
-
price_sym = cfg["symbol"].replace("cb", "") if cfg["symbol"].startswith("cb") else cfg["symbol"]
|
|
627
|
-
price = token_price(price_sym)
|
|
628
762
|
if price:
|
|
629
763
|
print(f" Token Price: ${price:>13,.2f}")
|
|
630
764
|
print(f" TVL: ${ts / 10**d * price:>13,.2f}")
|
|
631
765
|
|
|
632
|
-
# Show the signer's pool deposit when a key is configured;
|
|
633
|
-
#
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
except RuntimeError:
|
|
637
|
-
return
|
|
638
|
-
my_bal = contract.functions.balanceOf(acct.address).call()
|
|
639
|
-
if my_bal > 0:
|
|
766
|
+
# Show the signer's pool deposit when a key is configured; the balanceOf leg is
|
|
767
|
+
# read in the same multicall batch above — we just print it here.
|
|
768
|
+
my_bal = raw.get("balanceOf")
|
|
769
|
+
if my_bal is not None and my_bal > 0:
|
|
640
770
|
print(f" My Deposit: {my_bal / 10**d:.4f} {cfg['symbol']}")
|
|
641
771
|
|
|
642
772
|
def cmd_my_positions():
|
|
643
773
|
acct = get_account()
|
|
644
774
|
w3 = get_w3()
|
|
775
|
+
# The Wallet: line MUST always print so the operator can verify the resolved
|
|
776
|
+
# signer address even when every other line is suppressed.
|
|
645
777
|
print(f"Wallet: {acct.address}")
|
|
646
778
|
|
|
647
|
-
# Wallet ETH (native Base asset, used for gas).
|
|
779
|
+
# Wallet ETH (native Base asset, used for gas). Suppress when below dust so a clean
|
|
780
|
+
# readout doesn't carry a noisy `ETH: 0.000000` line.
|
|
648
781
|
eth = w3.eth.get_balance(acct.address) / 1e18
|
|
649
|
-
|
|
650
|
-
|
|
782
|
+
if eth >= 1e-9:
|
|
783
|
+
print(f"ETH: {eth:.6f}")
|
|
784
|
+
|
|
785
|
+
# Batch every per-pool read into ONE Multicall3 eth_call: for each pool, the wallet
|
|
786
|
+
# ERC20 balanceOf + the pool balanceOf (the EOA's deposit) + the pool getBorrowed.
|
|
787
|
+
# Previously 3 RPCs per pool × 8 pools = 24 sequential round-trips; now 1 round-trip
|
|
788
|
+
# regardless of pool count.
|
|
789
|
+
legs = []
|
|
790
|
+
pool_meta = []
|
|
651
791
|
for name, cfg in POOLS.items():
|
|
792
|
+
contract, _, _ = get_pool_contract(name)
|
|
793
|
+
token_cs = Web3.to_checksum_address(cfg["token"])
|
|
794
|
+
token = w3.eth.contract(address=token_cs, abi=ERC20_ABI)
|
|
795
|
+
proxy_cs = Web3.to_checksum_address(cfg["proxy"])
|
|
796
|
+
legs.append((token_cs, bytes.fromhex(token.encode_abi("balanceOf", args=[acct.address])[2:])))
|
|
797
|
+
legs.append((proxy_cs, bytes.fromhex(contract.encode_abi("balanceOf", args=[acct.address])[2:])))
|
|
798
|
+
legs.append((proxy_cs, bytes.fromhex(contract.encode_abi("getBorrowed", args=[acct.address])[2:])))
|
|
799
|
+
pool_meta.append((name, cfg))
|
|
800
|
+
try:
|
|
801
|
+
results = multicall(w3, legs)
|
|
802
|
+
except Exception as e:
|
|
803
|
+
print(f" pool reads failed via multicall: {type(e).__name__}: {e}")
|
|
804
|
+
results = [(False, b"")] * len(legs)
|
|
805
|
+
for i, (name, cfg) in enumerate(pool_meta):
|
|
806
|
+
wallet_ok, wallet_rd = results[i * 3]
|
|
807
|
+
pool_ok, pool_rd = results[i * 3 + 1]
|
|
808
|
+
borrow_ok, borrow_rd = results[i * 3 + 2]
|
|
652
809
|
try:
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
bal = token.functions.balanceOf(acct.address).call()
|
|
657
|
-
if bal > 0:
|
|
658
|
-
print(f" Wallet {cfg['symbol']}: {bal / 10**cfg['decimals']:.4f}")
|
|
659
|
-
|
|
660
|
-
pool_bal = contract.functions.balanceOf(acct.address).call()
|
|
661
|
-
if pool_bal > 0:
|
|
662
|
-
print(f" Pool Deposit {cfg['symbol']}: {pool_bal / 10**cfg['decimals']:.4f}")
|
|
663
|
-
|
|
664
|
-
borrowed = contract.functions.getBorrowed(acct.address).call()
|
|
665
|
-
if borrowed > 0:
|
|
666
|
-
print(f" Borrowed {cfg['symbol']}: {borrowed / 10**cfg['decimals']:.4f}")
|
|
667
|
-
|
|
810
|
+
wallet_bal = w3.codec.decode(["uint256"], wallet_rd)[0] if wallet_ok and wallet_rd else 0
|
|
811
|
+
pool_bal = w3.codec.decode(["uint256"], pool_rd)[0] if pool_ok and pool_rd else 0
|
|
812
|
+
borrowed = w3.codec.decode(["uint256"], borrow_rd)[0] if borrow_ok and borrow_rd else 0
|
|
668
813
|
except Exception as e:
|
|
669
|
-
print(f" {name}: {e}")
|
|
814
|
+
print(f" {name}: decode failed ({type(e).__name__})")
|
|
815
|
+
continue
|
|
816
|
+
if not wallet_ok:
|
|
817
|
+
print(f" {name}: wallet balanceOf leg reverted in multicall")
|
|
818
|
+
if not pool_ok:
|
|
819
|
+
print(f" {name}: pool balanceOf leg reverted in multicall")
|
|
820
|
+
if not borrow_ok:
|
|
821
|
+
print(f" {name}: getBorrowed leg reverted in multicall")
|
|
822
|
+
if wallet_bal > 0:
|
|
823
|
+
print(f" Wallet {cfg['symbol']}: {wallet_bal / 10**cfg['decimals']:.4f}")
|
|
824
|
+
if pool_bal > 0:
|
|
825
|
+
print(f" Pool Deposit {cfg['symbol']}: {pool_bal / 10**cfg['decimals']:.4f}")
|
|
826
|
+
if borrowed > 0:
|
|
827
|
+
print(f" Borrowed {cfg['symbol']}: {borrowed / 10**cfg['decimals']:.4f}")
|
|
670
828
|
|
|
671
829
|
try:
|
|
672
830
|
pa = get_prime_account(w3, acct.address)
|
|
673
831
|
if pa:
|
|
674
832
|
print(f"\nDegen Account: {pa}")
|
|
675
833
|
pa_eth = w3.eth.get_balance(Web3.to_checksum_address(pa)) / 1e18
|
|
676
|
-
|
|
834
|
+
if pa_eth >= 1e-9:
|
|
835
|
+
print(f" ETH balance: {pa_eth:.6f}")
|
|
677
836
|
else:
|
|
678
837
|
print("\nNo Degen Account yet. Create with: degenprime create-account --execute")
|
|
679
838
|
except Exception as e:
|
|
@@ -921,7 +1080,12 @@ def cmd_summary():
|
|
|
921
1080
|
back to balances-only if the RedStone gateway is unreachable or a view reverts.
|
|
922
1081
|
Note: per-asset USD is best-effort - only symbols with a RedStone primary-prod
|
|
923
1082
|
feed are priced here. Symbols sourced on-chain from BaseOracle TWAP show as
|
|
924
|
-
balance-only (the SolvencyFacet still values them for the total/debt figures).
|
|
1083
|
+
balance-only (the SolvencyFacet still values them for the total/debt figures).
|
|
1084
|
+
|
|
1085
|
+
Multicall: stage A batches getAllOwnedAssets + getDebts (2 -> 1 RPC). Stage B
|
|
1086
|
+
batches one getBalance per owned asset (N -> 1 RPC). Stage C batches the four
|
|
1087
|
+
RedStone-gated solvency views + getPrices (4-5 -> 1 RPC), each leg carrying the
|
|
1088
|
+
same RedStone payload appended."""
|
|
925
1089
|
w3 = get_w3()
|
|
926
1090
|
acct = get_account()
|
|
927
1091
|
pa = get_prime_account(w3, acct.address)
|
|
@@ -935,13 +1099,27 @@ def cmd_summary():
|
|
|
935
1099
|
print(f" Native ETH (gas): {pa_eth:.6f}")
|
|
936
1100
|
|
|
937
1101
|
account = w3.eth.contract(address=Web3.to_checksum_address(pa), abi=PRIME_ACCOUNT_ABI)
|
|
938
|
-
|
|
1102
|
+
pa_cs = account.address
|
|
1103
|
+
stage_a_legs = [
|
|
1104
|
+
("getAllOwnedAssets", ["bytes32[]"], account.encode_abi("getAllOwnedAssets", args=[])),
|
|
1105
|
+
("getDebts", ["(bytes32,uint256)[]"], account.encode_abi("getDebts", args=[])),
|
|
1106
|
+
]
|
|
1107
|
+
a_results = multicall(w3, [(pa_cs, bytes.fromhex(d[2:])) for _, _, d in stage_a_legs])
|
|
1108
|
+
owned_raw = w3.codec.decode(["bytes32[]"], a_results[0][1])[0] if a_results[0][0] else []
|
|
1109
|
+
debts_raw = w3.codec.decode(["(bytes32,uint256)[]"], a_results[1][1])[0] if a_results[1][0] else []
|
|
1110
|
+
owned = [a.rstrip(b"\x00").decode(errors="replace") for a in owned_raw]
|
|
1111
|
+
if owned:
|
|
1112
|
+
bal_legs = [(pa_cs, bytes.fromhex(account.encode_abi("getBalance", args=[asset_b32(sym)])[2:]))
|
|
1113
|
+
for sym in owned]
|
|
1114
|
+
bal_results = multicall(w3, bal_legs)
|
|
1115
|
+
else:
|
|
1116
|
+
bal_results = []
|
|
939
1117
|
supplied = []
|
|
940
|
-
for sym in owned:
|
|
941
|
-
bal =
|
|
1118
|
+
for sym, (ok, rd) in zip(owned, bal_results):
|
|
1119
|
+
bal = w3.codec.decode(["uint256"], rd)[0] if ok and rd else 0
|
|
942
1120
|
supplied.append({"symbol": sym, "raw": bal, "decimals": _asset_decimals(w3, sym)})
|
|
943
1121
|
borrowed = []
|
|
944
|
-
for n, v in
|
|
1122
|
+
for n, v in debts_raw:
|
|
945
1123
|
sym = n.rstrip(b"\x00").decode(errors="replace")
|
|
946
1124
|
if v > 0:
|
|
947
1125
|
borrowed.append({"symbol": sym, "raw": v, "decimals": _asset_decimals(w3, sym)})
|
|
@@ -950,18 +1128,54 @@ def cmd_summary():
|
|
|
950
1128
|
# without signed price calldata appended. Fetch a fresh RedStone payload covering
|
|
951
1129
|
# every feed the solvency math touches (RedStone-feed symbols only - others come
|
|
952
1130
|
# from BaseOracle on-chain) and eth_call the views with it appended. No tx.
|
|
1131
|
+
# Each leg in the multicall carries the same payload — redundant on the wire,
|
|
1132
|
+
# but correct: the SolvencyFacet parses the payload from the calldata tail per leg.
|
|
953
1133
|
solvency = {"total": None, "debt": None, "ratio": None, "solvent": None, "error": None, "prices": {}}
|
|
954
1134
|
try:
|
|
955
1135
|
feeds = degen_account_price_feeds(account)
|
|
956
1136
|
payload = build_redstone_payload(feeds)
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1137
|
+
payload_hex = payload.hex()
|
|
1138
|
+
price_syms = [s for s in dict.fromkeys(r["symbol"] for r in supplied + borrowed)
|
|
1139
|
+
if s and s in REDSTONE_AVAILABLE_FEEDS]
|
|
1140
|
+
solv_legs = [
|
|
1141
|
+
("getTotalValue", ["uint256"], account.encode_abi("getTotalValue", args=[])),
|
|
1142
|
+
("getDebt", ["uint256"], account.encode_abi("getDebt", args=[])),
|
|
1143
|
+
("getHealthRatio", ["uint256"], account.encode_abi("getHealthRatio", args=[])),
|
|
1144
|
+
("isSolvent", ["bool"], account.encode_abi("isSolvent", args=[])),
|
|
1145
|
+
]
|
|
1146
|
+
if price_syms:
|
|
1147
|
+
solv_legs.append(("getPrices", ["uint256[]"],
|
|
1148
|
+
account.encode_abi("getPrices",
|
|
1149
|
+
args=[[asset_b32(s) for s in price_syms]])))
|
|
1150
|
+
solv_results = multicall(w3, [(pa_cs, bytes.fromhex(d[2:]) + bytes.fromhex(payload_hex))
|
|
1151
|
+
for _, _, d in solv_legs])
|
|
1152
|
+
decoded_solv = {}
|
|
1153
|
+
for (name, out_types, _d), (ok, rd) in zip(solv_legs, solv_results):
|
|
1154
|
+
if not ok or not rd:
|
|
1155
|
+
decoded_solv[name] = None
|
|
1156
|
+
continue
|
|
1157
|
+
try:
|
|
1158
|
+
decoded_solv[name] = w3.codec.decode(out_types, rd)[0]
|
|
1159
|
+
except Exception:
|
|
1160
|
+
decoded_solv[name] = None
|
|
1161
|
+
if decoded_solv.get("getTotalValue") is not None:
|
|
1162
|
+
solvency["total"] = decoded_solv["getTotalValue"] / 1e18
|
|
1163
|
+
if decoded_solv.get("getDebt") is not None:
|
|
1164
|
+
solvency["debt"] = decoded_solv["getDebt"] / 1e18
|
|
1165
|
+
if decoded_solv.get("getHealthRatio") is not None:
|
|
1166
|
+
ratio = decoded_solv["getHealthRatio"] / 1e18
|
|
1167
|
+
# With negligible debt the ratio is astronomically large (e.g. 1e59) - render
|
|
1168
|
+
# that as None and show ">1000" rather than a junk number.
|
|
1169
|
+
solvency["ratio"] = None if ratio > 1000 else ratio
|
|
1170
|
+
if decoded_solv.get("isSolvent") is not None:
|
|
1171
|
+
solvency["solvent"] = bool(decoded_solv["isSolvent"])
|
|
1172
|
+
prices = {}
|
|
1173
|
+
if price_syms and decoded_solv.get("getPrices") is not None:
|
|
1174
|
+
raw_prices = decoded_solv["getPrices"]
|
|
1175
|
+
for i, s in enumerate(price_syms):
|
|
1176
|
+
if i < len(raw_prices):
|
|
1177
|
+
prices[s] = raw_prices[i] / 1e8
|
|
1178
|
+
solvency["prices"] = prices
|
|
965
1179
|
except Exception as e:
|
|
966
1180
|
solvency["error"] = type(e).__name__
|
|
967
1181
|
|
|
@@ -1735,11 +1949,16 @@ def _dispatch():
|
|
|
1735
1949
|
|
|
1736
1950
|
cmd = args[0]
|
|
1737
1951
|
if cmd == "pool-info":
|
|
1738
|
-
pool
|
|
1952
|
+
# First positional after `pool-info` is the pool name; --json is an opt-in flag
|
|
1953
|
+
# that switches output from human tables to a compact JSON shape (one object for
|
|
1954
|
+
# a named pool, dict-of-objects for `all`).
|
|
1955
|
+
as_json = "--json" in args
|
|
1956
|
+
positional = [a for a in args[1:] if not a.startswith("--")]
|
|
1957
|
+
pool = positional[0] if positional else "all"
|
|
1739
1958
|
if pool != "all" and pool not in POOLS:
|
|
1740
1959
|
print(f"Unknown pool '{pool}'. Choose from: {', '.join(POOLS)}, all")
|
|
1741
1960
|
return
|
|
1742
|
-
cmd_pool_info(pool)
|
|
1961
|
+
cmd_pool_info(pool, as_json)
|
|
1743
1962
|
elif cmd == "my-positions":
|
|
1744
1963
|
cmd_my_positions()
|
|
1745
1964
|
elif cmd == "deposit":
|