Cadence チュートリアル(3)Fungible Tokens

Ara
Flow Japan

--

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

前回にも増して、かなり読み応えのあるチュートリアルとなっています。
Flow の Fungible Token はとても変わっていますが、これはもともと Cadence が、Non-Fungible Token の扱いを意識して設計されているためだと考えられます。Non-Fungible Token の解説の前に、この Fungible Token の解説を読むことで、台帳ベースの所有権管理とはまったく違う考え方を体感できると思います。

目次

  • Flow エミュレータ上での Fungible Tokens
  • Fungible Tokens をより深く知る
  • 所有権の分権化
  • リソースによる直感的な所有権
  • Public 領域のセキュリティ担保:Capability によるセキュリティ
  • 安全な実装のためにインターフェースを使う
  • Flow プレイグラウンドで Fungible Token と対話する
  • Vault への Capabilities および参照を、作成・保存・公開する
  • トークンを他のユーザーに送る
  • アカウントの残高を確認する
  • Flow の Non-Fungible Token

このチュートリアルでは、Fungible Token のデプロイ、保存、送信を行います。

Flow プレイグラウンドでこのチュートリアルのコードを開きます。
https://play.onflow.org/ce1f7b8e-13fc-4e09-b5fa-db28e20923f1

チュートリアルでは、このコードと対話するために様々なアクションを実行します。

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

今日のブロックチェーンで最も人気のあるコントラクトのひとつは Fungible Token です。これらのコントラクトは、他のユーザーに送付して通貨として使用できるトークンを作成します(例:Ethereum の ERC-20)。

一般に、中央台帳がユーザーのトークンの残高を管理します。しかし Cadence では、リソースという新しい考え方で Fungible Token を実装して、中央台帳を使うことを回避します。

Flow ネットワークのトークン

Flow では、ネットワークのネイティブ・トークンは、このチュートリアルで紹介するものと同様のスマートコントラクトを使って実装される予定です。ネイティブ・トークンには、追加のコントラクトとフックを使ったトランザクション実行時の支払いやステーキングの機能がありますが、それ以外の機能は、開発者やユーザーも利用可能です。

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

  1. アカウント 0x01 に Fungible Token のコントラクトをデプロイします。
  2. Fungible Token のオブジェクトを作り、アカウントのストレージに保存します。
  3. 他の人があなたにトークンを送れるようにする参照を作ります。
  4. 同じように別のアカウントをセットアップします。
  5. あるアカウントから別のアカウントにトークンを送信します。
  6. スクリプトを使って、アカウントの残高を確認します。

このチュートリアルを進める前に、「最初のステップ」と「Hello World!」の説明に沿って、言語の基本とプレイグラウンドについて学ぶことをお勧めします。

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

まず、このリンクから、Fungible Token のコントラクト、トランザクション、スクリプトがあらかじめ読み込まれているプレイグラウンドのセッションを開きます。
https://play.onflow.org/ce1f7b8e-13fc-4e09-b5fa-db28e20923f1

アカウント 0x01 のタブを開きます。FungibleToken.cdc には Fungible Token のフルコードが含まれています。これは Fungible Token をアカウントに保存したり、トークンを送信・受信する、中核となる機能を提供します。

Cadence での Fungible Token の実装は、最初は理解しづらいかもしれません。この機能とコードの詳細については、次のセクションをお読みください。

あるいは、すぐに導入してプレイグラウンドで使いたい場合は、このチュートリアルの「Fungible Token とのインタラクション」のセクションまでスキップしてください。

Fungible Tokens をより深く知る

Flow における Fungible Token の実装方法は、他のプログラミング言語と異なります。結果的に以下のようになります。

  • 所有権は分権化され、中央の台帳に依存しない
  • バグや脆弱性のリスクは少なく、攻撃の機会が少ない
  • 整数のアンダーフロー・オーバーフローのリスクがない
  • アセットは複製できず、失くなったり、盗まれたり、破壊されたりすることが極めて困難
  • コンポーザリティが高い(訳注:コードを組み合わせやすい)
  • ルールをイミュータブルにすることができる
  • コードは意図せず公開されない

所有権の分権化

Flowは、中央台帳システムを使用する代わりに、新しいアセット所有の考え方で各アカウントにアセットの所有権を紐付けます。以下の例では、Solidity が Fungible Token をどのように実装しているかを紹介します。簡便化のため、トークンの格納と送信のコードのみ示します。

ご覧のように、Solidity は Fungible Token のために中央台帳システムを使います。トークンの状態を管理するコントラクトが 1 つあり、ユーザーがトークンで何かをしたいときは、毎回、中央の ERC20 コントラクトとインタラクションして、関数を呼び出して残高を更新しなければなりません。このコントラクトは、すべての機能のアクセス制御を処理し、正しいかどうかのチェックを自身ですべて実装し、すべてのユーザーにルールを適用します。

Flow は、中央台帳システムを使用する代わりに、違ったコンセプトで、スマートコントラクトの開発者・ユーザーに、より優れた安全性、セキュリティ、明確性を提供します。このセクションでは、Flow のリソース、インターフェイス、その他の機能がどのように採用されているかを、Fungible Token の例を用いて紹介します。

リソースで所有権を直感的にする

Cadence の重要なコンセプトは、線形型(訳注:宣言した変数は 1 回しか使えない)であるリソースです。リソースは、構造体と同様に、独自に定義されたフィールドと関数を持つ複合型です。違いは、リソースオブジェクトには、コピーや紛失を防ぐ特別なルールがあることです。リソースは、アセットの所有権に関する新しい考え方です。トークンの所有権を中央台帳のスマートコントラクトで表現するのではなく、各アカウントがアカウントストレージ内にリソースオブジェクトを所有します。その各リソースオブジェクトは、持っているトークンの数を記録しています。このようにして、ユーザーがお互いに取引をしたい場合、中央のトークンのコントラクトを介さずに、ピアツーピアで取引を行なえます。トークンを送り合うには、中央の transfer 関数ではなく、各ユーザーが自分のリソースオブジェクトと相手のリソースオブジェクト内にある transfer 関数を呼び出します。

このアプローチはアクセス制御をよりシンプルにします。なぜなら、中央コントラクトが関数を呼び出したユーザーをチェックする代わりに、ほとんどの関数の呼び出しがユーザーアカウント内に保管されたリソースオブジェクト上で発生するからです。

このコンセプトは、capability-besed security と呼ばれます。後のセクションで詳しく説明します。

このアプローチは、潜在的なバグからの保護にも役立ちます。すべてのロジックが中央のコントラクトに含まれる Solidity のコントラクトでは、悪用された場合、コントラクトに関与しているすべてのユーザに影響を与える可能性があります。Cadence では、リソースのロジックにバグがある場合、攻撃者は各トークンホルダーのアカウントに個別にバグを突いていかなければならず、中央台帳システムよりも複雑で時間のかかる作業となります。

下記は、Fungible Token の Vault(金庫)リソースの例です。トークンを所有するユーザーは、このリソースを自分のアカウントに保管しています。各アカウントには Vault リソースのコピーが保存され、 FungibleToken コントラクト全体のコピーは保存されないことが重要です。FungibleToken コントラクトを保管する必要があるのは、トークンの定義を管理する最初のアカウント(訳注:デプロイしたアカウント)のみです。

このコードは学習用であり、完全なものではありませんが、トークンのリソースがどのように機能するかを示しています。各トークンのリソースオブジェクトは、残高とその関連機能(入金、出金など)を持っています。ユーザーがこれらのトークンを使いたい場合、ユーザーはアカウントストレージに、残高ゼロのリソースをインスタンス化します。なお、この言語では、一度だけ実行される init() 関数で、すべてのメンバ変数を初期化しなければなりません。

// ユーザーの Vault の残高
// 符号なし固定小数を使う理由は、負の数は必要なく、精度が必要なため
pub
var balance: UFix64

init(balance: UFix64) {
self.balance = balance
}

すると、この deposit() 関数はトークン送信先となるあらゆるアカウントで有効になります。

pub fun deposit(from: @Vault) {
self.balance = self.balance + from.balance
destroy from
}

あるアカウントがトークンを別のアカウントに送信したい場合、送信側のアカウントはまず自身の withdraw() 関数を呼び出します。この関数は、リソースの残高からトークンを差し引き、その残高を保持する新しいリソースオブジェクトを一時的に作成します。送信側のアカウントは、受信側のアカウントの deposit() 関数を呼び出し、リソースのインスタンスを他のアカウントに文字通り移動して、リソースの残高に追加し、使用されたリソースを破棄します。Cadence はリソースとのインタラクションに厳格なルールを課しているため、リソースを破棄する必要があります。リソースをコードの中に残したままにすることはできません。明示的に破棄するか、アカウントのストレージに保存する必要があります。

リソースを操作する際には、 @ 記号と特別な移動演算子 <- を使います。

pub fun withdraw(amount: UInt64): @Vault { 

この @ 記号は、フィールド・引数・戻り値にリソース型を指定する場合に必要です。移動演算子 <- は、リソースが代入・パラメータ・戻り値で使用された場合、新しい場所に移動して、古い場所からは無くなることを明確にします。これにより、リソースは一度に一つの場所にだけ存在することが保証されます。

リソースがアカウントのストレージから移動されたときは、その後、同じまたは別のアカウントのストレージに移動されるか、明示的に破棄される必要があります。

destroy from

この行は、現実の価値を表すことが多いリソースが、コーディングエラーによって失われないようにします。

あなたは、次の演算が明示的に保護されていないことに気づくでしょう。

self.balance = self.balance - amount

Solidity では、整数のオーバーフローやアンダーフローのリスクがありますが、Cadence にはオーバーフローやアンダーフローの保護機能が組み込まれているので、リスクはありません。また、この例では、符号なし整数を使っているので、金庫の残高が 0 以下になることはありません。

さらに、トークン受信時、受け取るアカウントにはトークンのリソース型のコピーがないといけないので、間違ったアドレスに送られて資金が失われることはありません。あるアドレスに正しいリソース型がインポートされていない場合、トランザクションは中止(revert)されるので、間違ったアドレスに資金が送られて失われることはありません。

withdraw() 内の新しい Vault 生成している行では、関数呼び出し部分に定義されている balance という名前のパラメータがあります。

return <-create Vault(balance: amount)

これも、Cadence が持つ、コードの明快さを向上させる機能です。開発者が特にオーバーライドしない限り、すべての関数呼び出しは、渡す引数の名前を指定する必要があります。

Public 領域のセキュリティ担保:Capability によるセキュリティ

Cadence のもうひとつの重要な機能は、Capability Security を活用していることです。この機能により、withdraw 関数は公開されていても、意図したユーザーと、そのユーザーによって承認されたユーザー以外は、彼らの Vault からトークンを引き出せません。

Cadence のセキュリティモデルでは、アカウントのストレージに保存されたオブジェクトは、そのオブジェクトを所有しているアカウントからしかアクセスできないことが保証されます。他のユーザーもアクセスできるようにしたい場合、ユーザーは参照を公開することができます。これは「API」のようなもので、他のユーザーが自分のオブジェクトの関数を呼び出すことを許可できます。

アカウントが、別のアカウントのオブジェクトのフィールド・関数にアクセスできるのは、そのオブジェクトへの参照を持っている場合のみです。オブジェクトの所有者のみが、そのオブジェクトへの参照を作れます。したがって、ユーザーが自分のアカウントで Vault を作成する場合、そのユーザーは、 deposit() 関数と balance への参照のみを公開して、withdraw() は所有者のみが呼び出せる関数として隠すことができます。

これで、アクセス制御のために msg.sender をチェックする必要がなくなりました。オブジェクトの所有者ではない場合、および、所有者が作ったオブジェクトへの有効な参照がない場合は、そのオブジェクトにアクセスできません。

安全な実装のためにインターフェースを使う

次に説明する Cadence の重要なコンセプトは、契約による設計です。これは、事前条件事後条件を使って、プログラムによって引き起こされる状態の変化を記述し、プログラム的にアサート(想定通りであることを確認)するものです。これらの条件は、型がどのように定義されどう振る舞うかのルールを強制するインターフェースの中に記述されます。これらの条件はイミュータブル(不変)な形式でオンチェーンで保存されます。そのため、特定のコードの一部分で、一定の標準に準拠している事を保証するため、これらの条件をインポートして実装する事ができます。

上で定義した Vault リソースのインターフェースがどのように見えるか、その例を以下に示します。

この例では、Vault リソースは、これらのインターフェースの両方を実装します。インターフェースは、特定のフィールド・関数がリソースの実装に存在し、それらの関数が実行前・実行後に特定の条件を満たすことを保証します。インターフェースはオンチェーンに保存され、他のコントラクトまたはリソースにインポートできるため、これらの要件は、人的エラーの影響を受けにくい、不変で信頼できる情報源により適用されます。

また、関数・フィールドに pub キーワードがついていることがわかります。Cadence のすべてのフィールド・関数はデフォルトではプライベートで、ローカルスコープからしかアクセスできないため、明示的にパブリックと定義しています。ユーザーは、所有する型の一部を明示的に公開する必要があります。これにより、型が意図せずにパブリックなコードを持つことを防げます。

Flow プレイグラウンドで Fungible Token と対話する

Fungible Token がどのように動作するか説明したので、アカウントにコントラクトをデプロイして、それと対話するためのトランザクションを送信しましょう。

このページの上部にあるリンクをクリックして、プレイグラウンドでFungible Token のテンプレートが開かれていることを確認します。アカウント 0x01 が開かれ、以下のコードが表示されているはずです。

エディタ右下の [Deploy] ボタンをクリックして、コードをデプロイします。

このデプロイ作業は、Fungible Token のコントラクトを、トランザクションにインポートできるように、アクティブなアカウント(アカウント 0x01)に保存します。

コントラクトの init() 関数は、コントラクト生成時に実行され、その後は二度と実行されません。この例では、この関数は、初期値の残高が 30 の Vault オブジェクトのインスタンスと、新しいトークンを mint するために使う VaultMinter オブジェクトを、実行したアカウントのストレージに保存します。

// 初期値 30 の残高で Vault を作り、 `to` で指定したパスに格納する.
// パス は、<ドメイン> と <識別子> からなる.
// <ドメイン> は `storage` `private` `public` のいずれか.
// <識別子> は任意の名前.
self.account.save(<-create Vault(balance: UFix64(30)), to: /storage/MainVault)

この行は、新しい @Vault オブジェクトをストレージに保存します。アカウントストレージは、ドメインと識別子からなるパスでインデックス化されます。パスは /<ドメイン>/<識別子> で構成されます。パスには 3 つのドメインのみが使えます。

  • storage : すべてのオブジェクトを保存する場所。アカウントの所有者のみがアクセスできる。
  • private : オブジェクトへのリンク(capability とも呼ばれる)を保存する場所。アカウントの所有者のみアクセスできる。
  • public : オブジェクトへのリンクを保存する場所。ネットワーク内の誰でもアクセスできる。

コントラクトは self.account を使って、それが格納されているアカウントのプライベートな AuthAccount オブジェクトにアクセスできます。このオブジェクトには、ストレージを変更できる様々な関数があります。呼び出せるすべての関数の一覧は、用語集を参照してください。

この行では、保存する値の型に @Vault を指定して、オブジェクトをストレージに保存する save() 関数を呼び出しています。第 1 引数には保存する値、第 2 引数には保存するパスを指定します。save() では、パスは /storage ドメインでなければなりません。

次の行で、 VaultMinter オブジェクトも同様に /storage に保存します。

self.account.save(<-create VaultMinter(), to: /Storage/MainMinter)

画面下部の [STORAGE] ボックスをみると、FungibleToken.VaultFungibleToken.VauktMinter のリソースオブジェクトがアカウントストレージに保存されていることを確認できます。

これで、FungibleToken を扱うトランザクションを実行する準備が整いました!

Vault への Capabilities および参照を、作成・保存・公開する

参照は、他の言語でいうポインタのようなものです。これらはアカウントストレージ内のオブジェクトへのリンクであり、リンクが参照するオブジェクトのフィールドを読んだり、関数を実行するために使えます。オブジェクトを直接 移動・変更することはできません。

Fungible Token の Vault への参照を作成する状況は様々です。トランザクションのどこからでも Vault の関数を呼び出せる簡単な方法が欲しい場合があるかもしれません。他の人が自分の代わりにトークンを送信できるよう、Vaultwithdraw() 関数への参照だけを含むリソースを送ることもできます。Vault への参照を引数にして、その参照の関数を呼び出し、その後、参照を破棄する、という関数を作ることもできます。このシナリオは、おそらく参照が最も頻繁に使われるケースでしょう。 Vault リソースの mintTokens() 関数では、すでにこのパターンを使っています。

// 新しいトークンを mint し、 `Receiver` 参照を使って、
// アカウントの Vault に入金する関数
// `recipient` は Receiver インターフェースを実装しているどのリソースでも良いので
// `&AnyResource{Receiver}` の型になっている
pub fun mintTokens(
amount: UFix64, recipient: &AnyResource{Receiver}
) {
FungibleToken.totalSupply = FungibleToken.totalSupply
+ UFix64(amount)
recipient.deposit(from: <-create Vault(balance: amount))
}

この関数は、 Receiver インターフェースを実装している任意のリソースへの参照を受け取り、その参照を使ってリソースの deposit() 関数を呼び出します。

別のアカウントがトークンを送信できるように、あなたの Vault への参照を作成してみましょう。

Transaction1.cdc という名前のトランザクションを開きます。
Transaction1.cdc には、保存されている Vault への参照を作成するための以下のコードが含まれます。

参照を使うためには、まずオブジェクトの capability を作成する必要があります。オブジェクトの capability とは、フィールドの読み取りや関数の呼び出しができない、ストレージ内のオブジェクトへのリンクです。参照は capability から作成できますが、参照を保存することはできません。参照はトランザクション実行の最後に消失する必要があります。この制限は、元の実行が終了する前に悪意のあるユーザが同じ関数を何度も何度も呼び出す攻撃であるリエントランシー攻撃を防ぐためです。オブジェクトに対して一度に 1 つの参照のみを許可することで、ストレージ内のオブジェクトに対するこれらの攻撃を防ぐことができます。

capability を作成するには、link() 関数を使います。

// `Receiver` `Balance` インターフェースのフィールド・関数に制限された、
// Vault のリンクを作成する
// これは balance フィールドと、 deposit() 関数のみを公開している
//
acct.link<&FungibleToken.
Vault{FungibleToken.Receiver, FungibleToken.Balance}>
(/public/MainReceiver, target: /storage/MainVault)

link() は、第 1 引数のパスに保存される、第 2 引数の target をターゲットとする新しい capability を作成します。リンクを制限する型は <> で指定します。 &FungibleToken.Vault{FungibleToken.Receiver, FungibleToken.Balance} を使うと、リンクが Receiver インターフェイスを実装していて、キャストされている限り、どのリソースであってもよいという意味になります。これが参照を記述する一般的なフォーマットです。最初に、& の後に具体的な型を続けて書き、中括弧 {} でインターフェイスを記述することで、そのインターフェイスを実装して指定されたフィールドのみを含む参照であることを示します。

この capability を /public/MainReceiver に入れたのは、パブリックにアクセスできるようにしたいからです。アカウントの public ドメインは、アカウントの PublicAccount オブジェクトを介してネットワーク内の誰もがアクセスできます。

次に、トランザクションの post フェーズについてです。

post {
// capabilities が正しく作成されかどうかを、
// パブリックに取得できて、 `Receiver` インターフェースを実装した
// Vault オブジェクトであることのチェックによって確認する
getAccount(0x01).getCapability(/public/MainReceiver)!
.check<&FungibleToken.Vault{FungibleToken.Receiver}>():
"Vault Receiver 参照は正しく作成されませんでした"
}

post フェーズは、トランザクションの実行後に特定の条件が満たされていることを確認するためのフェーズです。ここでは、/public/ パスから capability を取得し、 check() 関数を呼び出して、capability にストレージ内のオブジェクトへの有効なリンクが含まれていることを確認しています。

! 記号を使用していることに気づくかもしれません。これは、force-unwrap 演算子です。ストレージから値を読んだり、capability を作成する際には、その場所が空である可能性があります。その場所に何もなければ、それらと対話する関数は何もしないで終わる可能性があります。Optional は、オブジェクトがないことを表現できる型です。Optional 型は ? をつけることで宣言します。Optional には、指定した型のオブジェクトがある場合と、何もない場合の、2つのケースがあります。そのため、オブジェクトが nil の場合についても処理する必要があります。force-unwrap 演算子 ! を使うと、Optional にオブジェクトが含まれている場合に、それを取得できます。nil の場合には、プログラムの実行は中断されます。

唯一の署名者としてアカウント 0x01 を選択します。
[Send] ボタンをクリックしてトランザクションを送信します。
このトランザクションでは、Vault への新しいパブリックな参照が作成され、正しく作成されたかどうかが確認されます。

トークンを他のユーザーに送る

10 個のトークンをアカウント 0x02 に送るトランザクションを実行してみましょう。アカウント 0x01 の Vault で withdraw() 関数を呼び出し、トークンを移動するための一時的な Vault オブジェクトを作成して、 deposit() を呼び出してそのトークンをアカウント 0x02 に預けます。

ここで、Cadence に導入されている安全性を高める機能を紹介しましょう。トークンを所有するには、アカウントに Vault オブジェクトを保存しておく必要がありますが、トークンを受け取る準備ができていないアカウントにトークンを送ろうとすると、トランザクションは失敗します。このように、トークンを送る際に、間違ったアカウントのアドレスを入力してしまった場合でも、Cadence がユーザーを保護します。

アカウント 0x02 はトークンを受け取るようにセットアップされていないので、まずそのセットアップを行ないます。

トランザクション Transaction2.cdc を開きます。
アカウント
0x02 を唯一の署名者として選択します。
[Send] ボタンをクリックして、アカウント
0x02 がトークンを受け取れるようにセットアップします。

ここでは、アカウント 0x01 で実施した Vault のセットアップと同じことを、ひとつのトランザクションで実施します。アカウント 0x02 は、財産をつくりあげる準備ができました。ご覧のように、アカウント 0x02Vault を作るには、createEmptyVault() 関数を呼び出して残高が 0 の Vault を作成しなければなりません。リソースの作成は、それが定義されているコントラクト内に制限されています。そのため、このように、Fungible Token のスマートコントラクトは、誰もが好き勝手に新しいトークンを作成できないように制御できます。

FungibleToken コントラクトのデプロイの初期処理で、アカウント 0x01VaultMinter オブジェクトを作成しています。このオブジェクトを所有しているアカウントは新しいトークンを mint できます。いまはアカウント 0x01 が所有しているため、新しいトークンを mint する権限はアカウント 0x01 にしかありません。コントラクトで mintTokens() 関数を定義することもできますが、そうすると、関数呼び出しの送信者が権限を持っているかどうかをチェックしなければなりません。

前に説明したように、リソースモデルcapability によるセキュリティはこのアクセス制御をコードで定義するのではなく、組み込みの言語の構造として処理してくれます。アカウント 0x01 が他のアカウントにトークンを mint する権限を与えたい場合、 VaultMinter オブジェクトを他のアカウントに移動させるか、他のアカウントにVaultMinter へのプライベートな capability を与えることができます。また、デプロイ後に mint できないようにしたい場合は、コントラクトの初期化時にすべてのトークンを mint して、コントラクトに VaultMinter を含めないこともできます。

次のトランザクションでは、アカウント 0x01 は 30 個の新しいトークンを mint して、アカウント 0x02 の新しく作成された Vault に預けます。

アカウント 0x01 を唯一の署名者として選択し、アカウント 0x02 に 30 個のトークンを mint するため Transaction3.cdc を送信します。

Transaction3.cdc には以下のコードが含まれています。

execute フェーズでは、30 個のトークンを mint して、参照を使ってアカウント 0x02Vault に預け入れます。

これは、異なるステージをまたぐローカル・トランザクション変数を利用するトランザクションの最初の例です。mintingRefreceiverRef 変数を prepare フェーズ外で宣言していますが、これらは prepare フェーズ内で初期化する必要があります。その後、トランザクションの post ステージでそれらを使用できます。

このトランザクションでは、capability から参照を取得(借用)できることに加えて、ストレージ内のオブジェクトから直接、参照を取得できることがわかります。

// 保存済みのプライベートな minter リソースへの参照を取得する
self
.mintingRef = acct.borrow<&FungibleToken.VaultMinter>(from: /storage/MainMinter) ?? panic("Could not borrow a reference to the minter")

ここでは、参照先が /storage/MainMinter の借用結果を、VaultMinter の参照として指定しています。参照は Optional として借用されているので、値が nil でないことを確認するために nil-coalescing 演算子 (?) を使用しています。値が nil の場合はトランザクションは中止され、エラーメッセージが表示されます。

組み込み関数 getAccount() を使って、任意のアカウントのパブリックアカウントオブジェクトを取得できます。パブリックアカウントオブジェクトを使うと、アカウントの public ドメインから、capability を取得できます。ここには、パブリックな capability が格納されています。

public パスから capability を取得するために getCapability() 関数を使い、その後、capability の borrow() 関数を使って、capability から Vault{Reveicer} として型が指定された参照を取得します。

// パブリックな Receiver の capability を取得
let cap = recipient.getCapability(/public/MainReceiver)!
// capability から参照を借りる
self.receiverRef = cap.borrow<
&FungibleToken.Vault{FungibleToken.Receiver}>()
?? panic("Could not borrow a reference to the receiver")

execute フェーズでは、単純に 30 個のトークンを mint するために参照を使い、アカウント 0x02Vault に預け入れます。

アカウントの残高を確認する

さて、アカウント 0x01 とアカウント 0x02 は両方とも、残高 30 トークンの Vault オブジェクトをストレージに持っているはずです。また、両方とも、保管されている Vault にリンクする Reveicer の capability が /public/ ドメインに保存されているはずです。

アカウントは、トークンを受け取れるようにセットアップされていない限り、どの種類のトークンも受け取れません。これにより、間違ったアドレスにトークンを送るすることは難しくなります。ただし、アカウントの Vault のセットアップが正しく行われなかった場合も、トークンは送れません。

スクリプトを実行して、Vault が正しくセットアップされていることを確認してみましょう。

スクリプトを使うと、アカウントのパブリックなステートにアクセスできます。スクリプトはどのアカウントにも署名されておらず、ステートは変更できません。

以下のコードはエミュレーター内のそれぞれのアカウントの残高を出力します。

左側の [SCRIPT TEMPLATES] から Script1.cdc というスクリプトを開きます。Script1.cdc には以下のコードが含まれています。

[Execute] ボタンをクリックして Script1.cdc を実行します。
これにより、以下のことが保証されます。

  • アカウント 0x01 残高は 30
  • アカウント 0x02 残高は 30

セットアップが正しく実行されていれば、以下のような行が表示されます。

> "Account 1 Balance"
> 30.00000000
> "Account 2 Balance"
> 30.00000000
Result > {"type":"Void"}

エラーが発生した場合は、おそらく以前のステップを見逃しているので、トランザクションを再実行する必要があるかもしれません。

このチュートリアルの最初の状態に戻すために、エミュレータを再起動する方法については、プレイグラウンド のマニュアル を参照してください。

いま、私たちは 2 つのアカウントにそれぞれ Vault を持っているので、トークン送付を試すことができます。

Transaction4.cdc という名前のトランザクションを開きます。
署名者としてアカウント
0x02 を選択し、トランザクションを送信します。

Transaction4.cdc には、トークンを他のユーザーに送信するための以下のコードが含まれています。

この例では、まず署名者が withdraw() 関数を使って自分のストレージの Vault オブジェクトからトークンを引き出します。ここで、トークンを送るために使われる、残高 が 10 の一時的な Vault リソースオブジェクトが作られます。 execute フェーズでは、別のユーザーの deposit() 関数を使って、そのリソースをそのユーザーのストレージに移動します。一時的な Vault オブジェクトは、その残高が受信者のストレージの Vault に追加された後に破棄されます。

トークンの送付に、なぜ 2 回の関数呼び出しを使う必要があるのか不思議に思われるかもしれません。これは、Cadence のリソースの仕組みに起因しています。台帳ベースのモデルでは、transfer を呼び出すだけで台帳が更新されます。しかし、Cadence ではトークンの保存場所が重要です。多くの場合、トークン送付はアカウントからアカウントへの直接の送付ではありません。ほとんどの場合、トークンはまず、送金とは別の目的(何かを購入するような)のために使用され、それはアカウントのストレージに預け入れられる前に、Vault を別途送金して検証する必要があります。

ほとんどの場合、トークンは何かを購入するような目的で使われるので、アカウントの保管場所に預けられる前に、何度か Vault を移動する必要があります。引き出し・預け入れの 2 つに分けることで、トランザクションの pre セクションで、アカウントのどの部分が変更可能かを静的に検証できる利点も生かせます。これにより、アプリから署名のために提示されたトランザクションを取得する際に、ユーザーが安心して利用できるようになります。

Script1.cdc を再度実行します。

正しく実行されれば、アカウント 0x01 の残高が 40、アカウント 0x02 の残高が 20 であることを示す以下の行が表示されるはずです。

> "Account 1 Balance"
> 40.00000000
> "Account 2 Balance"
> 20.00000000
Result > {"type":"Void"}

これで、Cadence および Flow での Fungible Token の基本的な使い方がわかりました!

これから、Fungible Token を、例えば次のように拡張することもできます。

  • これらのトークンの Faucet
  • 残高が一定額に達した場合のみ引き出せるエスクロー
  • 新しいトークンを mint する関数をリソースに追加する!

Flow の Non-Fungible Token

これで、Flow の Fungible Token がどのように機能するかを理解できたので、いよいよ Non-Fungible Token を扱う準備ができました!

--

--

Ara
Flow Japan

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