Saltar al contenido principal

Errores

El ejemplo de errores demuestra cómo definir y generar errores en un contrato que los invocadores del contrato pueden entender y manejar. Este ejemplo es una extensión del ejemplo de almacenamiento de datos.

Abrir en Gitpod

Ejecutar el ejemplo

Primero pasa por el proceso de Configuración para configurar tu entorno de desarrollo, luego clona la etiqueta v21.6.0 del repositorio soroban-examples:

git clone -b v21.6.0 https://github.com/stellar/soroban-examples

O, salta la configuración del entorno de desarrollo y abre este ejemplo en Gitpod.

Para ejecutar las pruebas del ejemplo, navega al directorio errors, y usa cargo test.

cd errors
cargo test

Deberías ver una salida que comienza así:

running 2 tests

count: U32(0)
count: U32(1)
count: U32(2)
count: U32(3)
count: U32(4)
count: U32(5)
Status(ContractError(1))
contract call invocation resulted in error Status(ContractError(1))
test test::test ... ok

thread 'test::test_panic' panicked at 'called `Result::unwrap()` on an `Err` value: HostError
Value: Status(ContractError(1))

Debug events (newest first):
0: "Status(ContractError(1))"
1: "count: U32(5)"
2: "count: U32(4)"
3: "count: U32(3)"
...
test test::test_panic - should panic ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.33s

Código

errors/src/lib.rs
#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum Error {
LimitReached = 1,
}

const COUNTER: Symbol = symbol_short!("COUNTER");
const MAX: u32 = 5;

#[contract]
pub struct IncrementContract;

#[contractimpl]
impl IncrementContract {
/// Increment increments an internal counter, and returns the value. Errors
/// if the value is attempted to be incremented past 5.
pub fn increment(env: Env) -> Result<u32, Error> {
// Get the current count.
let mut count: u32 = env.storage().instance().get(&COUNTER).unwrap_or(0); // If no value set, assume 0.
log!(&env, "count: {}", count);

// Increment the count.
count += 1;

// Check if the count exceeds the max.
if count <= MAX {
// Save the count.
env.storage().instance().set(&COUNTER, &count);

// Return the count to the caller.
Ok(count)
} else {
// Return an error if the max is exceeded.
Err(Error::LimitReached)
}
}
}

Ref: https://github.com/stellar/soroban-examples/tree/v21.6.0/errors

Cómo funciona

Abre el archivo errors/src/lib.rs para seguir.

Definiendo un error

Los errores de contrato son enumeraciones Rust u32 donde cada variante de la enumeración tiene un valor entero asignado. El atributo #[contracterror] se utiliza para configurar el error para que pueda ser utilizado en el valor de retorno de las funciones del contrato.

La enumeración tiene algunas restricciones:

  • Debe tener el atributo #[repr(u32)].
  • Debe tener el atributo #[derive(Copy)].
  • Cada variante debe tener un valor entero explícito asignado.
#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum Error {
LimitReached = 1,
}

Los errores de contrato no pueden ser almacenados como datos de contrato y, por lo tanto, no pueden ser usados como tipos en campos de tipos de contrato.

consejo

Si se devuelve un error de una función, todo lo que la función ha hecho es revertido. Si las entradas del ledger han sido alteradas o se han almacenado datos del contrato, todos esos cambios son revertidos y no serán persistidos.

Devolviendo un error

Los errores pueden ser devueltos desde las funciones de contrato devolviendo Result<_, E>.

La función de incremento devuelve un Result<u32, Error>, lo que significa que devuelve Ok(u32) en el caso exitoso y Err(Error) en el caso de error.

pub fn increment(env: Env) -> Result<u32, Error> {
// ...
if count <= MAX {
// ...
Ok(count)
} else {
// ...
Err(Error::LimitReached)
}
}

Entrando en pánico con un error

Los errores también pueden provocarse en lugar de ser devueltos desde la función.

La función de incremento también podría ser escrita como sigue con un valor de retorno u32. El error puede ser pasado al entorno usando el macro panic_with_error!.

pub fn increment(env: Env) -> u32 {
// ...
if count <= MAX {
// ...
count
} else {
// ...
panic_with_error!(&env, Error::LimitReached)
}
}
advertencia

Las funciones que no devuelven un tipo Result<_, E> no especifican cuáles son los posibles valores de error. Esto hace que sea más difícil para otros contratos y clientes integrarse con el contrato. Sin embargo, esto podría ser ideal si los errores son diagnósticos y de depuración, y no están destinados a ser manejados.

Pruebas

Abre el archivo errors/src/test.rs para seguir.

errors/src/test.rs
#[test]
fn test() {
let env = Env::default();
let contract_id = env.register_contract(None, IncrementContract);
let client = IncrementContractClient::new(&env, &contract_id);

assert_eq!(client.try_increment(), Ok(Ok(1)));
assert_eq!(client.try_increment(), Ok(Ok(2)));
assert_eq!(client.try_increment(), Ok(Ok(3)));
assert_eq!(client.try_increment(), Ok(Ok(4)));
assert_eq!(client.try_increment(), Ok(Ok(5)));
assert_eq!(client.try_increment(), Err(Ok(Error::LimitReached)));

std::println!("{}", env.logs().all().join("\n"));
}

#[test]
#[should_panic(expected = "Status(ContractError(1))")]
#E3256B
fn test_panic() {
let env = Env::default();
let contract_id = env.register_contract(None, IncrementContract);
let client = IncrementContractClient::new(&env, &contract_id);

assert_eq!(client.increment(), 1);
assert_eq!(client.increment(), 2);
assert_eq!(client.increment(), 3);
assert_eq!(client.increment(), 4);
assert_eq!(client.increment(), 5);
client.increment();
}

En cualquier prueba, lo primero que siempre se requiere es un Env, que es el entorno de Soroban en el que el contrato se ejecutará.

let env = Env::default();

El contrato se registra con el entorno usando el tipo de contrato.

let contract_id = env.register_contract(None, IncrementContract);

Todas las funciones públicas dentro de un bloque impl que está anotado con el atributo #[contractimpl] tienen una función correspondiente generada en un tipo de cliente generado. El tipo de cliente tendrá el mismo nombre que el tipo de contrato con Client añadido. Por ejemplo, en nuestro contrato, el tipo de contrato es IncrementContract, y el cliente se llama IncrementContractClient.

let client = IncrementContractClient::new(&env, &contract_id);

Se generan dos funciones para cada función de contrato, una que devuelve un Result<>, y la otra que no maneja errores y provoca un pánico si ocurre un error.

try_increment

En la primera prueba, se llama a la función try_increment y devuelve Result<Result<u32, _>, Result<Error, Status>>.

assert_eq!(client.try_increment(), Ok(Ok(5)));
assert_eq!(client.try_increment(), Err(Ok(Error::LimitReached)));
  • Si la llamada a la función es exitosa, se devuelve Ok(Ok(u32)).

  • Si la llamada a la función es exitosa pero devuelve un valor que no es u32, se devuelve Ok(Err(_)).

  • Si la llamada a la función no tiene éxito, se devuelve Err(Ok(Error)).

  • Si la llamada a la función no tiene éxito pero devuelve un código de error que no está en la enumeración Error, o devuelve un código de error del sistema, se devuelve Err(Err(Status)) y el Status puede ser inspeccionado.

increment

En la segunda prueba, se llama a la función increment y devuelve u32. Cuando se realiza la última llamada, la función provoca un pánico.

assert_eq!(client.increment(), 5);
client.increment();
  • Si la llamada a la función es exitosa, se devuelve u32.

  • Si la llamada a la función es exitosa pero devuelve un valor que no es u32, se produce un pánico.

  • Si la llamada a la función no tiene éxito, se produce un pánico.

Crear el contrato

Para crear el contrato, usa el comando stellar contract build.

stellar contract build

Un archivo .wasm debería ser generado en el directorio target:

target/wasm32-unknown-unknown/release/soroban_errors_contract.wasm

Ejecutar el contrato

Despliegue el contrato en Testnet para que podamos ejecutarlo. El valor proporcionado como --source se configuró en nuestra guía para Comenzar; por favor cambia según sea necesario si creaste una identidad diferente.

stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/soroban_errors_contract.wasm \
--source alice \
--network testnet

El comando anterior generará el id del contrato, que en nuestro caso es CC3UMHVTIEH6GGDBW7MM72Q545HBDCXGU3GMIXP23PQVSBFKNZRWT37X.

Ahora que hemos desplegado el contrato, podemos invocarlo.

stellar contract invoke \
--id CC3UMHVTIEH6GGDBW7MM72Q545HBDCXGU3GMIXP23PQVSBFKNZRWT37X \
--network testnet \
--source alice \
-- \
increment

Ejecuta el comando varias veces y en la sexta invocación deberías ver un error como este:

...
error: transaction simulation failed: host invocation failed

Caused by:
HostError: Error(Contract, #1)

Event log (newest first):
0: [Diagnostic Event] contract:<your contract id>, topics:[error, Error(Contract, #1)], data:"escalating Ok(ScErrorType::Contract) frame-exit to Err"
1: [Diagnostic Event] topics:[fn_call, Bytes(b7461eb3410fe31861b7d8cfea1de74e118ae6a6ccc45dfadbe15904aa6e6369), increment], data:Void
...

Para recuperar el valor actual del contador, usa el comando stellar contract read.

stellar contract read \
--id CC3UMHVTIEH6GGDBW7MM72Q545HBDCXGU3GMIXP23PQVSBFKNZRWT37X \
--network testnet \
--source alice \
--durability persistent \
--output json