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

この記事は筆者の[ソロ　Advent Calendar](https://qiita.com/advent-calendar/2022/panda) 2022 23日目の記事です。

引き続きflowブロックチェーン上にスマートコントラクトを実装するためのCadenceという言語について公式ドキュメントのチュートリアルをやってみたのでその備忘録です。

今回は前回に引き続きNon-Fungible Token(NFT)のチュートリアルになります！

[NFT特化flowブロックチェーンに入門するためにcadence言語を学ぶ[Hello World編]](https://qiita.com/JY8752/private/3f8c742409598d18630b)
[NFT特化flowブロックチェーンに入門するためにcadence言語を学ぶ[リソース編]](https://qiita.com/JY8752/items/e3027344e5607ee9e7f0)
[NFT特化flowブロックチェーンに入門するためにcadence言語を学ぶ[capability リンクの参照とスクリプト]](https://qiita.com/JY8752/items/3850784684da9b888c05)
[NFT特化flowブロックチェーンに入門するためにcadence言語を学ぶ[Non Fungible Tokens(NFT)編1]](https://qiita.com/JY8752/items/a53322bcbdeabe4cc820)
[NFT特化flowブロックチェーンに入門するためにcadence言語を学ぶ[Non Fungible Tokens(NFT)編2]](https://qiita.com/JY8752/items/0e2195c296c0f026bcf8) <- 今ここ
[NFT特化flowブロックチェーンに入門するためにcadence言語を学ぶ[Fungible Tokens(FT)編]](https://qiita.com/JY8752/items/9760c6238b21e19151a0)

チュートリアルplaygroundはこちら

https://play.onflow.org/f08e8e0d-d28e-4cbe-8d72-3afe2349c629

前回のチュートリアルでNFTを自分のアカウントストレージに格納することを学びましたが、1つのリソースを取り扱うだけでスケーラビリティがあまりないことがわかります。ユーザーが1つの場所で全てのNFTを管理するためには以下のように**Dictionaries**を使用することで実現することができます。

```typescript
// 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でデータを表現するデータ型です。

```typescript
pub let myNFTs: @{Int: NFT}
```

このような例の場合、keyhがIntでvalueにNFT型の辞書型を宣言しています。今回の場合はNFT型がリソースとなっているためリソースを表す記号である```＠```を先頭に付けることでフィールド全体をリソースとしています。

このようにNFTの保存に辞書型を使用すれば、NFTごとに異なるストレージパスを使用する必要はなく、同じストレージパス上で複数のNFTを管理することはできますが、これだけではまだ不十分です。

代わりに、Cadenceの強力な機能の一つである```Collection```を使用することができます。

チュートリアルplaygroundの0x01アカウントにExampleNFTが用意されているので中身を見てみると以下のようになっています。

```typescript: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という名前のコントラクトを宣言しており、まず３種類のストレージパスをフィールドに宣言しています。そして、複数のNFTを完全に識別する為にカウント用のIDを宣言しています。これらの初期化は後述のinit関数内で行う。

```typescript
    // 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だけを持ちます。

```typescript
    // 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に実装し、リンクを公開することでリンクの使用者はこのインターフェイスで用意されている関数を使用することができることがわかります。

他のプログラミング言語と同様インターフェイスは関数宣言のみ行い、処理は実装しません。
```typescript
   // 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関数で空の辞書を使用し初期化しています。
```typescript
   // 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
```typescript
        // 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をキーに格納します。
```typescript
        // 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```から値が取得できるかどうかで判断します。
```typescript
        // 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]```のように宣言することができます。
```typescript
        // 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コマンドで破棄された場合、格納されているリソースをどう処理すべきか知っている必要があり、明示的に破棄するか移動させる必要があります。今回の例では破棄しています。
```typescript
        destroy() {
            destroy self.ownedNFTs
        }
```

これらの関数はインターフェイスとして宣言している```deposit``` ```idExists``` ```getIDs```は外部から使用することが可能ですが、インターフェイスで宣言をしていない```withdraw``` ```destroy```は外部から使用することはできません。

## mintと初期化
残りの処理を見ていきます。以下の関数はCollection関数を使用し、空のCollectionリソースを作成し、返却しています。
```typescript
    // creates a new empty Collection resource and returns it
    pub fun createEmptyCollection(): @Collection {
        return <- create Collection()
    }
```

以下の関数はNFTのmint関数になっています。コントラクトで記録しているトークンIDを使用して　NFTリソースを作成し、返却しています。NFTを作成した後は、トークンIDをインクリメントしてコントラクトの値を更新しておきます。
```typescript
    // 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への参照を持つリンクを作成します。

```typescript
	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```関数を実行するスクリプトが用意されているので実行してみます。

```typescript
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が用意されているので実行してみます。

```typescript
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```のトランザクションを実行します。

```typescript
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のアカウントにして実行します。

```typescript
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```で準備されているスクリプトを実行します。

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