Solana[part2]_Solana开发入门

Solana[part2]_Solana开发入门

虚拟机

Solana的执行环境与其他区块链存在显著差异,其虚拟机架构是实现高吞吐量的核心基础之一:

  • EVM(Ethereum Virtual Machine):以太坊等区块链采用的虚拟机,基于栈式架构,Solana不直接支持EVM,但可通过跨链桥或兼容层(如Neon EVM)实现EVM合约迁移。
  • WASM(WebAssembly):通用二进制指令格式,Solana早期曾考虑采用,但最终选择了更轻量的方案。
  • Sealevel VM:Solana原生虚拟机,支持并行执行(区别于EVM的串行执行),是Solana高TPS(每秒交易数)的关键,可同时处理数千个独立合约调用。
  • BPF扩展指令集:Sealevel VM基于BPF(Berkeley Packet Filter)扩展,具有高效、安全、低资源占用的特点,适合区块链场景的沙箱执行环境。

项目搭建

1. Native Rust开发(原生开发方式)

直接使用Rust编写Solana合约,适合深入理解底层机制。

环境准备

  • 安装Rust:curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  • 安装Solana CLI:sh -c "$(curl -sSfL https://release.solana.com/v1.18.4/install)"(版本号可按需更新)
  • 配置网络:solana config set --url https://api.devnet.solana.com(使用devnet测试网)
  • 获取测试网SOL(用于支付gas):solana airdrop 1(每次最多1 SOL,不足可重复申请)

步骤详解

  1. 创建项目

    cargo new --lib native-rust && cd native-rust
    cargo add solana-program  # 添加Solana合约核心依赖
  2. 修改编译配置
    编辑Cargo.toml,指定输出为动态链接库(Solana合约要求):

    # 顶部添加(解决edition2024兼容问题)
    cargo-features = ["edition2024"]
    
    [package]
    name = "native-rust"
    version = "0.1.0"
    edition = "2024"  # 需与cargo-features对应
    
    [lib]
    crate-type = ["cdylib", "lib"]  # 输出为动态链接库
    
    [dependencies]
    solana-program = "1.18.0"  # 建议指定明确版本
  3. 编写基础合约
    编辑src/lib.rs,实现合约入口函数(Solana合约必须包含entrypoint!宏定义的入口):

    use solana_program::{
        entrypoint,
        account_info::AccountInfo,
        entrypoint::ProgramResult,
        msg,  // 用于日志输出(链上可见)
        pubkey::Pubkey,
    };
    
    // 定义入口函数
    entrypoint!(process_instruction);
    
    /// 合约核心逻辑
    /// 参数说明:
    /// - program_id: 合约本身的公钥
    /// - accounts: 本次交互涉及的账户列表
    /// - instruction_data: 传入的指令数据(二进制)
    fn process_instruction(
        program_id: &Pubkey,
        accounts: &[AccountInfo],
        instruction_data: &[u8],
    ) -> ProgramResult {
        // 链上日志(可通过solana logs查看)
        msg!("Hello Solana!");
        msg!("Program ID: {}", program_id);
        msg!("Accounts involved: {:?}", accounts.len());
        msg!("Instruction data length: {}", instruction_data.len());
        
        Ok(())  // 返回成功结果
    }
  4. 构建合约
    使用Solana的BPF编译器编译(生成.so文件):

    cargo build-sbf  # 编译为Solana兼容的BPF格式

    编译成功后,输出文件路径:target/sbpf-solana-solana/release/native_rust.so

  5. 部署与验证

    • 部署到测试网:

      solana program deploy target/sbpf-solana-solana/release/native_rust.so

      部署成功后会返回Program Id: <YOUR_PROGRAM_ID>(记录此ID用于后续交互)。

    • 验证部署:

      solana program show <YOUR_PROGRAM_ID>  # 查看合约状态
  6. 合约管理

    • 升级合约(需提前配置升级权限):
      solana program upgrade <new_so_file> <YOUR_PROGRAM_ID> --upgrade-authority <AUTHORITY_KEYPAIR>
    • 关闭合约(回收存储空间,需合约所有者权限):
      solana program close <YOUR_PROGRAM_ID>

常见问题

  • 编译报错feature edition2024 is required:确保Cargo.toml顶部添加了cargo-features = ["edition2024"]
  • 部署失败Insufficient funds:通过solana airdrop 1获取更多测试网SOL。
  • 日志无法查看:使用solana logs --url https://api.devnet.solana.com监听链上日志。

2. Playground(在线开发工具)

适合快速原型开发,无需本地环境配置,推荐新手入门使用。

操作步骤

  1. 访问在线IDE:Solana Playground
  2. 点击New Project → 选择Solana Program,自动生成基础框架。
  3. 编辑src/lib.rs(同Native方式的合约逻辑)。
  4. 连接钱包:点击左侧Build & DeployConnect Wallet(推荐使用Phantom钱包,切换到devnet)。
  5. 构建部署:点击Build编译,成功后点击Deploy,等待部署完成(需支付少量gas)。
  6. 查看部署详情:部署成功后,左下角会显示Program ID,点击链接可在Solana Explorer中查看合约信息。

交互示例(客户端)

通过自动生成的client.ts(TypeScript)与合约交互:

import { Connection, PublicKey, Transaction, SystemProgram } from "@solana/web3.js";
import { sendTransaction } from "./utils/sendTransaction";

// 合约地址(替换为你的Program ID)
const PROGRAM_ID = new PublicKey("YOUR_PROGRAM_ID");

async function main() {
  const connection = new Connection("https://api.devnet.solana.com");
  const payer = (await window.solana.connect()).publicKey;

  // 构建交易
  const transaction = new Transaction().add({
    programId: PROGRAM_ID,
    keys: [{ pubkey: payer, isSigner: true, isWritable: false }],  // 传入 payer 账户
    data: Buffer.from([]),  // 空指令数据
  });

  // 发送交易
  const signature = await sendTransaction(transaction, connection);
  console.log("Transaction signature:", signature);
  console.log("View on Explorer: https://explorer.solana.com/tx/" + signature + "?cluster=devnet");
}

main();

点击Run执行客户端代码,在控制台查看交易结果。

3. Anchor(开发框架)

Anchor是Solana生态最流行的开发框架,简化了合约编写、测试和部署流程,内置类型安全和序列化工具。

环境安装

# 安装Anchor CLI
cargo install --git https://github.com/coral-xyz/anchor anchor-cli --locked

# 验证安装
anchor --version  # 需显示0.29.0+版本

# 安装yarn
npm install -g yarn
或者
brew install yarn # 这个会默认系统配置

项目操作

  1. 创建项目

    anchor init anchor-project && cd anchor-project

    项目结构:

    anchor-project/
    ├── programs/          # 合约代码(Rust)
    │   └── anchor_project/
    ├── tests/             # 测试代码(TypeScript)
    ├── migrations/        # 部署脚本
    ├── Anchor.toml        # 项目配置(网络、合约地址等)
    └── Cargo.toml         # 依赖管理
  2. 编写合约(以计数器为例)
    编辑programs/anchor_project/src/lib.rs

    use anchor_lang::prelude::*;
    
    // 声明程序ID(需与Anchor.toml中一致)
    declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
    
    #[program]
    pub mod anchor_project {
        use super::*;
    
        // 初始化计数器
        pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
            let counter = &mut ctx.accounts.counter;
            counter.count = 0;  // 初始值为0
            msg!("Counter initialized! Current count: {}", counter.count);
            Ok(())
        }
    
        // 增加计数
        pub fn increment(ctx: Context<Increment>) -> Result<()> {
            let counter = &mut ctx.accounts.counter;
            counter.count += 1;
            msg!("Count incremented! New count: {}", counter.count);
            Ok(())
        }
    }
    
    // 初始化时的账户约束
    #[derive(Accounts)]
    pub struct Initialize<'info> {
        // 计数器账户(需要存储数据,因此is_writable=true)
        #[account(init, payer = user, space = 8 + 8)]  // 8字节 discriminator + 8字节u64
        pub counter: Account<'info, Counter>,
        // 支付者(签名者)
        #[account(mut)]
        pub user: Signer<'info>,
        // 系统程序(用于创建账户)
        pub system_program: Program<'info, System>,
    }
    
    // 增加计数时的账户约束
    #[derive(Accounts)]
    pub struct Increment<'info> {
        #[account(mut)]  // 可写(需要修改count)
        pub counter: Account<'info, Counter>,
    }
    
    // 计数器数据结构
    #[account]
    pub struct Counter {
        pub count: u64,
    }
  3. 配置网络
    编辑Anchor.toml,指定部署网络:

    [provider]
    cluster = "devnet"  # 可选:localnet, testnet, mainnet-beta
    wallet = "~/.config/solana/id.json"  # 本地钱包路径
    
    [programs.devnet]
    anchor_project = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"  # 程序ID(需与合约中一致)
  4. 测试合约
    编辑tests/anchor_project.ts编写测试用例:

    import * as anchor from "@coral-xyz/anchor";
    import { Program } from "@coral-xyz/anchor";
    import { AnchorProject } from "../target/types/anchor_project";
    import { expect } from "chai";
    
    describe("anchor-project", () => {
      // 配置Anchor provider和program
      const provider = anchor.AnchorProvider.env();
      anchor.setProvider(provider);
    
      const program = anchor.workspace.AnchorProject as Program<AnchorProject>;
      
      // 为每个测试生成一个新的账户密钥对
      let counterKeypair: anchor.web3.Keypair;
    
      beforeEach(() => {
        counterKeypair = anchor.web3.Keypair.generate();
      });
    
      it("Initialize counter", async () => {
        // 调用initialize方法
        const tx = await program.methods.initialize()
          .accounts({
            counter: counterKeypair.publicKey,
            user: provider.wallet.publicKey,
            systemProgram: anchor.web3.SystemProgram.programId,
          })
          .signers([counterKeypair])
          .rpc();
        
        console.log("Your transaction signature", tx);
    
        // 获取counter账户并验证初始值
        const counter = await program.account.counter.fetch(counterKeypair.publicKey);
        expect(counter.count.toNumber()).to.equal(0);
        console.log("Initial count:", counter.count.toString());
      });
    
      it("Increment counter", async () => {
        // 首先初始化账户
        await program.methods.initialize()
          .accounts({
            counter: counterKeypair.publicKey,
            user: provider.wallet.publicKey,
            systemProgram: anchor.web3.SystemProgram.programId,
          })
          .signers([counterKeypair])
          .rpc();
    
        // 调用increment方法
        const tx = await program.methods.increment()
          .accounts({ 
            counter: counterKeypair.publicKey 
          })
          .rpc();
        
        console.log("Your transaction signature", tx);
    
        // 获取counter账户并验证计数增加
        const counter = await program.account.counter.fetch(counterKeypair.publicKey);
        expect(counter.count.toNumber()).to.equal(1);
        console.log("After increment:", counter.count.toString());
      });
    
      it("Increment counter multiple times", async () => {
        // 首先初始化账户
        await program.methods.initialize()
          .accounts({
            counter: counterKeypair.publicKey,
            user: provider.wallet.publicKey,
            systemProgram: anchor.web3.SystemProgram.programId,
          })
          .signers([counterKeypair])
          .rpc();
    
        // 多次调用increment方法
        await program.methods.increment()
          .accounts({ 
            counter: counterKeypair.publicKey 
          })
          .rpc();
    
        await program.methods.increment()
          .accounts({ 
            counter: counterKeypair.publicKey 
          })
          .rpc();
    
        await program.methods.increment()
          .accounts({ 
            counter: counterKeypair.publicKey 
          })
          .rpc();
    
        // 获取counter账户并验证计数
        const counter = await program.account.counter.fetch(counterKeypair.publicKey);
        expect(counter.count.toNumber()).to.equal(3);
        console.log("After 3 increments:", counter.count.toString());
      });
    });

    运行测试:

    anchor test  # 自动启动本地节点,运行测试后关闭

    测试完成:

    anchor-project
    Your transaction signature 3DBSMCVTW65CDsuPLx4jB3CosozZSjQbCurXo2KcfMSkfr2nZRHz5XQuEzEXr56bf1qY2MVsoYuQsYePwAuLLY4F
    Initial count: 0
     ✔ Initialize counter (592ms)
    Your transaction signature 36wPyPxho2rGRiRxM2ihF2cvfKENbNFKzNEr3ida4mEVeiRSmnEMbKqeg7BuTWdiDeKzd7dktn5WjbpNv4mWhNnF
    After increment: 1
     ✔ Increment counter (915ms)
    After 3 increments: 3
     ✔ Increment counter multiple times (1905ms)
     3 passing (3s)
  5. 部署合约

    anchor deploy  # 部署到Anchor.toml指定的网络
    -------------
    ➜  anchor-project anchor deploy
    Error: error sending request for url (http://127.0.0.1:8899/): error trying to connect: tcp connect error: Connection refused (os error 61)
    
    Caused by:
        0: error sending request for url (http://127.0.0.1:8899/): error trying to connect: tcp connect error: Connection refused (os error 61)
        1: error trying to connect: tcp connect error: Connection refused (os error 61)
        2: tcp connect error: Connection refused (os error 61)
        3: Connection refused (os error 61)

    部署失败,发现没有切换 devnet

    [provider]
    cluster = "devnet"                  # 可选:localnet, testnet, mainnet-beta
    wallet = "~/.config/solana/id.json" # 本地钱包路径

    部署成功后,会返回Program Id: <DEPLOYED_PROGRAM_ID>,可在Solana Explorer中查看。

    ➜  anchor-project anchor deploy
    Deploying cluster: https://api.devnet.solana.com
    Upgrade authority: /Users/tinachan/.config/solana/id.json
    Deploying program "anchor_project"...
    Program path: /Users/tinachan/rust/anchor-project/target/deploy/anchor_project.so...
    Program Id: HDvugLuzT7XtdaskGkYw9DJZqNLtDLeVXfMeET9bne9y
    
    Signature: 4ArrCsQQu4zCU7455yeoo3L5LvTiWngXq3gMCHoA42Z25tSHgMrVKPiQd4yhSvu37n28cwWPysrDiMAMfA21BNfF
    
    Deploy success

    image-20250805231022756

优势总结

  • 自动处理账户序列化/反序列化(无需手动处理二进制数据)。
  • 内置测试框架,支持TypeScript测试合约。
  • 简化账户权限管理(通过#[derive(Accounts)]约束)。
  • 自动生成客户端SDK(target/types/目录下),方便前端集成。

踩坑

  1. 程序ID不匹配问题
    • Rust代码中的[declare_id!](javascript:void(0))与Anchor.toml中的程序ID不一致
    • 我们将Rust代码中的程序ID从Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS更新为5vhtLSUBXoqe3a2Kxtx8tcNdTNTaXaSgFT7MGN1bWxz5,与Anchor.toml保持一致
  2. 测试环境配置问题
    • 将Anchor.toml中的[cluster](javascript:void(0))从devnet改为localnet,这样测试可以在本地运行而不需要连接到Devnet
  3. 断言比较问题
    • Solana程序返回的数字是BN(BigNumber)类型,而我们之前直接与JavaScript数字比较
    • 通过使用.toNumber()方法将BN类型转换为普通数字进行比较,解决了断言失败问题
  4. 依赖安装
    • 确保安装了chai依赖以支持测试断言 npm install chai

总结

  • Native Rust:适合深入理解Solana底层,灵活性高但开发效率低。
  • Playground:适合快速验证想法,无需配置环境,适合新手入门。
  • Anchor:生产环境首选,大幅提升开发效率,生态工具丰富。