# QUIC Transaction Submission

Use QUIC when you want the lowest-overhead path for sending signed Solana transaction bytes to NextBlock from Rust.

The flow is:

1. Connect to a regional QUIC endpoint such as `london.nextblock.io:11100`
2. Authenticate once with your API key on a bidirectional stream
3. Reuse the connection and send each transaction on its own unidirectional stream

## Example

```rust
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;

use anyhow::{anyhow, Context, Result};
use quinn::{ClientConfig, Connection, Endpoint, TransportConfig};
use tokio::io::{AsyncReadExt, AsyncWriteExt};

const ALPN_NB_TX: &[u8] = b"nb-tx/1";
const AUTH_OK: u8 = 0x00;
const MAX_TX_SIZE: usize = 1232;

pub struct NextblockQuicClient {
    // Keep the endpoint alive for as long as the connection is in use.
    endpoint: Endpoint,
    connection: Connection,
}

impl NextblockQuicClient {
    pub async fn connect(server_addr: &str, api_key: &str) -> Result<Self> {
        let endpoint = create_endpoint()?;

        let addr: SocketAddr = tokio::net::lookup_host(server_addr)
            .await
            .context("dns lookup failed")?
            .next()
            .ok_or_else(|| anyhow!("no addresses found for {server_addr}"))?;

        let server_name = server_addr
            .split(':')
            .next()
            .ok_or_else(|| anyhow!("invalid server address"))?;

        let connection = endpoint
            .connect(addr, server_name)?
            .await
            .context("quic handshake failed")?;

        let (mut send, mut recv) = connection
            .open_bi()
            .await
            .context("failed to open auth stream")?;

        send.write_all(api_key.as_bytes())
            .await
            .context("failed to send api key")?;
        send.finish().context("failed to close auth stream")?;

        let mut response = [0u8; 1];
        recv.read_exact(&mut response)
            .await
            .context("failed to read auth response")?;

        if response[0] != AUTH_OK {
            return Err(anyhow!("authentication rejected"));
        }

        Ok(Self { endpoint, connection })
    }

    pub async fn send_transaction(&self, raw_tx: &[u8]) -> Result<()> {
        if raw_tx.len() > MAX_TX_SIZE {
            return Err(anyhow!("transaction too large: {}", raw_tx.len()));
        }

        let mut send = self
            .connection
            .open_uni()
            .await
            .context("failed to open tx stream")?;

        send.write_all(raw_tx)
            .await
            .context("failed to write transaction")?;
        send.finish().context("failed to finish tx stream")?;

        Ok(())
    }
}

impl Drop for NextblockQuicClient {
    fn drop(&mut self) {
        self.connection.close(quinn::VarInt::from_u32(0), b"client closing");
    }
}

fn create_endpoint() -> Result<Endpoint> {
    let mut roots = rustls::RootCertStore::empty();
    let native_certs = rustls_native_certs::load_native_certs();
    for cert in native_certs.certs {
        roots.add(cert).ok();
    }

    let mut crypto = rustls::ClientConfig::builder()
        .with_root_certificates(roots)
        .with_no_client_auth();
    crypto.alpn_protocols = vec![ALPN_NB_TX.to_vec()];

    let mut transport = TransportConfig::default();
    transport.max_idle_timeout(Some(quinn::IdleTimeout::try_from(Duration::from_secs(60))?));
    transport.keep_alive_interval(Some(Duration::from_secs(15)));

    let mut client_config = ClientConfig::new(Arc::new(
        quinn::crypto::rustls::QuicClientConfig::try_from(crypto)?,
    ));
    client_config.transport_config(Arc::new(transport));

    let mut endpoint = Endpoint::client("0.0.0.0:0".parse()?)?;
    endpoint.set_default_client_config(client_config);

    Ok(endpoint)
}

#[tokio::main]
async fn main() -> Result<()> {
    let api_key = std::env::var("NEXTBLOCK_API_KEY")
        .context("set NEXTBLOCK_API_KEY before running")?;

    let client = NextblockQuicClient::connect("london.nextblock.io:11100", &api_key).await?;

    // Replace this with the signed bytes from your existing Solana flow.
    // For example: let raw_tx = bincode::serialize(&signed_transaction)?;
    let raw_tx: Vec<u8> = vec![0; 32];

    client.send_transaction(&raw_tx).await?;
    println!("transaction queued");

    Ok(())
}
```

## What To Replace

* Swap `london.nextblock.io:11100` for the region closest to you.
* Replace `raw_tx` with the serialized bytes of your signed transaction.
* Read your API key from `NEXTBLOCK_API_KEY` or your own config loader.

## Notes

* Keep the client alive and reuse it for many sends.
* The server expects raw transaction bytes, not base64.
* QUIC does not support the extra gRPC submission flags or atomic bundle submission.
* If you already build transactions with `solana-sdk`, serializing the signed transaction is enough before calling `send_transaction()`.

See [QUIC Transaction Submission](/api/quic-transaction-submission.md) for the full endpoint list and protocol summary.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.nextblock.io/api/examples/rust/quic.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
