sagantaf

メモレベルの技術記事を書くブログ。

Cadence リソースの扱い方

Cadence言語のリソースという概念の扱い方を整理。

目次

コントラクトを定義する

リソースを使うためには基本的に

  • リソースを定義する
  • リソースを生成する関数を定義する
  • デプロイ時の初期化処理を実装する

という流れで実装する

  • コードのイメージは以下:

    • 解説は後述
pub contract BasicNFT {
    // リソースを定義する
    pub resource NFT {
        pub let id: UInt64
        pub var metadata: {String: String}
        init(initID: UInt64) {
            self.id = initID
            self.metadata = {}
        }
    }
    // リソースを生成する関数を定義する
    pub fun createNFT(id: UInt64): @NFT {
        return <-create NFT(initID: id)
    }
    // デプロイ時の初期化処理を実装する
    init() {
        self.account.save<@NFT>(<-create NFT(initID: 1), to: /storage/BasicNFTPath)
    }
}

リソースを定義する

// リソースを定義する
pub resource NFT {
    // 必要なフィールドを定義する
    pub let id: UInt64
    pub var metadata: {String: String}

    // 定義したフィールドを初期化する
    init(initID: UInt64) {
        self.id = initID
        self.metadata = {}
    }
}
  • リソースが生成されるごとに実行される処理となる
  • 必ず全てのフィールドを初期化しなければならない
    • 試しに Playgound で self.metadataを削除するとエラーになる

リソースを生成する関数を定義する

// リソースを生成する関数を定義する
// `@`マークはそれがリソースオブジェクトであることを示している
// リソースは `create`と`<-`を使って生成できる
pub fun createNFT(id: UInt64): @NFT {
    return <-create NFT(initID: id)
}
  • この関数はトランザクションで NFT を生成するために利用される

    • コントラクト内で定義されたリソースはそのスコープ内でのみインスタンス化できる(create NFT(initID: id)を書ける)
      • 外部から自由にリソースを生成できないようにするためであり、言語仕様で守られていると言える
    • だから、外部からでも NFT を生成できるようにするために、本例のようにcreateNFTという関数でラップし、後述の Capability や borrow() を使ってアクセス制御する方法が取られる
  • <-は Move operater と呼ばれ、値の「コピー」ではなく「移動」であることを表現している

    • 以下のタイミングで利用する
      • リソースが初期化(インスタンス化)されるとき
      • リソースが別の変数に設定されるとき
      • リソースが関数の引数として渡されるとき
      • リソースが関数から返されるとき
  • 例えば以下のコードの場合、second_resource にリソースインスタンスが格納されたら、first_resource には何も入っていない状態になる

    • first_resourcenil になるわけではなく、そもそも first_resource は存在しないことになり、アクセスできなくなる
    var fist_resource: @HogeResource <- create AnyResource()
    var second_resource <- first_resource
    

デプロイ時の初期化処理を実装する

// デプロイ時の初期化処理を実装する
// デプロイしたアカウントのストレージに1つだけ NFT を保存している
init() {
    self.account.save<@NFT>(<-create NFT(initID: 1), to: /storage/BasicNFTPath)
}
  • コントラクトの init()関数に処理を記載すると、デプロイ時に一度だけ実行させることができる
  • 本例では NFT リソースを /storage/BasicNFTPath に ID:1 をつけて格納している
  • コントラクトはアカウントのストレージへの読み書きアクセスを self.account オブジェクトを使って実装できる
    • 本オブジェクトは AuthAccountオブジェクトと呼ばれ、アカウントのストレージを操作するための関数にアクセスできる権限を持っている

デプロイする

上記のコントラクトを 0x01 でデプロイすると以下の図の状態になる。

コントラクトをデプロイ

トランザクションでリソースを扱う

新たにリソースを生成する

  • デプロイしたコントラクトを使ってリソースの生成や移動をするには、トランザクションコードを実装する
  • 以下はリソースを新たに生成し、ストレージに保存する例
    • リソースは署名したアカウントのストレージに保存される
    • 本例では特に制限していないため 0x01 以外のアカウントでも自由にリソースを生成し保存できる(権限制御は後述)
import BasicNFT from 0x01

transaction {
    prepare(acct: AuthAccount){
        // 0x01 がデプロイしたコントラクト `BasicNFT` の`createNFT()`を使ってリソースを生成
        let newNFT <- BasicNFT.createNFT(id: 1)
        // このトランザクションを署名した AuthAccount の save メソッドを使ってストレージに生成した NFT を保存
        acct.save<@BasicNFT.NFT>(<-newNFT, to: /storage/BasicNFTPath2)
    }

    execute {
        log("Saved NFT")
    }
}
  • prepare について

    • prepare では、トランザクション秘密鍵で承認したアカウントのストレージにアクセスできる
    • AuthAccount オブジェクトを通じてそのアカウントに何をするかをコーディングする
    • 上記では例えば、acct.save を使って、newNFT というリソースを保存(移動)している
    • prepare では、capability が保存された /private//public/へのリンクを作ることもできる
      • capability には、そのアカウントに対して実行できる関数がコーディングされている
      • この capability を使って関数やリソースへのアクセス制限ができるようになっている
  • リソースの移動について

    • ここでは AuthAccount のメソッドを使ってリソースを移動している
    • 使い方は AuthAccount.save<T>(_ value: T, to: StoragePath)
    • 詳細は、account storage APIに記載されている
    • T はリソースオブジェクトの型を設定しないといけないため、ここでは newNFT の型、つまり @BasicNFT.NFT になる
    • 指定した移動先にすでにデータが存在していたり、newNFT にリソースが存在しない場合は、プログラムが止まるようになっている
    • そのためパスコンフリクトが置きないように、ユニークで明確なパス名を指定する必要がある
  • 0x02 をトランザクションを実行すると以下の図になる

コントラクトを使ってNFTを生成

アカウントに保存したリソースを取り出す

  • 最後に、保存したリソースを取り出す方法を確認する
    • ここでは 0x01 が署名して実行したとする
import BasicNFT from 0x01

transaction {
    prepare(acct: AuthAccount){
        // ストレージからリソースを読み込み、NFTResourceに移動する
        //  この時、オプショナル型で格納される
        let NFTResource <- acct.load<@BasicNFT.NFT>(from: /storage/BasicNFTPath)
        log(NFTResource?.id) // 値がloadできなかった場合は nil になる
        log(NFTResource?.metadata) // 値がloadできなかった場合は nil になる
        // アカウントストレージにリソースを再度saveしている
        //  この時、ストレージにはオプショナル型は入れられないため!でforce-unwrapしている
        //  nilの場合はforce-unwrapできないためpanicになりrevertする
        acct.save<@BasicNFT.NFT>(<-NFTResource!, to: /storage/BasicNFTPath)
    }
}
  • ストレージからリソースを取得した時には、その値は optional 型で return される

    • optional 型とは、元々の値の型と nil のどちらも保持した型を指す
    • そのため NFTResource を使うときはオプショナルチェイン(?)を使って optional 型であることを明示する必要がある
    • 明示しないとエラーになり、そもそもトランザクションが実行できない
    • 例えば、間違ったストレージパスを load 時に指定すると
      • log では nil が返ってくる
      • save では panic が発生し、処理が revert される
  • イメージ図

トランザクションでNFTを取り出し

  • ただし、情報を参照するだけであれば、通常はわざわざリソースを取り出すようなことはせず、スクリプトを使って署名せずに情報を参照できるようになっている
    • トランザクションにしてしまうとリソースの移動というリスクが伴う、ガス代がかかる、などのデメリットがある