Estrutura do programa em Rust
Os programas Solana escritos em Rust têm requisitos estruturais mínimos,
permitindo flexibilidade na organização do código. O único requisito é que um
programa deve ter um entrypoint
, que define onde a execução de um programa se
inicia.
Estrutura do programa
Embora não existam regras rígidas para a estrutura de arquivos, os programas Solana geralmente seguem um padrão comum:
entrypoint.rs
: Define o ponto de entrada que direciona as instruções recebidas.state.rs
: Define o estado do programa (dados da conta).instructions.rs
: Define as instruções que o programa pode executar.processor.rs
: Define os manipuladores de instruções (funções) que implementam a lógica de negócios para cada instrução.error.rs
: Define erros personalizados que o programa pode retornar.
Por exemplo, veja o Token Program.
Programa de exemplo
Para demonstrar como construir um programa nativo em Rust com múltiplas instruções, vamos percorrer um programa contador simples que implementa duas instruções:
InitializeCounter
: Cria e inicializa uma nova conta com um valor inicial.IncrementCounter
: Incrementa o valor armazenado em uma conta existente.
Para simplificar, o programa será implementado em um único arquivo lib.rs
,
embora na prática você possa querer dividir programas maiores em vários
arquivos.
Parte 1: Escrevendo o programa
Vamos começar construindo o programa contador. Criaremos um programa que pode inicializar um contador com um valor inicial e incrementá-lo.
Criar um novo programa
Primeiro, vamos criar um novo projeto Rust para nosso programa Solana.
$cargo new counter_program --lib$cd counter_program
Você deverá ver os arquivos padrão src/lib.rs
e Cargo.toml
.
Atualize o campo edition
no arquivo Cargo.toml
para 2021. Caso contrário,
você poderá encontrar um erro ao compilar o programa.
Adicionar dependências
Agora vamos adicionar as dependências necessárias para construir um programa
Solana. Precisamos do solana-program
para o SDK principal e do borsh
para
serialização.
$cargo add solana-program@2.2.0$cargo add borsh
Não há exigência de usar Borsh. No entanto, é uma biblioteca de serialização comumente usada para programas Solana.
Configurar crate-type
Os programas Solana devem ser compilados como bibliotecas dinâmicas. Adicione a
seção [lib]
para configurar como o Cargo compila o programa.
[lib]crate-type = ["cdylib", "lib"]
Se você não incluir esta configuração, o diretório target/deploy não será gerado quando você compilar o programa.
Configurar ponto de entrada do programa
Todo programa Solana tem um ponto de entrada, que é a função que é chamada quando o programa é invocado. Vamos começar adicionando as importações necessárias para o programa e configurando o ponto de entrada.
Adicione o seguinte código ao 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(())}
A macro
entrypoint
lida com a desserialização dos dados input
nos parâmetros da função
process_instruction
.
Um entrypoint
de programa Solana tem a seguinte assinatura de função. Os
desenvolvedores têm liberdade para criar sua própria implementação da função
entrypoint
.
#[no_mangle]pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;
Definir estado do programa
Agora vamos definir a estrutura de dados que será armazenada em nossas contas de
contador. Esses são os dados que serão armazenados no campo data
da conta.
Adicione o seguinte código ao lib.rs
:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {pub count: u64,}
Definir enum de instrução
Vamos definir as instruções que nosso programa pode executar. Usaremos um enum onde cada variante representa uma instrução diferente.
Adicione o seguinte código ao lib.rs
:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 },IncrementCounter,}
Implementar desserialização de instrução
Agora precisamos desserializar o instruction_data
(bytes brutos) em uma das
nossas variantes do enum CounterInstruction
. O método Borsh try_from_slice
lida com essa conversão automaticamente.
Atualize a função process_instruction
para usar a desserialização 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(())}
Rotear instruções para manipuladores
Agora vamos atualizar a função principal process_instruction
para rotear
instruções para suas funções manipuladoras apropriadas.
Este padrão de roteamento é comum em programas Solana. O instruction_data
é
desserializado em uma variante de um enum representando a instrução, então a
função manipuladora apropriada é chamada. Cada função manipuladora inclui a
implementação para essa instrução.
Adicione o seguinte código ao lib.rs
atualizando a função
process_instruction
e adicionando os manipuladores para as instruções
InitializeCounter
e 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 manipulador de inicialização
Vamos implementar o manipulador para criar e inicializar uma nova conta de contador. Como apenas o System Program pode criar contas na Solana, usaremos uma Cross Program Invocation (CPI), essencialmente chamando outro programa a partir do nosso programa.
Nosso programa faz uma CPI para chamar a instrução create_account
do System
Program. A nova conta é criada com nosso programa como proprietário, dando ao
nosso programa a capacidade de escrever na conta e inicializar os dados.
Adicione o seguinte código a lib.rs
atualizando a função
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 instrução é apenas para fins de demonstração. Não inclui verificações de segurança e validação que são necessárias para programas em produção.
Implementar o manipulador de incremento
Agora vamos implementar o manipulador que incrementa um contador existente. Esta instrução:
- Lê o campo
data
da conta para ocounter_account
- Deserializa-o em uma estrutura
CounterAccount
- Incrementa o campo
count
em 1 - Serializa a estrutura
CounterAccount
de volta para o campodata
da conta
Adicione o seguinte código a lib.rs
atualizando a função
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 instrução é apenas para fins de demonstração. Não inclui verificações de segurança e validação que são necessárias para programas em produção.
Programa completo
Parabéns! Você construiu um programa Solana completo que demonstra a estrutura básica compartilhada por todos os programas Solana:
- Ponto de entrada: Define onde a execução do programa começa e direciona todas as solicitações recebidas para os manipuladores de instruções apropriados
- Manipulação de instruções: Define instruções e suas funções de manipulação associadas
- Gerenciamento de estado: Define estruturas de dados de contas e gerencia seu estado em contas pertencentes ao programa
- Cross Program Invocation (CPI): Chama o System Program para criar novas contas pertencentes ao programa
O próximo passo é testar o programa para garantir que tudo funcione corretamente.
Parte 2: Testando o programa
Agora vamos testar nosso programa de contador. Usaremos o LiteSVM, um framework de testes que nos permite testar programas sem implantá-los em um cluster.
Adicionar dependências de teste
Primeiro, vamos adicionar as dependências necessárias para testes. Usaremos
litesvm
para testes e solana-sdk
.
$cargo add litesvm@0.6.1 --dev$cargo add solana-sdk@2.2.0 --dev
Criar módulo de teste
Agora vamos adicionar um módulo de teste ao nosso programa. Começaremos com a estrutura básica e importações.
Adicione o seguinte código ao lib.rs
, diretamente abaixo do código do
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}}
O atributo #[cfg(test)]
garante que este código seja compilado apenas ao
executar testes.
Inicializar ambiente de teste
Vamos configurar o ambiente de teste com o LiteSVM e financiar uma conta pagadora.
O LiteSVM simula o ambiente de execução da Solana, permitindo testar nosso programa sem implantá-lo em um cluster real.
Adicione o seguinte código ao lib.rs
atualizando a função
test_counter_program
:
let mut svm = LiteSVM::new();let payer = Keypair::new();svm.airdrop(&payer.pubkey(), 1_000_000_000).expect("Failed to airdrop");
Carregar o programa
Agora precisamos compilar e carregar nosso programa no ambiente de teste.
Execute o comando cargo build-sbf
para compilar o programa. Isso gerará o
arquivo counter_program.so
no diretório target/deploy
.
$cargo build-sbf
Certifique-se de que o edition
em Cargo.toml
esteja definido como 2021
.
Após a compilação, podemos carregar o programa.
Atualize a função test_counter_program
para carregar o programa no ambiente de
teste.
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");
Você deve executar cargo build-sbf
antes de executar os testes para gerar o
arquivo .so
. O teste carrega o programa compilado.
Testar instrução de inicialização
Vamos testar a instrução de inicialização criando uma nova conta de contador com um valor inicial.
Adicione o seguinte código a lib.rs
atualizando a função
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 inicialização
Após a inicialização, vamos verificar se a conta do contador foi criada corretamente com o valor esperado.
Adicione o seguinte código a lib.rs
atualizando a função
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);
Testar instrução de incremento
Agora vamos testar a instrução de incremento para garantir que ela atualize corretamente o valor do contador.
Adicione o seguinte código a lib.rs
atualizando a função
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 resultados finais
Finalmente, vamos verificar se o incremento funcionou corretamente verificando o valor atualizado do contador.
Adicione o seguinte código a lib.rs
atualizando a função
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);
Execute os testes com o seguinte comando. A flag --nocapture
imprime a saída
do teste.
$cargo test -- --nocapture
Saída 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 o programa
Agora vamos adicionar um script cliente para invocar o programa.
Criar exemplo de cliente
Vamos criar um cliente em Rust para interagir com nosso programa implantado.
$mkdir examples$touch examples/client.rs
Adicione a seguinte configuração ao Cargo.toml
:
[[example]]name = "client"path = "examples/client.rs"
Instale as dependências do cliente:
$cargo add solana-client@2.2.0 --dev$cargo add tokio --dev
Implementar código do cliente
Agora vamos implementar o cliente que irá invocar nosso programa implantado.
Execute o seguinte comando para obter o ID do seu programa a partir do arquivo keypair:
$solana address -k ./target/deploy/counter_program-keypair.json
Adicione o código do cliente ao examples/client.rs
e substitua o program_id
com a saída do 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: Implantando o programa
Agora que temos nosso programa e cliente prontos, vamos compilar, implantar e invocar o programa.
Compilar o programa
Primeiro, vamos compilar nosso programa.
$cargo build-sbf
Este comando compila seu programa e gera dois arquivos importantes em
target/deploy/
:
counter_program.so # The compiled programcounter_program-keypair.json # Keypair for the program ID
Você pode visualizar o ID do seu programa executando o seguinte comando:
$solana address -k ./target/deploy/counter_program-keypair.json
Exemplo de saída:
HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Iniciar o validator local
Para desenvolvimento, usaremos um validator de teste local.
Primeiro, configure a CLI da Solana para usar o localhost:
$solana config set -ul
Exemplo de saída:
Config File: ~/.config/solana/cli/config.ymlRPC URL: http://localhost:8899WebSocket URL: ws://localhost:8900/ (computed)Keypair Path: ~/.config/solana/id.jsonCommitment: confirmed
Agora inicie o validator de teste em um terminal separado:
$solana-test-validator
Implantar o programa
Com o validator em execução, implante seu programa no cluster local:
$solana program deploy ./target/deploy/counter_program.so
Exemplo de saída:
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFSignature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h
Você pode verificar a implantação usando o comando solana program show
com o
ID do seu programa:
$solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Exemplo de saída:
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFOwner: BPFLoaderUpgradeab1e11111111111111111111111ProgramData Address: 47MVf5tRZ4zWXQMX7ydrkgcFQr8XTk1QBjohwsUzaiuMAuthority: 4kh6HxYZiAebF8HWLsUWod2EaQQ6iWHpHYCz8UcmFbM1Last Deployed In Slot: 16Data Length: 82696 (0x14308) bytesBalance: 0.57676824 SOL
Execute o cliente
Com o validator local ainda em execução, execute o cliente:
$cargo run --example client
Saída esperada:
Requesting airdrop...Airdrop confirmedInitializing counter...Counter initialized!Transaction: 2uenChtqNeLC1fitqoVE2LBeygSBTDchMZ4gGqs7AiDvZZVJguLDE5PfxsfkgY7xs6zFWnYsbEtb82dWv9tDT14kCounter address: EppPAmwqD42u4SCPWpPT7wmWKdFad5VnM9J4R9ZfofcyIncrementing counter...Counter incremented!Transaction: 4qv1Rx6FHu1M3woVgDQ6KtYUaJgBzGcHnhej76ZpaKGCgsTorbcHnPKxoH916UENw7X5ppnQ8PkPnhXxEwrYuUxS
Com o validator local em execução, você pode visualizar as transações no
Solana Explorer usando as
assinaturas de transação da saída. Observe que o cluster no Solana Explorer deve
ser definido como "Custom RPC URL", que por padrão é http://localhost:8899
em
que o solana-test-validator
está sendo executado.
Is this page helpful?