Skip to content

Add RPC support with account-key isolation and custom endpoint URLs#51

Open
johnpmitsch wants to merge 4 commits into
mainfrom
rpc-support-key-isolation
Open

Add RPC support with account-key isolation and custom endpoint URLs#51
johnpmitsch wants to merge 4 commits into
mainfrom
rpc-support-key-isolation

Conversation

@johnpmitsch

@johnpmitsch johnpmitsch commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Adds RPC support to the SDK across all four languages (qn.rpc.call(...)), plus the admin tooling-access endpoints used to mint the short-lived session tokens RPC calls authenticate with.

How it works

RPC data-plane calls authenticate with an ephemeral Bearer JWT minted via the control-plane tooling-access/token endpoint. The client caches the token in memory, refreshes proactively before exp, and reactively retries once on a 401. Callers can seed a token from disk via RpcConfig.seed and route multichain calls via RpcConfig.networks.

Account-key isolation

The point of the ephemeral-token model is that the long-lived account key never reaches the provisioned RPC endpoint. RPC calls go through a dedicated HTTP client that omits the account x-api-key default header, so the data-plane request carries only the Bearer JWT (or nothing, for a custom URL). A test asserts x-api-key is absent on RPC requests.

Custom HTTP URL for RPC calls

endpoint_url points a call at a fully-formed HTTP URL instead of the Tooling Access endpoint. A custom URL is treated as self-authenticating: no token is minted and no Authorization header is attached. Two ways to set it:

  • RpcConfig.endpoint_url (client-wide default)
  • per-call endpoint_url argument (overrides the default)

endpoint_url and network are mutually exclusive (a custom URL is not multichain-routed) and passing both returns a Config error.

call signature and argument combinations

call(method, params?, network?, endpoint_url?) — the two trailing args are independently optional.

Rust

// Tooling endpoint, default network (mints/refreshes the JWT)
qn.rpc.call("eth_blockNumber", None, None, None).await?;

// With params
qn.rpc.call("eth_getBalance", Some(json!(["0xabc...", "latest"])), None, None).await?;

// Multichain: route by network key (seed the map first via set_networks)
qn.rpc.call("getSlot", None, Some("solana-mainnet".into()), None).await?;

// Custom URL per-call (no JWT, no Authorization header)
qn.rpc.call("eth_blockNumber", None, None, Some("https://my-endpoint.example/rpc".into())).await?;

// Custom URL as a client-wide default
let mut cfg = SdkFullConfig::from_api_key(key);
cfg.rpc = Some(RpcConfig { endpoint_url: Some("https://my-endpoint.example/rpc".into()), ..Default::default() });

Python

await qn.rpc.call("eth_blockNumber")                               # tooling, default network
await qn.rpc.call("eth_getBalance", ["0xabc...", "latest"])        # with params
await qn.rpc.call("getSlot", network="solana-mainnet")             # multichain
await qn.rpc.call("eth_blockNumber", endpoint_url="https://my-endpoint.example/rpc")  # custom URL
QuicknodeSdk(config=SdkFullConfig(api_key=key, rpc=RpcConfig(endpoint_url="https://my-endpoint.example/rpc")))  # client-wide

Node

await qn.rpc.call("eth_blockNumber");                                       // tooling, default network
await qn.rpc.call("eth_getBalance", ["0xabc...", "latest"]);                // with params
await qn.rpc.call("getSlot", [], "solana-mainnet");                         // multichain
await qn.rpc.call("eth_blockNumber", [], undefined, "https://my-endpoint.example/rpc");  // custom URL
new RpcConfig({ endpointUrl: "https://my-endpoint.example/rpc" });          // client-wide default

Ruby

qn.rpc.call(method: "eth_blockNumber")                                      # tooling, default network
qn.rpc.call(method: "eth_getBalance", params: ["0xabc...", "latest"])       # with params
qn.rpc.call(method: "getSlot", network: "solana-mainnet")                   # multichain
qn.rpc.call(method: "eth_blockNumber", endpoint_url: "https://my-endpoint.example/rpc")  # custom URL
# client-wide default: rpc: { endpoint_url: "https://my-endpoint.example/rpc" } in the SDK config

Passing both network and endpoint_url in any language raises/returns a Config error.

Also

  • CachedToken's Debug redacts the JWT (it's a live credential).
  • Dropped the unused RpcConfig.base_url field (token minting resolves against the admin base URL).

Verification

cargo test (242 pass, incl. new custom-URL tests), just lint, just python-build, just node-build, just ruby-build all green. Examples updated and syntax-checked in all four languages.

RPC calls now go through a dedicated HTTP client that does not carry the
account x-api-key default header, so the long-lived account key never
leaves the control plane. The data-plane request authenticates only with
the short-lived Bearer JWT, as the ephemeral-token design intends.

Also redact the JWT in CachedToken's Debug output and drop the unused
RpcConfig.base_url field (token minting resolves against the admin base
URL, not this field).
Adds an endpoint_url option so callers can point JSON-RPC calls at a
fully-formed HTTP URL instead of the provisioned Tooling Access endpoint.
A custom URL is treated as self-authenticating: no session token is
minted and no Authorization header is attached, only the JSON-RPC POST.

Available two ways: RpcConfig.endpoint_url sets a client-wide default,
and a per-call endpoint_url argument overrides it. A custom URL is not
multichain-routed, so passing both endpoint_url and network returns a
Config error. Wired through all four language bindings with examples and
README updates.
@johnpmitsch johnpmitsch changed the title Add RPC support with account-key isolation Add RPC support with account-key isolation and custom endpoint URLs Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant