dolomite-cli 1.0.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.
- dolomite_cli-1.0.0/.gitignore +57 -0
- dolomite_cli-1.0.0/LICENSE +21 -0
- dolomite_cli-1.0.0/PKG-INFO +127 -0
- dolomite_cli-1.0.0/README.md +106 -0
- dolomite_cli-1.0.0/dolomite_cli.py +891 -0
- dolomite_cli-1.0.0/pyproject.toml +30 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# === Secrets & credentials ===
|
|
2
|
+
.env
|
|
3
|
+
*.env
|
|
4
|
+
**/.env
|
|
5
|
+
**/config.json
|
|
6
|
+
!projects/pokemon-alerts/sku-database.json
|
|
7
|
+
secrets/
|
|
8
|
+
tokens/
|
|
9
|
+
|
|
10
|
+
# === OpenClaw config (contains API keys) ===
|
|
11
|
+
openclaw.json
|
|
12
|
+
|
|
13
|
+
# === Temp & cache ===
|
|
14
|
+
__pycache__/
|
|
15
|
+
*.pyc
|
|
16
|
+
*.pyo
|
|
17
|
+
.DS_Store
|
|
18
|
+
*.swp
|
|
19
|
+
*.swo
|
|
20
|
+
*~
|
|
21
|
+
.cache/
|
|
22
|
+
tmp/
|
|
23
|
+
*.tmp
|
|
24
|
+
|
|
25
|
+
# === Node modules ===
|
|
26
|
+
node_modules/
|
|
27
|
+
|
|
28
|
+
# === Large binary/media files ===
|
|
29
|
+
*.ogg
|
|
30
|
+
*.wav
|
|
31
|
+
*.mp3
|
|
32
|
+
*.mp4
|
|
33
|
+
*.jpg
|
|
34
|
+
*.jpeg
|
|
35
|
+
*.png
|
|
36
|
+
*.gif
|
|
37
|
+
*.zip
|
|
38
|
+
*.tar.gz
|
|
39
|
+
|
|
40
|
+
# === Runtime state (changes every poll cycle) ===
|
|
41
|
+
projects/pokemon-alerts/data/poller_state.json
|
|
42
|
+
projects/pokemon-alerts/data/server_comparison.json
|
|
43
|
+
dashboard/pokemon-scan-results.json
|
|
44
|
+
dashboard/pokemon-geocache.json
|
|
45
|
+
memory/heartbeat-state.json
|
|
46
|
+
memory/usage-tracking.json
|
|
47
|
+
|
|
48
|
+
# === Sensitive memory files ===
|
|
49
|
+
# MEMORY.md contains personal info - keep out of public repos
|
|
50
|
+
# But we want it in private repo for backup
|
|
51
|
+
# (repo is private, so this is fine)
|
|
52
|
+
|
|
53
|
+
# === Codecks data (contains tokens in practice) ===
|
|
54
|
+
codecks/*.json
|
|
55
|
+
llmrouter-app/
|
|
56
|
+
dg-scan/
|
|
57
|
+
bn_pokemon.json
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 OpenClaw
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dolomite-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: CLI tool for querying Dolomite protocol data across all chains
|
|
5
|
+
Project-URL: Homepage, https://github.com/openclaw/dolomite-cli
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: blockchain,cli,defi,dolomite,interest-rates
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# dolomite CLI
|
|
23
|
+
|
|
24
|
+
Query Dolomite protocol data across all chains. Returns structured JSON. No API keys required.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pipx install dolomite-cli
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or with pip:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install dolomite-cli
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Requires Python 3.10+. Zero dependencies (stdlib only).
|
|
39
|
+
|
|
40
|
+
### From source
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
git clone https://github.com/openclaw/dolomite-cli
|
|
44
|
+
cd dolomite-cli
|
|
45
|
+
pip install -e .
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Commands
|
|
49
|
+
|
|
50
|
+
| Command | Description |
|
|
51
|
+
|---------|-------------|
|
|
52
|
+
| `dolomite rates` | All markets with supply/borrow rates |
|
|
53
|
+
| `dolomite positions` | Top borrowing positions by size |
|
|
54
|
+
| `dolomite flows` | Recent large deposits/withdrawals |
|
|
55
|
+
| `dolomite liquidations` | Recent liquidation events |
|
|
56
|
+
| `dolomite tvl` | Protocol TVL summary per chain |
|
|
57
|
+
| `dolomite markets --token USDC` | Detailed info for a specific token |
|
|
58
|
+
| `dolomite account <address>` | Full position detail for an address |
|
|
59
|
+
| `dolomite risks` | High-risk positions and utilization alerts |
|
|
60
|
+
| `dolomite schema` | Show all commands, chains, entities, examples |
|
|
61
|
+
|
|
62
|
+
## Examples
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Stablecoin rates across all chains
|
|
66
|
+
dolomite rates --stables-only
|
|
67
|
+
|
|
68
|
+
# Top 20 positions on Ethereum
|
|
69
|
+
dolomite positions --chain ethereum --top 20
|
|
70
|
+
|
|
71
|
+
# Whale flows in the last 48 hours
|
|
72
|
+
dolomite flows --hours 48 --min-usd 100000
|
|
73
|
+
|
|
74
|
+
# Deposits only
|
|
75
|
+
dolomite flows --type deposit --hours 72
|
|
76
|
+
|
|
77
|
+
# Look up a specific whale
|
|
78
|
+
dolomite account 0x8be46b25d59616e594f0a9e20147fb14c1b989d9
|
|
79
|
+
|
|
80
|
+
# High-risk positions (>80% LTV, >$100K)
|
|
81
|
+
dolomite risks --min-ltv 80 --min-usd 100000
|
|
82
|
+
|
|
83
|
+
# Berachain rates only
|
|
84
|
+
dolomite rates --chain berachain
|
|
85
|
+
|
|
86
|
+
# USDC across all chains
|
|
87
|
+
dolomite markets --token USDC
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Output
|
|
91
|
+
|
|
92
|
+
All commands output JSON to stdout. Errors go to stderr. Pipe to `jq` for filtering:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Top 5 stablecoin rates
|
|
96
|
+
dolomite rates --stables-only | jq '.markets[:5][] | {chain, symbol, supply_rate_pct}'
|
|
97
|
+
|
|
98
|
+
# Total protocol TVL
|
|
99
|
+
dolomite tvl | jq '.total_supply_usd'
|
|
100
|
+
|
|
101
|
+
# Addresses with >85% LTV
|
|
102
|
+
dolomite risks --min-ltv 85 | jq '.high_ltv_positions[] | {address, ltv_pct, supply_usd}'
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Chains
|
|
106
|
+
|
|
107
|
+
| Chain | Chain ID | Status |
|
|
108
|
+
|-------|----------|--------|
|
|
109
|
+
| Ethereum | 1 | Active (~$370M TVL) |
|
|
110
|
+
| Arbitrum | 42161 | Active (~$52M TVL) |
|
|
111
|
+
| Berachain | 80094 | Active (~$50M TVL) |
|
|
112
|
+
| Mantle | 5000 | Minimal |
|
|
113
|
+
| Base | 8453 | Minimal |
|
|
114
|
+
| X Layer | 196 | Minimal |
|
|
115
|
+
|
|
116
|
+
Default: queries Ethereum, Arbitrum, Berachain. Use `--chain all` for everything.
|
|
117
|
+
|
|
118
|
+
## Data Sources
|
|
119
|
+
|
|
120
|
+
- **Subgraph API**: `subgraph.api.dolomite.io` — on-chain positions, flows, liquidations
|
|
121
|
+
- **REST API**: `api.dolomite.io` — token prices, interest rates, market data
|
|
122
|
+
|
|
123
|
+
No authentication required. All data is public.
|
|
124
|
+
|
|
125
|
+
## For AI Agents
|
|
126
|
+
|
|
127
|
+
Run `dolomite schema` to get a complete description of all commands, available entities, and example queries. The output is JSON — parseable by any model.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# dolomite CLI
|
|
2
|
+
|
|
3
|
+
Query Dolomite protocol data across all chains. Returns structured JSON. No API keys required.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pipx install dolomite-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or with pip:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install dolomite-cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires Python 3.10+. Zero dependencies (stdlib only).
|
|
18
|
+
|
|
19
|
+
### From source
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
git clone https://github.com/openclaw/dolomite-cli
|
|
23
|
+
cd dolomite-cli
|
|
24
|
+
pip install -e .
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Commands
|
|
28
|
+
|
|
29
|
+
| Command | Description |
|
|
30
|
+
|---------|-------------|
|
|
31
|
+
| `dolomite rates` | All markets with supply/borrow rates |
|
|
32
|
+
| `dolomite positions` | Top borrowing positions by size |
|
|
33
|
+
| `dolomite flows` | Recent large deposits/withdrawals |
|
|
34
|
+
| `dolomite liquidations` | Recent liquidation events |
|
|
35
|
+
| `dolomite tvl` | Protocol TVL summary per chain |
|
|
36
|
+
| `dolomite markets --token USDC` | Detailed info for a specific token |
|
|
37
|
+
| `dolomite account <address>` | Full position detail for an address |
|
|
38
|
+
| `dolomite risks` | High-risk positions and utilization alerts |
|
|
39
|
+
| `dolomite schema` | Show all commands, chains, entities, examples |
|
|
40
|
+
|
|
41
|
+
## Examples
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Stablecoin rates across all chains
|
|
45
|
+
dolomite rates --stables-only
|
|
46
|
+
|
|
47
|
+
# Top 20 positions on Ethereum
|
|
48
|
+
dolomite positions --chain ethereum --top 20
|
|
49
|
+
|
|
50
|
+
# Whale flows in the last 48 hours
|
|
51
|
+
dolomite flows --hours 48 --min-usd 100000
|
|
52
|
+
|
|
53
|
+
# Deposits only
|
|
54
|
+
dolomite flows --type deposit --hours 72
|
|
55
|
+
|
|
56
|
+
# Look up a specific whale
|
|
57
|
+
dolomite account 0x8be46b25d59616e594f0a9e20147fb14c1b989d9
|
|
58
|
+
|
|
59
|
+
# High-risk positions (>80% LTV, >$100K)
|
|
60
|
+
dolomite risks --min-ltv 80 --min-usd 100000
|
|
61
|
+
|
|
62
|
+
# Berachain rates only
|
|
63
|
+
dolomite rates --chain berachain
|
|
64
|
+
|
|
65
|
+
# USDC across all chains
|
|
66
|
+
dolomite markets --token USDC
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Output
|
|
70
|
+
|
|
71
|
+
All commands output JSON to stdout. Errors go to stderr. Pipe to `jq` for filtering:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# Top 5 stablecoin rates
|
|
75
|
+
dolomite rates --stables-only | jq '.markets[:5][] | {chain, symbol, supply_rate_pct}'
|
|
76
|
+
|
|
77
|
+
# Total protocol TVL
|
|
78
|
+
dolomite tvl | jq '.total_supply_usd'
|
|
79
|
+
|
|
80
|
+
# Addresses with >85% LTV
|
|
81
|
+
dolomite risks --min-ltv 85 | jq '.high_ltv_positions[] | {address, ltv_pct, supply_usd}'
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Chains
|
|
85
|
+
|
|
86
|
+
| Chain | Chain ID | Status |
|
|
87
|
+
|-------|----------|--------|
|
|
88
|
+
| Ethereum | 1 | Active (~$370M TVL) |
|
|
89
|
+
| Arbitrum | 42161 | Active (~$52M TVL) |
|
|
90
|
+
| Berachain | 80094 | Active (~$50M TVL) |
|
|
91
|
+
| Mantle | 5000 | Minimal |
|
|
92
|
+
| Base | 8453 | Minimal |
|
|
93
|
+
| X Layer | 196 | Minimal |
|
|
94
|
+
|
|
95
|
+
Default: queries Ethereum, Arbitrum, Berachain. Use `--chain all` for everything.
|
|
96
|
+
|
|
97
|
+
## Data Sources
|
|
98
|
+
|
|
99
|
+
- **Subgraph API**: `subgraph.api.dolomite.io` — on-chain positions, flows, liquidations
|
|
100
|
+
- **REST API**: `api.dolomite.io` — token prices, interest rates, market data
|
|
101
|
+
|
|
102
|
+
No authentication required. All data is public.
|
|
103
|
+
|
|
104
|
+
## For AI Agents
|
|
105
|
+
|
|
106
|
+
Run `dolomite schema` to get a complete description of all commands, available entities, and example queries. The output is JSON — parseable by any model.
|
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
dolomite — CLI tool for querying Dolomite protocol data across all chains.
|
|
4
|
+
|
|
5
|
+
Provides structured JSON output of rates, positions, flows, liquidations,
|
|
6
|
+
TVL, market details, and account portfolios from Dolomite's subgraphs and APIs.
|
|
7
|
+
|
|
8
|
+
No API keys required. All data is public on-chain.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
dolomite rates [--chain CHAIN] [--stables-only] [--min-tvl N]
|
|
12
|
+
dolomite positions [--chain CHAIN] [--top N] [--min-usd N]
|
|
13
|
+
dolomite flows [--chain CHAIN] [--hours N] [--min-usd N] [--type deposit|withdrawal]
|
|
14
|
+
dolomite liquidations [--chain CHAIN] [--hours N] [--limit N]
|
|
15
|
+
dolomite tvl
|
|
16
|
+
dolomite markets [--token SYMBOL] [--chain CHAIN]
|
|
17
|
+
dolomite account <address> [--chain CHAIN]
|
|
18
|
+
dolomite risks [--min-ltv N] [--min-usd N]
|
|
19
|
+
dolomite schema
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
import sys
|
|
25
|
+
import time
|
|
26
|
+
from datetime import datetime, timezone
|
|
27
|
+
from urllib.request import Request, urlopen
|
|
28
|
+
from urllib.error import HTTPError, URLError
|
|
29
|
+
|
|
30
|
+
__version__ = "1.0.0"
|
|
31
|
+
|
|
32
|
+
# ─── Constants ────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
SUBGRAPH_BASE = "https://subgraph.api.dolomite.io/api/public/1301d2d1-7a9d-4be4-9e9a-061cb8611549/subgraphs"
|
|
35
|
+
API_BASE = "https://api.dolomite.io"
|
|
36
|
+
|
|
37
|
+
CHAINS = {
|
|
38
|
+
"ethereum": {"name": "Ethereum", "subgraph": "dolomite-ethereum", "chain_id": 1},
|
|
39
|
+
"arbitrum": {"name": "Arbitrum", "subgraph": "dolomite-arbitrum", "chain_id": 42161},
|
|
40
|
+
"berachain": {"name": "Berachain", "subgraph": "dolomite-berachain-mainnet", "chain_id": 80094},
|
|
41
|
+
"mantle": {"name": "Mantle", "subgraph": "dolomite-mantle", "chain_id": 5000},
|
|
42
|
+
"base": {"name": "Base", "subgraph": "dolomite-base", "chain_id": 8453},
|
|
43
|
+
"xlayer": {"name": "X Layer", "subgraph": "dolomite-x-layer", "chain_id": 196},
|
|
44
|
+
"botanix": {"name": "Botanix", "subgraph": "dolomite-botanix", "chain_id": 3637},
|
|
45
|
+
"polygonzkevm": {"name": "Polygon zkEVM", "subgraph": "dolomite-polygon-zk-evm", "chain_id": 1101},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
ACTIVE_CHAINS = ["ethereum", "arbitrum", "berachain", "mantle", "botanix"] # all chains except X Layer and Polygon zkEVM (older subgraph schema)
|
|
49
|
+
|
|
50
|
+
STABLECOINS = {
|
|
51
|
+
"USDC", "USDC.e", "USDT", "DAI", "USD1", "HONEY", "BYUSD",
|
|
52
|
+
"savUSD", "sDAI", "LUSD", "USD₮0", "rUSD", "MIM", "FRAX"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
def _http_json(url, payload=None, timeout=20):
|
|
58
|
+
"""Make an HTTP request and return parsed JSON."""
|
|
59
|
+
if payload:
|
|
60
|
+
data = json.dumps(payload).encode("utf-8")
|
|
61
|
+
req = Request(url, data=data, headers={"Content-Type": "application/json"})
|
|
62
|
+
else:
|
|
63
|
+
req = Request(url)
|
|
64
|
+
try:
|
|
65
|
+
with urlopen(req, timeout=timeout) as resp:
|
|
66
|
+
return json.loads(resp.read())
|
|
67
|
+
except (HTTPError, URLError, TimeoutError) as e:
|
|
68
|
+
_err(f"HTTP error: {e}")
|
|
69
|
+
return {}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _gql(subgraph_name, query, variables=None, timeout=20):
|
|
73
|
+
"""Execute a GraphQL query against a Dolomite subgraph."""
|
|
74
|
+
url = f"{SUBGRAPH_BASE}/{subgraph_name}/latest/gn"
|
|
75
|
+
payload = {"query": query}
|
|
76
|
+
if variables:
|
|
77
|
+
payload["variables"] = variables
|
|
78
|
+
result = _http_json(url, payload, timeout)
|
|
79
|
+
if "errors" in result:
|
|
80
|
+
_err(f"GraphQL errors: {result['errors']}")
|
|
81
|
+
return result.get("data", {})
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _api(path, timeout=15):
|
|
85
|
+
"""Fetch from Dolomite REST API."""
|
|
86
|
+
return _http_json(f"{API_BASE}{path}", timeout=timeout)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _out(data):
|
|
90
|
+
"""Print JSON output to stdout."""
|
|
91
|
+
json.dump(data, sys.stdout, indent=2)
|
|
92
|
+
sys.stdout.write("\n")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _err(msg):
|
|
96
|
+
"""Print error to stderr."""
|
|
97
|
+
print(f"error: {msg}", file=sys.stderr)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _resolve_chains(chain_arg):
|
|
101
|
+
"""Resolve --chain argument to list of chain keys."""
|
|
102
|
+
if chain_arg:
|
|
103
|
+
key = chain_arg.lower()
|
|
104
|
+
if key == "all":
|
|
105
|
+
return list(CHAINS.keys())
|
|
106
|
+
if key not in CHAINS:
|
|
107
|
+
_err(f"Unknown chain '{key}'. Available: {', '.join(CHAINS.keys())}")
|
|
108
|
+
sys.exit(1)
|
|
109
|
+
return [key]
|
|
110
|
+
return ACTIVE_CHAINS
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _ts_hours_ago(hours):
|
|
114
|
+
"""Return unix timestamp N hours ago."""
|
|
115
|
+
return int(time.time()) - (hours * 3600)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ─── Commands ─────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
def cmd_rates(args):
|
|
121
|
+
"""Fetch all markets with supply/borrow rates across chains."""
|
|
122
|
+
chains = _resolve_chains(args.chain)
|
|
123
|
+
min_tvl = args.min_tvl or 1000
|
|
124
|
+
markets = []
|
|
125
|
+
|
|
126
|
+
for chain_key in chains:
|
|
127
|
+
ci = CHAINS[chain_key]
|
|
128
|
+
try:
|
|
129
|
+
tokens_resp = _api(f"/tokens/{ci['chain_id']}")
|
|
130
|
+
rates_resp = _api(f"/tokens/{ci['chain_id']}/interest-rates")
|
|
131
|
+
prices_resp = _api(f"/tokens/{ci['chain_id']}/prices")
|
|
132
|
+
|
|
133
|
+
tokens = {t["id"]: t for t in tokens_resp.get("tokens", [])}
|
|
134
|
+
rates = {r["token"]["tokenAddress"]: r for r in rates_resp.get("interestRates", [])}
|
|
135
|
+
prices = prices_resp.get("prices", {})
|
|
136
|
+
|
|
137
|
+
for addr, token in tokens.items():
|
|
138
|
+
rate = rates.get(addr, {})
|
|
139
|
+
price = float(prices.get(addr, 0))
|
|
140
|
+
supply = float(token.get("supplyLiquidity", 0))
|
|
141
|
+
borrow = float(token.get("borrowLiquidity", 0))
|
|
142
|
+
supply_rate = float(rate.get("totalSupplyInterestRate", 0)) * 100
|
|
143
|
+
borrow_rate = float(rate.get("borrowInterestRate", 0)) * 100
|
|
144
|
+
util = (borrow / supply * 100) if supply > 0 else 0
|
|
145
|
+
supply_usd = supply * price
|
|
146
|
+
borrow_usd = borrow * price
|
|
147
|
+
is_stable = token["symbol"] in STABLECOINS
|
|
148
|
+
|
|
149
|
+
if supply_usd < min_tvl:
|
|
150
|
+
continue
|
|
151
|
+
if args.stables_only and not is_stable:
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
markets.append({
|
|
155
|
+
"chain": ci["name"],
|
|
156
|
+
"symbol": token["symbol"],
|
|
157
|
+
"supply_rate_pct": round(supply_rate, 3),
|
|
158
|
+
"borrow_rate_pct": round(borrow_rate, 3),
|
|
159
|
+
"utilization_pct": round(util, 1),
|
|
160
|
+
"supply_usd": round(supply_usd),
|
|
161
|
+
"borrow_usd": round(borrow_usd),
|
|
162
|
+
"price_usd": round(price, 4),
|
|
163
|
+
"is_stablecoin": is_stable,
|
|
164
|
+
})
|
|
165
|
+
except Exception as e:
|
|
166
|
+
_err(f"Failed to fetch {ci['name']} rates: {e}")
|
|
167
|
+
|
|
168
|
+
markets.sort(key=lambda x: -x["supply_usd"])
|
|
169
|
+
_out({"timestamp": datetime.now(timezone.utc).isoformat(), "count": len(markets), "markets": markets})
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def cmd_positions(args):
|
|
173
|
+
"""Fetch top borrowing positions by size."""
|
|
174
|
+
chains = _resolve_chains(args.chain)
|
|
175
|
+
top_n = args.top or 50
|
|
176
|
+
min_usd = args.min_usd or 10000
|
|
177
|
+
positions = []
|
|
178
|
+
|
|
179
|
+
for chain_key in chains:
|
|
180
|
+
ci = CHAINS[chain_key]
|
|
181
|
+
try:
|
|
182
|
+
# Fetch in batches — subgraph max is 1000
|
|
183
|
+
limit = min(top_n * 2, 1000) # fetch extra, filter later
|
|
184
|
+
d = _gql(ci["subgraph"], f"""{{
|
|
185
|
+
marginAccounts(
|
|
186
|
+
where: {{hasBorrowValue: true}}
|
|
187
|
+
orderBy: lastUpdatedTimestamp
|
|
188
|
+
orderDirection: desc
|
|
189
|
+
first: {limit}
|
|
190
|
+
) {{
|
|
191
|
+
id
|
|
192
|
+
effectiveUser {{ id }}
|
|
193
|
+
accountNumber
|
|
194
|
+
lastUpdatedTimestamp
|
|
195
|
+
tokenValues(first: 30) {{
|
|
196
|
+
token {{ symbol id }}
|
|
197
|
+
valuePar
|
|
198
|
+
}}
|
|
199
|
+
borrowTokens {{ symbol }}
|
|
200
|
+
supplyTokens {{ symbol }}
|
|
201
|
+
}}
|
|
202
|
+
}}""", timeout=30)
|
|
203
|
+
|
|
204
|
+
prices = _api(f"/tokens/{ci['chain_id']}/prices").get("prices", {})
|
|
205
|
+
|
|
206
|
+
for acct in d.get("marginAccounts", []):
|
|
207
|
+
supply_usd = 0
|
|
208
|
+
borrow_usd = 0
|
|
209
|
+
position_detail = []
|
|
210
|
+
|
|
211
|
+
for tv in acct["tokenValues"]:
|
|
212
|
+
val = float(tv["valuePar"])
|
|
213
|
+
price = float(prices.get(tv["token"]["id"], 0))
|
|
214
|
+
usd = val * price
|
|
215
|
+
if val > 0:
|
|
216
|
+
supply_usd += usd
|
|
217
|
+
else:
|
|
218
|
+
borrow_usd += abs(usd)
|
|
219
|
+
if abs(usd) > 100:
|
|
220
|
+
position_detail.append({
|
|
221
|
+
"token": tv["token"]["symbol"],
|
|
222
|
+
"amount": round(val, 4),
|
|
223
|
+
"usd": round(usd, 2),
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
if supply_usd < min_usd and borrow_usd < min_usd:
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
ltv = (borrow_usd / supply_usd * 100) if supply_usd > 0 else 0
|
|
230
|
+
positions.append({
|
|
231
|
+
"chain": ci["name"],
|
|
232
|
+
"address": acct["effectiveUser"]["id"],
|
|
233
|
+
"account_number": acct["accountNumber"],
|
|
234
|
+
"supply_usd": round(supply_usd),
|
|
235
|
+
"borrow_usd": round(borrow_usd),
|
|
236
|
+
"net_usd": round(supply_usd - borrow_usd),
|
|
237
|
+
"ltv_pct": round(ltv, 1),
|
|
238
|
+
"supply_tokens": [t["symbol"] for t in acct["supplyTokens"]],
|
|
239
|
+
"borrow_tokens": [t["symbol"] for t in acct["borrowTokens"]],
|
|
240
|
+
"positions": position_detail,
|
|
241
|
+
"last_active": int(acct["lastUpdatedTimestamp"]),
|
|
242
|
+
})
|
|
243
|
+
except Exception as e:
|
|
244
|
+
_err(f"Failed to fetch {ci['name']} positions: {e}")
|
|
245
|
+
|
|
246
|
+
positions.sort(key=lambda x: -x["supply_usd"])
|
|
247
|
+
positions = positions[:top_n]
|
|
248
|
+
_out({"timestamp": datetime.now(timezone.utc).isoformat(), "count": len(positions), "positions": positions})
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def cmd_flows(args):
|
|
252
|
+
"""Fetch recent large deposits and withdrawals."""
|
|
253
|
+
chains = _resolve_chains(args.chain)
|
|
254
|
+
hours = args.hours or 24
|
|
255
|
+
min_usd = args.min_usd or 50000
|
|
256
|
+
flow_type = args.type # deposit, withdrawal, or None (both)
|
|
257
|
+
ts_floor = _ts_hours_ago(hours)
|
|
258
|
+
flows = []
|
|
259
|
+
|
|
260
|
+
for chain_key in chains:
|
|
261
|
+
ci = CHAINS[chain_key]
|
|
262
|
+
try:
|
|
263
|
+
queries = []
|
|
264
|
+
if flow_type != "withdrawal":
|
|
265
|
+
queries.append(("deposit", f"""{{
|
|
266
|
+
deposits(
|
|
267
|
+
orderBy: serialId, orderDirection: desc, first: 100,
|
|
268
|
+
where: {{amountUSDDeltaWei_gte: "{min_usd}"}}
|
|
269
|
+
) {{
|
|
270
|
+
transaction {{ timestamp blockNumber }}
|
|
271
|
+
marginAccount {{ effectiveUser {{ id }} }}
|
|
272
|
+
token {{ symbol }}
|
|
273
|
+
amountDeltaWei
|
|
274
|
+
amountUSDDeltaWei
|
|
275
|
+
}}
|
|
276
|
+
}}"""))
|
|
277
|
+
if flow_type != "deposit":
|
|
278
|
+
queries.append(("withdrawal", f"""{{
|
|
279
|
+
withdrawals(
|
|
280
|
+
orderBy: serialId, orderDirection: desc, first: 100,
|
|
281
|
+
where: {{amountUSDDeltaWei_gte: "{min_usd}"}}
|
|
282
|
+
) {{
|
|
283
|
+
transaction {{ timestamp blockNumber }}
|
|
284
|
+
marginAccount {{ effectiveUser {{ id }} }}
|
|
285
|
+
token {{ symbol }}
|
|
286
|
+
amountDeltaWei
|
|
287
|
+
amountUSDDeltaWei
|
|
288
|
+
}}
|
|
289
|
+
}}"""))
|
|
290
|
+
|
|
291
|
+
for ftype, query in queries:
|
|
292
|
+
d = _gql(ci["subgraph"], query, timeout=20)
|
|
293
|
+
entity_key = "deposits" if ftype == "deposit" else "withdrawals"
|
|
294
|
+
for item in d.get(entity_key, []):
|
|
295
|
+
ts = int(item["transaction"]["timestamp"])
|
|
296
|
+
if ts < ts_floor:
|
|
297
|
+
continue
|
|
298
|
+
flows.append({
|
|
299
|
+
"type": ftype,
|
|
300
|
+
"chain": ci["name"],
|
|
301
|
+
"address": item["marginAccount"]["effectiveUser"]["id"],
|
|
302
|
+
"token": item["token"]["symbol"],
|
|
303
|
+
"amount": round(float(item["amountDeltaWei"]), 4),
|
|
304
|
+
"usd": round(float(item["amountUSDDeltaWei"])),
|
|
305
|
+
"timestamp": ts,
|
|
306
|
+
"block": int(item["transaction"]["blockNumber"]),
|
|
307
|
+
"time": datetime.fromtimestamp(ts, tz=timezone.utc).isoformat(),
|
|
308
|
+
})
|
|
309
|
+
except Exception as e:
|
|
310
|
+
_err(f"Failed to fetch {ci['name']} flows: {e}")
|
|
311
|
+
|
|
312
|
+
flows.sort(key=lambda x: -x["timestamp"])
|
|
313
|
+
_out({
|
|
314
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
315
|
+
"period_hours": hours,
|
|
316
|
+
"min_usd": min_usd,
|
|
317
|
+
"count": len(flows),
|
|
318
|
+
"flows": flows,
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def cmd_liquidations(args):
|
|
323
|
+
"""Fetch recent liquidation events."""
|
|
324
|
+
chains = _resolve_chains(args.chain)
|
|
325
|
+
hours = args.hours or 168 # default 7 days
|
|
326
|
+
limit = args.limit or 100
|
|
327
|
+
ts_floor = _ts_hours_ago(hours)
|
|
328
|
+
liquidations = []
|
|
329
|
+
|
|
330
|
+
for chain_key in chains:
|
|
331
|
+
ci = CHAINS[chain_key]
|
|
332
|
+
try:
|
|
333
|
+
d = _gql(ci["subgraph"], f"""{{
|
|
334
|
+
liquidations(
|
|
335
|
+
orderBy: serialId, orderDirection: desc, first: {min(limit, 200)}
|
|
336
|
+
) {{
|
|
337
|
+
transaction {{ timestamp blockNumber }}
|
|
338
|
+
solidMarginAccount {{ effectiveUser {{ id }} }}
|
|
339
|
+
liquidMarginAccount {{ effectiveUser {{ id }} }}
|
|
340
|
+
heldToken {{ symbol }}
|
|
341
|
+
borrowedToken {{ symbol }}
|
|
342
|
+
heldTokenAmountDeltaWei
|
|
343
|
+
borrowedTokenAmountDeltaWei
|
|
344
|
+
heldTokenAmountUSD
|
|
345
|
+
borrowedTokenAmountUSD
|
|
346
|
+
heldTokenLiquidationRewardWei
|
|
347
|
+
heldTokenLiquidationRewardUSD
|
|
348
|
+
}}
|
|
349
|
+
}}""", timeout=20)
|
|
350
|
+
|
|
351
|
+
prices = _api(f"/tokens/{ci['chain_id']}/prices").get("prices", {})
|
|
352
|
+
|
|
353
|
+
for liq in d.get("liquidations", []):
|
|
354
|
+
ts = int(liq["transaction"]["timestamp"])
|
|
355
|
+
if ts < ts_floor:
|
|
356
|
+
continue
|
|
357
|
+
liquidations.append({
|
|
358
|
+
"chain": ci["name"],
|
|
359
|
+
"liquidated_address": liq["liquidMarginAccount"]["effectiveUser"]["id"],
|
|
360
|
+
"liquidator_address": liq["solidMarginAccount"]["effectiveUser"]["id"],
|
|
361
|
+
"held_token": liq["heldToken"]["symbol"],
|
|
362
|
+
"borrowed_token": liq["borrowedToken"]["symbol"],
|
|
363
|
+
"held_usd": round(float(liq.get("heldTokenAmountUSD", 0)), 2),
|
|
364
|
+
"borrowed_usd": round(float(liq.get("borrowedTokenAmountUSD", 0)), 2),
|
|
365
|
+
"reward_usd": round(float(liq.get("heldTokenLiquidationRewardUSD", 0)), 2),
|
|
366
|
+
"timestamp": ts,
|
|
367
|
+
"block": int(liq["transaction"]["blockNumber"]),
|
|
368
|
+
"time": datetime.fromtimestamp(ts, tz=timezone.utc).isoformat(),
|
|
369
|
+
})
|
|
370
|
+
except Exception as e:
|
|
371
|
+
_err(f"Failed to fetch {ci['name']} liquidations: {e}")
|
|
372
|
+
|
|
373
|
+
liquidations.sort(key=lambda x: -x["timestamp"])
|
|
374
|
+
_out({
|
|
375
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
376
|
+
"period_hours": hours,
|
|
377
|
+
"count": len(liquidations),
|
|
378
|
+
"liquidations": liquidations,
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def cmd_tvl(args):
|
|
383
|
+
"""Fetch protocol-level TVL summary per chain."""
|
|
384
|
+
chains = _resolve_chains(getattr(args, 'chain', None))
|
|
385
|
+
summary = {}
|
|
386
|
+
|
|
387
|
+
for chain_key in chains:
|
|
388
|
+
ci = CHAINS[chain_key]
|
|
389
|
+
try:
|
|
390
|
+
d = _gql(ci["subgraph"], """{
|
|
391
|
+
dolomiteMargins(first: 1) {
|
|
392
|
+
supplyLiquidityUSD borrowLiquidityUSD userCount
|
|
393
|
+
totalTradeVolumeUSD totalLiquidationVolumeUSD
|
|
394
|
+
liquidationCount borrowPositionCount numberOfMarkets
|
|
395
|
+
}
|
|
396
|
+
}""")
|
|
397
|
+
margin = d["dolomiteMargins"][0]
|
|
398
|
+
supply = float(margin["supplyLiquidityUSD"])
|
|
399
|
+
borrow = float(margin["borrowLiquidityUSD"])
|
|
400
|
+
summary[ci["name"]] = {
|
|
401
|
+
"supply_usd": round(supply),
|
|
402
|
+
"borrow_usd": round(borrow),
|
|
403
|
+
"utilization_pct": round(borrow / supply * 100, 1) if supply > 0 else 0,
|
|
404
|
+
"users": int(margin["userCount"]),
|
|
405
|
+
"markets": margin["numberOfMarkets"],
|
|
406
|
+
"borrow_positions": int(margin["borrowPositionCount"]),
|
|
407
|
+
"total_trade_volume_usd": round(float(margin["totalTradeVolumeUSD"])),
|
|
408
|
+
"total_liquidation_volume_usd": round(float(margin["totalLiquidationVolumeUSD"])),
|
|
409
|
+
"liquidation_count": int(margin["liquidationCount"]),
|
|
410
|
+
}
|
|
411
|
+
except Exception as e:
|
|
412
|
+
_err(f"Failed to fetch {ci['name']} TVL: {e}")
|
|
413
|
+
|
|
414
|
+
total_supply = sum(v["supply_usd"] for v in summary.values())
|
|
415
|
+
total_borrow = sum(v["borrow_usd"] for v in summary.values())
|
|
416
|
+
total_users = sum(v["users"] for v in summary.values())
|
|
417
|
+
|
|
418
|
+
_out({
|
|
419
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
420
|
+
"total_supply_usd": total_supply,
|
|
421
|
+
"total_borrow_usd": total_borrow,
|
|
422
|
+
"total_utilization_pct": round(total_borrow / total_supply * 100, 1) if total_supply > 0 else 0,
|
|
423
|
+
"total_users": total_users,
|
|
424
|
+
"chains": summary,
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def cmd_markets(args):
|
|
429
|
+
"""Fetch detailed info for a specific token across chains."""
|
|
430
|
+
chains = _resolve_chains(args.chain)
|
|
431
|
+
target = args.token.upper() if args.token else None
|
|
432
|
+
results = []
|
|
433
|
+
|
|
434
|
+
for chain_key in chains:
|
|
435
|
+
ci = CHAINS[chain_key]
|
|
436
|
+
try:
|
|
437
|
+
tokens_resp = _api(f"/tokens/{ci['chain_id']}")
|
|
438
|
+
rates_resp = _api(f"/tokens/{ci['chain_id']}/interest-rates")
|
|
439
|
+
prices_resp = _api(f"/tokens/{ci['chain_id']}/prices")
|
|
440
|
+
|
|
441
|
+
tokens = {t["id"]: t for t in tokens_resp.get("tokens", [])}
|
|
442
|
+
rates = {r["token"]["tokenAddress"]: r for r in rates_resp.get("interestRates", [])}
|
|
443
|
+
prices = prices_resp.get("prices", {})
|
|
444
|
+
|
|
445
|
+
for addr, token in tokens.items():
|
|
446
|
+
if target and token["symbol"].upper() != target:
|
|
447
|
+
continue
|
|
448
|
+
rate = rates.get(addr, {})
|
|
449
|
+
price = float(prices.get(addr, 0))
|
|
450
|
+
supply = float(token.get("supplyLiquidity", 0))
|
|
451
|
+
borrow = float(token.get("borrowLiquidity", 0))
|
|
452
|
+
supply_rate = float(rate.get("totalSupplyInterestRate", 0)) * 100
|
|
453
|
+
borrow_rate = float(rate.get("borrowInterestRate", 0)) * 100
|
|
454
|
+
util = (borrow / supply * 100) if supply > 0 else 0
|
|
455
|
+
|
|
456
|
+
results.append({
|
|
457
|
+
"chain": ci["name"],
|
|
458
|
+
"symbol": token["symbol"],
|
|
459
|
+
"address": addr,
|
|
460
|
+
"price_usd": round(price, 6),
|
|
461
|
+
"supply_rate_pct": round(supply_rate, 3),
|
|
462
|
+
"borrow_rate_pct": round(borrow_rate, 3),
|
|
463
|
+
"utilization_pct": round(util, 1),
|
|
464
|
+
"supply_amount": round(supply, 4),
|
|
465
|
+
"supply_usd": round(supply * price),
|
|
466
|
+
"borrow_amount": round(borrow, 4),
|
|
467
|
+
"borrow_usd": round(borrow * price),
|
|
468
|
+
"available_amount": round(supply - borrow, 4),
|
|
469
|
+
"available_usd": round((supply - borrow) * price),
|
|
470
|
+
})
|
|
471
|
+
except Exception as e:
|
|
472
|
+
_err(f"Failed to fetch {ci['name']} markets: {e}")
|
|
473
|
+
|
|
474
|
+
results.sort(key=lambda x: -x["supply_usd"])
|
|
475
|
+
_out({"timestamp": datetime.now(timezone.utc).isoformat(), "count": len(results), "markets": results})
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def cmd_account(args):
|
|
479
|
+
"""Fetch full position detail for a specific address."""
|
|
480
|
+
address = args.address.lower()
|
|
481
|
+
chains = _resolve_chains(args.chain)
|
|
482
|
+
account_data = {
|
|
483
|
+
"address": address,
|
|
484
|
+
"chains": {},
|
|
485
|
+
"total_supply_usd": 0,
|
|
486
|
+
"total_borrow_usd": 0,
|
|
487
|
+
"total_net_usd": 0,
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
for chain_key in chains:
|
|
491
|
+
ci = CHAINS[chain_key]
|
|
492
|
+
try:
|
|
493
|
+
prices = _api(f"/tokens/{ci['chain_id']}/prices").get("prices", {})
|
|
494
|
+
|
|
495
|
+
# Get all margin accounts for this user
|
|
496
|
+
d = _gql(ci["subgraph"], """
|
|
497
|
+
query($user: String!) {
|
|
498
|
+
marginAccounts(where: {effectiveUser: $user}, first: 50) {
|
|
499
|
+
id
|
|
500
|
+
accountNumber
|
|
501
|
+
lastUpdatedTimestamp
|
|
502
|
+
hasBorrowValue
|
|
503
|
+
tokenValues(first: 30) {
|
|
504
|
+
token { id symbol }
|
|
505
|
+
valuePar
|
|
506
|
+
}
|
|
507
|
+
borrowTokens { symbol }
|
|
508
|
+
supplyTokens { symbol }
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
""", {"user": address}, timeout=20)
|
|
512
|
+
|
|
513
|
+
accounts = []
|
|
514
|
+
chain_supply = 0
|
|
515
|
+
chain_borrow = 0
|
|
516
|
+
|
|
517
|
+
for acct in d.get("marginAccounts", []):
|
|
518
|
+
supply_usd = 0
|
|
519
|
+
borrow_usd = 0
|
|
520
|
+
holdings = []
|
|
521
|
+
|
|
522
|
+
for tv in acct["tokenValues"]:
|
|
523
|
+
val = float(tv["valuePar"])
|
|
524
|
+
if abs(val) < 0.0001:
|
|
525
|
+
continue
|
|
526
|
+
price = float(prices.get(tv["token"]["id"], 0))
|
|
527
|
+
usd = val * price
|
|
528
|
+
if val > 0:
|
|
529
|
+
supply_usd += usd
|
|
530
|
+
else:
|
|
531
|
+
borrow_usd += abs(usd)
|
|
532
|
+
holdings.append({
|
|
533
|
+
"token": tv["token"]["symbol"],
|
|
534
|
+
"amount": round(val, 6),
|
|
535
|
+
"usd": round(usd, 2),
|
|
536
|
+
"side": "supply" if val > 0 else "borrow",
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
if not holdings:
|
|
540
|
+
continue
|
|
541
|
+
|
|
542
|
+
ltv = (borrow_usd / supply_usd * 100) if supply_usd > 0 else 0
|
|
543
|
+
chain_supply += supply_usd
|
|
544
|
+
chain_borrow += borrow_usd
|
|
545
|
+
|
|
546
|
+
accounts.append({
|
|
547
|
+
"account_number": acct["accountNumber"],
|
|
548
|
+
"has_borrow": acct["hasBorrowValue"],
|
|
549
|
+
"supply_usd": round(supply_usd),
|
|
550
|
+
"borrow_usd": round(borrow_usd),
|
|
551
|
+
"net_usd": round(supply_usd - borrow_usd),
|
|
552
|
+
"ltv_pct": round(ltv, 1),
|
|
553
|
+
"last_active": int(acct["lastUpdatedTimestamp"]),
|
|
554
|
+
"last_active_time": datetime.fromtimestamp(int(acct["lastUpdatedTimestamp"]), tz=timezone.utc).isoformat(),
|
|
555
|
+
"holdings": sorted(holdings, key=lambda h: -abs(h["usd"])),
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
if accounts:
|
|
559
|
+
account_data["chains"][ci["name"]] = {
|
|
560
|
+
"supply_usd": round(chain_supply),
|
|
561
|
+
"borrow_usd": round(chain_borrow),
|
|
562
|
+
"net_usd": round(chain_supply - chain_borrow),
|
|
563
|
+
"accounts": sorted(accounts, key=lambda a: -a["supply_usd"]),
|
|
564
|
+
}
|
|
565
|
+
account_data["total_supply_usd"] += round(chain_supply)
|
|
566
|
+
account_data["total_borrow_usd"] += round(chain_borrow)
|
|
567
|
+
|
|
568
|
+
# Also fetch recent activity
|
|
569
|
+
flows = _gql(ci["subgraph"], """
|
|
570
|
+
query($user: String!) {
|
|
571
|
+
deposits(
|
|
572
|
+
where: {marginAccount_: {effectiveUser: $user}}
|
|
573
|
+
orderBy: serialId, orderDirection: desc, first: 10
|
|
574
|
+
) {
|
|
575
|
+
transaction { timestamp }
|
|
576
|
+
token { symbol }
|
|
577
|
+
amountDeltaWei
|
|
578
|
+
amountUSDDeltaWei
|
|
579
|
+
}
|
|
580
|
+
withdrawals(
|
|
581
|
+
where: {marginAccount_: {effectiveUser: $user}}
|
|
582
|
+
orderBy: serialId, orderDirection: desc, first: 10
|
|
583
|
+
) {
|
|
584
|
+
transaction { timestamp }
|
|
585
|
+
token { symbol }
|
|
586
|
+
amountDeltaWei
|
|
587
|
+
amountUSDDeltaWei
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
""", {"user": address}, timeout=20)
|
|
591
|
+
|
|
592
|
+
recent_activity = []
|
|
593
|
+
for dep in flows.get("deposits", []):
|
|
594
|
+
usd = float(dep["amountUSDDeltaWei"])
|
|
595
|
+
if usd < 100:
|
|
596
|
+
continue
|
|
597
|
+
recent_activity.append({
|
|
598
|
+
"type": "deposit",
|
|
599
|
+
"chain": ci["name"],
|
|
600
|
+
"token": dep["token"]["symbol"],
|
|
601
|
+
"amount": round(float(dep["amountDeltaWei"]), 4),
|
|
602
|
+
"usd": round(usd),
|
|
603
|
+
"timestamp": int(dep["transaction"]["timestamp"]),
|
|
604
|
+
"time": datetime.fromtimestamp(int(dep["transaction"]["timestamp"]), tz=timezone.utc).isoformat(),
|
|
605
|
+
})
|
|
606
|
+
for wd in flows.get("withdrawals", []):
|
|
607
|
+
usd = float(wd["amountUSDDeltaWei"])
|
|
608
|
+
if usd < 100:
|
|
609
|
+
continue
|
|
610
|
+
recent_activity.append({
|
|
611
|
+
"type": "withdrawal",
|
|
612
|
+
"chain": ci["name"],
|
|
613
|
+
"token": wd["token"]["symbol"],
|
|
614
|
+
"amount": round(float(wd["amountDeltaWei"]), 4),
|
|
615
|
+
"usd": round(usd),
|
|
616
|
+
"timestamp": int(wd["transaction"]["timestamp"]),
|
|
617
|
+
"time": datetime.fromtimestamp(int(wd["transaction"]["timestamp"]), tz=timezone.utc).isoformat(),
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
if recent_activity:
|
|
621
|
+
recent_activity.sort(key=lambda x: -x["timestamp"])
|
|
622
|
+
if ci["name"] in account_data["chains"]:
|
|
623
|
+
account_data["chains"][ci["name"]]["recent_activity"] = recent_activity
|
|
624
|
+
|
|
625
|
+
except Exception as e:
|
|
626
|
+
_err(f"Failed to fetch {ci['name']} account: {e}")
|
|
627
|
+
|
|
628
|
+
account_data["total_net_usd"] = account_data["total_supply_usd"] - account_data["total_borrow_usd"]
|
|
629
|
+
account_data["timestamp"] = datetime.now(timezone.utc).isoformat()
|
|
630
|
+
account_data["debank_url"] = f"https://debank.com/profile/{address}"
|
|
631
|
+
_out(account_data)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def cmd_risks(args):
|
|
635
|
+
"""Identify high-risk positions and dangerous utilization levels."""
|
|
636
|
+
min_ltv = args.min_ltv or 75
|
|
637
|
+
min_usd = args.min_usd or 50000
|
|
638
|
+
risks = {
|
|
639
|
+
"high_ltv_positions": [],
|
|
640
|
+
"high_utilization_markets": [],
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
# Get positions across all chains
|
|
644
|
+
for chain_key in ACTIVE_CHAINS:
|
|
645
|
+
ci = CHAINS[chain_key]
|
|
646
|
+
try:
|
|
647
|
+
d = _gql(ci["subgraph"], """{
|
|
648
|
+
marginAccounts(
|
|
649
|
+
where: {hasBorrowValue: true}
|
|
650
|
+
orderBy: lastUpdatedTimestamp
|
|
651
|
+
orderDirection: desc
|
|
652
|
+
first: 200
|
|
653
|
+
) {
|
|
654
|
+
effectiveUser { id }
|
|
655
|
+
accountNumber
|
|
656
|
+
tokenValues(first: 30) {
|
|
657
|
+
token { symbol id }
|
|
658
|
+
valuePar
|
|
659
|
+
}
|
|
660
|
+
supplyTokens { symbol }
|
|
661
|
+
borrowTokens { symbol }
|
|
662
|
+
}
|
|
663
|
+
}""", timeout=30)
|
|
664
|
+
|
|
665
|
+
prices = _api(f"/tokens/{ci['chain_id']}/prices").get("prices", {})
|
|
666
|
+
|
|
667
|
+
for acct in d.get("marginAccounts", []):
|
|
668
|
+
supply_usd = 0
|
|
669
|
+
borrow_usd = 0
|
|
670
|
+
for tv in acct["tokenValues"]:
|
|
671
|
+
val = float(tv["valuePar"])
|
|
672
|
+
price = float(prices.get(tv["token"]["id"], 0))
|
|
673
|
+
if val > 0:
|
|
674
|
+
supply_usd += val * price
|
|
675
|
+
else:
|
|
676
|
+
borrow_usd += abs(val * price)
|
|
677
|
+
|
|
678
|
+
if supply_usd < min_usd:
|
|
679
|
+
continue
|
|
680
|
+
ltv = (borrow_usd / supply_usd * 100) if supply_usd > 0 else 0
|
|
681
|
+
if ltv >= min_ltv:
|
|
682
|
+
risks["high_ltv_positions"].append({
|
|
683
|
+
"chain": ci["name"],
|
|
684
|
+
"address": acct["effectiveUser"]["id"],
|
|
685
|
+
"supply_usd": round(supply_usd),
|
|
686
|
+
"borrow_usd": round(borrow_usd),
|
|
687
|
+
"ltv_pct": round(ltv, 1),
|
|
688
|
+
"supply_tokens": [t["symbol"] for t in acct["supplyTokens"]],
|
|
689
|
+
"borrow_tokens": [t["symbol"] for t in acct["borrowTokens"]],
|
|
690
|
+
})
|
|
691
|
+
except Exception as e:
|
|
692
|
+
_err(f"Failed to fetch {ci['name']} risk positions: {e}")
|
|
693
|
+
|
|
694
|
+
# Check market utilization
|
|
695
|
+
for chain_key in ACTIVE_CHAINS:
|
|
696
|
+
ci = CHAINS[chain_key]
|
|
697
|
+
try:
|
|
698
|
+
tokens_resp = _api(f"/tokens/{ci['chain_id']}")
|
|
699
|
+
rates_resp = _api(f"/tokens/{ci['chain_id']}/interest-rates")
|
|
700
|
+
prices_resp = _api(f"/tokens/{ci['chain_id']}/prices")
|
|
701
|
+
|
|
702
|
+
tokens = {t["id"]: t for t in tokens_resp.get("tokens", [])}
|
|
703
|
+
rates = {r["token"]["tokenAddress"]: r for r in rates_resp.get("interestRates", [])}
|
|
704
|
+
prices = prices_resp.get("prices", {})
|
|
705
|
+
|
|
706
|
+
for addr, token in tokens.items():
|
|
707
|
+
price = float(prices.get(addr, 0))
|
|
708
|
+
supply = float(token.get("supplyLiquidity", 0))
|
|
709
|
+
borrow = float(token.get("borrowLiquidity", 0))
|
|
710
|
+
supply_usd = supply * price
|
|
711
|
+
if supply_usd < 10000:
|
|
712
|
+
continue
|
|
713
|
+
util = (borrow / supply * 100) if supply > 0 else 0
|
|
714
|
+
if util >= 80:
|
|
715
|
+
rate = rates.get(addr, {})
|
|
716
|
+
risks["high_utilization_markets"].append({
|
|
717
|
+
"chain": ci["name"],
|
|
718
|
+
"symbol": token["symbol"],
|
|
719
|
+
"utilization_pct": round(util, 1),
|
|
720
|
+
"supply_usd": round(supply_usd),
|
|
721
|
+
"borrow_usd": round(borrow * price),
|
|
722
|
+
"available_usd": round((supply - borrow) * price),
|
|
723
|
+
"supply_rate_pct": round(float(rate.get("totalSupplyInterestRate", 0)) * 100, 3),
|
|
724
|
+
"borrow_rate_pct": round(float(rate.get("borrowInterestRate", 0)) * 100, 3),
|
|
725
|
+
})
|
|
726
|
+
except Exception as e:
|
|
727
|
+
_err(f"Failed to fetch {ci['name']} market risks: {e}")
|
|
728
|
+
|
|
729
|
+
risks["high_ltv_positions"].sort(key=lambda x: -x["ltv_pct"])
|
|
730
|
+
risks["high_utilization_markets"].sort(key=lambda x: -x["utilization_pct"])
|
|
731
|
+
risks["timestamp"] = datetime.now(timezone.utc).isoformat()
|
|
732
|
+
risks["thresholds"] = {"min_ltv_pct": min_ltv, "min_position_usd": min_usd, "min_utilization_pct": 80}
|
|
733
|
+
_out(risks)
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def cmd_schema(args):
|
|
737
|
+
"""Show available subgraph entities and example queries."""
|
|
738
|
+
_out({
|
|
739
|
+
"description": "Dolomite CLI — query Dolomite protocol data across all chains",
|
|
740
|
+
"version": __version__,
|
|
741
|
+
"subgraph_base": SUBGRAPH_BASE,
|
|
742
|
+
"api_base": API_BASE,
|
|
743
|
+
"chains": {k: {"name": v["name"], "chain_id": v["chain_id"], "subgraph": v["subgraph"]} for k, v in CHAINS.items()},
|
|
744
|
+
"active_chains": ACTIVE_CHAINS,
|
|
745
|
+
"commands": {
|
|
746
|
+
"rates": "All markets with supply/borrow rates. Options: --chain, --stables-only, --min-tvl",
|
|
747
|
+
"positions": "Top borrowing positions. Options: --chain, --top N, --min-usd N",
|
|
748
|
+
"flows": "Recent large deposits/withdrawals. Options: --chain, --hours N, --min-usd N, --type deposit|withdrawal",
|
|
749
|
+
"liquidations": "Recent liquidation events. Options: --chain, --hours N, --limit N",
|
|
750
|
+
"tvl": "Protocol-level TVL summary per chain",
|
|
751
|
+
"markets": "Detailed info for a specific token. Options: --token SYMBOL, --chain",
|
|
752
|
+
"account": "Full position detail for an address. Args: <address>. Options: --chain",
|
|
753
|
+
"risks": "High-risk positions and dangerous utilization. Options: --min-ltv N, --min-usd N",
|
|
754
|
+
"schema": "This help message with available entities and example queries",
|
|
755
|
+
},
|
|
756
|
+
"key_subgraph_entities": [
|
|
757
|
+
"DolomiteMargin — protocol-level aggregates (TVL, users, volumes)",
|
|
758
|
+
"Token — listed assets with addresses and metadata",
|
|
759
|
+
"MarginAccount — user positions with tokenValues (supply/borrow per asset)",
|
|
760
|
+
"BorrowPosition — individual borrow positions with status",
|
|
761
|
+
"Deposit — deposit events with amounts, timestamps, users",
|
|
762
|
+
"Withdrawal — withdrawal events with amounts, timestamps, users",
|
|
763
|
+
"Liquidation — liquidation events with seized collateral and debt",
|
|
764
|
+
"Trade — trade/swap events within margin accounts",
|
|
765
|
+
"AmmLiquidity* — AMM pool data (AmmLiquidityPosition, AmmLiquiditySnapshot)",
|
|
766
|
+
],
|
|
767
|
+
"example_queries": {
|
|
768
|
+
"get_all_rates": "dolomite rates",
|
|
769
|
+
"stablecoin_rates_only": "dolomite rates --stables-only",
|
|
770
|
+
"ethereum_positions": "dolomite positions --chain ethereum --top 20",
|
|
771
|
+
"whale_flows_48h": "dolomite flows --hours 48 --min-usd 100000",
|
|
772
|
+
"deposits_only": "dolomite flows --type deposit --hours 72",
|
|
773
|
+
"specific_token": "dolomite markets --token USDC",
|
|
774
|
+
"lookup_whale": "dolomite account 0x8be46b25d59616e594f0a9e20147fb14c1b989d9",
|
|
775
|
+
"high_risk": "dolomite risks --min-ltv 80 --min-usd 100000",
|
|
776
|
+
"berachain_only": "dolomite rates --chain berachain",
|
|
777
|
+
},
|
|
778
|
+
"custom_queries": {
|
|
779
|
+
"description": "The CLI commands are convenience wrappers. You can also query the Dolomite subgraphs directly via GraphQL for ANY data not covered by the built-in commands. The subgraphs contain the COMPLETE on-chain history — every deposit, withdrawal, liquidation, trade, position change, and margin account update since deployment. There are NO time limits or data caps.",
|
|
780
|
+
"subgraph_url_pattern": "POST https://subgraph.api.dolomite.io/api/public/1301d2d1-7a9d-4be4-9e9a-061cb8611549/subgraphs/{subgraph_name}/latest/gn",
|
|
781
|
+
"subgraph_names": {
|
|
782
|
+
"ethereum": "dolomite-ethereum",
|
|
783
|
+
"arbitrum": "dolomite-arbitrum",
|
|
784
|
+
"berachain": "dolomite-berachain-mainnet",
|
|
785
|
+
"mantle": "dolomite-mantle",
|
|
786
|
+
"base": "dolomite-base",
|
|
787
|
+
"xlayer": "dolomite-x-layer"
|
|
788
|
+
},
|
|
789
|
+
"auth": "None required. All endpoints are public.",
|
|
790
|
+
"how_to_query": "Send a POST request with JSON body: {\"query\": \"{ ... }\", \"variables\": {...}}. Returns {\"data\": {...}}.",
|
|
791
|
+
"pagination": "Subgraph max is 1000 items per query. Use 'first: N, skip: M' or cursor-based pagination with 'where: {id_gt: lastId}' for larger datasets.",
|
|
792
|
+
"introspection": "Run this to discover all entity types and fields: {\"query\": \"{__schema{types{name fields{name type{name}}}}}\"}. Or for a single entity: {\"query\": \"{__type(name:\\\"Deposit\\\"){fields{name type{name}}}}\"}",
|
|
793
|
+
"example_custom_queries": {
|
|
794
|
+
"all_deposits_by_address_all_time": "curl -s -X POST '{subgraph_url}' -H 'Content-Type: application/json' -d '{\"query\": \"{deposits(where: {marginAccount_: {effectiveUser: \\\"0xADDRESS\\\"}}, orderBy: serialId, orderDirection: desc, first: 1000) { transaction { timestamp } token { symbol } amountDeltaWei amountUSDDeltaWei }}\"}'",
|
|
795
|
+
"trades_in_last_30_days": "curl -s -X POST '{subgraph_url}' -H 'Content-Type: application/json' -d '{\"query\": \"{trades(where: {transaction_: {timestamp_gte: UNIX_30_DAYS_AGO}}, orderBy: serialId, orderDirection: desc, first: 100) { transaction { timestamp } makerToken { symbol } takerToken { symbol } makerTokenDeltaWei takerTokenDeltaWei }}\"}'",
|
|
796
|
+
"total_protocol_stats": "curl -s -X POST '{subgraph_url}' -H 'Content-Type: application/json' -d '{\"query\": \"{dolomiteMargins(first:1) { supplyLiquidityUSD borrowLiquidityUSD userCount totalTradeVolumeUSD totalLiquidationVolumeUSD liquidationCount borrowPositionCount numberOfMarkets }}\"}'",
|
|
797
|
+
"all_borrow_positions_for_token": "curl -s -X POST '{subgraph_url}' -H 'Content-Type: application/json' -d '{\"query\": \"{borrowPositions(where: {borrowToken_: {symbol: \\\"USDC\\\"}, status: Open}, first: 100, orderBy: amounts__borrowAmountUSD, orderDirection: desc) { effectiveUser { id } amounts { borrowAmountUSD supplyAmountUSD } }}\"}'",
|
|
798
|
+
"margin_account_full_detail": "curl -s -X POST '{subgraph_url}' -H 'Content-Type: application/json' -d '{\"query\": \"{marginAccounts(where: {effectiveUser: \\\"0xADDRESS\\\"}, first: 50) { id accountNumber lastUpdatedTimestamp hasBorrowValue tokenValues(first: 30) { token { symbol id } valuePar } supplyTokens { symbol } borrowTokens { symbol } }}\"}'"
|
|
799
|
+
},
|
|
800
|
+
"rest_api": {
|
|
801
|
+
"description": "Dolomite also has a REST API for real-time prices, interest rates, and token metadata.",
|
|
802
|
+
"base_url": "https://api.dolomite.io",
|
|
803
|
+
"endpoints": {
|
|
804
|
+
"tokens": "/tokens/{chainId} — all listed tokens with supply/borrow amounts",
|
|
805
|
+
"interest_rates": "/tokens/{chainId}/interest-rates — current supply and borrow interest rates",
|
|
806
|
+
"prices": "/tokens/{chainId}/prices — current USD prices for all tokens"
|
|
807
|
+
},
|
|
808
|
+
"chain_ids": {"ethereum": 1, "arbitrum": 42161, "berachain": 80094, "mantle": 5000, "base": 8453, "xlayer": 196}
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
})
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
# ─── CLI Setup ────────────────────────────────────────────────────────────────
|
|
815
|
+
|
|
816
|
+
def main():
|
|
817
|
+
parser = argparse.ArgumentParser(
|
|
818
|
+
prog="dolomite",
|
|
819
|
+
description="Query Dolomite protocol data across all chains. Returns structured JSON.",
|
|
820
|
+
)
|
|
821
|
+
parser.add_argument("--version", action="version", version=f"dolomite {__version__}")
|
|
822
|
+
sub = parser.add_subparsers(dest="command", help="Available commands")
|
|
823
|
+
|
|
824
|
+
# rates
|
|
825
|
+
p = sub.add_parser("rates", help="All markets with supply/borrow rates")
|
|
826
|
+
p.add_argument("--chain", help="Filter by chain (ethereum, arbitrum, berachain, all)")
|
|
827
|
+
p.add_argument("--stables-only", action="store_true", help="Only show stablecoin markets")
|
|
828
|
+
p.add_argument("--min-tvl", type=float, help="Minimum TVL in USD (default: 1000)")
|
|
829
|
+
|
|
830
|
+
# positions
|
|
831
|
+
p = sub.add_parser("positions", help="Top borrowing positions")
|
|
832
|
+
p.add_argument("--chain", help="Filter by chain")
|
|
833
|
+
p.add_argument("--top", type=int, help="Number of positions to return (default: 50)")
|
|
834
|
+
p.add_argument("--min-usd", type=float, help="Minimum position size in USD (default: 10000)")
|
|
835
|
+
|
|
836
|
+
# flows
|
|
837
|
+
p = sub.add_parser("flows", help="Recent large deposits/withdrawals")
|
|
838
|
+
p.add_argument("--chain", help="Filter by chain")
|
|
839
|
+
p.add_argument("--hours", type=int, help="Lookback period in hours (default: 24)")
|
|
840
|
+
p.add_argument("--min-usd", type=float, help="Minimum flow size in USD (default: 50000)")
|
|
841
|
+
p.add_argument("--type", choices=["deposit", "withdrawal"], help="Filter by flow type")
|
|
842
|
+
|
|
843
|
+
# liquidations
|
|
844
|
+
p = sub.add_parser("liquidations", help="Recent liquidation events")
|
|
845
|
+
p.add_argument("--chain", help="Filter by chain")
|
|
846
|
+
p.add_argument("--hours", type=int, help="Lookback period in hours (default: 168 = 7 days)")
|
|
847
|
+
p.add_argument("--limit", type=int, help="Max events to return (default: 100)")
|
|
848
|
+
|
|
849
|
+
# tvl
|
|
850
|
+
p = sub.add_parser("tvl", help="Protocol TVL summary per chain")
|
|
851
|
+
p.add_argument("--chain", help="Filter by chain")
|
|
852
|
+
|
|
853
|
+
# markets
|
|
854
|
+
p = sub.add_parser("markets", help="Detailed info for a specific token")
|
|
855
|
+
p.add_argument("--token", help="Token symbol (e.g., USDC, WETH)")
|
|
856
|
+
p.add_argument("--chain", help="Filter by chain")
|
|
857
|
+
|
|
858
|
+
# account
|
|
859
|
+
p = sub.add_parser("account", help="Full position detail for an address")
|
|
860
|
+
p.add_argument("address", help="Ethereum address (0x...)")
|
|
861
|
+
p.add_argument("--chain", help="Filter by chain")
|
|
862
|
+
|
|
863
|
+
# risks
|
|
864
|
+
p = sub.add_parser("risks", help="High-risk positions and utilization alerts")
|
|
865
|
+
p.add_argument("--min-ltv", type=float, help="Minimum LTV to flag (default: 75)")
|
|
866
|
+
p.add_argument("--min-usd", type=float, help="Minimum position size (default: 50000)")
|
|
867
|
+
|
|
868
|
+
# schema
|
|
869
|
+
sub.add_parser("schema", help="Show available data and example queries")
|
|
870
|
+
|
|
871
|
+
args = parser.parse_args()
|
|
872
|
+
if not args.command:
|
|
873
|
+
parser.print_help()
|
|
874
|
+
sys.exit(1)
|
|
875
|
+
|
|
876
|
+
cmd_map = {
|
|
877
|
+
"rates": cmd_rates,
|
|
878
|
+
"positions": cmd_positions,
|
|
879
|
+
"flows": cmd_flows,
|
|
880
|
+
"liquidations": cmd_liquidations,
|
|
881
|
+
"tvl": cmd_tvl,
|
|
882
|
+
"markets": cmd_markets,
|
|
883
|
+
"account": cmd_account,
|
|
884
|
+
"risks": cmd_risks,
|
|
885
|
+
"schema": cmd_schema,
|
|
886
|
+
}
|
|
887
|
+
cmd_map[args.command](args)
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
if __name__ == "__main__":
|
|
891
|
+
main()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dolomite-cli"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "CLI tool for querying Dolomite protocol data across all chains"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
keywords = ["dolomite", "defi", "cli", "blockchain", "interest-rates"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Office/Business :: Financial",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
dolomite = "dolomite_cli:main"
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/openclaw/dolomite-cli"
|