Cadence チュートリアル(4)Non-Fungible Tokens

Ara
Flow Japan

--

この記事は、Flow 公式の Cadence チュートリアル “4. Non-Fungible Tokens” の日本語 翻訳版です。
前回の記事:「
Cadence チュートリアル(3)Fungible-Tokens

目次

  • Flow エミュレータ上での Non-Fungible Tokens
  • アカウントに NFT を追加する
  • 複数の NFT をコレクションに保存する
  • ディクショナリ
  • リソースを所有するリソース
  • NFT コレクションへのアクセスを制限する
  • スクリプトを実行する
  • 管理者としてトークンを発行、配布する
  • NFT を送る
  • 全コードのまとめ
  • Flow のマーケットプレイスの作成

このチュートリアルでは、Non-Fungible Tokens(NFT)のデプロイ、保存、送付を行ないます。

Flow Playground でこのチュートリアルのスターターコードを開きます: https://play.onflow.org/b9e7b3f7-41dc-49ae-8435-d8a82f7fb3aa

チュートリアルでは、このコードと対話するために様々なアクションを取るように求めています。

アクションを要求する指示は、このような装飾で書かれています。これらのアクションの箇所だけ読んでもコードを実行できますが、説明をいっしょに読むことで、より言語のデザインを理解できます。

NFT はブロックチェーン技術に欠かせないものです。私たちは、ユニークで分割できない資産を表現するために NFT を必要としています(CryptoKittiesのように!)

多くのスマートコントラクト言語は各 NFT を中央台帳で表現しますが、Cadence は、各 NFT をユーザーが自分のアカウントに保存するリソースとして表現します。

今回のチュートリアルでは、NFT に慣れてもらうために、以下の手順で進めていただきます。

  1. NFT のコントラクトと型定義をデプロイする
  2. NFT オブジェクトを作り、アカウントストレージに保存する
  3. NFT コレクションのオブジェクトを作り、複数の NFT をアカウントに保存する
  4. NFTMinter を作り、それを使って NFT を発行する
  5. 他の人がトークンを送るための、コレクションの参照を作る
  6. 同様に別のアカウントをセットアップする
  7. あるアカウントから別のアカウントに NFT を送付する
  8. スクリプトを使って、各アカウントのコレクションに保存されている NFT を確認する

このチュートリアルに進む前に、Playground ツールの使い方や Cadence の基礎を学ぶために、最初のステップ」「Hello World!を読むことを強くお勧めします。ここでもいくつかのコンセプトを再度取り上げますが、すべてではありません。

Flow エミュレータ上での Non-Fungible Tokens

Cadence では、各 NFT は整数の ID を持つリソースで表現されます。リソースには重要な所有権のルールがあり、それが型システムによって強制されます。そのため、リソースは、NFT を表現するのに最適な型です。リソースは一人の所有者しか持つことができず、コピーできず、誤ってあるいは悪意を持って紛失・複製することはできません。これらの保護により、所有者は自分の NFT が安全であり、真の価値を持つ資産であることを知ることができます。

また、NFT は通常、名前や画像のようなメタデータによって表現されます。歴史的には、このメタデータのほとんどはオフチェーンに保存されており、オンチェーンのトークンには、オフチェーンのメタデータを指す URL などが含まれているだけです。Flow でもこれは可能ですが、Flow の目標は、トークンに関連するすべてのメタデータをオンチェーンで保存できるようにすることです。しかし、これはこのチュートリアルの範囲外です。この枠組みはまだ設計中で、Flow の NFT リポジトリの関連する Issue を確認・参画できます。

Flow 上のユーザーがお互いに取引したい場合、各ユーザーのアカウントで、リソースに定義されたメソッドを呼び出すことで、中央の NFT コントラクトと対話することなく、ピアツーピアで取引できます。

アカウントに NFT を追加する

まず、このリンクを辿って、Non-Fungible Token のコントラクト、トランザクション、スクリプトが事前にロードされたプレイグラウンドのセッションを開く必要があります: https://play.onflow.org/b9e7b3f7-41dc-49ae-8435-d8a82f7fb3aa

NFTv1.cdc をみるためにアカウント 0x01 を開いてください。NFTv1.cdc には以下のコードが含まれているはずです。

このコントラクトでは、NFT は整数の ID とメタデータ用のフィールドを持つリソースです。

コントラクトの init 関数では、新しい NFT オブジェクトを作成して、アカウントストレージに移動します。

// ストレージに保存
self.account.save<@NFT>(<-create NFT(initID: 1), to: /storage/NFT1)

ここでは、コントラクトがデプロイされたアカウントの AuthAccount オブジェクトにアクセスして、その save メソッドを呼び出し、保存する型として @NFT を指定します。また、同じ行で NFT を作成し、それを save の第一引数に渡して、オブジェクトが保存される場所である /storage ドメインに保存します。

エディタの右下にある [Deploy] ボタンをクリックして、NFTv1 をデプロイします。

これで、アカウントに NFT が入っているはずです。トランザクションを実行して確認してみましょう。

Transaction1.cdc を開き、アカウント 0x01 を唯一の署名者として選択し、トランザクションを送信します。
Transaction1.cdc は以下のようになっているはずです。

ここでは、ストレージ内の NFT から直接参照を借りようとしています。オブジェクトが存在する場合、借りることに成功して参照の Optional は nil にはなりません。しかし、失敗すると Optional は nil になります。

"The token exists!" と表示されるはずです。

お疲れ様でした。これであなたのアカウントに最初の NFT ができました。

複数の NFT をコレクションに保存する

NFT はストレージのトップレベルにも格納できますが、多くの NFT を持っている場合、NFT の整理が紛らわしくなってきます。このアプローチはスケーラブルではありません。しかし、必要なだけたくさんの NFT を保持できるデータ構造を使うことで、この問題を克服できます。配列やディクショナリを使ってこれを実現することもできますが、これらの型はあまり明瞭ではありません。代わりに、NFT コレクションのリソースを使うことで、より洗練された方法で NFT と対話できます。

アカウント 0x02 を開いて NFTv2.cdc を確認します。

エディタの右下にある [Deploy] ボタンをクリックして、コントラクトをデプロイします。

NFTv2.cdc には以下のコードが含まれているはずです。NFTv1.cdcの内容に加えて、コントラクト本体に追加のリソースの宣言が含まれています。

NFT を所有しているユーザーは、アカウント内に NFTの Collection リソースのインスタンスが保存されています。このコレクションは、Vault リソースが balance フィールドにすべてのトークンを格納するのと同じように、整数の ID に NFT をマップするディクショナリに、すべての NFT を格納します。もう一つ似ている点は、各コレクションが depositwithdraw の関数を持っている点です。これらの関数により、ユーザーは最初に自分のコレクションからトークンを引き出し、次に別のコレクションに預けるというパターンに従うことができます。

ユーザーが NFT をアカウントに保存したい場合は、NonFungibleToken スマートコントラクトの createEmptyCollection 関数を呼び出して、空のコレクションをインスタンス化します。これは、アカウントのストレージに格納できる空の Collection オブジェクトを返します。

この例で使う新機能がいくつかあります。

ディクショナリ

このリソースは、ディクショナリを使用しています。ディクショナリは、key-value で関連付けられた、順序なしのコレクションです。

pub var ownedNFTs: @{Int: NFT}

ディクショナリでは、すべてのキーは同じ型でなければならず、また、すべての値は同じ型でなければなりません。この場合、整数型の ID に NFT リソースオブジェクトをマッピングしています。ディクショナリの定義では、通常、型の指定に @ 記号を使いません。しかし、 OwnedNFTs はリソースを格納するので、フィールド全体もリソース型になっていなければなりません。そのため、フィールドには、リソース型であることを示す @ 記号が付いています。つまり、リソースに適用されるすべてのルールがこの型に適用されます。

NFT コレクションリソースが destroy コマンドで破棄された場合、そのディクショナリが格納しているリソースをどうするかがわかるようにする必要があります。このため、他のリソースを格納するリソースには、そのリソースの destroy が呼び出されたときに実行される destroy 関数が含まれていなければなりません。この destroy 関数では、含まれているリソースを明示的に破棄するか、別の場所に移動しなければなりません。この例では、それらを破棄します。

destroy() {
destroy self.ownedNFTs
}

コレクションが作成されたとき init 関数が実行されますが、ここで、すべてのメンバ変数を明示的に初期化しなければなりません。これは、初期化されていないフィールドがバグの原因となるスマートコントラクトの問題を防ぐのに役立ちます。この後、init 関数は二度と実行できません。ここでは、リソース型として空のディクショナリで初期化しています。

init () {
self.ownedNFTs <- {}.
}

ディクショナリのもう一つの機能として、組み込みの keys 関数を使って、ディクショナリのキーの配列を取得する機能があります。

// getIDs は、コレクション内にある ID の配列を返す
pub fun getIDs(): [UInt64] {
return self.ownedNFTs.keys
}

これは、ディクショナリを反復処理したり、保存されているもののリストを見るために使用できます。ご覧のように、可変長の配列型は、メンバの型を角括弧 [] で囲むことで宣言されています。

リソースを所有するリソース

NFTv2.cdc の NFT コレクションの例は、重要な機能を説明しています:
リソースは他のリソースを所有することができます。

この例では、あるユーザは 1 つの NFT を別のユーザに送付できます。さらに、Collection はその中の NFT を明示的に所有しているので、所有者は単一のコレクションを送付するだけで、一度にすべての NFT を送付できます。

これは、多くの追加のユースケースを可能にする重要な機能です。簡単なバッチ送付が可能になるだけでなく、これは、ユニークな NFT が別のユニークな NFT を所有したい場合、例えば CryptoKitty が帽子のアクセサリーを所有する場合、キティは文字通り帽子を自身のストレージに保存し、事実上 所有していることを意味します。帽子は、それが格納されている CryptoKitty に属しており、帽子はそれぞれ別々に、または、それを所有している CryptoKitty と一緒に、送付することができます。

他のリソースに格納されているリソースに対して Capability を作成することはできませんが、参照は作成できます。リソースを所有しているリソースは、それを制御しており、保存されたリソースに対する外部呼び出しのアクセスの種類を制御します。

NFT コレクションへのアクセスを制限する

NFT コレクションでは、すべての関数とフィールドは公開されていますが、ネットワーク上のすべての人が私たちの withdraw 関数を呼び出せるようにはしたくありません。そこで、Cadence のアクセス制御のセカンドレイヤーの出番です。Cadence は Capability Security を利用しています。これは、あるオブジェクトに対して、以下のいずれかに該当するユーザが、そのオブジェクトのフィールドやメソッドへのアクセスを許可していることを意味します。

  • オブジェクトの所有者
  • そのフィールドまたはメソッドへの有効な参照を持っている(参照はCapability からのみ作成でき、Capability はオブジェクトの所有者によってのみ作成できることに注意)

ユーザーが NFT コレクションをアカウントストレージに保存すると、デフォルトでは他のユーザーがアクセスできなくなります。ユーザーのアカウントストレージオブジェクトは、その所有者以外の誰からもアクセスできません。外部アカウントが deposit 関数、getIDs 関数、 idExists関数にアクセスできるようにするには、所有者はこれらのフィールドのみを含むインターフェースを使った参照を作ります。

pub resource interface NFTReceiver {
pub fun deposit(token: @NFT)
pub fun getIDs(): [UInt64]
pub fun idExists(id: UInt64): Bool
}

そして、そのインターフェースを使って、ストレージ内のオブジェクトへのリンクを作成し、そのリンクには NFTReceiver インターフェースの関数のみを含むことを指定します。このリンクは capability と呼ばれます。ここから、所有者はその capability を好きなように使えます。一度だけ使うために関数のパラメータとして渡したり、アカウントの /public/ ドメインに入れて、誰でもアクセスできるようにすることもできます。もしユーザーがこの capability を使って withdraw 関数を呼ぼうとした場合、その関数はインターフェースおよび参照に存在しないため、機能しません。

リンクおよび capability を作成するコードは、init 関数の中、NFTv2.cdc の 132 行目にあります。

// ストレージ内のコレクションへの参照を公開する
self.account.link<&{NFTReceiver}>(/public/NFTReceiver, target: /storage/NFTCollection)

link 関数は、capability が &AnyResource{NFTReceiver} として型付けされ、それらのフィールドと関数のみを公開することを指定します。そして、リンクは誰でもアクセスできる /public/ に格納されます。このリンクは、先ほど作成した /storage/NFTCollection をターゲットにしています。

これにより、このユーザーは /storage に NFT コレクションを持つと共に、そのコレクションを対象とする capability を持ち、それを他のユーザーが使うことで、このユーザーがどの NFT を所有しているか確認したり、このユーザーに NFT を送ったりすることができるようになりました。

スクリプトを実行して、これが正しいことを確認してみましょう。

スクリプトを実行する

Cadence のスクリプトは、アカウントの権限なしで実行される、ブロックチェーンから情報を読み取るだけのシンプルなトランザクションです。

Script1.cdc という名前のスクリプトファイルを開きます。Script1.cdcには、以下のコードが含まれているはずです。

エディタボックスの右下にある [Execute] ボタンをクリックして、Script1.cdc を実行してください。
このスクリプトは、アカウント
0x02 が所有している NFT のリストを表示します。

アカウント 0x02 は現在何も所有していないので、空の配列が表示されます。

"Account 2 NFTs"
[]
Result > "void"

スクリプトが実行できない場合、おそらく NFT コレクションがアカウント 0x02 に正しく保存されていないことを意味します。問題が発生した場合は、署名者としてアカウント 0x02 を選択し、前の手順に正しく従っていることを確認してください。

管理者としてトークンを発行、配布する

NFT を作成する一つの方法は、管理者が新しいトークンを発行してユーザーに送ることです。ほとんどの場合、NFT Minterリソースを持つことでこれを実現します。このリソースの所有者はトークンを発行でき、他のユーザやコントラクトにトークンを発行する能力を与えたい場合は、所有者は mintNFT 関数への参照を与えることができます(Capability Security モデルの利用)。台帳ベースのモデルのように、トランザクションの送信者を明示的にチェックする必要はありません。

NFT Minter を使って、トークンを発行してみましょう。

NFTv2.cdcNonFungibleToken コントラクトを参照すると、 NFTMinter という別のリソースを定義していることがわかります。これは、発行の権限を持つ管理者が、新しい NFT を発行するために所有するものです。これはシンプルな例であり、単純に、NFT を発行するためのひとつの関数と、NFT に一意の ID を割り当てるための、インクリメントされる整数のフィールドを持っています。

アカウント 0x02 のアカウントストレージに NonFungibleToken.NFTMinterリソースが保存されていることがわかるはずです。これはエディタの下のRESOURCE BOX に表示されています。

これで、保存されている NFTMinter を使って新しい NFT を発行し、アカウント 0x02 のコレクションに預け入れることができます。

Transaction2.cdc というファイルを開きます。
アカウント
0x02 を唯一の署名者として選択し、トランザクションを送信します。
このトランザクションは、発行された NFT をアカウント所有者の NFT コレクションに預け入れます。

Script1.cdc を再度開き、スクリプトを実行してください。
これはアカウント
0x02 が所有している NFT の一覧を表示します。

アカウント 0x02id=1 の NFT を所有していることがわかるはずです。

"Account 2 NFTs"
[1]

NFT を送る

NFT を他のアカウントに送付する前に、そのアカウントに自身の NFT コレクションをセットアップしてもらい、そのアカウントが NFT を受信できるようにする必要があります。

Transaction3.cdc というファイルを開き、アカウント 0x01 を唯一の署名者として選択しトランザクションを送信します。

アカウント 0x01 のアカウントストレージには、空の Collection リソースが保存されているはずです。また、/public/ ドメインに、コレクションへの capability が作成・保存されているはずです。

Transaction4.cdc というファイルを開き、アカウント 0x02 を唯一の署名者として選択し、トランザクションを送信します。
このトランザクションは、トークンをアカウント
0x02 からアカウント 0x01 に送付します。

これで、両方のアカウントのコレクションを確認すると、アカウント 0x01 がトークンを所有し、アカウント 0x02 は何も持っていないことがわかります。

Script2.cdc を実行して、各アカウントのトークンを確認してください:

出力にはこのようなものが表示されるはずです。

"Account 1 NFTs"
[1]
"Account 2 NFTs"
[]

アカウント 0x01 には ID=1 の NFT が 1 つあり、アカウント 0x02 には 1 つもありません。これは、NFT がアカウント0x02 からアカウント 0x01に送付されたことを示しています。

おめでとう!これで、動作する NFT を手に入れました。

全コードのまとめ

今まで説明してきた NFT コントラクトの全体をまとめると以下の通りです。すでにデプロイしたコードと全く同じものですので、このコードを使って新たに作業をする必要はありません。

Flow のマーケットプレイスの作成

NFT が動作するようになったので、自分で NFT の機能を拡張することもできますし、Fungible Token と NFT の両方を使ったマーケットプレイスを作る方法を学ぶこともできます。

--

--

Ara
Flow Japan

ソフトウェアエンジニア。生物学、民俗学、仏教、神道、メディアアート、博物学、フォント、ブロックチェーンなどに興味あり。