Solana 学习计划 - 第二阶段:开发入门
目录
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
📚 本章总结
核心要点
- ✅ Anchor框架极大简化了Solana程序开发
- ✅ Accounts、Instructions、CPIs是Anchor核心概念
- ✅ 使用
anchor test进行本地测试 - ✅ 使用
anchor deploy部署到Devnet - ✅ 前端通过@solana/web3.js调用程序
关键代码
| 功能 | 代码 |
|---|---|
| 定义账户 | #[derive(Accounts)] |
| 定义指令 | #[program] + pub fn xxx() |
| 账户约束 | init, mut, payer, space |
| 部署 | anchor deploy |
| 前端调用 | program.methods.xxx().accounts({...}).rpc() |
➡️ 下一步
第三阶段:进阶开发
核心内容:
- SPL Token标准
- 创建自定义代币
- PDA深入理解
- 权限和安全
准备好了吗?