QUIC Transaction Submission
This guide introduces how to connect to the high-performance transaction landing service using the QUIC protocol, ensuring your transactions are packaged and confirmed quickly and reliably.
Overview
QUIC transaction submission service provides the following features:
- High Performance: Submit transactions through private QUIC endpoints for lower latency and higher throughput
- Secure & Reliable: mTLS mutual authentication + API Key authorization ensure connection security
- Auto-Reconnect: Built-in connection health checks and automatic reconnection mechanisms
- No Subscription Required: Pay-as-you-go based on Tips mode
Prerequisites
1. Obtain Your API Key
Contact us to get your dedicated API Key.
2. Prepare Your Transaction Signing Account
You need a real account holding SOL to sign transactions. This account must:
- Hold sufficient SOL balance (recommended at least 0.0003 SOL)
3. Ensure Tip Account Funding
Each transaction must include a System Program transfer (≥0.0003 SOL = 300,000 lamports) to one of the following Tip accounts:
7HkiWXe5deJvzn4D6kgMUFCADwX9Z4DMrdjNSSxN6bPp
zanrUknLZXzT9JPj968A7RfgCjp77Lx1W1xKRAtfshb
zanHbk2UsiT3jKsKjD7UuEqS5Vgpmcd4pG9HycAAV8g
zanNazKCXNRoKnPS9BBbFTELTpNwUDJxeKEb1JtZJer
zan3gbFhXCjGLHhRe2vaXRDta5fCrYiYr3Dq4RLvpfU
zan6WoE3DX5aK7FMQT1vSGsGrgZG1ngns3oCsFMnBHU
zan8Nb9fB4zMDsuTRP9R65QZbc9v2Cjn5a4Hjwnj8D3
zanJgoR7ALJAJ6ohoKs6aS9T71D9ZkNN9gYM5xUsi3u
zanAtYifQP7Bo6kStB97mJvzqSDW1toKNibWibwcKDdEndpoints
| Region | Endpoint Address |
|---|---|
| Frankfurt (Recommended) | fra.zan.top:21000 |
| Singapore | sgp.zan.top:21000 |
Quick Start
Rust Environment Requirements
- Rust 1.70+
- Cargo
Cargo.toml Configuration
Add the following dependencies to your project's Cargo.toml:
[dependencies]
# QUIC Protocol
quinn = "0.11.3"
rustls = { version = "0.23.15", default-features = false }
# Solana Related
solana-sdk = "3.0.0"
solana-client = "3.0.0"
solana-tls-utils = "3.0.0"
solana-system-interface = { version = "3.0.0", features = ["bincode"] }
solana-perf = "3.1.5"
# Async Runtime
tokio = { version = "1.39", features = ["macros", "rt-multi-thread", "net", "time", "sync"] }
# Cryptography & Certificates
sha2 = "0.10"
rcgen = "0.13"
# Thread Safety
arc-swap = "1.6"
# Serialization
bincode = "1.3.3"
serde = { version = "1.0", features = ["derive"] }
# Error Handling
anyhow = "1.0.98"1. Client Example
//! Zan-style QUIC Client Example (mTLS + API Key Authentication)
//!
//! Usage: cargo run --bin client_quic_demo -- [BOOSTER_QUIC_ADDR] [KEYPAIR_PATH] [API_KEY] [RPC_URL]
//!
//! Features:
//! - Reads keypair from file for transaction signing (real account with SOL)
//! - Generates mTLS certificate using api_key, CN = api_key for authentication
//! - Supports automatic reconnection
use anyhow::{Context, Result};
use arc_swap::ArcSwap;
use quinn::{
crypto::rustls::QuicClientConfig, ClientConfig, Connection, Endpoint, IdleTimeout,
TransportConfig,
};
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
message::Message,
signature::{read_keypair_file, Keypair, Signer},
transaction::Transaction,
};
use solana_system_interface::instruction;
use solana_perf::packet::PACKET_DATA_SIZE;
use std::{net::SocketAddr, sync::Arc, time::Duration};
use tokio::sync::Mutex;
const ALPN_TPU_PROTOCOL_ID: &[u8] = b"solana-tpu";
const ZAN_SERVER: &str = "zan-node";
const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(10);
const MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
/// Zan QUIC client with automatic reconnection.
///
/// Thread-safe: can be shared across tasks via `Arc<ZanClient>`.
pub struct ZanClient {
endpoint: Endpoint,
client_config: ClientConfig,
addr: SocketAddr,
connection: ArcSwap<Connection>,
reconnect: Mutex<()>,
}
impl ZanClient {
/// Connect to a Zan endpoint.
///
/// # Arguments
/// * `endpoint_addr` - Server address (e.g., "fra.zan.top:21000")
/// * `api_key` - Your Zan API key (e.g., "bs_2294d1eb...")
pub async fn connect(endpoint_addr: &str, api_key: &str) -> Result<Self> {
// Derive keypair from API key using SHA256 hash
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(api_key.as_bytes());
let seed: [u8; 32] = hasher.finalize().into();
// Generate keypair from seed
let keypair = Keypair::new_from_array(seed);
let (cert, key) = new_dummy_x509_certificate(&keypair, api_key);
let mut crypto = rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(SkipServerVerification::new())
.with_client_auth_cert(vec![cert], key)
.context("failed to configure client certificate")?;
crypto.alpn_protocols = vec![ALPN_TPU_PROTOCOL_ID.to_vec()];
let client_crypto = QuicClientConfig::try_from(crypto)
.context("failed to convert rustls config into quinn crypto config")?;
let mut client_config = ClientConfig::new(Arc::new(client_crypto));
let mut transport = TransportConfig::default();
transport.keep_alive_interval(Some(KEEP_ALIVE_INTERVAL));
transport.max_idle_timeout(Some(IdleTimeout::try_from(MAX_IDLE_TIMEOUT)?));
client_config.transport_config(Arc::new(transport));
let mut endpoint = Endpoint::client("0.0.0.0:0".parse().context("parse bind address")?)?
;
endpoint.set_default_client_config(client_config.clone());
let addr = resolve(endpoint_addr).await?;
let connection = endpoint.connect(addr, ZAN_SERVER)?.await?;
Ok(Self {
endpoint,
client_config,
addr,
connection: ArcSwap::from_pointee(connection),
reconnect: Mutex::new(()),
})
}
/// Send a serialized transaction to the network.
///
/// Automatically reconnects on failure and retries once.
pub async fn send_transaction(&self, transaction_bytes: &[u8]) -> Result<()> {
let connection = self.connection.load_full();
if Self::try_send_bytes(&connection, transaction_bytes)
.await
.is_ok()
{
return Ok(());
}
// Connection lost, reconnect and retry
self.reconnect().await?;
let connection = self.connection.load_full();
Self::try_send_bytes(&connection, transaction_bytes).await
}
async fn reconnect(&self) -> Result<()> {
let _guard = self.reconnect.try_lock()?;
let connection = self
.endpoint
.connect_with(self.client_config.clone(), self.addr, ZAN_SERVER)?
.await?;
self.connection.store(Arc::new(connection));
Ok(())
}
async fn try_send_bytes(connection: &Connection, payload: &[u8]) -> Result<()> {
let mut stream = connection.open_uni().await?;
stream.write_all(payload).await?;
stream.finish()?;
Ok(())
}
}
async fn resolve(addr: &str) -> Result<SocketAddr> {
if let Ok(parsed) = addr.parse() {
return Ok(parsed);
}
tokio::net::lookup_host(addr)
.await?
.next()
.context("failed to resolve address")
}
/// Generate a self-signed X.509 certificate from a Solana keypair.
/// The CommonName (CN) is set to the api_key for authentication.
fn new_dummy_x509_certificate(
keypair: &Keypair,
api_key: &str,
) -> (CertificateDer<'static>, PrivateKeyDer<'static>) {
use rcgen::{CertificateParams, KeyPair, PKCS_ED25519};
let secret = keypair.secret_bytes();
let public = keypair.pubkey().to_bytes();
let mut keypair_bytes = [0u8; 64];
keypair_bytes[..32].copy_from_slice(secret);
keypair_bytes[32..].copy_from_slice(&public);
let pkcs8_der = keypair_to_pkcs8(&keypair_bytes);
let key_pair = KeyPair::from_pkcs8_der_and_sign_algo(
&rustls::pki_types::PrivatePkcs8KeyDer::from(pkcs8_der.clone()),
&PKCS_ED25519,
)
.expect("valid keypair");
let mut params = CertificateParams::new(vec!["localhost".into()]).expect("valid params");
params.distinguished_name = rcgen::DistinguishedName::new();
// CN = api_key for mTLS authentication (like client_quic)
params.distinguished_name.push(rcgen::DnType::CommonName, api_key);
let cert = params.self_signed(&key_pair).expect("valid cert");
let key_der = PrivateKeyDer::try_from(pkcs8_der).expect("valid key");
(cert.der().clone(), key_der)
}
fn keypair_to_pkcs8(keypair_bytes: &[u8; 64]) -> Vec<u8> {
let mut pkcs8 = vec![
0x30, 0x53, 0x02, 0x01, 0x01, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04,
0x20,
];
pkcs8.extend_from_slice(&keypair_bytes[..32]);
pkcs8.extend_from_slice(&[0xa1, 0x23, 0x03, 0x21, 0x00]);
pkcs8.extend_from_slice(&keypair_bytes[32..]);
pkcs8
}
/// Server certificate verifier that skips verification (for self-signed certs).
#[derive(Debug)]
struct SkipServerVerification(Arc<rustls::crypto::CryptoProvider>);
impl SkipServerVerification {
fn new() -> Arc<Self> {
Arc::new(Self(Arc::new(rustls::crypto::ring::default_provider())))
}
}
impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
fn verify_server_cert(
&self,
_: &CertificateDer<'_>,
_: &[CertificateDer<'_>],
_: &rustls::pki_types::ServerName<'_>,
_: &[u8],
_: rustls::pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
rustls::crypto::verify_tls12_signature(
message,
cert,
dss,
&self.0.signature_verification_algorithms,
)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
rustls::crypto::verify_tls13_signature(
message,
cert,
dss,
&self.0.signature_verification_algorithms,
)
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
self.0.signature_verification_algorithms.supported_schemes()
}
}
#[tokio::main]
async fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
let addr = args.get(1).cloned().unwrap_or_else(|| "fra.zan.top:21000".to_string());
let keypair_path = args.get(2).cloned().unwrap_or_else(|| "client_id.json".to_string());
let api_key = args.get(3).cloned().unwrap_or_else(|| "00f7xxxc088".to_string());
let rpc_url = args.get(4).cloned().unwrap_or_else(|| "https://api.zan.top/node/v1/solana/mainnet/00f7xxxc088".to_string());
// Read real account with SOL from file (for transaction signing)
let keypair = read_keypair_file(&keypair_path)
.map_err(|e| anyhow::anyhow!("read keypair: {}", e))?;
let rpc = RpcClient::new(rpc_url);
let blockhash = rpc.get_latest_blockhash().context("get blockhash")?;
let msg = Message::new(
&[instruction::transfer(
&keypair.pubkey(),
&keypair.pubkey(),
1_000,
)],
Some(&keypair.pubkey()),
);
let tx = Transaction::new(&[&keypair], msg, blockhash);
let serialized = bincode::serialize(&tx).context("serialize tx")?;
if serialized.len() > PACKET_DATA_SIZE {
anyhow::bail!(
"tx size {} > {}",
serialized.len(),
PACKET_DATA_SIZE
);
}
// Send transaction bytes directly (mTLS cert CN contains api_key for authentication)
let payload = &serialized;
println!(
"Connecting to Zan QUIC server: {} (keypair: {}, api_key: {})...",
addr,
keypair.pubkey(),
&api_key[..8.min(api_key.len())]
);
let client = ZanClient::connect(&addr, &api_key).await?;
println!("Connected!");
println!("Sending transaction via uni stream ({} bytes, mTLS auth)...", payload.len());
let start = std::time::Instant::now();
client.send_transaction(&payload).await?;
let elapsed = start.elapsed();
let sig = tx.signatures.first().map(|s| s.to_string()).unwrap_or_default();
println!(
"Transaction sent successfully! signature: {}, latency: {:.2} ms",
sig,
elapsed.as_secs_f64() * 1000.0
);
Ok(())
}
2. Build the Client
cargo build --release --bin client_quic_demo3. Run the Example
# cargo run --bin client_quic_demo <QUIC_ENDPOINT> <KEYPAIR_PATH> <API_KEY> <RPC_URL>
cargo run --bin client_quic_demo -- \
"fra.zan.top:21000" \
"./client_id.json" \
"00f7xxxc088" \
"https://api.zan.top/node/v1/solana/mainnet/00f7xxxc088"Parameter Descriptions
| Parameter | Description | Example |
|---|---|---|
| QUIC Endpoint | QUIC server address | fra.zan.top:21000 |
| Keypair Path | JSON file path for transaction signing account | ./client_id.json |
| API_KEY | Your API Key | 00f7xxxxc088 |
| RPC_URL | Solana RPC Node URL | https://api.zan.top/node/v1/solana/mainnet/00f7xxxxc088 |
Usage Example
cargo run --bin client_quic_demo -- \
"fra.zan.top:21000" \
"./client_id.json" \
"00f7xxxc088" \
"https://api.zan.top/node/v1/solana/mainnet/00f7xxxc088"4. Output Example
Connecting to Zan QUIC server: fra.zan.top:21000 (keypair: ..., api_key: 00f76627)...
Connected!
Sending transaction via uni stream (123 bytes, mTLS auth)...
Transaction sent successfully! signature: xxx, latency: 45.32 msCode Integration
If you want to integrate the QUIC client into your own project, you can reference the core implementation in client_quic_demo.rs:
Main Interfaces
/// Create Zan QUIC client and connect
/// endpoint_addr: QUIC server address, e.g., "fra.zan.top:21000"
/// api_key: Your API Key
let client = ZanClient::connect(endpoint_addr, api_key).await?;
/// Send transaction (handles reconnection automatically)
client.send_transaction(&transaction_bytes).await?;Core Features
- mTLS Authentication: Generates self-signed certificate using API Key, CN field contains API Key for server-side authentication
- Auto-Reconnect: Automatically reconnects and retries sending on connection loss
- Keep-Alive Mechanism: Sends heartbeat every 10 seconds to maintain connection activity
Important Notes
- Tip Transaction: Each transaction must include a transfer of at least 0.0003 SOL to a Tip account
- Transaction Size: Serialized transaction cannot exceed 1232 bytes (PACKET_DATA_SIZE)
- Connection Maintenance: Proactively reconnect when sending intervals are long to avoid idle timeout
- Key Security: Keep your keypair file and API Key secure; do not share them with others
FAQ
Q: What should I do if connection fails?
A: Please check:
- Is the API Key correct?
- Can the network access the target endpoint?
- Is the keypair file path correct?
Q: What should I do if transaction submission fails?
A: Ensure:
- The signing account has sufficient SOL balance
- Tip account has sufficient balance (0.0003 SOL per transaction)
- Transaction size does not exceed the limit
Q: How do I choose an endpoint?
A: We recommend selecting the endpoint closest to your physical location for the lowest latency. You can test response times from different endpoints to choose the optimal one.
Updated about 2 hours ago
