返回 SC 笔记
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


今日目标

  1. 理解 Anchor 事件系统#[event] 宏、emit!)的原理和使用
  2. 理解 IDL(Interface Definition Language) 的作用和结构
  3. 使用 @coral-xyz/anchor TypeScript 客户端与程序交互
  4. 掌握 Solana 前端集成的完整工作流

核心概念

1. Solana 事件(Events)

在以太坊中,事件(Events)通过 emit 关键字触发,存储在交易的 logs 中。Solana 的事件机制类似但实现不同。

EVM vs Solana 事件对比

维度EVM EventsSolana 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 客户端通过以下方式解析:

  1. 监听程序的交易日志
  2. 识别以事件 discriminator(前 8 字节哈希)开头的日志
  3. 使用 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 ABIAnchor IDL
格式JSONJSON
函数参数
账户信息无(不需要)有(必须,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)

关键要点总结

  1. Anchor 事件是结构化的 Program Log: 通过 #[event] 宏定义,emit! 触发,客户端通过 IDL 自动反序列化
  2. IDL 是 Solana 程序的 "API 文档": 包含指令、账户结构、事件、错误的完整描述
  3. TypeScript 客户端是标准做法: @coral-xyz/anchor 提供了类型安全的 SDK
  4. 事件监听是实时的: addEventListener 通过 WebSocket 订阅交易日志
  5. 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 的事件索引?"

参考回答:

  1. 原生方案: 使用 getSignaturesForAddress + 解析交易日志,但效率低
  2. Geyser 插件: Solana 验证者插件,可以实时流式传输账户变更和交易日志
  3. 第三方索引: Helius、Triton 等提供类 The Graph 的索引服务
  4. Anchor Events + WebSocket: 适合实时监听,不适合历史查询
  5. 数据库方案: 自建索引器,监听交易并存入数据库

Q: "如何让非 Anchor 的前端与 Anchor 程序交互?"

参考回答: IDL 是关键。只要有 IDL,任何 TypeScript/JavaScript 客户端都可以通过 @coral-xyz/anchor 库与程序交互,无需关心程序是否用 Anchor 编写。也可以使用 @solana/web3.js 直接构建原始指令。


参考资源