随着区块链技术的迅速发展,Solana 凭借其超高的 TPS 和低廉的交易费用,成为全球增长最快的公链之一。本课程将带你从零开始,系统掌握 Solana 区块链的开发核心,深入理解其账户模型、智能合约编写方法(Program)、以及与前端 DApp 的集成方式。 本课程特别适合有前端、后端或区块链基础的开发者,目标是让你具备独立开发 Solana 应用的能力,从入门到实战,全面掌握 Solana 开发技术。
作者
经验值
阅读时间
课程目标:让你从整体上理解 Solana 是什么、它的核心优势和工作机制,并了解它在区块链生态中的定位。
Solana 是什么?它解决了什么问题?
与以太坊等主流区块链的对比
Solana 如何实现高性能(核心机制:PoH + PoS)
Solana 的账户与交易基本概念
当前生态发展现状(DeFi、NFT、游戏等)
Solana 是一个高性能、高吞吐量的公链平台,目标是支持全球规模的去中心化应用运行。
开发语言:Rust / C / C++
吞吐量:官方宣称可达 65,000 TPS(每秒交易数)
区块时间:400ms ~ 600ms(远快于以太坊的 12s)
手续费:极低(一般 < 0.00001 SOL)
Solana 主要面向 DeFi、NFT、Web3 游戏、支付等对速度和成本敏感的应用。
项目 | Solana | Ethereum |
共识机制 | PoH + PoS | PoS |
TPS | 2,000+(主网平均) | ~15 |
手续费 | 极低 | 相对较高 |
合约语言 | Rust / C / C++ | Solidity |
启动年份 | 2020 | 2015 |
优势:速度快、费用低
劣势:运行要求高、曾多次宕机(目前已改进)
Solana 的创新之处在于 PoH(历史证明) —— 一种加密时钟机制,用于在节点间产生顺序共识,加快交易处理。
每个交易都有明确的时间戳
节点可以在不协商的情况下快速验证交易顺序
极大地提升了并行处理能力
在 PoH 提供排序的基础上,Solana 仍采用 PoS 来选出验证者。
Solana 采用独特的账户模型:
普通账户(user account):由用户控制,用于存放 SOL 或代币
程序账户(program account):部署智能合约的账户
数据账户(data account):合约状态的存储空间
对比以太坊:Solana 的账户是状态分离的,更适合并发处理,但学习曲线略陡。
截至 2025 年,Solana 已形成活跃的生态系统:
DeFi:Jupiter、Raydium、Orca、Drift 等
NFT:Magic Eden、Tensor、Metaplex
钱包:Phantom、Backpack、Solflare
工具链:Anchor、Solana CLI、Solana Explorer、Web3.js
Solana 是一个主打高速和低成本的公链平台
采用独创的 PoH 共识机制,实现高并发交易处理
生态系统已涵盖 DEX、NFT、钱包、支付等多个领域
阅读 Solana 官网文档:https://docs.solana.com/
安装 Phantom 钱包,切换至 Devnet 并获取测试币
浏览 Solana Explorer:https://explorer.solana.com/
课程目标:完成基础开发环境准备,掌握主流钱包的使用,并学会使用命令行工具与区块链交互,为后续合约开发打下基础。
安装 Phantom / Solflare 钱包并连接 Devnet
领取测试代币(Airdrop)并查看账户余额
使用 Solana CLI 工具进行链上交互
理解并使用常见 RPC 浏览器(solana explorer / solscan / solana.fm)
访问官网:https://phantom.app
安装浏览器插件(推荐 Chrome / Brave)
创建新钱包,备份助记词(不要截图 / 上传网盘)
切换到 Devnet:设置 → Network → Devnet
同样支持 Devnet、主网和本地钱包
界面简洁,适合做 Ledger 硬件钱包整合演示
复制钱包地址
粘贴地址,点击“Get SOL”领取
solana airdrop 2
注意:测试币只能在 Devnet 使用,不具备真实价值。
sh -c "$(curl -sSfL https://release.solana.com/stable/install)"
安装后执行:
solana --version
solana config set --url https://api.devnet.solana.com
solana-keygen new
会生成 ~/.config/solana/id.json 钱包文件
solana airdrop 2
solana balance
打开 Phantom 钱包 → 切换 Devnet → 拷贝地址
打开 solfaucet.com → 粘贴地址领取测试币
安装 CLI 工具 → 创建钱包 → Airdrop → 查看余额
打开 Solana Explorer 输入钱包地址 → 查看余额变化
使用 Solscan / Solana.fm 演示如何查看交易详情和代币信息
创建一个 Devnet 钱包并成功领取至少 1 SOL
使用 CLI 工具创建新钱包并获取测试币
在 solana.fm 中查找并分析你的交易详情
比较 Phantom 和 Solflare 钱包在体验和功能上的差异
Phantom / Solflare 是 Solana 常用钱包,推荐 Phantom 做开发调试
Devnet 提供免费的测试环境和测试币,非常适合 DApp 开发
Solana CLI 是强大的开发工具,后续开发和部署合约将频繁使用
三大区块链浏览器工具能帮助我们追踪交易、调试错误、查看合约状态
课程目标:完成 Rust 和 Anchor 框架的安装,并运行本地 Solana 节点进行合约调试,建立起完整的开发基础设施。
安装 Rust 工具链
安装 Anchor 框架与 Solana CLI
使用 Anchor 初始化第一个项目
启动本地 Devnet(solana-test-validator
)并进行交互
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
安装完成后运行:
rustc --version
cargo --version
⚠️ 注意:Rust 编译时间较长,建议使用 SSD 设备。
sh -c "$(curl -sSfL https://release.solana.com/stable/install)"
设置默认网络为 Devnet:
solana config set --url https://api.devnet.solana.com
Anchor 是 Solana 上最流行的开发框架,类似以太坊的 Hardhat 或 Truffle。
npm install -g yarn
cargo install --git https://github.com/coral-xyz/anchor anchor-cli --locked
anchor --version
创建一个新项目:
anchor init mysolanaapp
cd mysolanaapp
项目结构:
mysolanaapp/
├── Anchor.toml # Anchor 配置文件
├── programs/ # 合约源码
├── tests/ # 前端测试脚本(JavaScript)
├── migrations/ # 构建 & 部署流程定义
├── target/ # 编译输出
└── app/(可选) # DApp 前端代码目录
你已经成功搭建了一个完整的 Anchor 合约项目!
Solana 提供本地验证节点工具,用于脱链开发。
solana-test-validator
此命令将运行一个本地的区块链模拟环境,并自动提供测试币和合约部署功能。
默认启动时:
自动生成本地测试钱包
自动 Airdrop SOL
可用于部署与调用智能合约(Program)
另开终端窗口即可继续使用 CLI 与本地链交互。
在项目根目录设置:
solana config set --url http://127.0.0.1:8899
anchor build
anchor deploy
anchor test
如果本地测试失败,多半是忘了 启动 test-validator
anchor deploy
会自动完成构建、部署、注册 ID 等操作
若项目目录中不存在钱包,可以使用 solana-keygen new
创建
配置多个网络环境建议使用 .env
管理 RPC 和密钥路径
独立完成 Anchor 项目的初始化与构建
启动本地 test-validator 并部署合约
尝试修改合约名称并重新部署
在 tests/
目录中写一段 JavaScript 脚本,调用你部署的本地合约(下一课扩展)
已安装并配置 Rust、Solana CLI、Anchor 框架
学会使用 anchor init
快速创建项目骨架
了解并掌握本地模拟链 solana-test-validator
的使用
为本地部署和调试合约打下良好基础
课程目标:通过 Anchor 编写并部署一个简单的 HelloWorld 合约(Program),掌握 Solana 智能合约的结构与部署流程。
Solana Program(合约)与以太坊合约的区别
使用 Anchor 编写第一个合约逻辑
构建并部署合约到 Devnet
理解 Anchor 项目结构和 IDL 的作用
Solana 上的智能合约称为 Program,使用 Rust 编写。
所有数据必须以 Account 存储,与 EVM 的 storage 模型不同。
Program 本身是 无状态的,只能通过账户读写数据。
项目 | Solana Program | Ethereum Contract |
语言 | Rust | Solidity / Vyper |
存储 | Account-based | Contract storage |
费用 | 使用 CU(Compute Units)计费 | 使用 Gas 计费 |
部署 | Devnet / Mainnet | Testnet / Mainnet |
状态 | Program 无状态 | 合约可持有状态 |
anchor init hello-solana
cd hello-solana
use anchor_lang::prelude::*;
declare_id!("Fg6PaFpoGXkYsidMpWxTWxAdxNYdFFcc9Ad9cm9sZ69y");
#[program]
pub mod hello_solana {
use super::*;
pub fn hello(ctx: Context<Hello>) -> Result<()> {
msg!("Hello, Solana!");
Ok(())
}
}
#[derive(Accounts)]
pub struct Hello {}
模块 | 说明 |
#[program] | 主逻辑模块,定义对外方法 |
msg!() | 打印日志(链上可见) |
#[derive(Accounts)] | 验证传入账户结构 |
declare_id!() | 声明合约 ID(与部署绑定) |
solana config set --url https://api.devnet.solana.com
anchor build
生成的 .so 文件位于 target/deploy/,并生成 IDL 文件用于前端调用。
anchor deploy
部署成功后会输出:
Program Id: Fg6PaFpoGXkYsidMpWxTWxAdxNYdFFcc9Ad9cm9sZ69y
这就是你合约的唯一地址,可用于调用和查询。
使用 Explorer 查看合约详情:
打开:https://explorer.solana.com/address/你的ProgramId?cluster=devnet
你可以看到部署者、区块高度、代码大小、调用历史等。
Anchor 项目自带 tests/hello-solana.ts,可用 JavaScript 与合约交互。
示例:
it('Says hello!', async () => {
const tx = await program.methods.hello().rpc();
console.log("Your transaction signature", tx);
});
运行测试:
anchor test
修改 HelloWorld 程序,增加一个日志 Hello from {wallet address}
尝试部署多个 Program(记得更新 declare_id!()
)
查看部署的 Program 在 Solana Explorer 上的状态
使用 Anchor 测试脚本发送交易并查看链上日志
理解了 Solana Program 与传统智能合约的区别
使用 Anchor 编写了一个最简单的 HelloWorld
成功部署合约到 Devnet 并在区块浏览器查看
熟悉了 Anchor 的结构、部署流程和 CLI 使用方式
课程目标:了解 Solana 的账户模型,学习如何定义和初始化账户结构,并通过 Anchor 实现链上数据的存储与读取。
Solana 上账户的基本概念
如何使用 Anchor 定义自定义账户结构
如何初始化账户并写入数据
如何读取链上账户数据并显示日志或返回值
在 Solana 中:
类型 | 功能 |
Program Account | 部署后的合约 |
User Wallet Account | 用户钱包(Signer) |
PDA(Program Derived Address) | 程序控制的数据账户 |
Data Account | 用于储存链上状态的账户(合约变量) |
所有合约状态都必须写入账户(通常是 PDA)。
我们将创建一个名为 MyAccount 的账户,它存储一个 u64 类型的数值,并提供初始化和修改方法。
use anchor_lang::prelude::*;
declare_id!("你的ProgramID");
#[program]
pub mod myapp {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let my_account = &mut ctx.accounts.my_account;
my_account.data = 0;
Ok(())
}
pub fn update(ctx: Context<Update>, value: u64) -> Result<()> {
let my_account = &mut ctx.accounts.my_account;
my_account.data = value;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = user, space = 8 + 8)] // 8 bytes for discriminator, 8 for u64
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>,
}
#[account]
pub struct MyAccount {
pub data: u64,
}
anchor build
anchor deploy
在 tests/myapp.ts 中:
it('Initializes and updates account data', async () => {
const myAccount = anchor.web3.Keypair.generate();
// 初始化账户
await program.methods.initialize().accounts({
myAccount: myAccount.publicKey,
user: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
}).signers([myAccount]).rpc();
// 更新账户数据
await program.methods.update(new anchor.BN(123)).accounts({
myAccount: myAccount.publicKey,
}).rpc();
// 查询账户数据
const account = await program.account.myAccount.fetch(myAccount.publicKey);
console.log("Stored data:", account.data.toString()); // 输出:123
});
操作 | 方法 |
初始化账户 | #[account(init)] + 指定空间 |
写入数据 | 修改 ctx.accounts.my_account |
读取数据 | program.account.<Struct>.fetch() |
使用 PDA | 下一课讲解 |
修改合约增加一个 owner: Pubkey
字段,并只允许 owner 更新
创建两个账户分别存储不同数值,测试是否独立
使用 solana explorer 查看账户内容(需 base64 解码)
准备开始第6课——使用 PDA 自动创建账户
深入理解 Solana 的账户模型与数据存储方式
使用 Anchor 的 #[account]
实现状态存储
编写合约逻辑以创建和更新链上数据
完成本地与 Devnet 上的数据验证
课程目标:理解 Solana 中的 PDA(Program Derived Address),学会通过 Anchor 自动创建账户,实现更安全、可控的数据管理方式。
什么是 PDA(Program 派生地址)
如何在 Anchor 中使用 PDA
自动初始化 PDA 账户
常见 PDA 创建错误与调试技巧
是程序可以控制的账户地址
不需要私钥,由程序 + 种子自动生成
通常用于合约的数据账户,让程序完全掌控其生命周期
特性 | 描述 |
派生算法 | Pubkey::find_program_address(seeds, program_id) |
不可签名 | PDA 不能被签名,但程序可以“签署”PDA |
安全性 | 不能伪造,不能用私钥访问,只有程序能修改 |
创建一个 PDA 账户 MyPdaAccount,结构为:
#[account]
pub struct MyPdaAccount {
pub authority: Pubkey, // 账户所有者
pub count: u64, // 某个可变数据
}
用户可调用程序初始化 PDA,后续只能由该用户更新其 PDA 数据。
use anchor_lang::prelude::*;
declare_id!("你的ProgramID");
#[program]
pub mod mypdaapp {
use super::*;
pub fn create_pda(ctx: Context<CreatePda>) -> Result<()> {
let account = &mut ctx.accounts.pda_account;
account.authority = *ctx.accounts.user.key;
account.count = 0;
Ok(())
}
pub fn update_pda(ctx: Context<UpdatePda>, value: u64) -> Result<()> {
let account = &mut ctx.accounts.pda_account;
require_keys_eq!(account.authority, ctx.accounts.user.key(), CustomError::Unauthorized);
account.count = value;
Ok(())
}
}
#[derive(Accounts)]
#[instruction()]
pub struct CreatePda<'info> {
#[account(
init,
seeds = [b"my-seed", user.key().as_ref()],
bump,
payer = user,
space = 8 + 32 + 8
)]
pub pda_account: Account<'info, MyPdaAccount>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct UpdatePda<'info> {
#[account(
mut,
seeds = [b"my-seed", user.key().as_ref()],
bump
)]
pub pda_account: Account<'info, MyPdaAccount>,
pub user: Signer<'info>,
}
#[account]
pub struct MyPdaAccount {
pub authority: Pubkey,
pub count: u64,
}
#[error_code]
pub enum CustomError {
#[msg("Unauthorized access.")]
Unauthorized,
}
使用 Anchor TypeScript SDK 调用:
const [pda, bump] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("my-seed"), wallet.publicKey.toBuffer()],
program.programId
);
await program.methods.createPda()
.accounts({
pdaAccount: pda,
user: wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
await program.methods.updatePda(new anchor.BN(42))
.accounts({
pdaAccount: pda,
user: wallet.publicKey,
})
.rpc();
错误信息 | 原因与解决方式 |
"account not initialized" | PDA 尚未用 init 初始化 |
"seeds constraint violated" | seeds 拼写、顺序或 bump 错误 |
"Unauthorized" | 错误的签名人或非法操作 |
内容 | 技术 |
自动账户创建 | 使用 #[account(init, seeds, bump)] |
用户自定义账户 | 种子中加入 user.key() |
安全验证 | 自定义错误 + require_keys_eq! 宏 |
PDA 授权模型 | 仅创建者可修改数据 |
修改合约为每个用户创建不同的 PDA 数据账户(带名称、时间戳)
将 count
改为可递增,增加 increment()
方法
在前端界面中显示用户 PDA 地址和数据内容
探索 PDA 地址是否可被 solscan 搜索(使用 Base58 地址)
掌握 PDA 的原理与用途
使用 Anchor 实现安全的自动账户创建
强化对合约数据结构和权限控制的理解
为构建完整 DApp 奠定数据层基础
课程目标:掌握如何通过 React 前端与 Solana 合约进行交互,读取和写入链上数据,搭建一个基本的 Solana Web DApp 界面。
使用 Anchor 提供的 IDL 文件与前端通信
搭建基于 React + Vite 的开发环境
钱包连接:集成 Phantom 钱包
前端读取链上 PDA 数据、调用合约方法
npm create vite@latest solana-dapp -- --template react
cd solana-dapp
npm install
npm install @solana/web3.js @project-serum/anchor @solana/wallet-adapter-react @solana/wallet-adapter-wallets @solana/wallet-adapter-react-ui
创建 wallet.tsx 文件用于集成 Phantom:
// src/wallet.tsx
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
import {
ConnectionProvider,
WalletProvider
} from "@solana/wallet-adapter-react";
import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
import {
PhantomWalletAdapter,
} from "@solana/wallet-adapter-wallets";
import React, { FC, useMemo } from "react";
require("@solana/wallet-adapter-react-ui/styles.css");
export const WalletConnectionProvider: FC<{ children: React.ReactNode }> = ({ children }) => {
const network = WalletAdapterNetwork.Devnet;
const wallets = useMemo(() => [new PhantomWalletAdapter()], []);
return (
<ConnectionProvider endpoint="https://api.devnet.solana.com">
<WalletProvider wallets={wallets} autoConnect>
<WalletModalProvider>{children}</WalletModalProvider>
</WalletProvider>
</ConnectionProvider>
);
};
在 Anchor 项目目录下运行:
anchor build
会在 target/idl/your_program_name.json 生成 IDL 文件,复制到前端 src/idl/ 目录。
import idl from './idl/mypdaapp.json';
import { AnchorProvider, Program, web3, utils, BN } from '@project-serum/anchor';
const programID = new web3.PublicKey(idl.metadata.address);
const provider = AnchorProvider.local('https://api.devnet.solana.com');
const program = new Program(idl as any, programID, provider);
import { useWallet } from '@solana/wallet-adapter-react';
import { WalletMultiButton } from '@solana/wallet-adapter-react-ui';
const Home = () => {
const { publicKey } = useWallet();
const [pdaData, setPdaData] = useState<number | null>(null);
const [pda, bump] = useMemo(() => {
if (!publicKey) return [null, null];
return web3.PublicKey.findProgramAddressSync(
[Buffer.from("my-seed"), publicKey.toBuffer()],
program.programId
);
}, [publicKey]);
const createAccount = async () => {
await program.methods.createPda()
.accounts({
pdaAccount: pda,
user: publicKey,
systemProgram: web3.SystemProgram.programId,
})
.rpc();
alert("PDA created");
};
const fetchData = async () => {
const account = await program.account.myPdaAccount.fetch(pda);
setPdaData(account.count.toNumber());
};
return (
<div className="p-4">
<WalletMultiButton />
{publicKey && (
<>
<button onClick={createAccount}>创建 PDA</button>
<button onClick={fetchData}>读取数据</button>
<p>当前计数值:{pdaData}</p>
</>
)}
</div>
);
};
添加按钮实现 updatePda
函数,更新 count
显示 PDA 地址和已派生的数据
增加状态提示(创建中、读取中)
实现错误捕获和错误提示
内容 | 技术 |
钱包连接 | Wallet Adapter + Phantom |
Anchor 集成 | 加载 IDL,初始化 Program |
PDA 交互 | 前端派生 PDA,传入账户 |
状态展示 | React 状态管理、异步调用合约 |
增加表单支持用户输入数值,更新 count
字段
为多个用户展示各自的 PDA 账户地址
将 Devnet 切换为本地 test-validator
测试网络
使用 Tailwind CSS 美化界面
你已经完成了从部署合约到前端交互的全流程!
在 Solana 中,账户就是状态的载体,不像以太坊将所有状态集中保存在合约内部,Solana 每一份用户或项目的数据都是一个链上账户。这种设计虽然更高效、更可并行,但也意味着你必须学会如何合理组织多个账户的关系和生命周期。
为管理复杂应用中的链上状态,开发者通常会设计如下几类账户:
账户类型 | 用途示例 | 是否 PDA | 生命周期 |
全局状态账户 | 平台参数、管理员地址 | ✅ 是 | 长期存在 |
用户状态账户 | 用户资料、积分、行为记录 | ✅ 是 | 用户级别持久 |
实体/资源账户 | 一篇留言、一笔订单、一场投票等 | ✅ 是 | 持久/可删除 |
SPL Token账户 | 管理代币余额 | ❌ 否 | 持久 |
临时缓存账户 | 多步骤操作中传递中间状态 | ✅ 是 | 临时可释放 |
PDA 是由合约程序和种子值生成的唯一地址,只有该程序有权限“签名”使用这个地址:
const [pda, bump] = await PublicKey.findProgramAddress(
[Buffer.from("user"), wallet.publicKey.toBuffer()],
programId
);
在 Anchor 中使用 PDA:
#[account(
init,
seeds = [b"user", authority.key().as_ref()],
bump,
payer = authority,
space = 8 + UserAccount::LEN
)]
pub user_account: Account<'info, UserAccount>,
通过 PDA,我们可以确保:
用户账户和主账户之间有明确绑定关系
账户地址可预测、可验证
状态管理更安全
继续前一课的留言板项目,我们现在要引入用户状态追踪功能。
设计如下账户结构:
账户名 | 数据内容 | PDA 构造种子 |
GlobalState | 总留言数、管理员地址 | [b"global"] |
UserAccount | 用户留言次数、上次留言时间 | [b"user", user_wallet] |
MessageAccount | 单条留言内容、时间戳 | [b"message", message_id] |
用户留言前,检查是否已创建 UserAccount
,若无则初始化
创建一个新的 MessageAccount
保存留言内容
同步更新 GlobalState
和 UserAccount
里的状态
Anchor 提供了一些强大的账户校验语法:
#[account(
mut,
has_one = authority,
constraint = user_account.is_active @ ErrorCode::UserBanned
)]
pub user_account: Account<'info, UserAccount>,
has_one
: 自动检查字段匹配(如 user_account.authority == authority.key()
)
constraint
: 自定义逻辑判断
init_if_needed
: 有则复用,无则初始化,防止重复错误
不同生命周期的状态应拆分为不同账户
Anchor 初始化账户需精确声明大小,space = 8 + N(8字节用于 account discriminator)
将大数组或可变数据提取为独立账户结构,便于后期升级
使用 PDA 与 has_one 管理权限控制边界
Solana 多账户架构的设计原则
PDA 的使用方法和状态绑定技巧
Anchor 中账户初始化与验证的高级写法
给留言板添加 UserAccount
,记录用户留言次数
留言前检查该账户是否存在,动态创建
前端读取用户状态并展示留言历史
在 Solana 上部署的程序(Program)默认是不可变的,这有助于保障链上安全性。但在实际开发中,我们常常需要:
添加新功能(如新指令、支持新账户类型)
修复逻辑错误
修改状态结构(添加字段、优化存储)
因此,Solana 支持通过 BPF Loader 升级权限 来实现“程序的可控升级”。
每一个 Program 有一个 upgrade authority(升级权限地址),只有该地址的持有者,才能进行以下操作:
升级程序代码(Program Binary)
转移升级权限
永久放弃升级权限(使合约不可更改)
solana program deploy --upgrade-authority upgrade-keypair.json my_program.so
如果你使用 Anchor 开发,自动为你处理了 UpgradeableLoader 格式的部署。
修改 .rs 文件中逻辑或数据结构。
anchor build
anchor upgrade target/deploy/your_program.so --provider.cluster devnet
确保你拥有当前部署程序的升级权限。
合约升级最麻烦的不是“换代码”,而是处理“旧状态结构”与“新状态结构”的兼容问题。
在原结构中添加字段
通过版本号字段判断状态解析逻辑
保持原有字段顺序与长度
#[account]
pub struct UserAccount {
pub authority: Pubkey,
pub points: u64,
pub version: u8, // 用于识别版本
pub nickname: Option<String>, // 新增字段
}
删除原字段
改变字段类型
调整字段顺序
缩小字段空间(可能会被序列化/反序列化失败)
如果你必须进行非兼容的结构升级,可以选择:
发布迁移指令,在合约中读取旧状态账户,转换为新结构并写入新账户或更新原账户。
#[derive(Accounts)]
pub struct MigrateUser<'info> {
#[account(mut)]
pub old_user: Account<'info, OldUserAccount>,
#[account(init, seeds = [...], bump)]
pub new_user: Account<'info, NewUserAccount>,
}
适合:
有大量旧账户需要迁移
迁移过程需确保原子性、安全性
前端检测旧账户结构(如长度不足)
提示用户点击“升级数据”
调用新指令迁移账户
适合:
用户数较少
前端控制较强
严格版本管理:使用 Cargo.toml
或常量记录合约版本
添加回滚路径:保留旧逻辑接口一段时间
模拟迁移测试:在 localnet
或 devnet
上演练迁移流程
合约模块化设计:分拆为多个独立模块,减少升级影响面
合约升级的机制与权限控制
Anchor 合约如何进行升级
链上状态迁移的设计方法
给留言板合约添加一个版本号字段
添加 nickname
字段到 UserAccount
编写一条迁移指令,将旧账户升级到新结构(保留旧数据)
Solana 合约一旦部署上线,链上操作真实且不可逆,因此提前做好测试是保障 DApp 正常运行的关键环节。
良好的测试可以帮助你:
快速验证业务逻辑正确性
提前发现权限或账户配置问题
模拟多用户/边界/异常场景
降低部署出错成本
Solana 提供多种开发环境,你可以根据开发阶段选择合适的平台:
环境 | 特点 | 命令 |
localnet | 本地链,全控制、快、无网络依赖 | anchor localnet |
devnet | 公网测试链,速度快,带浏览器 | --provider.cluster devnet |
testnet | 主网镜像,较稳定 | --provider.cluster testnet |
mainnet | 主网,慎用 | - |
Anchor 集成了 Mocha + TypeScript 测试环境,支持模拟账户、状态验证、事件监听等高级功能。
anchor init myproject
cd myproject/tests/
创建文件 myproject.ts,结构如下:
describe("myproject", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.Myproject;
it("初始化留言板全局状态", async () => {
const [globalPda, _] = await PublicKey.findProgramAddress(
[Buffer.from("global")],
program.programId
);
await program.methods
.initializeGlobal()
.accounts({ globalState: globalPda, authority: provider.wallet.publicKey })
.rpc();
const global = await program.account.globalState.fetch(globalPda);
assert.equal(global.totalMessages.toNumber(), 0);
});
});
const user = anchor.web3.Keypair.generate();
const airdropSig = await provider.connection.requestAirdrop(user.publicKey, 1e9);
await provider.connection.confirmTransaction(airdropSig);
program.addEventListener("MessageCreated", (event, slot) => {
console.log("监听到事件:", event);
});
await assert.rejects(async () => {
await program.methods.doSomething().rpc();
}, /Custom error: 6001/);
运行本地链模拟完整操作流程
anchor localnet
搭配 tests/ 中的脚本可以实现:
自动部署合约
自动创建测试数据
快速回归所有测试逻辑
在 Anchor.toml 中可指定测试集群:
[provider]
cluster = "localnet"
建议 | 说明 |
每个指令写一个单独的测试案例 | 便于定位问题 |
覆盖边界值、权限、非法输入 | 模拟真实用户的各种误操作 |
使用断言验证账户字段是否更新 | 比如留言数增加、时间戳变化等 |
给测试数据设置固定种子 | 例如 PDA 的种子,避免因随机值而导致不可预测的地址变化 |
多账户场景下切换 provider.wallet | 模拟不同用户身份的行为 |
Anchor 测试环境与框架结构
如何在 localnet 环境进行完整模拟
多账户模拟、事件监听、错误捕捉等高级技巧
在开发完成、测试通过后,部署到 Solana 主网需谨慎进行。部署步骤包括:
编译合约产出 .so
文件
上传程序到主网
初始化状态账户
配置 Upgrade Authority 权限
确认部署合约地址并前端接入
anchor build
solana program deploy target/deploy/my_program.so --url https://api.mainnet-beta.solana.com
项目 | 推荐做法 |
RPC 节点 | 使用官方或稳定的主网节点,例如 QuickNode、Helius 等 |
Upgrade Authority | 设为多签地址或直接放弃(不可升级) |
状态账户初始化 | 建议通过脚本完成,便于追踪和调试 |
程序地址(Program ID) | 写入前端配置,并做好版本管理 |
可选设置不可升级:
solana program deploy --upgrade-authority /dev/null target/deploy/my_program.so
这样程序一旦部署,就不可更改,安全性更高。
有则需校验来源
避免用户传入恶意账户替代状态账户
是否使用 has_one
检查
是否验证签名者 (Signer
)
是否允许更换 authority
多签权限是否能更新或冻结?
指令是否幂等(多次调用影响相同)
是否记录时间戳/nonce 防止重复操作
漏洞类型 | 描述 | 防护措施 |
错误权限判断 | 放错签名检查、has_one 漏用 | 写 test 覆盖所有逻辑路径 |
数据覆盖或重写风险 | allow init 重复执行 | 用 PDA 判断 seeds 是否已存在 |
资金转移无验证 | 转账对象可控 | 明确转账目标账户 + 权限判断 |
滥用 CPI 调用 | 调用外部程序时未验证输入 | 严格检查所有 CPI 输入 |
包括 program_id、PDA 地址、状态账户地址
建议将版本号写入 GlobalState 中,方便后期兼容判断
合约升级
状态重置
资金提取
以上操作建议绑定多签权限或时间锁逻辑(Time-lock)。
搭配 Helius / Triton 监听程序行为
使用 Discord bot 推送异常操作通知(如有大额转账)
项目 | devnet / testnet | mainnet-beta |
转账费用 | 免费或模拟 | 真实 SOL,成本需考虑 |
节点负载 | 相对轻 | 网络拥堵时可能失败重试 |
数据持久性 | 常被清除 | 主网永久保存 |
使用 RPC 限制 | 高速开放 | 主网通常需付费 RPC(稳定性更好) |
Solana 主网部署的标准流程
合约升级权限设置的重要性
主网上线前的安全检查列表
合约中常见漏洞与应对策略
使用外部工具监控主网合约行为
完成一次将你的 Anchor 合约部署到 devnet 的实操
使用一个多签地址作为 Upgrade Authority 进行测试
撰写一份“部署清单”,记录部署程序 ID、权限设置与账户地址
CPI(Cross-Program Invocation)是 Solana 合约之间相互调用的核心机制。它允许你在自己的合约中调用其他合约程序的逻辑,从而实现模块复用、权限继承、资产管理等高级功能。
场景 | 描述 |
SPL Token 转账 | 调用系统的 Token Program 来转账 |
多合约协作 | 合约 A 调用合约 B 实现权限校验或状态同步 |
资产托管 | 将 NFT 或 SOL 托管到另一个合约控制的账户中 |
引入另一个程序的 CPI 接口
在 Accounts
中声明目标程序及账户
调用其 CPI 方法
use anchor_spl::token::{self, Transfer, TokenAccount, Token};
pub fn transfer_tokens(ctx: Context<TransferCtx>, amount: u64) -> Result<()> {
let cpi_accounts = Transfer {
from: ctx.accounts.from.to_account_info(),
to: ctx.accounts.to.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
};
let cpi_ctx = CpiContext::new(ctx.accounts.token_program.to_account_info(), cpi_accounts);
token::transfer(cpi_ctx, amount)
}
你必须声明 CPI 所需的所有账户,通常包括:
Token program 的地址(或其他目标程序)
所调用逻辑所需的账户(如 token.from、token.to)
签名者或 PDA(如 authority)
注意:调用的合约不会自动校验账户合法性,一切靠你自己传对。
风险/限制 | 描述 | 应对策略 |
重入攻击 | 合约调用合约可能造成状态异常 | 写入状态前检查是否已处理(幂等性) |
签名权限 | CPI 不具备签名权 | 需结合 PDA 签名(invoke_signed) |
Gas 限制 | CPI 调用成本更高 | 限制调用次数,避免复杂循环 |
依赖程序变化 | 被调用合约升级可能影响你 | 固定版本依赖、测试覆盖调用逻辑 |
Anchor 背后使用的是 Solana SDK 的 invoke 和 invoke_signed。
invoke_signed(
&ix, // 调用指令
&account_infos, // 所有涉及账户
&[&seeds[..]] // PDA 的 seeds 和 bump
)?;
常用于:
用 PDA 转账
创建账户或调用其他程序需要 PDA 权限
假设你调用一个自定义合约B的某个方法 add_member:
let cpi_program = ctx.accounts.program_b.to_account_info();
let cpi_accounts = AddMember {
group: ctx.accounts.group.clone(),
user: ctx.accounts.user.clone(),
};
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
program_b::cpi::add_member(cpi_ctx)?;
其中:
program_b::cpi::add_member
是通过 declare_id!
和 Anchor 自动生成的 CPI 接口
你必须引入合约B的 .rs
crate 或 .idl.json
方法 | 描述 |
使用 solana logs | 查看 CPI 调用链和错误原因 |
本地搭建多个合约项目 | 在 devnet / localnet 中部署多个程序测试 CPI |
Anchor 的 cpi::invoke 返回值检查 | 每个调用都应 ? 处理错误 |
撰写测试覆盖所有 CPI 路径 | 特别是签名、账户来源校验是否正确 |
CPI 的核心概念与 Anchor 实现方式
SPL Token 的 CPI 调用模板
如何处理带 PDA 的签名 CPI
自定义合约之间调用的结构组织方式
CPI 的安全风险与调试方法
修改你的合约,使用 SPL Token CPI 进行转账
尝试将一个合约逻辑拆分为两个程序,通过 CPI 协作完成
编写测试覆盖 CPI 出错的场景,例如账户传错或 PDA 签名失败
Solana 上的 NFT 并非单独的新类型资产,而是遵循 SPL Token 标准,配合 Metaplex Metadata Program 实现图像、名称、描述等元信息绑定。
组件 | 说明 |
一个 mint(总量为 1) | 表示该 NFT 的唯一标识 |
一个 token account | 存储 NFT 到具体地址 |
一个 metadata account | 存储图像、标题、描述等元数据 |
(可选)一个 master edition | 用于限定版本控制和系列铸造 |
Metaplex 团队开发的 Metadata Program 是目前 Solana NFT 的事实标准。
Program ID: metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s
所有主流市场都遵循该协议(Magic Eden, Tensor 等)
创建一个 mint,总量为 1
为用户创建 token account(接收该 NFT)
将 mint 铸造 1 个 token 到该账户
调用 Metaplex 的 create_metadata_accounts_v3
方法添加元数据
(可选)调用 create_master_edition_v3
来创建主版本
你可以通过 CPI 调用 Metaplex 的元数据程序。
以下是调用 create_metadata_accounts_v3 的关键步骤:
let cpi_accounts = CreateMetadataAccountsV3 {
metadata: ctx.accounts.metadata.clone(),
mint: ctx.accounts.mint.clone(),
mint_authority: ctx.accounts.mint_authority.clone(),
update_authority: ctx.accounts.update_authority.clone(),
payer: ctx.accounts.payer.clone(),
system_program: ctx.accounts.system_program.clone(),
rent: ctx.accounts.rent.clone(),
};
let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.token_metadata_program.to_account_info(),
cpi_accounts,
signer_seeds,
);
mpl_token_metadata::cpi::create_metadata_accounts_v3(
cpi_ctx,
DataV2 {
name: "My NFT".to_string(),
symbol: "MNFT".to_string(),
uri: "https://my-nft-json-host.com/nft1.json".to_string(),
seller_fee_basis_points: 500, // 5%
creators: Some(vec![...]), // 支持设置版税
collection: None,
uses: None,
},
true, // is_mutable
true, // update_authority_is_signer
)?;
你的 uri 链接的必须是一个符合以下结构的 JSON 文件:
{
"name": "My NFT",
"symbol": "MNFT",
"description": "这是一个 Solana NFT 教程作品。",
"image": "https://example.com/nft.png",
"attributes": [
{ "trait_type": "Level", "value": "3" },
{ "trait_type": "Type", "value": "Magic" }
],
"properties": {
"files": [{ "uri": "nft.png", "type": "image/png" }],
"category": "image",
"creators": [
{ "address": "XXX", "share": 100 }
]
}
}
使用 Arweave + Bundlr 网络(稳定不易丢)
或者 NFT.Storage / IPFS(也可接受)
只要你按照 Metaplex 标准创建的 NFT,便可自动识别、上架于大多数 NFT 市场。
使用 verified creator(标记官方身份)
使用 collection 功能归类 NFT 系列
添加 symbol 与属性以增强展示效果
问题 | 说明 | 排查方法 |
Metadata 创建失败 | PDA 计算错误或账户未初始化 | 检查 metadata PDA 与 mint 一致性 |
URI 不显示图片 | JSON 无效或图片链接错误 | 手动打开链接测试 |
上架市场失败 | 未使用 verified creator 或缺少元数据 | 使用 Metaplex Explorer 校验 |
CPI 调用失败 | signer/bump 未提供或账户顺序错误 | 使用 Anchor 的 signer_seeds 正确构造 signer |
使用 Anchor 创建一个 mint 和 metadata
上传自己的图片 JSON 到 Arweave 或 NFT.Storage
将 NFT 铸造给自己,并尝试上架 Magic Eden(测试网)
额外:创建 Master Edition 实现 NFT 系列
本课你学到:
DAO(去中心化自治组织)是一种以智能合约为核心运作逻辑的组织结构,通过链上规则取代传统管理者,实现公平、透明、可编程的治理。
快速交易确认(<1s)
低 Gas 成本
可编程账户模型适合复杂治理逻辑
生态中已有 SPL Governance 框架支持
模块 | 功能说明 |
治理 Token(GovToken) | 投票权凭证,代表 DAO 成员治理权重 |
提案系统(Proposal) | 成员发起提案进行治理决策 |
投票系统(Vote) | 成员按持币比例对提案投票 |
执行系统(Execute) | 提案通过后,自动或手动执行链上操作 |
Solana 官方提供了一个治理框架:SPL Governance Program
Program ID(主网): govvQzY5jV4xjN5nJYBAuGdR3kKhJKzj8TqBF4ZyzcQ
已被 Realms(https://realms.today) 等多个 DAO 平台采用
支持多种投票类型(加权、1人1票等)
支持提案延时、撤回等机制
可对 Program、Token Mint、Account 等执行治理
拥有完整 UI 支持(via Realms)
如果不使用 SPL Governance,也可自定义 DAO 系统:
#[account]
pub struct Proposal {
pub proposer: Pubkey,
pub title: String,
pub description: String,
pub yes_votes: u64,
pub no_votes: u64,
pub deadline: i64, // timestamp
pub executed: bool,
}
#[account]
pub struct VoteRecord {
pub voter: Pubkey,
pub proposal: Pubkey,
pub voted_yes: bool,
}
通过调用流程管理整个 DAO 生命周期:
创建提案
投票计数
到期判断是否通过
执行逻辑(如更换管理员、转账等)
有效的 DAO 不只是投票,更要有激励机制来驱动参与。
方式 | 说明 |
投票奖励 | 按参与率分发 Token |
贡献积分系统 | 鼓励做事、写文档、开发工具等 |
NFT 身份标识 | 用 NFT 代表资深成员、创始人等身份 |
锁仓治理权 | 长期持有 GovToken 可获得更高投票权重(如 ve-model) |
模型 | 特点 | 适用场景 |
Token 权重制 | 持币越多权力越大 | 资金驱动型项目 |
一人一票制 | 每个成员平等投票 | 小型社区、知识协作 |
多签+提案组合 | 多签执行提案 | 技术性项目决策 |
Quadratic Voting | 减少鲸鱼影响力 | 平衡大户与散户权重 |
所有投票数据建议使用 PDA 锚定,防止伪造
采用延迟执行,允许社区有时间复查并否决
每个提案建议包含逻辑摘要哈希(提防合约更换)
所有状态写入时间戳,便于外部索引与审计
使用 Anchor 构建一个 DAO 合约,支持创建提案与投票
每个提案投票权重来自一个治理 Token(自行 Mint)
投票期结束后统计结果并执行一个链上转账操作(如拨款)
将结果通过前端展示
本课你掌握了
尽管 Solana 具备高性能链上计算能力,但在 DApp 的实际开发中,链下服务依然扮演着重要角色,主要原因包括:
区块链查询成本高,不适合频繁扫描
无法链上存储大体积数据(如图像、日志、状态快照)
用户界面交互需要聚合链上与链下信息
需要自定义排序、过滤、分页等复杂逻辑
类型 | 作用 |
Indexer(索引器) | 扫描链上交易/账户变化,存入数据库 |
Backend API 服务 | 提供 REST / GraphQL 接口给前端 |
事件订阅系统 | 实时监听程序事件,触发通知/计算 |
任务调度器(Crons) | 周期性触发合约交互或链上检查 |
Solana 的节点提供了 RPC 和 WebSocket 接口。Indexer 的职责是解析交易数据并结构化保存。
GET /getSignaturesForAddress
GET /getTransaction
GET /getAccountInfo
订阅目标程序(Program ID)相关账户或交易签名
解析 Instruction
、Log
、Account
数据
转换为结构化数据并存入数据库
提供 API 接口供前端调用
Node.js + TypeScript
Solana Web3.js
PostgreSQL / MongoDB
WebSocket + RPC
connection.onLogs(programId, (logInfo) => {
const logs = logInfo.logs;
const txSignature = logInfo.signature;
// 解析日志中的事件
if (logs.some(log => log.includes("event: CreateProposal"))) {
// 拉取交易详情,解析账户变化
fetchTransactionAndStore(txSignature);
}
});
建议配合 Anchor 事件格式 #[event] 使用,更方便日志解析
一个优秀的索引器需配合良好的数据库模型。建议:
使用唯一标识(如 PDA 地址或 Proposal ID)作为主键
为常用字段建索引(如 proposer、时间戳)
使用归档库定期备份历史数据
CREATE TABLE proposals (
id TEXT PRIMARY KEY,
proposer TEXT,
title TEXT,
created_at TIMESTAMP,
status TEXT
);
链下服务通常会暴露如下接口供前端调用:
接口 | 功能 |
GET /proposals | 获取所有提案列表,支持分页/筛选 |
GET /proposals/:id | 获取单个提案详情 |
GET /users/:wallet/participation | 获取用户投票记录或 DAO 活动轨迹 |
可选择 REST 或 GraphQL,根据开发习惯确定架构。
使用事务签名缓存避免重复解析交易
为断点续扫设计“最后处理高度”存储点
若使用 WebSocket,建议使用 Redis 保证消息可靠传输
设置报警系统检测索引延迟或失效
工具 | 说明 |
Helius | 高性能 Solana 索引 API,提供 NFT、Token 等结构化数据 |
SolanaFM | 区块链分析平台,部分 API 可供开发者使用 |
Subsquid | 通用链上数据抓取工具,支持 Solana 子模块 |
Geyser Plugin | Solana 官方节点插件,允许将链上事件实时写入 DB 或消息队列 |
本课掌握了:
在 Solana 网络中,NFT(Non-Fungible Token)本质上是遵循 SPL Token 标准 的一种特殊代币:
总量为 1(即 supply = 1)
不可分割(decimals = 0)
搭配元数据(Metadata)账户记录 NFT 的图像、名称、描述等信息
NFT 的关键组件是:
组件 | 说明 |
Mint 账户 | NFT 的 Token 发行地址 |
Token Account | 用户钱包中存放该 NFT 的 Token 账户 |
Metadata 账户 | 存储 NFT 名称、图像 URI、属性等 |
Master Edition(可选) | 限量发行控制(如 1/1、1/100 系列) |
Metaplex 是 Solana 上最流行的 NFT 工具包和标准协议,由 Solana 官方支持。它提供了:
标准化 NFT 结构(Metadata Program)
支持图像、视频、音频等丰富内容类型
NFT 系列管理、版税收取、授权机制等
📦 核心程序:metaplex-token-metadata
Program ID(主网):
metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s
const mint = Keypair.generate();
await createMint(
connection,
payer,
mint.publicKey,
payer.publicKey, // mint authority
0 // decimals = 0
);
const tokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
payer,
mint.publicKey,
userPublicKey
);
await mintTo(
connection,
payer,
mint.publicKey,
tokenAccount.address,
payer.publicKey,
1
);
const metadataPDA = await findMetadataPda(mint.publicKey);
await program.methods
.createMetadata(name, symbol, uri)
.accounts({
metadata: metadataPDA,
mint: mint.publicKey,
mintAuthority: payer.publicKey,
payer: payer.publicKey,
updateAuthority: payer.publicKey,
systemProgram: SystemProgram.programId,
rent: SYSVAR_RENT_PUBKEY,
})
.rpc();
NFT 的元数据是一个 JSON 文件,必须上传到 IPFS、Arweave 或其他 CDN:
{
"name": "Solana NFT #001",
"symbol": "SNFT",
"description": "This is a unique Solana NFT",
"image": "https://arweave.net/xxx.png",
"attributes": [
{ "trait_type": "Background", "value": "Red" },
{ "trait_type": "Eyes", "value": "Laser" }
]
}
元数据的 URI 存储在 Metadata 帐户中,并被钱包和市场读取。
使用 Candy Machine
(Metaplex 开源工具)
支持批量生成图像 + metadata 文件
可设置公开铸造时间、钱包白名单、铸造费用
用于 PFP 项目(如猴子、熊猫、机器人等)
遵循 Metaplex 标准
Metadata 格式正确,且可通过公开网关访问
NFT 图像非违规内容
若为系列 NFT,需有 Creator 签名验证(已认证合集)
📢 Magic Eden Creator Hub 支持你申请系列认证
Creator 可设置 seller_fee_basis_points
字段(如 500 = 5%)
NFT 市场在二级交易时自动分配版税
多个 Creator 时按照权重自动拆分收益
"properties": {
"creators": [
{
"address": "CreatorWalletPubkey1",
"share": 80
},
{
"address": "ContributorWalletPubkey2",
"share": 20
}
]
}
风险 | 防范建议 |
图片 URI 被删除 | 使用永久存储平台(如 Arweave) |
未授权 NFT 出现 | 使用 Verified Creator 签名 |
用户伪造 Metadata | 校验 Metadata PDA 来源是否为合约创建 |
元数据作弊(如隐藏色情) | 预览前拉取 metadata 并用白名单校验合规性 |
本课你掌握了:
Solana 网络上有多种稳定币可用于 DApp 支付、DeFi、薪资系统等场景,其中最主流的是:
稳定币 | Program ID(主网) | 发行方 |
USDC | AjU3j...(Circle) | Circle (via SPL Token) |
USDT | Es9vM... | Tether |
UXD | UXD9Q... | UXD Protocol(链上算法稳定币) |
在构建支付系统时,推荐使用合规、流动性高的 USDC。
USDC 是一种 SPL Token,其核心特性:
decimals = 6(即最小单位为 0.000001 USDC)
可使用 Token Program 的常规转账、mint、burn 操作
可集成 Anchor 合约进行业务逻辑封装
pub fn pay_with_usdc(ctx: Context<PayWithUsdc>, amount: u64) -> Result<()> {
// 从用户转 USDC 到商户钱包
let cpi_accounts = Transfer {
from: ctx.accounts.user_token.to_account_info(),
to: ctx.accounts.merchant_token.to_account_info(),
authority: ctx.accounts.user.to_account_info(),
};
let cpi_ctx = CpiContext::new(ctx.accounts.token_program.to_account_info(), cpi_accounts);
token::transfer(cpi_ctx, amount)?;
// 更新订单记录
let order = &mut ctx.accounts.order;
order.amount = amount;
order.user = ctx.accounts.user.key();
order.timestamp = Clock::get()?.unix_timestamp;
Ok(())
}
常见 DApp 场景中,往往需要将支付收入按比例拆分到多个钱包(如平台抽佣 + 商户收入):
let platform_share = amount * 5 / 100;
let merchant_share = amount - platform_share;
transfer(user_token, platform_token, platform_share)?;
transfer(user_token, merchant_token, merchant_share)?;
推荐使用 PDA 管理平台账户
分账比例写死或配置为链上可更新变量
定义一个 Order 结构体作为账单记录:
#[account]
pub struct Order {
pub user: Pubkey,
pub amount: u64,
pub timestamp: i64,
pub paid: bool,
}
结合 PDA + seeds
保证每个用户订单唯一
可通过 indexer(如 Helius)聚合所有支付信息
用户连接钱包(如 Phantom)
查询商户的 USDC Token Address(ATA)
用户签名发送 SPL 转账交易
合约完成结算并返回支付成功状态
🎯 UI 可参考 Stripe 或 WeChat Pay 风格
风险点 | 对策 |
USDC 转错地址 | 显示商户名 + Token 预览(icon、symbol |
用户重复支付 | 合约端防重复写入相同 Order |
黑客合约伪造收款人 | 使用 allowlist 白名单校验收款地址 |
合约漏洞导致盗转 | 使用 PDA 管理收款地址,不暴露私钥 |
如构建多币种支付(SOL / USDC / UXD),可结合 Chainlink Price Feed 实现等价换算支付:
let price_feed = &ctx.accounts.sol_usdc_price;
let sol_to_usdc = price_feed.price as u64;
let expected_sol = usdc_amount * 1_000_000 / sol_to_usdc;
本课你学到了: