polynode 0.5.5__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.
- polynode/__init__.py +41 -0
- polynode/_version.py +1 -0
- polynode/cache/__init__.py +11 -0
- polynode/client.py +635 -0
- polynode/engine.py +201 -0
- polynode/errors.py +35 -0
- polynode/orderbook.py +243 -0
- polynode/orderbook_state.py +77 -0
- polynode/redemption_watcher.py +339 -0
- polynode/short_form.py +321 -0
- polynode/subscription.py +137 -0
- polynode/testing.py +83 -0
- polynode/trading/__init__.py +19 -0
- polynode/trading/clob_api.py +158 -0
- polynode/trading/constants.py +31 -0
- polynode/trading/cosigner.py +86 -0
- polynode/trading/eip712.py +163 -0
- polynode/trading/onboarding.py +242 -0
- polynode/trading/signer.py +91 -0
- polynode/trading/sqlite_backend.py +208 -0
- polynode/trading/trader.py +506 -0
- polynode/trading/types.py +191 -0
- polynode/types/__init__.py +8 -0
- polynode/types/enums.py +51 -0
- polynode/types/events.py +270 -0
- polynode/types/orderbook.py +66 -0
- polynode/types/rest.py +376 -0
- polynode/types/short_form.py +35 -0
- polynode/types/ws.py +38 -0
- polynode/ws.py +278 -0
- polynode-0.5.5.dist-info/METADATA +133 -0
- polynode-0.5.5.dist-info/RECORD +33 -0
- polynode-0.5.5.dist-info/WHEEL +4 -0
polynode/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""PolyNode Python SDK — real-time prediction market data and trading."""
|
|
2
|
+
|
|
3
|
+
from ._version import __version__
|
|
4
|
+
from .client import AsyncPolyNode, PolyNode
|
|
5
|
+
from .engine import EngineView, OrderbookEngine
|
|
6
|
+
from .errors import ApiError, PolyNodeError, WsError
|
|
7
|
+
from .orderbook import OrderbookWS
|
|
8
|
+
from .orderbook_state import LocalOrderbook
|
|
9
|
+
from .redemption_watcher import RedeemableAlert, RedemptionWatcher, TrackedPosition
|
|
10
|
+
from .short_form import ShortFormStream
|
|
11
|
+
from .subscription import Subscription, SubscriptionBuilder
|
|
12
|
+
from .testing import get_active_test_wallet, get_active_test_wallets
|
|
13
|
+
from .ws import PolyNodeWS
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"__version__",
|
|
17
|
+
# Clients
|
|
18
|
+
"PolyNode",
|
|
19
|
+
"AsyncPolyNode",
|
|
20
|
+
# WebSocket
|
|
21
|
+
"PolyNodeWS",
|
|
22
|
+
"SubscriptionBuilder",
|
|
23
|
+
"Subscription",
|
|
24
|
+
# Orderbook
|
|
25
|
+
"OrderbookWS",
|
|
26
|
+
"LocalOrderbook",
|
|
27
|
+
"OrderbookEngine",
|
|
28
|
+
"EngineView",
|
|
29
|
+
# Streams
|
|
30
|
+
"ShortFormStream",
|
|
31
|
+
"RedemptionWatcher",
|
|
32
|
+
"RedeemableAlert",
|
|
33
|
+
"TrackedPosition",
|
|
34
|
+
# Testing
|
|
35
|
+
"get_active_test_wallet",
|
|
36
|
+
"get_active_test_wallets",
|
|
37
|
+
# Errors
|
|
38
|
+
"PolyNodeError",
|
|
39
|
+
"ApiError",
|
|
40
|
+
"WsError",
|
|
41
|
+
]
|
polynode/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.5"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Cache module placeholder — local SQLite-backed trade/position history.
|
|
2
|
+
|
|
3
|
+
This module will be implemented in a future release.
|
|
4
|
+
The cache/ directory structure matches the TypeScript SDK:
|
|
5
|
+
- sqlite_backend.py — SQLite storage (settlements, trades, positions)
|
|
6
|
+
- watchlist.py — JSON watchlist file management
|
|
7
|
+
- backfill.py — Rate-limited REST backfill orchestrator
|
|
8
|
+
- query_builder.py — Fluent query builder
|
|
9
|
+
- views.py — Analytical views (wallet_dashboard, leaderboard, etc.)
|
|
10
|
+
- export.py — CSV/JSON export
|
|
11
|
+
"""
|
polynode/client.py
ADDED
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
"""Sync and async REST clients for the PolyNode API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .errors import ApiError, PolyNodeError
|
|
11
|
+
from .types.rest import (
|
|
12
|
+
ActivityResponse,
|
|
13
|
+
ApiKeyResponse,
|
|
14
|
+
CandlesResponse,
|
|
15
|
+
EventDetailResponse,
|
|
16
|
+
EventSearchResponse,
|
|
17
|
+
LeaderboardResponse,
|
|
18
|
+
MarketsByCategoryResponse,
|
|
19
|
+
MarketsListResponse,
|
|
20
|
+
MarketsResponse,
|
|
21
|
+
MidpointResponse,
|
|
22
|
+
MoversResponse,
|
|
23
|
+
OrderbookResponse,
|
|
24
|
+
SearchResponse,
|
|
25
|
+
SettlementsResponse,
|
|
26
|
+
SpreadResponse,
|
|
27
|
+
StatusResponse,
|
|
28
|
+
TraderPnlResponse,
|
|
29
|
+
TraderProfile,
|
|
30
|
+
TrendingResponse,
|
|
31
|
+
WalletResponse,
|
|
32
|
+
)
|
|
33
|
+
from .types.enums import CandleResolution, MarketSortField
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PolyNode:
|
|
37
|
+
"""Synchronous PolyNode REST client."""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
api_key: str,
|
|
42
|
+
*,
|
|
43
|
+
base_url: str = "https://api.polynode.dev",
|
|
44
|
+
ws_url: str = "wss://ws.polynode.dev/ws",
|
|
45
|
+
ob_url: str = "wss://ob.polynode.dev/ws",
|
|
46
|
+
rpc_url: str = "https://rpc.polynode.dev",
|
|
47
|
+
timeout: float = 10.0,
|
|
48
|
+
) -> None:
|
|
49
|
+
if not api_key:
|
|
50
|
+
raise PolyNodeError("api_key is required")
|
|
51
|
+
self.api_key = api_key
|
|
52
|
+
self.base_url = base_url.rstrip("/")
|
|
53
|
+
self.ws_url = ws_url.rstrip("/")
|
|
54
|
+
self.ob_url = ob_url.rstrip("/")
|
|
55
|
+
self.rpc_url = rpc_url.rstrip("/")
|
|
56
|
+
self._timeout = timeout
|
|
57
|
+
self._http = httpx.Client(timeout=timeout)
|
|
58
|
+
self._ws = None
|
|
59
|
+
self._orderbook = None
|
|
60
|
+
|
|
61
|
+
def __enter__(self) -> PolyNode:
|
|
62
|
+
return self
|
|
63
|
+
|
|
64
|
+
def __exit__(self, *args: Any) -> None:
|
|
65
|
+
self.close()
|
|
66
|
+
|
|
67
|
+
def close(self) -> None:
|
|
68
|
+
self._http.close()
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def ws(self):
|
|
72
|
+
"""Lazy-initialized WebSocket client."""
|
|
73
|
+
if self._ws is None:
|
|
74
|
+
from .ws import PolyNodeWS
|
|
75
|
+
self._ws = PolyNodeWS(self.api_key, self.ws_url)
|
|
76
|
+
return self._ws
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def orderbook(self):
|
|
80
|
+
"""Lazy-initialized orderbook WebSocket client."""
|
|
81
|
+
if self._orderbook is None:
|
|
82
|
+
from .orderbook import OrderbookWS
|
|
83
|
+
self._orderbook = OrderbookWS(self.api_key, self.ob_url)
|
|
84
|
+
return self._orderbook
|
|
85
|
+
|
|
86
|
+
# ── Internal ──
|
|
87
|
+
|
|
88
|
+
def _fetch(
|
|
89
|
+
self,
|
|
90
|
+
path: str,
|
|
91
|
+
*,
|
|
92
|
+
method: str = "GET",
|
|
93
|
+
body: dict | None = None,
|
|
94
|
+
auth: bool = True,
|
|
95
|
+
query: dict[str, Any] | None = None,
|
|
96
|
+
) -> Any:
|
|
97
|
+
url = f"{self.base_url}{path}"
|
|
98
|
+
params = {}
|
|
99
|
+
if query:
|
|
100
|
+
params = {k: str(v) for k, v in query.items() if v is not None}
|
|
101
|
+
|
|
102
|
+
headers = {}
|
|
103
|
+
if auth:
|
|
104
|
+
headers["x-api-key"] = self.api_key
|
|
105
|
+
|
|
106
|
+
resp = self._http.request(
|
|
107
|
+
method,
|
|
108
|
+
url,
|
|
109
|
+
params=params or None,
|
|
110
|
+
headers=headers,
|
|
111
|
+
json=body,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if resp.status_code >= 400:
|
|
115
|
+
msg = f"HTTP {resp.status_code}"
|
|
116
|
+
try:
|
|
117
|
+
err_body = resp.json()
|
|
118
|
+
msg = err_body.get("error") or err_body.get("message") or msg
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
raise ApiError(msg, resp.status_code)
|
|
122
|
+
|
|
123
|
+
content_type = resp.headers.get("content-type", "")
|
|
124
|
+
if "application/json" in content_type:
|
|
125
|
+
return resp.json()
|
|
126
|
+
return resp.text
|
|
127
|
+
|
|
128
|
+
# ── System ──
|
|
129
|
+
|
|
130
|
+
def healthz(self) -> str:
|
|
131
|
+
return self._fetch("/healthz", auth=False)
|
|
132
|
+
|
|
133
|
+
def readyz(self) -> Any:
|
|
134
|
+
return self._fetch("/readyz", auth=False)
|
|
135
|
+
|
|
136
|
+
def status(self) -> StatusResponse:
|
|
137
|
+
return StatusResponse.model_validate(self._fetch("/v1/status"))
|
|
138
|
+
|
|
139
|
+
def create_key(self, name: str = "unnamed") -> ApiKeyResponse:
|
|
140
|
+
return ApiKeyResponse.model_validate(
|
|
141
|
+
self._fetch("/v1/keys", method="POST", body={"name": name}, auth=False)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# ── Markets ──
|
|
145
|
+
|
|
146
|
+
def markets(self, *, count: int | None = None) -> MarketsResponse:
|
|
147
|
+
return MarketsResponse.model_validate(
|
|
148
|
+
self._fetch("/v1/markets", query={"count": count})
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def market(self, token_id: str) -> dict:
|
|
152
|
+
return self._fetch(f"/v1/markets/{quote(token_id, safe='')}")
|
|
153
|
+
|
|
154
|
+
def market_by_slug(self, slug: str) -> dict:
|
|
155
|
+
return self._fetch(f"/v1/markets/slug/{quote(slug, safe='')}")
|
|
156
|
+
|
|
157
|
+
def market_by_condition(self, condition_id: str) -> dict:
|
|
158
|
+
return self._fetch(f"/v1/markets/condition/{quote(condition_id, safe='')}")
|
|
159
|
+
|
|
160
|
+
def markets_list(
|
|
161
|
+
self,
|
|
162
|
+
*,
|
|
163
|
+
count: int | None = None,
|
|
164
|
+
sort: MarketSortField | None = None,
|
|
165
|
+
category: str | None = None,
|
|
166
|
+
min_volume: float | None = None,
|
|
167
|
+
active_only: bool | None = None,
|
|
168
|
+
cursor: int | None = None,
|
|
169
|
+
) -> MarketsListResponse:
|
|
170
|
+
return MarketsListResponse.model_validate(
|
|
171
|
+
self._fetch(
|
|
172
|
+
"/v1/markets/list",
|
|
173
|
+
query={
|
|
174
|
+
"count": count,
|
|
175
|
+
"sort": sort,
|
|
176
|
+
"category": category,
|
|
177
|
+
"min_volume": min_volume,
|
|
178
|
+
"active_only": active_only,
|
|
179
|
+
"cursor": cursor,
|
|
180
|
+
},
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def search(
|
|
185
|
+
self, query: str, *, limit: int | None = None, include_inactive: bool | None = None
|
|
186
|
+
) -> SearchResponse:
|
|
187
|
+
return SearchResponse.model_validate(
|
|
188
|
+
self._fetch("/v1/search", query={"q": query, "limit": limit, "include_inactive": include_inactive})
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# ── Pricing ──
|
|
192
|
+
|
|
193
|
+
def candles(
|
|
194
|
+
self,
|
|
195
|
+
token_id: str,
|
|
196
|
+
*,
|
|
197
|
+
resolution: CandleResolution | None = None,
|
|
198
|
+
limit: int | None = None,
|
|
199
|
+
) -> CandlesResponse:
|
|
200
|
+
return CandlesResponse.model_validate(
|
|
201
|
+
self._fetch(
|
|
202
|
+
f"/v1/candles/{quote(token_id, safe='')}",
|
|
203
|
+
query={"resolution": resolution, "limit": limit},
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def stats(self, token_id: str) -> dict:
|
|
208
|
+
return self._fetch(f"/v1/stats/{quote(token_id, safe='')}")
|
|
209
|
+
|
|
210
|
+
# ── Settlements ──
|
|
211
|
+
|
|
212
|
+
def recent_settlements(self, *, count: int | None = None) -> SettlementsResponse:
|
|
213
|
+
return SettlementsResponse.model_validate(
|
|
214
|
+
self._fetch("/v1/settlements/recent", query={"count": count})
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def token_settlements(self, token_id: str, *, count: int | None = None) -> SettlementsResponse:
|
|
218
|
+
return SettlementsResponse.model_validate(
|
|
219
|
+
self._fetch(f"/v1/settlements/token/{quote(token_id, safe='')}", query={"count": count})
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def wallet_settlements(self, address: str, *, count: int | None = None) -> SettlementsResponse:
|
|
223
|
+
return SettlementsResponse.model_validate(
|
|
224
|
+
self._fetch(f"/v1/settlements/wallet/{quote(address, safe='')}", query={"count": count})
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# ── Wallets ──
|
|
228
|
+
|
|
229
|
+
def wallet(self, address: str) -> WalletResponse:
|
|
230
|
+
return WalletResponse.model_validate(
|
|
231
|
+
self._fetch(f"/v1/wallets/{quote(address, safe='')}")
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def wallet_trades(self, address: str, *, limit: int | None = None, offset: int | None = None) -> dict:
|
|
235
|
+
return self._fetch(
|
|
236
|
+
f"/v1/wallets/{quote(address, safe='')}/trades",
|
|
237
|
+
query={"limit": limit, "offset": offset},
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def market_trades(
|
|
241
|
+
self,
|
|
242
|
+
id: str,
|
|
243
|
+
*,
|
|
244
|
+
limit: int | None = None,
|
|
245
|
+
offset: int | None = None,
|
|
246
|
+
side: str | None = None,
|
|
247
|
+
user: str | None = None,
|
|
248
|
+
) -> dict:
|
|
249
|
+
return self._fetch(
|
|
250
|
+
f"/v1/markets/{quote(id, safe='')}/trades",
|
|
251
|
+
query={"limit": limit, "offset": offset, "side": side, "user": user},
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
def wallet_positions(self, address: str, *, limit: int | None = None, offset: int | None = None) -> dict:
|
|
255
|
+
return self._fetch(
|
|
256
|
+
f"/v1/wallets/{quote(address, safe='')}/positions",
|
|
257
|
+
query={"limit": limit, "offset": offset},
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
def wallet_onchain_positions(self, address: str) -> dict:
|
|
261
|
+
return self._fetch(f"/v2/wallets/{quote(address, safe='')}/positions/onchain")
|
|
262
|
+
|
|
263
|
+
# ── Orderbook (REST) ──
|
|
264
|
+
|
|
265
|
+
def orderbook_rest(self, token_id: str) -> OrderbookResponse:
|
|
266
|
+
return OrderbookResponse.model_validate(
|
|
267
|
+
self._fetch(f"/v1/orderbook/{quote(token_id, safe='')}")
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def midpoint(self, token_id: str) -> MidpointResponse:
|
|
271
|
+
return MidpointResponse.model_validate(
|
|
272
|
+
self._fetch(f"/v1/midpoint/{quote(token_id, safe='')}")
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
def spread(self, token_id: str) -> SpreadResponse:
|
|
276
|
+
return SpreadResponse.model_validate(
|
|
277
|
+
self._fetch(f"/v1/spread/{quote(token_id, safe='')}")
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# ── Enriched Data ──
|
|
281
|
+
|
|
282
|
+
def leaderboard(self, *, period: str | None = None, sort: str | None = None) -> LeaderboardResponse:
|
|
283
|
+
return LeaderboardResponse.model_validate(
|
|
284
|
+
self._fetch("/v1/leaderboard", query={"period": period, "sort": sort})
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def trending(self) -> TrendingResponse:
|
|
288
|
+
return TrendingResponse.model_validate(self._fetch("/v1/trending"))
|
|
289
|
+
|
|
290
|
+
def activity(self) -> ActivityResponse:
|
|
291
|
+
return ActivityResponse.model_validate(self._fetch("/v1/activity"))
|
|
292
|
+
|
|
293
|
+
def movers(self) -> MoversResponse:
|
|
294
|
+
return MoversResponse.model_validate(self._fetch("/v1/movers"))
|
|
295
|
+
|
|
296
|
+
def trader_profile(self, wallet: str) -> TraderProfile:
|
|
297
|
+
return TraderProfile.model_validate(
|
|
298
|
+
self._fetch(f"/v1/trader/{quote(wallet, safe='')}")
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
def trader_pnl(self, wallet: str, *, period: str | None = None) -> TraderPnlResponse:
|
|
302
|
+
return TraderPnlResponse.model_validate(
|
|
303
|
+
self._fetch(f"/v1/trader/{quote(wallet, safe='')}/pnl", query={"period": period})
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def event(self, slug: str) -> EventDetailResponse:
|
|
307
|
+
return EventDetailResponse.model_validate(
|
|
308
|
+
self._fetch(f"/v1/event/{quote(slug, safe='')}")
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
def search_events(self, query: str, *, limit: int | None = None) -> EventSearchResponse:
|
|
312
|
+
return EventSearchResponse.model_validate(
|
|
313
|
+
self._fetch("/v1/events/search", query={"q": query, "limit": limit})
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
def markets_by_category(self, category: str) -> MarketsByCategoryResponse:
|
|
317
|
+
return MarketsByCategoryResponse.model_validate(
|
|
318
|
+
self._fetch(f"/v1/markets/{quote(category, safe='')}")
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# ── RPC ──
|
|
322
|
+
|
|
323
|
+
def rpc(self, method: str, params: list | None = None) -> Any:
|
|
324
|
+
resp = self._http.post(
|
|
325
|
+
self.rpc_url,
|
|
326
|
+
headers={"Content-Type": "application/json", "x-api-key": self.api_key},
|
|
327
|
+
json={"jsonrpc": "2.0", "method": method, "params": params or [], "id": 1},
|
|
328
|
+
)
|
|
329
|
+
if resp.status_code >= 400:
|
|
330
|
+
raise ApiError(f"RPC HTTP {resp.status_code}", resp.status_code)
|
|
331
|
+
data = resp.json()
|
|
332
|
+
if data.get("error"):
|
|
333
|
+
raise ApiError(data["error"]["message"], data["error"]["code"])
|
|
334
|
+
return data.get("result")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class AsyncPolyNode:
|
|
338
|
+
"""Asynchronous PolyNode REST client."""
|
|
339
|
+
|
|
340
|
+
def __init__(
|
|
341
|
+
self,
|
|
342
|
+
api_key: str,
|
|
343
|
+
*,
|
|
344
|
+
base_url: str = "https://api.polynode.dev",
|
|
345
|
+
ws_url: str = "wss://ws.polynode.dev/ws",
|
|
346
|
+
ob_url: str = "wss://ob.polynode.dev/ws",
|
|
347
|
+
rpc_url: str = "https://rpc.polynode.dev",
|
|
348
|
+
timeout: float = 10.0,
|
|
349
|
+
) -> None:
|
|
350
|
+
if not api_key:
|
|
351
|
+
raise PolyNodeError("api_key is required")
|
|
352
|
+
self.api_key = api_key
|
|
353
|
+
self.base_url = base_url.rstrip("/")
|
|
354
|
+
self.ws_url = ws_url.rstrip("/")
|
|
355
|
+
self.ob_url = ob_url.rstrip("/")
|
|
356
|
+
self.rpc_url = rpc_url.rstrip("/")
|
|
357
|
+
self._timeout = timeout
|
|
358
|
+
self._http = httpx.AsyncClient(timeout=timeout)
|
|
359
|
+
self._ws = None
|
|
360
|
+
self._orderbook = None
|
|
361
|
+
|
|
362
|
+
async def __aenter__(self) -> AsyncPolyNode:
|
|
363
|
+
return self
|
|
364
|
+
|
|
365
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
366
|
+
await self.close()
|
|
367
|
+
|
|
368
|
+
async def close(self) -> None:
|
|
369
|
+
await self._http.aclose()
|
|
370
|
+
if self._ws:
|
|
371
|
+
self._ws.disconnect()
|
|
372
|
+
|
|
373
|
+
@property
|
|
374
|
+
def ws(self):
|
|
375
|
+
if self._ws is None:
|
|
376
|
+
from .ws import PolyNodeWS
|
|
377
|
+
self._ws = PolyNodeWS(self.api_key, self.ws_url)
|
|
378
|
+
return self._ws
|
|
379
|
+
|
|
380
|
+
@property
|
|
381
|
+
def orderbook(self):
|
|
382
|
+
if self._orderbook is None:
|
|
383
|
+
from .orderbook import OrderbookWS
|
|
384
|
+
self._orderbook = OrderbookWS(self.api_key, self.ob_url)
|
|
385
|
+
return self._orderbook
|
|
386
|
+
|
|
387
|
+
# ── Internal ──
|
|
388
|
+
|
|
389
|
+
async def _fetch(
|
|
390
|
+
self,
|
|
391
|
+
path: str,
|
|
392
|
+
*,
|
|
393
|
+
method: str = "GET",
|
|
394
|
+
body: dict | None = None,
|
|
395
|
+
auth: bool = True,
|
|
396
|
+
query: dict[str, Any] | None = None,
|
|
397
|
+
) -> Any:
|
|
398
|
+
url = f"{self.base_url}{path}"
|
|
399
|
+
params = {}
|
|
400
|
+
if query:
|
|
401
|
+
params = {k: str(v) for k, v in query.items() if v is not None}
|
|
402
|
+
|
|
403
|
+
headers = {}
|
|
404
|
+
if auth:
|
|
405
|
+
headers["x-api-key"] = self.api_key
|
|
406
|
+
|
|
407
|
+
resp = await self._http.request(
|
|
408
|
+
method,
|
|
409
|
+
url,
|
|
410
|
+
params=params or None,
|
|
411
|
+
headers=headers,
|
|
412
|
+
json=body,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
if resp.status_code >= 400:
|
|
416
|
+
msg = f"HTTP {resp.status_code}"
|
|
417
|
+
try:
|
|
418
|
+
err_body = resp.json()
|
|
419
|
+
msg = err_body.get("error") or err_body.get("message") or msg
|
|
420
|
+
except Exception:
|
|
421
|
+
pass
|
|
422
|
+
raise ApiError(msg, resp.status_code)
|
|
423
|
+
|
|
424
|
+
content_type = resp.headers.get("content-type", "")
|
|
425
|
+
if "application/json" in content_type:
|
|
426
|
+
return resp.json()
|
|
427
|
+
return resp.text
|
|
428
|
+
|
|
429
|
+
# ── System ──
|
|
430
|
+
|
|
431
|
+
async def healthz(self) -> str:
|
|
432
|
+
return await self._fetch("/healthz", auth=False)
|
|
433
|
+
|
|
434
|
+
async def readyz(self) -> Any:
|
|
435
|
+
return await self._fetch("/readyz", auth=False)
|
|
436
|
+
|
|
437
|
+
async def status(self) -> StatusResponse:
|
|
438
|
+
return StatusResponse.model_validate(await self._fetch("/v1/status"))
|
|
439
|
+
|
|
440
|
+
async def create_key(self, name: str = "unnamed") -> ApiKeyResponse:
|
|
441
|
+
return ApiKeyResponse.model_validate(
|
|
442
|
+
await self._fetch("/v1/keys", method="POST", body={"name": name}, auth=False)
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# ── Markets ──
|
|
446
|
+
|
|
447
|
+
async def markets(self, *, count: int | None = None) -> MarketsResponse:
|
|
448
|
+
return MarketsResponse.model_validate(
|
|
449
|
+
await self._fetch("/v1/markets", query={"count": count})
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
async def market(self, token_id: str) -> dict:
|
|
453
|
+
return await self._fetch(f"/v1/markets/{quote(token_id, safe='')}")
|
|
454
|
+
|
|
455
|
+
async def market_by_slug(self, slug: str) -> dict:
|
|
456
|
+
return await self._fetch(f"/v1/markets/slug/{quote(slug, safe='')}")
|
|
457
|
+
|
|
458
|
+
async def market_by_condition(self, condition_id: str) -> dict:
|
|
459
|
+
return await self._fetch(f"/v1/markets/condition/{quote(condition_id, safe='')}")
|
|
460
|
+
|
|
461
|
+
async def markets_list(
|
|
462
|
+
self,
|
|
463
|
+
*,
|
|
464
|
+
count: int | None = None,
|
|
465
|
+
sort: MarketSortField | None = None,
|
|
466
|
+
category: str | None = None,
|
|
467
|
+
min_volume: float | None = None,
|
|
468
|
+
active_only: bool | None = None,
|
|
469
|
+
cursor: int | None = None,
|
|
470
|
+
) -> MarketsListResponse:
|
|
471
|
+
return MarketsListResponse.model_validate(
|
|
472
|
+
await self._fetch(
|
|
473
|
+
"/v1/markets/list",
|
|
474
|
+
query={
|
|
475
|
+
"count": count,
|
|
476
|
+
"sort": sort,
|
|
477
|
+
"category": category,
|
|
478
|
+
"min_volume": min_volume,
|
|
479
|
+
"active_only": active_only,
|
|
480
|
+
"cursor": cursor,
|
|
481
|
+
},
|
|
482
|
+
)
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
async def search(
|
|
486
|
+
self, query: str, *, limit: int | None = None, include_inactive: bool | None = None
|
|
487
|
+
) -> SearchResponse:
|
|
488
|
+
return SearchResponse.model_validate(
|
|
489
|
+
await self._fetch("/v1/search", query={"q": query, "limit": limit, "include_inactive": include_inactive})
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# ── Pricing ──
|
|
493
|
+
|
|
494
|
+
async def candles(
|
|
495
|
+
self,
|
|
496
|
+
token_id: str,
|
|
497
|
+
*,
|
|
498
|
+
resolution: CandleResolution | None = None,
|
|
499
|
+
limit: int | None = None,
|
|
500
|
+
) -> CandlesResponse:
|
|
501
|
+
return CandlesResponse.model_validate(
|
|
502
|
+
await self._fetch(
|
|
503
|
+
f"/v1/candles/{quote(token_id, safe='')}",
|
|
504
|
+
query={"resolution": resolution, "limit": limit},
|
|
505
|
+
)
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
async def stats(self, token_id: str) -> dict:
|
|
509
|
+
return await self._fetch(f"/v1/stats/{quote(token_id, safe='')}")
|
|
510
|
+
|
|
511
|
+
# ── Settlements ──
|
|
512
|
+
|
|
513
|
+
async def recent_settlements(self, *, count: int | None = None) -> SettlementsResponse:
|
|
514
|
+
return SettlementsResponse.model_validate(
|
|
515
|
+
await self._fetch("/v1/settlements/recent", query={"count": count})
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
async def token_settlements(self, token_id: str, *, count: int | None = None) -> SettlementsResponse:
|
|
519
|
+
return SettlementsResponse.model_validate(
|
|
520
|
+
await self._fetch(f"/v1/settlements/token/{quote(token_id, safe='')}", query={"count": count})
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
async def wallet_settlements(self, address: str, *, count: int | None = None) -> SettlementsResponse:
|
|
524
|
+
return SettlementsResponse.model_validate(
|
|
525
|
+
await self._fetch(f"/v1/settlements/wallet/{quote(address, safe='')}", query={"count": count})
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
# ── Wallets ──
|
|
529
|
+
|
|
530
|
+
async def wallet(self, address: str) -> WalletResponse:
|
|
531
|
+
return WalletResponse.model_validate(
|
|
532
|
+
await self._fetch(f"/v1/wallets/{quote(address, safe='')}")
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
async def wallet_trades(self, address: str, *, limit: int | None = None, offset: int | None = None) -> dict:
|
|
536
|
+
return await self._fetch(
|
|
537
|
+
f"/v1/wallets/{quote(address, safe='')}/trades",
|
|
538
|
+
query={"limit": limit, "offset": offset},
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
async def market_trades(
|
|
542
|
+
self,
|
|
543
|
+
id: str,
|
|
544
|
+
*,
|
|
545
|
+
limit: int | None = None,
|
|
546
|
+
offset: int | None = None,
|
|
547
|
+
side: str | None = None,
|
|
548
|
+
user: str | None = None,
|
|
549
|
+
) -> dict:
|
|
550
|
+
return await self._fetch(
|
|
551
|
+
f"/v1/markets/{quote(id, safe='')}/trades",
|
|
552
|
+
query={"limit": limit, "offset": offset, "side": side, "user": user},
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
async def wallet_positions(self, address: str, *, limit: int | None = None, offset: int | None = None) -> dict:
|
|
556
|
+
return await self._fetch(
|
|
557
|
+
f"/v1/wallets/{quote(address, safe='')}/positions",
|
|
558
|
+
query={"limit": limit, "offset": offset},
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
async def wallet_onchain_positions(self, address: str) -> dict:
|
|
562
|
+
return await self._fetch(f"/v2/wallets/{quote(address, safe='')}/positions/onchain")
|
|
563
|
+
|
|
564
|
+
# ── Orderbook (REST) ──
|
|
565
|
+
|
|
566
|
+
async def orderbook_rest(self, token_id: str) -> OrderbookResponse:
|
|
567
|
+
return OrderbookResponse.model_validate(
|
|
568
|
+
await self._fetch(f"/v1/orderbook/{quote(token_id, safe='')}")
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
async def midpoint(self, token_id: str) -> MidpointResponse:
|
|
572
|
+
return MidpointResponse.model_validate(
|
|
573
|
+
await self._fetch(f"/v1/midpoint/{quote(token_id, safe='')}")
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
async def spread(self, token_id: str) -> SpreadResponse:
|
|
577
|
+
return SpreadResponse.model_validate(
|
|
578
|
+
await self._fetch(f"/v1/spread/{quote(token_id, safe='')}")
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
# ── Enriched Data ──
|
|
582
|
+
|
|
583
|
+
async def leaderboard(self, *, period: str | None = None, sort: str | None = None) -> LeaderboardResponse:
|
|
584
|
+
return LeaderboardResponse.model_validate(
|
|
585
|
+
await self._fetch("/v1/leaderboard", query={"period": period, "sort": sort})
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
async def trending(self) -> TrendingResponse:
|
|
589
|
+
return TrendingResponse.model_validate(await self._fetch("/v1/trending"))
|
|
590
|
+
|
|
591
|
+
async def activity(self) -> ActivityResponse:
|
|
592
|
+
return ActivityResponse.model_validate(await self._fetch("/v1/activity"))
|
|
593
|
+
|
|
594
|
+
async def movers(self) -> MoversResponse:
|
|
595
|
+
return MoversResponse.model_validate(await self._fetch("/v1/movers"))
|
|
596
|
+
|
|
597
|
+
async def trader_profile(self, wallet: str) -> TraderProfile:
|
|
598
|
+
return TraderProfile.model_validate(
|
|
599
|
+
await self._fetch(f"/v1/trader/{quote(wallet, safe='')}")
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
async def trader_pnl(self, wallet: str, *, period: str | None = None) -> TraderPnlResponse:
|
|
603
|
+
return TraderPnlResponse.model_validate(
|
|
604
|
+
await self._fetch(f"/v1/trader/{quote(wallet, safe='')}/pnl", query={"period": period})
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
async def event(self, slug: str) -> EventDetailResponse:
|
|
608
|
+
return EventDetailResponse.model_validate(
|
|
609
|
+
await self._fetch(f"/v1/event/{quote(slug, safe='')}")
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
async def search_events(self, query: str, *, limit: int | None = None) -> EventSearchResponse:
|
|
613
|
+
return EventSearchResponse.model_validate(
|
|
614
|
+
await self._fetch("/v1/events/search", query={"q": query, "limit": limit})
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
async def markets_by_category(self, category: str) -> MarketsByCategoryResponse:
|
|
618
|
+
return MarketsByCategoryResponse.model_validate(
|
|
619
|
+
await self._fetch(f"/v1/markets/{quote(category, safe='')}")
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
# ── RPC ──
|
|
623
|
+
|
|
624
|
+
async def rpc(self, method: str, params: list | None = None) -> Any:
|
|
625
|
+
resp = await self._http.post(
|
|
626
|
+
self.rpc_url,
|
|
627
|
+
headers={"Content-Type": "application/json", "x-api-key": self.api_key},
|
|
628
|
+
json={"jsonrpc": "2.0", "method": method, "params": params or [], "id": 1},
|
|
629
|
+
)
|
|
630
|
+
if resp.status_code >= 400:
|
|
631
|
+
raise ApiError(f"RPC HTTP {resp.status_code}", resp.status_code)
|
|
632
|
+
data = resp.json()
|
|
633
|
+
if data.get("error"):
|
|
634
|
+
raise ApiError(data["error"]["message"], data["error"]["code"])
|
|
635
|
+
return data.get("result")
|