For AI agents: the documentation index is at /llms.txt. Markdown versions of pages are available by appending .md to the URL.
Skip to main content

Solana Client

hypersync-client-solana is the Rust client for Solana HyperSync. It speaks the Arrow endpoint (POST /query/arrow), retries transient failures, waits out rate limits, and paginates a slot range across many concurrent requests for you.

Use 0.2.0 or newer

0.2.0 is the first release of the locked query API, and it also fixes a silent data-loss bug: on stream_arrow / collect_arrow, any chunk the server truncated (because it hit a row or time cap) dropped its tail instead of paginating it, losing up to 99% of rows on dense ranges. Nothing in the API surfaced the loss, so a stream on an older client looks healthy and is simply short. If you are streaming with any earlier version, upgrade before you trust the row counts.

Install

[dependencies]
hypersync-client-solana = "0.2"
tokio = { version = "1", features = ["full"] }

Quick start

use std::sync::Arc;

use hypersync_client_solana::{config::ClientConfig, Client};
use hypersync_solana_net_types::query::SolanaQuery;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = Arc::new(Client::new(ClientConfig {
url: "https://solana.hypersync.xyz".into(),
bearer_token: std::env::var("HYPERSYNC_BEARER_TOKEN").ok(),
..Default::default()
})?);

let height = client.get_height().await?;

let query = SolanaQuery {
from_slot: height.saturating_sub(100),
to_slot: Some(height),
include_all_blocks: true,
..Default::default()
};

let resp = client.get(&query).await?;
println!("{} blocks, next_slot {}", resp.blocks.len(), resp.next_slot);
Ok(())
}

ClientConfig fields: url, bearer_token, http_req_timeout (30s), max_num_retries (12), retry_base_ms (500), retry_ceiling_ms (5000), and proactive_rate_limit_sleep (true). See API tokens for the bearer token.

Typed rows or Arrow

Every method comes in two flavours: typed structs, or the raw Arrow record batches the server sent.

What you wantSingle queryWhole slot range
Typed structsgetcollect
Arrow record batchesget_arrowcollect_arrow

The typed structs live in hypersync_client_solana::simple_types: Block, Transaction, InstructionCall, Log, AccountActivity, Reward, bundled into a SolanaResponse with one Vec per table. Arrow responses instead carry data.tables, a map keyed by table name (blocks, transactions, instruction_calls, logs, account_activity, rewards).

Reach for Arrow when you are feeding a columnar pipeline (Polars, DataFusion, Parquet) or want to skip the per-row decode entirely; reach for the typed structs for ordinary application code.

Every field is Option<T>

field_selection can project any column away, so a None means exactly "not selected, or the source could not supply it" - never zero or false. Addresses, hashes, and signatures are the base58 newtypes Address, Hash, and Signature, which parse strictly and reject a malformed value loudly rather than matching nothing. InstructionCall::stack_height() is a convenience view over instruction_address (its length, matching Solana's native stack height).

Streaming a range

collect and collect_arrow fan a slot range out across concurrent requests and merge the results; stream_arrow gives you the same engine but yields each response as it arrives, in slot order, through an mpsc receiver.

use hypersync_client_solana::config::StreamConfig;

let resp = client
.collect(query, StreamConfig::default())
.await?;

StreamConfig for the Solana client:

OptionDefaultWhat it does
concurrency10Requests in flight. The main throughput knob, and the lever for making fewer requests per unit time.
batch_size1_000Slots per chunk before any response size has been measured.
min_batch_size100Lower clamp on the adaptive chunk size.
max_batch_size200_000Upper clamp on the adaptive chunk size.
response_bytes_ceiling500_000Responses above this shrink the next chunk.
response_bytes_floor250_000Responses below this grow the next chunk.

These are Solana-specific names: the EVM client's StreamConfig targets a single response_bytes_target instead of a floor/ceiling pair, so the tuning guide transfers as advice but not field for field.

Pagination and reorgs

A single get covers as much of the range as the server's budget allows, so use the response's next_slot as the next request's from_slot. collect and stream_arrow do this for you.

Responses can carry a rollback_guard describing the server's in-memory head window, so you can detect a shallow reorg before committing near-head data. It is absent when the server has no complete window to describe, and on a paginated collect it is the guard of the last page. See Reorg detection for the algorithm.

Rate limits

The client waits out rate limits and retries, so a stream slows down rather than failing. To read the quota yourself, the Solana client exposes the same surface as the EVM client: get_with_rate_limit / get_arrow_with_rate_limit, rate_limit_info(), wait_for_rate_limit(), and the proactive_rate_limit_sleep config field. See Inspecting rate limits from your code for the fields, the header mapping, and the one behavioural difference from the EVM client (the Solana *_with_rate_limit methods retry a 429; the EVM ones do not).

Node bindings

The repository also contains napi-rs Node bindings (node/), exposing SolanaClient with getHeight(), query(), getWithRateLimit(), rateLimitInfo(), and waitForRateLimit(). They are not published to npm yet, so build them from source (repo); the streaming methods are Rust only for now.

Two things to know if you are using them:

  • The query object is camelCase (fromSlot, instructionCalls, executingAccount, fieldSelection), but fieldSelection values are the snake_case column names from Available fields, for example { instructionCall: ["executing_account", "tx_success"] }.
  • response.tables is keyed by table name, so instruction rows are under instruction_calls.
  • includeAccountActivity was removed and now throws with guidance: use accountActivity: [{}].

Upgrading to 0.2.0

0.2.0 locked the query API, and the renames are breaking on the response side while the request side still accepts the legacy names as aliases. The full mapping, including is_committed to tx_success and the account_activity.owner split into pre_owner / post_owner, is in Renamed fields and compatibility.