Rust SDK
Async, tonic-based clients for the Flint perps API, shipped as three crates that release together with the rest of the Flint SDK suite:
| Crate | Library | What it covers |
|---|---|---|
flint-margin-user-client | flint_margin_user_client | Market and account services, plus owner-authenticated notifications. |
flint-margin-maker-client | flint_margin_maker_client | flint.margin.maker.v1 — a managed Client with sessions, node discovery, reconnecting streams, and chain state; a quote builder with landing receipts; every maker service. Re-exports the user client as public_api. |
flint-margin-onchain | flint_margin_onchain | Instruction builders, account decoders, PDAs, and error codes for the margin program. Re-exported by both clients as onchain. See On-chain clients. |
All three are edition 2024 with an MSRV of Rust 1.89, and share
transport, auth, and version-stamping code through
flint-client-core, which Cargo pulls in for you.
Install
From the Kellnr registry:
[dependencies]
flint-margin-user-client = { version = "=7.0.4", registry = "kellnr" }
flint-margin-maker-client = { version = "=7.0.4", registry = "kellnr" }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }Add only the crate you need — a read-only frontend or analytics job
uses flint-margin-user-client. Each client crate pins its
flint-client-core and flint-margin-onchain — and the maker crate
its flint-margin-user-client — to the exact same version, so you
never mix layouts. Add features = ["rpc"] to the maker crate for
RPC and QUIC fan-out (Cargo features).
The registry is the same one the spot SDK uses. Point Cargo at it in
~/.cargo/config.toml:
[registries.kellnr]
credential-provider = "cargo:token"
index = "sparse+https://kellnr.xantasoft.com/api/v1/crates/"then store your read-only registry token with
cargo login --registry kellnr, or export
CARGO_REGISTRIES_KELLNR_TOKEN=<token> in CI. When the operator enables SDK
credentials, the Developer tab of the maker dashboard and MakerService.GetSdkCredentials return a read-only registry token
and index URL for a trader linked to an organization. Otherwise, obtain package
access from the operator. Use the dashboard URL for your network.
From git
Each release is also tagged in the SDK repository (rust-v<version>),
and the crates build straight from a checkout — build.rs falls back
to the flint-protos submodule when no vendored protos are present.
The repository is private, so Cargo needs GitHub credentials that can
read it (for example net.git-fetch-with-cli = true plus your own git
auth).
[dependencies]
flint-margin-user-client = { git = "https://github.com/superis-labs/flint-api-client", tag = "rust-v7.0.4" }Public data — flint-margin-user-client
Client owns one tonic channel to the public host and hands out a
generated client per service. Every call stamps the SDK identity
headers; the public data calls below need no credentials.
use std::str::FromStr;
use flint_margin_user_client::proto::{
GetAccountRequest, ListPerpMarketsRequest, SubscribeOrderBookRequest, TraderId,
};
use flint_margin_user_client::{Client, DEVNET_PUBLIC_ENDPOINT};
use rust_decimal::Decimal;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// This example selects devnet explicitly; use the operator's URL for another network.
let client = Client::connect(DEVNET_PUBLIC_ENDPOINT).await?;
let markets = client
.perp_market()
.list_perp_markets(ListPerpMarketsRequest {
perp_ids: vec![],
statuses: vec![],
page_size: 0,
page_token: String::new(),
})
.await?
.into_inner()
.markets;
for market in &markets {
println!("{} tick={:?}", market.symbol, market.tick_size);
}
let account = client
.account()
.get_account(GetAccountRequest {
trader_id: Some(TraderId { id: 42 }),
})
.await?
.into_inner()
.account;
if let Some(health) = account.and_then(|a| a.health) {
let equity = Decimal::from_str(&health.net_equity.unwrap_or_default().value)?;
println!("net equity {equity} USD");
}
// Server stream: a snapshot, then sequenced deltas.
let mut book = client
.market_data()
.subscribe_order_book(SubscribeOrderBookRequest {
perp_id: markets[0].perp_id.clone(),
depth: 20,
})
.await?
.into_inner();
while let Some(update) = book.message().await? {
println!("snapshot={} seq={}", update.snapshot, update.sequence);
}
Ok(())
}Client exposes market(), account(), risk(), sponsorship(),
funding(), perp_market(), market_data(), and order(). It is
cheap to clone. For full control, build any generated client in
flint_margin_user_client::proto (for example
proto::account_service_client::AccountServiceClient) over your own
tonic::transport::Channel.
Account notifications
Client::notifications() exposes owner-only email settings. Each request needs an
authorization: Bearer <margin-session> metadata header, and the channel must
connect to the same server endpoint that minted the session. You can also pair
the generated proto::notifications::notification_service_client::NotificationServiceClient
with a margin-maker AuthFlow::for_trader and its bearer interceptor:
use flint_margin_maker_client::auth::AuthFlow;
use flint_margin_user_client::proto::notifications::{
RequestEmailVerificationRequest, VerifyEmailRequest,
notification_service_client::NotificationServiceClient,
};
let auth = AuthFlow::for_trader(channel.clone(), owner_signer, trader_id);
auth.token().await?;
let mut notifications = NotificationServiceClient::with_interceptor(channel, auth.interceptor());
let settings = notifications.get_settings(()).await?.into_inner();
let challenge = notifications.request_email_verification(RequestEmailVerificationRequest {
email: email.to_owned(),
}).await?.into_inner();
// After the user enters the code sent to that address:
auth.token().await?;
notifications.verify_email(VerifyEmailRequest {
challenge_id: challenge.challenge_id,
code: code.to_owned(),
}).await?;Refresh the session with auth.token().await? before subsequent calls; the Rust
interceptor cannot refresh it by itself. Delegates cannot access notifications.
Requesting a code leaves the current verified destination unchanged. Verifying
the single-use code enables email for new fills without a historical backlog.
disable_email(()) disables email and cancels pending verification. Public fill
history and client-local read/dismiss state remain independent of email delivery.
See the TypeScript notification guide
for settings and verification response fields.
Reading the chain
flint_margin_onchain (re-exported as onchain by both clients) adds
readers for the market's quoting and order-book accounts and for whole
transactions:
| Module | Helpers |
|---|---|
quoting | EncodedPerpQuotingEntry::from(&QuotingEntry), decode_aggregated_quote_book(&entries, slot), decode_quote_entry_side(&entry, slot, QuoteSide::Bid), has_configured_quote_data |
resting | read_resting_orders(&book), is_resting_live(&order, slot, unix_seconds), order_expiry |
transaction | decode_flint_transaction(&RpcJsonTransaction) and decode_flint_transaction_for(.., &program_id) |
error | error_name(code), flint_error(code), custom_error_code(&err) |
use flint_margin_user_client::onchain::accounts::{LinearPerpOrderBook, LinearPerpQuoting};
use flint_margin_user_client::onchain::quoting::{
EncodedPerpQuotingEntry, decode_aggregated_quote_book,
};
use flint_margin_user_client::onchain::resting::read_resting_orders;
let quoting = LinearPerpQuoting::parse_account(&rpc.get_account(&LinearPerpQuoting::find_pda(perp_id).0).await?)?;
let entries: Vec<EncodedPerpQuotingEntry> =
quoting.live_entries()?.iter().map(EncodedPerpQuotingEntry::from).collect();
let book = decode_aggregated_quote_book(&entries, slot);
let order_book = LinearPerpOrderBook::parse_account(&rpc.get_account(&LinearPerpOrderBook::find_pda(perp_id).0).await?)?;
let resting = read_resting_orders(&order_book)?;decode_flint_transaction takes a getTransaction response requested with
"encoding": "json", deserialized into RpcJsonTransaction, and returns
every Flint instruction (inner ones included) with the events it emitted.
complete is false when an instruction carried bytes this client does not
know.
use flint_margin_user_client::onchain::error::{custom_error_code, error_name};
use flint_margin_user_client::onchain::transaction::{RpcJsonTransaction, decode_flint_transaction};
let response: RpcJsonTransaction = serde_json::from_value(raw)?;
let decoded = decode_flint_transaction(&response)?;
for top in &decoded.instructions {
if let Some(flint) = &top.flint {
println!("{} {}", top.index, flint.name());
}
}
if let Some((_, code)) = response.meta.as_ref().and_then(|m| m.err.as_ref()).and_then(custom_error_code) {
println!("{:?}", error_name(code));
}Preparing perpetual orders
onchain::perp_order builds place_perp_order with the crossing makers'
trader accounts attached in ascending ID order — the same rules, rounding,
and messages as the TypeScript buildPerpOrderInstruction:
build_perp_order_instruction(&BuildPerpOrderInput, &PerpBookSnapshot)— market orders throughreference_price_ticksplusmax_slippage_bps, limit orders throughlimit_price; GTC, GTD, IOC, and FOK; post-only, reduce-only, minimum fill, and delegates.build_perp_close_instruction(&BuildPerpCloseInput, &PerpBookSnapshot)— rounds an integer fraction of the position down to lots and forces reduce-only FOK.select_crossing_makersandPerpLiquidityError(NoLiquidity,OutsidePriceLimit).- Grid math:
exact_grid_units,market_limit_ticks,base_for_lots,fractional_close_size_base,reverse_size_base.
With the user client's rpc feature, prepare_perp_order reads the
quoting account, order book, and Clock sysvar in one processed
getMultipleAccounts and builds the instruction, retrying while maker
quotes are unavailable. It accepts a solana_rpc_client nonblocking
RpcClient or anything implementing PerpPreparationRpc. It does not sign
or send.
flint-margin-user-client = { version = "=8.1.0", registry = "kellnr", features = ["rpc"] }use flint_margin_user_client::onchain::perp_order::{BuildPerpOrderInput, OrderTimeInForce};
use flint_margin_user_client::onchain::resting::PerpSide;
use flint_margin_user_client::{PerpPreparationOptions, prepare_perp_order};
let mut input = BuildPerpOrderInput::new(
payer.pubkey(),
trader_id,
perp_id,
PerpSide::Buy,
"0.003",
"0.001",
"0.001",
);
input.reference_price_ticks = Some(mark_ticks.to_string());
input.max_slippage_bps = Some(100);
input.time_in_force = Some(OrderTimeInForce::Ioc);
let prepared = prepare_perp_order(&input, &rpc, PerpPreparationOptions::default()).await?;
println!("{} lots, limit {}, makers {:?}", prepared.lots, prepared.limit_price_ticks, prepared.maker_trader_ids);
let instruction = prepared.order.instruction;PerpPreparationOptions defaults to 16 attempts, 300 ms between retries, a
5 s request timeout, and a 16-slot quote expiry buffer;
on_waiting_for_quotes fires once when it starts retrying. Live quotes
outside the bound fail immediately with
PerpPreparationError::Order(PerpOrderError::Liquidity(..)); the SDK never
widens the bound. prepared.slot is the RPC read context.
rust/margin-user/examples/perps_e2e.rs places, modifies, cancels, and
closes orders and settles funding and PnL on devnet. Run it with RPC_URL,
KEYPAIR (the owner's keypair file) and TRADER_ID set:
cargo run -p flint-margin-user-client --features rpc --example perps_e2e.
Maker client — flint-margin-maker-client
Authentication
The maker API authenticates a Solana key that owns a trader account
or is an active TRADE- or QUOTE-scoped delegate on one. AuthFlow
runs Challenge → sign → Authenticate, signing
b"FLINT-MARGIN-AUTH-V1:" || nonce, caches the bearer token, and
shares one in-flight handshake across concurrent callers.
Client combines an AuthFlow, all maker services and a public client.
It manages token refresh, session heartbeats and stream reconnection.
Configure it with the authenticated maker endpoint; TxService is available
only there.
use std::sync::Arc;
use flint_margin_maker_client::Client;
use flint_margin_maker_client::proto::{GetQuotingStateRequest, SubscribeFillsRequest};
use solana_sdk::signer::keypair::read_keypair_file;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let keypair = read_keypair_file("/path/to/maker.json").map_err(|e| anyhow::anyhow!("{e}"))?;
// This example selects devnet explicitly; use the operator's URLs for another network.
let client = Client::builder()
.devnet()
.wallet(Arc::new(keypair))
.build()
.await?;
// Mints a session, then refreshes it and answers the session
// heartbeat in the background until `shutdown()`.
let session = client.authenticate().await?;
println!("trader {} (controls {:?})", session.trader_id, session.trader_ids);
let state = client
.maker()
.get_quoting_state(GetQuotingStateRequest { perp_ids: vec![] })
.await?
.into_inner();
println!("{:?}", state.entries);
// Reconnecting stream: keep the handle alive while you read.
let fills = client.subscribe_maker_fills(SubscribeFillsRequest { perp_ids: vec![] });
let mut events = fills.subscribe().await;
while let Ok(fill) = events.recv().await {
println!("{} {:?} @ {:?}", fill.signature, fill.size, fill.price);
}
client.shutdown().await;
Ok(())
}- A key that controls several trader accounts gets the lowest id by
default. Use
.trader_id(id)on the builder,Client::connect_for_trader(endpoint, signer, trader_id), orAuthFlow::for_traderto pick one; the session'strader_idslists them all. Client::connect(endpoint, signer)is shorthand for the builder with defaults.WalletisArc<dyn Signer + Send + Sync>, so anysolana_sdk::signer::Signer— keypair, hardware wallet, remote signer — can implement that interface.client.auth()returns theAuthFlowfortoken(),refresh(),revoke(), orinterceptor(). The interceptor only attaches the cached token;authenticate()starts the refresh loop that keeps it fresh.shutdown()stops the refresh, heartbeat, discovery, and chain-tip tasks without revoking the session. They also stop when the last clone of theClientdrops.- There is no maker balance RPC: read collateral and health with
client.account().get_account(...)orclient.subscribe_account(...).
To configure a generated service client directly:
use std::sync::Arc;
use flint_margin_maker_client::auth::AuthFlow;
use flint_margin_maker_client::proto::maker_service_client::MakerServiceClient;
use flint_margin_maker_client::DEVNET_MAKER_ENDPOINT;
let channel = tonic::transport::Channel::from_static(DEVNET_MAKER_ENDPOINT)
.connect()
.await?;
let auth = AuthFlow::new(channel.clone(), Arc::new(keypair));
auth.token().await?;
let _refresh = auth.spawn_refresh_loop();
let mut maker = MakerServiceClient::with_interceptor(channel, auth.interceptor());Channel::from_static doesn't configure TLS or keep-alives; prefer
the Client, which does.
Client options
| Builder method | Default |
|---|---|
.endpoint(url), .devnet(), .mainnet() | Mainnet authed host. |
.public_endpoint(url) | The devnet public host after .devnet(), otherwise mainnet's. |
.channel(ch), .public_channel(ch) | Supply your own channels; used unchanged. |
.wallet(wallet) | Required. |
.trader_id(id) | The lowest trader id the key controls. |
.auth_skew(duration) | Refresh 30 s before the session expires. |
.auto_reconnect(bool) | On: streams and the heartbeat reopen after failures. |
.auto_session_heartbeat(bool) | On: authenticate() starts the SessionService heartbeat. |
.endpoint_discovery(bool), .discovery_interval(duration) | On for hostnames, every 10 s. See Node discovery. |
.log_messages(bool), .message_log_limit(chars) | Off. See Logging. |
client.public() is a full flint_margin_user_client::Client on the
public host; market_data(), perp_market(), and account() are
shortcuts. client.node() reaches NodeService, and
client.escalation() pages Flint's on-call team when your trader
belongs to an organization:
use flint_margin_maker_client::proto::PageTheTeamRequest;
let page = client
.escalation()
.page_the_team(PageTheTeamRequest { message: "quotes failing since slot 412000000".into() })
.await?
.into_inner();
println!("paged: {}", page.page_id);Reconnecting streams
The subscribe_* methods return an Arc<ResilientStream<T>>. Call
subscribe().await for a tokio::sync::broadcast::Receiver; the RPC
opens on the first subscriber, reopens on a 500 ms → 30 s backoff with
the original filters, and re-authenticates on UNAUTHENTICATED.
| Method | Stream |
|---|---|
subscribe_maker_market_snapshots, subscribe_maker_fills, subscribe_quotes | MakerService |
subscribe_tx_status, subscribe_blockhash, subscribe_slots, subscribe_leaders | TxService |
subscribe_spread_protect | SpreadProtectService |
subscribe_order_book, subscribe_trades, subscribe_candles, subscribe_market_stats, subscribe_account | Public, over the public channel |
Keep the handle alive while you read the receiver. state() and
current_state() report Connecting, Connected, or
Disconnected; disconnects() counts drops, so a fast reconnect
can't hide a gap. Snapshot streams resend state on reconnect; fill
and quote streams don't replay what you missed — reconcile with the
matching unary RPC. shutdown() on a stream closes its receivers.
Session heartbeat and chain state
After authenticate(), the client holds SessionService.Session
open and answers each server ping, which is what puts your session in
ListQuoterSessions with its RTT and clock drift. last_heartbeat()
returns the latest ping answered.
spawn_chain_tip()? starts the blockhash, slot, and leader streams
once per client and caches them in server_state():
get_blockhash(), blockhash_age(), get_current_slot(),
get_last_valid_block_height(), the priority-fee recommendation in
recommended_cu_price, and wait_ready() to block until all three
have arrived. A disconnected feed clears its value rather than going
stale.
Organization API key and external submission
ClientBuilder::api_key(key) plus trader_id(perp_maker_id) builds a
client with no wallet: the key is sent as x-api-key, and margin
MakerService resolves it to the organization's perps trader. TxService,
SessionService, and SpreadProtectService need a keypair session, so such
a client can't authenticate() or spawn_chain_tip(). Publish the chain tip
yourself with server_state().publish_blockhash(..) and publish_slot(..).
To quote without TxService, start the core with
QuotingCore::start_with_signer(client, config, signer) and deliver through
QuoteBuilder::commit_externally(&mut core, &submitter). A
QuotingSigner::Local(wallet) core hands your submitter signed transactions,
and a QuotingSigner::External(pubkey) core hands it unsigned ones to sign.
Sign tx.message and send tx.with_signature(signature)?, which verifies
the signature against the fee payer and returns the wire bytes. Either way
the submitter returns the signature, or Signature::default() when signing
happens later elsewhere: that delivery is not retried or reported as failed,
and its intents stay unknown in the receipt. Only Err is retried. Report outcomes through
core.tx_outcome_publisher(); see the spot
auth guide for the
full model.
Market catalog and units
refresh_config().await? reads the margin program id from
NodeService and pages through every perp market, then caches the
result in config_cache(). Each market carries exact MarketUnits
for converting decimals to the on-chain grid:
use std::str::FromStr;
use rust_decimal::Decimal;
let config = client.refresh_config().await?;
let market = config.catalog.perp(perp_id)?; // or .market("SOL-USDC-PERP")?
let bid_ticks = market.units.bid_ticks(Decimal::from_str("150.25")?)?; // rounds down
let ask_ticks = market.units.ask_ticks(Decimal::from_str("150.35")?)?; // rounds up
let lots = market.units.lots(Decimal::from_str("2.5")?)?; // rounds downQuoting
QuotingCore::start(client, config) checks that the node serves the
program in QuotingConfig::program_id, authenticates, starts the
chain-tip streams, loads your quoting entries, and follows the
spread-protection feed. QuoteBuilder then builds density,
relative-density, and order-list updates in ticks and lots, allocates
sequence numbers above what the chain has seen, signs, and submits:
use std::time::Duration;
use flint_margin_maker_client::onchain::types::DensityFair;
use flint_margin_maker_client::quoting::{QuoteBuilder, QuotingConfig, QuotingCore};
let mut core = QuotingCore::start(client, QuotingConfig::default()).await?;
let slot = core.client().server_state().get_current_slot().unwrap_or_default();
// Needs a density entry with configured bands on perp 1.
let mut result = QuoteBuilder::new()
.density_fair(1, DensityFair { slot, bid_fair: 150_000, ask_fair: 150_010 })
.commit(&mut core)
.await?;
for failed in &result.failed {
eprintln!("{}: {}", failed.transaction.signatures[0], failed.error);
}
let outcome = result.receipt.settled_within(Duration::from_secs(10)).await?;
if !outcome.is_complete() {
// Reconcile outcome.failed and outcome.unknown before rebuilding.
}QuotingConfigdefaults: the devnet programFLINT_ID, 200,000 compute units per transaction, the server's priority-fee recommendation, a 10 s readiness timeout, a 15 s maximum blockhash age, and v1 transactions (tx_version; setTxVersion::V0withlookup_tables, orTxVersion::Legacy). Setdelegate_trader_accountwhen the wallet isn't the owner.- The builder's standalone
UpdateQuotinginstructions need the owner or aTRADEdelegate. AQUOTEdelegate can authenticate but must build the batch instructions itself. - Updates to one market stay in one transaction; a group too large for one packet (4,096 bytes as v1, 1,232 as legacy or v0) is rejected rather than split.
commitretries transport failures with the same signed bytes (commit_with_retrytakes aRetryConfig; default 3 attempts, 100 ms apart). Rejected submissions land inresult.failed.- Receipts report processed observations, not finality.
settled_withingroups intent indices intolanded,failed, andunknown; it returnsCommitError::Timeout,Lagged, orDisconnectedwhen it can't tell, andreceipt.outcome()keeps the partial picture. A transport error or timeout leaves execution uncertain — reconcile the signature before rebuilding. - Call
refresh_quoting_state()after admission or strategy changes. core.spread_mult()is the highest spread-protection multiplier active at the current slot andspread_protect_window()the window behind it. Neither touches your quotes; apply them yourself.
With the rpc feature, with_rpcs(...) also broadcasts each signed
transaction through your own Solana RPCs, and with_quic(...) sends it
straight to the next two leaders' TPU over QUIC, using the
SubscribeLeaders schedule. Both are best effort: a QUIC write is not
treated as acceptance.
use std::sync::Arc;
use flint_margin_maker_client::quoting::quic::QuicTxClient;
let core = core.with_quic(Arc::new(QuicTxClient::new()));Submitting transactions
To submit transactions you build yourself, sign them and hand the
wire bytes to TxService.SubmitTx — or to QuotingCore::submit,
which returns the same receipts. The program's instruction builders
are in flint_margin_maker_client::onchain. The session's key must be
one of the transaction's required signers.
use flint_margin_maker_client::proto::SubmitTxRequest;
let ack = client
.tx()
.submit_tx(SubmitTxRequest {
transaction: tx_bytes, // bincode-serialized, fully signed VersionedTransaction
client_sent_at: None,
})
.await?
.into_inner();
println!("accepted {}", ack.signature);SubmitTx returns on acceptance. Watch SubscribeTxStatus for
landed or failed, and SubscribeBlockhash for a fresh blockhash
and priority-fee recommendation.
Decimal handling
Every price, size, balance, and rate is Decimal { value: String }.
Parse with rust_decimal —
never f64:
use rust_decimal::Decimal;
use std::str::FromStr;
let price = Decimal::from_str(&level.price.as_ref().unwrap().value)?;
let quantity = Decimal::from_str(&level.quantity.as_ref().unwrap().value)?;
let notional = price * quantity;Ids are plain integers: PerpId and SpotId wrap u32, TraderId
wraps u64. Timestamps are Timestamp { micros: u64 }.
Errors
Generated service calls return tonic::Status; branch on
status.code(). The SDK's own helpers return the crate's Error
(flint_client_core::CoreError): Config, Auth, Status, or
Transport. Quote receipts add quoting::CommitError for outcomes
that couldn't be observed. See Errors.
Transport defaults
Both clients build channels with a 5 s connect timeout, a 10 s
request timeout, HTTP/2 keep-alive pings every 30 s (including while
idle), and TCP keep-alive every 60 s. https:// endpoints use rustls
with the platform's native roots. The user client connects to the
endpoint you give it; the maker client probes and pins the fastest
healthy node behind each hostname — see
Node discovery.
Logging
The crates log through tracing:
channel connects and endpoint probes at debug; node switches and
refresh-loop, heartbeat, and stream failures at warn. Install a
subscriber to see them.
.log_messages(true) on the maker builder also logs the payloads of
SDK-managed RPCs and stream items at trace, truncated to
.message_log_limit(chars) (zero disables truncation). Auth,
credential, and signed-submission payloads are redacted and headers
are never logged. Calls you make on the generated clients directly
aren't covered.
Cargo features
| Crate | Feature | Default | What it adds |
|---|---|---|---|
flint-margin-user-client, flint-margin-maker-client | tls | yes | HTTPS via rustls + native roots. Disable only for plaintext local stacks. |
flint-margin-user-client | rpc | no | prepare_perp_order and the perp_preparation module. Pulls in the Solana RPC client. |
flint-margin-maker-client | rpc | no | QuotingCore::with_rpcs and with_quic, and the quoting::quic module. Pulls in the Solana RPC and QUIC clients. |
Version constants
Each client crate exports SDK_VERSION, PROTO_HASH, and
PROTO_REV — the values it stamps on requests as x-flint-sdk,
x-flint-proto-hash, and x-flint-proto-rev. Compare PROTO_HASH
with the server's x-flint-server-proto-hash response header when
debugging a schema mismatch.