helius-python 0.3.2__tar.gz → 0.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {helius_python-0.3.2 → helius_python-0.3.3}/AGENTS.md +1 -12
  2. {helius_python-0.3.2 → helius_python-0.3.3}/PKG-INFO +4 -1
  3. {helius_python-0.3.2 → helius_python-0.3.3}/README.md +2 -0
  4. helius_python-0.3.3/TODO.md +4 -0
  5. {helius_python-0.3.2 → helius_python-0.3.3}/pyproject.toml +2 -1
  6. {helius_python-0.3.2 → helius_python-0.3.3}/src/helius/laserstream/websockets.py +2 -1
  7. {helius_python-0.3.2 → helius_python-0.3.3}/src/helius/solana_rpc/client.py +85 -0
  8. {helius_python-0.3.2 → helius_python-0.3.3}/src/helius/solana_rpc/models.py +98 -0
  9. {helius_python-0.3.2 → helius_python-0.3.3}/tests/unit/solana_rpc/test_client.py +333 -0
  10. helius_python-0.3.2/TODO.md +0 -3
  11. {helius_python-0.3.2 → helius_python-0.3.3}/.editorconfig +0 -0
  12. {helius_python-0.3.2 → helius_python-0.3.3}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  13. {helius_python-0.3.2 → helius_python-0.3.3}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  14. {helius_python-0.3.2 → helius_python-0.3.3}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  15. {helius_python-0.3.2 → helius_python-0.3.3}/.github/workflows/python-package.yml +0 -0
  16. {helius_python-0.3.2 → helius_python-0.3.3}/.github/workflows/python-publish.yml +0 -0
  17. {helius_python-0.3.2 → helius_python-0.3.3}/.gitignore +0 -0
  18. {helius_python-0.3.2 → helius_python-0.3.3}/CLAUDE.md +0 -0
  19. {helius_python-0.3.2 → helius_python-0.3.3}/CONTRIBUTING.md +0 -0
  20. {helius_python-0.3.2 → helius_python-0.3.3}/LICENSE +0 -0
  21. {helius_python-0.3.2 → helius_python-0.3.3}/examples/block_explorer.py +0 -0
  22. {helius_python-0.3.2 → helius_python-0.3.3}/examples/devnet_airdrop.py +0 -0
  23. {helius_python-0.3.2 → helius_python-0.3.3}/examples/network_status.py +0 -0
  24. {helius_python-0.3.2 → helius_python-0.3.3}/examples/priority_fees.py +0 -0
  25. {helius_python-0.3.2 → helius_python-0.3.3}/examples/stake_overview.py +0 -0
  26. {helius_python-0.3.2 → helius_python-0.3.3}/examples/token_inspector.py +0 -0
  27. {helius_python-0.3.2 → helius_python-0.3.3}/examples/transaction_inspector.py +0 -0
  28. {helius_python-0.3.2 → helius_python-0.3.3}/examples/wallet_tracker.py +0 -0
  29. {helius_python-0.3.2 → helius_python-0.3.3}/requirements.txt +0 -0
  30. {helius_python-0.3.2 → helius_python-0.3.3}/src/helius/__init__.py +0 -0
  31. {helius_python-0.3.2 → helius_python-0.3.3}/src/helius/admin/__init__.py +0 -0
  32. {helius_python-0.3.2 → helius_python-0.3.3}/src/helius/admin/admin.py +0 -0
  33. {helius_python-0.3.2 → helius_python-0.3.3}/src/helius/rpc/__init__.py +0 -0
  34. {helius_python-0.3.2 → helius_python-0.3.3}/src/helius/rpc/json_rpc_request.py +0 -0
  35. {helius_python-0.3.2 → helius_python-0.3.3}/src/helius/solana_rpc/__init__.py +0 -0
  36. {helius_python-0.3.2 → helius_python-0.3.3}/tests/fixtures/account.json +0 -0
  37. {helius_python-0.3.2 → helius_python-0.3.3}/tests/fixtures/supply.json +0 -0
  38. {helius_python-0.3.2 → helius_python-0.3.3}/tests/unit/admin/test_admin.py +0 -0
  39. {helius_python-0.3.2 → helius_python-0.3.3}/tests/unit/lasterstream/test_websockets.py +0 -0
  40. {helius_python-0.3.2 → helius_python-0.3.3}/tests/unit/rpc/test_json_rpc_request.py +0 -0
  41. {helius_python-0.3.2 → helius_python-0.3.3}/tests/unit/solana_rpc/test_models.py +0 -0
@@ -1,14 +1,3 @@
1
-
2
- ## Conventions
3
-
4
- - Use **double-quoted triple strings** (`"""..."""`).
5
- - Wrap docstring lines at 88 columns.
6
- - Refer to parameters in backticks: `` `commitment` ``, `` `min_context_slot` ``.
7
- - Refer to other client methods with their snake_case name in backticks: `` `get_balance` ``.
8
- - Do NOT include the upstream JSON-RPC method name in the summary — that's already in the See Also URLs.
9
- - Do NOT copy-paste large chunks from the Helius docs. Summarize and link.
10
- - Examples (`Example:` section) are encouraged for methods with non-trivial argument combinations (e.g. `get_block_production`, `get_token_accounts_by_owner`), optional for everything else.
11
-
12
1
  ## Implementation conventions
13
2
 
14
3
  - **If the RPC returns an `RpcResponse` wrapper (`{context, value}`), the Python method MUST return `(context, value)`** — never silently drop `context`. For methods whose `value` is itself a small composite, flatten the tuple (e.g. `get_latest_blockhash` returns `tuple[dict, str, int]`, not `tuple[dict, tuple[str, int]]`). Check the upstream Helius API reference page to see whether the response is wrapped.
@@ -124,7 +113,7 @@ For methods with branching logic (e.g. `get_block_production`, `get_token_accoun
124
113
  ## Running
125
114
 
126
115
  ```bash
127
- pytest
116
+ .venv/bin/pytest
128
117
  ```
129
118
 
130
119
  All tests must pass and there must be no real network traffic. If a test fails because it tried to hit the network, that's a bug in the test — add the missing `@respx.mock` or `respx` route.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: helius-python
3
- Version: 0.3.2
3
+ Version: 0.3.3
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: typing-extensions
16
17
  Requires-Dist: websockets
17
18
  Provides-Extra: dev
18
19
  Requires-Dist: pytest; extra == 'dev'
@@ -322,6 +323,8 @@ continuously.
322
323
  | `minimumLedgerSlot` | `minimum_ledger_slot()` | [guide](https://www.helius.dev/docs/rpc/guides/minimumledgerslot), [reference](https://www.helius.dev/docs/api-reference/rpc/http/minimumledgerslot) |
323
324
  | `requestAirdrop` | `request_airdrop(...)` | [guide](https://www.helius.dev/docs/rpc/guides/requestairdrop), [reference](https://www.helius.dev/docs/api-reference/rpc/http/requestairdrop) |
324
325
  | `sendTransaction` | `send_transaction(...)` | [guide](https://www.helius.dev/docs/rpc/guides/sendtransaction), [reference](https://www.helius.dev/docs/api-reference/rpc/http/sendtransaction) |
326
+ | `getTransactionsForAddress` | `get_transactions_for_address(...)` | [reference](https://www.helius.dev/docs/api-reference/rpc/http/gettransactionsforaddress) |
327
+ | `getTransfersByAddress` | `get_transfers_by_address(...)` | [guide](https://www.helius.dev/docs/rpc/gettransfersbyaddress), [reference](https://www.helius.dev/docs/api-reference/rpc/http/gettransfersbyaddress) |
325
328
 
326
329
  ## WebSocket subscriptions
327
330
 
@@ -301,6 +301,8 @@ continuously.
301
301
  | `minimumLedgerSlot` | `minimum_ledger_slot()` | [guide](https://www.helius.dev/docs/rpc/guides/minimumledgerslot), [reference](https://www.helius.dev/docs/api-reference/rpc/http/minimumledgerslot) |
302
302
  | `requestAirdrop` | `request_airdrop(...)` | [guide](https://www.helius.dev/docs/rpc/guides/requestairdrop), [reference](https://www.helius.dev/docs/api-reference/rpc/http/requestairdrop) |
303
303
  | `sendTransaction` | `send_transaction(...)` | [guide](https://www.helius.dev/docs/rpc/guides/sendtransaction), [reference](https://www.helius.dev/docs/api-reference/rpc/http/sendtransaction) |
304
+ | `getTransactionsForAddress` | `get_transactions_for_address(...)` | [reference](https://www.helius.dev/docs/api-reference/rpc/http/gettransactionsforaddress) |
305
+ | `getTransfersByAddress` | `get_transfers_by_address(...)` | [guide](https://www.helius.dev/docs/rpc/gettransfersbyaddress), [reference](https://www.helius.dev/docs/api-reference/rpc/http/gettransfersbyaddress) |
304
306
 
305
307
  ## WebSocket subscriptions
306
308
 
@@ -0,0 +1,4 @@
1
+ - Mangling __
2
+ - WebSocket subscription manager
3
+ - Use typeddicts where useful
4
+ - When Python 3.10 and 3.11 go out of support we should use TypedDict from typing instead of typing_extensions
@@ -10,7 +10,7 @@ build-backend = "hatchling.build"
10
10
 
11
11
  [project]
12
12
  name = "helius-python"
13
- version = "0.3.2"
13
+ version = "0.3.3"
14
14
  authors = [
15
15
  { name="Markos Narinian", email="manarinian@gmail.com" },
16
16
  ]
@@ -21,6 +21,7 @@ dependencies = [
21
21
  "httpx",
22
22
  "pydantic",
23
23
  "python-dotenv",
24
+ "typing-extensions",
24
25
  "websockets",
25
26
  ]
26
27
  classifiers = [
@@ -1,11 +1,12 @@
1
1
  import json
2
2
  from os import environ
3
- from typing import Annotated, Literal, TypedDict
3
+ from typing import Annotated, Literal
4
4
 
5
5
  import httpx
6
6
  from dotenv import dotenv_values
7
7
  from pydantic import AliasGenerator, BaseModel, ConfigDict, Field, model_validator
8
8
  from pydantic.alias_generators import to_camel
9
+ from typing_extensions import TypedDict
9
10
  from websockets.sync.client import connect
10
11
 
11
12
  from helius.rpc import JsonRpcRequest
@@ -25,7 +25,12 @@ from helius.solana_rpc.models import (
25
25
  TokenAccountBalance,
26
26
  TokenSupply,
27
27
  Transaction,
28
+ TransactionFilters,
29
+ TransactionFullDetail,
28
30
  TransactionSignature,
31
+ TransactionSignaturesDetail,
32
+ Transfer,
33
+ TransferFilters,
29
34
  VotingAccount,
30
35
  )
31
36
 
@@ -983,3 +988,83 @@ class SolanaRpcClient:
983
988
  )
984
989
  response = self._send(request)
985
990
  return response["result"]
991
+
992
+ @validate_call
993
+ def get_transactions_for_address(
994
+ self,
995
+ *,
996
+ address: str,
997
+ transaction_details: Literal["signatures", "full"] | None = None,
998
+ sort_order: Literal["asc", "desc"] | None = None,
999
+ commitment: Literal["confirmed", "finalized"] | None = None,
1000
+ min_context_slot: int | None = None,
1001
+ limit: Annotated[int, Field(ge=1, le=1000)] | None = None,
1002
+ pagination_token: str | None = None,
1003
+ encoding: Literal["json", "jsonParsed", "base58", "base64"] | None = None,
1004
+ max_supported_transaction_version: int | None = None,
1005
+ filters: TransactionFilters | None = None,
1006
+ ) -> tuple[
1007
+ list[TransactionSignaturesDetail] | list[TransactionFullDetail], str | None
1008
+ ]:
1009
+ request = (
1010
+ JsonRpcRequest(method="getTransactionsForAddress")
1011
+ .add(address)
1012
+ .set("transactionDetails", transaction_details)
1013
+ .set("sortOrder", sort_order)
1014
+ .set("commitment", commitment)
1015
+ .set("minContextSlot", min_context_slot)
1016
+ .set("limit", limit)
1017
+ .set("paginationToken", pagination_token)
1018
+ .set("encoding", encoding)
1019
+ .set("maxSupportedTransactionVersion", max_supported_transaction_version)
1020
+ .set("filters", filters)
1021
+ .build()
1022
+ )
1023
+ response = self._send(request)
1024
+ pagination_token = response["result"]["paginationToken"]
1025
+ data = response["result"]["data"]
1026
+ ta = TypeAdapter(
1027
+ list[TransactionSignaturesDetail]
1028
+ if transaction_details == "signatures"
1029
+ else list[TransactionFullDetail]
1030
+ )
1031
+ transactions = ta.validate_python(data)
1032
+ return transactions, pagination_token
1033
+
1034
+ @validate_call
1035
+ def get_transfers_by_address(
1036
+ self,
1037
+ *,
1038
+ address: str,
1039
+ with_address: str | None = None,
1040
+ direction: Literal["in", "out", "any"] | None = None,
1041
+ mint: str | None = None,
1042
+ sol_mode: Literal["merged", "separate"] | None = None,
1043
+ filters: TransferFilters | None = None,
1044
+ limit: Annotated[int, Field(ge=1, le=100)] | None = None,
1045
+ pagination_token: str | None = None,
1046
+ commitment: Literal["finalized", "confirmed"] | None = None,
1047
+ min_context_slot: int | None = None,
1048
+ sort_order: Literal["asc", "desc"] | None = None,
1049
+ ) -> tuple[list[Transfer], str | None]:
1050
+ request = (
1051
+ JsonRpcRequest(method="getTransfersByAddress")
1052
+ .add(address)
1053
+ .set("with", with_address)
1054
+ .set("direction", direction)
1055
+ .set("mint", mint)
1056
+ .set("solMode", sol_mode)
1057
+ .set("filters", filters)
1058
+ .set("limit", limit)
1059
+ .set("paginationToken", pagination_token)
1060
+ .set("commitment", commitment)
1061
+ .set("minContextSlot", min_context_slot)
1062
+ .set("sortOrder", sort_order)
1063
+ .build()
1064
+ )
1065
+ response = self._send(request)
1066
+ pagination_token = response["result"]["paginationToken"]
1067
+ data = response["result"]["data"]
1068
+ ta = TypeAdapter(list[Transfer])
1069
+ transfers = ta.validate_python(data)
1070
+ return transfers, pagination_token
@@ -2,6 +2,9 @@ from typing import Literal
2
2
 
3
3
  from pydantic import AliasGenerator, BaseModel, ConfigDict
4
4
  from pydantic.alias_generators import to_camel
5
+ from typing_extensions import TypedDict
6
+
7
+ # TODO: Improve camelCase <-> snake_case
5
8
 
6
9
 
7
10
  class Account(BaseModel):
@@ -221,3 +224,98 @@ class Transaction(BaseModel):
221
224
  meta: TransactionMetadata | None
222
225
  transaction: dict | list
223
226
  version: Literal["legacy"] | int | None = None
227
+
228
+
229
+ class RangeFilter(TypedDict, total=False):
230
+ gt: int
231
+ gte: int
232
+ lt: int
233
+ lte: int
234
+
235
+
236
+ class ComparisonFilter(TypedDict, total=False):
237
+ gte: int
238
+ gt: int
239
+ lte: int
240
+ lt: int
241
+ eq: int
242
+
243
+
244
+ TokenTransferFilter = TypedDict(
245
+ "TokenTransferFilter",
246
+ {
247
+ "with": str,
248
+ "direction": Literal["in", "out", "any"],
249
+ "mint": str,
250
+ "amount": RangeFilter,
251
+ },
252
+ total=False,
253
+ )
254
+
255
+
256
+ class TransactionFilters(TypedDict, total=False):
257
+ slot: RangeFilter
258
+ blockTime: ComparisonFilter
259
+ signature: RangeFilter
260
+ status: Literal["succeeded", "failed", "any"]
261
+ tokenAccounts: Literal["none", "balanceChanged", "all"]
262
+ tokenTransfer: TokenTransferFilter
263
+
264
+
265
+ class TransferFilters(TypedDict, total=False):
266
+ amount: RangeFilter
267
+ blockTime: RangeFilter
268
+ slot: RangeFilter
269
+
270
+
271
+ class Transfer(BaseModel):
272
+ model_config = ConfigDict(alias_generator=AliasGenerator(validation_alias=to_camel))
273
+
274
+ signature: str
275
+ slot: int
276
+ block_time: int
277
+ type: Literal[
278
+ "transfer",
279
+ "mint",
280
+ "burn",
281
+ "wrap",
282
+ "unwrap",
283
+ "changeOwner",
284
+ "withdrawWithheldFee",
285
+ ]
286
+ from_user_account: str | None
287
+ to_user_account: str | None
288
+ mint: str
289
+ amount: str
290
+ decimals: int
291
+ ui_amount: str
292
+ confirmation_status: Literal["finalized", "confirmed"]
293
+ transaction_idx: int
294
+ instruction_idx: int
295
+ inner_instruction_idx: int
296
+ from_token_account: str | None = None
297
+ to_token_account: str | None = None
298
+ fee_amount: str | None = None
299
+ fee_ui_amount: str | None = None
300
+
301
+
302
+ class TransactionSignaturesDetail(BaseModel):
303
+ model_config = ConfigDict(alias_generator=AliasGenerator(validation_alias=to_camel))
304
+
305
+ signature: str
306
+ slot: int
307
+ transaction_index: int
308
+ err: dict | None
309
+ memo: str | None
310
+ block_time: int | None
311
+ confirmation_status: Literal["finalized", "confirmed"] | None
312
+
313
+
314
+ class TransactionFullDetail(BaseModel):
315
+ model_config = ConfigDict(alias_generator=AliasGenerator(validation_alias=to_camel))
316
+
317
+ slot: int
318
+ transaction_index: int
319
+ transaction: dict
320
+ meta: dict | list
321
+ block_time: int | None
@@ -1333,6 +1333,339 @@ def test_get_transaction_count():
1333
1333
  assert_api_key(route)
1334
1334
 
1335
1335
 
1336
+ # ---------------------------------------------------------------------------
1337
+ # get_transactions_for_address
1338
+ # ---------------------------------------------------------------------------
1339
+
1340
+
1341
+ @respx.mock
1342
+ def test_get_transactions_for_address_signatures():
1343
+ route = mock_rpc(
1344
+ {
1345
+ "data": [
1346
+ {
1347
+ "signature": "5h6xBEauJ3PK6SWCZ1PGjBvj8vDdWG3KpwATGy1ARAXFSDwt8GFXM7W5Ncn16wmqokgpiKRLuS83KUxyZyv2sUYv",
1348
+ "slot": 1054,
1349
+ "transactionIndex": 42,
1350
+ "err": None,
1351
+ "memo": None,
1352
+ "blockTime": 1641038400,
1353
+ "confirmationStatus": "finalized",
1354
+ },
1355
+ {
1356
+ "signature": "kwjd820slPK6SWCZ1PGjBvj8vDdWG3KpwATGy1ARAXFSDwt8GFXM7W5Ncn16wmqokgpiKRLuS83KUxyZyv2sUYv",
1357
+ "slot": 1055,
1358
+ "transactionIndex": 15,
1359
+ "err": None,
1360
+ "memo": None,
1361
+ "blockTime": 1641038460,
1362
+ "confirmationStatus": "finalized",
1363
+ },
1364
+ ],
1365
+ "paginationToken": "1055:5",
1366
+ }
1367
+ )
1368
+ with SolanaRpcClient(api_key="test") as client:
1369
+ data, pagination_token = client.get_transactions_for_address(
1370
+ address="Vote111111111111111111111111111111111111111",
1371
+ transaction_details="signatures",
1372
+ sort_order="desc",
1373
+ limit=50,
1374
+ filters={
1375
+ "status": "succeeded",
1376
+ "slot": {"gte": 1000, "lt": 2000},
1377
+ "tokenTransfer": {
1378
+ "direction": "in",
1379
+ "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
1380
+ },
1381
+ },
1382
+ )
1383
+ assert len(data) == 2
1384
+ assert data[0].signature.startswith("5h6x")
1385
+ assert data[0].slot == 1054
1386
+ assert data[0].transaction_index == 42
1387
+ assert data[0].err is None
1388
+ assert data[0].memo is None
1389
+ assert data[0].block_time == 1641038400
1390
+ assert data[0].confirmation_status == "finalized"
1391
+ assert data[1].slot == 1055
1392
+ assert data[1].transaction_index == 15
1393
+ assert pagination_token == "1055:5"
1394
+ assert body(route)["method"] == "getTransactionsForAddress"
1395
+ assert body(route)["params"] == [
1396
+ "Vote111111111111111111111111111111111111111",
1397
+ {
1398
+ "transactionDetails": "signatures",
1399
+ "sortOrder": "desc",
1400
+ "limit": 50,
1401
+ "filters": {
1402
+ "status": "succeeded",
1403
+ "slot": {"gte": 1000, "lt": 2000},
1404
+ "tokenTransfer": {
1405
+ "direction": "in",
1406
+ "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
1407
+ },
1408
+ },
1409
+ },
1410
+ ]
1411
+ assert_api_key(route)
1412
+
1413
+
1414
+ @respx.mock
1415
+ def test_get_transactions_for_address_full():
1416
+ route = mock_rpc(
1417
+ {
1418
+ "data": [
1419
+ {
1420
+ "slot": 1054,
1421
+ "transactionIndex": 42,
1422
+ "transaction": {
1423
+ "signatures": [
1424
+ "5h6xBEauJ3PK6SWCZ1PGjBvj8vDdWG3KpwATGy1ARAXFSDwt8GFXM7W5Ncn16wmqokgpiKRLuS83KUxyZyv2sUYv"
1425
+ ],
1426
+ "message": {
1427
+ "accountKeys": ["AccountA", "AccountB"],
1428
+ "instructions": [],
1429
+ },
1430
+ },
1431
+ "meta": {
1432
+ "fee": 5000,
1433
+ "preBalances": [1000000, 2000000],
1434
+ "postBalances": [999995000, 2000000],
1435
+ },
1436
+ "blockTime": 1641038400,
1437
+ }
1438
+ ],
1439
+ "paginationToken": "1055:5",
1440
+ }
1441
+ )
1442
+ with SolanaRpcClient(api_key="test") as client:
1443
+ data, pagination_token = client.get_transactions_for_address(
1444
+ address="Vote111111111111111111111111111111111111111",
1445
+ transaction_details="full",
1446
+ encoding="jsonParsed",
1447
+ max_supported_transaction_version=0,
1448
+ )
1449
+ assert len(data) == 1
1450
+ assert data[0].slot == 1054
1451
+ assert data[0].transaction_index == 42
1452
+ assert data[0].block_time == 1641038400
1453
+ assert data[0].meta["fee"] == 5000
1454
+ assert data[0].transaction["message"]["accountKeys"] == ["AccountA", "AccountB"]
1455
+ assert pagination_token == "1055:5"
1456
+ assert body(route)["method"] == "getTransactionsForAddress"
1457
+ assert body(route)["params"] == [
1458
+ "Vote111111111111111111111111111111111111111",
1459
+ {
1460
+ "transactionDetails": "full",
1461
+ "encoding": "jsonParsed",
1462
+ "maxSupportedTransactionVersion": 0,
1463
+ },
1464
+ ]
1465
+ assert_api_key(route)
1466
+
1467
+
1468
+ @respx.mock
1469
+ def test_get_transactions_for_address_minimal():
1470
+ route = mock_rpc({"data": [], "paginationToken": None})
1471
+ with SolanaRpcClient(api_key="test") as client:
1472
+ data, pagination_token = client.get_transactions_for_address(address="Addr")
1473
+ assert data == []
1474
+ assert pagination_token is None
1475
+ assert body(route)["params"] == ["Addr"]
1476
+ assert_api_key(route)
1477
+
1478
+
1479
+ @respx.mock
1480
+ def test_get_transactions_for_address_rejects_out_of_range_limit():
1481
+ mock_rpc({"data": [], "paginationToken": None})
1482
+ with SolanaRpcClient(api_key="test") as client:
1483
+ with pytest.raises(Exception):
1484
+ client.get_transactions_for_address(address="Addr", limit=1001)
1485
+
1486
+
1487
+ # ---------------------------------------------------------------------------
1488
+ # get_transfers_by_address
1489
+ # ---------------------------------------------------------------------------
1490
+
1491
+
1492
+ @respx.mock
1493
+ def test_get_transfers_by_address():
1494
+ route = mock_rpc(
1495
+ {
1496
+ "data": [
1497
+ {
1498
+ "signature": "5GEX7Q3X5Q8yJGbKYoR7mtzQmG8tpoEwzjPgqVmn3y5xg3yKwqXcDdN5YVcc9V6vA4TuH5iM6FHRVhTxvz4AX2zG",
1499
+ "slot": 315073428,
1500
+ "blockTime": 1736159420,
1501
+ "type": "transfer",
1502
+ "fromUserAccount": "7hPhaUpydpvm8wtiS3k4LPZKUmivQRs7YQmpE1hFshHx",
1503
+ "toUserAccount": "86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY",
1504
+ "fromTokenAccount": "HcvK3EJ74iM9g11cUgsaPvLSrhCvCwcrWxBNd87LsC1x",
1505
+ "toTokenAccount": "CBcYniR9G9CN3zGMnwNE4SWbqkYWvCFVreEob9xHnQCY",
1506
+ "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
1507
+ "amount": "2500000",
1508
+ "decimals": 6,
1509
+ "uiAmount": "2.5",
1510
+ "confirmationStatus": "finalized",
1511
+ "transactionIdx": 35,
1512
+ "instructionIdx": 1,
1513
+ "innerInstructionIdx": 0,
1514
+ }
1515
+ ],
1516
+ "paginationToken": "315073428:35:1:0:splTransfer",
1517
+ }
1518
+ )
1519
+ with SolanaRpcClient(api_key="test") as client:
1520
+ data, pagination_token = client.get_transfers_by_address(
1521
+ address="86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY",
1522
+ with_address="7hPhaUpydpvm8wtiS3k4LPZKUmivQRs7YQmpE1hFshHx",
1523
+ direction="in",
1524
+ mint="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
1525
+ sol_mode="separate",
1526
+ filters={
1527
+ "amount": {"gte": 1000000000, "lt": 10000000000},
1528
+ "blockTime": {"gte": 1735718400},
1529
+ },
1530
+ limit=50,
1531
+ pagination_token="315069220:308:2:1:splTransfer",
1532
+ sort_order="desc",
1533
+ )
1534
+ assert len(data) == 1
1535
+ transfer = data[0]
1536
+ assert transfer.signature.startswith("5GEX")
1537
+ assert transfer.slot == 315073428
1538
+ assert transfer.block_time == 1736159420
1539
+ assert transfer.type == "transfer"
1540
+ assert transfer.from_user_account == "7hPhaUpydpvm8wtiS3k4LPZKUmivQRs7YQmpE1hFshHx"
1541
+ assert transfer.to_user_account == "86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY"
1542
+ assert transfer.from_token_account == "HcvK3EJ74iM9g11cUgsaPvLSrhCvCwcrWxBNd87LsC1x"
1543
+ assert transfer.to_token_account == "CBcYniR9G9CN3zGMnwNE4SWbqkYWvCFVreEob9xHnQCY"
1544
+ assert transfer.mint == "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
1545
+ assert transfer.amount == "2500000"
1546
+ assert transfer.decimals == 6
1547
+ assert transfer.ui_amount == "2.5"
1548
+ assert transfer.confirmation_status == "finalized"
1549
+ assert transfer.transaction_idx == 35
1550
+ assert transfer.instruction_idx == 1
1551
+ assert transfer.inner_instruction_idx == 0
1552
+ assert transfer.fee_amount is None
1553
+ assert transfer.fee_ui_amount is None
1554
+ assert pagination_token == "315073428:35:1:0:splTransfer"
1555
+ assert body(route)["method"] == "getTransfersByAddress"
1556
+ assert body(route)["params"] == [
1557
+ "86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY",
1558
+ {
1559
+ "with": "7hPhaUpydpvm8wtiS3k4LPZKUmivQRs7YQmpE1hFshHx",
1560
+ "direction": "in",
1561
+ "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
1562
+ "solMode": "separate",
1563
+ "filters": {
1564
+ "amount": {"gte": 1000000000, "lt": 10000000000},
1565
+ "blockTime": {"gte": 1735718400},
1566
+ },
1567
+ "limit": 50,
1568
+ "paginationToken": "315069220:308:2:1:splTransfer",
1569
+ "sortOrder": "desc",
1570
+ },
1571
+ ]
1572
+ assert_api_key(route)
1573
+
1574
+
1575
+ @respx.mock
1576
+ def test_get_transfers_by_address_native_sol_transfer():
1577
+ """Native SOL transfers omit token-account fields."""
1578
+ route = mock_rpc(
1579
+ {
1580
+ "data": [
1581
+ {
1582
+ "signature": "5GEX7Q3X5Q8yJGbKYoR7mtzQmG8tpoEwzjPgqVmn3y5xg3yKwqXcDdN5YVcc9V6vA4TuH5iM6FHRVhTxvz4AX2zG",
1583
+ "slot": 315073428,
1584
+ "blockTime": 1736159420,
1585
+ "type": "transfer",
1586
+ "fromUserAccount": "7hPhaUpydpvm8wtiS3k4LPZKUmivQRs7YQmpE1hFshHx",
1587
+ "toUserAccount": "86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY",
1588
+ "mint": "So11111111111111111111111111111111111111111",
1589
+ "amount": "1000000000",
1590
+ "decimals": 9,
1591
+ "uiAmount": "1",
1592
+ "confirmationStatus": "finalized",
1593
+ "transactionIdx": 0,
1594
+ "instructionIdx": 0,
1595
+ "innerInstructionIdx": 0,
1596
+ }
1597
+ ],
1598
+ "paginationToken": None,
1599
+ }
1600
+ )
1601
+ with SolanaRpcClient(api_key="test") as client:
1602
+ data, pagination_token = client.get_transfers_by_address(
1603
+ address="86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY",
1604
+ sol_mode="merged",
1605
+ )
1606
+ assert pagination_token is None
1607
+ assert data[0].from_token_account is None
1608
+ assert data[0].to_token_account is None
1609
+ assert data[0].mint == "So11111111111111111111111111111111111111111"
1610
+ assert body(route)["params"][1] == {"solMode": "merged"}
1611
+
1612
+
1613
+ @respx.mock
1614
+ def test_get_transfers_by_address_fee_bearing_transfer():
1615
+ """Token-2022 fee-bearing transfers include feeAmount and feeUiAmount."""
1616
+ route = mock_rpc(
1617
+ {
1618
+ "data": [
1619
+ {
1620
+ "signature": "5GEX7Q3X5Q8yJGbKYoR7mtzQmG8tpoEwzjPgqVmn3y5xg3yKwqXcDdN5YVcc9V6vA4TuH5iM6FHRVhTxvz4AX2zG",
1621
+ "slot": 315073428,
1622
+ "blockTime": 1736159420,
1623
+ "type": "transfer",
1624
+ "fromUserAccount": "7hPhaUpydpvm8wtiS3k4LPZKUmivQRs7YQmpE1hFshHx",
1625
+ "toUserAccount": "86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY",
1626
+ "fromTokenAccount": "HcvK3EJ74iM9g11cUgsaPvLSrhCvCwcrWxBNd87LsC1x",
1627
+ "toTokenAccount": "CBcYniR9G9CN3zGMnwNE4SWbqkYWvCFVreEob9xHnQCY",
1628
+ "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
1629
+ "amount": "2500000",
1630
+ "feeAmount": "13450000",
1631
+ "decimals": 6,
1632
+ "uiAmount": "2.5",
1633
+ "feeUiAmount": "13.45",
1634
+ "confirmationStatus": "finalized",
1635
+ "transactionIdx": 35,
1636
+ "instructionIdx": 1,
1637
+ "innerInstructionIdx": 0,
1638
+ }
1639
+ ],
1640
+ "paginationToken": None,
1641
+ }
1642
+ )
1643
+ with SolanaRpcClient(api_key="test") as client:
1644
+ data, _ = client.get_transfers_by_address(address="Addr")
1645
+ assert data[0].fee_amount == "13450000"
1646
+ assert data[0].fee_ui_amount == "13.45"
1647
+ assert_api_key(route)
1648
+
1649
+
1650
+ @respx.mock
1651
+ def test_get_transfers_by_address_minimal():
1652
+ route = mock_rpc({"data": [], "paginationToken": None})
1653
+ with SolanaRpcClient(api_key="test") as client:
1654
+ data, pagination_token = client.get_transfers_by_address(address="Addr")
1655
+ assert data == []
1656
+ assert pagination_token is None
1657
+ assert body(route)["params"] == ["Addr"]
1658
+ assert_api_key(route)
1659
+
1660
+
1661
+ @respx.mock
1662
+ def test_get_transfers_by_address_rejects_out_of_range_limit():
1663
+ mock_rpc({"data": [], "paginationToken": None})
1664
+ with SolanaRpcClient(api_key="test") as client:
1665
+ with pytest.raises(Exception):
1666
+ client.get_transfers_by_address(address="Addr", limit=101)
1667
+
1668
+
1336
1669
  # ---------------------------------------------------------------------------
1337
1670
  # get_version
1338
1671
  # ---------------------------------------------------------------------------
@@ -1,3 +0,0 @@
1
- - Mangling __
2
- - WebSocket subscription manager
3
- - Use typeddicts where useful
File without changes
File without changes
File without changes