- Go back to TOP(インデックスページへ)
- Overview
- Implementing a Contract with Resources
- Reviewing the Resource Contract
Overview
💡 TIP
このチュートリアルのスターターコードを Flow Playground で開きます。
https://play.flow.com/ddf0177e-81c8-4512-ac2e-28036b1a3f89
チュートリアルでは、このコードとやり取りするためにさまざまな操作を行うよう求められます。
ACTION
ユーザーにアクションを要求する指示は、常にこの吹き出しボックス内に表示されます。ハイライトされたアクションは、コードを実行するために必要なすべてですが、言語の設計を理解するには、残りの部分を読むことも必要です。
このチュートリアルは、前回のHello World
チュートリアルを基にしています。このチュートリアルを開始する前に、次の内容を理解しておく必要があります。
- Accounts
- Transactions
- Signers
- Field Types
このチュートリアルではアカウントについて理解を深めつつ、Resourceを紹介し、これによってアカウントとどうやりとりするのかを習得していきます。
リソースは、Cadenceの最も重要な機能の1つです。
Cadenceでは、リソースは他の言語における構造体やクラスに似た複合型の一つですが、いくつかの特別なルールを持っています。
以下に、リソースの宣言例を示します。
access(all) resource Money {
access(all) let balance: Int
init() {
self.balance = 0
}
}
ご覧下さい、リソースは通常のstruct
定義みたいに見えます。その違いは動作内にあります。
Resourceは、アセット及びオブジェクトの直接所有(direct ownership)をモデル化したい場合に便利です。直接所有により、アクセスを可能にするパスワードや証明書の代わりにあなたの資産を表す実際のオブジェクトを所有できることを意味します。他のプログラミング言語の従来の構造体やクラスは、コピーが可能であるため、直接所有を表す理想的な方法にはなりません。コーディングエラーによって同じ資産の複数コピーが簡単に作成することが出来ると、資産の価値を持たせるための希少性を台無しにするからです。私たちは、家屋、自動車、数百万ドルの銀行口座、あるいは馬などの規模での損失や盗難を考慮しなければなりません。リソースはこの点で、資産を明示的に作成、破棄、移動させることでこの問題を解決させます。
このチュートリアルでは以下のことを行います。
- リソースを宣言したスマートコントラクトをデプロイします
- リソースをアカウントのストレージに保存します
- トランザクションを使用して作成したリソースに対しやりとりします
Implementing a Contract with Resources
リソースに対してやり取りするには、いくつかの重要な概念を学んでください。
-
create
キーワードの使用 -
<-
移動演算子 - アカウントストレージのAPI
まず、create
キーワードと<-
移動演算子を使ってどうやってリソースを作成していくかを見ていきましょう。
create
キーワードを使用してリソースを初期化します。リソースはそれを定義したスマートコントラクトの中でのみ作成できます。
<-
移動演算子は、リソースを変数に移動させるために使用します。リソースには代入演算子=
を使用できないため、リソースを初期化したり、新しい変数に代入する時には、<-
移動演算子を使用する必要があります。これは、リソースが文字通りある場所から別の場所に移動することを意味します。そのリソースを保持していた古い変数または場所は移動後、無効な値になります。これがCadenceがリソースを1場所にしか同時に存在させないことを保証する手段の1つです。
ACTION
ファイル名がHelloWorldResource.cdc
であるアカウント0x06
タブを開きます。
HelloWorldResource.cdc
には、以下のコードが含まれているはずです。
access(all) contract HelloWorld {
// Declare a resource that only includes one function.
access(all) resource HelloAsset {
// A transaction can call this function to get the "Hello, World!"
// message from the resource.
access(all) view fun hello(): String {
return "Hello, World!"
}
}
// We're going to use the built-in create function to create a new instance
// of the HelloAsset resource
access(all) fun createHelloAsset(): @HelloAsset {
return <-create HelloAsset()
}
}
ACTION
Deployボタンを使用して、このコードをアカウント0x06
にデプロイします。
まず私達は、0x06
アカウントに新しいHelloWorld
コントラクトを宣言します。このHelloWorld
コントラクトで私たちは以下をしています。
-
HelloAsset
リソースをパブリックスコープであるaccess(all)
で宣言します。 -
hello()
リソース関数をHelloAsset
の中に、パブリックスコープであるaccess(all)
で宣言します。 -
HelloAsset
リソースをcreate
するcreateHelloAsset()
コントラクト関数を宣言します。 -
createHelloAsset()
関数はリソースを関数が返すように、移動演算子(<-
)を使用します。
これは、スマートコントラクトで実行できることの一例です。Cadenceは、デプロイされたスマートコントラクト内で型の定義を宣言できます。(補足: Cadenceでは一つ一つのリソースが一つ一つの型になります。)型の定義は、特定のデータセットがどのように構成されているかを説明するものです。型の定義自体は、そのデータのコピーやインスタンスではありません。他のアカウントはこれらの定義をインポートし、それらの型のオブジェクトとやり取りすることができます。
私達が先ほどデプロイしたこのスマートコントラクトでは、HelloAsset
リソースの定義とリソースを作成する関数の宣言をしています。
このスマートコントラクトについて、リソース部分から詳しく見ていきましょう。リソースとは、Cadenceがスマートコントラクト設計に導入した最も重要なものの1つです。
access(all)
resource HelloAsset {
access(all)
view fun hello(): String {
return "Hello, World!"
}
}
Resources
resource
と、struct
またはclass
の主な違いは、リソースのアクセス範囲です。
- resourceの各インスタンスは、厳密に1つの場所にのみ存在することができ、コピーすることはできません。ここでいう場所とは、アカウントストレージ、関数内の一時変数、スマートコントラクト内のストレージ フィールドなどを指します。
- resourceは、アクセスされると、明示的に1つの場所から別の場所に移動されなければなりません。
- また、resourceは関数実行の終了時にスコープ外に出ることもできません。明示的にどこかに保存するか、明示的に破棄しなければなりません。
これらの特性により、コーディングのミスからリソースを誤って失うことが不可能になります。
resourceは、resourceが定義されたスコープ(補足: スマートコントラクト)内でのみ作成できます。
これにより、他のユーザーが定義したリソースオブジェクトを、誰でも任意の数だけ作成できることを防止できます。
The Move Operator (<-)
今回の例では、HelloAsset
リソースを作成できる関数を宣言しました。
access(all)
fun createHelloAsset(): @HelloAsset {
return <-create HelloAsset()
}
@
シンボルは、それがスマートコントラクト内で定義したHelloAsset
型のリソースであることを指し示しています。この関数では、HelloAsset
型のリソースを作成しそれを返すために、移動演算子を使用しています。新しいリソースオブジェクトを作成するためには、create
キーワードを使用します。
ここで、<-
記号を使用しています。これは移動演算子です。<-
移動演算子は、リソースにおける代入の代入演算子=
を置き換えます。リソースの代入である、と明示する必要があるため、以下の処理を行う時には<-
移動演算子を使用する必要があります。
- リソースを定数または変数の初期値とする時
- リソースが代入で別の変数に移動する時
- リソースが引数として関数に移動する時
- リソースが関数から返される時
リソースが移動すると、古い場所は無効となり、リソースオブジェクトは新しい場所のコンテキストに移動します。
例えば、変数first_resource
にリソースがあるとすると:
// Note the `@` symbol to specify that it is a resource
var first_resource: @AnyResource <- create AnyResource()
そして、それをsecond_resource
という新しい変数に代入したいとします。代入を行った後、first_resource
は無効となります。これは、リソースが新しい変数に移動したためです。
var second_resource <- first_resource
// first_resource is now invalid. Nothing can be done with it
リソースを通常の代入することは許可されていません。通常の代入は値をコピーするだけだからです。リソースは同時に1つの場所にしか存在できないため、移動は移動演算子<-
を使用してコード内で明示して示される必要があります。
Create Hello Transaction
では次に、createHelloAsset()
関数を実行し、HelloAsset
リソースをアカウントのストレージに保存するトランザクション処理を行います。
ACTION
Create Hello
という名前のトランザクションを開きます。
Create Hello
には以下のコードを含めます。
/// create_hello.cdc
/// This transaction calls the createHelloAsset() function from the contract
/// to create a resource, then saves the resource
/// in the signer's account storage using the "save" method.
import HelloWorld from 0x06
transaction {
/// `auth(SaveValue) &Account` signifies an account object
/// that has the `SaveValue` authorization entitlement, which means
/// that this transaction can't do anything with the &Account object
/// besides saving values to storage.
/// You will learn more about entitlements later
prepare(acct: auth(SaveValue) &Account) {
// Here we create a resource and move it to the variable newHello,
// then we save it in the signer's account storage
let newHello <- HelloWorld.createHelloAsset()
acct.storage.save(<-newHello, to: /storage/HelloAssetTutorial)
}
// In execute, we log a string to confirm that the transaction executed successfully.
execute {
log("Saved Hello Resource to account.")
}
}
このトランザクションの処理は以下の通りです。
-
HelloWorld
の定義をアカウント0x06
からインポートします。 -
createHelloAsset()
関数を使用してリソースを作成し、それをnewHello
に移動します。 - 作成したリソースを、トランザクションに署名したアカウントのストレージ(
/storage/HelloAssetTutorial
のパス)にsave
します。 - 「
Saved Hello Resource to account.
」というテキストをコンソールにログ出力します。
これは、prepare
フェーズを使用した最初のトランザクションです。prepare
フェーズとは、アカウント参照(&Account)を介して署名をしたアカウントにアクセスが出来る、唯一の場所です。アカウント参照は、アカウントとのやり取りに使用出来るさまざまなメソッドにアクセスすることが出来ます。一例として、アカウントのストレージがあります。上記の例では、トランザクションは auth(SaveValue) &Account
というものを使用しており、これはSaveValue
という認証権限(authorization entitlement)を意味しています。このトランザクションは、&Account
アカウント参照オブジェクトに対して、アカウントのストレージに値を保存することは出来るけど、それ以外の操作はできないということを意味しています。権限(Entitlement)については、後のチュートリアルで詳しく説明します。詳細については、権限に関するドキュメント(the entitlements documentation)を参照してください。
また、全アカウント権限(Entitlement)に関する可能な限りのドキュメントを、言語リファレンスのAccountセクションで見ることが出来ます。このチュートリアルでは、アカウントストレージの/storage/
への保存やそこからロードを行うアカウントの関数を使用していきます。
アカウントはオブジェクトをパス(paths)に保存します。パスは基本的にアカウントのファイルシステムを表し、ユーザー定義のパスを使って、どこにでもオブジェクトを保存することができます。多くの場合、スマートコントラクトはそのスマートコントラクトのオブジェクトをどこに保存されるかをユーザーに明示しています。これによって、コードは標準的な方法でこれらのオブジェクトにアクセスする方法を知ることが出来ます。
executeフェーズにおけるアカウントストレージへのアクセスを禁止していること、および権限(Entitlement)の使用によって、任意のトランザクションが署名者アカウントのどのアセットや領域/パス(areas/path)を変更できるかを静的に検証可能にします。 トランザクションの送信を行うブラウザウォレットやアプリケーションは、これを使用して、トランザクションが変更するであろう内容をユーザーに表示することができます。これにより、ウォレットがユーザーの実行するトランザクションに関する情報をユーザーに提供し、アプリケーションやウォレットから悪意のある危険なトランザクションが送信されることはないという確信を与えることができます。
トランザクションについて、もう少し詳しく見ていきましょう。HelloAsset
リソースを作成するために、スマートコントラクトのcreateHelloAsset()
関数にアクセスし、それによって作成されたリソースをnewHello
変数に移動させます。
let newHello <- HelloWorld.createHelloAsset()
次に、リソースをアカウントストレージに保存します。Flowでアカウントストレージとやり取りするには、アカウントストレージAPIを使用します。リソースを保存するには、アカウントストレージAPIのsave()メソッドを使用して、アカウントの/storage/HelloAssetTutorial
パスにリソースを保存します。
acct.storage.save(<-newHello, to: /storage/HelloAssetTutorial)
save
に対する最初のパラメータは、保存されるオブジェクトであり、to
パラメータはオブジェクトが保存されるパスです。パスはstorageパスでなければならず、to
パラメータではドメインは/storage/
のみが許可されます。
指定したパスにすでにオブジェクトが保存されている場合、プログラムは中止(abort)します。Cadenceの型システムにより、リソースが誤って失われることは決してないことを覚えておいてください。リソースをフィールド、配列、ディクショナリ、またはストレージに移動する場合、その場所にすでにリソースが含まれている可能性があります。Cadenceでは、既存のリソースを処理するように開発者を強制し、上書きによって誤って失われることがないようにしています。
また、あなたが識別子としてパス名をチョイスする時、プロジェクトに固有であるものを選ぶことが非常に重要です。現在、アカウントの保存パスはグローバルであるため、プロジェクトが同じ保存パスを使用する可能性があり、パスが競合する可能性があります。これは厄介な問題となる可能性があるため、このような問題を避けるために固有のパス名を選択してください。(補足: Cadenceではリソース宣言自体が一つの型であるため型自体は重複しないと考えられる)
最後に、executeフェーズでは、"Saved Hello Resource to account."
という文言をコンソールにログ出力します。
log("Saved Hello Resource to account.")
ACTION
0x06
アカウントを唯一の署名者として選択します。Send
ボタンをクリックして、トランザクションを送信します。
以下のような表示がされるはずです。
"Saved Hello Resource to account."
ACTION
newHello
をストレージに保存(save)するコード行の削除を試してもいいです。
newHello
のエラーが表示されるはずです。そのエラーには「loss of resource
」と表示されます。これは、リソースが適切に扱われていないことを意味します。プログラムでこのエラーが表示された場合、明示的に保存または破棄されていないリソースがどこかにあることを意味し、プログラムが不正であることを意味します。
トランザクションのチェックが適切になるようにするために、コード行を再追加します。
この場合、選択したアカウントで何かを保存するのは初めてなので、ストレージの/storage/HelloAssetTutorial
の場所は空であることが分かります。実際のアプリケーションでは、誤って上書きをしようとしてトランザクションを中止しないように、保存する場所のパスで、必要なチェックとアクションを実行するでしょう。
トランザクションを実行したので、アカウント0x06
のストレージには新たに作成されたHelloWorld.HelloAsset
リソースが格納されているはずです。これを確認するには、左下のアカウント0x06
をクリックします。これにより、アカウント内のさまざまなコントラクトとオブジェクトのビューが開きます。HelloWorld
コントラクトとHelloAsset
リソースのエントリが表示されるはずです。
Deployed Contracts:
[
{
"contract": "HelloWorld",
"height": 6
}
]
Account Storage:
{
"Private": null,
"Public": {},
"Storage": {
"HelloAssetTutorial": {
"Fields": [
39
],
"ResourceType": {
"Fields": [
{
"Identifier": "uuid",
"Type": {}
}
],
"Initializers": null,
"Location": {
"Address": "0x0000000000000005",
"Name": "HelloWorld",
"Type": "AddressLocation"
},
"QualifiedIdentifier": "HelloWorld.HelloAsset"
}
}
}
}
FlowToken
オブジェクトも表示されているのがわかるでしょう。すべてのアカウントは、FlowTokenアセットを使用する能力が自動的に初期化されています(補足: FlowTokenはトランザクションフィーに必要なので全てのアカウントは初期化時にこれを保持します)。現時点では、それらについて心配する必要はありません。
Load Hello Transaction
次に、トランザクションでHelloAsset
リソースのhello()
メソッドを実行します。
ACTION
Load Hello
という名前がついたトランザクションを開きます。
Load Hello
には、以下のコードが含まれているはずです。
import HelloWorld from 0x06
// This transaction calls the "hello" method on the HelloAsset object
// that is stored in the account's storage by removing that object
// from storage, calling the method, and then saving it back to the same storage path
transaction {
/// In this prepare block, we have to load a value from storage
/// in addition to saving it, so we also need the `LoadValue` entitlement
/// which additionally allows loading values from storage
prepare(acct: auth(LoadValue, SaveValue) &Account) {
// Load the resource from storage, specifying the type to load it as
// and the path where it is stored
let helloResource <- acct.storage.load<@HelloWorld.HelloAsset>(from: /storage/HelloAssetTutorial)
?? panic("The signer does not have the HelloAsset resource stored at /storage/HelloAssetTutorial. Run the `Create Hello` Transaction again to store the resource")
// log the hello world message
log(helloResource.hello())
// Put the resource back in storage at the same spot
acct.storage.save(<-helloResource, to: /storage/HelloAssetTutorial)
}
}
このトランザクションは以下の処理を行います。
-
HelloWorld
定義を0x06
アカウントからインポートします。 -
HelloAsset
オブジェクトを移動演算子とアカウントストレージAPIのload
関数を用いて、ストレージからhelloResource
に移動します。 -
helloResource
に保存されたHelloAsset
リソースのhello()
関数を呼び出し、結果をログに記録します。 - リソースを、私たちが移動させる前にあったアカウントのパス
/storage/HelloAssetTutorial
に保存(save)し直します
私達はprepare
フェーズを再び使用し、渡されるアカウントの参照を使ってリソースをloadします。
トランザクションについて、さらに詳しく見ていきましょう。
HelloAsset
リソースをストレージからロードします。
ストレージからオブジェクトを取り除くには、アカウントストレージAPIのload
メソッドを使用します。
let helloResource <- acct.storage.load<@HelloWorld.HelloAsset>(from: /storage/HelloAssetTutorial)
指定した型のオブジェクトがパスに存在する場合は、この関数はそのオブジェクトを返します。そして、アカウントストレージには、指定されたパス以下のオブジェクトは含まれなくなります。
ロードするオブジェクトの型の、型パラメータは<>
に含まれています。 ここで、私達は基本的に@HelloWorld.HelloAsset
のリソースオブジェクトをこのパスからロードするということを述べています。 パラメータにある型の引数は明示的に指定する必要があります。 (リソースであることを意味する@
記号に注目してください)
from
パスはストレージパスでなければなりません。そのため、/storage/
ドメインのみが許可されます。
指定した型のオブジェクトが指定したパスに存在しない場合、この関数は何も返さない、すなわちnil
を返します。これはオプショナル型といい、
オプショナル型とは、値の存在または非存在を表すことができる値です。オプショナル型には2つのケースがあります。指定した型の値があるか、または何も存在しない(nil
)かのどちらかです。オプショナル型は、?
接尾辞を用いて宣言します。
let newResource: HelloAsset? // could either have a value of type `HelloAsset`
// or it could have a value of `nil`, which represents nothing
オプショナル型により、開発者はnil
の場合をより適切に処理することができるようになります。ここで私達は、load
で取得したhelloResource
オブジェクトがnil
である可能性をはっきりと考慮する必要があります(なぜなら、load
はロードするものがない場合はnil
を返すからです)。
nil 結合演算子 (??
) を使用して、オプショナルを「unwrap」します。これは基本的に私達は、load
メソッドがnil
を返した場合の対応を処理することを意味します。もしnil
が返ってきた時は、??
の後のコードブロックが実行されます。 ここでは、panic
を実行していまます。これはエラーメッセージとともにトランザクションの実行を中断します。
オプショナル型の詳細と使用方法については、「Optionals In Cadence」を参照してください。
コード内でもし何か問題が生じた時に、常にその詳細なエラーメッセージを開発者に提供することは、ユーザーや開発者にとって何が修正されるべきかを明白にする上で極めて重要です。
エラーメッセージには、可能であれば以下の内容を含めるべきです。
- スマートコントラクトから由来した場合は、コントラクト名と関数名
- 起きているリテラル エラー(the literal error)の説明
- エラーの原因となっている可能性がある理由の高レベルな説明
- エラーに直結している可能性があるメタデータまたは変数値
- 可能ならば修正方法の提案
私たちのエラーメッセージを見てもらえば分かるとおり、私たちは問題点を正確に説明しています。つまり、リソースがしかるべき保存パスに保存されていないことを説明しています。そして、エラーを修正するための解決策を提案しています。それは、「Create Hello」トランザクションを実行してリソースを保存することです。
Flow NFTのGitHubリポジトリのスマートコントラクトやトランザクションのエラーメッセージを参考に、詳細かつヘルプフルなエラーメッセージの例をチェックしてみてください。
hello()
関数を呼び出す
次に、hello()
関数を呼び出し、出力をログに出します。
log(helloResource.hello())
リソースを署名者のアカウントに再保存する
次に私達は、save
を再び使用して、オブジェクトを元のストレージの場所に戻します。
acct.storage.save(<-helloResource, to: /storage/HelloAssetTutorial)
ACTION
0x06
アカウントを唯一の署名者として選択します。Send
ボタンをクリックして、トランザクションを送信します。
以下のような表示がされるはずです。
"Hello, World!"
Reviewing the Resource Contract
このチュートリアルでは、アカウントストレージAPIを使用してトランザクションでリソースとのやり取りを行いながら、Cadenceのリソースの紹介を行いました。
あなたはすべてのスコープからアクセス可能なスマートコントラクトを実装しました。スマートコントラクトにはhello()
という「Hello, World!
」文字列を返す関数が実装されたリソースが宣言されてあり、また、リソースを作成できる関数を宣言しました。
次に、あなたはこのコントラクトをアカウントにデプロイし、スマートコントラクトの中でリソースを生成するトランザクションを作成し、そのリソースをトランザクションの署名者としていた0x06
アカウントの中に保存しました。
最後に、トランザクションを使用してHelloAsset
リソースをアカウントストレージから移動させ、hello
メソッドを呼び出し、アカウントストレージに再び戻しました。
今ここであなたはこのチュートリアルを完了しましたので、シンプルなCadenceプログラムを書くための以下の基本的知識が身に付きました。
- スマートコントラクトの中にリソースを実装する
- アカウントストレージAPIと
<-
移動演算子を使用して、リソースを保存、移動、そしてロードすること - トランザクションの
prepare
フェーズを使用して、アカウントストレージからリソースをロードする(取り出す)こと
異なるリソースを作成するためにスマートコントラクトを好きなように変更してください、自由に利用可能なアカウントストレージAPIを実験してください、そして自由にあなたのスマートコントラクトの違う関数を実行する新しいトランザクションを作成してください。リソースでできることについてさらに詳しく知りたい場合は、リソースリファレンスページをご覧ください。
翻訳元->https://cadence-lang.org/docs/tutorial/resources