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

lesson-2_mint機能実装の下準備をしよう

📝 コントラクトの情報の更新+NFT の mint の下準備を実装しよう

前回まででコントラクトに必要なデータは宣言することができました。

ここからはデータの初期化、NFTのmintの下準備を実装していきます!

まずはlib.rsへ移動して下のように書き換えましょう。 [lib.rs]

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::collections::{LazyOption, LookupMap, UnorderedMap, UnorderedSet};
use near_sdk::json_types::U128;
use near_sdk::serde::{Deserialize, Serialize};
use near_sdk::{env, near_bindgen, AccountId, Balance, CryptoHash, PanicOnDefault, Promise};

mod vote;
mod enumeration;
mod internal;
mod metadata;
mod mint;
mod nft_core;

pub use crate::enumeration::*;
use crate::internal::*;
pub use crate::metadata::*;
pub use crate::mint::*;
pub use crate::nft_core::*;
pub use vote::*;

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Contract {
// contract state value
pub owner_id: AccountId,
pub tokens_per_owner: LookupMap<AccountId, UnorderedSet<TokenId>>,
pub tokens_per_kind: LookupMap<TokenKind, UnorderedSet<TokenId>>,
pub tokens_by_id: LookupMap<TokenId, TokenOwner>,
pub token_metadata_by_id: UnorderedMap<TokenId, TokenMetadata>,
pub metadata: LazyOption<NFTContractMetadata>,
pub token_id_counter: u128,
pub likes_per_candidate: LookupMap<TokenId, Likes>,
pub added_voter_list: LookupMap<ReceiverId, TokenId>,
pub voted_voter_list: LookupMap<ReceiverId, u128>,
pub is_election_closed: bool,
}

#[derive(BorshSerialize)]
pub enum StorageKey {
TokensPerOwner,
TokensPerKind,
TokensPerOwnerInner { account_id_hash: CryptoHash },
TokensPerKindInner { token_kind: TokenKind },
TokensById,
TokenMetadataById,
TokensPerTypeInner { token_type_hash: CryptoHash },
NFTContractMetadata,
LikesPerCandidate,
AddedVoterList,
VotedVoterList,
}

+ #[near_bindgen]
+ impl Contract {
+ // function for initialization(new_default_meta)
+ #[init]
+ pub fn new(owner_id: AccountId, metadata: NFTContractMetadata) -> Self {
+ let this = Self {
+ owner_id,
+ tokens_per_owner: LookupMap::new(StorageKey::TokensPerOwner.try_to_vec().unwrap()),
+ tokens_per_kind: LookupMap::new(StorageKey::TokensPerKind.try_to_vec().unwrap()),
+ tokens_by_id: LookupMap::new(StorageKey::TokensById.try_to_vec().unwrap()),
+ token_metadata_by_id: UnorderedMap::new(
+ StorageKey::TokenMetadataById.try_to_vec().unwrap(),
+ ),
+ metadata: LazyOption::new(
+ StorageKey::NFTContractMetadata.try_to_vec().unwrap(),
+ Some(&metadata),
+ ),
+ token_id_counter: 0,
+ likes_per_candidate: LookupMap::new(
+ StorageKey::LikesPerCandidate.try_to_vec().unwrap(),
+ ),
+ added_voter_list: LookupMap::new(StorageKey::AddedVoterList.try_to_vec().unwrap()),
+ voted_voter_list: LookupMap::new(StorageKey::VotedVoterList.try_to_vec().unwrap()),
+ is_election_closed: false,
+ };
+ this
+ }
+
+ // initialization function
+ #[init]
+ pub fn new_default_meta(owner_id: AccountId) -> Self {
+ Self::new(
+ owner_id,
+ NFTContractMetadata {
+ spec: "nft-1.0.0".to_string(),
+ name: "Near Vote Contract".to_string(),
+ reference: "This contract is design for fair election!".to_string(),
+ },
+ )
+ }
+ }

はじめの#[near_bindgen]はnearのチェーンで有効な構造体、関数を宣言できるようにするためのものです。

次のimpl Contractについてですが、これはContractという構造体に{}内のメソッドを持たせるということを意味しています。

#[near_bindgen]
impl Contract

その次にある#[init]は初期化のための関数であることを示しています。lesson-1で説明したDefaultOnPanicというトレイトを覚えているでしょうか? デプロイした後に初期化の関数をまず動かさないと、他の関数を走らせることはできないというものです。

このトレイトは#[init]という印が付いている関数を初期化のための関数と認識しています。

ここで宣言しているnew関数では、引数としてAccountId型のowner_idという変数を必要とすることがわかります。metadataという変数についても同じことです。

そして->SelfというのはContractという構造体自身を返すということです。これはコントラクト自体のインスタンスを関数内で生成して、それを返り値として返すということです。

#[init]
pub fn new(owner_id: AccountId, metadata: NFTContractMetadata) -> Self {

中身ではそれぞれの変数の初期化をしています。

例えばtokens_per_ownerを例にとると、LookupMapという型が持つnewというメソッドによって初期化されて新しいインスタンスが生み出されます。

その後try_to_vec()によってResult型のベクター(他の言語では配列)が作られて、unwrap()というメソッドによって Result<Vec<u8>>->Vec<u8>に変換されます。くわしくはResult の説明unwrap の説明をご覧ください

UnorderedMapLookupMapはどちらもMap形式の型なのですがUnorderedMapはそれぞれのmapがインデックス化されておりベクター型のmapなのですが、LookupMapはインデックス化されておらず、mapだけが存在しているものです。

最後のthisというのはここで宣言したthisという変数を返り値とすることを示しています。rustではreturnを明示的に使わなくても、最後に返したい値を;なしで記述すれば暗黙的に返り値なんだと解釈してくれます。

{
let this = Self {
owner_id,
tokens_per_owner: LookupMap::new(StorageKey::TokensPerOwner.try_to_vec().unwrap()),
tokens_per_kind: LookupMap::new(StorageKey::TokensPerKind.try_to_vec().unwrap()),
tokens_by_id: LookupMap::new(StorageKey::TokensById.try_to_vec().unwrap()),
token_metadata_by_id: UnorderedMap::new(
StorageKey::TokenMetadataById.try_to_vec().unwrap(),
),
metadata: LazyOption::new(
StorageKey::NFTContractMetadata.try_to_vec().unwrap(),
Some(&metadata),
),
token_id_counter: 0,
likes_per_candidate: LookupMap::new(
StorageKey::LikesPerCandidate.try_to_vec().unwrap(),
),
added_voter_list: LookupMap::new(StorageKey::AddedVoterList.try_to_vec().unwrap()),
voted_voter_list: LookupMap::new(StorageKey::VotedVoterList.try_to_vec().unwrap()),
is_election_closed: false,
};

this
}

次のnew_default_meta関数はコントラクトのオーナーのWallet Idを引数として受け取ります。Self::newというのはSelfつまりこのコントラクト自体がもつnewという関数を呼び出していることを表しています。

これはnew_default_meta関数を呼び出すことでnew関数new_default_meta関数の両方を読んでいることになるので、一度で初期化が完了していることを意味しています。なのでnew関数を読んでないけど初期化できているの? と疑問に思う時がきてもこのような理由で完了していることを理解しておいてください。

中身ではspec,name,referenceの3つのコントラクトに関する情報とowner_idを更新しています。

ここでowner_idに何も値が入っていないと疑問に思った方もいるかもしれませんが大丈夫です。rustでは引数と同じものであれば省略できるというルールがあるからです。なのでここでは引数として入れられた値がそのままowner_idに入っていることになります。

それ以外の変数は好きなように変えていただいて構いません。

ただ、ここでどの変数にもto_string()というメソッドが適用されていることに疑問を抱いた方が多いと思います。その解説についてはこちらをご覧ください。

#[init]
pub fn new_default_meta(owner_id: AccountId) -> Self {
Self::new(
owner_id,
NFTContractMetadata {
spec: "nft-1.0.0".to_string(),
name: "Near Vote Contract".to_string(),
reference: "This contract is design for fair election!".to_string(),
},
)
}

これでコントラクトの初期化機能の実装は完了しました。次のレッスンではNFTをmint,transfer機能を実装しましょう。

🎭 NFT を mint, transfer しよう

まずはmint機能を実装するためにmint.rsに移動して以下のコードを記述しましょう。

エラーが発生しているとおもいますがそれは他のファイルで定義すべきメソッドを定義していないことが原因なので今は気にしないで大丈夫です。

[mint.rs]

+ use crate::*;
+
+ #[near_bindgen]
+ impl Contract {
+ #[payable]
+
+ //mint token
+ pub fn nft_mint(&mut self, mut metadata: TokenMetadata, receiver_id: AccountId) {
+ // set token id
+ assert!(
+ !(&self.is_election_closed),
+ "You can add candidate or voter because this election has been closed!"
+ );
+ metadata.token_id = Some(self.token_id_counter);
+ let initial_storage_usage = env::storage_usage();
+ let receiver_id_clone = receiver_id.clone();
+ let token = TokenOwner {
+ owner_id: receiver_id,
+ };
+ let token_id = self.token_id_counter;
+ let token_kind = metadata.token_kind.clone();
+
+ assert!(
+ self.tokens_by_id
+ .insert(&self.token_id_counter, &token)
+ .is_none(),
+ "Token already exists"
+ );
+
+ // add info(key: receiver_id, value: token metadata ) to map
+ self.token_metadata_by_id
+ .insert(&self.token_id_counter, &metadata);
+
+ // add info(key: receiver id, value: token id ) to map
+ self.internal_add_token_to_owner(&token.owner_id, &token_id);
+
+ // add info(key: token id, value: token kind ) to map
+ self.internal_add_token_to_kind_map(&token_id, token_kind);
+
+ // add data(key: token id, value: number of likes)
+ self.likes_per_candidate
+ .insert(&self.token_id_counter, &(0 as Likes));
+
+ // add info(key: receiver id, value: token id ) to map(-> this list is for check voter get vote ticket)
+ self.added_voter_list
+ .insert(&receiver_id_clone, &self.token_id_counter);
+
+ // increment token id counter
+ self.token_id_count();
+
+ // calculate storage user used
+ let required_storage_in_bytes = env::storage_usage() - initial_storage_usage;
+
+ // refund unused payment deposit
+ refund_deposit(required_storage_in_bytes);
+ }
+
+ // count token id
+ pub fn token_id_count(&mut self) {
+ self.token_id_counter = self.token_id_counter + 1;
+ }
+
+ // get next token id
+ pub fn show_token_id_counter(&self) -> u128 {
+ self.token_id_counter
+ }
+ }

次にnft_mintという関数について説明します。この関数では引数としてNFTの情報、受け取るユーザーのWallet Idをもらいます。

初めの#[payable]はtokenを授受できるようにするための注釈です。

#[payable]

//mint token
pub fn nft_mint(&mut self, mut metadata: TokenMetadata, receiver_id: AccountId)

関数の中身としてはまずコントラクトのis_election_closedという変数がfalseである、つまりまだ投票が終了していないことを確認して、もししまっている(true)のときはもう投票できないというメッセージをコンソールに出力します。

// set token id
assert!(
!(&self.is_election_closed),
"You can add candidate or voter because this election has been closed!"
);

次の部分ではmetadataという引数のあるプロパティを更新します。最終的にこの値はNFTのメタデータとしてコントラクトに格納されることになります。このmetadataのうちのtoken_idというプロパティを更新します。

入る値としてはコントラクトで独立に定義されているtoken_id_counterという変数の値が入ることになります。selfはこのコントラクト自体を示すのでself.token_id_counterと記述されています。この値をSome()で囲んでいるのはtoken_idがOption型だからです。

Option型というのは値があればSome(値)となり、値が存在しない場合はNoneとなるものです。これで値が存在しないストレージにアクセスするというバグがなくなるのです。詳しくはこちらをご覧ください。

metadata.token_id = Some(self.token_id_counter);

この次の部分では関数で使う変数を定義しています。 次のinitial_storage_usageという変数にはstorage_usage()という関数でその時点でのコントラクトが占有しているストレージの領域が格納されます。これはmintによって占有されたストレージの領域を計算するために使われます。

receiver_id_cloneには引数として受け取ったreceiver_idを格納されています。 clone()というメソッドはデータの値をコピーして、全く新しいストレージにそのデータを格納することを示しています。これは引数として受け取ったreceiver_idの所有権が次に宣言するtokenに移動してしまうため、その前に値をコピーして他のストレージに移動するという意図があって行われています。

tokenという変数はトークンのオーナーを格納しています。

token_idにはコントラクトに入れられているtokenのidが格納されています。

token_kindには引数として受け取ったmetadataに格納されているtoken_kindというプロパティが入れられます。

let initial_storage_usage = env::storage_usage();
let receiver_id_clone = receiver_id.clone();
let token = TokenOwner {
owner_id: receiver_id,
};
let token_id = self.token_id_counter;
let token_kind = metadata.token_kind.clone();

次のassert!()というメソッドではmintするユーザーが所有するNFTの内、これからmintするNFTのtokenのidを持つものが含まれていないかを確認しています。これは二重にmintすることを防いでいます。

assert!(
self.tokens_by_id
.insert(&self.token_id_counter, &token)
.is_none(),
"Token already exists"
);

次の部分ではtokenのidとNFTのmetadataを紐づけるためのマップであるtoken_metadata_by_idにデータを格納しています。

self.token_metadata_by_id.insert(&self.token_id_counter, &metadata);

この2つの関数はinternal.rsで定義する関数です。

1つ目のinternal_add_token_to_ownerという関数はtokenのidとその保有者のWallet Idを紐づけるためのmapであるtokens_per_ownerにそれぞれの値を格納する関数です。

次のinternal_add_token_to_kind_mapはtokenのidとその種類(候補者のNFTか投票券のNFTか)を紐づけるmapであるtokens_per_kindにそれぞれの値を格納する関数です。

self.internal_add_token_to_owner(&token.owner_id, &token_id);
self.internal_add_token_to_kind_map(&token_id, token_kind);

ここでは候補者のtokenのidとその得票数を紐づけるmapであるlikes_per_candidateにそれぞれの値を格納しています。

self.likes_per_candidate.insert(&self.token_id_counter, &(0 as Likes));

ここでは投票者とそのtokenのidを紐づけるためのベクターにそれぞれの値を格納しています。

self.added_voter_list.insert(&receiver_id_clone, &self.token_id_counter);

これは次の部分で記述する関数を呼び出しており、次にmintされるNFTのためにtokenのidをインクリメント(1大きくすること)しています。

self.token_id_count();

この変数は今のコントラクトが占有しているストレージの領域からmint前に占有していた領域を引くことでmintによって占有されたストレージの領域を導き出しています。

let required_storage_in_bytes = env::storage_usage() - initial_storage_usage;

この関数はinternal.rsファイルに宣言する関数で、ユーザーがmint時にdepositしてくれたNEAR(暗号通貨)に対して多すぎた場合に返金する関数です。

refund_deposit(required_storage_in_bytes);

🙋‍♂️ 質問する

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

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

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

これでmintの下準備はできました! しかしまだ実装されていない関数があるので次のレッスンでmint機能を完成させましょう!