Solana

2026年01月25日更新 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 个人金库开发全流程实操 Anchor 中一个隐蔽但致命的坑:Accounts 顺序导致 AccountNotInitialized 从核心逻辑到上链部署:Solana Anchor 托管程序实战全记录 Solana 开发实战:使用 @solana/web3.js 与 Bun 铸造首个 SPL 代币

从核心逻辑到上链部署:Solana Anchor 托管程序实战全记录

从核心逻辑到上链部署:SolanaAnchor托管程序实战全记录托管(Escrow)是去中心化金融的基石。在Solana这种基于账户模型的链上,如何安全地管理互不信任的资产交换?答案在于对PDA(程序派生地址)权限与交易原子性的深度掌握。本文是一份完整的工程实战记录。我们将从Merm

从核心逻辑到上链部署:Solana Anchor 托管程序实战全记录

托管(Escrow)是去中心化金融的基石。在 Solana 这种基于账户模型的链上,如何安全地管理互不信任的资产交换?答案在于对 PDA(程序派生地址)权限交易原子性的深度掌握。

本文是一份完整的工程实战记录。我们将从 Mermaid 逻辑架构图出发,深度拆解 Anchor 框架下托管合约的底层实现——从状态账户定义到 MakeTakeRefund 三大指令的权限约束,再到本地与 Devnet 开发网的自动化部署验证。这不仅是代码的堆砌,更是一次完整的链上资产管理方案闭环。

🧩 Escrow 全流程 Mermaid 图

image-20260124210021767.png

🧠 场景说明

  • Maker:先出 Token A
  • Taker:后出 Token B
  • Vault:PDA 控制的 Token A 保险箱
  • 真正收钱的地方永远是 ATA,不是钱包地址

🚀 实操

创建项目

anchor init blueshift_anchor_escrow

cd blueshift_anchor_escrow

添加依赖

cargo add anchor-lang --features init-if-needed
cargo add anchor-spl

查看项目目录结构

blueshift_anchor_escrow on  master [?] via 🦀 1.89.0 took 18.0s 
➜ tree . -L 6 -I "docs|target|node_modules"
.
├── Anchor.toml
├── Cargo.lock
├── Cargo.toml
├── Makefile
├── app
├── cliff.toml
├── deny.toml
├── deploy_out
│   └── blueshift_anchor_escrow.so
├── migrations
│   └── deploy.ts
├── package.json
├── pnpm-lock.yaml
├── programs
│   └── blueshift_anchor_escrow
│       ├── Cargo.toml
│       └── src
│           ├── errors.rs
│           ├── instructions
│           │   ├── make.rs
│           │   ├── mod.rs
│           │   ├── refund.rs
│           │   └── take.rs
│           ├── lib.rs
│           └── state.rs
├── rust-toolchain.toml
├── tests
│   └── blueshift_anchor_escrow.ts
└── tsconfig.json

9 directories, 21 files

实现程序

lib.rs 文件

use anchor_lang::prelude::*;
mod errors;
mod instructions;
mod state;
use instructions::*;

declare_id!("22222222222222222222222222222222222222222222");

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

    #[instruction(discriminator = 0)]
    pub fn make(ctx: Context<Make>, seed: u64, receive: u64, amount: u64) -> Result<()> {
        instructions::make::handler(ctx, seed, receive, amount)
    }

    #[instruction(discriminator = 1)]
    pub fn take(ctx: Context<Take>) -> Result<()> {
        instructions::take::handler(ctx)
    }

    #[instruction(discriminator = 2)]
    pub fn refund(ctx: Context<Refund>) -> Result<()> {
        instructions::refund::handler(ctx)
    }
}

state.rs 文件

use anchor_lang::prelude::*;

pub const ESCROW_SEED: &[u8] = b"escrow";

#[derive(InitSpace)]
#[account(discriminator = 1)]
pub struct Escrow {
    pub seed: u64,
    pub maker: Pubkey,
    pub mint_a: Pubkey,
    pub mint_b: Pubkey,
    pub receive: u64,
    pub bump: u8,
}

errors.rs 文件

use anchor_lang::prelude::*;

#[error_code]
pub enum EscrowError {
    #[msg("Invalid amount")]
    InvalidAmount,
    #[msg("Invalid maker")]
    InvalidMaker,
    #[msg("Invalid mint a")]
    InvalidMintA,
    #[msg("Invalid mint b")]
    InvalidMintB,
}

instructions/mod.rs 文件

pub mod make;
pub mod refund;
pub mod take;

pub use make::*;
pub use refund::*;
pub use take::*;

instructions/make.rs 文件

use anchor_lang::prelude::*;
use anchor_spl::{
    associated_token::AssociatedToken,
    token_interface::{transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked},
};

use crate::{
    errors::EscrowError,
    state::{Escrow, ESCROW_SEED},
};

/// 1. 初始化托管记录并存储所有条款。
/// 2. 创建金库(一个由 escrow 拥有的 mint_a 的关联代币账户 (ATA))。
/// 3. 使用 CPI 调用 SPL-Token 程序,将创建者的 Token A 转移到该金库中。

#[derive(Accounts)]
#[instruction(seed: u64)]
pub struct Make<'info> {
    #[account(mut)]
    pub maker: Signer<'info>,

    #[account(
        init,
        payer = maker,
        space = Escrow::INIT_SPACE + Escrow::DISCRIMINATOR.len(),
        seeds = [ESCROW_SEED, maker.key().as_ref(), seed.to_le_bytes().as_ref()],
        bump,
    )]
    pub escrow: Account<'info, Escrow>,

    /// Token Accounts
    #[account(
        mint::token_program = token_program
    )]
    pub mint_a: InterfaceAccount<'info, Mint>,

    #[account(
        mint::token_program = token_program
    )]
    pub mint_b: InterfaceAccount<'info, Mint>,

    #[account(
        mut,
        associated_token::mint = mint_a,
        associated_token::authority = maker,
        associated_token::token_program = token_program
    )]
    pub maker_ata_a: InterfaceAccount<'info, TokenAccount>,
    #[account(
        init,
        payer = maker,
        associated_token::mint = mint_a,
        associated_token::authority = escrow,
        associated_token::token_program = token_program
    )]
    pub vault: InterfaceAccount<'info, TokenAccount>,
    /// Programs
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub token_program: Interface<'info, TokenInterface>,
    pub system_program: Program<'info, System>,
}

impl<'info> Make<'info> {
    /// Create the Escrow
    pub(crate) fn populate_escrow(&mut self, seed: u64, amount: u64, bump: u8) -> Result<()> {
        self.escrow.set_inner(Escrow {
            seed,
            maker: self.maker.key(),
            mint_a: self.mint_a.key(),
            mint_b: self.mint_b.key(),
            receive: amount,
            bump,
        });
        Ok(())
    }

    ///  Deposit the tokens
    pub(crate) fn deposit_tokens(&self, amount: u64) -> Result<()> {
        let cpi_accounts = TransferChecked {
            from: self.maker_ata_a.to_account_info(),
            to: self.vault.to_account_info(),
            mint: self.mint_a.to_account_info(),
            authority: self.maker.to_account_info(),
        };

        let cpi_ctx = CpiContext::new(self.token_program.to_account_info(), cpi_accounts);

        transfer_checked(cpi_ctx, amount, self.mint_a.decimals)
    }
}

pub fn handler(ctx: Context<Make>, seed: u64, receive: u64, amount: u64) -> Result<()> {
    // Validate the amount
    require_gt!(receive, 0, EscrowError::InvalidAmount);
    require_gt!(amount, 0, EscrowError::InvalidAmount);
    // Save the Escrow Data
    ctx.accounts
        .populate_escrow(seed, receive, ctx.bumps.escrow)?;
    // Deposit Tokens
    ctx.accounts.deposit_tokens(amount)?;
    Ok(())
}

这段代码实现了 Escrow 合约中 make 指令的完整创建流程:首先通过 PDA(由 ESCROW_SEED + maker + seed 派生)初始化并存储一份托管协议数据,明确交易双方将使用的 mint_amint_b 以及期望接收的数量;随后为该 escrow 创建一个由其自身控制的 Token A 金库(vault,对应 escrow 作为 authority 的 ATA);最后通过 CPI 调用 SPL Token Program 的 transfer_checked,将创建者(maker)账户中的 Token A 安全转入金库中。整个过程在账户约束层完成权限与一致性校验,在逻辑层完成状态写入与资产托管,确保 escrow 在创建阶段即具备完整、可信的交易条件。

instructions/take.rs 文件

use anchor_lang::prelude::*;
use anchor_spl::{
    associated_token::AssociatedToken,
    token_interface::{
        close_account, transfer_checked, CloseAccount, Mint, TokenAccount, TokenInterface,
        TransferChecked,
    },
};

use crate::{
    errors::EscrowError,
    state::{Escrow, ESCROW_SEED},
};

/// 1. 关闭托管记录,将其租金 lamports 返还给创建者。
/// 2. 将 Token A 从保管库转移到接受者,然后关闭保管库。
/// 3. 将约定数量的 Token B 从接受者转移到创建者。

#[derive(Accounts)]
pub struct Take<'info> {
    #[account(mut)]
    pub taker: Signer<'info>,

    #[account(mut)]
    pub maker: SystemAccount<'info>,

    #[account(
        mut,
        close = maker,
        seeds = [ESCROW_SEED, maker.key().as_ref(), escrow.seed.to_le_bytes().as_ref()],
        bump = escrow.bump,
        has_one = maker @ EscrowError::InvalidMaker,
        has_one = mint_a @ EscrowError::InvalidMintA,
        has_one = mint_b @ EscrowError::InvalidMintB,
    )]
    pub escrow: Box<Account<'info, Escrow>>,
    /// Token Accounts
    pub mint_a: Box<InterfaceAccount<'info, Mint>>,
    pub mint_b: Box<InterfaceAccount<'info, Mint>>,
    #[account(
        mut,
        associated_token::mint = mint_a,
        associated_token::authority = escrow,
        associated_token::token_program = token_program
    )]
    pub vault: Box<InterfaceAccount<'info, TokenAccount>>,
    #[account(
        init_if_needed,
        payer = taker,
        associated_token::mint = mint_a,
        associated_token::authority = taker,
        associated_token::token_program = token_program
    )]
    pub taker_ata_a: Box<InterfaceAccount<'info, TokenAccount>>,
    #[account(
        mut,
        associated_token::mint = mint_b,
        associated_token::authority = taker,
        associated_token::token_program = token_program
    )]
    pub taker_ata_b: Box<InterfaceAccount<'info, TokenAccount>>,
    #[account(
        init_if_needed,
        payer = taker,
        associated_token::mint = mint_b,
        associated_token::authority = maker,
        associated_token::token_program = token_program
    )]
    pub maker_ata_b: Box<InterfaceAccount<'info, TokenAccount>>,
    /// Programs
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub token_program: Interface<'info, TokenInterface>,
    pub system_program: Program<'info, System>,
}

impl<'info> Take<'info> {
    fn transfer_to_maker(&mut self) -> Result<()> {
        transfer_checked(
            CpiContext::new(
                self.token_program.to_account_info(),
                TransferChecked {
                    from: self.taker_ata_b.to_account_info(),
                    to: self.maker_ata_b.to_account_info(),
                    mint: self.mint_b.to_account_info(),
                    authority: self.taker.to_account_info(),
                },
            ),
            self.escrow.receive,
            self.mint_b.decimals,
        )?;
        Ok(())
    }

    fn withdraw_and_close_vault(&mut self) -> Result<()> {
        let binding = self.maker.to_account_info().key();
        // Create the signer seeds for the vault
        let signer_seeds: [&[&[u8]]; 1] = [&[
            b"escrow",
            binding.as_ref(),
            &self.escrow.seed.to_le_bytes()[..],
            &[self.escrow.bump],
        ]];

        let amount = self.vault.amount;
        require!(amount > 0, EscrowError::InvalidAmount);

        // Transfer Token A (Vault -> Taker)
        transfer_checked(
            CpiContext::new_with_signer(
                self.token_program.to_account_info(),
                TransferChecked {
                    from: self.vault.to_account_info(),
                    to: self.taker_ata_a.to_account_info(),
                    mint: self.mint_a.to_account_info(),
                    authority: self.escrow.to_account_info(),
                },
                &signer_seeds,
            ),
            amount,
            self.mint_a.decimals,
        )?;
        // Close the vault
        close_account(CpiContext::new_with_signer(
            self.token_program.to_account_info(),
            CloseAccount {
                account: self.vault.to_account_info(),
                authority: self.escrow.to_account_info(),
                destination: self.maker.to_account_info(),
            },
            &signer_seeds,
        ))?;
        Ok(())
    }
}

pub fn handler(ctx: Context<Take>) -> Result<()> {
    // Transfer Token B to Maker
    ctx.accounts.transfer_to_maker()?;
    // Withdraw and close the Vault
    ctx.accounts.withdraw_and_close_vault()?;
    Ok(())
}

这段代码实现了 Escrow 合约中的 take 指令,用于完成托管交易的最终成交与清算流程:在账户约束层首先校验 escrow 的 PDA、创建者、以及参与的两种 Mint 的一致性;在执行逻辑中,先由接受者(taker)将约定数量的 Token B 转移给创建者(maker),随后通过 PDA 签名将托管金库(vault)中的全部 Token A 转移给 taker,并在转移完成后关闭 vault,将其剩余租金返还给 maker;同时 escrow 账户本身也在指令结束时被自动关闭,从而保证资产与状态一次性结算、无残留账户,完成一个完整、原子化的 escrow 成交流程。

instructions/refund.rs 文件

use anchor_lang::prelude::*;
use anchor_spl::{
    associated_token::AssociatedToken,
    token_interface::{
        close_account, transfer_checked, CloseAccount, Mint, TokenAccount, TokenInterface,
        TransferChecked,
    },
};

use crate::{
    errors::EscrowError,
    state::{Escrow, ESCROW_SEED},
};

/// 1. 关闭托管 PDA,并将其租金 lamports 返还给创建者。
/// 2. 将金库中的全部 Token A 余额转回创建者,然后关闭金库账户。
/// 只有 Maker 能在没人 Take 的情况下,
/// 把 Vault 里的 Token A 原路拿回,并彻底销毁 Escrow。
/// 它解决的是这个问题:
/// ❓ 如果一直没人来换,我的 Token A 会不会被锁死?
/// 答案:不会,Refund 就是逃生门。

#[derive(Accounts)]
pub struct Refund<'info> {
    #[account(mut)]
    pub maker: Signer<'info>,

    #[account(
        mut,
        close = maker,
        seeds = [ESCROW_SEED, maker.key().as_ref(), escrow.seed.to_le_bytes().as_ref()],
        bump = escrow.bump,
        has_one = maker @ EscrowError::InvalidMaker, // 校验 maker 与 escrow 一致。
        has_one = mint_a @ EscrowError::InvalidMintA
    )]
    pub escrow: Account<'info, Escrow>,

    /// Token Accounts
    #[account(mint::token_program = token_program)]
    pub mint_a: InterfaceAccount<'info, Mint>,

    /// Vault holding Token A
    #[account(
        mut,
        associated_token::mint = mint_a,
        associated_token::authority = escrow,
        associated_token::token_program = token_program
    )]
    pub vault: InterfaceAccount<'info, TokenAccount>,

    #[account(
        init_if_needed,
        payer = maker,
        associated_token::mint = mint_a,
        associated_token::authority = maker,
        associated_token::token_program = token_program
    )]
    pub maker_ata_a: InterfaceAccount<'info, TokenAccount>,

    /// Programs
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub token_program: Interface<'info, TokenInterface>,
    pub system_program: Program<'info, System>,
}

impl<'info> Refund<'info> {
    pub fn refund(&mut self) -> Result<()> {
        self.withdraw_and_close_vault()?;
        Ok(())
    }

    fn withdraw_and_close_vault(&self) -> Result<()> {
        let escrow = &self.escrow;
        let seed_bytes = escrow.seed.to_le_bytes();
        let signer_seeds: [&[&[u8]]; 1] = [&[
            ESCROW_SEED,
            escrow.maker.as_ref(),
            seed_bytes.as_ref(),
            &[escrow.bump],
        ]];

        let amount = self.vault.amount;
        if amount == 0 {
            return Ok(());
        }

        // if amount > 0 {
        // 1️⃣ Transfer all Token A from vault → maker
        // // Vault -> Maker (Token A)
        transfer_checked(
            CpiContext::new_with_signer(
                self.token_program.to_account_info(),
                TransferChecked {
                    from: self.vault.to_account_info(),
                    to: self.maker_ata_a.to_account_info(),
                    mint: self.mint_a.to_account_info(),
                    authority: self.escrow.to_account_info(),
                },
                &signer_seeds,
            ),
            amount,
            self.mint_a.decimals,
        )?;
        // }

        // 2️⃣ Close vault account, refund rent → maker
        close_account(CpiContext::new_with_signer(
            self.token_program.to_account_info(),
            CloseAccount {
                account: self.vault.to_account_info(),
                authority: self.escrow.to_account_info(),
                destination: self.maker.to_account_info(),
            },
            &signer_seeds,
        ))?;

        // 3️⃣ Escrow PDA will be closed automatically (close = maker)
        Ok(())
    }
}

pub fn handler(ctx: Context<Refund>) -> Result<()> {
    ctx.accounts.refund()?;
    Ok(())
}

Refund 和 Take 的“本质区别”

指令 Token A 去向 Token B 去向 谁能调用
Take Vault → Taker Taker → Maker 任意 Taker
Refund Vault → Maker ❌ 不存在 只有 Maker

👉 Refund 不涉及 Token B

👉 Refund 只关心 Token A + 权限

Refund 的安全目标(非常重要)

Refund 必须保证 3 件事:

1️⃣ 只有 Maker 能退

2️⃣ Token A 只能退回 Maker

3️⃣ Escrow 和 Vault 只能被关闭一次

这段代码实现了 Escrow 合约中的 refund 指令,用来处理“无人成交时的安全退出”场景:只有创建者(maker)才能调用该指令,通过 PDA 签名从 escrow 控制的 vault 中将全部 Token A 原路转回自己的关联代币账户,并在转移完成后关闭 vault,将其租金返还给 maker;与此同时,close = maker 约束会在指令结束时自动销毁 escrow PDA,本次托管状态被彻底清理,从而保证即使一直没有 taker 出现,创建者的资产也不会被锁死,Escrow 始终具备可回退、可清算的“逃生门”机制。

构建一个 Anchor 托管(Escrow)程序

image-20260124210241720.png

  • 上面一条线

    👉 Maker Token A → Vault → Taker Token A

  • 下面一条线

    👉 Taker Token B → Maker Token B

✔️ Vault 只碰 Token A

✔️ Token B 永远直接给 Maker

✔️ 两条线在一次 take原子完成

Token B 给 Maker 的前提是: 在同一个 take 指令里, Token A 必须“同时”从 Vault 转给 Taker。

否则:

  • Token B 转不出去
  • 整笔交易回滚
  • 状态不变,谁都拿不到东西

测试

编写测试


import * as anchor from "@coral-xyz/anchor"
import { Program } from "@coral-xyz/anchor"
import type { BlueshiftAnchorEscrow } from "../target/types/blueshift_anchor_escrow.js"
import {
    getOrCreateAssociatedTokenAccount,
    createMint,
    mintTo,
    getAssociatedTokenAddressSync,
    getAccount,
    TOKEN_PROGRAM_ID,
    ASSO...

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

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

0 条评论

请先 登录 后评论