En los tutoriales anteriores, el parámetro seeds=[] siempre estaba vacío. Si le introducimos datos, se comporta como una clave o claves en un mapping de Solidity.
Considera el siguiente ejemplo:
contract ExampleMapping {
struct SomeNum {
uint64 num;
}
mapping(uint64 => SomeNum) public exampleMap;
function setExampleMap(uint64 key, uint64 val) public {
exampleMap[key] = SomeNum(val);
}
}
Ahora creamos un programa de Solana Anchor example_map.
Inicializando un mapping: Rust
Al principio, solo mostraremos el paso de inicialización porque introducirá una nueva sintaxis que necesitamos explicar.
use anchor_lang::prelude::*;
use std::mem::size_of;
declare_id!("DntexDPByFxpVeBSjd6nLqQQSqZmSaDkP8TUbcJ9jAgt");
#[program]
pub mod example_map {
use super::*;
pub fn initialize(ctx: Context<Initialize>, key: u64) -> Result<()> {
Ok(())
}
}
#[derive(Accounts)]
#[instruction(key: u64)]
pub struct Initialize<'info> {
#[account(init,
payer = signer,
space = size_of::<Val>() + 8,
seeds=[&key.to_le_bytes().as_ref()],
bump)]
val: Account<'info, Val>,
#[account(mut)]
signer: Signer<'info>,
system_program: Program<'info, System>,
}
#[account]
pub struct Val {
value: u64,
}
Así es como puedes pensar en el map:
El parámetro de semillas (seeds) key en &key.to_le_bytes().as_ref() puede considerarse como una “clave” para el map, de manera similar a la construcción en Solidity:
mapping(uint256 => uint256) myMap;
myMap[key] = val
Las partes menos familiares del código son #[instruction(key: u64)] y seeds=[&key.to_le_bytes().as_ref()].
seeds = [&key.to_le_bytes().as_ref()]
Se espera que los elementos en seeds sean bytes. Sin embargo, estamos pasando un u64, que no es de tipo bytes. Para convertirlo a bytes, usamos to_le_bytes(). El “le” significa “little endian”. Las semillas (seeds) no tienen que estar codificadas obligatoriamente como bytes little endian, simplemente elegimos eso para este ejemplo. Big endian también funciona siempre que seas consistente. Para convertir a big endian, habríamos usado to_be_bytes().
#[instruction(key: u64)]
Para poder “pasar” el argumento de función key en initialize(ctx: Context<Initialize>, key: u64) necesitamos usar la macro instruction, de lo contrario, nuestra macro init no tendría forma de “ver” el argumento key de initialize.
Inicializando un mapping: Typescript
El código a continuación muestra cómo inicializar la cuenta:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { ExampleMap } from "../target/types/example_map";
describe("example_map", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.ExampleMap as Program<ExampleMap>;
it("Initialize mapping storage", async () => {
const key = new anchor.BN(42);
const seeds = [key.toArrayLike(Buffer, "le", 8)];
let valueAccount = anchor.web3.PublicKey.findProgramAddressSync(
seeds,
program.programId
)[0];
await program.methods.initialize(key).accounts({val: valueAccount}).rpc();
});
});
El código key.toArrayLike(Buffer, "le", 8) especifica que estamos intentando crear un búfer de bytes de un tamaño de 8 bytes usando el valor de key. Elegimos 8 bytes porque nuestra clave es de 64 bits, y 64 bits son 8 bytes. El “le” corresponde a little endian para que coincida con el código en Rust.
Cada “valor” (value) en el mapping es una cuenta separada y debe inicializarse por separado.
Establecer un mapping: Rust
El código adicional en Rust que necesitamos para establecer el valor. Toda la sintaxis aquí debería resultarte familiar.
// inside the #[program] module
pub fn set(ctx: Context<Set>, key: u64, val: u64) -> Result<()> {
ctx.accounts.val.value = val;
Ok(())
}
//...
#[derive(Accounts)]
#[instruction(key: u64)]
pub struct Set<'info> {
#[account(mut)]
val: Account<'info, Val>,
}
Establecer y leer un mapping: Typescript
Dado que derivamos la dirección de la cuenta donde se almacena el valor en el cliente (Typescript), leemos y escribimos en ella tal como lo hacemos con las cuentas que tienen el array seeds vacío. La sintaxis para leer los datos de la cuenta de Solana y escribir es idéntica a la de los tutoriales anteriores:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { ExampleMap } from "../target/types/example_map";
describe("example_map", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.ExampleMap as Program<ExampleMap>;
it("Initialize and set value", async () => {
const key = new anchor.BN(42);
const value = new anchor.BN(1337);
const seeds = [key.toArrayLike(Buffer, "le", 8)];
let valueAccount = anchor.web3.PublicKey.findProgramAddressSync(
seeds,
program.programId
)[0];
await program.methods.initialize(key).accounts({val: valueAccount}).rpc();
// set the account
await program.methods.set(key, value).accounts({val: valueAccount}).rpc();
// read the account back
let result = await program.account.val.fetch(valueAccount);
console.log(`the value ${result.value} was stored in ${valueAccount.toBase58()}`);
});
});
Aclarando los “nested mappings”
En lenguajes como Python o JavaScript, un nested mapping verdadero es un hashmap que apunta a otro hash map.
En Solidity, sin embargo, los “nested mappings” son solo un map único con múltiples claves comportándose como si fueran una sola clave.
En un nested mapping “verdadero”, puedes proporcionar solo la primera clave y obtener otro hashmap como resultado.
Los “nested mappings” de Solidity no son “verdaderos” nested mappings: no puedes proporcionar una sola clave y obtener un map de vuelta; debes proporcionar todas las claves y obtener el resultado final.
Si utilizas seeds para simular nested mappings de manera similar a Solidity, te enfrentarás a la misma restricción. Debes proporcionar todas las seeds; Solana no aceptará solo una seed.
Inicializando un nested mapping: Rust
El array seeds puede contener tantos elementos como queramos, de manera similar a un nested mapping en Solidity. Por supuesto, está sujeto a los límites de cómputo impuestos en cada transacción. El código para realizar la inicialización y asignación se muestra a continuación.
No necesitamos ninguna sintaxis especial para hacer esto, es solo cuestión de tomar más argumentos en la función y colocar más elementos en seeds, por lo que mostraremos el código completo sin mayor explicación.
Nested mapping en Rust
use anchor_lang::prelude::*;
use std::mem::size_of;
declare_id!("DntexDPByFxpVeBSjd6nLqQQSqZmSaDkP8TUbcJ9jAgt");
#[program]
pub mod example_map {
use super::*;
pub fn initialize(ctx: Context<Initialize>, key1: u64, key2: u64) -> Result<()> {
Ok(())
}
pub fn set(ctx: Context<Set>, key1: u64, key2: u64, val: u64) -> Result<()> {
ctx.accounts.val.value = val;
Ok(())
}
}
#[derive(Accounts)]
#[instruction(key1: u64, key2: u64)] // new key args added
pub struct Initialize<'info> {
#[account(init,
payer = signer,
space = size_of::<Val>() + 8,
seeds=[&key1.to_le_bytes().as_ref(), &key2.to_le_bytes().as_ref()], // 2 seeds
bump)]
val: Account<'info, Val>,
#[account(mut)]
signer: Signer<'info>,
system_program: Program<'info, System>,
}
#[derive(Accounts)]
#[instruction(key1: u64, key2: u64)] // new key args added
pub struct Set<'info> {
#[account(mut)]
val: Account<'info, Val>,
}
#[account]
pub struct Val {
value: u64,
}
Nested mapping en Typescript
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { ExampleMap } from "../target/types/example_map";
describe("example_map", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.ExampleMap as Program<ExampleMap>;
it("Initialize and set value", async () => {
// we now have two keys
const key1 = new anchor.BN(42);
const key2 = new anchor.BN(43);
const value = new anchor.BN(1337);
// seeds has two values
const seeds = [key1.toArrayLike(Buffer, "le", 8), key2.toArrayLike(Buffer, "le", 8)];
let valueAccount = anchor.web3.PublicKey.findProgramAddressSync(
seeds,
program.programId
)[0];
// functions now take two keys
await program.methods.initialize(key1, key2).accounts({val: valueAccount}).rpc();
await program.methods.set(key1, key2, value).accounts({val: valueAccount}).rpc();
// read the account back
let result = await program.account.val.fetch(valueAccount);
console.log(`the value ${result.value} was stored in ${valueAccount.toBase58()}`);
});
});
Ejercicio: Modifica el código anterior para formar un nested mapping que reciba tres claves.
Inicializando más de un map
Una manera directa de lograr tener más de un map es añadir otra variable al array seeds y tratarla como una forma de “indexar” el primer map, el segundo map, y así sucesivamente.
El siguiente código muestra un ejemplo de cómo inicializar which_map, que solo contiene una clave.
#[derive(Accounts)]
#[instruction(which_map: u64, key: u64)]
pub struct InitializeMap<'info> {
#[account(init,
payer = signer,
space = size_of::<Val1>() + 8,
seeds=[&which_map.to_le_bytes().as_ref(), &key.to_le_bytes().as_ref()],
bump)]
val: Account<'info, Val1>,
#[account(mut)]
signer: Signer<'info>,
system_program: Program<'info, System>,
}
Ejercicio: Completa el código de Rust y Typescript para crear un programa que tenga dos mappings: el primero con una sola clave y el segundo con dos claves. Piensa en cómo convertir un map de dos niveles en un map de un solo nivel cuando se especifica el primer map.
Aprende Solana con RareSkills
Consulta nuestro curso de Solana para ver el resto de nuestros tutoriales sobre Solana.
Publicado originalmente el 27 de febrero de 2024