Solana入门:区块链新手速成指南(第二阶段:开发入门)

Solana 学习计划 - 第二阶段:开发入门

目录

  1. Anchor框架简介
  2. 编写第一个Anchor程序
  3. 测试与部署
  4. 前端集成
  5. 实战项目

1. Anchor框架简介

1.1 为什么使用Anchor?

对比项 原生Rust开发 Anchor框架
安全性 需手动验证账户 自动验证和约束检查
开发效率 代码量大 简洁的IDL定义
易用性 学习曲线陡峭 类似以太坊的DSL
测试 复杂 内置测试框架

Anchor是一个专为Solana设计的开发框架,大大简化了程序开发。

1.2 Anchor核心概念

Accounts
rust 复制代码
// 定义一个账户结构
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = user, space = 8 + 8)]
    pub my_account: Account<'info, MyData>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}
Instructions
rust 复制代码
// 定义指令处理器
#[program]
pub mod my_program {
    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        // 初始化逻辑
        ctx.accounts.my_account.data = 42;
        Ok(())
    }
}
CPI(跨程序调用)
rust 复制代码
// 调用其他程序
ctx.accounts.system_program.transfer(
    CPIContext::new(
        ctx.accounts.system_program.to_account_info(),
        Transfer {
            from: ctx.accounts.user.to_account_info(),
            to: ctx.accounts.recipient.to_account_info(),
        },
    ),
    amount,
)?;

1.3 安装Anchor

bash 复制代码
# 方法1:npm安装(推荐)
npm install -g @coral-xyz/anchor-cli

# 方法2:源码安装
cargo install --git https://github.com/coral-xyz/anchor anchor-cli

# 验证安装
anchor --version
# 输出:anchor-cli 0.30.0

1.4 创建Anchor项目

bash 复制代码
# 初始化新项目
anchor init my-first-program

# 进入项目目录
cd my-first-program

# 查看项目结构
ls -la

项目结构

复制代码
my-first-program/
├── Anchor.toml           # 配置文件
├── programs/
│   └── my-first-program/
│       ├── Cargo.toml
│       └── src/
│           └── lib.rs    # 程序主代码
├── app/                  # 前端代码
│   └── src/
├── tests/                # 测试文件
├── migrations/          # 部署脚本
└── package.json

2. 编写第一个Anchor程序

2.1 程序结构

programs/my-first-program/src/lib.rs

rust 复制代码
use anchor_lang::prelude::*;

declare_id!("GENEreseC8H9eTWn4Rxj8F8R6sQEa5W6vMjTLZ4U5JqN1");

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        msg!("初始化程序,data设为0");
        ctx.accounts.my_account.data = 0;
        Ok(())
    }

    pub fn update(ctx: Context<Update>, new_data: u64) -> Result<()> {
        msg!("更新数据为: {}", new_data);
        ctx.accounts.my_account.data = new_data;
        Ok(())
    }

    pub fn increment(ctx: Context<Update>) -> Result<()> {
        msg!("数据加1");
        ctx.accounts.my_account.data += 1;
        Ok(())
    }
}

// 初始化指令的账户约束
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = user, space = 8 + 8)]
    pub my_account: Account<'info, MyAccount>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

// 更新指令的账户约束
#[derive(Accounts)]
pub struct Update<'info> {
    #[account(mut)]
    pub my_account: Account<'info, MyAccount>,
    pub user: Signer<'info>,
}

// 账户数据结构
#[account]
pub struct MyAccount {
    pub data: u64,
}

2.2 Anchor.toml配置

toml 复制代码
[features]
seeds = false
skip-lint = false

[programs.mainnet]
my_first_program = "GENEreseC8H9eTWn4Rxj8F8R6sQEa5W6vMjTLZ4U5JqN1"

[programs.devnet]
my_first_program = "GENEreseC8H9eTWn4Rxj8F8R6sQEa5W6vMjTLZ4U5JqN1"

[registry]
url = "https://apricot.xyzu.dev"

[provider]
wallet = "~/.config/solana/id.json"
cluster = "devnet"

[test]
start_date = "2024-01-01"

[test.validator]
url = "https://api.devnet.solana.com"

2.3 关键概念详解

Space计算
rust 复制代码
#[account(init, payer = user, space = 8 + 8)]
pub my_account: Account<'info, MyAccount>,

// 8字节 = Anchor账户的 discriminator
// 8字节 = MyAccount结构的大小 (u64 = 8字节)
PDA(程序派生地址)
rust 复制代码
// 使用PDA生成确定性地址
#[account(
    init,
    payer = user,
    space = 8 + 8,
    seeds = [b"my_seed", user.key.as_ref()],
    bump
)]
pub my_account: Account<'info, MyAccount>,
权限约束
约束 说明
#[account(init)] 初始化新账户
#[account(mut)] 账户可修改
payer = user 由谁支付创建费用
space = N 账户空间大小
seeds PDA种子
bump PDA碰撞值

3. 测试与部署

3.1 本地测试

bash 复制代码
# 运行测试
anchor test

# 输出示例:
#   my_first_program
#     ✓ initializes successfully (210ms)
#     ✓ updates data successfully (100ms)
#     ✓ increments data (100ms)
#
#   3 passing (410ms)

3.2 测试代码示例

tests/my-first-program.ts

typescript 复制代码
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { MyFirstProgram } from "../target/types/my_first_program";
import { assert } from "chai";

describe("my_first_program", () => {
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);

  const program = anchor.workspace.MyFirstProgram as Program<MyFirstProgram>;

  it("initializes successfully", async () => {
    // 生成PDA
    const myAccountKey = anchor.web3.PublicKey.findProgramAddressSync(
      [Buffer.from("my_account")],
      program.programId
    )[0];

    // 调用initialize指令
    await program.methods
      .initialize()
      .accounts({
        myAccount: myAccountKey,
        user: provider.wallet.publicKey,
        systemProgram: anchor.web3.SystemProgram.programId,
      })
      .rpc();

    // 验证账户数据
    const account = await program.account.myAccount.fetch(myAccountKey);
    assert.equal(account.data.toNumber(), 0);
  });

  it("updates data successfully", async () => {
    const myAccountKey = anchor.web3.PublicKey.findProgramAddressSync(
      [Buffer.from("my_account")],
      program.programId
    )[0];

    // 调用update指令
    await program.methods
      .update(new anchor.BN(42))
      .accounts({
        myAccount: myAccountKey,
        user: provider.wallet.publicKey,
      })
      .rpc();

    // 验证更新后的数据
    const account = await program.account.myAccount.fetch(myAccountKey);
    assert.equal(account.data.toNumber(), 42);
  });

  it("increments data", async () => {
    const myAccountKey = anchor.web3.PublicKey.findProgramAddressSync(
      [Buffer.from("my_account")],
      program.programId
    )[0];

    // 调用increment指令
    await program.methods.increment().accounts({
      myAccount: myAccountKey,
      user: provider.wallet.publicKey,
    }).rpc();

    // 验证递增后的数据
    const account = await program.account.myAccount.fetch(myAccountKey);
    assert.equal(account.data.toNumber(), 43);
  });
});

3.3 部署到Devnet

bash 复制代码
# 1. 构建程序
anchor build

# 2. 部署到Devnet
anchor deploy --provider.cluster devnet

# 输出示例:
# Deploying program my_first_program...
# Deploy complete. TX: 4xKm...8Vkq
# Program ID: GENEreseC8H9eTWn4Rxj8F8R6sQEa5W6vMjTLZ4U5JqN1

3.4 部署后操作

bash 复制代码
# 查看程序信息
anchor show

# 输出:
# my_first_program
#   Program ID: GENEreseC8H9eTWn4Rxj8F8R6sQEa5W6vMjTLZ4U5JqN1
#   Provider: devnet

4. 前端集成

4.1 项目初始化

bash 复制代码
# 创建React应用
npx create-react-app my-dapp --template typescript
cd my-dapp

# 安装依赖
npm install @solana/web3.js @coral-xyz/anchor @solana/wallet-adapter-react
npm install @ant-design/icons antd

4.2 连接钱包

App.tsx

typescript 复制代码
import React, { useEffect, useState } from "react";
import {
  Connection, PublicKey, LAMPORTS_PER_SOL
} from "@solana/web3.js";
import {
  WalletProvider, ConnectionProvider
} from "@solana/wallet-adapter-react";
import { PhantomWalletAdapter } from "@solana/wallet-adapter-wallets";

const network = "https://api.devnet.solana.com";
const App = () => {
  const wallets = [new PhantomWalletAdapter()];

  return (
    <ConnectionProvider endpoint={network}>
      <WalletProvider wallets={wallets} autoConnect>
        <Dashboard />
      </WalletProvider>
    </ConnectionProvider>
  );
};

4.3 调用程序

typescript 复制代码
import { useAnchorProgram } from "./hooks/useAnchorProgram";

const Dashboard = () => {
  const { program, wallet, connect } = useAnchorProgram();
  const [data, setData] = useState<number>(0);

  // 初始化
  const initialize = async () => {
    if (!program || !wallet) return;

    const [myAccountKey] = PublicKey.findProgramAddressSync(
      [Buffer.from("my_account")],
      program.programId
    );

    await program.methods
      .initialize()
      .accounts({
        myAccount: myAccountKey,
        user: wallet.publicKey,
        systemProgram: SystemProgram.programId,
      })
      .rpc();
  };

  // 更新数据
  const updateData = async (newData: number) => {
    if (!program || !wallet) return;

    const [myAccountKey] = PublicKey.findProgramAddressSync(
      [Buffer.from("my_account")],
      program.programId
    );

    await program.methods
      .update(new anchor.BN(newData))
      .accounts({
        myAccount: myAccountKey,
        user: wallet.publicKey,
      })
      .rpc();

    // 读取更新后的数据
    const account = await program.account.myAccount.fetch(myAccountKey);
    setData(account.data.toNumber());
  };

  // 递增
  const increment = async () => {
    if (!program || !wallet) return;

    const [myAccountKey] = PublicKey.findProgramAddressSync(
      [Buffer.from("my_account")],
      program.programId
    );

    await program.methods.increment().accounts({
      myAccount: myAccountKey,
      user: wallet.publicKey,
    }).rpc();

    const account = await program.account.myAccount.fetch(myAccountKey);
    setData(account.data.toNumber());
  };

  return (
    <div className="dashboard">
      <h1>我的第一个DApp</h1>
      <p>当前数据: {data}</p>
      
      {!wallet ? (
        <button onClick={connect}>连接钱包</button>
      ) : (
        <div>
          <button onClick={initialize}>初始化</button>
          <button onClick={() => updateData(100)}>更新为100</button>
          <button onClick={increment}>+1</button>
        </div>
      )}
    </div>
  );
};

4.4 获取余额

typescript 复制代码
const getBalance = async () => {
  if (!wallet) return;
  
  const connection = new Connection(network);
  const balance = await connection.getBalance(wallet.publicKey);
  
  console.log(`余额: ${balance / LAMPORTS_PER_SOL} SOL`);
};

5. 实战项目

5.1 项目目标

开发一个简单的代币计数器DApp

  • 初始化一个计数器(初始值0)
  • 点击按钮递增计数器
  • 显示当前计数值
  • 显示钱包余额

5.2 项目结构

复制代码
my-counter-dapp/
├── programs/
│   └── counter/
│       ├── Cargo.toml
│       └── src/
│           └── lib.rs
├── app/
│   ├── src/
│   │   ├── App.tsx
│   │   ├── Counter.tsx
│   │   └── hooks/
│   │       └── useCounter.ts
│   └── package.json
├── tests/
│   └── counter.ts
└── Anchor.toml

5.3 核心代码

Counter程序 (lib.rs)

rust 复制代码
use anchor_lang::prelude::*;

declare_id!("CounteRB5V5GG2oBJ8Qs5xQj5cR3z6K6fQe9zJxL4MnN4");

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        ctx.accounts.counter.count = 0;
        ctx.accounts.counter.bump = ctx.bumps.counter;
        Ok(())
    }

    pub fn increment(ctx: Context<Increment>) -> Result<()> {
        ctx.accounts.counter.count += 1;
        Ok(())
    }

    pub fn decrement(ctx: Context<Increment>) -> Result<()> {
        ctx.accounts.counter.count -= 1;
        Ok(())
    }

    pub fn reset(ctx: Context<Increment>) -> Result<()> {
        ctx.accounts.counter.count = 0;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = user, space = 8 + 8 + 1, seeds = [b"counter"], bump)]
    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, seeds = [b"counter"], bump)]
    pub counter: Account<'info, Counter>,
    pub user: Signer<'info>,
}

#[account]
pub struct Counter {
    pub count: i64,
    pub bump: u8,
}

5.4 前端组件

App.tsx

typescript 复制代码
import React from "react";
import { WalletProvider, ConnectionProvider } from "@solana/wallet-adapter-react";
import { PhantomWalletAdapter } from "@solana/wallet-adapter-wallets";
import { Counter } from "./Counter";
import "./App.css";

const network = "https://api.devnet.solana.com";

function App() {
  const wallets = [new PhantomWalletAdapter()];

  return (
    <ConnectionProvider endpoint={network}>
      <WalletProvider wallets={wallets} autoConnect>
        <div className="App">
          <h1>🧮 Solana Counter</h1>
          <Counter />
        </div>
      </WalletProvider>
    </ConnectionProvider>
  );
}

export default App;

Counter.tsx

typescript 复制代码
import React, { useState, useEffect } from "react";
import { useWallet, useConnection } from "@solana/wallet-adapter-react";
import {
  PublicKey, LAMPORTS_PER_SOL, SystemProgram
} from "@solana/web3.js";
import { useAnchorProgram } from "./hooks/useCounter";

export const Counter = () => {
  const { publicKey, signTransaction, connected } = useWallet();
  const { connection } = useConnection();
  const program = useAnchorProgram();
  const [count, setCount] = useState(0);
  const [balance, setBalance] = useState(0);
  const [loading, setLoading] = useState(false);

  // 查找PDA地址
  const [counterPDA] = PublicKey.findProgramAddressSync(
    [Buffer.from("counter")],
    program?.programId || PublicKey.default
  );

  // 获取余额
  useEffect(() => {
    if (!publicKey) return;

    const getBalance = async () => {
      const bal = await connection.getBalance(publicKey);
      setBalance(bal / LAMPORTS_PER_SOL);
    };

    getBalance();
    const interval = setInterval(getBalance, 10000);
    return () => clearInterval(interval);
  }, [publicKey, connection]);

  // 获取计数器数据
  useEffect(() => {
    if (!program || !publicKey) return;

    const getCount = async () => {
      try {
        const account = await program.account.counter.fetch(counterPDA);
        setCount(account.count.toNumber());
      } catch {
        setCount(0);
      }
    };

    getCount();
  }, [program, publicKey, counterPDA]);

  // 初始化计数器
  const initialize = async () => {
    if (!program || !publicKey || !signTransaction) return;
    setLoading(true);

    try {
      await program.methods
        .initialize()
        .accounts({
          counter: counterPDA,
          user: publicKey,
          systemProgram: SystemProgram.programId,
        })
        .rpc();
      
      const account = await program.account.counter.fetch(counterPDA);
      setCount(account.count.toNumber());
    } catch (err) {
      console.error("初始化失败:", err);
    } finally {
      setLoading(false);
    }
  };

  // 递增
  const increment = async () => {
    if (!program || !publicKey) return;
    setLoading(true);

    try {
      await program.methods.increment().accounts({
        counter: counterPDA,
        user: publicKey,
      }).rpc();

      const account = await program.account.counter.fetch(counterPDA);
      setCount(account.count.toNumber());
    } catch (err) {
      console.error("递增失败:", err);
    } finally {
      setLoading(false);
    }
  };

  // 重置
  const reset = async () => {
    if (!program || !publicKey) return;
    setLoading(true);

    try {
      await program.methods.reset().accounts({
        counter: counterPDA,
        user: publicKey,
      }).rpc();

      setCount(0);
    } catch (err) {
      console.error("重置失败:", err);
    } finally {
      setLoading(false);
    }
  };

  if (!connected) {
    return <div className="connect-wallet">请连接钱包开始</div>;
  }

  return (
    <div className="counter">
      <div className="balance">余额: {balance.toFixed(4)} SOL</div>
      <div className="count-display">{count}</div>
      <div className="buttons">
        <button onClick={initialize} disabled={loading}>
          初始化
        </button>
        <button onClick={increment} disabled={loading}>
          +1
        </button>
        <button onClick={reset} disabled={loading}>
          重置
        </button>
      </div>
    </div>
  );
};

5.5 运行项目

bash 复制代码
# 1. 启动后端(Anchor程序)
cd programs/counter
anchor deploy --provider.cluster devnet

# 2. 启动前端
cd app
npm install
npm start

# 3. 打开浏览器
# http://localhost:3000

📚 本章总结

核心要点

  1. ✅ Anchor框架极大简化了Solana程序开发
  2. ✅ Accounts、Instructions、CPIs是Anchor核心概念
  3. ✅ 使用anchor test进行本地测试
  4. ✅ 使用anchor deploy部署到Devnet
  5. ✅ 前端通过@solana/web3.js调用程序

关键代码

功能 代码
定义账户 #[derive(Accounts)]
定义指令 #[program] + pub fn xxx()
账户约束 init, mut, payer, space
部署 anchor deploy
前端调用 program.methods.xxx().accounts({...}).rpc()

➡️ 下一步

第三阶段:进阶开发

核心内容:

  • SPL Token标准
  • 创建自定义代币
  • PDA深入理解
  • 权限和安全

准备好了吗?

相关推荐
Rust语言中文社区3 小时前
【Rust日报】2026-04-24 Vizia 0.4 发布——纯 Rust 声明式响应式 GUI 框架
开发语言·后端·rust
潇楠Web3哨兵8 小时前
桌面级Web3交易终端的底层炼狱:自研多源报价引擎、移除重型依赖、跨进程钱包桥接与强制安全拦截
算法·web3
techdashen9 小时前
用自家产品构建自家产品:Cloudflare Images 的工程架构解析
开发语言·架构·rust
恋喵大鲤鱼10 小时前
RUST 的特色概念与 Go 到 Rust 的思维模式转变
rust
光影少年12 小时前
vite+rust生态链工具链
开发语言·前端·后端·rust·前端框架
techdashen13 小时前
服务不停,升级照常:Cloudflare 是怎么做到零中断重启的
开发语言·rust
Rust研习社13 小时前
Reqwest 兼顾简洁与高性能的现代 HTTP 客户端
开发语言·网络·后端·http·rust
Bczheng11 天前
二十.读写交易索引和验证交易
区块链