> ## 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 Go 客户端

> orbitflare-apex-go 是 OrbitFlare Apex 的 Go 客户端：支持 0-RTT 重连的持久 QUIC 连接，以及用于 JSON-RPC、批量发送和捆绑包的 HTTP 辅助工具。

`orbitflare-apex-go` 是 Apex 的 Go 客户端。它为每个 Apex 端点保持一个持久 QUIC 连接，使用由您的 API 密钥派生的客户端证书进行身份验证，每个流发送一笔序列化后的交易，并在连接断开时以 0-RTT 重连。它的功能与 [Rust 客户端](/cn/apex/rust-client)相同，其客户端证书与 Rust 客户端的逐字节一致。

## 安装

该模块为 `github.com/orbitflare/orbitflare-apex-go`；源码托管在 [GitHub](https://github.com/orbitflare/orbitflare-apex-go)：

```bash theme={null}
go get github.com/orbitflare/orbitflare-apex-go@latest
go get github.com/gagliardetto/solana-go@v1.24.0
```

该模块包含两个包：

| 包 | 导入路径 | 内容 |
| - | - | - |
| `apex` | `github.com/orbitflare/orbitflare-apex-go` | QUIC 客户端、区域、小费以及线路格式 |
| `rpc` | `github.com/orbitflare/orbitflare-apex-go/rpc` | 通过 HTTP 访问 Apex 端点，以及适用于任意 Solana RPC 的 blockhash 和确认辅助工具 |

交易使用 [`solana-go`](https://github.com/gagliardetto/solana-go) 构建，它支持 legacy、v0 和 [v1](/cn/apex/transaction-v1) 消息。QUIC 路径不依赖 `rpc` 包。

该模块需要 Go 1.26 或更高版本。

## 快速开始

一个完整的程序。它构建一笔带有计算预算和小费的 memo 交易，通过 QUIC 发送，并等待确认。它读取与[快速入门](/cn/apex/quickstart)相同的环境变量：`APEX_API_KEY`、`KEYPAIR_PATH` 和 `SOLANA_RPC_URL`。

```go main.go theme={null}
package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"time"

	"github.com/gagliardetto/solana-go"
	computebudget "github.com/gagliardetto/solana-go/programs/compute-budget"

	apex "github.com/orbitflare/orbitflare-apex-go"
	"github.com/orbitflare/orbitflare-apex-go/rpc"
)

func getenv(name, fallback string) string {
	if v := os.Getenv(name); v != "" {
		return v
	}
	return fallback
}

func main() {
	ctx := context.Background()
	apiKey := os.Getenv("APEX_API_KEY")
	payer, err := solana.PrivateKeyFromSolanaKeygenFile(getenv("KEYPAIR_PATH", "payer.json"))
	if err != nil {
		log.Fatal(err)
	}
	region := apex.Frankfurt

	client, err := apex.Connect(ctx, region, apiKey)
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	tipAccounts, err := rpc.New(region, apiKey).GetTipAccounts(ctx)
	if err != nil {
		log.Fatal(err)
	}
	tipAccount, _ := apex.PickTipAccount(tipAccounts)

	solanaRPC := rpc.NewSolanaRPC(getenv("SOLANA_RPC_URL", "https://api.mainnet-beta.solana.com"))
	blockhash, err := solanaRPC.LatestBlockhash(ctx)
	if err != nil {
		log.Fatal(err)
	}

	memo := solana.NewInstruction(
		solana.MustPublicKeyFromBase58("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"),
		solana.AccountMetaSlice{solana.Meta(payer.PublicKey()).WRITE().SIGNER()},
		[]byte(fmt.Sprintf("apex go %d", time.Now().UnixNano())),
	)
	tx, err := solana.NewTransaction([]solana.Instruction{
		computebudget.NewSetComputeUnitLimitInstruction(100_000).Build(),
		computebudget.NewSetComputeUnitPriceInstruction(10_000).Build(),
		memo,
		apex.TipInstruction(payer.PublicKey(), tipAccount, apex.MinTipLamports),
	}, blockhash, solana.TransactionPayer(payer.PublicKey()))
	if err != nil {
		log.Fatal(err)
	}
	if _, err := tx.Sign(func(solana.PublicKey) *solana.PrivateKey { return &payer }); err != nil {
		log.Fatal(err)
	}

	sentAt := time.Now()
	signature, err := client.SendTransaction(ctx, tx)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("sent in %d us\n", time.Since(sentAt).Microseconds())

	slot, landed, err := solanaRPC.Confirm(ctx, signature.String(), 30*time.Second)
	switch {
	case err != nil:
		log.Fatal(err)
	case landed:
		fmt.Printf("landed in slot %d after %d ms: %s\n", slot, time.Since(sentAt).Milliseconds(), signature)
	default:
		fmt.Printf("not confirmed within 30 s: %s\n", signature)
	}
}
```

```bash theme={null}
go mod init apexdemo
go get github.com/orbitflare/orbitflare-apex-go@latest github.com/gagliardetto/solana-go@v1.24.0
APEX_API_KEY=... KEYPAIR_PATH=payer.json go run .
```

`SendTransaction` 在交易写入流后立即返回，耗时仅几微秒，不等待任何确认。下面的代码片段可直接放入该程序中：它们复用其中的 `ctx`、`client`、`apiKey`、`payer`、`tipAccount`、`blockhash`、`solanaRPC` 和 `tx`。请添加代码片段用到的标准库导入，例如 `errors`、`encoding/base64` 或 `sync`。

## 带应答发送

双向流会在返回前读取 Apex 的应答：交易被接受时返回签名，被拒绝时返回带有[准入码](/cn/apex/errors-and-rate-limits#quic-准入码)和消息的 `*apex.RejectedError`。它多花费一次往返，因此请在集成阶段使用，或在您需要直接获得原因时使用。

```go theme={null}
signature, err := client.SendTransactionWithResponse(ctx, tx)
var rejected *apex.RejectedError
switch {
case errors.As(err, &rejected):
	fmt.Printf("rejected: %s: %s\n", rejected.Code, rejected.Message)
case err != nil:
	log.Fatal(err)
default:
	fmt.Println("accepted:", signature)
}
```

`SendWithResponse(ctx, wire)` 对您已持有的字节执行相同操作，并返回原始的 `apex.Admission`（`Accepted`、`Signature`、`Code`、`Message`）。

## v1 交易

v1 交易最大可达 4096 字节。计算预算位于消息的 `solana.TransactionConfig` 中，而不是 ComputeBudget 指令中，并且每个未设置的限制都为 0，因此请设置计算单元上限、已加载账户数据大小上限，以及用于优先级的费用。参见 [Transaction v1](/cn/apex/transaction-v1)。

```go theme={null}
config := solana.TransactionConfig{}.
	WithComputeUnitLimit(100_000).
	WithLoadedAccountsDataSizeLimit(1024 * 1024).
	WithPriorityFee(4_000)

tx, err := solana.NewTransaction([]solana.Instruction{
	yourInstruction,
	apex.TipInstruction(payer.PublicKey(), tipAccount, apex.MinTipLamports),
}, blockhash,
	solana.TransactionPayer(payer.PublicKey()),
	solana.TransactionV1Config(config),
)
if err != nil {
	log.Fatal(err)
}
if _, err := tx.Sign(func(solana.PublicKey) *solana.PrivateKey { return &payer }); err != nil {
	log.Fatal(err)
}
signature, err := client.SendTransaction(ctx, tx)
if err != nil {
	log.Fatal(err)
}
fmt.Println("sent v1:", signature)
```

## 原始字节

如果您的交易已经序列化（来自其他签名者、其他进程或文件），请按原样发送这些字节。不会对其重新编码。

```go theme={null}
wire, err := base64.StdEncoding.DecodeString(signedBase64)
if err != nil {
	log.Fatal(err)
}
if err := client.SendTransactionBytes(ctx, wire); err != nil {
	log.Fatal(err)
}
```

`apex.EncodePacket(wire, mevProtect, maxRetries)` 返回客户端写入流的精确数据包：8 字节长度、交易本身，以及 3 字节的尾部。它可作为其他语言的参考。

## HTTP

`rpc` 包覆盖了 Apex 端点上的每一条 HTTP 路由。它通过 `x-api-key` 请求头发送密钥。

```go theme={null}
apexRPC := rpc.New(apex.Frankfurt, apiKey)

if err := apexRPC.Ping(ctx); err != nil {
	log.Fatal(err)
}

wire, err := apex.SerializeTransaction(tx)
if err != nil {
	log.Fatal(err)
}

signature, err := apexRPC.SendTransaction(ctx, wire, false, nil)
if err != nil {
	log.Fatal(err)
}
fmt.Println("JSON-RPC:", signature)

signature, err = apexRPC.SendTransactionBinary(ctx, wire, false, apex.RetryBudget(20))
if err != nil {
	var rpcErr *rpc.Error
	if errors.As(err, &rpcErr) {
		fmt.Println("rejected:", rpcErr.Message)
	}
	log.Fatal(err)
}
fmt.Println("send-bin:", signature)
```

`SendTransaction` 即 JSON-RPC `sendTransaction`，可直接替换现有代码。`SendTransactionBinary` 将原始字节提交到 `/send-bin`，这是开销最低的 HTTP 路径。最后两个参数是 `mevProtect` 和 `maxRetries`；传入 `nil` 则沿用端点默认的重试预算。

### 批量发送

`SendBatch` 在一个请求中最多发送 16 笔交易。每笔交易独立准入，结果会说明哪些被接受。

```go theme={null}
wires := make([][]byte, 0, len(txs))
for _, tx := range txs {
	wire, err := apex.SerializeTransaction(tx)
	if err != nil {
		log.Fatal(err)
	}
	wires = append(wires, wire)
}

result, err := apexRPC.SendBatch(ctx, wires, false, nil)
if err != nil {
	log.Fatal(err)
}
fmt.Printf("%d accepted, %d rejected\n", result.Accepted, result.Rejected)
for i, item := range result.Results {
	if item.Accepted {
		fmt.Println(i, "accepted:", item.Signature)
	} else {
		fmt.Println(i, "rejected:", item.Error, item.Message)
	}
}
```

### 捆绑包

`SendBundle` 发送一到四笔按顺序上链的交易，要么全部上链，要么全部不上链，其中恰好一笔带小费。轮询 `BundleStatuses`，直到捆绑包上链或失败。参见[捆绑包](/cn/apex/bundles)。

```go theme={null}
firstWire, _ := apex.SerializeTransaction(first)
secondWire, _ := apex.SerializeTransaction(second)

accepted, err := apexRPC.SendBundle(ctx, [][]byte{firstWire, secondWire})
if err != nil {
	log.Fatal(err)
}
fmt.Println("bundle", accepted.BundleID)

for range 60 {
	statuses, err := apexRPC.BundleStatuses(ctx, []string{accepted.BundleID})
	if err != nil {
		log.Fatal(err)
	}
	if len(statuses) > 0 {
		switch statuses[0].State {
		case rpc.BundleLanded:
			fmt.Println("landed in slot", *statuses[0].LandedSlot)
			return
		case rpc.BundleFailed, rpc.BundleInvalid:
			fmt.Println("did not land:", statuses[0].State)
			return
		}
	}
	time.Sleep(500 * time.Millisecond)
}
```

## 并发发送

`*apex.Client` 可安全地并发使用，每次发送都会在同一个连接上获得自己的流。只需创建一次客户端，然后可以从任意数量的 goroutine 中发送。

```go theme={null}
var wg sync.WaitGroup
for _, tx := range txs {
	wg.Add(1)
	go func() {
		defer wg.Done()
		signature, err := client.SendTransaction(ctx, tx)
		if err != nil {
			log.Println("send failed:", err)
			return
		}
		log.Println("sent", signature)
	}()
}
wg.Wait()
```

## 区域

每个 Apex 端点都是一个 `apex.Region` 常量。`apex.ParseRegion` 接受代码或城市名称，`apex.AllRegions` 列出全部区域。

| 常量 | 代码 |
| - | - |
| `apex.Frankfurt` | `fra` |
| `apex.Amsterdam` | `ams` |
| `apex.Dublin` | `dub` |
| `apex.London` | `lon` |
| `apex.NewYork` | `nyc` |
| `apex.SaltLakeCity` | `slc` |
| `apex.Singapore` | `sgp` |
| `apex.Tokyo` | `tyo` |
| `apex.Siauliai` | `sqq` |
| `apex.Global` | `global` |

```go theme={null}
region, ok := apex.ParseRegion("tokyo")
if !ok {
	log.Fatal("unknown region")
}
fmt.Println(region.Code(), region.QUICEndpoint(), region.RPCURL())
```

`QUICEndpoint()` 返回 `<code>.apex.orbitflare.com:7001`，`RPCURL()` 返回 `http://<code>.apex.orbitflare.com`。参见[端点与区域](/cn/apex/endpoints)。

## 传输方式

| | QUIC 单向 | QUIC 双向 | HTTP 二进制 | JSON-RPC |
| - | - | - | - | - |
| **调用** | `SendTransaction`, `SendTransactionBytes` | `SendTransactionWithResponse`, `SendWithResponse` | `rpc.Client.SendTransactionBinary`, `SendBatch` | `rpc.Client.SendTransaction` |
| **返回** | 签名。不读取任何返回内容 | 已接受，或拒绝代码和消息 | 签名，或带标签的错误 | 签名，或 JSON-RPC 错误 |
| **每次发送的开销** | 预热连接上的一个流 | 一个流加一次往返 | 一个携带原始字节的 HTTP 请求 | 一个在 JSON 中携带 base64 的 HTTP 请求 |
| **最适合** | 使用持久连接的机器人 | 集成、调试，以及希望直接获得原因的工具 | 批量发送 | 直接替换 `sendTransaction` 代码 |

传输方式不会改变优先级或路由。起作用的是小费。

## API

| 项 | 描述 |
| - | - |
| `apex.Connect(ctx, region, apiKey)` | 使用临时本地端口和默认选项连接 |
| `apex.ConnectWithOptions(ctx, opts, apiKey)` | 使用 [`apex.Options`](#选项) 连接 |
| `SendTransaction(ctx, tx)` | 序列化（legacy、v0 或 v1）并在单向流上发送。返回第一个签名 |
| `SendTransactionBytes(ctx, wire)` | 同上，但使用您已持有的字节。不会对其重新编码 |
| `SendTransactionWithResponse(ctx, tx)` | 双向流。被拒绝时返回 `*apex.RejectedError` |
| `SendWithResponse(ctx, wire)` | 同上，但使用字节。返回原始的 `apex.Admission` |
| `Health()`, `ReconnectsTotal()`, `ZeroRTTResumptionsTotal()`, `RemoteAddr()` | 连接状态，用于您的指标 |
| `Reconnect(ctx)`, `Close()` | 生命周期 |
| `apex.TipInstruction(payer, tipAccount, lamports)` | 构建小费转账 |
| `apex.PickTipAccount(accounts)` | 随机选择一个小费账户 |
| `apex.MinTipLamports` | 标准等级下限：1,000,000 lamports |
| `rpc.Client` | 通过 HTTP 访问 Apex 端点：`GetTipAccounts`、`SendTransaction` (JSON-RPC)、`SendTransactionBinary`、`SendBatch`、`SendBundle`、`BundleStatuses`、`Ping` |
| `rpc.SolanaRPC` | 任意 Solana RPC：`LatestBlockhash`、`Confirm(ctx, signature, timeout)` |
| `rpc.FetchVaults(ctx, solanaRPCURL)` | 从链上读取小费程序的金库账户（`rpc.TipProgramID`）。这不是小费列表：它包含端点会拒绝的未发布金库。请使用 `GetTipAccounts` 获取可供打小费的账户，而此方法仅用于检查某个账户是否为真实的金库 |
| `apex.ClientPubkey(apiKey)` | 您的 API 密钥派生出的证书密钥，与控制台上显示的一致 |
| `apex.SerializeTransaction(tx)` | 规范的线路字节，适用于 legacy、v0 和 v1 |
| `apex.EncodePacket`, `apex.FrameParts`, `apex.DecodeAdmission` | QUIC 线路格式，可作为其他语言的参考 |
| `apex.RetryBudget(n)` | 用于选项和 HTTP 调用的 `MaxRetries` 值 |

每个涉及网络的调用都接受一个 `context.Context`，因此您可以取消一次发送或为其设置截止时间。用完后请调用 `Close`；它会停止后台重连并关闭套接字。

## 选项

每个字段的零值即为默认值，因此只需设置您需要的字段：

| 字段 | 默认值 | 描述 |
| - | - | - |
| `Endpoint` | 法兰克福的 QUIC 地址 | QUIC 地址的 `host:port` |
| `MEVProtect` | `false` | 跳过 Shield 黑名单上的领导者。参见 [MEV 保护](/cn/apex/mev-protection) |
| `MaxRetries` | `nil` | 每笔交易的重试预算，以 `apex.RetryBudget(n)` 表示。`nil` 使用端点的默认值 |
| `BindAddr` | 临时端口 | 本地 UDP 绑定地址，用于防火墙白名单 |
| `ConnectTimeout` | 3 s | 握手超时 |
| `SendTimeout` | 2 s | 单次发送的超时 |
| `KeepAlive` | 1 s | QUIC PING 间隔。端点的空闲超时为 30 s |
| `DisableAutoReconnect` | `false` | 为 `false` 时，因连接已断开而失败的发送会重新连接并再发送一次 |
| `DisableProactiveReconnect` | `false` | 为 `false` 时，后台 goroutine 会在发现断线后立即重新握手，这样下一次发送就不必为此付出代价 |

```go theme={null}
client, err := apex.ConnectWithOptions(ctx, apex.Options{
	Endpoint:       apex.NewYork.QUICEndpoint(),
	BindAddr:       "0.0.0.0:47001",
	MEVProtect:     true,
	MaxRetries:     apex.RetryBudget(30),
	ConnectTimeout: 3 * time.Second,
	SendTimeout:    time.Second,
	KeepAlive:      time.Second,
}, apiKey)
if err != nil {
	log.Fatal(err)
}
defer client.Close()
```

## 保活与重连

连接通过每秒一次的 QUIC PING 保持打开，而 Apex 端点的空闲超时为 30 秒。在预热的连接上，一次发送就是打开一个流并写入一次。

如果连接断开：

* 后台 goroutine 会发现并立即重新握手，因此下一次发送通常能找到一个可用的连接。如果是端点本身关闭了连接（未知密钥、连接数过多），该 goroutine 每次尝试后都会将等待时间加倍，最长 30 秒，而不是反复强行重试。
* 重连会使用缓存的会话票据，并在握手的首批数据包中发送等待中的交易（**0-RTT**）。如果端点拒绝早期数据，客户端会在握手完成后重新发送。
* 因连接已断开而失败的发送会重新连接并重试一次。设置 `DisableAutoReconnect` 可自行处理这种情况。
* 每次重连都会重新解析主机名，因此使用 `apex.Global` 的客户端在自己所连的端点宕机时，会跟随负载均衡器切换到下一个最近的端点，无需重启。解析发生在后台重连中，而不是在发送时；如果解析失败或耗时超过两秒，客户端会保留原有地址。IP 地址形式的端点永远不会被解析。

将连接状态导出到您的指标中：

```go theme={null}
go func() {
	for range time.Tick(10 * time.Second) {
		log.Printf("apex health=%s reconnects=%d zero_rtt=%d remote=%s",
			client.Health(), client.ReconnectsTotal(), client.ZeroRTTResumptionsTotal(), client.RemoteAddr())
	}
}()
```

## 错误

错误会包装一个哨兵错误，因此 `errors.Is` 可以告诉您失败发生在哪个阶段，而 `errors.As` 可以取得带类型的错误。

| 错误 | 含义 |
| - | - |
| `*apex.RejectedError` | 端点在双向流上拒绝了交易。`Code` 和 `Message` 是[准入码](/cn/apex/errors-and-rate-limits#quic-准入码)及其原因 |
| `*apex.TooLargeError` | 超过 4096 字节。在发送任何内容之前于本地捕获 |
| `apex.ErrTimeout` | 连接或发送超时已到 |
| `apex.ErrResolve`, `ErrBind`, `ErrTLS`, `ErrConnect`, `ErrConnection`, `ErrWrite`, `ErrRead` | 传输失败 |
| `apex.ErrSerialize`, `ErrBadAdmission`, `ErrNoSignature` | 编码问题 |
| `apex.ErrClosed` | 无法结束一个流 |
| `apex.ErrClientStopped` | 在 `Close` 之后发送 |

```go theme={null}
_, err := client.SendTransaction(ctx, tx)
var tooLarge *apex.TooLargeError
switch {
case err == nil:
case errors.As(err, &tooLarge):
	fmt.Println("transaction is", tooLarge.Size, "bytes")
case errors.Is(err, apex.ErrTimeout):
	fmt.Println("timed out, the transaction may still have been sent")
case errors.Is(err, apex.ErrConnect), errors.Is(err, apex.ErrConnection):
	fmt.Println("connection problem:", err)
default:
	fmt.Println(err)
}
```

`rpc` 包在 JSON-RPC 或普通 HTTP 拒绝时返回带有代码和消息的 `*rpc.Error`，在无法解析响应时返回 `rpc.ErrBadResponse`。普通 HTTP 拒绝的代码为 0，消息为 `label: message`，其中的标签列于[错误与速率限制](/cn/apex/errors-and-rate-limits)。

## 检查您的密钥

打印您的 API 密钥派生出的证书密钥。它必须与控制台上该密钥旁显示的客户端公钥一致；如果不一致，说明密钥复制有误。

```go theme={null}
fmt.Println(apex.ClientPubkey(apiKey))
```

## 仓库中的示例

所有示例都读取 `APEX_API_KEY`、`KEYPAIR_PATH`（默认 `payer.json`）、`SOLANA_RPC_URL`，以及可选的 `APEX_REGION`、`APEX_QUIC`、`APEX_RPC`、`TIP_LAMPORTS`、`APEX_TX_VERSION`（`legacy` 或 `v1`）、`APEX_MEMO_BYTES` 和 `APEX_CU_LIMIT`。每个示例都会发送一笔带小费的 memo 交易，并报告其上链的 slot。

| 示例 | 展示内容 |
| - | - |
| `quic_send` | 单向流：最快的路径，然后通过 Solana RPC 确认 |
| `quic_send_with_response` | 双向流：已接受或被拒绝的应答，以及如何解读拒绝 |
| `rpc_send` | 通过 HTTP 使用 JSON-RPC `sendTransaction`，小费规则相同 |
| `http_binary` | `/ping`、携带原始字节的 `/send-bin`，以及包含三笔交易的 `/send-batch` |
| `raw_bytes` | 线路上的预序列化字节，以及精确的数据包布局 |
| `throughput` | 一个预热连接，N 个并发发送，每次发送的 p50 和 p99，以及上链数量 |
| `bundle` | 由两笔交易组成的原子捆绑包（其中一笔带小费），以及它上链前的状态 |
| `transfer` | 向 `TO` 转账 `LAMPORTS` 的带小费 SOL 转账，legacy 或 v1，附带准入应答 |
| `client_pubkey` | API 密钥派生出的证书密钥，用于与您的控制台进行比较 |

```bash theme={null}
git clone https://github.com/orbitflare/orbitflare-apex-go.git
cd orbitflare-apex-go
APEX_API_KEY=... KEYPAIR_PATH=payer.json SOLANA_RPC_URL=https://... \
  go run ./examples/quic_send
```

## 客户端不做的事

* 构建或签名交易，或为您选择优先费。
* 模拟或运行预检。在您和领导者之间，没有任何环节会这样做。


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.