Cadence チュートリアル(2)Hello World
この記事は、Flow 公式の Cadence チュートリアル “2. Hello World” の日本語 翻訳版です。
前回の記事:「Cadence チュートリアル(1)最初のステップ」少し長いですが、この一回分のチュートリアルを終えると、Flow を理解する上で重要な、アカウント、トランザクション、スクリプト、リソース、参照などの枠組みがよくわかります。
目次
- アカウントとトランザクション
- スマートコントラクトを作る
- コードをデプロイする
- トランザクションを作る
- リソースを作る
- リソースと対話する
- リソースの参照を作る
- スクリプトを実行する
- プレイグラウドの使い方:アカウント
- プレイグラウドの使い方:トランザクション
最初のスマートコントラクトを書いてデプロイしてみましょう!
Flow プレイグラウンド でこのチュートリアルのコードを開いてください。
https://play.onflow.org/5b5bd86b-3845-4c16-9a29-626ef6228846チュートリアルでは、このコードと対話するために様々なアクションを実行します。
アクションを要求する指示は、このような装飾で書かれています。これらのアクションの箇所だけ読んでもコードを実行できますが、説明をいっしょに読むことで、より言語のデザインを理解できます。
スマートコントラクトとは、信頼できる第三者を介さずに契約を検証・実行するプログラムのことです。ブロックチェーン上で実行されるプログラムは一般にスマートコントラクトと呼ばれますが、これは銀行などの中央機関に頼ることなく重要な機能(通貨など)を実現するためです。
Flow を使い始める前に、アカウントとトランザクションがどのようにモデル化されるかを理解する必要があります。
アカウントとトランザクション
ほとんどのブロックチェーンと同様、Flow のプログラミングモデルはアカウントとトランザクションが中心です。すべての永続的な状態とそのインターフェイス(対話する方法)はアカウントに格納されており、すべてのコード実行はトランザクション内で行われます。トランザクションは、永続的な状態と対話するために外部ユーザーによって送信されるコードのブロックであり、アカウントのストレージを直接変更できます。
各ユーザーは、1 つ以上の秘密鍵で制御されるアカウントを持っています。これは、マルチシグウォレットがデフォルトでサポートされていることを意味します。
アカウントは 2 つの領域に分かれています。
- 最初の領域はコントラクト領域です。これは、スマートコントラクトと呼ばれるプログラムを格納する領域です。プログラムは、共通の機能として型定義、フィールド、関数を含んでいます。また、他のコントラクトから利用する際のガイドラインとなる、コントラクトのインターフェースを格納できます。この領域はトランザクションから直接アクセスできません。アカウントの所有者は、ここにコントラクトを保存・上書きできます。
- 二つ目の領域はアカウントのファイルシステムです。この領域はトランザクションによってアクセスでき、アカウントが所有するオブジェクトとそのアクセスを制御する capabilities(実行可能な関数の一覧)を格納します。オブジェクトはアカウントのファイルシステムの特定のパスに格納されます。パスはドメインと識別子で構成されます。
パスは /
で始まり、ドメイン、区切り文字 /
、そして識別子で終わります。例えば、パス /storage/test
には、ドメイン storage
と識別子 test
が含まれています。
アカウントのファイルシステムの有効なドメインは、 storage
private
public
の 3 つだけです。
識別子は、そのパスに何が保存されているか示す任意の名前にできます。
storage
ドメインは、すべてのオブジェクト(トークンや NFT など)が保存されている場所です。アカウントの所有者のみがアクセスできます。private
ドメインはプライベートな API のようなものです。オプションで、格納されているアセットの capabilities(実行可能な関数の一覧)を保存できます。所有者とアクセス権を与えられた人だけが、プライベートなアセットに定義されている関数を呼び出すために、これらのインターフェイスを使用できます。ユーザーは通常、他のアカウントのプライベートなオブジェクトを参照する、プライベートな capabilities をここに格納します。public
ドメインは、アカウントのパブリックな API のようなものです。所有者は、ネットワーク内の誰しもがアカウントに格納されているプライベートなアセットと対話できるように capabilities をここに格納できます。
Flow におけるトランザクションは、1 つ以上のアカウントによって署名された、任意のサイズの Cadence コードのブロックとして定義されます。トランザクションは、トランザクションに署名したアカウントの /storage/
および /private/
ドメインにアクセスでき、これらのドメインへの読み書きが可能です。また、パブリックなコントラクトや他のユーザのアカウントの public
ドメインの関数を呼び出せます。
スマートコントラクトを作る
「Hello World!」を返す public 関数を持つスマートコントラクトを書くことから始めましょう。
まず、このリンクから、Hello World のコントラクト、トランザクション、スクリプトがあるプレイグラウンドのセッションを開きます。
https://play.onflow.org/5b5bd86b-3845-4c16-9a29-626ef6228846アカウント
0x01
のタブで、HelloWorld.cdc
ファイルを開きます。HelloWorld.cdc
には次のコードが含まれます。
コントラクトとは、Flow のアカウントのコントラクト領域に存在するコード(機能)とデータ(状態)の集合体です。現在のところ、アカウントは 1 つのコントラクトまたは 1 つのコントラクト・インターフェースのみを持つことができます。 pub let greeting: String
の行は、 pub
let
キーワードを使って、String
型の greeting
という public な定数を宣言しています。もし変数を宣言したい場合は var
を使います。
pub
キーワードは、すべてのスコープでアクセスできるが、書き込むことはできないアクセス制御を意味します。厳密に記述したい場合は、 access(all)
と書いても同じ意味です。Cadence で許可されているアクセス制御のレベルについては、用語集のアクセス制御のセクションを参照してください。
init()
セクションはイニシャライザと呼ばれます。これは、コントラクトが作成されたときに一度だけ実行される関数です。この例では、イニシャライザは greeting
フィールドに "Hello, World!"
をセットします。
次に、 String
型の値を返す public 関数の宣言です。コントラクトをインポートした人は誰でもコントラクトの public 関数(pub
または access(all)
が指定されているもの)を呼び出せます。
これで、このコントラクトをアカウントにデプロイして、その関数を呼び出すトランザクションを実行できます。
コードをデプロイする
Cadence のコードが用意できたので、自分のアカウントにデプロイしてみましょう。
アカウント 0x01
のタブが選択されていて、 HelloWorld.cdc
ファイルがエディタにあることを確認してください。
[Deploy] ボタンをクリックして、エディタの内容をアカウント 0x01
にデプロイします。
[DEPLOYMENT RESULT] エリアに、デプロイが成功したことを示すログが表示されます。
Deployed Contract To: 0x01
選択したアカウントタブのアカウント番号の下には、コントラクト名が表示されます。これは、 HelloWorld
のコントラクトがそのアカウントにデプロイされていることを示しています。このタブを見て、どのアカウントにどのコントラクトがあるか確認できます。なお、1 つのアカウントにつき、1 つのコントラクトしかデプロイできません。(訳注:少なくともいまのプレイグラウンドでは)
トランザクションを作る
Transaction1.cdc
という名前のトランザクションを開きます。Transaction1.cdc
には次のコードが含まれます。
これが Cadence のトランザクションです。トランザクションには、他のアカウントからのインポート、アカウントストレージとのやり取り、他のアカウントとのやり取りなど、任意のコードを含められます。
スマートコントラクトと対話するには、トランザクションはまず、そのコントラクトが格納されているアドレスから、コントラクトをインポートする行を書く必要があります。これにより、そのコントラクトのインターフェイス、リソース、および public 関数がインポートされます。トランザクションはそれらを使ってコントラクト自体またはそのコントラクトを利用する他のアカウントと対話できるようになります。
他のアカウントからスマートコントラクトをインポートするには、 次の行を入力します。
import <コントラクト名> from <アドレス>
トランザクションは、 prepare
フェーズと execute
フェーズに分かれています。
prepare
フェーズは、署名するアカウントのAuthAccount
オブジェクトにアクセスできる唯一の場所です。AuthAccount
には、/storage/
と/private/
の読み書きと、/storage/
内のオブジェクトへのリンクを/public/
に作成するための特別なメソッドがあります。execute
フェーズでは、prepare
フェーズで取得したオブジェクトを変更したり、 外部のコントラクトやオブジェクトに対して関数を呼び出したりできます。
execute
フェーズでアカウントストレージにアクセスできないようにすることで、あるトランザクションが、署名したアカウントのどのアセットと領域を変更できるか、静的に検証できます。これにより、ユーザーにトランザクションを送信させるブラウザウォレットやアプリケーションは、トランザクションが変更する可能性のあるものを表示できます。そしてユーザーは、アプリケーションが生成したトランザクションが、悪意のある、または危険なものでないことを確認できます。なぜこれが重要なのかについては、FAQ で例を見ることができます。
プレイグラウンドで、複数のアカウントのアバターをクリックと、複数の署名者を設定できます。ただし、トランザクションの prepare
の引数のパラメータ数と、署名者の数は同じである必要があります。そうしないとエラーになります。
このトランザクションでは、デプロイされたアドレスからコントラクトをインポートし、 hello
関数を呼び出します。
エディタの右下のボックスで、トランザクションの署名者としてアカウント
0x01
を選択します。
[Send] ボタンをクリックしてトランザクションを送信します。
下記のように表示されるはずです。
"Hello, World!"
おめでとう!Cadence の最初のトランザクションを実行しました!💯
リソースを作る
次に、Cadence の定義の一つであるリソースを使った例を実行してみましょう。リソースは構造体やクラスのような複合型ですが、いくつかの特別なルールがあります。
アカウント
0x02
タブでHelloWorldResource.cdc
というファイルを開きます。HelloWorldResource.cdc
には下記のコードが含まれます。
[Deploy] ボタンをクリックして、アカウント
0x02
にこのコードをデプロイします。
リソースは、構造体やクラスに似た複合型で、その中に任意の数のフィールドや関数を持つことができます。違いは、コードがそれらとどうインタラクションできるかです。リソースは、直感的に所有権をモデル化したい場合に有用です。リソースの各インスタンスは、常に 1 つの場所に存在し、コピーはできません。アクセスする時は、明示的に移動しなければならならず、誤って紛失することはありません。(訳注:このようなコードを書くとエラーになります。)
従来のプログラミング言語の構造体は、コピーが可能なため、所有権を表現するのに理想的ではありません。これは、コーディングミスによって同じアセットの複数のコピーが容易に生じ、アセットが真の価値を持つために必要な希少性の要件を壊してしまうことを意味します。損失や盗難については、家、車、数百万ドルの銀行口座の規模で考えなければなりません。一方で、リソースは、アセットの生成、破壊、移動を明示的にすることで、この問題を解決します。
init() {
// …
この例では、リソースの関数として init()
を宣言しています。コントラクト、リソース、構造体などすべての複合型には、オプションの init()
関数を書くことができます。これはオブジェクト生成時に 1 度だけ実行されます。Cadence は、すべてのフィールドを明示的に初期化する必要があるため、オブジェクトにフィールドがある場合は、この関数を使ってフィールドを初期化しなければなりません。
また、コントラクトは、ビルトインされている self.account
オブジェクトを使って、デプロイ先アカウントのストレージを読み書きできます。これは AuthAccount
オブジェクトであり、アカウントの private ストレージを操作できる様々な関数が用意されています。
このコントラクトの init()
関数では、 create
キーワードを使用して HelloAsset
型のインスタンスを作成し、それをローカル変数に保存しています。新しいリソースオブジェクトを生成するには、 create
キーワードの後に、init()
で定義した任意の引数を指定してリソース名を書きます。リソースは、定義されているスコープの中でのみ生成できます。これにより、他の人が定義したリソースのオブジェクトを誰でも生成することはできなくなります。
let newHello <- create HelloAsset()
ここでは <-
という記号を使っています。これは移動演算子です。移動演算子 <-
は、リソースの場合に、代入演算子 =
の代わりになります。これはリソースの代入を明示的にするためであり、次のような場合には、この演算子を使わなければなりません。
- リソースが、定数・変数の初期値である場合
- リソースが、代入で別の変数に移動する場合
- リソースが、引数として関数に移動する場合
- リソースが、関数から返される場合
リソースが移動されると、そのオブジェクトは古い場所では無効となり、新しい場所のコンテキストに移動します。リソースの通常の代入(値のコピー)は許可されません。リソースは一度に一つの場所にしか存在できないので、移動はコードの中で明示的に示す必要があります。
その後、リソースをアカウントストレージに保存するためには AuthAccount.save()
関数を使います。
self.account.save(<- newHello, to: /storage/Hello)
コントラクトは、 self
キーワードを使って、自身のメンバ関数とフィールドを参照できます。すべてのコントラクトは、デプロイされているアカウントのストレージにアクセスできます。self.account
を使うことで AuthAccount
オブジェクトにアクセスできます。
AuthAccount
オブジェクトには、アカウントのストレージを操作するための多くの関数があります。これらの関数の説明は、言語リファレンスの Storage のセクション、または、用語集を参照してください。 save()
関数は、オブジェクトをアカウントストレージに保存します。<>
に囲まれた type
パラメータは、保存されたオブジェクトがどの型であるかを示します。また、引数の型から推測することもできます。
save()
関数の第 1 引数は保存するオブジェクトで、2番目の to
パラメータはオブジェクトが保存されるパスです。パスは storage のパスでなければなりません。つまり、 /storage/
ドメインのみが許されます。
与えられたパスにすでにオブジェクトが保存されている場合、プログラムは停止(abort)します。Cadence の型システムは、リソースが誤って失われないようになっています。リソースを、フィールド、配列、ディクショナリ、ストレージに移動させる場合、その場所にすでにリソースが存在する可能性があります。Cadence は、既存のリソースが上書きによって誤って失われることがないように、開発者に既存のリソースがある場合の処理を記述することを強制します。newHello()
関数で、何もしないのにエラーになる理由や、指定したパスにすでにリソースがある場合に save()
が失敗する理由もこれです。
今回の場合、これは最初に実行するトランザクションなので、/sotrage/Hello
は空だと私たちは知っていますが、実際のアプリケーションでは、誤って上書きしてトランザクションを中断してしまわないように、保存する場所のチェックや必要なアクションを実行することが多いでしょう。
アカウントにリソースを保存すると、エディタの下の [STORAGE] ボックスにそのリソースが表示されます。このボックスには、選択したアカウントに保存されているリソースと、それらのリソース内のフィールドの値が表示されます。いま、HelloAsset
リソースはアカウント 0x02
のストレージに保存されており、name
というデフォルトのフィールドを持っていることがわかります。
リソースと対話する
Transaction2.cdc
という名前のトランザクションを開きます。Transaction2.cdc
には下記のコードが含まれます。
このトランザクションは、先ほどデプロイしたアカウントから HelloWorld
定義をインポートし、ストレージに保存してある HelloAsset
リソースの hello()
関数を呼び出します。
ストレージからオブジェクトを取り出すには、 load
関数を使用します。
let helloResource <- acct.load<@HelloWorld.HelloAsset>(from: /storage/Hello)
指定されたパスにオブジェクトが保存されていない場合、この関数は nil を返します。この関数が実行されると、指定されたパスにはオブジェクトは存在しなくなります。
<>
内にはオブジェクトの型を表す type パラメータを指定します。これは明示的に指定する必要があり、ここでは HelloWorld.HelloAsset
です。
from
は、/storage
ドメインのパスである必要があります。
次に、hello()
関数を呼び出して、その結果をログに記録します。
log(helloResource?.hello())
ストレージの値は Optional として返されるため、?
を使います。Optional は、オブジェクトが存在しない場合を表現できる型です。Optional には、指定された型のオブジェクトがある場合、何もない場合(nil)の 2 つのケースがあります。Optional 型は、?
という接尾辞を使って宣言されます。
let newResource: HelloAsset? // `HelloAsset` 型の値の場合もある
// もしくは `nil` の場合もある
Optional を使うことで、開発者はより優雅に nil のケースを考慮できます。ここでは、load()
で取得した helloResource
オブジェクトが nil である可能性を明示的に考慮しなければなりません。 ?
を使うと、 hello()
を呼び出す前に Optional を “unwrap”(オブジェクトがあればそれを取り出す)します。hello()
関数の呼び出し時に ?
を使うと、取り出したリソースが nil でない場合のみ関数が呼び出されます。
次に、 save()
を再び使い、オブジェクトをストレージの同じ場所に戻します。
acct.save(<- helloResource!, to: /storage/Hello)
helloResource
はまだ Optional なので、nil である可能性を処理しなければいけません。ここでは、force-unwrap 演算子(!
)を使用します。この演算子は、Optional にオブジェクトが含まれている場合はそのオブジェクトを取得し、nil の場合はトランザクション全体を中止します。これは Optional を扱う、よりリスクが高いやり方ですが、もしあなたのプログラムが、値が nil のときトランザクション全体の目的が損なわれるのなら、force-unwrap 演算子はその対処に良い選択です。
Optional とその使い方の詳細は、Optionals In Cadence を参照してください。
署名者にアカウント
0x02
を選択します(この一人だけにする)。
[Send] ボタンをクリックしてトランザクションを送信します。
このような結果が表示されます。
"Hello, World!"
リソースの参照を作る
この例では、 HelloAsset
リソースオブジェクトへのリンクと参照を作り、その参照を使って hello()
関数を呼び出します。このトランザクションで何が起こっているかの詳細な説明は、トランザクションのコードの下にあります。
Transaction3.cdc
という名前のトランザクションを開きます・Transaction3.cdc
には下記のコードが含まれます。
アカウント
0x02
をトランザクションの署名者として選択します。
[Send] ボタンをクリックしてトランザクションを送信します。
コンソールに "Hello, World!"
が再び表示されるはずです。これは、 HelloAsset
オブジェクト用の capability(オブジェクトへのリンク。説明は後述)を作り、その capability を /public/Hello
に格納し、capability から参照を借りて、その参照を使ってオブジェクトの hello()
関数を呼び出したためです。
このトランザクションで何が起こっているのかを詳しくみてみましょう。
まず、 /storage/
にあるプライベートな HelloAsset
オブジェクトにリンクされた capability を作ります。
let capability = account.link<&HelloWorld.HelloAsset>(/public/Hello, target: /storage/Hello)
HelloAsset
オブジェクトは /storage/Hello
に格納されており、アカウントの所有者のみがアクセスできます。所有者は他の人に hello()
関数を呼ばせたいですが、実際の HelloAsset
オブジェクトへの完全なアクセス権は必ずしも必要ではありません。capability はこのためにあります。
capability は、他の言語でいうポインタのようなものです。これはアカウントストレージのオブジェクトへのリンクです。これはオブジェクトへの参照を取得するために使われ、参照したオブジェクトのフィールドを読んだり関数を呼び出したりするために使えますが、オブジェクトを直接コピー、移動、変更することはできません。
AuthAccount.link()
関数を使うことで、ストレージ内のオブジェクトにリンクした新しい capability を作ります。 <>
内の型は、capability が表す制限付きで参照される型です。これは &
記号をつけることで表現できます。ここでは、capability が HelloAsset
オブジェクトを参照するので、型として <&HelloWorld.HelloAsset>
を指定します。関数の第 1 引数は capability を格納するパスで、引数 target
はリンク先となるストレージ内のオブジェクトのパスです。
capability からオブジェクトへの参照を取得するには、capability の borrow()
関数を使用します。
let helloReference = capability!.borrow()
この関数は、 link()
関数にて <>
で指定した型で参照を作成します。ここでは、capability が Optional であるため、force-unwrap 演算子(!
)を使用しています。capability が nil の場合、トランザクションは中止されます。また、対象となるストレージの場所が空であったり、既に借りられていたり、要求された型が capability で許可されている値を超えていたりした場合も、nil を返します。
このプロセスを capability と参照に分けたのは、悪意のある人がオブジェクトを何度も呼び出せる Reentrancy バグから保護するためです。このようなバグは、スマートコントラクトの言語を悩ませてきました。オブジェクトへの参照は一度にひとつしか存在できないので、この種の脆弱性はストレージのオブジェクトには存在しません。
さらに、オブジェクトの所有者は、元になっているオブジェクトを移動することで、作成した capability を効率的に無効化できます。参照されているオブジェクトが移動されると、それにリンクされている capability は無効となります。
これで、誰でもあなたの HelloAsset
オブジェクトの hello()
関数を呼び出せるようになります。
では最後に、借りてきた参照を使って hello()
関数を呼び出します。
// HelloAsset リソースの参照を使って、hello() 関数を呼び出すlog(helloReference?.hello())
スクリプトを実行する
スクリプトは、ブロックチェーンへの書き込みはできず、アカウントの状態の読み取りしかできません。Cadence の非常にシンプルなトランザクションの一種です。どのアカウントからも権限不要で実行できます。
スクリプトを実行するには、 pub fun main()
という関数を書きます。スクリプトの [Execute] ボタンをクリックするとスクリプトが実行されます。スクリプトの結果はコンソールに出力されます。
Script1.cdc
というファイルを開きます。Script1.cdc
には下記が含まれます。
このスクリプトはまず、getAccount()
で PublicAccount
オブジェクトを取得します。
let helloAccount = getAccount(0x02)
PublicAccount
オブジェクトは、ネットワーク内の誰でもすべてのアカウントに対して利用可能ですが、アカウントの /public/
ドメインから読み取れる一部の機能にしかアクセスできません。
次に、Transaction3.cdc
の中で作成された capability を取得します。
// 所有者のアカウントの public パスから、 public な capability を取得する
let helloCapability = helloAccount.getCapability(/public/Hello)
アカウントに保存されている capability を取得するには、 account.getCapability()
関数を使います。この関数は AuthAccount
と PublicAccount
で利用可能で、指定されたパスにある capability を返します。ターゲットが存在するかどうかはチェックしませんが、capability が無効な場合は取得処理が失敗します。
その後、スクリプトは capability から参照を借ります。借りる参照の型として &HelloWorld.HelloAsset
を指定します。
let helloReference = helloCapability!
.borrow<&HelloWorld.HelloAsset>()
そして、スクリプトは参照を使って hello()
関数を呼び出し、その結果を表示します。
スクリプトを実行して、正しく実行されることを確認しましょう。
プレイグラウンドの [Execute] ボタンをクリックします。
次のような結果が出力されます。
> "Hello, World!"
> Result > {"type":"Void"}
お疲れ様でした。最初の Cadence のスマートコントラクトをデプロイし、トランザクションとスクリプトを使って対話できました。
ここからは、プレイグラウンドの使い方について、いくつかのポイントを紹介します。
アカウント
プレイグラウンドを開くと、デフォルトのアカウントが、設定可能な数で初期化されます。
プレイグラウンドでは、画面左のセクションでそのアカウントのタブを選択することで、そのアカウントにデプロイされるコントラクトを編集できます。そのアカウントに対応するコントラクトがエディタに表示され、編集して、ブロックチェーンにデプロイできます。
トランザクション
コントラクトがデプロイされると、それと対話するためのトランザクションを送信できます。画面左側のトランザクション選択セクションでは、編集して送信する様々なトランザクションを選択できます。トランザクションが開いている間は、1 つまたは複数のアカウントを選択してトランザクションに署名できます。これは、Flow では複数のアカウントが同じトランザクションに署名することで、private ストレージへのアクセス権を与えられるようにするためです。複数のアカウントを署名者として選択する場合は、トランザクションコードの prepare()
関数の引数が、複数の署名者に対応している必要があります。
// 1人の署名者
transaction {
prepare(acct1: AuthAccount) {}
}
// 2人の署名者
transaction {
prepare(acct1: AuthAccount, acct2: AuthAccount) {}
}
より多くを試したい場合、新しいアカウントで、実行済みのいくつかのトランザクションを実行すると、いくつかの異なるインタラクションや、潜在的なエラーメッセージを探索することができます。
Flow で最初のスマートコントラクトを書いてローンチしたので、次はもっと複雑なものに挑戦してみましょう!