NUEVO: Helius adquiere Light Protocol
Introducción a Anchor
Blog/Desarrollo

Introducción a Anchor: guía para principiantes sobre cómo crear programas de Solana

Developer Experience Engineer0xIchigo en X0xIchigo en LinkedIn0xIchigo en GitHub
46 min de lectura

Muchas gracias a Noah, Mike, Jonas, Ryan, Prames y bl0ckpain por revisar este artículo.

¿De qué trata este artículo?

Rust suele describirse como la lingua franca del desarrollo de programas de Solana. Sin embargo, sería más preciso describir así a Anchor, ya que la mayor parte del desarrollo en Rust utiliza este framework. Anchor es un framework potente y con convenciones definidas, diseñado para crear rápidamente programas seguros de Solana. Agiliza el proceso de desarrollo al reducir el código repetitivo en áreas como la (des)serialización de cuentas y los datos de instrucciones, realizar comprobaciones de seguridad esenciales, generar bibliotecas cliente automáticamente y proporcionar un entorno de pruebas completo.

Este artículo explica cómo desarrollar programas con Anchor. Cubre la instalación de Anchor, el uso de Solana Playground y la creación, compilación y publicación de un programa sencillo de Hello, World! Luego veremos con más detalle cómo Anchor agiliza el proceso de desarrollo mediante el análisis de las IDL, las macros, la estructura de los programas de Anchor, los tipos y las restricciones de las cuentas, y el manejo de errores. También abordaremos brevemente las invocaciones entre programas y las direcciones derivadas de programas. Este artículo te dará todo lo que necesitas saber para empezar hoy mismo con Anchor.

Conocimientos previos

Este artículo asume que conoces el modelo de programación de Solana. Si es la primera vez que desarrollas en Solana, te recomiendo leer mi publicación anterior: El modelo de programación de Solana: introducción al desarrollo en Solana. 

No te preocupes si Rust es nuevo para ti: no necesitas conocimientos avanzados para empezar a desarrollar con Anchor. La documentación de Anchor señala que solo necesitas dominar los fundamentos de Rust, es decir, los primeros nueve capítulos del libro de Rust. Te recomiendo ver La guía de supervivencia de Rust para obtener un buen resumen de los conceptos esenciales de programación en Rust. También es fundamental comprender las reglas de memoria, propiedad y préstamo de Rust.

Para facilitar el aprendizaje, recomiendo que quienes no tengan experiencia con lenguajes de programación de bajo nivel repasen distintos conceptos específicos de la programación de sistemas que los recursos sobre Rust suelen omitir. Por ejemplo, te recomiendo consultar temas como tamaños de variables, punteros y fugas de memoria. También recomiendo Rust con ejemplos y mi repositorio sobre diversas estructuras de datos y algoritmos escritos en Rust para ver ejemplos prácticos de Rust.

¿Prefieres usar TypeScript? Aprende cómo escribir programas de Solana en TypeScript con el framework de Poseidon para transpilar TypeScript a Rust y generar programas válidos de Anchor.

Este artículo se centra única y exclusivamente en el desarrollo con Anchor. No explicaremos cómo desarrollar programas en Rust nativo ni asumiremos que tienes conocimientos sobre ello. Tampoco cubriremos el desarrollo del lado del cliente con Anchor. En un próximo artículo explicaremos cómo probar e interactuar con programas de Anchor mediante TypeScript.

Dicho esto, ¡empecemos con Anchor!

Instalación de Anchor

Configurar Anchor requiere unos pocos pasos sencillos para instalar las herramientas y los paquetes necesarios. Esta sección cubre la instalación de estas herramientas y paquetes: Rust, el conjunto de herramientas de Solana, Yarn y Anchor Version Manager.

Instalación de Rust

Puedes instalar Rust desde el sitio web oficial de Rust o mediante la línea de comandos:

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

Instalación del conjunto de herramientas de Solana

Anchor también requiere el conjunto de herramientas de Solana. Puedes instalar la versión más reciente (1.17.16 al momento de escribir este artículo) con el siguiente comando en macOS y Linux:

Código
sh -c "$(curl -sSfL https://release.solana.com/v1.17.16/install)"

En Windows, puedes instalar el conjunto de herramientas de Solana con el siguiente comando:

Código
cmd /c "curl https://release.solana.com/v1.17.16/solana-install-init-x86_64-pc-windows-msvc.exe --output C:\solana-install-tmp\solana-install-init.exe --create-dirs"

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

También puedes sustituir v1.17.16 por la etiqueta de la versión que quieras descargar. Otra opción es usar los nombres de canal stable, beta o edge. Después de la instalación, ejecuta solana –-version para confirmar que está instalada la versión deseada de solana.

Instalación de Yarn

Anchor también requiere Yarn. Puedes instalarlo con Corepack, incluido en todas las versiones oficiales de Node.js a partir de Node.js 14.9 y 16.9. Sin embargo, durante su fase experimental debes activarlo manualmente. Por eso, debes ejecutar corepack enable antes de usarlo. Algunos distribuidores externos pueden no incluir Corepack de forma predeterminada. En ese caso, quizá debas ejecutar npm install -g corepack antes de corepack enable.

Instalación de Anchor con AVM

La documentación de Anchor recomienda instalar Anchor mediante Anchor Version Manager (AVM). AVM simplifica la administración y selección de varias instalaciones del binario anchor-cli. Esto puede ser necesario para producir compilaciones verificables o trabajar con versiones distintas en diferentes programas. Puedes instalarlo mediante Cargo con el comando cargo install --git [https://github.com/coral-xyz/anchor](https://github.com/coral-xyz/anchor) avm --locked --force. Después, instala y utiliza la versión más reciente:

Código
avm install latest
avm use latest

# Verify the installation
avm --version

Para consultar las versiones disponibles de anchor-cli, usa el comando avm list. Puedes usar avm use <version> para seleccionar una versión específica. Esta versión seguirá en uso hasta que la cambies. Puedes desinstalar una versión específica con el comando avm uninstall <version>.

Instalación de Anchor mediante binarios y compilación desde el código fuente

En Linux, los binarios de Anchor están disponibles mediante el paquete npm @coral-xyz/anchor-cli. Actualmente, solo se admite Linux x86_64. Por eso, en otros sistemas operativos debes compilar desde el código fuente. Puedes usar Cargo para instalar la CLI directamente. Por ejemplo:

Código
cargo install --git https://github.com/coral-xyz/anchor --tag v0.29.0 anchor-cli --locked

Modifica el argumento --tag para instalar otra versión de Anchor. Si la instalación con Cargo falla, quizá debas instalar dependencias adicionales. Por ejemplo, en Ubuntu:

Código
sudo apt-get update && sudo apt-get upgrade && sudo apt-get install -y pkg-config build-essential libudev-dev

Luego puedes verificar la instalación de Anchor con el comando anchor --version.

Solana Playground

Como alternativa, puedes empezar a usar Anchor mediante Solana Playground (Solpg). Solana Playground es un IDE basado en el navegador que facilita el desarrollo, las pruebas y la publicación rápida de programas de Solana. 

La primera vez que uses Solana Playground, debes crear una Playground Wallet. Haz clic en el indicador rojo de estado con la etiqueta No conectado en la esquina inferior izquierda de la pantalla. Aparecerá el siguiente cuadro de diálogo:

Te recomendamos guardar una copia de seguridad del archivo de par de claves de la wallet antes de hacer clic en Continuar. Esto se debe a que Playground Wallet se guarda en el almacenamiento local del navegador. Borrar la caché del navegador eliminará la wallet. 

Haz clic en Continuar para crear una wallet de devnet lista para usar en el IDE.

Para agregar fondos a la wallet, ejecuta el comando solana airdrop <amount> en la terminal de Playground y sustituye <amount> por la cantidad deseada de SOL de devnet. También puedes visitar este faucet para obtener SOL de devnet. Te recomiendo consultar la siguiente guía para obtener SOL de devnet.

Ten en cuenta que podrías encontrar el siguiente error:

Código
Error: unable to confirm transaction. This can happen in situations such as transaction expiration and insufficient fee-payer funds

Esto suele ocurrir porque el faucet de devnet está agotado o porque solicitaste demasiado SOL. El límite actual es de 5 SOL, más que suficiente para publicar este programa. Por eso, te recomendamos solicitar 5 SOL al faucet o ejecutar el comando solana airdrop 5. Solicitar cantidades menores de forma progresiva puede provocar una limitación de solicitudes.

Hello, World!

Los programas Hello, World! se consideran una excelente introducción a nuevos frameworks o lenguajes de programación. Gracias a su sencillez, desarrolladores de cualquier nivel pueden comprenderlos. También muestran la estructura y la sintaxis básicas del nuevo modelo de programación sin introducir lógica ni funciones complejas. Se han convertido en un programa inicial muy común, así que es natural que escribamos uno para Anchor. Esta sección explica cómo compilar y publicar un programa Hello, World! tanto con una configuración local de Anchor como con Solana Playground.

Creación de un proyecto nuevo con una configuración local de Anchor

Crear un proyecto nuevo con Anchor instalado es tan sencillo como ejecutar:

Código
anchor init hello-world
cd hello-world

Estos comandos inicializarán un proyecto nuevo de Anchor llamado hello-world y abrirán su directorio. Dentro de este directorio, ve a hello-world/programs/hello-world/src/lib.rs. Este archivo contiene el siguiente código inicial:

Código
use anchor_lang::prelude::*;

declare_id!("HZfVb1ohL1TejhZNkgFSKqGsyTznYtrwLV6GpA8BwV5Q");

#[program]
pub mod hello-world {
    use super::*;

    pub fn initialize(ctx: Context) -> Result<()> {
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize {}

Anchor preparó varios archivos y directorios. En concreto:

  • Una carpeta app vacía para el cliente del programa
  • Una carpeta programs que contendrá todos nuestros programas de Solana
  • Una carpeta tests para realizar pruebas con JavaScript. Incluye un archivo de prueba generado automáticamente para el código inicial
  • Un archivo de configuración Anchor.toml. Si Rust es nuevo para ti, un archivo TOML es un formato de configuración mínimo y fácil de leer gracias a su semántica. El archivo Anchor.toml configura cómo interactuará Anchor con el programa. Por ejemplo, especifica en qué clúster debe publicarse.

Creación de un proyecto nuevo con Solana Playground

Crear un proyecto nuevo en Solana Playground es muy sencillo. Ve a la esquina superior izquierda y haz clic en Crear un proyecto nuevo:

Aparecerá el siguiente cuadro de diálogo:

Asigna un nombre a tu programa, selecciona Anchor(Rust) y haz clic en Crear. Esto creará un proyecto nuevo de Anchor directamente en tu navegador. En la sección Programa de la izquierda verás un directorio src. Este contiene lib.rs, que incluye el siguiente código inicial:

Código
use anchor_lang::prelude::*;

// This is your program's public key and it will update
// automatically when you build the project.
declare_id!("11111111111111111111111111111111");

#[program]
mod hello_anchor {
    use super::*;
    pub fn initialize(ctx: Context, data: u64) -> Result<()> {
        ctx.accounts.new_account.data = data;
        msg!("Changed data to: {}!", data); // Message will show up in the tx logs
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    // We must specify the space in order to initialize an account.
    // First 8 bytes are default account discriminator,
    // next 8 bytes come from NewAccount.data being type u64.
    // (u64 = 64 bits unsigned integer = 8 bytes)
    #[account(init, payer = signer, space = 8 + 8)]
    pub new_account: Account<'info, NewAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[account]
pub struct NewAccount {
    data: u64
}

Observa que Solana Playground solo genera los archivos client.ts y anchor.test.ts. Te recomiendo leer la sección sobre cómo crear un programa localmente con Anchor para conocer los archivos que suelen generarse en un proyecto nuevo de Anchor.

Escritura de Hello, World!

Sin importar si usas Anchor localmente o mediante Solana Playground, sustituye el código inicial por el siguiente para crear un programa Hello, World! muy sencillo:

Código
use anchor_lang::prelude::*;

declare_id!("HZfVb1ohL1TejhZNkgFSKqGsyTznYtrwLV6GpA8BwV5Q");

#[program]
mod hello_world {
use super::*;

pub fn hello(_ctx: Context<Hello>) -> Result<()> {
	msg!("Hello, World!");
	Ok(())
}

#[derive(Accounts)]
pub struct Hello {}
}

Explicaremos los detalles exactos de cada parte en las siguientes secciones. Por ahora, es importante observar el uso de macros y traits para simplificar el proceso de desarrollo. La macro declare_id! establece la clave pública del programa. Para el desarrollo local, el comando anchor init que configura el programa generará un par de claves en el directorio target/deploy y completará esta macro. Solana Playground también lo hará automáticamente.

En nuestro módulo principal hello_world, creamos una función que registra Hello, World!. También devuelve Ok(()) para indicar que el programa se ejecutó correctamente. Observa que agregamos un guion bajo como prefijo de ctx para evitar advertencias en la consola sobre variables sin usar. Hello es una estructura de cuenta que no requiere pasar ninguna cuenta, ya que el programa solo registra un mensaje nuevo.

¡Eso es todo! No necesitas recibir cuentas ni implementar lógica compleja. El código anterior crea un programa que registra Hello, World!

Compilación y publicación local

Esta sección se centra en la publicación en Localhost. Aunque Solana Playground utiliza devnet de forma predeterminada, un entorno de desarrollo local ofrece una experiencia mucho mejor. Además de ser más rápido, evita varios problemas habituales al realizar pruebas en devnet. Por ejemplo, un saldo de SOL insuficiente para las transacciones, publicaciones lentas y la imposibilidad de probar cuando devnet no está disponible. En cambio, el desarrollo local puede garantizar un estado nuevo con cada prueba. Esto permite un entorno de desarrollo más controlado y eficiente.

Configuración de nuestras herramientas

Primero, asegúrate de que el conjunto de herramientas de Solana esté configurado correctamente para desarrollar en Localhost. Ejecuta el comando solana config set --url localhost para garantizar que todas las configuraciones apunten a las URL de Localhost. 

También debes tener un par de claves local para interactuar localmente con Solana. Necesitas una wallet de Solana con saldo de SOL para publicar un programa mediante la CLI de Solana. Ejecuta el comando solana address para comprobar si ya tienes un par de claves local. Si aparece un error, ejecuta el comando solana-keygen new. De forma predeterminada, se creará una wallet en el sistema de archivos en la ruta ~/.config/solana/id.json. También recibirás una frase de recuperación que puedes usar para recuperar las claves pública y privada. Te recomendamos guardar este par de claves, aunque solo lo uses localmente. Ten en cuenta que, si ya tienes una wallet del sistema de archivos guardada en la ubicación predeterminada, el comando solana-keygen new no la sobrescribirá a menos que lo especifiques con el comando --force.

Configuración de Anchor.toml

A continuación, asegúrate de que el archivo Anchor.toml apunte correctamente a Localhost. Verifica que contenga el siguiente código:

Código
...
[programs.localnet]
hello-world = "EJTW6qsbfya86xeLRQpKLM8qhn11cJXmU35QbJwE11R8"
...
[provider]
cluster = "Localnet"
wallet = '~config/solana/id.json'

Aquí, [programs.localnet] hace referencia al ID del programa en localnet, es decir, Localhost. El ID del programa siempre se especifica en relación con el clúster. Esto se debe a que el mismo programa puede publicarse en una dirección distinta en otro clúster. Desde la perspectiva de la experiencia del desarrollador, puede resultar molesto declarar nuevos ID de programa para programas publicados en diferentes clústeres. 

El ID del programa es público. Sin embargo, su par de claves se almacena en la carpeta target/deploy. Sigue una convención de nombres específica basada en el nombre del programa. Por ejemplo, si el programa se llama hello_world, Anchor buscará un par de claves en target/deploy/hello-world-keypair.json. Si Anchor no encuentra este archivo durante la publicación, generará un par de claves nuevo. Esto dará lugar a un nuevo ID de programa. Por eso, es fundamental actualizar el ID del programa después de la primera publicación. El archivo hello-world-keypair.json sirve como prueba de propiedad del programa. Si el par de claves se filtra, actores maliciosos podrían realizar cambios no autorizados en el programa. 

Con [provider], le indicamos a Anchor que use Localhost y la wallet especificada para pagar el almacenamiento y las transacciones.

Compilación, publicación y ejecución de un ledger local

Usa el comando anchor build para compilar el programa. Para compilar un programa específico por su nombre, usa el comando anchor build -p <program name> y sustituye <program name> por el nombre del programa. Como desarrollamos en localnet, podemos usar los comandos de localnet de la CLI de Anchor para agilizar el proceso de desarrollo. Por ejemplo, anchor localnet --skip-build resulta especialmente útil para omitir la compilación de un programa en el espacio de trabajo. Esto puede ahorrar tiempo al ejecutar pruebas cuando el código del programa no ha cambiado.

Si ahora intentamos ejecutar el comando anchor deploy, recibiremos un error. Esto se debe a que no hay ningún clúster de Solana ejecutándose en nuestro equipo para realizar las pruebas. Podemos ejecutar un ledger local para simular un clúster en nuestro equipo. La CLI de Solana incluye un validador de pruebas. El comando solana-test-validator iniciará un clúster completo de un solo nodo en tu estación de trabajo. Esto ofrece varias ventajas: no hay límites de solicitudes RPC ni de airdrops, permite publicar programas directamente on-chain, cargar cuentas desde archivos y clonar cuentas de un clúster público. El validador de pruebas debe ejecutarse en otra ventana abierta de la terminal y permanecer activo para que el clúster de localhost siga en línea y puedas interactuar con él. 

Ahora podemos ejecutar correctamente anchor deploy para publicar el programa en nuestro ledger local. Todos los datos transmitidos al ledger local se guardarán en una carpeta test-ledger generada en el directorio de trabajo actual. Te recomendamos agregar esta carpeta al archivo .gitignore para evitar incluirla en tu repositorio. Además, cerrar el ledger local, es decir, presionar Ctrl + C en la terminal, no eliminará los datos enviados al clúster. Para eliminarlos, borra la carpeta test-ledger o ejecuta solana-test-validator --reset.

¡Felicitaciones! Acabas de publicar tu primer programa de Solana en Localhost.

Solana Explorer

También puedes configurar Solana Explorer con tu ledger local. Ve a Solana Explorer. En la barra de navegación, haz clic en el botón verde que muestra el clúster actual:

Se abrirá una barra lateral donde podrás elegir un clúster. Haz clic en URL RPC personalizada. El campo debería completarse automáticamente con http://localhost:8899. Si no es así, ingrésalo para que el explorador apunte a tu equipo mediante el puerto 8899:

Esto resulta muy útil por varios motivos:

  • Permite inspeccionar las transacciones del ledger local en tiempo real, con las mismas capacidades que normalmente ofrece un explorador de bloques para analizar devnet o mainnet
  • Facilita la visualización del estado de las cuentas, los tokens y los programas como si funcionaran en un clúster activo
  • Proporciona información detallada sobre los errores y fallos de las transacciones
  • Ofrece una experiencia de desarrollo uniforme entre clústeres mediante una interfaz conocida

Publicación en Devnet

Aunque recomendamos desarrollar en Localhost, también puedes publicar en devnet si quieres realizar pruebas específicamente en ese clúster. El proceso es prácticamente el mismo, excepto que no necesitas ejecutar un ledger local, ya que puedes interactuar con un clúster completo de Solana.

Ejecuta el comando solana config set --url devnet para cambiar el clúster seleccionado a devnet. A partir de ahora, cualquier comando solana que ejecutes en la terminal se ejecutará en devnet. Luego, en el archivo Anchor.toml, duplica la sección [programs.localnet] y cámbiale el nombre a [programs.devnet]. Cambia también [provider] para que apunte a devnet:

Código
...
[programs.localnet]
hello-world = "EJTW6qsbfya86xeLRQpKLM8qhn11cJXmU35QbJwE11R8"

[programs.devnet]
hello-world = "EJTW6qsbfya86xeLRQpKLM8qhn11cJXmU35QbJwE11R8"
...
[provider]
cluster = "Devnet"
wallet = '~config/solana/id.json'

Asegúrate de tener SOL de devnet para publicar el programa. Usa el comando solana airdrop <amount> para realizar un airdrop en la ubicación predeterminada del par de claves: ~/.config/solana/id.json. También puedes especificar la dirección de una wallet mediante solana aidrop <amount> <wallet address>. Otra opción es visitar este faucet para obtener SOL de devnet. Te recomiendo consultar la siguiente guía para obtener SOL de devnet.

Esto suele ocurrir porque el faucet de devnet está agotado o porque solicitaste demasiado SOL a la vez. El límite actual es de 5 SOL, más que suficiente para publicar este programa. Por eso, te recomendamos solicitar 5 SOL al faucet o ejecutar el comando solana airdrop 5. Solicitar cantidades menores de forma progresiva puede provocar una limitación de solicitudes.

Ahora, compila y publica el programa con los siguientes comandos:

Código
anchor build
anchor deploy

¡Felicitaciones! Acabas de publicar localmente tu primer programa de Solana en devnet.

Compilación y publicación en Solana Playground

En Solana Playground, ve al icono Herramientas de la barra lateral izquierda. Haz clic en Compilar. Deberías ver lo siguiente en la consola:

Código
Building...
Build successful. Completed in 2.20s..

Observa que se sobrescribió el ID de la macro declare_id!. Publicaremos el programa en esta nueva dirección. Ahora, haz clic en Publicar. Deberías ver algo similar a esto en la consola:

Código

Deploying... This could take a while depending on the program size and network conditions.
Warning: 41 transactions not confirmed, retrying...
Deployment successful. Completed in 17s

¡Felicitaciones! Acabas de publicar tu primer programa de Solana en devnet mediante Solana Playground.

Abstracción eficaz: IDL y macros

Anchor simplifica el desarrollo de programas mediante una abstracción eficaz. Es decir, simplifica conceptos complejos de programación en blockchain para hacerlos más accesibles y fáciles de usar. Por ejemplo, Anchor emplea un lenguaje de definición de interfaces (IDL) para definir la interfaz del programa. Al compilar un programa, Anchor genera un archivo JSON que representa la IDL del programa. Esta estructura puede usarse en el cliente para definir cómo interactuar con las funciones y estructuras de datos del programa. Anchor también ofrece abstracciones de alto nivel para gestionar el estado. Permite que los desarrolladores definan el estado de su programa mediante structs de Rust, lo que puede resultar más intuitivo que trabajar con matrices de bytes sin procesar o serializaciones manuales. Así, los desarrolladores pueden definir el estado como lo harían con cualquier estructura de datos típica de Rust, mientras Anchor se encarga de la serialización subyacente y del almacenamiento en cuentas.

También es muy sencillo publicar una IDL on-chain. Los desarrolladores pueden publicar una IDL con el siguiente comando:

Código
anchor idl init --filepath   --provider.cluster  --provider.wallet

Asegúrate de que la wallet proporcionada sea la autoridad del programa y tenga suficiente SOL para la transacción. Los desarrolladores ahora pueden consultar su IDL en un explorador de bloques como Orb.

Por ejemplo, aquí está la IDL v4 del agregador de DFlow en Orb.

Las macros de Anchor son una de sus abstracciones más importantes, si no la más importante. En Rust, una macro es una porción de código que genera otra porción de código. Esta es una forma de metaprogramación. Las macros declarativas son el tipo de macro más utilizado en Rust. Permiten que los desarrolladores escriban algo similar a una expresión match mediante la construcción macro_rules!. Las macros procedurales funcionan más como una función: reciben código como entrada, operan sobre él y producen una salida. En Anchor, por ejemplo, la macro #[account] define y aplica restricciones a las cuentas de Solana. Esto ayuda a reducir la complejidad y los posibles errores relacionados con la gestión de cuentas. Explicar las macros de Anchor requiere inevitablemente hablar sobre la estructura de sus programas.

Estructura de un programa Anchor

La estructura de los programas Anchor está diseñada para aprovechar una combinación de macros y traits que generan código repetitivo y aplican la lógica del programa. Esta filosofía de diseño agiliza considerablemente el proceso de desarrollo y garantiza la coherencia y fiabilidad del comportamiento del programa.

Las declaraciones use se encuentran al principio del archivo. Ten en cuenta que pertenecen a la semántica general del lenguaje Rust y no son específicas de Anchor. Estas declaraciones crean uno o más enlaces de nombres locales equivalentes a otra ruta: las declaraciones use acortan la ruta necesaria para hacer referencia a un elemento de un módulo. Pueden aparecer en módulos o bloques. Además, la palabra clave self puede enlazar una lista de rutas con un prefijo común y su módulo principal compartido. Por ejemplo, todas estas son declaraciones use válidas: 

Código
use anchor_lang::prelude::*;
use std::collections::hash_map::{self, HashMap};

use a::b::{c, d, e::f, g::h::i};
use a::b::{self, c, d::e};

La primera macro de Anchor que encontrará un desarrollador es declare_id!. Se usa para declarar la dirección del programa (el ID del programa) y garantizar que todas las interacciones se dirijan correctamente a él. Anchor genera un nuevo par de claves cuando un desarrollador compila un programa Anchor por primera vez. Este es el par de claves que se usa para desplegar el programa, salvo que se indique lo contrario. La clave pública del par de claves debe proporcionarse como ID del programa para la macro declare_id!:

Código
declare_id!("HZfVb1ohL1TejhZNkgFSKqGsyTznYtrwLV6GpA8BwV5Q");

La macro de atributo #[program] identifica el módulo que contiene la lógica de instrucciones del programa. Actúa como punto de entrada y define cómo interpreta y ejecuta el programa las instrucciones entrantes. Esta macro simplifica el enrutamiento de las instrucciones hacia la función correspondiente del programa, lo que hace que el código sea más organizado y manejable. Cada función dentro de este módulo se trata como una instrucción independiente. Cada función recibe como primer argumento un parámetro de contexto (ctx) de tipo Context. Los desarrolladores pueden acceder a las cuentas, al ID del programa en ejecución y a las cuentas restantes.

El tipo Context se define así:

Código
pub struct Context<'a, 'b, 'c, 'info, T: Bumps> {
    pub program_id: &'a Pubkey,
    pub accounts: &'b mut T,
    pub remaining_accounts: &'c [AccountInfo<'info>],
    pub bumps: T::Bumps,
}

Esto ayuda a proporcionar entradas que no son argumentos a un programa determinado. El campo program_id es de tipo Pubkey y representa el ID del programa que se está ejecutando. accounts hace referencia a las cuentas serializadas, mientras que remaining_accounts se refiere a las cuentas restantes que se proporcionaron, pero no se deserializaron ni validaron. Ten mucho cuidado al usarlo directamente. El campo bumps es de tipo Bumps , generado por #[derive(Accounts)]. Representa las semillas bump encontradas durante la validación de restricciones. Veremos las restricciones de cuentas en una sección posterior. Por ahora, es importante saber que esto se incluye para evitar que los handlers tengan que volver a calcular las semillas bump o pasarlas como argumentos.

Ten en cuenta que Context es un tipo genérico. En Rust, los genéricos permiten que los desarrolladores escriban código flexible y reutilizable que funciona con cualquier tipo de datos. Permiten definir tipos para structs, enums, funciones y métodos sin especificar el tipo exacto con el que trabajarán. En su lugar, se usa un marcador de posición para esos tipos, que suele representarse como T. Los genéricos reducen el código repetitivo y aumentan la claridad. Por ejemplo, se puede definir un enum que contenga tipos de datos genéricos:

Código
enum Option<T> {
  Some(T),
  None,
}

El fragmento de código anterior muestra el enum Option<T>. Es un enum estándar de Rust que puede encapsular un valor de cualquier tipo (es decir, Some(T)) o ningún tipo (None).

Para nuestros fines, Context es un tipo genérico en el que T especifica las cuentas necesarias para una instrucción (es decir, cualquier tipo que un desarrollador quiera crear para almacenar datos). Al usar Context, los desarrolladores pueden definir T como un struct que implemente el trait Accounts. Por ejemplo, Context<SetData>. Los desarrolladores pueden acceder a los campos del tipo Context mediante notación de punto. Por ejemplo, ctx.accounts accede al campo accounts del struct Context.

Como se mencionó, la macro #[account] define tipos de cuenta personalizados. En las siguientes secciones, analizaremos los tipos de cuenta y las restricciones mediante #[account(...)]. Por ahora, es importante señalar que el struct Accounts es donde un desarrollador define qué cuentas debe esperar una instrucción y qué restricciones deben cumplir.

Tipos de cuenta

El tipo Account se usa cuando una instrucción quiere acceder a los datos deserializados de una cuenta. El struct Account es genérico respecto de T y se define así: 

Código
pub struct Account<'info, T: AccountSerialize + AccountDeserialize + Clone> { /* private fields */ }

Es un wrapper de AccountInfo que verifica la propiedad del programa y deserializa los datos subyacentes en un tipo de Rust. Comprueba la propiedad del programa de modo que Account.info.owner == T::owner(). Es decir, verifica que el propietario de los datos coincida con ID (el que se creó antes con declare_id!) del crate donde se usa #[account]. Esto significa que el tipo de datos que envuelve Account (=T) debe implementar el trait Owner. El atributo #[account] implementa el trait para un struct mediante crate::ID, declarado por declare_id! en el mismo programa. En la mayoría de los casos, los desarrolladores pueden usar simplemente el atributo #[account] para añadir los traits y las implementaciones necesarios a sus datos. El atributo #[account] genera implementaciones para los siguientes traits:

Al implementar traits para la serialización de cuentas, los primeros 8 bytes se reservan para un discriminador de cuenta único. Este discriminador se determina a partir de los primeros 8 bytes del hash SHA-256 del identificador de la cuenta en Rust. Toda llamada a try_deserialize de AccountDeserialize comprobará este discriminador y detendrá la deserialización de la cuenta con un error si se proporcionó una cuenta no válida.

Habrá casos en los que los desarrolladores necesiten interactuar con programas que no usan Anchor. En esos casos, pueden obtener todos los beneficios de Account si crean su propio tipo de wrapper personalizado en lugar de usar #[account]. Toma como ejemplo el siguiente fragmento de código:

Código
use anchor_lang::prelude::*;
use anchor_spl::token::TokenAccount;

// Rest of the program

#[derive(Accounts)]
pub struct SetData<'info> {
    #[account(mut)]
    pub my_account: Account<'info, MyAccount>,
    #[account(
        constraint = my_account.mint == token_account.mint,
        has_one = owner
    )]
    pub token_account: Account<'info, TokenAccount>,
    pub owner: Signer<'info>
}

La mayor parte de la validación de cuentas se realiza mediante restricciones de cuentas, que veremos en la siguiente sección. Por ahora, observa cómo se usa el tipo TokenAccount para garantizar que la cuenta entrante pertenezca al programa de tokens. TokenAccount envuelve el struct Account del programa de tokens y añade las funciones necesarias. Esto garantiza que Anchor pueda deserializar la cuenta y permite que los desarrolladores usen sus campos en las restricciones de cuentas y en la función de instrucción.

Observa también en el fragmento de código anterior que la macro derive encapsula todo el struct. Esto implementa un deserializador Accounts en SetData y se usa para validar las cuentas entrantes.

En el struct de validación de cuentas pueden usarse varios tipos Account, entre ellos:

Restricciones de cuentas

Las restricciones de cuentas son fundamentales para desarrollar programas Anchor seguros. En próximos artículos, abordaremos con mayor profundidad la seguridad de los programas Solana y el hacking de programas Anchor. Sin embargo, aquí es importante explicar las restricciones. Estas permiten que los desarrolladores verifiquen si determinadas cuentas o los datos que contienen cumplen requisitos predefinidos. Se pueden aplicar varios tipos de restricciones mediante el atributo #[account(...)], que también puede hacer referencia a otras estructuras de datos. El formato es el siguiente:

Código
#[account(constraint goes here)]
pub account: AccountType

También es importante señalar que, dentro de la macro Accounts, los desarrolladores pueden acceder a los argumentos de la instrucción mediante el atributo #[instruction(...)]. Deben enumerar los argumentos de la instrucción en el mismo orden en el que aparecen en ella, pero pueden omitir todos los argumentos posteriores al último que necesiten. Por ejemplo, según la documentación de Anchor: 

Código
...
pub fn initialize(ctx: Context, bump: u8, authority: Pubkey, data: u64) -> anchor_lang::Result<()> {
    ...
    Ok(())
}
...
#[derive(Accounts)]
#[instruction(bump: u8)]
pub struct Initialize<'info> {
    ...
}

Las restricciones de cuentas pueden dividirse en restricciones normales y restricciones SPL. Revisaremos restricciones específicas durante el resto de este artículo. En estos ejemplos, <expr> representa una expresión arbitraria que puede pasarse siempre que se evalúe como un valor del tipo esperado. Por ejemplo, owner = token_program.key().

Análisis de las restricciones de un programa

Te recomiendo consultar la documentación de Anchor sobre cuentas para obtener una lista más completa de las posibles restricciones. Sería demasiado laborioso revisar cada restricción y ofrecer una definición formal en una especie de tabla. Para nuestros fines, resulta más útil analizar el siguiente programa y ver cómo funcionan las restricciones de cuentas en la práctica:

Código
use anchor_lang::prelude::*;
#[cfg(not(feature = "no-entrypoint"))]
use {default_env::default_env, solana_security_txt::security_txt};

declare_id!("fanqeMu3fw8R4LwKNbahPtYXJsyLL6NXyfe2BqzhfB6");

pub mod errors;
pub mod instructions;
pub mod state;

pub use instructions::*;
pub use state::*;

#[cfg(not(feature = "no-entrypoint"))]
security_txt! {
  name: "Fanout",
  project_url: "http://helium.com",
  contacts: "email:hello@helium.foundation",
  policy: "https://github.com/helium/helium-program-library/tree/master/SECURITY.md",

  // Optional Fields
  preferred_languages: "en",
  source_code: "https://github.com/helium/helium-program-library/tree/master/programs/fanout",
  source_revision: default_env!("GITHUB_SHA", ""),
  source_release: default_env!("GITHUB_REF_NAME", ""),
  auditors: "Sec3"
}

#[program]
pub mod fanout {
  use super::*;

  pub fn initialize_fanout_v0(
    ctx: Context<InitializeFanoutV0>,
    args: InitializeFanoutArgsV0,
  ) -> Result<()> {
    instructions::initialize_fanout_v0::handler(ctx, args)
  }

  pub fn stake_v0(ctx: Context<StakeV0>, args: StakeArgsV0) -> Result<()> {
    instructions::stake_v0::handler(ctx, args)
  }

  pub fn unstake_v0(ctx: Context<UnstakeV0>) -> Result<()> {
    instructions::unstake_v0::handler(ctx)
  }

  pub fn distribute_v0(ctx: Context<DistributeV0>) -> Result<()> {
    instructions::distribute_v0::handler(ctx)
  }
}

Este es el programa Fanout de Helium. Es un programa razonablemente complejo que distribuye tokens entre sus titulares de forma proporcional a sus tenencias. Por ahora, el proyecto no parece muy útil para nosotros porque no hay restricciones. Sin embargo, si analizamos el struct StakeV0 de la instrucción stake_v0, encontraremos muchas restricciones para explorar.

mut

La primera restricción de esta instrucción es la restricción de cuenta mut. mut se define como #[account(mut)] o #[account(mut @ <custom_error>)] y admite errores personalizados mediante la notación @. Esta restricción comprueba si una cuenta determinada es mutable y hace que Anchor conserve cualquier cambio de estado. En el programa de Helium, la restricción garantiza que la cuenta payer sea mutable:

Código
...
pub struct StakeV0<'info> {
  #[account(mut)]
  pub payer: Signer<'info>,
  pub staker: Signer<'info>,
  /// CHECK: Just needed to receive nft
  pub recipient: AccountInfo<'info>,
...

has_one

La restricción has_one se define como #[account(has_one = <target_account)] o #[account(has_one = <target_account> @ <custom_error>)]. Comprueba el campo target_account para determinar si la cuenta coincide con la clave del campo target_account del struct Accounts. Se admiten errores personalizados mediante la anotación @.

En el contexto del struct StakeV0, la restricción has_one se usa para comprobar si la cuenta tiene un membership_mint, un token_account y un membership_collection:

Código
...
#[account(
  mut,
  has_one = membership_mint,
  has_one = token_account,
  has_one = membership_collection
)]
pub fanout: Box<Account<'info, FanoutV0>>,
pub membership_mint: Box<Account<'info, Mint>>,
pub token_account: Box<Account<'info, TokenAccount>>,
pub membership_collection: Box<Account<'info, Mint>>,
...

Observa que hay varias restricciones has_one y que también se usa la restricción mut. Es posible aplicar simultáneamente varias restricciones a una misma cuenta.

seeds, bump

Las restricciones seeds y bump se usan para comprobar que una cuenta determinada sea una PDA derivada del programa actualmente en ejecución, las semillas y, si se proporciona, el bump:

  • #[account(seeds = <seeds>, bump)]
  • #[account(seeds = <seeds>, bump, seeds::program = <expr>)]
  • #[account(seeds = <seeds>, bump = <expr>)]
  • #[account(seeds = <seeds>, bump = <expr>, seeds::program = <expr>)]

Si no se proporciona el bump, Anchor usará el bump canónico. Seeds::program = <expr> puede usarse para derivar la PDA desde un programa diferente del que está actualmente en ejecución.

En el programa Fanout de Helium, la restricción seeds comprueba si el texto “metadata”, la clave token_metadata_program, la clave membership_collection y el texto “edition” son las semillas usadas para derivar esta PDA. La restricción seeds::program garantiza que se use token_metadata_program para derivar la PDA, en lugar del programa actual:

Código
...
#[account(
  mut,
  seeds = ["metadata".as_bytes(), token_metadata_program.key().as_ref(), membership_collection.key().as_ref()],
  seeds::program = token_metadata_program.key(),
  bump,
)]
pub collection_metadata: UncheckedAccount<'info>,
...

token::mint, token::authority

Las restricciones token::mint y token::authority se definen así:

  • #[account(token::mint = <target account>, token::authority = <target account>)]
  • #[account(token::mint = <target account>, token::authority = <target account>, token::token_program = <target account>)]

Las restricciones de token mint y authority se usan para verificar la dirección de mint y la autoridad de un TokenAccount. Estas restricciones pueden usarse como comprobación o junto con la restricción init para crear una cuenta de token con la dirección de mint y la autoridad indicadas. Cuando se usan como comprobación, es posible especificar solo un subconjunto de las restricciones. 

En el contexto del programa de Helium, estas restricciones se usan para comprobar si el mint de associated_token es igual a membership_mint y si la autoridad del token está configurada como staker:

Código
...
#[account(
  mut,
  associated_token::mint = membership_mint,
  associated_token::authority = staker,
)]
pub from_account: Box<Account<'info, TokenAccount>>,
...

init, payer, space

En este punto, conviene avanzar un poco en el código para analizar las restricciones init, payer y space. La restricción init se define como [#account(init, payer = <target_account>, space = <num_bytes>)]. Esta restricción crea la cuenta mediante una CPI al System Program y la inicializa configurando su discriminador de cuenta. Esto marca la cuenta como mutable y es mutuamente excluyente con mut. Para cuentas de más de 10 kibibytes, usa #[account(zero)].

La restricción init debe usarse con algunas restricciones adicionales. Requiere la restricción payer, que especifica la cuenta que pagará la creación de la cuenta. También requiere que el System Program exista en el struct y se llame system_program. También debe definirse la restricción space. En la sección Espacio de la cuenta, analizaremos con más detalle esta restricción y los requisitos de espacio.

En el programa Fanout de Helium, el comando init crea una cuenta nueva. payer se establece como payer, definido anteriormente en el struct como pub payer: Signer<'info>. El espacio de la cuenta se establece en el tamaño de FanoutVoucherV0, más 8 bytes para el discriminador y 61 bytes adicionales de espacio:

Código
...
#[account(
  init,
  payer = payer,
  space = 60 + 8 + std::mem::size_of::<FanoutVoucherV0>() + 1,
  seeds = ["fanout_voucher".as_bytes(), mint.key().as_ref()],
  bump,
)]
pub voucher: Box<Account<'info, FanoutVoucherV0>>,
...

init_if_needed

La restricción init_if_needed se define como #[account(init_if_nedded, payer = <target_Account>)] o #[account(init)if_needed, payer = <target_account>, space = <num_bytes>)]. Esta restricción tiene exactamente la misma funcionalidad que init. Sin embargo, solo se ejecuta si la cuenta aún no existe. Si la cuenta existe, init_if_needed sigue verificando que se cumplan todas las restricciones de inicialización, como que la cuenta tenga asignada la cantidad correcta de espacio o las semillas correctas en el caso de una PDA.

init_if_needed debe usarse con precaución, ya que está detrás de una feature flag debido a los posibles riesgos. Para habilitarla, importa anchor-lang con la feature de cargo init-if-needed. Al usar init_if_needed, es crucial protegerse contra ataques de reinicialización. Los desarrolladores deben asegurarse de que su código incluya comprobaciones que impidan restablecer la cuenta a su estado inicial después de inicializarla, a menos que este comportamiento sea intencional. Se considera una práctica recomendada mantener simples las rutas de ejecución de las instrucciones para mitigar estos ataques. Considera dividir las instrucciones en una para la inicialización y las demás para las operaciones posteriores.

El programa Fanout de Helium usa la restricción init_if_needed para inicializar recipient_account si la cuenta aún no existe:

Código
...
#[account(
  init_if_needed,
  payer = payer,
  associated_token::mint = mint,
  associated_token::authority = recipient,
)]
pub receipt_account: Box<Account<'info, TokenAccount>>,
...

constraint

La restricción constraint se define como #[account(constraint = <expr>)] o #[account(constraint = <expr> @ <custom_error>)]. Comprueba si la expresión proporcionada se evalúa como verdadera. Esto resulta útil cuando ninguna otra restricción se ajusta al caso de uso previsto. También admite errores personalizados mediante la anotación @.

El programa Fanout usa constraint para comprobar si el suministro del mint está establecido en cero:

Código
...
#[account(
  mut,
  constraint = mint.supply == 0,
  mint::decimals = 0,
  mint::authority = voucher,
  mint::freeze_authority = voucher,
)]
pub mint: Box<Account<'info, Mint>>,
...

mint::authority, mint::decimals, mint::freeze_authority 

En el fragmento de código anterior, las restricciones mint::decimals, mint::authority y mint::freeze_authority se usan para comprobar si los decimales del mint están establecidos en cero y si voucher tiene autoridad y autoridad de congelación.

Como contexto, las restricciones mint::authority, mint::decimals y mint::freeze_authority se definen así:

  • #[account(mint::authority = <target account>, mint::decimals = <expr>)]
  • #[account(mint::authority = <target account>, mint::decimals = <expr>, mint::freeze_authority = <target account>)]

Estas restricciones son evidentes: comprueban la autoridad, los decimales y la autoridad de congelación del token, respectivamente. Pueden usarse como comprobación o junto con init para crear una cuenta de mint con los decimales y la autoridad de mint indicados. La autoridad de congelación es completamente opcional cuando se usa con init. Cuando se usan como comprobación, es posible especificar solo un subconjunto de estas restricciones.

Espacio de la cuenta 

Cada cuenta que utiliza un programa en Solana debe tener su espacio de almacenamiento asignado explícitamente. Esta asignación es crucial para gestionar los recursos de forma eficiente y garantizar que solo se almacenen en la cadena los datos necesarios. También permite predecir los costos de las transacciones y mejora la eficiencia de su ejecución, ya que las transacciones pueden procesarse sin asignar ni redimensionar dinámicamente el almacenamiento de las cuentas. Además, asignar previamente el espacio garantiza que la cuenta tenga capacidad suficiente para almacenar todos los datos necesarios, lo que reduce el riesgo de transacciones fallidas o posibles vulnerabilidades de seguridad.

Dimensionamiento de variables

Cada tipo de dato requiere una cantidad de espacio diferente. Esta guía simplificada te ayudará a estimar los requisitos de espacio:

  • Tipos básicos: los tipos de datos simples como bool, u8, i8, u16, i16, u32, i32, u64, i64, u128 e i128 tienen tamaños fijos. Estos van desde 1 byte para un bool (aunque solo usa 1 bit) hasta 16 bytes para u128 / i128
  • Arreglos: para un arreglo [T;amount], el espacio se calcula multiplicando el tamaño de T por el número de elementos (es decir, amount). Por ejemplo, un arreglo de 16 u16 requeriría 32 bytes
  • Pubkey:  una clave pública siempre ocupa 32 bytes en Solana
  • Tipos dinámicos: String y Vec<T> requieren especial atención. Ambos necesitan 4 bytes para almacenar su longitud, además del espacio para el contenido real. Es esencial asignar espacio suficiente para el tamaño máximo esperado. Para un String, esto equivale a 4 bytes más la longitud del String en bytes. Para un Vec<T>, equivale a 4 bytes más el espacio del tipo dado multiplicado por el número de elementos esperados (es decir, 4 + space(T) * amount)
  • Opciones y enumeraciones: un tipo Option<T> requiere 1 byte más el espacio para el tipo T. Las enumeraciones requieren 1 byte para el discriminador de la enumeración más el espacio necesario para la variante más grande
  • Puntos flotantes: tipos como f32 y f64 ocupan 4 y 8 bytes, respectivamente. Ten cuidado con los valores NaN, ya que pueden provocar errores de serialización

La siguiente guía solo se aplica a las cuentas que no usan la serialización zero-copy. La serialización de copia cero se indica con el atributo #[zero_copy]. Esta aprovecha el atributo repr(c) para la disposición de memoria, lo que permite hacer una conversión directa de punteros para acceder a los datos. Es una forma eficiente de trabajar con datos en la cadena sin la sobrecarga de la deserialización tradicional. #[zero_copy] es una forma abreviada de aplicar #[derive(Copy, Clone)], #[derive(bytemuck::Zeroable)], #[derive(bytemuck::Pod)] e #[repr(C)]. Estos atributos garantizan que la cuenta pueda tratarse de forma segura como una secuencia de bytes y que sea compatible con la deserialización de copia cero. La deserialización de copia cero es crucial para las cuentas que requieren tamaños considerablemente grandes, es decir, cuentas que no pueden serializarse de manera eficiente mediante Borsh o los mecanismos de serialización predeterminados de Anchor sin superar los límites del heap o la pila.

Discriminador interno de Anchor

Los desarrolladores deben sumar 8 a la restricción space para incluir el discriminador interno de Anchor. Por ejemplo, si una cuenta requiere 32 bytes, necesitará 40. Se considera una buena práctica definir la restricción de espacio como space = 8 + <account size> para dejar claro que el discriminador interno se incluye en el cálculo del espacio.  

Como aclaración, un discriminador es un identificador único que permite distinguir entre distintos tipos de datos. Resulta útil para diferenciar entre distintos tipos de estructuras de datos de cuentas durante la ejecución. También se usa como prefijo de las instrucciones, lo que ayuda a dirigirlas a sus métodos correspondientes dentro de un programa de Anchor. El discriminador es un arreglo de 8 bytes que representa el identificador único del tipo de dato.

Cálculo del espacio inicial

Calcular el espacio inicial necesario para una cuenta puede ser difícil. La macro InitSpace agrega una constante INIT_SPACE que puede usarse en la estructura de la cuenta. No es necesario que la estructura contenga la macro #[account] para generar la constante. La documentación de Anchor ofrece el siguiente ejemplo:

Código
#[account]
#[derive(InitSpace)]
pub struct ExampleAccount {
  pub data: u64,
  // max_len represents the length of the structure
  #[max_len(50)]
  pub string_one: String,
  #[max_len(10, 5)]
  pub nested: Vec<Vec<u8>>,
}

#[derive(Accounts)]
pub struct Initialize<'info> {
  #[account(mut)]
  pub payer: Signer<'info>,
  pub system_program: Program<'info, System>,
  #[account(init, payer = payer, space = 8 + ExampleAccount::INIT_SPACE)]
  pub data: Account<'info, ExampleAccount>,
}

En este ejemplo, ExampleAccount::INIT_SPACE calcula automáticamente el espacio necesario para ExampleAccount. También incluye el discriminador interno de Anchor en el cálculo del espacio.

Redimensionamiento del espacio del programa

La restricción realloc se usa para ajustar el espacio de una cuenta de programa al comienzo de una instrucción. Requiere que la cuenta sea mutable (es decir, mut) y se aplica a los tipos Account o AccountLoader. Se define como #[account(realloc = <space>, realloc::payer = <target>, realloc::zero = <bool>)]. Cuando aumenta la longitud de los datos de la cuenta, se transfieren lamports desde realloc::payer a la cuenta de programa para mantener la exención de renta. Si la longitud de los datos disminuye, los lamports regresan de la cuenta de programa a realloc::payer. La restricción realloc::zero determina si la memoria recién asignada debe inicializarse en cero. La inicialización en cero garantiza que la nueva memoria esté limpia y libre de datos residuales o no deseados.

No se recomienda usar AccountInfo::realloc manualmente en lugar de la restricción realloc. Esto se debe a que no existen comprobaciones durante la ejecución que garanticen que la reasignación no supere el límite MAX_PERMITTED_DATA_INCREASE, lo que podría sobrescribir datos de otras cuentas. La restricción también comprueba e impide reasignaciones repetidas dentro de una sola instrucción.

Por ejemplo:

Código
#[derive(Accounts)]
pub struct Data {
#[account(mut)]
pub payer: Signer<'info>,
  #[account(
    mut,
    seeds = [b"data"],
    bump,
    realloc = 8 + std::mem::size_of::<()>() + 48,
    realloc::payer = payer,
    realloc::zero = false
  )]
  pub update_account: Account<'info, NewData>,
  system_program: Program<'info, System>,
}

Errores

La gestión de errores es un aspecto esencial del desarrollo de programas. Es un mecanismo para identificar y gestionar errores que podrían detener la ejecución de un programa. La gestión de errores debe ser deliberada y planificada para garantizar la calidad, el mantenimiento y la funcionalidad del código. Anchor simplifica este proceso con mecanismos sólidos de gestión de errores. Los errores de los programas de Anchor pueden dividirse en AnchorErrors y errores que no pertenecen a Anchor. Esta sección se centrará en AnchorErrors, ya que los errores que no pertenecen a Anchor abarcan una amplia variedad de errores de Rust. Para estos últimos, recomiendo consultar el capítulo sobre gestión de errores de Rust Book y la sección sobre gestión de errores de Rust By Example.

El siguiente struct define AnchorError:

Código
pub struct AnchorError {
  pub error_name: String,
  pub error_code_number: u32,
  pub error_msg: String,
  pub error_origin: Option<ErrorOrigin>,
  pub compared_values: Option<ComparedValues>,
}

Estos campos son relativamente sencillos. error_name es una cadena que representa el nombre del error. error_code_number es un identificador único del error (es decir, un entero único sin signo que ocupa 32 bits). error_msg es un mensaje descriptivo que explica el error. error_origin es un campo opcional que proporciona información sobre el origen del error, como el archivo fuente o la cuenta involucrada. compared_values es un campo opcional que detalla los valores que se estaban comparando cuando ocurrió el error. Esto resulta sumamente útil para la depuración.

AnchorError implementa un método de registro. Este incluye información sobre el origen del error y los valores involucrados, lo que facilita la depuración y resolución de errores. Este método usa error_origin e compared_values para proporcionar dicha información.

Los AnchorError pueden dividirse en errores internos de Anchor y errores personalizados. Anchor tiene una larga lista de códigos de error internos que puede devolver. Estos errores internos no están diseñados para que los usuarios los utilicen. Sin embargo, resulta útil conocer la correspondencia entre los códigos y sus causas. Por lo general, se generan cuando se infringe una restricción. Los códigos de error internos siguen este esquema:

  • >= 100 son códigos de error de instrucciones
  • >= 1000 son códigos de error de IDL
  • >= 2000 son códigos de error de restricciones
  • >= 3000 son códigos de error de cuentas
  • >= 4100 son códigos de errores varios
  • = 5000 son códigos de error obsoletos.

Los errores personalizados comienzan en ERROR_CODE_OFFSET (es decir, 6000).

Los desarrolladores pueden implementar sus propios errores personalizados mediante el atributo error_code. Este atributo se usa en una enumeración, cuyas variantes pueden usarse como errores en todo el programa. Se puede agregar un mensaje para cada variante. El cliente puede mostrar este mensaje si ocurre el error. Por ejemplo:

Código
#[error_code]
pub enum HeliusError {
  #[msg(“This RPC provider is too good”)]
  RPCTooGood
}

Las macros err! y error! pueden usarse para generar estos errores. Por ejemplo:

Código
require!(rpc.speed > 9000, HeliusError::RPCTooGood);

Es importante señalar que hay varias macros require disponibles. La gran mayoría de estas macros se relacionan con valores que no son claves públicas. Por ejemplo, la macro require_gte comprueba si el primer valor que no es una clave pública es mayor o igual que el segundo valor que no es una clave pública:

Código
pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> {
    require_gte!(ctx.accounts.data.data, 1);
    ctx.accounts.data.data = data;
    Ok(());
}

También hay algunas consideraciones al comparar claves públicas. Por ejemplo, los desarrolladores deben usar require_keys_eq en lugar de require_eq, ya que este último es más costoso.

Todos los programas devolverán un ProgramError. Este tipo de error incluye un campo específico para un número de error personalizado, que Anchor usa para almacenar sus códigos de error internos y personalizados. Sin embargo, solo es un número, por lo que no resulta tan útil. El registro antes mencionado de Anchor con los AnchorError es mucho más útil. Los clientes de Anchor están diseñados para analizar estos registros. Sin embargo, hay situaciones en las que esto puede resultar difícil. Por ejemplo, recuperar los registros de transacciones procesadas con las comprobaciones previas desactivadas no es tan sencillo. Asimismo, Anchor emplea un mecanismo alternativo para programas antiguos o que no pertenecen a Anchor y que no registran los AnchorError de la manera estándar. En estos casos, Anchor comprueba si el número de error devuelto por la transacción corresponde a un código de error interno de Anchor o a un número de error definido en la IDL del programa. Cuando encuentra una coincidencia, Anchor enriquece la información del error para proporcionar más contexto. Siempre que sea posible, Anchor también intenta analizar la pila de errores del programa para rastrear la causa original del error. ProgramError funciona como un tipo de error fundamental, cuya utilidad se amplía mediante los mecanismos de registro y análisis de Anchor para proporcionar información detallada sobre los errores.

Invocaciones entre programas (CPI)

Las invocaciones entre programas (CPI) se han mencionado a lo largo de este artículo, así que merecen una sección propia. Las CPI son fundamentales para la componibilidad de Solana, ya que permiten que los programas llamen directamente a otros programas. Esto convierte al ecosistema de Solana en una API enorme e interconectada para los desarrolladores. Para mantener la brevedad, recomiendo leer la documentación de Anchor sobre las CPI, que ofrece un ejemplo útil del funcionamiento de las CPI con un programa de marioneta y titiritero.

Una CPI puede definirse como una llamada de un programa a otro dirigida a una instrucción específica del programa llamado. El programa invocador se detiene hasta que el programa invocado termina de procesar la instrucción. 

Escalamiento de privilegios

Las CPI permiten que un programa invocador extienda sus privilegios de firmante al programa invocado. Extender privilegios es práctico, pero puede ser muy peligroso. Si una CPI apunta accidentalmente a un programa malicioso, ese programa obtiene los mismos privilegios que el invocador. Anchor mitiga este riesgo con dos medidas de protección:

  • El tipo Program<’info, T> garantiza que la cuenta especificada coincida con el programa esperado (T)
  • Aunque no se use el tipo Program, la función CPI generada automáticamente verificará que el argumento cpi_program corresponda al programa esperado

Ejecución de una CPI

Un programa puede ejecutar una CPI mediante invoke o invoke_signed del crate solana_program. Anchor también proporciona la estructura CpiContext para especificar entradas sin argumentos para las CPI.

invoke

La función invoke se usa cuando no se requiere una PDA como firma. En este caso, el entorno de ejecución extiende la firma original del programa invocador al programa invocado. La función se define así:

Código
pub fn invoke(
    instruction: &Instruction,
    account_infos: &[AccountInfo<'_>]
) -> ProgramResult

Invocar otro programa requiere crear un Instruction que incluya el ID del programa, los datos de la instrucción para el programa invocado y una lista de las cuentas a las que este accederá. Un programa solo recibe valores AccountInfo del entorno de ejecución en su punto de entrada. Cualquier cuenta que el programa invocado necesite para su ejecución debe incluirla y proporcionarla el programa que lo llama. Por ejemplo, si el programa invocado necesita modificar una cuenta específica, el programa invocador debe incluirla en la lista de valores AccountInfo. Esto también se aplica al ID del programa invocado (es decir, el invocador debe especificar explícitamente qué programa está llamando mediante la inclusión del ID del programa invocado).

El Instruction suele construirse dentro del programa invocador, aunque puede deserializarse desde una salida externa.

La transacción completa fallará de inmediato si el programa invocado encuentra un error o se interrumpe. Esto se debe a que la función invoke solo regresa cuando la operación tiene éxito. Usa las funciones set_return_data o get_return_data para devolver datos como resultado de una CPI. Ten en cuenta que el tipo devuelto debe implementar los traits AnchorSerialize e AnchorDeserialize. Como alternativa, haz que el programa invocado escriba los datos en una cuenta específica

Aunque un programa puede llamarse a sí mismo de forma recursiva, las llamadas recursivas indirectas (es decir, la reentrada) realizadas por otro programa provocarán que la transacción falle de inmediato.

Por ejemplo, si tuviéramos un programa que transfiriera tokens mediante una CPI, usaríamos invoke así:

Código
pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> {
    require_gte!(ctx.accounts.data.data, 1);
    ctx.accounts.data.data = data;
    Ok(());
}

invoke_signed

invoke_signed se usa en las CPI que requieren una PDA como firmante. Permite que un programa invocador actúe en nombre de una PDA al proporcionar las semillas necesarias para derivarla:

Código
pub fn invoke_signed(
	instruction: &Instruction,
	account_infos: &[AccountInfo<'_>],
  signers_seeds: &[&[&[u8]]]
) -> ProgramResult

Las PDA también pueden actuar como firmantes en una CPI. El entorno de ejecución usará las semillas proporcionadas y el program_id del programa invocador para generar internamente la PDA mediante create_program_address. Luego, la PDA se valida contra las direcciones incluidas en la instrucción (es decir, account_infos) para confirmar que sea un firmante válido.

Con esta función, una invocación puede firmar en nombre de una o más PDA controladas por el programa invocador. Esto permite que el programa invocado interactúe con las cuentas dadas como si estuvieran firmadas criptográficamente. signer_seeds consta de segmentos de semillas usados para derivar la PDA. Durante la invocación, el entorno de ejecución considera como «firmada» cualquier cuenta coincidente en account_info. Por ejemplo, si tuviéramos un programa que crea una cuenta para una PDA, llamaríamos a invoke_signed así:

Código
invoke_signed(
	&system_instruction::create_account(
  	&payer.key,
  	&vault_pda.key,
  	lamports,
  	vault_size,
  	&program_id,
  ),
  &[
  	payer.clone(),
  	vault_pda.clone(),
  ],
  &[
  	&[
  		b"vault",
  		payer.key.as_ref(),
  		&[vault_bump_seed],
  	],
  ]
)?;

CpiContext

Anchor proporciona CpiContext como una forma más sencilla de realizar CPI, en lugar de usar invoke o invoke_signed. Esta estructura especifica las entradas sin argumentos necesarias para las CPI y refleja en gran medida la funcionalidad de Context. Proporciona información sobre las cuentas necesarias para la instrucción, cualquier cuenta adicional involucrada, el ID del programa invocado y las semillas para derivar las PDA si es necesario. Usa CpiContext::new para las CPI sin PDA e CpiContext::new_with_signer para las CPI que requieren firmantes PDA.

CpiContext se define de la siguiente manera, donde T es un tipo genérico que abarca cualquier objeto que implemente los traits ToAccountMetas e ToAccountInfos<’info>:

Código
pub struct CpiContext<'a, 'b, 'c, 'info, T>where
    T: ToAccountMetas + ToAccountInfos<'info>,{
    pub accounts: T,
    pub remaining_accounts: Vec>,
    pub program: AccountInfo<'info>,
    pub signer_seeds: &'a [&'b [&'c [u8]]],
}

Accounts es un tipo genérico, lo que permite usar cualquier objeto que implemente los traits ToAccountMetas e ToAccountInfos<’info>. Esto se habilita mediante la macro de atributo #[derive(Accounts)] para facilitar la organización del código y mejorar la seguridad de tipos. 

CpiContext simplifica la invocación de programas de Anchor y programas que no pertenecen a Anchor. Para los programas de Anchor, solo tienes que declarar una dependencia en el archivo Cargo.toml del proyecto y usar el módulo cpi generado por Anchor:

Código
[dependencies]
callee = { path = "../callee", features = ["cpi"]}

Definir features = [“cpi”] otorga al programa acceso al módulo callee::cpi. Anchor genera este módulo automáticamente y expone las instrucciones del programa como una función de Rust. Esta función recibe un CpiContext y cualquier dato adicional de la instrucción. Su formato refleja el de las funciones de instrucción normales de los programas de Anchor, pero sustituye Context por CpiContext. El módulo cpi también proporciona las estructuras de cuenta necesarias para llamar a las instrucciones.

Por ejemplo, si el programa invocado tiene una instrucción llamada hello_there que requiere cuentas específicas definidas en la estructura GeneralKenobi, invócala así:

Código
// We assume "jedi" is an Anchor program with a published crate
use jedi::cpi::accounts::GeneralKenobi;
use jedi::cpi::hello_there;
use anchor_lang::prelude::*;

#[program]
pub mod fight_on_utapau {
use super::*;

pub fn call_hello_there(ctx: Context<CallGeneralKenobi>, data: GreetingParams) -> Result<()> {
	let cpi_accounts = GeneralKenobi {
		jedi: ctx.accounts.jedi.to_account_info(),
		// Other account infos needed for the GeneralKenobi struct go here
	};

	let cpi_program = ctx.accounts.jedi_program.to_account_info();
	let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);

	hello_there(cpi_ctx, data);
}

#[derive(Accounts)]
pub struct CallGeneralKenobi<'info> {
	pub jedi: UncheckedAccount<'info>,
	pub jedi_program: Program<'info, Jedi>,
	// Other required accounts
}

pub struct GreetingParams {
	// Params required for the hello_there function
}

En el módulo fight_on_utapau, se realiza una CPI mediante CpiContext. La función call_hello_there está diseñada para interactuar con el programa jedi. Crea un CpiContext con la información de las cuentas necesaria para la estructura de cuenta GeneralKenobi del programa jedi y la información de la cuenta del programa jedi. Este contexto invoca hello_there y pasa cualquier parámetro adicional requerido que especifique la estructura GreetingParams. La estructura CallGeneralKenobi define las cuentas necesarias para esta función, lo que simplifica el proceso.

Por último, cuando invoques instrucciones de programas que no pertenecen a Anchor, comprueba si los responsables del programa publicaron su propio crate con funciones auxiliares para llamar a su programa. Si no hay funciones auxiliares para el programa cuyas instrucciones deben invocarse, usa invoke e invoke_signer para organizar y preparar las CPI.

Direcciones derivadas de programas (PDA)

Recuerda que las PDA están fuera de la curva y no tienen una clave privada asociada. Permiten que los programas firmen instrucciones y que los desarrolladores creen estructuras similares a mapas hash en la cadena. Una PDA se deriva mediante una lista de semillas opcionales, una semilla bump y un ID de programa. 

En resumen, las siguientes restricciones se usan para comprobar que una cuenta dada sea una PDA derivada del programa que se ejecuta actualmente, las semillas y, si se proporciona, el bump:

  • #[account(seeds = <seeds>, bump)]
  • #[account(seeds = <seeds>, bump, seeds::program = <expr>)]
  • #[account(seeds = <seeds>, bump = <expr>)]
  • #[account(seeds = <seeds>, bump = <expr>, seeds::program = <expr>)]

Si no se proporciona el bump, Anchor usará el bump canónico. Seeds::program = <expr> puede usarse para derivar la PDA desde un programa distinto del que se ejecuta actualmente.

Usar las restricciones seeds e bump simplifica el proceso de derivación:

Código
#[derive(Accounts)]
struct ExamplePDA<'info> {
	#[account(seeds = [b"example"], bump)]
	pub example_pda: Account<'info, AccountType>,
}

Aquí, la restricción seeds se usa para derivar la PDA. Anchor verifica automáticamente que la cuenta enviada a la instrucción coincida con la PDA derivada de las semillas. Anchor usa de forma predeterminada el bump canónico cuando se aplica la restricción bump sin un valor específico.

Anchor también permite usar semillas dinámicas basadas en otros campos de cuentas o en los datos de la instrucción. Para ello, se hace referencia a otros campos de la estructura o se usa la macro de atributo #[instruction(...)] para incluir datos deserializados de la instrucción. Por ejemplo, en la siguiente estructura, example_pda está restringida para usar una combinación de una semilla estática, datos de la instrucción y la clave pública del firmante:

Código
#[derive(Accounts)]
#[instruction(instruction_data: String)]
pub struct ExamplePDA<'info> {
	#[account(seeds = [b"example", signor.key().as_ref(), instruction_data.as_bytes()], bump)]
 	pub example_pda: Account<'info, AccountType>,
	#[account(mut)]
	pub signoooorrr: Signer<'info>
}

Conclusión

Decir que Anchor es un framework potente se queda corto. Su capacidad para agilizar el proceso de desarrollo queda clara al explorar las distintas macros y traits que Anchor utiliza para reducir el código. Cuenta con documentación bien mantenida y un sólido ecosistema de tutoriales y crates relacionados. La gran mayoría de los desarrolladores de Solana usan y adoran Anchor.

Este artículo es una guía muy, muy completa para desarrollar programas con Anchor. Explica cómo instalar Anchor, usar Solana Playground y crear, compilar e implementar un programa Hello, World! Después, exploramos los métodos de abstracción eficaz de Anchor, la estructura de un programa típico de Anchor y los numerosos tipos de cuentas y restricciones disponibles. También explica la importancia de asignar espacio a las cuentas y gestionar errores. Por último, exploramos las CPI y PDA. Este es el artículo sobre Anchor: tiene todo lo que necesitas para empezar a desarrollar programas en Solana hoy mismo.

Si llegaste hasta aquí, ¡gracias, anon! Ingresa tu correo electrónico a continuación para no perderte ninguna novedad de Solana. ¿Quieres profundizar? Únete a nuestro Discord para empezar a desarrollar programas con Anchor.

Recursos adicionales

Suscríbete a Helius

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

Imagen ampliada