Sui.

Пост

Поделитесь своими знаниями.

article banner.
harry phan.
Apr 24, 2025
Статья

Логика привязки к душе и шаблон возврата квитанции

#Логика привязки к душе и схема получения возврата

Теперь перейдем к специальному шаблону проектирования, использующему механизм «родитель-потомок»:объекты, привязанные к душа. Объект, привязанный к душе, должен быть привязан к определенному владельцу и не подлежит постоянной передаче. Однако, возможно, вы захотите временно предоставить кому-либо доступ к нему в рамках одной транзакции (например, выполнить с ним какую-либо операцию), но при этом обязательно вернуть его вам к концу этой транзакции. Это достигается с помощью так называемого шаблонаReturnReceiption, представляющего собой разновидность паттерна «горячая картошка».

###Схема приготовления горячего картофеля по-суи

Узор «горячая картошка» назван в честь детской игры: если вам вручат горячую картошку 🌡️🥔, вы не сможете ее удержать; вы должны быстро передать ее. С точки зрения Move, горячая картошка — это обычно предмет или ресурс, которые обязательно нужно съесть или передать в одну и ту же транзакцию, иначе транзакция завершится неудачей. Часто для этого нужно запретить удалять объект (чтобы его нельзя было просто проигнорировать или бросить) и разработать логику, согласно которой единственный способ избавиться от него — выполнить необходимое действие (например, вернуть кредит).

Типичный вариант использования — «срочный» займ**: в ходе транзакции вы одалживаете несколько монет (получаете объект займа плюс «долговой» токен). Если вы не выплатите кредит до конца сделки, вы не сможете ее завершить, потому что у вас все еще есть долговой токен, на списание которого вы не имеете разрешения, что, по сути, вынуждает вас вернуть кредит или сделать аборт.

###Пример объекта, привязанного к душу

Допустим, у нас есть объект SoulBound, и мы всегда хотим сохранить его первоначальный адрес владельца. Мы можем потребовать, чтобы, если кто-то «одолжил» объект (в качестве дочернего объекта, принадлежащего другому родителю), он должен вернуть его в той же транзакции. Как? Используя квитанцию о возврате товара.

Ниже приведена упрощенная версия, вдохновленная документацией Sui: пример объектов, привязанных к душе

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

В этом дизайне:

  • SoulBound — это объект, предназначенный только для ключа. Отказав ему в хранении, мы запрещаем использовать на нем команды public_transfer или public_receive, то есть передавать или получать его можно только с помощью функций определяющего модуля (при условии использования нашей собственной логики).
  • take_soul_bound (аналогично «get_object» в документации) — это функция, которую владелец вызывал бы, чтобы забрать свой объект, привязанный к душе, у какого-либо родителя. Она вызывает transfer: :receive для получения объекта SoulBound. Поскольку у SoulBound нет хранилища, этот вызов разрешен только здесь, в модуле (и это нормально). Затем создается структура ReturnReceipt, содержащая идентификатор объекта, привязанного к душе, и адрес, к которому нужно вернуться (адрес родителя). Мы возвращаемобообъект и чек.
  • ReturnReceipt не может копировать, сохранять или копировать (мы ничего не декларировали, поэтому к нему относятся как к ресурсу, который нельзя удалить или дублировать). Это означает, что транзакциядолжнас ней что-то сделать**; в противном случае это останется неиспользованным ресурсом и виртуальная машина не допустит успешного завершения транзакции. По сути, ReturnReceipt — это наш популярный токен 🔥🥔.
  • Единственное, что можно сделать с ReturnReceipt, — это вызвать return_soul_bound (или аналогичную назначенную функцию) в той же транзакции. В return_soul_bound мы проверяем, совпадает ли идентификатор объекта SoulBound с записью чека (чтобы никто не попытался вернуть другой объект с неправильным чеком). Затем мы вызываем команду transfer: :transfer (sb, receipt.return_to) и отправляем объект SoulBound обратно по адресу, указанному в чеке (первоначальному владельцу). Это эффективно восстанавливает объект, привязанный к душе, на его законное место. ReturnReceipt используется в качестве аргумента (таким образом, уничтожается).
  • Если пользователь попытается завершить транзакцию без вызова return_soul_bound, он все равно будет держать в руках ReturnReceipt (из выходных данных take_soul_bound). Поскольку ReturnReceipt не теряется, виртуальная машина Move откажется завершить транзакцию (в Move вы не можете просто сбросить ресурс; его нужно использовать или где-то хранить). Транзакция будет прервана или признана недействительной, то есть вся операция будет отменена.Результат: вы не можете просто сбежать с объектом, привязанным к душе; если вы его не вернете, запрос не будет выполнен и он останется у родителя.

С момента вызова take_soul_bound до вызова return_soul_bound в транзакции у вас есть доступное значение объекта SoulBound. Предположительно, для выполнения с ним некоторых разрешенных операций (например, для чтения или использования по мере необходимости). Но выобязанывернуть его до завершения транзакции, поскольку чек был подтвержден в силе.

Этот паттерн часто называют «привязанным к душе = не могу расстаться с владельцем», но точнее сказать, что он может остаться в течение одной транзакции в качестве дочернего объекта с гарантией возврата. Это все равно, что взять книгу в библиотеку: ее нужно вернуть до закрытия библиотеки на целый день 😅.

Почему бы просто не передавать её вообще? Есть сценарии, в которых полезно временно перенести объект в другой объект (или контекст). Одним из примеров является контекстобщего объекта: возможно, объект SoulBound во время некоторой операции хранится в общем объекте и должен быть удален первоначальным владельцем. Другой вариант — разрешить контролируемую композицию — возможно, вы захотите разрешить какому-то модулю работать с вашим объектом, передав его объекту этого модуля, а затем вернуть обратно.

Сделав использование только ключа SoulBound, мы позаботились о том, чтобы ни один внешний пользователь public_receive не смог получить его без участия нашего модуля. Используя чек, мы обязуемся соблюдать правила возврата товара. В комментарии, приведенном в примере кода Суи, даже отмечается, что ReturnReceipt не позволяет обменивать их местами: если за одну транзакцию было изъято два предмета, переплетенных с душой, каждому чеку присваивается определенный идентификатор объекта, поэтому вы не сможете перепутать их и вернуть не тот предмет, чтобы обмануть его.

Обобщая идею: Шаблон ReturnReceipt можно использовать всякий раз, когда вы хотите принудительно вернуть объект. Флэш-кредиты используют аналогичную концепцию (кредитный токен и долговой токен, который необходимо погасить). Каждый раз, когда у вас есть инвариант «объект X должен быть возвращен по адресу Y к концу tx», вы можете создать ресурс чеков, подтверждающий его.

###Краткое описание:

-Объекты, привязанные к душа: только ключи, извлекаемые только по логике модуля. -ReturnReceipt: фиктивный ресур��, возвращаемый вместе с объектом для обеспечения его возврата. Он не удаляется, поэтому пользователь должен вызвать функцию return в противном случае. -Горячая картошка: если вы держите в руках квитанцию о возврате товара, лучше обработать ее (вернуть объект) до того, как «музыка остановится» (транзакция завершится).

  • Если объект не будет возвращен, транзакция не будет успешно выполнена — внесенные изменения будут отменены, что позволит эффективно соблюдать правило, связанное с душой.

На этом мы рассмотрели основные концепции отношений между родителями и детьми в Sui Move!

##5. Структура проекта и стратегия тестирования (готовы к использованию GitHub)

Чтобы закрепить эти концепции, вы можете создать проект Sui Move и поэкспериментировать с примерами. Вот предлагаемый макет проекта, включающий модули для приведенных выше примеров. Можно компилировать и запускать модульные тесты или использовать интерфейс командной строки Sui для взаимодействия с ними.

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: Обязательно укажите фреймворк Sui и любые адреса вашего пакета. Например:

[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"
  • (Используйте правильный адрес вместо 0x0, если того требует Sui, или именованный адрес, который вы укажете при публикации. ) *

В каталоге исходников:

-toy_box.moveможет содержать пример модуля: :toy_box из части 2 с определениями игрушек и коробок и функцией take_toy. -parcel.moveиwarehouse.moveбудут содержать модули из части 3. -soul_bound.moveбудет содержать модуль из части 4.

Каждый модуль должен использовать sui::... по мере необходимости для передачи, объекта, tx_context и т. д., как показано в примерах. Затем tests/parent_child_test.move может импортировать эти модули и моделировать сценарии:

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.
}

Вышесказанное является концептуальной иллюстрацией. На практике тестовый контекст Sui может отличаться — возможно, вам придется использовать transfer: :public_transfer в тестах при переносе в объект (если это не входит в модуль) и использовать функцию Получение из контекста транзакции. В Sui предусмотрена функция transfer: :make_receiver (функция, предназначенная только для тестирования), которая позволяет создать файл Receiving с указанием идентификатора и версии, что полезно в модульных тестах для моделирования ввода дочернего объекта.

Выполнение тестов: Вы можете запустить тест sui move, который выполнит функции # [test]. Или разверните пакет в локальной сети Sui и вызовите функции ввода через интерфейс командной строки или SDK, чтобы наблюдать за поведением:

  • Создайте посылку, создайте склад, вызовите transfer: :public_transfer через интерфейс командной строки, чтобы поместить посылку на склад, а затем вызовите withdraw_parcel.
  • Создайте объект SoulBound (например, сделав запись забавной), передайте ее какому-нибудь родительскому объекту (или общему объекту), затем вызовите take_soul_bound за одну транзакцию и пропустите return_soul_bound, чтобы увидеть, что транзакция завершилась неудачно (ожидается), вместо того чтобы включить обратный вызов, чтобы убедиться в успехе.

Каждая часть этого проекта посвящена одной из тем:

  • toy_box.move: базовый родитель-ребенок и получение.
  • parcel.move и warehouse.move: межмодульные дочерние элементы и public_receive.
  • soul_bound.move: в переплете с чеком возврата товара.

Поэкспериментировав с ними, вы углубите свое понимание объектной модели Суи. Код разработан таким образом, чтобы он был образовательным, и его не следует использовать в рабочей среде без проверок безопасности, но он служит хорошей отправной точкой.


Вывод: Функциональность родительско-дочерних объектов Sui Move очень мощна. Она позволяет создавать сложные структуры данных в блокчейне (например, запасы, кошельки, коллекции) с детальным контролем доступа. Сочетание transfer: :receive/public_receive и системы типов Move позволяет извлекать дочерние объекты только авторизованным кодом, а такие шаблоны, как ReturnReceipt, позволяют применять правила временного владения (привязка к персональным данным, мгновенные займы и т. д.). Мы привели несколько забавных аналогий и смайликов, но, в конце концов, это надежные инструменты для разработчиков. А теперь иди и создай магию из вложенных объектов! 🚀🔥

Когда объект передается другому объекту вместо адреса учетной записи, они образуют отношениеродитель-дочерь. Вы можете представить себе это следующим образом:

-Родитель= объект-контейнер -Ребенок= вещь, помещенная внутрь

Move не имеет значения, переходите ли вы на аккаунт или на идентификатор объекта. Он просто перемещается.

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

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

##Создание родительских и дочерних объектов

Родитель должен быть изменчивым и существовать. Нельзя складывать вещи в неизменную коробку!

  • Sui
  • Architecture
4
Поделиться
Комментарии
.

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

610Посты1335Ответы
Sui.X.Peera.

Заработай свою долю из 1000 Sui

Зарабатывай очки репутации и получай награды за помощь в развитии сообщества Sui.

Кампания вознагражденийИюль