📚

AstarチェーンにWasmコントラクト(ERC721)をデプロイする

2023/02/12に公開

web3に限らずいろんな領域で耳にするようになったWebAssembly(Wasm)ですが、現状Wasmを書くならRustが選択されることが多いでしょう。なんとなくRustの学習のハードルが高そうという理由で避けてきましたが、Astarが本格的にWasm推しになってきたり、全然関係ないところですがFlutterがWasm対応する方針を発表したりWasmの話題がなんだかんだ盛り上がってきそうな雰囲気なので一回触ってみたいなというのとWasmでスマートコントラクトの開発をする体験をしてみたかったので0からERC721のスマートコントラクトを実装しローカルのPolkadotノードにデプロイするところまでをやってみましたのでその備忘録です。

Astarチェーンについて

説明不要かもしれませんが日本初のL1のパブリックブロックチェーンです。AstarはPolkadotのパラチェーンの一つで、計3つのチェーンがあり一つはPolkadotネットワーク上にあるAstar、もう一つはPolkadotの試験的な試みを試す場のKusamaネットワーク上にShidenがあり、テストチェーンとしてShibuyaが用意されています。Polkadotはさまざまなチェーンをつなぐマルチチェーンという思想のもと作られたチェーンですがPolkadotについて知りたい方は公式を読んでみてください。

https://docs.astar.network/

https://polkadot.network/development/docs/

AstarはEVMが実装されているのでSolidityで書いたスマートコントラクトをデプロイできていましたが、ShidenとShibuyaにはWasmのランタイムが実装されているためWasmで書いたスマートコントラクトをデプロイできます。

また、AstarのチェーンはSubstrateという簡単に言うとブロックチェーンを作成するためのフレームワークを使用しており、理論上Wasmにコンパイルできる言語であればスマートコントラクトを実装することは可能ですがWasmで実装されたスマートコントラクトをデプロイ・実行するためのサンドボックス環境としてpallet-contractsと呼ばれるものが組み込まれており、このpallet-contractsのAPIとの互換性を保つ必要があります。

現在、pallet-contractsに特化したeDSLとしてRust製のink!とAssemblyScript製のask!の二つが用意されているため、どちらか好きな方で実装することになります。

https://use.ink/

https://ask-lang.github.io/ask-docs/

開発準備

Rust環境

最初はAssemblyScriptを使用したask!で実装してみようとしたのですが、ink!では動くのにask!だと動かないみたいなことがあったり、ink!の方がやはり対応が進んでいるようなので今回はink!を使用します。ink!はRust製のためRustの環境構築が必要です。

curl https://sh.rustup.rs -sSf | sh
source ~/.cargo/env
rustup default stable
rustup update
rustup update nightly
rustup target add wasm32-unknown-unknown --toolchain nightly

以下、執筆時のバージョン。(上記でインストールした場合、stableになってると思いますがstableのままでも大丈夫です。)

rustc --version
> rustc 1.69.0-nightly (658fad6c5 2023-02-03)

rustup --version
> rustup 1.25.2 (17db695f1 2023-02-01)

cargo-contract

CLIツールであるcargo-contractをインストールします。これはink!での開発をいい感じに支援してくれます。インストールするためにはコントラクトのWebAssemblyバイトコードを最適化するbinaryenというパッケージのインストールが必要なのでMacであればbrewでインストールします。

brew install binaryen

以下の依存関係も必要なためインストールします。

cargo install cargo-dylint dylint-link

全てインストールしたらcargo-contractをインストールします。

cargo install cargo-contract --force --locked

以下でインストール確認。

cargo contract --version
> cargo-contract-contract 2.0.0-rc-unknown-aarch64-apple-darwin

スマートコントラクトの実装

準備ができたので実装を進めていきます。今回はexampleで紹介されていたERC721の実装をしていきますが、完全に仕様に沿ったものではないのであくまで学習用として作成します。学習のために自前で実装していますが、SolidityでのOpenZeppelinのようなものでOpenBrushというものが用意されているので実際のプロダクトで実装するならこちらを使用することができます。なお、OpenBrushではPolkadotの仕様にあわせてpspという規格を採用しており、ERC20がpsp22、ERC721がpsp34という規格で用意されています。

プロジェクトの作成

以下のコマンドでプロジェクトの雛形を作成します。

cargo contract new <project名>

もしエラーが出る場合は以下のようにしてnightlyバージョンを使用するようにしてください。

cargo +nightly contract new <project名>
or
cargo default nightly

成功すると以下の2ファイルが作成されていると思います。

Cargo.toml
[package]
name = "demo_contract"
version = "0.1.0"
authors = ["[your_name] <[your_email]>"]
edition = "2021"

[dependencies]
ink = { version = "4.0.0-rc", default-features = false }

scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2.3", default-features = false, features = ["derive"], optional = true }

[lib]
path = "lib.rs"

[features]
default = ["std"]
std = [
    "ink/std",
    "scale/std",
    "scale-info/std",
]
ink-as-dependency = []
lib.rs
#![cfg_attr(not(feature = "std"), no_std)]

#[ink::contract]
mod demo_contract {

    /// Defines the storage of your contract.
    /// Add new fields to the below struct in order
    /// to add new static storage fields to your contract.
    #[ink(storage)]
    pub struct DemoContract {
        /// Stores a single `bool` value on the storage.
        value: bool,
    }

    impl DemoContract {
        /// Constructor that initializes the `bool` value to the given `init_value`.
        #[ink(constructor)]
        pub fn new(init_value: bool) -> Self {
            Self { value: init_value }
        }

        /// Constructor that initializes the `bool` value to `false`.
        ///
        /// Constructors can delegate to other constructors.
        #[ink(constructor)]
        pub fn default() -> Self {
            Self::new(Default::default())
        }

        /// A message that can be called on instantiated contracts.
        /// This one flips the value of the stored `bool` from `true`
        /// to `false` and vice versa.
        #[ink(message)]
        pub fn flip(&mut self) {
            self.value = !self.value;
        }

        /// Simply returns the current value of our `bool`.
        #[ink(message)]
        pub fn get(&self) -> bool {
            self.value
        }
    }

    /// Unit tests in Rust are normally defined within such a `#[cfg(test)]`
    /// module and test functions are marked with a `#[test]` attribute.
    /// The below code is technically just normal Rust code.
    #[cfg(test)]
    mod tests {
        /// Imports all the definitions from the outer scope so we can use them here.
        use super::*;

        /// We test if the default constructor does its job.
        #[ink::test]
        fn default_works() {
            let demo_contract = DemoContract::default();
            assert_eq!(demo_contract.get(), false);
        }

        /// We test a simple use case of our contract.
        #[ink::test]
        fn it_works() {
            let mut demo_contract = DemoContract::new(false);
            assert_eq!(demo_contract.get(), false);
            demo_contract.flip();
            assert_eq!(demo_contract.get(), true);
        }
    }
}

最初はflipperのコントラクトでbool値を更新するだけのコントラクトが用意されていますので一旦、こちらを使用し動作確認をしていきます。

// ビルド
cargo contract build

// テスト
cargo contract test

こちらももしエラーになった場合はnightlyバージョンの指定をして実行してください。ビルドが成功するとtarget配下にいろいろファイルが生成されていますがtarget/ink配下にある拡張子がcontractのファイルが生成されているのでデプロイ時にはこちらのファイルをデプロイすることになります。

ストレージの実装

ちなみにですが筆者は今回初めてRustを触っていますがUdemyで基礎文法を学んでからコントラクトの実装をしています。(サクッと基礎文法を学ぶならUdemyおすすめです。公式チュートリアルよりはハードルが低そうな体感です。)必須ではないですがRustの経験がなければ基礎文法を学んでからink!に入った方が理解が早いと思います。

とりあえず、flipperの実装は一旦削除して以下のように変更します。ink!では#[]で記述するマクロアノテーションを基本的には使用していきます。#![cfg_attr(not(feature = "std"), no_std)]の宣言はRustの標準ライブラリがなかったときにno_stdを使用するための宣言です。no_stdとは一般的なOSの上ではないときにRustを動作させたいときに使用されるようです。

コントラクトの宣言にはmodでモジュールを宣言した後に#[ink::contract]を記述します。

Solidityを書いたことがある方ならコントラクトでストレージにデータを保持するのにmappingを使用したことがあるかと思いますが、同じような感じでデータを保持するのにink::storage::Mappingを使用するためにインポートしておきます。
scaleは後述しますがコントラクトが実装したmessageの戻り値の型にはこのscale::Encodeを実装している必要があるためインポートしておきます。

符号なしの32bit整数をTokenIdとして型宣言しておきます。

ストレージはコントラクトの中に1つだけしか存在することはできず、構造体で定義します。

lib.rs
#![cfg_attr(not(feature = "std"), no_std)]

#[ink::contract]
mod erc721 {
  use ink::storage::Mapping;
  use scale::{Decode, Encode};

  pub type TokenId = u32; // TokenId

  // ストレージ定義
  #[ink(storage)]
  #[derive(Default)] // Default traitを実装
  pub struct Erc721 {
      token_owner: Mapping<TokenId, AccountId>,
      token_approvals: Mapping<TokenId, AccountId>,
      owned_tokens_count: Mapping<AccountId, u32>,
      operator_approvals: Mapping<(AccountId, AccountId), ()>,
  }
}

構造体のフィールドには

  • token_owner: keyがトークンIDで誰が所有しているかの情報を格納します。
  • token_approvals: keyがトークンIDでApprovedされているアカウントの情報を格納します。
  • owned_tokens_count: keyがアカウントアドレスでアカウントが持っているトークンの数を保持します。
  • operator_approvals: keyが2つのアカウントアドレスを持つタプルで誰が誰に対してapproveしているかの情報を格納します。

ちなみに、Rustの標準ライブラリにHashMapがありますがストレージのデータ型にはink::storage::Mappingを使用する必要がありますので注意してください。

エラー定義

エラーは以下のようなenumで定義します。

lib.rs
    // (略)

    // エラー定義
    #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Copy)] // いろいろtraitを実装
    #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
    pub enum Error {
        NotOwner,
        NotApproved,
        TokenExists,
        TokenNotFound,
        CannotInsert,
        CannotFetchValue,
        NotAllowed,
    }

いろいろtraitを実装してますがこのあと実装するmessageの戻り値でこのErrorを返しますが、messageの戻り値にはscale::Encodeを実装している必要があるためEncodeの実装は必要です。

TypeInfoの方はよくわからなかったのですが型情報を実行時に取得するためのもののようです。

イベントの定義

イベントの定義はストレージと同様構造体で定義します。構造体には#[ink(event)]を設定します。indexedを指定したいフィールドには#[ink(topic)]をつけることでindexedを指定することが可能です。

lib.rs
    // (略)

    #[ink(event)]
    pub struct Transfer {
        #[ink(topic)] // indexedを追加
        from: Option<AccountId>,
        #[ink(topic)]
        to: Option<AccountId>,
        #[ink(topic)]
        id: TokenId,
    }

    // (略)

コンストラクタの実装

コンストラクタや後述するメッセージを定義するために以下のようにして関数を定義していきます。コンストラクタの定義には#[ink(constractor)]を関数に指定することで定義できます。今回は構造体にDefaultトレイトを実装しているのでデフォルト値で構造体を作成し返すだけの実装にしています。コンストラクタは複数定義可能であり、引数を指定することも可能ですが必ず1つはコンストラクタの定義が必要になっています。

lib.rs
    // (略)
    // コントラクトの実装
    impl Erc721 {
        // コンストラクタ
        #[ink(constructor)]
        pub fn new() -> Self {
          Default::default()
        }

        // (略)
    }

メッセージの定義

外部に公開する関数は以下のようにpublicで宣言した関数に#[ink(message)]を指定します。この関数はいくらでも定義することは可能ですが最低でも1つは定義する必要があります。

lib.rs
        // (略)
        #[ink(message)]
        pub fn balance_of(&self, owner: AccountId) -> u32 {
            self.balance_of_or_zero(&owner)
        }
        // (略)

テスト

テストは通常のRustでのテストの書き方で基本的に書けるようになっていますがテスト関数には#[ink::test]を設定するようにします。ink::env::test::default_accounts::<ink::env::DefaultEnvironment>()でaliceやbobといったデフォルトのアカウントが取得できるのでこのアカウントを使用しテストを書くことができます。コントラクトはコンストラクタを呼び出してインスタンスを作成します。

lib.rs
    #[cfg(test)]
    mod tests {
        use super::*;

        #[ink::test]
        fn mint_works() {
            let accounts = ink::env::test::default_accounts::<ink::env::DefaultEnvironment>();
            let mut erc721 = Erc721::new();

            // まだトークンがmintされていないので所有者はいない
            assert_eq!(erc721.owner_of(1), None);
            // デフォルトユーザーでまだmintしていないのでトークンをもっていない
            assert_eq!(erc721.balance_of(accounts.alice), 0);
            // mint成功するはず
            assert_eq!(erc721.mint(), Ok(()));
            // mintしたのでトークンを所有しているはず
            assert_eq!(erc721.balance_of(accounts.alice), 1);
        }
    }

トークンURIを返す

NFTコントラクトを実装するならトークンURIを返すようにしたいなと思ったのでストレージにtokenIdを追加し、以下のような関数を作成してみた。

lib.rs
        const TOKEN_URI: &str = "https://example.com/";

        // 略

        #[ink(message)]
        pub fn token_uri(&self) -> String {
            String::from(TOKEN_URI) + &self.token_id.to_string()
        }

ただし、この実装だとコンパイルエラーになってしまう。前述したようにmessageの関数の戻り値にはscale::Encodeが実装されている必要があるからで、もしStringで返したい場合はink::prelude::stringを使用する。

lib.rs
        use ink::prelude::string::{String, ToString};

        // 略

        #[ink(message)]
        pub fn token_uri(&self) -> String {
            String::from(TOKEN_URI) + &ToString::to_string(&self.token_id)
        }
ここまでで実装したコードの全体図
lib.rs
// 標準ライブラリがなかったら標準ライブラリを使わない宣言
#![cfg_attr(not(feature = "std"), no_std)]

// Contract定義のエントリーポイント
#[ink::contract]
mod erc721 {
    use ink::prelude::string::{String, ToString};
    use ink::storage::Mapping; // inkからMapping structをimport.スマートコントラクト用に用意されているのでMapにはこれを使う。
    use scale::{Decode, Encode};

    pub type TokenId = u32; // TokenId

    // metadata.jsonのあるとこ
    const TOKEN_URI: &str = "https://example.com/";

    // ストレージ定義
    #[ink(storage)]
    #[derive(Default)] // Default traitを実装
    pub struct Erc721 {
        token_owner: Mapping<TokenId, AccountId>,
        token_approvals: Mapping<TokenId, AccountId>,
        owned_tokens_count: Mapping<AccountId, u32>,
        operator_approvals: Mapping<(AccountId, AccountId), ()>,
        token_id: TokenId,
    }

    // エラー定義
    #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Copy)] // いろいろtraitを実装
    #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
    pub enum Error {
        NotOwner,
        NotApproved,
        TokenExists,
        TokenNotFound,
        CannotInsert,
        CannotFetchValue,
        NotAllowed,
    }

    // イベント定義

    // トークンがTransferされたときのイベント
    #[ink(event)]
    pub struct Transfer {
        #[ink(topic)] // indexedを追加
        from: Option<AccountId>,
        #[ink(topic)]
        to: Option<AccountId>,
        #[ink(topic)]
        id: TokenId,
    }

    // 承認されたときのイベント
    #[ink(event)]
    pub struct Approval {
        #[ink(topic)]
        from: AccountId,
        #[ink(topic)]
        to: AccountId,
        #[ink(topic)]
        id: TokenId,
    }

    #[ink(event)]
    pub struct ApprovalForAll {
        #[ink(topic)]
        owner: AccountId,
        #[ink(topic)]
        operator: AccountId,
        approved: bool,
    }

    // コントラクトの実装
    impl Erc721 {
        // コンストラクタ
        #[ink(constructor)]
        pub fn new() -> Self {
            Erc721 {
                token_owner: Default::default(),
                token_approvals: Default::default(),
                owned_tokens_count: Default::default(),
                operator_approvals: Default::default(),
                token_id: 1, // 最初は1から
            }
        }

        // #[ink(message)]
        // 全てのパブリック関数はこの属性を使用する必要がある
        // 少なくとも一つの#[ink(message)]属性を持つ関数が定義されている必要がある
        // コントラクトと対話するための関数定義に使用

        // アカウントが持つトークンの数を返す
        #[ink(message)]
        pub fn balance_of(&self, owner: AccountId) -> u32 {
            self.balance_of_or_zero(&owner)
        }

        #[ink(message)]
        pub fn token_uri(&self) -> String {
            String::from(TOKEN_URI) + &ToString::to_string(&self.token_id)
        }

        // トークンの所有者を取得する
        #[ink(message)]
        pub fn owner_of(&self, id: TokenId) -> Option<AccountId> {
            self.token_owner.get(id)
        }

        // 承認済みのアカウントIDを取得する
        #[ink(message)]
        pub fn get_approved(&self, id: TokenId) -> Option<AccountId> {
            self.token_approvals.get(id)
        }

        // 指定のアカウント間で全てApproveされているかどうか
        #[ink(message)]
        pub fn is_approved_for_all(&self, owner: AccountId, operator: AccountId) -> bool {
            self.approved_for_all(owner, operator)
        }

        // 指定のアカウントに対しての全承認をセットする
        #[ink(message)]
        pub fn set_approval_for_all(&mut self, to: AccountId, approved: bool) -> Result<(), Error> {
            self.approve_for_all(to, approved)?;
            Ok(())
        }

        // 指定のアカウントがトークンに対しての操作をApproveする
        #[ink(message)]
        pub fn approve(&mut self, to: AccountId, id: TokenId) -> Result<(), Error> {
            self.approve_for(&to, id)?;
            Ok(())
        }

        // トークンを移送
        #[ink(message)]
        pub fn transfer(&mut self, destinaion: AccountId, id: TokenId) -> Result<(), Error> {
            let caller = self.env().caller();
            self.transfer_token_from(&caller, &destinaion, id)?;
            Ok(())
        }

        // トークンを指定のアカウントからアカウントへ移送
        #[ink(message)]
        pub fn transfer_from(
            &mut self,
            from: AccountId,
            to: AccountId,
            id: TokenId,
        ) -> Result<(), Error> {
            self.transfer_token_from(&from, &to, id)?;
            Ok(())
        }

        // mint
        #[ink(message)]
        pub fn mint(&mut self) -> Result<(), Error> {
            let caller = self.env().caller();
            let id = self.token_id;
            self.add_token_to(&caller, id)?;

            // イベント発火
            self.env().emit_event(Transfer {
                from: Some(AccountId::from([0x0; 32])),
                to: Some(caller),
                id,
            });

            // インクリメント
            self.token_id += 1;

            Ok(())
        }

        // burn
        #[ink(message)]
        pub fn burn(&mut self, id: TokenId) -> Result<(), Error> {
            let caller = self.env().caller();
            let Self {
                token_owner,
                owned_tokens_count,
                ..
            } = self;

            let owner = token_owner.get(id).ok_or(Error::TokenNotFound)?;
            if owner != caller {
                return Err(Error::NotOwner);
            }

            // トークン所持情報削除
            let count = owned_tokens_count
                .get(caller)
                .map(|c| c - 1)
                .ok_or(Error::CannotFetchValue)?;
            owned_tokens_count.insert(caller, &count);
            token_owner.remove(id);

            // イベント発火
            self.env().emit_event(Transfer {
                from: Some(caller),
                to: Some(AccountId::from([0x0; 32])),
                id,
            });

            Ok(())
        }

        fn transfer_token_from(
            &mut self,
            from: &AccountId,
            to: &AccountId,
            id: TokenId,
        ) -> Result<(), Error> {
            let caller = self.env().caller();

            if !self.exists(id) {
                return Err(Error::TokenNotFound);
            }

            if !self.approved_or_owner(Some(caller), id) {
                return Err(Error::NotApproved);
            }

            // Approval情報をクリア
            self.clear_approval(id);
            // トークンの所有情報を削除
            self.remove_token_from(from, id)?;
            // トークンの所有情報を追加
            self.add_token_to(to, id)?;

            // イベント発火
            self.env().emit_event(Transfer {
                from: Some(*from),
                to: Some(*to),
                id,
            });

            Ok(())
        }

        fn add_token_to(&mut self, to: &AccountId, id: TokenId) -> Result<(), Error> {
            let Self {
                token_owner,
                owned_tokens_count,
                ..
            } = self;

            // 既にトークン誰か持ってる
            if token_owner.contains(id) {
                return Err(Error::TokenExists);
            }

            // ゼロアドレス
            if *to == AccountId::from([0x0; 32]) {
                return Err(Error::NotAllowed);
            }

            let count = owned_tokens_count.get(to).map(|c| c + 1).unwrap_or(1);

            owned_tokens_count.insert(to, &count);
            token_owner.insert(id, to);

            Ok(())
        }

        fn clear_approval(&self, id: TokenId) {
            self.token_approvals.remove(id);
        }

        fn remove_token_from(&mut self, from: &AccountId, id: TokenId) -> Result<(), Error> {
            // 構造体からフィールドを取り出す
            let Self {
                token_owner,
                owned_tokens_count,
                ..
            } = self;

            // トークンがない
            if !token_owner.contains(id) {
                return Err(Error::TokenNotFound);
            }

            let count = owned_tokens_count
                .get(from) // トークンの所有数
                .map(|c| c - 1) // 1減らす
                .ok_or(Error::CannotFetchValue)?; // 見つからなかったらエラー返す

            // トークン所有数を更新
            owned_tokens_count.insert(from, &count);
            // トークン所有者を削除する
            token_owner.remove(id);

            Ok(())
        }

        // 指定のアドレスが所有者 または 指定のトークンに対してのApprovalがある または allでApprovalされてる
        fn approved_or_owner(&self, from: Option<AccountId>, id: TokenId) -> bool {
            let owner = self.owner_of(id);
            from != Some(AccountId::from([0x0; 32]))
                && (from == owner
                    || from == self.token_approvals.get(id)
                    || self.approved_for_all(
                        owner.expect("Error with AccountId"),
                        from.expect("Error with AccountId"),
                    ))
        }

        fn exists(&self, id: TokenId) -> bool {
            self.token_owner.contains(id)
        }

        fn approve_for(&mut self, to: &AccountId, id: TokenId) -> Result<(), Error> {
            // 呼び出しもと
            let caller = self.env().caller();
            // トークン所有者
            let owner = self.owner_of(id);

            // 呼び出しもとと所有者が同じまたは、既にApproveされてる
            if !(owner == Some(caller)
                || self.approved_for_all(owner.expect("Error with AccountId"), caller))
            {
                return Err(Error::NotAllowed);
            }

            // 0アドレス
            if *to == AccountId::from([0x0; 32]) {
                return Err(Error::NotAllowed);
            }

            // ストレージに追加
            if self.token_approvals.contains(id) {
                return Err(Error::CannotInsert);
            } else {
                self.token_approvals.insert(id, to);
            }

            // イベント発火
            self.env().emit_event(Approval {
                from: caller,
                to: *to,
                id,
            });

            Ok(())
        }

        fn approve_for_all(&mut self, to: AccountId, approved: bool) -> Result<(), Error> {
            let caller = self.env().caller();
            if to == caller {
                return Err(Error::NotAllowed);
            }

            // イベント発火
            self.env().emit_event(ApprovalForAll {
                owner: caller,
                operator: to,
                approved,
            });

            if approved {
                self.operator_approvals.insert((&caller, &to), &());
            } else {
                self.operator_approvals.remove((&caller, &to));
            }

            Ok(())
        }

        fn balance_of_or_zero(&self, of: &AccountId) -> u32 {
            self.owned_tokens_count.get(of).unwrap_or(0)
        }

        fn approved_for_all(&self, owner: AccountId, operator: AccountId) -> bool {
            self.operator_approvals.contains((&owner, &operator))
        }
    }

    #[cfg(test)]
    mod tests {
        use super::*;

        #[ink::test]
        fn mint_works() {
            let accounts = ink::env::test::default_accounts::<ink::env::DefaultEnvironment>();
            let mut erc721 = Erc721::new();

            // まだトークンがmintされていないので所有者はいない
            assert_eq!(erc721.owner_of(1), None);
            // デフォルトユーザーでまだmintしていないのでトークンをもっていない
            assert_eq!(erc721.balance_of(accounts.alice), 0);
            // mint成功するはず
            assert_eq!(erc721.mint(), Ok(()));
            // mintしたのでトークンを所有しているはず
            assert_eq!(erc721.balance_of(accounts.alice), 1);
        }
    }
}

コントラクトをデプロイする

ローカルでノードを起動する

一通りコントラクトの実装ができたのでローカルでノードを起動してみる。Astarのリポジトリのリリースノートからバイナリがダウンロードできるのでダウンロードする。ダウンロードが完了したらバイナリを移してパスを通しておく。

mv ./astar-collator /usr/local/bin
chmod +x /usr/local/bin/astar-collator

astar-collator --version
> astar-collator 4.46.1-acaecc594c7

パスが通せたらノードを起動してみる

astar-collator --dev --tmp

Polkadot{.js}拡張をインストールする

Metamaskの代わりとなるウォレットのようなものが必要なのでクローム拡張をインストールする。

https://polkadot.js.org/extension/

インストールが完了したらアカウントの作成までしておく。

コントラクトをデプロイする

Webにチェーンの管理画面のようなものが用意されているので以下のリンクからローカルノードにつなぎ、コントラクトをデプロイする。

https://polkadotjs-apps.web.app/#/explorer

他に、contract-uiというサイトからも可能。

以下のようにサイドメニューからローカルノードを選択し、switchする。

画面がローカルノードに切り替わったらディベロッパータブのコントラクトを選択する。

Upload & deploy codeを選択する。

アカウント選択でテスト用のアカウントがいくつか選択できるようになっているので選択する。

ファイル選択でビルドした拡張子がcontractのファイルを選択し、次へ進む。

コンストラクタの関数などが表示されるので問題なければデプロイをする。

デプロイが完了するとコントラクトの関数を確認できる。

適当な関数を実行可能

Sibuyaテストネットにデプロイする

ローカルと同じような操作でテストネットにもデプロイすることができる。ただし、テストネットにデプロイするときのガス代を払うためにfaucetからトークンをもらう必要がある。テストネット用のトークンはAstarのdiscordからもらえる。

TESTNET-shibuya-faucetチャンネルで/drip Shibuya <ウォレットアドレス>という風にコマンドを実行するとすぐにトークンがもらえる。

ローカルノードへのデプロイで使用したサイトのチェーンをテストネットのShibuyaに変更後、同じ要領でデプロイができる。

まとめ

以上でゼロからink!を使用したスマートコントラクトの実装とローカルでのノードの立ち上げとローカル、テストネットへのデプロイまでを実行しました。一回一通りの操作をやると非常に簡単にコントラクトの実装とデプロイができるなという印象。特にwebのUIからデプロイできるのは非常に簡単でいいなと思いました。

ハマりどころとしておそらくバージョンや依存関係のバージョン不一致によるエラーなど環境構築まわりではまりそうな気がしますがたぶんエラーがでるとしたら執筆時点でSwanky CLIあたりかなと思うのでもしどうしても解決できないようであれば直接cargo-contractを使用し、ノードを直接起動すれば開発できるのでSwanky CLIにこだわらなくてもたぶん大丈夫だと思います。

今回使用しなかったOpenBrushやSwanky CLIを使用したスマートコントラクトの開発もやってみたいなと思います。ただ、Astarの開発ドキュメントにあるようなXVMやXCMなどまで踏み込んでやろうとするとどちらかといえばPolkadotやSubstrateの理解を深める必要がありそうだなと感じました。

あとは思ったよりもRustの学習になったなと感じました。Rustの難しい部分であるライフタイムだったり所有権だったりみたいなのがそんなに出てこなかったので思ったほど難しくないじゃんと思った一方、逆に出てくると理解が足りなくて難しいなと思うこともありました。Wasmでのコントラクト開発よりもRustをもう少し使えるようになりたいなと思いました!Rustのハードルの高さで足踏みしている方がもしいれば思っているほど難しくないのでぜひRustデビューしてみてください!今回は以上です。

おまけ

Rustでの開発にはVSCodeを使用しました。特にエディタにこだわりがなければVSCodeが楽だと思います。拡張機能はrust-analyzerを入れておけばとりあえず問題ないと思います。

GitHubで編集を提案

Discussion