Solana

2026年01月22日更新 17 人订阅
原价: ¥ 8.6 限时优惠
专栏简介 Solana 开发学习之Solana 基础知识 Solana 开发学习之通过RPC与Solana交互 Web3与Solana实操指南:如何签名与发送交易 Web3 新玩法:Solana Nonce Account 让你交易无忧 Web3 快上手:Solana 造你的链上名片 Web3 开发实战:用 Anchor 打造 Solana 猜数游戏 @solana/web3.js 2.0:Solana 转账全流程解析 玩转 Web3 Solana:从零到代币开发 Web3 开发入门:Solana CLI 配置与本地验证器实战 Web3 Eclipse 开发环境搭建与资产跨链桥接实战指南 用 Gill 库解锁 Web3:Solana 代币转账实战 Web3开发:用Rust实现Solana SOL转账教程 用 Rust 开发 Solana:解锁 Web3 交易费用计算 Web3开发入门:Solana账户创建与Rust实践全攻略 Web3 实战:用 Anchor 打造 Solana 智能合约全流程 Solana Web3 快速入门:创建并获取钱包账户的完整指南 Web3 开发实操:用 Anchor 在 Solana 创建代币 Mint Account 从零到 Web3:使用 @solana/kit 快速查询 Solana 账户余额 快速上手 Web3:用 @solana/kit 在 Solana 上创建钱包并查询余额 Web3实战:使用Anchor与Rust开发和调用Solana智能合约 Web3实战:Solana CPI全解析,从Anchor封装到PDA转账 用 Rust 在 Solana 上打造你的专属代币:从零到一的 Web3 实践 探索Solana SDK实战:Web3开发的双路径与轻量模块化 手把手教你用 Solana Token-2022 创建支持元数据的区块链代币 Solana 开发实战:Rust 客户端调用链上程序全流程 Solana 开发进阶:在 Devnet 上实现链上程序部署、调用与更新 Solana 开发进阶:链上事件到链下解析全攻略 从零打造Solana空投工具库:Rust开发实战指南 从零开始:用 Rust 开发 Solana 链上 Token 元数据查询工具 Solana 智能合约终极部署指南:从入门到主网,定制你的专属靓号 Program ID 【Solana 开发实战】轻松搞定链上 IDL:从上传到获取全解析 Solana 投票 DApp 开发实战:从合约到部署的完整指南 Surfpool:Solana 上的 Anvil,本地开发闪电般⚡️ 【Solana实操】64字节私钥文件解析难题:用三种姿势安全获取钱包地址 Solana 密钥实战:一文搞懂私钥、公钥、PDA 的底层关系与 CLI 操作 Solana 地址进阶:从 TS/JS 到 Rust SDK V3,完全掌握公钥与 PDA 的底层逻辑 Solana 开发者笔记:PDA 与账户操作的10个关键要点 拒绝“版本代差”:基于 Solana SDK V3 的「链上动态存储器」工业级实现 从零到 Devnet:Solana Anchor Vault 个人金库开发全流程实操

从零到 Devnet:Solana Anchor Vault 个人金库开发全流程实操

从零到Devnet:SolanaAnchorVault个人金库开发全流程实操在Solana开发中,如何安全地管理用户资金并实现账户隔离是每一位开发者必须跨过的门槛。本文将通过一个实战项目anchor_vault,带你深入Anchor0.32.1的开发世界。我们不仅会撸出一个支持

从零到 Devnet:Solana Anchor Vault 个人金库开发全流程实操

在 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)取回资金,并支持在资金清空后通过销毁状态账户来回收租金,从而实现了用户资金在链上的安全隔离与生命周期管理。

代码核心逻辑拆解:

  • Deposit(存款):通过 init_if_needed 自动为新用户初始化状态空间,并将 SOL 从用户钱包转入程序控制的 PDA 账户。
  • Withdraw(取款):这是最关键的部分,程序利用 seedsbump 生成签名,证明程序拥有该 PDA 的控制权,从而将资金转回给用户。
  • Close(销毁):通过 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 () => {...

剩余50%的内容订阅专栏后可查看

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论