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:
InitializeCounter
: Crea e inicializa una nueva cuenta con un valor inicial.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.
$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.
$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.
[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
:
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
:
#[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
:
#[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:
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
:
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
:
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 elcounter_account
- Lo deserializa en una estructura
CounterAccount
- Incrementa el campo
count
en 1 - Serializa la estructura
CounterAccount
de vuelta al campodata
de la cuenta
Añade el siguiente código a lib.rs
actualizando la función
process_increment_counter
:
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.
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
.
$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:
#[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
:
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
.
$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.
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
:
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
:
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
:
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
:
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.
$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: 42Testing 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
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.
$mkdir examples$touch examples/client.rs
Añade la siguiente configuración a Cargo.toml
:
[[example]]name = "client"path = "examples/client.rs"
Instala las dependencias del cliente:
$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:
$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:
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH").expect("Invalid program ID");
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 deploymentlet program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH").expect("Invalid program ID");// Connect to local clusterlet rpc_url = String::from("http://localhost:8899");let client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());// Generate a new keypair for paying feeslet payer = Keypair::new();// Request airdrop of 1 SOL for transaction feesprintln!("Requesting airdrop...");let airdrop_signature = client.request_airdrop(&payer.pubkey(), 1_000_000_000).expect("Failed to request airdrop");// Wait for airdrop confirmationloop {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 datalet 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 datalet 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);}}}
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.
$cargo build-sbf
Este comando compila tu programa y genera dos archivos importantes en
target/deploy/
:
counter_program.so # The compiled programcounter_program-keypair.json # Keypair for the program ID
Puedes ver el ID de tu programa ejecutando el siguiente comando:
$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:
$solana config set -ul
Ejemplo de salida:
Config File: ~/.config/solana/cli/config.ymlRPC URL: http://localhost:8899WebSocket URL: ws://localhost:8900/ (computed)Keypair Path: ~/.config/solana/id.jsonCommitment: confirmed
Ahora inicia el validator de prueba en una terminal separada:
$solana-test-validator
Desplegar el programa
Con el validator en ejecución, despliega tu programa en el clúster local:
$solana program deploy ./target/deploy/counter_program.so
Ejemplo de salida:
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFSignature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h
Puedes verificar el despliegue usando el comando solana program show
con el ID
de tu programa:
$solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Ejemplo de salida:
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFOwner: BPFLoaderUpgradeab1e11111111111111111111111ProgramData Address: 47MVf5tRZ4zWXQMX7ydrkgcFQr8XTk1QBjohwsUzaiuMAuthority: 4kh6HxYZiAebF8HWLsUWod2EaQQ6iWHpHYCz8UcmFbM1Last Deployed In Slot: 16Data Length: 82696 (0x14308) bytesBalance: 0.57676824 SOL
Ejecutar el cliente
Con el validator local aún en ejecución, ejecuta el cliente:
$cargo run --example client
Salida esperada:
Requesting airdrop...Airdrop confirmedInitializing counter...Counter initialized!Transaction: 2uenChtqNeLC1fitqoVE2LBeygSBTDchMZ4gGqs7AiDvZZVJguLDE5PfxsfkgY7xs6zFWnYsbEtb82dWv9tDT14kCounter address: EppPAmwqD42u4SCPWpPT7wmWKdFad5VnM9J4R9ZfofcyIncrementing 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?