Skip to content
solana_kit

Native#

solana_kit_native is the transport for programs that run on an operating system with Tokio: CLIs, servers, bots and desktop backends. It provides NativeHttp, a bounded HTTP/1 transport over Hyper with rustls TLS, and NativeWebSocket, a bounded text WebSocket. Each one owns its connection directly. There is no pool, background driver or ambient configuration. You supply the endpoint, headers, trust roots and deadlines.

Add the crate#

[dependencies]
solana_kit_client = { git = "https://github.com/pina-rs/solana_kit" }
solana_kit_native = { git = "https://github.com/pina-rs/solana_kit" }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

See Installation for how to pin a revision. The crate uses Tokio for sockets and timers, but it never creates a runtime. You start the runtime yourself.

Optional features:

FeatureAdds
keyskeys::NativeEntropy (OS randomness) and keys::write_keypair_new for explicit Solana-keygen JSON backups
mwamwa::NativeAssociation and mwa::NativeMobileWallet, a caller-owned Mobile Wallet Adapter session (see Wallets)
surfpoolsurfpool::deploy and process ownership for a local Surfpool validator (see Surfpool)

Make an RPC call#

NativeHttp::new validates the endpoint and configuration, then returns a Transport. Pass it to Client::new:

use solana_kit_client::{Client, Limits, methods::Balance};
use solana_kit_core::Address;
use solana_kit_native::{HeaderValue, HttpConfig, NativeHttp};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut config = HttpConfig::default();
    config.headers.insert(
        "authorization",
        HeaderValue::from_static("Bearer your-token"),
    );

    let transport = NativeHttp::new("https://rpc.invalid/", config)?;
    let client = Client::new(transport, Limits::default());

    let balance = client
        .call(Balance(Address::from_bytes([0; 32]), None))
        .await?;
    println!("{} lamports at slot {}", balance.value, balance.context.slot);
    Ok(())
}

Construction performs no network request. Each call opens its own connection, sends one POST and reads the complete reply.

The exchange future is Send. You can move a client into tokio::spawn on a multi-threaded runtime. The test suite runs a typed getBalance call this way and checks that u64::MAX survives intact.

Configure the transport#

HttpConfig holds every policy for one transport. Its fields are public:

FieldDefaultMeaning
headersemptyYour authentication and application headers. Framing, Host, content type and hop-by-hop headers are rejected.
connect_timeout10 sBounds DNS, TCP and the TLS handshake.
exchange_timeout30 sBounds the whole exchange, from the first poll to the last body byte.
max_header_bytes16,384Limit for the status line plus headers, and for chunk trailers.
max_headers64Limit on response header fields.
max_read_buffer16,384Hyper's read buffer. Must be at least 8,192 and cover the header limit.
tlsTlsRoots::WebPkiTrust roots for HTTPS.

The reply body limit comes from the client's request budget (Limits), not from HttpConfig. The transport checks it before each reserve and copy, whatever Content-Length says. Compressed bodies are not decompressed.

Trust roots#

TlsRoots has two variants:

  • TlsRoots::WebPki uses Mozilla's root set, pinned through webpki-roots. The OS trust store is never read.
  • TlsRoots::Certificates(Vec<Vec<u8>>) trusts only the DER-encoded CA certificates you supply.

Certificate chain and hostname verification are always on, and you cannot turn them off.

What the transport will not do#

It does not follow redirects, retry, use a proxy, keep cookies or look up credentials. A non-success status comes back unchanged so the client can decide what to do with it. URLs that contain userinfo or a fragment are rejected. Put credentials in headers.

Deadlines and cancellation#

The exchange_timeout deadline covers DNS, TCP, TLS, response headers and the whole body. Dropping the exchange future closes its socket, TLS session and HTTP driver.

One caveat: Tokio resolves hostnames on a blocking thread. The OS resolver can finish after you cancel. It can no longer send the HTTP request.

If you spawned the call, dropping the JoinHandle only detaches it. To cancel, abort the handle and then await it:

let handle = tokio::spawn(async move {
    client.call(Balance(Address::from_bytes([0; 32]), None)).await
});

handle.abort();
let _ = handle.await; // the peer now sees the connection close

WebSockets#

NativeWebSocket::connect(endpoint, WsConfig::default()).await opens one ws:// or wss:// socket that you own. It carries text frames. Pair it with an independent solana_kit_client::subscriptions::Connection, which handles subscription requests, correlation and typed notifications. The socket only moves frames.

This example is adapted from the loopback test tests/websocket_subscriptions.rs:

use std::time::{Duration, Instant};

use solana_kit_client::subscriptions::{Budget, Connection, Drain, Root, Status};
use solana_kit_native::{NativeWebSocket, WsConfig};

const GENERATION: u64 = 1;
// Acknowledgement timeout, in the milliseconds `now` counts.
const ACK_TIMEOUT: u64 = 5_000;

let started = Instant::now();
let now = || u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX);

let mut socket = NativeWebSocket::connect("wss://rpc.invalid/", WsConfig::default()).await?;
let mut connection = Connection::new(GENERATION, Budget::default());

let handle = connection.subscribe(Root, now() + ACK_TIMEOUT)?;

while let Some(frame) = connection.outbound() {
    socket.send(std::str::from_utf8(&frame)?).await?;
}

// Feed every received text frame back in, tagged with this socket's generation.
// `tick` enforces the subscribe deadline; `next` reports a missed one as an error.
loop {
    connection.tick(now(), now() + ACK_TIMEOUT)?;

    match connection.next(&handle)? {
        Drain::Notification(root) => {
            println!("new root: {root}");
            break;
        }
        Drain::Closed => break,
        Drain::Pending => {}
    }

    if let Ok(received) = tokio::time::timeout(Duration::from_millis(100), socket.receive()).await {
        let Some(text) = received? else { break };
        connection.ingest(GENERATION, text.as_bytes())?;
    }
}

// Unsubscribing only queues the request. Keep reading until the node
// acknowledges it or the deadline passes, then close the socket.
let deadline = now() + ACK_TIMEOUT;
connection.unsubscribe(&handle, deadline)?;

while let Some(frame) = connection.outbound() {
    socket.send(std::str::from_utf8(&frame)?).await?;
}

while connection.status(&handle)? == Status::Unsubscribing {
    connection.tick(now(), deadline)?;

    if let Ok(received) = tokio::time::timeout(Duration::from_millis(100), socket.receive()).await {
        let Some(text) = received? else { break };
        connection.ingest(GENERATION, text.as_bytes())?;
    }
}

// Reports a missed acknowledgement as an error.
connection.next(&handle)?;
connection.forget(handle)?;
socket.close().await?;

You own the clock: here now counts milliseconds since the connection opened, and connection.tick(now, cleanup_deadline) turns a missed acknowledgement into an error. If ingest fails for the current generation, drop the socket. Subscriptions covers the Connection contract in full.

WsConfig holds the socket's limits:

FieldDefaultMeaning
headersemptyAuthentication and application headers. Protocol and cookie headers are rejected.
connect_timeout10 sDNS, TCP, TLS and the upgrade handshake together.
operation_timeout30 sEach send, receive and the graceful close.
max_frame_bytes1 MiBLargest incoming frame and outgoing text.
max_message_bytes1 MiBLargest reassembled incoming message.
read_buffer_bytes8,192Read buffer. Must be positive and at most the frame limit.
max_write_buffer_bytes1 MiB plus frame headerEncoded bytes held for writing.
max_request_bytes16,384Endpoint plus header bytes.
tlsTlsRoots::WebPkiTrust roots for wss://.

Cancellation rules:

  • Cancelling receive() keeps the socket and its parser state, so you can call it again.
  • Cancelling send() can leave a partial write. Drop the socket afterward, and do not resend automatically.
  • close(self) consumes the socket and waits for the peer's close within one operation deadline.

There is no reconnect, replay, automatic ping timer or implicit unsubscribe. Use wss:// on untrusted networks. Accept endpoints only from trusted configuration, because there is no private-destination filter.

Status#

CapabilityStatus
NativeHttp HTTP/1 and TLSRuns in CI on Ubuntu, macOS and Windows against local fixtures, plus an MSRV (1.89) build
NativeWebSocket WS and WSS text, all nine subscription familiesRuns in CI on Ubuntu, macOS and Windows with Rust 1.99.1 and 1.89.0
NativeSubscriptionSocket (Session adapter, binary frames)Source extension; the crate README marks it as not yet executed
NativeRest (provider REST routes)Source extension; real REST peer replay is pending
HTTP/2, connection poolingNot provided

The receipts are native HTTP and native WebSocket. All tests use loopback fixtures and synthetic credentials. No public cluster is involved.

esc