Crea Tu Primer Programa de Solana

Este inicio rápido utiliza el proyecto base del framework Anchor generado por anchor init. Crearás el proyecto de forma local, ejecutarás sus pruebas, compilarás el programa y revisarás el código del programa que Anchor genera.

Requisitos previos

Antes de comenzar, instala las herramientas de desarrollo de Solana. La instalación incluye Rust, la CLI de Solana y la CLI de Anchor.

Usa la versión 1.1.2 o superior de la CLI de Anchor para esta plantilla. Verifica tu versión instalada:

Terminal
$
anchor --version

Crear el proyecto

Ejecuta los siguientes comandos en tu terminal:

Terminal
$
anchor init my-program
$
cd my-program

El proyecto base incluye un programa de Solana en programs/my-program. El programa incluye dos instrucciones: una para inicializar una cuenta de contador y otra para incrementar el contador.

Algunas partes de la plantilla demuestran patrones comunes de programas de Solana: derivar direcciones de cuenta PDA, realizar un Cross Program Invocation (CPI) para transferir SOL, y usar verificaciones de error personalizadas para detener una instrucción cuando una condición falla.

Anchor.toml
Cargo.toml
Cargo.toml
lib.rs
constants.rs
error.rs
instructions.rs
initialize.rs
increment.rs
state.rs
test_initialize.rs

Compilar el programa

Ejecuta anchor build para compilar el programa base:

Terminal
$
anchor build

El programa compilado se escribe en target/deploy/my_program.so. Cuando el programa se despliega, el contenido de este archivo .so se almacena en una cuenta onchain.

Ejecutar prueba

Ejecuta la prueba predeterminada:

Terminal
$
anchor test

El Anchor.toml de esta plantilla utiliza el comando de prueba de Rust:

Anchor.toml
skip_local_validator = true
[scripts]
test = "cargo test"

La prueba carga el programa compilado en LiteSVM, crea un pagador, envía las instrucciones initialize y increment, luego verifica el estado de la cuenta del contador.

Ejecutar anchor test también compila el programa, por lo que no es necesario ejecutar anchor build primero al realizar pruebas localmente.

Desplegar programa

Las pruebas locales son el ciclo de retroalimentación más rápido. Cuando estés listo para desplegar en una red, por ejemplo devnet, primero compila y luego despliega en un clúster.

Desplegar un programa de Solana requiere SOL porque el programa se almacena en un program account, y dicha cuenta debe pagar por el espacio que utiliza. En devnet, solicita SOL de devnet de forma gratuita desde el Solana Faucet o con la CLI de Solana:

Terminal
$
solana airdrop 2 --url devnet
Terminal
$
anchor build
$
anchor deploy --provider.cluster devnet

Archivos fuente

El directorio src contiene el programa de Solana. La documentación de Estructura del programa de Anchor explica los macros principales utilizados aquí, incluyendo declare_id!, #[program], #[derive(Accounts)] y #[account]. Esta sección recorre los archivos de la plantilla.

lib.rs

lib.rs es el punto de entrada del programa. Conecta los archivos fuente, define la dirección del programa y define las instrucciones del programa que los usuarios pueden invocar.

programs/my-program/src/lib.rs
pub mod constants;
pub mod error;
pub mod instructions;
pub mod state;
use anchor_lang::prelude::*;
pub use constants::*;
pub use instructions::*;
pub use state::*;
declare_id!("82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd");
#[program]
pub mod my_program {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
crate::instructions::initialize::handle_initialize(ctx)
}
pub fn increment(ctx: Context<Increment>) -> Result<()> {
crate::instructions::increment::handle_increment(ctx)
}
}
Anchor.toml
[programs.localnet]
my_program = "82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd"
lib.rs
declare_id!("82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd");

La misma dirección del programa aparece en la configuración y en el código. Anchor.toml indica a Anchor qué dirección desplegar o invocar en un clúster. declare_id! define la dirección del programa en el código para las verificaciones de seguridad.

constants.rs

constants.rs centraliza los valores compartidos en un solo lugar. En esta plantilla, COUNTER_SEED deriva el PDA del contador, HELLO_WORLD_LAMPORTS se transfiere durante la inicialización, y MAX_COUNT se verifica antes de incrementar.

programs/my-program/src/constants.rs
use anchor_lang::prelude::*;
#[constant]
pub const COUNTER_SEED: &[u8] = b"counter";
#[constant]
pub const HELLO_WORLD_LAMPORTS: u64 = 1;
#[constant]
pub const MAX_COUNT: u64 = 10;
initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
// ...
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
// ...
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
// ...
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
Ok(())
}

initialize.rs usa dos constantes:

  • COUNTER_SEED obtiene la dirección PDA del contador.
  • HELLO_WORLD_LAMPORTS establece la cantidad transferida desde el pagador a la cuenta del contador.
increment.rs
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
// ...
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
Ok(())
}

increment.rs usa MAX_COUNT como límite superior del contador. Si el conteo actual ya se encuentra en el máximo, require! devuelve CounterOverflow y los datos de la cuenta no se modifican.

state.rs

state.rs define tipos de datos personalizados para las cuentas que el programa crea y administra. El programa define instrucciones para crear, inicializar y actualizar esos datos, pero los datos del contador no se almacenan dentro del programa en sí. Se almacenan en una cuenta separada con su propia dirección.

programs/my-program/src/state.rs
use anchor_lang::prelude::*;
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub count: u64,
pub authority: Pubkey,
}
initialize.rs
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
// ...
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
Ok(())
}
increment.rs
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
// ...
ctx.accounts.counter.count += 1;
Ok(())
}

state.rs define los datos de la cuenta Counter. Los archivos de instrucciones utilizan ese tipo cuando crean y actualizan la cuenta:

  • Counter::INIT_SPACE dimensiona la cuenta para los campos definidos en state.rs.
  • count y authority son los valores de los campos que se escriben cuando se inicializa la cuenta.
  • count += 1 actualiza el valor del contador almacenado una vez que la validación es exitosa.

error.rs

error.rs define los errores personalizados del programa. En esta plantilla, los errores demuestran cómo los manejadores de instrucciones se detienen cuando un llamante no tiene permiso para actualizar el contador o cuando el contador ya ha alcanzado MAX_COUNT.

programs/my-program/src/error.rs
use anchor_lang::prelude::*;
#[error_code]
pub enum ErrorCode {
#[msg("Only the counter authority can update this counter")]
Unauthorized,
#[msg("Counter has reached the maximum value")]
CounterOverflow,
}
increment.rs
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

error.rs nombra los errores que la instrucción puede devolver:

  • ErrorCode::Unauthorized se devuelve cuando el firmante no es la autoridad almacenada en la cuenta del contador.
  • ErrorCode::CounterOverflow se devuelve cuando el contador ya ha alcanzado MAX_COUNT.

instructions.rs

instructions.rs conecta los archivos de instrucciones con el crate del programa para que lib.rs pueda acceder al código de instrucciones initialize y increment. Cada archivo de instrucciones define las cuentas requeridas por esa instrucción y la lógica del manejador que se ejecuta después de que Anchor valida dichas cuentas.

programs/my-program/src/instructions.rs
pub mod initialize;
pub mod increment;
pub use initialize::*;
pub use increment::*;

initialize.rs

initialize.rs define las cuentas necesarias para crear la cuenta del contador y luego escribe los valores iniciales de la cuenta. La estructura #[derive(Accounts)] utiliza las restricciones de cuenta de Anchor para especificar qué cuentas son requeridas y cómo se crea la nueva cuenta del contador.

programs/my-program/src/instructions/initialize.rs
use anchor_lang::prelude::*;
use crate::{constants::*, state::Counter};
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Contexto de Cuenta

La estructura Initialize define las cuentas que deben incluirse cuando un usuario llama a la instrucción initialize. Anchor verifica estas cuentas antes de que se ejecute el manejador.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

Cuenta del Pagador

La cuenta payer paga la creación de la cuenta del contador. El tipo Signer<'info> significa que el pagador debe firmar la transacción, y #[account(mut)] indica que la cuenta del pagador puede modificarse porque se deducirán lamports.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

Cuenta del Contador

La cuenta counter almacena los datos Counter de state.rs. init le indica a Anchor que cree esta cuenta antes de que se ejecute el manejador, y payer = payer le indica a Anchor qué cuenta paga por la creación.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

Tamaño de la Cuenta

La restricción space le indica a Anchor cuántos datos de cuenta debe asignar. Anchor almacena primero un discriminador de 8 bytes, luego los bytes necesarios para los campos Counter. El discriminador permite que Anchor reconozca esta cuenta como una cuenta Counter antes de deserializar los datos de la cuenta.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

Dirección del Contador

Las restricciones seeds y bump definen la dirección PDA esperada para la cuenta del contador. Anchor verifica que la cuenta counter proporcionada corresponda a esa dirección. La plantilla usa una PDA para que los usuarios puedan derivar la dirección del contador a partir del ID del programa y el seed, haciendo que la dirección del contador sea determinista.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

System Program

La cuenta system_program es necesaria porque la creación de una nueva cuenta utiliza el System Program.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

Función del Manejador

La función handle_initialize se ejecuta después de que Anchor valida las cuentas en Initialize. El valor ctx le da al manejador acceso a esas cuentas verificadas.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Datos Iniciales

El manejador escribe los primeros valores en la nueva cuenta del contador. El conteo comienza en 0, y el pagador se convierte en la autoridad autorizada para incrementar el contador más adelante.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Cuentas de Transferencia

Esta CPI de transferencia se incluye únicamente para demostrar cómo una CPI pasa cuentas a otro programa. La estructura Transfer enumera las cuentas utilizadas por la transferencia del System Program.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Contexto CPI

CpiContext::new combina el programa que se invoca con las cuentas que se le pasan a dicho programa. Esta es la forma básica de un CPI: elegir el programa a invocar, recopilar las cuentas que ese programa espera y luego pasar ambos a la invocación. Aquí, el programa que se invoca es el System Program.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Invocar Transfer

anchor_lang::system_program::transfer invoca la instrucción de transferencia del System Program. En esta plantilla, la transferencia es un pequeño ejemplo de cómo llamar a otro programa desde tu programa. Si el CPI de transferencia falla, la instrucción initialize también falla.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Registrar Mensaje

msg! escribe un mensaje en los registros del programa. Ok(()) indica que la instrucción se completó correctamente.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Contexto de Cuenta

La estructura Initialize define las cuentas que deben incluirse cuando un usuario llama a la instrucción initialize. Anchor verifica estas cuentas antes de que se ejecute el manejador.

Cuenta del Pagador

La cuenta payer paga la creación de la cuenta del contador. El tipo Signer<'info> significa que el pagador debe firmar la transacción, y #[account(mut)] indica que la cuenta del pagador puede modificarse porque se deducirán lamports.

Cuenta del Contador

La cuenta counter almacena los datos Counter de state.rs. init le indica a Anchor que cree esta cuenta antes de que se ejecute el manejador, y payer = payer le indica a Anchor qué cuenta paga por la creación.

Tamaño de la Cuenta

La restricción space le indica a Anchor cuántos datos de cuenta debe asignar. Anchor almacena primero un discriminador de 8 bytes, luego los bytes necesarios para los campos Counter. El discriminador permite que Anchor reconozca esta cuenta como una cuenta Counter antes de deserializar los datos de la cuenta.

Dirección del Contador

Las restricciones seeds y bump definen la dirección PDA esperada para la cuenta del contador. Anchor verifica que la cuenta counter proporcionada corresponda a esa dirección. La plantilla usa una PDA para que los usuarios puedan derivar la dirección del contador a partir del ID del programa y el seed, haciendo que la dirección del contador sea determinista.

System Program

La cuenta system_program es necesaria porque la creación de una nueva cuenta utiliza el System Program.

Función del Manejador

La función handle_initialize se ejecuta después de que Anchor valida las cuentas en Initialize. El valor ctx le da al manejador acceso a esas cuentas verificadas.

Datos Iniciales

El manejador escribe los primeros valores en la nueva cuenta del contador. El conteo comienza en 0, y el pagador se convierte en la autoridad autorizada para incrementar el contador más adelante.

Cuentas de Transferencia

Esta CPI de transferencia se incluye únicamente para demostrar cómo una CPI pasa cuentas a otro programa. La estructura Transfer enumera las cuentas utilizadas por la transferencia del System Program.

Contexto CPI

CpiContext::new combina el programa que se invoca con las cuentas que se le pasan a dicho programa. Esta es la forma básica de un CPI: elegir el programa a invocar, recopilar las cuentas que ese programa espera y luego pasar ambos a la invocación. Aquí, el programa que se invoca es el System Program.

Invocar Transfer

anchor_lang::system_program::transfer invoca la instrucción de transferencia del System Program. En esta plantilla, la transferencia es un pequeño ejemplo de cómo llamar a otro programa desde tu programa. Si el CPI de transferencia falla, la instrucción initialize también falla.

Registrar Mensaje

msg! escribe un mensaje en los registros del programa. Ok(()) indica que la instrucción se completó correctamente.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

increment.rs

increment.rs define las cuentas necesarias para actualizar una cuenta de contador existente. El manejador verifica que el firmante sea la autoridad almacenada, comprueba que el conteo no haya alcanzado el MAX_COUNT especificado y luego incrementa el conteo.

programs/my-program/src/instructions/increment.rs
use anchor_lang::prelude::*;
use crate::{constants::*, error::ErrorCode, state::Counter};
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Contexto de Cuentas

La estructura Increment define las cuentas que deben incluirse cuando un usuario llama a la instrucción increment. Anchor verifica estas cuentas antes de que se ejecute el manejador.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

Cuenta del Contador

La cuenta counter almacena los datos de Counter. La restricción mut permite al manejador actualizar el conteo almacenado, y las restricciones seeds y bump verifican la dirección PDA del contador.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

Firmante con Autoridad

La cuenta authority debe firmar la transacción. El manejador luego verifica que este firmante coincida con la autoridad almacenada en la cuenta del contador.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

Función Handler

La función handle_increment se ejecuta después de que Anchor valida las cuentas en Increment. El valor ctx le da al handler acceso a esas cuentas verificadas.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Verificación de Autoridad

La primera verificación se asegura de que el firmante tenga permiso para actualizar este contador. Si la dirección del firmante no coincide con counter.authority, la instrucción se detiene con ErrorCode::Unauthorized. Esto demuestra la autorización a nivel de aplicación: el programa es dueño de los datos del contador, pero implementa una regla sobre qué firmante puede modificar esos datos.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Verificación del Conteo Máximo

La segunda verificación evita que el contador supere MAX_COUNT. Si el contador ya alcanzó el límite, la instrucción se detiene con ErrorCode::CounterOverflow. Este límite es una regla artificial en la plantilla para que puedas ver cómo los errores personalizados detienen una instrucción antes de que se modifiquen los datos de la cuenta.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Actualizar Contador

Solo después de que ambas verificaciones sean exitosas, el handler actualiza los datos de la cuenta. Esta línea suma uno al valor del contador almacenado.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Mensaje de Registro

msg! escribe el conteo actualizado en los registros del programa. Ok(()) indica que la instrucción se completó con éxito.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Contexto de Cuentas

La estructura Increment define las cuentas que deben incluirse cuando un usuario llama a la instrucción increment. Anchor verifica estas cuentas antes de que se ejecute el manejador.

Cuenta del Contador

La cuenta counter almacena los datos de Counter. La restricción mut permite al manejador actualizar el conteo almacenado, y las restricciones seeds y bump verifican la dirección PDA del contador.

Firmante con Autoridad

La cuenta authority debe firmar la transacción. El manejador luego verifica que este firmante coincida con la autoridad almacenada en la cuenta del contador.

Función Handler

La función handle_increment se ejecuta después de que Anchor valida las cuentas en Increment. El valor ctx le da al handler acceso a esas cuentas verificadas.

Verificación de Autoridad

La primera verificación se asegura de que el firmante tenga permiso para actualizar este contador. Si la dirección del firmante no coincide con counter.authority, la instrucción se detiene con ErrorCode::Unauthorized. Esto demuestra la autorización a nivel de aplicación: el programa es dueño de los datos del contador, pero implementa una regla sobre qué firmante puede modificar esos datos.

Verificación del Conteo Máximo

La segunda verificación evita que el contador supere MAX_COUNT. Si el contador ya alcanzó el límite, la instrucción se detiene con ErrorCode::CounterOverflow. Este límite es una regla artificial en la plantilla para que puedas ver cómo los errores personalizados detienen una instrucción antes de que se modifiquen los datos de la cuenta.

Actualizar Contador

Solo después de que ambas verificaciones sean exitosas, el handler actualiza los datos de la cuenta. Esta línea suma uno al valor del contador almacenado.

Mensaje de Registro

msg! escribe el conteo actualizado en los registros del programa. Ok(()) indica que la instrucción se completó con éxito.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

Archivo de prueba

programs/my-program/tests/test_initialize.rs es una prueba de integración en Rust. No incia un validator local. En su lugar, carga el archivo compilado .so en LiteSVM, construye transacciones que llaman al programa y lee la cuenta del contador después de cada transacción. La prueba construye instrucciones para una transacción de Solana especificando el ID del programa a invocar, proporcionando instruction data y pasando las cuentas requeridas.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
test_initialize.rs
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Initialize {}.data(),
my_program::accounts::Initialize {
payer: payer.pubkey(),
counter,
system_program: system_program::ID,
}
.to_account_metas(None),
);

El contexto de cuentas Initialize define las cuentas requeridas por la instrucción initialize. La prueba pasa esas mismas cuentas al helper generado my_program::accounts::Initialize:

  • payer se pasa como payer: payer.pubkey().
  • counter se pasa como counter.
  • system_program se pasa como system_program::ID.

my_program::instruction::Initialize {}.data() crea el instruction data. Aquí es donde se codificarían los argumentos de instrucción, pero esta instrucción initialize no requiere ningún argumento.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
test_initialize.rs
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Increment {}.data(),
my_program::accounts::Increment {
counter,
authority: payer.pubkey(),
}
.to_account_metas(None),
);

El contexto de cuentas Increment define las cuentas requeridas por la instrucción increment. La prueba pasa esas mismas cuentas al helper generado my_program::accounts::Increment:

  • counter se pasa como counter.
  • authority se pasa como authority: payer.pubkey().

my_program::instruction::Increment {}.data() crea el instruction data. Aquí es donde se codificarían los argumentos de instrucción, pero esta instrucción increment no requiere ningún argumento.

programs/my-program/tests/test_initialize.rs
use {
anchor_lang::{
prelude::Pubkey,
solana_program::{instruction::Instruction, system_program},
AccountDeserialize, InstructionData, ToAccountMetas,
},
litesvm::LiteSVM,
solana_keypair::Keypair,
solana_message::{Message, VersionedMessage},
solana_signer::Signer,
solana_transaction::versioned::VersionedTransaction,
};
#[test]
fn test_initialize() {
let program_id = my_program::id();
let payer = Keypair::new();
let counter = Pubkey::find_program_address(
&[my_program::constants::COUNTER_SEED],
&program_id,
)
.0;
let mut svm = LiteSVM::new();
let bytes = include_bytes!(concat!(
env!("CARGO_TARGET_TMPDIR"),
"/../deploy/my_program.so"
));
svm.add_program(program_id, bytes).unwrap();
svm.airdrop(&payer.pubkey(), 1_000_000_000).unwrap();
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Initialize {}.data(),
my_program::accounts::Initialize {
payer: payer.pubkey(),
counter,
system_program: system_program::ID,
}
.to_account_metas(None),
);
let blockhash = svm.latest_blockhash();
let msg = Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[&payer]).unwrap();
let res = svm.send_transaction(tx);
assert!(res.is_ok());
let counter_account = svm.get_account(&counter).unwrap();
let mut data: &[u8] = &counter_account.data;
let counter_state = my_program::state::Counter::try_deserialize(&mut data).unwrap();
assert_eq!(counter_state.count, 0);
assert_eq!(counter_state.authority, payer.pubkey());
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Increment {}.data(),
my_program::accounts::Increment {
counter,
authority: payer.pubkey(),
}
.to_account_metas(None),
);
let blockhash = svm.latest_blockhash();
let msg = Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[&payer]).unwrap();
let res = svm.send_transaction(tx);
assert!(res.is_ok());
let counter_account = svm.get_account(&counter).unwrap();
let mut data: &[u8] = &counter_account.data;
let counter_state = my_program::state::Counter::try_deserialize(&mut data).unwrap();
assert_eq!(counter_state.count, 1);
assert_eq!(counter_state.authority, payer.pubkey());
}

Configuración del proyecto

Los archivos raíz del proyecto indican a Anchor y Cargo cómo compilar, probar e implementar el programa. Para una referencia completa, consulta la documentación de Anchor para la configuración de Anchor.toml y la CLI de Anchor.

Anchor.toml
skip_local_validator = true
[toolchain]
[features]
resolution = true
skip-lint = false
[programs.localnet]
my_program = "82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd"
[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"
[scripts]
test = "cargo test"
[hooks]

Is this page helpful?

Tabla de Contenidos

Editar Página