从零到Devnet:SolanaAnchorVault个人金库开发全流程实操在Solana开发中,如何安全地管理用户资金并实现账户隔离是每一位开发者必须跨过的门槛。本文将通过一个实战项目anchor_vault,带你深入Anchor0.32.1的开发世界。我们不仅会撸出一个支持
在 Solana 开发中,如何安全地管理用户资金并实现账户隔离是每一位开发者必须跨过的门槛。本文将通过一个实战项目 anchor_vault,带你深入 Anchor 0.32.1 的开发世界。我们不仅会撸出一个支持存款、取款和销毁回收的 Lamport 金库,还会演示如何从本地环境一步步部署到全球测试网(Devnet),并完成专业的 IDL 版本归档。
solana --version
solana-cli 3.0.13 (src:90098d26; feat:3604001754, client:Agave)
anchor --version
anchor-cli 0.32.1
rustc --version
rustc 1.89.0 (29483883e 2025-08-04)
anchor init blueshift_anchor_vault
yarn install v1.22.22
info No lockfile found.
[1/4] 🔍 Resolving packages...
warning mocha > glob@7.2.0: Glob versions prior to v9 are no longer supported
warning mocha > glob > inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 🔨 Building fresh packages...
success Saved lockfile.
✨ Done in 85.43s.
Failed to install node modules
hint: Using 'master' as the name for the initial branch. This default branch name
hint: will change to "main" in Git 3.0. To configure the initial branch name
hint: to use in all of your new repositories, which will suppress this warning,
hint: call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
hint:
hint: Disable this message with "git config set advice.defaultBranchName false"
Initialized empty Git repository in /Users/qiaopengjun/Code/Solana/blueshift_anchor_vault/.git/
blueshift_anchor_vault initialized
cd blueshift_anchor_vault
blueshift_anchor_vault on master [?] via 🦀 1.89.0
➜ tree . -L 6 -I "docs|target|test-ledger|node_modules|mochawesome-report"
.
├── Anchor.toml
├── Cargo.lock
├── Cargo.toml
├── Makefile
├── app
├── clients
├── cliff.toml
├── deny.toml
├── idls
│ ├── anchor_vault-2026-01-20-010240.json
│ └── blueshift_anchor_vault.so
├── migrations
│ └── deploy.ts
├── package.json
├── pnpm-lock.yaml
├── programs
│ ├── anchor_vault
│ │ ├── Cargo.toml
│ │ └── src
│ │ └── lib.rs
│ └── blueshift_anchor_vault
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── rust-toolchain.toml
├── scripts
├── tests
│ ├── anchor_vault.ts
│ └── blueshift_anchor_vault.ts
└── tsconfig.json
12 directories, 19 files
anchor_vault/src/lib.rs 文件
use anchor_lang::prelude::*;
declare_id!("hFnPxXhvNpkzeBG5cXsCjbsJVmzshnG5ok4W8ax9gd9");
#[program]
pub mod anchor_vault {
use anchor_lang::system_program::{transfer, Transfer};
use super::*;
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
// 1. 业务逻辑校验:确保金额大于 0
require_gt!(amount, 0, VaultError::InvalidAmount);
// 2. 执行转账
let cpi_program = ctx.accounts.system_program.to_account_info();
let cpi_accounts = Transfer {
from: ctx.accounts.signer.to_account_info(),
to: ctx.accounts.vault.to_account_info(),
};
transfer(CpiContext::new(cpi_program, cpi_accounts), amount)?;
msg!("Deposited {} lamports to vault.", amount);
Ok(())
}
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
// 1. 业务逻辑校验:检查余额是否足够
let vault_balance = ctx.accounts.vault.lamports();
require!(vault_balance >= amount, VaultError::InsufficientFunds);
// 2. 准备签名种子
let signer_key = ctx.accounts.signer.key();
let bump = ctx.bumps.vault;
let seeds = &[b"vault".as_ref(), signer_key.as_ref(), &[bump]];
let signer_seeds = &[&seeds[..]];
// 3. 执行转账
let cpi_program = ctx.accounts.system_program.to_account_info();
let cpi_accounts = Transfer {
from: ctx.accounts.vault.to_account_info(),
to: ctx.accounts.signer.to_account_info(),
};
transfer(
CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds),
amount,
)?;
msg!("Withdrew {} lamports from vault.", amount);
Ok(())
}
pub fn close(ctx: Context<Close>) -> Result<()> {
// 检查 vault 是否已经清空,只有空保险库才允许关闭 state 账户
require_eq!(ctx.accounts.vault.lamports(), 0, VaultError::VaultNotEmpty);
Ok(())
}
}
#[derive(Accounts)]
pub struct Deposit<'info> {
#[account(mut)]
pub signer: Signer<'info>,
#[account(
init_if_needed,
payer = signer,
space = 8 + VaultState::INIT_SPACE,
seeds = [b"state", signer.key().as_ref()],
bump
)]
/// 校验点:确保这个 State 账户归属于当前签名者
pub state: Account<'info, VaultState>,
#[account(
mut,
seeds = [b"vault", signer.key().as_ref()],
bump
)]
/// 校验点:Anchor 会自动校验生成的 PDA 是否匹配 seeds
pub vault: SystemAccount<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Withdraw<'info> {
#[account(mut)]
pub signer: Signer<'info>,
#[account(
mut,
// 校验点:必须是之前初始化过的 state 账户
seeds = [b"state", signer.key().as_ref()],
bump,
)]
pub state: Account<'info, VaultState>,
#[account(
mut,
seeds = [b"vault", signer.key().as_ref()],
bump,
)]
pub vault: SystemAccount<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Close<'info> {
#[account(mut)]
pub signer: Signer<'info>,
#[account(
mut,
seeds = [b"state", signer.key().as_ref()],
bump,
close = signer
)]
pub state: Account<'info, VaultState>,
#[account(
mut,
seeds = [b"vault", signer.key().as_ref()],
bump
)]
pub vault: SystemAccount<'info>,
pub system_program: Program<'info, System>,
}
#[account]
#[derive(InitSpace)]
pub struct VaultState {}
#[error_code]
pub enum VaultError {
#[msg("Deposit amount must be greater than 0.")]
InvalidAmount,
#[msg("Insufficient funds in the vault.")]
InsufficientFunds,
#[msg("Vault is not empty.")]
VaultNotEmpty,
}
这段代码是一个基于 Anchor 框架 实现的 个人保险库(Vault)程序,它利用 PDA(程序派生地址) 技术为每个签名者派生出独有的资金池(Vault)和状态记录账户(State),允许用户安全地存入 SOL、在经过余额校验后通过程序签名(PDA Signing)取回资金,并支持在资金清空后通过销毁状态账户来回收租金,从而实现了用户资金在链上的安全隔离与生命周期管理。
init_if_needed 自动为新用户初始化状态空间,并将 SOL 从用户钱包转入程序控制的 PDA 账户。seeds 和 bump 生成签名,证明程序拥有该 PDA 的控制权,从而将资金转回给用户。close = signer 约束,在确认资金已清空后抹除数据账户,并将存储该账户所需的 租金(Rent) 退还给用户钱包。blueshift_anchor_vault on master [?] via 🦀 1.89.0
➜ make build
Formatting Rust code...
Building program 'anchor_vault'...
Finished `release` profile [optimized] target(s) in 0.28s
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.29s
Running unittests src/lib.rs (/Users/qiaopengjun/Code/Solana/blueshift_anchor_vault/target/debug/deps/anchor_vault-ac7b216444bde95b)
import * as anchor from "@coral-xyz/anchor"
import { Program } from "@coral-xyz/anchor"
import { AnchorVault } from "../target/types/anchor_vault" // 确保名称匹配
import { expect } from "chai"
describe("anchor-vault-tests", () => {
// 配置 Provider
const provider = anchor.AnchorProvider.env()
anchor.setProvider(provider)
const program = anchor.workspace.AnchorVault as Program<AnchorVault>
const signer = provider.wallet as anchor.Wallet
// 派生 PDA 地址
const [statePda] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("state"), signer.publicKey.toBuffer()],
program.programId
)
const [vaultPda] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("vault"), signer.publicKey.toBuffer()],
program.programId
)
const oneSol = new anchor.BN(anchor.web3.LAMPORTS_PER_SOL)
it("1. 成功存款 (Initial Deposit)", async () => {
try {
await program.methods
.deposit(oneSol)
.accounts({
signer: signer.publicKey,
})
.rpc()
const vaultBalance = await provider.connection.getBalance(vaultPda)
expect(vaultBalance).to.equal(oneSol.toNumber())
} catch (err) {
console.error("Deposit error:", err)
throw err
}
})
it("2. 追加存款 (Top-up)", async () => {
const topUpAmount = new anchor.BN(0.5 * anchor.web3.LAMPORTS_PER_SOL)
await program.methods
.deposit(topUpAmount)
.accounts({ signer: signer.publicKey })
.rpc()
const vaultBalance = await provider.connection.getBalance(vaultPda)
expect(vaultBalance).to.equal(oneSol.add(topUpAmount).toNumber())
})
it("3. 提取部分资金 (Withdraw Partial)", async () => {
const withdrawAmount = new anchor.BN(0.8 * anchor.web3.LAMPORTS_PER_SOL)
await program.methods
.withdraw(withdrawAmount)
.accounts({ signer: signer.publicKey })
.rpc()
const vaultBalance = await provider.connection.getBalance(vaultPda)
// 1.5 - 0.8 = 0.7 SOL
expect(vaultBalance).to.equal(0.7 * anchor.web3.LAMPORTS_PER_SOL)
})
it("4. 尝试超额提款 (Should Fail)", async () => {
const excessiveAmount = new anchor.BN(10 * anchor.web3.LAMPORTS_PER_SOL)
try {
await program.methods
.withdraw(excessiveAmount)
.accounts({ signer: signer.publicKey })
.rpc()
expect.fail("应该报错:余额不足")
} catch (err: any) {
// 检查错误码是否符合合约定义的 InsufficientFunds
expect(err.error.errorCode.code).to.equal("InsufficientFunds")
}
})
it("5. 尝试在有余额时关闭金库 (Should Fail)", async () => {... 如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!