この記事は筆者のソロ Advent Calendar 2022 23日目の記事です。
引き続きflowブロックチェーン上にスマートコントラクトを実装するためのCadenceという言語について公式ドキュメントのチュートリアルをやってみたのでその備忘録です。
今回は前回に引き続きNon-Fungible Token(NFT)のチュートリアルになります!
NFT特化flowブロックチェーンに入門するためにcadence言語を学ぶ[Hello World編]
NFT特化flowブロックチェーンに入門するためにcadence言語を学ぶ[リソース編]
NFT特化flowブロックチェーンに入門するためにcadence言語を学ぶ[capability リンクの参照とスクリプト]
NFT特化flowブロックチェーンに入門するためにcadence言語を学ぶ[Non Fungible Tokens(NFT)編1]
NFT特化flowブロックチェーンに入門するためにcadence言語を学ぶ[Non Fungible Tokens(NFT)編2] <- 今ここ
NFT特化flowブロックチェーンに入門するためにcadence言語を学ぶ[Fungible Tokens(FT)編]
チュートリアルplaygroundはこちら
前回のチュートリアルでNFTを自分のアカウントストレージに格納することを学びましたが、1つのリソースを取り扱うだけでスケーラビリティがあまりないことがわかります。ユーザーが1つの場所で全てのNFTを管理するためには以下のようにDictionariesを使用することで実現することができます。
// Define a dictionary to store the NFTs in
let myNFTs: @{Int: BasicNFT.NFT} = {}
// Create a new NFT
let newNFT <- BasicNFT.createNFT(id: 1)
// Save the new NFT to the dictionary
myNFTs[newNFT.id] <- newNFT
// Save the NFT to a new storage path
account.save(<-myNFTs, to: /storage/basicNFTDictionary)
DictionariesとCollection
辞書型は他の言語もあり、連想配列はmapと同じようにkeyとvalueでデータを表現するデータ型です。
pub let myNFTs: @{Int: NFT}
このような例の場合、keyhがIntでvalueにNFT型の辞書型を宣言しています。今回の場合はNFT型がリソースとなっているためリソースを表す記号である@
を先頭に付けることでフィールド全体をリソースとしています。
このようにNFTの保存に辞書型を使用すれば、NFTごとに異なるストレージパスを使用する必要はなく、同じストレージパス上で複数のNFTを管理することはできますが、これだけではまだ不十分です。
代わりに、Cadenceの強力な機能の一つであるCollection
を使用することができます。
チュートリアルplaygroundの0x01アカウントにExampleNFTが用意されているので中身を見てみると以下のようになっています。
pub contract ExampleNFT {
// Declare Path constants so paths do not have to be hardcoded
// in transactions and scripts
pub let CollectionStoragePath: StoragePath
pub let CollectionPublicPath: PublicPath
pub let MinterStoragePath: StoragePath
// Tracks the unique IDs of the NFT
pub var idCount: UInt64
// Declare the NFT resource type
pub resource NFT {
// The unique ID that differentiates each NFT
pub let id: UInt64
// Initialize both fields in the init function
init(initID: UInt64) {
self.id = initID
}
}
// We define this interface purely as a way to allow users
// to create public, restricted references to their NFT Collection.
// They would use this to publicly expose only the deposit, getIDs,
// and idExists fields in their Collection
pub resource interface NFTReceiver {
pub fun deposit(token: @NFT)
pub fun getIDs(): [UInt64]
pub fun idExists(id: UInt64): Bool
}
// The definition of the Collection resource that
// holds the NFTs that a user owns
pub resource Collection: NFTReceiver {
// dictionary of NFT conforming tokens
// NFT is a resource type with an `UInt64` ID field
pub var ownedNFTs: @{UInt64: NFT}
// Initialize the NFTs field to an empty collection
init () {
self.ownedNFTs <- {}
}
// withdraw
//
// Function that removes an NFT from the collection
// and moves it to the calling context
pub fun withdraw(withdrawID: UInt64): @NFT {
// If the NFT isn't found, the transaction panics and reverts
let token <- self.ownedNFTs.remove(key: withdrawID)
?? panic("Cannot withdraw the specified NFT ID")
return <-token
}
// deposit
//
// Function that takes a NFT as an argument and
// adds it to the collections dictionary
pub fun deposit(token: @NFT) {
// add the new token to the dictionary with a force assignment
// if there is already a value at that key, it will fail and revert
self.ownedNFTs[token.id] <-! token
}
// idExists checks to see if a NFT
// with the given ID exists in the collection
pub fun idExists(id: UInt64): Bool {
return self.ownedNFTs[id] != nil
}
// getIDs returns an array of the IDs that are in the collection
pub fun getIDs(): [UInt64] {
return self.ownedNFTs.keys
}
destroy() {
destroy self.ownedNFTs
}
}
// creates a new empty Collection resource and returns it
pub fun createEmptyCollection(): @Collection {
return <- create Collection()
}
// mintNFT
//
// Function that mints a new NFT with a new ID
// and returns it to the caller
pub fun mintNFT(): @NFT {
// create a new NFT
var newNFT <- create NFT(initID: self.idCount)
// change the id so that each ID is unique
self.idCount = self.idCount + 1
return <-newNFT
}
init() {
self.CollectionStoragePath = /storage/nftTutorialCollection
self.CollectionPublicPath = /public/nftTutorialCollection
self.MinterStoragePath = /storage/nftTutorialMinter
// initialize the ID count to one
self.idCount = 1
// store an empty NFT Collection in account storage
self.account.save(<-self.createEmptyCollection(), to: self.CollectionStoragePath)
// publish a reference to the Collection in storage
self.account.link<&{NFTReceiver}>(self.CollectionPublicPath, target: self.CollectionStoragePath)
}
}
前回のチュートリアルで使用された内容にいくつか新しい機能や概念が追加されており、だいぶボリュームが増してきたので上から順番に確認してみます。
ストレージパスとID
ExampleNFTという名前のコントラクトを宣言しており、まず3種類のストレージパスをフィールドに宣言しています。そして、複数のNFTを完全に識別する為にカウント用のIDを宣言しています。これらの初期化は後述のinit関数内で行う。
// Declare Path constants so paths do not have to be hardcoded
// in transactions and scripts
pub let CollectionStoragePath: StoragePath
pub let CollectionPublicPath: PublicPath
pub let MinterStoragePath: StoragePath
// Tracks the unique IDs of the NFT
pub var idCount: UInt64
NFTリソースの宣言
NFTのリソースを宣言します。宣言したリソースは他の NFTと識別するためのIDだけを持ちます。
// Declare the NFT resource type
pub resource NFT {
// The unique ID that differentiates each NFT
pub let id: UInt64
// Initialize both fields in the init function
init(initID: UInt64) {
self.id = initID
}
}
リソースインターフェースの宣言
アカウントが持つ NFTにアクセスできるのは通常オーナーのみですが、アカウントが持つリソースへの参照を交換する為にCadenceではCapabilityを作成してリソースへのリンクを公開することを前回までのチュートリアルで学びました。
後述のCollectionに対しての特定の操作を外部に公開するために以下のようなインターフェイスを宣言しています。これは後述のCollectionに実装し、リンクを公開することでリンクの使用者はこのインターフェイスで用意されている関数を使用することができることがわかります。
他のプログラミング言語と同様インターフェイスは関数宣言のみ行い、処理は実装しません。
// We define this interface purely as a way to allow users
// to create public, restricted references to their NFT Collection.
// They would use this to publicly expose only the deposit, getIDs,
// and idExists fields in their Collection
pub resource interface NFTReceiver {
pub fun deposit(token: @NFT)
pub fun getIDs(): [UInt64]
pub fun idExists(id: UInt64): Bool
}
Collectionの宣言
Collectionの宣言はpub resource Collection {}
で行います。ここでは、前述したNFTReceiverインターフェイスを実装させています。CollectionではmintしdepositされたNFTを格納するための辞書型で宣言された ownedNFTs
を宣言しており、init関数で空の辞書を使用し初期化しています。
// The definition of the Collection resource that
// holds the NFTs that a user owns
pub resource Collection: NFTReceiver {
// dictionary of NFT conforming tokens
// NFT is a resource type with an `UInt64` ID field
pub var ownedNFTs: @{UInt64: NFT}
// Initialize the NFTs field to an empty collection
init () {
self.ownedNFTs <- {}
}
}
各種関数を定義しています。
withdraw
// withdraw
//
// Function that removes an NFT from the collection
// and moves it to the calling context
pub fun withdraw(withdrawID: UInt64): @NFT {
// If the NFT isn't found, the transaction panics and reverts
let token <- self.ownedNFTs.remove(key: withdrawID)!
return <-token
}
指定されたトークンIDがキーの値を辞書型のownedNFTs
から取り除き、そのトークンを返却しています。トークンはリソースであるため移動演算子を使用する必要があります。
deposit
指定の NFTリソースを辞書型のownedNFTs
にNFTリソースのidをキーに格納します。
// deposit
//
// Function that takes a NFT as an argument and
// adds it to the collections dictionary
pub fun deposit(token: @NFT) {
// add the new token to the dictionary with a force assignment
// if there is already a value at that key, it will fail and revert
self.ownedNFTs[token.id] <-! token
}
idExists
指定のトークンIDのトークンが存在しているかどうかを、辞書型のownedNFTs
から値が取得できるかどうかで判断します。
// idExists checks to see if a NFT
// with the given ID exists in the collection
pub fun idExists(id: UInt64): Bool {
return self.ownedNFTs[id] != nil
}
getIds
辞書型は組み込みの関数でkeys
という関数を使用することでkeyのみの配列を取得することができます。これを使用してownedNFTs
のキーの一覧を返しています。Cadenceでは配列は[UInt64]
のように宣言することができます。
// getIDs returns an array of the IDs that are in the collection
pub fun getIDs(): [UInt64] {
return self.ownedNFTs.keys
}
destroy
destroy
キーワードを使用することでownedNFTs
を破棄します。辞書型の変数にリソースを格納している場合、格納している辞書自体もリソースとなっています。Collectionリソースがdestroyコマンドで破棄された場合、格納されているリソースをどう処理すべきか知っている必要があり、明示的に破棄するか移動させる必要があります。今回の例では破棄しています。
destroy() {
destroy self.ownedNFTs
}
これらの関数はインターフェイスとして宣言しているdeposit
idExists
getIDs
は外部から使用することが可能ですが、インターフェイスで宣言をしていないwithdraw
destroy
は外部から使用することはできません。
mintと初期化
残りの処理を見ていきます。以下の関数はCollection関数を使用し、空のCollectionリソースを作成し、返却しています。
// creates a new empty Collection resource and returns it
pub fun createEmptyCollection(): @Collection {
return <- create Collection()
}
以下の関数はNFTのmint関数になっています。コントラクトで記録しているトークンIDを使用して NFTリソースを作成し、返却しています。NFTを作成した後は、トークンIDをインクリメントしてコントラクトの値を更新しておきます。
// mintNFT
//
// Function that mints a new NFT with a new ID
// and returns it to the caller
pub fun mintNFT(): @NFT {
// create a new NFT
var newNFT <- create NFT(initID: self.idCount)
// change the id so that each ID is unique
self.idCount = self.idCount + 1
return <-newNFT
}
最後に初期化ブロックで各変数とCollectionの初期化およびCollectionへの参照を持つリンクを作成します。
init() {
self.CollectionStoragePath = /storage/nftTutorialCollection
self.CollectionPublicPath = /public/nftTutorialCollection
self.MinterStoragePath = /storage/nftTutorialMinter
// initialize the ID count to one
self.idCount = 1
// store an empty NFT Collection in account storage
self.account.save(<-self.createEmptyCollection(), to: self.CollectionStoragePath)
// publish a reference to the Collection in storage
self.account.link<&{NFTReceiver}>(self.CollectionPublicPath, target: self.CollectionStoragePath)
}
ユーザーがアカウントにこのNFTを保存したい場合は、createEmptyCollection
関数を呼び出して空のCollectionを作成し、ユーザーはアカウントストレージにこのCollectionを保存します。そして、mintしたNFTをこのコレクションに保存することができます。また、capabilityを利用することで他の人が自分のCollectionを確認することが可能となっています。
スクリプトでNFTReceiverの関数を実行してみる
チュートリアルplaygroundに以下のgetIds
関数を実行するスクリプトが用意されているので実行してみます。
import ExampleNFT from 0x01
// Print the NFTs owned by account 0x01.
pub fun main() {
// Get the public account object for account 0x01
let nftOwner = getAccount(0x01)
// Find the public Receiver capability for their Collection
let capability = nftOwner.getCapability<&{ExampleNFT.NFTReceiver}>(ExampleNFT.CollectionPublicPath)
// borrow a reference from the capability
let receiverRef = capability.borrow()
?? panic("Could not borrow receiver reference")
// Log the NFTs that they own as an array of IDs
log("Account 1 NFTs")
log(receiverRef.getIDs())
}
実行結果
Print 0x01 NFTs "Account 1 NFTs"
Print 0x01 NFTs []
Print 0x01 NFTs Result {"type":"Void"}
まだNFTを所持していないので空の配列が取得できました。
mintしてみる
チュートリアルplaygroundのtransactionにMint NFTという名前のtransactionが用意されているので実行してみます。
import ExampleNFT from 0x01
// This transaction allows the Minter account to mint an NFT
// and deposit it into its collection.
transaction {
// The reference to the collection that will be receiving the NFT
let receiverRef: &{ExampleNFT.NFTReceiver}
prepare(acct: AuthAccount) {
// Get the owner's collection capability and borrow a reference
self.receiverRef = acct.getCapability<&{ExampleNFT.NFTReceiver}>(ExampleNFT.CollectionPublicPath)
.borrow()
?? panic("Could not borrow receiver reference")
}
execute {
// Use the minter reference to mint an NFT, which deposits
// the NFT into the collection that is sent as a parameter.
let newNFT <- ExampleNFT.mintNFT()
self.receiverRef.deposit(token: <-newNFT)
log("NFT Minted and deposited to Account 1's Collection")
}
}
実行した後に、前述のスクリプトを実行してみるとmintしたトークンが格納されていることがわかります。
Print 0x01 NFTs "Account 1 NFTs"
Print 0x01 NFTs [1]
Print 0x01 NFTs Result {"type":"Void"}
NFTを他のアカウントにtransferする
0x01のアカウントから0x02のアカウントにNFTをtransferする。transferをする前に0x02のアカウントにCollectionをセットする必要があるためチュートリアルplaygroundのSetup Account
のトランザクションを実行します。
import ExampleNFT from 0x01
// This transaction configures a user's account
// to use the NFT contract by creating a new empty collection,
// storing it in their account storage, and publishing a capability
transaction {
prepare(acct: AuthAccount) {
// Create a new empty collection
let collection <- ExampleNFT.createEmptyCollection()
// store the empty NFT Collection in account storage
acct.save<@ExampleNFT.Collection>(<-collection, to: ExampleNFT.CollectionStoragePath)
log("Collection created for account 2")
// create a public capability for the Collection
acct.link<&{ExampleNFT.NFTReceiver}>(ExampleNFT.CollectionPublicPath, target: ExampleNFT.CollectionStoragePath)
log("Capability created")
}
}
セットアップが完了したらTransfer
のトランザクションを開き、Signerを0x01のアカウントにして実行します。
import ExampleNFT from 0x01
// This transaction transfers an NFT from one user's collection
// to another user's collection.
transaction {
// The field that will hold the NFT as it is being
// transferred to the other account
let transferToken: @ExampleNFT.NFT
prepare(acct: AuthAccount) {
// Borrow a reference from the stored collection
let collectionRef = acct.borrow<&ExampleNFT.Collection>(from: ExampleNFT.CollectionStoragePath)
?? panic("Could not borrow a reference to the owner's collection")
// Call the withdraw function on the sender's Collection
// to move the NFT out of the collection
self.transferToken <- collectionRef.withdraw(withdrawID: 1)
}
execute {
// Get the recipient's public account object
let recipient = getAccount(0x02)
// Get the Collection reference for the receiver
// getting the public capability and borrowing a reference from it
let receiverRef = recipient.getCapability<&{ExampleNFT.NFTReceiver}>(ExampleNFT.CollectionPublicPath)
.borrow()
?? panic("Could not borrow receiver reference")
// Deposit the NFT in the receivers collection
receiverRef.deposit(token: <-self.transferToken)
log("NFT ID 1 transferred from account 1 to account 2")
}
}
NFTが移動できたことを確認する為にPrint All NFTs
で準備されているスクリプトを実行します。
// Print All NFTs
import ExampleNFT from 0x01
// Print the NFTs owned by accounts 0x01 and 0x02.
pub fun main() {
// Get both public account objects
let account1 = getAccount(0x01)
let account2 = getAccount(0x02)
// Find the public Receiver capability for their Collections
let acct1Capability = account1.getCapability(ExampleNFT.CollectionPublicPath)
let acct2Capability = account2.getCapability(ExampleNFT.CollectionPublicPath)
// borrow references from the capabilities
let receiver1Ref = acct1Capability.borrow<&{ExampleNFT.NFTReceiver}>()
?? panic("Could not borrow account 1 receiver reference")
let receiver2Ref = acct2Capability.borrow<&{ExampleNFT.NFTReceiver}>()
?? panic("Could not borrow account 2 receiver reference")
// Print both collections as arrays of IDs
log("Account 1 NFTs")
log(receiver1Ref.getIDs())
log("Account 2 NFTs")
log(receiver2Ref.getIDs())
}
実行結果
Print All NFTs "Account 1 NFTs"
Print All NFTs []
Print All NFTs "Account 2 NFTs"
Print All NFTs [1]
NFTがちゃんと移動されていることが確認できました!
まとめ
今回は以下の内容について紹介いたしました。
- CadenceのDictionaies型とCollectionリソースの使い方
- Cadenceにおけるinterfaceの使い方について
- Collectionとinterfaceを使用した基本的なNFTコントラクトの作成について
- 作成したNFTコントラクトから NFTをmintしたら他のアカウントにtransferする方法について
だいぶCadenceにおけるコントラクトの実装イメージが湧いてきました!次回はFungible Token(FT)についてのチュートリアルをやっていきたいと思います。以上です!