Estructura de programa en Rust

Los programas de Solana escritos en Rust tienen requisitos estructurales mínimos, lo que permite flexibilidad en la organización del código. El único requisito es que un programa debe tener un entrypoint, que define dónde comienza la ejecución de un programa.

Estructura del programa

Aunque no hay reglas estrictas para la estructura de archivos, los programas de Solana típicamente siguen un patrón común:

  • entrypoint.rs: Define el punto de entrada que dirige las instrucciones entrantes.
  • state.rs: Define el estado del programa (datos de la cuenta).
  • instructions.rs: Define las instrucciones que el programa puede ejecutar.
  • processor.rs: Define los manejadores de instrucciones (funciones) que implementan la lógica de negocio para cada instrucción.
  • error.rs: Define errores personalizados que el programa puede devolver.

Por ejemplo, consulta el Programa Token.

Programa de ejemplo

Para demostrar cómo construir un programa nativo en Rust con múltiples instrucciones, veremos un programa contador simple que implementa dos instrucciones:

  1. InitializeCounter: Crea e inicializa una nueva cuenta con un valor inicial.
  2. IncrementCounter: Incrementa el valor almacenado en una cuenta existente.

Para simplificar, el programa se implementará en un solo archivo lib.rs, aunque en la práctica es posible que quieras dividir programas más grandes en múltiples archivos.

Parte 1: Escribiendo el programa

Comencemos construyendo el programa contador. Crearemos un programa que pueda inicializar un contador con un valor inicial e incrementarlo.

Crear un nuevo programa

Primero, vamos a crear un nuevo proyecto de Rust para nuestro programa de Solana.

Terminal
$
cargo new counter_program --lib
$
cd counter_program

Deberías ver los archivos predeterminados src/lib.rs y Cargo.toml.

Actualiza el campo edition en Cargo.toml a 2021. De lo contrario, podrías encontrar un error al compilar el programa.

Añadir dependencias

Ahora vamos a añadir las dependencias necesarias para construir un programa de Solana. Necesitamos solana-program para el SDK principal y borsh para la serialización.

Terminal
$
cargo add solana-program@2.2.0
$
cargo add borsh

No es obligatorio usar Borsh. Sin embargo, es una biblioteca de serialización comúnmente utilizada para programas de Solana.

Configurar crate-type

Los programas de Solana deben compilarse como bibliotecas dinámicas. Añade la sección [lib] para configurar cómo Cargo compila el programa.

Cargo.toml
[lib]
crate-type = ["cdylib", "lib"]

Si no incluyes esta configuración, el directorio target/deploy no se generará cuando compiles el programa.

Configurar el punto de entrada del programa

Cada programa de Solana tiene un punto de entrada, que es la función que se llama cuando se invoca el programa. Comencemos añadiendo las importaciones que necesitaremos para el programa y configurando el punto de entrada.

Añade el siguiente código a lib.rs:

lib.rs
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
sysvar::{rent::Rent, Sysvar},
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
Ok(())
}

El macro entrypoint maneja la deserialización de los datos input en los parámetros de la función process_instruction.

Un entrypoint de programa de Solana tiene la siguiente firma de función. Los desarrolladores tienen libertad para crear su propia implementación de la función entrypoint.

#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;

Definir el estado del programa

Ahora vamos a definir la estructura de datos que se almacenará en nuestras cuentas de contador. Estos son los datos que se almacenarán en el campo data de la cuenta.

Añade el siguiente código a lib.rs:

lib.rs
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
pub count: u64,
}

Definir enum de instrucciones

Vamos a definir las instrucciones que nuestro programa puede ejecutar. Usaremos un enum donde cada variante representa una instrucción diferente.

Añade el siguiente código a lib.rs:

lib.rs
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum CounterInstruction {
InitializeCounter { initial_value: u64 },
IncrementCounter,
}

Implementar deserialización de instrucciones

Ahora necesitamos deserializar el instruction_data (bytes en bruto) en una de nuestras variantes del enum CounterInstruction. El método try_from_slice de Borsh maneja esta conversión automáticamente.

Actualiza la función process_instruction para usar la deserialización de Borsh:

lib.rs
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let instruction = CounterInstruction::try_from_slice(instruction_data)
.map_err(|_| ProgramError::InvalidInstructionData)?;
Ok(())
}

Dirigir instrucciones a los manejadores

Ahora vamos a actualizar la función principal process_instruction para dirigir las instrucciones a sus funciones manejadoras apropiadas.

Este patrón de enrutamiento es común en los programas de Solana. El instruction_data se deserializa en una variante de un enum que representa la instrucción, luego se llama a la función manejadora apropiada. Cada función manejadora incluye la implementación para esa instrucción.

Añade el siguiente código a lib.rs actualizando la función process_instruction y añadiendo los manejadores para las instrucciones InitializeCounter y IncrementCounter:

lib.rs
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let instruction = CounterInstruction::try_from_slice(instruction_data)
.map_err(|_| ProgramError::InvalidInstructionData)?;
match instruction {
CounterInstruction::InitializeCounter { initial_value } => {
process_initialize_counter(program_id, accounts, initial_value)?
}
CounterInstruction::IncrementCounter => {
process_increment_counter(program_id, accounts)?
}
};
Ok(())
}
fn process_initialize_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
initial_value: u64,
) -> ProgramResult {
Ok(())
}
fn process_increment_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
Ok(())
}

Implementar manejador de inicialización

Vamos a implementar el manejador para crear e inicializar una nueva cuenta de contador. Como solo el System Program puede crear cuentas en Solana, usaremos una Cross Program Invocation (CPI), esencialmente llamando a otro programa desde nuestro programa.

Nuestro programa hace una CPI para llamar a la instrucción create_account del System Program. La nueva cuenta se crea con nuestro programa como propietario, dando a nuestro programa la capacidad de escribir en la cuenta e inicializar los datos.

Añade el siguiente código a lib.rs actualizando la función process_initialize_counter:

lib.rs
fn process_initialize_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
initial_value: u64,
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let counter_account = next_account_info(accounts_iter)?;
let payer_account = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;
let account_space = 8;
let rent = Rent::get()?;
let required_lamports = rent.minimum_balance(account_space);
invoke(
&system_instruction::create_account(
payer_account.key,
counter_account.key,
required_lamports,
account_space as u64,
program_id,
),
&[
payer_account.clone(),
counter_account.clone(),
system_program.clone(),
],
)?;
let counter_data = CounterAccount {
count: initial_value,
};
let mut account_data = &mut counter_account.data.borrow_mut()[..];
counter_data.serialize(&mut account_data)?;
msg!("Counter initialized with value: {}", initial_value);
Ok(())
}

Esta instrucción es solo para fines de demostración. No incluye comprobaciones de seguridad y validación que son necesarias para programas en producción.

Implementar el manejador de incremento

Ahora vamos a implementar el manejador que incrementa un contador existente. Esta instrucción:

  • Lee el campo data de la cuenta para el counter_account
  • Lo deserializa en una estructura CounterAccount
  • Incrementa el campo count en 1
  • Serializa la estructura CounterAccount de vuelta al campo data de la cuenta

Añade el siguiente código a lib.rs actualizando la función process_increment_counter:

lib.rs
fn process_increment_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let counter_account = next_account_info(accounts_iter)?;
if counter_account.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
let mut data = counter_account.data.borrow_mut();
let mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;
counter_data.count = counter_data
.count
.checked_add(1)
.ok_or(ProgramError::InvalidAccountData)?;
counter_data.serialize(&mut &mut data[..])?;
msg!("Counter incremented to: {}", counter_data.count);
Ok(())
}

Esta instrucción es solo para fines de demostración. No incluye comprobaciones de seguridad y validación que son necesarias para programas en producción.

Programa completado

¡Felicidades! Has construido un programa completo de Solana que demuestra la estructura básica compartida por todos los programas de Solana:

  • Punto de entrada: Define dónde comienza la ejecución del programa y dirige todas las solicitudes entrantes a los manejadores de instrucciones apropiados
  • Manejo de instrucciones: Define las instrucciones y sus funciones manejadoras asociadas
  • Gestión de estado: Define las estructuras de datos de las cuentas y gestiona su estado en cuentas propiedad del programa
  • Cross Program Invocation (CPI): Llama al System Program para crear nuevas cuentas propiedad del programa

El siguiente paso es probar el programa para asegurarse de que todo funciona correctamente.

Crear un nuevo programa

Primero, vamos a crear un nuevo proyecto de Rust para nuestro programa de Solana.

Terminal
$
cargo new counter_program --lib
$
cd counter_program

Deberías ver los archivos predeterminados src/lib.rs y Cargo.toml.

Actualiza el campo edition en Cargo.toml a 2021. De lo contrario, podrías encontrar un error al compilar el programa.

Añadir dependencias

Ahora vamos a añadir las dependencias necesarias para construir un programa de Solana. Necesitamos solana-program para el SDK principal y borsh para la serialización.

Terminal
$
cargo add solana-program@2.2.0
$
cargo add borsh

No es obligatorio usar Borsh. Sin embargo, es una biblioteca de serialización comúnmente utilizada para programas de Solana.

Configurar crate-type

Los programas de Solana deben compilarse como bibliotecas dinámicas. Añade la sección [lib] para configurar cómo Cargo compila el programa.

Cargo.toml
[lib]
crate-type = ["cdylib", "lib"]

Si no incluyes esta configuración, el directorio target/deploy no se generará cuando compiles el programa.

Configurar el punto de entrada del programa

Cada programa de Solana tiene un punto de entrada, que es la función que se llama cuando se invoca el programa. Comencemos añadiendo las importaciones que necesitaremos para el programa y configurando el punto de entrada.

Añade el siguiente código a lib.rs:

lib.rs
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
sysvar::{rent::Rent, Sysvar},
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
Ok(())
}

El macro entrypoint maneja la deserialización de los datos input en los parámetros de la función process_instruction.

Un entrypoint de programa de Solana tiene la siguiente firma de función. Los desarrolladores tienen libertad para crear su propia implementación de la función entrypoint.

#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;

Definir el estado del programa

Ahora vamos a definir la estructura de datos que se almacenará en nuestras cuentas de contador. Estos son los datos que se almacenarán en el campo data de la cuenta.

Añade el siguiente código a lib.rs:

lib.rs
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
pub count: u64,
}

Definir enum de instrucciones

Vamos a definir las instrucciones que nuestro programa puede ejecutar. Usaremos un enum donde cada variante representa una instrucción diferente.

Añade el siguiente código a lib.rs:

lib.rs
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum CounterInstruction {
InitializeCounter { initial_value: u64 },
IncrementCounter,
}

Implementar deserialización de instrucciones

Ahora necesitamos deserializar el instruction_data (bytes en bruto) en una de nuestras variantes del enum CounterInstruction. El método try_from_slice de Borsh maneja esta conversión automáticamente.

Actualiza la función process_instruction para usar la deserialización de Borsh:

lib.rs
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let instruction = CounterInstruction::try_from_slice(instruction_data)
.map_err(|_| ProgramError::InvalidInstructionData)?;
Ok(())
}

Dirigir instrucciones a los manejadores

Ahora vamos a actualizar la función principal process_instruction para dirigir las instrucciones a sus funciones manejadoras apropiadas.

Este patrón de enrutamiento es común en los programas de Solana. El instruction_data se deserializa en una variante de un enum que representa la instrucción, luego se llama a la función manejadora apropiada. Cada función manejadora incluye la implementación para esa instrucción.

Añade el siguiente código a lib.rs actualizando la función process_instruction y añadiendo los manejadores para las instrucciones InitializeCounter y IncrementCounter:

lib.rs
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let instruction = CounterInstruction::try_from_slice(instruction_data)
.map_err(|_| ProgramError::InvalidInstructionData)?;
match instruction {
CounterInstruction::InitializeCounter { initial_value } => {
process_initialize_counter(program_id, accounts, initial_value)?
}
CounterInstruction::IncrementCounter => {
process_increment_counter(program_id, accounts)?
}
};
Ok(())
}
fn process_initialize_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
initial_value: u64,
) -> ProgramResult {
Ok(())
}
fn process_increment_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
Ok(())
}

Implementar manejador de inicialización

Vamos a implementar el manejador para crear e inicializar una nueva cuenta de contador. Como solo el System Program puede crear cuentas en Solana, usaremos una Cross Program Invocation (CPI), esencialmente llamando a otro programa desde nuestro programa.

Nuestro programa hace una CPI para llamar a la instrucción create_account del System Program. La nueva cuenta se crea con nuestro programa como propietario, dando a nuestro programa la capacidad de escribir en la cuenta e inicializar los datos.

Añade el siguiente código a lib.rs actualizando la función process_initialize_counter:

lib.rs
fn process_initialize_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
initial_value: u64,
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let counter_account = next_account_info(accounts_iter)?;
let payer_account = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;
let account_space = 8;
let rent = Rent::get()?;
let required_lamports = rent.minimum_balance(account_space);
invoke(
&system_instruction::create_account(
payer_account.key,
counter_account.key,
required_lamports,
account_space as u64,
program_id,
),
&[
payer_account.clone(),
counter_account.clone(),
system_program.clone(),
],
)?;
let counter_data = CounterAccount {
count: initial_value,
};
let mut account_data = &mut counter_account.data.borrow_mut()[..];
counter_data.serialize(&mut account_data)?;
msg!("Counter initialized with value: {}", initial_value);
Ok(())
}

Esta instrucción es solo para fines de demostración. No incluye comprobaciones de seguridad y validación que son necesarias para programas en producción.

Implementar el manejador de incremento

Ahora vamos a implementar el manejador que incrementa un contador existente. Esta instrucción:

  • Lee el campo data de la cuenta para el counter_account
  • Lo deserializa en una estructura CounterAccount
  • Incrementa el campo count en 1
  • Serializa la estructura CounterAccount de vuelta al campo data de la cuenta

Añade el siguiente código a lib.rs actualizando la función process_increment_counter:

lib.rs
fn process_increment_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let counter_account = next_account_info(accounts_iter)?;
if counter_account.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
let mut data = counter_account.data.borrow_mut();
let mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;
counter_data.count = counter_data
.count
.checked_add(1)
.ok_or(ProgramError::InvalidAccountData)?;
counter_data.serialize(&mut &mut data[..])?;
msg!("Counter incremented to: {}", counter_data.count);
Ok(())
}

Esta instrucción es solo para fines de demostración. No incluye comprobaciones de seguridad y validación que son necesarias para programas en producción.

Programa completado

¡Felicidades! Has construido un programa completo de Solana que demuestra la estructura básica compartida por todos los programas de Solana:

  • Punto de entrada: Define dónde comienza la ejecución del programa y dirige todas las solicitudes entrantes a los manejadores de instrucciones apropiados
  • Manejo de instrucciones: Define las instrucciones y sus funciones manejadoras asociadas
  • Gestión de estado: Define las estructuras de datos de las cuentas y gestiona su estado en cuentas propiedad del programa
  • Cross Program Invocation (CPI): Llama al System Program para crear nuevas cuentas propiedad del programa

El siguiente paso es probar el programa para asegurarse de que todo funciona correctamente.

Cargo.toml
lib.rs
[package]
name = "counter_program"
version = "0.1.0"
edition = "2021"
[dependencies]

Parte 2: Probando el programa

Ahora vamos a probar nuestro programa contador. Usaremos LiteSVM, un framework de pruebas que nos permite probar programas sin desplegarlos en un clúster.

Añadir dependencias de prueba

Primero, vamos a añadir las dependencias necesarias para las pruebas. Usaremos litesvm para las pruebas y solana-sdk.

Terminal
$
cargo add litesvm@0.6.1 --dev
$
cargo add solana-sdk@2.2.0 --dev

Crear módulo de prueba

Ahora vamos a añadir un módulo de prueba a nuestro programa. Comenzaremos con la estructura básica y las importaciones.

Añade el siguiente código a lib.rs, directamente debajo del código del programa:

lib.rs
#[cfg(test)]
mod test {
use super::*;
use litesvm::LiteSVM;
use solana_sdk::{
account::ReadableAccount,
instruction::{AccountMeta, Instruction},
message::Message,
signature::{Keypair, Signer},
system_program,
transaction::Transaction,
};
#[test]
fn test_counter_program() {
// Test implementation will go here
}
}

El atributo #[cfg(test)] asegura que este código solo se compile cuando se ejecutan pruebas.

Inicializar entorno de prueba

Vamos a configurar el entorno de prueba con LiteSVM y financiar una cuenta de pagador.

LiteSVM simula el entorno de ejecución de Solana, permitiéndonos probar nuestro programa sin desplegarlo en un clúster real.

Añade el siguiente código a lib.rs actualizando la función test_counter_program:

lib.rs
let mut svm = LiteSVM::new();
let payer = Keypair::new();
svm.airdrop(&payer.pubkey(), 1_000_000_000)
.expect("Failed to airdrop");

Cargar el programa

Ahora necesitamos compilar y cargar nuestro programa en el entorno de prueba. Ejecuta el comando cargo build-sbf para compilar el programa. Esto generará el archivo counter_program.so en el directorio target/deploy.

Terminal
$
cargo build-sbf

Asegúrate de que el edition en Cargo.toml esté configurado como 2021.

Después de compilar, podemos cargar el programa.

Actualiza la función test_counter_program para cargar el programa en el entorno de prueba.

lib.rs
let program_keypair = Keypair::new();
let program_id = program_keypair.pubkey();
svm.add_program_from_file(
program_id,
"target/deploy/counter_program.so"
).expect("Failed to load program");

Debes ejecutar cargo build-sbf antes de ejecutar las pruebas para generar el archivo .so. La prueba carga el programa compilado.

Probar la instrucción de inicialización

Vamos a probar la instrucción de inicialización creando una nueva cuenta de contador con un valor inicial.

Añade el siguiente código a lib.rs actualizando la función test_counter_program:

lib.rs
let counter_keypair = Keypair::new();
let initial_value: u64 = 42;
println!("Testing counter initialization...");
let init_instruction_data =
borsh::to_vec(&CounterInstruction::InitializeCounter { initial_value })
.expect("Failed to serialize instruction");
let initialize_instruction = Instruction::new_with_bytes(
program_id,
&init_instruction_data,
vec![
AccountMeta::new(counter_keypair.pubkey(), true),
AccountMeta::new(payer.pubkey(), true),
AccountMeta::new_readonly(system_program::id(), false),
],
);
let message = Message::new(&[initialize_instruction], Some(&payer.pubkey()));
let transaction = Transaction::new(
&[&payer, &counter_keypair],
message,
svm.latest_blockhash()
);
let result = svm.send_transaction(transaction);
assert!(result.is_ok(), "Initialize transaction should succeed");
let logs = result.unwrap().logs;
println!("Transaction logs:\n{:#?}", logs);

Verificar la inicialización

Después de la inicialización, vamos a verificar que la cuenta del contador se creó correctamente con el valor esperado.

Añade el siguiente código a lib.rs actualizando la función test_counter_program:

lib.rs
let account = svm
.get_account(&counter_keypair.pubkey())
.expect("Failed to get counter account");
let counter: CounterAccount = CounterAccount::try_from_slice(account.data())
.expect("Failed to deserialize counter data");
assert_eq!(counter.count, 42);
println!("Counter initialized successfully with value: {}", counter.count);

Probar la instrucción de incremento

Ahora vamos a probar la instrucción de incremento para asegurarnos de que actualiza correctamente el valor del contador.

Añade el siguiente código a lib.rs actualizando la función test_counter_program:

lib.rs
println!("Testing counter increment...");
let increment_instruction_data =
borsh::to_vec(&CounterInstruction::IncrementCounter)
.expect("Failed to serialize instruction");
let increment_instruction = Instruction::new_with_bytes(
program_id,
&increment_instruction_data,
vec![AccountMeta::new(counter_keypair.pubkey(), true)],
);
let message = Message::new(&[increment_instruction], Some(&payer.pubkey()));
let transaction = Transaction::new(
&[&payer, &counter_keypair],
message,
svm.latest_blockhash()
);
let result = svm.send_transaction(transaction);
assert!(result.is_ok(), "Increment transaction should succeed");
let logs = result.unwrap().logs;
println!("Transaction logs:\n{:#?}", logs);

Verificar los resultados finales

Finalmente, vamos a verificar que el incremento funcionó correctamente comprobando el valor actualizado del contador.

Añade el siguiente código a lib.rs actualizando la función test_counter_program:

lib.rs
let account = svm
.get_account(&counter_keypair.pubkey())
.expect("Failed to get counter account");
let counter: CounterAccount = CounterAccount::try_from_slice(account.data())
.expect("Failed to deserialize counter data");
assert_eq!(counter.count, 43);
println!("Counter incremented successfully to: {}", counter.count);

Ejecuta las pruebas con el siguiente comando. La bandera --nocapture muestra la salida de la prueba.

Terminal
$
cargo test -- --nocapture

Salida esperada:

Testing counter initialization...
Transaction logs:
[
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]",
"Program 11111111111111111111111111111111 invoke [2]",
"Program 11111111111111111111111111111111 success",
"Program log: Counter initialized with value: 42",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 3803 of 200000 compute units",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",
]
Counter initialized successfully with value: 42
Testing counter increment...
Transaction logs:
[
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]",
"Program log: Counter incremented to: 43",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 762 of 200000 compute units",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",
]
Counter incremented successfully to: 43

Añadir dependencias de prueba

Primero, vamos a añadir las dependencias necesarias para las pruebas. Usaremos litesvm para las pruebas y solana-sdk.

Terminal
$
cargo add litesvm@0.6.1 --dev
$
cargo add solana-sdk@2.2.0 --dev

Crear módulo de prueba

Ahora vamos a añadir un módulo de prueba a nuestro programa. Comenzaremos con la estructura básica y las importaciones.

Añade el siguiente código a lib.rs, directamente debajo del código del programa:

lib.rs
#[cfg(test)]
mod test {
use super::*;
use litesvm::LiteSVM;
use solana_sdk::{
account::ReadableAccount,
instruction::{AccountMeta, Instruction},
message::Message,
signature::{Keypair, Signer},
system_program,
transaction::Transaction,
};
#[test]
fn test_counter_program() {
// Test implementation will go here
}
}

El atributo #[cfg(test)] asegura que este código solo se compile cuando se ejecutan pruebas.

Inicializar entorno de prueba

Vamos a configurar el entorno de prueba con LiteSVM y financiar una cuenta de pagador.

LiteSVM simula el entorno de ejecución de Solana, permitiéndonos probar nuestro programa sin desplegarlo en un clúster real.

Añade el siguiente código a lib.rs actualizando la función test_counter_program:

lib.rs
let mut svm = LiteSVM::new();
let payer = Keypair::new();
svm.airdrop(&payer.pubkey(), 1_000_000_000)
.expect("Failed to airdrop");

Cargar el programa

Ahora necesitamos compilar y cargar nuestro programa en el entorno de prueba. Ejecuta el comando cargo build-sbf para compilar el programa. Esto generará el archivo counter_program.so en el directorio target/deploy.

Terminal
$
cargo build-sbf

Asegúrate de que el edition en Cargo.toml esté configurado como 2021.

Después de compilar, podemos cargar el programa.

Actualiza la función test_counter_program para cargar el programa en el entorno de prueba.

lib.rs
let program_keypair = Keypair::new();
let program_id = program_keypair.pubkey();
svm.add_program_from_file(
program_id,
"target/deploy/counter_program.so"
).expect("Failed to load program");

Debes ejecutar cargo build-sbf antes de ejecutar las pruebas para generar el archivo .so. La prueba carga el programa compilado.

Probar la instrucción de inicialización

Vamos a probar la instrucción de inicialización creando una nueva cuenta de contador con un valor inicial.

Añade el siguiente código a lib.rs actualizando la función test_counter_program:

lib.rs
let counter_keypair = Keypair::new();
let initial_value: u64 = 42;
println!("Testing counter initialization...");
let init_instruction_data =
borsh::to_vec(&CounterInstruction::InitializeCounter { initial_value })
.expect("Failed to serialize instruction");
let initialize_instruction = Instruction::new_with_bytes(
program_id,
&init_instruction_data,
vec![
AccountMeta::new(counter_keypair.pubkey(), true),
AccountMeta::new(payer.pubkey(), true),
AccountMeta::new_readonly(system_program::id(), false),
],
);
let message = Message::new(&[initialize_instruction], Some(&payer.pubkey()));
let transaction = Transaction::new(
&[&payer, &counter_keypair],
message,
svm.latest_blockhash()
);
let result = svm.send_transaction(transaction);
assert!(result.is_ok(), "Initialize transaction should succeed");
let logs = result.unwrap().logs;
println!("Transaction logs:\n{:#?}", logs);

Verificar la inicialización

Después de la inicialización, vamos a verificar que la cuenta del contador se creó correctamente con el valor esperado.

Añade el siguiente código a lib.rs actualizando la función test_counter_program:

lib.rs
let account = svm
.get_account(&counter_keypair.pubkey())
.expect("Failed to get counter account");
let counter: CounterAccount = CounterAccount::try_from_slice(account.data())
.expect("Failed to deserialize counter data");
assert_eq!(counter.count, 42);
println!("Counter initialized successfully with value: {}", counter.count);

Probar la instrucción de incremento

Ahora vamos a probar la instrucción de incremento para asegurarnos de que actualiza correctamente el valor del contador.

Añade el siguiente código a lib.rs actualizando la función test_counter_program:

lib.rs
println!("Testing counter increment...");
let increment_instruction_data =
borsh::to_vec(&CounterInstruction::IncrementCounter)
.expect("Failed to serialize instruction");
let increment_instruction = Instruction::new_with_bytes(
program_id,
&increment_instruction_data,
vec![AccountMeta::new(counter_keypair.pubkey(), true)],
);
let message = Message::new(&[increment_instruction], Some(&payer.pubkey()));
let transaction = Transaction::new(
&[&payer, &counter_keypair],
message,
svm.latest_blockhash()
);
let result = svm.send_transaction(transaction);
assert!(result.is_ok(), "Increment transaction should succeed");
let logs = result.unwrap().logs;
println!("Transaction logs:\n{:#?}", logs);

Verificar los resultados finales

Finalmente, vamos a verificar que el incremento funcionó correctamente comprobando el valor actualizado del contador.

Añade el siguiente código a lib.rs actualizando la función test_counter_program:

lib.rs
let account = svm
.get_account(&counter_keypair.pubkey())
.expect("Failed to get counter account");
let counter: CounterAccount = CounterAccount::try_from_slice(account.data())
.expect("Failed to deserialize counter data");
assert_eq!(counter.count, 43);
println!("Counter incremented successfully to: {}", counter.count);

Ejecuta las pruebas con el siguiente comando. La bandera --nocapture muestra la salida de la prueba.

Terminal
$
cargo test -- --nocapture

Salida esperada:

Testing counter initialization...
Transaction logs:
[
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]",
"Program 11111111111111111111111111111111 invoke [2]",
"Program 11111111111111111111111111111111 success",
"Program log: Counter initialized with value: 42",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 3803 of 200000 compute units",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",
]
Counter initialized successfully with value: 42
Testing counter increment...
Transaction logs:
[
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]",
"Program log: Counter incremented to: 43",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 762 of 200000 compute units",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",
]
Counter incremented successfully to: 43
Cargo.toml
[package]
name = "counter_program"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[dependencies]
borsh = "1.5.7"
solana-program = "2.2.0"
[dev-dependencies]
litesvm = "0.6.1"
solana-sdk = "2.2.0"

Parte 3: Invocando el programa

Ahora vamos a añadir un script cliente para invocar el programa.

Crear ejemplo de cliente

Vamos a crear un cliente en Rust para interactuar con nuestro programa desplegado.

Terminal
$
mkdir examples
$
touch examples/client.rs

Añade la siguiente configuración a Cargo.toml:

Cargo.toml
[[example]]
name = "client"
path = "examples/client.rs"

Instala las dependencias del cliente:

Terminal
$
cargo add solana-client@2.2.0 --dev
$
cargo add tokio --dev

Implementar código del cliente

Ahora vamos a implementar el cliente que invocará nuestro programa desplegado.

Ejecuta el siguiente comando para obtener el ID de tu programa desde el archivo keypair:

Terminal
$
solana address -k ./target/deploy/counter_program-keypair.json

Añade el código del cliente a examples/client.rs y reemplaza el program_id con la salida del comando anterior:

examples/client.rs
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH")
.expect("Invalid program ID");
examples/client.rs
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
commitment_config::CommitmentConfig,
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
signature::{Keypair, Signer},
system_program,
transaction::Transaction,
};
use std::str::FromStr;
use counter_program::CounterInstruction;
#[tokio::main]
async fn main() {
// Replace with your actual program ID from deployment
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH")
.expect("Invalid program ID");
// Connect to local cluster
let rpc_url = String::from("http://localhost:8899");
let client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());
// Generate a new keypair for paying fees
let payer = Keypair::new();
// Request airdrop of 1 SOL for transaction fees
println!("Requesting airdrop...");
let airdrop_signature = client
.request_airdrop(&payer.pubkey(), 1_000_000_000)
.expect("Failed to request airdrop");
// Wait for airdrop confirmation
loop {
if client
.confirm_transaction(&airdrop_signature)
.unwrap_or(false)
{
break;
}
std::thread::sleep(std::time::Duration::from_millis(500));
}
println!("Airdrop confirmed");
println!("\nInitializing counter...");
let counter_keypair = Keypair::new();
let initial_value = 100u64;
// Serialize the initialize instruction data
let instruction_data = borsh::to_vec(&CounterInstruction::InitializeCounter { initial_value })
.expect("Failed to serialize instruction");
let initialize_instruction = Instruction::new_with_bytes(
program_id,
&instruction_data,
vec![
AccountMeta::new(counter_keypair.pubkey(), true),
AccountMeta::new(payer.pubkey(), true),
AccountMeta::new_readonly(system_program::id(), false),
],
);
let mut transaction =
Transaction::new_with_payer(&[initialize_instruction], Some(&payer.pubkey()));
let blockhash = client
.get_latest_blockhash()
.expect("Failed to get blockhash");
transaction.sign(&[&payer, &counter_keypair], blockhash);
match client.send_and_confirm_transaction(&transaction) {
Ok(signature) => {
println!("Counter initialized!");
println!("Transaction: {}", signature);
println!("Counter address: {}", counter_keypair.pubkey());
}
Err(err) => {
eprintln!("Failed to initialize counter: {}", err);
return;
}
}
println!("\nIncrementing counter...");
// Serialize the increment instruction data
let increment_data = borsh::to_vec(&CounterInstruction::IncrementCounter)
.expect("Failed to serialize instruction");
let increment_instruction = Instruction::new_with_bytes(
program_id,
&increment_data,
vec![AccountMeta::new(counter_keypair.pubkey(), true)],
);
let mut transaction =
Transaction::new_with_payer(&[increment_instruction], Some(&payer.pubkey()));
transaction.sign(&[&payer, &counter_keypair], blockhash);
match client.send_and_confirm_transaction(&transaction) {
Ok(signature) => {
println!("Counter incremented!");
println!("Transaction: {}", signature);
}
Err(err) => {
eprintln!("Failed to increment counter: {}", err);
}
}
}

Crear ejemplo de cliente

Vamos a crear un cliente en Rust para interactuar con nuestro programa desplegado.

Terminal
$
mkdir examples
$
touch examples/client.rs

Añade la siguiente configuración a Cargo.toml:

Cargo.toml
[[example]]
name = "client"
path = "examples/client.rs"

Instala las dependencias del cliente:

Terminal
$
cargo add solana-client@2.2.0 --dev
$
cargo add tokio --dev

Implementar código del cliente

Ahora vamos a implementar el cliente que invocará nuestro programa desplegado.

Ejecuta el siguiente comando para obtener el ID de tu programa desde el archivo keypair:

Terminal
$
solana address -k ./target/deploy/counter_program-keypair.json

Añade el código del cliente a examples/client.rs y reemplaza el program_id con la salida del comando anterior:

examples/client.rs
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH")
.expect("Invalid program ID");
Cargo.toml
[package]
name = "counter_program"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[dependencies]
borsh = "1.5.7"
solana-program = "2.2.0"
[dev-dependencies]
litesvm = "0.6.1"
solana-sdk = "2.2.0"
solana-client = "2.2.0"
tokio = "1.47.1"
[[example]]
name = "client"
path = "examples/client.rs"

Parte 4: Desplegar el programa

Ahora que tenemos nuestro programa y cliente listos, vamos a compilar, desplegar e invocar el programa.

Compilar el programa

Primero, vamos a compilar nuestro programa.

Terminal
$
cargo build-sbf

Este comando compila tu programa y genera dos archivos importantes en target/deploy/:

counter_program.so # The compiled program
counter_program-keypair.json # Keypair for the program ID

Puedes ver el ID de tu programa ejecutando el siguiente comando:

Terminal
$
solana address -k ./target/deploy/counter_program-keypair.json

Ejemplo de salida:

HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF

Iniciar el validator local

Para desarrollo, usaremos un validator de prueba local.

Primero, configura la CLI de Solana para usar localhost:

Terminal
$
solana config set -ul

Ejemplo de salida:

Config File: ~/.config/solana/cli/config.yml
RPC URL: http://localhost:8899
WebSocket URL: ws://localhost:8900/ (computed)
Keypair Path: ~/.config/solana/id.json
Commitment: confirmed

Ahora inicia el validator de prueba en una terminal separada:

Terminal
$
solana-test-validator

Desplegar el programa

Con el validator en ejecución, despliega tu programa en el clúster local:

Terminal
$
solana program deploy ./target/deploy/counter_program.so

Ejemplo de salida:

Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Signature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h

Puedes verificar el despliegue usando el comando solana program show con el ID de tu programa:

Terminal
$
solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF

Ejemplo de salida:

Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Owner: BPFLoaderUpgradeab1e11111111111111111111111
ProgramData Address: 47MVf5tRZ4zWXQMX7ydrkgcFQr8XTk1QBjohwsUzaiuM
Authority: 4kh6HxYZiAebF8HWLsUWod2EaQQ6iWHpHYCz8UcmFbM1
Last Deployed In Slot: 16
Data Length: 82696 (0x14308) bytes
Balance: 0.57676824 SOL

Ejecutar el cliente

Con el validator local aún en ejecución, ejecuta el cliente:

Terminal
$
cargo run --example client

Salida esperada:

Requesting airdrop...
Airdrop confirmed
Initializing counter...
Counter initialized!
Transaction: 2uenChtqNeLC1fitqoVE2LBeygSBTDchMZ4gGqs7AiDvZZVJguLDE5PfxsfkgY7xs6zFWnYsbEtb82dWv9tDT14k
Counter address: EppPAmwqD42u4SCPWpPT7wmWKdFad5VnM9J4R9Zfofcy
Incrementing counter...
Counter incremented!
Transaction: 4qv1Rx6FHu1M3woVgDQ6KtYUaJgBzGcHnhej76ZpaKGCgsTorbcHnPKxoH916UENw7X5ppnQ8PkPnhXxEwrYuUxS

Con el validator local en ejecución, puedes ver las transacciones en Solana Explorer usando las firmas de transacción de la salida. Ten en cuenta que el cluster en Solana Explorer debe estar configurado como "Custom RPC URL", que por defecto es http://localhost:8899 en el que el solana-test-validator está ejecutándose.

Is this page helpful?

Tabla de Contenidos

Editar Página