sof-tx
sof-tx is the transaction SDK for SOF.
It provides:
- ergonomic transaction/message building
- signed and pre-signed submission APIs
- route-plan based submission:
SubmitPlan::rpc_only()SubmitPlan::jito_only()SubmitPlan::direct_only()SubmitPlan::ordered(...)SubmitPlan::hybrid()SubmitPlan::all_at_once(...)
- routing policy and signature-level dedupe
It works both as:
- a standalone transaction SDK for RPC-backed, Jito-backed, or signed-byte submit flows, and
- a lower-latency execution SDK when paired with
sofor another local control-plane source for direct or hybrid routing
Together with sof, this is intended for low-latency execution services that want locally
sourced control-plane state and predictable submit behavior, not just a generic wallet helper.
At a Glance
- Build
V0or legacy Solana transactions - Submit through RPC, Jito block engine, direct leader routing, or hybrid fallback
- Attach live
sofruntime adapters when you want local leader/blockhash signals - Use replayable derived-state adapters when your service must survive restart or replay
- Read control-plane snapshots from lower-contention adapter state instead of serializing all readers through one mutex
- Evaluate typed flow-safety policies before acting on local control-plane state
- Use optional kernel-bypass direct transports for custom low-latency networking
Install
Enable SOF runtime adapters when you want provider values from live sof plugin events:
= { = "0.17.3", = ["sof-adapters"] }
Enable kernel-bypass transport hooks for kernel-bypass direct submit integrations:
= { = "0.17.3", = ["kernel-bypass"] }
Use sof-solana-compat when you want the Solana-native TxBuilder plus unsigned convenience
submission helpers on top of sof-tx:
= "0.17.3"
Quick Start
Start with the simplest unsigned-submit path:
use ;
use ;
use Keypair;
use Signer;
use instruction as system_instruction;
async
Pick the setup that matches what you need:
use Arc;
use ;
let mut rpc_client = builder
.with_rpc_defaults?
.build;
let mut jito_client = builder
.with_jito_defaults?
.build;
let mut signed_only_client = builder
.with_rpc_transport
.build;
with_rpc_defaults(...): use one RPC URL for unsigned RPC-route submission.with_jito_defaults(...): use one RPC URL for blockhash plus Jito transport for Jito-route submission.with_rpc_transport(...): enough forsubmit_signed_via(...)because that path does not build the transaction inside the client and does not need a blockhash provider.
The builder gives you the product-level paths first. Drop down to the provider APIs only when you need custom control-plane wiring.
For most services, the practical order is:
- start with
SubmitPlan::rpc_only(),SubmitPlan::jito_only(), orsubmit_signed_via(...) - add direct or hybrid only when you have a trustworthy local routing source and a measured reason to use it
Core Types
TxSubmitClientBuilder: configure common RPC, Jito, direct, and control-plane paths without wiring providers by hand first.sof_solana_compat::TxBuilder: compose transaction instructions and signing inputs.TxSubmitClient: submit through one or more configured routes.SubmitPlan: primary route-plan API.SubmitRoute: one concrete route (Rpc,Jito,Direct).SubmitStrategy: ordered fallback or all-at-once execution.SubmitMode: legacy preset compatibility surface.SignedTx: submit externally signed transaction bytes.RoutingPolicy: leader/backup fanout controls.LeaderProviderandRecentBlockhashProvider: provider boundaries.TxMessageVersion: selectV0(default) orLegacymessage output.MAX_TRANSACTION_WIRE_BYTES: current max over-the-wire transaction bytes.MAX_TRANSACTION_ACCOUNT_LOCKS: current max account locks per transaction.
Message Version and Limits
TxBuilder emits V0 messages by default without requiring address lookup tables.
Legacy output remains available through with_legacy_message().
Current protocol-aligned limits exposed by sof-tx:
MAX_TRANSACTION_WIRE_BYTES = 1232MAX_TRANSACTION_ACCOUNT_LOCKS = 128
SOF Adapter Layer
With sof-adapters enabled, PluginHostTxProviderAdapter can be:
- registered as a SOF plugin to ingest blockhash/leader/topology events, and
- passed directly into
TxSubmitClientas both providers. - evaluated with
evaluate_flow_safety(...)before using its control-plane state for direct send decisions.
See the full example:
crates/sof-solana-compat/examples/submit_all_at_once_with_sof.rs
That example shows:
PluginHostTxProviderAdapterwired into aPluginHost- RPC blockhash refresh plus SOF-backed leader routing
- direct, RPC, and Jito transports configured together
SubmitPlan::all_at_once(vec![Direct, Rpc, Jito])
For restart-safe services built on SOF derived-state, use DerivedStateTxProviderAdapter instead.
It consumes the replayable derived-state feed, supports checkpoint persistence, and exposes the
same evaluate_flow_safety(...) helper for control-plane freshness checks.
Those SOF adapter paths are complete today with raw-shred or gossip-backed observer runtimes. Built-in processed provider adapters such as Yellowstone, LaserStream, and websocket are transaction-first today:
- they can drive recent blockhash state through observed provider transactions
- they do not, by themselves, provide the full
sof-txcontrol-plane feed for direct routing - direct submit still needs leaders/topology from gossip, manual target injection, or another control-plane source
So a mixed setup is already valid:
- provider-stream transactions for recent blockhash freshness
- gossip full or
control_plane_onlyfor cluster topology PluginHostTxProviderAdapter::topology_only(...)when that mixed setup is topology-backed but does not also emit leader-schedule hooks- one shared SOF adapter feeding both into
sof-txin a custom embedding where your host/runtime composition supplies both surfaces together
The packaged observer runtime now supports one honest mixed built-in shape:
- built-in websocket / Yellowstone / LaserStream transaction ingress
- gossip bootstrap for cluster topology in the same runtime
That mixed packaged mode still does not synthesize leader-schedule or reorg hooks. So:
- use
PluginHostTxProviderAdapter::default()when SOF emits recent blockhash, topology, and leader schedule - use
PluginHostTxProviderAdapter::topology_only(...)when SOF emits recent blockhash plus topology, but not leader schedule - use
ProviderStreamMode::Genericwhen your custom producer supplies the full control-plane feed
The observer-side feed now also emits canonical control-plane quality snapshots, so services can
source freshness and confidence metadata from sof first and keep sof-tx focused on send-time
guard decisions.
For services that do not want to maintain a parallel checkpoint file format, use the adapter
persistence helper backed by SOF's generic DerivedStateCheckpointStore.
Direct submit needs TPU endpoints for scheduled leaders. That requirement applies to any submit
plan that includes SubmitRoute::Direct. The adapter gets those from on_cluster_topology
events, or you can inject them manually with:
set_leader_tpu_addr(pubkey, tpu_addr)remove_leader_tpu_addr(pubkey)
The flow-safety report is intended to keep stale or degraded control-plane state from silently driving direct or hybrid sends. Typical checks include:
- missing recent blockhash
- stale tip slot
- missing leader schedule when that input is enabled
- missing TPU addresses for targeted leaders
- degraded topology freshness
Simplifying OpenBook + CPMM Flows
The recommended pattern is to keep strategy-specific instruction creation separate, and route both through one shared SDK pipeline.
use ;
use ;
use Instruction;
use Keypair;
use Signer;
async
This gives one consistent path for signing, dedupe, routing, and fallback behavior.
Submitting Pre-Signed Transactions
If your signer is external (wallet/HSM/offline), submit bytes directly. This path does not need a
blockhash provider inside TxSubmitClient because the transaction is already signed before submit:
use ;
async
Developer Tip
TxBuilder::tip_developer() adds a developer-support transfer instruction using a default tip of 5000 lamports.
- default amount:
5000lamports (DEFAULT_DEVELOPER_TIP_LAMPORTS) - default recipient:
G3WHMVjx7Cb3MFhBAHe52zw8yhbHodWnas5gYLceaqze - custom amount:
tip_developer_lamports(...) - custom recipient + amount:
tip_to(...)
Thanks in advance for supporting continued SDK development.
Submit Plans
Use SubmitPlan as the primary API:
SubmitPlan::rpc_only(): maximum compatibility.SubmitPlan::jito_only(): Jito block-engine only when your strategy already includes the right fee/tip shape for that path.SubmitPlan::direct_only(): lowest path latency when leader targets are reliable.SubmitPlan::ordered(vec![...]): custom ordered-fallback route plan.SubmitPlan::hybrid(): practical default for latency plus RPC fallback resilience.SubmitPlan::all_at_once(vec![...]): preferred multi-route shape when you want to maximize the chance that one of several configured routes accepts the same transaction quickly, without depending on any single route's transient latency or availability. The submit call returns on the first accepted route; later background accepts are reported throughTxSubmitOutcomeReporter, and built-in telemetry counts those accepts without mutating the returnedSubmitResult. The reporter path is asynchronous and best-effort through one bounded FIFO dispatcher per reporter instance, shared across clients that use that same reporter, so it stays off the submit hot path while preserving callback order for queued outcomes. If that reporter path drops or cannot deliver outcomes, the built-in telemetry snapshot surfaces it throughreporter_outcomes_droppedandreporter_outcomes_unavailable.
Arbitrary plans are first-class:
use ;
let plan = ordered;
let burst_plan = all_at_once;
let _ = ;
When you intentionally configure more than one route, prefer SubmitPlan::all_at_once(...)
unless you specifically need ordered fallback semantics.
SubmitMode still exists as a legacy preset surface. It maps to exact ordered-fallback plans:
RpcOnly->SubmitPlan::rpc_only()JitoOnly->SubmitPlan::jito_only()DirectOnly->SubmitPlan::direct_only()Hybrid->SubmitPlan::hybrid()
Use submit_*_via(...) when you want explicit multi-route behavior. Keep submit_* (...) with
SubmitMode only for compatibility or when one legacy preset is enough.
Jito Configuration
Jito is split into two layers on purpose:
- transport-level configuration on
JitoJsonRpcTransport - per-submit behavior on
JitoSubmitConfig
Default transport settings:
- endpoint:
JitoBlockEngineEndpoint::mainnet()which resolves tohttps://mainnet.block-engine.jito.wtf request_timeout = 10s
Default submit settings:
bundle_only = false
That means the default path is:
- standard Jito
sendTransaction - base64 payload encoding
- no auth header unless you configure one
- no revert-protection query parameter unless you opt into
bundle_only
Example with explicit tuning:
use ;
use ;
use Url;
let block_engine_url = parse?;
let jito_transport = new;
let client = blockhash_only
.with_jito_transport
.with_jito_config;
Use bundle_only = true when you want Jito’s revert-protection behavior. Leave it false when
you want the default sendTransaction path.
If your submit plan includes SubmitRoute::Jito, make sure the transaction also includes the
economic shape that path expects. Jito's current transaction path documents a minimum tip of
1000 lamports for bundles, and during competitive periods that floor may still be too low for
good landing probability. In practice, Jito should be treated as both a transport choice and a
fee/tip policy choice, not just another endpoint toggle.
Regional endpoint selection is available for the documented Jito mainnet regions:
use ;
let jito_transport = with_endpoint?;
With the jito-grpc feature enabled, sof-tx also exposes JitoGrpcTransport. That path sends
transactions as single-transaction bundles over Jito searcher gRPC. If Jito is the route that
accepts before return, the bundle UUID is available in SubmitResult.jito_bundle_id; if another
route wins first, the later Jito accept still carries its bundle UUID through
TxSubmitOutcomeReporter, while built-in telemetry still counts that Jito accept even if the
reporter queue drops it under sustained pressure.
Reliability Profiles
Direct and hybrid modes include built-in reliability defaults through SubmitReliability.
LowLatency: minimal retries for fastest failover (direct_target_rounds=1,hybrid_direct_attempts=1)Balanced(default): extra direct retries before fallback (direct_target_rounds=2,hybrid_direct_attempts=2)HighReliability: most direct retrying (direct_target_rounds=3,hybrid_direct_attempts=3)
Use TxSubmitClient::with_reliability(...) for presets, or with_direct_config(...) for full control.
Reliability profiles are transport-side only. If you are sourcing blockhash, leader, and topology
state from SOF, pair them with evaluate_flow_safety(...) or TxSubmitGuardPolicy before sending.
KernelBypass Direct Transport
With kernel-bypass enabled, implement KernelBypassDatagramSocket for your socket type and wrap it with
KernelBypassDirectTransport.
use ;
use async_trait;
use ;
;
AF_XDP Demo and E2E
Linux-only AF_XDP demo (runs in an isolated user+network namespace via unshare -Urn):
Ignored integration test for AF_XDP kernel-bypass direct submit:
Requirements:
unsharefrom util-linuxipfrom iproute2- Linux kernel with AF_XDP support
Feature Model
Current implementation supports RPC/Jito/direct/hybrid runtime modes through one API. Compile-time capability flags from ADR-0006 can be introduced incrementally as the SDK stabilizes.
Docs
- ADR for SDK scope:
../../docs/architecture/adr/0006-transaction-sdk-and-dual-submit-routing.md - Workspace docs index:
../../docs/README.md - Architecture docs:
../../docs/architecture/README.md - Contribution guide:
../../CONTRIBUTING.md