En Ethereum, a menudo vemos una declaración require restringiendo los valores que puede tener el argumento de una función. Considera el siguiente ejemplo:
function foobar(uint256 x) public {
require(x < 100, "I'm not happy with the number you picked");
// rest of the function logic
}
En el código anterior, la transacción se revertirá si a foobar se le pasa un valor de 100 o mayor.
¿Cómo hacemos esto en Solana, o específicamente, en el framework Anchor?
Anchor tiene equivalentes para el custom error y las declaraciones require de Solidity. Su documentación sobre el tema es bastante buena, pero también explicaremos cómo detener las transacciones cuando los argumentos de la función no son los que queremos que sean.
El programa de Solana a continuación tiene una función limit_range que solo aceptará valores del 10 al 100 inclusive:
use anchor_lang::prelude::*;
declare_id!("8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY");
#[program]
pub mod day4 {
use super::*;
pub fn limit_range(ctx: Context<LimitRange>, a: u64) -> Result<()> {
if a < 10 {
return err!(MyError::AisTooSmall);
}
if a > 100 {
return err!(MyError::AisTooBig);
}
msg!("Result = {}", a);
Ok(())
}
}
#[derive(Accounts)]
pub struct LimitRange {}
#[error_code]
pub enum MyError {
#[msg("a is too big")]
AisTooBig,
#[msg("a is too small")]
AisTooSmall,
}
El siguiente código realiza pruebas unitarias del programa anterior:
import * as anchor from "@coral-xyz/anchor";
import { Program, AnchorError } from "@coral-xyz/anchor"
import { Day4 } from "../target/types/day4";
import { assert } from "chai";
describe("day4", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Day4 as Program<Day4>;
it("Input test", async () => {
// Add your test here.
try {
const tx = await program.methods.limitRange(new anchor.BN(9)).rpc();
console.log("Your transaction signature", tx);
} catch (_err) {
assert.isTrue(_err instanceof AnchorError);
const err: AnchorError = _err;
const errMsg =
"a is too small";
assert.strictEqual(err.error.errorMessage, errMsg);
console.log("Error number:", err.error.errorCode.number);
}
try {
const tx = await program.methods.limitRange(new anchor.BN(101)).rpc();
console.log("Your transaction signature", tx);
} catch (_err) {
assert.isTrue(_err instanceof AnchorError);
const err: AnchorError = _err;
const errMsg =
"a is too big";
assert.strictEqual(err.error.errorMessage, errMsg);
console.log("Error number:", err.error.errorCode.number);
}
});
});
Ejercicio:
- ¿Qué patrón notas con el número de Error? ¿Qué pasa con los códigos de error si cambias el orden de los errores en el
enum MyError? - Usa este bloque de código que añade la nueva
funcy el error al código existente:
#[program]
pub mod day_4 {
use super::*;
pub fn limit_range(ctxThen : Context<LimitRange>, a: u64) -> Result<()> {
require!(a >= 10, MyError::AisTooSmall);
require!(a <= 100, MyError::AisTooBig);
msg!("Result = {}", a);
Ok(())
}
// NEW FUNCTION
pub fn func(ctx: Context<LimitRange>) -> Result<()> {
msg!("Will this print?");
return err!(MyError::AlwaysErrors);
}
}
#[derive(Accounts)]
pub struct LimitRange {}
#[error_code]
pub enum MyError {
#[msg("a is too small")]
AisTooSmall,
#[msg("a is too big")]
AisTooBig,
#[msg("Always errors")] // NEW ERROR, what do you think the error code will be?
AlwaysErrors,
}
Y añade esta prueba:
it("Error test", async () => {
// Add your test here.
try {
const tx = await program.methods.func().rpc();
console.log("Your transaction signature", tx);
} catch (_err) {
assert.isTrue(_err instanceof AnchorError);
const err: AnchorError = _err;
const errMsg =
"Always errors";
assert.strictEqual(err.error.errorMessage, errMsg);
console.log("Error number:", err.error.errorCode.number);
}
});
Antes de ejecutar esto, ¿cuál crees que será el nuevo código de error?
La diferencia significativa entre cómo Ethereum y Solana detienen las transacciones con parámetros inválidos es que Ethereum desencadena un revert y Solana devuelve un error.
Uso de declaraciones require
Existe un macro require!, que es conceptualmente lo mismo que el require de Solidity, el cual podemos usar para consolidar nuestro código. Cambiando de comprobaciones if (que toman tres líneas) a llamadas require!, nuestro código anterior se traduce a lo siguiente:
pub fn limit_range(ctx: Context<LimitRange>, a: u64) -> Result<()> {
require!(a >= 10, Day4Error::AisTooSmall);
require!(a <= 100, Day4Error::AisTooBig);
msg!("Result = {}", a);
Ok(())
}
En Ethereum, sabemos que no se registra nada si una función se revierte, incluso si el revert ocurre después del registro. Por ejemplo, una llamada a tryToLog en el contrato a continuación no registraría nada, porque la función se revierte:
contract DoesNotLog {
event SomeEvent(uint256);
function tryToLog() public {
emit SomeEvent(100);
require(false);
}
}
Ejercicio: ¿Qué sucede si pones un macro msg! antes de las declaraciones return error en una función de un programa de Solana? ¿Qué sucede si reemplazas return err! con Ok(())? A continuación tenemos una función que registra algo con msg! y luego devuelve un error. Comprueba si el contenido del macro msg! se registra.
pub fn func(ctx: Context<ReturnError>) -> Result<()> {
msg!("Will this print?");
return err!(Day4Error::AlwaysErrors);
}
#[derive(Accounts)]
pub struct ReturnError {}
#[error_code]
pub enum Day4Error {
#[msg("AlwaysErrors")]
AlwaysErrors,
}
Bajo el capó, el macro require! no es diferente de devolver un error, es simplemente azúcar sintáctico.
El resultado esperado es que “Will this print?” se imprimirá cuando devuelvas Ok(()) y no se imprimirá cuando devuelvas un error.
Diferencias entre Solana y Solidity con respecto a los errores
En Solidity, la declaración require detiene la ejecución con el op code revert. Solana no detiene la ejecución, sino que simplemente devuelve un valor diferente. Esto es análogo a cómo linux devuelve 0 o 1 en caso de éxito. Si se devuelve un 0 (equivalente a devolver Ok(())), todo salió bien.
Por lo tanto, los programas de Solana siempre deben devolver algo, ya sea un Ok(()) o un Error.
En Anchor, los errores son un enum con el atributo #[error_code].
Nota cómo todas las funciones en Solana tienen un tipo de retorno Result<()>. Un result es un tipo que podría ser un Ok(()) o un error.
Preguntas y Respuestas
¿Por qué Ok(()) no tiene un punto y coma al final?
Si lo añades, tu código no compilará. Si la declaración final en Rust no tiene un punto y coma, entonces se devuelve el valor de esa línea.
¿Por qué Ok(()) tiene un conjunto adicional de paréntesis?
El () significa “unit” en Rust, que puedes imaginar como un void en C o un Nothing en Haskell. Aquí, Ok es un enum que contiene un tipo unit. Eso es lo que se devuelve. Las funciones que no devuelven cosas devuelven implícitamente el tipo unit en Rust. Un Ok(()) sin punto y coma es sintácticamente equivalente a return Ok(());. Nota el punto y coma al final.
¿Por qué a las declaraciones if anteriores les faltan paréntesis?
Estos son opcionales en Rust.
Aprende más con RareSkills
Este tutorial es parte de nuestro curso de Solana gratuito.
Publicado originalmente el 11 de febrero de 2024