2026 年要在 Solana 上写程序,你几乎肯定会用 Anchor。它是目前主流的开发框架,比直接用原始 Solana SDK 高效很多,生态也成熟。
但 Anchor 用起来太顺手,反而会把一些在生产环境很关键的 RPC 层细节藏在背后——你不知道它做了什么,等出问题才发现。
这篇文章讲清楚三件事:你调用 Anchor 程序时,底层到底发生了什么;Anchor 的哪些抽象容易让你踩坑;以及哪些做法能让你的 Anchor + RPC 集成稳定可靠,而不是表面能跑、上了量就崩。
Anchor 到底是什么
Anchor 不是一个东西,而是一整套:
- Rust 框架——写 Solana 程序时减少样板代码
- 宏系统——做账户验证
- IDL(接口定义语言)生成器——描述你程序的接口
- 客户端 SDK——基于 IDL 给客户端代码提供类型化访问
- CLI 工具——做构建、部署、测试
跟"客户端怎么跟 RPC 集成"这件事最相关的,是 IDL 加客户端 SDK 这两块。Anchor 会从你程序的 IDL 自动生成 TypeScript 或 Rust 客户端代码,然后帮你处理账户派生、指令序列化、交易构造。
Anchor 客户端调用是怎么跑的
你写的代码长这样:
const tx = await program.methods
.yourInstruction(arg1, arg2)
.accounts({
accountA: pdaA,
accountB: pdaB,
})
.signers([signer])
.rpc();
底层实际发生的是:
- Anchor 根据 IDL 把指令数据序列化
- Anchor 校验你传的账户是否符合 IDL 的要求
- Anchor 用这条指令构造一笔 Transaction
- Transaction 通过底层的 Connection 签名并提交
- Anchor 等确认、把签名返回给你
末尾的 .rpc() 做的就是标准的 sendTransaction 工作。Anchor 给你的"便利"在于:你不用一个字节一个字节地手动构造指令。
这种便利把什么东西藏起来了
把 Anchor 当成全自动魔法看,会踩到的几个常见问题:
账户校验在客户端就发生了。 Anchor 会校验你传的账户是否匹配 IDL。如果你传错了顺序或者读写标志,客户端在提交前就抛错。这点其实是好事——能早抓 bug;但你不知情的话会被吓一跳。
交易构造细节被藏起来了。 你看不到 recentBlockhash、feePayer、compute budget 这些字段。Anchor 用了一套合理的默认值,但默认值不总是对你这种场景最优。
.rpc() 给你的控制权很有限。 它直接用 connection 上的默认值——skipPreflight、重试逻辑都按默认走。生产代码经常需要显式控制这些参数。
确认行为是有立场的。 Anchor 默认等到 confirmed commitment 才返回。延迟敏感的代码可能想要不一样的行为。
对大多数用例来说,这种便利是净赚的。但对有具体性能或可靠性要求的生产代码,有时候就得绕开 .rpc()、走更底层的路径。
生产里的做法:拿回低层控制权
不用 .rpc(),自己构造交易、通过你想用的那个 RPC 提交:
import { ComputeBudgetProgram } from "@solana/web3.js";
// 用 Anchor 构建交易(但不立即发送)
const tx = await program.methods
.yourInstruction(arg1, arg2)
.accounts({ accountA: pdaA, accountB: pdaB })
.transaction();
// 显式加上 compute budget
tx.add(
ComputeBudgetProgram.setComputeUnitLimit({ units: 250_000 }),
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 100_000 })
);
// 拿新 blockhash、签名
tx.recentBlockhash = (await writeConnection.getLatestBlockhash("confirmed")).blockhash;
tx.feePayer = wallet.publicKey;
tx.sign(signer);
// 通过你自己挑的写 RPC 提交
const signature = await writeConnection.sendRawTransaction(
tx.serialize(),
{
skipPreflight: true,
maxRetries: 0,
}
);
这种写法代码多一些,但换来的是:
- compute budget 显式可控
- blockhash 管理在你手里
- 通过最适合你负载的 RPC 提交,而不是固定走 provider
- 发送选项你说了算
如果你的 Anchor 程序跟交易场景挂钩——DEX、借贷协议、任何对"上链速度"敏感的场景——这就是该走的模式。
Anchor + Anti-MEV
Anchor 程序里凡是涉及 swap 或者其他方向交易的调用,都跟普通 Solana 交易一样能被夹。框架本身不处理 MEV 保护——这一层在 RPC。
正确做法是:把 Anti-MEV 写 RPC 当作 provider 用的 connection 传进去。Anchor 负责构造指令,RPC 负责路由保护。
import { Connection } from "@solana/web3.js";
import { AnchorProvider, Program } from "@coral-xyz/anchor";
const writeConnection = new Connection(
"https://bolttx.io/?api-key=YOUR_API_KEY",
"processed"
);
// wallet 必须实现 Anchor 的 Wallet 接口(signTransaction、signAllTransactions、publicKey)
const provider = new AnchorProvider(writeConnection, wallet, {
commitment: "confirmed",
skipPreflight: true,
});
// Anchor v0.30+: new Program(idl, provider);programId 从 IDL 里拿
// Anchor v0.29 及之前: new Program(idl, programId, provider)
const program = new Program(idl, provider);
PDA 计算和缓存
Anchor 程序大量用 PDA。算 PDA 是确定性操作,但不免费——findProgramAddressSync 内部会循环试 bump,有 CPU 开销。频繁访问的 PDA 应该缓存:
class PdaCache {
private cache = new Map<string, PublicKey>();
getPda(seeds: Buffer[]): PublicKey {
const key = seeds.map(s => s.toString("hex")).join("|");
let pda = this.cache.get(key);
if (!pda) {
[pda] = PublicKey.findProgramAddressSync(seeds, programId);
this.cache.set(key, pda);
}
return pda;
}
}
每次单独算一个 PDA 看起来不贵,但在高频代码路径里累加起来不可忽略。
错误处理模式
Anchor 抛出的错误是结构化的、带错误码的。正确的处理方式:
try {
const tx = await program.methods.yourInstruction().rpc();
} catch (e) {
if (e.error?.errorCode?.code === "ConstraintRaw") {
// 这是某条 Anchor 约束失败
} else if (e.error?.errorCode?.code === "AccountDidNotDeserialize") {
// 账户数据反序列化失败
} else {
// 其他通用错误
}
}
错误的结构在 Anchor 框架文档里有完整说明。生产代码应该针对不同错误类型做不同处理,而不是一个 catch 全部吞掉。
订阅 Anchor 程序事件
Anchor 程序能发出结构化事件,客户端可以直接订阅:
const listener = program.addEventListener(
"YourEventName",
(event, slot) => {
// event 是按 IDL 类型化好的事件对象,直接拿字段就行
handleEvent(event, slot);
}
);
// 用完记得清理
await program.removeEventListener(listener);
这比自己解析原始交易日志干净得多,事件驱动架构里很好用。
常见的 Anchor RPC 集成错误
生产代码直接用 .rpc()。 改用 .transaction() + 手动提交,这样发送选项才在你掌控里。
Anchor 调用没设 compute budget。 Anchor 不会自动加 compute budget 指令,要你自己显式加上。
没设 priority fee。 同上,默认不会替你加。
完全信任 Anchor 默认的 commitment。 延迟敏感的代码必须显式选 commitment 等级。
没意识到账户校验在客户端就做了。 账户传错时交易会在提交前就失败——这是好事,但你没准备就会一脸懵。
忽略 IDL 版本。 程序升级后 IDL 会变,你客户端代码必须同步更新,不然行为对不上。
这周可以做什么
如果你正在搭一个调 Anchor 程序的客户端:
- 生产代码用
.transaction()构建交易,不要用.rpc()。 - 显式加上 compute budget 指令。
- 显式设 priority fee。 别让默认值替你做决定。
- 写侧用专门优化交易提交的 RPC,尤其是涉及交易的程序。
- 用事件订阅,不要轮询状态变化。
- 频繁访问的 PDA 都缓存起来。
- 针对具体的 Anchor 错误码做处理。 一把抓的 catch-all 会把真 bug 藏起来。
Anchor 程序提交侧试一下 BoltTx
import { Connection } from "@solana/web3.js";
import { AnchorProvider, Program } from "@coral-xyz/anchor";
const writeConnection = new Connection(
"https://bolttx.io/?api-key=YOUR_API_KEY",
"processed"
);
const provider = new AnchorProvider(writeConnection, wallet, {
commitment: "confirmed",
skipPreflight: true,
});
// Anchor v0.30+ 写法
const program = new Program(idl, provider);
const tx = await program.methods.yourMethod(args)
.accounts({...})
.transaction();
// ... 加 compute budget、设 blockhash、签名、提交 ...
原生 Anti-MEV 路由,意味着涉及 swap 的程序调用在提交环节不会被夹。免费档注册。
常见问题
必须用 Anchor 吗? 不一定。原生写 Solana 程序也完全可以。大多数现代项目选 Anchor 主要是为了开发效率。
Anchor 自带的测试框架怎么样?
anchor test 做集成测试很合适。要跑得更快的单元测试,Bankrun 是更好的选择。
Anchor 客户端能跟非 Anchor 程序配合用吗? 只要你能写出或拿到对应的 IDL,就能用 Anchor 客户端跟任何程序交互。这种用法不常见,但可行。
IDL 应该缓存还是每次都重新拉? 缓存。IDL 只在程序升级时才会变,平时没必要每次都拉。
Anchor 调用应该用哪个 commitment?
大多数情况下选 "confirmed"。如果你的代码延迟敏感、又能容忍偶尔 reorg,可以用 "processed"。