Open10

Flowにふれる

hironowhironow

Cadenceの特徴: 原文ママ

  • Type safety and a strong static type system
  • Resource-oriented programming, a new paradigm that pairs linear types with object capabilities to create a secure and declarative model for digital ownership by ensuring that resources (and their associated assets) can only exist in one location at a time, cannot be copied, and cannot be accidentally lost or deleted
  • Built-in pre-conditions and post-conditions for functions and transactions
  • The utilization of capability-based security, which enforces access control by requiring that access to objects is restricted to only the owner and those who have a valid reference to the object
hironowhironow

最もシンプルな スマートコントラクトのDeploy → Transaction による Interact → Transaction への Sign のフロー:

pub contract HelloWorld {
    pub let greeting: String

    init() {
        self.greeting = "Hello, World!"
    }

    pub fun hello(): String {
        return self.greeting
    }
}

Accountsの特徴:

  • contract area: ここは数制限なし
  • account storage: private領域

Transactionsの特徴:

  • authorizes a transaction = access to the authorizers' private storage(account storage)
  • authorizers = signers とも
  • prepare と execute phase を持つ
import HelloWorld from 0x01

transaction {
  prepare(acct: AuthAccount) {}

  execute {
    log(HelloWorld.hello())
  }
}
hironowhironow

Resourceの前に…概念ワードの整理:

  • Accounts
  • Transactions
  • Signers
  • Field types

Resource = direct ownership の model

Resouceの定義から <- move operator を使ったsaveとload、prepare phaseでのResourceのaccount storageからの読み取りの一連のフロー:

使用方法:

  • <- の move operator と create によってResourceをinitializeする(移動したResouceの移動元はinvalidとなる)
  • @ で始まるsymbol は Resource の型であることを示す
pub contract HelloWorld {
    pub resource HelloAsset {
        pub fun hello(): String  {
            return "Hello World!"
        }
    }

    pub fun createHelloAsset(): @HelloAsset  {
        return <-create HelloAsset()
    }

    init() {
        log("Hello Asset")
    }
}

Resourcesの特徴: 原文ママ

  • Each instance of a resource can only exist in exactly one location and cannot be copied. Here, location refers to account storage, a temporary variable in a function, a storage field in a contract, etc.
  • Resources must be explicitly moved from one location to another when accessed.
  • Resources also cannot go out of scope at the end of function execution. They must be explicitly stored somewhere or destroyed.

Transactionで利用する:

  • prepare phaseは AuthAcount にアクセス可能な唯一の場所
  • save() のdomainとしては /storage/ のみが許可される
    • 現在このdomain内のpathはglobalなので、uniqueにすることが大事
    • 同じpathへ上書きはされないで abort される
import HelloWorld from 0x01

transaction {
    prepare(acct: AuthAccount) {
        let newHellow <- HelloWorld.createHelloAsset()

        acct.save(<-newHellow, to: /storage/HelloAssetTutorial)
    }

    execute{
        log("Saved Hellow Resource to account.")
    }
}

エラー対応:

  • loss of resource : どこかで定義されたResourceが明示的に stored or destroyed されていない = program = invalid である状態
  • Error failed to save object: path /storage/HelloAssetTutorial in account 0x0000000000000001 already stores an object すでにpathにResourceが存在する状態
import HelloWorld from 0x01

transaction {
    prepare(acct: AuthAccount){
        let helloResource <- acct.load<@HelloWorld.HelloAsset>(from: /storage/HelloAssetTutorial)

        log(helloResource?.hello())

        acct.save(<-helloResource!, to: /storage/HelloAssetTutorial)
    }
}

関連メモ:

  • acct.load では nil を返すことがある
    • 読み取り時の型違いはエラーになる
    • なので ?.hello() をcallしている
    • なので .!save() をcallしている
hironowhironow

Capabilityの前に…概念ワードの整理:

  • Account
  • Resource

ResourceのAccess Scopeを拡張するフロー:

Capabilityのユースケース: 原文ママ

  • For example, if you're working on an app that allows users to exchange tokens. While you definitely need to sign write access for a feature like withdrawing tokens from an account, your app should allow anybody to deposit tokens. After your user authenticates your app for the first time, you can create a capability that allows your app to withdraw tokens, this makes it more convenient to write transactions that can withdraw an account's tokens to spend or trade them.

Capabilityの特徴:

  • Resourceと相互作用する機能を実装する
import HelloWorld from 0x01

transaction {
  prepare(account: AuthAccount) {
    let capability = account.link<&HelloWorld.HelloAsset>(/public/HelloAssetTutorial, target: /storage/HelloAssetTutorial)

    let helloReference = capability!.borrow() ?? panic("Could not borrow a reference to the hello capability")

    log(helloReference.hello())
  }
}

関連メモ:

  • & 指定はCapabilityの参照しているHelloAssetの型を示す
  • borrow()!. でcallしている
  • ?? でlink結果がnilの場合はpanicしている
    • nilになる場合は、対象となるstorageが空、すでにborrow済み、指定した型がCapabilityで許可されているものでない場合、など
  • 出力は同じだが、これは HellowAsset への Capability が作られたことになる(ポインタのようなイメージであり、Public APIのようなイメージ)

コードメモ:

  • borrow() は参照を作るCapabilityのメソッド(借用の意味)
  • その参照はフィールドとメソッドのみにアクセス可能
  • link() は /private/ か /public/ に必ず作る
    • 少数ユーザが参照するなら private 、 ネットワークの全ユーザーなら public
  • コード終了時に hellowReference は失われる

機能詳細メモ:

  • 参照を分離している理由は reentrancy attacks を避けるため
    • reentrancy attacks: 悪意のある行為者がオブジェクトを複数回呼び出すことができる攻撃
hironowhironow

Script: blockchainに書き込みはできず、AccountやContractの読み取りだけができる

  • pub fun main()
import HelloWorld from 0x01

pub fun main() {
    let helloAccount = getAccount(0x01)

    let helloCapability = helloAccount.getCapability<&HelloWorld.HelloAsset>(/public/HelloAssetTutorial)

    let helloReference = helloCapability!.borrow() ?? panic("Could not borrow a reference to the hello capability")

    log(helloReference.hello())
}

関連メモ:

  • getAccount() はbuilt-in関数で返しているのは PublicAccount 、transactionの AuthAccount ではない
  • /public/ から明示的にAccountのResourceを利用できる
  • Capabilityからborrowするときには !.?? panic("") がここでも必要

以上でなにができるようになったか?: 原文ママ

  • Implement a resource in a smart contract
  • Create capabilities to grant access to resources in an account
  • Interact with resources using both signed transactions and scripts
hironowhironow

いよいよ Non-Fungible Tokens (NFTs)

deploy -> store -> transfer のフロー:

  • Cadenceは、NFTを Account の Resource として表現
  • Resource = 単一所有者 かつ 複製不可 = 安全
  • FlowのNFTは相互作用可能 = Flow上のNFT Token Standard を実装しているため ※production-ready は https://github.com/onflow/flow-nft より

前提となる情報

  • Resourceは int IDを持つ
  • ResourceでNFTを表現する
  • Flow上のユーザ同士だけの取引ではResource定義のメソッドを呼び出すことで、中央のNFT contractとのやり取りなしで行うことができる
hironowhironow
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)
    }
}

関連メモ:

  • このNFTは整数値IDとメタデータフィールドを持つResource
  • NFTは他のNFTを所有することもできる
    • 次のチュートリアルで…
  • init() ではID: 1をAccountに保存している

NFTの存在を確認する:

import BasicNFT from 0x01

transaction {
    prepare(acct: AuthAccount) {
        if acct.borrow<&BasicNFT.NFT>(from: /storage/BasicNFTPath) != nil {
            log("The token exists!")
        } else {
            log("No token found!")
        }
    }
}

関連メモ:

  • ここではNFT(Resource)から直接 borrow (借用)を実行しているので nil 以外が返ることの確認だけでよい
hironowhironow

基本的なTransfer:

  • 一番簡単な方法
    • 複数のアカウントがtransactionに署名することができ、同じtransactionに署名することで、それぞれの private storage にアクセスできるようになる
import BasicNFT from 0x01

transaction {
    prepare(signer1: AuthAccount, signer2: AuthAccount) {
        let nft <- signer1.load<@BasicNFT.NFT>(from: /storage/BasicNFTPath) ?? panic("Could not load NFT")

        signer2.save<@BasicNFT.NFT>(<-nft, to: /storage/BasicNFTPath)          
    }
}

関連メモ:

  • load と save で storage から 取り出し 保存 できる
  • Resouceとして扱っているので @<- が必要

今後のメモ:

  • これではNFTごとに異なるパスの記憶が必要
  • Transferに複数の署名が必要

→ あまりUXがよくない