客户端开发
设置 Anchor 客户端
Solana 程序部署后,你需要构建前端(Web/Mobile)或后端服务来与它交互。Anchor 极大地简化了这一过程,它能根据程序的 IDL(接口定义语言)自动生成 TypeScript 类型定义和 SDK。
通常的项目结构如下:
src/ ├── anchor/ │ ├── solana_university_counter.json # IDL (编译器生成的 JSON) │ └── solana_university_counter.ts # 类型定义 (anchor idl type 生成) └── app.ts
创建 Provider
Provider 是 Anchor 客户端的核心抽象,它整合了:
- Connection: 与 RPC 节点的连接。
- Wallet: 用户的钱包(负责签名)。
- Options: 确认级别 (Commitment) 等配置。
tsximport { AnchorProvider, setProvider, Program } from "@coral-xyz/anchor"; import { Connection, clusterApiUrl } from "@solana/web3.js"; import { useAnchorWallet, useConnection } from "@solana/wallet-adapter-react"; // 在 React 组件中 function useProgram() { const { connection } = useConnection(); const wallet = useAnchorWallet(); const provider = new AnchorProvider(connection, wallet, { commitment: "confirmed", }); // 设置为全局 Provider (可选,方便测试) setProvider(provider); // 初始化 Program 实例 // IDL 是你的程序接口定义 return new Program(IDL, provider); }
调用指令 (Methods)
Anchor 使用链式调用风格构建指令,非常直观:
tsximport { Program } from "@coral-xyz/anchor"; import { SolanaUniversityCounter } from "./solana_university_counter"; async function initializeCounter(program: Program<SolanaUniversityCounter>) { // 生成一个新的密钥对作为 Counter 账户 const counter = Keypair.generate(); const tx = await program.methods .initialize() // 方法名 (camelCase) .accounts({ // 传入账户 (只需传入关键账户,系统账户等通常会自动推断) counter: counter.publicKey, authority: program.provider.publicKey, systemProgram: SystemProgram.programId, }) .signers([counter]) // 额外的签名者 (Payer 默认已包含) .rpc(); // 发送交易 console.log("交易签名:", tx); return counter.publicKey; }
获取账户数据 (Account Fetch)
Anchor 自动处理账户数据的反序列化(Borsh 解码):
tsx// 获取单个账户 const counter = await program.account.counter.fetch(counterAddress); console.log("当前计数:", counter.count.toNumber()); // 注意:u64 会转为 BN.js 对象 // 获取多个账户 const counters = await program.account.counter.fetchMultiple([ address1, address2, ]); // 获取所有账户 (慎用,类似 SQL 的 SELECT *) const allCounters = await program.account.counter.all(); // 带过滤器获取 (服务端过滤,更高效) const filteredCounters = await program.account.counter.all([ { memcmp: { offset: 8 + 8, // 跳过鉴别器(8) 和 count(8) bytes: authorityAddress.toBase58(), // 查找特定 authority 的计数器 }, }, ]);
构建交易 (Transaction Builder)
有时你不想立即发送交易(例如为了让用户手动确认,或者为了组合多个指令)。
tsx// 获取 Transaction 对象 const tx = await program.methods .increment() .accounts({ counter: counterAddress, authority: wallet.publicKey, }) .transaction(); // 返回 Transaction 对象 // 手动签名和发送 tx.feePayer = wallet.publicKey; tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; // 如果使用钱包适配器 const signedTx = await wallet.signTransaction(tx); const signature = await connection.sendRawTransaction(signedTx.serialize());
组合多个指令
实现原子操作(Atomic Operations):
tsximport { Transaction } from "@solana/web3.js"; // 创建指令 A const ix1 = await program.methods .increment() .accounts({ counter: counter1, authority: wallet.publicKey }) .instruction(); // 返回 TransactionInstruction 对象 // 创建指令 B const ix2 = await program.methods .increment() .accounts({ counter: counter2, authority: wallet.publicKey }) .instruction(); // 组合到一个交易包中 const tx = new Transaction().add(ix1, ix2); await sendTransaction(tx, connection);
监听事件 (Events)
程序端发出事件:
rust#[event] pub struct CounterUpdated { pub counter: Pubkey, pub old_value: u64, pub new_value: u64, } // 在指令中: emit!(CounterUpdated { ... });
客户端实时监听:
tsxconst listenerId = program.addEventListener("counterUpdated", (event, slot) => { console.log("计数器更新:"); console.log(" 地址:", event.counter.toString()); console.log(" 旧值:", event.oldValue.toNumber()); console.log(" 新值:", event.newValue.toNumber()); console.log(" Slot:", slot); }); // 停止监听 await program.removeEventListener(listenerId);
处理 PDA
在客户端计算 PDA 地址同样重要:
tsximport { PublicKey } from "@solana/web3.js"; // 派生 PDA (必须与 Rust 端的 seeds 逻辑一致) const [vaultAddress, bump] = PublicKey.findProgramAddressSync( [ Buffer.from("vault"), wallet.publicKey.toBuffer() ], program.programId ); // 使用 PDA await program.methods .deposit(new BN(1000000)) .accounts({ vault: vaultAddress, // 传入计算出的地址 depositor: wallet.publicKey, systemProgram: SystemProgram.programId, }) .rpc();