時間が空いてしまいましたが、今回選んだのは「Astar-SocialFi」。
せっかくWASMも勉強したし、一旦Astarのアプリを構築してみようと思う。
まずはSection 0。
Lesson 1. チェーンの特徴とアプリ概要
技術的には、
を使います。
それぞれ別で記事を書いた方が良さそう。
アプリの概要としては、
- ログイン画面
- ホーム画面
- プロフィール画面
- メッセージ画面
を作っていくことになります。
Lessson 2. 環境構築
バックエンド
今回はWASM meetupで作った環境を流用。
- Rustのインストール
- swankyのインストール
astar-collatorというノードがあるっぽいが、今回はswanky-nodeで代用することとする。(できるのか???)
polkadot.jsを使って、ノードとつながれば問題なし。
フロントエンド
Next.jsを利用する。
「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
を適切にセットしないとビルドもできないので要注意。。。
ファイルを手動で作っていったら、動かなくて焦った、、、
*.rs
はsrc
配下に置きたかったので、諸々修正。
※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専用のウォレット作成が必要。
Chromeでインストールして完了。
まとめ
技術的には、
- ink!、というかRustでバックエンド(コントラクト)実装
- Next.jsでフロントエンド実装
していくということ。
WASM Meetupのときはソースコードをちゃんと読んでなかったが、今回はある程度読んでみたけど、プログラミング言語なのでクセはあるものの大体把握はできた。
とにかく、ベストプラクティスをサクッと知ってディレクトリ構成とかファイル構成とかを知って、わかりやすく作っていきたいなと思った。