Saltar al contenido principal

Tipos Personalizados

El ejemplo de tipos personalizados muestra cómo definir tus propias estructuras de datos que pueden almacenarse en el ledger o usarse como entradas y salidas en las invocaciones de contratos. Este ejemplo es una extensión del ejemplo de almacenamiento de datos.

Abrir en Gitpod

Ejecutar el Ejemplo

Primero sigue el proceso de Configuración para preparar tu entorno de desarrollo, luego clona la etiqueta v22.0.1 del repositorio soroban-examples:

git clone -b v22.0.1 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 custom_types y usa cargo test.

cd custom_types
cargo test

Deberías ver la salida:

running 1 test
test test::test ... ok

Código

custom_types/src/lib.rs
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct State {
pub count: u32,
pub last_incr: u32,
}

const STATE: Symbol = symbol_short!("STATE");

#[contract]
pub struct IncrementContract;

#[contractimpl]
impl IncrementContract {
/// Increment increments an internal counter, and returns the value.
pub fn increment(env: Env, incr: u32) -> u32 {
// Get the current count.
let mut state = Self::get_state(env.clone());

// Increment the count.
state.count += incr;
state.last_incr = incr;

// Save the count.
env.storage().instance().set(&STATE, &state);

// Return the count to the caller.
state.count
}
/// Return the current state.
pub fn get_state(env: Env) -> State {
env.storage().instance().get(&STATE).unwrap_or(State {
count: 0,
last_incr: 0,
}) // If no value set, assume 0.
}
}

Ref: https://github.com/stellar/soroban-examples/tree/v22.0.1/custom_types

Cómo Funciona

Los tipos personalizados se definen usando el atributo #[contracttype] sobre un struct o un enum.

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

Tipo Personalizado: Estructura

Los structs se almacenan en el ledger como un mapa de pares clave-valor, donde la clave es una cadena de hasta 32 caracteres que representa el nombre del campo y el valor es el valor codificado.

Los nombres de los campos no deben tener más de 32 caracteres.

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct State {
pub count: u32,
pub last_incr: u32,
}

Tipo Personalizado: Enumeración

El ejemplo no contiene enumeraciones, pero las enumeraciones también pueden ser tipos de contrato.

Los enums que contienen variantes unitarias y de tupla se almacenan en el ledger como un vector de dos elementos, donde el primer elemento es el nombre de la variante del enum en forma de cadena de hasta 32 caracteres, y el valor es el valor si la variante tiene uno.

Solo se admiten variantes unitarias y variantes de un solo valor, como A y B en el ejemplo a continuación.

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Enum {
A,
B(...),
}

Las enumeraciones que contienen valores enteros se almacenan en el ledger como el valor u32.

#[contracttype]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(u32)]
pub enum Enum {
A = 1,
B = 2,
}

Uso de Tipos en Funciones

Los tipos que han sido anotados con #[contracttype] pueden almacenarse como datos de contrato y recuperarse después.

Los tipos también pueden usarse como entradas y salidas en las funciones del contrato.

pub fn increment(env: Env, incr: u32) -> u32 {
let mut state = Self::get_state(env.clone());
state.count += incr;
state.last_incr = incr;
env.storage().instance().set(&STATE, &state);
state.count
}

pub fn get_state(env: Env) -> State {
env.storage().instance().get(&STATE).unwrap_or(State {
count: 0,
last_incr: 0,
}) // If no value set, assume 0.
}

Pruebas

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

custom_types/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.increment(&1), 1);
assert_eq!(client.increment(&10), 11);
assert_eq!(
client.get_state(),
State {
count: 11,
last_incr: 10
}
);
}

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

let env = Env::default();

El contrato se registra en 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 anotado con el atributo #[contractimpl] tienen una función correspondiente generada en un tipo cliente generado. El tipo cliente tendrá el mismo nombre que el tipo de contrato con "Client" añadido al final. Por ejemplo, en nuestro contrato el tipo de contrato es IncrementContract, y el cliente se llama IncrementContractClient.

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

La prueba invoca la función increment en el contrato registrado, lo que provoca que el tipo State se almacene y actualice un par de veces.

assert_eq!(client.increment(&1), 1);
assert_eq!(client.increment(&10), 11);

Luego, la prueba invoca la función get_state para obtener el valor State almacenado, y puede hacer aserciones sobre sus valores.

assert_eq!(
client.get_state(),
State {
count: 11,
last_incr: 10
}
);

Crear el contrato

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

stellar contract build

Se debería generar un archivo .wasm en el directorio target:

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

Ejecutar el Contrato

Si tienes stellar-cli instalado, puedes desplegar e invocar la función del contrato.

stellar contract invoke \
--id CACDYF3CYMJEJTIVFESQYZTN67GO2R5D5IUABTCUG3HXQSRXCSOROBAN \
--source alice \
--network testnet \
-- \
increment \
--incr 5

La siguiente salida debería ocurrir al usar el código anterior.

5

Ejecuta varias veces más con diferentes cantidades para incrementar y observa cómo cambia el contador.

Usa el stellar-cli para inspeccionar el estado del contador después de varias ejecuciones.

stellar contract read --id 1 --key STATE
STATE,"{""count"":25,""last_incr"":15}"