Solana[part10]_Anchor实战:创建用户profile

Solana[part10]_Anchor实战:创建用户profile

一、项目架构概览

1.1 整体架构

anchor_social/
├── programs/               # Solana 智能合约源代码
│   └── anchor_social/      # 核心合约模块
│       ├── src/              # Rust 源代码
│       │   ├── lib.rs        # 程序入口和指令路由
│       │   ├── instructions/ # 指令处理逻辑
│       │   └── state/        # 链上状态定义
│       └── Cargo.toml        # Rust 项目配置
├── app/                    # 前端交互代码
│   ├── api/                  # API 接口模块
│   │   ├── wallet.ts         # 钱包管理
│   │   └── profile.ts        # 用户资料接口
│   └── index.ts            # 主程序入口
├── tests/                  # 测试模块
├── migrations/             # 合约部署脚本
├── Anchor.toml             # Anchor 项目配置
├── package.json            # 前端依赖配置
└── tsconfig.json           # TypeScript 配置

1.2 技术栈

  • 区块链平台: Solana
  • 开发框架: Anchor v0.31.1
  • 智能合约语言: Rust
  • 前端交互: TypeScript
  • 依赖管理:
    • 前端: yarn
    • 合约: Cargo

二、核心模块详解

2.1 智能合约模块 (programs/anchor_social)

2.1.1 程序入口 (lib.rs)

declare_id!("35vQtxXXv5rb99eiVrVVrwYMRYc7vscZvXas8zjEnnK5");

pub mod instructions;
pub mod state;
use instructions::*;

#[program]
pub mod anchor_social {
    use super::*;

    pub fn create_profile(ctx: Context<CreateProfile>, display_name: String) -> Result<()> {
        instructions::profile::create_profile(ctx, display_name)
    }
}
  • declare_id!: 定义程序地址
  • #[program]: Anchor 宏标记程序入口
  • 指令路由: 将 create_profile 指令路由到 instructions 模块

2.1.2 指令处理 (instructions/profile.rs)

pub fn create_profile(ctx: Context<CreateProfile>, display_name: String) -> Result<()> {
    ctx.accounts.profile.display_name = display_name;
    Ok(())
}

#[derive(Accounts)]
pub struct CreateProfile<'info> {
    #[account(init, payer = user, space = 8 + Profile::INIT_SPACE, seeds = [Profile::SEED_PREFIX.as_bytes(), user.key().as_ref()], bump)]
    pub profile: Account<'info, Profile>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}
  • 业务逻辑: 简单设置显示名称
  • 账户约束:
    • init: 创建新账户
    • payer = user: 指定支付账户
    • seeds: PDA 计算种子
    • mut: 表示可变账户

2.1.3 状态定义 (state/profile.rs)

#[account]
#[derive(InitSpace)]
pub struct Profile {
    #[max_len(20)]
    pub display_name: String,
}
impl Profile {
    pub const SEED_PREFIX: &'static str = "profile";
}
  • #[account]: 标记为链上账户结构
  • #[derive(InitSpace)]: 自动生成空间计算代码
  • #[max_len(20)]: 字符串长度限制

2.2 前端交互模块 (app/)

2.2.1 钱包管理 (api/wallet.ts)

// 默认钱包
export function useDefaultWallet() {
    return anchor.Wallet.local();
}

// 访客钱包
export function useVisitorWallet() {
    const keypair = anchor.web3.Keypair.fromSecretKey(new Uint8Array([...]));
    return new anchor.Wallet(keypair);
}
  • 钱包类型:
    • local(): 使用本地环境变量钱包
    • Keypair: 通过私钥创建密钥对
    • Wallet: 包装签名功能
  • 重要约束:
    • 需要 payer 属性进行交易签名
    • 必须处理异步签名操作

2.2.2 用户资料接口 (api/profile.ts)

export async function createProfile(wallet: anchor.Wallet, displayName: string) {
    return await program.methods.createProfile(displayName).accounts({
        user: wallet.publicKey,
    })
    .signers([wallet.payer])  // 必须显式传递签名者
    .rpc();
}

export async function getProfile(wallet: anchor.Wallet) {
    const [profilePda,] = anchor.web3.PublicKey.findProgramAddressSync(
        [Buffer.from("profile"), wallet.publicKey.toBuffer()],
        program.programId,
    );
    return await program.account.profile.fetch(profilePda);
}
  • 交易构建:
    • accounts: 绑定账户参数
    • signers: 指定额外签名者
    • rpc(): 发送交易
  • PDA 计算:
    • 使用种子生成确定性账户地址
    • 保证账户地址一致性

2.2.3 主程序入口 (index.ts)

const defaultWallet = useDefaultWallet();
const visitorWallet = useVisitorWallet();

// 创建用户资料
async function createProfiles() {
    try {
        await createProfile(defaultWallet, "Bob");
        await createProfile(visitorWallet, "Alice");
    } catch (error) {
        console.error("创建资料失败:", error.message);
    }
}

// 获取用户资料
async function fetchProfiles() {
    try {
        console.log("默认钱包资料:", await getProfile(defaultWallet));
        console.log("访客钱包资料:", await getProfile(visitorWallet));
    } catch (error) {
        console.error("获取资料失败:", error.message);
    }
}
  • 异步处理: 使用 try-catch 捕获异常
  • 流程控制: 分离创建和查询操作
  • 日志记录: 详细输出执行结果

三、关键实现细节

3.1 钱包签名机制

class NodeWallet {
    constructor(payer) {
        this.payer = payer;  // Keypair 实例
    }

    async signTransaction(tx) {
        if (isVersionedTransaction(tx)) {
            tx.sign([this.payer]);  // 版本化交易
        } else {
            tx.partialSign(this.payer);  // 传统交易
        }
        return tx;
    }
}
  • 签名流程:
    1. 检测交易版本
    2. 根据版本选择签名方式
    3. 返回签名后的交易

3.2 PDA 账户生成

const [profilePda, bump] = PublicKey.findProgramAddressSync(
    [Buffer.from("profile"), wallet.publicKey.toBuffer()],
    program.programId
);
  • 生成规则:
    1. 种子由常量和用户地址组成
    2. 使用程序ID作为程序标识
    3. 确保地址唯一性和可预测性

3.3 交易错误处理

try {
    await createProfile(wallet, "Alice");
} catch (error) {
    console.log("错误详情:", {
        message: error.message,
        logs: error.logs,  // 包含链上日志
        errorCode: error.errorCode
    });
}
  • 错误分析要点:
    • 查看完整日志堆栈
    • 检查链上返回码
    • 验证账户状态

四、常见问题及解决方案

4.1 类型错误 (TS2345)

错误信息:

Argument of type 'Keypair' is not assignable to parameter of type 'Wallet'

解决方案:

// 错误方式
return Keypair.fromSecretKey(...)

// 正确方式
return new anchor.Wallet(Keypair.fromSecretKey(...))

4.2 账户不存在错误

错误信息:

Account does not exist or has no data

解决方案:

  1. 验证钱包地址:
solana address -k ~/.config/solana/t3.json
  1. 空投 SOL:
solana airdrop 1 <WALLET_ADDRESS>
  1. 确认 PDA 计算正确性:
console.log("预期PDA:", profilePda.toBase58())

4.3 交易签名错误

错误信息:

Transfer: `from` must not carry data

解决方案:

// 确保正确传递签名者
.signers([wallet.payer])

五、开发最佳实践

  1. 模块化开发:

    • 将指令和状态分离到不同文件
    • 使用 pub(crate) 控制可见性
  2. 错误处理:

    • 使用 anyhow 或 thiserror 简化错误处理
    • 添加详细的日志记录
  3. 测试策略:

    • 单元测试: 使用 anchor 的测试框架
    • 集成测试: 使用本地网络测试完整流程
    • 边界测试: 验证输入长度限制
  4. 安全性建议:

    • 避免硬编码密钥
    • 使用 Anchor 的账户约束
    • 验证所有输入参数