SC Day 44
Solana/Anchor - 事件(emit!) + 前端交互(IDL/anchor-client)
### 1. Solana 事件(Events)
2026-05-14
第二阶段:框架实战 (41-48)SolanaAnchor事件IDL前端集成TypeScript
日期: 2026-05-14 方向: Solana 阶段: 第二阶段:框架实战 (41-48) 标签: #Solana #Anchor #事件 #IDL #前端集成 #TypeScript
今日目标
- 理解 Anchor 事件系统(
#[event]宏、emit!)的原理和使用 - 理解 IDL(Interface Definition Language) 的作用和结构
- 使用
@coral-xyz/anchorTypeScript 客户端与程序交互 - 掌握 Solana 前端集成的完整工作流
核心概念
1. Solana 事件(Events)
在以太坊中,事件(Events)通过 emit 关键字触发,存储在交易的 logs 中。Solana 的事件机制类似但实现不同。
EVM vs Solana 事件对比
| 维度 | EVM Events | Solana Events (Anchor) |
|---|---|---|
| 存储位置 | Transaction Logs (bloom filter 索引) | Transaction Logs(Program Log) |
| 声明方式 | event Transfer(address, uint256) | #[event] struct TransferEvent { ... } |
| 触发方式 | emit Transfer(from, amount) | emit!(TransferEvent { from, amount }) |
| 索引查询 | indexed 参数支持 topic 过滤 | 无索引,需要解析全部日志 |
| 监听方式 | contract.on("Transfer", callback) | program.addEventListener("transferEvent", callback) |
| 历史查询 | getLogs + filter | 解析历史交易的 logs |
Anchor 事件的实现原理
Anchor 事件本质上是一段特殊格式的 Program Log:
Program log: <base64 编码的事件数据>
Anchor 客户端通过以下方式解析:
- 监听程序的交易日志
- 识别以事件 discriminator(前 8 字节哈希)开头的日志
- 使用 IDL 中的事件定义反序列化数据
2. IDL(Interface Definition Language)
IDL 是 Anchor 自动生成的 JSON 格式接口描述文件,类似于 Solidity 的 ABI,但更完整。
IDL 包含的内容
{
"version": "0.1.0",
"name": "vault",
"instructions": [
{
"name": "deposit",
"accounts": [
{ "name": "depositor", "isMut": true, "isSigner": true },
{ "name": "vaultState", "isMut": true, "isSigner": false }
],
"args": [
{ "name": "amount", "type": "u64" }
]
}
],
"accounts": [
{
"name": "VaultState",
"type": {
"kind": "struct",
"fields": [
{ "name": "authority", "type": "publicKey" },
{ "name": "totalDeposited", "type": "u64" }
]
}
}
],
"events": [
{
"name": "DepositEvent",
"fields": [
{ "name": "depositor", "type": "publicKey" },
{ "name": "amount", "type": "u64" }
]
}
],
"errors": [
{ "code": 6000, "name": "ZeroAmount", "msg": "Amount must be greater than zero" }
]
}
IDL vs ABI 对比
| 维度 | Solidity ABI | Anchor IDL |
|---|---|---|
| 格式 | JSON | JSON |
| 函数参数 | 有 | 有 |
| 账户信息 | 无(不需要) | 有(必须,Solana 的核心) |
| 数据结构 | 无 | 有(完整的 account 结构) |
| 错误定义 | 有(custom errors) | 有 |
| 事件定义 | 有 | 有 |
| 生成方式 | 编译器生成 | anchor build 生成 |
| 文件位置 | artifacts/ | target/idl/ |
3. Anchor TypeScript 客户端
前端交互架构:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 前端 UI │ │ Anchor Client│ │ Solana 节点 │
│ (React/Vue) │────►│ (TS/JS) │────►│ (RPC) │
│ │ │ │ │ │
│ 用户操作 │ │ 构建交易 │ │ 执行程序 │
│ 显示结果 │◄────│ 解析返回 │◄────│ 返回结果 │
└──────────────┘ └──────────────┘ └──────────────┘
│
▼
┌──────────────┐
│ IDL JSON │
│ (接口描述) │
└──────────────┘
代码实战
1. 增强版 Vault 程序(添加完整事件)
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Mint, Token, TokenAccount, Transfer};
declare_id!("Vault222222222222222222222222222222222222222");
#[program]
pub mod vault_with_events {
use super::*;
pub fn initialize(ctx: Context<Initialize>, vault_name: String) -> Result<()> {
require!(vault_name.len() <= 32, VaultError::NameTooLong);
let vault = &mut ctx.accounts.vault_state;
vault.authority = ctx.accounts.authority.key();
vault.token_mint = ctx.accounts.token_mint.key();
vault.vault_token_account = ctx.accounts.vault_token_account.key();
vault.total_deposited = 0;
vault.depositor_count = 0;
vault.bump = ctx.bumps.vault_state;
vault.created_at = Clock::get()?.unix_timestamp;
// 发出初始化事件
emit!(VaultInitialized {
vault: ctx.accounts.vault_state.key(),
authority: vault.authority,
token_mint: vault.token_mint,
vault_name,
timestamp: vault.created_at,
});
Ok(())
}
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
require!(amount > 0, VaultError::ZeroAmount);
let vault = &mut ctx.accounts.vault_state;
let record = &mut ctx.accounts.deposit_record;
let clock = Clock::get()?;
// 如果是新存款人
let is_new_depositor = record.amount == 0 && record.depositor == Pubkey::default();
if is_new_depositor {
record.depositor = ctx.accounts.depositor.key();
record.deposit_count = 0;
vault.depositor_count += 1;
}
record.amount = record.amount.checked_add(amount)
.ok_or(VaultError::MathOverflow)?;
record.deposit_count += 1;
record.last_deposit_time = clock.unix_timestamp;
// CPI: 转 Token
let cpi_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.depositor_token_account.to_account_info(),
to: ctx.accounts.vault_token_account.to_account_info(),
authority: ctx.accounts.depositor.to_account_info(),
},
);
token::transfer(cpi_ctx, amount)?;
vault.total_deposited = vault.total_deposited
.checked_add(amount)
.ok_or(VaultError::MathOverflow)?;
// 发出存款事件(包含丰富的上下文信息)
emit!(DepositMade {
vault: ctx.accounts.vault_state.key(),
depositor: ctx.accounts.depositor.key(),
amount,
total_user_deposit: record.amount,
deposit_count: record.deposit_count,
vault_total: vault.total_deposited,
is_new_depositor,
timestamp: clock.unix_timestamp,
slot: clock.slot,
});
Ok(())
}
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
require!(amount > 0, VaultError::ZeroAmount);
let record = &mut ctx.accounts.deposit_record;
require!(record.amount >= amount, VaultError::InsufficientBalance);
record.amount = record.amount.checked_sub(amount)
.ok_or(VaultError::MathOverflow)?;
let vault = &ctx.accounts.vault_state;
let authority_key = vault.authority;
let seeds = &[
b"vault_state".as_ref(),
authority_key.as_ref(),
&[vault.bump],
];
let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.vault_token_account.to_account_info(),
to: ctx.accounts.depositor_token_account.to_account_info(),
authority: ctx.accounts.vault_state.to_account_info(),
},
&[&seeds[..]],
);
token::transfer(cpi_ctx, amount)?;
let vault_mut = &mut ctx.accounts.vault_state;
vault_mut.total_deposited = vault_mut.total_deposited
.checked_sub(amount)
.ok_or(VaultError::MathOverflow)?;
let clock = Clock::get()?;
let is_full_withdrawal = record.amount == 0;
emit!(WithdrawalMade {
vault: ctx.accounts.vault_state.key(),
depositor: ctx.accounts.depositor.key(),
amount,
remaining_deposit: record.amount,
vault_total: vault_mut.total_deposited,
is_full_withdrawal,
timestamp: clock.unix_timestamp,
slot: clock.slot,
});
Ok(())
}
}
// ===== 事件定义 =====
#[event]
pub struct VaultInitialized {
pub vault: Pubkey,
pub authority: Pubkey,
pub token_mint: Pubkey,
pub vault_name: String,
pub timestamp: i64,
}
#[event]
pub struct DepositMade {
pub vault: Pubkey,
pub depositor: Pubkey,
pub amount: u64,
pub total_user_deposit: u64,
pub deposit_count: u32,
pub vault_total: u64,
pub is_new_depositor: bool,
pub timestamp: i64,
pub slot: u64,
}
#[event]
pub struct WithdrawalMade {
pub vault: Pubkey,
pub depositor: Pubkey,
pub amount: u64,
pub remaining_deposit: u64,
pub vault_total: u64,
pub is_full_withdrawal: bool,
pub timestamp: i64,
pub slot: u64,
}
// ===== 账户结构 =====
#[account]
#[derive(InitSpace)]
pub struct VaultState {
pub authority: Pubkey,
pub token_mint: Pubkey,
pub vault_token_account: Pubkey,
pub total_deposited: u64,
pub depositor_count: u32,
pub bump: u8,
pub created_at: i64,
}
#[account]
#[derive(InitSpace)]
pub struct DepositRecord {
pub depositor: Pubkey,
pub amount: u64,
pub deposit_count: u32,
pub last_deposit_time: i64,
}
#[error_code]
pub enum VaultError {
#[msg("Amount must be greater than zero")]
ZeroAmount,
#[msg("Insufficient balance")]
InsufficientBalance,
#[msg("Math overflow")]
MathOverflow,
#[msg("Vault name too long (max 32 chars)")]
NameTooLong,
}
// ===== 账户约束(与 Day42 类似,这里省略重复部分) =====
#[derive(Accounts)]
#[instruction(vault_name: String)]
pub struct Initialize<'info> {
#[account(mut)]
pub authority: Signer<'info>,
pub token_mint: Account<'info, Mint>,
#[account(
init,
payer = authority,
space = 8 + VaultState::INIT_SPACE,
seeds = [b"vault_state", authority.key().as_ref()],
bump
)]
pub vault_state: Account<'info, VaultState>,
#[account(
init,
payer = authority,
token::mint = token_mint,
token::authority = vault_state,
seeds = [b"vault_token", authority.key().as_ref()],
bump
)]
pub vault_token_account: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>,
}
#[derive(Accounts)]
pub struct Deposit<'info> {
#[account(mut)]
pub depositor: Signer<'info>,
#[account(mut, seeds = [b"vault_state", vault_state.authority.as_ref()], bump = vault_state.bump)]
pub vault_state: Account<'info, VaultState>,
#[account(
init_if_needed,
payer = depositor,
space = 8 + DepositRecord::INIT_SPACE,
seeds = [b"deposit", vault_state.key().as_ref(), depositor.key().as_ref()],
bump
)]
pub deposit_record: Account<'info, DepositRecord>,
#[account(mut, constraint = depositor_token_account.owner == depositor.key())]
pub depositor_token_account: Account<'info, TokenAccount>,
#[account(mut, constraint = vault_token_account.key() == vault_state.vault_token_account)]
pub vault_token_account: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Withdraw<'info> {
#[account(mut)]
pub depositor: Signer<'info>,
#[account(mut, seeds = [b"vault_state", vault_state.authority.as_ref()], bump = vault_state.bump)]
pub vault_state: Account<'info, VaultState>,
#[account(
mut,
seeds = [b"deposit", vault_state.key().as_ref(), depositor.key().as_ref()],
bump,
constraint = deposit_record.depositor == depositor.key()
)]
pub deposit_record: Account<'info, DepositRecord>,
#[account(mut, constraint = depositor_token_account.owner == depositor.key())]
pub depositor_token_account: Account<'info, TokenAccount>,
#[account(mut, constraint = vault_token_account.key() == vault_state.vault_token_account)]
pub vault_token_account: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
}
2. TypeScript 前端客户端
// client/vault-client.ts
import * as anchor from "@coral-xyz/anchor";
import { Program, AnchorProvider, BN, web3 } from "@coral-xyz/anchor";
import {
createMint,
mintTo,
getOrCreateAssociatedTokenAccount,
getAccount,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import { VaultWithEvents } from "../target/types/vault_with_events";
import idl from "../target/idl/vault_with_events.json";
// ===== 配置 =====
const PROGRAM_ID = new web3.PublicKey("Vault222222222222222222222222222222222222222");
// ===== Provider 设置 =====
function getProvider(): AnchorProvider {
// 方式1: 本地测试(使用 Anchor.toml 中配置的 wallet)
const provider = AnchorProvider.env();
anchor.setProvider(provider);
return provider;
// 方式2: 浏览器钱包(React 应用中使用)
// const connection = new web3.Connection("https://api.devnet.solana.com");
// const wallet = useAnchorWallet(); // 从 @solana/wallet-adapter-react
// return new AnchorProvider(connection, wallet, {
// commitment: "confirmed",
// });
}
// ===== 辅助函数: 推导 PDA =====
function findVaultState(authority: web3.PublicKey): [web3.PublicKey, number] {
return web3.PublicKey.findProgramAddressSync(
[Buffer.from("vault_state"), authority.toBuffer()],
PROGRAM_ID
);
}
function findVaultTokenAccount(authority: web3.PublicKey): [web3.PublicKey, number] {
return web3.PublicKey.findProgramAddressSync(
[Buffer.from("vault_token"), authority.toBuffer()],
PROGRAM_ID
);
}
function findDepositRecord(
vaultState: web3.PublicKey,
depositor: web3.PublicKey
): [web3.PublicKey, number] {
return web3.PublicKey.findProgramAddressSync(
[Buffer.from("deposit"), vaultState.toBuffer(), depositor.toBuffer()],
PROGRAM_ID
);
}
// ===== 主要客户端类 =====
class VaultClient {
private program: Program<VaultWithEvents>;
private provider: AnchorProvider;
constructor() {
this.provider = getProvider();
// 从 IDL 创建 Program 实例
this.program = new Program<VaultWithEvents>(
idl as any,
PROGRAM_ID,
this.provider
);
}
/// 初始化 Vault
async initializeVault(tokenMint: web3.PublicKey, vaultName: string) {
const authority = this.provider.wallet.publicKey;
const [vaultState] = findVaultState(authority);
const [vaultTokenAccount] = findVaultTokenAccount(authority);
const tx = await this.program.methods
.initialize(vaultName)
.accounts({
authority,
tokenMint,
vaultState,
vaultTokenAccount,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: web3.SystemProgram.programId,
rent: web3.SYSVAR_RENT_PUBKEY,
})
.rpc();
console.log("Vault initialized, tx:", tx);
return tx;
}
/// 存入 Token
async deposit(amount: number, decimals: number = 6) {
const authority = this.provider.wallet.publicKey;
const [vaultState] = findVaultState(authority);
const [vaultTokenAccount] = findVaultTokenAccount(authority);
const [depositRecord] = findDepositRecord(vaultState, authority);
// 获取用户的 Token Account
const vault = await this.program.account.vaultState.fetch(vaultState);
const userAta = await getOrCreateAssociatedTokenAccount(
this.provider.connection,
(this.provider.wallet as any).payer,
vault.tokenMint,
authority
);
const amountBN = new BN(amount * 10 ** decimals);
const tx = await this.program.methods
.deposit(amountBN)
.accounts({
depositor: authority,
vaultState,
depositRecord,
depositorTokenAccount: userAta.address,
vaultTokenAccount,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: web3.SystemProgram.programId,
})
.rpc();
console.log(`Deposited ${amount} tokens, tx:`, tx);
return tx;
}
/// 提取 Token
async withdraw(amount: number, decimals: number = 6) {
const authority = this.provider.wallet.publicKey;
const [vaultState] = findVaultState(authority);
const [vaultTokenAccount] = findVaultTokenAccount(authority);
const [depositRecord] = findDepositRecord(vaultState, authority);
const vault = await this.program.account.vaultState.fetch(vaultState);
const userAta = await getOrCreateAssociatedTokenAccount(
this.provider.connection,
(this.provider.wallet as any).payer,
vault.tokenMint,
authority
);
const amountBN = new BN(amount * 10 ** decimals);
const tx = await this.program.methods
.withdraw(amountBN)
.accounts({
depositor: authority,
vaultState,
depositRecord,
depositorTokenAccount: userAta.address,
vaultTokenAccount,
tokenProgram: TOKEN_PROGRAM_ID,
})
.rpc();
console.log(`Withdrawn ${amount} tokens, tx:`, tx);
return tx;
}
/// 获取 Vault 状态
async getVaultInfo() {
const authority = this.provider.wallet.publicKey;
const [vaultState] = findVaultState(authority);
const vault = await this.program.account.vaultState.fetch(vaultState);
return {
authority: vault.authority.toString(),
tokenMint: vault.tokenMint.toString(),
totalDeposited: vault.totalDeposited.toNumber(),
depositorCount: vault.depositorCount,
createdAt: new Date(vault.createdAt.toNumber() * 1000),
};
}
/// 获取用户存款记录
async getDepositRecord(depositor?: web3.PublicKey) {
const authority = this.provider.wallet.publicKey;
const [vaultState] = findVaultState(authority);
const user = depositor || authority;
const [depositRecord] = findDepositRecord(vaultState, user);
try {
const record = await this.program.account.depositRecord.fetch(depositRecord);
return {
depositor: record.depositor.toString(),
amount: record.amount.toNumber(),
depositCount: record.depositCount,
lastDepositTime: new Date(record.lastDepositTime.toNumber() * 1000),
};
} catch {
return null; // 尚未存款
}
}
/// 监听事件(实时)
setupEventListeners() {
// 监听存款事件
const depositListener = this.program.addEventListener(
"depositMade",
(event, slot, signature) => {
console.log("=== Deposit Event ===");
console.log(" Depositor:", event.depositor.toString());
console.log(" Amount:", event.amount.toNumber());
console.log(" Total User Deposit:", event.totalUserDeposit.toNumber());
console.log(" Deposit Count:", event.depositCount);
console.log(" Vault Total:", event.vaultTotal.toNumber());
console.log(" New Depositor:", event.isNewDepositor);
console.log(" Slot:", slot);
console.log(" Signature:", signature);
}
);
// 监听提款事件
const withdrawListener = this.program.addEventListener(
"withdrawalMade",
(event, slot, signature) => {
console.log("=== Withdrawal Event ===");
console.log(" Depositor:", event.depositor.toString());
console.log(" Amount:", event.amount.toNumber());
console.log(" Remaining:", event.remainingDeposit.toNumber());
console.log(" Full Withdrawal:", event.isFullWithdrawal);
}
);
// 返回清理函数
return () => {
this.program.removeEventListener(depositListener);
this.program.removeEventListener(withdrawListener);
};
}
/// 获取历史事件(通过解析交易日志)
async getHistoricalEvents(limit: number = 20) {
const signatures = await this.provider.connection.getSignaturesForAddress(
PROGRAM_ID,
{ limit }
);
const events: any[] = [];
for (const sig of signatures) {
const tx = await this.provider.connection.getTransaction(sig.signature, {
commitment: "confirmed",
});
if (tx?.meta?.logMessages) {
// Anchor 事件以 "Program data:" 开头
const eventLogs = tx.meta.logMessages.filter(
(log) => log.startsWith("Program data:")
);
// 实际解析需要使用 EventParser
if (eventLogs.length > 0) {
events.push({
signature: sig.signature,
blockTime: sig.blockTime,
logs: eventLogs,
});
}
}
}
return events;
}
}
// ===== 使用示例 =====
async function main() {
const client = new VaultClient();
// 设置事件监听
const cleanup = client.setupEventListeners();
try {
// 查看 vault 信息
const info = await client.getVaultInfo();
console.log("Vault Info:", info);
// 存入 100 Token
await client.deposit(100);
// 查看存款记录
const record = await client.getDepositRecord();
console.log("Deposit Record:", record);
// 提取 50 Token
await client.withdraw(50);
} finally {
// 清理事件监听
cleanup();
}
}
main().catch(console.error);
3. React 组件集成示例
// components/VaultDashboard.tsx
import { useEffect, useState, useCallback } from 'react';
import { useConnection, useWallet, useAnchorWallet } from '@solana/wallet-adapter-react';
import { Program, AnchorProvider, BN, web3 } from '@coral-xyz/anchor';
import idl from '../idl/vault_with_events.json';
const PROGRAM_ID = new web3.PublicKey("Vault222222222222222222222222222222222222222");
interface VaultInfo {
totalDeposited: number;
depositorCount: number;
}
interface DepositEvent {
depositor: string;
amount: number;
timestamp: Date;
isNew: boolean;
}
export function VaultDashboard() {
const { connection } = useConnection();
const wallet = useAnchorWallet();
const [vaultInfo, setVaultInfo] = useState<VaultInfo | null>(null);
const [events, setEvents] = useState<DepositEvent[]>([]);
const [depositAmount, setDepositAmount] = useState('');
// 创建 Program 实例
const getProgram = useCallback(() => {
if (!wallet) return null;
const provider = new AnchorProvider(connection, wallet, {
commitment: 'confirmed',
});
return new Program(idl as any, PROGRAM_ID, provider);
}, [connection, wallet]);
// 加载 Vault 信息
useEffect(() => {
const program = getProgram();
if (!program || !wallet) return;
const [vaultState] = web3.PublicKey.findProgramAddressSync(
[Buffer.from("vault_state"), wallet.publicKey.toBuffer()],
PROGRAM_ID
);
program.account.vaultState.fetch(vaultState)
.then(vault => {
setVaultInfo({
totalDeposited: (vault.totalDeposited as BN).toNumber(),
depositorCount: vault.depositorCount as number,
});
})
.catch(() => setVaultInfo(null));
}, [getProgram, wallet]);
// 监听实时事件
useEffect(() => {
const program = getProgram();
if (!program) return;
const listener = program.addEventListener('depositMade', (event) => {
setEvents(prev => [{
depositor: event.depositor.toString().slice(0, 8) + '...',
amount: (event.amount as BN).toNumber() / 1e6,
timestamp: new Date((event.timestamp as BN).toNumber() * 1000),
isNew: event.isNewDepositor as boolean,
}, ...prev].slice(0, 50)); // 保留最近 50 条
});
return () => { program.removeEventListener(listener); };
}, [getProgram]);
// 存款操作
const handleDeposit = async () => {
const program = getProgram();
if (!program || !wallet || !depositAmount) return;
// ... 构建交易并发送(同上面的客户端代码)
console.log("Depositing", depositAmount, "tokens");
};
if (!wallet) {
return <div>请连接钱包</div>;
}
return (
<div className="vault-dashboard">
<h2>Vault Dashboard</h2>
{vaultInfo && (
<div className="vault-info">
<p>总存入: {vaultInfo.totalDeposited / 1e6} Token</p>
<p>存款人数: {vaultInfo.depositorCount}</p>
</div>
)}
<div className="deposit-form">
<input
type="number"
value={depositAmount}
onChange={e => setDepositAmount(e.target.value)}
placeholder="存入数量"
/>
<button onClick={handleDeposit}>存入</button>
</div>
<h3>实时事件</h3>
<div className="events-list">
{events.map((event, i) => (
<div key={i} className="event-item">
<span>{event.isNew ? '新用户' : ''}</span>
<span>{event.depositor}</span>
<span>存入 {event.amount} Token</span>
<span>{event.timestamp.toLocaleTimeString()}</span>
</div>
))}
</div>
</div>
);
}
Solana 前端集成工作流总结
开发流程:
1. 编写 Anchor 程序 (Rust)
│
▼
2. anchor build → 生成 IDL + 类型
│
▼
3. 前端导入 IDL + @coral-xyz/anchor
│
▼
4. 创建 Provider(连接 + 钱包)
│
▼
5. 创建 Program 实例(IDL + Provider)
│
▼
6. 调用 program.methods.xxx().accounts({}).rpc()
│
▼
7. 读取账户: program.account.xxx.fetch(pubkey)
│
▼
8. 监听事件: program.addEventListener(name, callback)
关键要点总结
- Anchor 事件是结构化的 Program Log: 通过
#[event]宏定义,emit!触发,客户端通过 IDL 自动反序列化 - IDL 是 Solana 程序的 "API 文档": 包含指令、账户结构、事件、错误的完整描述
- TypeScript 客户端是标准做法:
@coral-xyz/anchor提供了类型安全的 SDK - 事件监听是实时的:
addEventListener通过 WebSocket 订阅交易日志 - PDA 推导在客户端也需要做: 前端需要用相同的 seeds 计算 PDA 地址,这是 Solana 开发的一大特点
常见误区
误区1: "Solana 事件可以像以太坊一样用 indexed 过滤"
纠正: Solana 没有 indexed events。所有事件都是非索引的 Program Log。要查询特定条件的事件,需要客户端解析全部日志然后过滤。对于大量事件,建议使用 Geyser 插件或 Helius 等索引服务。
误区2: "IDL 需要手动编写"
纠正: IDL 由 anchor build 自动从 Rust 代码生成。但如果程序不是用 Anchor 写的,可能需要手动编写或使用其他工具。
误区3: "监听事件后不需要 removeEventListener"
纠正: 必须调用 removeEventListener 清理,否则 WebSocket 连接不会关闭,导致内存泄漏。在 React 中应在 useEffect 的 cleanup 函数中处理。
面试关联
Q: "Solana 上如何实现类似以太坊 The Graph 的事件索引?"
参考回答:
- 原生方案: 使用
getSignaturesForAddress+ 解析交易日志,但效率低 - Geyser 插件: Solana 验证者插件,可以实时流式传输账户变更和交易日志
- 第三方索引: Helius、Triton 等提供类 The Graph 的索引服务
- Anchor Events + WebSocket: 适合实时监听,不适合历史查询
- 数据库方案: 自建索引器,监听交易并存入数据库
Q: "如何让非 Anchor 的前端与 Anchor 程序交互?"
参考回答: IDL 是关键。只要有 IDL,任何 TypeScript/JavaScript 客户端都可以通过 @coral-xyz/anchor 库与程序交互,无需关心程序是否用 Anchor 编写。也可以使用 @solana/web3.js 直接构建原始指令。
参考资源
- Anchor Events 文档 - 官方事件指南
- Anchor Client - TypeScript 客户端
- Solana Wallet Adapter - 钱包连接库
- Helius RPC & Webhooks - Solana 索引服务
- Anchor by Example: Events - 事件示例