> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbitflare.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 使用 Apex 发送交易

> 通过 JSON-RPC sendTransaction、二进制 HTTP 路由 /send-bin 和 /send-batch， 或持久 QUIC 流，将 Solana 交易发送到 OrbitFlare Apex。

## 选择传输方式

|         | JSON-RPC        | `POST /send`   | `POST /send-bin` | `POST /send-batch` | QUIC 单向  | QUIC 双向  |
| ------- | --------------- | -------------- | ---------------- | ------------------ | -------- | -------- |
| **请求体** | JSON 中的 base64  | JSON 中的 base64 | 原始字节             | 最多 16 个原始帧         | 每个流一个数据包 | 每个流一个数据包 |
| **响应**  | 签名或 JSON-RPC 错误 | 签名或 JSON 错误    | 签名或 JSON 错误      | 每帧的结果              | 无        | 准入码      |
| **最适合** | 直接替换现有代码        | 简单的 JSON 客户端   | HTTP 开销最低        | 一次请求发送多笔           | 最快的路径    | 直接获得拒绝原因 |

所有传输方式都采用相同的[小费规则](/cn/apex/tips)、相同的验证和相同的路由。传输方式只改变字节到达 Apex 端点的方式。

交易大小限制在各处都相同：legacy 和 v0 交易最大 1232 字节，[交易 v1](/cn/apex/transaction-v1) 最大 4096 字节。

## 构建已签名并带小费的交易

本页的示例在每种语言中共用一个辅助模块。它构建一笔带有计算预算和小费的 memo 交易，对其签名，并且可以确认签名。直接运行它会以 base64 形式输出一笔已签名交易，cURL 示例使用的就是这个输出。

首先设置环境变量：

```bash theme={null}
export APEX_API_KEY="YOUR_API_KEY"
export APEX_RPC="http://fra.apex.orbitflare.com"
export SOLANA_RPC_URL="https://api.mainnet-beta.solana.com"
export KEYPAIR_PATH="payer.json"
```

<CodeGroup>
  ```javascript apex-tx.mjs theme={null}
  // npm install @solana/web3.js
  // Print a signed, tipped transaction as base64: node apex-tx.mjs
  import {
    ComputeBudgetProgram, Connection, Keypair, PublicKey, SystemProgram,
    TransactionInstruction, TransactionMessage, VersionedTransaction,
  } from "@solana/web3.js";
  import { readFileSync } from "node:fs";
  import { fileURLToPath } from "node:url";

  export const APEX_RPC = process.env.APEX_RPC ?? "http://fra.apex.orbitflare.com";
  export const API_KEY = process.env.APEX_API_KEY ?? "";
  const SOLANA_RPC = process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com";
  const TIP_LAMPORTS = Number(process.env.TIP_LAMPORTS ?? 1_000_000);
  const MEMO_PROGRAM = new PublicKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");

  export const payer = Keypair.fromSecretKey(
    Uint8Array.from(JSON.parse(readFileSync(process.env.KEYPAIR_PATH ?? "payer.json", "utf8"))),
  );
  export const connection = new Connection(SOLANA_RPC, "confirmed");

  export async function apexRpc(method, params) {
    const res = await fetch(APEX_RPC, {
      method: "POST",
      headers: { "Content-Type": "application/json", "x-api-key": API_KEY },
      body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
    });
    const body = await res.json();
    if (body.error) throw new Error(`${method} failed ${body.error.code}: ${body.error.message}`);
    return body.result;
  }

  let tipAccounts;
  let counter = 0;

  // Returns a signed VersionedTransaction. Pass { tip: false } for a bundle member without a tip.
  export async function buildTx({ tip = true, label = "apex" } = {}) {
    tipAccounts ??= await apexRpc("getTipAccounts", []);
    const tipAccount = new PublicKey(tipAccounts[Math.floor(Math.random() * tipAccounts.length)]);
    const { blockhash } = await connection.getLatestBlockhash("confirmed");
    const instructions = [
      ComputeBudgetProgram.setComputeUnitLimit({ units: 100_000 }),
      ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 10_000 }),
      new TransactionInstruction({
        programId: MEMO_PROGRAM,
        keys: [{ pubkey: payer.publicKey, isSigner: true, isWritable: true }],
        data: Buffer.from(`${label} ${Date.now()} ${counter++}`),
      }),
    ];
    if (tip) {
      instructions.push(
        SystemProgram.transfer({ fromPubkey: payer.publicKey, toPubkey: tipAccount, lamports: TIP_LAMPORTS }),
      );
    }
    const message = new TransactionMessage({
      payerKey: payer.publicKey,
      recentBlockhash: blockhash,
      instructions,
    }).compileToV0Message();
    const tx = new VersionedTransaction(message);
    tx.sign([payer]);
    return tx;
  }

  // Poll getSignatureStatuses on the Solana RPC. Returns the slot, or null on timeout.
  export async function confirm(signature, timeoutMs = 30_000) {
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
      const { value } = await connection.getSignatureStatuses([signature]);
      const status = value[0];
      if (status?.err) throw new Error(`failed on chain: ${JSON.stringify(status.err)}`);
      if (status?.confirmationStatus === "confirmed" || status?.confirmationStatus === "finalized") {
        return status.slot;
      }
      await new Promise((resolve) => setTimeout(resolve, 500));
    }
    return null;
  }

  if (process.argv[1] === fileURLToPath(import.meta.url)) {
    const tx = await buildTx();
    console.log(Buffer.from(tx.serialize()).toString("base64"));
  }
  ```

  ```python apex_tx.py theme={null}
  # pip install solders requests
  # Print a signed, tipped transaction as base64: python apex_tx.py
  import base64
  import itertools
  import json
  import os
  import random
  import time

  import requests
  from solders.compute_budget import set_compute_unit_limit, set_compute_unit_price
  from solders.hash import Hash
  from solders.instruction import AccountMeta, Instruction
  from solders.keypair import Keypair
  from solders.message import Message
  from solders.pubkey import Pubkey
  from solders.system_program import TransferParams, transfer
  from solders.transaction import Transaction

  APEX_RPC = os.environ.get("APEX_RPC", "http://fra.apex.orbitflare.com")
  API_KEY = os.environ.get("APEX_API_KEY", "")
  SOLANA_RPC = os.environ.get("SOLANA_RPC_URL", "https://api.mainnet-beta.solana.com")
  TIP_LAMPORTS = int(os.environ.get("TIP_LAMPORTS", "1000000"))
  MEMO_PROGRAM = Pubkey.from_string("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr")

  # One session keeps the HTTP connection to the Apex endpoint open between sends.
  session = requests.Session()
  session.headers.update({"x-api-key": API_KEY})

  with open(os.environ.get("KEYPAIR_PATH", "payer.json")) as f:
      payer = Keypair.from_bytes(bytes(json.load(f)))

  _tip_accounts = None
  _counter = itertools.count()


  def apex_rpc(method, params):
      r = session.post(APEX_RPC, json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params}, timeout=5)
      body = r.json()
      if "error" in body:
          raise RuntimeError(f"{method} failed {body['error']['code']}: {body['error']['message']}")
      return body["result"]


  def solana_rpc(method, params):
      r = requests.post(SOLANA_RPC, json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params}, timeout=10)
      return r.json()["result"]


  def build_tx(tip=True, label="apex"):
      """Returns a signed Transaction. Pass tip=False for a bundle member without a tip."""
      global _tip_accounts
      if _tip_accounts is None:
          _tip_accounts = apex_rpc("getTipAccounts", [])
      tip_account = Pubkey.from_string(random.choice(_tip_accounts))
      blockhash = Hash.from_string(
          solana_rpc("getLatestBlockhash", [{"commitment": "confirmed"}])["value"]["blockhash"])
      instructions = [
          set_compute_unit_limit(100_000),
          set_compute_unit_price(10_000),
          Instruction(MEMO_PROGRAM, f"{label} {time.time_ns()} {next(_counter)}".encode(),
                      [AccountMeta(payer.pubkey(), is_signer=True, is_writable=True)]),
      ]
      if tip:
          instructions.append(
              transfer(TransferParams(from_pubkey=payer.pubkey(), to_pubkey=tip_account, lamports=TIP_LAMPORTS)))
      return Transaction([payer], Message.new_with_blockhash(instructions, payer.pubkey(), blockhash), blockhash)


  def confirm(signature, timeout_s=30.0):
      """Poll getSignatureStatuses on the Solana RPC. Returns the slot, or None on timeout."""
      deadline = time.time() + timeout_s
      while time.time() < deadline:
          status = solana_rpc("getSignatureStatuses", [[signature]])["value"][0]
          if status and status.get("err"):
              raise RuntimeError(f"failed on chain: {status['err']}")
          if status and status.get("confirmationStatus") in ("confirmed", "finalized"):
              return status["slot"]
          time.sleep(0.5)
      return None


  if __name__ == "__main__":
      print(base64.b64encode(bytes(build_tx())).decode())
  ```
</CodeGroup>

对于 cURL 示例，请在每次发送前签名一笔新的交易：

```bash theme={null}
TX_BASE64=$(node apex-tx.mjs)      # or: TX_BASE64=$(python apex_tx.py)
```

交易只在其 blockhash 有效期间有效，大约 60 到 90 秒，因此请在发送前一刻签名。

## JSON-RPC sendTransaction

向 Apex 端点的根路径发送 `POST`。这是标准的 Solana `sendTransaction` 格式，因此现有代码只需替换 URL 并添加 API 密钥即可迁移。

| 参数                     | 类型         | 描述                                                                           |
| ---------------------- | ---------- | ---------------------------------------------------------------------------- |
| `params[0]`            | string     | 已签名的交易，base64 编码                                                             |
| `params[1].encoding`   | string     | `"base64"`                                                                   |
| `params[1].maxRetries` | number，可选  | 该交易的重试预算。省略则使用端点的默认值                                                         |
| `params[2]`            | boolean，可选 | `mevProtect`。`true` 会跳过 Shield 黑名单上的领导者。参见 [MEV 保护](/cn/apex/mev-protection) |

<CodeGroup>
  ```bash cURL theme={null}
  curl -s "$APEX_RPC" \
    -H "Content-Type: application/json" \
    -H "x-api-key: $APEX_API_KEY" \
    -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"sendTransaction\",\"params\":[\"$TX_BASE64\",{\"encoding\":\"base64\",\"maxRetries\":30},false]}"
  ```

  ```javascript JavaScript theme={null}
  import { apexRpc, buildTx, confirm } from "./apex-tx.mjs";

  const tx = await buildTx({ label: "apex json-rpc" });
  const signature = await apexRpc("sendTransaction", [
    Buffer.from(tx.serialize()).toString("base64"),
    { encoding: "base64", maxRetries: 30 },
    false, // mevProtect
  ]);
  console.log("accepted:", signature);
  console.log("landed in slot", await confirm(signature));
  ```

  ```python Python theme={null}
  import base64

  from apex_tx import apex_rpc, build_tx, confirm

  tx = build_tx(label="apex json-rpc")
  signature = apex_rpc("sendTransaction", [
      base64.b64encode(bytes(tx)).decode(),
      {"encoding": "base64", "maxRetries": 30},
      False,  # mevProtect
  ])
  print("accepted:", signature)
  print("landed in slot", confirm(signature))
  ```

  ```rust Rust theme={null}
  // `tx` is a signed, tipped VersionedTransaction, built as in the Quickstart.
  use apex_sender_client::rpc::RpcClient;
  use apex_sender_client::{serialize_transaction, Region};

  let rpc = RpcClient::new(Region::Frankfurt, &api_key);
  let wire = serialize_transaction(&tx)?;
  // mev_protect = false, max_retries = Some(30)
  let signature = rpc.send_transaction(&wire, false, Some(30)).await?;
  println!("accepted: {signature}");
  ```
</CodeGroup>

响应：

```json theme={null}
{ "jsonrpc": "2.0", "id": 1, "result": "<signature>" }
```

Apex 不会进行模拟或预检。`skipPreflight` 和 `preflightCommitment` 不起作用。如果您需要模拟，请在发送前先在您的常规 RPC 上调用 `simulateTransaction`。

## 普通 HTTP 路由

这些路由与 JSON-RPC 位于同一主机和端口上，并省去了 JSON-RPC 封装。

| 路由                  | 请求体                                                                       | 成功响应                                          |
| ------------------- | ------------------------------------------------------------------------- | --------------------------------------------- |
| `POST /send`        | JSON：`{"transaction": "<base64>", "mevProtect"?: bool, "maxRetries"?: n}` | `{"signature": "..."}`                        |
| `POST /send-bin`    | 原始交易字节，`Content-Type: application/octet-stream`                           | `{"signature": "..."}`                        |
| `POST /send-batch`  | 最多 16 帧，每帧为一个大端序 u16 长度后跟交易字节                                             | `attempted`、`accepted`、`rejected` 和 `results` |
| `POST /send-bundle` | 1 到 4 帧，帧格式相同。参见[捆绑包](/cn/apex/bundles)                                   | `{"bundle_id": "...", "signatures": [...]}`   |
| `GET /ping`         | 无，且无需 API 密钥                                                              | `pong`                                        |

在二进制路由上，选项以查询标志的形式传递：`?mev_protect=1` 和 `&max_retries=N`。

错误为带有 HTTP 状态码的 JSON：`{"error": "<label>", "message": "..."}`。参见[错误与速率限制](/cn/apex/errors-and-rate-limits#普通-http-错误)。

### POST /send

<CodeGroup>
  ```bash cURL theme={null}
  curl -s "$APEX_RPC/send" \
    -H "Content-Type: application/json" \
    -H "x-api-key: $APEX_API_KEY" \
    -d "{\"transaction\":\"$TX_BASE64\",\"mevProtect\":false,\"maxRetries\":30}"
  ```

  ```javascript JavaScript theme={null}
  import { APEX_RPC, API_KEY, buildTx, confirm } from "./apex-tx.mjs";

  const tx = await buildTx({ label: "apex send" });
  const res = await fetch(`${APEX_RPC}/send`, {
    method: "POST",
    headers: { "Content-Type": "application/json", "x-api-key": API_KEY },
    body: JSON.stringify({ transaction: Buffer.from(tx.serialize()).toString("base64"), mevProtect: false }),
  });
  const body = await res.json();
  if (!body.signature) throw new Error(`rejected ${res.status} ${body.error}: ${body.message}`);
  console.log("accepted:", body.signature);
  console.log("landed in slot", await confirm(body.signature));
  ```

  ```python Python theme={null}
  import base64

  from apex_tx import APEX_RPC, build_tx, confirm, session

  tx = build_tx(label="apex send")
  r = session.post(f"{APEX_RPC}/send",
                   json={"transaction": base64.b64encode(bytes(tx)).decode(), "mevProtect": False}, timeout=5)
  body = r.json()
  if "signature" not in body:
      raise RuntimeError(f"rejected {r.status_code} {body['error']}: {body['message']}")
  print("accepted:", body["signature"])
  print("landed in slot", confirm(body["signature"]))
  ```
</CodeGroup>

### POST /send-bin

开销最低的 HTTP 路径：传入时无需 base64，也无需 JSON。请求体就是序列化后的交易，逐字节一致。

<CodeGroup>
  ```bash cURL theme={null}
  echo "$TX_BASE64" | base64 --decode | curl -s "$APEX_RPC/send-bin?mev_protect=0" \
    -H "Content-Type: application/octet-stream" \
    -H "x-api-key: $APEX_API_KEY" \
    --data-binary @-
  ```

  ```javascript JavaScript theme={null}
  import { APEX_RPC, API_KEY, buildTx, confirm } from "./apex-tx.mjs";

  const tx = await buildTx({ label: "apex send-bin" });
  const res = await fetch(`${APEX_RPC}/send-bin`, {
    method: "POST",
    headers: { "Content-Type": "application/octet-stream", "x-api-key": API_KEY },
    body: tx.serialize(),
  });
  const body = await res.json();
  if (!body.signature) throw new Error(`rejected ${res.status} ${body.error}: ${body.message}`);
  console.log("accepted:", body.signature);
  console.log("landed in slot", await confirm(body.signature));
  ```

  ```python Python theme={null}
  from apex_tx import APEX_RPC, build_tx, confirm, session

  tx = build_tx(label="apex send-bin")
  r = session.post(f"{APEX_RPC}/send-bin", headers={"Content-Type": "application/octet-stream"},
                   data=bytes(tx), timeout=5)
  body = r.json()
  if "signature" not in body:
      raise RuntimeError(f"rejected {r.status_code} {body['error']}: {body['message']}")
  print("accepted:", body["signature"])
  print("landed in slot", confirm(body["signature"]))
  ```

  ```rust Rust theme={null}
  // `tx` is a signed, tipped VersionedTransaction, built as in the Quickstart.
  use apex_sender_client::rpc::RpcClient;
  use apex_sender_client::{serialize_transaction, Region};

  let rpc = RpcClient::new(Region::Frankfurt, &api_key);
  let wire = serialize_transaction(&tx)?;
  let signature = rpc.send_transaction_binary(&wire, false, None).await?;
  println!("accepted: {signature}");
  ```
</CodeGroup>

### POST /send-batch

在一次请求中发送最多 16 笔相互独立的交易。请求体是一个帧序列：

```text theme={null}
u16 big-endian   length of the transaction bytes
bytes            the serialized transaction
... repeated, up to 16 frames
```

每笔交易单独进行准入判定。批量**不具备原子性**：部分帧可能被接受，而其他帧被拒绝。如需要么全部成功、要么全部不执行，请使用[捆绑包](/cn/apex/bundles)。

<CodeGroup>
  ```bash cURL theme={null}
  : > batch.bin
  for i in 1 2 3; do
    node apex-tx.mjs | base64 --decode > tx.bin
    printf '%04x' $(($(wc -c < tx.bin))) | xxd -r -p >> batch.bin   # u16 big-endian length
    cat tx.bin >> batch.bin
  done

  curl -s "$APEX_RPC/send-batch" \
    -H "Content-Type: application/octet-stream" \
    -H "x-api-key: $APEX_API_KEY" \
    --data-binary @batch.bin
  ```

  ```javascript JavaScript theme={null}
  import { APEX_RPC, API_KEY, buildTx, confirm } from "./apex-tx.mjs";

  const txs = [];
  for (let i = 0; i < 3; i++) txs.push(await buildTx({ label: `apex batch ${i}` }));

  // Each frame: u16 big-endian length, then the transaction bytes.
  const frames = txs.map((tx) => {
    const wire = Buffer.from(tx.serialize());
    const length = Buffer.alloc(2);
    length.writeUInt16BE(wire.length);
    return Buffer.concat([length, wire]);
  });

  const res = await fetch(`${APEX_RPC}/send-batch`, {
    method: "POST",
    headers: { "Content-Type": "application/octet-stream", "x-api-key": API_KEY },
    body: Buffer.concat(frames),
  });
  const body = await res.json();
  if (!body.results) throw new Error(`rejected ${res.status} ${body.error}: ${body.message}`);
  console.log(`accepted ${body.accepted} of ${body.attempted}`);
  for (const result of body.results) {
    if (result.signature) console.log(result.signature, "landed in slot", await confirm(result.signature));
    else console.log("rejected:", result.error, result.message);
  }
  ```

  ```python Python theme={null}
  import struct

  from apex_tx import APEX_RPC, build_tx, confirm, session

  txs = [bytes(build_tx(label=f"apex batch {i}")) for i in range(3)]
  # Each frame: u16 big-endian length, then the transaction bytes.
  payload = b"".join(struct.pack(">H", len(wire)) + wire for wire in txs)

  r = session.post(f"{APEX_RPC}/send-batch", headers={"Content-Type": "application/octet-stream"},
                   data=payload, timeout=5)
  body = r.json()
  if "results" not in body:
      raise RuntimeError(f"rejected {r.status_code} {body['error']}: {body['message']}")
  print(f"accepted {body['accepted']} of {body['attempted']}")
  for result in body["results"]:
      if "signature" in result:
          print(result["signature"], "landed in slot", confirm(result["signature"]))
      else:
          print("rejected:", result["error"], result["message"])
  ```

  ```rust Rust theme={null}
  // `txs` is a Vec of signed, tipped VersionedTransaction values.
  use apex_sender_client::rpc::{BatchItem, RpcClient};
  use apex_sender_client::{serialize_transaction, Region};

  let rpc = RpcClient::new(Region::Frankfurt, &api_key);
  let wires: Vec<Vec<u8>> = txs.iter().map(serialize_transaction).collect::<Result<_, _>>()?;
  let refs: Vec<&[u8]> = wires.iter().map(Vec::as_slice).collect();

  let batch = rpc.send_batch(&refs, false, None).await?;
  println!("accepted {} of {}", batch.accepted, batch.attempted);
  for item in &batch.results {
      match item {
          BatchItem::Accepted(signature) => println!("accepted: {signature}"),
          BatchItem::Rejected { error, message } => println!("rejected: {error}: {message}"),
      }
  }
  ```
</CodeGroup>

只要请求本身有效，响应始终是 HTTP 200，每帧一个结果，按帧的顺序排列：

```json theme={null}
{
  "attempted": 3,
  "accepted": 2,
  "rejected": 1,
  "results": [
    { "signature": "<signature 1>" },
    { "signature": "<signature 2>" },
    { "error": "below_floor", "message": "..." }
  ]
}
```

### GET /ping

返回 `pong`。无需 API 密钥。用它在需要之前打开并预热 HTTP 连接，以及让空闲连接保持存活。参见[最佳实践](/cn/apex/best-practices#保持连接预热)。

```bash theme={null}
curl -s "$APEX_RPC/ping"
```

### CORS

每个 HTTP 响应都带有 `Access-Control-Allow-Origin: *`，并且 `OPTIONS` 预检请求会得到应答，因此上述所有路由都可以在浏览器代码中使用。在将密钥下发到前端之前，请参见[浏览器注意事项](/cn/apex/authentication#浏览器)。

## QUIC

QUIC 是最快的接入方式。您与 Apex 端点保持一个持久连接，通过[由您的 API 密钥派生的客户端证书](/cn/apex/authentication#quic-客户端证书)完成一次身份验证，并为每笔交易打开一个流。在预热的连接上，一次发送就是打开一个流并写入一次，只占用客户端几微秒的时间。

流分为两种：

| 流      | 返回内容                | 用途                  |
| ------ | ------------------- | ------------------- |
| **单向** | 无。发出后不再理会           | 在最快路径上进行生产环境发送      |
| **双向** | 一个准入响应：已接受，或拒绝代码和消息 | 集成、调试，以及希望直接获得原因的工具 |

### Rust

[`apex-sender-client`](/cn/apex/rust-client) crate 实现了整个传输层：证书、保活、0-RTT 恢复和重连。该程序在两种流上各发送一笔交易。它使用与[快速入门](/cn/apex/quickstart#rust-依赖)相同的 `Cargo.toml`。

```rust theme={null}
use std::str::FromStr;
use std::time::Duration;

use apex_sender_client::rpc::{RpcClient, SolanaRpc};
use apex_sender_client::{tip, tip_instruction, ApexSenderClient, Error, Region, MIN_TIP_LAMPORTS};
use solana_compute_budget_interface::ComputeBudgetInstruction;
use solana_instruction::{AccountMeta, Instruction};
use solana_keypair::{read_keypair_file, Keypair};
use solana_pubkey::Pubkey;
use solana_signer::Signer;
use solana_transaction::versioned::VersionedTransaction;
use solana_transaction::Transaction;

async fn tipped_memo(
    payer: &Keypair,
    tip_accounts: &[Pubkey],
    solana: &SolanaRpc,
    label: &str,
) -> Result<VersionedTransaction, Box<dyn std::error::Error>> {
    let nonce = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)?
        .as_nanos();
    let tip_account = tip::pick_tip_account(tip_accounts).ok_or("no tip accounts")?;
    let instructions = [
        ComputeBudgetInstruction::set_compute_unit_limit(100_000),
        ComputeBudgetInstruction::set_compute_unit_price(10_000),
        Instruction::new_with_bytes(
            Pubkey::from_str("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr")?,
            format!("{label} {nonce}").as_bytes(),
            vec![AccountMeta::new(payer.pubkey(), true)],
        ),
        tip_instruction(&payer.pubkey(), &tip_account, MIN_TIP_LAMPORTS),
    ];
    let blockhash = solana.latest_blockhash().await?;
    Ok(VersionedTransaction::from(Transaction::new_signed_with_payer(
        &instructions,
        Some(&payer.pubkey()),
        &[payer],
        blockhash,
    )))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let api_key = std::env::var("APEX_API_KEY")?;
    let solana = SolanaRpc::new(
        std::env::var("SOLANA_RPC_URL").unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".into()),
    );
    let payer = read_keypair_file(std::env::var("KEYPAIR_PATH").unwrap_or_else(|_| "payer.json".into()))?;
    let region = Region::Frankfurt;

    let client = ApexSenderClient::connect(region, &api_key).await?;
    let tip_accounts = RpcClient::new(region, &api_key).get_tip_accounts().await?;

    // Unidirectional stream: fire and forget. Returns the signature, reads nothing back.
    let tx = tipped_memo(&payer, &tip_accounts, &solana, "apex quic uni").await?;
    let signature = client.send_transaction(&tx).await?;
    println!("sent: {signature}");
    println!("slot: {:?}", solana.confirm(&signature.to_string(), Duration::from_secs(30)).await?);

    // Bidirectional stream: the endpoint answers accepted or rejected before racing it.
    let tx = tipped_memo(&payer, &tip_accounts, &solana, "apex quic bidi").await?;
    match client.send_transaction_with_response(&tx).await {
        Ok(signature) => {
            println!("accepted: {signature}");
            println!("slot: {:?}", solana.confirm(&signature.to_string(), Duration::from_secs(30)).await?);
        }
        // Fix the transaction, not the client. The message names the floor when the tip is short.
        Err(Error::Rejected { code, message }) => println!("rejected: {code:?}: {message}"),
        Err(e) => return Err(e.into()),
    }
    Ok(())
}
```

### 准入码

双向流会返回以下之一：

| 代码  | 名称               | 含义                    |
| --- | ---------------- | --------------------- |
| `0` | ok               | 已接受。交易已被跟踪，并正在竞速送达领导者 |
| `1` | unauthorized     | 该连接的密钥无效              |
| `2` | rate limited     | 您的密钥超出了每秒限制           |
| `3` | invalid          | 交易未通过合法性检查。消息会说明哪项未通过 |
| `4` | no tip           | 未找到向 Apex 小费账户的转账     |
| `5` | below floor      | 小费低于您所在等级的下限。消息中会注明下限 |
| `6` | busy             | 端点正在卸载负载。请稍后重试        |
| `7` | malformed packet | 数据包的帧格式错误             |

对该数据包而言，拒绝是最终结果。在响应到达之前发生的传输错误可以安全重试，因为端点按签名去重。

### 在其他语言中使用 QUIC

您可以使用任何支持客户端证书的 QUIC 库，在任意语言中实现该传输层。

**连接**

* QUIC (RFC 9000)，使用 TLS 1.3 和 ALPN `solana-tpu`。
* 服务器证书是自签名的占位证书。不要验证它。
* 客户端必须出示[由您的 API 密钥派生的证书](/cn/apex/authentication#quic-客户端证书)。
* 每个 Apex 端点保持一个打开的连接。定期发送 QUIC PING（Rust 客户端每秒 ping 一次）。端点的空闲超时为 30 秒。已启用 0-RTT。

**交易数据包**

打开一个流，恰好写入一个数据包，然后结束该流：

```text theme={null}
u64 LE   length of the transaction bytes
bytes    the serialized transaction: legacy or v0 at most 1232 bytes, v1 at most 4096
u8       mev_protect: 1 to skip Shield-blocklisted leaders, else 0
u8       max_retries present: 0 or 1
u16 LE   max_retries, only if present
```

这正是对 `{ wire_transaction: Vec<u8>, mev_protect: bool, max_retry: Option<u16> }` 使用 bincode 默认的定长整数小端序选项进行编码的结果。整个数据包最大不得超过 4160 字节。

**准入帧**（仅限双向流）

```text theme={null}
u8       status: one of the admission codes above
[64]     status 0 only: the transaction's first signature
u16 LE   status != 0: message length
bytes    status != 0: UTF-8 message
```

在 Rust 中，`wire::encode_packet` 和 `wire::decode_admission` 是这两种帧的参考实现。
