メインコンテンツまでスキップ

lesson-2_コントラクトを書いてみよう

🛫 Rust の新規プロジェクトを作成しよう

お気に入りのコードエディタ(お持ちでない方はVSCodeをインストールしましょう)でnear_bike_share_dappを開きましょう。 contractディレクトリには簡単なコントラクトであるgreetingプロジェクトが既に入っています。 今回は0からコントラクトを書くのでcontractディレクトリごと削除しましょう。

$ rm -r contract

Rustで新しいプロジェクトを作成する下記のコマンドをnear_bike_share_dapp直下で実行しましょう!

$ cargo new bike_share --lib

bike_shareはプロジェクト名です。 --libはライブラリを作成するという指定です。 デフォルトでは--binが設定されていて、これはバイナリファイル(実行可能ファイル)の作成を指定します。 NEARのコントラクトはライブラリ形式で作成するので--libを指定します。

bike_shareという名前のディレクトリが作成されましたが, frontendとの対比がわかりやすいように今回はディレクトリ名をcontractに変更しましょう。

$ mv bike_share contract

frontendディレクトリとcontractディレクトリが同じ階層にいることを確認してください。

それではcontractディレクトリの中身を見ていきます。 階層はこのようになっています。

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

Cargo.tomlはこのプロジェクトの情報(プロジェクト名や依存ファイル情報など)が記入されています。 実際のコードはsrcディレクトリの中に配置します。 Cargo.tomlの中身をこのように書き換えます。 (詳細はCargo.toml についてNEAR 開発での Cargo.toml の書き方を参照してください)

// Cargo.toml

[package]
authors = ["Near Inc <hello@near.org>"]
edition = "2021"
name = "bike_share"
version = "1.0.0"

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

[dependencies]
near-sdk = "4.0.0"
uint = {version = "0.9.3", default-features = false}

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

[workspace]
members = []

また.gitignoreファイルを追加しましょう。 中身は以下を記述してください

/target
/Cargo.lock

contractディレクトリの構成をチェックします。

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

🚀 コントラクトを書きましょう

contract/src/lib.rsの中身を以下のように書き換えてください!

// lib.rs

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

const DEFAULT_NUM_OF_BIKES: usize = 5;

/// コントラクトを定義します。
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct Contract {
num_of_bikes: usize,
}

/// デフォルト処理を定義します。
impl Default for Contract {
fn default() -> Self {
Self {
num_of_bikes: DEFAULT_NUM_OF_BIKES,
}
}
}

/// メソッドの実装です。
#[near_bindgen]
impl Contract {
pub fn num_of_bikes(&self) -> usize {
self.num_of_bikes
}
}

/// テストの実装です。
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn check_default() {
let contract = Contract::default();

assert_eq!(contract.num_of_bikes(), DEFAULT_NUM_OF_BIKES);
}
}

コードを上から見ていきましょう。

まずこのコード内で必要になるライブラリ(near_sdkとその中のいくつかのライブラリ)をuseを使ってスコープ内に取り込んでいます。

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

バイクの総数のデフォルト値を定義しています。 データ型に関してはこちらを参照してください。

const DEFAULT_NUM_OF_BIKES: usize = 5;

コントラクトの核となるストラクトを定義しています。

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct Contract {
num_of_bikes: usize,
}

ストラクト名は任意ですが今回はContractとします。 Rustでコントラクトを書くには、#[near_bindgen]という注釈を付与したメインのストラクトを1つ用意します。 メインのストラクトはコントラクトのステート(状態)を表す役目を担います。 例えばContractフィールドのnum_of_bikesを読み込むだけ(viewメソッド)ならコントラクトのステートは変更しません。 逆に変更する(changeメソッド)のならコントラクトのステートを変更するということになります。

#[near_bindgen]とは Rust において#[]は注釈を与える構文です。 注釈なのであらゆる意味がありますが, #[near_bindgen]をつけておくとコードがコントラクトとして成立するように処理をしてくれます。 そして外部から呼び出されるよう意図されたメソッドを外部に公開してくれます。

#[derive(BorshDeserialize, BorshSerialize)]とは ブロックチェーンネットワーク上のデータ転送で行われるシリアライズ・デシリアライズに関わるものです。 ノードがブロックチェーン上のデータを読み込む際にはバイトコードから適したデータ構造へデシリアライズが行われ, ブロックチェーン上にデータを書き込む場合はデータ構造をバイトコードへとシリアライズします。 データ構造に#[derive(BorshDeserialize, BorshSerialize)]をつけると, borsh シリアライズ(バイナリ形式でのシリアライズ)を利用することを明示します。 データをフロントエンドで読み取りやすくするために json 形式のシリアライズを利用する場合もあります。 borsh シリアライズはバイナリ形式であり変換のオーバーヘッドが少ないため処理を早くすることができます。 データ構造をブロックチェーン上でやり取りをするのみならborsh シリアライズの指定をすれば良いでしょう。 borshシリアライズを実装しているストラクトはborshシリアライズを実装しているデータ構造のみフィールドに含むことができます。 詳しくは以下はご覧ください。 serialization protocols > Byte Serialization in Blockchains > Does NEAR need both Serialize and BorshSerialize?

次にコントラクトの初期化をするdefault関数を用意します。 先ほど定義したDEFAULT_NUM_OF_BIKESによりメンバー変数を初期化しています。

impl Default for Contract {
fn default() -> Self {
Self {
num_of_bikes: DEFAULT_NUM_OF_BIKES,
}
}
}

ここではいくつかRustの書き方について確認する必要があるので1つずつ触れていきましょう。

関数の定義 構文はこのようになっています。

fn 関数名(引数...) -> 返り値 {
実装
}

上記コード内で出てきた

fn default() -> Self {
Self {
num_of_bikes: DEFAULT_NUM_OF_BIKES,
}
}

という部分は、defaultという名前の関数を引数なしで、返り値はSelfで定義していました。 Selfとは呼び出しているオブジェクト自体の型を表します。 つまりここではSelfContractになり、Contract型のオブジェクトを中身を詰めて返却しています。

トレイト トレイトとは他の言語でのインターフェースに類似しているRustの概念です。 トレイトは「共通の振る舞い」を定義します。 上記にあるDefaultトレイトです。 例えばDefaultトレイトの定義(ライブラリの中身なのでコントラクト内にはありません)を覗くとこのようになっています。

pub trait Default: Sized {
// ...

fn default() -> Self;
}

trait Defaultの宣言の{}内にfn default() -> Self;という記述がありますね。 Defaultトレイトは「名前がdefault 、引数なし、返り値は呼び出しているオブジェクトの型」という関数(振る舞い)を定義しています。

そして標準では全てのコントラクトのメインとなるストラクトはDefaultトレイトを実装することが期待されています。 (後に出てきますがinit関数により代替することも可能です) なのでコントラクト内ではこのようにDefaultトレイトを実装していました。 impl Default for ContractContractDefaultトレイトを実装するという宣言です。

impl Default for Contract {
fn default() -> Self {
Self {
num_of_bikes: DEFAULT_NUM_OF_BIKES,
}
}
}

トレイトの何が嬉しいのかというと、オブジェクトの振る舞いを抽象化できることです。 実際、コントラクトのメインストラクトはDefaultトレイトを実装することが期待されていても、そのストラクトの中身までは縛られていません。 決まった振る舞いをしていれば型も実装の中身もなんでも良いです。 つまり様々なコントラクトに対して「初期化で使うのでDefaultトレイトを実装しておいてください」というような共通の決め事ができ、固定の型に依存せず一貫性を持てます。 トレイトについて詳しくはこちらを参照してください。

次にメソッド実装しています。

#[near_bindgen]
impl Contract {
pub fn num_of_bikes(&self) -> usize {
self.num_of_bikes
}
}

下記のような構文でストラクトにメソッドを定義できます。

impl ストラクトの型名 {
メソッド
}

ここではバイクの総数を返すnum_of_bikesメソッドを定義しました。 引数selfについては次のセクションでお話しします。

最後に簡単なテストを書いています。

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

#[test]
fn check_default() {
let contract = Contract::default();

assert_eq!(contract.num_of_bikes(), DEFAULT_NUM_OF_BIKES);
}
}

#[test]check_default関数がテスト関数であることを示します(テストコマンド実行時に識別するためです)。 そしてデフォルト関数を呼び出し、オブジェクトがしっかりDEFAULT_NUM_OF_BIKESを保持できているか確認しています。 assert系マクロは引数に渡したものが期待するものでなければパニックします。

パニック パニックはプログラムを終了させます。 テスト時のパニックはテストの失敗を知らせます。 コントラクト内のパニックはメソッド呼び出しを中断します。

assert_eqは引数に渡した2つの値が同じものかを判断します。 マクロはここでは関数と捉えましょう。

letについて letは変数であることを宣言し、デフォルトでは不変になります。 一度定義したら値を書き換えることができません。 可変な変数を宣言する場合はlet mutを使用します。

  • その他の構文について #[cfg(test)]cargo testというテストコマンドを実行した時のみコードをコンパイルし走らせるよう指示します。 mod testsでテストモジュール(モジュールはコードをひとかたまりにまとめたもの)を宣言しています。 モジュールの中から1階層上(Contractの階層)を参照するためuse super::*;を使用しています。 この辺りは後からの理解でも充分です。🙆‍♀️

unitテストはテスト対象コードと同じファイルにこのように書くのが慣習です。

それではcontractディレクトリに移動し以下のコマンドでテストを実行してください!

$ cargo test

テストが成功すれば以下のような表示がされます。

🙋‍♂️ 質問する

ここまでの作業で何かわからないことがある場合は、Discordの#nearで質問をしてください。

ヘルプをするときのフローが円滑になるので、エラーレポートには下記の3点を記載してください ✨


1. 質問が関連しているセクション番号とレッスン番号
2. 何をしようとしていたか
3. エラー文をコピー&ペースト
4. エラー画面のスクリーンショット


おめでとうございます! Rustでコードを記述し、テストを実行できました 🎉 次のレッスンではさらに機能を増やしていきます!