chainstream-sdk 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.
Files changed (261) hide show
  1. chainstream/__init__.py +46 -0
  2. chainstream/client.py +104 -0
  3. chainstream/openapi_client/__init__.py +156 -0
  4. chainstream/openapi_client/api/__init__.py +20 -0
  5. chainstream/openapi_client/api/blockchain_api.py +549 -0
  6. chainstream/openapi_client/api/defi_sol_moonshot_api.py +590 -0
  7. chainstream/openapi_client/api/defi_sol_pumpfun_api.py +314 -0
  8. chainstream/openapi_client/api/dex_api.py +1576 -0
  9. chainstream/openapi_client/api/dex_pool_api.py +645 -0
  10. chainstream/openapi_client/api/endpoint_api.py +1934 -0
  11. chainstream/openapi_client/api/ipfs_api.py +283 -0
  12. chainstream/openapi_client/api/jobs_api.py +562 -0
  13. chainstream/openapi_client/api/kyt_api.py +3743 -0
  14. chainstream/openapi_client/api/ranking_api.py +2067 -0
  15. chainstream/openapi_client/api/red_packet_api.py +2444 -0
  16. chainstream/openapi_client/api/token_api.py +9211 -0
  17. chainstream/openapi_client/api/trade_api.py +1352 -0
  18. chainstream/openapi_client/api/transaction_api.py +882 -0
  19. chainstream/openapi_client/api/wallet_api.py +1608 -0
  20. chainstream/openapi_client/api/watchlist_api.py +316 -0
  21. chainstream/openapi_client/api_client.py +801 -0
  22. chainstream/openapi_client/api_response.py +21 -0
  23. chainstream/openapi_client/configuration.py +572 -0
  24. chainstream/openapi_client/exceptions.py +217 -0
  25. chainstream/openapi_client/models/__init__.py +122 -0
  26. chainstream/openapi_client/models/address_exposure.py +94 -0
  27. chainstream/openapi_client/models/address_risk_response_dto.py +112 -0
  28. chainstream/openapi_client/models/alert_detail.py +100 -0
  29. chainstream/openapi_client/models/alterya_identification.py +106 -0
  30. chainstream/openapi_client/models/balance_change_type.py +38 -0
  31. chainstream/openapi_client/models/balance_token_type.py +38 -0
  32. chainstream/openapi_client/models/balance_update_dto.py +130 -0
  33. chainstream/openapi_client/models/balance_update_page.py +104 -0
  34. chainstream/openapi_client/models/blockchain_dto.py +94 -0
  35. chainstream/openapi_client/models/blockchain_latest_block_dto.py +90 -0
  36. chainstream/openapi_client/models/boolean_result_dto.py +88 -0
  37. chainstream/openapi_client/models/calculate_pnl_input.py +88 -0
  38. chainstream/openapi_client/models/candle.py +101 -0
  39. chainstream/openapi_client/models/chain.py +39 -0
  40. chainstream/openapi_client/models/chain_symbol.py +39 -0
  41. chainstream/openapi_client/models/chainalysis_address_identification.py +92 -0
  42. chainstream/openapi_client/models/claim_red_packet_input.py +103 -0
  43. chainstream/openapi_client/models/create_endpoint_input.py +102 -0
  44. chainstream/openapi_client/models/create_red_packet_input.py +112 -0
  45. chainstream/openapi_client/models/create_red_packet_reply.py +90 -0
  46. chainstream/openapi_client/models/create_token_input.py +110 -0
  47. chainstream/openapi_client/models/create_token_reply.py +90 -0
  48. chainstream/openapi_client/models/dev_token_dto.py +106 -0
  49. chainstream/openapi_client/models/dex_dto.py +94 -0
  50. chainstream/openapi_client/models/dex_page.py +106 -0
  51. chainstream/openapi_client/models/dex_pool_dto.py +161 -0
  52. chainstream/openapi_client/models/dex_pool_snapshot_dto.py +111 -0
  53. chainstream/openapi_client/models/dex_pool_snapshot_page.py +104 -0
  54. chainstream/openapi_client/models/dex_pool_token_liquidity.py +100 -0
  55. chainstream/openapi_client/models/dex_pool_token_snapshot_dto.py +100 -0
  56. chainstream/openapi_client/models/dex_quote_response.py +98 -0
  57. chainstream/openapi_client/models/direct_exposure_detail.py +90 -0
  58. chainstream/openapi_client/models/endpoint_list_response.py +102 -0
  59. chainstream/openapi_client/models/endpoint_operation_response.py +88 -0
  60. chainstream/openapi_client/models/endpoint_response.py +108 -0
  61. chainstream/openapi_client/models/endpoint_secret_response.py +88 -0
  62. chainstream/openapi_client/models/estimate_gas_limit_input.py +94 -0
  63. chainstream/openapi_client/models/estimate_gas_limit_response.py +90 -0
  64. chainstream/openapi_client/models/filter_condition.py +102 -0
  65. chainstream/openapi_client/models/gas_price_response.py +90 -0
  66. chainstream/openapi_client/models/job_dto.py +90 -0
  67. chainstream/openapi_client/models/job_streaming_dto.py +90 -0
  68. chainstream/openapi_client/models/kyt_register_transfer_request.py +108 -0
  69. chainstream/openapi_client/models/kyt_register_withdrawal_request.py +111 -0
  70. chainstream/openapi_client/models/link.py +97 -0
  71. chainstream/openapi_client/models/moonshot_create_token_input.py +145 -0
  72. chainstream/openapi_client/models/moonshot_create_token_reply.py +90 -0
  73. chainstream/openapi_client/models/moonshot_submit_create_token200_response.py +90 -0
  74. chainstream/openapi_client/models/moonshot_submit_create_token_input.py +90 -0
  75. chainstream/openapi_client/models/network_identification_org.py +88 -0
  76. chainstream/openapi_client/models/on_chain_activity.py +88 -0
  77. chainstream/openapi_client/models/pump_create_token_input.py +155 -0
  78. chainstream/openapi_client/models/pump_create_token_reply.py +90 -0
  79. chainstream/openapi_client/models/red_packet_claim_dto.py +103 -0
  80. chainstream/openapi_client/models/red_packet_claims_page.py +104 -0
  81. chainstream/openapi_client/models/red_packet_dto.py +121 -0
  82. chainstream/openapi_client/models/red_packet_reply.py +88 -0
  83. chainstream/openapi_client/models/red_packet_send_tx_input.py +88 -0
  84. chainstream/openapi_client/models/red_packet_send_tx_response.py +88 -0
  85. chainstream/openapi_client/models/red_packets_page.py +104 -0
  86. chainstream/openapi_client/models/register_address_request.py +88 -0
  87. chainstream/openapi_client/models/register_address_response_dto.py +88 -0
  88. chainstream/openapi_client/models/resolution.py +46 -0
  89. chainstream/openapi_client/models/send_tx_input.py +102 -0
  90. chainstream/openapi_client/models/send_tx_response.py +92 -0
  91. chainstream/openapi_client/models/swap_input.py +119 -0
  92. chainstream/openapi_client/models/swap_reply.py +90 -0
  93. chainstream/openapi_client/models/swap_route_input.py +127 -0
  94. chainstream/openapi_client/models/swap_route_response.py +98 -0
  95. chainstream/openapi_client/models/token.py +158 -0
  96. chainstream/openapi_client/models/token_creation_dto.py +107 -0
  97. chainstream/openapi_client/models/token_creation_page.py +106 -0
  98. chainstream/openapi_client/models/token_creators_dto.py +92 -0
  99. chainstream/openapi_client/models/token_extra_dto.py +128 -0
  100. chainstream/openapi_client/models/token_holder.py +94 -0
  101. chainstream/openapi_client/models/token_holder_page.py +106 -0
  102. chainstream/openapi_client/models/token_liquidity_snapshot_dto.py +106 -0
  103. chainstream/openapi_client/models/token_liquidity_snapshot_page.py +104 -0
  104. chainstream/openapi_client/models/token_list_page.py +106 -0
  105. chainstream/openapi_client/models/token_market_data.py +174 -0
  106. chainstream/openapi_client/models/token_metadata.py +132 -0
  107. chainstream/openapi_client/models/token_page.py +108 -0
  108. chainstream/openapi_client/models/token_price_dto.py +94 -0
  109. chainstream/openapi_client/models/token_price_page.py +106 -0
  110. chainstream/openapi_client/models/token_social_medias_dto.py +112 -0
  111. chainstream/openapi_client/models/token_stat.py +340 -0
  112. chainstream/openapi_client/models/token_trader.py +113 -0
  113. chainstream/openapi_client/models/token_trader_tag.py +45 -0
  114. chainstream/openapi_client/models/top_traders_dto.py +110 -0
  115. chainstream/openapi_client/models/top_traders_page.py +106 -0
  116. chainstream/openapi_client/models/trade_detail_dto.py +136 -0
  117. chainstream/openapi_client/models/trade_event.py +113 -0
  118. chainstream/openapi_client/models/trade_page.py +106 -0
  119. chainstream/openapi_client/models/trade_type.py +38 -0
  120. chainstream/openapi_client/models/transfer_alerts_response_dto.py +96 -0
  121. chainstream/openapi_client/models/transfer_base_response_dto.py +112 -0
  122. chainstream/openapi_client/models/transfer_direct_exposure_response_dto.py +92 -0
  123. chainstream/openapi_client/models/transfer_network_identifications_response_dto.py +98 -0
  124. chainstream/openapi_client/models/update_endpoint_input.py +104 -0
  125. chainstream/openapi_client/models/wallet_balance_detail_dto.py +136 -0
  126. chainstream/openapi_client/models/wallet_balances_dto.py +100 -0
  127. chainstream/openapi_client/models/wallet_pnl_dto.py +132 -0
  128. chainstream/openapi_client/models/withdrawal_address_identifications_response_dto.py +98 -0
  129. chainstream/openapi_client/models/withdrawal_base_response_dto.py +106 -0
  130. chainstream/openapi_client/models/withdrawal_fraud_assessment_response_dto.py +96 -0
  131. chainstream/openapi_client/rest.py +213 -0
  132. chainstream/openapi_client/test/__init__.py +0 -0
  133. chainstream/openapi_client/test/test_address_exposure.py +59 -0
  134. chainstream/openapi_client/test/test_address_risk_response_dto.py +81 -0
  135. chainstream/openapi_client/test/test_alert_detail.py +65 -0
  136. chainstream/openapi_client/test/test_alterya_identification.py +69 -0
  137. chainstream/openapi_client/test/test_balance_change_type.py +34 -0
  138. chainstream/openapi_client/test/test_balance_token_type.py +34 -0
  139. chainstream/openapi_client/test/test_balance_update_dto.py +93 -0
  140. chainstream/openapi_client/test/test_balance_update_page.py +105 -0
  141. chainstream/openapi_client/test/test_blockchain_api.py +46 -0
  142. chainstream/openapi_client/test/test_blockchain_dto.py +59 -0
  143. chainstream/openapi_client/test/test_blockchain_latest_block_dto.py +55 -0
  144. chainstream/openapi_client/test/test_boolean_result_dto.py +53 -0
  145. chainstream/openapi_client/test/test_calculate_pnl_input.py +52 -0
  146. chainstream/openapi_client/test/test_candle.py +65 -0
  147. chainstream/openapi_client/test/test_chain.py +34 -0
  148. chainstream/openapi_client/test/test_chain_symbol.py +34 -0
  149. chainstream/openapi_client/test/test_chainalysis_address_identification.py +57 -0
  150. chainstream/openapi_client/test/test_claim_red_packet_input.py +58 -0
  151. chainstream/openapi_client/test/test_create_endpoint_input.py +63 -0
  152. chainstream/openapi_client/test/test_create_red_packet_input.py +64 -0
  153. chainstream/openapi_client/test/test_create_red_packet_reply.py +55 -0
  154. chainstream/openapi_client/test/test_create_token_input.py +63 -0
  155. chainstream/openapi_client/test/test_create_token_reply.py +55 -0
  156. chainstream/openapi_client/test/test_defi_sol_moonshot_api.py +46 -0
  157. chainstream/openapi_client/test/test_defi_sol_pumpfun_api.py +39 -0
  158. chainstream/openapi_client/test/test_dev_token_dto.py +441 -0
  159. chainstream/openapi_client/test/test_dex_api.py +67 -0
  160. chainstream/openapi_client/test/test_dex_dto.py +56 -0
  161. chainstream/openapi_client/test/test_dex_page.py +70 -0
  162. chainstream/openapi_client/test/test_dex_pool_api.py +46 -0
  163. chainstream/openapi_client/test/test_dex_pool_dto.py +88 -0
  164. chainstream/openapi_client/test/test_dex_pool_snapshot_dto.py +96 -0
  165. chainstream/openapi_client/test/test_dex_pool_snapshot_page.py +81 -0
  166. chainstream/openapi_client/test/test_dex_pool_token_liquidity.py +65 -0
  167. chainstream/openapi_client/test/test_dex_pool_token_snapshot_dto.py +65 -0
  168. chainstream/openapi_client/test/test_dex_quote_response.py +57 -0
  169. chainstream/openapi_client/test/test_direct_exposure_detail.py +55 -0
  170. chainstream/openapi_client/test/test_endpoint_api.py +81 -0
  171. chainstream/openapi_client/test/test_endpoint_list_response.py +72 -0
  172. chainstream/openapi_client/test/test_endpoint_operation_response.py +52 -0
  173. chainstream/openapi_client/test/test_endpoint_response.py +66 -0
  174. chainstream/openapi_client/test/test_endpoint_secret_response.py +52 -0
  175. chainstream/openapi_client/test/test_estimate_gas_limit_input.py +58 -0
  176. chainstream/openapi_client/test/test_estimate_gas_limit_response.py +55 -0
  177. chainstream/openapi_client/test/test_filter_condition.py +54 -0
  178. chainstream/openapi_client/test/test_gas_price_response.py +55 -0
  179. chainstream/openapi_client/test/test_ipfs_api.py +39 -0
  180. chainstream/openapi_client/test/test_job_dto.py +55 -0
  181. chainstream/openapi_client/test/test_job_streaming_dto.py +55 -0
  182. chainstream/openapi_client/test/test_jobs_api.py +46 -0
  183. chainstream/openapi_client/test/test_kyt_api.py +130 -0
  184. chainstream/openapi_client/test/test_kyt_register_transfer_request.py +59 -0
  185. chainstream/openapi_client/test/test_kyt_register_withdrawal_request.py +65 -0
  186. chainstream/openapi_client/test/test_link.py +55 -0
  187. chainstream/openapi_client/test/test_moonshot_create_token_input.py +79 -0
  188. chainstream/openapi_client/test/test_moonshot_create_token_reply.py +55 -0
  189. chainstream/openapi_client/test/test_moonshot_submit_create_token200_response.py +53 -0
  190. chainstream/openapi_client/test/test_moonshot_submit_create_token_input.py +54 -0
  191. chainstream/openapi_client/test/test_network_identification_org.py +53 -0
  192. chainstream/openapi_client/test/test_on_chain_activity.py +53 -0
  193. chainstream/openapi_client/test/test_pump_create_token_input.py +71 -0
  194. chainstream/openapi_client/test/test_pump_create_token_reply.py +54 -0
  195. chainstream/openapi_client/test/test_ranking_api.py +67 -0
  196. chainstream/openapi_client/test/test_red_packet_api.py +88 -0
  197. chainstream/openapi_client/test/test_red_packet_claim_dto.py +67 -0
  198. chainstream/openapi_client/test/test_red_packet_claims_page.py +81 -0
  199. chainstream/openapi_client/test/test_red_packet_dto.py +85 -0
  200. chainstream/openapi_client/test/test_red_packet_reply.py +53 -0
  201. chainstream/openapi_client/test/test_red_packet_send_tx_input.py +53 -0
  202. chainstream/openapi_client/test/test_red_packet_send_tx_response.py +53 -0
  203. chainstream/openapi_client/test/test_red_packets_page.py +99 -0
  204. chainstream/openapi_client/test/test_register_address_request.py +53 -0
  205. chainstream/openapi_client/test/test_register_address_response_dto.py +53 -0
  206. chainstream/openapi_client/test/test_resolution.py +34 -0
  207. chainstream/openapi_client/test/test_send_tx_input.py +55 -0
  208. chainstream/openapi_client/test/test_send_tx_response.py +57 -0
  209. chainstream/openapi_client/test/test_swap_input.py +65 -0
  210. chainstream/openapi_client/test/test_swap_reply.py +55 -0
  211. chainstream/openapi_client/test/test_swap_route_input.py +69 -0
  212. chainstream/openapi_client/test/test_swap_route_response.py +85 -0
  213. chainstream/openapi_client/test/test_token.py +352 -0
  214. chainstream/openapi_client/test/test_token_api.py +193 -0
  215. chainstream/openapi_client/test/test_token_creation_dto.py +64 -0
  216. chainstream/openapi_client/test/test_token_creation_page.py +76 -0
  217. chainstream/openapi_client/test/test_token_creators_dto.py +54 -0
  218. chainstream/openapi_client/test/test_token_extra_dto.py +72 -0
  219. chainstream/openapi_client/test/test_token_holder.py +59 -0
  220. chainstream/openapi_client/test/test_token_holder_page.py +70 -0
  221. chainstream/openapi_client/test/test_token_liquidity_snapshot_dto.py +71 -0
  222. chainstream/openapi_client/test/test_token_liquidity_snapshot_page.py +83 -0
  223. chainstream/openapi_client/test/test_token_list_page.py +154 -0
  224. chainstream/openapi_client/test/test_token_market_data.py +101 -0
  225. chainstream/openapi_client/test/test_token_metadata.py +110 -0
  226. chainstream/openapi_client/test/test_token_page.py +155 -0
  227. chainstream/openapi_client/test/test_token_price_dto.py +59 -0
  228. chainstream/openapi_client/test/test_token_price_page.py +70 -0
  229. chainstream/openapi_client/test/test_token_social_medias_dto.py +64 -0
  230. chainstream/openapi_client/test/test_token_stat.py +257 -0
  231. chainstream/openapi_client/test/test_token_trader.py +65 -0
  232. chainstream/openapi_client/test/test_token_trader_tag.py +34 -0
  233. chainstream/openapi_client/test/test_top_traders_dto.py +75 -0
  234. chainstream/openapi_client/test/test_top_traders_page.py +86 -0
  235. chainstream/openapi_client/test/test_trade_api.py +53 -0
  236. chainstream/openapi_client/test/test_trade_detail_dto.py +101 -0
  237. chainstream/openapi_client/test/test_trade_event.py +77 -0
  238. chainstream/openapi_client/test/test_trade_page.py +112 -0
  239. chainstream/openapi_client/test/test_trade_type.py +34 -0
  240. chainstream/openapi_client/test/test_transaction_api.py +53 -0
  241. chainstream/openapi_client/test/test_transfer_alerts_response_dto.py +71 -0
  242. chainstream/openapi_client/test/test_transfer_base_response_dto.py +77 -0
  243. chainstream/openapi_client/test/test_transfer_direct_exposure_response_dto.py +57 -0
  244. chainstream/openapi_client/test/test_transfer_network_identifications_response_dto.py +61 -0
  245. chainstream/openapi_client/test/test_update_endpoint_input.py +64 -0
  246. chainstream/openapi_client/test/test_wallet_api.py +67 -0
  247. chainstream/openapi_client/test/test_wallet_balance_detail_dto.py +101 -0
  248. chainstream/openapi_client/test/test_wallet_balances_dto.py +111 -0
  249. chainstream/openapi_client/test/test_wallet_pnl_dto.py +97 -0
  250. chainstream/openapi_client/test/test_watchlist_api.py +39 -0
  251. chainstream/openapi_client/test/test_withdrawal_address_identifications_response_dto.py +65 -0
  252. chainstream/openapi_client/test/test_withdrawal_base_response_dto.py +71 -0
  253. chainstream/openapi_client/test/test_withdrawal_fraud_assessment_response_dto.py +73 -0
  254. chainstream/stream/__init__.py +74 -0
  255. chainstream/stream/client.py +761 -0
  256. chainstream/stream/fields.py +284 -0
  257. chainstream/stream/models.py +415 -0
  258. chainstream_sdk-0.1.0.dist-info/METADATA +80 -0
  259. chainstream_sdk-0.1.0.dist-info/RECORD +261 -0
  260. chainstream_sdk-0.1.0.dist-info/WHEEL +4 -0
  261. chainstream_sdk-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,761 @@
1
+ """Centrifuge WebSocket client implementation for ChainStream Stream API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import logging
8
+ from dataclasses import dataclass
9
+ from typing import Any, Callable, Dict, List, Optional, TypeVar
10
+
11
+ import aiohttp
12
+
13
+ from chainstream.stream.fields import replace_filter_fields
14
+ from chainstream.stream.models import (
15
+ DexPoolBalance,
16
+ MetricType,
17
+ NewToken,
18
+ RankingTokenList,
19
+ RankingType,
20
+ Resolution,
21
+ TokenCandle,
22
+ TokenHolder,
23
+ TokenLiquidity,
24
+ TokenMaxLiquidity,
25
+ TokenMetadata,
26
+ TokenStat,
27
+ TokenSupply,
28
+ TokenTotalLiquidity,
29
+ TradeActivity,
30
+ WalletBalance,
31
+ WalletTokenPnl,
32
+ )
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+ T = TypeVar("T")
37
+
38
+
39
+ @dataclass
40
+ class Unsubscribe:
41
+ """Unsubscribe handle for stream subscriptions."""
42
+
43
+ channel: str
44
+ callback_id: int
45
+ _api: "StreamApi"
46
+
47
+ def unsubscribe(self) -> None:
48
+ """Unsubscribe from the channel."""
49
+ self._api._remove_listener(self.channel, self.callback_id)
50
+
51
+
52
+ class StreamApi:
53
+ """Stream API client for real-time data subscriptions using Centrifuge protocol."""
54
+
55
+ def __init__(self, url: str, access_token: str) -> None:
56
+ """Initialize the StreamApi.
57
+
58
+ Args:
59
+ url: The WebSocket URL for the stream server.
60
+ access_token: The access token for authentication.
61
+ """
62
+ # Build URL with token
63
+ if "?" in url:
64
+ self._url = f"{url}&token={access_token}"
65
+ else:
66
+ self._url = f"{url}?token={access_token}"
67
+
68
+ self._access_token = access_token
69
+ self._connected = False
70
+ self._command_id = 0
71
+ self._callback_id = 0
72
+ self._listeners: Dict[str, List[Dict[str, Any]]] = {}
73
+ self._subscriptions: Dict[str, int] = {}
74
+ self._ws: Optional[aiohttp.ClientWebSocketResponse] = None
75
+ self._session: Optional[aiohttp.ClientSession] = None
76
+ self._read_task: Optional[asyncio.Task] = None
77
+ self._lock = asyncio.Lock()
78
+
79
+ def _next_command_id(self) -> int:
80
+ """Get the next command ID."""
81
+ self._command_id += 1
82
+ return self._command_id
83
+
84
+ def _next_callback_id(self) -> int:
85
+ """Get the next callback ID."""
86
+ self._callback_id += 1
87
+ return self._callback_id
88
+
89
+ @property
90
+ def is_connected(self) -> bool:
91
+ """Check if the client is connected."""
92
+ return self._connected
93
+
94
+ async def connect(self) -> None:
95
+ """Connect to the WebSocket server."""
96
+ async with self._lock:
97
+ if self._connected:
98
+ return
99
+
100
+ self._session = aiohttp.ClientSession()
101
+ self._ws = await self._session.ws_connect(self._url)
102
+
103
+ # Send connect command with token
104
+ connect_cmd = {
105
+ "id": self._next_command_id(),
106
+ "connect": {"token": self._access_token},
107
+ }
108
+ await self._ws.send_json(connect_cmd)
109
+
110
+ self._connected = True
111
+
112
+ # Start read task
113
+ self._read_task = asyncio.create_task(self._read_loop())
114
+
115
+ # Give the read task a chance to start
116
+ await asyncio.sleep(0.1)
117
+
118
+ async def _read_loop(self) -> None:
119
+ """Read messages from the WebSocket."""
120
+ if not self._ws:
121
+ return
122
+
123
+ try:
124
+ async for msg in self._ws:
125
+ if msg.type == aiohttp.WSMsgType.TEXT:
126
+ try:
127
+ response = json.loads(msg.data)
128
+ await self._handle_response(response)
129
+ except json.JSONDecodeError:
130
+ pass # Ignore malformed messages
131
+ elif msg.type == aiohttp.WSMsgType.CLOSED:
132
+ logger.info("[streaming] connection closed")
133
+ self._connected = False
134
+ break
135
+ elif msg.type == aiohttp.WSMsgType.ERROR:
136
+ logger.error("[streaming] connection error")
137
+ self._connected = False
138
+ break
139
+ except asyncio.CancelledError:
140
+ pass
141
+ except Exception as e:
142
+ logger.error(f"[streaming] read error: {e}")
143
+ self._connected = False
144
+
145
+ async def _handle_response(self, response: Dict[str, Any]) -> None:
146
+ """Handle a response from the server."""
147
+ # Handle push messages (publications)
148
+ if "push" in response:
149
+ push = response["push"]
150
+ channel = push.get("channel", "")
151
+ pub = push.get("pub", {})
152
+ if pub and "data" in pub:
153
+ self._dispatch_message(channel, pub["data"])
154
+
155
+ # Handle errors
156
+ if "error" in response:
157
+ error = response["error"]
158
+ logger.error(
159
+ f"[streaming] error: code={error.get('code')}, message={error.get('message')}"
160
+ )
161
+
162
+ # Connect and subscribe responses are handled silently
163
+
164
+ def _dispatch_message(self, channel: str, data: Any) -> None:
165
+ """Dispatch a message to listeners."""
166
+ listeners = self._listeners.get(channel, [])
167
+ for listener in listeners:
168
+ try:
169
+ listener["callback"](data)
170
+ except Exception as e:
171
+ logger.error(f"[streaming] callback error: {e}")
172
+
173
+ async def _send_subscribe(self, channel: str, filter_expr: Optional[str] = None) -> None:
174
+ """Send a subscribe command."""
175
+ if not self._ws:
176
+ return
177
+
178
+ cmd: Dict[str, Any] = {
179
+ "id": self._next_command_id(),
180
+ "subscribe": {
181
+ "channel": channel,
182
+ "delta": "fossil",
183
+ },
184
+ }
185
+
186
+ if filter_expr:
187
+ cmd["subscribe"]["filter"] = filter_expr
188
+
189
+ await self._ws.send_json(cmd)
190
+
191
+ async def _send_unsubscribe(self, channel: str) -> None:
192
+ """Send an unsubscribe command."""
193
+ if not self._ws:
194
+ return
195
+
196
+ cmd = {
197
+ "id": self._next_command_id(),
198
+ "unsubscribe": {"channel": channel},
199
+ }
200
+ await self._ws.send_json(cmd)
201
+
202
+ def _add_listener(self, channel: str, callback: Callable[[Any], None]) -> int:
203
+ """Add a listener for a channel."""
204
+ callback_id = self._next_callback_id()
205
+
206
+ if channel not in self._listeners:
207
+ self._listeners[channel] = []
208
+
209
+ self._listeners[channel].append({"id": callback_id, "callback": callback})
210
+ return callback_id
211
+
212
+ def _remove_listener(self, channel: str, callback_id: int) -> None:
213
+ """Remove a listener from a channel."""
214
+ if channel not in self._listeners:
215
+ return
216
+
217
+ self._listeners[channel] = [
218
+ l for l in self._listeners[channel] if l["id"] != callback_id
219
+ ]
220
+
221
+ # If no more listeners, unsubscribe
222
+ if not self._listeners[channel]:
223
+ del self._listeners[channel]
224
+ asyncio.create_task(self._send_unsubscribe(channel))
225
+ if channel in self._subscriptions:
226
+ del self._subscriptions[channel]
227
+ logger.info(f"[streaming] unsubscribed from channel: {channel}")
228
+
229
+ async def disconnect(self) -> None:
230
+ """Disconnect from the WebSocket server."""
231
+ async with self._lock:
232
+ if self._read_task:
233
+ self._read_task.cancel()
234
+ try:
235
+ await self._read_task
236
+ except asyncio.CancelledError:
237
+ pass
238
+ self._read_task = None
239
+
240
+ if self._ws:
241
+ await self._ws.close()
242
+ self._ws = None
243
+
244
+ if self._session:
245
+ await self._session.close()
246
+ self._session = None
247
+
248
+ self._connected = False
249
+ logger.info("[streaming] disconnected")
250
+
251
+ async def subscribe(
252
+ self,
253
+ channel: str,
254
+ callback: Callable[[Any], None],
255
+ filter_expr: Optional[str] = None,
256
+ method_name: Optional[str] = None,
257
+ ) -> Unsubscribe:
258
+ """Subscribe to a channel with a raw callback.
259
+
260
+ Args:
261
+ channel: The channel to subscribe to.
262
+ callback: The callback function to call when data is received.
263
+ filter_expr: Optional filter expression.
264
+ method_name: Optional method name for field mapping.
265
+
266
+ Returns:
267
+ An Unsubscribe handle to cancel the subscription.
268
+ """
269
+ # Ensure connected
270
+ if not self._connected:
271
+ await self.connect()
272
+
273
+ # Process filter if method name is provided
274
+ processed_filter = None
275
+ if filter_expr and method_name:
276
+ processed_filter = replace_filter_fields(filter_expr, method_name)
277
+ elif filter_expr:
278
+ processed_filter = filter_expr
279
+
280
+ # Check if already subscribed to this channel
281
+ needs_subscribe = channel not in self._subscriptions
282
+
283
+ # Add callback
284
+ callback_id = self._add_listener(channel, callback)
285
+
286
+ # Subscribe to channel if not already subscribed
287
+ if needs_subscribe:
288
+ await self._send_subscribe(channel, processed_filter)
289
+ self._subscriptions[channel] = self._next_command_id()
290
+ logger.info(f"[streaming] subscribed to channel: {channel}")
291
+
292
+ return Unsubscribe(channel=channel, callback_id=callback_id, _api=self)
293
+
294
+ # ==================== Subscription Methods ====================
295
+
296
+ async def subscribe_token_candles(
297
+ self,
298
+ chain: str,
299
+ token_address: str,
300
+ resolution: Resolution,
301
+ callback: Callable[[TokenCandle], None],
302
+ filter_expr: Optional[str] = None,
303
+ ) -> Unsubscribe:
304
+ """Subscribe to token candle data.
305
+
306
+ Args:
307
+ chain: The blockchain (e.g., "sol").
308
+ token_address: The token address.
309
+ resolution: The candle resolution.
310
+ callback: The callback function to call when data is received.
311
+ filter_expr: Optional filter expression.
312
+
313
+ Returns:
314
+ An Unsubscribe handle to cancel the subscription.
315
+ """
316
+ channel = f"dex-candle:{chain}_{token_address}_{resolution.value}"
317
+
318
+ def parse_callback(data: Dict[str, Any]) -> None:
319
+ candle = TokenCandle(
320
+ open=str(data.get("o", "")),
321
+ close=str(data.get("c", "")),
322
+ high=str(data.get("h", "")),
323
+ low=str(data.get("l", "")),
324
+ volume=str(data.get("v", "")),
325
+ resolution=str(data.get("r", "")),
326
+ time=int(data.get("t", 0)),
327
+ number=int(data.get("n", 0)),
328
+ )
329
+ callback(candle)
330
+
331
+ return await self.subscribe(channel, parse_callback, filter_expr, "subscribe_token_candles")
332
+
333
+ async def subscribe_token_stats(
334
+ self,
335
+ chain: str,
336
+ token_address: str,
337
+ callback: Callable[[TokenStat], None],
338
+ filter_expr: Optional[str] = None,
339
+ ) -> Unsubscribe:
340
+ """Subscribe to token statistics.
341
+
342
+ Args:
343
+ chain: The blockchain.
344
+ token_address: The token address.
345
+ callback: The callback function.
346
+ filter_expr: Optional filter expression.
347
+
348
+ Returns:
349
+ An Unsubscribe handle.
350
+ """
351
+ channel = f"dex-token-stats:{chain}_{token_address}"
352
+
353
+ def parse_callback(data: Dict[str, Any]) -> None:
354
+ stat = TokenStat(
355
+ address=str(data.get("a", "")),
356
+ timestamp=int(data.get("t", 0)),
357
+ buys_1m=data.get("b1m"),
358
+ sells_1m=data.get("s1m"),
359
+ buyers_1m=data.get("be1m"),
360
+ sellers_1m=data.get("se1m"),
361
+ buy_volume_in_usd_1m=data.get("bviu1m"),
362
+ sell_volume_in_usd_1m=data.get("sviu1m"),
363
+ price_1m=data.get("p1m"),
364
+ open_in_usd_1m=data.get("oiu1m"),
365
+ close_in_usd_1m=data.get("ciu1m"),
366
+ price=data.get("p"),
367
+ )
368
+ callback(stat)
369
+
370
+ return await self.subscribe(channel, parse_callback, filter_expr, "subscribe_token_stats")
371
+
372
+ async def subscribe_new_token(
373
+ self,
374
+ chain: str,
375
+ callback: Callable[[NewToken], None],
376
+ filter_expr: Optional[str] = None,
377
+ ) -> Unsubscribe:
378
+ """Subscribe to new tokens.
379
+
380
+ Args:
381
+ chain: The blockchain.
382
+ callback: The callback function.
383
+ filter_expr: Optional filter expression.
384
+
385
+ Returns:
386
+ An Unsubscribe handle.
387
+ """
388
+ channel = f"dex-new-token:{chain}"
389
+
390
+ def parse_callback(data: Dict[str, Any]) -> None:
391
+ token = NewToken(
392
+ token_address=str(data.get("a", "")),
393
+ name=str(data.get("n", "")),
394
+ symbol=str(data.get("s", "")),
395
+ decimals=data.get("d"),
396
+ created_at_ms=int(data.get("cts", 0)),
397
+ )
398
+ callback(token)
399
+
400
+ return await self.subscribe(channel, parse_callback, filter_expr, "subscribe_new_token")
401
+
402
+ async def subscribe_token_trade(
403
+ self,
404
+ chain: str,
405
+ token_address: str,
406
+ callback: Callable[[TradeActivity], None],
407
+ filter_expr: Optional[str] = None,
408
+ ) -> Unsubscribe:
409
+ """Subscribe to token trades.
410
+
411
+ Args:
412
+ chain: The blockchain.
413
+ token_address: The token address.
414
+ callback: The callback function.
415
+ filter_expr: Optional filter expression.
416
+
417
+ Returns:
418
+ An Unsubscribe handle.
419
+ """
420
+ channel = f"dex-trade:{chain}_{token_address}"
421
+
422
+ def parse_callback(data: Dict[str, Any]) -> None:
423
+ trade = TradeActivity(
424
+ token_address=str(data.get("a", "")),
425
+ timestamp=int(data.get("t", 0)),
426
+ kind=str(data.get("k", "")),
427
+ buy_amount=str(data.get("ba", "")),
428
+ buy_amount_in_usd=str(data.get("baiu", "")),
429
+ buy_token_address=str(data.get("btma", "")),
430
+ buy_token_name=str(data.get("btn", "")),
431
+ buy_token_symbol=str(data.get("bts", "")),
432
+ buy_wallet_address=str(data.get("bwa", "")),
433
+ sell_amount=str(data.get("sa", "")),
434
+ sell_amount_in_usd=str(data.get("saiu", "")),
435
+ sell_token_address=str(data.get("stma", "")),
436
+ sell_token_name=str(data.get("stn", "")),
437
+ sell_token_symbol=str(data.get("sts", "")),
438
+ sell_wallet_address=str(data.get("swa", "")),
439
+ tx_hash=str(data.get("h", "")),
440
+ )
441
+ callback(trade)
442
+
443
+ return await self.subscribe(channel, parse_callback, filter_expr, "subscribe_token_trades")
444
+
445
+ async def subscribe_wallet_balance(
446
+ self,
447
+ chain: str,
448
+ wallet_address: str,
449
+ callback: Callable[[WalletBalance], None],
450
+ filter_expr: Optional[str] = None,
451
+ ) -> Unsubscribe:
452
+ """Subscribe to wallet balance.
453
+
454
+ Args:
455
+ chain: The blockchain.
456
+ wallet_address: The wallet address.
457
+ callback: The callback function.
458
+ filter_expr: Optional filter expression.
459
+
460
+ Returns:
461
+ An Unsubscribe handle.
462
+ """
463
+ channel = f"dex-wallet-balance:{chain}_{wallet_address}"
464
+
465
+ def parse_callback(data: Dict[str, Any]) -> None:
466
+ balance = WalletBalance(
467
+ wallet_address=str(data.get("a", "")),
468
+ token_address=str(data.get("ta", "")),
469
+ token_price_in_usd=str(data.get("tpiu", "")),
470
+ balance=str(data.get("b", "")),
471
+ timestamp=int(data.get("t", 0)),
472
+ )
473
+ callback(balance)
474
+
475
+ return await self.subscribe(
476
+ channel, parse_callback, filter_expr, "subscribe_wallet_balance"
477
+ )
478
+
479
+ async def subscribe_token_holders(
480
+ self,
481
+ chain: str,
482
+ token_address: str,
483
+ callback: Callable[[TokenHolder], None],
484
+ filter_expr: Optional[str] = None,
485
+ ) -> Unsubscribe:
486
+ """Subscribe to token holders.
487
+
488
+ Args:
489
+ chain: The blockchain.
490
+ token_address: The token address.
491
+ callback: The callback function.
492
+ filter_expr: Optional filter expression.
493
+
494
+ Returns:
495
+ An Unsubscribe handle.
496
+ """
497
+ channel = f"dex-token-holder:{chain}_{token_address}"
498
+
499
+ def parse_callback(data: Dict[str, Any]) -> None:
500
+ holder = TokenHolder(
501
+ token_address=str(data.get("a", "")),
502
+ timestamp=int(data.get("ts", 0)),
503
+ holders=data.get("h"),
504
+ top100_amount=data.get("t100a"),
505
+ top10_amount=data.get("t10a"),
506
+ top100_holders=data.get("t100h"),
507
+ top10_holders=data.get("t10h"),
508
+ top100_ratio=data.get("t100r"),
509
+ top10_ratio=data.get("t10r"),
510
+ )
511
+ callback(holder)
512
+
513
+ return await self.subscribe(
514
+ channel, parse_callback, filter_expr, "subscribe_token_holders"
515
+ )
516
+
517
+ async def subscribe_token_supply(
518
+ self,
519
+ chain: str,
520
+ token_address: str,
521
+ callback: Callable[[TokenSupply], None],
522
+ filter_expr: Optional[str] = None,
523
+ ) -> Unsubscribe:
524
+ """Subscribe to token supply.
525
+
526
+ Args:
527
+ chain: The blockchain.
528
+ token_address: The token address.
529
+ callback: The callback function.
530
+ filter_expr: Optional filter expression.
531
+
532
+ Returns:
533
+ An Unsubscribe handle.
534
+ """
535
+ channel = f"dex-token-supply:{chain}_{token_address}"
536
+
537
+ def parse_callback(data: Dict[str, Any]) -> None:
538
+ supply = TokenSupply(
539
+ token_address=str(data.get("a", "")),
540
+ timestamp=int(data.get("ts", 0)),
541
+ supply=data.get("s"),
542
+ market_cap_in_usd=data.get("mciu"),
543
+ )
544
+ callback(supply)
545
+
546
+ return await self.subscribe(channel, parse_callback, filter_expr, "subscribe_token_supply")
547
+
548
+ async def subscribe_dex_pool_balance(
549
+ self,
550
+ chain: str,
551
+ pool_address: str,
552
+ callback: Callable[[DexPoolBalance], None],
553
+ filter_expr: Optional[str] = None,
554
+ ) -> Unsubscribe:
555
+ """Subscribe to DEX pool balance.
556
+
557
+ Args:
558
+ chain: The blockchain.
559
+ pool_address: The pool address.
560
+ callback: The callback function.
561
+ filter_expr: Optional filter expression.
562
+
563
+ Returns:
564
+ An Unsubscribe handle.
565
+ """
566
+ channel = f"dex-pool-balance:{chain}_{pool_address}"
567
+
568
+ def parse_callback(data: Dict[str, Any]) -> None:
569
+ balance = DexPoolBalance(
570
+ pool_address=str(data.get("a", "")),
571
+ token_a_address=str(data.get("taa", "")),
572
+ token_a_liquidity_in_usd=str(data.get("taliu", "")),
573
+ token_b_address=str(data.get("tba", "")),
574
+ token_b_liquidity_in_usd=str(data.get("tbliu", "")),
575
+ )
576
+ callback(balance)
577
+
578
+ return await self.subscribe(
579
+ channel, parse_callback, filter_expr, "subscribe_dex_pool_balance"
580
+ )
581
+
582
+ async def subscribe_token_max_liquidity(
583
+ self,
584
+ chain: str,
585
+ token_address: str,
586
+ callback: Callable[[TokenMaxLiquidity], None],
587
+ filter_expr: Optional[str] = None,
588
+ ) -> Unsubscribe:
589
+ """Subscribe to token max liquidity.
590
+
591
+ Args:
592
+ chain: The blockchain.
593
+ token_address: The token address.
594
+ callback: The callback function.
595
+ filter_expr: Optional filter expression.
596
+
597
+ Returns:
598
+ An Unsubscribe handle.
599
+ """
600
+ channel = f"dex-token-liquidity:{chain}_{token_address}"
601
+
602
+ def parse_callback(data: Dict[str, Any]) -> None:
603
+ liquidity = TokenMaxLiquidity(
604
+ token_address=str(data.get("a", "")),
605
+ pool_address=str(data.get("p", "")),
606
+ liquidity_in_usd=str(data.get("liu", "")),
607
+ liquidity_in_native=str(data.get("lin", "")),
608
+ timestamp=int(data.get("ts", 0)),
609
+ )
610
+ callback(liquidity)
611
+
612
+ return await self.subscribe(
613
+ channel, parse_callback, filter_expr, "subscribe_token_max_liquidity"
614
+ )
615
+
616
+ async def subscribe_token_total_liquidity(
617
+ self,
618
+ chain: str,
619
+ token_address: str,
620
+ callback: Callable[[TokenTotalLiquidity], None],
621
+ filter_expr: Optional[str] = None,
622
+ ) -> Unsubscribe:
623
+ """Subscribe to token total liquidity.
624
+
625
+ Args:
626
+ chain: The blockchain.
627
+ token_address: The token address.
628
+ callback: The callback function.
629
+ filter_expr: Optional filter expression.
630
+
631
+ Returns:
632
+ An Unsubscribe handle.
633
+ """
634
+ channel = f"dex-token-total-liquidity:{chain}_{token_address}"
635
+
636
+ def parse_callback(data: Dict[str, Any]) -> None:
637
+ liquidity = TokenTotalLiquidity(
638
+ token_address=str(data.get("a", "")),
639
+ liquidity_in_usd=str(data.get("liu", "")),
640
+ liquidity_in_native=str(data.get("lin", "")),
641
+ pool_count=int(data.get("pc", 0)),
642
+ timestamp=int(data.get("ts", 0)),
643
+ )
644
+ callback(liquidity)
645
+
646
+ return await self.subscribe(
647
+ channel, parse_callback, filter_expr, "subscribe_token_total_liquidity"
648
+ )
649
+
650
+ async def subscribe_wallet_pnl(
651
+ self,
652
+ chain: str,
653
+ wallet_address: str,
654
+ callback: Callable[[WalletTokenPnl], None],
655
+ filter_expr: Optional[str] = None,
656
+ ) -> Unsubscribe:
657
+ """Subscribe to wallet PnL.
658
+
659
+ Args:
660
+ chain: The blockchain.
661
+ wallet_address: The wallet address.
662
+ callback: The callback function.
663
+ filter_expr: Optional filter expression.
664
+
665
+ Returns:
666
+ An Unsubscribe handle.
667
+ """
668
+ channel = f"dex-wallet-pnl:{chain}_{wallet_address}"
669
+
670
+ def parse_callback(data: Dict[str, Any]) -> None:
671
+ pnl = WalletTokenPnl(
672
+ wallet_address=str(data.get("a", "")),
673
+ token_address=str(data.get("ta", "")),
674
+ token_price_in_usd=str(data.get("tpiu", "")),
675
+ timestamp=int(data.get("t", 0)),
676
+ open_time=int(data.get("ot", 0)),
677
+ last_time=int(data.get("lt", 0)),
678
+ close_time=int(data.get("ct", 0)),
679
+ buy_amount=str(data.get("ba", "")),
680
+ buy_amount_in_usd=str(data.get("baiu", "")),
681
+ buy_count=int(data.get("bs", 0)),
682
+ buy_count_30d=int(data.get("bs30d", 0)),
683
+ buy_count_7d=int(data.get("bs7d", 0)),
684
+ sell_amount=str(data.get("sa", "")),
685
+ sell_amount_in_usd=str(data.get("saiu", "")),
686
+ sell_count=int(data.get("ss", 0)),
687
+ sell_count_30d=int(data.get("ss30d", 0)),
688
+ sell_count_7d=int(data.get("ss7d", 0)),
689
+ held_duration_timestamp=int(data.get("hdts", 0)),
690
+ average_buy_price_in_usd=str(data.get("abpiu", "")),
691
+ average_sell_price_in_usd=str(data.get("aspiu", "")),
692
+ unrealized_profit_in_usd=str(data.get("upiu", "")),
693
+ unrealized_profit_ratio=str(data.get("upr", "")),
694
+ realized_profit_in_usd=str(data.get("rpiu", "")),
695
+ realized_profit_ratio=str(data.get("rpr", "")),
696
+ total_realized_profit_in_usd=str(data.get("trpiu", "")),
697
+ total_realized_profit_ratio=str(data.get("trr", "")),
698
+ )
699
+ callback(pnl)
700
+
701
+ return await self.subscribe(channel, parse_callback, filter_expr, "subscribe_wallet_pnl")
702
+
703
+ async def subscribe_new_tokens_metadata(
704
+ self,
705
+ chain: str,
706
+ callback: Callable[[TokenMetadata], None],
707
+ filter_expr: Optional[str] = None,
708
+ ) -> Unsubscribe:
709
+ """Subscribe to new tokens metadata.
710
+
711
+ Args:
712
+ chain: The blockchain.
713
+ callback: The callback function.
714
+ filter_expr: Optional filter expression.
715
+
716
+ Returns:
717
+ An Unsubscribe handle.
718
+ """
719
+ channel = f"dex-new-token-metadata:{chain}"
720
+
721
+ def parse_callback(data: Dict[str, Any]) -> None:
722
+ metadata = TokenMetadata(
723
+ token_address=str(data.get("a", "")),
724
+ name=data.get("n"),
725
+ symbol=data.get("s"),
726
+ image_url=data.get("iu"),
727
+ description=data.get("de"),
728
+ created_at_ms=data.get("cts"),
729
+ )
730
+ callback(metadata)
731
+
732
+ return await self.subscribe(
733
+ channel, parse_callback, filter_expr, "subscribe_new_tokens_metadata"
734
+ )
735
+
736
+ async def subscribe_ranking_tokens_list(
737
+ self,
738
+ chain: str,
739
+ ranking_type: RankingType,
740
+ callback: Callable[[RankingTokenList], None],
741
+ filter_expr: Optional[str] = None,
742
+ ) -> Unsubscribe:
743
+ """Subscribe to ranking tokens list.
744
+
745
+ Args:
746
+ chain: The blockchain.
747
+ ranking_type: The ranking type.
748
+ callback: The callback function.
749
+ filter_expr: Optional filter expression.
750
+
751
+ Returns:
752
+ An Unsubscribe handle.
753
+ """
754
+ channel = f"dex-ranking-token-list:{chain}_{ranking_type.value}"
755
+
756
+ def parse_callback(data: Dict[str, Any]) -> None:
757
+ ranking = RankingTokenList()
758
+ # TODO: Parse nested objects if needed
759
+ callback(ranking)
760
+
761
+ return await self.subscribe(channel, parse_callback, filter_expr, None)