Serie de Solidity a Rust Pt. 2
Autor
Julian Martinez
Fecha de publicación
En esta guía, nos sumergiremos en las similitudes y diferencias entre los tipos de datos en Solidity de EVM y Rust de Soroban. Si estás buscando involucrarte más en la construcción de contratos inteligentes, no olvides revisar nuestro artículo anterior sobre cómo escribir un contrato “Hello World” en ambos lenguajes aquí.
Tipos de Datos
Los tipos de datos son críticos en programación, sirven para definir el tipo de datos con los que tratamos. Aseguran la integridad de los datos, consistencia y una asignación de memoria eficiente, optimizando así el uso de recursos. Los tipos de datos se categorizan en primitivos, como booleanos y enteros, y no primitivos, tales como estructuras, mapeos y enumeraciones. Su uso correcto es esencial para una programación robusta y eficiente, particularmente en el desarrollo de contratos inteligentes.
Solidity
En Solidity, encontramos una variedad de tipos de datos, incluyendo:
bool
representa valores Booleanos (verdadero
o falso
).int
y uint
denotan enteros con y sin signo, respectivamente, con tamaños variables (ej., int256
, uint256
).address
se usa para direcciones Ethereum.string
es para datos de cadena de longitud dinámica.bytes
para arreglos dinámicos de bytes, y bytes32
representa arreglos de bytes de tamaño fijo, útiles para almacenamiento eficiente.Arreglos
pueden ser de tamaño dinámico o fijo y admiten tanto tipos primitivos como compuestos.struct
permite definir nuevos tipos agrupando variables (de ambos tipos primitivos y compuestos).enum
permite la creación de tipos personalizados con un conjunto limitado de valores constantes.mapping
es un almacén de clave-valor para asociar claves únicas con valores.Rust de Soroban
Soroban utiliza tipos de datos de Rust, con una mezcla de tipos familiares y únicos, incluyendo:
Bool (bool)
: Representa valores verdadero o falso.Enteros de 32 bits
: Firmados (i32
) y sin firmar (u32
).Enteros de 64 bits
: Firmados (i64
) y sin firmar (u64
).Enteros de 128 bits
: Firmados (i128
) y sin firmar (u128
).Símbolo
: Cadenas eficientes de hasta 32 caracteres de longitud. Symbol::new
para cadenas de hasta 32 caracteres y symbol_short!
para cadenas de hasta 9 caracteres. Limitado a caracteres a-zA-Z0-9_
y codificado en enteros de 64 bits.Bytes, Cadenas (Bytes, BytesN)
: Arreglos de bytes y cadenas que pueden pasarse a contratos y almacenes.Vec (Vec)
: Un tipo de colección secuencial e indexable que puede crecer.Mapa (Map)
: Un diccionario ordenado de clave-valor.Dirección (Address)
: Un identificador opaco universal utilizado en contratos.Cadena (String)
: Un tipo de arreglo contiguo creciente que contiene u8s, requiere que se pase un entorno.Estructuras (con Campos Nombrados)
: Tipos personalizados que consisten en campos nombrados almacenados en el ledger como un mapa de pares clave-valor.Estructuras (con Campos Sin Nombrar)
: Tipos personalizados que consisten en campos sin nombrar almacenados en el ledger como un vector de valores.Enum (Variantes de Unidad y Tupla)
: Tipos personalizados que consisten en variantes de unidad y tupla almacenadas en el ledger como un vector de dos elementos, siendo el primer elemento el nombre de la variante y el segundo el valor.Enum (Variantes Enteras)
: Tipos personalizados que consisten en variantes enteras almacenadas en el ledger como el valor u32
.Diferencias
Ambos entornos admiten tipos de datos mutables y constantes, pero hay matices:
// SPDX-License-Identifier: MIT
// compiler version must be greater than or equal to 0.8.24 and less than 0.9.0
pragma solidity ^0.8.24;
contract HelloWorld {
string public greet = "Hello, World!";
}
#![no_std]
use soroban_sdk::{contract, contractimpl, Env, Symbol};
#[contract]
pub struct HelloWorldContract;
#[contractimpl]
impl HelloWorldContract {
pub fn greet(env: &Env) -> Symbol {
Symbol::new(env, "Hello, World!")
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract AddressDemo {
function getMsgSender() public view returns (address) {
return msg.sender;
}
function getContractId() public view returns (address) {
return address(this);
}
}
En el ejemplo a continuación, las direcciones se acceden y manipulan directamente dentro de la lógica del contrato. El método require_auth se utiliza para asegurar que la llamada se realice desde la dirección especificada, proporcionando una capa de autenticación y seguridad dentro del contrato.
#![no_std]
use soroban_sdk::{contract, contractimpl, Address, Env};
#[contract]
pub struct AddressDemo;
#[contractimpl]
impl AddressDemo {
pub fn get_user(addr: Address) -> Address {
// Uses the require_auth method to ensure that the call is made from the given address.
addr.require_auth();
addr
}
pub fn get_contract(env: &Env) -> Address {
env.current_contract_address()
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract ArrayAndBytes {
// Several ways to initialize an array
uint256[] public arr;
uint256[] public arr2 = [1, 2, 3];
// Fixed sized array, all elements initialize to 0 or values can be preset
// Array cannot exceed fixed size
uint256[10] public myFixedSizeArr;
uint256[5] public myFixedSizeArr2 = [1, 2, 3, 4, 5];
// Bytes (dynamic-size and fixed-size)
bytes public dynamicBytes = "hello, solidity";
bytes32 public fixedBytes = "hello, solidity";
function get(uint256 i) public view returns (uint256) {
return arr[i];
}
// Solidity can return the entire array.
// But this function should be avoided for
// arrays that can grow indefinitely in length.
function getArr() public view returns (uint256[] memory) {
return arr;
}
function push(uint256 i) public {
// Append to array
// This will increase the array length by 1.
arr.push(i);
}
function pop() public {
// Remove last element from array
// This will decrease the array length by 1
arr.pop();
}
function getLength() public view returns (uint256) {
return arr.length;
}
function remove(uint256 index) public {
// Delete does not change the array length.
// It resets the value at index to it's default value,
// in this case 0
delete arr[index];
}
}
#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, vec, Bytes, BytesN, Env, Vec};
// Define an enum to represent different data keys (state variable storage)
#[derive(Clone)]
#[contracttype]
pub enum DataKey {
Vec,
}
// Define the contract
#[contract]
pub struct ArrayAndBytesDemo;
#[contractimpl]
impl ArrayAndBytesDemo {
// Function to demonstrate dynamic bytes creation
pub fn dynamic_bytes_example(env: &Env) -> Bytes {
// Bytes (Bytes and BytesN)
let msg = "Hello, World!";
Bytes::from_slice(env, msg.as_bytes())
}
// Function to demonstrate fixed bytes creation; Results in a 32-byte array
pub fn fixed_bytes_example(env: &Env) -> BytesN<32> {
// Bytes (Bytes and BytesN)
let msg = "Hello, World!";
let msg_to_array = msg.as_bytes();
let mut msg_array = [0u8; 32];
msg_array[..msg_to_array.len()].copy_from_slice(&msg_to_array);
BytesN::from_array(env, &msg_array)
}
// Function to demonstrate creation of a Vec
pub fn vec_example(env: &Env) -> Vec<u32> {
// Vec
let vec: Vec<u32> = vec![&env, 0, 1, 2, 3];
vec
}
// Function to get a Vec from storage
pub fn get_vec(env: &Env) -> Vec<u32> {
// Vec
env.storage()
.persistent()
.get(&DataKey::Vec)
.unwrap_or(vec![&env, 0])
}
// Function to get an element from a Vec by index
pub fn get_vec_index(env: &Env, index: u32) -> u32 {
// Vec
let vec: Vec<u32> = env
.storage()
.persistent()
.get(&DataKey::Vec)
.unwrap_or(vec![&env, 0]);
vec.get(index).unwrap_or(0)
}
// Function to get the length of a Vec
pub fn get_vec_length(env: &Env) -> u32 {
// Vec
let vec: Vec<u32> = env
.storage()
.persistent()
.get(&DataKey::Vec)
.unwrap_or(vec![&env, 0]);
vec.len()
}
// Function to push an element into a Vec
pub fn push(env: &Env, value: u32) -> Vec<u32> {
// Vec
let mut vec = env
.storage()
.persistent()
.get(&DataKey::Vec)
.unwrap_or(vec![&env, 0]);
vec.push_back(value);
env.storage().persistent().set(&DataKey::Vec, &vec);
vec
}
// Function to remove the last element from a Vec
pub fn pop(env: &Env) -> Vec<u32> {
// Vec
let mut vec = env
.storage()
.persistent()
.get(&DataKey::Vec)
.unwrap_or(vec![&env, 0]);
vec.pop_back();
env.storage().persistent().set(&DataKey::Vec, &vec);
vec
}
// Function to remove an element from a Vec by index
pub fn remove(env: &Env, index: u32) -> Vec<u32> {
// Vec
let mut vec = env
.storage()
.persistent()
.get(&DataKey::Vec)
.unwrap_or(vec![&env, 0]);
vec.remove(index);
env.storage().persistent().set(&DataKey::Vec, &vec);
vec
}
}
¿Cómo se utilizan los tipos de datos?
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract MyContract {
bool public isActive = true;
uint256 public balance = 100;
string public name = "Alice";
// Function to deactivate the contract
function deactivate() public {
isActive = false;
}
// Function to activate the contract
function activate() public {
isActive = true;
}
// Function to update balance
function updateBalance(uint256 newBalance) public {
require(isActive, "Contract is not active");
balance = newBalance;
}
// Function to update name
function updateName(string memory newName) public {
require(isActive, "Contract is not active");
name = newName;
}
// Returns isActive
function getIsActive() public view returns (bool) {
return isActive;
}
// Returns balance
function getBalance() public view returns (uint256) {
return balance;
}
// Returns name
function getName() public view returns (string memory) {
return name;
}
}
Los contratos inteligentes de Soroban Rust comúnmente usan enumeraciones para representar diferentes aspectos o estados del contrato, especialmente para almacenar variables de estado.
#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, symbol_short, Env, Symbol};
// Define a contract type enum to represent different aspects of the contract
#[derive(Clone)]
#[contracttype]
pub enum DataKey {
IsActive,
Balance,
Name,
}
// Define the contract
#[contract]
pub struct MyContract;
#[contractimpl]
impl MyContract {
// Function to deactivate the contract
pub fn deactivate(env: &Env) {
env.storage().persistent().set(&DataKey::IsActive, &false); // Set the IsActive state to false
}
// Function to activate the contract
pub fn activate(env: &Env) {
env.storage().persistent().set(&DataKey::IsActive, &true); // Set the IsActive state to true
}
// Function to update the balance
pub fn update_balance(env: Env, new_balance: u128) {
let is_active: bool = env
.storage()
.persistent()
.get(&DataKey::IsActive)
.unwrap_or(false); // Get the IsActive state from storage or default to false
assert!(is_active, "Contract is not active"); // Ensure the contract is active
env.storage()
.persistent()
.set(&DataKey::Balance, &new_balance); // Update the balance in storage
}
// Function to update the name
pub fn update_name(env: Env, new_name: Symbol) {
let is_active: bool = env
.storage()
.persistent()
.get(&DataKey::IsActive)
.unwrap_or(false); // Get the IsActive state from storage or default to false
assert!(is_active, "Contract is not active"); // Ensure the contract is active
env.storage().persistent().set(&DataKey::Name, &new_name); // Update the name in storage
}
// Function to get the balance
pub fn get_balance(env: &Env) -> u128 {
env.storage()
.persistent()
.get(&DataKey::Balance)
.unwrap_or(0) // Get the balance from storage or default to 0
}
// Function to get the name
pub fn get_name(env: &Env) -> Symbol {
env.storage()
.persistent()
.get(&DataKey::Name)
.unwrap_or(symbol_short!("Unknown")) // Get the name from storage or default to "Unknown"
}
// Function to check if the contract is active
pub fn is_active(env: &Env) -> bool {
env.storage()
.persistent()
.get(&DataKey::IsActive)
.unwrap_or(false) // Get the IsActive state from storage or default to false
}
}
Struct
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
// This contract defines an account with a name and balance, and provides functions to update them.
contract AccountContract {
// Define a struct to represent an account with a name and balance
struct Account {
string name; // Name of the account holder
uint balance; // Balance of the account
}
// Declare a public variable of type Account to store the account details
Account public account;
// Constructor to initialize the account with a name and balance
constructor(string memory _name, uint _balance) {
account.name = _name; // Set the initial name
account.balance = _balance; // Set the initial balance
}
// Returns the account data
function get() public view returns (Account) {
return account;
}
// Function to update the balance of the account
function updateBalance(uint newBalance) public {
account.balance = newBalance;
}
// Function to update the name of the account holder
function updateName(string memory newName) public {
account.name = newName;
}
}
#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, symbol_short, Env, Symbol};
// Define a contract type for representing account data
#[contracttype]
pub struct Account {
pub name: Symbol, // Name of the account holder
pub balance: u128, // Balance of the account
}
// Define a contract type enum for representing data keys
#[derive(Clone)]
#[contracttype]
pub enum DataKey {
Account, // Represents the account data
}
// Define the main contract
#[contract]
pub struct MyContract;
#[contractimpl]
impl MyContract {
// Function to create a new account
pub fn new_account(env: &Env, new_name: Symbol, new_balance: u128) -> Account {
// Store the new account data
env.storage().persistent().set(
&DataKey::Account,
&Account {
name: new_name.clone(),
balance: new_balance,
},
);
// Return the created account
Account {
name: new_name.clone(),
balance: new_balance,
}
}
// Function to retrieve the account data
pub fn get_account(env: &Env) -> Account {
// Get the account data from storage or return default values if not found
env.storage()
.persistent()
.get(&DataKey::Account)
.unwrap_or(Account {
name: symbol_short!("none"),
balance: 0,
})
}
// Function to update the account balance
pub fn update_balance(env: &Env, new_balance: u128) {
// Get the current account data from storage
let account = env
.storage()
.persistent()
.get(&DataKey::Account)
.unwrap_or(Account {
name: symbol_short!("none"),
balance: 0,
});
// Update the account balance in storage
env.storage().persistent().set(
&DataKey::Account,
&Account {
name: account.name,
balance: new_balance,
},
);
}
// Function to update the account name
pub fn update_name(env: &Env, new_name: Symbol) {
// Get the current account data from storage
let account = env
.storage()
.persistent()
.get(&DataKey::Account)
.unwrap_or(Account {
name: symbol_short!("none"),
balance: 0,
});
// Update the account name in storage
env.storage().persistent().set(
&DataKey::Account,
&Account {
name: new_name,
balance: account.balance,
},
);
}
}
Enums
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Enum {
// Enum representing shipping status
enum Status {
Pending,
Shipped,
Accepted,
Rejected,
Canceled
}
// Default value is the first element listed in
// definition of the type, in this case "Pending"
Status public status;
// Returns uint
// Pending - 0
// Shipped - 1
// Accepted - 2
// Rejected - 3
// Canceled - 4
function get() public view returns (Status) {
return status;
}
// Update status by passing uint into input
function set(Status _status) public {
status = _status;
}
// You can update to a specific enum like this
function cancel() public {
status = Status.Canceled;
}
// delete resets the enum to its first value, 0
function reset() public {
delete status;
}
}
Este contrato inteligente de Soroban Rust, llamado StateContract
, está diseñado para gestionar el estado de un pedido usando el Estado
enum. El enum Estado define varios estados del pedido, incluyendo Pendiente
, Enviado
, Aceptado
, Rechazado
, y Cancelado
. Cada variante del enum está asociada con un valor entero, que se almacena como un u32
valor en el libro mayor.
#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, Env};
// Enum para representar diferentes claves de datos
#[derive(Clone)]
#[contracttype]
pub enum DataKey {
Status,
}
// Definir el enum para representar diferentes estados
#[derive(Clone, Copy, PartialEq, Eq)]
#[contracttype]
pub enum Status {
Pending = 0,
Shipped = 1,
Accepted = 2,
Rejected = 3,
Canceled = 4,
}
#[contract]
pub struct StateContract;
#[contractimpl]
impl StateContract {
// Inicializar el contrato con el estado inicial
pub fn initialize(env: &Env) {
env.storage()
.persistent()
.set(&DataKey::Status, &Status::Pending);
}
// Función para obtener el estado actual del contrato
pub fn get_state(env: &Env) -> Status {
env.storage()
.persistent()
.get(&DataKey::Status)
.unwrap_or(Status::Pending)
}
// Función para establecer el estado del contrato
pub fn set_state(env: &Env, new_state: Status) {
env.storage().persistent().set(&DataKey::Status, &new_state);
}
// }
// Función para establecer el estado del contrato a enviado
pub fn ship(env: &Env) {
env.storage()
.persistent()
.set(&DataKey::Status, &Status::Shipped);
}
}
Mapeo
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Mapping {
// Mapeo de dirección a uint
mapping(address => uint256) public myMap;
mapping(address => mapping(uint256 => bool)) public nested;
function get(address _addr) public view returns (uint256) {
// El mapeo siempre devuelve un valor.
// Si el valor nunca fue establecido, devolverá el valor por defecto.
return myMap[_addr];
}
function set(address _addr, uint256 _i) public {
// Actualizar el valor en esta dirección
myMap[_addr] = _i;
}
function remove(address _addr) public {
// Restablecer el valor al valor por defecto.
delete myMap[_addr];
}
function getNestedMap(address _addr1, uint256 _i)
public
view
returns (bool)
{
// Puedes obtener valores de un mapeo anidado
// incluso cuando no está inicializado
return nested[_addr1][_i];
}
function setNestedMap(
address _addr1,
uint256 _i,
bool _boo
) public {
nested[_addr1][_i] = _boo;
}
function removeNestedMap(address _addr1, uint256 _i) public {
delete nested[_addr1][_i];
}
}
En Soroban Rust, aunque hay un tipo Map disponible para mapear datos, es una práctica común usar enums para representar diferentes tipos de mapeos.
En el contrato inteligente de Soroban Rust proporcionado, el DataKey
enum se utiliza para representar diferentes claves de almacenamiento para mapear datos. Define dos claves distintas que toman diferentes elementos:
Dirección
. Esta variante permite mapear una dirección específica a un valor correspondiente.Dirección
y un u64
índice. Esta variante permite la creación de una estructura de mapeo más compleja donde cada dirección puede tener varios valores booleanos asociados con diferentes índices.#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, Address, Env};
#[derive(Clone, PartialEq, Eq)]
#[contracttype]
pub enum DataKey {
Map(Address),
NestedMap(Address, u64),
}
#[contract]
pub struct MappingContract;
#[contractimpl]
impl MappingContract {
pub fn get(env: &Env, addr: Address) -> u64 {
env.storage()
.persistent()
.get(&DataKey::Map(addr))
.unwrap_or(0)
}
pub fn set(env: &Env, addr: Address, value: u64) {
env.storage().persistent().set(&DataKey::Map(addr), &value);
}
pub fn remove(env: &Env, addr: Address) {
env.storage().persistent().remove(&DataKey::Map(addr));
}
pub fn get_nested_map(env: &Env, addr: Address, index: u64) -> bool {
env.storage()
.persistent()
.get(&DataKey::NestedMap(addr, index))
.unwrap_or(false)
}
pub fn set_nested_map(env: &Env, addr: Address, index: u64, value: bool) {
env.storage()
.persistent()
.set(&DataKey::NestedMap(addr, index), &value);
}
pub fn remove_nested_map(env: &Env, addr: Address, index: u64) {
env.storage().persistent().remove(&DataKey::NestedMap(addr, index));
}
}
Rust vs Solidity
La transición de Solidity a Soroban Rust implica entender las diferencias y similitudes en los tipos de datos. Mientras que algunos conceptos son directamente traducibles, Soroban introduce eficiencias y características adaptadas a su entorno. Para documentación detallada, por favor echa un vistazo a la documentación de Soroban y únete a nuestro Discord para ejemplos reales y más información. Abraza el viaje para aprovechar el potencial completo de Rust en el desarrollo de contratos inteligentes. ¡Feliz codificación!
Serie de Solidity a Rust Pt. 1
¡Consulta la Primera Parte de La Guía Esencial para el Desarrollo en Rust!