1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

panda(Kotlin, Test, BlockChain)Advent Calendar 2022

Day 23

NFT特化flowブロックチェーンに入門するためにcadence言語を学ぶ[Non Fungible Tokens(NFT)編2]

Last updated at Posted at 2022-12-22

この記事は筆者のソロ 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が用意されているので中身を見てみると以下のようになっています。

ExampleNFT.cdc
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)についてのチュートリアルをやっていきたいと思います。以上です!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?