Skip to content
Open
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
8 changes: 8 additions & 0 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ bon = { version = "3.9", optional = true }
config = "0.15"
url = "2.5.8"
secrecy = "0.8"
# sync-only: provides tokio::sync::Mutex for single-flight JWT refresh in the
# rpc client without pulling in the tokio runtime. The async runtime itself is
# supplied by the caller (bindings or the user's own runtime).
tokio = { version = "1", default-features = false, features = ["sync"] }

[[example]]
name = "admin"
Expand All @@ -55,6 +59,10 @@ required-features = ["rust"]
name = "webhooks_e2e"
required-features = ["rust"]

[[example]]
name = "rpc"
required-features = ["rust"]

[dev-dependencies]
tokio = { version = "1.0", features = ["rt-multi-thread", "macros"] }
wiremock = "0.6"
64 changes: 63 additions & 1 deletion crates/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1760,6 +1760,67 @@ let schema = qn.sql.get_schema("hyperliquid-core-mainnet").await?;
println!("{} tables", schema.tables.len());
```

---

### RPC & Tooling Access

Tooling Access provisions a single multichain, read-only endpoint per account and
mints short-lived session JWTs. `qn.rpc` makes JSON-RPC calls directly against that
endpoint, minting and refreshing the JWT automatically — no endpoint URL or token to
manage.

Tooling Access must be enabled once (admin role + eligible plan). The control-plane
methods live on `qn.admin`:

```rust
// Rust
let status = qn.admin.tooling_access_status().await?;
if !status.enabled {
qn.admin.enable_tooling_access().await?; // idempotent; admin role required
}

// call(method, params, network, endpoint_url). params is Option<serde_json::Value>;
// None defaults to []. Both trailing args are Option and independently omittable.
let block_number = qn.rpc.call("eth_blockNumber", None, None, None).await?;
let balance = qn
.rpc
.call(
"eth_getBalance",
Some(serde_json::json!(["0xabc...", "latest"])),
None,
None,
)
.await?;

// Multichain: seed the per-network URL map (from get_endpoint_urls), then pass
// the network key as the third arg.
let urls = qn.admin.get_endpoint_urls(endpoint_id).await?;
if let Some(data) = urls.data {
if let Some(mc) = data.multichain_urls {
qn.rpc
.set_networks(mc.into_iter().map(|(k, v)| (k, v.http_url)).collect());
}
}
let slot = qn.rpc.call("getSlot", None, Some("solana-mainnet".into()), None).await?;

// Custom endpoint URL: send to a fully-formed HTTP URL, bypassing Tooling Access
// and the JWT (no Authorization header). Per-call via the 4th arg, or client-wide
// via RpcConfig { endpoint_url, .. }. endpoint_url and network are mutually
// exclusive (a custom URL is not multichain-routed).
let block = qn
.rpc
.call("eth_blockNumber", None, None, Some("https://my-endpoint.example/rpc".into()))
.await?;

// A JSON-RPC error member is returned as SdkError::Rpc { code, message }.
```

A host that persists across processes can snapshot the cached token with
`qn.rpc.current_token()` and re-seed it via `RpcConfig { seed, .. }`;
`refresh_margin_secs` (default 60) tunes how early the token is refreshed. Set
`RpcConfig { endpoint_url, .. }` to route every call to a custom HTTP URL by
default (no JWT minted); a per-call `endpoint_url` overrides it.

## Error Handling

Every binding exposes a typed exception hierarchy derived from the core `SdkError`
Expand All @@ -1775,8 +1836,9 @@ subclass to branch on transport vs. API semantics.
| `ConnectionError` | connection refused / DNS / TLS (subclass of `HttpError`) | — |
| `ApiError` | non-2xx HTTP response | `status`, `body` |
| `DecodeError` | 2xx response but JSON parse failed | `body` |
| `RpcError` | JSON-RPC call returned an `error` member | `code`, `message` |

Variants: pattern-match on `SdkError { Http, Api, Decode, UrlParse, Config }`; use `err.http_kind()` to classify `Http` into `Timeout`, `Connect`, or `Other`.
Variants: pattern-match on `SdkError { Http, Api, Decode, UrlParse, Config, Rpc }`; use `err.http_kind()` to classify `Http` into `Timeout`, `Connect`, or `Other`.

```rust
// Rust
Expand Down
3 changes: 3 additions & 0 deletions crates/core/examples/admin_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,7 @@ async fn main() {
webhooks: None,
kvstore: None,
sql: None,
rpc: None,
};
let headered = QuicknodeSdk::new(&with_headers).expect("build sdk with custom headers");
match headered
Expand All @@ -900,6 +901,8 @@ async fn main() {
streams: None,
webhooks: None,
kvstore: None,
sql: None,
rpc: None,
};
let tiny = QuicknodeSdk::new(&blackhole).expect("build tiny sdk");
match tiny
Expand Down
83 changes: 83 additions & 0 deletions crates/core/examples/rpc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use quicknode_sdk::{errors::SdkError, 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");

// Ensure Tooling Access is provisioned (idempotent; requires admin role).
let status = qn
.admin
.tooling_access_status()
.await
.expect("tooling_access_status failed");
println!("tooling access enabled: {}", status.enabled);
if !status.enabled {
match qn.admin.enable_tooling_access().await {
Ok(s) => println!("enabled tooling access: {}", s.enabled),
Err(e) => {
eprintln!("could not enable tooling access: {e}");
return;
}
}
}

// Make a JSON-RPC call. The SDK mints and refreshes the session JWT.
match qn.rpc.call("eth_blockNumber", None, None, None).await {
Ok(result) => println!("eth_blockNumber => {result}"),
Err(e) => eprintln!("rpc call error: {e}"),
}

// Multichain: seed the per-network URL map from the endpoint id (returned by
// status), then route a call to a specific network by its key.
if let Some(id) = &status.endpoint_id {
if let Ok(urls) = qn.admin.get_endpoint_urls(id).await {
if let Some(mc) = urls.data.and_then(|d| d.multichain_urls) {
qn.rpc
.set_networks(mc.into_iter().map(|(k, v)| (k, v.http_url)).collect());
match qn
.rpc
.call("getSlot", None, Some("solana-mainnet".to_string()), None)
.await
{
Ok(result) => println!("solana getSlot => {result}"),
Err(e) => eprintln!("solana rpc error: {e}"),
}
}
}
}

// Demonstrate the typed JSON-RPC error path.
match qn
.rpc
.call(
"eth_getBalance",
Some(serde_json::json!(["not-an-address"])),
None,
None,
)
.await
{
Ok(result) => println!("unexpected ok: {result}"),
Err(SdkError::Rpc { code, message }) => {
println!("got expected RpcError: code={code} message={message}");
}
Err(e) => eprintln!("other error: {e}"),
}

// Custom endpoint URL: send a call to a fully-formed HTTP URL, bypassing the
// Tooling Access endpoint and the session JWT entirely. Useful for a
// provisioned `.quiknode.pro` URL or a self-hosted node. Set it per-call
// here, or client-wide via `RpcConfig::endpoint_url`.
if let Ok(custom_url) = std::env::var("QN_RPC_ENDPOINT_URL") {
match qn
.rpc
.call("eth_blockNumber", None, None, Some(custom_url))
.await
{
Ok(result) => println!("custom endpoint eth_blockNumber => {result}"),
Err(e) => eprintln!("custom endpoint rpc error: {e}"),
}
}
}
4 changes: 4 additions & 0 deletions crates/core/src/admin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub mod endpoints;
pub mod logs;
pub mod tags;
pub mod teams;
pub mod tooling_access;
pub mod usage;

pub use account::{AccountInfo, AccountInfoResponse, AccountSubscription};
Expand Down Expand Up @@ -66,6 +67,7 @@ pub use teams::{
TeamDetail, TeamEndpoint, TeamMessageData, TeamSummary, TeamUser, UpdateTeamEndpointsData,
UpdateTeamEndpointsRequest, UpdateTeamEndpointsResponse,
};
pub use tooling_access::ToolingAccessStatus;

pub use usage::{
ChainUsage, EndpointUsage, GetUsageByChainResponse, GetUsageByEndpointResponse,
Expand Down Expand Up @@ -1957,6 +1959,7 @@ mod tests {
webhooks: None,
kvstore: None,
sql: None,
rpc: None,
})
.unwrap()
}
Expand Down Expand Up @@ -3847,6 +3850,7 @@ mod tests {
webhooks: None,
kvstore: None,
sql: None,
rpc: None,
});
assert!(matches!(result, Err(crate::errors::SdkError::Config(_))));
}
Expand Down
Loading
Loading