helius-python 0.2.0__py3-none-any.whl → 0.3.1__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.
@@ -0,0 +1,15 @@
1
+ from helius.admin.admin import (
2
+ AccountManagementClient,
3
+ BillingCycle,
4
+ ProjectUsage,
5
+ SubscriptionDetails,
6
+ Usage,
7
+ )
8
+
9
+ __all__ = [
10
+ "AccountManagementClient",
11
+ "BillingCycle",
12
+ "ProjectUsage",
13
+ "SubscriptionDetails",
14
+ "Usage",
15
+ ]
helius/admin/admin.py ADDED
@@ -0,0 +1,95 @@
1
+ from os import environ
2
+
3
+ import httpx
4
+ from dotenv import dotenv_values
5
+ from pydantic import AliasGenerator, BaseModel, ConfigDict
6
+ from pydantic.alias_generators import to_camel
7
+
8
+
9
+ class BillingCycle(BaseModel):
10
+ start: str
11
+ end: str
12
+
13
+
14
+ class SubscriptionDetails(BaseModel):
15
+ model_config = ConfigDict(alias_generator=AliasGenerator(validation_alias=to_camel))
16
+
17
+ billing_cycle: BillingCycle
18
+ credits_limit: float
19
+ plan: str
20
+
21
+
22
+ class Usage(BaseModel):
23
+ model_config = ConfigDict(alias_generator=AliasGenerator(validation_alias=to_camel))
24
+
25
+ api: int
26
+ archival: int
27
+ das: int
28
+ grpc: int
29
+ grpc_geyser: int
30
+ photon: int
31
+ rpc: int
32
+ stream: int
33
+ webhook: int
34
+ websocket: int
35
+
36
+
37
+ class ProjectUsage(BaseModel):
38
+ model_config = ConfigDict(alias_generator=AliasGenerator(validation_alias=to_camel))
39
+
40
+ credits_remaining: float
41
+ credits_used: float
42
+ prepaid_credits_remaining: float
43
+ prepaid_credits_used: float
44
+ subscription_details: SubscriptionDetails
45
+ usage: Usage
46
+
47
+
48
+ class AccountManagementClient:
49
+ def __init__(
50
+ self,
51
+ *,
52
+ base_url: str = "https://admin-api.helius.xyz/v0/admin/projects/{id}/usage",
53
+ api_key: str | None = None,
54
+ project_id: str | None = None,
55
+ headers: dict[str, str] | None = None,
56
+ proxy: str | None = None,
57
+ ) -> None:
58
+ base_url = base_url
59
+ api_key = (
60
+ api_key
61
+ or environ.get("HELIUS_API_KEY")
62
+ or dotenv_values().get("HELIUS_API_KEY")
63
+ or None
64
+ )
65
+ self.project_id = project_id
66
+ client_options: dict = {
67
+ "base_url": base_url,
68
+ "headers": headers,
69
+ "proxy": proxy,
70
+ }
71
+ if api_key is not None:
72
+ client_options.update({"params": {"api-key": api_key}})
73
+ self._client = httpx.Client(**client_options)
74
+
75
+ def __enter__(self):
76
+ return self
77
+
78
+ def __exit__(self, exc_type, exc_value, traceback):
79
+ self.close()
80
+
81
+ def __del__(self):
82
+ self.close()
83
+
84
+ def close(self) -> None:
85
+ self._client.close()
86
+
87
+ def get_project_usage(self, project_id: str | None = None) -> ProjectUsage:
88
+ project_id = project_id or self.project_id or None
89
+ if project_id is None:
90
+ raise ValueError("No project ID provided.")
91
+ response = self._client.request(
92
+ method="GET", url="/", params={"id": project_id}
93
+ )
94
+ response.raise_for_status()
95
+ return ProjectUsage.model_validate(response.json())
@@ -0,0 +1,398 @@
1
+ import json
2
+ from os import environ
3
+ from typing import Annotated, Literal, TypedDict
4
+
5
+ import httpx
6
+ from dotenv import dotenv_values
7
+ from pydantic import AliasGenerator, BaseModel, ConfigDict, Field, model_validator
8
+ from pydantic.alias_generators import to_camel
9
+ from websockets.sync.client import connect
10
+
11
+ from helius.rpc import JsonRpcRequest
12
+
13
+
14
+ class Notification(BaseModel):
15
+ model_config = ConfigDict(alias_generator=AliasGenerator(validation_alias=to_camel))
16
+
17
+
18
+ class TransactionNotification(Notification):
19
+ transaction: dict
20
+ signature: str
21
+ slot: int
22
+
23
+
24
+ class AccountNotification(Notification):
25
+ lamports: int
26
+ owner: str
27
+ data: list | dict | str
28
+ executable: bool
29
+ rent_epoch: int
30
+ space: int | None = None
31
+
32
+
33
+ class BlockNotification(Notification):
34
+ slot: int
35
+ err: dict | None
36
+ block: dict | None
37
+
38
+
39
+ class LogsNotification(Notification):
40
+ signature: str
41
+ err: dict | str | None
42
+ logs: list[str]
43
+
44
+
45
+ class ProgramNotification(Notification):
46
+ pubkey: str
47
+ account: AccountNotification
48
+
49
+
50
+ class RootNotification(Notification):
51
+ root: int
52
+
53
+ @model_validator(mode="before")
54
+ @classmethod
55
+ def wrap_scalar(cls, data):
56
+ if not isinstance(data, dict):
57
+ return {"root": data}
58
+ return data
59
+
60
+
61
+ class SignatureNotification(Notification):
62
+ value: dict[str, None | str]
63
+
64
+ @model_validator(mode="before")
65
+ @classmethod
66
+ def wrap_value(cls, data):
67
+ return {"value": data}
68
+
69
+
70
+ class SlotNotification(Notification):
71
+ parent: int
72
+ root: int
73
+ slot: int
74
+
75
+
76
+ class SlotsUpdatesNotification(Notification):
77
+ err: str | None = None
78
+ parent: int | None = None
79
+ slot: int
80
+ stats: dict[str, int] | None = None
81
+ timestamp: int
82
+ type: Literal[
83
+ "firstShredReceived",
84
+ "completed",
85
+ "createdBank",
86
+ "frozen",
87
+ "dead",
88
+ "optimisticConfirmation",
89
+ "root",
90
+ ]
91
+
92
+
93
+ class VoteNotification(Notification):
94
+ hash: str
95
+ slots: list[int]
96
+ timestamp: int | None
97
+ signature: str
98
+ vote_pubkey: str
99
+
100
+
101
+ class WebSocketClient:
102
+ class MentionsFilter(TypedDict):
103
+ mentions: Annotated[list[str], Field(min_length=1, max_length=1)]
104
+
105
+ class BlockMentionsFilter(TypedDict):
106
+ mentionsAccountOrProgram: str
107
+
108
+ class MemcmpFilter(TypedDict):
109
+ offset: int
110
+ bytes: str
111
+
112
+ class DataSizeFilter(TypedDict):
113
+ dataSize: int
114
+
115
+ MODELS = {
116
+ "transactionNotification": TransactionNotification,
117
+ "accountNotification": AccountNotification,
118
+ "blockNotification": BlockNotification,
119
+ "logsNotification": LogsNotification,
120
+ "programNotification": ProgramNotification,
121
+ "rootNotification": RootNotification,
122
+ "signatureNotification": SignatureNotification,
123
+ "slotNotification": SlotNotification,
124
+ "slotsUpdatesNotification": SlotsUpdatesNotification,
125
+ "voteNotification": VoteNotification,
126
+ }
127
+
128
+ def __init__(
129
+ self,
130
+ *,
131
+ base_url="wss://mainnet.helius-rpc.com",
132
+ api_key: str | None = None,
133
+ proxy: str | None = None,
134
+ ):
135
+
136
+ base_url = base_url
137
+ api_key = (
138
+ api_key
139
+ or environ.get("HELIUS_API_KEY")
140
+ or dotenv_values().get("HELIUS_API_KEY")
141
+ or None
142
+ )
143
+ if not api_key:
144
+ raise ValueError("No API key provided.")
145
+ uri = httpx.URL(base_url).copy_with(path="/", params={"api-key": api_key})
146
+ self._websocket = connect(str(uri), proxy=proxy)
147
+
148
+ def close(self):
149
+ self._websocket.close()
150
+
151
+ def __enter__(self):
152
+ return self
153
+
154
+ def __exit__(self, exc_type, exc_value, traceback):
155
+ self.close()
156
+
157
+ def _send(self, request) -> dict:
158
+ self._websocket.send(json.dumps(request))
159
+ return json.loads(self._websocket.recv())
160
+
161
+ def _recv(self):
162
+ response = self._websocket.recv()
163
+ return json.loads(response)
164
+
165
+ def _unsubscribe(self, subscription_type, subscription) -> bool:
166
+ request = (
167
+ JsonRpcRequest(method=f"{subscription_type}Unsubscribe")
168
+ .add(subscription)
169
+ .build()
170
+ )
171
+ response = self._send(request)
172
+ return response["result"]
173
+
174
+ def transaction_subscribe(
175
+ self,
176
+ *,
177
+ vote: bool | None = None,
178
+ failed: bool | None = None,
179
+ signature: str | None = None,
180
+ account_include: list[str] | None = None,
181
+ account_exclude: list[str] | None = None,
182
+ account_required: list[str] | None = None,
183
+ commitment: Literal["finalized", "confirmed", "processed"] | None = None,
184
+ encoding: Literal["base58", "base64", "jsonParsed"] | None = None,
185
+ transaction_details: (
186
+ Literal["full", "signatures", "accounts", "none"] | None
187
+ ) = None,
188
+ show_rewards: bool | None = None,
189
+ max_supported_transaction_version: int | None = None,
190
+ ) -> int:
191
+ if max_supported_transaction_version is None and transaction_details in [
192
+ "accounts",
193
+ "full",
194
+ ]:
195
+ raise ValueError(
196
+ 'max_supported_transaction_version is required when transaction_details is set to "accounts" or "full".'
197
+ )
198
+ filter = {
199
+ key: value
200
+ for key, value in {
201
+ "vote": vote,
202
+ "failed": failed,
203
+ "signature": signature,
204
+ "accountInclude": account_include,
205
+ "accountExclude": account_exclude,
206
+ "accountRequired": account_required,
207
+ }.items()
208
+ if value is not None
209
+ }
210
+ request = (
211
+ JsonRpcRequest(method="transactionSubscribe")
212
+ .add(filter if filter else None)
213
+ .set("commitment", commitment)
214
+ .set("encoding", encoding)
215
+ .set("transactionDetails", transaction_details)
216
+ .set("showRewards", show_rewards)
217
+ .set("maxSupportedTransactionVersion", max_supported_transaction_version)
218
+ .build()
219
+ )
220
+ response = self._send(request)
221
+ subscription = response["result"]
222
+ return subscription
223
+
224
+ def account_subscribe(
225
+ self,
226
+ *,
227
+ pubkey: str,
228
+ encoding: Literal["base58", "base64", "base64+zstd", "jsonParsed"]
229
+ | None = None,
230
+ commitment: Literal["finalized", "confirmed", "processed"] | None = None,
231
+ ) -> int:
232
+ request = (
233
+ JsonRpcRequest(method="accountSubscribe")
234
+ .add(pubkey)
235
+ .set("commitment", commitment)
236
+ .set("encoding", encoding)
237
+ .build()
238
+ )
239
+ response = self._send(request)
240
+ subscription = response["result"]
241
+ return subscription
242
+
243
+ def block_subscribe(
244
+ self,
245
+ *,
246
+ filter: Literal["all"] | BlockMentionsFilter,
247
+ commitment: Literal["finalized", "confirmed", "processed"] | None = None,
248
+ encoding: Literal["base58", "base64", "base64+zstd", "jsonParsed"]
249
+ | None = None,
250
+ transaction_details: (
251
+ Literal["full", "signatures", "accounts", "none"] | None
252
+ ) = None,
253
+ max_supported_transaction_version: int | None = None,
254
+ show_rewards: bool | None = None,
255
+ ) -> int:
256
+ request = (
257
+ JsonRpcRequest(method="blockSubscribe")
258
+ .add(filter)
259
+ .set("commitment", commitment)
260
+ .set("encoding", encoding)
261
+ .set("transactionDetails", transaction_details)
262
+ .set("maxSupportedTransactionVersion", max_supported_transaction_version)
263
+ .set("showRewards", show_rewards)
264
+ .build()
265
+ )
266
+ response = self._send(request)
267
+ subscription = response["result"]
268
+ return subscription
269
+
270
+ def logs_subscribe(
271
+ self,
272
+ *,
273
+ filter: Literal["all", "allWithVotes"] | MentionsFilter,
274
+ commitment: Literal["finalized", "confirmed", "processed"] | None = None,
275
+ ):
276
+ request = (
277
+ JsonRpcRequest(method="logsSubscribe")
278
+ .add(filter)
279
+ .set("commitment", commitment)
280
+ .build()
281
+ )
282
+ response = self._send(request)
283
+ subscription = response["result"]
284
+ return subscription
285
+
286
+ def program_subscribe(
287
+ self,
288
+ *,
289
+ program_id: str,
290
+ commitment: Literal["finalized", "confirmed", "processed"] | None = None,
291
+ encoding: Literal["base58", "base64", "base64+zstd", "jsonParsed"]
292
+ | None = None,
293
+ filters: list[MemcmpFilter | DataSizeFilter] | None = None,
294
+ ) -> int:
295
+ request = (
296
+ JsonRpcRequest(method="programSubscribe")
297
+ .add(program_id)
298
+ .set("commitment", commitment)
299
+ .set("encoding", encoding)
300
+ .set("filters", filters)
301
+ .build()
302
+ )
303
+ response = self._send(request)
304
+ subscription = response["result"]
305
+ return subscription
306
+
307
+ def root_subscribe(self) -> int:
308
+ request = JsonRpcRequest(method="rootSubscribe").build()
309
+ response = self._send(request)
310
+ subscription = response["result"]
311
+ return subscription
312
+
313
+ def signature_subscribe(
314
+ self,
315
+ *,
316
+ signature: str,
317
+ commitment: Literal["finalized", "confirmed", "processed"] | None = None,
318
+ enable_received_notification: bool | None = None,
319
+ ) -> int:
320
+ request = (
321
+ JsonRpcRequest(method="signatureSubscribe")
322
+ .add(signature)
323
+ .set("commitment", commitment)
324
+ .set("enableReceivedNotification", enable_received_notification)
325
+ .build()
326
+ )
327
+ response = self._send(request)
328
+ subscription = response["result"]
329
+ return subscription
330
+
331
+ def slot_subscribe(self) -> int:
332
+ request = JsonRpcRequest(method="slotSubscribe").build()
333
+ response = self._send(request)
334
+ subscription = response["result"]
335
+ return subscription
336
+
337
+ def slots_updates_subscribe(self) -> int:
338
+ request = JsonRpcRequest(method="slotsUpdatesSubscribe").build()
339
+ response = self._send(request)
340
+ subscription = response["result"]
341
+ return subscription
342
+
343
+ def vote_subscribe(self) -> int:
344
+ request = JsonRpcRequest(method="voteSubscribe").build()
345
+ response = self._send(request)
346
+ subscription = response["result"]
347
+ return subscription
348
+
349
+ def transaction_unsubscribe(self, subscription) -> bool:
350
+ return self._unsubscribe("transaction", subscription)
351
+
352
+ def account_unsubscribe(self, subscription) -> bool:
353
+ return self._unsubscribe("account", subscription)
354
+
355
+ def block_unsubscribe(self, subscription) -> bool:
356
+ return self._unsubscribe("block", subscription)
357
+
358
+ def logs_unsubscribe(self, subscription) -> bool:
359
+ return self._unsubscribe("logs", subscription)
360
+
361
+ def program_unsubscribe(self, subscription) -> bool:
362
+ return self._unsubscribe("program", subscription)
363
+
364
+ def root_unsubscribe(self, subscription) -> bool:
365
+ return self._unsubscribe("root", subscription)
366
+
367
+ def signature_unsubscribe(self, subscription) -> bool:
368
+ return self._unsubscribe("signature", subscription)
369
+
370
+ def slot_unsubscribe(self, subscription) -> bool:
371
+ return self._unsubscribe("slot", subscription)
372
+
373
+ def slots_updates_unsubscribe(self, subscription) -> bool:
374
+ return self._unsubscribe("slotsUpdates", subscription)
375
+
376
+ def vote_unsubscribe(self, subscription) -> bool:
377
+ return self._unsubscribe("vote", subscription)
378
+
379
+ def receive(self) -> tuple[dict | None, Notification, int]:
380
+ response = json.loads(self._websocket.recv())
381
+ model = self.MODELS[response["method"]]
382
+ result = response["params"]["result"]
383
+ subscription = response["params"]["subscription"]
384
+ if isinstance(result, dict):
385
+ context = result.get("context")
386
+ value = result.get("value")
387
+ else:
388
+ context, value = None, None
389
+ if value is not None:
390
+ notification = model.model_validate(value)
391
+ else:
392
+ notification = model.model_validate(result)
393
+ return context, notification, subscription
394
+
395
+ def listen(self):
396
+ while True:
397
+ context, notification, subscription = self.receive()
398
+ yield context, notification, subscription
helius/rpc/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from helius.rpc.json_rpc_request import JsonRpcRequest
2
+
3
+ __all__ = ["JsonRpcRequest"]
@@ -0,0 +1,52 @@
1
+ from typing import Any
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class JsonRpcRequest:
7
+ class Request(BaseModel):
8
+ jsonrpc: str
9
+ method: str
10
+ params: list[Any] | None = None
11
+ id: str | int | None
12
+
13
+ def __init__(
14
+ self,
15
+ *,
16
+ jsonrpc: str = "2.0",
17
+ method: str,
18
+ id: str | int | None = 1,
19
+ ):
20
+ self._jsonrpc = jsonrpc
21
+ self._method = method
22
+ self._id = id
23
+ self._positional: list[Any] = []
24
+ self._config: dict[str, Any] = {}
25
+
26
+ def add(self, value, can_be_none: bool = False):
27
+ # If dict (for example) strip none values to remove the building burden from the function that calls it
28
+ if value is not None:
29
+ self._positional.append(value)
30
+ elif can_be_none:
31
+ self._positional.append(None)
32
+ return self
33
+
34
+ def set(self, key: str, value, can_be_none: bool = False):
35
+ if value is not None:
36
+ self._config.update({key: value})
37
+ elif can_be_none:
38
+ self._config.update({key: None})
39
+ return self
40
+
41
+ def build(self):
42
+ params = self._positional if self._positional else []
43
+ if self._config:
44
+ params.append(self._config)
45
+ request = {
46
+ "jsonrpc": self._jsonrpc,
47
+ "method": self._method,
48
+ "id": self._id,
49
+ }
50
+ if params:
51
+ request.update({"params": params})
52
+ return self.Request(**request).model_dump()
@@ -0,0 +1,51 @@
1
+ from helius.solana_rpc.client import SolanaRpcClient
2
+ from helius.solana_rpc.models import (
3
+ Account,
4
+ Block,
5
+ BlockCommitment,
6
+ ClusterNode,
7
+ EpochInfo,
8
+ EpochSchedule,
9
+ InflationGovernor,
10
+ InflationRate,
11
+ InflationReward,
12
+ LamportAccount,
13
+ PerformanceSample,
14
+ ProgramAccount,
15
+ Rewards,
16
+ SignatureStatus,
17
+ Supply,
18
+ TokenAccount,
19
+ TokenAccountBalance,
20
+ TokenSupply,
21
+ Transaction,
22
+ TransactionMetadata,
23
+ TransactionSignature,
24
+ VotingAccount,
25
+ )
26
+
27
+ __all__ = [
28
+ "Account",
29
+ "Block",
30
+ "BlockCommitment",
31
+ "ClusterNode",
32
+ "EpochInfo",
33
+ "EpochSchedule",
34
+ "InflationGovernor",
35
+ "InflationRate",
36
+ "InflationReward",
37
+ "LamportAccount",
38
+ "PerformanceSample",
39
+ "ProgramAccount",
40
+ "Rewards",
41
+ "SignatureStatus",
42
+ "SolanaRpcClient",
43
+ "Supply",
44
+ "TokenAccount",
45
+ "TokenAccountBalance",
46
+ "TokenSupply",
47
+ "Transaction",
48
+ "TransactionMetadata",
49
+ "TransactionSignature",
50
+ "VotingAccount",
51
+ ]
@@ -1,11 +1,12 @@
1
- import os
2
- from typing import Annotated, Any, Literal
1
+ from os import environ
2
+ from typing import Annotated, Literal
3
3
 
4
4
  import httpx
5
5
  from dotenv import dotenv_values
6
- from pydantic import BaseModel, Field, TypeAdapter, validate_call
6
+ from pydantic import Field, TypeAdapter, validate_call
7
7
 
8
- from helius.models import (
8
+ from helius.rpc import JsonRpcRequest
9
+ from helius.solana_rpc.models import (
9
10
  Account,
10
11
  Block,
11
12
  BlockCommitment,
@@ -17,6 +18,7 @@ from helius.models import (
17
18
  InflationReward,
18
19
  LamportAccount,
19
20
  PerformanceSample,
21
+ ProgramAccount,
20
22
  SignatureStatus,
21
23
  Supply,
22
24
  TokenAccount,
@@ -28,24 +30,27 @@ from helius.models import (
28
30
  )
29
31
 
30
32
 
33
+ # TODO: Use Pydantic typed dict where useful
31
34
  class SolanaRpcClient:
32
35
  def __init__(
33
36
  self,
34
37
  *,
35
38
  base_url: str = "https://mainnet.helius-rpc.com",
36
39
  api_key: str | None = None,
40
+ headers: dict[str, str] | None = None,
41
+ proxy: str | None = None,
37
42
  ) -> None:
38
- self.base_url = base_url
39
- self.api_key = (
43
+ base_url = base_url
44
+ api_key = (
40
45
  api_key
41
- or os.environ.get("HELIUS_API_KEY")
46
+ or environ.get("HELIUS_API_KEY")
42
47
  or dotenv_values().get("HELIUS_API_KEY")
48
+ or None
43
49
  )
44
- if not self.api_key:
50
+ if not api_key:
45
51
  raise ValueError("No API key provided.")
46
52
  self._client = httpx.Client(
47
- base_url=self.base_url,
48
- params={"api-key": self.api_key},
53
+ base_url=base_url, params={"api-key": api_key}, headers=headers, proxy=proxy
49
54
  )
50
55
 
51
56
  def __enter__(self):
@@ -82,7 +87,7 @@ class SolanaRpcClient:
82
87
  "Set both data_slice_length and data_slice_offset or neither."
83
88
  )
84
89
  request = (
85
- RpcRequest(method="getAccountInfo")
90
+ JsonRpcRequest(method="getAccountInfo")
86
91
  .add(public_key)
87
92
  .set("commitment", commitment)
88
93
  .set("encoding", encoding)
@@ -111,7 +116,7 @@ class SolanaRpcClient:
111
116
  min_context_slot: int | None = None,
112
117
  ) -> tuple[dict, int]:
113
118
  request = (
114
- RpcRequest(method="getBalance")
119
+ JsonRpcRequest(method="getBalance")
115
120
  .add(public_key)
116
121
  .set("commitment", commitment)
117
122
  .set("minContextSlot", min_context_slot)
@@ -135,7 +140,7 @@ class SolanaRpcClient:
135
140
  max_supported_transcation_version: int | None = None,
136
141
  ) -> Block | None:
137
142
  request = (
138
- RpcRequest(method="getBlock")
143
+ JsonRpcRequest(method="getBlock")
139
144
  .add(slot)
140
145
  .set("commitment", commitment)
141
146
  .set("encoding", encoding)
@@ -153,7 +158,7 @@ class SolanaRpcClient:
153
158
  *,
154
159
  slot: int,
155
160
  ) -> BlockCommitment:
156
- request = RpcRequest(method="getBlockCommitment").add(slot).build()
161
+ request = JsonRpcRequest(method="getBlockCommitment").add(slot).build()
157
162
  response = self._send(request)
158
163
  block_commitment = BlockCommitment.model_validate(response["result"])
159
164
  return block_commitment
@@ -165,7 +170,7 @@ class SolanaRpcClient:
165
170
  min_context_slot: int | None = None,
166
171
  ) -> int:
167
172
  request = (
168
- RpcRequest(method="getBlockHeight")
173
+ JsonRpcRequest(method="getBlockHeight")
169
174
  .set("commitment", commitment)
170
175
  .set("minContextSlot", min_context_slot)
171
176
  .build()
@@ -198,7 +203,7 @@ class SolanaRpcClient:
198
203
  if last_slot is not None:
199
204
  range.update({"lastSlot": last_slot})
200
205
  request = (
201
- RpcRequest(method="getBlockProduction")
206
+ JsonRpcRequest(method="getBlockProduction")
202
207
  .set("commitment", commitment)
203
208
  .set("range", range)
204
209
  .set("identity", identity)
@@ -221,7 +226,7 @@ class SolanaRpcClient:
221
226
  The range between start_slot and end_slot (or latest slot if end_slot is omitted) must not exceed 500,000 slots.
222
227
  """
223
228
  request = (
224
- RpcRequest(method="getBlocks")
229
+ JsonRpcRequest(method="getBlocks")
225
230
  .add(start_slot)
226
231
  .add(end_slot)
227
232
  .set("commitment", commitment)
@@ -238,7 +243,7 @@ class SolanaRpcClient:
238
243
  commitment: Literal["finalized", "confirmed", "processed"] | None = None,
239
244
  ) -> list[int]:
240
245
  request = (
241
- RpcRequest(method="getBlocksWithLimit")
246
+ JsonRpcRequest(method="getBlocksWithLimit")
242
247
  .add(start_slot)
243
248
  .add(limit)
244
249
  .set("commitment", commitment)
@@ -248,12 +253,12 @@ class SolanaRpcClient:
248
253
  return response["result"]
249
254
 
250
255
  def get_block_time(self, *, slot: int) -> int | None:
251
- request = RpcRequest(method="getBlockTime").add(slot).build()
256
+ request = JsonRpcRequest(method="getBlockTime").add(slot).build()
252
257
  response = self._send(request)
253
258
  return response["result"]
254
259
 
255
260
  def get_cluster_nodes(self) -> list[ClusterNode]:
256
- request = RpcRequest(method="getClusterNodes").build()
261
+ request = JsonRpcRequest(method="getClusterNodes").build()
257
262
  response = self._send(request)
258
263
  ta = TypeAdapter(list[ClusterNode])
259
264
  cluster_nodes = ta.validate_python(response["result"])
@@ -266,7 +271,7 @@ class SolanaRpcClient:
266
271
  min_context_slot: int | None = None,
267
272
  ) -> EpochInfo:
268
273
  request = (
269
- RpcRequest(method="getEpochInfo")
274
+ JsonRpcRequest(method="getEpochInfo")
270
275
  .set("commitment", commitment)
271
276
  .set("minContextSlot", min_context_slot)
272
277
  .build()
@@ -276,7 +281,7 @@ class SolanaRpcClient:
276
281
  return epoch_info
277
282
 
278
283
  def get_epoch_schedule(self) -> EpochSchedule:
279
- request = RpcRequest(method="getEpochSchedule").build()
284
+ request = JsonRpcRequest(method="getEpochSchedule").build()
280
285
  response = self._send(request)
281
286
  epoch_schedule = EpochSchedule.model_validate(response["result"])
282
287
  return epoch_schedule
@@ -289,7 +294,7 @@ class SolanaRpcClient:
289
294
  min_context_slot: int | None = None,
290
295
  ) -> tuple[dict, int | None]:
291
296
  request = (
292
- RpcRequest(method="getFeeForMessage")
297
+ JsonRpcRequest(method="getFeeForMessage")
293
298
  .add(message)
294
299
  .set("commitment", commitment)
295
300
  .set("minContextSlot", min_context_slot)
@@ -301,17 +306,17 @@ class SolanaRpcClient:
301
306
  return context, value
302
307
 
303
308
  def get_first_available_block(self) -> int:
304
- request = RpcRequest(method="getFirstAvailableBlock").build()
309
+ request = JsonRpcRequest(method="getFirstAvailableBlock").build()
305
310
  response = self._send(request)
306
311
  return response["result"]
307
312
 
308
313
  def get_genesis_hash(self) -> str:
309
- request = RpcRequest(method="getGenesisHash").build()
314
+ request = JsonRpcRequest(method="getGenesisHash").build()
310
315
  response = self._send(request)
311
316
  return response["result"]
312
317
 
313
318
  def get_health(self) -> bool:
314
- request = RpcRequest(method="getHealth").build()
319
+ request = JsonRpcRequest(method="getHealth").build()
315
320
  response = self._send(request)
316
321
  if "result" in response and response["result"] == "ok":
317
322
  return True
@@ -319,12 +324,12 @@ class SolanaRpcClient:
319
324
  return False
320
325
 
321
326
  def get_highest_snapshot_slot(self) -> dict:
322
- request = RpcRequest(method="getHighestSnapshotSlot").build()
327
+ request = JsonRpcRequest(method="getHighestSnapshotSlot").build()
323
328
  response = self._send(request)
324
329
  return response["result"]
325
330
 
326
331
  def get_identity(self) -> str:
327
- request = RpcRequest(method="getIdentity").build()
332
+ request = JsonRpcRequest(method="getIdentity").build()
328
333
  response = self._send(request)
329
334
  identity = response["result"]["identity"]
330
335
  return identity
@@ -335,7 +340,7 @@ class SolanaRpcClient:
335
340
  commitment: Literal["finalized", "confirmed", "processed"] | None = None,
336
341
  ) -> InflationGovernor:
337
342
  request = (
338
- RpcRequest(method="getInflationGovernor")
343
+ JsonRpcRequest(method="getInflationGovernor")
339
344
  .set("commitment", commitment)
340
345
  .build()
341
346
  )
@@ -344,7 +349,7 @@ class SolanaRpcClient:
344
349
  return inflation_governor
345
350
 
346
351
  def get_inflation_rate(self) -> InflationRate:
347
- request = RpcRequest(method="getInflationRate").build()
352
+ request = JsonRpcRequest(method="getInflationRate").build()
348
353
  response = self._send(request)
349
354
  inflation_rate = InflationRate.model_validate(response["result"])
350
355
  return inflation_rate
@@ -358,7 +363,7 @@ class SolanaRpcClient:
358
363
  min_context_slot: int | None = None,
359
364
  ) -> list[InflationReward | None]:
360
365
  request = (
361
- RpcRequest(method="getInflationReward")
366
+ JsonRpcRequest(method="getInflationReward")
362
367
  .add(addresses)
363
368
  .set("commitment", commitment)
364
369
  .set("epoch", epoch)
@@ -377,7 +382,7 @@ class SolanaRpcClient:
377
382
  filter: Literal["circulating", "nonCirculating"] | None = None,
378
383
  ) -> tuple[dict, list[LamportAccount]]:
379
384
  request = (
380
- RpcRequest(method="getLargestAccounts")
385
+ JsonRpcRequest(method="getLargestAccounts")
381
386
  .set("commitment", commitment)
382
387
  .set("filter", filter)
383
388
  .build()
@@ -396,7 +401,7 @@ class SolanaRpcClient:
396
401
  min_context_slot: int | None = None,
397
402
  ) -> tuple[dict, str, int]:
398
403
  request = (
399
- RpcRequest(method="getLatestBlockhash")
404
+ JsonRpcRequest(method="getLatestBlockhash")
400
405
  .set("commitment", commitment)
401
406
  .set("minContextSlot", min_context_slot)
402
407
  .build()
@@ -416,7 +421,7 @@ class SolanaRpcClient:
416
421
  identity: str | None = None,
417
422
  ) -> dict[str, list[int]] | None:
418
423
  request = (
419
- RpcRequest(method="getLeaderSchedule")
424
+ JsonRpcRequest(method="getLeaderSchedule")
420
425
  .add(slot)
421
426
  .set("commitment", commitment)
422
427
  .set("identity", identity)
@@ -427,13 +432,13 @@ class SolanaRpcClient:
427
432
  return result
428
433
 
429
434
  def get_max_retransmit_slot(self) -> int:
430
- request = RpcRequest(method="getMaxRetransmitSlot").build()
435
+ request = JsonRpcRequest(method="getMaxRetransmitSlot").build()
431
436
  response = self._send(request)
432
437
  result = response["result"]
433
438
  return result
434
439
 
435
440
  def get_max_shred_insert_slot(self) -> int:
436
- request = RpcRequest(method="getMaxShredInsertSlot").build()
441
+ request = JsonRpcRequest(method="getMaxShredInsertSlot").build()
437
442
  response = self._send(request)
438
443
  result = response["result"]
439
444
  return result
@@ -445,7 +450,7 @@ class SolanaRpcClient:
445
450
  commitment: Literal["finalized", "confirmed", "processed"] | None = None,
446
451
  ) -> int:
447
452
  request = (
448
- RpcRequest(method="getMinimumBalanceForRentExemption")
453
+ JsonRpcRequest(method="getMinimumBalanceForRentExemption")
449
454
  .add(data_length)
450
455
  .set("commitment", commitment)
451
456
  .build()
@@ -478,7 +483,7 @@ class SolanaRpcClient:
478
483
  "Data slice is only available for base58, base64, or base64+zstd encodings."
479
484
  )
480
485
  request = (
481
- RpcRequest(method="getMultipleAccounts")
486
+ JsonRpcRequest(method="getMultipleAccounts")
482
487
  .add(pubkeys)
483
488
  .set("commitment", commitment)
484
489
  .set("encoding", encoding)
@@ -513,13 +518,13 @@ class SolanaRpcClient:
513
518
  data_slice_length: int | None = None,
514
519
  changed_since_slot: int | None = None,
515
520
  filters: list[dict] | None = None,
516
- ) -> list[tuple[str, Account]]:
521
+ ) -> list[ProgramAccount]:
517
522
  if (data_slice_offset is None) != (data_slice_length is None):
518
523
  raise ValueError(
519
524
  "Set both data_slice_length and data_slice_offset or neither."
520
525
  )
521
526
  request = (
522
- RpcRequest(method="getProgramAccounts")
527
+ JsonRpcRequest(method="getProgramAccounts")
523
528
  .add(program_id)
524
529
  .set("commitment", commitment)
525
530
  .set("minContextSlot", min_context_slot)
@@ -538,15 +543,18 @@ class SolanaRpcClient:
538
543
  .build()
539
544
  )
540
545
  response = self._send(request)
541
- result = response["result"]
542
- return [(i["pubkey"], Account.model_validate(i["account"])) for i in result]
546
+ ta = TypeAdapter(list[ProgramAccount])
547
+ program_accounts = ta.validate_python(response["result"])
548
+ return program_accounts
543
549
 
544
550
  def get_recent_performance_samples(
545
551
  self,
546
552
  *,
547
553
  limit: int | None = None,
548
554
  ) -> list[PerformanceSample]:
549
- request = RpcRequest(method="getRecentPerformanceSamples").add(limit).build()
555
+ request = (
556
+ JsonRpcRequest(method="getRecentPerformanceSamples").add(limit).build()
557
+ )
550
558
  response = self._send(request)
551
559
  ta = TypeAdapter(list[PerformanceSample])
552
560
  return ta.validate_python(response["result"])
@@ -557,7 +565,7 @@ class SolanaRpcClient:
557
565
  locked_writable_accounts: list[str] | None = None,
558
566
  ) -> list[tuple[int, int]]:
559
567
  request = (
560
- RpcRequest(method="getRecentPrioritizationFees")
568
+ JsonRpcRequest(method="getRecentPrioritizationFees")
561
569
  .add(locked_writable_accounts)
562
570
  .build()
563
571
  )
@@ -576,7 +584,7 @@ class SolanaRpcClient:
576
584
  min_context_slot: int | None = None,
577
585
  ) -> list[TransactionSignature]:
578
586
  request = (
579
- RpcRequest(method="getSignaturesForAddress")
587
+ JsonRpcRequest(method="getSignaturesForAddress")
580
588
  .add(address)
581
589
  .set("limit", limit)
582
590
  .set("before", before)
@@ -597,7 +605,7 @@ class SolanaRpcClient:
597
605
  search_transaction_history: bool | None = None,
598
606
  ) -> tuple[dict, list[SignatureStatus | None]]:
599
607
  request = (
600
- RpcRequest(method="getSignatureStatuses")
608
+ JsonRpcRequest(method="getSignatureStatuses")
601
609
  .add(signatures)
602
610
  .set("searchTransactionHistory", search_transaction_history)
603
611
  .build()
@@ -617,7 +625,7 @@ class SolanaRpcClient:
617
625
  min_context_slot: int | None = None,
618
626
  ) -> int:
619
627
  request = (
620
- RpcRequest(method="getSlot")
628
+ JsonRpcRequest(method="getSlot")
621
629
  .set("commitment", commitment)
622
630
  .set("minContextSlot", min_context_slot)
623
631
  .build()
@@ -632,7 +640,7 @@ class SolanaRpcClient:
632
640
  min_context_slot: int | None = None,
633
641
  ) -> str:
634
642
  request = (
635
- RpcRequest(method="getSlotLeader")
643
+ JsonRpcRequest(method="getSlotLeader")
636
644
  .set("commitment", commitment)
637
645
  .set("minContextSlot", min_context_slot)
638
646
  .build()
@@ -647,7 +655,9 @@ class SolanaRpcClient:
647
655
  start_slot: int,
648
656
  limit: Annotated[int, Field(ge=1, le=5000)],
649
657
  ) -> list[str]:
650
- request = RpcRequest(method="getSlotLeaders").add(start_slot).add(limit).build()
658
+ request = (
659
+ JsonRpcRequest(method="getSlotLeaders").add(start_slot).add(limit).build()
660
+ )
651
661
  response = self._send(request)
652
662
  return response["result"]
653
663
 
@@ -657,7 +667,7 @@ class SolanaRpcClient:
657
667
  commitment: Literal["finalized", "confirmed", "processed"] | None = None,
658
668
  ) -> tuple[dict, int]:
659
669
  request = (
660
- RpcRequest(method="getStakeMinimumDelegation")
670
+ JsonRpcRequest(method="getStakeMinimumDelegation")
661
671
  .set("commitment", commitment)
662
672
  .build()
663
673
  )
@@ -673,7 +683,7 @@ class SolanaRpcClient:
673
683
  exclude_non_circulating_accounts_list: bool | None = None,
674
684
  ) -> tuple[dict, Supply]:
675
685
  request = (
676
- RpcRequest(method="getSupply")
686
+ JsonRpcRequest(method="getSupply")
677
687
  .set("commitment", commitment)
678
688
  .set(
679
689
  "excludeNonCirculatingAccountsList",
@@ -695,7 +705,7 @@ class SolanaRpcClient:
695
705
  commitment: Literal["finalized", "confirmed", "processed"] | None = None,
696
706
  ) -> tuple[dict, TokenAccountBalance]:
697
707
  request = (
698
- RpcRequest(method="getTokenAccountBalance")
708
+ JsonRpcRequest(method="getTokenAccountBalance")
699
709
  .add(token_account)
700
710
  .set("commitment", commitment)
701
711
  .build()
@@ -731,7 +741,7 @@ class SolanaRpcClient:
731
741
  )
732
742
  filter = {"mint": mint} if mint is not None else {"programId": program_id}
733
743
  request = (
734
- RpcRequest(method="getTokenAccountsByDelegate")
744
+ JsonRpcRequest(method="getTokenAccountsByDelegate")
735
745
  .add(delegate_pub_key)
736
746
  .add(filter)
737
747
  .set("commitment", commitment)
@@ -781,7 +791,7 @@ class SolanaRpcClient:
781
791
  )
782
792
  filter = {"mint": mint} if mint is not None else {"programId": program_id}
783
793
  request = (
784
- RpcRequest(method="getTokenAccountsByOwner")
794
+ JsonRpcRequest(method="getTokenAccountsByOwner")
785
795
  .add(owner_pub_key)
786
796
  .add(filter)
787
797
  .set("commitment", commitment)
@@ -814,7 +824,7 @@ class SolanaRpcClient:
814
824
  commitment: Literal["finalized", "confirmed", "processed"] | None = None,
815
825
  ) -> tuple[dict, list[TokenAccount]]:
816
826
  request = (
817
- RpcRequest(method="getTokenLargestAccounts")
827
+ JsonRpcRequest(method="getTokenLargestAccounts")
818
828
  .add(mint)
819
829
  .set("commitment", commitment)
820
830
  .build()
@@ -832,7 +842,7 @@ class SolanaRpcClient:
832
842
  commitment: Literal["finalized", "confirmed", "processed"] | None = None,
833
843
  ) -> tuple[dict, TokenSupply]:
834
844
  request = (
835
- RpcRequest(method="getTokenSupply")
845
+ JsonRpcRequest(method="getTokenSupply")
836
846
  .add(mint_address)
837
847
  .set("commitment", commitment)
838
848
  .build()
@@ -851,7 +861,7 @@ class SolanaRpcClient:
851
861
  max_supported_transaction_version: int | None = None,
852
862
  ) -> Transaction:
853
863
  request = (
854
- RpcRequest(method="getTransaction")
864
+ JsonRpcRequest(method="getTransaction")
855
865
  .add(transaction_signature)
856
866
  .set("commitment", commitment)
857
867
  .set("encoding", encoding)
@@ -869,7 +879,7 @@ class SolanaRpcClient:
869
879
  min_context_slot: int | None = None,
870
880
  ) -> int:
871
881
  request = (
872
- RpcRequest(method="getTransactionCount")
882
+ JsonRpcRequest(method="getTransactionCount")
873
883
  .set("commitment", commitment)
874
884
  .set("minContextSlot", min_context_slot)
875
885
  .build()
@@ -878,7 +888,7 @@ class SolanaRpcClient:
878
888
  return response["result"]
879
889
 
880
890
  def get_version(self) -> tuple[str, int]:
881
- request = RpcRequest(method="getVersion").build()
891
+ request = JsonRpcRequest(method="getVersion").build()
882
892
  response = self._send(request)
883
893
  result = response["result"]
884
894
  return result["solana-core"], result["feature-set"]
@@ -892,7 +902,7 @@ class SolanaRpcClient:
892
902
  delinquent_slot_distance: int | None = None,
893
903
  ) -> tuple[list[VotingAccount], list[VotingAccount]]:
894
904
  request = (
895
- RpcRequest(method="getVoteAccounts")
905
+ JsonRpcRequest(method="getVoteAccounts")
896
906
  .set("commitment", commitment)
897
907
  .set("votePubkey", vote_pubkey)
898
908
  .set("keepUnstakedDelinquents", keep_unstaked_delinquents)
@@ -913,7 +923,7 @@ class SolanaRpcClient:
913
923
  min_context_slot: int | None = None,
914
924
  ) -> tuple[dict, bool]:
915
925
  request = (
916
- RpcRequest(method="isBlockhashValid")
926
+ JsonRpcRequest(method="isBlockhashValid")
917
927
  .add(blockhash)
918
928
  .set("commitment", commitment)
919
929
  .set("minContextSlot", min_context_slot)
@@ -925,7 +935,7 @@ class SolanaRpcClient:
925
935
  return context, value
926
936
 
927
937
  def minimum_ledger_slot(self) -> int:
928
- request = RpcRequest(method="minimumLedgerSlot").build()
938
+ request = JsonRpcRequest(method="minimumLedgerSlot").build()
929
939
  response = self._send(request)
930
940
  return response["result"]
931
941
 
@@ -940,7 +950,7 @@ class SolanaRpcClient:
940
950
  Only available on Devnet and Testnet, not Mainnet Beta.
941
951
  """
942
952
  request = (
943
- RpcRequest(method="requestAirdrop")
953
+ JsonRpcRequest(method="requestAirdrop")
944
954
  .add(public_key)
945
955
  .add(lamports)
946
956
  .set("commitment", commitment)
@@ -962,7 +972,7 @@ class SolanaRpcClient:
962
972
  min_context_slot: int | None = None,
963
973
  ) -> str:
964
974
  request = (
965
- RpcRequest(method="sendTransaction")
975
+ JsonRpcRequest(method="sendTransaction")
966
976
  .add(transaction)
967
977
  .set("encoding", encoding)
968
978
  .set("skipPreflight", skip_preflight)
@@ -973,51 +983,3 @@ class SolanaRpcClient:
973
983
  )
974
984
  response = self._send(request)
975
985
  return response["result"]
976
-
977
-
978
- class RpcRequest:
979
- class Request(BaseModel):
980
- jsonrpc: str
981
- method: str
982
- params: list[Any] | None = None
983
- id: str | int | None
984
-
985
- def __init__(
986
- self,
987
- *,
988
- jsonrpc: str = "2.0",
989
- method: str,
990
- id: str | int | None = 1,
991
- ):
992
- self._jsonrpc = jsonrpc
993
- self._method = method
994
- self._id = id
995
- self._positional: list[Any] = []
996
- self._config: dict[str, Any] = {}
997
-
998
- def add(self, value, can_be_none: bool = False):
999
- if value is not None:
1000
- self._positional.append(value)
1001
- elif can_be_none:
1002
- self._positional.append(None)
1003
- return self
1004
-
1005
- def set(self, key: str, value, can_be_none: bool = False):
1006
- if value is not None:
1007
- self._config.update({key: value})
1008
- elif can_be_none:
1009
- self._config.update({key: None})
1010
- return self
1011
-
1012
- def build(self):
1013
- params = self._positional if self._positional else []
1014
- if self._config:
1015
- params.append(self._config)
1016
- request = {
1017
- "jsonrpc": self._jsonrpc,
1018
- "method": self._method,
1019
- "id": self._id,
1020
- }
1021
- if params:
1022
- request.update({"params": params})
1023
- return self.Request(**request).model_dump()
@@ -15,6 +15,13 @@ class Account(BaseModel):
15
15
  space: int | None = None
16
16
 
17
17
 
18
+ class ProgramAccount(BaseModel):
19
+ model_config = ConfigDict(alias_generator=AliasGenerator(validation_alias=to_camel))
20
+
21
+ pubkey: str
22
+ account: Account
23
+
24
+
18
25
  class Rewards(BaseModel):
19
26
  model_config = ConfigDict(alias_generator=AliasGenerator(validation_alias=to_camel))
20
27
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: helius-python
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: Typed Python client for the Helius API
5
5
  Project-URL: Homepage, https://github.com/markosnarinian/helius-python
6
6
  Project-URL: Issues, https://github.com/markosnarinian/helius-python/issues
@@ -13,6 +13,7 @@ Requires-Python: >=3.10
13
13
  Requires-Dist: httpx
14
14
  Requires-Dist: pydantic
15
15
  Requires-Dist: python-dotenv
16
+ Requires-Dist: websockets
16
17
  Provides-Extra: dev
17
18
  Requires-Dist: pytest; extra == 'dev'
18
19
  Requires-Dist: respx; extra == 'dev'
@@ -137,11 +138,16 @@ wraps it.
137
138
  Webhooks API, Mint API, token metadata, address lookups, and beyond.
138
139
  **(in progress)**
139
140
  - 🚧 **Platform features** — streaming, websockets, and any new
140
- capability Helius adds to its API. **(in progress)**
141
+ capability Helius adds to its API. The full WebSocket subscription
142
+ surface (`accountSubscribe`, `transactionSubscribe`, `logsSubscribe`,
143
+ `programSubscribe`, and the rest) is **supported today**; other
144
+ platform features are **in progress**.
141
145
 
142
- > **Current status:** only the standard Solana JSON-RPC surface is
143
- > implemented today. Support for Helius RPC extensions, REST endpoints,
144
- > and platform features is actively being worked on.
146
+ > **Current status:** the standard Solana JSON-RPC surface, the
147
+ > WebSocket subscription surface, and the Admin (account management)
148
+ > usage endpoint are implemented today. Support for Helius RPC
149
+ > extensions and the remaining REST endpoints is actively being worked
150
+ > on.
145
151
 
146
152
  ## Goals
147
153
 
@@ -152,6 +158,11 @@ wraps it.
152
158
  4. **Zero magic** — thin, predictable wrappers that map directly to the
153
159
  documented Helius API.
154
160
 
161
+ ## Installation via PyPI
162
+ ```bash
163
+ pip install helius-python
164
+ ```
165
+
155
166
  ## Authentication
156
167
 
157
168
  Pass your Helius API key explicitly:
@@ -312,6 +323,72 @@ continuously.
312
323
  | `requestAirdrop` | `request_airdrop(...)` | [guide](https://www.helius.dev/docs/rpc/guides/requestairdrop), [reference](https://www.helius.dev/docs/api-reference/rpc/http/requestairdrop) |
313
324
  | `sendTransaction` | `send_transaction(...)` | [guide](https://www.helius.dev/docs/rpc/guides/sendtransaction), [reference](https://www.helius.dev/docs/api-reference/rpc/http/sendtransaction) |
314
325
 
326
+ ## WebSocket subscriptions
327
+
328
+ `WebSocketClient` wraps the Solana/Helius WebSocket subscription surface.
329
+ It connects over `wss://` on construction, exposes one `*_subscribe` /
330
+ `*_unsubscribe` pair per subscription type, and parses incoming
331
+ notifications into typed pydantic models.
332
+
333
+ ```python
334
+ from helius.laserstream.websockets import WebSocketClient
335
+
336
+ with WebSocketClient(api_key="YOUR_HELIUS_API_KEY") as ws:
337
+ subscription = ws.account_subscribe(
338
+ pubkey="So11111111111111111111111111111111111111112",
339
+ commitment="confirmed",
340
+ )
341
+
342
+ for context, notification, sub in ws.listen():
343
+ print(notification) # typed AccountNotification
344
+
345
+ ws.account_unsubscribe(subscription)
346
+ ```
347
+
348
+ Like `SolanaRpcClient`, it supports the context-manager protocol and a
349
+ manual `close()`. The constructor defaults to
350
+ `wss://mainnet.helius-rpc.com` and reads `HELIUS_API_KEY` from the
351
+ environment or `.env` when `api_key` is omitted; an optional `proxy` is
352
+ also accepted.
353
+
354
+ Each `*_subscribe` call returns the integer subscription id. Use
355
+ `receive()` to read a single notification or `listen()` to iterate over
356
+ them; both yield a `(context, notification, subscription)` tuple where
357
+ `notification` is the model below.
358
+
359
+ | Subscribe method | Unsubscribe method | Notification model | Helius docs |
360
+ | ------------------------------- | --------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
361
+ | `account_subscribe(...)` | `account_unsubscribe(...)` | `AccountNotification` | [reference](https://www.helius.dev/docs/api-reference/rpc/websocket/accountsubscribe) |
362
+ | `block_subscribe(...)` | `block_unsubscribe(...)` | `BlockNotification` | [reference](https://www.helius.dev/docs/api-reference/rpc/websocket/blocksubscribe) |
363
+ | `logs_subscribe(...)` | `logs_unsubscribe(...)` | `LogsNotification` | [reference](https://www.helius.dev/docs/api-reference/rpc/websocket/logssubscribe) |
364
+ | `program_subscribe(...)` | `program_unsubscribe(...)` | `ProgramNotification` | [reference](https://www.helius.dev/docs/api-reference/rpc/websocket/programsubscribe) |
365
+ | `root_subscribe()` | `root_unsubscribe(...)` | `RootNotification` | [reference](https://www.helius.dev/docs/api-reference/rpc/websocket/rootsubscribe) |
366
+ | `signature_subscribe(...)` | `signature_unsubscribe(...)` | `SignatureNotification` | [reference](https://www.helius.dev/docs/api-reference/rpc/websocket/signaturesubscribe) |
367
+ | `slot_subscribe()` | `slot_unsubscribe(...)` | `SlotNotification` | [reference](https://www.helius.dev/docs/api-reference/rpc/websocket/slotsubscribe) |
368
+ | `slots_updates_subscribe()` | `slots_updates_unsubscribe(...)` | `SlotsUpdatesNotification` | [reference](https://www.helius.dev/docs/api-reference/rpc/websocket/slotsupdatessubscribe) |
369
+ | `vote_subscribe()` | `vote_unsubscribe(...)` | `VoteNotification` | [reference](https://www.helius.dev/docs/api-reference/rpc/websocket/votesubscribe) |
370
+ | `transaction_subscribe(...)` | `transaction_unsubscribe(...)` | `TransactionNotification` | [reference](https://www.helius.dev/docs/api-reference/rpc/websocket/slotunsubscribe) |
371
+
372
+ ## Admin API
373
+
374
+ `AccountManagementClient` wraps the Helius admin API. Today it exposes
375
+ project credit usage via `get_project_usage(...)`, which returns a typed
376
+ `ProjectUsage` model.
377
+
378
+ ```python
379
+ from helius.admin.admin import AccountManagementClient
380
+
381
+ with AccountManagementClient(api_key="YOUR_HELIUS_API_KEY") as admin:
382
+ usage = admin.get_project_usage(project_id="YOUR_PROJECT_ID")
383
+ print(usage.credits_remaining, usage.usage.rpc)
384
+ ```
385
+
386
+ The `project_id` can be passed per call or set once on the constructor;
387
+ if neither is provided, `get_project_usage` raises `ValueError`. The
388
+ client defaults to `https://admin-api.helius.xyz` and, like the others,
389
+ supports the context-manager protocol, a manual `close()`, and reads
390
+ `HELIUS_API_KEY` from the environment or `.env`.
391
+
315
392
  ## License
316
393
 
317
394
  [MIT](LICENSE)
@@ -0,0 +1,13 @@
1
+ helius/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ helius/admin/__init__.py,sha256=HboRJXZNzuDAc6r3kj2QZtEcu2U3EWJHv81dYDJGjaA,262
3
+ helius/admin/admin.py,sha256=dcmOgeu10Q43QZC8K-a25vp2JNAb4hs5gu3YWaovV4Y,2541
4
+ helius/laserstream/websockets.py,sha256=ASAltIRlfWKgjkAwox8xbYqULsU6GtJd7TN_nX_F8jA,12417
5
+ helius/rpc/__init__.py,sha256=ciwM1KfBNyZwCeqOmlWTcSrUD03cbrzgeeEZsn9msi0,85
6
+ helius/rpc/json_rpc_request.py,sha256=hm7WkPo0CnwMnuHE4qR-d93wGArZRPRm-B3uueykL3E,1479
7
+ helius/solana_rpc/__init__.py,sha256=Mfhb6ibrCqAX6AchtL3ViBlpCi9RKP8uhT9e_S5_Wb0,1006
8
+ helius/solana_rpc/client.py,sha256=8WDsBqFYlRcnAYiGB2_gPOm3UuQSSV4JlEY9qwpr63s,34116
9
+ helius/solana_rpc/models.py,sha256=ywzdyUNrJ4mPxwjuntsmJT-tBcHE64pkG96--jKAgIw,5474
10
+ helius_python-0.3.1.dist-info/METADATA,sha256=rtSGuVYxo8jVc1BSiBOBm0ysq0eMrPOb4EafQ4bpDWY,34392
11
+ helius_python-0.3.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
12
+ helius_python-0.3.1.dist-info/licenses/LICENSE,sha256=bZc2EDmq_GsWH77uK8LBn1kqqrOPcp2f9p-ys9bYa1E,1072
13
+ helius_python-0.3.1.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- helius/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- helius/models.py,sha256=O_T8WQB_C4AFXCs6CU2ges5CUxoOPahUkf9Z6S_AEnQ,5312
3
- helius/solana_rpc.py,sha256=yzxQqsx7UIJ295m366Oh6ZDdUwhXsrkJ99emAEEE9JM,34952
4
- helius_python-0.2.0.dist-info/METADATA,sha256=wxBqRq8eDo6a6DGYr3daRBgCB7dul4dfoVUS4YwoP_8,28985
5
- helius_python-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
6
- helius_python-0.2.0.dist-info/licenses/LICENSE,sha256=bZc2EDmq_GsWH77uK8LBn1kqqrOPcp2f9p-ys9bYa1E,1072
7
- helius_python-0.2.0.dist-info/RECORD,,