Solana 60 天课程

2025年02月27日更新 77 人订阅
原价: ¥ 36 限时优惠
专栏简介 开始 Solana - 安装与故障排除 Solana 和 Rust 中的算术与基本类型 Solana Anchor 程序 IDL Solana中的Require、Revert和自定义错误 Solana程序是可升级的,并且没有构造函数 Solidity开发者的Rust基础 Rust不寻常的语法 Rust 函数式过程宏 Rust 结构体与属性式和自定义派生宏 Rust 和 Solana 中的可见性与“继承” Solana时钟及其他“区块”变量 Solana 系统变量详解 Solana 日志、“事件”与交易历史 Tx.origin、msg.sender 和 onlyOwner 在 Solana 中:识别调用者 Solana 计算单元与交易费用介绍 在 Solana 和 Anchor 中初始化账户 Solana 计数器教程:在账户中读写数据 使用 Solana web3 js 和 Anchor 读取账户数据 在Solana中创建“映射”和“嵌套映射” Solana中的存储成本、最大存储容量和账户调整 在 Solana 中读取账户余额的 Anchor 方法:address(account).balance 功能修饰符(view、pure、payable)和回退函数在 Solana 中不存在的原因 在 Solana 上实现 SOL 转账及构建支付分配器 使用不同签名者修改账户 PDA(程序派生地址)与 Solana 中的密钥对账户 理解 Solana 中的账户所有权:从PDA中转移SOL Anchor 中的 Init if needed 与重初始化攻击 Solana 中的多重调用:批量交易与交易大小限制 Solana 中的所有者与权限 在Solana中删除和关闭账户与程序 在 Anchor 中:不同类型的账户 在链上读取另一个锚点程序账户数据 在 Anchor 中的跨程序调用(CPI) SPL Token 的运作方式 使用 Anchor 和 Web3.js 转移 SPL Token Solana 教程 - 如何实现 Token 出售 基础银行教程 Metaplex Token 元数据工作原理 使用Metaplex实施代币元数据 使用 LiteSVM 进行时间旅行测试

使用 LiteSVM 进行时间旅行测试

本文介绍了如何使用 LiteSVM 在 Solana 上测试依赖于时间的程序,以 Dutch auction 为例,展示了如何创建拍卖程序,并使用 LiteSVM 模拟时间流逝,验证价格随时间线性下降的逻辑。LiteSVM 允许开发者在本地测试环境中控制区块链时钟,加速测试过程。

在 Solana 中,编写依赖于时间流逝的测试用例是很棘手的。 我们可能想测试在一天过去后我们的代码中会发生什么,但是我们不能让我们的测试用例花费一天的时间来运行,因为这会使我们的测试不切实际。 LiteSVM 通过让你立即将区块链时钟向前移动来解决这个问题,就像为本地测试进行时间旅行一样。

为了展示这在实践中是如何运作的,我们将使用 Anchor 为 NFT 构建一个基本的荷兰式拍卖。 荷兰式拍卖以一个高价开始,该价格会随着时间的推移自动降低,直到买家接受当前价格。 这是一个时间敏感行为的清晰示例,LiteSVM 使测试变得更加简单。

LiteSVM 的工作方式类似于 Solana 的本地验证器 (solana-test-validator),但让我们能够更好地控制测试环境的本地区块链状态。 它可以用于我们的 TypeScript 测试中,并且可以轻松测试基于时间的逻辑,比如拍卖或 vesting。

如果你熟悉以太坊开发,LiteSVM 的时间操作能力类似于 Foundry 的 vm.warp(用于推进区块时间戳),但针对 Solana 基于 slot 的架构进行了定制。

以下是本文将涵盖的内容概述:

  • 我们将创建一个用于 NFT 销售的荷兰式拍卖程序,其中包含创建拍卖和以递减的价格购买 NFT 的功能
  • 我们将解释这些功能并为它们编写测试
  • 最后,我们将使用 LiteSVM 将时间向前调整 15 分钟来测试价格衰减,而无需等待,并验证拍卖价格是否正确下降

现在让我们创建荷兰式拍卖程序。

创建荷兰式拍卖程序

如前文所述,荷兰式拍卖以一个高价开始,并且随着时间的推移而降低,直到买家接受。 为了保证交付,我们将 NFT 锁定在程序控制的 vault(escrow)中。 这可以防止卖家撤回或重复出售 NFT,并让买家避免在付款后依赖卖家来释放 NFT。 vault 允许程序在买家接受后立即结算交换。

荷兰式拍卖程序将仅包含两个函数:

  • 一个 initialize_auction 函数,用于创建所需的帐户,并将卖家的 NFT 存入由我们的程序拥有的 vault 帐户
  • 一个 buy 函数,允许买家以当前拍卖价格使用 SOL 购买 NFT。

接下来,我们将创建以下帐户:

  • Auction:一个由我们的程序拥有的 PDA,用于存储拍卖详细信息,例如起始价格、拍卖持续时间等。
  • Vault Authority:这将是一个由我们的程序拥有的 PDA,用于授权在发生销售后将 NFT 转移给买家。 我们将在本文的“为什么我们有一个 vault authority PDA?”部分中详细介绍这一点。
  • Vault:一个关联的 token 帐户,用于保存存入的 NFT。 这由 Vault Authority PDA 拥有。

现在创建一个名为 dutch-auction 的新 Anchor 项目,并使用以下依赖项修改 programs/dutch-auction/Cargo.toml 文件:

  • anchor-spl 用于 SPL token 功能
  • anchor-spl/idl-buildfeatures 下,用于在生成的 IDL 文件中包含 SPL 类型
[package]
name = "dutch-auction"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"

[lib]
crate-type = ["cdylib", "lib"]
name = "dutch_auction"

[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]

[dependencies]
anchor-lang = "0.30.1"
anchor-spl = "0.30.1"

你可以运行 anchor build 来确认依赖项没有问题。

初始化拍卖程序

现在我们准备好依赖项了,用下面的代码替换 programs/dutch-auction/src/lib.rs 中的程序代码,其中包含 initialize_auction 函数,该函数执行以下操作:

  1. 它初始化拍卖帐户,并在帐户中记录拍卖详细信息和持续时间(以秒为单位)。
  2. 它将拍卖的 NFT 从卖家的关联 token 帐户 (ATA) 转移到我们程序拥有的 vault(也是一个 ATA)中。

use anchor_lang::prelude::*;
use anchor_lang::solana_program::{program::invoke, system_instruction};
use anchor_spl::associated_token::AssociatedToken;
use anchor_spl::token::{self, Mint, Token, TokenAccount, Transfer};

declare_id!("GKP6La354ejTfqNDW4gdzC2mzNjnP6cMY1vtH6EN15zq");

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

    pub fn initialize_auction(
        ctx: Context<InitializeAuction>,
        starting_price: u64,
        floor_price: u64,
        duration: i64, // 以秒为单位
    ) -> Result<()> {
                // 初始化拍卖帐户并设置卖家详细信息
        let auction = &mut ctx.accounts.auction;
        auction.seller = ctx.accounts.seller.key();
        auction.starting_price = starting_price;
        auction.floor_price = floor_price;
        auction.duration = duration;
        auction.start_time = Clock::get()?.unix_timestamp;
        auction.token_mint = ctx.accounts.mint.key();

        // 将 1 个 token 从卖家 ATA 转移到 vault escrow
        let cpi_accounts = Transfer {
            from: ctx.accounts.seller_ata.to_account_info(),
            to: ctx.accounts.vault.to_account_info(),
            authority: ctx.accounts.seller.to_account_info(),
        };
        let cpi_ctx = CpiContext::new(ctx.accounts.token_program.to_account_info(), cpi_accounts);
        token::transfer(cpi_ctx, 1)?;

        Ok(())
    }
}

运行 anchor keys sync 以将程序 ID 替换为你自己的 ID。

然后,将带有 Auction 状态的 InitializeAuction 帐户结构添加到程序代码中。

InitializeAuction 指定了拍卖初始化期间涉及的以下帐户:

  • auction:用于存储拍卖状态(起始价格、持续时间、卖家信息等)的帐户。
  • seller:创建拍卖并签署交易的 NFT 所有者。
  • seller_ata:卖家的关联 token 帐户,其中包含要拍卖的 NFT。
  • vault_auth:一个 PDA(程序派生地址),用作 vault 帐户的授权。 这使我们的程序可以控制 NFT 转移。
  • vault (escrow):一个关联的 token 帐户,用于在拍卖期间保存卖家存入的 NFT。 这由 Vault Authority PDA 拥有。
  • mint:NFT mint 帐户,表示正在拍卖的 token。
#[derive(Accounts)]
pub struct InitializeAuction<'info> {
    #[account(init, payer = seller, space = 8 + Auction::INIT_SPACE)]
    pub auction: Account<'info, Auction>,

    #[account(mut)]
    pub seller: Signer<'info>,

    #[account(\
        mut,\
        associated_token::mint = mint,\
        associated_token::authority = seller\
    )]
    pub seller_ata: Account<'info, TokenAccount>,

    /// CHECK: 这是将拥有 vault 的 PDA
    #[account(\
        seeds = [b"vault", auction.key().as_ref()],\
        bump\
    )]
    pub vault_auth: UncheckedAccount<'info>,

    #[account(\
        init,\
        payer = seller,\
        associated_token::mint = mint,\
        associated_token::authority = vault_auth\
    )]
    pub vault: Account<'info, TokenAccount>,

    pub mint: Account<'info, Mint>,
    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
}

#[account]
#[derive(InitSpace)]
pub struct Auction {
    pub seller: Pubkey,
    pub starting_price: u64,
    pub floor_price: u64,
    pub duration: i64,
    pub start_time: i64,
    pub token_mint: Pubkey,
    pub sold: bool,
}

我们现在不测试此函数,我们将在整个程序到位后稍后进行测试。

现在让我们添加购买拍卖 token 的函数。

购买拍卖的 token

在我们的拍卖设置中,NFT 位于由程序控制的 escrow (vault) 帐户中,直到它被出售。 购买拍卖的 token 意味着将 lamports 从买方转移到卖方,以换取 NFT。

我们现在将添加一个函数,使我们能够以当前的荷兰式拍卖价格购买 NFT。 这是该函数的作用

  1. 它检查 NFT 是否已售出
  2. 获取当前时间并检查拍卖是否有效,如果拍卖尚未开始,则会恢复并显示 AuctionNotStarted 错误;如果拍卖持续时间已过,则会显示 AuctionEnded 错误。
  3. 计算到目前为止的经过时间,并使用线性荷兰式拍卖的公式得出当前价格。
  4. 它确保买家有足够的 lamports。
  5. 将 lamports 从买方转移到卖方。
  6. 设置签名者种子,以便程序可以为 vault 签名。
  7. 将 NFT 从 vault 转移到买家的关联 token 帐户。
    pub fn buy(ctx: Context<Buy>) -> Result<()> {
        // 检查 NFT 是否已售出
        require!(
            ctx.accounts.auction.sold == false,
            AuctionError::NFTAlreadySold
        );
        let auction = &mut ctx.accounts.auction;
        let now = Clock::get()?.unix_timestamp; // 从时钟 sysvar 中获取当前时间

        // 验证拍卖时间
        require!(now >= auction.start_time, AuctionError::AuctionNotStarted);
        require!(
            now < auction.start_time + auction.duration,
            AuctionError::AuctionEnded
        );

        // 根据经过的时间计算当前价格(线性衰减)
        let elapsed_time = (now - auction.start_time).min(auction.duration) as u64;
        let total_price_drop = auction.starting_price - auction.floor_price;
        let price_dropped_so_far = total_price_drop * elapsed_time / auction.duration as u64;
        let price = auction.starting_price - price_dropped_so_far;

        // 验证资金并转移付款
        require!(
            ctx.accounts.buyer.lamports() >= price,
            AuctionError::InsufficientFunds
        );
        invoke(
            &system_instruction::transfer(
                &ctx.accounts.buyer.key(),
                &ctx.accounts.seller.key(),
                price,
            ),
            &[\
                ctx.accounts.buyer.to_account_info(),\
                ctx.accounts.seller.to_account_info(),\
                ctx.accounts.system_program.to_account_info(),\
            ],
        )?;

        // 将 NFT 转移到买家
        let auction_key = ctx.accounts.auction.key();
        let vault_auth_bump = ctx.bumps.vault_auth;
        let vault_signer_seeds = &[b"vault", auction_key.as_ref(), &[vault_auth_bump]]; // vault PDA 的签名者种子

        token::transfer(
            CpiContext::new_with_signer(
                ctx.accounts.token_program.to_account_info(),
                Transfer {
                    from: ctx.accounts.vault.to_account_info(),
                    to: ctx.accounts.buyer_ata.to_account_info(),
                    authority: ctx.accounts.vault_auth.to_account_info(),
                },
                &[vault_signer_seeds],
            ),
            1, // 转移 1 个 token(拍卖的 NFT)
        )?;

        Ok(())
    }

为什么我们有一个 vault authority PDA?

我们使用 vault ATA 在卖家存入 NFT 后保存它。 我们需要一个 vault authority PDA,以便我们的程序可以为该 ATA 签名转移,而无需外部密钥对或签名者。

回想一下 Token Sale with Total Supply 文章,我们展示了如何使 mint PDA 成为其自身的授权,以便程序可以自行 mint 新 token。 在这里,我们对 vault ATA 使用相同的概念,但授予我们的程序移动现有 token 的权力。 我们从 ["vault", auction.key().as_ref()] 派生 vault_auth,并将其设置为 ATA 的授权。

buy() 函数中,我们使用这些种子调用 CpiContext::new_with_signer。 Solana 运行时看到我们的程序控制 vault_auth 并允许它为 vault ATA 签名。 这允许我们的程序自动将 NFT 转移给买家,而无需外部签名者。

image.png

现在添加 Buy 帐户结构。 Buy 结构指定了我们的拍卖程序中 NFT 购买期间涉及的帐户:

  • auction:包含拍卖详细信息和状态的拍卖帐户。
  • seller:原始 NFT 卖家,将收到 SOL 付款。
  • buyer:以当前拍卖价格购买 NFT 的帐户,也是交易签名者。
  • buyer_ata:买家的关联 token 帐户,将收到购买的 NFT。
  • vault_auth:控制 vault 并授权将 NFT 转移给买家的 PDA 授权。
  • vault:保存 escrow NFT 的 vault 帐户,由 vault_auth PDA 拥有。
  • 最后两个帐户,Token program(用于 NFT 转移)和 System program(用于 SOL 转移),是我们与之交互的本机程序。


#[derive(Accounts)]
pub struct Buy<'info> {
    #[account(mut, has_one = seller)] // 确保我们传递正确的拍卖帐户
    pub auction: Account<'info, Auction>, // 拍卖帐户
    /// CHECK: 卖家帐户
    #[account(mut)]
    pub seller: AccountInfo<'info>, // 卖家帐户
    #[account(mut)]
    pub buyer: Signer<'info>, // 买家帐户

    #[account(\
        mut,\
        associated_token::mint = auction.token_mint,\
        associated_token::authority =...

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

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

0 条评论

请先 登录 后评论