NUEVO: Helius adquiere Light Protocol
el framework Steel para escribir contratos inteligentes de Solana
Blog/Desarrollo

Cómo escribir programas de Solana con Steel

Desarrollador de Solana, Assylm LabsPerelyn en XPerelyn en LinkedIn
15 min de lectura
Tabla de contenido

Steel es un framework ligero y modular para escribir programas nativos de Solana con un mínimo de código repetitivo y máximo control. Creado por Hardhat Chad (de Ore), Steel está diseñado para desarrolladores que buscan el rendimiento de Rust nativo sin sacrificar la experiencia de desarrollo.

En este artículo, aprenderás:

  • Qué es Steel y cómo se relaciona con Anchor y Pinocchio
  • Cómo definir instrucciones y estructurar un proyecto de Steel
  • Cómo crear un token SPL personalizado con Steel
  • Cómo probar tu programa con solana-program-test

Requisitos previos

Esta guía supone que conoces:

  • La sintaxis y las herramientas básicas de Rust
  • Los fundamentos del desarrollo en Solana (cuentas, instrucciones y programas)
  • El uso de CLI (por ejemplo, cargo, solana y curl)

Si puedes escribir programas básicos de Solana o Rust, ya puedes empezar a desarrollar con Steel.

¿Qué es Steel?

Steel es un nuevo framework modular para crear programas en Solana. Permite escribir programas con menos código repetitivo y es menos restrictivo que Anchor.

Steel ofrece macros y funciones auxiliares de invocación entre programas (CPI) que aceleran el desarrollo de programas de Solana de una forma similar al desarrollo nativo, sin un framework. Así obtienes un rendimiento similar al nativo con una mejor experiencia de desarrollo.

Veamos algunas de las macros y funciones auxiliares que ofrece Steel.

Macros de Steel

Algunas de las macros que ofrece Steel son:

account!

La macro account! define tipos Account en Steel y también les da acceso al trait AccountValidation, que proporciona funciones auxiliares para validar el estado de las cuentas durante el desarrollo.

instruction!

La macro instruction! define tipos Instruction en Steel y también les da acceso a una función to_bytes, que se usará en api/src/sdk.

Otras macros de Steel incluyen error y event; como indican sus nombres, se usan para errores y eventos, respectivamente.

Funciones auxiliares de CPI de Steel

Steel proporciona las funciones auxiliares que la mayoría de los desarrolladores necesitan para realizar invocaciones entre programas (CPI), como las instrucciones de system_program, entre ellas create_account, transfer y más. 

También incluye instrucciones de spl_token_program / spl_associated_token_program, como mint_to, burn, create_associated_token_account y más.

Optimizaciones de CU

Podrías esperar que Steel sea eficiente en CU por lo que hace, pero en realidad lo es por lo que no hace. Como el framework Steel es ligero y añade muy poca o ninguna sobrecarga a los programas de Solana, ofrece una eficiencia comparable a la de los programas escritos en Rust nativo. Incluso puede superarla gracias al uso de bytemuck como serializador de datos predeterminado.

Steel frente a Anchor

Anchor es un framework potente y con convenciones definidas que permite crear rápidamente programas seguros de Solana. Agiliza el desarrollo al reducir el código repetitivo en áreas como la serialización y deserialización de cuentas y los datos de instrucciones. También realiza comprobaciones de seguridad esenciales, genera bibliotecas cliente automáticamente y proporciona un entorno de pruebas completo.

¿Cuál es la principal diferencia entre Steel y Anchor?

Anchor es un framework de contratos inteligentes accesible para principiantes que permite a los desarrolladores de Solana de cualquier nivel escribir programas rápidamente. Se centra en ofrecer una experiencia de desarrollo intuitiva y sencilla, por eso tantos desarrolladores de Solana lo usan.

Sin embargo, esta simplicidad tiene un costo.

Anchor ha acumulado sobrecarga que aumenta el tamaño de los binarios de los programas de Solana y afecta negativamente su rendimiento on-chain. Por ejemplo, incrementa el costo de desplegar programas de Solana y llamar instrucciones.

Gracias a la velocidad y eficiencia de Solana, la mayoría de las personas no nota la sobrecarga que Anchor añade a los programas. La excepción son quienes desarrollan programas más complejos, como Ore y Code-vm, donde la sobrecarga los volvería inutilizables on-chain.

Por lo general, estos programas de Solana se crearían con Rust nativo. Sin embargo, sus responsables saben lo difícil que sería y necesitan un framework más accesible, comparable con Anchor, pero con el alto rendimiento de Rust nativo.

Ventajas y desventajas de Steel y Anchor

Aunque añade sobrecarga a los programas de Solana, Anchor ofrece la mejor experiencia de desarrollo del ecosistema y sigue siendo el framework recomendado para quienes empiezan a desarrollar en Solana.

La sintaxis de Anchor es fácil de entender. Además, proporciona lenguajes de definición de interfaces (IDL), que facilitan probar programas de Solana en otros lenguajes, como JavaScript, y desarrollar aplicaciones cliente que se comuniquen con ellos.

Los IDL de Anchor son tan potentes que herramientas como Codama pueden usarlos para generar automáticamente clientes, interfaces de línea de comandos (CLI) y documentación para programas de Solana.

El framework Steel todavía no ofrece IDL. Además, aunque su sintaxis es accesible, requiere que el desarrollador tenga un buen dominio de Rust.

Aunque Anchor se recomienda para nuevos desarrolladores, puede limitar a los más técnicos porque oculta el funcionamiento interno del desarrollo de programas de Solana mediante macros y su sintaxis. 

Steel, en cambio, da acceso a todos los elementos de un programa de Solana en su nivel más básico. Esta granularidad resulta especialmente útil durante las pruebas, ya que estas se escriben en Rust de forma predeterminada y permiten depurar directamente el código.

Steel es un gran framework de contratos inteligentes tanto por lo que es —una capa mínima sobre Rust nativo— como por lo que no es: sintaxis adicional que genera sobrecarga.

En pocas palabras, Steel es una versión más accesible de Rust nativo que conserva su potencia sin comprometer su eficiencia.

Steel frente a Pinocchio

Pinocchio es una biblioteca sin dependencias para crear programas de Solana en Rust. Febo la escribió como proyecto paralelo y después se convirtió en un proyecto completo de Anza. Aprovecha la forma en que los cargadores de SVM serializan los parámetros de entrada de un programa en un arreglo de bytes. Este se pasa al punto de entrada del programa para definir tipos de copia cero que permiten leer la entrada.

En pocas palabras, Pinocchio es una versión más ligera de solana_program que no depende de crates externos y evita usar tipos dinámicos.

Desde el lanzamiento de Pinocchio han surgido muchas ideas equivocadas sobre qué es. La biblioteca Pinocchio está diseñada para reemplazar la biblioteca solana_program; no compite con Anchor ni Steel. Complementa estos frameworks porque los hace más ligeros.

Lo que la mayoría considera un programa de Pinocchio es simplemente código Rust nativo que depende de pinocchio en lugar de solana_program.

Cómo crear un token con Steel

Para demostrar cómo funciona Steel, escribiremos un programa sencillo de Solana que crea un token SPL. Si prefieres aprender de forma visual, puedes ver el siguiente video.

Requisitos previos

  • Rust/Cargo
  • Solana
  • Steel

Instalar Rust

Puedes instalar Rust desde el sitio web oficial de Rust o mediante la CLI:

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

Instalar el conjunto de herramientas de Solana

Steel también requiere el conjunto de herramientas de Solana. Puedes instalar la versión más reciente —2.2.15 al escribir este artículo— con el siguiente comando para macOS y Linux:

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

En Windows, puedes instalar el conjunto de herramientas de Solana con el siguiente 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"

Sin embargo, recomendamos usar Subsistema de Windows para Linux (WSL). Esto te permitirá ejecutar un entorno Linux en tu equipo Windows sin un arranque dual ni una máquina virtual independiente. Si eliges esta opción, sigue las instrucciones de instalación para Linux, es decir, el comando curl.

Puedes reemplazar v2.2.15 por la etiqueta de la versión que quieres descargar o usar los nombres de canal stable, beta o edge. 

Después de instalarlo, ejecuta solana –-version para confirmar que se instaló la versión deseada de solana.

Instalar Steel

Podemos instalar Steel con Cargo mediante:

Código
cargo install steel-cli

Crear un proyecto de Steel

Crear un proyecto de Steel es tan sencillo como ejecutar:

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

// enter directory
cd create-token

Nuestro directorio token debería verse así:

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

La estructura predeterminada de un proyecto de Steel contiene dos carpetas llamadas api y program.

api contiene tipos como state e errors, que usaremos al implementar nuestro programa de Solana. La carpeta program contiene la lógica del programa. 

Al desarrollar programas con Steel, conviene comenzar por la carpeta api, ya que la carpeta program depende de ella.

Eliminar los módulos state, const y error

La carpeta api incluye algunos módulos que no usaremos en nuestro proyecto create-token, como state, const e error. Vamos a eliminarlos. 

Podemos eliminar los módulos de Steel con los siguientes 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/>)

Después de eliminar los módulos, debemos actualizar el archivo api/src/lib.rs porque los invoca.

Actualiza api/src/lib.rs para que se vea así:

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 instrucciones en Steel

En Steel, las instrucciones se definen en api/src/instructions.rs. Todas las instrucciones de un programa de Steel se definen en un enum, y cada instrucción es un struct.

El enum que contiene todas las instrucciones se ve así:

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]
}

Si una instrucción no requiere argumentos, como Initialize, no tiene campos. 

Las instrucciones que requieren datos usan una representación en bytes. Por ejemplo, Add::amount is [u8; 8], que corresponde a un u64.

Después de definir el enum y el struct de nuestras instrucciones, debemos pasarlos a la macro instruction!. El primer argumento es el enum de instrucciones y el segundo, el struct de la instrucción:

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

Nuestro programa create-token tiene una instrucción que recibe cuatro argumentos, por lo que api/src/instructions debería verse así:

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);

En Create, los campos name, symbol e uri son cadenas representadas como arreglos de bytes de tamaño fijo:

  • name: [u8; 16] — para nombres de hasta 16 bytes
  • symbol: [u8; 8] — los símbolos suelen ser cortos
  • uri: [u8; 128] — los URI suelen ser más largos

Estos tamaños dependen de la longitud máxima esperada en bytes, no en caracteres. Por ejemplo, los caracteres UTF-8 multibyte pueden requerir más espacio.

decimals es simplemente un u8, ya que el número de decimales del token cabe en un byte.

Actualizar el SDK

En api/src tenemos un archivo llamado sdk.rs. No lo usamos al implementar la lógica del programa, pero sí para ejecutar pruebas o código cliente de Rust. Contiene funciones que construyen individualmente todas las instrucciones de un programa de Steel. Como este programa solo tiene una instrucción, solo necesitaremos una función del SDK. Por lo tanto, api/src/sdk.rs debería verse así:

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(),
    }
}

Tenemos una función llamada create que recibe cinco argumentos: user es la clave pública de la cuenta que llamará esta instrucción; mint es la clave pública de la cuenta que representará a token mint; e name, symbol, uri e decimals son los datos que usaremos al implementar la lógica definida en api/src/instructions::Create.

Necesitamos almacenar los metadatos de nuestro token y usaremos el programa Metaplex Metadata para hacerlo. Primero, añadiremos:

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;

En este bloque de código intentamos obtener la dirección derivada de programa (PDA) donde almacenaremos los metadatos del token. Para derivar la dirección necesaria, usaremos las siguientes semillas:

  • La cadena “metadata” como bytes, es decir, "metadata".as_bytes()
  • El ID de programa del programa de metadatos como slice, es decir, mpl_token_metadata::ID.as_ref()
  • La clave pública del mint como slice, es decir, mint.as_ref()

En conjunto, todas estas entradas forman las semillas. Para el segundo argumento de Pubkey::find_program::address, solo necesitamos el ID de programa del programa Metadata.

En el bloque de código final, devolvemos el tipo Instruction que representa esta instrucción.

El tipo Instruction se ve así:

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(),
    }

 El tipo Instruction es un struct con tres campos:

  • program_id 
  • accounts
  • data

En este bloque, declaramos una instancia de Instruction que corresponde a la instrucción de nuestro programa.

Para obtener program_id desde api/src/lib.rs, usa:

Código
program_id: crate::ID 

El campo accounts es un vector de metadatos de cuenta, es decir, Vec<AccountMeta>. Por eso debemos declarar todas las cuentas que usará esta instrucción:

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 último, el campo data representa como bytes los argumentos que usaremos para estas instrucciones:

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

Ya terminamos con la carpeta api.

A continuación, añadiremos las dependencias necesarias y pasaremos a la carpeta program.

Añadir dependencias de Steel

En este momento, si ejecutas steel build para compilar el programa, debería fallar con estos errores:

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`

Esto indica que faltan los crates spl_token e mpl_token_metadata, necesarios para nuestro programa.

Para añadir los crates que faltan, agrega esto al archivo /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

En /api/Cargo.toml, añade: 

Código
// /api/Cargo.toml

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

Ahora, si ejecutamos steel build, los errores de dependencias deberían desaparecer.

Sin embargo, como eliminamos código de la carpeta api del que depende la carpeta program, seguiremos viendo errores como este:

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`

No te preocupes, corregiremos estos errores en la siguiente sección.

Implementar la lógica del programa con Steel

Los proyectos de Steel incluyen dos carpetas de forma predeterminada: api e program. Acabamos de definir los tipos que necesita nuestro programa en la carpeta api. Ahora debemos implementar la lógica en la carpeta program.

Para comenzar, actualiza /program/lib.rs con:

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);

En este archivo definimos nuestra función principal process_instruction, que pasamos a la macro entrypoint!. La macro genera el código repetitivo necesario para que el entorno de ejecución de Solana llame la lógica del programa.

Dentro de la función process_instruction hay dos bloques de código importantes que debemos explicar.

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

parse_instruction analiza una instrucción a partir de sus datos. Esto significa que podemos determinar qué instrucción invocar mediante los datos enviados a nuestro programa. 

Devuelve una tupla de instruction(ix) e instruction data(data) en un caso Ok().

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

Una vez que obtenemos instruction(ix) desde parse_instruction, usamos match para seleccionar la instrucción que se invocará. Solo hay un brazo de coincidencia porque nuestro programa tiene una única instrucción. 

Ya configuramos la lógica del programa para llamar la instrucción correcta cuando se invoque. Sin embargo, process_create y el mod create todavía no existen, así que vamos a crearlos.

Ejecuta en una terminal:

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 

Ahora actualiza program/src/create.rs con:

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(())
}

Veamos paso a paso lo que ocurre aquí.

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);
    };

En este bloque de código cargamos las cuentas necesarias para esta instrucción. Si las cuentas proporcionadas no coinciden con las definidas, el bloque generará el error ProgramError::NotEnoughAccountKeys.

Si observas con atención, notarás un patrón en los nombres de las cuentas:

  • Las cuentas “normales” terminan en info
  • Las cuentas de programa terminan en program
  • Las sysvars terminan en sysvar

Esta es una convención de Steel para nombrar cuentas. Puedes usar otra, ya que no tiene un impacto real en el programa.

A continuación, este bloque de código valida nuestras cuentas:

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

Después, creamos la cuenta mint con la función auxiliar create_account:

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

Después de crear las cuentas mint, deserializamos los datos de la instrucción de bytes a tipos de 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;

La primera línea convierte los datos de la instrucción, de tipo &[u8], a api/instructions.rs/Create. Las siguientes tres líneas convierten los campos de Create, que son bytes, en cadenas mediante la función auxiliar bytes_to_string.

Observa también que bytes_to_string recibe un parámetro genérico const, es decir, ::<32>. Este ayuda a generar la cadena con la longitud exacta para ahorrar unidades de cómputo.

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

A continuación, inicializamos la cuenta mint con la función 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()?;

Aquí creamos la cuenta metadata para el mint de nuestro token. Contiene información como el nombre, el símbolo y los creadores de la colección.

Ahora que terminamos con el archivo create.rs, ejecutemos steel build. 

Deberíamos ver los siguientes errores:

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`

Estos mensajes indican que tenemos errores de dependencias. Podemos corregirlos editando el archivo /program/Cargo.toml con:

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

Ahora, si volvemos a ejecutar steel build, encontraremos un último error:

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

Esto ocurre porque la función auxiliar initialize_mint necesita la funcionalidad spl de Steel para poder acceder a ella. Por lo tanto, debemos actualizar su importación en el archivo /Cargo.toml:

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

Ahora, si ejecutamos steel build, nuestro programa debería compilar sin errores. 

¡Felicidades por llegar hasta aquí!

Queda un último paso: debemos probar nuestro programa.

Probar tu programa de Steel

Las pruebas en Steel se escriben en Rust de forma predeterminada. Steel usa solana-program-test para las pruebas, pero puedes usar liteSVM o mollusk si lo prefieres.

Las pruebas se escriben en /program/tests/test.rs.

Comencemos por actualizarlo con:

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);
}

Nuestro archivo de pruebas contiene dos funciones: setup e run_test.

En la función setup hacemos tres cosas importantes:

  1. Creamos una instancia de ProgramTest, que incluye nuestro programa create_token_program de forma predeterminada
  2. Añadimos el programa token_metadata a nuestra instancia de ProgramTest porque el programa de tokens de Metaplex que usamos no forma parte de ProgramTest de forma predeterminada
  3. Iniciamos una instancia de ProgramTest con el método start, que devuelve una 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
}

En la primera parte de run_test, llamamos a la función setup y creamos un Keypair para el mint de nuestro token.

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

A continuación, preparamos los datos de la instrucción.

Como nuestra instrucción create espera representaciones en bytes, usamos la función auxiliar string_to_bytes para convertir nuestras cadenas en 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;

¿Recuerdas que en la carpeta api implementamos una función que no usamos en la lógica del programa? Era la función create de api/src/sdk.rs.

Es la misma función que llamamos primero en el bloque de código siguiente para crear una instancia de Instruction. La pasamos a nuestra instancia Transaction mediante Transaction::new_signed_with_payer y después enviamos nuestra transacción a banks.process_transaction(tx).await; para que se procese.

assert!(res.is_ok()); confirma que la transacción se procesó.

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());

Hasta ahora, ejecutamos nuestra instrucción en el entorno de pruebas (ProgramTest).

Ahora comprobemos que se ejecutó correctamente:

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);

Podríamos escribir más aserciones para comprobar otros aspectos, como los datos almacenados en la cuenta metadata, pero nos detendremos aquí para simplificar.

Si quieres, puedes añadirlas. Si necesitas ayuda, consulta esta guía para desarrolladores de Solana sobre pruebas de Steel.

Ahora que terminamos con el archivo de pruebas, ejecutemos el comando de prueba: steel test. 

Lamentablemente, fallará porque no tenemos el código fuente ni el archivo ELF del programa mpl_token_metadata.

No te preocupes, podemos solucionarlo ejecutando:

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

Ahora, si ejecutamos steel test, deberíamos obtener lo siguiente:

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

¡Felicidades de nuevo!

Si obtienes el mismo resultado, tu programa superó las pruebas.

Conclusión

Steel es un framework de desarrollo modular y ligero para crear programas de Solana inteligentes y optimizados para el rendimiento. Este artículo explicó cómo funciona Steel, lo comparó con Anchor y Pinocchio, y mostró paso a paso cómo crear un token nuevo con Steel.

Recursos adicionales

Para seguir aprendiendo sobre Steel y el desarrollo de programas de Solana, explora estos recursos:

Suscríbete a Helius

Mantente al día con las novedades del desarrollo en Solana y recibe actualizaciones cuando publiquemos