客户端开发
大多数 dApp 使用 TypeScript 与已部署的 Solana 程序交互。了解如何在客户端集成您的程序是构建功能性应用程序的关键。
Anchor Client SDK
Anchor 通过一个与您的程序结构相匹配的接口描述语言(IDL)文件简化了与 Solana 程序的客户端交互。
结合 Anchor 的 TypeScript 库(@coral-xyz/anchor),IDL 提供了一种简化的方式来构建指令和交易。
设置
@coral-xyz/anchor 包在创建 Anchor 程序时会自动安装。在运行 anchor build 后,Anchor 会生成:
- 位于
target/idl/<program-name>.json的 IDL - 位于
target/types/<program-name>.ts的 TypeScript SDK
这些文件抽象了许多底层的复杂性。使用以下结构将它们传输到您的 TypeScript 客户端:
src
├── anchor
│ ├── <program-name>.json
│ └── <program-name>.ts
└── integration.ts
要将钱包适配器与 Anchor 的 TypeScript SDK 一起使用,请创建一个 Provider 对象,该对象结合了连接(localhost、devnet 或 mainnet)和钱包(支付和签署交易的地址)。
设置钱包和连接:
import { useAnchorWallet, useConnection } from "@solana/wallet-adapter-react";
const { connection } = useConnection();
const wallet = useAnchorWallet();创建 Provider 对象并将其设置为默认值:
import { AnchorProvider, setProvider } from "@coral-xyz/anchor";
const provider = new AnchorProvider(connection, wallet, {
commitment: "confirmed",
});
setProvider(provider);
程序
Anchor 的 Program 对象为与 Solana 程序交互创建了一个自定义 API。此 API 是所有链上程序通信的核心接口:
- 发送交易,
- 获取反序列化的账户,
- 解码指令数据,
- 订阅账户更改,
- 监听事件
通过导入类型和IDL创建Program对象:
import <program-name> from "./<program-name>.json";
import type { <Program-Type> } from "./<program-name>.ts";
import { Program, Idl } from "@coral-xyz/anchor";
const program = new Program(<program-name> as <Program-Type>);如果您尚未设置默认提供者,请显式指定:
const program = new Program(<program-name> as <Program-Type>, provider);配置完成后,使用Anchor方法构建器来构建指令和交易。MethodsBuilder 使用IDL提供了一种简化的格式,用于构建调用程序指令的交易。
基本的MethodsBuilder模式:
await program.methods
.instructionName(instructionDataInputs)
.accounts({})
.signers([])
.rpc();通过.signers()传递提供者之外的额外签名者。
账户
使用点语法在MethodsBuilder上调用.accounts,并传递一个对象,其中包含指令根据IDL期望的每个账户。
交易
通过Anchor发送交易的默认方法是.rpc(),它将交易直接发送到区块链。
对于需要后端签名的场景(例如在前端使用用户的钱包创建交易,然后使用后端密钥对安全签名),请使用.transaction():
const transaction = await program.methods
.instructionName(instructionDataInputs)
.accounts({})
.transaction();
//... Sign the transaction in the backend
// Send the transaction to the chain
await sendTransaction(transaction, connection);要捆绑多个Anchor指令,请使用.instruction()获取指令对象:
// Create first instruction
const instructionOne = await program.methods
.instructionOneName(instructionOneDataInputs)
.accounts({})
.instruction();
// Create second instruction
const instructionTwo = await program.methods
.instructionTwoName(instructionTwoDataInputs)
.accounts({})
.instruction();
// Add both instructions to one transaction
const transaction = new Transaction().add(instructionOne, instructionTwo);
// Send transaction
await sendTransaction(transaction, connection);Fetch and Filter Accounts
当您的程序创建数百个账户时,跟踪它们会变得具有挑战性。Program对象提供了高效获取和筛选程序账户的方法。
获取特定账户类型的所有地址:
const accounts = await program.account.counter.all();使用memcmp标志筛选特定账户:
const accounts = await program.account.counter.all([
{
memcmp: {
offset: 8,
bytes: bs58.encode(new BN(0, "le").toArray()),
},
},
]);要检查账户数据是否发生变化,可以使用 fetch 获取特定账户的反序列化账户数据:
const account = await program.account.counter.fetch(ACCOUNT_ADDRESS);同时获取多个账户:
const accounts = await program.account.counter.fetchMultiple([
ACCOUNT_ADDRESS_ONE,
ACCOUNT_ADDRESS_TWO,
]);事件和 Webhooks
与其每次用户连接钱包时都获取链上数据,不如设置监听区块链的系统,并将相关数据存储到数据库中。
监听链上事件主要有两种方法:
- 轮询:客户端以一定的时间间隔反复检查新数据。服务器无论数据是否发生变化都会返回最新数据,可能会返回重复信息。
- 流式传输:服务器仅在更新发生时将数据推送到客户端。这种方式更高效,能够实时传输数据,因为只传输相关的变化。
对于流式传输 Anchor 指令,可以使用监听事件的 webhooks,并在事件发生时将其发送到您的服务器。例如,每当您的市场上发生 NFT 销售时,更新数据库条目。
Anchor 提供了两个用于发出事件的宏:
emit!():通过sol_log_data()系统调用直接将事件发出到程序日志中,事件数据以 base64 字符串编码,并以 "Program Data" 为前缀。emit_cpi!():通过跨程序调用(CPI)发出事件。事件数据被编码并包含在 CPI 的指令数据中,而不是程序日志中。
emit!() 宏
程序实现:
use anchor_lang::prelude::*;
declare_id!("8T7MsCZyzxboviPJg5Rc7d8iqEcDReYR2pkQKrmbg7dy");
#[program]
pub mod event {
use super::*;
pub fn emit_event(_ctx: Context<EmitEvent>, input: String) -> Result<()> {
emit!(CustomEvent { message: input });
Ok(())
}
}
#[derive(Accounts)]
pub struct EmitEvent {}
#[event]
pub struct CustomEvent {
pub message: String,
}使用 Anchor SDK 辅助工具进行 base64 解码的客户端事件监听:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Event } from "../target/types/event";
describe("event", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Event as Program<Event>;
it("Emits custom event", async () => {
// Set up listener before sending transaction
const listenerId = program.addEventListener("customEvent", event => {
// Process the event data
console.log("Event Data:", event);
});
});
});emit_cpi!() 宏
程序实现:
use anchor_lang::prelude::*;
declare_id!("2cDQ2LxKwQ8fnFUz4LLrZ157QzBnhPNeQrTSmWcpVin1");
#[program]
pub mod event_cpi {
use super::*;
pub fn emit_event(ctx: Context<EmitEvent>, input: String) -> Result<()> {
emit_cpi!(CustomEvent { message: input });
Ok(())
}
}
#[event_cpi]
#[derive(Accounts)]
pub struct EmitEvent {}
#[event]
pub struct CustomEvent {
pub message: String,
}客户端事件解码:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { EventCpi } from "../target/types/event_cpi";
describe("event-cpi", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.EventCpi as Program<EventCpi>;
it("Emits custom event", async () => {
// Fetch the transaction data
const transactionData = await program.provider.connection.getTransaction(
transactionSignature,
{ commitment: "confirmed" },
);
// Decode the event data from the CPI instruction data
const eventIx = transactionData.meta.innerInstructions[0].instructions[0];
const rawData = anchor.utils.bytes.bs58.decode(eventIx.data);
const base64Data = anchor.utils.bytes.base64.encode(rawData.subarray(8));
const event = program.coder.events.decode(base64Data);
console.log(event);
});
});