Serie de Solidity a Rust Pt. 2

EVM a Soroban Entendiendo los Tipos de Datos

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

¿Qué son los 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

¿Qué son los Tipos de Datos de Solidity?

En Solidity, encontramos una variedad de tipos de datos, incluyendo:

  • Tipos Primitivos: Estos son los tipos de datos básicos en Solidity.
    • 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.
  • Tipos Compuestos: Estos tipos permiten estructuras de datos más complejas.
    • 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

¿Qué son los Tipos de Datos de Rust de Soroban?

Soroban utiliza tipos de datos de Rust, con una mezcla de tipos familiares y únicos, incluyendo:

  • Tipos Primitivos:
    • 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.
  • Tipos Compuestos:
    • 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.
  • Tipos Personalizados:
    • 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

¿Cuáles son las diferencias entre los tipos de datos frecuentemente usados en Solidity vs. Soroban Rust?

Ambos entornos admiten tipos de datos mutables y constantes, pero hay matices:

  • Cadena vs. Símbolo: Solidity utiliza string para texto de tamaño variable, mientras que Soroban usa Símbolo para cadenas cortas o identificadores eficientes.

Solidity:

// 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!";
}

Rust de Soroban:

#![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!")
    }
}
  • Direcciones: Ambas plataformas usan direcciones para denotar cuentas o contratos, pero Soroban mejora la funcionalidad con características de autorización directa.

Solidity:

// 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);
    }
}

Rust de Soroban, siendo un entorno determinista y contenido, no tiene el concepto de msg.sender como se ve en Solidity. En Soroban, todas las operaciones se realizan dentro del contexto de la estructura Env, que encapsula el entorno de la ejecución del contrato inteligente. No hay variables globales; en su lugar, todo se accede y manipula a través de la estructura Env.

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()
    }
}
  • Bytes y Arrays: Conceptos similares pero con tipos específicos de Rust como Bytes, BytesN y Vec. En Soroban Rust, el equivalente a los arrays de Solidity se representa típicamente usando el tipo Vec de Rust. Mientras que los arrays en Solidity tienen un tamaño fijo y son adecuados para escenarios donde el tamaño de la colección se conoce de antemano, Soroban Rust favorece el tipo Vec más flexible para la mayoría de los casos de uso.

Solidity:

// 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];
    }
}

Soroban Rust:

#![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
    }
}
  • Structs y Enums: Ambos se utilizan de manera similar para agrupar datos o definir conjuntos finitos de valores, con Rust proporcionando variantes adicionales y optimizaciones de almacenamiento. Estos conceptos son fundamentales para el desarrollo de contratos inteligentes y se utilizan extensivamente tanto en Solidity como en Soroban Rust, como se demuestra en las siguientes secciones.

¿Cómo se utilizan los tipos de datos?

Cómo declarar variables de estado en Solidity vs Soroban Rust

Solidity:

// 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;
    }
}

Soroban Rust:

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

Cómo construir un Struct en Solidity vs Soroban Rust

Solidity:

// 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; 
    }
}

Soroban Rust:

#![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

Cómo Usar Enums en Solidity vs Soroban Rust

Solidity:

// 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;
    }
}

Soroban Rust:

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

Cómo Usar Mapeo en Solidity vs Soroban Rust

Solidity:

// 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];
    }
}

Soroban Rust:

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:

  • Map: Esta clave representa un mapeo simple de una dirección a un valor. Contiene un solo elemento, que es una Dirección. Esta variante permite mapear una dirección específica a un valor correspondiente.
  • NestedMap: Esta clave representa un mapeo anidado de una dirección y un índice a un valor booleano. Contiene dos elementos: una 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

¿Por Qué Elegir Rust Sobre 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

Cómo Migrar Contratos Inteligentes del Solidity de Ethereum a Soroban Rust

¡Consulta la Primera Parte de La Guía Esencial para el Desarrollo en Rust!