morpho-blue-py 0.1.0__py3-none-any.whl
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.
- morpho_blue/__init__.py +47 -0
- morpho_blue/_common.py +166 -0
- morpho_blue/async_client.py +377 -0
- morpho_blue/client.py +375 -0
- morpho_blue/exceptions.py +51 -0
- morpho_blue/export.py +82 -0
- morpho_blue/models.py +330 -0
- morpho_blue/py.typed +0 -0
- morpho_blue/queries.py +172 -0
- morpho_blue_py-0.1.0.dist-info/METADATA +189 -0
- morpho_blue_py-0.1.0.dist-info/RECORD +13 -0
- morpho_blue_py-0.1.0.dist-info/WHEEL +4 -0
- morpho_blue_py-0.1.0.dist-info/licenses/LICENSE +21 -0
morpho_blue/__init__.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""morpho-blue-py: a typed Python client for the Morpho Blue GraphQL API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ._common import DEFAULT_ENDPOINT
|
|
6
|
+
from .async_client import AsyncMorphoClient
|
|
7
|
+
from .client import MorphoClient
|
|
8
|
+
from .exceptions import GraphQLError, HTTPError, MorphoError
|
|
9
|
+
from .models import (
|
|
10
|
+
Asset,
|
|
11
|
+
Chain,
|
|
12
|
+
Market,
|
|
13
|
+
MarketPosition,
|
|
14
|
+
MarketPositionState,
|
|
15
|
+
MarketState,
|
|
16
|
+
PageInfo,
|
|
17
|
+
User,
|
|
18
|
+
Vault,
|
|
19
|
+
VaultAllocation,
|
|
20
|
+
VaultPosition,
|
|
21
|
+
VaultPositionState,
|
|
22
|
+
VaultState,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__version__ = "0.1.0"
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"DEFAULT_ENDPOINT",
|
|
29
|
+
"AsyncMorphoClient",
|
|
30
|
+
"MorphoClient",
|
|
31
|
+
"GraphQLError",
|
|
32
|
+
"HTTPError",
|
|
33
|
+
"MorphoError",
|
|
34
|
+
"Asset",
|
|
35
|
+
"Chain",
|
|
36
|
+
"Market",
|
|
37
|
+
"MarketPosition",
|
|
38
|
+
"MarketPositionState",
|
|
39
|
+
"MarketState",
|
|
40
|
+
"PageInfo",
|
|
41
|
+
"User",
|
|
42
|
+
"Vault",
|
|
43
|
+
"VaultAllocation",
|
|
44
|
+
"VaultPosition",
|
|
45
|
+
"VaultPositionState",
|
|
46
|
+
"VaultState",
|
|
47
|
+
]
|
morpho_blue/_common.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Shared, transport-agnostic helpers used by both sync and async clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from .exceptions import GraphQLError, HTTPError
|
|
8
|
+
from .models import Market, User, Vault
|
|
9
|
+
|
|
10
|
+
DEFAULT_ENDPOINT = "https://blue-api.morpho.org/graphql"
|
|
11
|
+
|
|
12
|
+
# Map a friendly "field" name to the API's MarketOrderBy enum value.
|
|
13
|
+
MARKET_ORDER_BY = {
|
|
14
|
+
"supply_apy": "SupplyApy",
|
|
15
|
+
"borrow_apy": "BorrowApy",
|
|
16
|
+
"net_supply_apy": "NetSupplyApy",
|
|
17
|
+
"net_borrow_apy": "NetBorrowApy",
|
|
18
|
+
"supply_assets_usd": "SupplyAssetsUsd",
|
|
19
|
+
"borrow_assets_usd": "BorrowAssetsUsd",
|
|
20
|
+
"total_liquidity_usd": "TotalLiquidityUsd",
|
|
21
|
+
"utilization": "Utilization",
|
|
22
|
+
"lltv": "Lltv",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
VAULT_ORDER_BY = {
|
|
26
|
+
"apy": "Apy",
|
|
27
|
+
"net_apy": "NetApy",
|
|
28
|
+
"total_assets": "TotalAssets",
|
|
29
|
+
"total_assets_usd": "TotalAssetsUsd",
|
|
30
|
+
"fee": "Fee",
|
|
31
|
+
"name": "Name",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def parse_response(payload: dict[str, Any]) -> dict[str, Any]:
|
|
36
|
+
"""Validate a GraphQL JSON payload and return its ``data`` object.
|
|
37
|
+
|
|
38
|
+
Raises :class:`GraphQLError` if the payload carries an ``errors`` array.
|
|
39
|
+
"""
|
|
40
|
+
errors = payload.get("errors")
|
|
41
|
+
if errors:
|
|
42
|
+
raise GraphQLError(errors)
|
|
43
|
+
data = payload.get("data")
|
|
44
|
+
if data is None:
|
|
45
|
+
raise GraphQLError([{"message": "Response contained no data"}])
|
|
46
|
+
assert isinstance(data, dict)
|
|
47
|
+
return data
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def check_status(status_code: int, text: str) -> None:
|
|
51
|
+
"""Raise :class:`HTTPError` for any non-2xx response."""
|
|
52
|
+
if status_code < 200 or status_code >= 300:
|
|
53
|
+
raise HTTPError(status_code, text)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def build_market_variables(
|
|
57
|
+
*,
|
|
58
|
+
first: Optional[int],
|
|
59
|
+
skip: Optional[int],
|
|
60
|
+
chain_id: Optional[int],
|
|
61
|
+
order_by: Optional[str],
|
|
62
|
+
order_direction: str,
|
|
63
|
+
where: Optional[dict[str, Any]],
|
|
64
|
+
) -> dict[str, Any]:
|
|
65
|
+
"""Build the GraphQL variables for a ``markets`` query.
|
|
66
|
+
|
|
67
|
+
Merges ``chain_id`` into the ``where`` filters as ``chainId_in`` and maps a
|
|
68
|
+
friendly ``order_by`` key to its ``MarketOrderBy`` enum value (passing
|
|
69
|
+
through any value not in :data:`MARKET_ORDER_BY`).
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
The ``{"first", "skip", "orderDirection", "where", "orderBy"?}`` variables
|
|
73
|
+
dict ready to send to the API.
|
|
74
|
+
"""
|
|
75
|
+
filters: dict[str, Any] = dict(where or {})
|
|
76
|
+
if chain_id is not None:
|
|
77
|
+
filters.setdefault("chainId_in", [chain_id])
|
|
78
|
+
variables: dict[str, Any] = {
|
|
79
|
+
"first": first,
|
|
80
|
+
"skip": skip,
|
|
81
|
+
"orderDirection": order_direction,
|
|
82
|
+
"where": filters or None,
|
|
83
|
+
}
|
|
84
|
+
if order_by is not None:
|
|
85
|
+
variables["orderBy"] = MARKET_ORDER_BY.get(order_by, order_by)
|
|
86
|
+
return variables
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def build_vault_variables(
|
|
90
|
+
*,
|
|
91
|
+
first: Optional[int],
|
|
92
|
+
skip: Optional[int],
|
|
93
|
+
chain_id: Optional[int],
|
|
94
|
+
order_by: Optional[str],
|
|
95
|
+
order_direction: str,
|
|
96
|
+
where: Optional[dict[str, Any]],
|
|
97
|
+
) -> dict[str, Any]:
|
|
98
|
+
"""Build the GraphQL variables for a ``vaults`` query.
|
|
99
|
+
|
|
100
|
+
Merges ``chain_id`` into the ``where`` filters as ``chainId_in`` and maps a
|
|
101
|
+
friendly ``order_by`` key to its ``VaultOrderBy`` enum value (passing through
|
|
102
|
+
any value not in :data:`VAULT_ORDER_BY`).
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
The ``{"first", "skip", "orderDirection", "where", "orderBy"?}`` variables
|
|
106
|
+
dict ready to send to the API.
|
|
107
|
+
"""
|
|
108
|
+
filters: dict[str, Any] = dict(where or {})
|
|
109
|
+
if chain_id is not None:
|
|
110
|
+
filters.setdefault("chainId_in", [chain_id])
|
|
111
|
+
variables: dict[str, Any] = {
|
|
112
|
+
"first": first,
|
|
113
|
+
"skip": skip,
|
|
114
|
+
"orderDirection": order_direction,
|
|
115
|
+
"where": filters or None,
|
|
116
|
+
}
|
|
117
|
+
if order_by is not None:
|
|
118
|
+
variables["orderBy"] = VAULT_ORDER_BY.get(order_by, order_by)
|
|
119
|
+
return variables
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def markets_from_data(data: dict[str, Any]) -> list[Market]:
|
|
123
|
+
"""Parse the ``markets.items`` of a response into :class:`Market` objects.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
The parsed markets (an empty list when the page has no items).
|
|
127
|
+
"""
|
|
128
|
+
items = data["markets"]["items"] or []
|
|
129
|
+
return [Market.model_validate(item) for item in items]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def vaults_from_data(data: dict[str, Any]) -> list[Vault]:
|
|
133
|
+
"""Parse the ``vaults.items`` of a response into :class:`Vault` objects.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
The parsed vaults (an empty list when the page has no items).
|
|
137
|
+
"""
|
|
138
|
+
items = data["vaults"]["items"] or []
|
|
139
|
+
return [Vault.model_validate(item) for item in items]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def market_from_data(data: dict[str, Any]) -> Market:
|
|
143
|
+
"""Parse the ``marketById`` field of a response into a :class:`Market`.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
The parsed single market.
|
|
147
|
+
"""
|
|
148
|
+
return Market.model_validate(data["marketById"])
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def vault_from_data(data: dict[str, Any]) -> Vault:
|
|
152
|
+
"""Parse the ``vaultByAddress`` field of a response into a :class:`Vault`.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
The parsed single vault.
|
|
156
|
+
"""
|
|
157
|
+
return Vault.model_validate(data["vaultByAddress"])
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def user_from_data(data: dict[str, Any]) -> User:
|
|
161
|
+
"""Parse the ``userByAddress`` field of a response into a :class:`User`.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
The parsed user with their market and vault positions.
|
|
165
|
+
"""
|
|
166
|
+
return User.model_validate(data["userByAddress"])
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""Asynchronous Morpho Blue GraphQL client (httpx.AsyncClient)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from types import TracebackType
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from . import queries
|
|
11
|
+
from ._common import (
|
|
12
|
+
DEFAULT_ENDPOINT,
|
|
13
|
+
build_market_variables,
|
|
14
|
+
build_vault_variables,
|
|
15
|
+
check_status,
|
|
16
|
+
market_from_data,
|
|
17
|
+
markets_from_data,
|
|
18
|
+
parse_response,
|
|
19
|
+
user_from_data,
|
|
20
|
+
vault_from_data,
|
|
21
|
+
vaults_from_data,
|
|
22
|
+
)
|
|
23
|
+
from .client import DEFAULT_TIMEOUT
|
|
24
|
+
from .models import Market, User, Vault
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AsyncMorphoClient:
|
|
28
|
+
"""An asyncio client for the Morpho Blue GraphQL API.
|
|
29
|
+
|
|
30
|
+
The async counterpart of :class:`~morpho_blue.client.MorphoClient`, exposing
|
|
31
|
+
the same methods as coroutines. Use it as an async context manager so the
|
|
32
|
+
underlying ``httpx.AsyncClient`` is closed automatically.
|
|
33
|
+
|
|
34
|
+
Example::
|
|
35
|
+
|
|
36
|
+
import asyncio
|
|
37
|
+
from morpho_blue import AsyncMorphoClient
|
|
38
|
+
|
|
39
|
+
async def main() -> None:
|
|
40
|
+
async with AsyncMorphoClient() as client:
|
|
41
|
+
markets = await client.top_markets_by_supply_apy(chain_id=1, limit=5)
|
|
42
|
+
for m in markets:
|
|
43
|
+
if m.loan_asset and m.state:
|
|
44
|
+
print(m.loan_asset.symbol, m.state.supply_apy)
|
|
45
|
+
|
|
46
|
+
asyncio.run(main())
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
endpoint: str = DEFAULT_ENDPOINT,
|
|
52
|
+
*,
|
|
53
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
54
|
+
headers: Optional[dict[str, str]] = None,
|
|
55
|
+
client: Optional[httpx.AsyncClient] = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Create an async client.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
endpoint: GraphQL endpoint URL. Defaults to the public Morpho Blue
|
|
61
|
+
API (:data:`~morpho_blue.DEFAULT_ENDPOINT`).
|
|
62
|
+
timeout: Per-request timeout in seconds. Ignored if ``client`` is
|
|
63
|
+
supplied.
|
|
64
|
+
headers: Extra HTTP headers merged into every request (on top of the
|
|
65
|
+
default ``Content-Type: application/json``).
|
|
66
|
+
client: A pre-configured ``httpx.AsyncClient`` to use. When provided,
|
|
67
|
+
the caller owns its lifecycle and :meth:`aclose` will not close
|
|
68
|
+
it; otherwise the client creates and owns one.
|
|
69
|
+
"""
|
|
70
|
+
self.endpoint = endpoint
|
|
71
|
+
default_headers = {"Content-Type": "application/json"}
|
|
72
|
+
if headers:
|
|
73
|
+
default_headers.update(headers)
|
|
74
|
+
self._owns_client = client is None
|
|
75
|
+
self._client = client or httpx.AsyncClient(timeout=timeout, headers=default_headers)
|
|
76
|
+
|
|
77
|
+
# -- lifecycle -------------------------------------------------------
|
|
78
|
+
async def aclose(self) -> None:
|
|
79
|
+
"""Close the underlying HTTP client if this instance owns it.
|
|
80
|
+
|
|
81
|
+
A no-op when an external ``httpx.AsyncClient`` was passed to
|
|
82
|
+
:meth:`__init__`, since the caller is responsible for closing it.
|
|
83
|
+
"""
|
|
84
|
+
if self._owns_client:
|
|
85
|
+
await self._client.aclose()
|
|
86
|
+
|
|
87
|
+
async def __aenter__(self) -> AsyncMorphoClient:
|
|
88
|
+
"""Enter the async context manager and return this client."""
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
async def __aexit__(
|
|
92
|
+
self,
|
|
93
|
+
exc_type: Optional[type[BaseException]],
|
|
94
|
+
exc: Optional[BaseException],
|
|
95
|
+
tb: Optional[TracebackType],
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Exit the async context manager, closing the client via :meth:`aclose`."""
|
|
98
|
+
await self.aclose()
|
|
99
|
+
|
|
100
|
+
# -- low level -------------------------------------------------------
|
|
101
|
+
async def execute(
|
|
102
|
+
self, query: str, variables: Optional[dict[str, Any]] = None
|
|
103
|
+
) -> dict[str, Any]:
|
|
104
|
+
"""POST a raw GraphQL query and return its ``data`` object.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
query: The GraphQL query document.
|
|
108
|
+
variables: Optional variables to bind into the query.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
The ``data`` object from the GraphQL response.
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
HTTPError: If the endpoint returns a non-2xx HTTP status.
|
|
115
|
+
GraphQLError: If the response carries a GraphQL ``errors`` array
|
|
116
|
+
(the API returns HTTP 200 even for query errors).
|
|
117
|
+
"""
|
|
118
|
+
response = await self._client.post(
|
|
119
|
+
self.endpoint, json={"query": query, "variables": variables or {}}
|
|
120
|
+
)
|
|
121
|
+
check_status(response.status_code, response.text)
|
|
122
|
+
return parse_response(response.json())
|
|
123
|
+
|
|
124
|
+
# -- markets ---------------------------------------------------------
|
|
125
|
+
async def get_markets(
|
|
126
|
+
self,
|
|
127
|
+
*,
|
|
128
|
+
chain_id: Optional[int] = None,
|
|
129
|
+
first: Optional[int] = 100,
|
|
130
|
+
skip: Optional[int] = 0,
|
|
131
|
+
order_by: Optional[str] = None,
|
|
132
|
+
order_direction: str = "Desc",
|
|
133
|
+
where: Optional[dict[str, Any]] = None,
|
|
134
|
+
) -> list[Market]:
|
|
135
|
+
"""Fetch a single page of markets, optionally filtered and sorted.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
chain_id: Restrict to one chain (e.g. ``1`` for Ethereum). When set,
|
|
139
|
+
it is merged into ``where`` as ``chainId_in``. Omit for all chains.
|
|
140
|
+
first: Page size (max number of markets to return).
|
|
141
|
+
skip: Offset for pagination.
|
|
142
|
+
order_by: Sort field. Accepts a friendly key (``"supply_apy"``,
|
|
143
|
+
``"supply_assets_usd"``, ``"utilization"``, ``"lltv"``, …) or a
|
|
144
|
+
raw ``MarketOrderBy`` enum value.
|
|
145
|
+
order_direction: ``"Desc"`` (default) or ``"Asc"``.
|
|
146
|
+
where: Additional raw ``MarketFilters`` (e.g.
|
|
147
|
+
``{"utilization_lte": 0.99}``), merged with ``chain_id``.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
The page of markets as :class:`~morpho_blue.models.Market` objects.
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
HTTPError: On a non-2xx HTTP status.
|
|
154
|
+
GraphQLError: If the API returns a GraphQL error.
|
|
155
|
+
"""
|
|
156
|
+
variables = build_market_variables(
|
|
157
|
+
first=first,
|
|
158
|
+
skip=skip,
|
|
159
|
+
chain_id=chain_id,
|
|
160
|
+
order_by=order_by,
|
|
161
|
+
order_direction=order_direction,
|
|
162
|
+
where=where,
|
|
163
|
+
)
|
|
164
|
+
data = await self.execute(queries.MARKETS_QUERY, variables)
|
|
165
|
+
return markets_from_data(data)
|
|
166
|
+
|
|
167
|
+
async def get_market(self, market_id: str, *, chain_id: int) -> Market:
|
|
168
|
+
"""Fetch a single market by its ``marketId`` (the unique key).
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
market_id: The market's unique key (0x-prefixed 32-byte hash). Note
|
|
172
|
+
the Morpho schema uses ``marketId``, not ``uniqueKey``.
|
|
173
|
+
chain_id: The chain the market lives on (required for this lookup).
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
The :class:`~morpho_blue.models.Market`.
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
HTTPError: On a non-2xx HTTP status.
|
|
180
|
+
GraphQLError: If the market is not found or the API errors.
|
|
181
|
+
"""
|
|
182
|
+
data = await self.execute(
|
|
183
|
+
queries.MARKET_BY_ID_QUERY,
|
|
184
|
+
{"marketId": market_id, "chainId": chain_id},
|
|
185
|
+
)
|
|
186
|
+
return market_from_data(data)
|
|
187
|
+
|
|
188
|
+
async def top_markets_by_supply_apy(
|
|
189
|
+
self,
|
|
190
|
+
*,
|
|
191
|
+
chain_id: Optional[int] = None,
|
|
192
|
+
limit: int = 10,
|
|
193
|
+
where: Optional[dict[str, Any]] = None,
|
|
194
|
+
) -> list[Market]:
|
|
195
|
+
"""Return the ``limit`` markets with the highest supply APY.
|
|
196
|
+
|
|
197
|
+
Sorts by raw supply APY descending, which can surface tiny, fully
|
|
198
|
+
utilized markets whose instantaneous rate spikes; pass
|
|
199
|
+
``where={"utilization_lte": 0.99}`` to exclude them.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
chain_id: Restrict to one chain, or omit for all chains.
|
|
203
|
+
limit: How many markets to return.
|
|
204
|
+
where: Additional raw ``MarketFilters`` to apply.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Up to ``limit`` markets ordered by descending supply APY.
|
|
208
|
+
|
|
209
|
+
Raises:
|
|
210
|
+
HTTPError: On a non-2xx HTTP status.
|
|
211
|
+
GraphQLError: If the API returns a GraphQL error.
|
|
212
|
+
"""
|
|
213
|
+
return await self.get_markets(
|
|
214
|
+
chain_id=chain_id,
|
|
215
|
+
first=limit,
|
|
216
|
+
order_by="supply_apy",
|
|
217
|
+
order_direction="Desc",
|
|
218
|
+
where=where,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
async def iter_markets(
|
|
222
|
+
self,
|
|
223
|
+
*,
|
|
224
|
+
chain_id: Optional[int] = None,
|
|
225
|
+
page_size: int = 100,
|
|
226
|
+
order_by: Optional[str] = None,
|
|
227
|
+
order_direction: str = "Desc",
|
|
228
|
+
where: Optional[dict[str, Any]] = None,
|
|
229
|
+
) -> list[Market]:
|
|
230
|
+
"""Fetch *all* matching markets, paginating automatically via ``skip``.
|
|
231
|
+
|
|
232
|
+
Repeatedly requests pages of ``page_size`` until a short page signals the
|
|
233
|
+
end, accumulating every market into one list.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
chain_id: Restrict to one chain, or omit for all chains.
|
|
237
|
+
page_size: Number of markets requested per underlying page.
|
|
238
|
+
order_by: Sort field (see :meth:`get_markets`).
|
|
239
|
+
order_direction: ``"Desc"`` (default) or ``"Asc"``.
|
|
240
|
+
where: Additional raw ``MarketFilters`` to apply.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Every matching market across all pages.
|
|
244
|
+
|
|
245
|
+
Raises:
|
|
246
|
+
HTTPError: On a non-2xx HTTP status.
|
|
247
|
+
GraphQLError: If the API returns a GraphQL error.
|
|
248
|
+
"""
|
|
249
|
+
out: list[Market] = []
|
|
250
|
+
skip = 0
|
|
251
|
+
while True:
|
|
252
|
+
page = await self.get_markets(
|
|
253
|
+
chain_id=chain_id,
|
|
254
|
+
first=page_size,
|
|
255
|
+
skip=skip,
|
|
256
|
+
order_by=order_by,
|
|
257
|
+
order_direction=order_direction,
|
|
258
|
+
where=where,
|
|
259
|
+
)
|
|
260
|
+
out.extend(page)
|
|
261
|
+
if len(page) < page_size:
|
|
262
|
+
break
|
|
263
|
+
skip += page_size
|
|
264
|
+
return out
|
|
265
|
+
|
|
266
|
+
# -- vaults ----------------------------------------------------------
|
|
267
|
+
async def get_vaults(
|
|
268
|
+
self,
|
|
269
|
+
*,
|
|
270
|
+
chain_id: Optional[int] = None,
|
|
271
|
+
first: Optional[int] = 100,
|
|
272
|
+
skip: Optional[int] = 0,
|
|
273
|
+
order_by: Optional[str] = None,
|
|
274
|
+
order_direction: str = "Desc",
|
|
275
|
+
where: Optional[dict[str, Any]] = None,
|
|
276
|
+
) -> list[Vault]:
|
|
277
|
+
"""Fetch a single page of MetaMorpho vaults, optionally filtered/sorted.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
chain_id: Restrict to one chain, or omit for all chains.
|
|
281
|
+
first: Page size (max number of vaults to return).
|
|
282
|
+
skip: Offset for pagination.
|
|
283
|
+
order_by: Sort field. Accepts a friendly key (``"net_apy"``,
|
|
284
|
+
``"total_assets_usd"``, ``"apy"``, ``"fee"``, …) or a raw
|
|
285
|
+
``VaultOrderBy`` enum value.
|
|
286
|
+
order_direction: ``"Desc"`` (default) or ``"Asc"``.
|
|
287
|
+
where: Additional raw ``VaultFilters`` to apply.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
The page of vaults as :class:`~morpho_blue.models.Vault` objects.
|
|
291
|
+
|
|
292
|
+
Raises:
|
|
293
|
+
HTTPError: On a non-2xx HTTP status.
|
|
294
|
+
GraphQLError: If the API returns a GraphQL error.
|
|
295
|
+
"""
|
|
296
|
+
variables = build_vault_variables(
|
|
297
|
+
first=first,
|
|
298
|
+
skip=skip,
|
|
299
|
+
chain_id=chain_id,
|
|
300
|
+
order_by=order_by,
|
|
301
|
+
order_direction=order_direction,
|
|
302
|
+
where=where,
|
|
303
|
+
)
|
|
304
|
+
data = await self.execute(queries.VAULTS_QUERY, variables)
|
|
305
|
+
return vaults_from_data(data)
|
|
306
|
+
|
|
307
|
+
async def get_vault(self, address: str, *, chain_id: Optional[int] = None) -> Vault:
|
|
308
|
+
"""Fetch a single vault by its contract ``address``.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
address: The vault contract address.
|
|
312
|
+
chain_id: The chain the vault lives on; recommended to disambiguate
|
|
313
|
+
the same address across chains.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
The :class:`~morpho_blue.models.Vault`.
|
|
317
|
+
|
|
318
|
+
Raises:
|
|
319
|
+
HTTPError: On a non-2xx HTTP status.
|
|
320
|
+
GraphQLError: If the vault is not found or the API errors.
|
|
321
|
+
"""
|
|
322
|
+
data = await self.execute(
|
|
323
|
+
queries.VAULT_BY_ADDRESS_QUERY,
|
|
324
|
+
{"address": address, "chainId": chain_id},
|
|
325
|
+
)
|
|
326
|
+
return vault_from_data(data)
|
|
327
|
+
|
|
328
|
+
async def top_vaults_by_apy(
|
|
329
|
+
self,
|
|
330
|
+
*,
|
|
331
|
+
chain_id: Optional[int] = None,
|
|
332
|
+
limit: int = 10,
|
|
333
|
+
where: Optional[dict[str, Any]] = None,
|
|
334
|
+
) -> list[Vault]:
|
|
335
|
+
"""Return the ``limit`` vaults with the highest net APY.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
chain_id: Restrict to one chain, or omit for all chains.
|
|
339
|
+
limit: How many vaults to return.
|
|
340
|
+
where: Additional raw ``VaultFilters`` to apply.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Up to ``limit`` vaults ordered by descending net APY.
|
|
344
|
+
|
|
345
|
+
Raises:
|
|
346
|
+
HTTPError: On a non-2xx HTTP status.
|
|
347
|
+
GraphQLError: If the API returns a GraphQL error.
|
|
348
|
+
"""
|
|
349
|
+
return await self.get_vaults(
|
|
350
|
+
chain_id=chain_id,
|
|
351
|
+
first=limit,
|
|
352
|
+
order_by="net_apy",
|
|
353
|
+
order_direction="Desc",
|
|
354
|
+
where=where,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# -- users / positions ----------------------------------------------
|
|
358
|
+
async def get_user(self, address: str, *, chain_id: Optional[int] = None) -> User:
|
|
359
|
+
"""Fetch a wallet's market and vault positions by ``address``.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
address: The wallet address to look up.
|
|
363
|
+
chain_id: The chain to read positions on; recommended.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
A :class:`~morpho_blue.models.User` with ``market_positions`` and
|
|
367
|
+
``vault_positions``.
|
|
368
|
+
|
|
369
|
+
Raises:
|
|
370
|
+
HTTPError: On a non-2xx HTTP status.
|
|
371
|
+
GraphQLError: If the API returns a GraphQL error.
|
|
372
|
+
"""
|
|
373
|
+
data = await self.execute(
|
|
374
|
+
queries.USER_BY_ADDRESS_QUERY,
|
|
375
|
+
{"address": address, "chainId": chain_id},
|
|
376
|
+
)
|
|
377
|
+
return user_from_data(data)
|