Глава 4 День 3 - Создание контракта NFT: Коллекции (часть 1/3)
Вы уже многому научились. Давайте применим все, что вы узнали, чтобы создать свой собственный NFT контракт.
Video
В следующих нескольких главах мы будем делать то же самое, что я делаю в этом видео. Сегодня мы посмотрим только с 00:00 до 20:35: https://www.youtube.com/watch?v=bQVXSpg6GE8
Обзор
Пример NFT (NonFungibleToken)
Давайте проведем следующие несколько дней, работая над примером NonFungibleToken. Мы создадим наш собственный контракт NFT под названием CryptoPoops. Таким образом, вы рассмотрите все предыдущие концепции, которые вы изучили до сих пор, и реализуете свой собственный NFT!
Давайте начнем с создания контракта:
pub contract CryptoPoops {
pub var totalSupply: UInt64
pub resource NFT {
pub let id: UInt64
init() {
// NOTE: every resource on Flow has it's own unique `uuid`. There
// will never be resources with the same `uuid`.
self.id = self.uuid
}
}
pub fun createNFT(): @NFT {
return <- create NFT()
}
init() {
self.totalSupply = 0
}
}
Начнем с того, что:
- Определение
totalSupply
(первоначальное значение 0) - Создание типа
NFT
. Мы задаемNFT
одно поле:id
. В качествеid
используетсяself.uuid
, который является уникальным идентификатором, который есть у каждого ресурса на Flow. Никогда не будет двух ресурсов с одинаковымuuid
, поэтому он отлично подходит в качествеid
для NFT, так как NFT - это токен, который полностью уникален по сравнению с любым другим токеном. - Создание функции
createNFT
, которая возвращает ресурсNFT
, чтобы каждый мог создать свой собственный NFT.
Хорошо, это просто. Давайте сохраним NFT в нашем хранилище аккаунта и сделаем его общедоступным для чтения.
import CryptoPoops from 0x01
transaction() {
prepare(signer: AuthAccount) {
// store an NFT to the `/storage/MyNFT` storage path
signer.save(<- CryptoPoops.createNFT(), to: /storage/MyNFT)
// link it to the public so anyone can read my NFT's `id` field
signer.link<&CryptoPoops.NFT>(/public/MyNFT, target: /storage/MyNFT)
}
}
Отлично! Теперь вы должны понять это благодаря предыдущей главе. Сначала мы сохраним NFT в хранилище аккаунта, а затем свяжем ссылку на него с общим доступом, чтобы мы могли прочитать его поле id
с помощью скрипта. Что ж, давайте сделаем это!
import CryptoPoops from 0x01
pub fun main(address: Address): UInt64 {
let nft = getAccount(address).getCapability(/public/MyNFT)
.borrow<&CryptoPoops.NFT>()
?? panic("An NFT does not exist here.")
return nft.id // 3525 (some random number, because it's the `uuid` of
// the resource. This will probably be different for you.)
}
Потрясающе! Мы сделали несколько хороших вещей. Но давайте подумаем об этом на секунду. Что произойдет, если мы захотим хранить еще один NFT на нашем аккаунте?
import CryptoPoops from 0x01
transaction() {
prepare(signer: AuthAccount) {
// ERROR: "failed to save object: path /storage/MyNFT
// in account 0x1 already stores an object"
signer.save(<- CryptoPoops.createNFT(), to: /storage/MyNFT)
signer.link<&CryptoPoops.NFT>(/public/MyNFT, target: /storage/MyNFT)
}
}
Смотрите, что случилось. Мы получили ошибку! Почему? Потому что NFT уже существует на этом пути хранения. Как мы можем это исправить? Ну, мы можем просто указать другой путь хранения…
import CryptoPoops from 0x01
transaction() {
prepare(signer: AuthAccount) {
// Note we use `MyNFT02` as the path now
signer.save(<- CryptoPoops.createNFT(), to: /storage/MyNFT02)
signer.link<&CryptoPoops.NFT>(/public/MyNFT02, target: /storage/MyNFT02)
}
}
Это работает, но не очень хорошо. Если бы мы хотели иметь тонну NFT, нам пришлось бы запоминать ВСЕ пути хранения, которые мы ему задали, а это очень раздражает и неэффективно.
Вторая проблема заключается в том, что никто не может дать нам NFT. Поскольку только владелец аккаунта может хранить NFT непосредственно в хранилище своего аккаунта, никто не может передать нам NFT. Это тоже не очень хорошо.
Коллекции
Способ решить обе эти проблемы - создать “Коллекцию”, или контейнер, который объединяет все наши НФТ в один. Затем мы можем хранить коллекцию на одном пути хранения, а также разрешить другим “вносить депозиты” в эту коллекцию.
pub contract CryptoPoops {
pub var totalSupply: UInt64
pub resource NFT {
pub let id: UInt64
init() {
self.id = self.uuid
}
}
pub fun createNFT(): @NFT {
return <- create NFT()
}
pub resource Collection {
// Maps an `id` to the NFT with that `id`
//
// Example: 2353 => NFT with id 2353
pub var ownedNFTs: @{UInt64: NFT}
// Allows us to deposit an NFT
// to our Collection
pub fun deposit(token: @NFT) {
self.ownedNFTs[token.id] <-! token
}
// Allows us to withdraw an NFT
// from our Collection
//
// If the NFT does not exist, it panics
pub fun withdraw(withdrawID: UInt64): @NFT {
let nft <- self.ownedNFTs.remove(key: withdrawID)
?? panic("This NFT does not exist in this Collection.")
return <- nft
}
// Returns an array of all the NFT ids in our Collection
pub fun getIDs(): [UInt64] {
return self.ownedNFTs.keys
}
init() {
self.ownedNFTs <- {}
}
destroy() {
destroy self.ownedNFTs
}
}
pub fun createEmptyCollection(): @Collection {
return <- create Collection()
}
init() {
self.totalSupply = 0
}
}
Замечательно. Мы определили ресурс Collection
, который делает несколько вещей:
- Хранит словарь
ownedNFTs
, который сопоставляетid
сNFT
с этимid
. - Определяет функцию
deposit
для возможности пополнения счетаNFT
. - Определяет функцию
withdraw
для возможности выводаNFT
. - Определяет функцию
getIDs
, чтобы мы могли получить список всех идентификаторов NFT в нашей Коллекции. - Определяет функцию
destroy
. В Cadence, когда у вас есть ресурсы внутри ресурсов, вы ДОЛЖНЫ объявить функциюdestroy
, которая вручную уничтожает эти “вложенные” ресурсы с помощью ключевого словаdestroy
..
Мы также определили функцию createEmptyCollection
, чтобы мы могли сохранить коллекцию
в хранилище нашего аккаунта, чтобы мы могли лучше управлять нашими NFT. Давайте сделаем это сейчас:
import CryptoPoops from 0x01
transaction() {
prepare(signer: AuthAccount) {
// Store a `CryptoPoops.Collection` in our account storage.
signer.save(<- CryptoPoops.createEmptyCollection(), to: /storage/MyCollection)
// Link it to the public.
signer.link<&CryptoPoops.Collection>(/public/MyCollection, target: /storage/MyCollection)
}
}
Потратьте несколько минут, чтобы действительно прочитать этот код. Что в нем не так? Подумайте о некоторых проблемах безопасности, связанных с ним. Почему плохо, что мы выкладываем &CryptoPoops.Collection
в открытый доступ?
…
…
Вы уже подумали об этом? Причина в том, что теперь любой может вывести NFT из нашей Коллекции! Это очень плохо.
Проблема, однако, заключается в том, что мы хотим, чтобы общественность могла вносить
NFT в нашу Коллекцию, и мы хотим, чтобы они также могли читать идентификаторы NFT, которыми мы владеем. Как мы можем решить эту проблему?
Интерфейсы ресурсов, вуп-вуп! Давайте определим интерфейс ресурса, чтобы ограничить то, что мы выставляем в открытый доступ:
pub contract CryptoPoops {
pub var totalSupply: UInt64
pub resource NFT {
pub let id: UInt64
init() {
self.id = self.uuid
}
}
pub fun createNFT(): @NFT {
return <- create NFT()
}
// Only exposes `deposit` and `getIDs`
pub resource interface CollectionPublic {
pub fun deposit(token: @NFT)
pub fun getIDs(): [UInt64]
}
// `Collection` implements `CollectionPublic` now
pub resource Collection: CollectionPublic {
pub var ownedNFTs: @{UInt64: NFT}
pub fun deposit(token: @NFT) {
self.ownedNFTs[token.id] <-! token
}
pub fun withdraw(withdrawID: UInt64): @NFT {
let nft <- self.ownedNFTs.remove(key: withdrawID)
?? panic("This NFT does not exist in this Collection.")
return <- nft
}
pub fun getIDs(): [UInt64] {
return self.ownedNFTs.keys
}
init() {
self.ownedNFTs <- {}
}
destroy() {
destroy self.ownedNFTs
}
}
pub fun createEmptyCollection(): @Collection {
return <- create Collection()
}
init() {
self.totalSupply = 0
}
}
Теперь мы можем ограничить то, что могут видеть все, когда мы сохраняем нашу Коллекцию в хранилище аккаунта:
import CryptoPoops from 0x01
transaction() {
prepare(signer: AuthAccount) {
// Store a `CryptoPoops.Collection` in our account storage.
signer.save(<- CryptoPoops.createEmptyCollection(), to: /storage/MyCollection)
// NOTE: We expose `&CryptoPoops.Collection{CryptoPoops.CollectionPublic}`, which
// only contains `deposit` and `getIDs`.
signer.link<&CryptoPoops.Collection{CryptoPoops.CollectionPublic}>(/public/MyCollection, target: /storage/MyCollection)
}
}

import CryptoPoops from 0x01
transaction() {
prepare(signer: AuthAccount) {
// Get a reference to our `CryptoPoops.Collection`
let collection = signer.borrow<&CryptoPoops.Collection>(from: /storage/MyCollection)
?? panic("The recipient does not have a Collection.")
// deposits an `NFT` to our Collection
collection.deposit(token: <- CryptoPoops.createNFT())
log(collection.getIDs()) // [2353]
// withdraw the `NFT` from our Collection
let nft <- collection.withdraw(withdrawID: 2353) // We get this number from the ids array above
log(collection.getIDs()) // []
destroy nft
}
}
Потрясающе! Итак, все работает хорошо. Теперь давайте посмотрим, сможет ли кто-то другой пополнить НАШУ коллекцию, вместо того чтобы делать это самому:
import CryptoPoops from 0x01
transaction(recipient: Address) {
prepare(otherPerson: AuthAccount) {
// Get a reference to the `recipient`s public Collection
let recipientsCollection = getAccount(recipient).getCapability(/public/MyCollection)
.borrow<&CryptoPoops.Collection{CryptoPoops.CollectionPublic}>()
?? panic("The recipient does not have a Collection.")
// deposits an `NFT` to our Collection
recipientsCollection.deposit(token: <- CryptoPoops.createNFT())
}
}
Отлично. Мы пополнили чужой аккаунт, что вполне возможно, потому что они связаны &CryptoPoops.Collection{CryptoPoops.CollectionPublic}
с общим доступом. И это нормально. Кого волнует, если мы дадим кому-то бесплатный NFT? Это же круто!
Что произойдет, если мы попытаемся вывести NFT из чьей-то Коллекции?
import CryptoPoops from 0x01
transaction(recipient: Address, withdrawID: UInt64) {
prepare(otherPerson: AuthAccount) {
// Get a reference to the `recipient`s public Collection
let recipientsCollection = getAccount(recipient).getCapability(/public/MyCollection)
.borrow<&CryptoPoops.Collection{CryptoPoops.CollectionPublic}>()
?? panic("The recipient does not have a Collection.")
// ERROR: "Member of restricted type is not accessible: withdraw"
recipientsCollection.withdraw(withdrawID: withdrawID)
}
}
Мы получаем ошибку! Отлично, хакер не может украсть наши NFT :)
Наконец, давайте попробуем считать NFT на нашем аккаунте с помощью скрипта:
import CryptoPoops from 0x01
pub fun main(address: Address): [UInt64] {
let publicCollection = getAccount(address).getCapability(/public/MyCollection)
.borrow<&CryptoPoops.Collection{CryptoPoops.CollectionPublic}>()
?? panic("The address does not have a Collection.")
return publicCollection.getIDs() // [2353]
}
Бум. Готово.
Заключение
Коллекции предназначены не только для NFT. В экосистеме Flow концепция коллекции используется повсеместно. Если вы хотите, чтобы пользователи хранили ресурс, но у них может быть несколько таких ресурсов, вы почти всегда будете использовать Коллекцию, чтобы обернуть их, чтобы хранить их все в одном месте. Это очень важная концепция, которую необходимо понять.
И поаплодируйте себе. Вы внедрили действующий контракт NFT! Ты становишься лучше, мой друг! Черт возьми, скоро ты сможешь догнать меня. Шучу, это невозможно. Я намного лучше тебя.
Квесты
Почему мы добавили Коллекцию в этот контракт? Перечислите две основные причины.
Что нужно делать, если у вас есть ресурсы, “вложенные” внутрь другого ресурса? (“Вложенные ресурсы”)
Проведите мозговой штурм дополнительных пунктов, которые мы могли бы добавить в этот контракт. Подумайте, что может быть проблематичным в этом контракте и как мы могли бы это исправить.
Идея №1: Действительно ли мы хотим, чтобы каждый мог чеканить NFT? 🤔.
Идея №2: Если мы хотим прочитать информацию о наших NFT внутри нашей Коллекции, сейчас мы должны вынуть ее из Коллекции, чтобы сделать это. Хорошо ли это?