> For the complete documentation index, see [llms.txt](https://node1.gitbook.io/node1-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://node1.gitbook.io/node1-docs/documentation/zh/solana/fast-landing/quic-api.md).

# QUIC API

本文档面向接入 `node1` QUIC 服务的客户端开发者，说明端点列表、认证流程、请求格式、返回格式，以及一个可直接参考的 Rust 示例。

> 注册账号会自动生成 Account UUID，并开通 2 TPS 的 Solana 交易上链服务。可直接将该 UUID 作为本文的 API key；如需更高 TPS，请前往 Discord 申请。

## 端点列表

可用区域端点如下：

* `ny.node1.me:16666`
* `fra.node1.me:16666`
* `ams.node1.me:16666`
* `lon.node1.me:16666`
* `tk.node1.me:16666`
* `sgp.node1.me:16666`
* `fra2.node1.me:16666`
* `dub.node1.me:16666`

建议：

* 选择离自己最近的区域接入
* `server_name` 使用与目标端点一致的域名

例如：

* 地址: `ny.node1.me:16666`
* `server_name`: `ny.node1.me`

## 协议概览

一条 QUIC 连接分两步：

1. 认证
2. 发送交易

协议约定：

* 第一个双向 stream 用于认证
* 后续每一笔交易都使用新的双向 stream
* 认证成功后，客户端应始终复用这条已认证连接发送后续交易
* 请求体不带长度前缀
* 一条交易 stream 只发送一笔交易
* 客户端发送完请求后必须调用 `finish()`

## 认证流程

客户端建立 QUIC 连接后：

1. 调用 `open_bi()` 打开第一个双向 stream
2. 发送 `api key` 对应 UUID 的原始 16 字节内容
3. 调用 `finish()`
4. 读取服务端返回

认证数据不是 UUID 字符串本身，而是 UUID 解析后的 16 字节二进制内容。

认证阶段也有超时限制：

* 客户端建立连接后，应尽快完成 auth
* 如果 5 秒内没有发起 auth，或者 5 秒内没有把 16 字节 UUID 发完，服务端会直接关闭连接

发送方式：

```rust
let api_key_uuid = Uuid::parse_str(api_key)?;
send.write_all(&api_key_uuid.into_bytes()).await?;
send.finish()?;
```

认证成功时，服务端会返回 1 字节：

* `0`: 认证成功

认证失败时，服务端可能：

* 返回 `1`
* 或直接关闭连接，并带上 QUIC 应用层错误码

## 交易发送流程

认证成功后，每笔交易按下面流程发送：

1. 调用 `open_bi()` 打开一个新的双向 stream
2. 直接写入交易二进制内容
3. 调用 `finish()`
4. 从返回 stream 中读取响应帧

请求体说明：

* 内容是交易的原始二进制
* 当前服务端按 Solana `VersionedTransaction` 的序列化内容解析
* 客户端只需要发送 `bincode::serialize(&versioned_tx)` 的结果

示例：

```rust
let tx_bytes = bincode::serialize(&versioned_tx)?;
send.write_all(&tx_bytes).await?;
send.finish()?;
```

注意：

* 请求前面不需要再加 `data len`
* 服务端通过 stream EOF 判断一笔请求结束
* 如果客户端超过 5 秒仍未调用 `finish()`，服务端会丢弃这笔请求，返回 `408 Request Timeout`，但不会关闭整条连接

## 返回格式

服务端返回的是一个二进制响应帧，格式如下。

### Header

* `status`: 2 字节，无符号整数，`big-endian`
* `msg_len`: 4 字节，无符号整数，`big-endian`

### Body

* `msg`: 长度为 `msg_len` 的 UTF-8 字节串

总格式：

```
+---------+---------+------------------+
| status  | msg_len | msg              |
| 2 bytes | 4 bytes | msg_len bytes    |
+---------+---------+------------------+
```

说明：

* 当前返回中没有额外的 `body` 字段
* 客户端读取 6 字节头后，再按 `msg_len` 读取消息体即可

## 返回结果说明

发交易成功时：

* `status = 200`
* `msg` 为 JSON 字符串

`msg` 格式示例：

```json
{"jsonrpc":"2.0","id":1,"result":"<transaction_signature>"}
```

客户端建议：

* 先判断 `status`
* 当 `status = 200` 时，再按成功格式解析 `msg`

发交易失败时：

* `status != 200`
* `msg` 为对应错误信息

具体错误状态码与错误含义，请参考网站上的[错误码集合页面](/node1-docs/documentation/zh/solana/fast-landing/error-codes.md)。

## 长连接建议

客户端在认证成功后，应持续复用同一条 QUIC 连接发送后续交易，不要每笔交易都重新建连和重新认证。

建议：

* 维持一条已认证的长连接
* 每笔交易使用新的双向 stream
* 对 `connect` / `auth` / 单笔发送 都设置客户端超时
* 配置 `keep_alive_interval`
* 配置合理的 `max_idle_timeout`

不推荐的方式：

* 每发一笔交易就重新 `connect`
* 每发一笔交易就重新做一次 auth

原因：

* QUIC 握手和 TLS 握手都有额外成本
* 频繁重连会放大延迟抖动
* 在高并发或高频场景下，这种模式明显更差

推荐模式：

* 客户端启动时建立连接
* 完成一次 auth
* 后续所有交易都在这条连接上反复 `open_bi()`
* 只有在连接确认断开后才重新连接并重新 auth

示例中的 keepalive 参数仅作为参考：

* `keep_alive_interval = 15s`
* `max_idle_timeout = 60s`

如果你的部署环境经过 NAT、LB 或边缘网络，建议保留 keepalive，避免空闲连接被中间设备回收。

### 客户端超时建议

客户端不要无限等待连接建立。

原因：

* 服务端可能已经宕机
* 前置 LB / 防火墙 / 网络路径可能处于黑洞状态
* 这类场景下如果客户端不设置超时，可能会一直卡住，既不返回结果，也不及时报错

建议给下面三个阶段添加超时：

* 建立连接 `connect`
* 首次认证 `auth`
* 单笔发送 `send_transaction`

### 连接空闲与资源释放

如果客户端既不主动 `close connection`，也没有任何业务流量或 keepalive，那么连接在超过 `max_idle_timeout` 后会被 QUIC 自动关闭。

但要注意：

* `max_idle_timeout` 是连接级超时，不是 stream 级超时
* 如果客户端还在发 keepalive，这条连接就不算空闲
* 不空闲的连接会继续保留

### 如何判断连接已经断开

客户端建议同时用下面两种方式判断：

1. 主动检查 `connection.close_reason()`
2. 在实际发交易时处理 `open_bi()` / `send` / `recv` 返回的连接错误

示例：

```rust
if let Some(reason) = connection.close_reason() {
    eprintln!("connection closed: {:?}", reason);
    // reconnect + authenticate again
}
```

以及：

```rust
let (mut send, mut recv) = match connection.open_bi().await {
    Ok(stream) => stream,
    Err(err) => {
        eprintln!("connection unusable: {:?}", err);
        // reconnect + authenticate again
        return Err(err.into());
    }
};
```

常见需要重连的情况包括：

* `ConnectionError::TimedOut`
* `ConnectionError::LocallyClosed`
* `ConnectionError::ApplicationClosed`
* 任意 `ConnectionLost(...)`

## Rust 示例

下面示例演示完整流程：

* 连接某个区域端点
* 发送 16 字节 UUID 做认证
* 持续复用同一条已认证连接
* 将 `VersionedTransaction` 用 `bincode` 编码
* 发送交易
* 解析服务端返回的 `status + msg`

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

use anyhow::{Context, Result};
use quinn::{Connection, Endpoint, TransportConfig};
use quinn::crypto::rustls::QuicClientConfig;
use solana_sdk::transaction::VersionedTransaction;
use solana_tls_utils::SkipServerVerification;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::time::timeout;
use uuid::Uuid;

async fn connect_quic(server_addr: &str, server_name: &str) -> Result<(Endpoint, Connection)> {
    let socket_addr = server_addr
        .to_socket_addrs()
        .context("resolve server addr failed")?
        .next()
        .context("server addr resolved to no socket address")?;

    let crypto = rustls::ClientConfig::builder()
        .dangerous()
        .with_custom_certificate_verifier(SkipServerVerification::new())
        .with_no_client_auth();

    let client_crypto = QuicClientConfig::try_from(crypto)
        .context("build quic tls config failed")?;
    let mut client_config = quinn::ClientConfig::new(Arc::new(client_crypto));

    let mut transport = TransportConfig::default();
    let idle_timeout = quinn::IdleTimeout::try_from(Duration::from_secs(60)).unwrap();
    transport.max_idle_timeout(Some(idle_timeout));
    transport.keep_alive_interval(Some(Duration::from_secs(15)));
    client_config.transport_config(Arc::new(transport));

    let mut endpoint = Endpoint::client("0.0.0.0:0".parse().unwrap())
        .context("create quic endpoint failed")?;
    endpoint.set_default_client_config(client_config);

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

    Ok((endpoint, connection))
}

async fn authenticate(connection: &Connection, api_key: &str) -> Result<()> {
    let api_key_uuid = Uuid::parse_str(api_key).context("invalid api key uuid")?;

    let (mut send, mut recv) = connection.open_bi().await?;
    send.write_all(&api_key_uuid.into_bytes()).await?;
    send.finish()?;

    let auth_reply = recv.read_to_end(8).await?;
    match auth_reply.first().copied() {
        Some(0) => Ok(()),
        Some(code) => anyhow::bail!("auth rejected, reply={code}"),
        None => anyhow::bail!("auth failed, empty reply"),
    }
}

async fn read_response(recv: &mut quinn::RecvStream) -> Result<(u16, String)> {
    let mut header = [0u8; 6];
    recv.read_exact(&mut header).await?;

    let status = u16::from_be_bytes(header[0..2].try_into().unwrap());
    let msg_len = u32::from_be_bytes(header[2..6].try_into().unwrap()) as usize;

    let mut msg = vec![0u8; msg_len];
    if msg_len > 0 {
        recv.read_exact(&mut msg).await?;
    }

    Ok((status, String::from_utf8_lossy(&msg).into_owned()))
}

async fn send_transaction(connection: &Connection, tx_bytes: &[u8]) -> Result<(u16, String)> {
    let (mut send, mut recv) = connection.open_bi().await?;
    send.write_all(tx_bytes).await?;
    send.finish()?;
    read_response(&mut recv).await
}

#[tokio::main]
async fn main() -> Result<()> {
    let server_addr = "ny.node1.me:16666";
    let server_name = "ny.node1.me";
    let api_key = std::env::var("NODE1_API_KEY_UUID")
        .context("set NODE1_API_KEY_UUID")?;

    let (endpoint, connection) = timeout(
        Duration::from_secs(5),
        connect_quic(server_addr, server_name),
    )
    .await
    .context("connect timeout")??;

    timeout(Duration::from_secs(5), authenticate(&connection, &api_key))
        .await
        .context("auth timeout")??;

    // 认证成功后，请持续复用这条 connection 发送后续交易。
    // 不要每笔交易都重新 connect + auth。
    // 后续每次要发新交易时，只需要继续调用 send_transaction(&connection, &tx_bytes)。

    let versioned_tx: VersionedTransaction = /* your signed transaction */;
    let tx_bytes = bincode::serialize(&versioned_tx)
        .context("serialize tx failed")?;

    let (status, msg) = timeout(
        Duration::from_secs(5),
        send_transaction(&connection, &tx_bytes),
    )
    .await
    .context("send timeout")??;
    println!("status: {}", status);
    println!("msg: {}", msg);

    connection.close(0u32.into(), b"done");
    endpoint.wait_idle().await;
    Ok(())
}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## 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, and the optional `goal` query parameter:

```
GET https://node1.gitbook.io/node1-docs/documentation/zh/solana/fast-landing/quic-api.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
