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.
- helius/admin/__init__.py +15 -0
- helius/admin/admin.py +95 -0
- helius/laserstream/websockets.py +398 -0
- helius/rpc/__init__.py +3 -0
- helius/rpc/json_rpc_request.py +52 -0
- helius/solana_rpc/__init__.py +51 -0
- helius/{solana_rpc.py → solana_rpc/client.py} +74 -112
- helius/{models.py → solana_rpc/models.py} +7 -0
- {helius_python-0.2.0.dist-info → helius_python-0.3.1.dist-info}/METADATA +82 -5
- helius_python-0.3.1.dist-info/RECORD +13 -0
- helius_python-0.2.0.dist-info/RECORD +0 -7
- {helius_python-0.2.0.dist-info → helius_python-0.3.1.dist-info}/WHEEL +0 -0
- {helius_python-0.2.0.dist-info → helius_python-0.3.1.dist-info}/licenses/LICENSE +0 -0
helius/admin/__init__.py
ADDED
|
@@ -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,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
|
|
2
|
-
from typing import Annotated,
|
|
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
|
|
6
|
+
from pydantic import Field, TypeAdapter, validate_call
|
|
7
7
|
|
|
8
|
-
from helius.
|
|
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
|
-
|
|
39
|
-
|
|
43
|
+
base_url = base_url
|
|
44
|
+
api_key = (
|
|
40
45
|
api_key
|
|
41
|
-
or
|
|
46
|
+
or environ.get("HELIUS_API_KEY")
|
|
42
47
|
or dotenv_values().get("HELIUS_API_KEY")
|
|
48
|
+
or None
|
|
43
49
|
)
|
|
44
|
-
if not
|
|
50
|
+
if not api_key:
|
|
45
51
|
raise ValueError("No API key provided.")
|
|
46
52
|
self._client = httpx.Client(
|
|
47
|
-
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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[
|
|
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
|
-
|
|
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
|
-
|
|
542
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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:**
|
|
143
|
-
>
|
|
144
|
-
>
|
|
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,,
|
|
File without changes
|
|
File without changes
|