Skip to main content

Command Palette

Search for a command to run...

CookStocker Smart Contract の作り方

Updated
4 min readView as Markdown
T
Founder & Architect of Blsqui. Building the cloud-native infrastructure layer that transforms standard video games into instant Web3 eSports platforms. Passionate about democratizing global revenue networks for the next generation of independent, AI-driven creators. Creating plug-and-play, no-code backend frameworks for Unity and Godot that integrate frictionless stablecoin microtransactions without client-side execution overhead.

Cadence言語はSwift言語に非常によく似ています (Cadenceは元Appleエンジニアによって開発されています)。Swift言語といっても、慣れたらJavascript と同じようにスラスラ書けますし、クセがありませんので開発方法を忘れることもありません。

今から、この画像にあるような冷蔵庫にある野菜保存室の管理アプリを作ります。(野菜を購入した日付をブロックチェーンで管理するアプリです)

では作りましょう。まずはスマートコントラクトからです。CookStocker.cdcというファイルをflow_blockchain/cadence/contractsフォルダの中に作ります。以下を入力しましょう。

access(all) contract CookStocker {

  access(self) let info: Info

  /* 構造体 */
  access(all) struct Info {

    access(contract) var data: {String: VegeData}

    /* setter */
    access(contract) fun set(id: String, add: [String], remove: [String]) {
      if let saved = self.data[id] {
        for name in add {
          saved.set(category: name, bought_at: getCurrentBlock().timestamp)
        }
        for name in remove {
          saved.unset(category: name)
        }
        // Save
        self.data[id] = saved
      } else {
        var data = VegeData()
        for name in add {
          data.set(category: name, bought_at: getCurrentBlock().timestamp)
        }
        for name in remove {
          data.unset(category: name)
        }
        self.data[id] = data
      }
    }

    access(contract) fun delete(id: String) {
      self.data[id] = nil
    }

    init() {
      self.data = {}
    }
  }

  /* Info内の構造体 */
  access(all) struct VegeData {

    access(contract) var data: {String: UFix64?}

    /* setter */
    access(contract) fun set(category: String, bought_at: UFix64) {
      self.data[category] = bought_at
    }

    access(contract) fun unset(category: String) {
      self.data[category] = nil
    }

    init() {
      self.data = {}
    }
  }

  // GET
  access(all) fun getCookStockerInfo(id: String): VegeData? {
    return self.info.data[id]
  }
  // PUT
  access(all) fun setCookStockerInfo(id: String, add: [String], remove: [String]) {
    self.info.set(id: id, add: add, remove: remove);
  }
  // DELETE
  access(all) fun deleteCookStockerInfo(id: String) {
    self.info.delete(id: id)
  }

  init() {
    self.info = Info()
  }
}

黄色と黄緑色で表示されているのは型です。addとremove引数が[String]となっていることからこれらは文字列の配列ということになります。紫色で表示されている nilとはnullのことです。(Cadenceではnullをnilと表現します。)

Swiftと文法がよく似ており、特に if let 文はSwift言語の特徴ですので、わからない所に出会ったら"Swift"と一緒に検索して調べるといいと思います。セミコロンはあってもなくてもいいです。

getCurrentBlock().timestamp は秒単位でブロックが生成される時間をエポック秒で取得します。(秒単位ですので、date関数に渡す時には後ろに’000’をつける必要があります)

このスマートコントラクトを blsqui.net のウェブサイトの左上にあるデプロイボタンでデプロイします。

スマートコントラクトに保存された情報を取得

まず、fcl (Flow Client Library)を用意する必要がありますので、インストールします。

npm install @onflow/fcl

このコマンドをTerminalで実行するとfclがJavaScriptで使えるようになります。Svelteだとフレームワーク設定が必要ないので簡単に開発に着手できます。

次に、以下のようなメソッドを用意します。(言語はJavascriptです) flow_blockchainフォルダにscripts.jsというファイルを新規作成して以下を入力します。

import { query } from "@onflow/fcl";

export const getInfo = async function (id) {
  const result = await query({
    cadence: `
    import CookStocker from 0x1234abcd5678efgh

    access(all) fun main(id: String): CookStocker.VegeData? {
        return CookStocker.getCookStockerInfo(id: id)
    }
    `,
    args: (arg, t) => [arg(id, t.String)],
  });
  return result;
};

(コードはこちらにあります。)

このJavascriptファイルは大体、flow_blockchainフォルダの下ぐらいに置きますがどこに配置しても構いません。

このメソッドはCookStockerスマートコントラクトに保存されている情報を取得するgetCookStockerInfoというメソッドを実行してその情報を返します。

あとはこのscripts.jsファイル内でexportされているgetInfoメソッドを呼び出すだけです。

import { config } from "@onflow/fcl";
import { getInfo } from '../../flow_blockchain/scripts';

config({
    'flow.network': 'testnet',
    'accessNode.api': 'https://rest-testnet.onflow.org',
});

const id = localStorage.getItem('ID') || Math.random().toString();
localStorage.setItem('ID', id);

setInterval(async () => {
    info = await getInfo(id);
    console.log(info); <- ここでブラウザのConsoleにログを出しています
}, 1500); (1秒おき以上だと無料で呼び出せます)

 {#each Object.keys(list) as name}
     <VegeCard name={name} pastDate={
         info ? new Date(parseInt(info.data[name]) * 1000).getTime() : null
     } />
 {/each}

最初はまだ情報がコントラクトに登録されていないのでnullがConsoleに出力されます。

スマートコントラクトに情報を保存する

スマートコントラクトのコード内に、ブロックチェーンに保存するメソッドと、保存データを削除するメソッドが書かれています。

スマートコントラクトにデータの保存を行うトランザクションは以下のように記述します。

import { mutate } from "@onflow/fcl";

  const txId = await mutate({
      cadence: `
          import CookStocker from 0xb576a3926d239682

          transaction(id: String, add: [String], remove: [String]) {
              prepare(signer: &Account) {
                  CookStocker.setCookStockerInfo(id: id, add: add, remove: remove)
              }
              execute {
                  log("success")
              }
          }
      `,
      args: (arg, t) => [
          arg(id.toString(), t.String),
          arg(add, t.Array(t.String)),
          arg(remove, t.Array(t.String)),
      ],

この下に承認関数が来るのですが、とりあえず省きます。

スマートコントラクトのsetCookStockerInfoメソッドを呼び出して引数に保存したい情報を渡しています。

全削除ボタンを押した時のトランザクションのメソッド

const txId = await mutate({
    cadence: `
        import CookStocker from 0xb576a3926d239682

        transaction(id: String) {
            prepare(signer: &Account) {
                CookStocker.deleteCookStockerInfo(id: id)
            }
            execute {
                log("success")
            }
        }
    `,
    args: (arg, t) => [arg(id.toString(), t.String)],

こちらのコードも承認関数を省いています。引数にはブロックチェーンに渡す値をセットしています。id.toString()となっている部分ですね。

idとはユーザー別に保存を管理するための識別子です。

スマートコントラクトの全削除を行うところのコードを見てみましょう。

    access(contract) fun delete(id: String) {
      self.data[id] = nil
    }

削除していますね。

もう一回スマートコントラクトの構造体のところを見てみましょう。

  access(self) let info: Info

  /* 構造体 */
  access(all) struct Info {

    access(contract) var data: {String: VegeData}

    /* setter */
    access(contract) fun set(id: String, add: [String], remove: [String]) {
      if let saved = self.data[id] {
        for name in add {
          saved.set(category: name, bought_at: getCurrentBlock().timestamp)
        }
        for name in remove {
          saved.unset(category: name)
        }
        // Save
        self.data[id] = saved
      } else {
        var data = VegeData()
        for name in add {
          data.set(category: name, bought_at: getCurrentBlock().timestamp)
        }
        for name in remove {
          data.unset(category: name)
        }
        self.data[id] = data
      }
    }

    access(contract) fun delete(id: String) {
      self.data[id] = nil
    }

    init() {
      self.data = {}
    }
  }

  /* Info内の構造体 */
  access(all) struct VegeData {

    access(contract) var data: {String: UFix64?}

    /* setter */
    access(contract) fun set(category: String, bought_at: UFix64) {
      self.data[category] = bought_at
    }

    access(contract) fun unset(category: String) {
      self.data[category] = nil
    }

    init() {
      self.data = {}
    }
  }

スマートコントラクトの構造体は複雑な構造のデータを保存することができます。(だから構造体。)

一番上に access(self) let info: Info というのが見えますね。これはスマートコントラクトにinfoというフィールドがあり、それはInfo型であることを示しています。

だからスマートコントラクトの中では

  access(all) fun getCookStockerInfo(id: String): VegeData? {
    return self.info.data[id]
  }

このようにして スマートコントラクトのinfoフィールドのデータにアクセスすることができます。

このコードの1行目の後ろをよくみるとVegeData?というのが見えますね。これはinfo.data が 以下のように定義されているためです。

  access(all) struct Info {

    access(contract) var data: {String: VegeData}

あとは簡単VegeDataという構造体を新しく作るだけです。

基本的に変数や配列以上に複雑な構造を作ろうとすると、それは全て構造体になります。構造体のフィールドがやはり変数や配列より複雑であればそれも構造体です。

慣れれば簡単、基本的な文法はSwiftというモダン言語と同じです。

今回は基本部分だけ。


この構造体の情報はスマートコントラクトを保存しているアカウント(つまりデプロイしているアカウント)に保存されます。

何に役立つかというと、消えない、世界で共通の情報として使うことができます。

構造体はスマートコントラクトをデプロイした人であれば、その情報を変えることは簡単なので、あくまでスマートコントラクトのロジックで必要な情報の格納先として使うのが正しい使い方だと思います。

だから、賞品やトロフィー🏆などは構造体内では扱いません。

35 views