En nuestros tutoriales de Solana hasta ahora, solo hemos tenido una cuenta que inicializa y escribe en la cuenta.
En la práctica, esto es muy restrictivo. Por ejemplo, si la usuaria Alice le está transfiriendo puntos a Bob, Alice debe poder escribir en una cuenta inicializada por el usuario Bob.
En este tutorial demostraremos cómo inicializar una cuenta con una billetera (wallet) y actualizarla con otra.
El paso de inicialización
El código en Rust que hemos estado usando para inicializar cuentas no cambia:
use anchor_lang::prelude::*;
use std::mem::size_of;
declare_id!("61As9Y8pREgvFZzps6rpFai8UkageeHT6kW1dnGRiefb");
#[program]
pub mod other_write {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
}
#[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,
}
Realizando la transacción de inicialización con una billetera alternativa
Sin embargo, hay un cambio importante en el código del cliente:
- Para fines de prueba, creamos una nueva billetera llamada
newKeypair. Esta es diferente a la que Anchor proporciona por defecto. - Le enviamos (airdrop) 1 SOL a esa nueva billetera para que pueda pagar las transacciones.
- Presta atención al comentario
// THIS MUST BE EXPLICITLY SPECIFIED. Estamos pasando lapublicKeyde esa billetera para el campoSigner. Cuando usamos el firmante predeterminado integrado en Anchor, Anchor pasa esto en segundo plano por nosotros. Sin embargo, cuando usamos una billetera diferente, necesitamos proporcionarlo explícitamente. - Establecemos que el firmante sea
newKeypaircon la configuración.signers([newKeypair]).
Explicaremos después de este fragmento de código por qué (aparentemente) estamos especificando el firmante dos veces:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { OtherWrite } from "../target/types/other_write";
// this airdrops sol to an address
async function airdropSol(publicKey, amount) {
let airdropTx = await anchor.getProvider().connection.requestAirdrop(publicKey, amount);
await confirmTransaction(airdropTx);
}
async function confirmTransaction(tx) {
const latestBlockHash = await anchor.getProvider().connection.getLatestBlockhash();
await anchor.getProvider().connection.confirmTransaction({
blockhash: latestBlockHash.blockhash,
lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
signature: tx,
});
}
describe("other_write", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.OtherWrite as Program<OtherWrite>;
it("Is initialized!", async () => {
const newKeypair = anchor.web3.Keypair.generate();
await airdropSol(newKeypair.publicKey, 1e9); // 1 SOL
let seeds = [];
const [myStorage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);
await program.methods.initialize().accounts({
myStorage: myStorage,
signer: newKeypair.publicKey // ** THIS MUST BE EXPLICITLY SPECIFIED **
}).signers([newKeypair]).rpc();
});
});
No es obligatorio que la clave signer se llame signer.
Ejercicio: En el código en Rust, cambia payer = signer a payer = fren y pub signer: Signer<'info>, a pub fren: Signer<'info>, y cambia signer: newKeypair.publicKey a fren: newKeypair.publicKey en la prueba. La inicialización debería tener éxito y la prueba debería pasar.
¿Por qué Anchor requiere especificar el Signer y la publicKey?
Al principio podría parecer redundante que estemos especificando el firmante dos veces, pero echemos un vistazo más de cerca:

En el recuadro rojo, vemos que el campo fren se especifica como una cuenta Signer. El tipo Signer significa que Anchor examinará la firma de la transacción y se asegurará de que la firma coincida con la dirección pasada aquí.
Veremos más adelante cómo podemos usar esto para validar que el Signer está autorizado para realizar cierta transacción.
Anchor ha estado haciendo esto todo el tiempo en segundo plano, pero dado que pasamos un Signer distinto al que Anchor usa por defecto, tenemos que ser explícitos sobre qué cuenta es el Signer.
Error: unknown signer en Solana Anchor
El error unknown signer ocurre cuando el firmante de la transacción no coincide con la clave pública pasada a Signer.
Supongamos que modificamos la prueba para eliminar la especificación .signers([newKeypair]). Anchor usará el firmante por defecto en su lugar, y el firmante por defecto no coincidirá con la publicKey de nuestra billetera newKeypair:
![Removing .signers([newKeypair])](https://static.wixstatic.com/media/935a00_2b7cffddcb2a490aaea4396ee71a47a1~mv2.png/v1/fill/w_1480,h_334,al_c,lg_1,q_90,enc_auto/935a00_2b7cffddcb2a490aaea4396ee71a47a1~mv2.png)
Obtendremos el siguiente error:

De manera similar, si no pasamos la publicKey explícitamente, Anchor usará silenciosamente el firmante por defecto:

Y obtendremos el siguiente Error: unknown signer:

De forma algo engañosa, Anchor no está diciendo que el firmante sea desconocido porque no se haya especificado per se. Anchor es capaz de deducir que si no se especifica ningún firmante, entonces usará el firmante predeterminado. Si eliminamos tanto el código .signers([newKeypair]) como el código fren: newKeypair.publicKey, entonces Anchor usará el firmante predeterminado tanto para la clave pública contra la cual verificar, como para la firma del firmante para verificar que coincida con la clave pública.
El siguiente código dará como resultado una inicialización exitosa porque tanto la clave pública del Signer como la cuenta que firma la transacción son el firmante predeterminado de Anchor.
await program.methods.initialize().accounts({
myStorage: myStorage
}).rpc();
});

Bob puede escribir en una cuenta que inicializó Alice
A continuación mostramos un programa en Anchor con funciones para inicializar una cuenta y escribir en ella.
Esto te resultará familiar de nuestro tutorial del programa contador en Solana, pero presta atención a la pequeña adición marcada por el comentario // THIS FIELD MUST BE INCLUDED cerca de la parte inferior:
use anchor_lang::prelude::*;
use std::mem::size_of;
declare_id!("61As9Y8pREgvFZzps6rpFai8UkageeHT6kW1dnGRiefb");
#[program]
pub mod other_write {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
pub fn update_value(ctx: Context<UpdateValue>, new_value: u64) -> Result<()> {
ctx.accounts.my_storage.x = new_value;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init,
payer = fren,
space=size_of::<MyStorage>() + 8,
seeds = [],
bump)]
pub my_storage: Account<'info, MyStorage>,
#[account(mut)]
pub fren: Signer<'info>, // A public key is passed here
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct UpdateValue<'info> {
#[account(mut, seeds = [], bump)]
pub my_storage: Account<'info, MyStorage>,
// THIS FIELD MUST BE INCLUDED
#[account(mut)]
pub fren: Signer<'info>,
}
#[account]
pub struct MyStorage {
x: u64,
}
El siguiente código del cliente creará una billetera para Alice y Bob y les enviará (airdrop) 1 SOL a cada uno. Alice inicializará la cuenta MyStorage, y Bob escribirá en ella:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { OtherWrite } from "../target/types/other_write";
// this airdrops sol to an address
async function airdropSol(publicKey, amount) {
let airdropTx = await anchor.getProvider().connection.requestAirdrop(publicKey, amount);
await confirmTransaction(airdropTx);
}
async function confirmTransaction(tx) {
const latestBlockHash = await anchor.getProvider().connection.getLatestBlockhash();
await anchor.getProvider().connection.confirmTransaction({
blockhash: latestBlockHash.blockhash,
lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
signature: tx,
});
}
describe("other_write", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.OtherWrite as Program<OtherWrite>;
it("Is initialized!", async () => {
const alice = anchor.web3.Keypair.generate();
const bob = anchor.web3.Keypair.generate();
const airdrop_alice_tx = await anchor.getProvider().connection.requestAirdrop(alice.publicKey, 1 * anchor.web3.LAMPORTS_PER_SOL);
await confirmTransaction(airdrop_alice_tx);
const airdrop_alice_bob = await anchor.getProvider().connection.requestAirdrop(bob.publicKey, 1 * anchor.web3.LAMPORTS_PER_SOL);
await confirmTransaction(airdrop_alice_bob);
let seeds = [];
const [myStorage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);
// ALICE INITIALIZE ACCOUNT
await program.methods.initialize().accounts({
myStorage: myStorage,
fren: alice.publicKey
}).signers([alice]).rpc();
// BOB WRITE TO ACCOUNT
await program.methods.updateValue(new anchor.BN(3)).accounts({
myStorage: myStorage,
fren: bob.publicKey
}).signers([bob]).rpc();
let value = await program.account.myStorage.fetch(myStorage);
console.log(`value stored is ${value.x}`);
});
});
Restringiendo escrituras en cuentas de Solana
En aplicaciones reales, no queremos que Bob escriba datos arbitrarios en cuentas arbitrarias. Creemos un ejemplo básico donde los usuarios puedan inicializar una cuenta con 10 puntos y transferir esos puntos a otra cuenta. (Hay un problema obvio de que un hacker puede crear tantas cuentas como quiera usando billeteras separadas, pero eso está fuera del alcance de nuestro ejemplo).
Construyendo un programa proto-ERC20
Alice debería poder modificar tanto su cuenta como la cuenta de Bob. Es decir, debería poder deducir sus puntos y acreditar los puntos de Bob. Ella no debería poder deducir los puntos de Bob; solo Bob debería poder hacer eso.
Por convención, en Solana llamamos “authority” a una dirección que puede realizar cambios privilegiados en una cuenta. Es un patrón común almacenar el campo “authority” en el struct de la cuenta para indicar que solo esa cuenta puede realizar operaciones sensibles en esa cuenta (como deducir puntos en nuestro ejemplo).
Esto es en cierto modo análogo al patrón onlyOwner en Solidity, excepto que en lugar de aplicarse a todo el contrato, se aplica solo a una única cuenta:
use anchor_lang::prelude::*;
use std::mem::size_of;
declare_id!("HFmGQX4wPgPYVMFe4WrBi925NKvGySrEG2LGyRXsXJ4Z");
const STARTING_POINTS: u32 = 10;
#[program]
pub mod points {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.player.points = STARTING_POINTS;
ctx.accounts.player.authority = ctx.accounts.signer.key();
Ok(())
}
pub fn transfer_points(ctx: Context<TransferPoints>,
amount: u32) -> Result<()> {
require!(ctx.accounts.from.authority == ctx.accounts.signer.key(), Errors::SignerIsNotAuthority);
require!(ctx.accounts.from.points >= amount, Errors::InsufficientPoints);
ctx.accounts.from.points -= amount;
ctx.accounts.to.points += amount;
Ok(())
}
}
#[error_code]
pub enum Errors {
#[msg("SignerIsNotAuthority")]
SignerIsNotAuthority,
#[msg("InsufficientPoints")]
InsufficientPoints
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init,
payer = signer,
space = size_of::<Player>() + 8,
seeds = [&(signer.as_ref().key().to_bytes())],
bump)]
player: Account<'info, Player>,
#[account(mut)]
signer: Signer<'info>,
system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct TransferPoints<'info> {
#[account(mut)]
from: Account<'info, Player>,
#[account(mut)]
to: Account<'info, Player>,
#[account(mut)]
signer: Signer<'info>,
}
#[account]
pub struct Player {
points: u32,
authority: Pubkey
}
Ten en cuenta que usamos la dirección del firmante (&(signer.as_ref().key().to_bytes())) para derivar la dirección de la cuenta donde se almacenan sus puntos. Esto se comporta como un mapping de Solidity en Solana, donde el “msg.sender / tx.origin” de Solana es la clave.
En la función initialize, el programa establece los puntos iniciales en 10 y la autoridad (authority) al signer. El usuario no tiene control sobre estos valores iniciales.
La función transfer_points usa macros require y macros de código de error de Solana Anchor para asegurar que 1) el Signer de la transacción sea la autoridad de la cuenta cuyo saldo se está deduciendo; y 2) la cuenta tenga suficientes puntos de saldo para transferir.
El código base de prueba debería ser fácil de entender. Alice y Bob inicializan sus cuentas, luego Alice transfiere 5 puntos a Bob:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Points } from "../target/types/points";
// this airdrops sol to an address
async function airdropSol(publicKey, amount) {
let airdropTx = await anchor.getProvider().connection.requestAirdrop(publicKey, amount);
await confirmTransaction(airdropTx);
}
async function confirmTransaction(tx) {
const latestBlockHash = await anchor.getProvider().connection.getLatestBlockhash();
await anchor.getProvider().connection.confirmTransaction({
blockhash: latestBlockHash.blockhash,
lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
signature: tx,
});
}
describe("points", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Points as Program<Points>;
it("Alice transfers points to Bob", async () => {
const alice = anchor.web3.Keypair.generate();
const bob = anchor.web3.Keypair.generate();
const airdrop_alice_tx = await anchor.getProvider().connection.requestAirdrop(alice.publicKey, 1 * anchor.web3.LAMPORTS_PER_SOL);
await confirmTransaction(airdrop_alice_tx);
const airdrop_alice_bob = await anchor.getProvider().connection.requestAirdrop(bob.publicKey, 1 * anchor.web3.LAMPORTS_PER_SOL);
await confirmTransaction(airdrop_alice_bob);
let seeds_alice = [alice.publicKey.toBytes()];
const [playerAlice, _bumpA] = anchor.web3.PublicKey.findProgramAddressSync(seeds_alice, program.programId);
let seeds_bob = [bob.publicKey.toBytes()];
const [playerBob, _bumpB] = anchor.web3.PublicKey.findProgramAddressSync(seeds_bob, program.programId);
// Alice and Bob initialize their accounts
await program.methods.initialize().accounts({
player: playerAlice,
signer: alice.publicKey,
}).signers([alice]).rpc();
await program.methods.initialize().accounts({
player: playerBob,
signer: bob.publicKey,
}).signers([bob]).rpc();
// Alice transfers 5 points to Bob. Note that this is a u32
// so we don't need a BigNum
await program.methods.transferPoints(5).accounts({
from: playerAlice,
to: playerBob,
signer: alice.publicKey,
}).signers([alice]).rpc();
console.log(`Alice has ${(await program.account.player.fetch(playerAlice)).points} points`);
console.log(`Bob has ${(await program.account.player.fetch(playerBob)).points} points`)
});
});
Ejercicio: Crea un par de claves (keypair) mallory e intenta hacer que mallory robe puntos de Alice o Bob usando mallory como el firmante en .signers([mallory]). Tu ataque debería fallar, pero deberías intentarlo de todos modos.
Usando Constraints de Anchor para reemplazar las macros require!
Una alternativa a escribir require!(ctx.accounts.from.authority == ctx.accounts.signer.key(), Errors::SignerIsNotAuthority); es usar un constraint de Anchor. La documentación de cuentas de Anchor nos da una lista de constraints (restricciones) disponibles para nosotros.
Constraint has_one en Anchor
El constraint has_one asume que hay una “clave compartida” entre #[derive(Accounts)] y #[account] y verifica que ambas claves tengan el mismo valor. La mejor manera de demostrar esto es con una imagen:

En segundo plano, Anchor bloqueará la transacción si la cuenta authority pasada como parte de la transacción (como el Signer) no es igual a la authority almacenada en la cuenta.
En nuestra implementación anterior, usamos la clave authority en la cuenta y signer en #[derive(Accounts)]. Esta falta de coincidencia en los nombres de las claves impedirá que esta macro funcione, por lo que el código anterior cambia la clave signer a authority. Authority no es una palabra clave especial, es simplemente una convención. Podrías, como ejercicio, cambiar todas las instancias de authority a fren y el código funcionará igual.
Constraint constraint en Anchor
También podemos reemplazar la macro require!(ctx.accounts.from.points >= amount, Errors::InsufficientPoints); con un constraint de Anchor.
La macro constraint nos permite establecer restricciones (constraints) arbitrarias sobre las cuentas pasadas a las transacciones y los datos en la cuenta. En nuestro caso, queremos asegurarnos de que el remitente tenga suficientes puntos:
#[derive(Accounts)]
#[instruction(amount: u32)] // amount must be passed as an instruction
pub struct TransferPoints<'info> {
#[account(mut,
has_one = authority,
constraint = from.points >= amount)]
from: Account<'info, Player>,
#[account(mut)]
to: Account<'info, Player>,
authority: Signer<'info>,
}
#[account]
pub struct Player {
points: u32,
authority: Pubkey
}
La macro es lo suficientemente inteligente como para reconocer que from se basa en la cuenta pasada en la clave from, y que esa cuenta tiene un campo points. El amount del argumento de la función transfer_points se debe pasar a través de la macro instruction para que la macro constraint pueda comparar amount con el saldo de puntos en la cuenta.
Agregando mensajes de error personalizados a los constraints de Anchor
Podemos mejorar la legibilidad de los mensajes de error cuando se violan las restricciones agregando errores personalizados, los mismos errores personalizados que pasamos a las macros require! usando la notación @:
#[derive(Accounts)]
#[instruction(amount: u32)]
pub struct TransferPoints<'info> {
#[account(mut,
has_one = authority @ Errors::SignerIsNotAuthority,
constraint = from.points >= amount @ Errors::InsufficientPoints)]
from: Account<'info, Player>,
#[account(mut)]
to: Account<'info, Player>,
authority: Signer<'info>,
}
#[account]
pub struct Player {
points: u32,
authority: Pubkey
}
El enum Errors se definió en el código en Rust anteriormente, el cual los usó en las macros require!.
Ejercicio: modifica las pruebas para violar la macro has_one y constraint y observa los mensajes de error.
Aprende más sobre Solana con RareSkills
Nuestros tutoriales de Solana cubren cómo aprender Solana siendo un desarrollador de Ethereum o EVM.
Publicado originalmente el 5 de marzo de 2024