Este tutorial muestra cómo leer datos de cuentas directamente desde el cliente Javascript de Solana web3 para que una aplicación web pueda leerlos en el frontend.
En el tutorial anterior usamos solana account <account address> para leer los datos que escribimos, pero esto no funcionará si estamos construyendo una dApp en un sitio web.
En su lugar, debemos calcular la dirección de la cuenta de almacenamiento, leer los datos y deserializarlos desde el cliente de Solana web3.
Imagina que en Ethereum quisiéramos evitar el uso de variables públicas o funciones de vista (view functions), pero aún así quisiéramos mostrar sus valores en el frontend. Para ver el valor en las variables de almacenamiento sin hacerlas públicas ni añadir una función de vista, usaríamos la API getStorageAt(contract_address, slot). Vamos a hacer algo similar en Solana, excepto que en lugar de pasar el par (contract_address, slot), simplemente pasamos la dirección del programa y derivamos la dirección de su(s) cuenta(s) de almacenamiento.
Aquí está el código en Rust del tutorial anterior. Inicializa MyStorage y escribe en x usando la función set. No lo modificaremos en este tutorial:
use anchor_lang::prelude::*;
use std::mem::size_of;
declare_id!("GLKUcCtHx6nkuDLTz5TNFrR4tt4wDNuk24Aid2GrDLC6");
#[program]
pub mod basic_storage {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
pub fn set(ctx: Context<Set>, new_x: u64) -> Result<()> {
ctx.accounts.my_storage.x = new_x;
Ok(())
}
}
#[derive(Accounts)]
pub struct Set<'info> {
#[account(mut, seeds = [], bump)]
pub my_storage: Account<'info, MyStorage>,
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init,
payer = signer,
space=size_of::<MyStorage>() + 8,
seeds = [],
bump)]
pub my_storage: Account<'info, MyStorage>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct MyStorage {
x: u64,
}
A continuación se muestra la prueba unitaria en Typescript que:
- inicializa la cuenta
- escribe
170en el almacenamiento - lee el valor de vuelta usando la función
fetch:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { BasicStorage } from "../target/types/basic_storage";
describe("basic_storage", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.BasicStorage as Program<BasicStorage>;
it("Is initialized!", async () => {
const seeds = []
const [myStorage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);
console.log("the storage account address is", myStorage.toBase58());
await program.methods.initialize().accounts({myStorage: myStorage}).rpc();
await program.methods.set(new anchor.BN(170)).accounts({myStorage: myStorage}).rpc();
// ***********************************
// *** NEW CODE TO READ THE STRUCT ***
// ***********************************
let myStorageStruct = await program.account.myStorage.fetch(myStorage);
console.log("The value of x is:",myStorageStruct.x.toString());
});
});
Ver una cuenta en Anchor se puede hacer con:
let myStorageStruct = await program.account.myStorage.fetch(myStorage);
console.log("The value of x is:", myStorageStruct.x.toString());
Anchor está calculando automáticamente la dirección de la cuenta MyStorage, leyéndola y formateándola como un objeto de Typescript.
Para entender cómo Anchor está convirtiendo mágicamente el struct de Rust en un struct de Typescript, echemos un vistazo al IDL en target/idl/basic_storage.json. Hacia el final del JSON, podemos ver una definición del struct que nuestro programa está creando:
"accounts": [
{
"name": "MyStorage",
"type": {
"kind": "struct",
"fields": [
{
"name": "x",
"type": "u64"
}
]
}
}
],
Este método solo funciona para cuentas que tu programa o cliente haya inicializado o creado y para las que tengas el IDL, no funcionará para una cuenta arbitraria.
Es decir, si eliges una cuenta aleatoria en Solana y usas el código anterior, la deserialización casi con certeza fallará. Más adelante en este artículo leeremos la cuenta de una manera más “cruda” (raw).
La función fetch no es mágica. Entonces, ¿cómo hacemos esto para una cuenta que no creamos?
Obtener datos de cuentas creadas por programas de Solana con Anchor
Si conocemos el IDL de otro programa que fue creado con Anchor, podemos leer convenientemente los datos de su cuenta.
Vamos a ejecutar anchor init para otro programa en otra terminal, luego haremos que inicialice una cuenta, y después estableceremos una única variable booleana en ese struct como true. Llamaremos a esta otra cuenta other_program y al struct que almacena su booleano TrueOrFalse:
use anchor_lang::prelude::*;
use std::mem::size_of;
declare_id!("4z4dduMSFKFJDnUAKaHnbhHySK8x1PwgArUBXzksjwa8");
#[program]
pub mod other_program {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
pub fn setbool(ctx: Context<SetFlag>, flag: bool) -> Result<()> {
ctx.accounts.true_or_false.flag = flag;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
signer: Signer<'info>,
system_program: Program<'info, System>,
#[account(init, payer = signer, space = size_of::<TrueOrFalse>() + 8, seeds=[], bump)]
true_or_false: Account<'info, TrueOrFalse>,
}
#[derive(Accounts)]
pub struct SetFlag<'info> {
#[account(mut)]
true_or_false: Account<'info, TrueOrFalse>,
}
#[account]
pub struct TrueOrFalse {
flag: bool,
}
Código en Typescript:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { OtherProgram } from "../target/types/other_program";
describe("other_program", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.OtherProgram as Program<OtherProgram>;
it("Is initialized!", async () => {
const seeds = []
const [TrueOrFalse, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);
console.log("address: ", program.programId.toBase58());
await program.methods.initialize().accounts({trueOrFalse: TrueOrFalse}).rpc();
await program.methods.setbool(true).accounts({trueOrFalse: TrueOrFalse}).rpc();
});
});
Ejecuta las pruebas en otra terminal contra un validador local. Toma nota del programId que se imprime. Lo necesitaremos para derivar la dirección de la cuenta de other_program.
programa read
En otra terminal, ejecuta anchor init para otro programa. Lo llamaremos read. Solo vamos a usar el código en Typescript para leer el struct TrueOrFalse de other_program, no se usa nada de Rust. Esto simula la lectura desde la cuenta de almacenamiento de otro programa.
La estructura de nuestro directorio es la siguiente:
parent_dir/
∟ other_program/
∟ read/
El siguiente código leerá el struct TrueOrFalse de other_program. Asegúrate de que:
- el
otherProgramAddresscoincida con el que se imprimió anteriormente - asegúrate de que estás leyendo el IDL
other_program.jsondesde la ubicación correcta del archivo - asegúrate de ejecutar las pruebas con
--skip-local-validatorpara garantizar que este código lea la cuenta que creó el otro programa
import * as anchor from "@coral-xyz/anchor";
describe("read", () => {
anchor.setProvider(anchor.AnchorProvider.env());
it("Read other account", async () => {
// the other program's programdId -- make sure the address is correct
const otherProgramAddress = "4z4dduMSFKFJDnUAKaHnbhHySK8x1PwgArUBXzksjwa8";
const otherProgramId = new anchor.web3.PublicKey(otherProgramAddress);
// load the other program's idl -- make sure the path is correct
const otherIdl = JSON.parse(
require("fs").readFileSync("../other_program/target/idl/other_program.json", "utf8")
);
const otherProgram = new anchor.Program(otherIdl, otherProgramId);
const seeds = []
const [trueOrFalseAcc, _bump] =
anchor.web3.PublicKey.findProgramAddressSync(seeds, otherProgramId);
let otherStorageStruct = await otherProgram.account.trueOrFalse.fetch(trueOrFalseAcc);
console.log("The value of flag is:", otherStorageStruct.flag.toString());
});
});
La salida esperada es la siguiente:

Nuevamente, esto solo funciona si el otro programa de Solana fue construido con Anchor. Esto depende de cómo Anchor serializa los structs.
Obtener los datos de una cuenta arbitraria
En la siguiente sección mostramos cómo leer datos sin la magia de Anchor.
Desafortunadamente, la documentación para el cliente Typescript de Solana es muy limitada y la biblioteca se ha actualizado suficientes veces como para volver obsoletos los tutoriales sobre el tema.
Tu mejor opción para intentar encontrar la función de Typescript de Solana web3 que necesitas es consultar los Métodos HTTP JSON RPC y buscar uno que parezca prometedor. En nuestro caso, getAccountInfo parece prometedor (flecha azul).

A continuación queremos intentar encontrar ese método en Solana web3 js. Preferiblemente, deberías usar un IDE con autocompletado para que puedas trastear hasta encontrar esa función, como demuestra el siguiente video:
A continuación mostramos la salida esperada al ejecutar la prueba nuevamente:

El recuadro verde alrededor del byte hexadecimal aa muestra que hemos recuperado con éxito el valor decimal 170 que almacenamos en la función set().
El siguiente paso es parsear el búfer de datos, lo cual no es algo que cubriremos aquí.
No hay una forma “obligatoria” en la que se serialicen los datos en una cuenta de Solana. Anchor serializa los structs a su propia manera, pero si alguien escribió un programa de Solana en Rust puro (sin Anchor) o usó su propio algoritmo de serialización, entonces tendrás que personalizar tu algoritmo de deserialización según cómo ellos hayan serializado los datos.
Sigue aprendiendo Solana
Puedes ver el resto de nuestro curso de Solana aquí.
Publicado originalmente el 26 de febrero de 2024