Previous << 2 - 基礎編Part2
Next >> 4 - 基礎編Part4
TL;DR Flowブロックチェーンで何ができるのか?
Flowブロックチェーンのノードは世界中にあるのでビットコインやイーサリアムと同じように世界中で取引が可能です。
Crypto Kittiesというゲームを作った人が開発したブロックチェーンなのでゲームと相性が良く、自由にプログラミングができます。
Flowブロックチェーンのスマートコントラクトは、そんなプログラミングコードが常に条件によって世界中で動くようになってます。
世界中で動くので常に平等に処理されることに注目してください。アフリカでも南極でも日本でもどこでも平等に処理されます。
それを使うと何ができるのか?というと、誰でもそれにアクセスして変更処理を書き加えることができます。
世界中の誰もがスマートコントラクトに対してトランザクション(情報更新処理)を行えるので、システムを作った人だけができる処理、というのが探してもありません。
それだと、スマートコントラクトを作った人のスマートコントラクトを誰でもその情報を書き換えられるって事?
はい、そうです。もしコントラクト内にpublic(=access(all)
)な関数があり、それがスマートコントラクトの情報の変更をするものであれば、誰もがそれの実行をできます。費用は0.001円未満です。
それではアプリを作るのに当然困るので、ちゃんとした機能があります。それがResourceです。日本語で資産という意味になりますが、資産には名前や等級があるように、Resourceもまたそれらステータスを呼び出すメソッドがあります。また、Resourceの持ち主はそれを通してスマートコントラクト内部の関数を呼び出すことができます。
それを使えば、出来ないことはありません。では一緒にResourceを使ってアプリを作ってみましょう!
Why Flow
FLOW(または$FLOW)トークンは、Flowネットワークのネイティブ通貨です。開発者およびユーザーは、FLOWを使用してネットワーク上で取引(transact)を行うことができます。開発者は、ピアツーピア決済(他人同士の決済)、サービス料金徴収、または消費者向け特典(rewards)のために、FLOWを直接アプリに統合することができます。
ということでやっていきます、P2P(ピアツーピア)決済アプリ開発!
💡もし、エミュレータの起動方法やスマートコントラクトのデプロイについて操作に自信がない場合はこちらを参照してください。
Resource, Capability そして Entitlement
Resource
は一種のオブジェクトで、スマートコントラクトの中でだけ生成できます。それをアカウントのストレージに保存します。このアカウントのストレージにはアカウントが鍵を使うことによってのみアクセスできます。
これだけでは少し不便です。あなたがゲームデベロッパーだとして、このResource
オブジェクトのIDを知りたいと思います。それがアカウントの鍵によってしか取得できなければ、ゲーム進行が難しくなります。
そこで誰でもアクセスして良い機能(IDなどフィールドの読み取りを行う)をアカウントのPublicストレージにCapability
として公開・保存します。
一度アカウントのCapabilitiesのパス(/public/
の下)に保存されれば、誰でもその情報にアクセスすることが出来ます。
Entitlement
は、トランザクションでアクセスを可能にする機能を細かく設定することが出来ます。例えばchangeGreeting
関数に誰でもアクセス出来ては困る場合、greeting
フィールドにだけアクセスが可能なCapabilityを作成することができます。
スマートコントラクトにResource
とEntitlement
を記述する
Day1の時のスマートコントラクトは以下のようになっていました。
access(all) contract HelloWorld {
access(all) let greeting: String
init() {
self.greeting = "Hello, World!"
}
}
これに対して、リソースを実装したいので、以下のように修正します。(リソースを保存したアカウントは、リソース関数であるchangeGreeting
関数を呼び出したり、リソースのgreeting
フィールドを読み取ることが出来ます)
access(all) contract HelloWorld {
access(all) entitlement ChangeGreeting
access(all) resource Greeting {
access(all) var greeting: String
access(ChangeGreeting) fun changeGreeting(newGreeting: String) {
self.greeting = newGreeting
}
init(greeting: String) {
self.greeting = greeting
}
}
access(all) fun createGreeting(greeting: String): @Greeting {
return <- create Greeting(greeting: greeting)
}
}
変更箇所は以下です。
- ChangeGreetingという
entitlement
を作成する - Greetingという
resource
を作成し、その中にHelloWorldコントラクトの中のフィールドやinit関数を移動する - greetingフィールドは変更可能である必要があるので、
let
からvar
に変更する - changeGreeting関数をChangeGreeting
entitlement
のアクセス権で新規に作成する - HelloWorldコントラクトにcreateGreetingという
resource
をcreate
する関数を作成する
変更したスマートコントラクトをデプロイする
エミュレータが起動している状態で、以下を実行します。
flow project deploy
Output:
Deploying 1 contracts for accounts: emulator-account
HelloWorld -> 0xf8d6e0586b0a20c7 (6acbe99c6fb968760219224e3a81d33f9e988ccee02864396f6386f0fca3799c)
🎉 All contracts deployed successfully
resource
を作成して、アカウントのストレージに保存する
HelloWorld.cdc
ファイルと同じ階層にstore_resource.cdc
ファイルを作成します。
以下のコードをstore_resource.cdc
に記載します。
import "HelloWorld"
transaction(greeting: String) {
prepare(signer: auth(Storage) &Account) {
signer.storage.save(<- HelloWorld.createGreeting(greeting: greeting), to: /storage/MyGreeting)
}
}
これを実行すると、Greetingリソースがアカウントのストレージに保存されます。
Flow CLIにはトランザクションを送信してくれるコマンドが用意されているので、それを使用してトランザクションを実行しました。
flow transactions send ./store_resource.cdc "Hello"
Output:
Transaction ID: b2ee6b02e41da2ec96d4930894324fc924b546e09b79af7411d109503e555947
Block ID 6ac4f5bdd49b29c8f73081db0d1104a8657c9b4815d7282bff4ba5c686cac42f
Block Height 2
Status ✅ SEALED
ID b2ee6b02e41da2ec96d4930894324fc924b546e09b79af7411d109503e555947
Payer f8d6e0586b0a20c7
Authorizers [f8d6e0586b0a20c7]
Proposal Key:
Address f8d6e0586b0a20c7
Index 0
Sequence 1
Capability
を保存するコードを追加する
このままではScripts
でgreetingリソースの情報にアクセスできないので、Capability
を発行し、ストレージの/public/
パスの下に保存します。
store_resource.cdc
のコードにCapabilityを保存するコードを追加します。
import "HelloWorld"
transaction(greeting: String) {
prepare(signer: auth(Storage, Capabilities) &Account) {
signer.storage.save(<- HelloWorld.createGreeting(greeting: greeting), to: /storage/MyGreeting)
let cap = signer.capabilities.storage.issue<&HelloWorld.Greeting>(/storage/MyGreeting)
signer.capabilities.publish(cap, at: /public/MyGreeting)
}
}
Cadenceではすべてのリソースは型を持っており、<&HelloWorld.Greeting>
の部分はその型を表しています。つまり、/public/MyGreeting
にこれから保存されるCapabilityはHelloWorld.Greeting
の型を持ちます。(スマートコントラクトが異なれば同名であっても型は異なります)
この状態で再度以下のコマンドを実行します。すると、既に同じパスにリソースが保存されている内容のエラーが表示されます。リソースをストレージのパスに対して上書きして保存できないためです。最初からリソースをストレージに保存する処理とCapabilityを公開する処理を書いて同時に説明すればよかったのですが、新しい概念を2つ同時に説明する文章力がありませんでした。
一度リソースを取り出せば同じパスに保存することができますが、リソースはソースコード内に放置して消滅させることができません。ストレージに保存するかdestroy
キーワードを使用して明示的に破棄する必要があります。そうしないとトランザクションが失敗します。
flow transactions send ./store_resource.cdc "Hello"
Output:
Transaction ID: ede2d376847f97fded05dcc3db7d2e019900515ca186a1401fba487e7355e3c7
Block ID 19761dfc060fafa98273d2ec9dfc0276f22fcdfb163101cf42a5bad3f8f60b6d
Block Height 3
❌ Transaction Error
[Error Code: 1101] error caused by: 1 error occurred:
* transaction execute failed: [Error Code: 1101] cadence runtime error: Execution failed:
error: failed to save object: path /storage/MyGreeting in account 0xf8d6e0586b0a20c7 already stores an object
--> ede2d376847f97fded05dcc3db7d2e019900515ca186a1401fba487e7355e3c7:5:4
|
5 | signer.storage.save(<- HelloWorld.createGreeting(greeting: greeting), to: /storage/MyGreeting)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ストレージに保存されたリソースを削除するには新しいトランザクションを作成する必要があるので、一度エミュレータを止めて、エミュレータ起動とコントラクトデプロイをやり直します。(そうするとエミュレータアカウントのストレージに保存されたリソースは消えてなくなります)
そして再度flow transactions send ./store_resource.cdc "Hello"
コマンドを実行します。
Output:
Transaction ID: 4249eefed0b4e276ebf3eac8a36811f0b853e6912dcdabe22db98cadd80006a1
Block ID ca6eb712dae839370f4dafc2f80e34cdeebfb88c0d0f1e0d88ebc9362654c0dd
Block Height 2
Status ✅ SEALED
ID 4249eefed0b4e276ebf3eac8a36811f0b853e6912dcdabe22db98cadd80006a1
Payer f8d6e0586b0a20c7
Authorizers [f8d6e0586b0a20c7]
Proposal Key:
Address f8d6e0586b0a20c7
Index 0
Sequence 1
これでgreetingというresource
を/storage/MyGreeting
のパスに、リソースのCapabilityを/public/MyGreeting
パスに保存することが出来ました。
Capability
にアクセスしてフィールドの情報を取得する
Capabilityの中には、greetingというaccess(all)
アクセス権のフィールドと、access(ChangeGreeting)
アクセス権の関数がありますが、Capabilityをパスに保存する時にChangeGreeting Entitlement
を指定していませんので、関数の方には誰もアクセスできません。
Day1
で作成したread_greeting.cdc
のコードを以下のように変更します。
import "HelloWorld"
access(all) fun main(user: Address): String {
let greetingRef: &HelloWorld.Greeting =
getAccount(user).capabilities.borrow<&HelloWorld.Greeting>(/public/MyGreeting)
?? panic("This public capability does not exist.")
return greetingRef.greeting
}
&は参照であることを意味します。(C言語のポインタのようなもの)
以下コマンドをTerminalウィンドウで実行します。
flow scripts execute ./read_greeting.cdc "0xf8d6e0586b0a20c7"
Output:
% flow scripts execute ./read_greeting.cdc "0xf8d6e0586b0a20c7"
Result: "Hello"
"0xf8d6e0586b0a20c7"
はリソースを保存しているユーザーのアドレスです。Scripts
はmain
関数から始まりますが、大抵の場合、アドレスをこのmain
関数の引数にセットします。アドレスさえ分かればgetAccount(user).capabilities.borrow
でCapabilityの参照を取得できるからです。そのアドレスをCLIのコマンドに引数として渡しています。ちなみにgetAccount
はScripts限定(読み取り専用)です。Transactions
で実行するとエラーになります。これは私もDiscordの開発者チャンネルで聞いて知りましたので不明点はDiscordで直接聞いてみるといいでしょう。
アカウントの鍵を取得してchangeGreetingを呼び出すトランザクションを実行する
HelloWorld.cdc
ファイルと同じ階層にchange_greeting.cdc
ファイルを作成します。
以下のコードをchange_greeting.cdc
に記載します。
import "HelloWorld"
transaction(greeting: String) {
prepare(signer: auth(Storage, Capabilities) &Account) {
let g =
signer
.storage
.borrow<auth(HelloWorld.ChangeGreeting) &HelloWorld.Greeting>(from: /storage/MyGreeting)
?? panic("A resource does not exist here.")
g.changeGreeting(newGreeting: greeting)
}
}
auth()
の中に書かれているのはEntitlementです。デフォルトでStorage, Capabilities, Keys, Contractsに対するEntitlementが多数用意されています。ここでは横着してStorage
をセットしましたが、これらの名前のは権限が強いのでもっと弱い権限をセットするべきだと思います。Interaction Templatesなどでここの情報が表示されるはずなので、権限が強いEntitlementはお勧めされません。(Capabilities
も渡しているけど多分必要ないですね.. Entitlementに関しては新機能なので私も模索しながら実装していました。)
以下のコマンドをTerminalウィンドウで実行し、トランザクションを送信します。
flow transactions send ./change_greeting.cdc "Goodbye."
Output:
Transaction ID: f50441805a3c745e55620d8c6a9cf36ae8b0032fcb0bcb997a6a9899dfdf2504
Block ID 3d6534c78d88d01ec4c9e65dbf5bccd2c8026c20592d37d74fd78f22772aead1
Block Height 3
Status ✅ SEALED
ID f50441805a3c745e55620d8c6a9cf36ae8b0032fcb0bcb997a6a9899dfdf2504
Payer f8d6e0586b0a20c7
Authorizers [f8d6e0586b0a20c7]
Proposal Key:
Address f8d6e0586b0a20c7
Index 0
Sequence 2
トランザクションで変更されたリソースのstateを読み取る
再度以下コマンドをTerminalウィンドウで実行し、greetingのstateを読み取るScriptsを実行します。
flow scripts execute ./read_greeting.cdc "0xf8d6e0586b0a20c7"
Output:
% flow scripts execute ./read_greeting.cdc "0xf8d6e0586b0a20c7"
Result: "Goodbye."
リソースのgreetingフィールドのstateがGoodbye.
に変更されていることを確認できました。このリソースはアカウントのストレージに保存されているものなので、リソースの情報が変更されたことを確認できたことになります。Day2の冒頭で触れた戦士リソースであればHPを減らす処理などが先ほどのトランザクションとスマートコントラクトでできることになります。