メッセージの保存機能を実装しよう
🔥 スマートコントラクトに機能を追加しましょう
前回のレ ッスンでは、Messengerというスマートコントラクトを作成しました。
今回のレッスンでは以下の機能を実装します。
- メッセージを格納するデータ構造の用意
- メッセージの送信
- テスト
💁 これから実装するコントラクトの内部の処理をここで整理したいと思います。
プロジェクト内で出てくる「メッセージ」とは、テキストとそこに添付されたトークンを総称して指すこととします。 メッセージのテキストをメッセージテキスト、メッセージに添付されたトークンをメッセージトークンと呼ぶこととします。
メッセージのやり取りは、送信者から受信者へ直接ではなく、間にコントラクトを介して行われます。
- 送信者がメッセージの送信を行うと、メッセージテキストや関連情報がコントラクト内で保存されます。
- また、メッセージトークンはコントラクトへ送信されます。
- 送信されたメッセージはコントラクト内で保留という状態を取ります。
- その後メッセージの受信者が自分宛のメッセージの確認を行い、保留中のメッセージに対して以下の2つの動作を実行することができます。
- 承諾 -> メッセージトークンを受け取ることができます。コントラクトから受信者へトークンが送信されます。
- 拒否 -> メッセージトークンは返却されます。コントラクトから送信者へトークンが送信されます。
🪙 トークン
ブロックチェーン上には多くのトークンが存在しますが,
本プロジェクトで出てくるトークンとはAVAXかETHをさします。
AVAXはAvalancheのネイティブトークンで、ETHはEthereumのネイティブトークンです。
どちらも、それぞれのブロックチェーン上でトランザクションの際に手数料として必要です。
ブロックチェーンに新しく情報を書き込むことを,トランザクションと呼びます。
ブロックチェーンは、AWSのようなクラウド上にデータを保存できるサーバーのようなものです。
しかし、誰もそのデータを所有していません。
ブロックチェーン上にデータを保存する作業をする人々が世界中に存在します。
この作業に対して、私たちは代金を支払います。
その代金が、ガス代です。 AvalancheのC-Chain上にデータを書き込む際、私たちは代金としてAVAXを支払う必要があります。
本プロジェクトにおいて,
C-ChainのEVM互換という特性から,
スマートコントラクトの開発はEthereum上で動くものを作り、デプロイ先はAvalancheのC-Chainです。
スマートコントラクトの開発としてはEthereum上で動くものとしてETH(またはwei) の単位を基準に実装した方がわかりやすいです。
しかし、実際にトランザクションで支払われるトークンはAVAXなので、アプリ全体の話としてAVAXという言葉もしばし出てきます。
以上のことを覚えておいて下さい 🙌
実装段階でETHを使用してい るものが、トランザクション時にどのようにAVAXの単位に変化されるのかはフロントエンドの章でお話しします。
🏠 メッセージを格納するデータ構造を用意しよう
Messenger.sol内を以下のコードで書き換えて下さい。
// Messenger.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "hardhat/console.sol";
contract Messenger {
// メッセージ情報を定義します。
struct Message {
address payable sender;
address payable receiver;
uint256 depositInWei;
uint256 timestamp;
string text;
bool isPending;
}
// メッセージの受取人アドレスをkeyにメッセージを保存しま す。
mapping(address => Message[]) private _messagesAtAddress;
constructor() payable {
console.log("Here is my first smart contract!");
}
}
追加した内容を見ていきましょう!
struct Message {
address payable sender;
address payable receiver;
uint256 depositInWei;
uint256 timestamp;
string text;
bool isPending;
}
メッセージの情報を構造体として定義しています。
構造体の中には型 変数名;という形で要素が並んでいます。まずは各変数が何を表すのか整理しましょう。
- sender メッセージの送信者のアドレス。
- receiver メッセージの受取人のアドレス。
- depositInWei
メッセージトークンの量を表します。
単位を
weiで記録します。weiは、イーサリアムの最小額面です。1ETH = 1,000,000,000,000,000,000 wei (10^18)です。 - タイムスタンプ 送信者がメッセージを投稿したタイムスタンプを表します。
- text メッセージテキストを表します。
- isPending
メッセージが保留中であるかどうかを表します。
isPending = trueであれば保留中です。
次に型について整理します。
uint256: 非常に大きな数を扱うことができる符号なし整数を格納できます。string: 文字列データに使用できます。bool:trueかfalseのどちらかの値をとります。address: ethereumのアドレス(20バイト)を表す値に使用できます。address payable:addressと同じですが、加えてトークンのやり取りを可能にする型です。 具体的にはtransferとsendというメンバーが追加されています(後ほど出てきます 😏)。
// メッセージの受取人アドレスをkeyにメッセージを保存します。
mapping(address => Message[]) private _messagesAtAddress;
ここではメッセージの情報をmappingというデータ構造を利用して格納できるように定義しています。
📓
mappingSolidity のmappingは、ほかの言語におけるハッシュテーブルや辞書のような役割を果たします。 これらは、下記のように_Keyと_Valueのペアの形式でデータを格納するために使用されます。mapping(_Key => _Value)public mappingName
また、Message[]はMessageの配列を表します。
今回は、ユーザーのアドレス(= _Key = address)をそのユーザー宛のメッセージの集合(= _Value = Message[])に関連付けるためにmappingを使用しました。
constructor() payable {
console.log("Here is my first smart contract!");
}
constructorにpayableという修飾子が追加されています。
payableはトランザクションにトークンのやり取りが発生することを伝える関数修飾子です。
関数修飾子については後ほど整理します。
スマートコントラクト自体に資金としてトークンを持たせておく場合、スマートコントラクトのデ プロイ時にトークンも併せて送信します。
そのためデプロイ時に実行するconstructorにpayableをつける必要があります。
⚠️ 本プロジェクトで作成するスマートコントラクトは、ユーザ間のトークンやり取りを仲介するだけなので スマートコントラクト自体が資金を持っておく必要がなくなりました。 そのため
constructorにつけるpayableと、この後デプロイ時に行うトークンの送信は必要ありませんが, 本手順ではこのまま進めます。 🙌
続いて以下のように関数を追加していきます。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "hardhat/console.sol";
contract Messenger {
// メッセージ情報を定義します。
struct Message {
address payable sender;
address payable receiver;
uint256 depositInWei;
uint256 timestamp;
string text;
bool isPending;
}
// メッセージの受取人アドレスをkeyにメッセージを保存します。
mapping(address => Message[]) private _messagesAtAddress;
constructor() payable {
console.log("Here is my first smart contract!");
}
+ // ユーザからメッセージを受け取り、状態変数に格納します。
+ function post(string memory _text, address payable _receiver)
+ public
+ payable
+ {
+ console.log(
+ "%s posts text:[%s] token:[%d]",
+ msg.sender,
+ _text,
+ msg.value
+ );
+
+ _messagesAtAddress[_receiver].push(
+ Message(
+ payable(msg.sender),
+ _receiver,
+ msg.value,
+ block.timestamp,
+ _text,
+ true
+ )
+ );
+ }
+
+ // ユーザのアドレス宛のメッセージを全て取得します。
+ function getOwnMessages() public view returns (Message[] memory) {
+ return _messagesAtAddress[msg.sender];
+ }
}
追加した関数を見ていきましょう。
function post(string memory _text, address payable _receiver)
public
payable
{
console.log(
"%s posts text:[%s] token:[%d]",
msg.sender,
_text,
msg.value
);
_messagesAtAddress[_receiver].push(
Message(
payable(msg.sender),
_receiver,
msg.value,
block.timestamp,
_text,
true
)
);
}
メッセージの送信時 に呼び出される関数です。
メッセージはコントラクト内に蓄積されるデータなので、実際には送信者から受信者への送信というより、送信者からコントラクトへの投稿という意味合いが強いです。
そのためコントラクト内ではpostという関数名にしています。
post関数は引数にテキストデータと受信者のアドレスを受け取ります。
さらに関数呼び出し時にはトークン(メッセージトークンに当たります)が併せて送られるので、修飾子としてpayableを指定しています。
関数内ではログの出力と、メッセージ情報を格納しています。
_messagesAtAddress[_receiver].push(
Message(
payable(msg.sender), // 関数を呼び出したアドレス値をメッセージ送信者として記録します。
_receiver,
msg.value, // 関数呼び出し時に送信されたトークンの値をメッセージトークンとして記録します。
block.timestamp, // 投稿時のタイムスタンプ
_text,
true // 全てのメッセージは投稿直後は保留状態となるので、isPendingをtrueとします。
)
);
ここでは_messagesAtAddress[_receiver]により、受信者アドレス(_receiver)に紐ついたメッセージ配列(Message[])を取り出しています。
pushメソッドにより配列に新たなメッセージ情報を追加します。
msgはグローバルな変数で,
msg.valueによって関数呼び出し時に送信されたトークンの値を
msg.senderによって関数を呼び出しアカウントのアドレス値を取得することができます。
関数を呼び出す際にどのようにトークンを送信するのかはテストで行います。
// ユーザのアドレス宛のメッセージを全て取得します。
function getOwnMessages() public view returns (Message[] memory) {
return _messagesAtAddress[msg.sender];
}
getOwnMessages関数は
関数を呼び出したユーザのアドレス宛のメッセ ージを_messagesAtAddress[msg.sender]にアクセスすることで取得できるようにしています。
🎁 Solidity の関数修飾子について
ここではSolidityの用意する関数修飾子について簡単に整理したいと思います。
🪟 関数へのアクセスに関連する修飾子
-
public:publicで定義された関数や変数は、それらが定義されているコントラクト、そのコントラクトが継承された別のコントラクト、それらコントラクトの外部と、基本的にどこからでも呼び出すことができます。Solidityでは、アクセス修飾子がついてない関数を、自動的にpublicとして扱います。 -
private:privateで定義された関数や変数は、それらが定義されたコントラクトでのみ呼び出すことができます。 -
internal:internalで定義された関数や変数は、それらが定義されたコントラクトと、そのコントラクトが継承された別のコントラクトの両方から呼び出すことができます。Solidityでは、アクセス修飾子がついてない変数を、自動的にinternalとして扱います。 -
external:externalで定義された関数や変数は、外部からのみ呼び出すことができます。
以下に、Solidityのアクセス修飾子とアクセス権限についてまとめています。

📡 関数の状態に関連する修飾子
Solidity開発ではこれらの修飾子を意識しておかないとデータを記録する際のコスト(=ガス代)が跳ね上がってしまうので注意が必要です。
ここでポイントとなるのは、ブロックチェーンに値を書き込むにはガス代を払う必要があること、そしてブロックチェーンから値を参照するだけなら、ガス代を払う必要がないことです。
view:view関数は、読み取り専用の関数であり、呼び出した後に関数の中で定義された状態変数が変更されないようにします。pure:pure関数は、関数の中で定義された状態変数を読み込んだり変更したりせず、関数に渡されたパラメータや関数に存在するローカル変数のみを使用して値を返します。
以下に、Solidityの関数修飾子pureとviewについてまとめています。

ここで重要なのは、pureやview関数を使用すれば、ガス代を削減できるということです。
同時に、ブロックチェーン上にデータを書き込まないことで、処理速度も向上します。
🪙 関数がトークンをやり取りすることに関連する修飾子
関数にpayable