Sui.

Publicación

Comparte tu conocimiento.

article banner.
harry phan.
Apr 24, 2025
Artículo

La lógica vinculante y el patrón de recibos de devolución

#La lógica vinculante y el patrón de recibos de devolución

Pasemos ahora a un patrón de diseño especial que usa el mecanismo padre-hijo:soul-bound objects. Un objeto vinculado al alma está pensado para estar vinculado a un propietario específico y no para ser transferible de forma permanente. Sin embargo, es posible que desees conceder a alguien acceso a él temporalmente en una sola transacción (por ejemplo, para realizar alguna operación en él), pero asegúrate de que te lo devuelvan al final de esa transacción. Esto se logra con lo que denominamos el patrónReturnReceip, una variante del patrón de «patatas calientes».

###El patrón de patatas calientes en Sui

El patrón de «patatas calientes» lleva el nombre de un juego infantil: si te dan una papa caliente 🌡️🥔, no puedes agarrarla; debes pasártela rápidamente. En términos de Move, una patata caliente suele ser un objeto o recurso que deber consumirse o transferirse en la misma transacción; de lo contrario, la transacción fallará. Esto se suele implementar dándole al objeto la habilidad de no caer (de forma que no puedas ignorarlo o dejarlo caer) y diseñando la lógica de forma que la única forma de deshacerte de él sea realizando la acción requerida (como devolver un préstamo).

Un caso de uso habitual es el de lospréstamos instantáneos: pides prestadas algunas monedas en una transacción (obtienes un objeto de préstamo más una ficha de «deuda»). Si no devuelves el dinero al final de la transacción, no puedes finalizarla porque aún conservas ese bono de deuda que no tienes permiso para dejar caer, lo que te obliga a devolver el préstamo o abortarlo.

###Ejemplo de un objeto ligado al alma

Supongamos que tenemos un objeto de SoulBound y queremos que siempre permanezca con la dirección de su propietario original. Podemos exigir que si alguien lo «toma prestado» (como objeto secundario de algún padre), debe devolverlo en la misma transacción. ¿Cómo? Mediante un recibo de devolución.

A continuación se muestra una versión simplificada inspirada en la documentación de Sui, un ejemplo de objetos ligados al alma

module demo::soul_bound {
    use sui::transfer::{Self, Receiving};
    use sui::object::UID;
    use sui::transfer;

    const EWrongObject: u64 = 0;

    /// A soul-bound object that cannot be permanently transferred.
    /// It has only `key` ability (no `store`), to tighten transfer/receive rules [oai_citation_attribution:40‡docs.sui.io](https://docs.sui.io/concepts/transfers/transfer-to-object#:~:text=%2F%2F%2F%20This%20object%20has%20,id%3A%20UID%2C).
    public struct SoulBound has key {
        id: UID,
        data: u64
    }

    /// A receipt that proves a SoulBound object must be returned.
    /// No abilities: cannot be copied, dropped, or stored (implicitly).
    public struct ReturnReceipt {
        /// The object ID of the soul-bound object that must be returned.
        object_id: UID,
        /// The address (or object ID) it must be returned to (the original owner).
        return_to: address
    }

    /// Allows the owner of `parent` to retrieve their SoulBound child.
    /// Returns the SoulBound object *and* a ReturnReceipt that compels return.
    public fun take_soul_bound(parent: &mut UID, sb_ticket: Receiving<SoulBound>): (SoulBound, ReturnReceipt) {
        let sb = transfer::receive(parent, sb_ticket);  // receive SoulBound (only this module can do it, since SoulBound has no store) [oai_citation_attribution:41‡docs.sui.io](https://docs.sui.io/concepts/transfers/transfer-to-object#:~:text=%2F%2F%2F%20,to_address%28%29%2C%20object_id%3A%20object%3A%3Aid%28%26soul_bound%29%2C) 
        let receipt = ReturnReceipt {
            object_id: sb.id,
            return_to: parent.to_address()
        };
        (sb, receipt)
    }

    /// Return a SoulBound object using the ReturnReceipt.
    /// This must be called in the same transaction after take_soul_bound.
    public fun return_soul_bound(sb: SoulBound, receipt: ReturnReceipt) {
        // Verify the receipt matches this SoulBound object
        assert!(sb.id == receipt.object_id, EWrongObject);
        // Send the SoulBound object back to the original owner address
        transfer::transfer(sb, receipt.return_to);
        // (ReturnReceipt is consumed/destroyed here as function param)
    }
}

En este diseño:

  • SoulBound es un objeto de solo clave. Al no darle almacenamiento, evitamos el uso de public_transfer o public_receive en él, lo que significa que la única forma de transferirlo o recibirlo es a través de las funciones de su módulo definitorio (garantizando que se utilice nuestra lógica personalizada).
  • take_soul_bound (análoga a «get_object» en la documentación) es una función que el propietario utilizaría para eliminar un objeto enlazado con el alma de algún elemento principal. Llama a transfer: :receive para obtener el objeto SoulBound. Como SoulBound no tiene tienda, esta llamada solo está permitida aquí en su módulo (lo cual está bien). A continuación, crea una estructura ReturnReceipt que contiene el ID del objeto enlazado con el alma y la dirección a la que volver (la dirección del objeto principal). Devolvemosamboel objeto y el recibo.
  • ReturnReceipt no permite borrar, almacenar ni copiar (no declaramos ninguno, por lo que se trata como un recurso que no se puede borrar ni duplicar). Esto significa que la transaccióndebehacer algo con ella; de lo contrario, será un recurso sobrante y la máquina virtual no permitirá que la transacción finalice correctamente. Básicamente, el ReturnReceipt es nuestra prueba de patatas calientes 🔥🥔.
  • Lo único válido con un ReturnReceipt es llamar a return_soul_bound (o a una función designada similar) en la misma transacción. En return_soul_bound, verificamos que el ID del objeto de SoulBound coincide con el registro del recibo (para evitar que alguien intente devolver un objeto diferente con un recibo incorrecto). A continuación, llamamos a transfer: :transfer (sb, receipt.return_to), que devuelve el objeto de SoulBound a la dirección que aparece en el recibo (la del propietario original). Esto restaura de manera efectiva el objeto vinculado al alma al lugar que le corresponde. El ReturnReceipt se consume como argumento (por lo tanto, se destruye).
  • Si el usuario intenta finalizar la transacción sin llamar a return_soul_bound, seguirá reteniendo el ReturnReceipt (de la salida de take_soul_bound). Dado que ReturnReceipt no contiene ninguna opción, la máquina virtual de Move se negará a completar la transacción (en Move, no puedes simplemente descartar un recurso, sino que debe usarse o almacenarse en algún lugar). La transacción se cancelaría o se consideraría inválida, lo que significa que toda la operación se anularía.Resultado: no puedes quedarte sin más con el objeto ligado al alma; si no lo devuelves, el impuesto falla y se queda en manos del objeto principal.

Desde el momento en que llamas a take_soul_bound hasta el momento en que llamas a return_soul_bound en la transacción, tienes el valor del objeto SoulBound disponible, presumiblemente para realizar algunas operaciones permitidas en él (tal vez leerlo o usarlo según sea necesario). Perodeberásdevolverlo antes de que finalice la transacción, gracias a la garantía de recepción.

Este patrón suele describirse como «con el alma limitada = no puede dejar a su propietario», pero para ser más exactos, puede mantener dentro de una sola transacción un objeto secundario, con la garantía de que volverá a aparecer. Es como sacar un libro de la biblioteca: tienes que devolverlo antes de que la biblioteca cierre por un día 😅.

¿Por qué no lo transfieres nunca? Hay situaciones en las que es útil transferir temporalmente un objeto a otro objeto (o contexto). Un ejemplo es el contexto de unobjeto compartido: tal vez el elemento de SoulBound se encuentre dentro de un objeto compartido durante alguna operación y el propietario original deba eliminarlo. Otra opción es permitir una composición controlada. Es posible que quieras dejar que algún módulo opere en tu objeto dándolo al objeto de ese módulo y recuperándolo.

Al hacer que SoulBound sea solo con clave, nos aseguramos de que ningún public_receive externo pueda extraerlo sin la participación de nuestro módulo. Al usar el recibo, obligamos a cumplir con la política de devoluciones. El comentario del código de ejemplo de Sui incluso señala que el ReturnReceipt evita el intercambio: si en una transacción se sacan dos objetos encuadernados en una transacción, cada recibo lleva un identificador de objeto específico para que no puedas mezclarlos y devolver el incorrecto para hacer trampa.

Generalizando la idea: El patrón ReturnReceipt se puede utilizar siempre que se quiera exigir la devolución de un objeto. Los préstamos flash utilizan un concepto similar (token de préstamo y token de deuda que deben reembolsarse). Siempre que te quede invariable la frase «el objeto X debe terminar de nuevo en la dirección Y antes de que finalice el período de impuestos», puedes crear un recurso de recibos para confirmarlo.

###Resumen rápido:

-Objetos ligados al alma: solo con clave, recuperados únicamente mediante la lógica de su módulo. -returnReceipt: un recurso ficticio que se devuelve junto con el objeto para garantizar que se devuelva. No se descarta, por lo que el usuario debe llamar a la función de devolución o fallar. -Patatas calientes: Si tienes en la mano un ReturnReceipt, es mejor que lo manipules (devuelvas el objeto) antes de que «la música se detenga» (finalice la transacción).

  • Si el objeto no se devuelve, la transacción no se ejecutará correctamente: los cambios se anulan y se aplica de manera efectiva la regla del límite máximo.

Y con eso, ¡hemos abordado los principales conceptos de las relaciones entre padres e hijos en Sui Move!

#5. Estructura del proyecto y estrategia de pruebas (lista para GitHub)

Para solidificar estos conceptos, es posible que desees crear un proyecto de Sui Move y jugar con los ejemplos. Este es un diseño de proyecto sugerido que incluye módulos para los ejemplos anteriores. Puedes compilar y ejecutar pruebas unitarias o usar la CLI de Sui para interactuar con ellas.

parent_child_demo/
├── Move.toml
├── sources/
│   ├── toy_box.move        (Parent & child in same module example)
│   ├── parcel.move         (Child module for Parcel)
│   ├── warehouse.move      (Parent module for Warehouse, uses public_receive)
│   └── soul_bound.move     (SoulBound and ReturnReceipt module)
└── tests/
    └── parent_child_test.move   (Integration tests for the modules)

Move.toml: Asegúrese de incluir el marco Sui y las direcciones de su paquete. Por ejemplo:

[package]
name = "parent_child_demo"
version = "0.0.1"
dependencies = [ 
    { name = "Sui", git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework", rev = "devnet" }
]

[addresses]
demo = "0x0"
  • (Utilice una dirección adecuada en lugar de 0x0 si Sui lo exige, o una dirección con nombre que asignará al publicar. ) *

En el directorio de fuentes:

-toy_box.movepodría contener el módulo example: :toy_box de la segunda parte, con las definiciones de Toy y Box y la función take_toy. -parcel.moveywarehouse.movecontendrán los módulos de la parte 3. -soul_bound.movecontendrá el módulo de la parte 4.

Cada módulo debe usar sui::... según sea necesario para la transferencia, el objeto, el tx_context, etc., como se muestra en los ejemplos. A continuación, el tests/parent_child_test.move puede importar estos módulos y simular escenarios:

module demo::parent_child_test {
    use 0xYourAddr::toy_box;
    use 0xYourAddr::warehouse;
    use 0xYourAddr::parcel;
    use 0xYourAddr::soul_bound;
    use sui::tx_context::TxContext;

    #[test]
    fun test_toy_withdraw() {
        let ctx = TxContext::new(); // pseudo, context is usually provided
        let my_box = toy_box::Box { id: object::new(&mut ctx) };
        let my_toy = toy_box::Toy { id: object::new(&mut ctx), name: b"Ball".to_vec() };
        // Transfer toy into the box
        transfer::transfer(my_toy, my_box.id); 
        // Now simulate receiving it back
        let ticket = transfer::Receiving<toy_box::Toy>{ /*...*/ }; // In real test, use transfer::make_receiver or a transaction call
        let returned_toy = toy_box::take_toy(&mut my_box, ticket);
        assert!(returned_toy.name == b"Ball".to_vec());
    }

    // Additional tests for warehouse/parcel and soul_bound can be written similarly.
}

Lo anterior es una ilustración conceptual. En la práctica, el contexto de prueba de Sui puede ser diferente: es posible que necesites usar transfer: :public_transfer en las pruebas cuando transfieras a un objeto (si está fuera de un módulo) y usar el contexto de recepción de la transacción. Sui proporciona una función transfer: :make_receiver (función solo de prueba) que puede fabricar un Receiving con un ID y una versión, lo que resulta útil en las pruebas unitarias para simular la entrada de un objeto secundario.

Ejecución de pruebas: Puedes ejecutar la prueba sui move, que ejecutará las funciones # [test]. O implementa el paquete en una red Sui local y llama a las funciones de entrada a través de la CLI o un SDK para observar el comportamiento:

  • Cree un paquete, cree un almacén, llame a transfer: :public_transfer mediante la CLI para colocar el paquete en el almacén y, a continuación, llame a draw_parcel.
  • Crea un objeto de SoulBound (tal vez haciendo que una entrada sea divertida para acuñar uno), transfiérelo a algún objeto principal (o compartido) y, a continuación, en una sola transacción, llama a take_soul_bound y omite return_soul_bound para ver que la transacción falla (lo esperado), en lugar de incluir la llamada de devolución para ver si se ha realizado correctamente.

Cada parte de este proyecto aborda uno de los siguientes temas:

  • toy_box.move: sistema básico de padre-hijo y recepción.
  • parcel.move & warehouse.move: hijos de varios módulos y public_receive.
  • soul_bound.move: enlazado con el alma con ReturnReceipt.

Al experimentar con ellos, profundizarás en tu comprensión del modelo de objetos de Sui. El código está estructurado para ser educativo y no debe usarse tal cual para su producción sin revisiones de seguridad, pero proporciona un punto de partida sólido.


Conclusión: La funcionalidad de objetos padre-hijo de Sui Move es poderosa. Le permite crear estructuras de datos complejas en cadena (como inventarios, carteras, colecciones) con un control de acceso detallado. La combinación de transfer: :receive/public_receive y el sistema de tipos de Move garantiza que solo el código autorizado pueda recuperar objetos secundarios, y patrones como ReturnReceipt permiten hacer cumplir las reglas de propiedad temporal (encuadernación, préstamos flash, etc.). Hemos incluido algunas analogías y emojis divertidos, pero al fin y al cabo, se trata de herramientas robustas para los creadores. ¡Ahora anímate y construye un poco de magia con objetos anidados! 🚀🔥

Cuando un objeto se transfiere a otro objeto en lugar de a una dirección de cuenta, forma unarelación padre-hijo. Puedes pensar en ello de la siguiente manera:

-Padre= objeto contenedor -Niño= cosa colocada dentro

A Move no le importa si lo transfieres a una cuenta o a un ID de objeto. Simplemente se mueve.

public struct Parent has key {
    id: UID,
    name: String,
}

public struct Child has key {
    id: UID,
    description: String,
}

##Creación de objetos principales e hijos

El elemento principal debe ser mutable y debe existir. ¡No puedes meter cosas en una caja inmutable!

  • Sui
  • Architecture
4
Cuota
Comentarios
.

Sui is a Layer 1 protocol blockchain designed as the first internet-scale programmable blockchain platform.

610Publicaciones1335Respuestas
Sui.X.Peera.

Gana tu parte de 1000 Sui

Gana puntos de reputación y obtén recompensas por ayudar a crecer a la comunidad de Sui.

Campaña de RecompensasJulio