Пост
Поделитесь своими знаниями.
Логика привязки к душе и шаблон возврата квитанции
#Логика привязки к душе и схема получения возврата
Теперь перейдем к специальному шаблону проектирования, использующему механизм «родитель-потомок»:объекты, привязанные к душа. Объект, привязанный к душе, должен быть привязан к определенному владельцу и не подлежит постоянной передаче. Однако, возможно, вы захотите временно предоставить кому-либо доступ к нему в рамках одной транзакции (например, выполнить с ним какую-либо операцию), но при этом обязательно вернуть его вам к концу этой транзакции. Это достигается с помощью так называемого шаблона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
Sui is a Layer 1 protocol blockchain designed as the first internet-scale programmable blockchain platform.
Заработай свою долю из 1000 Sui
Зарабатывай очки репутации и получай награды за помощь в развитии сообщества Sui.

- ... SUIBigSneh+1396
- ... SUISuiLover+1333
- ... SUI0xduckmove+1207
- ... SUIThorfin+1202
- ... SUIOwen+970
- ... SUIharry phan+847
- ... SUItheking+742
- Почему BCS требует точного порядка полей для десериализации, когда структуры Move содержат именованные поля?53
- «Ошибки проверки нескольких источников» в публикациях модуля Sui Move — автоматическое устранение ошибок43
- Сбой транзакции Sui: объекты, зарезервированные для другой транзакции25
- Как ограничения возможностей взаимодействуют с динамическими полями в гетерогенных коллекциях?05