👋

NEAR×Rustでスマートコントラクト!初心者向けステップバイステップ解説

2024/06/21に公開

概要

この記事の対象者:
NEARブロックチェーンとRustでスマートコントラクトを学びたい初心者向け。

この記事の内容:
NEARテストネットでのスマートコントラクト開発の手順を解説。

この記事を読むとわかること:
Rustを使ったスマートコントラクトの作成、テスト、デプロイの方法がわかります。

方法

必要なツールの準備

NEARのテストネットアカウントを作成

テストネットは実際のお金を使わないで試せるネットワークです。
NEARウォレットのサイトでアカウントを作成し、テスト用のNEARトークンを入手します。
NEARテストネットウォレット

near-cliのインストール

near-cliは、NEARとやり取りするためのツールです。
以下のコマンドでインストールします。

npm install -g near-cli
near --version

プロジェクトの作成

新しいRustプロジェクトを作成

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

cargo new rust-counter-tutorial
cd rust-counter-tutorial

プロジェクトの構造

プロジェクトを作成すると、以下のようなファイル構造ができます。

.
├── Cargo.toml
└── src
   └── main.rs

ファイルの修正

プロジェクトを次のように変更します。

.
├── Cargo.toml
└── src
   └── lib.rs

Cargo.tomlの内容

Cargo.tomlは、Rustプロジェクトの設定ファイルです。以下のように編集します。

[package]
name = "rust-counter-tutorial"
version = "0.1.0"
authors = ["NEAR Inc <hello@near.org>"]
edition = "2018"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
near-sdk = "3.1.0"

[profile.release]
codegen-units = 1
opt-level = "z"
lto = true
debug = false
panic = "abort"
overflow-checks = true

src/lib.rsの内容

lib.rsはスマートコントラクトのロジックを記述するファイルです。

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::{env, near_bindgen};

near_sdk::setup_alloc!();

#[near_bindgen]
#[derive(Default, BorshDeserialize, BorshSerialize)]
pub struct Counter {
    val: i8, // 8ビットの符号付き整数型
}

#[near_bindgen]
impl Counter {
    pub fn get_num(&self) -> i8 {
        return self.val;
    }

    pub fn increment(&mut self) {
        self.val += 1;
        let log_message = format!("Increased number to {}", self.val);
        env::log(log_message.as_bytes());
        after_counter_change();
    }

    pub fn decrement(&mut self) {
        self.val -= 1;
        let log_message = format!("Decreased number to {}", self.val);
        env::log(log_message.as_bytes());
        after_counter_change();
    }

    pub fn reset(&mut self) {
        self.val = 0;
        env::log(b"Reset counter to zero");
    }
}

fn after_counter_change() {
    env::log("Make sure you don't overflow, my friend.".as_bytes());
}

#[cfg(test)]
mod tests {
    use super::*;
    use near_sdk::MockedBlockchain;
    use near_sdk::{testing_env, VMContext};

    fn get_context(input: Vec<u8>, is_view: bool) -> VMContext {
        VMContext {
            current_account_id: "alice.testnet".to_string(),
            signer_account_id: "robert.testnet".to_string(),
            signer_account_pk: vec![0, 1, 2],
            predecessor_account_id: "jane.testnet".to_string(),
            input,
            block_index: 0,
            block_timestamp: 0,
            account_balance: 0,
            account_locked_balance: 0,
            storage_usage: 0,
            attached_deposit: 0,
            prepaid_gas: 10u64.pow(18),
            random_seed: vec![0, 1, 2],
            is_view,
            output_data_receivers: vec![],
            epoch_height: 19,
        }
    }

    #[test]
    fn increment() {
        let context = get_context(vec![], false);
        testing_env!(context);
        let mut contract = Counter { val: 0 };
        contract.increment();
        println!("Value after increment: {}", contract.get_num());
        assert_eq!(1, contract.get_num());
    }

    #[test]
    fn decrement() {
        let context = get_context(vec![], false);
        testing_env!(context);
        let mut contract = Counter { val: 0 };
        contract.decrement();
        println!("Value after decrement: {}", contract.get_num());
        assert_eq!(-1, contract.get_num());
    }

    #[test]
    fn increment_and_reset() {
        let context = get_context(vec![], false);
        testing_env!(context);
        let mut contract = Counter { val: 0 };
        contract.increment();
        contract.reset();
        println!("Value after reset: {}", contract.get_num());
        assert_eq!(0, contract.get_num());
    }
}

プログラムのテスト

以下のコマンドでプログラムのテストを実行します。

cargo test -- --nocapture

WASM モジュールのコンパイル

以下のコマンドでソースコードから WASM モジュールをコンパイルします。

cargo build --target wasm32-unknown-unknown --release

NEAR テストネットにログイン

near login

スマートコントラクトのデプロイ

ビルドした WASM のスマートコントラクトを NEAR テストネットにデプロイします。

near deploy <YOUR_ACCOUNT> target/wasm32-unknown-unknown/release/rust_counter_tutorial.wasm

<YOUR_ACCOUNT>は自分のアカウント名に変更してください
(例:taro.testnet)

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

カウンター値の取得

以下を実行し、カウンター値を取得します。

near view <YOUR_ACCOUNT> get_num

結果

View call: <YOUR_ACCOUNT>.get_num()
0

カウンターの現在の値は0とわかります。

カウンター値の増加

以下を実行し、カウンター値を増加させます。

near call <YOUR_ACCOUNT> increment --accountId <YOUR_ACCOUNT>
near view <YOUR_ACCOUNT> get_num

結果

Scheduling a call: <YOUR_ACCOUNT>.increment()
Receipt: ......
        Log [<YOUR_ACCOUNT>]: Increased number to 1
        Log [<YOUR_ACCOUNT>]: Make sure you don't overflow, my friend.
Transaction Id .....

View call: <YOUR_ACCOUNT>.get_num()
1

カウンター値が増加し、1になったことがわかります。

コードの紹介

Cargo.toml

Cargo.tomlは、Rustプロジェクトの設定ファイルで、プロジェクトのメタデータや依存関係、ビルド設定などを定義します。
特にスマートコントラクトを開発する際には、パフォーマンスやサイズ、安全性を考慮して適切な設定を行うことが重要です。
以下に、それぞれのセクションとその役割を説明します。

[package]セクション

[package]
name = "rust-counter-tutorial"
version = "0.1.0"
authors = ["NEAR Inc <hello@near.org>"]
edition = "2018"
  • name: プロジェクトの名前を指定します。これは必要で、プロジェクトの識別に使われます。
  • version: プロジェクトのバージョンを指定します。これは必要で、バージョン管理や依存関係の解決に使われます。
  • authors: プロジェクトの作者を指定します。これは任意ですが、プロジェクトのメンテナンスや問い合わせに役立ちます。
  • edition: Rustのエディションを指定します。これは必要で、Rustのコンパイラが使用する規則や機能のセットを決定します。

[lib]セクション

[lib]
crate-type = ["cdylib", "rlib"]
  • crate-type: ライブラリの種類を指定します。ここでは、"cdylib"(C動的ライブラリ)と"rlib"(Rustライブラリ)の2種類が指定されています。これは任意ですが、NEARのスマートコントラクトをビルドするために必要です。
    また、ここで[lib]セクションを使用しているため、lib.rsにコードを配置する必要があります

[dependencies]セクション

[dependencies]
near-sdk = "3.1.0"
  • near-sdk: NEARのスマートコントラクトを開発するためのライブラリです。バージョン3.1.0を使用しています。これは必要で、スマートコントラクトの機能を利用するために依存関係として指定します。

[profile.release]セクション

[profile.release]
codegen-units = 1
opt-level = "z"
lto = true
debug = false
panic = "abort"
overflow-checks = true
  • codegen-units: コード生成の単位数を指定します。1に設定するとコンパイルの最適化が行われます。これは任意ですが、パフォーマンス向上のために設定されています。
  • opt-level: 最適化レベルを指定します。"z"はコードサイズの最適化を意味します。これは任意ですが、デプロイサイズを小さくするために推奨されます。
  • lto: リンク時の最適化を有効にします。trueに設定されています。これは任意ですが、パフォーマンス向上のために設定されています。
  • debug: デバッグ情報を含むかどうかを指定します。falseに設定されており、リリースビルドではデバッグ情報を含めません。これは任意ですが、最終的なビルドサイズを小さくするために推奨されます。
  • panic: パニック時の動作を指定します。"abort"に設定されており、パニックが発生したら即座に中止します。これは任意ですが、パフォーマンスとサイズのために推奨されます。
  • overflow-checks: 算術オーバーフローのチェックを有効にします。trueに設定されています。これは任意ですが、安全性のために推奨されます。
    [*1]

src/lib.rs

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::{env, near_bindgen};

near_sdk::setup_alloc!();
  • use near_sdk::borsh: データのシリアライズとデシリアライズのためのライブラリをインポートしています。これにより、データをバイナリ形式で保存および読み取ることができます。
  • use near_sdk::env, near_bindgen: NEARの環境とスマートコントラクトのエントリポイントを定義するためのライブラリをインポートしています。
  • near_sdk::setup_alloc!(): スマートコントラクトのメモリ割り当てを設定するためのマクロです。
#[near_bindgen]
#[derive(Default, BorshDeserialize, BorshSerialize)]
pub struct Counter {
    val: i8, // 8ビットの符号付き整数型
}
  • #[near_bindgen]: この構造体がスマートコントラクトの一部であることを示します。[*2]
  • #[derive(Default, BorshDeserialize, BorshSerialize)]: デフォルト値の生成、シリアライズ、およびデシリアライズを自動的に実装します。
  • pub struct Counter: Counterという名前の構造体を定義し、カウンターの値(val)を保持します。
#[near_bindgen]
impl Counter {
    pub fn get_num(&self) -> i8 {
        return self.val;
    }

    pub fn increment(&mut self) {
        self.val += 1;
        let log_message = format!("Increased number to {}", self.val);
        env::log(log_message.as_bytes());
        after_counter_change();
    }

    pub fn decrement(&mut self) {
        self.val -= 1;
        let log_message = format!("Decreased number to {}", self.val);
        env::log(log_message.as_bytes());
        after_counter_change();
    }

    pub fn reset(&mut self) {
        self.val = 0;
        env::log(b"Reset counter to zero");
    }
}
  • #[near_bindgen]: Counter構造体の各メソッドがスマートコントラクトの一部であることを示します。
  • pub fn get_num(&self) -> i8: カウンターの現在の値を返すメソッドです。&selfは、このメソッドがカウンターの状態を変更しないことを示します。
  • pub fn increment(&mut self): カウンターの値を1増加させるメソッドです。&mut selfは、このメソッドがカウンターの状態を変更することを示します。値を増加させた後、ログメッセージを記録し、after_counter_change関数を呼び出します。
  • pub fn decrement(&mut self): カウンターの値を1減少させるメソッドです。incrementと同様に、ログメッセージを記録し、after_counter_change関数を呼び出します。
  • pub fn reset(&mut self): カウンターの値を0にリセットするメソッドです。リセット後、ログメッセージを記録します。
fn after_counter_change() {
    env::log("Make sure you don't overflow, my friend.".as_bytes());
}
  • fn after_counter_change(): カウンターの値が変更された後に呼び出される関数です。オーバーフローに関する警告メッセージをログに記録します。この関数はスマートコントラクトの一部ではなく、内部的に使用されるヘルパー関数です。
#[cfg(test)]
mod tests {
    use super::*;
    use near_sdk::MockedBlockchain;
    use near_sdk::{testing_env, VMContext};

    fn get_context(input: Vec<u8>, is_view: bool) -> VMContext {
        VMContext {
            current_account_id: "alice.testnet".to_string(),
            signer_account_id: "robert.testnet".to_string(),
            signer_account_pk: vec![0, 1, 2],
            predecessor_account_id: "jane.testnet".to_string(),
            input,
            block_index: 0,
            block_timestamp: 0,
            account_balance: 0,
            account_locked_balance: 0,
            storage_usage: 0,
            attached_deposit: 0,
            prepaid_gas: 10u64.pow(18),
            random_seed: vec![0, 1, 2],
            is_view,
            output_data_receivers: vec![],
            epoch_height: 19,
        }
    }

    #[test]
    fn increment() {
        let context = get_context(vec![], false);
        testing_env!(context);
        let mut contract = Counter { val: 0 };
        contract.increment();
        println!("Value after increment: {}", contract.get_num());
        assert_eq!(1, contract.get_num());
    }

    #[test]
    fn decrement() {
        let context = get_context(vec![], false);
        testing_env!(context);
        let mut contract = Counter { val: 0 };
        contract.decrement();
        println!("Value after decrement: {}", contract.get_num());
        assert_eq!(-1, contract.get_num());
    }

    #[test]
    fn increment_and_reset() {
        let context = get_context(vec![], false);
        testing_env!(context);
        let mut contract = Counter { val: 0 };
        contract.increment();
        contract.reset();
        println!("Value after reset: {}", contract.get_num());
        assert_eq!(0, contract.get_num());
    }
}
  • #[cfg(test)] mod tests: テストモジュールを定義します。このモジュールは、コンパイル時にテストコードが含まれるようにします。
  • use super::*: testsモジュール内で、Counter構造体やその他の関数にアクセスできるようにします。
  • use near_sdk::MockedBlockchain: モックブロックチェーンを使用してテスト環境を設定します。
  • use near_sdk::{testing_env, VMContext}: テスト環境と仮想マシンのコンテキストを使用してテストを実行します。
  • fn get_context(input: Vec<u8>, is_view: bool) -> VMContext: テスト用のコンテキストを設定する関数です。この関数は、テスト環境でのさまざまなパラメーターを設定します。
  • #[test] fn increment(): カウンターのインクリメントをテストする関数です。モックコンテキストを設定し、カウンターをインクリメントして、その結果が期待通りであることを確認します。
  • #[test] fn decrement(): カウンターのデクリメントをテストする関数です。同様に、モックコンテキストを設定し、カウンターをデクリメントして、その結果を確認します。
  • #[test] fn increment_and_reset(): カウンターのインクリメントとリセットをテストする関数です。カウンターをインクリメントし、その後リセットして、結果を確認します。

[*3][*4]

沼ポイント

can't find crate for core

再現方法

cargo build --target wasm32-unknown-unknown --releaseを実行したとき

解決方法

Rust コンパイラのインストール

curl https://sh.rustup.rs -sSf | sh
source ~/.cargo/env

Rust にターゲットアーキテクチャを追加

rustup target add wasm32-unknown-unknown

参考

[*1]
https://doc.rust-lang.org/cargo/reference/manifest.html

[*2]
https://doc.rust-lang.org/rust-by-example/generics/phantom.html

[*3]
https://docs.near.org/build/smart-contracts/anatomy/

[*4]
https://github.com/near-examples/counters/blob/main/contract-rs/src/lib.rs

ちゅらデータ株式会社

Discussion