Skip to content

Commit 8a98325

Browse files
author
Proto
committed
Init
0 parents  commit 8a98325

10 files changed

Lines changed: 828 additions & 0 deletions

File tree

.env.example

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
PUBLIC_API_BASE=https://api.rpcnode.dev
2+
3+
# UUID from the cabinet (X-Api-Key)
4+
PUBLIC_API_KEY=7c9e6679-7425-40de-944b-e07fc1f90ae7
5+
6+
# Secret shown once at key creation (sk_live_…) — used for HMAC X-Signature
7+
PUBLIC_API_SECRET=sk_live_xxxxxxxx
8+
9+
# Optional: network / address for mutate + create-payment smokes (prefer testnets)
10+
# PUBLIC_API_TEST_NETWORK=tron-nile
11+
# PUBLIC_API_TEST_ADDRESS=TNdoZ3TBwcpMM1cx6VEabi4CeLxysGkq1b
12+
13+
# Opt-in write smokes (default off). Already-exported env wins over this file.
14+
# PUBLIC_API_SMOKE_MUTATE=1
15+
# PUBLIC_API_SMOKE_CREATE_PAYMENT=1

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
__pycache__/
2+
*.egg-info/
3+
.pytest_cache/
4+
.venv/
5+
venv/
6+
.env
7+
dist/
8+
build/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 RpcNode
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# rpcnode
2+
3+
Official Python client for the RpcNode Public API: dedicated fullnode RPC (Ethereum, Solana, Bitcoin, Tron, BNB Chain, Polygon, Arbitrum, Optimism, Base, and more), HMAC-signed merchant HTTP, address watch webhooks, and crypto payments.
4+
5+
API: `https://api.rpcnode.dev`
6+
7+
Repo: [github.com/rpcnode/api-sdk-python](https://github.com/rpcnode/api-sdk-python)
8+
9+
## Install
10+
11+
```bash
12+
pip install "rpcnode @ git+https://github.com/rpcnode/api-sdk-python.git"
13+
# or:
14+
git clone git@github.com:rpcnode/api-sdk-python.git
15+
cd api-sdk-python && pip install -e ".[dev]"
16+
```
17+
18+
## Auth
19+
20+
Every request is HMAC-signed. Query string is **not** part of the signature.
21+
22+
| Header | Value |
23+
|--------|--------|
24+
| `X-Api-Key` | UUID from the cabinet |
25+
| `X-Timestamp` | Unix seconds (±300s) |
26+
| `X-Signature` | `hex(hmac_sha256(secret, canonical))` |
27+
28+
```
29+
{timestamp}\n{METHOD}\n{path}\n{sha256_hex(body)}
30+
```
31+
32+
Empty body → SHA-256 of `""`. Headers are added by the client.
33+
34+
Rate limit: **10 RPS per API key**. Over it the API returns HTTP 429 and `Rate limit exceeded.` (`Retry-After: 1`). This is not JSON-RPC endpoint RPS.
35+
36+
## Quickstart
37+
38+
```python
39+
import os
40+
from rpcnode import RpcNodeClient
41+
42+
client = RpcNodeClient(
43+
base_url=os.environ.get("PUBLIC_API_BASE", "https://api.rpcnode.dev"),
44+
api_key=os.environ["PUBLIC_API_KEY"], # UUID
45+
secret=os.environ["PUBLIC_API_SECRET"], # sk_live_…
46+
)
47+
48+
account = client.get_account()
49+
tokens = client.list_network_tokens(network_slug="tron-nile", include_native=True)
50+
```
51+
52+
## Methods
53+
54+
| Client | HTTP |
55+
|--------|------|
56+
| `get_account()` | `GET /v1/account` |
57+
| `get_account_usage(days=30)` | `GET /v1/account/usage` |
58+
| `list_endpoints()` | `GET /v1/endpoints` |
59+
| `list_available_networks()` | `GET /v1/endpoints/available-networks` |
60+
| `get_workspace_usage(days=None)` | `GET /v1/endpoints/usage` |
61+
| `create_endpoint(**body)` | `POST /v1/endpoints` |
62+
| `get_endpoint_usage(id, days=None)` | `GET /v1/endpoints/{id}/usage` |
63+
| `update_endpoint_status(id, status)` | `PATCH /v1/endpoints/{id}/status` |
64+
| `update_endpoint_allowed_ips(id, ips)` | `PATCH /v1/endpoints/{id}/allowed-ips` |
65+
| `get_address_watch_account()` | `GET /v1/address-watch/account` |
66+
| `list_addresses(**params)` | `GET /v1/address-watch/addresses` |
67+
| `add_address(network_slug=, address=)` | `POST /v1/address-watch/addresses` |
68+
| `delete_address(watch_id)` | `DELETE /v1/address-watch/addresses` |
69+
| `list_events(**params)` | `GET /v1/address-watch/events` |
70+
| `get_webhook()` | `GET /v1/address-watch/webhook` |
71+
| `set_webhook(enabled=, url=, events=)` | `PUT /v1/address-watch/webhook` |
72+
| `list_payments(**params)` | `GET /v1/payments` |
73+
| `create_payment(**body)` | `POST /v1/payments` |
74+
| `get_payment(payment_id)` | `GET /v1/payments/{id}` |
75+
| `get_payment_by_order_id(order_id)` | `GET /v1/payments/orders/{orderId}` |
76+
| `list_network_tokens(network_slug=, include_native=)` | `GET /v1/network-tokens` |
77+
78+
## Tests
79+
80+
```bash
81+
cp .env.example .env # PUBLIC_API_KEY + PUBLIC_API_SECRET for live smokes
82+
pytest -q
83+
```
84+
85+
HMAC unit tests run without credentials. Live reads use `.env` or the environment. Writes: `PUBLIC_API_SMOKE_MUTATE=1`, `PUBLIC_API_SMOKE_CREATE_PAYMENT=1`.
86+
87+
## License
88+
89+
MIT

pyproject.toml

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
[build-system]
2+
requires = ["setuptools>=68", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "rpcnode"
7+
version = "0.1.0"
8+
description = "Official Python SDK for the RpcNode Public API: dedicated fullnode RPC endpoints (Ethereum, Solana, Bitcoin, Tron, BNB Chain, Polygon, Arbitrum, Optimism, Base, and more), HMAC-signed merchant HTTP, address watch webhooks, and crypto payments."
9+
readme = "README.md"
10+
requires-python = ">=3.10"
11+
license = { text = "MIT" }
12+
dependencies = []
13+
keywords = [
14+
"rpcnode",
15+
"rpc",
16+
"json-rpc",
17+
"fullnode",
18+
"full-node",
19+
"archive-node",
20+
"blockchain-node",
21+
"crypto-node",
22+
"web3",
23+
"blockchain",
24+
"cryptocurrency",
25+
"rpc-endpoint",
26+
"dedicated-node",
27+
"ethereum",
28+
"geth",
29+
"erigon",
30+
"solana",
31+
"bitcoin",
32+
"tron",
33+
"bsc",
34+
"bnb-chain",
35+
"polygon",
36+
"matic",
37+
"arbitrum",
38+
"optimism",
39+
"base",
40+
"hyperliquid",
41+
"xrpl",
42+
"xrp",
43+
"dogecoin",
44+
"cardano",
45+
"sui",
46+
"stellar",
47+
"hedera",
48+
"litecoin",
49+
"dash",
50+
"bitcoin-cash",
51+
"aptos",
52+
"polkadot",
53+
"ethereum-classic",
54+
"shibarium",
55+
"hmac",
56+
"merchant-api",
57+
"payments",
58+
"address-watch",
59+
"webhook",
60+
]
61+
classifiers = [
62+
"Development Status :: 4 - Beta",
63+
"Intended Audience :: Developers",
64+
"License :: OSI Approved :: MIT License",
65+
"Programming Language :: Python :: 3",
66+
"Programming Language :: Python :: 3.10",
67+
"Programming Language :: Python :: 3.11",
68+
"Programming Language :: Python :: 3.12",
69+
"Topic :: Internet",
70+
"Topic :: Software Development :: Libraries :: Python Modules",
71+
]
72+
73+
[project.urls]
74+
Homepage = "https://api.rpcnode.dev"
75+
Repository = "https://github.com/rpcnode/api-sdk-python"
76+
Issues = "https://github.com/rpcnode/api-sdk-python/issues"
77+
Documentation = "https://api.rpcnode.dev"
78+
79+
[project.optional-dependencies]
80+
dev = ["pytest>=8.0"]
81+
82+
[tool.setuptools.packages.find]
83+
include = ["rpcnode*"]
84+
85+
[tool.pytest.ini_options]
86+
testpaths = ["tests"]

rpcnode/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from .client import RpcNodeApiError, RpcNodeClient, __version__
2+
from .auth import sign_request, sha256_hex, hmac_sha256_hex, build_canonical_string
3+
4+
__all__ = [
5+
"RpcNodeClient",
6+
"RpcNodeApiError",
7+
"sign_request",
8+
"sha256_hex",
9+
"hmac_sha256_hex",
10+
"build_canonical_string",
11+
"__version__",
12+
]

rpcnode/auth.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from __future__ import annotations
2+
3+
import hashlib
4+
import hmac
5+
import time
6+
from typing import Any
7+
8+
9+
def sha256_hex(body: str) -> str:
10+
return hashlib.sha256(body.encode("utf-8")).hexdigest()
11+
12+
13+
def hmac_sha256_hex(secret: str, message: str) -> str:
14+
return hmac.new(secret.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).hexdigest()
15+
16+
17+
def build_canonical_string(timestamp: int, method: str, path: str, body_hash: str) -> str:
18+
normalized = "/" + path.lstrip("/")
19+
20+
return "\n".join([str(timestamp), method.upper().strip(), normalized, body_hash])
21+
22+
23+
def sign_request(
24+
*,
25+
api_key: str,
26+
secret: str,
27+
method: str,
28+
path: str,
29+
body: str = "",
30+
timestamp: int | None = None,
31+
) -> dict[str, Any]:
32+
method_u = method.upper().strip()
33+
raw = path.strip() or "/"
34+
q = raw.find("?")
35+
sign_path = "/" + (raw if q == -1 else raw[:q]).lstrip("/")
36+
query = "" if q == -1 else raw[q:]
37+
request_path = f"{sign_path}{query}"
38+
ts = int(time.time()) if timestamp is None else timestamp
39+
body_hash = sha256_hex(body)
40+
canonical = build_canonical_string(ts, method_u, sign_path, body_hash)
41+
signature = hmac_sha256_hex(secret, canonical)
42+
43+
headers: dict[str, str] = {
44+
"X-Api-Key": api_key.strip(),
45+
"X-Timestamp": str(ts),
46+
"X-Signature": signature,
47+
}
48+
49+
if body != "":
50+
headers["Content-Type"] = "application/json"
51+
52+
return {
53+
"sign_path": sign_path,
54+
"request_path": request_path,
55+
"body": body,
56+
"headers": headers,
57+
}

0 commit comments

Comments
 (0)