Глава 4 День 1 - Хранилище аккаунта
Мы прошли уже 3 главы. Но впереди еще много всего ;) Поехали.
Видео
Посмотрите видео до 14:45: https://www.youtube.com/watch?v=01zvWVoDKmU
Остальное мы рассмотрим завтра.
Аккаунты на Flow
Если вы помните первый день второй главы, когда мы изучали транзакции, я также говорил об аккаунтах на flow и о том, как они могут хранить данные. Я скопирую и вставлю это ниже, потому что это полезно для обзора:
На Flow аккаунты могут хранить свои собственные данные. Что это значит? Если я владею NFT на Flow, то этот NFT хранится на моем аккаунте. Это очень отличается от других блокчейнов, таких как Ethereum. В Ethereum ваш NFT хранится в смарт-контракте. В Flow мы фактически позволяем аккаунтам самим хранить свои данные, что очень здорово. Но как нам получить доступ к данным на своем аккаунте? Мы можем сделать это с помощью типа AuthAccount
. Каждый раз, когда пользователь (вроде нас с вами) отправляет транзакцию, вы должны оплатить ее, а затем “подписать” ее. Это означает, что вы нажали на кнопку, говорящую: “Эй, я хочу одобрить эту транзакцию”. Когда вы подписываете ее, транзакция принимает ваш AuthAccount
и может получить доступ к данным вашего аккаунта.
Вы можете видеть, как это делается в части транзакции prepare
, и в этом весь смысл фазы prepare
: получить доступ к информации/данным вашего счета. С другой стороны, фаза execute
не может этого сделать. Но она может вызывать функции и делать то, что нужно для изменения данных в блокчейне. ПРИМЕЧАНИЕ: В действительности вам никогда не понадобится фаза execute
. Технически вы можете сделать все в фазе prepare
, но так код будет менее понятным. Лучше разделить логику.
Что находится в аккаунте?
Как вы читали выше, на Flow аккаунты фактически хранят свои собственные данные. Это означает, что если у меня есть ресурс NFT
, я могу хранить его в своем собственном аккаунте. Но где?
Используя приведенный выше рисунок (я так горжусь им), давайте поговорим о том, что живет в аккаунте:
- Код контракта - контракт развертывается на аккаунте и живет внутри аккаунта. Несколько контрактов могут жить в одном аккаунте.
- Хранилище аккаунта - все ваши данные хранятся в хранилище аккаунта
Хранилище аккаунта
Что же тогда представляет собой хранилище аккаунта? Хранилище аккаунта можно представить как “контейнер” данных, который находится по определенному пути: /storage/
. В аккаунте Flow есть 3 пути для доступа к определенным данным:
/storage/
- доступ сюда может получить только владелец аккаунта (слава богу, иначе кто-то мог бы украсть все ваши данные). Здесь хранятся ВСЕ ваши данные./public/
- доступен всем/private/
- доступно только владельцу аккаунта и людям, которым владелец аккаунта предоставляет доступ
Главное помнить, что только владелец аккаунта может получить доступ к своему /storage/
, но у него есть возможность помещать вещи в пути /public/
и /private/
, если он захочет. Например, если я хочу просто показать вам свой NFT, я могу поместить демонстративную версию своего NFT в путь /public/
, чтобы вы могли его увидеть, но ограничить его настолько, чтобы вы не могли забрать его с моего аккаунта.
Подсказка: Вы видите, как интерфейсы ресурсов могут быть полезны здесь? ;).
Возможно, вы задаетесь вопросом: “а как мне получить доступ к моему /storage/
?“. Ответ заключается в вашем типе AuthAccount
. Если вы помните, когда вы подписываете транзакцию, AuthAccount
подписанта помещается в качестве параметра на этапе prepare
, как показано ниже:
transaction() {
prepare(signer: AuthAccount) {
// We can access the signer's /storage/ path here!
}
execute {
}
}
Как вы можете видеть выше, мы можем получить доступ к /storage/
подписанта на этапе prepare
. Это означает, что мы можем делать с его учетной записью все, что захотим. Вот почему так страшно подумать о случайном подписании транзакции! Будьте осторожны, друзья.
Функции сохранения и загрузки
Давайте попрактикуемся в хранении чего-либо на аккаунте. Сначала определим контракт:
pub contract Stuff {
pub resource Test {
pub var name: String
init() {
self.name = "Jacob"
}
}
pub fun createTest(): @Test {
return <- create Test()
}
}
Мы определили простой контракт, позволяющий создавать и возвращать тип ресурса @Test
. Давайте получим это в транзакции:
import Stuff from 0x01
transaction() {
prepare(signer: AuthAccount) {
let testResource <- Stuff.createTest()
destroy testResource
}
execute {
}
}
Все, что мы делаем, это создаем и уничтожаем @Test
. Но что если мы захотим сохранить его в нашем аккаунте? Давайте посмотрим, как это делается, а затем пройдемся по этому вопросу:
import Stuff from 0x01
transaction() {
prepare(signer: AuthAccount) {
let testResource <- Stuff.createTest()
signer.save(<- testResource, to: /storage/MyTestResource)
// saves `testResource` to my account storage at this path:
// /storage/MyTestResource
}
execute {
}
}
Посмотрите, как мы сохранили его в нашем аккаунте. Во-первых, мы должны иметь AuthAccount
для сохранения. В данном случае у нас есть переменная signer
. Затем мы можем выполнить signer.save(...)
, что означает, что мы сохраняем что-то по пути /storage/
.
.save()
принимает два параметра:
- Фактические данные, которые нужно сохранить
- параметр
to
, который является путем, куда мы должны сохранить его (это должен быть путь/storage/
)
В примере выше я сохранил testResource
(обратите внимание на синтаксис <-
, поскольку это ресурс) по пути /storage/MyTestResource
. Теперь, когда мы захотим получить его, мы можем обратиться к этому пути. Давайте сделаем это ниже.
import Stuff from 0x01
transaction() {
prepare(signer: AuthAccount) {
let testResource <- signer.load<@Stuff.Test>(from: /storage/MyTestResource)
// takes `testResource` out of my account storage
destroy testResource
}
execute {
}
}
В приведенном выше примере мы используем функцию .load()
для извлечения данных ИЗ хранилища нашего аккаунта.
Вы заметите, что мы должны сделать эту странную вещь: <@Stuff.Test>
. Что это такое? Ну, когда вы взаимодействуете с хранилищем учетных записей, вы должны указать тип, который вы рассматриваете. Cadence понятия не имеет, что @Stuff.Test
хранится на этом пути хранения. Но как программист, мы знаем, что это то, что там хранится, поэтому мы должны поставить <@Stuff.Test>
, чтобы сказать “мы ожидаем, что @Stuff.Test
выйдет из этого пути хранения”.
.load()
принимает один параметр:
- параметр
from
- путь, из которого мы должны его взять (это должен быть путь/storage/
)
Еще один важный момент: когда вы загружаете
данные из хранилища, оно возвращает необязательный тип. testResource
на самом деле имеет тип @Stuff.Test?
. Причина этого в том, что Cadence понятия не имеет, что вы говорите правду и что там действительно что-то есть, или что это вообще правильный тип. Поэтому, если вы ошиблись, он вернет nil
. Давайте рассмотрим пример:
import Stuff from 0x01
transaction() {
prepare(signer: AuthAccount) {
let testResource <- signer.load<@Stuff.Test>(from: /storage/MyTestResource)
log(testResource.name) // ERROR: "value of type `Stuff.Test?` has no member `name`."
destroy testResource
}
execute {
}
}
Видите? Это необязательный параметр. Чтобы исправить это, мы можем использовать либо panic
, либо оператор !
. Мне нравится использовать panic
, потому что вы можете указать сообщение об ошибке.
import Stuff from 0x01
transaction() {
prepare(signer: AuthAccount) {
let testResource <- signer.load<@Stuff.Test>(from: /storage/MyTestResource)
?? panic("A `@Stuff.Test` resource does not live here.")
log(testResource.name) // "Jacob"
destroy testResource
}
execute {
}
}
Функция заимствования
Раньше мы сохраняли и загружали из аккаунта. Но что, если мы просто хотим посмотреть что-то в аккаунте? Вот тут-то и приходят на помощь ссылки и функция .borrow()
.
import Stuff from 0x01
transaction() {
prepare(signer: AuthAccount) {
// NOTICE: This gets a `&Stuff.Test`, not a `@Stuff.Test`
let testResource = signer.borrow<&Stuff.Test>(from: /storage/MyTestResource)
?? panic("A `@Stuff.Test` resource does not live here.")
log(testResource.name) // "Jacob"
}
execute {
}
}
Видно, что мы использовали функцию .borrow()
для получения ссылки на ресурс в нашем хранилище, а не самого ресурса. Поэтому мы используем тип <&Stuff.Test>
, а не <@Stuff.Test>
.
.borrow()
принимает один параметр (такой же, как .load()
):
- параметр
from
, который является путем, из которого мы должны взять его
Также обратите внимание, что поскольку мы не используем .load()
, ресурс все время остается в хранилище нашего аккаунта. Ух ты, ссылки - это круто!
Заключение
Давайте снова посмотрим на этот рисунок:
Теперь вы должны понимать, что такое /storage/
. В завтрашней главе мы поговорим о путях /public/
и /private/
.
Квесты
Объясните, что находится внутри аккаунта.
В чем разница между путями
/storage/
,/public/
и/private/
?Что делает
.save()
? Что делает.load()
? Что делает.borrow()
?Объясните, почему мы не можем сохранить что-то в хранилище аккаунта внутри скрипта.
Объясните, почему я не смог сохранить что-то на ваш аккаунт.
Определите контракт, возвращающий ресурс, в котором есть хотя бы одно поле. Затем напишите две транзакции:
Транзакция, которая сначала сохраняет ресурс в хранилище аккаунта, затем загружает его из хранилища аккаунта, регистрирует поле внутри ресурса и уничтожает его.
Транзакция, которая сначала сохраняет ресурс в хранилище аккаунта, затем заимствует ссылку на него и регистрирует поле внутри ресурса.