Challenge UNCHAIN vol.8 – Astar-SocialFi Start!! –

時間が空いてしまいましたが、今回選んだのは「Astar-SocialFi」。

せっかくWASMも勉強したし、一旦Astarのアプリを構築してみようと思う。

まずはSection 0。

Lesson 1. チェーンの特徴とアプリ概要

No Title

本プロジェクトでは、 ASTAR というチェーン上でスマートコントラクトの実装とそれとやりとりをするwebアプリケーションを作成します。 使用する技術は以下のものです。 今すべてを理解している必要はありません。 わからないことがあったらインターネットで検索したり、コミュニティで質問しながらプロジェクトを進めていきましょう! 最近話題になっている日本初のパブリックブロックチェーン ASTAR をご存じの方も多いのではないかと思いますが、そのチェーンの特徴とはどのようなものなのでしょうか? その最大の特徴は Polkadotのパラチェーン(Polkadotに接続するための特別なチェーン)であることです。なぜこれがすごいのかというと、 Polkadot のパラチェーンとして認められるのは100個までと決められているのですが、ASTARは世界で 3 番目の速さでその中に選ばれたからです! では技術的な特徴とは何なのでしょうか? 主なものは以下の4つとなります。 1.開発者への報酬制度 ASTARは EVMと WASM を用いたスマートコントラクトの開発者をサポートしています。これらどちらかで作成されたdAppをAstar上にデプロイしてASTARトークンの保有者に気に入ってもらえればASTARトークンを受け取れるという仕組みが用意されています。 TPS(Transaction Per Second) が他のチェーンと比べて小さく、ユーザーはトランザクションを待つのに時間を無駄にすることが少ないです。 ASTARが用意してくれている基礎的なシステムをしようすれば、開発者は独自のチェーンを簡単に作成することができます。 本アプリはブラウザで使用可能なフルオンチェーンSNS webアプリです。 また投稿に対して受け取ったいいねの数に従って、トークンを獲得できるという特徴があります! では具体的にこのアプリが持つ機能を画面ごとに簡単に紹介していきます。 この画面の Connect ボタンを押すことでウォレットを接続できるようになっています。 うまく接続が完了すれば次にホーム画面へ飛ぶようにできています。 この画面では全体のユーザーの投稿を最新のものから順番に見られるようになっています。 また、それらの投稿に いいね を押せたりまだフォローしていないユーザーをフォローすることもできます。 このフォローによってそのアカウントとメッセージでやりとりを行うことができます。 加えて、いいねの数に従ってトークンを獲得できる機能も備えておりロゴとプロフィールアイコンの間に残高が表示されます。 自分のプロフィール、フォロワー数、フォロー数、投稿内容を見ることができます。 また、プロフィールの画像と名前を変更することもできます。 フォローしている、またはされているユーザーとメッセージのやりとりができる画面です。 以上を踏まえて実装する機能は下のようになっています。 1.ウォレット接続 2.投稿 3.メッセージのやりとり 4.フォロー これらの機能を Rust, Next.js を使って実装していきます! この学習コンテンツは、 UNCHAIN License のもとで運用されています。 プロジェクトに参加していて、「こうすればもっと分かりやすいのに!」「これは間違っている!」と思ったら、ぜひ pull request を送ってください。 GitHubから直接コードを編集して直接 pull requestを送る方法は、 こちら を参照してください。 どんなリクエストでも大歓迎です 🎉 また、プロジェクトを自分のGitHubアカウントに Forkして、中身を編集してから pull request を送ることもできます。 👋 UNCHAIN-projectsにpull requestを送る!

技術的には、

を使います。

それぞれ別で記事を書いた方が良さそう。

アプリの概要としては、

  • ログイン画面
  • ホーム画面
  • プロフィール画面
  • メッセージ画面

を作っていくことになります。

Lessson 2. 環境構築

バックエンド

今回はWASM meetupで作った環境を流用。

  • Rustのインストール
  • swankyのインストール

astar-collatorというノードがあるっぽいが、今回はswanky-nodeで代用することとする。(できるのか???)

polkadot.jsを使って、ノードとつながれば問題なし。

フロントエンド

Next.jsを利用する。

Next.js by Vercel – The React Framework

Production grade React applications that scale. The world’s leading companies use Next.js by Vercel to build static and dynamic websites and web applications.

「by Vercel」とあるので、あのVercelが提供しているんだと驚く。

tailwind cssも利用する。

これも個別記事になるくらい。

next.jsのプロジェクトを作るときに、ディレクトリ構成を聞かれるのでちょこちょこいじってたら、デフォルトと違う感じになっていたので、tailwind.config.jsをちょっといじって完了。

$ yarn dev

で、サイトが立ち上がればSection 0は完了。

せっかくなので、思い切って、Section 1も実施!!

Lesson 1. コントラクトの基礎

#![cfg_attr(not(feature = "std"), no_std)]
use ink_lang as ink;
#[ink::contract]

ink!でコントラクトを描く場合のお作法として、最初に記述する。

   #[ink(storage)]
   pub struct AstarSnsContract {
       value: bool,
   }

まずは、#[ink(storage)]で構造体を定義して、状態変数を記載。

impl AstarSnsContract {
       #[ink(constructor)]
       pub fn new(init_value: bool) -> Self {
           Self { value: init_value }
       }

       #[ink(constructor)]
       pub fn default() -> Self {
           Self::new(Default::default())
       }

       #[ink(message)]
       pub fn flip(&mut self) {
           self.value = !self.value;
       }

       #[ink(message)]
       pub fn get(&self) -> bool {
           self.value
       }
}

構造体として定義したAstarSnsContractを実装していく。

#[ink(constructor)]でコンストラクタを定義。

#[ink(message)]で外部から呼び出すことができるファンクションを定義。

&mutなんかはRustの文法で、参照渡しでオブジェクトそのものを書き換えますよ、という宣言。

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

       #[ink::test]
       fn default_works() {
           let astar_sns_contract = AstarSnsContract::default();
           assert_eq!(astar_sns_contract.get(), false);
       }

       #[ink::test]
       fn it_works() {
           let mut astar_sns_contract = AstarSnsContract::new(false);
           assert_eq!(astar_sns_contract.get(), false);
           astar_sns_contract.flip();
           assert_eq!(astar_sns_contract.get(), true);
       }
   }

impl内に上記テストコードを埋め込むことができる。

#[cfg(test)]がテストコードの宣言。4行まではほぼ固定かな。

#[ink::test]がテストケースの宣言。

Cargo.tomlについて。

[package]
name = "astar_sns_contract"
version = "0.1.0"
authors = ["[your_name] <[your_email]>"]
edition = "2021"

[dependencies]
ink_primitives = { version = "=3.3.0", default-features = false }
ink_metadata = { version = "=3.3.0", default-features = false, features = ["derive"], optional = true }
ink_env = { version = "=3.3.0", default-features = false }
ink_storage = { version = "=3.3.0", default-features = false }
ink_lang = { version = "=3.3.0", default-features = false }
ink_prelude = { version = "~3.3.0", default-features = false }
ink_engine = { version = "~3.3.0", default-features = false, optional = true }
ink_lang_codegen = { version = "=3.3.0", default-features = false }
ink_lang_ir = { version = "=3.3.0", default-features = false }

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

openbrush = { version = "2.2.0", default-features = false, features = ["ownable"]  }

[lib]
name = "astar_sns_contract"
path = "lib.rs"
crate-type = [
	# Used for normal contract Wasm blobs.
	"cdylib",
]

[features]
default = ["std"]
std = [
    "ink_metadata/std",
    "ink_metadata",
    "ink_env/std",
    "ink_storage/std",
    "ink_primitives/std",
    "ink_lang/std",
    "scale/std",
    "scale-info",
    "scale-info/std",

    "openbrush/std",
]
ink-as-dependency = []

そもそもcargoはRustのパッケージマネージャなので、その設定ファイル。

注意するとしたら、[lib]pathを適切にセットしないとビルドもできないので要注意。。。

ファイルを手動で作っていったら、動かなくて焦った、、、

*.rssrc配下に置きたかったので、諸々修正。

※Lesson 4のlib.rs

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

mod ft;
mod follow;
mod message;
mod metadata;
mod post;
mod profile;

use ink_lang as ink;

#[ink::contract]
mod astar_sns_contract {
    use ink_env::debug_println;
    use ink_lang::codegen::Env;
    use ink_prelude::string::String;
    use ink_prelude::vec::Vec;
    use openbrush::storage::Mapping;
    use openbrush::test_utils::*;

    pub use crate::follow::*;
    pub use crate::message::*;
    pub use crate::metadata::*;
    pub use crate::post::*;

    #[ink(storage)]
    pub struct AstarSnsContract {
        pub profile_map: Mapping<AccountId, Profile>,
        pub post_map: Mapping<u128, Post>,
        pub post_map_counter: u128,
        pub message_list_map: Mapping<u128, Vec<Message>>,
        pub message_list_map_counter: u128,
        pub asset_mapping: Mapping<AccountId, u128>,
    }

    impl AstarSnsContract {
        /// Constructor that initializes the `bool` value to the given `init_value`.
        #[ink(constructor, payable)]
        pub fn new() -> Self {
            Self {
                profile_map: Mapping::default(),
                post_map: Mapping::default(),
                post_map_counter: 0,
                message_list_map: Mapping::default(),
                message_list_map_counter: 0,
                asset_mapping: Mapping::default(),
            }
        }

        // 投稿関数
        #[ink(message)]
        pub fn release_post(
            &mut self,
            description: String,
            created_time: String,
            post_img_url: String,
        ) {
            let caller: AccountId = self.env().caller();
            self.release_post_fn(caller, description, created_time, post_img_url);
        }

        // 全体の投稿を取得する関数
        #[ink(message)]
        pub fn get_general_post(&self, num: u128) -> Vec<Post> {
            let general_post_list = self.get_general_post_fn(num);
            general_post_list
        }

        // 指定したアカウントの投稿を取得
        #[ink(message)]
        pub fn get_individual_post(&self, num: u128, account_id: AccountId) -> Vec<Post> {
            let individual_post_list = self.get_individual_post_fn(num, account_id);
            individual_post_list
        }

        // いいねを加える関数
        #[ink(message)]
        pub fn add_likes(&mut self, post_id: u128) {
            self.add_likes_fn(post_id);
        }

        // メッセージ送信関数
        #[ink(message)]
        pub fn send_message(
            &mut self,
            message: String,
            message_list_id: u128,
            created_time: String,
        ) {
            let caller: AccountId = self.env().caller();
            self.send_message_fn(message, message_list_id, caller, created_time);
        }

        // 指定されたidに紐づくメッセージリストの取得関数
        #[ink(message)]
        pub fn get_message_list(&self, message_list_id: u128, num: u128) -> Vec<Message> {
            let message_list: Vec<Message> =
                self.get_message_list_fn(message_list_id, num as usize);
            message_list
        }

        // 指定されたidに紐づくメッセージリストの最後のメッセージを取得する関数
        #[ink(message)]
        pub fn get_last_message(&self, message_list_id: u128) -> Message {
            let message: Message = self.get_last_message_fn(message_list_id);
            message
        }

        // プロフィール作成関数
        #[ink(message)]
        pub fn create_profile(&mut self) {
            let caller: AccountId = self.env().caller();
            self.create_profile_fn(caller);
        }

        // プロフィールの名前と画像を設定する関数
        #[ink(message)]
        pub fn set_profile_info(&mut self, name: String, img_url: String) {
            let caller: AccountId = self.env().caller();
            self.set_profile_info_fn(caller, name, img_url);
        }

        // フォロー関数
        #[ink(message)]
        pub fn follow(&mut self, followed_account_id: AccountId) {
            let caller: AccountId = self.env().caller();
            self.follow_fn(caller, followed_account_id);
        }

        // 自分がフォローしているアカウントのリストを取得する関数
        #[ink(message)]
        pub fn get_following_list(&self, account_id: AccountId) -> Vec<AccountId> {
            let following_list: Vec<AccountId> = self.get_following_list_fn(account_id);
            following_list
        }

        // 自分をフォローしているアカウントのリストを取得する関数
        #[ink(message)]
        pub fn get_follower_list(&self, account_id: AccountId) -> Vec<AccountId> {
            let follower_list: Vec<AccountId> = self.get_follower_list_fn(account_id);
            follower_list
        }

        // プロフィール情報を取得する
        #[ink(message)]
        pub fn get_profile_info(&self, account_id: AccountId) -> Profile {
            let profile: Profile = self.get_profile_info_fn(account_id);
            profile
        }

        // 既にプロフィールが作成されているか確認する関数
        #[ink(message)]
        pub fn check_created_info(&self, account_id: AccountId) -> bool {
            let is_already_connected: bool = self.check_created_profile_fn(account_id);
            is_already_connected
        }

        // 指定のアカウントの投稿に対するいいねの総数を取得する関数
        #[ink(message)]
        pub fn get_total_likes(&self, account_id: AccountId) -> u128 {
            let num_of_likes = self.get_total_likes_fn(account_id);
            num_of_likes
        }

        // 指定のアカウントのトークン保有量を取得
        #[ink(message)]
        pub fn balance_of(&self, account_id: AccountId) -> u128 {
            let asset = self.balance_of_fn(account_id);
            asset
        }

        // トークンを送信する関数
        #[ink(message)]
        pub fn transfer(&mut self, to_id: AccountId, amount: u128) {
            let caller: AccountId = self.env().caller();
            self.transfer_fn(caller, to_id, amount);
        }

        // いいね数に応じてトークンを配布する関数
        #[ink(message)]
        pub fn distribute_refer_likes(&mut self) {
            let caller: AccountId = self.env().caller();
            let total_likes = self.get_total_likes_fn(caller);
            let asset = self.balance_of_fn(caller);
            let calculated_amount = total_likes * 20;
            if asset < calculated_amount {
                self.distribute_fn(caller, calculated_amount - asset);
            }
        }
    }
}

lib.rsを見ると全体が把握できる。

最初のmodの宣言で各種モジュール(ファイル)を呼び出している。

pub use create::xxxxになぜかprofileとftがないのが気になるけど、使えるようにしてるんだろうなと。

lib.rsが肥大化しないように、機能ごとにrsファイルを作って管理している模様。

Rustの最適な書き方なのか、スマートコントラクトの最適な書き方なのかわからないけど、そこらへんもうちょっと知りたいところ。

Lesson 2. 構造体/投稿機能/フォロー機能

use ink_env::AccountId;
use ink_prelude::string::String;
use ink_prelude::vec::Vec;
use ink_storage::traits::{PackedLayout, SpreadLayout, StorageLayout};

#[derive(Debug, Clone, scale::Encode, scale::Decode, SpreadLayout, PartialEq, PackedLayout)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, StorageLayout))]
pub struct Xxxx {
    pub name: String,
    pub user_id: AccountId,
    pub num: u128,
}

構造体をmetadata.rsとして定義。

この書き方を覚えておけば、構造体を記述できそう。

use crate::metadata::*;
use ink_env::AccountId;
use ink_prelude::string::String;
use ink_prelude::string::ToString;
use ink_prelude::vec::Vec;

use crate::astar_sns_contract::AstarSnsContract;

impl AstarSnsContract {
    // 投稿するための関数
    pub fn release_post_fn(
        &mut self,
        account_id: AccountId,
        description: String,
        created_time: String,
        post_img_url: String,
    ) {
    }

    // 全ての投稿から指定の最新度の投稿を取得
    pub fn get_general_post_fn(&self, num: u128) -> Vec<Post> {
    }

    // 個人の投稿を取得
    pub fn get_individual_post_fn(&self, num: u128, account_id: AccountId) -> Vec<Post> {
    }

    // 指定された投稿にいいねを追加
    pub fn add_likes_fn(&mut self, post_id: u128) {
    }

    // 指定されたアカウントの投稿に含まれているいいねの総数を取得する関数
    pub fn get_total_likes_fn(&self, account_id: AccountId) -> u128 {
    }

}

投稿機能はpost.rsに定義。

※ロジックについては省略

use crate::metadata::*;
use ink_env::AccountId;
use ink_prelude::vec::Vec;

use crate::astar_sns_contract::AstarSnsContract;

impl AstarSnsContract {
    // フォロー関数
    pub fn follow_fn(&mut self, following_account_id: AccountId, followed_account_id: AccountId) {
    }

    // get following list of specified account
    pub fn get_following_list_fn(&self, account_id: AccountId) -> Vec<AccountId> {
    }

    // 指定したユーザーがフォローしているアカウントのフォローワーリストを取得
    pub fn get_follower_list_fn(&self, account_id: AccountId) -> Vec<AccountId> {
    }
}

フォロー機能はfollow.rsに定義。

※ロジックについては省略

Lesson 3. メッセージ機能/プロフィール機能/FT機能

use crate::metadata::*;
use ink_env::AccountId;
use ink_prelude::string::String;
use ink_prelude::vec::Vec;

use crate::astar_sns_contract::AstarSnsContract;

impl AstarSnsContract {
    // メッセージ送信関数
    pub fn send_message_fn(
        &mut self,
        message: String,
        message_list_id: u128,
        sender_id: AccountId,
        created_time: String,
    ) {
    }

    // メッセージリストを取得`
    pub fn get_message_list_fn(&self, message_list_id: u128, num: usize) -> Vec<Message> {
    }

    // 指定したメッセージリストのIDに紐づいたメッセージリストの最後のメッセージを取得
    pub fn get_last_message_fn(&self, message_list_id: u128) -> Message {
    }
}

メッセージ機能はmessage.rsに定義。

※ロジックについては省略

use crate::metadata::*;
use ink_env::AccountId;
use ink_prelude::string::String;
use ink_prelude::vec::Vec;

use crate::astar_sns_contract::AstarSnsContract;

impl AstarSnsContract {
    // 新しいユーザーのウォレットに接続した際に自動的に実行されるプロフィール作成関数
    // フロントでは最初はプロフィールの名前はunknown, imgUrlも指定されることになる。
    pub fn create_profile_fn(&mut self, account_id: AccountId) {
    }

    // プロフィールの名前と画像のURLを設定
    pub fn set_profile_info_fn(&mut self, account_id: AccountId, name: String, img_url: String) {
    }

    // get profile info
    pub fn get_profile_info_fn(&self, account_id: AccountId) -> Profile {
    }

    pub fn check_created_profile_fn(&self, account_id: AccountId) -> bool {
    }
}

プロフィール機能はprofile.rsに定義。

※ロジックについては省略

use crate::astar_sns_contract::AstarSnsContract;
use ink_env::AccountId;

impl AstarSnsContract {
    // トークン残高を確認する関数
    pub fn balance_of_fn(&self, account_id: AccountId) -> u128 {
    }

    // 送信者と受信者を指定してトークンを送信
    pub fn transfer_fn(&mut self, from_id: AccountId, to_id: AccountId, amount: u128) {
    }

    // コントラクトから指定されたアカウントへトークンを送信
    pub fn distribute_fn(&mut self, to_id: AccountId, amount: u128) {
    }
}

FT機能はft.rsに定義。

※ロジックについては省略

※ここでのトークンは実際に送信というわけではなく、このコントラクトの中でどのくらいのトークンを持ってるかを保持しているだけのコード。

Lesson 4. コンパイル/テスト

すでに記載した通り、メインとなるプログラムはlib.rsにある状態だが、ここにテストコードを追加して完成。

ちょっと気になるのは、テストコードはlib.rsからは分けた方がいいんじゃないかなと・・・

$ swanky contract compile astar_sns_contract

swankyでプロジェクトを作っているので、swanky contract compileでコンパイル実施。

$ cargo +nightly test

テストについては、コードのディレクトリまで行って(cd contracts/astar_sns_contract)コマンド実行。

AstarはPolkadotのパラチェーンなので、Metamaskでは動かない・・・と思われるので、Polkadot専用のウォレット作成が必要。

Polkadot-js extension, manage accounts for substrate based chains

This browser extension does one thing: it manages accounts and allows the signing of transactions with those accounts. It does not inject providers for use by dapps at this early point, nor does it perform wallet functions, e.g send funds. This extension is open source and the code is available on GitHub.

Chromeでインストールして完了。

まとめ

技術的には、

  • ink!、というかRustでバックエンド(コントラクト)実装
  • Next.jsでフロントエンド実装

していくということ。

WASM Meetupのときはソースコードをちゃんと読んでなかったが、今回はある程度読んでみたけど、プログラミング言語なのでクセはあるものの大体把握はできた。

とにかく、ベストプラクティスをサクッと知ってディレクトリ構成とかファイル構成とかを知って、わかりやすく作っていきたいなと思った。