Comenzando con el Marco de Trabajo de Anchor

El marco de trabajo de Anchor utiliza macros de Rust para reducir el código repetitivo y simplificar la implementación de las comprobaciones de seguridad necesarias para escribir programas en Solana.

Piensa en Anchor como un marco de trabajo para hacer programas en Solana, del mismo modo que Next.js lo es para desarrollar aplicaciones web. Al igual que Next.js permite a los desarrolladores crear sitios web utilizando React en lugar de depender únicamente de HTML y TypeScript, Anchor proporciona un conjunto de herramientas y abstracciones que hacen que la creación de programas en Solana sea más intuitiva y segura.

Las macros principales encontradas en un programa de Anchor incluyen:

  • declare_id: Especifica la dirección del programa en la cadena de bloques
  • #[program]: Especifica el módulo que contiene la lógica de las instrucciones del programa
  • #[derive(Accounts)]: Se aplica a un struct para indicar la lista de cuentas que requiere una instrucción
  • #[account]: Se aplica a un struct para crear una cuenta personalizada y específica del programa

Programas de Anchor #

A continuación se muestra un programa de Anchor sencillo con una única instrucción que crea una nueva cuenta. Lo vamos a recorrer explicando la estructura básica de un programa de Anchor. Aquí está el programa en Solana Playground.

lib.rs
use anchor_lang::prelude::*;
 
declare_id!("11111111111111111111111111111111");
 
#[program]
mod hello_anchor {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
        ctx.accounts.new_account.data = data;
        msg!("Changed data to: {}!", data);
        Ok(())
    }
}
 
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[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,
}

Macro declare_id #

La macro declare_id se utiliza para especificar la dirección del programa (ID del programa) en la cadena de bloques.

lib.rs
use anchor_lang::prelude::*;
 
declare_id!("11111111111111111111111111111111");

Cuando se compila un programa de Anchor por primera vez, el marco de trabajo genera un nuevo par de llaves (keypair) utilizado para desplegar el programa (a menos que se especifique lo contrario). La llave pública de este par de llaves debe utilizarse como ID del programa en la macro declare_id.

  • Cuando se utiliza Solana Playground, el ID del programa se actualiza automáticamente y se puede exportar utilizando la interfaz de usuario.
  • Cuando se trabaja localmente, el par de llaves del programa se encuentra en /target/deploy/el_nombre_de_tu_programa.json

Macro program #

La macro #[program] especifica el módulo que contiene todas las instrucciones del programa. Cada función pública del módulo representa una instrucción independiente para el programa.

En cada función, el primer parámetro es siempre de tipo Context. Los parámetros siguientes, que son opcionales, definen cualquier data adicional requerida por la instrucción.

lib.rs
use anchor_lang::prelude::*;
 
declare_id!("11111111111111111111111111111111");
 
#[program]
mod hello_anchor {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
        ctx.accounts.new_account.data = data;
        msg!("Changed data to: {}!", data);
        Ok(())
    }
}
 
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[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,
}

El tipo Context proporciona a la instrucción acceso a las siguientes entradas sin argumentos:

pub struct Context<'a, 'b, 'c, 'info, T> {
    /// Id del programa actualmente en ejecución.
    pub program_id: &'a Pubkey,
    /// Cuentas deserializadas.
    pub accounts: &'b mut T,
    /// Cuentas restantes dadas pero no deserializadas o validadas.
    /// Tenga mucho cuidado al utilizarlo directamente.
    pub remaining_accounts: &'c [AccountInfo<'info>],
    /// Bump seeds encontradas durante la validación de restricciones. Esto
    /// se proporciona como una conveniencia para que los handlers no tengan
    /// que volver a calcular las semillas de protuberancia o pasarlas como argumentos.
    pub bumps: BTreeMap<String, u8>,
}

Context es un tipo genérico donde T representa el conjunto de cuentas requeridas por una instrucción. Al definir el Context de la instrucción, el tipo T es un struct que implementa el trait Accounts (Context<Initialize>).

Este parámetro de Context permite a la instrucción acceder a:

  • ctx.accounts: Las cuentas de la instrucción
  • ctx.program_id: La dirección del programa
  • ctx.remaining_accounts: Todas las cuentas restantes proporcionadas a la instrucción pero no especificadas en el struct de Accounts
  • ctx.bumps: Bump seeds para cualquier cuenta Program Derived Address (PDA) especificada en el structde Accounts

Macro derive(Accounts) #

La macro #[derive(Accounts)] se aplica a un struct e implementa el trait Accounts. Se utiliza para especificar y validar un conjunto de cuentas necesarias para una instrucción concreta.

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[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>,
}

Cada campo del struct representa una cuenta requerida por la instrucción. La denominación de cada campo es arbitraria, pero se recomienda utilizar un nombre descriptivo que indique la finalidad de la cuenta.

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[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>,
}

Cuando se construyen programas en Solana, es esencial validar las cuentas proporcionadas por el cliente. Esta validación se consigue en Anchor mediante restricciones de cuenta y especificando los tipos de cuenta adecuados:

  • Restricciones de cuenta: Las restricciones definen condiciones adicionales que una cuenta debe satisfacer para ser considerada válida para la instrucción. Las restricciones se aplican mediante el atributo #[account(..)], que se coloca sobre un campo de cuenta en el struct de Accounts.

    #[derive(Accounts)]
    pub struct Initialize<'info> {
        #[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>,
    }
  • Tipos de cuenta: Anchor proporciona varios tipos de cuenta para ayudar a garantizar que la cuenta proporcionada por el cliente coincida con lo que espera el programa.

    #[derive(Accounts)]
    pub struct Initialize<'info> {
        #[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>,
    }

Las cuentas dentro del struct de Accounts son accesibles en una instrucción a través del Context, utilizando la sintaxis ctx.accounts.

lib.rs
use anchor_lang::prelude::*;
 
declare_id!("11111111111111111111111111111111");
 
#[program]
mod hello_anchor {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
        ctx.accounts.new_account.data = data;
        msg!("Changed data to: {}!", data);
        Ok(())
    }
}
 
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[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,
}

Cuando se invoca una instrucción en un programa de Anchor, se realizan las siguientes comprobaciones según se especifica en el struct de Accounts:

  • Verificación del tipo de cuenta: Verifica que las cuentas pasadas a la instrucción corresponden con los tipos de cuenta definidos en el Context de la instrucción.

  • Comprobación de restricciones: Comprueba las cuentas frente a cualquier restricción adicional especificada.

Esto ayuda a garantizar que las cuentas pasadas a la instrucción desde el cliente son válidas. Si falla alguna comprobación, la instrucción falla con un error antes de llegar a la lógica principal de la función asociada a la instrucción.

Para obtener ejemplos más detallados, consulte las secciones restricciones y tipos de cuenta de la documentación de Anchor.

Macro account #

La macro #[account] se aplica a un struct para definir el tipo de datos alojados en una cuenta personalizada de un programa. Cada campo del struct representa un campo que se almacenará en los datos de la cuenta.

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

Esta macro implementa varios traits detallados aquí. Las principales funciones de la macro #[account] son las siguientes:

  • Asignar propiedad: Al crear una cuenta, la propiedad de la misma se asigna automáticamente al programa especificado en el declare_id.
  • Establecer un discriminator: Un discriminador único de 8 bytes, específico para el tipo de cuenta, se añade como los primeros 8 bytes de los datos de la cuenta durante su inicialización. Esto ayuda a diferenciar los tipos de cuenta y la validación de cuentas.
  • Serialización y deserialización de datos: Los datos de la cuenta correspondientes al tipo de cuenta se serializan y deserializan automáticamente.
lib.rs
use anchor_lang::prelude::*;
 
declare_id!("11111111111111111111111111111111");
 
#[program]
mod hello_anchor {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
        ctx.accounts.new_account.data = data;
        msg!("Changed data to: {}!", data);
        Ok(())
    }
}
 
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[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,
}

En Anchor, un discriminador de cuenta es un identificador de 8 bytes, único para cada tipo de cuenta. Este identificador se obtiene a partir de los 8 primeros bytes del hash SHA256 del nombre del tipo de la cuenta. Los 8 primeros bytes de los datos de una cuenta se reservan específicamente para este discriminador.

#[account(init, payer = signer, space = 8 + 8)]
pub new_account: Account<'info, NewAccount>,

El discriminador se utiliza en las dos situaciones siguientes:

  • Inicialización: Durante la inicialización de una cuenta, al discriminador se le asigna el discriminador del tipo de cuenta.
  • Deserialización: Cuando se deserializan los datos de la cuenta, el discriminador dentro de los datos se comprueba con el discriminador esperado del tipo de cuenta.

Si hay una disparidad, indica que el cliente ha proporcionado una cuenta inesperada. Este mecanismo sirve como validación de cuentas en los programas en Anchor, garantizando que se utilizan las cuentas correctas.

Archivo IDL #

Cuando se construye un programa en Anchor, este genera un archivo de lenguaje de descripción de interfaz que representa la estructura del programa. Este archivo IDL proporciona un formato estandarizado basado en JSON para construir instrucciones de programa y obtener cuentas de programa.

A continuación se muestran ejemplos de cómo se relaciona un archivo IDL con el código del programa.

Instrucciones #

El arreglo instructions del IDL corresponde con las instrucciones del programa y especifica las cuentas y parámetros necesarios para cada instrucción.

IDL.json
{
  "version": "0.1.0",
  "name": "hello_anchor",
  "instructions": [
    {
      "name": "initialize",
      "accounts": [
        { "name": "newAccount", "isMut": true, "isSigner": true },
        { "name": "signer", "isMut": true, "isSigner": true },
        { "name": "systemProgram", "isMut": false, "isSigner": false }
      ],
      "args": [{ "name": "data", "type": "u64" }]
    }
  ],
  "accounts": [
    {
      "name": "NewAccount",
      "type": {
        "kind": "struct",
        "fields": [{ "name": "data", "type": "u64" }]
      }
    }
  ]
}
lib.rs
use anchor_lang::prelude::*;
 
declare_id!("11111111111111111111111111111111");
 
#[program]
mod hello_anchor {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
        ctx.accounts.new_account.data = data;
        msg!("Changed data to: {}!", data);
        Ok(())
    }
}
 
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[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,
}

Cuentas #

El arreglo accounts del IDL corresponde con structs de la macro #[account], que especifica la estructura de las cuentas de datos del programa.

IDL.json
{
  "version": "0.1.0",
  "name": "hello_anchor",
  "instructions": [
    {
      "name": "initialize",
      "accounts": [
        { "name": "newAccount", "isMut": true, "isSigner": true },
        { "name": "signer", "isMut": true, "isSigner": true },
        { "name": "systemProgram", "isMut": false, "isSigner": false }
      ],
      "args": [{ "name": "data", "type": "u64" }]
    }
  ],
  "accounts": [
    {
      "name": "NewAccount",
      "type": {
        "kind": "struct",
        "fields": [{ "name": "data", "type": "u64" }]
      }
    }
  ]
}
lib.rs
use anchor_lang::prelude::*;
 
declare_id!("11111111111111111111111111111111");
 
#[program]
mod hello_anchor {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
        ctx.accounts.new_account.data = data;
        msg!("Changed data to: {}!", data);
        Ok(())
    }
}
 
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[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,
}
 

Cliente #

Anchor proporciona una biblioteca cliente Typescript (@coral-xyz/anchor) que simplifica el proceso de interacción con los programas de Solana desde el cliente.

Para utilizar la biblioteca cliente, primero se debe crear una instancia de un Program utilizando el archivo IDL generado por Anchor.

Program en el Cliente #

La creación de una instancia del Program requiere la IDL del programa, su dirección en la cadena de bloques (programId) y un AnchorProvider. Un AnchorProvider combina dos cosas:

  • Connection - la conexión a un clúster de Solana (es decir, localhost, devnet, mainnet)
  • Wallet - (opcional) una billetera por defecto utilizada para pagar y firmar transacciones

Al construir un programa Anchor localmente, la configuración para crear una instancia del Program se hace automáticamente en el archivo de prueba. El archivo IDL se encuentra en la carpeta /target.

import * as anchor from "@coral-xyz/anchor";
import { Program, BN } from "@coral-xyz/anchor";
import { HelloAnchor } from "../target/types/hello_anchor";
 
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.HelloAnchor as Program<HelloAnchor>;

Cuando se integre con un frontend utilizando el adaptador de billeteras, tendrá que configurar manualmente el AnchorProvider y el Program.

import { Program, Idl, AnchorProvider, setProvider } from "@coral-xyz/anchor";
import { useAnchorWallet, useConnection } from "@solana/wallet-adapter-react";
import { IDL, HelloAnchor } from "./idl";
 
const { connection } = useConnection();
const wallet = useAnchorWallet();
 
const provider = new AnchorProvider(connection, wallet, {});
setProvider(provider);
 
const programId = new PublicKey("...");
const program = new Program<HelloAnchor>(IDL, programId);

Alternativamente, puedes crear una instancia del Program utilizando solo el IDL y la Connection a un clúster de Solana. Esto significa que no hay una Wallet por defecto, pero le permite utilizar el Program para obtener cuentas antes de que se conecte una wallet.

import { Program } from "@coral-xyz/anchor";
import { clusterApiUrl, Connection, PublicKey } from "@solana/web3.js";
import { IDL, HelloAnchor } from "./idl";
 
const programId = new PublicKey("...");
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
 
const program = new Program<HelloAnchor>(IDL, programId, {
  connection,
});

Invocar instrucciones #

Una vez configurado el Program, puedes utilizar el MethodsBuilder de Anchor para construir una instrucción, una transacción, o construir y enviar una transacción. El formato básico es el siguiente:

  • program.methods - Esta es la API del constructor para crear llamadas a instrucciones relacionadas con el IDL del programa
  • .instructionName - Instrucción específica del IDL del programa, pasando cualquier dato de la instrucción como valores separados por comas
  • .accounts - Introduce la dirección de cada cuenta requerida por la instrucción tal y como se especifica en el IDL
  • .signers - Opcionalmente pasa un arreglo de pares de llaves que deben firmar la instrucción
await program.methods
  .instructionName(instructionData1, instructionData2)
  .accounts({})
  .signers([])
  .rpc();

A continuación se muestran ejemplos de cómo invocar una instrucción utilizando el MethodsBuilder.

#

El método rpc() envía una transacción firmada con la instrucción especificada y devuelve una TransactionSignature. Cuando se utiliza .rpc, la Wallet del Provider se incluye automáticamente como firmante.

// Genera el par de llaves para la cuenta nueva
const newAccountKp = new Keypair();
 
const data = new BN(42);
const transactionSignature = await program.methods
  .initialize(data)
  .accounts({
    newAccount: newAccountKp.publicKey,
    signer: wallet.publicKey,
    systemProgram: SystemProgram.programId,
  })
  .signers([newAccountKp])
  .rpc();

#

El método transaction() construye una Transaction y añade la instrucción especificada a la transacción (sin enviarla automáticamente).

// Genera un par de llaves para la cuenta nueva
const newAccountKp = new Keypair();
 
const data = new BN(42);
const transaction = await program.methods
  .initialize(data)
  .accounts({
    newAccount: newAccountKp.publicKey,
    signer: wallet.publicKey,
    systemProgram: SystemProgram.programId,
  })
  .transaction();
 
const transactionSignature = await connection.sendTransaction(transaction, [
  wallet.payer,
  newAccountKp,
]);

#

El método instruction() construye una TransactionInstruction utilizando la instrucción especificada. Esto es útil si desea añadir manualmente la instrucción a una transacción y combinarla con otras instrucciones.

// Genera un par de llaves para la cuenta nueva
const newAccountKp = new Keypair();
 
const data = new BN(42);
const instruction = await program.methods
  .initialize(data)
  .accounts({
    newAccount: newAccountKp.publicKey,
    signer: wallet.publicKey,
    systemProgram: SystemProgram.programId,
  })
  .instruction();
 
const transaction = new Transaction().add(instruction);
 
const transactionSignature = await connection.sendTransaction(transaction, [
  wallet.payer,
  newAccountKp,
]);

Solicitar cuentas #

El cliente Program también permite buscar y filtrar fácilmente cuentas de programas. Basta con utilizar program.account y luego especificar el nombre del tipo de cuenta en el IDL. Seguidamente, Anchor deserializa y devuelve todas las cuentas especificadas.

#

Utilice all() para obtener todas las cuentas existentes para un tipo de cuenta específico.

const accounts = await program.account.newAccount.all();

#

Utilice memcmp para filtrar las cuentas que almacenan datos que coinciden con un valor específico en un offset específico. Al calcular el offset, recuerde que los 8 primeros bytes están reservados para el discriminador de cuentas creado por el programa de Anchor. El uso de memcmp requiere que se conozca la posición de los bytes del campo de datos para el tipo de cuenta que está solicitando.

const accounts = await program.account.newAccount.all([
  {
    memcmp: {
      offset: 8,
      bytes: "",
    },
  },
]);

#

Utilice fetch() para obtener los datos de una cuenta específica introduciendo la dirección de la cuenta

const account = await program.account.newAccount.fetch(ACCOUNT_ADDRESS);

#

Utilice fetchMultiple() para obtener los datos de varias cuentas introduciendo un arreglo de direcciones de cuenta

const accounts = await program.account.newAccount.fetchMultiple([
  ACCOUNT_ADDRESS_ONE,
  ACCOUNT_ADDRESS_TWO,
]);