Solana में, समय बीतने पर निर्भर करने वाले टेस्ट केस लिखना मुश्किल है। हम यह टेस्ट करना चाह सकते हैं कि एक दिन बीतने के बाद हमारे कोड में कुछ होता है, लेकिन हम अपने टेस्ट केस को चलने के लिए एक दिन का समय नहीं दे सकते क्योंकि यह हमारी टेस्टिंग को अव्यावहारिक बना देगा। LiteSVM आपको ब्लॉकचेन क्लॉक को तुरंत आगे बढ़ाने की अनुमति देकर इसे हल करता है, जो आपके लोकल टेस्ट्स के लिए टाइम ट्रैवल की तरह है।
यह वास्तव में कैसे काम करता है, यह दिखाने के लिए, हम Anchor का उपयोग करके NFT के लिए एक बेसिक Dutch auction (डच नीलामी) बनाएंगे। Dutch auction एक उच्च कीमत के साथ शुरू होता है जो समय के साथ स्वचालित रूप से तब तक कम होता जाता है जब तक कि कोई खरीदार वर्तमान कीमत को स्वीकार नहीं कर लेता। यह टाइम-सेंसिटिव व्यवहार (समय-संवेदनशील व्यवहार) का एक स्पष्ट उदाहरण है जहां LiteSVM टेस्टिंग को बहुत आसान बना देता है।
LiteSVM Solana के लोकल वैलिडेटर (solana-test-validator) की तरह काम करता है लेकिन हमें टेस्ट वातावरण के लिए लोकल ब्लॉकचेन स्टेट पर अधिक नियंत्रण देता है। इसका उपयोग हमारे TypeScript टेस्ट्स में किया जा सकता है, और यह ऑक्शन या वेस्टिंग जैसे टाइम-बेस्ड लॉजिक को टेस्ट करना आसान बनाता है।
यदि आप Ethereum डेवलपमेंट से परिचित हैं, तो LiteSVM की टाइम मैनिपुलेशन क्षमताएं Foundry के vm.warp (ब्लॉक टाइमस्टैम्प को आगे बढ़ाने के लिए) के समान हैं, लेकिन इन्हें Solana के स्लॉट-बेस्ड आर्किटेक्चर के लिए तैयार किया गया है।
यहां इस लेख में हम जो कवर करेंगे उसका अवलोकन दिया गया है:
- हम घटती कीमतों पर ऑक्शन बनाने और NFT खरीदने के फ़ंक्शन्स के साथ NFT बिक्री के लिए एक Dutch auction प्रोग्राम बनाएंगे
- हम इन फ़ंक्शन्स को समझाएंगे और उनके लिए टेस्ट लिखेंगे
- अंत में, हम प्रतीक्षा किए बिना प्राइस डिके (मूल्य में कमी) को टेस्ट करने के लिए LiteSVM का उपयोग करके समय को 15 मिनट आगे (warp) बढ़ाएंगे, और सत्यापित करेंगे कि ऑक्शन की कीमत सही ढंग से गिरती है
अब चलिए Dutch auction प्रोग्राम बनाते हैं।
Dutch auction प्रोग्राम बनाएं
जैसा कि पहले उल्लेख किया गया है, एक Dutch auction उच्च कीमत पर शुरू होता है और समय के साथ तब तक गिरता है जब तक कि कोई खरीदार स्वीकार न कर ले। डिलीवरी की गारंटी देने के लिए, हम NFT को एक प्रोग्राम-नियंत्रित वॉल्ट (escrow) में लॉक करते हैं। यह विक्रेता को NFT वापस लेने या दो बार बेचने से रोकता है, और यह खरीदारों को भुगतान के बाद NFT जारी करने के लिए विक्रेता पर निर्भर रहने से बचाता है। वॉल्ट प्रोग्राम को खरीदार द्वारा स्वीकार किए जाने के बाद स्वैप को एटॉमिक रूप से सेटल करने की अनुमति देता है।
Dutch auction प्रोग्राम में केवल दो फ़ंक्शन शामिल होंगे:
- एक
initialize_auctionफ़ंक्शन जो आवश्यक अकाउंट्स बनाता है, और विक्रेता के NFT को हमारे प्रोग्राम के स्वामित्व वाले वॉल्ट अकाउंट में जमा करता है। - एक
buyफ़ंक्शन जो खरीदारों को वर्तमान ऑक्शन कीमत पर SOL के साथ NFT खरीदने की अनुमति देता है।
आगे, हम निम्नलिखित अकाउंट्स बनाएंगे:
Auction: ऑक्शन का विवरण स्टोर करने के लिए हमारे प्रोग्राम के स्वामित्व वाला एक PDA, जैसे कि शुरुआती कीमत, ऑक्शन की अवधि, आदि।Vault Authority: बिक्री होने के बाद खरीदार को NFT ट्रांसफर को अधिकृत करने के लिए यह हमारे प्रोग्राम के स्वामित्व वाला एक PDA होगा। हम इस लेख के “Why do we have a vault authority PDA?” अनुभाग में इसके बारे में अधिक विस्तार से जानेंगे।Vault: जमा किए गए NFT को रखने के लिए एक एसोसिएटेड टोकन अकाउंट (associated token account)। यहVault AuthorityPDA के स्वामित्व में है।
अब dutch-auction नाम का एक नया Anchor प्रोजेक्ट बनाएं और निम्नलिखित डिपेंडेंसीज के साथ programs/dutch-auction/Cargo.toml फ़ाइल को मॉडिफाई करें:
- SPL टोकन कार्यक्षमता के लिए
anchor-spl - जनरेटेड IDL फ़ाइल में SPL प्रकारों को शामिल करने के लिए
featuresके तहतanchor-spl/idl-build
[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 फ़ंक्शन शामिल है जो निम्नलिखित कार्य करता है:
- यह ऑक्शन अकाउंट को इनिशियलाइज़ करता है और अकाउंट में ऑक्शन के विवरण और अवधि (सेकंड में) को रिकॉर्ड करता है।
- यह ऑक्शन किए गए NFT को विक्रेता के एसोसिएटेड टोकन अकाउंट (ATA) से हमारे प्रोग्राम-स्वामित्व वाले वॉल्ट (जो कि एक 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, // in seconds
) -> Result<()> {
// Initialize the auction account and set seller details
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();
// Move 1 token from seller ATA into 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(())
}
}
प्रोग्राम ID को अपने ID से बदलने के लिए anchor keys sync रन करें।
फिर, प्रोग्राम कोड में Auction स्टेट के साथ InitializeAuction अकाउंट्स स्ट्रक्ट (struct) जोड़ें।
InitializeAuction ऑक्शन इनिशियलाइज़ेशन के दौरान शामिल निम्नलिखित अकाउंट्स को निर्दिष्ट करता है:
auction: ऑक्शन स्टेट (शुरुआती कीमत, अवधि, विक्रेता की जानकारी, आदि) को स्टोर करने के लिए अकाउंट।seller: NFT का मालिक जो ऑक्शन बनाता है और ट्रांज़ैक्शन पर साइन करता है।seller_ata: विक्रेता का एसोसिएटेड टोकन अकाउंट जिसमें ऑक्शन किया जाने वाला NFT है।vault_auth: एक PDA (Program Derived Address) जो वॉल्ट अकाउंट के लिए अथॉरिटी (authority) के रूप में कार्य करता है। यह हमारे प्रोग्राम को NFT ट्रांसफर को नियंत्रित करने की अनुमति देता है।vault(escrow): एक एसोसिएटेड टोकन अकाउंट जो ऑक्शन के दौरान विक्रेता के जमा किए गए NFT को रखता है। यहVault AuthorityPDA के स्वामित्व में है।mint: NFT मिंट अकाउंट जो ऑक्शन किए जा रहे टोकन का प्रतिनिधित्व करता है।
#[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: This is the PDA that will own the vault
#[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,
}
हम अभी इस फ़ंक्शन को टेस्ट नहीं करेंगे, पूरा प्रोग्राम तैयार हो जाने के बाद हम इसे बाद में करेंगे।
अब ऑक्शन किए गए टोकन को खरीदने के लिए फ़ंक्शन जोड़ते हैं।
ऑक्शन किया गया टोकन खरीदना
हमारे ऑक्शन सेटअप में, NFT तब तक प्रोग्राम द्वारा नियंत्रित एस्क्रो (वॉल्ट) अकाउंट में रहता है जब तक कि इसे बेच नहीं दिया जाता। ऑक्शन किए गए टोकन को खरीदने का अर्थ है NFT के बदले खरीदार से विक्रेता को lamports ट्रांसफर करना।
अब हम एक फ़ंक्शन जोड़ेंगे जो हमें वर्तमान Dutch auction कीमत पर NFT खरीदने देता है। यह फ़ंक्शन क्या करता है, यह नीचे दिया गया है:
- यह जांचता है कि NFT पहले ही बिक तो नहीं गया है
- वर्तमान समय प्राप्त करता है और जांचता है कि ऑक्शन सक्रिय है, यदि ऑक्शन अभी तक शुरू नहीं हुआ है तो यह
AuctionNotStartedएरर के साथ वापस आ जाता है (revert करता है), या यदि ऑक्शन की अवधि समाप्त हो गई है तोAuctionEndedएरर देता है। - अब तक बीते हुए समय की गणना करता है और एक लीनियर Dutch auction के फॉर्मूले का उपयोग करके वर्तमान कीमत प्राप्त करता है।
- यह सुनिश्चित करता है कि खरीदार के पास पर्याप्त lamports हैं।
- खरीदार से विक्रेता को lamports ट्रांसफर करता है।
- साइनर सीड्स (signer seeds) सेट अप करता है ताकि प्रोग्राम वॉल्ट के लिए साइन कर सके।
- NFT को वॉल्ट से खरीदार के एसोसिएटेड टोकन अकाउंट में ट्रांसफर करता है।
pub fn buy(ctx: Context<Buy>) -> Result<()> {
// Check if the NFT is already sold
require!(
ctx.accounts.auction.sold == false,
AuctionError::NFTAlreadySold
);
let auction = &mut ctx.accounts.auction;
let now = Clock::get()?.unix_timestamp; // Get the current time from the clock sysvar
// Validate auction timing
require!(now >= auction.start_time, AuctionError::AuctionNotStarted);
require!(
now < auction.start_time + auction.duration,
AuctionError::AuctionEnded
);
// Calculate current price based on elapsed time (linear decay)
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;
// Verify funds and transfer payment
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(),
],
)?;
// Transfer NFT to buyer
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]]; // Signer seeds for the 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, // transfer 1 token (the auctioned NFT)
)?;
Ok(())
}
हमारे पास एक vault authority PDA क्यों है?
विक्रेता द्वारा इसे जमा करने के बाद हम NFT को रखने के लिए एक वॉल्ट ATA का उपयोग करते हैं। हमें एक वॉल्ट अथॉरिटी PDA की आवश्यकता है ताकि हमारा प्रोग्राम किसी बाहरी कीपेयर (keypair) या साइनर की आवश्यकता के बिना उस ATA के लिए ट्रांसफर पर साइन कर सके।
याद करें कि Token Sale with Total Supply लेख में, हमने दिखाया था कि मिंट PDA को अपना स्वयं का अथॉरिटी (authority) कैसे बनाया जाए ताकि प्रोग्राम स्वयं नए टोकन मिंट कर सके। यहां हम वॉल्ट ATA के लिए उसी अवधारणा का उपयोग करते हैं, लेकिन अपने प्रोग्राम को मौजूदा टोकन को स्थानांतरित करने की शक्ति प्रदान करने के लिए। हम ["vault", auction.key().as_ref()] से vault_auth प्राप्त (derive) करते हैं और इसे ATA के अथॉरिटी के रूप में सेट करते हैं।
buy() फ़ंक्शन में, हम उन सीड्स (seeds) के साथ CpiContext::new_with_signer को कॉल करते हैं। Solana रनटाइम देखता है कि हमारा प्रोग्राम vault_auth को नियंत्रित करता है और इसे वॉल्ट ATA के लिए साइन करने देता है। यह हमारे प्रोग्राम को बिना किसी बाहरी साइनर के स्वचालित रूप से खरीदार को NFT ट्रांसफर करने की अनुमति देता है।

अब Buy अकाउंट्स स्ट्रक्ट (struct) जोड़ें। Buy स्ट्रक्ट हमारे ऑक्शन प्रोग्राम में NFT खरीदारी के दौरान शामिल अकाउंट्स को निर्दिष्ट करता है:
auction: ऑक्शन अकाउंट जिसमें ऑक्शन का विवरण और स्टेट शामिल है।seller: मूल NFT विक्रेता जिसे SOL भुगतान प्राप्त होगा।buyer: वह अकाउंट जो वर्तमान ऑक्शन कीमत पर NFT खरीद रहा है, यह ट्रांज़ैक्शन साइनर भी है।buyer_ata: खरीदार का एसोसिएटेड टोकन अकाउंट जो खरीदा गया NFT प्राप्त करेगा।vault_auth: PDA अथॉरिटी जो वॉल्ट को नियंत्रित करता है और खरीदार को NFT ट्रांसफर को अधिकृत करता है।vault: एस्क्रो किए गए NFT को रखने वाला वॉल्ट अकाउंट, जोvault_authPDA के स्वामित्व में है।- अंतिम दो अकाउंट्स, Token program (NFT ट्रांसफर के लिए), और System program (SOL ट्रांसफर के लिए), नेटिव प्रोग्राम हैं जिनके साथ हम इंटरैक्ट करते हैं।
#[derive(Accounts)]
pub struct Buy<'info> {
#[account(mut, has_one = seller)] // ensure we pass the right auction account
pub auction: Account<'info, Auction>, // auction account
/// CHECK: seller account
#[account(mut)]
pub seller: AccountInfo<'info>, // seller account
#[account(mut)]
pub buyer: Signer<'info>, // buyer account
#[account(
mut,
associated_token::mint = auction.token_mint,
associated_token::authority = buyer
)]
pub buyer_ata: Account<'info, TokenAccount>, // Buyer's ATA
#[account(
mut,
seeds = [b"vault", auction.key().as_ref()],
bump
)]
/// CHECK: PDA authority for the vault
pub vault_auth: AccountInfo<'info>, // Vault authority PDA
#[account(
mut,
associated_token::mint = auction.token_mint,
associated_token::authority = vault_auth
)]
pub vault: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>, // SPL Token program
pub system_program: Program<'info, System>, // System program
}
AuctionError एरर जोड़ें, हम इसका उपयोग buy फ़ंक्शन में करते हैं:
#[error_code]
pub enum AuctionError {
#[msg("Auction hasn't started")]
AuctionNotStarted,
#[msg("Buyer has insufficient funds")]
InsufficientFunds,
#[msg("Auction has ended")]
AuctionEnded,
#[msg("NFT is already sold")]
NFTAlreadySold,
}
हमारा प्रोग्राम अब पूरा हो गया है, तो चलिए इसके लिए कुछ टेस्ट लिखते हैं।
LiteSVM के साथ Dutch Auction प्रोग्राम की टेस्टिंग
Dutch auction में, समय के साथ आइटम की कीमत घटती (decay) है। इस टेस्ट का लक्ष्य यह पुष्टि करना है कि ऑक्शन की कीमत समय के साथ ठीक से घटती है। ऐसा करने के लिए, हम समय को आगे बढ़ाने (warp) के लिए LiteSVM का उपयोग करेंगे और उस बिंदु पर कीमत में गिरावट को लॉग करेंगे।
एक असली ऑन-चेन Dutch auction में, हमें कीमत में गिरावट देखने के लिए वास्तविक समय (real time) में प्रतीक्षा करनी होगी। LiteSVM के साथ, हम समय को आगे बढ़ाकर (warping) इस प्रतीक्षा को छोड़ सकते हैं।
हम टेस्ट को टुकड़ों में जोड़ेंगे, इसलिए सबसे पहले, tests/dutch-auction.ts में प्रोग्राम टेस्ट को नीचे दिए गए कोड से बदलें।
हमने कुछ टेस्ट कांस्टेंट्स (constants) परिभाषित किए हैं। इस टेस्ट के लिए दो प्रमुख लाइब्रेरीज़ पर भी ध्यान दें:
litesvmहमें अपने TypeScript क्लाइंट से सीधे एक लोकल Solana टेस्ट वैलिडेटर स्पिन अप (चालू) करने देता है और टाइम वारपिंग (time warping) के लिए वैलिडेटर की क्लॉक पर हमें नियंत्रण देता है।anchor-litesvmलाइब्रेरी हमारे Anchor प्रोजेक्ट को LiteSVM से जोड़ती है, जिससे इस LiteSVM सेटअप में Anchor प्रोग्राम्स को टेस्ट करना संभव हो जाता है।
import * as anchor from "@coral-xyz/anchor";
import { BN, Program } from "@coral-xyz/anchor";
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
createAssociatedTokenAccountInstruction,
createInitializeMintInstruction,
createMintToInstruction,
getAssociatedTokenAddress,
MINT_SIZE,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import {
Keypair,
LAMPORTS_PER_SOL,
PublicKey,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import { fromWorkspace, LiteSVMProvider } from "anchor-litesvm";
import { assert } from "chai";
import { Clock, LiteSVM } from "litesvm";
import { DutchAuction } from "../target/types/dutch_auction";
// Constants
const STARTING_PRICE = new BN(2_000_000_000); // 2 SOL
const FLOOR_PRICE = new BN(500_000_000); // 0.5 SOL
const DURATION = new BN(3600); // 1 hour
इसके साथ टेस्ट डिपेंडेंसीज़ इंस्टॉल करें: npm install anchor-litesvm litesvm @solana/spl-token
describe ब्लॉक जोड़ें। हम यहां केवल टेस्ट अकाउंट्स और वेरिएबल्स (variables) घोषित करते हैं (हम बाद में उन पर चर्चा करेंगे)।
describe("dutch-auction", () => {
// Define our test variables
let svm: LiteSVM;
let provider: LiteSVMProvider;
let program: Program<DutchAuction>;
// Define our test accounts
const seller = Keypair.generate();
const buyer = Keypair.generate();
let auctionAccount: Keypair;
let mintKp: Keypair;
let sellerAta: PublicKey;
let buyerAta: PublicKey;
let vaultAuth: PublicKey;
let vault: PublicKey;
});
अब, describe ब्लॉक के अंदर इस before टेस्ट ब्लॉक को जोड़ें।
हम निम्नलिखित कार्य करके यहां LiteSVM और अकाउंट्स सेटअप करते हैं:
- LiteSVM टेस्ट वातावरण को इनिशियलाइज़ करें
- टेस्ट अकाउंट्स (खरीदार और विक्रेता) को SOL एयरड्रॉप करें
- खरीदार और विक्रेता दोनों के लिए एक टोकन मिंट और एसोसिएटेड टोकन अकाउंट्स बनाएं (
@solana/spl-tokenलाइब्रेरी के साथ)। - विक्रेता को NFT मिंट करें
- वॉल्ट अथॉरिटी के लिए ऑक्शन अकाउंट और PDA बनाएं
- शुरुआती पैरामीटर्स (parameters) के साथ ऑक्शन को इनिशियलाइज़ करें
इस टेस्ट में हम जिस तरह से ट्रांज़ैक्शन बनाते हैं, वह अतीत में हमारे द्वारा किए गए तरीके से थोड़ा अलग है। इसका कारण (rationale) कोड ब्लॉक के बाद चर्चा किया जाएगा।
before(async () => {
// Initialize LiteSVM from the workspace and add SPL/Builtins/Sysvars
svm = fromWorkspace("./").withSplPrograms().withBuiltins().withSysvars();
provider = new LiteSVMProvider(svm);
anchor.setProvider(provider);
program = anchor.workspace.DutchAuction;
// Airdrop funds to seller and buyer
svm.airdrop(seller.publicKey, BigInt(10 * LAMPORTS_PER_SOL)); // Airdrop 10 SOL to seller
svm.airdrop(buyer.publicKey, BigInt(10 * LAMPORTS_PER_SOL)); // Airdrop 10 SOL to buyer
// Create NFT mint (0 decimals) with seller as mint authority
mintKp = Keypair.generate();
const LAMPORTS_FOR_MINT = 1_000_000_000; // sufficient for rent in tests
const createMintIx = SystemProgram.createAccount({
fromPubkey: seller.publicKey,
newAccountPubkey: mintKp.publicKey,
lamports: LAMPORTS_FOR_MINT,
space: MINT_SIZE,
programId: TOKEN_PROGRAM_ID,
});
const initMintIx = createInitializeMintInstruction(
mintKp.publicKey,
0, // decimals
seller.publicKey, // mint authority
null // freeze authority
);
const mintTx = new Transaction().add(createMintIx, initMintIx);
mintTx.recentBlockhash = svm.latestBlockhash();
mintTx.feePayer = seller.publicKey;
mintTx.sign(seller, mintKp);
svm.sendTransaction(mintTx);
// Create ATA for the seller
sellerAta = await getAssociatedTokenAddress(mintKp.publicKey, seller.publicKey);
const createSellerAtaIx = createAssociatedTokenAccountInstruction(
seller.publicKey,
sellerAta,
seller.publicKey,
mintKp.publicKey
);
const sellerAtaTx = new Transaction().add(createSellerAtaIx);
sellerAtaTx.recentBlockhash = svm.latestBlockhash();
sellerAtaTx.feePayer = seller.publicKey;
sellerAtaTx.sign(seller);
svm.sendTransaction(sellerAtaTx);
// Create ATA for the buyer
buyerAta = await getAssociatedTokenAddress(mintKp.publicKey, buyer.publicKey);
const createBuyerAtaIx = createAssociatedTokenAccountInstruction(
buyer.publicKey,
buyerAta,
buyer.publicKey,
mintKp.publicKey
);
const buyerAtaTx = new Transaction().add(createBuyerAtaIx);
buyerAtaTx.recentBlockhash = svm.latestBlockhash();
buyerAtaTx.feePayer = buyer.publicKey;
buyerAtaTx.sign(buyer);
svm.sendTransaction(buyerAtaTx);
// Mint 1 token to seller's ATA
const mintToIx = createMintToInstruction(
mintKp.publicKey,
sellerAta,
seller.publicKey,
BigInt(1)
);
const mintToTx = new Transaction().add(mintToIx);
mintToTx.recentBlockhash = svm.latestBlockhash();
mintToTx.feePayer = seller.publicKey;
mintToTx.sign(seller);
svm.sendTransaction(mintToTx);
// Find PDA for vault authority and associated token account
[vaultAuth] = PublicKey.findProgramAddressSync(
[Buffer.from("vault"), auctionAccount.publicKey.toBuffer()],
program.programId
);
vault = await getAssociatedTokenAddress(
mintKp.publicKey,
vaultAuth,
true
);
// Initialize the auction (moves 1 token from seller ATA to vault)
await program.methods
.initializeAuction(STARTING_PRICE, FLOOR_PRICE, DURATION)
.accounts({
auction: auctionAccount.publicKey,
seller: seller.publicKey,
sellerAta,
vaultAuth,
vault,
mint: mintKp.publicKey,
tokenProgram: TOKEN_PROGRAM_ID,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
})
.signers([seller, auctionAccount])
.rpc();
});
हम ऊपर दिए गए टेस्ट में कुछ LiteSVM फीचर्स और मेथड्स (methods) का उपयोग करते हैं, हम बाद में उन पर चर्चा करेंगे।
इसके बाद, नीचे दिया गया टेस्ट ब्लॉक जोड़ें। यह ब्लॉक बस यह सुनिश्चित (assert) करता है कि ऑक्शन प्रोग्राम before ब्लॉक में ठीक से इनिशियलाइज़ किया गया था।
it("initializes auction state correctly", async () => {
const auction = await program.account.auction.fetch(auctionAccount.publicKey);
assert.ok(auction.seller.equals(seller.publicKey));
assert.equal(auction.startingPrice.toNumber(), STARTING_PRICE.toNumber());
assert.equal(auction.floorPrice.toNumber(), FLOOR_PRICE.toNumber());
assert.equal(auction.tokenMint.toBase58(), mintKp.publicKey.toBase58());
// Seller's NFT should have moved to vault during initialization
const vaultAcc = svm.getAccount(vault);
assert.isNotNull(vaultAcc, "Vault ATA must exist");
});
टेस्ट रन करने से पहले, आइए LiteSVM लाइब्रेरीज़ और अब तक हमने उनका उपयोग कैसे किया है, इस पर एक नज़र डालते हैं।
टेस्ट कोड वॉकथ्रू (Test code walkthrough)
LiteSVM और LiteSVMProvider
जैसा कि नीचे दी गई छवि में दिखाया गया है, हमने अपने टेस्ट के लिए दो मुख्य वेरिएबल्स घोषित किए हैं:

svm: यहlitesvmलाइब्रेरी का एकLiteSVMहै। यह एक लोकल Solana टेस्ट वैलिडेटर की तरह कार्य करता है जिसे हम नियंत्रित कर सकते हैं, जिसमें टाइम वारपिंग के लिए इसकी क्लॉक भी शामिल है।provider: यहanchor-litesvmलाइब्रेरी का एकLiteSVMProviderहै। यह एक सामान्य Anchor प्रोवाइडर की तरह कार्य करता है लेकिन LiteSVM के साथ काम करता है, इसलिए हम टाइम वारपिंग के साथ टेस्ट चला सकते हैं।
LiteSVM को इनिशियलाइज़ करना

जैसा कि ऊपर दी गई छवि में दिखाया गया है, हम LiteSVM को इनिशियलाइज़ करके, LiteSVM प्रोवाइडर बनाकर, और इस प्रोवाइडर का उपयोग करने के लिए Anchor को कॉन्फ़िगर करके अपना टेस्टिंग वातावरण सेट करते हैं।
आइए समझते हैं कि प्रत्येक भाग क्या करता है:
-
litesvmलाइब्रेरी सेfromWorkspace("./")वर्तमान डायरेक्टरी से एक LiteSVM इंस्टेंस बनाता है, जो LiteSVM को बताता है कि हमारे प्रोजेक्ट की फ़ाइलें कहां खोजना है। फिर हम इस इंस्टेंस से कई मेथड्स को चेन (chain) करते हैं:.withSplPrograms()SPL टोकन प्रोग्राम्स जोड़ता है, जो हमारे टेस्ट्स में टोकन कार्यक्षमता को सक्षम करता है.withBuiltins()बिल्ट-इन (built-in) प्रोग्राम्स जोड़ता है, जिससे हमें नेटिव Solana प्रोग्राम्स तक पहुंच मिलती है.withSysvars()सिस्टम वेरिएबल्स जोड़ता है, यह हमें Solana सिस्टम की जानकारी जैसे क्लॉक तक पहुंच प्रदान करता है
यह फ़ंक्शन चेन एक पूरी तरह से कॉन्फ़िगर किया गया
LiteSVMऑब्जेक्ट लौटाता है, जिसे हमsvmको असाइन करते हैं। -
anchor-litesvmलाइब्रेरी सेnew LiteSVMProvider(svm)एक प्रोवाइडर बनाता है जो LiteSVM के साथ काम करता है लेकिन फिर भी Anchor के अपेक्षित इंटरफ़ेस का पालन करता है -
anchor.setProvider(provider)Anchor को हमारे LiteSVM-कम्पैटिबल (LiteSVM-अनुकूल) प्रोवाइडर का उपयोग करने के लिए कहता है
इस सेटअप के साथ, अब हम समय को आगे बढ़ाने (warp) के लिए LiteSVM प्रोवाइडर का उपयोग करने के लिए तैयार हैं।
अब जब हमने अपना टेस्ट वातावरण सेट कर लिया है, तो आइए देखें कि हम LiteSVM में SPL टोकन और एसोसिएटेड टोकन अकाउंट्स (ATAs) कैसे बनाते हैं। उस विशिष्ट दृष्टिकोण के विपरीत जहां आप SPL टोकन लाइब्रेरी से createMint() और createAssociatedTokenAccount() जैसे हेल्पर फ़ंक्शन्स का उपयोग करते हैं, LiteSVM के साथ हमें इन निर्देशों (instructions) को मैन्युअल रूप से बनाना होगा और उन्हें svm.sendTransaction() के साथ निष्पादित (execute) करना होगा।
LiteSVM में टोकन और ATAs बनाना
LiteSVM में, हम टोकन और ATAs मैन्युअल रूप से बनाते हैं क्योंकि @solana/spl-token हेल्पर फ़ंक्शन्स LiteSVM के टेस्टिंग वातावरण के साथ पूरी तरह से संगत (compatible) नहीं हैं।
आइए देखें कि हम ऑक्शन के लिए अपना NFT कैसे बनाते हैं।
चरण 1: मिंट अकाउंट (Mint Account) बनाएं
सबसे पहले, हम मिंट अकाउंट बनाते हैं।

ऊपर दी गई छवि में क्या हो रहा है, वह यह है:
- अकाउंट बनाना (Create the account): हम ऑन-चेन स्पेस आवंटित करने और टोकन प्रोग्राम को स्वामित्व सौंपने के लिए
SystemProgram.createAccount()(@solana/web3.jsसे इम्पोर्ट किया गया) का उपयोग करते हैं। यह SPL टोकन स्टैण्डर्ड का अनुपालन करने के लिए है जहां मान्य होने के लिए सभी टोकन मिंट अकाउंट्स टोकन प्रोग्राम के स्वामित्व में होने चाहिए। - मिंट के रूप में इनिशियलाइज़ करना (Initialize as mint): हम उस रॉ (raw) अकाउंट को 0 डेसिमल्स (decimals) के साथ उचित टोकन मिंट में बदलने के लिए
createInitializeMintInstruction()(@solana/spl-tokenसे इम्पोर्ट किया गया) का उपयोग करते हैं। हम ऐसा इसलिए करते हैं क्योंकि Solana के NFT स्टैण्डर्ड को नॉन-डिविज़िबिलिटी (गैर-विभाज्यता) सुनिश्चित करने के लिए 0 डेसिमल्स की आवश्यकता होती है। - निष्पादित करना (Execute): हम ट्रांज़ैक्शन बनाते हैं, ब्लॉकहैश (blockhash) और फी पेयर (fee payer) सेट करते हैं, उस पर साइन करते हैं, फिर इसे
svm.sendTransaction()का उपयोग करके अपने LiteSVM इंस्टेंस पर भेजते हैं। यह Solana ब्लॉकचेन पर ट्रांज़ैक्शन भेजने के समान है, लेकिन लोकल LiteSVM वातावरण में।
चरण 2: टोकन अकाउंट्स (Token Accounts) बनाएं
इसके बाद, हम अपने विक्रेता और खरीदार के लिए एसोसिएटेड टोकन अकाउंट्स (ATAs) बनाते हैं। हम अकाउंट पतों (addresses) को प्राप्त करने के लिए getAssociatedTokenAddress() (@solana/spl-token से इम्पोर्ट किया गया) का उपयोग करते हैं और निर्देश (instruction) बनाने के लिए createAssociatedTokenAccountInstruction() (@solana/spl-token से इम्पोर्ट किया गया) का उपयोग करते हैं जो इन अकाउंट्स को इनिशियलाइज़ करेगा।

ये ATAs ऑक्शन बिक्री से पहले और बाद में विक्रेता और खरीदार के लिए NFT रखेंगे।
क्योंकि LiteSVM में टोकन ऑपरेशन्स के लिए हेल्पर फ़ंक्शन्स नहीं हैं, हम मैन्युअल रूप से ट्रांज़ैक्शन बनाने, साइन करने और उसे svm.sendTransaction() के साथ भेजने के पैटर्न का पालन करते हैं।
ऑक्शन के लीनियर प्राइस डिके (Linear Price Decay) को टेस्ट करने के लिए समय को सिमुलेट (Simulate) करना
अब जब हमने अपना टेस्ट वातावरण सेट कर लिया है और आवश्यक टोकन अकाउंट्स बना लिए हैं, तो हम समय बीतने को सिमुलेट करने और ऑक्शन की कीमत समय के साथ ठीक से गिरती है या नहीं, यह जांचने के अपने लक्ष्य की ओर बढ़ सकते हैं।
हम क्लॉक को आगे (warp) बढ़ाएंगे और एक विशिष्ट बिंदु पर अपने प्रोग्राम से buy फ़ंक्शन को कॉल करके यह पुष्टि करेंगे कि कीमत अपेक्षित मूल्य गिरावट को दर्शाती है।
अब निम्नलिखित टेस्ट ब्लॉक जोड़ें। इस टेस्ट में, हम यह पुष्टि करना चाहते हैं कि समय बीतने के साथ ऑक्शन की कीमत सही ढंग से कम होती है। हम यह करते हैं:
- ऑन-चेन ऑक्शन डेटा प्राप्त करें और प्रमुख पैरामीटर्स (शुरुआती समय, अवधि, आदि) निकालें।
- गणना करें कि ऑक्शन के 15 मिनट बाद कीमत क्या होनी चाहिए (जो कुल अवधि - 60 मिनट का 25% है)।
- LiteSVM का उपयोग करके Solana क्लॉक को 15 मिनट आगे (warp) बढ़ाएं।
- खरीदारी करने से पहले खरीदार का SOL बैलेंस जांचें।
- वर्तमान कीमत पर खरीदारी का अनुकरण (simulate) करने के लिए
buyफ़ंक्शन को कॉल करें। - खरीदार का नया बैलेंस जांचें और यह देखने के लिए कि कितना भुगतान किया गया था, इसे पिछले वाले से घटाएं।
- पुष्टि करें कि भुगतान की गई कीमत अपेक्षित कीमत (1.625 SOL) से मेल खाती है।
it("executes buy at 25% time with expected price and transfers NFT", async () => {
const auction = await program.account.auction.fetch(auctionAccount.publicKey);
const startTime = auction.startTime.toNumber();
const duration = auction.duration.toNumber();
const quarterTime = startTime + Math.floor(duration / 4);
// Warp clock to 25% into the auction
const c = svm.getClock();
svm.setClock(
new Clock(c.slot, c.epochStartTimestamp, c.epoch, c.leaderScheduleEpoch, BigInt(quarterTime))
);
// Check buyer's lamports before purchase
const balanceBefore = svm.getBalance(buyer.publicKey)!;
// Execute the buy transaction
console.log('Executing buy transaction...');
await program.methods
.buy()
.accounts({
auction: auctionAccount.publicKey,
seller: seller.publicKey,
buyer: buyer.publicKey,
buyerAta,
vaultAuth,
vault,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
})
.signers([buyer])
.rpc();
// Check buyer's lamports after purchase
const balanceAfter = svm.getBalance(buyer.publicKey)!;
// Calculate the price paid and log it
const pricePaid = Number(balanceBefore - balanceAfter);
console.log(`Actual price paid: ${lamportsToSol(pricePaid)}`);
// Expected price at 25% through the auction duration:
// Starting price - ((Starting price - Floor price) * 0.25) =
// 2 SOL - ((2 SOL - 0.5 SOL) * 0.25) = 1.625 SOL = 1,625,000,000 lamports
const expectedPriceAt25Percent = 1_625_000_000;
// Assert that the price paid is equal to the expected price
assert.equal(
pricePaid,
expectedPriceAt25Percent,
"Buyer should pay the 25% elapsed linear price"
);
// Verify buyer received the NFT (amount stored at bytes 64..72)
const buyerAtaAcc = svm.getAccount(buyerAta)!;
// Read the token amount as u64 (little-endian) from offset 64
const amount = Number(Buffer.from(buyerAtaAcc.data).readBigUInt64LE(64));
assert.equal(amount, 1, "Buyer ATA should now contain 1 token");
});
पहले, हमने 2 SOL की शुरुआती कीमत और 60 मिनट की अवधि के साथ अपने टेस्ट में ऑक्शन को इनिशियलाइज़ किया था।

15 मिनट के बाद (जो ऑक्शन के समय का 25% है), हमें उम्मीद है कि हमारे प्रोग्राम के buy फ़ंक्शन में उपयोग किए गए फॉर्मूले के आधार पर कीमत में 25% की गिरावट आएगी। यह हमें 1.625 SOL देता है, जो हमारे टेस्ट में अपेक्षित कीमत है।

समय को 15 मिनट आगे (warp) बढ़ाना
ऊपर दिए गए टेस्ट ब्लॉक से, हम Clock sysvar को अधिलेखित (overwrite) करने के लिए svm.setClock का उपयोग करते हैं और ऑक्शन के दौरान बाद के समय के बिंदु का अनुकरण (simulate) करते हैं।

यह मेथड एक Clock ऑब्जेक्ट लेता है (litesvm से इम्पोर्ट किया गया) जहां हम unixTimestamp को ऑक्शन शुरू होने के समय (quarterTime द्वारा परिभाषित) के 15 मिनट बाद सेट करते हैं। यह हमें वास्तविक समय में प्रतीक्षा किए बिना प्राइस डिके (मूल्य में गिरावट) का टेस्ट करने की अनुमति देता है।
यह सब LiteSVM इनिशियलाइज़ेशन और अकाउंट क्रिएशन द्वारा संभव हुआ है जो हमने before ब्लॉक में किया था।
अब जब हमारे सभी टेस्ट मौजूद हैं, तो हम उन्हें anchor test के साथ चला सकते हैं जैसे हम एक सामान्य Anchor प्रोग्राम के लिए करते हैं, और टेस्ट पास हो जाता है।

सारांश (Summary)
इस ट्यूटोरियल में, हमने एक Dutch auction लागू किया जहां टोकन की कीमत समय के साथ लीनियर रूप से (linearly) घटती है, और हमने लॉजिक को सत्यापित करने के लिए एक टेस्ट लिखा।
हमने एक लोकल टेस्ट वातावरण बनाने के लिए litesvm (और anchor-litesvm) का उपयोग किया जहां हम समय को आगे (warp) बढ़ा सकते थे। इसने हमें प्रतीक्षा किए बिना 15 मिनट बीतने का अनुकरण (simulate) करने की अनुमति दी, और यह पुष्टि की कि ऑक्शन की कीमत सही ढंग से 2 SOL से गिरकर 1.625 SOL (ऑक्शन में 25% समय पर) हो गई।
Clock sysvar को अधिलेखित (overwrite) करके और LiteSVM के APIs का उपयोग करके, हम टाइम-बेस्ड लॉजिक (time-based logic) को डिटर्मिनिस्टिक (deterministic) रूप से टेस्ट करने में सक्षम थे।
यह लेख Solana पर ट्यूटोरियल सीरीज़ का हिस्सा है।