helius-python 0.3.4__tar.gz → 0.4.1__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.
- {helius_python-0.3.4 → helius_python-0.4.1}/PKG-INFO +63 -11
- {helius_python-0.3.4 → helius_python-0.4.1}/README.md +62 -10
- {helius_python-0.3.4 → helius_python-0.4.1}/TODO.md +2 -0
- helius_python-0.4.1/examples/webhooks/webhook_crud.py +190 -0
- helius_python-0.4.1/examples/webhooks/webhook_receiver.py +109 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/pyproject.toml +1 -1
- {helius_python-0.3.4 → helius_python-0.4.1}/src/helius/laserstream/websockets.py +1 -1
- {helius_python-0.3.4 → helius_python-0.4.1}/src/helius/solana_rpc/client.py +1 -1
- {helius_python-0.3.4 → helius_python-0.4.1}/src/helius/solana_rpc/models.py +0 -2
- helius_python-0.4.1/src/helius/utils/__init__.py +3 -0
- helius_python-0.4.1/src/helius/webhooks/__init__.py +0 -0
- helius_python-0.4.1/src/helius/webhooks/webhooks.py +722 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/test_examples.py +10 -0
- {helius_python-0.3.4/tests/unit/rpc → helius_python-0.4.1/tests/unit/utils}/test_json_rpc_request.py +1 -1
- helius_python-0.4.1/tests/unit/webhooks/test_webhook.py +26 -0
- helius_python-0.4.1/tests/unit/webhooks/test_webhooks_api_client.py +168 -0
- helius_python-0.3.4/examples/README.md +0 -4
- helius_python-0.3.4/src/helius/rpc/__init__.py +0 -3
- {helius_python-0.3.4 → helius_python-0.4.1}/.editorconfig +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/.github/workflows/python-package.yml +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/.github/workflows/python-publish.yml +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/.gitignore +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/AGENTS.md +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/CLAUDE.md +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/CONTRIBUTING.md +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/LICENSE +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/examples/laserstream/websocket_logs.py +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/examples/solana_rpc/address_transactions.py +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/examples/solana_rpc/address_transfers.py +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/examples/solana_rpc/block_explorer.py +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/examples/solana_rpc/devnet_airdrop.py +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/examples/solana_rpc/network_status.py +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/examples/solana_rpc/priority_fees.py +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/examples/solana_rpc/stake_overview.py +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/examples/solana_rpc/token_inspector.py +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/examples/solana_rpc/transaction_inspector.py +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/examples/solana_rpc/wallet_tracker.py +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/requirements.txt +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/src/helius/__init__.py +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/src/helius/admin/__init__.py +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/src/helius/admin/admin.py +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/src/helius/solana_rpc/__init__.py +0 -0
- {helius_python-0.3.4/src/helius/rpc → helius_python-0.4.1/src/helius/utils}/json_rpc_request.py +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/tests/fixtures/account.json +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/tests/fixtures/supply.json +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/tests/unit/admin/test_admin.py +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/tests/unit/lasterstream/test_websockets.py +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/tests/unit/solana_rpc/test_client.py +0 -0
- {helius_python-0.3.4 → helius_python-0.4.1}/tests/unit/solana_rpc/test_models.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: helius-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -135,9 +135,10 @@ wraps it.
|
|
|
135
135
|
- 🚧 **All Helius-specific RPC extensions** — enhanced transactions, DAS
|
|
136
136
|
(Digital Asset Standard) methods, priority fee estimation, and the
|
|
137
137
|
rest of the Helius-only RPC namespace. **(in progress)**
|
|
138
|
-
-
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
- ✅ **Webhooks API** — create, fetch, update, enable/disable, and delete
|
|
139
|
+
Helius webhooks. **(supported today)**
|
|
140
|
+
- 🚧 **Remaining Helius REST endpoints** — Enhanced Transactions API,
|
|
141
|
+
Mint API, token metadata, address lookups, and beyond. **(in progress)**
|
|
141
142
|
- 🚧 **Platform features** — streaming, websockets, and any new
|
|
142
143
|
capability Helius adds to its API. The full WebSocket subscription
|
|
143
144
|
surface (`accountSubscribe`, `transactionSubscribe`, `logsSubscribe`,
|
|
@@ -145,10 +146,10 @@ wraps it.
|
|
|
145
146
|
platform features are **in progress**.
|
|
146
147
|
|
|
147
148
|
**Current status:** the standard Solana JSON-RPC surface, the
|
|
148
|
-
WebSocket subscription surface, and the Admin
|
|
149
|
-
usage endpoint are implemented today. Support for
|
|
150
|
-
extensions and the remaining REST endpoints is actively
|
|
151
|
-
on.
|
|
149
|
+
WebSocket subscription surface, the Webhooks API, and the Admin
|
|
150
|
+
(account management) usage endpoint are implemented today. Support for
|
|
151
|
+
Helius RPC extensions and the remaining REST endpoints is actively
|
|
152
|
+
being worked on.
|
|
152
153
|
|
|
153
154
|
## Goals
|
|
154
155
|
|
|
@@ -266,9 +267,9 @@ The method names map 1:1 to the Solana JSON-RPC spec, just converted to
|
|
|
266
267
|
## Status
|
|
267
268
|
|
|
268
269
|
Actively expanding toward full coverage of the Helius API. See
|
|
269
|
-
[`src/helius/solana_rpc.py`](src/helius/solana_rpc.py) for the
|
|
270
|
-
|
|
271
|
-
continuously.
|
|
270
|
+
[`src/helius/solana_rpc/client.py`](src/helius/solana_rpc/client.py) for the
|
|
271
|
+
current Solana JSON-RPC implementation; supported surfaces are listed below,
|
|
272
|
+
and missing endpoints are tracked as issues and added continuously.
|
|
272
273
|
|
|
273
274
|
| Solana JSON-RPC method | Python method | Helius docs |
|
|
274
275
|
| ----------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
@@ -372,6 +373,44 @@ them; both yield a `(context, notification, subscription)` tuple where
|
|
|
372
373
|
| `vote_subscribe()` | `vote_unsubscribe(...)` | `VoteNotification` | [reference](https://www.helius.dev/docs/api-reference/rpc/websocket/votesubscribe) |
|
|
373
374
|
| `transaction_subscribe(...)` | `transaction_unsubscribe(...)` | `TransactionNotification` | [reference](https://www.helius.dev/docs/api-reference/rpc/websocket/slotunsubscribe) |
|
|
374
375
|
|
|
376
|
+
## Webhooks API
|
|
377
|
+
|
|
378
|
+
`WebhooksApiClient` wraps the Helius Webhooks REST API. It can create,
|
|
379
|
+
fetch, list, update, enable/disable, and delete webhook subscriptions,
|
|
380
|
+
returning typed `Webhook` models where the endpoint returns a webhook.
|
|
381
|
+
|
|
382
|
+
```python
|
|
383
|
+
from helius.webhooks.webhooks import WebhooksApiClient
|
|
384
|
+
|
|
385
|
+
with WebhooksApiClient(api_key="YOUR_HELIUS_API_KEY") as webhooks:
|
|
386
|
+
webhook = webhooks.create_webhook(
|
|
387
|
+
webhook_url="https://example.com/helius-webhook",
|
|
388
|
+
transaction_types=["TRANSFER"],
|
|
389
|
+
account_addresses=["So11111111111111111111111111111111111111112"],
|
|
390
|
+
webhook_type="enhanced",
|
|
391
|
+
auth_header="Bearer your-shared-secret",
|
|
392
|
+
encoding="jsonParsed",
|
|
393
|
+
txn_status="all",
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
webhooks.toggle_webhook(webhook.webhook_id, active=False)
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Like the other clients, it supports the context-manager protocol and a
|
|
400
|
+
manual `close()`. The constructor defaults to
|
|
401
|
+
`https://mainnet.helius-rpc.com/v0/webhooks` and reads `HELIUS_API_KEY`
|
|
402
|
+
from the environment or `.env` when `api_key` is omitted; optional
|
|
403
|
+
`headers` and `proxy` arguments are also accepted.
|
|
404
|
+
|
|
405
|
+
| REST operation | Python method | Helius docs |
|
|
406
|
+
| -------------- | -------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
407
|
+
| Create webhook | `create_webhook(...)` | [reference](https://www.helius.dev/docs/api-reference/webhooks/create-webhook) |
|
|
408
|
+
| Get webhook | `get_webhook(...)` | [reference](https://www.helius.dev/docs/api-reference/webhooks/get-webhook) |
|
|
409
|
+
| List webhooks | `get_all_webhooks()` | [reference](https://www.helius.dev/docs/api-reference/webhooks/get-all-webhooks) |
|
|
410
|
+
| Update webhook | `update_webhook(...)` | [reference](https://www.helius.dev/docs/api-reference/webhooks/update-webhook) |
|
|
411
|
+
| Toggle webhook | `toggle_webhook(...)` | [reference](https://www.helius.dev/docs/api-reference/webhooks/toggle-webhook) |
|
|
412
|
+
| Delete webhook | `delete_webhook(...)` | [reference](https://www.helius.dev/docs/api-reference/webhooks/delete-webhook) |
|
|
413
|
+
|
|
375
414
|
## Admin API
|
|
376
415
|
|
|
377
416
|
`AccountManagementClient` wraps the Helius admin API. Today it exposes
|
|
@@ -392,6 +431,19 @@ client defaults to `https://admin-api.helius.xyz` and, like the others,
|
|
|
392
431
|
supports the context-manager protocol, a manual `close()`, and reads
|
|
393
432
|
`HELIUS_API_KEY` from the environment or `.env`.
|
|
394
433
|
|
|
434
|
+
## AI
|
|
435
|
+
AI was used to assist in the development of this project, which was started beacause I needed a Helius client written in Python myself for another project. It has eliminated the need to perform repetitive tasks, such as writing functions that are similar to those written by hand before them. It has also been useful in drafting and updating the documentation (exclusively README.md at the time of writing), writing tests and examples.
|
|
436
|
+
That said, AI-generated code is reviewed in full, line-by-line by human(s) against the Helius API specifications.
|
|
437
|
+
|
|
438
|
+
To roughly describe my approach to using AI in this project, I generally write the code by hand, which makes it easy to structure the code well and maintain an intuitive understanding of how the code works. Then, when I got to the part where I had to implement 100+ similar Python functions, I turned to AI to write the massive part of the codebase, while I reviewed the result at the same time.
|
|
439
|
+
|
|
440
|
+
To sum up:
|
|
441
|
+
- I generally structure** and write a good part of what I'm programming by hand, including boilerplate.
|
|
442
|
+
- I find it best to prompt agents to implement very limited functionality within narrow scope, performing something closer to pair-programming.
|
|
443
|
+
- Even though at least half the LoC in this project are hand-typed, I find that AI is incredibly capable, can be enjoyable to use, but most importantly, using AI allows me to focus my time and energy on what matters.
|
|
444
|
+
|
|
445
|
+
By the way, this README section is completely brain-made and hand-typed.
|
|
446
|
+
|
|
395
447
|
## License
|
|
396
448
|
|
|
397
449
|
[MIT](LICENSE)
|
|
@@ -113,9 +113,10 @@ wraps it.
|
|
|
113
113
|
- 🚧 **All Helius-specific RPC extensions** — enhanced transactions, DAS
|
|
114
114
|
(Digital Asset Standard) methods, priority fee estimation, and the
|
|
115
115
|
rest of the Helius-only RPC namespace. **(in progress)**
|
|
116
|
-
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
- ✅ **Webhooks API** — create, fetch, update, enable/disable, and delete
|
|
117
|
+
Helius webhooks. **(supported today)**
|
|
118
|
+
- 🚧 **Remaining Helius REST endpoints** — Enhanced Transactions API,
|
|
119
|
+
Mint API, token metadata, address lookups, and beyond. **(in progress)**
|
|
119
120
|
- 🚧 **Platform features** — streaming, websockets, and any new
|
|
120
121
|
capability Helius adds to its API. The full WebSocket subscription
|
|
121
122
|
surface (`accountSubscribe`, `transactionSubscribe`, `logsSubscribe`,
|
|
@@ -123,10 +124,10 @@ wraps it.
|
|
|
123
124
|
platform features are **in progress**.
|
|
124
125
|
|
|
125
126
|
**Current status:** the standard Solana JSON-RPC surface, the
|
|
126
|
-
WebSocket subscription surface, and the Admin
|
|
127
|
-
usage endpoint are implemented today. Support for
|
|
128
|
-
extensions and the remaining REST endpoints is actively
|
|
129
|
-
on.
|
|
127
|
+
WebSocket subscription surface, the Webhooks API, and the Admin
|
|
128
|
+
(account management) usage endpoint are implemented today. Support for
|
|
129
|
+
Helius RPC extensions and the remaining REST endpoints is actively
|
|
130
|
+
being worked on.
|
|
130
131
|
|
|
131
132
|
## Goals
|
|
132
133
|
|
|
@@ -244,9 +245,9 @@ The method names map 1:1 to the Solana JSON-RPC spec, just converted to
|
|
|
244
245
|
## Status
|
|
245
246
|
|
|
246
247
|
Actively expanding toward full coverage of the Helius API. See
|
|
247
|
-
[`src/helius/solana_rpc.py`](src/helius/solana_rpc.py) for the
|
|
248
|
-
|
|
249
|
-
continuously.
|
|
248
|
+
[`src/helius/solana_rpc/client.py`](src/helius/solana_rpc/client.py) for the
|
|
249
|
+
current Solana JSON-RPC implementation; supported surfaces are listed below,
|
|
250
|
+
and missing endpoints are tracked as issues and added continuously.
|
|
250
251
|
|
|
251
252
|
| Solana JSON-RPC method | Python method | Helius docs |
|
|
252
253
|
| ----------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
@@ -350,6 +351,44 @@ them; both yield a `(context, notification, subscription)` tuple where
|
|
|
350
351
|
| `vote_subscribe()` | `vote_unsubscribe(...)` | `VoteNotification` | [reference](https://www.helius.dev/docs/api-reference/rpc/websocket/votesubscribe) |
|
|
351
352
|
| `transaction_subscribe(...)` | `transaction_unsubscribe(...)` | `TransactionNotification` | [reference](https://www.helius.dev/docs/api-reference/rpc/websocket/slotunsubscribe) |
|
|
352
353
|
|
|
354
|
+
## Webhooks API
|
|
355
|
+
|
|
356
|
+
`WebhooksApiClient` wraps the Helius Webhooks REST API. It can create,
|
|
357
|
+
fetch, list, update, enable/disable, and delete webhook subscriptions,
|
|
358
|
+
returning typed `Webhook` models where the endpoint returns a webhook.
|
|
359
|
+
|
|
360
|
+
```python
|
|
361
|
+
from helius.webhooks.webhooks import WebhooksApiClient
|
|
362
|
+
|
|
363
|
+
with WebhooksApiClient(api_key="YOUR_HELIUS_API_KEY") as webhooks:
|
|
364
|
+
webhook = webhooks.create_webhook(
|
|
365
|
+
webhook_url="https://example.com/helius-webhook",
|
|
366
|
+
transaction_types=["TRANSFER"],
|
|
367
|
+
account_addresses=["So11111111111111111111111111111111111111112"],
|
|
368
|
+
webhook_type="enhanced",
|
|
369
|
+
auth_header="Bearer your-shared-secret",
|
|
370
|
+
encoding="jsonParsed",
|
|
371
|
+
txn_status="all",
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
webhooks.toggle_webhook(webhook.webhook_id, active=False)
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Like the other clients, it supports the context-manager protocol and a
|
|
378
|
+
manual `close()`. The constructor defaults to
|
|
379
|
+
`https://mainnet.helius-rpc.com/v0/webhooks` and reads `HELIUS_API_KEY`
|
|
380
|
+
from the environment or `.env` when `api_key` is omitted; optional
|
|
381
|
+
`headers` and `proxy` arguments are also accepted.
|
|
382
|
+
|
|
383
|
+
| REST operation | Python method | Helius docs |
|
|
384
|
+
| -------------- | -------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
385
|
+
| Create webhook | `create_webhook(...)` | [reference](https://www.helius.dev/docs/api-reference/webhooks/create-webhook) |
|
|
386
|
+
| Get webhook | `get_webhook(...)` | [reference](https://www.helius.dev/docs/api-reference/webhooks/get-webhook) |
|
|
387
|
+
| List webhooks | `get_all_webhooks()` | [reference](https://www.helius.dev/docs/api-reference/webhooks/get-all-webhooks) |
|
|
388
|
+
| Update webhook | `update_webhook(...)` | [reference](https://www.helius.dev/docs/api-reference/webhooks/update-webhook) |
|
|
389
|
+
| Toggle webhook | `toggle_webhook(...)` | [reference](https://www.helius.dev/docs/api-reference/webhooks/toggle-webhook) |
|
|
390
|
+
| Delete webhook | `delete_webhook(...)` | [reference](https://www.helius.dev/docs/api-reference/webhooks/delete-webhook) |
|
|
391
|
+
|
|
353
392
|
## Admin API
|
|
354
393
|
|
|
355
394
|
`AccountManagementClient` wraps the Helius admin API. Today it exposes
|
|
@@ -370,6 +409,19 @@ client defaults to `https://admin-api.helius.xyz` and, like the others,
|
|
|
370
409
|
supports the context-manager protocol, a manual `close()`, and reads
|
|
371
410
|
`HELIUS_API_KEY` from the environment or `.env`.
|
|
372
411
|
|
|
412
|
+
## AI
|
|
413
|
+
AI was used to assist in the development of this project, which was started beacause I needed a Helius client written in Python myself for another project. It has eliminated the need to perform repetitive tasks, such as writing functions that are similar to those written by hand before them. It has also been useful in drafting and updating the documentation (exclusively README.md at the time of writing), writing tests and examples.
|
|
414
|
+
That said, AI-generated code is reviewed in full, line-by-line by human(s) against the Helius API specifications.
|
|
415
|
+
|
|
416
|
+
To roughly describe my approach to using AI in this project, I generally write the code by hand, which makes it easy to structure the code well and maintain an intuitive understanding of how the code works. Then, when I got to the part where I had to implement 100+ similar Python functions, I turned to AI to write the massive part of the codebase, while I reviewed the result at the same time.
|
|
417
|
+
|
|
418
|
+
To sum up:
|
|
419
|
+
- I generally structure** and write a good part of what I'm programming by hand, including boilerplate.
|
|
420
|
+
- I find it best to prompt agents to implement very limited functionality within narrow scope, performing something closer to pair-programming.
|
|
421
|
+
- Even though at least half the LoC in this project are hand-typed, I find that AI is incredibly capable, can be enjoyable to use, but most importantly, using AI allows me to focus my time and energy on what matters.
|
|
422
|
+
|
|
423
|
+
By the way, this README section is completely brain-made and hand-typed.
|
|
424
|
+
|
|
373
425
|
## License
|
|
374
426
|
|
|
375
427
|
[MIT](LICENSE)
|
|
@@ -2,3 +2,5 @@
|
|
|
2
2
|
- WebSocket subscription manager
|
|
3
3
|
- Use typeddicts where useful
|
|
4
4
|
- When Python 3.10 and 3.11 go out of support we should use TypedDict from typing instead of typing_extensions
|
|
5
|
+
- Improve camelCase <-> snake_case in typing
|
|
6
|
+
- Handle HTTP response codes and idiomatic error messages in responses
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Create, list, update, toggle, and delete Helius webhooks.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
|
|
5
|
+
export HELIUS_API_KEY=your_helius_api_key
|
|
6
|
+
|
|
7
|
+
# Print all configured webhooks.
|
|
8
|
+
python examples/webhooks/webhook_crud.py list
|
|
9
|
+
|
|
10
|
+
# Inspect one webhook.
|
|
11
|
+
python examples/webhooks/webhook_crud.py show <WEBHOOK_ID>
|
|
12
|
+
|
|
13
|
+
# Create a webhook that watches one or more accounts.
|
|
14
|
+
python examples/webhooks/webhook_crud.py create \
|
|
15
|
+
https://example.com/helius/webhook \
|
|
16
|
+
<ACCOUNT_ADDRESS> [<ACCOUNT_ADDRESS> ...] \
|
|
17
|
+
--transaction-type TRANSFER \
|
|
18
|
+
--auth-header "Bearer shared-secret"
|
|
19
|
+
|
|
20
|
+
# Update every mutable field on an existing webhook.
|
|
21
|
+
python examples/webhooks/webhook_crud.py update <WEBHOOK_ID> \
|
|
22
|
+
https://example.com/helius/webhook \
|
|
23
|
+
<ACCOUNT_ADDRESS> [<ACCOUNT_ADDRESS> ...]
|
|
24
|
+
|
|
25
|
+
# Pause, resume, or delete.
|
|
26
|
+
python examples/webhooks/webhook_crud.py toggle <WEBHOOK_ID> --inactive
|
|
27
|
+
python examples/webhooks/webhook_crud.py delete <WEBHOOK_ID> --yes
|
|
28
|
+
|
|
29
|
+
Docs:
|
|
30
|
+
https://www.helius.dev/docs/api-reference/webhooks/create-webhook
|
|
31
|
+
https://www.helius.dev/docs/api-reference/webhooks/get-all-webhooks
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import argparse
|
|
37
|
+
import json
|
|
38
|
+
import sys
|
|
39
|
+
from typing import Any
|
|
40
|
+
|
|
41
|
+
import httpx
|
|
42
|
+
|
|
43
|
+
from helius.webhooks.webhooks import Webhook, WebhooksApiClient
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def print_webhook(webhook: Webhook) -> None:
|
|
47
|
+
print(json.dumps(webhook.model_dump(mode="json"), indent=2, sort_keys=True))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def print_dry_run(action: str, payload: dict[str, Any]) -> int:
|
|
51
|
+
print(f"Would {action} with:")
|
|
52
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def add_webhook_payload_args(parser: argparse.ArgumentParser) -> None:
|
|
57
|
+
parser.add_argument("webhook_url", help="Public HTTPS endpoint Helius will POST to")
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"account_addresses",
|
|
60
|
+
nargs="+",
|
|
61
|
+
help="One or more account addresses to watch",
|
|
62
|
+
)
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--transaction-type",
|
|
65
|
+
dest="transaction_types",
|
|
66
|
+
action="append",
|
|
67
|
+
help="Transaction type to subscribe to; repeat for multiple (default TRANSFER)",
|
|
68
|
+
)
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--webhook-type",
|
|
71
|
+
default="enhanced",
|
|
72
|
+
choices=(
|
|
73
|
+
"enhanced",
|
|
74
|
+
"raw",
|
|
75
|
+
"discord",
|
|
76
|
+
"enhancedDevnet",
|
|
77
|
+
"rawDevnet",
|
|
78
|
+
"discordDevnet",
|
|
79
|
+
),
|
|
80
|
+
help="Webhook payload type (default enhanced)",
|
|
81
|
+
)
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"--auth-header",
|
|
84
|
+
default="",
|
|
85
|
+
help="Value Helius should include in the Authorization header",
|
|
86
|
+
)
|
|
87
|
+
parser.add_argument(
|
|
88
|
+
"--encoding",
|
|
89
|
+
default="jsonParsed",
|
|
90
|
+
help="Webhook transaction encoding (default jsonParsed)",
|
|
91
|
+
)
|
|
92
|
+
parser.add_argument(
|
|
93
|
+
"--txn-status",
|
|
94
|
+
default="all",
|
|
95
|
+
choices=("all", "success", "failed"),
|
|
96
|
+
help="Transaction status filter (default all)",
|
|
97
|
+
)
|
|
98
|
+
parser.add_argument(
|
|
99
|
+
"--dry-run",
|
|
100
|
+
action="store_true",
|
|
101
|
+
help="Print the request payload without sending it",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def webhook_payload(args: argparse.Namespace) -> dict[str, Any]:
|
|
106
|
+
return {
|
|
107
|
+
"webhook_url": args.webhook_url,
|
|
108
|
+
"transaction_types": args.transaction_types or ["TRANSFER"],
|
|
109
|
+
"account_addresses": args.account_addresses,
|
|
110
|
+
"webhook_type": args.webhook_type,
|
|
111
|
+
"auth_header": args.auth_header,
|
|
112
|
+
"encoding": args.encoding,
|
|
113
|
+
"txn_status": args.txn_status,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
118
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
119
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
120
|
+
|
|
121
|
+
subparsers.add_parser("list", help="List all webhooks")
|
|
122
|
+
|
|
123
|
+
show = subparsers.add_parser("show", help="Show one webhook")
|
|
124
|
+
show.add_argument("webhook_id")
|
|
125
|
+
|
|
126
|
+
create = subparsers.add_parser("create", help="Create a webhook")
|
|
127
|
+
add_webhook_payload_args(create)
|
|
128
|
+
|
|
129
|
+
update = subparsers.add_parser("update", help="Replace an existing webhook")
|
|
130
|
+
update.add_argument("webhook_id")
|
|
131
|
+
add_webhook_payload_args(update)
|
|
132
|
+
|
|
133
|
+
toggle = subparsers.add_parser("toggle", help="Activate or deactivate a webhook")
|
|
134
|
+
toggle.add_argument("webhook_id")
|
|
135
|
+
status = toggle.add_mutually_exclusive_group(required=True)
|
|
136
|
+
status.add_argument("--active", action="store_true", help="Resume the webhook")
|
|
137
|
+
status.add_argument("--inactive", action="store_true", help="Pause the webhook")
|
|
138
|
+
toggle.add_argument("--dry-run", action="store_true")
|
|
139
|
+
|
|
140
|
+
delete = subparsers.add_parser("delete", help="Delete a webhook")
|
|
141
|
+
delete.add_argument("webhook_id")
|
|
142
|
+
delete.add_argument("--yes", action="store_true", help="Confirm permanent deletion")
|
|
143
|
+
delete.add_argument("--dry-run", action="store_true")
|
|
144
|
+
|
|
145
|
+
return parser
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def main() -> int:
|
|
149
|
+
parser = build_parser()
|
|
150
|
+
args = parser.parse_args()
|
|
151
|
+
|
|
152
|
+
if args.command in {"create", "update"} and args.dry_run:
|
|
153
|
+
return print_dry_run(args.command, webhook_payload(args))
|
|
154
|
+
if args.command == "toggle" and args.dry_run:
|
|
155
|
+
return print_dry_run(
|
|
156
|
+
"toggle", {"webhook_id": args.webhook_id, "active": args.active}
|
|
157
|
+
)
|
|
158
|
+
if args.command == "delete":
|
|
159
|
+
if args.dry_run:
|
|
160
|
+
return print_dry_run("delete", {"webhook_id": args.webhook_id})
|
|
161
|
+
if not args.yes:
|
|
162
|
+
print("Refusing to delete without --yes", file=sys.stderr)
|
|
163
|
+
return 2
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
with WebhooksApiClient() as client:
|
|
167
|
+
if args.command == "list":
|
|
168
|
+
webhooks = client.get_all_webhooks()
|
|
169
|
+
print(json.dumps([w.model_dump(mode="json") for w in webhooks], indent=2))
|
|
170
|
+
elif args.command == "show":
|
|
171
|
+
print_webhook(client.get_webhook(args.webhook_id))
|
|
172
|
+
elif args.command == "create":
|
|
173
|
+
print_webhook(client.create_webhook(**webhook_payload(args)))
|
|
174
|
+
elif args.command == "update":
|
|
175
|
+
print_webhook(
|
|
176
|
+
client.update_webhook(args.webhook_id, **webhook_payload(args))
|
|
177
|
+
)
|
|
178
|
+
elif args.command == "toggle":
|
|
179
|
+
print_webhook(client.toggle_webhook(args.webhook_id, active=args.active))
|
|
180
|
+
elif args.command == "delete":
|
|
181
|
+
print(client.delete_webhook(args.webhook_id))
|
|
182
|
+
except httpx.HTTPStatusError as exc:
|
|
183
|
+
print(f"HTTP {exc.response.status_code}: {exc.response.text}", file=sys.stderr)
|
|
184
|
+
return 1
|
|
185
|
+
|
|
186
|
+
return 0
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
if __name__ == "__main__":
|
|
190
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Run a tiny local HTTP server that prints Helius webhook payloads.
|
|
2
|
+
|
|
3
|
+
This receiver uses only the Python standard library. It is useful for local
|
|
4
|
+
development with a tunnel such as ngrok, Cloudflare Tunnel, or `ssh -R`.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
|
|
8
|
+
python examples/webhooks/webhook_receiver.py --port 8080
|
|
9
|
+
|
|
10
|
+
# In another shell, expose it publicly and use that URL in Helius:
|
|
11
|
+
ngrok http 8080
|
|
12
|
+
|
|
13
|
+
# If your webhook was created with authHeader="Bearer shared-secret":
|
|
14
|
+
python examples/webhooks/webhook_receiver.py \
|
|
15
|
+
--auth-header "Bearer shared-secret"
|
|
16
|
+
|
|
17
|
+
The handler accepts POST requests on any path, verifies the optional
|
|
18
|
+
Authorization header, pretty-prints JSON payloads, and replies with 200 OK.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import json
|
|
25
|
+
import sys
|
|
26
|
+
from http import HTTPStatus
|
|
27
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class WebhookHandler(BaseHTTPRequestHandler):
|
|
32
|
+
expected_auth_header: str | None = None
|
|
33
|
+
|
|
34
|
+
def do_POST(self) -> None: # noqa: N802 - stdlib handler hook name
|
|
35
|
+
if self.expected_auth_header is not None:
|
|
36
|
+
actual = self.headers.get("Authorization")
|
|
37
|
+
if actual != self.expected_auth_header:
|
|
38
|
+
self.respond(HTTPStatus.UNAUTHORIZED, {"ok": False})
|
|
39
|
+
print("Rejected request with missing or invalid Authorization header")
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
length = int(self.headers.get("Content-Length", "0"))
|
|
43
|
+
raw_body = self.rfile.read(length)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
payload: Any = json.loads(raw_body)
|
|
47
|
+
except json.JSONDecodeError:
|
|
48
|
+
payload = raw_body.decode("utf-8", errors="replace")
|
|
49
|
+
|
|
50
|
+
print("\n=== Helius webhook received ===")
|
|
51
|
+
print(f"Path: {self.path}")
|
|
52
|
+
print(f"User-Agent: {self.headers.get('User-Agent', '-')}")
|
|
53
|
+
if isinstance(payload, (dict, list)):
|
|
54
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
55
|
+
else:
|
|
56
|
+
print(payload)
|
|
57
|
+
|
|
58
|
+
self.respond(HTTPStatus.OK, {"ok": True})
|
|
59
|
+
|
|
60
|
+
def do_GET(self) -> None: # noqa: N802 - stdlib handler hook name
|
|
61
|
+
self.respond(
|
|
62
|
+
HTTPStatus.OK, {"ok": True, "message": "POST webhook payloads here"}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def log_message(self, format: str, *args: Any) -> None:
|
|
66
|
+
# Keep the default access log, but send it to stdout next to payload logs.
|
|
67
|
+
print(f"{self.address_string()} - {format % args}")
|
|
68
|
+
|
|
69
|
+
def respond(self, status: HTTPStatus, payload: dict[str, Any]) -> None:
|
|
70
|
+
body = json.dumps(payload).encode("utf-8")
|
|
71
|
+
self.send_response(status)
|
|
72
|
+
self.send_header("Content-Type", "application/json")
|
|
73
|
+
self.send_header("Content-Length", str(len(body)))
|
|
74
|
+
self.end_headers()
|
|
75
|
+
self.wfile.write(body)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def main() -> int:
|
|
79
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"--host", default="127.0.0.1", help="Bind host (default 127.0.0.1)"
|
|
82
|
+
)
|
|
83
|
+
parser.add_argument("--port", type=int, default=8080, help="Bind port (default 8080)")
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"--auth-header",
|
|
86
|
+
help="Required Authorization header value, matching the webhook authHeader",
|
|
87
|
+
)
|
|
88
|
+
args = parser.parse_args()
|
|
89
|
+
|
|
90
|
+
WebhookHandler.expected_auth_header = args.auth_header
|
|
91
|
+
server = ThreadingHTTPServer((args.host, args.port), WebhookHandler)
|
|
92
|
+
|
|
93
|
+
print(f"Listening on http://{args.host}:{args.port}")
|
|
94
|
+
if args.auth_header:
|
|
95
|
+
print("Authorization header verification is enabled")
|
|
96
|
+
print("Press Ctrl+C to stop", flush=True)
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
server.serve_forever()
|
|
100
|
+
except KeyboardInterrupt:
|
|
101
|
+
print("\nStopping receiver", file=sys.stderr)
|
|
102
|
+
finally:
|
|
103
|
+
server.server_close()
|
|
104
|
+
|
|
105
|
+
return 0
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
raise SystemExit(main())
|
|
@@ -5,7 +5,6 @@ import httpx
|
|
|
5
5
|
from dotenv import dotenv_values
|
|
6
6
|
from pydantic import Field, TypeAdapter, validate_call
|
|
7
7
|
|
|
8
|
-
from helius.rpc import JsonRpcRequest
|
|
9
8
|
from helius.solana_rpc.models import (
|
|
10
9
|
Account,
|
|
11
10
|
Block,
|
|
@@ -33,6 +32,7 @@ from helius.solana_rpc.models import (
|
|
|
33
32
|
TransferFilters,
|
|
34
33
|
VotingAccount,
|
|
35
34
|
)
|
|
35
|
+
from helius.utils import JsonRpcRequest
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
# TODO: Use Pydantic typed dict where useful
|
|
@@ -4,8 +4,6 @@ from pydantic import AliasGenerator, BaseModel, ConfigDict
|
|
|
4
4
|
from pydantic.alias_generators import to_camel
|
|
5
5
|
from typing_extensions import TypedDict
|
|
6
6
|
|
|
7
|
-
# TODO: Improve camelCase <-> snake_case
|
|
8
|
-
|
|
9
7
|
|
|
10
8
|
class Account(BaseModel):
|
|
11
9
|
model_config = ConfigDict(alias_generator=AliasGenerator(validation_alias=to_camel))
|
|
File without changes
|