helius-python 0.0.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/__init__.py +0 -0
- helius/client.py +941 -0
- helius/models.py +206 -0
- helius_python-0.0.1.dist-info/METADATA +231 -0
- helius_python-0.0.1.dist-info/RECORD +7 -0
- helius_python-0.0.1.dist-info/WHEEL +4 -0
- helius_python-0.0.1.dist-info/licenses/LICENSE +21 -0
helius/client.py
ADDED
|
@@ -0,0 +1,941 @@
|
|
|
1
|
+
from typing import Annotated, Any, Literal
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
from dotenv import dotenv_values
|
|
5
|
+
from pydantic import BaseModel, Field, TypeAdapter, validate_call
|
|
6
|
+
|
|
7
|
+
from helius.models import (
|
|
8
|
+
Account,
|
|
9
|
+
Block,
|
|
10
|
+
BlockCommitment,
|
|
11
|
+
ClusterNode,
|
|
12
|
+
EpochInfo,
|
|
13
|
+
EpochSchedule,
|
|
14
|
+
InflationGovernor,
|
|
15
|
+
InflationRate,
|
|
16
|
+
LamportAccount,
|
|
17
|
+
PerformanceSample,
|
|
18
|
+
SignatureStatus,
|
|
19
|
+
Supply,
|
|
20
|
+
TokenAccount,
|
|
21
|
+
TokenAccountBalance,
|
|
22
|
+
TokenSupply,
|
|
23
|
+
Transaction,
|
|
24
|
+
TransactionSignature,
|
|
25
|
+
VotingAccount,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class HeliusClient:
|
|
30
|
+
# BUG: check which endpoints return meaningful data in context
|
|
31
|
+
# BUG: handle helius errors that do not show by HTTP response code
|
|
32
|
+
# TODO: check all http methods and all guides and implement pagination and other non-implemented features
|
|
33
|
+
# TODO: consider refactoring | None = None to Optional[] = None
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
*,
|
|
37
|
+
base_url: str = "https://mainnet.helius-rpc.com",
|
|
38
|
+
api_key: str | None = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
self.base_url = base_url
|
|
41
|
+
self.api_key = api_key or dotenv_values().get("HELIUS_API_KEY")
|
|
42
|
+
if not self.api_key:
|
|
43
|
+
raise ValueError("No API key provided.")
|
|
44
|
+
self._client = httpx.Client(
|
|
45
|
+
base_url=self.base_url,
|
|
46
|
+
params={"api-key": self.api_key},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def __enter__(self):
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
53
|
+
self.close()
|
|
54
|
+
|
|
55
|
+
def __del__(self):
|
|
56
|
+
self.close()
|
|
57
|
+
|
|
58
|
+
def close(self) -> None:
|
|
59
|
+
self._client.close()
|
|
60
|
+
|
|
61
|
+
def _send(self, json: dict, method="POST", url="/") -> dict:
|
|
62
|
+
response = self._client.request(method=method, url=url, json=json)
|
|
63
|
+
response.raise_for_status()
|
|
64
|
+
return response.json()
|
|
65
|
+
|
|
66
|
+
def get_account_info(
|
|
67
|
+
self,
|
|
68
|
+
public_key: str,
|
|
69
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
70
|
+
encoding: (
|
|
71
|
+
Literal["base58", "base64", "base64+zstd", "jsonParsed"] | None
|
|
72
|
+
) = None,
|
|
73
|
+
data_slice_offset: int | None = None,
|
|
74
|
+
data_slice_length: int | None = None,
|
|
75
|
+
min_context_slot: int | None = None,
|
|
76
|
+
) -> tuple[dict, Account | None]:
|
|
77
|
+
if (data_slice_offset is None) != (data_slice_length is None):
|
|
78
|
+
raise ValueError(
|
|
79
|
+
"Set both data_slice_length and data_slice_offset or neither."
|
|
80
|
+
)
|
|
81
|
+
request = (
|
|
82
|
+
RpcRequest(method="getAccountInfo")
|
|
83
|
+
.add(public_key)
|
|
84
|
+
.set("commitment", commitment)
|
|
85
|
+
.set("encoding", encoding)
|
|
86
|
+
.set(
|
|
87
|
+
"dataSlice",
|
|
88
|
+
(
|
|
89
|
+
{"offset": data_slice_offset, "length": data_slice_length}
|
|
90
|
+
if data_slice_offset is not None
|
|
91
|
+
else None
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
.set("minContextSlot", min_context_slot)
|
|
95
|
+
.build()
|
|
96
|
+
)
|
|
97
|
+
response = self._send(request)
|
|
98
|
+
context = response["result"]["context"]
|
|
99
|
+
value = response["result"]["value"]
|
|
100
|
+
account_info = Account.model_validate(value) if value is not None else None
|
|
101
|
+
return context, account_info
|
|
102
|
+
|
|
103
|
+
def get_balance(
|
|
104
|
+
self,
|
|
105
|
+
public_key: str,
|
|
106
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
107
|
+
min_context_slot: int | None = None,
|
|
108
|
+
) -> tuple[dict, int]:
|
|
109
|
+
request = (
|
|
110
|
+
RpcRequest(method="getBalance")
|
|
111
|
+
.add(public_key)
|
|
112
|
+
.set("commitment", commitment)
|
|
113
|
+
.set("minContextSlot", min_context_slot)
|
|
114
|
+
.build()
|
|
115
|
+
)
|
|
116
|
+
response = self._send(request)
|
|
117
|
+
context = response["result"]["context"]
|
|
118
|
+
value = response["result"]["value"]
|
|
119
|
+
return context, value
|
|
120
|
+
|
|
121
|
+
def get_block(
|
|
122
|
+
self,
|
|
123
|
+
slot: int,
|
|
124
|
+
commitment: Literal["finalized", "confirmed"] | None = None,
|
|
125
|
+
encoding: Literal["jsonParsed", "base58", "base64", "base64+std"] | None = None,
|
|
126
|
+
transaction_details: (
|
|
127
|
+
Literal["full", "accounts", "signatures", "none"] | None
|
|
128
|
+
) = None,
|
|
129
|
+
rewards: bool | None = None,
|
|
130
|
+
max_supported_transcation_version: int | None = None,
|
|
131
|
+
) -> Block | None:
|
|
132
|
+
request = (
|
|
133
|
+
RpcRequest(method="getBlock")
|
|
134
|
+
.add(slot)
|
|
135
|
+
.set("commitment", commitment)
|
|
136
|
+
.set("encoding", encoding)
|
|
137
|
+
.set("transactionDetails", transaction_details)
|
|
138
|
+
.set("rewards", rewards)
|
|
139
|
+
.set("maxSupportedTransactionVersion", max_supported_transcation_version)
|
|
140
|
+
.build()
|
|
141
|
+
)
|
|
142
|
+
response = self._send(request)
|
|
143
|
+
block = Block.model_validate(response["result"])
|
|
144
|
+
return block
|
|
145
|
+
|
|
146
|
+
def get_block_commitment(
|
|
147
|
+
self,
|
|
148
|
+
slot: int,
|
|
149
|
+
) -> BlockCommitment:
|
|
150
|
+
request = RpcRequest(method="getBlockCommitment").add(slot).build()
|
|
151
|
+
response = self._send(request)
|
|
152
|
+
block_commitment = BlockCommitment.model_validate(response["result"])
|
|
153
|
+
return block_commitment
|
|
154
|
+
|
|
155
|
+
def get_block_height(
|
|
156
|
+
self,
|
|
157
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
158
|
+
min_context_slot: int | None = None,
|
|
159
|
+
) -> int:
|
|
160
|
+
request = (
|
|
161
|
+
RpcRequest(method="getBlockHeight")
|
|
162
|
+
.set("commitment", commitment)
|
|
163
|
+
.set("minContextSlot", min_context_slot)
|
|
164
|
+
.build()
|
|
165
|
+
)
|
|
166
|
+
response = self._send(request)
|
|
167
|
+
return response["result"]
|
|
168
|
+
|
|
169
|
+
def get_block_production(
|
|
170
|
+
self,
|
|
171
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
172
|
+
first_slot: int | None = None,
|
|
173
|
+
last_slot: int | None = None,
|
|
174
|
+
identity: str | None = None,
|
|
175
|
+
) -> tuple[dict, dict]:
|
|
176
|
+
"""
|
|
177
|
+
At least one of identity or first_slot must be provided.
|
|
178
|
+
"""
|
|
179
|
+
if first_slot is None:
|
|
180
|
+
if identity is None:
|
|
181
|
+
raise ValueError(
|
|
182
|
+
"At least one of identity or first_slot must be provided."
|
|
183
|
+
)
|
|
184
|
+
if last_slot is not None:
|
|
185
|
+
raise ValueError("To set last_slot, first_slot is required.")
|
|
186
|
+
if last_slot is None:
|
|
187
|
+
range = None
|
|
188
|
+
else:
|
|
189
|
+
range = {"firstSlot": first_slot}
|
|
190
|
+
if last_slot is not None:
|
|
191
|
+
range.update({"lastSlot": last_slot})
|
|
192
|
+
request = (
|
|
193
|
+
RpcRequest(method="getBlockProduction")
|
|
194
|
+
.set("commitment", commitment)
|
|
195
|
+
.set("range", range)
|
|
196
|
+
.set("identity", identity)
|
|
197
|
+
.build()
|
|
198
|
+
)
|
|
199
|
+
response = self._send(request)
|
|
200
|
+
context = response["result"]["context"]
|
|
201
|
+
value = response["result"]["value"]
|
|
202
|
+
return context, value
|
|
203
|
+
|
|
204
|
+
def get_blocks(
|
|
205
|
+
self,
|
|
206
|
+
start_slot: int,
|
|
207
|
+
end_slot: int | None,
|
|
208
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
209
|
+
) -> list[int]:
|
|
210
|
+
"""
|
|
211
|
+
If not provided, the query will return blocks up to the latest confirmed slot from start_slot.
|
|
212
|
+
The range between start_slot and end_slot (or latest slot if end_slot is omitted) must not exceed 500,000 slots.
|
|
213
|
+
"""
|
|
214
|
+
request = (
|
|
215
|
+
RpcRequest(method="getBlocks")
|
|
216
|
+
.add(start_slot)
|
|
217
|
+
.add(end_slot)
|
|
218
|
+
.set("commitment", commitment)
|
|
219
|
+
.build()
|
|
220
|
+
)
|
|
221
|
+
response = self._send(request)
|
|
222
|
+
return response["result"]
|
|
223
|
+
|
|
224
|
+
def get_blocks_with_limit(
|
|
225
|
+
self,
|
|
226
|
+
start_slot: int,
|
|
227
|
+
limit: int,
|
|
228
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
229
|
+
) -> list[int]:
|
|
230
|
+
request = (
|
|
231
|
+
RpcRequest(method="getBlocksWithLimit")
|
|
232
|
+
.add(start_slot)
|
|
233
|
+
.add(limit)
|
|
234
|
+
.set("commitment", commitment)
|
|
235
|
+
.build()
|
|
236
|
+
)
|
|
237
|
+
response = self._send(request)
|
|
238
|
+
return response["result"]
|
|
239
|
+
|
|
240
|
+
def get_block_time(self, slot: int) -> int | None:
|
|
241
|
+
request = RpcRequest(method="getBlockTime").add(slot).build()
|
|
242
|
+
response = self._send(request)
|
|
243
|
+
return response["result"]
|
|
244
|
+
|
|
245
|
+
def get_cluster_nodes(self) -> list[ClusterNode]:
|
|
246
|
+
request = RpcRequest(method="getClusterNodes").build()
|
|
247
|
+
response = self._send(request)
|
|
248
|
+
ta = TypeAdapter(list[ClusterNode])
|
|
249
|
+
cluster_nodes = ta.validate_python(response["result"])
|
|
250
|
+
return cluster_nodes
|
|
251
|
+
|
|
252
|
+
def get_epoch_info(
|
|
253
|
+
self,
|
|
254
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
255
|
+
min_context_slot: int | None = None,
|
|
256
|
+
) -> EpochInfo:
|
|
257
|
+
request = (
|
|
258
|
+
RpcRequest(method="getEpochInfo")
|
|
259
|
+
.set("commitment", commitment)
|
|
260
|
+
.set("minContextSlot", min_context_slot)
|
|
261
|
+
.build()
|
|
262
|
+
)
|
|
263
|
+
response = self._send(request)
|
|
264
|
+
epoch_info = EpochInfo.model_validate(response["result"])
|
|
265
|
+
return epoch_info
|
|
266
|
+
|
|
267
|
+
def get_epoch_schedule(self) -> EpochSchedule:
|
|
268
|
+
request = RpcRequest(method="getEpochSchedule").build()
|
|
269
|
+
response = self._send(request)
|
|
270
|
+
epoch_schedule = EpochSchedule.model_validate(response["result"])
|
|
271
|
+
return epoch_schedule
|
|
272
|
+
|
|
273
|
+
def get_fee_for_message(
|
|
274
|
+
self,
|
|
275
|
+
message: str,
|
|
276
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
277
|
+
min_context_slot: int | None = None,
|
|
278
|
+
) -> tuple[dict, int | None]:
|
|
279
|
+
request = (
|
|
280
|
+
RpcRequest(method="getFeeForMessage")
|
|
281
|
+
.add(message)
|
|
282
|
+
.set("commitment", commitment)
|
|
283
|
+
.set("minContextSlot", min_context_slot)
|
|
284
|
+
.build()
|
|
285
|
+
)
|
|
286
|
+
response = self._send(request)
|
|
287
|
+
context = response["result"]["context"]
|
|
288
|
+
value = response["result"]["value"]
|
|
289
|
+
return context, value
|
|
290
|
+
|
|
291
|
+
def get_first_available_block(self) -> int:
|
|
292
|
+
request = RpcRequest(method="getFirstAvailableBlock").build()
|
|
293
|
+
response = self._send(request)
|
|
294
|
+
return response["result"]
|
|
295
|
+
|
|
296
|
+
def get_genesis_hash(self) -> str:
|
|
297
|
+
request = RpcRequest(method="getGenesisHash").build()
|
|
298
|
+
response = self._send(request)
|
|
299
|
+
return response["result"]
|
|
300
|
+
|
|
301
|
+
def get_health(self) -> bool:
|
|
302
|
+
request = RpcRequest(method="getHealth").build()
|
|
303
|
+
response = self._send(request)
|
|
304
|
+
if "result" in response and response["result"] == "ok":
|
|
305
|
+
return True
|
|
306
|
+
else:
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
def get_highest_snapshot_slot(self) -> dict:
|
|
310
|
+
request = RpcRequest(method="getHighestSnapshotSlot").build()
|
|
311
|
+
response = self._send(request)
|
|
312
|
+
return response["result"]
|
|
313
|
+
|
|
314
|
+
def get_identity(self) -> str:
|
|
315
|
+
request = RpcRequest(method="getIdentity").build()
|
|
316
|
+
response = self._send(request)
|
|
317
|
+
identity = response["result"]["identity"]
|
|
318
|
+
return identity
|
|
319
|
+
|
|
320
|
+
def get_inflation_governor(
|
|
321
|
+
self,
|
|
322
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
323
|
+
) -> InflationGovernor:
|
|
324
|
+
request = (
|
|
325
|
+
RpcRequest(method="getInflationGovernor")
|
|
326
|
+
.set("commitment", commitment)
|
|
327
|
+
.build()
|
|
328
|
+
)
|
|
329
|
+
response = self._send(request)
|
|
330
|
+
inflation_governor = InflationGovernor.model_validate(response["result"])
|
|
331
|
+
return inflation_governor
|
|
332
|
+
|
|
333
|
+
def get_inflation_rate(self) -> InflationRate:
|
|
334
|
+
request = RpcRequest(method="getInflationRate").build()
|
|
335
|
+
response = self._send(request)
|
|
336
|
+
inflation_rate = InflationRate.model_validate(response["result"])
|
|
337
|
+
return inflation_rate
|
|
338
|
+
|
|
339
|
+
# TODO: getInflationReward
|
|
340
|
+
|
|
341
|
+
def get_largest_accounts(
|
|
342
|
+
self,
|
|
343
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
344
|
+
filter: Literal["circulating", "nonCirculating"] | None = None,
|
|
345
|
+
) -> tuple[dict, list[LamportAccount]]:
|
|
346
|
+
request = (
|
|
347
|
+
RpcRequest(method="getLargestAccounts")
|
|
348
|
+
.set("commitment", commitment)
|
|
349
|
+
.set("filter", filter)
|
|
350
|
+
.build()
|
|
351
|
+
)
|
|
352
|
+
response = self._send(request)
|
|
353
|
+
context = response["result"]["context"]
|
|
354
|
+
value = response["result"]["value"]
|
|
355
|
+
ta = TypeAdapter(list[LamportAccount])
|
|
356
|
+
largest_accounts = ta.validate_python(value)
|
|
357
|
+
return context, largest_accounts
|
|
358
|
+
|
|
359
|
+
def get_latest_blockhash(
|
|
360
|
+
self,
|
|
361
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
362
|
+
min_context_slot: int | None = None,
|
|
363
|
+
) -> tuple[dict, str, int]:
|
|
364
|
+
request = (
|
|
365
|
+
RpcRequest(method="getLatestBlockhash")
|
|
366
|
+
.set("commitment", commitment)
|
|
367
|
+
.set("minContextSlot", min_context_slot)
|
|
368
|
+
.build()
|
|
369
|
+
)
|
|
370
|
+
response = self._send(request)
|
|
371
|
+
context = response["result"]["context"]
|
|
372
|
+
value = response["result"]["value"]
|
|
373
|
+
blockhash = value["blockhash"]
|
|
374
|
+
last_valid_block_height = value["lastValidBlockHeight"]
|
|
375
|
+
return context, blockhash, last_valid_block_height
|
|
376
|
+
|
|
377
|
+
def get_leader_schedule(
|
|
378
|
+
self,
|
|
379
|
+
slot: int | None = None,
|
|
380
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
381
|
+
identity: str | None = None,
|
|
382
|
+
) -> dict[str, list[int]] | None:
|
|
383
|
+
request = (
|
|
384
|
+
RpcRequest(method="getLeaderSchedule")
|
|
385
|
+
.add(slot)
|
|
386
|
+
.set("commitment", commitment)
|
|
387
|
+
.set("identity", identity)
|
|
388
|
+
.build()
|
|
389
|
+
)
|
|
390
|
+
response = self._send(request)
|
|
391
|
+
result = response["result"]
|
|
392
|
+
return result
|
|
393
|
+
|
|
394
|
+
def get_max_retransmit_slot(self) -> int:
|
|
395
|
+
request = RpcRequest(method="getMaxRetransmitSlot").build()
|
|
396
|
+
response = self._send(request)
|
|
397
|
+
result = response["result"]
|
|
398
|
+
return result
|
|
399
|
+
|
|
400
|
+
def get_max_shred_insert_slot(self) -> int:
|
|
401
|
+
request = RpcRequest(method="getMaxShredInsertSlot").build()
|
|
402
|
+
response = self._send(request)
|
|
403
|
+
result = response["result"]
|
|
404
|
+
return result
|
|
405
|
+
|
|
406
|
+
def get_minimum_balance_for_rent_exemption(
|
|
407
|
+
self,
|
|
408
|
+
data_length: int,
|
|
409
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
410
|
+
) -> int:
|
|
411
|
+
request = (
|
|
412
|
+
RpcRequest(method="getMinimumBalanceForRentExemption")
|
|
413
|
+
.add(data_length)
|
|
414
|
+
.set("commitment", commitment)
|
|
415
|
+
.build()
|
|
416
|
+
)
|
|
417
|
+
response = self._send(request)
|
|
418
|
+
result = response["result"]
|
|
419
|
+
return result
|
|
420
|
+
|
|
421
|
+
def get_multiple_accounts(
|
|
422
|
+
self,
|
|
423
|
+
pubkeys: list[str],
|
|
424
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
425
|
+
encoding: (
|
|
426
|
+
Literal["base64", "base58", "base64+zstd", "jsonParsed"] | None
|
|
427
|
+
) = None,
|
|
428
|
+
data_slice_offset: int | None = None,
|
|
429
|
+
data_slice_length: int | None = None,
|
|
430
|
+
) -> tuple[dict, list[Account | None]]:
|
|
431
|
+
if (data_slice_offset is None) != (data_slice_length is None):
|
|
432
|
+
raise ValueError(
|
|
433
|
+
"Set both data_slice_length and data_slice_offset or neither."
|
|
434
|
+
)
|
|
435
|
+
if (
|
|
436
|
+
data_slice_length is not None
|
|
437
|
+
and data_slice_offset is not None
|
|
438
|
+
and encoding not in ["base58", "base64", "base64+zstd"]
|
|
439
|
+
):
|
|
440
|
+
raise ValueError(
|
|
441
|
+
"Data slice is only available for base58, base64, or base64+zstd encodings."
|
|
442
|
+
)
|
|
443
|
+
request = (
|
|
444
|
+
RpcRequest(method="getMultipleAccounts")
|
|
445
|
+
.add(pubkeys)
|
|
446
|
+
.set("commitment", commitment)
|
|
447
|
+
.set("encoding", encoding)
|
|
448
|
+
.set(
|
|
449
|
+
"dataSlice",
|
|
450
|
+
(
|
|
451
|
+
{"offset": data_slice_offset, "length": data_slice_length}
|
|
452
|
+
if data_slice_offset is not None
|
|
453
|
+
else None
|
|
454
|
+
),
|
|
455
|
+
)
|
|
456
|
+
.build()
|
|
457
|
+
)
|
|
458
|
+
response = self._send(request)
|
|
459
|
+
context = response["result"]["context"]
|
|
460
|
+
value = response["result"]["value"]
|
|
461
|
+
accounts = [Account.model_validate(i) if i is not None else None for i in value]
|
|
462
|
+
return context, accounts
|
|
463
|
+
|
|
464
|
+
@validate_call
|
|
465
|
+
def get_program_accounts(
|
|
466
|
+
self,
|
|
467
|
+
program_id: str,
|
|
468
|
+
commitment: Literal["confirmed", "finalized", "processed"] | None = None,
|
|
469
|
+
min_context_slot: int | None = None,
|
|
470
|
+
with_context: bool | None = None,
|
|
471
|
+
encoding: (
|
|
472
|
+
Literal["jsonParsed", "base58", "base64", "base64+zstd"] | None
|
|
473
|
+
) = None,
|
|
474
|
+
data_slice_offset: int | None = None,
|
|
475
|
+
data_slice_length: int | None = None,
|
|
476
|
+
changed_since_slot: int | None = None,
|
|
477
|
+
filters: list[dict] | None = None,
|
|
478
|
+
) -> list[tuple[str, Account]]:
|
|
479
|
+
if (data_slice_offset is None) != (data_slice_length is None):
|
|
480
|
+
raise ValueError(
|
|
481
|
+
"Set both data_slice_length and data_slice_offset or neither."
|
|
482
|
+
)
|
|
483
|
+
request = (
|
|
484
|
+
RpcRequest(method="getProgramAccounts")
|
|
485
|
+
.add(program_id)
|
|
486
|
+
.set("commitment", commitment)
|
|
487
|
+
.set("minContextSlot", min_context_slot)
|
|
488
|
+
.set("withContext", with_context)
|
|
489
|
+
.set("encoding", encoding)
|
|
490
|
+
.set(
|
|
491
|
+
"dataSlice",
|
|
492
|
+
(
|
|
493
|
+
{"offset": data_slice_offset, "length": data_slice_length}
|
|
494
|
+
if data_slice_offset is not None
|
|
495
|
+
else None
|
|
496
|
+
),
|
|
497
|
+
)
|
|
498
|
+
.set("changedSinceSlot", changed_since_slot)
|
|
499
|
+
.set("filters", filters)
|
|
500
|
+
.build()
|
|
501
|
+
)
|
|
502
|
+
response = self._send(request)
|
|
503
|
+
result = response["result"]
|
|
504
|
+
return [(i["pubkey"], Account.model_validate(i["account"])) for i in result]
|
|
505
|
+
|
|
506
|
+
def get_recent_performance_samples(
|
|
507
|
+
self,
|
|
508
|
+
limit: int | None = None,
|
|
509
|
+
) -> list[PerformanceSample]:
|
|
510
|
+
request = RpcRequest(method="getRecentPerformanceSamples").add(limit).build()
|
|
511
|
+
response = self._send(request)
|
|
512
|
+
ta = TypeAdapter(list[PerformanceSample])
|
|
513
|
+
return ta.validate_python(response["result"])
|
|
514
|
+
|
|
515
|
+
def get_recent_prioritization_fees(
|
|
516
|
+
self,
|
|
517
|
+
locked_writable_accounts: list[str] | None = None,
|
|
518
|
+
) -> list[tuple[int, int]]:
|
|
519
|
+
request = (
|
|
520
|
+
RpcRequest(method="getRecentPrioritizationFees")
|
|
521
|
+
.add(locked_writable_accounts)
|
|
522
|
+
.build()
|
|
523
|
+
)
|
|
524
|
+
response = self._send(request)
|
|
525
|
+
return [(i["slot"], i["prioritizationFee"]) for i in response["result"]]
|
|
526
|
+
|
|
527
|
+
@validate_call
|
|
528
|
+
def get_signatures_for_address(
|
|
529
|
+
self,
|
|
530
|
+
address: str,
|
|
531
|
+
limit: Annotated[int, Field(ge=1, le=1000)] = 1000,
|
|
532
|
+
before: str | None = None,
|
|
533
|
+
until: str | None = None,
|
|
534
|
+
commitment: Literal["finalized", "confirmed"] | None = None,
|
|
535
|
+
min_context_slot: int | None = None,
|
|
536
|
+
) -> list[TransactionSignature]:
|
|
537
|
+
request = (
|
|
538
|
+
RpcRequest(method="getSignaturesForAddress")
|
|
539
|
+
.add(address)
|
|
540
|
+
.set("limit", limit)
|
|
541
|
+
.set("before", before)
|
|
542
|
+
.set("until", until)
|
|
543
|
+
.set("commitment", commitment)
|
|
544
|
+
.set("minContextSlot", min_context_slot)
|
|
545
|
+
.build()
|
|
546
|
+
)
|
|
547
|
+
response = self._send(request)
|
|
548
|
+
ta = TypeAdapter(list[TransactionSignature])
|
|
549
|
+
transaction_signatures = ta.validate_python(response["result"])
|
|
550
|
+
return transaction_signatures
|
|
551
|
+
|
|
552
|
+
def get_signature_statuses(
|
|
553
|
+
self,
|
|
554
|
+
signatures: list[str],
|
|
555
|
+
search_transaction_history: bool | None = None,
|
|
556
|
+
) -> tuple[dict, list[SignatureStatus | None]]:
|
|
557
|
+
request = (
|
|
558
|
+
RpcRequest(method="getSignatureStatuses")
|
|
559
|
+
.add(signatures)
|
|
560
|
+
.set("searchTransactionHistory", search_transaction_history)
|
|
561
|
+
.build()
|
|
562
|
+
)
|
|
563
|
+
response = self._send(request)
|
|
564
|
+
context = response["result"]["context"]
|
|
565
|
+
signature_statuses = [
|
|
566
|
+
SignatureStatus.model_validate(value) if value is not None else value
|
|
567
|
+
for value in response["result"]["value"]
|
|
568
|
+
]
|
|
569
|
+
return context, signature_statuses
|
|
570
|
+
|
|
571
|
+
def get_slot(
|
|
572
|
+
self,
|
|
573
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
574
|
+
min_context_slot: int | None = None,
|
|
575
|
+
) -> int:
|
|
576
|
+
request = (
|
|
577
|
+
RpcRequest(method="getSlot")
|
|
578
|
+
.set("commitment", commitment)
|
|
579
|
+
.set("minContextSlot", min_context_slot)
|
|
580
|
+
.build()
|
|
581
|
+
)
|
|
582
|
+
response = self._send(request)
|
|
583
|
+
return response["result"]
|
|
584
|
+
|
|
585
|
+
def get_slot_leader(
|
|
586
|
+
self,
|
|
587
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
588
|
+
min_context_slot: int | None = None,
|
|
589
|
+
) -> str:
|
|
590
|
+
request = (
|
|
591
|
+
RpcRequest(method="getSlotLeader")
|
|
592
|
+
.set("commitment", commitment)
|
|
593
|
+
.set("minContextSlot", min_context_slot)
|
|
594
|
+
.build()
|
|
595
|
+
)
|
|
596
|
+
response = self._send(request)
|
|
597
|
+
return response["result"]
|
|
598
|
+
|
|
599
|
+
@validate_call
|
|
600
|
+
def get_slot_leaders(
|
|
601
|
+
self,
|
|
602
|
+
start_slot: int,
|
|
603
|
+
limit: Annotated[int, Field(ge=1, le=5000)],
|
|
604
|
+
) -> list[str]:
|
|
605
|
+
request = RpcRequest(method="getSlotLeaders").add(start_slot).add(limit).build()
|
|
606
|
+
response = self._send(request)
|
|
607
|
+
return response["result"]
|
|
608
|
+
|
|
609
|
+
def get_stake_minimum_delegation(
|
|
610
|
+
self,
|
|
611
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
612
|
+
) -> tuple[dict, int]:
|
|
613
|
+
request = (
|
|
614
|
+
RpcRequest(method="getStakeMinimumDelegation")
|
|
615
|
+
.set("commitment", commitment)
|
|
616
|
+
.build()
|
|
617
|
+
)
|
|
618
|
+
response = self._send(request)
|
|
619
|
+
context = response["result"]["context"]
|
|
620
|
+
value = response["result"]["value"]
|
|
621
|
+
return context, value
|
|
622
|
+
|
|
623
|
+
def get_supply(
|
|
624
|
+
self,
|
|
625
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
626
|
+
exclude_non_circulating_accounts_list: bool | None = None,
|
|
627
|
+
) -> tuple[dict, Supply]:
|
|
628
|
+
request = (
|
|
629
|
+
RpcRequest(method="getSupply")
|
|
630
|
+
.set("commitment", commitment)
|
|
631
|
+
.set(
|
|
632
|
+
"excludeNonCirculatingAccountsList",
|
|
633
|
+
exclude_non_circulating_accounts_list,
|
|
634
|
+
)
|
|
635
|
+
.build()
|
|
636
|
+
)
|
|
637
|
+
response = self._send(request)
|
|
638
|
+
context = response["result"]["context"]
|
|
639
|
+
supply = Supply.model_validate(response["result"]["value"])
|
|
640
|
+
return context, supply
|
|
641
|
+
|
|
642
|
+
# TODO: use getProgramAccountsV2 which supports pagination
|
|
643
|
+
|
|
644
|
+
def get_token_account_balance(
|
|
645
|
+
self,
|
|
646
|
+
token_account: str,
|
|
647
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
648
|
+
) -> tuple[dict, TokenAccountBalance]:
|
|
649
|
+
request = (
|
|
650
|
+
RpcRequest(method="getTokenAccountBalance")
|
|
651
|
+
.add(token_account)
|
|
652
|
+
.set("commitment", commitment)
|
|
653
|
+
.build()
|
|
654
|
+
)
|
|
655
|
+
response = self._send(request)
|
|
656
|
+
context = response["result"]["context"]
|
|
657
|
+
balance = TokenAccountBalance.model_validate(response["result"]["value"])
|
|
658
|
+
return context, balance
|
|
659
|
+
|
|
660
|
+
def get_token_accounts_by_delegate(
|
|
661
|
+
self,
|
|
662
|
+
delegate_pub_key: str,
|
|
663
|
+
mint: str | None = None,
|
|
664
|
+
program_id: str | None = None,
|
|
665
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
666
|
+
encoding: (
|
|
667
|
+
Literal["base58", "base64", "base64+zstd", "jsonParsed"] | None
|
|
668
|
+
) = None,
|
|
669
|
+
data_slice_offset: int | None = None,
|
|
670
|
+
data_slice_length: int | None = None,
|
|
671
|
+
min_context_slot: int | None = None,
|
|
672
|
+
) -> tuple[dict, list[tuple[str, Account]]]:
|
|
673
|
+
if (mint is None) == (program_id is None):
|
|
674
|
+
raise ValueError("Provide exactly one of mint or program_id.")
|
|
675
|
+
if (data_slice_offset is None) != (data_slice_length is None):
|
|
676
|
+
raise ValueError(
|
|
677
|
+
"Set both data_slice_offset and data_slice_length or neither."
|
|
678
|
+
)
|
|
679
|
+
if data_slice_offset is not None and encoding == "jsonParsed":
|
|
680
|
+
raise ValueError(
|
|
681
|
+
"dataSlice is only for bas58, bas64 and base64+zstd encodings."
|
|
682
|
+
)
|
|
683
|
+
filter = {"mint": mint} if mint is not None else {"programId": program_id}
|
|
684
|
+
request = (
|
|
685
|
+
RpcRequest(method="getTokenAccountsByDelegate")
|
|
686
|
+
.add(delegate_pub_key)
|
|
687
|
+
.add(filter)
|
|
688
|
+
.set("commitment", commitment)
|
|
689
|
+
.set("encoding", encoding)
|
|
690
|
+
.set(
|
|
691
|
+
"dataSlice",
|
|
692
|
+
(
|
|
693
|
+
{"offset": data_slice_offset, "length": data_slice_length}
|
|
694
|
+
if data_slice_offset is not None
|
|
695
|
+
else None
|
|
696
|
+
),
|
|
697
|
+
)
|
|
698
|
+
.set("minContextSlot", min_context_slot)
|
|
699
|
+
.build()
|
|
700
|
+
)
|
|
701
|
+
response = self._send(request)
|
|
702
|
+
context = response["result"]["context"]
|
|
703
|
+
token_accounts = [
|
|
704
|
+
(i["pubkey"], Account.model_validate(i["account"]))
|
|
705
|
+
for i in response["result"]["value"]
|
|
706
|
+
]
|
|
707
|
+
return context, token_accounts
|
|
708
|
+
|
|
709
|
+
def get_token_accounts_by_owner(
|
|
710
|
+
self,
|
|
711
|
+
owner_pub_key: str,
|
|
712
|
+
mint: str | None = None,
|
|
713
|
+
program_id: str | None = None,
|
|
714
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
715
|
+
encoding: (
|
|
716
|
+
Literal["base58", "base64", "base64+zstd", "jsonParsed"] | None
|
|
717
|
+
) = None,
|
|
718
|
+
data_slice_offset: int | None = None,
|
|
719
|
+
data_slice_length: int | None = None,
|
|
720
|
+
min_context_slot: int | None = None,
|
|
721
|
+
) -> tuple[dict, list[tuple[str, Account]]]:
|
|
722
|
+
if (mint is None) == (program_id is None):
|
|
723
|
+
raise ValueError("Provide exactly one of mint or program_id.")
|
|
724
|
+
if (data_slice_offset is None) != (data_slice_length is None):
|
|
725
|
+
raise ValueError(
|
|
726
|
+
"Set both data_slice_offset and data_slice_length or neither."
|
|
727
|
+
)
|
|
728
|
+
if data_slice_offset is not None and encoding == "jsonParsed":
|
|
729
|
+
raise ValueError(
|
|
730
|
+
"dataSlice is only for bas58, bas64 and base64+zstd encodings."
|
|
731
|
+
)
|
|
732
|
+
filter = {"mint": mint} if mint is not None else {"programId": program_id}
|
|
733
|
+
request = (
|
|
734
|
+
RpcRequest(method="getTokenAccountsByOwner")
|
|
735
|
+
.add(owner_pub_key)
|
|
736
|
+
.add(filter)
|
|
737
|
+
.set("commitment", commitment)
|
|
738
|
+
.set("encoding", encoding)
|
|
739
|
+
.set(
|
|
740
|
+
"dataSlice",
|
|
741
|
+
(
|
|
742
|
+
{"offset": data_slice_offset, "length": data_slice_length}
|
|
743
|
+
if data_slice_offset is not None
|
|
744
|
+
else None
|
|
745
|
+
),
|
|
746
|
+
)
|
|
747
|
+
.set("minContextSlot", min_context_slot)
|
|
748
|
+
.build()
|
|
749
|
+
)
|
|
750
|
+
response = self._send(request)
|
|
751
|
+
context = response["result"]["context"]
|
|
752
|
+
token_accounts = [
|
|
753
|
+
(i["pubkey"], Account.model_validate(i["account"]))
|
|
754
|
+
for i in response["result"]["value"]
|
|
755
|
+
]
|
|
756
|
+
return context, token_accounts
|
|
757
|
+
|
|
758
|
+
# TODO: use getTokenAccountsByOwnerV2 and do pagination
|
|
759
|
+
|
|
760
|
+
def get_token_largest_accounts(
|
|
761
|
+
self,
|
|
762
|
+
mint: str,
|
|
763
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
764
|
+
) -> tuple[dict, list[TokenAccount]]:
|
|
765
|
+
request = (
|
|
766
|
+
RpcRequest(method="getTokenLargestAccounts")
|
|
767
|
+
.add(mint)
|
|
768
|
+
.set("commitment", commitment)
|
|
769
|
+
.build()
|
|
770
|
+
)
|
|
771
|
+
response = self._send(request)
|
|
772
|
+
context = response["result"]["context"]
|
|
773
|
+
ta = TypeAdapter(list[TokenAccount])
|
|
774
|
+
largest_accounts = ta.validate_python(response["result"]["value"])
|
|
775
|
+
return context, largest_accounts
|
|
776
|
+
|
|
777
|
+
def get_token_supply(
|
|
778
|
+
self,
|
|
779
|
+
mint_address: str,
|
|
780
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
781
|
+
) -> tuple[dict, TokenSupply]:
|
|
782
|
+
request = (
|
|
783
|
+
RpcRequest(method="getTokenSupply")
|
|
784
|
+
.add(mint_address)
|
|
785
|
+
.set("commitment", commitment)
|
|
786
|
+
.build()
|
|
787
|
+
)
|
|
788
|
+
response = self._send(request)
|
|
789
|
+
context = response["result"]["context"]
|
|
790
|
+
token_supply = TokenSupply.model_validate(response["result"]["value"])
|
|
791
|
+
return context, token_supply
|
|
792
|
+
|
|
793
|
+
def get_transaction(
|
|
794
|
+
self,
|
|
795
|
+
transaction_signature: str,
|
|
796
|
+
commitment: Literal["finalized", "confirmed"] | None = None,
|
|
797
|
+
encoding: Literal["json", "jsonParsed", "base58", "base64"] | None = None,
|
|
798
|
+
max_supported_transaction_version: int | None = None,
|
|
799
|
+
) -> Transaction:
|
|
800
|
+
request = (
|
|
801
|
+
RpcRequest(method="getTransaction")
|
|
802
|
+
.add(transaction_signature)
|
|
803
|
+
.set("commitment", commitment)
|
|
804
|
+
.set("encoding", encoding)
|
|
805
|
+
.set("maxSupportedTransactionVersion", max_supported_transaction_version)
|
|
806
|
+
.build()
|
|
807
|
+
)
|
|
808
|
+
response = self._send(request)
|
|
809
|
+
transaction = Transaction.model_validate(response["result"])
|
|
810
|
+
return transaction
|
|
811
|
+
|
|
812
|
+
def get_transaction_count(
|
|
813
|
+
self,
|
|
814
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
815
|
+
min_context_slot: int | None = None,
|
|
816
|
+
) -> int:
|
|
817
|
+
request = (
|
|
818
|
+
RpcRequest(method="getTransactionCount")
|
|
819
|
+
.set("commitment", commitment)
|
|
820
|
+
.set("minContextSlot", min_context_slot)
|
|
821
|
+
.build()
|
|
822
|
+
)
|
|
823
|
+
response = self._send(request)
|
|
824
|
+
return response["result"]
|
|
825
|
+
|
|
826
|
+
def get_version(self) -> tuple[str, int]:
|
|
827
|
+
request = RpcRequest(method="getVersion").build()
|
|
828
|
+
response = self._send(request)
|
|
829
|
+
result = response["result"]
|
|
830
|
+
return result["solana-core"], result["feature-set"]
|
|
831
|
+
|
|
832
|
+
def get_vote_accounts(
|
|
833
|
+
self,
|
|
834
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
835
|
+
vote_pubkey: str | None = None,
|
|
836
|
+
keep_unstaked_delinquents: bool | None = None,
|
|
837
|
+
delinquent_slot_distance: int | None = None,
|
|
838
|
+
) -> tuple[list[VotingAccount], list[VotingAccount]]:
|
|
839
|
+
request = (
|
|
840
|
+
RpcRequest(method="getVoteAccounts")
|
|
841
|
+
.set("commitment", commitment)
|
|
842
|
+
.set("votePubkey", vote_pubkey)
|
|
843
|
+
.set("keepUnstakedDelinquents", keep_unstaked_delinquents)
|
|
844
|
+
.set("delinquentSlotDistance", delinquent_slot_distance)
|
|
845
|
+
.build()
|
|
846
|
+
)
|
|
847
|
+
response = self._send(request)
|
|
848
|
+
ta = TypeAdapter(list[VotingAccount])
|
|
849
|
+
current = ta.validate_python(response["result"]["current"])
|
|
850
|
+
delinquent = ta.validate_python(response["result"]["delinquent"])
|
|
851
|
+
return current, delinquent
|
|
852
|
+
|
|
853
|
+
def is_blockhash_valid(
|
|
854
|
+
self,
|
|
855
|
+
blockhash: str,
|
|
856
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
857
|
+
min_context_slot: int | None = None,
|
|
858
|
+
) -> tuple[dict, bool]:
|
|
859
|
+
request = (
|
|
860
|
+
RpcRequest(method="isBlockhashValid")
|
|
861
|
+
.add(blockhash)
|
|
862
|
+
.set("commitment", commitment)
|
|
863
|
+
.set("minContextSlot", min_context_slot)
|
|
864
|
+
.build()
|
|
865
|
+
)
|
|
866
|
+
response = self._send(request)
|
|
867
|
+
context = response["result"]["context"]
|
|
868
|
+
value = response["result"]["value"]
|
|
869
|
+
return context, value
|
|
870
|
+
|
|
871
|
+
def request_airdrop(
|
|
872
|
+
self,
|
|
873
|
+
public_key: str,
|
|
874
|
+
lamports: int,
|
|
875
|
+
commitment: Literal["finalized", "confirmed", "processed"] | None = None,
|
|
876
|
+
) -> str:
|
|
877
|
+
"""
|
|
878
|
+
Only available on Devnet and Testnet, not Mainnet Beta.
|
|
879
|
+
"""
|
|
880
|
+
request = (
|
|
881
|
+
RpcRequest(method="requestAirdrop")
|
|
882
|
+
.add(public_key)
|
|
883
|
+
.add(lamports)
|
|
884
|
+
.set("commitment", commitment)
|
|
885
|
+
.build()
|
|
886
|
+
)
|
|
887
|
+
response = self._send(request)
|
|
888
|
+
return response["result"]
|
|
889
|
+
|
|
890
|
+
def minimum_ledger_slot(self) -> int:
|
|
891
|
+
request = RpcRequest(method="minimumLedgerSlot").build()
|
|
892
|
+
response = self._send(request)
|
|
893
|
+
return response["result"]
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
class RpcRequest:
|
|
897
|
+
class Request(BaseModel):
|
|
898
|
+
jsonrpc: str
|
|
899
|
+
method: str
|
|
900
|
+
params: list[Any] | None = None
|
|
901
|
+
id: str | int | None
|
|
902
|
+
|
|
903
|
+
def __init__(
|
|
904
|
+
self,
|
|
905
|
+
*,
|
|
906
|
+
jsonrpc: str = "2.0",
|
|
907
|
+
method: str,
|
|
908
|
+
id: str | int | None = 1,
|
|
909
|
+
):
|
|
910
|
+
self._jsonrpc = jsonrpc
|
|
911
|
+
self._method = method
|
|
912
|
+
self._id = id
|
|
913
|
+
self._positional: list[Any] = []
|
|
914
|
+
self._config: dict[str, Any] = {}
|
|
915
|
+
|
|
916
|
+
def add(self, value, can_be_none: bool = False):
|
|
917
|
+
if value is not None:
|
|
918
|
+
self._positional.append(value)
|
|
919
|
+
elif can_be_none:
|
|
920
|
+
self._positional.append(None)
|
|
921
|
+
return self
|
|
922
|
+
|
|
923
|
+
def set(self, key: str, value, can_be_none: bool = False):
|
|
924
|
+
if value is not None:
|
|
925
|
+
self._config.update({key: value})
|
|
926
|
+
elif can_be_none:
|
|
927
|
+
self._config.update({key: None})
|
|
928
|
+
return self
|
|
929
|
+
|
|
930
|
+
def build(self):
|
|
931
|
+
params = self._positional if self._positional else []
|
|
932
|
+
if self._config:
|
|
933
|
+
params.append(self._config)
|
|
934
|
+
request = {
|
|
935
|
+
"jsonrpc": self._jsonrpc,
|
|
936
|
+
"method": self._method,
|
|
937
|
+
"id": self._id,
|
|
938
|
+
}
|
|
939
|
+
if params:
|
|
940
|
+
request.update({"params": params})
|
|
941
|
+
return self.Request(**request).model_dump()
|