En Solana, escribir casos de prueba que dependan del paso del tiempo es complicado. Podríamos querer probar que algo sucede en nuestro código después de que pase un día, pero no podemos permitir que nuestro caso de prueba tarde un día en ejecutarse, ya que esto haría que nuestras pruebas no fueran prácticas. LiteSVM soluciona esto permitiéndote avanzar instantáneamente el reloj de la blockchain, como un viaje en el tiempo para tus pruebas locales.
Para mostrar cómo funciona esto en la práctica, construiremos una subasta holandesa básica para un NFT utilizando Anchor. Una subasta holandesa comienza con un precio alto que disminuye automáticamente con el tiempo hasta que un comprador acepta el precio actual. Es un claro ejemplo de comportamiento sensible al tiempo donde LiteSVM simplifica mucho las pruebas.
LiteSVM funciona como el validador local de Solana (solana-test-validator) pero nos da más control sobre el estado de la blockchain local para el entorno de pruebas. Se puede usar en nuestras pruebas de TypeScript y facilita la prueba de lógica basada en el tiempo, como subastas o vesting.
Si estás familiarizado con el desarrollo en Ethereum, las capacidades de manipulación del tiempo de LiteSVM son similares a vm.warp de Foundry (para avanzar las marcas de tiempo de los bloques), pero adaptadas para la arquitectura basada en slots de Solana.
Aquí tienes un resumen de lo que cubriremos en este artículo:
- Crearemos un programa de subasta holandesa para ventas de NFT con funciones para crear subastas y comprar NFTs a precios decrecientes
- Explicaremos estas funciones y escribiremos pruebas para ellas
- Finalmente, usaremos LiteSVM para avanzar el tiempo 15 minutos para probar la disminución del precio sin tener que esperar, y verificaremos que el precio de la subasta caiga correctamente
Ahora vamos a crear el programa de subasta holandesa.
Crear el programa de subasta holandesa
Como se mencionó anteriormente, una subasta holandesa comienza alta y cae con el tiempo hasta que un comprador acepta. Para garantizar la entrega, bloqueamos el NFT en una bóveda (escrow) controlada por el programa. Esto evita que el vendedor retire o venda dos veces el NFT, y permite a los compradores evitar depender del vendedor para liberar el NFT después del pago. La bóveda permite al programa liquidar el intercambio de forma atómica una vez que un comprador acepta.
El programa de subasta holandesa constará de solo dos funciones:
- Una función
initialize_auctionque crea las cuentas requeridas y deposita el NFT del vendedor en una cuenta de bóveda propiedad de nuestro programa. - Una función
buyque permite a los compradores adquirir el NFT con SOL al precio de subasta actual.
A continuación, crearemos las siguientes cuentas:
Auction: Una PDA propiedad de nuestro programa para almacenar los detalles de la subasta, como el precio inicial, la duración de la subasta, etc.Vault Authority: Esta será una PDA propiedad de nuestro programa para autorizar la transferencia del NFT al comprador una vez que ocurra la venta. Entraremos en más detalles sobre esto en la sección “¿Por qué tenemos una PDA Vault Authority?” de este artículo.Vault: Una Associated Token Account para retener el NFT depositado. Esta es propiedad de la PDAVault Authority.
Ahora crea un nuevo proyecto Anchor llamado dutch-auction y modifica el archivo programs/dutch-auction/Cargo.toml con las siguientes dependencias:
anchor-splpara la funcionalidad del token SPLanchor-spl/idl-buildbajofeaturespara incluir tipos SPL en el archivo IDL generado
[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"
Puedes ejecutar anchor build para confirmar que no hay problemas con las dependencias.
Inicializar el programa de subasta
Ahora que tenemos las dependencias listas, reemplaza el código del programa en programs/dutch-auction/src/lib.rs con el código a continuación, que contiene la función initialize_auction que hace lo siguiente:
- Inicializa la cuenta de la subasta y registra los detalles de la misma y su duración (en segundos) en la cuenta.
- Transfiere el NFT subastado desde la Associated Token Account (ATA) del vendedor hacia nuestra bóveda controlada por el programa (también una 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(())
}
}
Ejecuta anchor keys sync para reemplazar el ID del programa con el tuyo.
Luego, añade el struct de cuentas InitializeAuction, con el estado Auction al código del programa.
InitializeAuction especifica las siguientes cuentas involucradas durante la inicialización de la subasta:
auction: La cuenta para almacenar el estado de la subasta (precio inicial, duración, información del vendedor, etc.).seller: El propietario del NFT que crea la subasta y firma la transacción.seller_ata: La Associated Token Account del vendedor que contiene el NFT a subastar.vault_auth: Una PDA (Program Derived Address) que sirve como autoridad para la cuenta de la bóveda. Esto permite que nuestro programa controle las transferencias de NFT.vault(escrow): Una Associated Token Account que retiene el NFT depositado por el vendedor durante la subasta. Esta es propiedad de la PDAVault Authority.mint: La cuenta mint del NFT que representa el token que se está subastando.
#[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,
}
Aún no probaremos esta función, lo haremos más tarde una vez que el programa completo esté en su lugar.
Ahora vamos a añadir la función para comprar el token subastado.
Comprar el token subastado
En nuestra configuración de la subasta, el NFT se encuentra en una cuenta de escrow (bóveda) controlada por el programa hasta que se vende. Comprar el token subastado significa transferir lamports del comprador al vendedor a cambio del NFT.
Ahora añadiremos una función que nos permita comprar el NFT al precio actual de la subasta holandesa. Esto es lo que hace la función:
- Comprueba si el NFT ya se ha vendido.
- Obtiene el tiempo actual y comprueba que la subasta esté activa; se revierte con un error
AuctionNotStartedsi la subasta aún no ha comenzado, o con un errorAuctionEndedsi la duración de la subasta ha transcurrido. - Calcula el tiempo transcurrido hasta el momento y deriva el precio actual usando la fórmula para una subasta holandesa lineal.
- Asegura que el comprador tenga suficientes lamports.
- Transfiere lamports del comprador al vendedor.
- Configura los seeds del firmante (signer seeds) para que el programa pueda firmar en nombre de la bóveda.
- Transfiere el NFT desde la bóveda a la Associated Token Account del comprador.
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(())
}
¿Por qué tenemos una PDA Vault Authority?
Usamos una ATA como bóveda para retener el NFT después de que el vendedor lo deposita. Necesitamos una PDA Vault Authority para que nuestro programa pueda firmar transferencias para esa ATA sin la necesidad de un keypair o un firmante externo.
Recuerda que en el artículo Token Sale with Total Supply, mostramos cómo hacer de una PDA mint su propia autoridad para que el programa pueda acuñar nuevos tokens por sí mismo. Aquí utilizamos el mismo concepto para la ATA de la bóveda, pero para conceder a nuestro programa el poder de mover un token existente. Derivamos vault_auth de ["vault", auction.key().as_ref()] y la establecemos como la autoridad de la ATA.
En la función buy(), llamamos a CpiContext::new_with_signer con esos seeds. El tiempo de ejecución de Solana ve que nuestro programa controla vault_auth y le permite firmar para la ATA de la bóveda. Esto permite que nuestro programa transfiera el NFT al comprador automáticamente, sin ningún firmante externo.

Ahora añade el struct de cuentas Buy. El struct Buy especifica las cuentas involucradas durante las compras de NFT en nuestro programa de subasta:
auction: La cuenta de la subasta que contiene los detalles y el estado de la subasta.seller: El vendedor original del NFT que recibirá el pago en SOL.buyer: La cuenta que compra el NFT al precio actual de la subasta, también es el firmante de la transacción.buyer_ata: La Associated Token Account del comprador que recibirá el NFT adquirido.vault_auth: La autoridad de PDA que controla la bóveda y autoriza la transferencia del NFT al comprador.vault: La cuenta de bóveda que contiene el NFT en escrow, propiedad de la PDAvault_auth.- Las dos últimas cuentas, Token program (para transferencias de NFT) y System program (para transferencias de SOL), son los programas nativos con los que interactuamos.
#[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
}
Añade el error AuctionError, utilizamos este en la función 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,
}
Nuestro programa ahora está completo, así que vamos a escribir algunas pruebas para él.
Probar el programa de subasta holandesa con LiteSVM
En una subasta holandesa, el precio del artículo disminuye con el tiempo. El objetivo de esta prueba es confirmar que el precio de la subasta disminuye correctamente con el tiempo. Para hacer eso, usaremos LiteSVM para avanzar el tiempo y registrar la caída del precio en ese punto.
En una subasta holandesa real on-chain, tendríamos que esperar en tiempo real para ver caer el precio. Con LiteSVM, podemos saltarnos la espera avanzando el tiempo.
Añadiremos la prueba por partes, así que primero, reemplaza la prueba del programa en tests/dutch-auction.ts con el código a continuación.
Hemos definido algunas constantes de prueba. También fíjate en las dos bibliotecas clave para esta prueba:
litesvmnos permite iniciar un validador de prueba local de Solana directamente desde nuestro cliente TypeScript y nos da el control sobre el reloj del validador para manipular el tiempo.- La biblioteca
anchor-litesvmconecta nuestro proyecto Anchor con LiteSVM, lo que hace posible probar programas de Anchor en esta configuración de LiteSVM.
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
Instala las dependencias de prueba con: npm install anchor-litesvm litesvm @solana/spl-token
Añade el bloque describe. Aquí simplemente declaramos cuentas y variables de prueba (las discutiremos más adelante).
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;
});
Ahora, añade este bloque de prueba before dentro del bloque describe.
Aquí configuramos LiteSVM y las cuentas, haciendo lo siguiente:
- Inicializar el entorno de prueba LiteSVM
- Enviar SOL mediante airdrop a las cuentas de prueba (comprador y vendedor)
- Crear un token mint y Associated Token Accounts tanto para el comprador como para el vendedor (con la biblioteca
@solana/spl-token). - Acuñar un NFT para el vendedor
- Crear la cuenta de subasta y la PDA para la autoridad de la bóveda
- Inicializar la subasta con los parámetros iniciales
La forma en que construimos las transacciones en esta prueba es ligeramente diferente a cómo lo hacíamos en el pasado. La razón de esto se discutirá después del bloque de código.
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();
});
Usamos algunas características y métodos de LiteSVM en la prueba anterior, los discutiremos más adelante.
A continuación, añade el bloque de prueba a continuación. Este bloque simplemente afirma que el programa de la subasta se inicializó correctamente en el bloque 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");
});
Antes de ejecutar la prueba, repasemos las bibliotecas de LiteSVM y cómo las hemos utilizado hasta ahora.
Explicación del código de prueba
LiteSVM y LiteSVMProvider
Como se muestra en la imagen a continuación, declaramos dos variables principales para nuestra prueba:

svm: Este es unLiteSVMde la bibliotecalitesvm. Actúa como un validador de prueba local de Solana que podemos controlar, incluyendo su reloj para la manipulación del tiempo.provider: Este es unLiteSVMProviderde la bibliotecaanchor-litesvm. Actúa como un proveedor normal de Anchor pero funciona con LiteSVM, por lo que podemos ejecutar pruebas avanzando el tiempo.
Inicializar LiteSVM

Como se muestra en la imagen de arriba, configuramos nuestro entorno de pruebas inicializando LiteSVM, creando un proveedor LiteSVM y configurando Anchor para utilizar este proveedor.
Desglosemos lo que hace cada parte:
-
fromWorkspace("./")de la bibliotecalitesvmcrea una instancia de LiteSVM a partir del directorio actual, lo que le indica a LiteSVM dónde encontrar nuestros archivos del proyecto. Luego encadenamos varios métodos a esta instancia:.withSplPrograms()añade programas de tokens SPL, habilitando la funcionalidad de tokens en nuestras pruebas.withBuiltins()añade programas integrados, dándonos acceso a programas nativos de Solana.withSysvars()añade variables del sistema, esto nos da acceso a la información del sistema de Solana, como el reloj
Esta cadena de funciones devuelve un objeto
LiteSVMcompletamente configurado, que asignamos asvm. -
new LiteSVMProvider(svm)de la bibliotecaanchor-litesvmcrea un proveedor que funciona con LiteSVM pero que sigue la interfaz esperada por Anchor -
anchor.setProvider(provider)le dice a Anchor que use nuestro proveedor compatible con LiteSVM
Con esta configuración, ya estamos listos para utilizar el proveedor de LiteSVM para alterar el tiempo.
Ahora que hemos configurado nuestro entorno de prueba, vamos a repasar cómo creamos tokens SPL y Associated Token Accounts (ATAs) en LiteSVM. A diferencia del enfoque típico donde usarías funciones de ayuda como createMint() y createAssociatedTokenAccount() de la biblioteca SPL Token, con LiteSVM necesitamos construir estas instrucciones manualmente y ejecutarlas con svm.sendTransaction().
Crear Tokens y ATAs en LiteSVM
En LiteSVM, creamos tokens y ATAs manualmente porque las funciones de ayuda de @solana/spl-token no son completamente compatibles con el entorno de pruebas de LiteSVM.
Veamos cómo creamos nuestro NFT para la subasta.
Paso 1: Crear la cuenta Mint
Primero, creamos la cuenta mint.

Esto es lo que está ocurriendo en la imagen de arriba:
- Crear la cuenta: Usamos
SystemProgram.createAccount()(importado de@solana/web3.js) para asignar espacio on-chain y asignar la propiedad al Token Program. Esto es para cumplir con el estándar SPL Token, donde todas las cuentas de token mint deben ser propiedad del Token Program para ser válidas. - Inicializar como mint: Usamos
createInitializeMintInstruction()(importado de@solana/spl-token) para convertir esa cuenta cruda en un token mint adecuado con 0 decimales. Hacemos esto porque el estándar de NFT de Solana requiere 0 decimales para garantizar la indivisibilidad. - Ejecutar: Construimos la transacción, establecemos el blockhash y el pagador de la tarifa, la firmamos, y luego la enviamos a nuestra instancia de LiteSVM usando
svm.sendTransaction(). Esto es similar a enviar una transacción a la blockchain de Solana, pero en el entorno local de LiteSVM.
Paso 2: Crear cuentas de Tokens
A continuación, creamos las associated token accounts (ATAs) para nuestro vendedor y comprador. Usamos getAssociatedTokenAddress() (importado de @solana/spl-token) para derivar las direcciones de las cuentas y createAssociatedTokenAccountInstruction() (importado de @solana/spl-token) para crear la instrucción que inicializará estas cuentas.

Estas ATAs contendrán los NFTs para el vendedor y el comprador antes y después de la venta de la subasta.
Debido a que LiteSVM no tiene funciones de ayuda para operaciones de tokens, seguimos el patrón de construir la transacción manualmente, firmarla y enviarla con svm.sendTransaction().
Simular el tiempo para probar la caída lineal del precio de la subasta
Ahora que hemos configurado nuestro entorno de prueba y creado las cuentas de tokens necesarias, podemos pasar a nuestro objetivo de simular el paso del tiempo y comprobar que el precio de la subasta disminuye correctamente.
Avanzaremos el reloj y llamaremos a la función buy de nuestro programa en un punto específico para confirmar que el precio refleja la disminución esperada.
Ahora añade el siguiente bloque de prueba. En esta prueba, queremos confirmar que el precio de la subasta disminuye correctamente a medida que pasa el tiempo. Esto es lo que hacemos:
- Obtenemos los datos de la subasta on-chain y extraemos los parámetros clave (hora de inicio, duración, etc).
- Calculamos cuál debería ser el precio a los 15 minutos de iniciada la subasta (que es el 25% de la duración total de 60 minutos).
- Avanzamos el reloj de Solana 15 minutos usando LiteSVM.
- Comprobamos el saldo en SOL del comprador antes de realizar la compra.
- Llamamos a la función
buypara simular una compra al precio actual. - Comprobamos el nuevo saldo del comprador y lo restamos del anterior para ver cuánto se pagó.
- Confirmamos que el precio pagado coincide con el precio esperado (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");
});
Anteriormente, inicializamos la subasta en nuestra prueba con un precio inicial de 2 SOL y una duración de 60 minutos.

Después de 15 minutos (que es el 25% del tiempo de la subasta), esperamos que el precio caiga un 25%, basado en la fórmula utilizada en la función buy de nuestro programa. Eso nos da 1.625 SOL, que es el precio esperado en nuestra prueba.

Avanzar el tiempo 15 minutos
A partir del bloque de prueba anterior, utilizamos svm.setClock para sobrescribir la sysvar Clock y simular un punto posterior en el tiempo durante la subasta.

Este método toma un objeto Clock (importado de litesvm) en el que establecemos el unixTimestamp a 15 minutos después de la hora de inicio de la subasta (definido por quarterTime). Esto nos permite probar la disminución del precio sin tener que esperar en tiempo real.
Todo esto es posible gracias a la inicialización de LiteSVM y a la creación de las cuentas que llevamos a cabo en el bloque before.
Ahora que todas nuestras pruebas están en su lugar, podemos ejecutarlas con anchor test al igual que lo hacemos con un programa normal de Anchor, y la prueba pasa exitosamente.

Hemos viajado en el tiempo con éxito para simular la caída del precio de la subasta.
Resumen
En este tutorial, implementamos una subasta holandesa donde el precio del token disminuye de manera lineal a lo largo del tiempo, y escribimos una prueba para verificar la lógica.
Utilizamos litesvm (y anchor-litesvm) para crear un entorno de pruebas local donde pudimos alterar el tiempo. Esto nos permitió simular el paso de 15 minutos sin esperar, y confirmar que el precio de la subasta había disminuido correctamente de 2 SOL a 1.625 SOL (25% del tiempo de la subasta).
Al sobrescribir la sysvar Clock y utilizando las APIs de LiteSVM, pudimos probar lógica basada en el tiempo de manera determinista.
Este artículo es parte de una serie de tutoriales sobre Solana.