Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ This is a polyglot SDK: one Rust core library with Python, Node.js, and Ruby bin
- `ruby/` — Ruby package directory (`lib/quicknode_sdk.rb` entry point, `examples/`)

### Core Pattern
- `QuickNodeSdk` is the root entry point holding sub-clients (e.g., `admin: AdminApiClient`). All clients share a `SdkConfig(Arc<SdkConfigInner>)` wrapping one `reqwest` HTTP client and the API key.
- There are clients per QuickNode product, with functions mapping to API calls
- `QuicknodeSdk` is the root entry point holding sub-clients (e.g., `admin: AdminApiClient`). All clients share a `SdkConfig(Arc<SdkConfigInner>)` wrapping one `reqwest` HTTP client and the API key.
- There are clients per Quicknode product, with functions mapping to API calls
- Request params and Responses should be fully typed structs

### Per-Sub-Client Config Pattern
Expand Down Expand Up @@ -114,17 +114,17 @@ Each binding exposes a typed exception hierarchy rooted at a shared base class s

| `SdkError` variant | Python / Ruby class | Node class | Base |
|---|---|---|---|
| `Config`, `UrlParse` | `ConfigError` | `ConfigError` | `QuickNodeError` |
| `Config`, `UrlParse` | `ConfigError` | `ConfigError` | `QuicknodeError` |
| `Http` + `HttpKind::Timeout` | `TimeoutError` | `TimeoutError` | `HttpError` |
| `Http` + `HttpKind::Connect` | `ConnectionError` | `ConnectionError` | `HttpError` |
| `Http` + `HttpKind::Other` | `HttpError` | `HttpError` | `QuickNodeError` |
| `Api { status, body }` | `ApiError` (with `.status`, `.body`) | `ApiError` (with `.status`, `.body`) | `QuickNodeError` |
| `Decode { body, .. }` | `DecodeError` (with `.body`) | `DecodeError` (with `.body`) | `QuickNodeError` |
| `Http` + `HttpKind::Other` | `HttpError` | `HttpError` | `QuicknodeError` |
| `Api { status, body }` | `ApiError` (with `.status`, `.body`) | `ApiError` (with `.status`, `.body`) | `QuicknodeError` |
| `Decode { body, .. }` | `DecodeError` (with `.body`) | `DecodeError` (with `.body`) | `QuicknodeError` |

Each binding owns its mapping in a dedicated `errors.rs` file:
- **Python** — `crates/python/src/errors.rs` uses `create_exception!` macros; `map_sdk_err` sets `.status` / `.body` attributes via `setattr` on the exception instance. Exceptions are registered on the module in `add_to_module`.
- **Node** — `crates/node/src/errors.rs` encodes the variant, status, and body into a tagged message (`[<kind>|<status>|<body_len>]<msg>\x1f<body>`) because napi-rs only supports plain `napi::Error`. The JS wrapper `npm/errors.js` (`fromNapiError` + `wrapClient` Proxy) parses the prefix and rethrows as the typed subclass. All client methods must be wrapped via `wrapClient` so sync throws and rejected promises are both re-tagged.
- **Ruby** — `crates/ruby/src/errors.rs` uses `module.define_error` to build the class hierarchy under `QuickNodeSdk::Error`; `map_err` instantiates the class and sets `@status` / `@body` ivars (exposed via `attr_reader`-style methods). Classes are captured once in a `OnceLock<Opaque<ExceptionClass>>`.
- **Ruby** — `crates/ruby/src/errors.rs` uses `module.define_error` to build the class hierarchy under `QuicknodeSdk::Error`; `map_err` instantiates the class and sets `@status` / `@body` ivars (exposed via `attr_reader`-style methods). Classes are captured once in a `OnceLock<Opaque<ExceptionClass>>`.

When adding a new `SdkError` variant:
1. Add the variant to `crates/core/src/errors.rs` and update `http_kind()` if it's transport-level.
Expand All @@ -139,7 +139,7 @@ When adding a new `SdkError` variant:
`crates/node/src/lib.rs` uses `#[napi(constructor)]` and `#[napi(getter)]` macros. napi handles async conversion automatically.

### Ruby Binding Pattern
`crates/ruby/src/lib.rs` uses the `magnus` crate. All async SDK calls are wrapped via a single shared `tokio::runtime` (static `OnceLock`) using `.block_on()` to produce a synchronous Ruby API. Methods returning data return **JSON strings** — callers must parse with `JSON.parse()`. All parameters are passed as a single Ruby Hash with symbol keys (e.g. `get_endpoints(limit: 20)`). Required keys throw `ArgumentError` if missing; unknown keys also throw `ArgumentError` (validated via `validate_keys`). The magnus arity limit of 15 is why this pattern is used uniformly — all methods are registered with arity `1` (or `0` for zero-param methods). Classes are exposed under the `QuickNodeSdk` module and registered via `#[magnus::init(name = "quicknode_sdk")]`.
`crates/ruby/src/lib.rs` uses the `magnus` crate. All async SDK calls are wrapped via a single shared `tokio::runtime` (static `OnceLock`) using `.block_on()` to produce a synchronous Ruby API. Methods returning data return **JSON strings** — callers must parse with `JSON.parse()`. All parameters are passed as a single Ruby Hash with symbol keys (e.g. `get_endpoints(limit: 20)`). Required keys throw `ArgumentError` if missing; unknown keys also throw `ArgumentError` (validated via `validate_keys`). The magnus arity limit of 15 is why this pattern is used uniformly — all methods are registered with arity `1` (or `0` for zero-param methods). Classes are exposed under the `QuicknodeSdk` module and registered via `#[magnus::init(name = "quicknode_sdk")]`.

When adding a new Ruby method:
1. Accept `opts: RHash` as the single parameter
Expand Down
60 changes: 30 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Quicknode SDK

A unified SDK for building on QuickNode.
A unified SDK for building on Quicknode.

Rust SDK with Python, Node.js, and Ruby bindings.

Expand Down Expand Up @@ -73,19 +73,19 @@ sdk/

**Node.js:** `npm install quicknode-sdk`

**Ruby:** `gem install quicknode-sdk` _(not yet published — see Development below)_
**Ruby:** `gem install quicknode_sdk`

## Quick Start

Construct the SDK once, then reach into the four sub-clients (`admin`, `streams`, `webhooks`, `kvstore`). Subsequent API Reference snippets assume you have a `qn` handle from one of these blocks.

```rust
// Rust
use quicknode_sdk::{QuickNodeSdk, SdkFullConfig};
use quicknode_sdk::{QuicknodeSdk, SdkFullConfig};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let qn = QuickNodeSdk::from_env()?;
let qn = QuicknodeSdk::from_env()?;
let resp = qn.admin.get_endpoints(&Default::default()).await?;
println!("{} endpoints", resp.data.len());
Ok(())
Expand All @@ -95,10 +95,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
```python
# Python
import asyncio
from sdk import QuickNodeSdk
from sdk import QuicknodeSdk

async def main():
qn = QuickNodeSdk.from_env()
qn = QuicknodeSdk.from_env()
resp = await qn.admin.get_endpoints()
print(f"{len(resp.data)} endpoints")

Expand All @@ -107,9 +107,9 @@ asyncio.run(main())

```typescript
// Node.js
import { QuickNodeSdk } from "quicknode-sdk";
import { QuicknodeSdk } from "quicknode-sdk";

const qn = QuickNodeSdk.fromEnv();
const qn = QuicknodeSdk.fromEnv();
const resp = await qn.admin.getEndpoints();
console.log(`${resp.data.length} endpoints`);
```
Expand All @@ -119,7 +119,7 @@ console.log(`${resp.data.length} endpoints`);
require "json"
require "quicknode_sdk"

qn = QuickNodeSdk::SDK.from_env
qn = QuicknodeSdk::SDK.from_env
resp = JSON.parse(qn.admin.get_endpoints({}))
puts "#{resp["data"].length} endpoints"
```
Expand All @@ -132,45 +132,45 @@ There are two ways to configure the SDK.

```python
# Python
from sdk import QuickNodeSdk, SdkFullConfig, HttpConfig
qn = QuickNodeSdk(SdkFullConfig(api_key="your-key", http=HttpConfig(timeout_secs=30)))
from sdk import QuicknodeSdk, SdkFullConfig, HttpConfig
qn = QuicknodeSdk(SdkFullConfig(api_key="your-key", http=HttpConfig(timeout_secs=30)))
```

```typescript
// Node.js
import { QuickNodeSdk } from "quicknode-sdk";
const qn = new QuickNodeSdk({ apiKey: "your-key", http: { timeoutSecs: 30 } });
import { QuicknodeSdk } from "quicknode-sdk";
const qn = new QuicknodeSdk({ apiKey: "your-key", http: { timeoutSecs: 30 } });
```

```rust
// Rust
let qn = QuickNodeSdk::new(&SdkFullConfig::builder().api_key("your-key").build())?;
let qn = QuicknodeSdk::new(&SdkFullConfig::builder().api_key("your-key").build())?;
```

### Option B — Load from environment (`from_env()`)

```python
# Python
qn = QuickNodeSdk.from_env()
qn = QuicknodeSdk.from_env()
```
```typescript
// Node.js
const qn = QuickNodeSdk.fromEnv();
const qn = QuicknodeSdk.fromEnv();
```
```ruby
# Ruby
qn = QuickNodeSdk::SDK.from_env
qn = QuicknodeSdk::SDK.from_env
```
```rust
// Rust
let qn = QuickNodeSdk::from_env()?;
let qn = QuicknodeSdk::from_env()?;
```

Environment variables (prefix `QN_SDK__`, separator `__`):

| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `QN_SDK__API_KEY` | yes | — | Your QuickNode API key |
| `QN_SDK__API_KEY` | yes | — | Your Quicknode API key |
| `QN_SDK__HTTP__TIMEOUT_SECS` | no | 30 | HTTP request timeout in seconds |
| `QN_SDK__HTTP__POOL_MAX_IDLE_PER_HOST` | no | — | Max idle HTTP connections per host |
| `QN_SDK__ADMIN__BASE_URL` | no | `https://api.quicknode.com/v0/` | Override admin API base URL (HTTPS, must end with `/`) |
Expand Down Expand Up @@ -1796,7 +1796,7 @@ resp = JSON.parse(qn.admin.get_account_metrics(period: "day", metric: "credits_o

##### `list_chains` / `listChains`

Lists the blockchains supported by QuickNode along with their networks.
Lists the blockchains supported by Quicknode along with their networks.

**Parameters**: none.

Expand Down Expand Up @@ -2104,7 +2104,7 @@ Wrapper naming per language:
- **Rust**: `DestinationAttributes::Webhook(WebhookAttributes { .. })` etc.
- **Python**: `StreamWebhookDestination(WebhookAttributes(...))`, `StreamS3Destination(S3Attributes(...))`, etc.
- **Node.js**: a discriminated object `{ destination: "webhook", attributes: { ... } }` using string discriminators.
- **Ruby**: factory methods on `QuickNodeSdk::DestinationAttributes`, e.g. `QuickNodeSdk::DestinationAttributes.webhook(url: ..., ...)`.
- **Ruby**: factory methods on `QuicknodeSdk::DestinationAttributes`, e.g. `QuicknodeSdk::DestinationAttributes.webhook(url: ..., ...)`.

#### Streams methods

Expand Down Expand Up @@ -2195,7 +2195,7 @@ const stream = await qn.streams.createStream({

```ruby
# Ruby
dest = QuickNodeSdk::DestinationAttributes.webhook(
dest = QuicknodeSdk::DestinationAttributes.webhook(
url: "https://webhook.site/...",
max_retry: 3,
retry_interval_sec: 1,
Expand Down Expand Up @@ -3303,13 +3303,13 @@ qn.kvstore.delete_list(key: "my-list")
## Error Handling

Every binding exposes a typed exception hierarchy derived from the core `SdkError`
enum (`crates/core/src/errors.rs`). Catch the base class (`QuickNodeError` /
`QuickNodeSdk::Error` / `SdkError`) for any SDK-originated failure, or a specific
enum (`crates/core/src/errors.rs`). Catch the base class (`QuicknodeError` /
`QuicknodeSdk::Error` / `SdkError`) for any SDK-originated failure, or a specific
subclass to branch on transport vs. API semantics.

| Logical class | When it fires | Extra fields |
|----------------------|-------------------------------------------------------------|----------------------|
| `QuickNodeError` | base class; catches everything below | — |
| `QuicknodeError` | base class; catches everything below | — |
| `ConfigError` | invalid config or URL surfaced at construction time | — |
| `HttpError` | transport failure that isn't a timeout/connect | — |
| `TimeoutError` | request timed out (subclass of `HttpError`) | — |
Expand All @@ -3320,9 +3320,9 @@ subclass to branch on transport vs. API semantics.
Per-language names:

- **Rust** — pattern-match on `SdkError { Http, Api, Decode, UrlParse, Config }`; use `err.http_kind()` to classify `Http` into `Timeout`, `Connect`, or `Other`.
- **Python** — `QuickNodeError`, `ConfigError`, `HttpError`, `TimeoutError`, `ConnectionError`, `ApiError`, `DecodeError` (importable from `sdk`).
- **Python** — `QuicknodeError`, `ConfigError`, `HttpError`, `TimeoutError`, `ConnectionError`, `ApiError`, `DecodeError` (importable from `sdk`).
- **Node.js** — same class names, importable from `@quicknode/sdk`, all extend `Error`.
- **Ruby** — `QuickNodeSdk::Error`, `QuickNodeSdk::ConfigError`, `QuickNodeSdk::HttpError`, `QuickNodeSdk::TimeoutError`, `QuickNodeSdk::ConnectionError`, `QuickNodeSdk::ApiError`, `QuickNodeSdk::DecodeError`; all extend `StandardError`. Hash-key validation still raises `ArgumentError`.
- **Ruby** — `QuicknodeSdk::Error`, `QuicknodeSdk::ConfigError`, `QuicknodeSdk::HttpError`, `QuicknodeSdk::TimeoutError`, `QuicknodeSdk::ConnectionError`, `QuicknodeSdk::ApiError`, `QuicknodeSdk::DecodeError`; all extend `StandardError`. Hash-key validation still raises `ArgumentError`.

```rust
// Rust
Expand Down Expand Up @@ -3366,9 +3366,9 @@ try {
# Ruby
begin
qn.admin.show_endpoint(id: "missing")
rescue QuickNodeSdk::ApiError => e
rescue QuicknodeSdk::ApiError => e
warn "api #{e.status}: #{e.body}" if e.status == 404
rescue QuickNodeSdk::TimeoutError
rescue QuicknodeSdk::TimeoutError
warn "timed out"
end
```
Expand Down Expand Up @@ -3553,7 +3553,7 @@ The Python package is published to PyPI as `quicknode-sdk`. Wheels and the sdist
5. **Verify.**
```bash
pip install quicknode-sdk==0.1.0a6
python -c "import sdk; print(sdk.QuickNodeSdk)"
python -c "import sdk; print(sdk.QuicknodeSdk)"
```

**First publish:** the repo secret `PYPI_API_TOKEN` must be set. Project-scoped tokens only work after the project exists on PyPI, so the first upload needs a user-scoped token; rotate to a project-scoped token after.
Expand Down
4 changes: 2 additions & 2 deletions crates/core/examples/admin.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use quicknode_sdk::{admin::GetEndpointsRequest, QuickNodeSdk, SdkFullConfig};
use quicknode_sdk::{admin::GetEndpointsRequest, QuicknodeSdk, SdkFullConfig};

#[tokio::main]
#[allow(clippy::unwrap_used, clippy::expect_used)]
async fn main() {
let config = SdkFullConfig::from_env().expect("Config from env failed");
let qn = QuickNodeSdk::new(&config).expect("sdk failed to initialize");
let qn = QuicknodeSdk::new(&config).expect("sdk failed to initialize");

let params = GetEndpointsRequest::builder()
.limit(20)
Expand Down
6 changes: 3 additions & 3 deletions crates/core/examples/admin_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ use quicknode_sdk::{
UpdateTeamEndpointsRequest,
},
errors::SdkError,
AdminConfig, HttpConfig, QuickNodeSdk, SdkFullConfig,
AdminConfig, HttpConfig, QuicknodeSdk, SdkFullConfig,
};

#[tokio::main]
#[allow(clippy::unwrap_used, clippy::expect_used)]
async fn main() {
let config = SdkFullConfig::from_env().expect("Config from env failed");
let qn = QuickNodeSdk::new(&config).expect("sdk failed to initialize");
let qn = QuicknodeSdk::new(&config).expect("sdk failed to initialize");

let run_suffix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
Expand Down Expand Up @@ -821,7 +821,7 @@ async fn main() {
webhooks: None,
kvstore: None,
};
let tiny = QuickNodeSdk::new(&blackhole).expect("build tiny sdk");
let tiny = QuicknodeSdk::new(&blackhole).expect("build tiny sdk");
match tiny
.admin
.get_endpoints(&GetEndpointsRequest::default())
Expand Down
4 changes: 2 additions & 2 deletions crates/core/examples/kvstore_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ use quicknode_sdk::{
kvstore::{
AddListItemParams, BulkSetsParams, CreateListParams, CreateSetParams, UpdateListParams,
},
QuickNodeSdk, SdkFullConfig,
QuicknodeSdk, SdkFullConfig,
};

#[tokio::main]
#[allow(clippy::unwrap_used, clippy::expect_used)]
async fn main() {
let config = SdkFullConfig::from_env().expect("Config from env failed");
let qn = QuickNodeSdk::new(&config).expect("sdk failed to initialize");
let qn = QuicknodeSdk::new(&config).expect("sdk failed to initialize");

// ── Sets ────────────────────────────────────────────────────────────────

Expand Down
4 changes: 2 additions & 2 deletions crates/core/examples/streams.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ use quicknode_sdk::{
CreateStreamParams, DestinationAttributes, StreamDataset, StreamMetadataLocation,
StreamRegion, StreamStatus, WebhookAttributes,
},
QuickNodeSdk, SdkFullConfig,
QuicknodeSdk, SdkFullConfig,
};

#[tokio::main]
#[allow(clippy::unwrap_used, clippy::expect_used)]
async fn main() {
let config = SdkFullConfig::from_env().expect("Config from env failed");
let qn = QuickNodeSdk::new(&config).expect("sdk failed to initialize");
let qn = QuicknodeSdk::new(&config).expect("sdk failed to initialize");

let params = CreateStreamParams::builder()
.name("My Stream".to_string())
Expand Down
4 changes: 2 additions & 2 deletions crates/core/examples/streams_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ use quicknode_sdk::{
StreamMetadataLocation, StreamRegion, StreamStatus, TestFilterParams, UpdateStreamParams,
WebhookAttributes,
},
QuickNodeSdk, SdkFullConfig,
QuicknodeSdk, SdkFullConfig,
};

#[tokio::main]
#[allow(clippy::unwrap_used, clippy::expect_used)]
async fn main() {
let config = SdkFullConfig::from_env().expect("Config from env failed");
let qn = QuickNodeSdk::new(&config).expect("sdk failed to initialize");
let qn = QuicknodeSdk::new(&config).expect("sdk failed to initialize");

let before = qn
.streams
Expand Down
4 changes: 2 additions & 2 deletions crates/core/examples/webhooks_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ use quicknode_sdk::{
GetWebhooksParams, TemplateArgs, UpdateWebhookParams, WebhookDestinationAttributes,
WebhookStartFrom,
},
QuickNodeSdk, SdkFullConfig,
QuicknodeSdk, SdkFullConfig,
};

#[tokio::main]
#[allow(clippy::unwrap_used, clippy::expect_used)]
async fn main() {
let config = SdkFullConfig::from_env().expect("Config from env failed");
let qn = QuickNodeSdk::new(&config).expect("sdk failed to initialize");
let qn = QuicknodeSdk::new(&config).expect("sdk failed to initialize");

let before = qn
.webhooks
Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/admin/chains.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ pub struct ChainNetwork {
pub chain_id: Option<i64>,
}

/// A blockchain supported by QuickNode along with its networks.
/// A blockchain supported by Quicknode along with its networks.
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/admin/endpoint_security.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ pub struct CreateIpRequest {
#[cfg_attr(feature = "rust", derive(Builder))]
#[derive(Debug, Clone, Default, Serialize)]
pub struct CreateDomainMaskRequest {
/// Custom domain that will mask the endpoint's QuickNode URL.
/// Custom domain that will mask the endpoint's Quicknode URL.
#[serde(skip_serializing_if = "Option::is_none")]
pub domain_mask: Option<String>,
}
Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/admin/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ pub struct Pagination {
pub struct Endpoint {
/// Unique endpoint identifier.
pub id: String,
/// QuickNode-assigned subdomain.
/// Quicknode-assigned subdomain.
pub name: String,
/// Human-readable label.
pub label: Option<String>,
Expand Down
Loading
Loading