NOVO: Helius adquire a Light Protocol
o framework Steel para escrever contratos inteligentes na Solana
Blog/Desenvolvimento

Como escrever programas Solana com Steel

Desenvolvedor Solana, Assylm LabsPerelyn no XPerelyn no LinkedIn
15 min de leitura

Steel é um framework leve e modular para escrever programas Solana nativos com o mínimo de código boilerplate e o máximo de controle. Criado por Hardhat Chad (da Ore), Steel foi desenvolvido para quem busca o desempenho do Rust nativo sem sacrificar a experiência do desenvolvedor.

Neste artigo, você aprenderá:

  • O que é Steel e como ele se relaciona com Anchor e Pinocchio
  • Como definir instruções e estruturar um projeto Steel
  • Como criar um token SPL personalizado usando Steel
  • Como testar seu programa com solana-program-test

Pré-requisitos

Este guia pressupõe que você conheça:

  • Sintaxe básica e toolchain do Rust
  • Fundamentos de desenvolvimento na Solana (contas, instruções, programas)
  • Uso da CLI (por exemplo, cargo, solana, curl)

Se você já consegue escrever programas básicos em Solana ou Rust, está pronto para criar com Steel.

O que é Steel?

Steel é um novo framework modular para criar programas na Solana. Ele permite escrever programas com menos código boilerplate e adota menos convenções rígidas que o Anchor.

Steel oferece macros e auxiliares de Cross-Program Invocation (CPI) que aceleram o desenvolvimento de programas Solana de forma semelhante à nativa (sem um framework). Assim, você obtém desempenho próximo ao nativo com uma experiência de desenvolvimento melhor.

Vamos conhecer algumas das macros e dos auxiliares oferecidos pelo Steel.

Macros do Steel

Algumas das macros oferecidas pelo Steel incluem:

account!

A macro account! define tipos Account no Steel e também dá a eles acesso ao trait AccountValidation, que fornece auxiliares para validar o estado das contas durante o desenvolvimento.

instruction!

A macro instruction! define tipos Instruction no Steel e também dá a eles acesso a uma função to_bytes, que será usada em api/src/sdk.

Outras macros do Steel incluem error e event; como os nomes sugerem, elas são usadas para erros e eventos, respectivamente.

Auxiliares de CPI do Steel

Steel fornece funções auxiliares necessárias para a maioria das Cross-Program Invocations (CPIs) durante o desenvolvimento de um programa, como instruções de system_program, incluindo create_account, transfer e outras. 

Ele também inclui instruções de spl_token_program / spl_associated_token_program, como mint_to, burn, create_associated_token_account e outras.

Otimizações de CU

Você pode esperar que Steel seja eficiente em CUs pelo que ele faz, mas essa eficiência vem, na verdade, do que ele não faz. Como o framework Steel é leve e adiciona pouca ou nenhuma sobrecarga aos programas Solana, ele é tão otimizado quanto programas Solana escritos em Rust nativo. Além disso, usa bytemuck como serializador de dados padrão.

Steel vs. Anchor

Anchor é um framework poderoso e opinativo criado para desenvolver programas Solana seguros rapidamente. Ele simplifica o processo de desenvolvimento ao reduzir o código boilerplate em áreas como (des)serialização de contas e dados de instruções, realizar verificações essenciais de segurança, gerar bibliotecas cliente automaticamente e fornecer um ambiente de testes completo.

Qual é a principal diferença entre Steel e Anchor?

Anchor é um framework de contratos inteligentes acessível para iniciantes que permite a desenvolvedores Solana de qualquer nível de experiência escrever programas rapidamente. Anchor prioriza uma experiência de desenvolvimento intuitiva e amigável, motivo pelo qual tantos desenvolvedores Solana dependem dele.

Porém, essa simplicidade tem um preço.

Anchor acumulou uma sobrecarga que deixa os binários de programas Solana mais pesados, afetando negativamente o desempenho on-chain. Por exemplo, isso aumenta o custo de implantar programas Solana e chamar instruções.

Devido à velocidade e à eficiência da Solana, a maioria das pessoas não percebe a sobrecarga adicionada pelo Anchor aos programas Solana. As exceções são quem desenvolve programas mais complexos, como Ore e Code-vm, nos quais essa sobrecarga os tornaria inviáveis on-chain.

Normalmente, programas Solana como esses seriam criados com Rust nativo. No entanto, seus mantenedores entendem como isso seria trabalhoso e precisam usar um framework mais amigável, comparável ao Anchor, mas com o alto desempenho do Rust nativo.

Benefícios e compromissos entre Steel e Anchor

Mesmo com a sobrecarga que adiciona aos programas Solana, Anchor oferece a melhor experiência de desenvolvimento do ecossistema Solana e continua sendo o framework recomendado para novos desenvolvedores Solana.

A sintaxe do Anchor é fácil de entender. Ele também fornece Interface Definition Languages (IDLs), que facilitam os testes de programas Solana em outras linguagens, como JavaScript, e o desenvolvimento de aplicações cliente que se comunicam com esses programas.

As IDLs do Anchor são tão poderosas que ferramentas como Codama podem usá-las para gerar automaticamente clientes, interfaces de linha de comando (CLIs) e documentação para programas Solana.

Atualmente, o framework Steel não oferece IDLs. Embora sua sintaxe seja amigável, Steel exige que o desenvolvedor tenha um bom nível de familiaridade com Rust.

Embora Anchor seja recomendado para novos desenvolvedores, ele pode limitar os mais técnicos, pois oculta em macros e em sua sintaxe o funcionamento interno do desenvolvimento de programas Solana. 

Steel, por outro lado, dá ao desenvolvedor acesso a todos os elementos de um programa Solana em seu nível mais básico. Essa granularidade é especialmente útil durante os testes, pois eles são escritos em Rust por padrão, proporcionando uma experiência direta de depuração.

Steel é um excelente framework de contratos inteligentes pelo que ele é — um wrapper mínimo sobre Rust nativo — e pelo que não é — sintaxe adicional que gera sobrecarga.

Em resumo, Steel é uma versão mais amigável do Rust nativo para desenvolvedores, preservando seu poder sem comprometer a eficiência.

Steel vs. Pinocchio

Pinocchio é uma biblioteca sem dependências para criar programas Solana em Rust. Ela foi escrita por Febo como um projeto paralelo e depois se tornou um projeto completo da Anza. Ela aproveita a forma como os loaders da SVM serializam os parâmetros de entrada de um programa em um array de bytes, que é então passado ao ponto de entrada do programa para definir tipos zero-copy usados na leitura da entrada.

Em resumo, Pinocchio é uma versão mais enxuta de solana_program que não depende de crates externas e evita tipos dinâmicos.

Desde o lançamento do Pinocchio, surgiram muitos equívocos sobre o que ele é. A biblioteca Pinocchio foi criada para substituir a biblioteca solana_program — ela não compete com Anchor nem Steel. Ela complementa esses frameworks ao torná-los mais leves.

O que a maioria das pessoas chama de programa Pinocchio é apenas código Rust nativo que depende de pinocchio em vez de solana_program.

Como criar um token usando Steel

Para demonstrar como Steel funciona, escreveremos um programa Solana simples que cria um token SPL. Se você aprende melhor visualmente, assista ao vídeo a seguir.

Pré-requisitos

  • Rust/Cargo
  • Solana
  • Steel

Instalar o Rust

Você pode instalar o Rust pelo site oficial do Rust ou pela CLI:

Código
curl --proto '=https' --tlsv1.2 -sSf <https://sh.rustup.rs> | sh

Instalar o conjunto de ferramentas da Solana

Steel também exige o conjunto de ferramentas da Solana. A versão mais recente — 2.2.15 no momento em que este artigo foi escrito — pode ser instalada com o seguinte comando no macOS e no Linux:

Código
sh -c "$(curl -sSfL <https://release.anza.xyz/v2.2.14/install>)"

Usuários do Windows podem instalar o conjunto de ferramentas da Solana com o seguinte comando:

Código
cmd /c "curl <https://release.anza.xyz/v2.2.14/agave-install-init-x86_64-pc-windows-msvc.exe> --output C:\\agave-install-tmp\\agave-install-init.exe --create-dirs"

No entanto, recomendamos fortemente usar o Windows Subsystem for Linux (WSL). Ele permite executar um ambiente Linux em sua máquina Windows sem dual boot nem uma máquina virtual separada. Se escolher essa opção, siga as instruções de instalação para Linux, ou seja, o comando curl.

Os desenvolvedores podem substituir v2.2.15 pela tag da versão desejada para download ou usar os nomes de canal stable, beta ou edge. 

Após a instalação, execute solana –-version para confirmar que a versão desejada de solana está instalada.

Instalar o Steel

Podemos instalar Steel com Cargo executando:

Código
cargo install steel-cli

Criar um projeto Steel

Criar um projeto Steel é tão simples quanto executar:

Código
// creates a new Steel project named `create-token`
steel new token

// enter directory
cd create-token

Nosso diretório token deve ficar assim:

Código
Cargo.toml (workspace)
⌙ api
  ⌙ Cargo.toml
  ⌙ src
    ⌙ consts.rs
    ⌙ error.rs
    ⌙ instruction.rs
    ⌙ lib.rs
    ⌙ sdk.rs
    ⌙ state
      ⌙ mod.rs
      ⌙ account_1.rs
      ⌙ account_2.rs
⌙ program
  ⌙ Cargo.toml
  ⌙ src
    ⌙ lib.rs
    ⌙ instruction_1.rs
    ⌙ instruction_2.rs

A estrutura padrão de um projeto Steel contém duas pastas chamadas api e program.

api contém tipos como state e errors, que usaremos ao implementar nosso programa Solana. Já a pasta program contém a lógica do programa. 

Ao desenvolver programas com Steel, é melhor começar pela pasta api, pois a pasta program depende dela.

Remover os módulos state, const e error

Na pasta api, há alguns módulos que não usaremos no projeto create-token, como state, const e error. Portanto, vamos removê-los. 

Podemos remover os módulos do Steel executando os seguintes comandos:

Código
# you should be at the root of the `create-token` project

# enter the api/src directory
cd api/src

# delete the modules we don't need 
rm -rf state [consts.rs](<http://consts.rs/>) [error.rs](<http://error.rs/>)

Depois de excluir os módulos, precisamos atualizar o arquivo api/src/lib.rs, pois ele chama esses módulos.

Atualize api/src/lib.rs para que fique assim:

Código
pub mod instruction;
pub mod sdk;

pub mod prelude {
    pub use crate::instruction::*;
    pub use crate::sdk::*;
}

use steel::*;

// TODO Set program id
declare_id!("z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35");

Definir instruções no Steel

No Steel, as instruções são definidas em api/src/instructions.rs. Todas as instruções de um programa Steel são definidas em um enum, e cada instrução é uma struct.

O enum que contém todas as instruções fica assim:

Código
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)]
pub enum CreateTokenInstruction {
    Initialize = 0,
    Add = 1
}
While each instruction typically looks like this:
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct Initialize {}

#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct Add {
    pub amount: [u8; 8]
}

Quando uma instrução não exige argumentos, como Initialize, ela não tem campos. 

As instruções que exigem dados usam uma representação em bytes. Por exemplo, Add::amount is [u8; 8], que corresponde a um u64.

Depois de definir o enum e a struct das instruções, precisamos passá-los à macro instruction!. O primeiro argumento é o enum das instruções, e o segundo é a struct da instrução:

Código
instruction!(CreateTokenInstruction, Initialize);
instruction!(CreateTokenInstruction, Add);

Nosso programa create-token tem uma instrução que recebe quatro argumentos. Portanto, api/src/instructions deve ficar assim:

Código
use steel::*;

#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)]
pub enum CreateTokenInstruction {
    Create = 0,
}

#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct Create {
    pub name: [u8; 32],
    pub symbol: [u8; 8],
    pub uri: [u8; 128],
    pub decimals: u8,
}

instruction!(CreateTokenInstruction, Create);

Em Create, os campos name, symbol e uri são strings representadas como arrays de bytes de tamanho fixo:

  • name: [u8; 16] — para nomes de até 16 bytes
  • symbol: [u8; 8] — símbolos geralmente são curtos
  • uri: [u8; 128] — URIs costumam ser mais longos

Esses tamanhos dependem do comprimento máximo esperado em bytes, não em caracteres. Por exemplo, caracteres UTF-8 multibyte podem exigir mais espaço.

decimals é apenas um u8, pois a quantidade de casas decimais do token cabe em um byte.

Atualizar o SDK

Em api/src, há um arquivo chamado sdk.rs. Não o usamos ao implementar a lógica do programa, mas ele será usado para executar testes ou código cliente em Rust. Esse arquivo contém funções que criam individualmente todas as instruções de um programa Steel. Como este programa tem apenas uma instrução, precisamos de apenas uma função do SDK. Portanto, api/src/sdk.rs deve ficar assim:

Código
use steel::*;

use crate::prelude::*;

pub fn create(
    user: Pubkey,
    mint: Pubkey,
    name: [u8; 32],
    symbol: [u8; 8],
    uri: [u8; 128],
    decimals: u8,
) -> Instruction {
    let metadata = Pubkey::find_program_address(
        &[
            "metadata".as_bytes(),
            mpl_token_metadata::ID.as_ref(),
            mint.as_ref(),
        ],
        &mpl_token_metadata::ID,
    )
    .0;

    Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(user, true),
            AccountMeta::new(mint, true),
            AccountMeta::new(metadata, false),
            AccountMeta::new_readonly(spl_token::ID, false),
            AccountMeta::new_readonly(mpl_token_metadata::ID, false),
            AccountMeta::new_readonly(system_program::ID, false),
            AccountMeta::new_readonly(sysvar::rent::ID, false),
        ],
        data: Create {
            name,
            symbol,
            uri,
            decimals,
        }
        .to_bytes(),
    }
}

Temos uma função chamada create que recebe cinco argumentos: user é a chave pública da conta que chamará essa instrução; mint é a chave pública da conta que representará token mint; e name, symbol, uri e decimals são os dados usados na implementação da lógica do programa definida em api/src/instructions::Create.

Precisamos armazenar os metadados do nosso token e usaremos o programa Metaplex Metadata para isso. Primeiro, adicionaremos:

Código
let metadata = Pubkey::find_program_address(
        &[
            "metadata".as_bytes(),
            mpl_token_metadata::ID.as_ref(),
            mint.as_ref(),
        ],
        &mpl_token_metadata::ID,
    )
    .0;

Neste bloco de código, buscamos o Program Derived Address (PDA) em que armazenaremos os metadados do token. Para derivar o endereço necessário, usaremos as seguintes seeds:

  • A string “metadata” como bytes (ou seja, "metadata".as_bytes())
  • O ID do programa de metadados como slice (ou seja, mpl_token_metadata::ID.as_ref())
  • A chave pública do mint como slice (ou seja, mint.as_ref())

Todas essas entradas formam as seeds. Para o segundo argumento de Pubkey::find_program::address, precisamos apenas do ID do programa Metadata.

No bloco de código final, retornamos o tipo Instruction que representa essa instrução.

O tipo Instruction fica assim:

Código
 Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(user, true),
            AccountMeta::new(mint, true),
            AccountMeta::new(metadata, false),
            AccountMeta::new_readonly(spl_token::ID, false),
            AccountMeta::new_readonly(mpl_token_metadata::ID, false),
            AccountMeta::new_readonly(system_program::ID, false),
            AccountMeta::new_readonly(sysvar::rent::ID, false),
        ],
        data: Create {
            name,
            symbol,
            uri,
            decimals,
        }
        .to_bytes(),
    }

 O tipo Instruction é uma struct com três campos:

  • program_id 
  • accounts
  • data

Neste bloco, declaramos uma instância de Instruction adequada à instrução do nosso programa.

Para obter program_id de api/src/lib.rs, use:

Código
program_id: crate::ID 

O campo accounts é um vetor de metadados de contas, ou seja, Vec<AccountMeta>. Portanto, precisamos declarar todas as contas usadas nessa instrução:

Código
accounts: vec![
            AccountMeta::new(user, true),
            AccountMeta::new(mint, true),
            AccountMeta::new(metadata, false),
            AccountMeta::new_readonly(spl_token::ID, false),
            AccountMeta::new_readonly(mpl_token_metadata::ID, false),
            AccountMeta::new_readonly(system_program::ID, false),
            AccountMeta::new_readonly(sysvar::rent::ID, false),
        ],

Por fim, o campo data representa, como bytes, os argumentos que usaremos nessas instruções:

Código
data: Create {
            name,
            symbol,
            uri,
            decimals,
        }
        .to_bytes(),

Com isso, terminamos a pasta api.

Agora, vamos adicionar as dependências necessárias e seguir para a pasta program.

Adicionar dependências do Steel

Neste momento, se você executar steel build para compilar o programa, ele deverá falhar com estes erros:

Código
error[E0433]: failed to resolve: use of undeclared crate or module `mpl_token_metadata`
  --> api/src/sdk.rs:16:13
   |
16 |             mpl_token_metadata::ID.as_ref(),
   |             ^^^^^^^^^^^^^^^^^^ use of undeclared crate or module `mpl_token_metadata`

error[E0433]: failed to resolve: use of undeclared crate or module `mpl_token_metadata`
  --> api/src/sdk.rs:19:10
   |
19 |         &mpl_token_metadata::ID,
   |          ^^^^^^^^^^^^^^^^^^ use of undeclared crate or module `mpl_token_metadata`

error[E0433]: failed to resolve: use of undeclared crate or module `spl_token`
  --> api/src/sdk.rs:29:39
   |
29 |             AccountMeta::new_readonly(spl_token::ID, false),
   |                                       ^^^^^^^^^ use of undeclared crate or module `spl_token`

error[E0433]: failed to resolve: use of undeclared crate or module `mpl_token_metadata`
  --> api/src/sdk.rs:30:39
   |
30 |             AccountMeta::new_readonly(mpl_token_metadata::ID, false),
   |                                       ^^^^^^^^^^^^^^^^^^ use of undeclared crate or module `mpl_token_metadata`

Isso indica que faltam as crates spl_token e mpl_token_metadata, necessárias para o programa.

Para adicionar as crates ausentes, inclua isto no arquivo /Cargo.toml:

Código
// /Cargo.toml

[workspace.dependencies]
...
...
mpl-token-metadata = "5.1.0"
spl-token = { version = "8.0.0", features = ["no-entrypoint"] }
In /api/Cargo.toml add: 
// /api/Cargo.toml

[dependencies]
...
...
mpl-token-metadata.workspace = true
spl-token.workspace = true

Em /api/Cargo.toml, adicione: 

Código
// /api/Cargo.toml

[dependencies]
...
...
mpl-token-metadata.workspace = true
spl-token.workspace = true

Agora, se executarmos steel build, os erros de dependência deverão desaparecer.

No entanto, como excluímos da pasta api códigos dos quais a pasta program depende, ainda veremos alguns erros como estes:

Código
error[E0599]: no variant or associated item named `Initialize` found for enum `create_token_api::instruction::CreateTokenInstruction` in the current scope
  --> program/src/lib.rs:18:33
   |
18 |         CreateTokenInstruction::Initialize => process_initialize(accounts, data)?,
   |                                 ^^^^^^^^^^ variant or associated item not found in `CreateTokenInstruction`

error[E0599]: no variant or associated item named `Add` found for enum `create_token_api::instruction::CreateTokenInstruction` in the current scope
  --> program/src/lib.rs:19:33
   |
19 |         CreateTokenInstruction::Add => process_add(accounts, data)?,
   |                                 ^^^ variant or associated item not found in `CreateTokenInstruction`

Não se preocupe. Corrigiremos esses erros na próxima seção.

Implementar a lógica do programa com Steel

Por padrão, os projetos Steel têm duas pastas: api e program. Acabamos de definir na pasta api os tipos necessários para o programa. Agora, precisamos implementar a lógica do programa na pasta program.

Para começar, atualize /program/lib.rs com:

Código
mod create;

use create::*;

use create_token_api::prelude::*;
use steel::*;

pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    data: &[u8],
) -> ProgramResult {
    let (ix, data) = parse_instruction(&create_token_api::ID, program_id, data)?;

    match ix {
        CreateTokenInstruction::Create => process_create(accounts, data)?,
    }

    Ok(())
}

entrypoint!(process_instruction);

Nesse arquivo, definimos nossa função principal process_instruction, que passamos à macro entrypoint!. A macro gera o código boilerplate necessário para que o runtime da Solana chame a lógica do programa.

Dentro da função process_instruction, há dois blocos de código importantes que precisamos abordar.

Código
 let (ix, data) = parse_instruction(&create_token_api::ID, program_id, data)?;

parse_instruction analisa uma instrução com base nos dados da instrução. Isso significa que podemos determinar qual instrução invocar a partir dos dados passados ao programa. 

Ele retorna uma tupla de instruction(ix) e instruction data(data) em um caso Ok().

Código
match ix {
        CreateTokenInstruction::Create => process_create(accounts, data)?,
    }

Depois de obter instruction(ix) de parse_instruction, usamos match para selecionar a instrução a ser invocada. Há apenas um braço de correspondência porque nosso programa tem apenas uma instrução. 

Agora, configuramos a lógica do programa para chamar a instrução correta quando for invocado. No entanto, process_create e o mod create ainda não existem. Vamos criá-los.

Em um terminal, execute:

Código
// you should be at the root of your project 

 // enter the program/src directory
 cd program/src
 
 // delete add.rs and initialize.rs
 rm -rf add.rs initialize.rs
 
 // create create.rs
 touch create.rs 

Agora, atualize program/src/create.rs com:

Código
use create_token_api::prelude::*;
use solana_program::{msg, program_pack::Pack};
use steel::*;

pub fn process_create(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult {
    // Load accounts.
    let [user_info, mint_info, metadata_info, token_program, token_metadata_program, system_program, rent_sysvar] =
        accounts
    else {
        return Err(ProgramError::NotEnoughAccountKeys);
    };

    // validate
    user_info.is_signer()?;
    mint_info.is_empty()?.is_signer()?;
    metadata_info.is_empty()?.is_writable()?;
    token_program.is_program(&spl_token::ID)?;
    token_metadata_program.is_program(&mpl_token_metadata::ID)?;
    system_program.is_program(&system_program::ID)?;
    rent_sysvar.is_sysvar(&sysvar::rent::ID)?;

    // create mint account
    create_account(
        user_info,
        mint_info,
        system_program,
        spl_token::state::Mint::LEN,
        &token_program.key,
    )?;

    msg!("create account");

    let args = Create::try_from_bytes(data)?;
    let name = bytes_to_string::<32>(&args.name)?;
    let symbol = bytes_to_string::<8>(&args.symbol)?;
    let uri = bytes_to_string::<128>(&args.uri)?;
    let decimals = args.decimals;
   
    // initialize mint
    initialize_mint(
        mint_info,
        user_info,
        Some(user_info),
        token_program,
        rent_sysvar,
        decimals,
    )?;

    msg!("initialize mint");

    // create metadata account
    mpl_token_metadata::instructions::CreateMetadataAccountV3Cpi {
        __program: token_metadata_program,
        metadata: metadata_info,
        mint: mint_info,
        mint_authority: user_info,
        payer: user_info,
        update_authority: (user_info, true),
        system_program,
        rent: Some(rent_sysvar),
        __args: mpl_token_metadata::instructions::CreateMetadataAccountV3InstructionArgs {
            data: mpl_token_metadata::types::DataV2 {
                name,
                symbol,
                uri,
                seller_fee_basis_points: 0,
                creators: None,
                collection: None,
                uses: None,
            },
            is_mutable: true,
            collection_details: None,
        },
    }
    .invoke()?;
    msg!("metadata account created");

    Ok(())
}

Vamos entender o que acontece aqui.

Código
// Load accounts.
    let [user_info, mint_info, metadata_info, token_program, token_metadata_program, system_program, rent_sysvar] =
        accounts
    else {
        return Err(ProgramError::NotEnoughAccountKeys);
    };

Neste bloco de código, carregamos as contas necessárias para a instrução. Se as contas fornecidas não corresponderem às contas definidas, esse bloco lançará o erro ProgramError::NotEnoughAccountKeys.

Se observar com atenção, você perceberá um padrão nos nomes das contas:

  • Contas “comuns” terminam com info
  • Contas de programas terminam com program
  • Sysvars terminam com sysvar

Essa é uma convenção opinativa do Steel para nomear contas. Você pode optar por outra, pois ela não afeta de fato o programa.

Em seguida, este bloco de código valida nossas contas:

Código
// validate
user_info.is_signer()?; // user is a signer
mint_info.is_empty()?.is_signer()?; // mint is empty and is a signer
metadata_info.is_empty()?.is_writable()?; // metadata is empty and is writable
token_program.is_program(&spl_token::ID)?; // token program == spl_token::ID
token_metadata_program.is_program(&mpl_token_metadata::ID)?; // token meatadata == mpl_token_metadata::ID
system_program.is_program(&system_program::ID)?; // system program == system_program::ID
rent_sysvar.is_sysvar(&sysvar::rent::ID)?; // rent sysvar == sysvar::rent::ID

Em seguida, criamos a conta mint usando o auxiliar create_account:

Código
// create mint account
    create_account(
        user_info,
        mint_info,
        system_program,
        spl_token::state::Mint::LEN,
        &token_program.key,
    )?;

Depois de criar as contas mint, desserializamos os dados da instrução, convertendo bytes em tipos Rust:

Código
    let args = Create::try_from_bytes(data)?;
    let name = bytes_to_string::<32>(&args.name)?;
    let symbol = bytes_to_string::<8>(&args.symbol)?;
    let uri = bytes_to_string::<128>(&args.uri)?;
    let decimals = args.decimals;

A primeira linha converte os dados da instrução, que são do tipo &[u8], em api/instructions.rs/Create. As três linhas seguintes convertem os campos de Create, que são bytes, em strings com o auxiliar bytes_to_string.

Observe também que bytes_to_string recebe um parâmetro genérico const, ou seja, ::<32>, que ajuda a gerar a string com o tamanho exato para economizar unidades computacionais.

Código
// initialize mint
    initialize_mint(
        mint_info,
        user_info,
        Some(user_info),
        token_program,
        rent_sysvar,
        decimals,
    )?;

Em seguida, inicializamos a conta mint com a função auxiliar initialize_mint.

Código
// create metadata account
    mpl_token_metadata::instructions::CreateMetadataAccountV3Cpi {
        __program: token_metadata_program,
        metadata: metadata_info,
        mint: mint_info,
        mint_authority: user_info,
        payer: user_info,
        update_authority: (user_info, true),
        system_program,
        rent: Some(rent_sysvar),
        __args: mpl_token_metadata::instructions::CreateMetadataAccountV3InstructionArgs {
            data: mpl_token_metadata::types::DataV2 {
                name,
                symbol,
                uri,
                seller_fee_basis_points: 0,
                creators: None,
                collection: None,
                uses: None,
            },
            is_mutable: true,
            collection_details: None,
        },
    }
    .invoke()?;

Aqui, criamos a conta metadata para o mint do token. Ela contém informações como nome, símbolo e criadores da coleção.

Agora que terminamos o arquivo create.rs, vamos executar steel build. 

Devemos ver os seguintes erros:

Código
error[E0433]: failed to resolve: use of undeclared crate or module `spl_token`
  --> program/src/create.rs:28:9
   |
28 |         spl_token::state::Mint::LEN,
   |         ^^^^^^^^^ use of undeclared crate or module `spl_token`

error[E0433]: failed to resolve: use of undeclared crate or module `mpl_token_metadata`
  --> program/src/create.rs:69:5
   |
69 |     mpl_token_metadata::instructions::CreateMetadataAccountV3Cpi {
   |     ^^^^^^^^^^^^^^^^^^ use of undeclared crate or module `mpl_token_metadata`

error[E0433]: failed to resolve: use of undeclared crate or module `mpl_token_metadata`
  --> program/src/create.rs:78:17
   |
78 |         __args: mpl_token_metadata::instructions::CreateMetadataAccountV3Instru...
   |                 ^^^^^^^^^^^^^^^^^^ use of undeclared crate or module `mpl_token_metadata`

error[E0433]: failed to resolve: use of undeclared crate or module `mpl_token_metadata`
  --> program/src/create.rs:79:19
   |
79 |             data: mpl_token_metadata::types::DataV2 {
   |                   ^^^^^^^^^^^^^^^^^^ use of undeclared crate or module `mpl_token_metadata`

Esses erros indicam problemas com dependências. Podemos corrigi-los editando o arquivo /program/Cargo.toml com:

Código
[dependencies]
...
...
mpl-token-metadata.workspace = true
spl-token.workspace = true

Agora, se executarmos steel build novamente, encontraremos um último erro:

Código
error[E0425]: cannot find function `initialize_mint` in this scope
  --> program/src/create.rs:57:5
   |
57 |     initialize_mint(
   |     ^^^^^^^^^^^^^^^ not found in this scope

Isso acontece porque, para acessar a função auxiliar initialize_mint, precisamos da feature spl no Steel. Portanto, precisamos atualizar sua importação no arquivo /Cargo.toml:

Código
[workspace.dependencies]
...
...
steel = { version = "3.0", features = ["spl"] }

Agora, se executarmos steel build, o programa deverá compilar sem erros. 

Parabéns por chegar até aqui!

Só falta uma etapa: precisamos testar nosso programa.

Testar seu programa Steel

Por padrão, os testes no Steel são escritos em Rust. Steel usa solana-program-test para testes, mas você pode usar liteSVM ou mollusk, se preferir.

Os testes são escritos em /program/tests/test.rs.

Vamos começar atualizando-o com:

Código
use create_token_api::prelude::*;
use solana_program::hash::Hash;
use solana_program_test::{processor, BanksClient, ProgramTest};
use solana_sdk::{
    program_pack::Pack, signature::Keypair, signer::Signer, transaction::Transaction,
};
use steel::*;

async fn setup() -> (BanksClient, Keypair, Hash) {
    let mut program_test = ProgramTest::new(
        "create_token_program",
        create_token_api::ID,
        processor!(create_token_program::process_instruction),
    );

    program_test.add_program("token_metadata", mpl_token_metadata::ID, None);

    program_test.prefer_bpf(true);
    program_test.start().await
}

#[tokio::test]
async fn run_test() {
    // Setup test
    let (mut banks, payer, blockhash) = setup().await;
    let mint_keypair = Keypair::new();

    let name = string_to_bytes::<32>("ANATOLY").unwrap();
    let symbol = string_to_bytes::<8>("MERT").unwrap();
    let uri = string_to_bytes::<128>("blah blah blah").unwrap();
    let decimals = 9;

    // Submit create transaction.
    let ix = create(
        payer.pubkey(),
        mint_keypair.pubkey(),
        name,
        symbol,
        uri,
        decimals,
    );
    let tx = Transaction::new_signed_with_payer(
        &[ix],
        Some(&payer.pubkey()),
        &[&payer, &mint_keypair],
        blockhash,
    );
    let res = banks.process_transaction(tx).await;
    assert!(res.is_ok());

    let serialized_mint_data = banks
        .get_account(mint_keypair.pubkey())
        .await
        .unwrap()
        .unwrap()
        .data;

    let mint_data = spl_token::state::Mint::unpack(&serialized_mint_data).unwrap();
    assert!(mint_data.is_initialized);
    assert_eq!(mint_data.mint_authority.unwrap(), payer.pubkey());
    assert_eq!(mint_data.decimals, decimals);
}

Nosso arquivo de teste contém duas funções: setup e run_test.

Fazemos três coisas importantes na função setup:

  1. Criamos uma instância de ProgramTest, que já inclui por padrão nosso programa create_token_program
  2. Adicionamos o programa token_metadata à instância de ProgramTest, pois o programa de tokens da Metaplex que usamos não faz parte de ProgramTest por padrão
  3. Iniciamos uma instância de ProgramTest com o método start, que retorna uma tupla de (BanksClient, Keypair, Hash)
Código
async fn setup() -> (BanksClient, Keypair, Hash) {
    let mut program_test = ProgramTest::new(
        "create_token_program",
        create_token_api::ID,
        processor!(create_token_program::process_instruction),
    );

    program_test.add_program("token_metadata", mpl_token_metadata::ID, None);

    program_test.prefer_bpf(true);
    program_test.start().await
}

Na primeira parte de run_test, chamamos a função setup e criamos um Keypair para o mint do token.

Código
// Setup test
 let (mut banks, payer, blockhash) = setup().await;
 let mint_keypair = Keypair::new();

Em seguida, preparamos os dados da instrução.

Como nossa instrução create espera representações em bytes, usamos o auxiliar string_to_bytes para converter as strings em bytes.

Código
   let name = string_to_bytes::<32>("ANATOLY").unwrap();
    let symbol = string_to_bytes::<8>("MERT").unwrap();
    let uri = string_to_bytes::<128>("blah blah blah").unwrap();
    let decimals = 9;

Lembra que, na pasta api, implementamos uma função que não usamos na lógica do programa, a função create em api/src/sdk.rs?

É essa mesma função que chamamos primeiro no bloco de código abaixo para criar uma instância de Instruction. Nós a passamos para nossa instância Transaction com Transaction::new_signed_with_payer e, depois, enviamos a transação a banks.process_transaction(tx).await; para processamento.

assert!(res.is_ok()); confirma que a transação foi processada.

Código
// Submit create transaction.
    let ix = create(
        payer.pubkey(),
        mint_keypair.pubkey(),
        name,
        symbol,
        uri,
        decimals,
    );
    
    let tx = Transaction::new_signed_with_payer(
        &[ix],
        Some(&payer.pubkey()),
        &[&payer, &mint_keypair],
        blockhash,
    );
    
    let res = banks.process_transaction(tx).await;
    assert!(res.is_ok());

Até aqui, executamos nossa instrução no ambiente de teste (ProgramTest).

Agora, vamos testar se ela foi executada corretamente:

Código
// get serialized data of mint account 
let serialized_mint_data = banks
        .get_account(mint_keypair.pubkey())
        .await
        .unwrap()
        .unwrap()
        .data;

// unpack the mint account data to get the SPL Mint information
let mint_data = spl_token::state::Mint::unpack(&serialized_mint_data).unwrap();

// check if the mint account was initilized 
assert!(mint_data.is_initialized);

// check if the mint authority matches the one we set
assert_eq!(mint_data.mint_authority.unwrap(), payer.pubkey());

// check if the decimals match
assert_eq!(mint_data.decimals, decimals);

Poderíamos escrever mais asserções para verificar outros elementos, como os dados armazenados na conta metadata, mas vamos parar aqui para manter a simplicidade.

Se quiser, você pode adicioná-las. Caso precise de ajuda, consulte este guia para desenvolvedores Solana sobre testes no Steel.

Agora que terminamos nosso arquivo de teste, vamos executar o comando de teste: steel test. 

Infelizmente, ele falhará porque não temos o código-fonte/arquivo ELF do programa mpl_token_metadata.

Não se preocupe. Podemos corrigir isso executando:

Código
// you have to be at the root of your project 

// create a folder called fixtures in program/tests 
// ProgramTest is going to check this folder for the ELF file for token metadata
mkdir program/tests/fixtures

// dump the ELF file for the Metaplex metadata program in the fixtures folder
solana program dump metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s program/tests/fixtures/token_metadata.so

Agora, se executarmos steel test, deveremos obter o seguinte:

Código
running 1 test
[2025-06-08T14:46:12.240628000Z INFO  solana_program_test] "create_token_program" SBF program from /Users/perelyn/helius/create-token/target/deploy/create_token_program.so, modified 3 seconds, 112 ms, 833 µs and 660 ns ago
[2025-06-08T14:46:12.242336000Z INFO  solana_program_test] "token_metadata" SBF program from tests/fixtures/token_metadata.so, modified 1 minute, 49 seconds, 247 ms, 367 µs and 400 ns ago
[2025-06-08T14:46:12.381492000Z DEBUG solana_runtime::message_processor::stable_log] Program z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35 invoke [1]
[2025-06-08T14:46:12.382734000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [2]
[2025-06-08T14:46:12.383272000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success
[2025-06-08T14:46:12.383298000Z DEBUG solana_runtime::message_processor::stable_log] Program log: create account
[2025-06-08T14:46:12.383562000Z DEBUG solana_runtime::message_processor::stable_log] Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]
[2025-06-08T14:46:12.383783000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Instruction: InitializeMint
[2025-06-08T14:46:12.386049000Z DEBUG solana_runtime::message_processor::stable_log] Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 2968 of 192320 compute units
[2025-06-08T14:46:12.386068000Z DEBUG solana_runtime::message_processor::stable_log] Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success
[2025-06-08T14:46:12.386099000Z DEBUG solana_runtime::message_processor::stable_log] Program log: initialize mint
[2025-06-08T14:46:12.386409000Z DEBUG solana_runtime::message_processor::stable_log] Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s invoke [2]
[2025-06-08T14:46:12.387342000Z DEBUG solana_runtime::message_processor::stable_log] Program log: IX: Create Metadata Accounts v3
[2025-06-08T14:46:12.387576000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [3]
[2025-06-08T14:46:12.387588000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success
[2025-06-08T14:46:12.387999000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Allocate space for the account
[2025-06-08T14:46:12.388226000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [3]
[2025-06-08T14:46:12.388264000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success
[2025-06-08T14:46:12.388306000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Assign the account to the owning program
[2025-06-08T14:46:12.388851000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [3]
[2025-06-08T14:46:12.388873000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success
[2025-06-08T14:46:12.392769000Z DEBUG solana_runtime::message_processor::stable_log] Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s consumed 37330 of 185782 compute units
[2025-06-08T14:46:12.392790000Z DEBUG solana_runtime::message_processor::stable_log] Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s success
[2025-06-08T14:46:12.392842000Z DEBUG solana_runtime::message_processor::stable_log] Program log: metadata account created
[2025-06-08T14:46:12.395012000Z DEBUG solana_runtime::message_processor::stable_log] Program z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35 consumed 51973 of 200000 compute units
[2025-06-08T14:46:12.395031000Z DEBUG solana_runtime::message_processor::stable_log] Program z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35 success
test run_test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.16s

Parabéns novamente!

Se você obteve a mesma saída, seu programa passou nos testes.

Conclusão

Steel é um framework de desenvolvimento modular e leve para criar programas Solana inteligentes e otimizados para desempenho. Este artigo explicou como Steel funciona, comparou Steel com Anchor e Pinocchio e apresentou um exemplo de como criar um novo token usando Steel.

Recursos adicionais

Para continuar aprendendo sobre Steel e desenvolvimento de programas Solana, explore estes recursos:

Assine a Helius

Acompanhe as novidades mais recentes do desenvolvimento Solana e receba atualizações quando publicarmos