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 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")