【Block Chain入門】ReactでTODOアプリを作ってみる【Smart Contract】
前書き
jhcoderです。今回はかねてから興味のあったBlock Chain技術に触れてみたので自分の備忘録兼アウトプットとして記事を書きました。まずどんな技術も最初はTODOアプリだろうと思い、題材としております。多くのエンジニアがBlock Chainって難しそう...と思い避けてしまいがちだとは思いますがこの記事を読んで意外と簡単に触れるんだな!アプリ作れるんだな!と思っていただけたら嬉しいです。なので今回はあまり周辺技術について細かい説明はなく、TODOアプリを作成するのに必要最低限の説明となってます。より興味が湧いた方は最新の文献はほぼ英語ですがぐぐってみてください。
今回書くこと
- solidityを用いて簡易的なコントラクトの書き方
- Reactを用いたTODOアプリの作り方
今回書かないこと
- ブロックチェーンに関する詳しい話
- イーサリアムについての詳しい話
- solidityの文法
まずBlock Chainを用いて開発をする際に必要となる知識を簡潔に紹介し、そのあと実際にTODOアプリ作成の説明をしていきます。
導入
スマートコントラクトとは?
まずよく耳にするけどそれってなんなの?というスマートコントラクトについて説明します。
スマートコントラクトとは、ブロックチェーン上で契約を自動的に実行する仕組みのことです。
よく自動販売機で例えられることが多いですが、スマートコントラクトの考え方というものは特段新しいものではありません。ユーザーが自動販売機にお金を入れて飲み物のボタンを押すとそのタイミングで売買契約が成立し飲み物が手に入る、といったイメージが近いです。事前に定義された契約が存在していて、イベントが実行されるとそこから自動で契約が執行され、所有権が移動するといった流れが行われているだけです。このイベントが実行=>契約執行、所有権の移動が自動で行われるのでスマート(自動的に実行)コントラクト(契約)と呼ばれています。
詳しくは:https://bitflyer.com/ja-jp/s/glossary/smartcontract
次にブロックチェーンを扱う際に必要となるお財布について説明します。
Metamaskとは?
一般的に仮想通貨は仮想通貨専用のウォレットで管理し、ウォレットを用いてブロックチェーンとやり取りします。MetamaskはETH(イーサリアム系)の仮想通貨のウォレットの一つで、他にも torusというウォレットもあります。
今回はMetamaskを使用していきたいと思います。
Metamask導入
MetamaskはChrome拡張を用いてブラウザに導入するのが便利です。一応スマートフォンアプリも存在しており、そちらでも可能です。
公式: https://metamask.io/
Chrome拡張: https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn?hl=ja
①拡張機能を導入するとwelcomeページに飛ばされるのでそこからウォレットの作成を始めてください
②基本指示通りにCreate Walletを行い、進めて大丈夫です
③Secret Backup Phrase画面で12個の英単語が生成されます。この英単語はWalletを復元する際に必要なので安全に保管してください。次の画面でこの生成された単語の入力を求められるので必ずメモしておいてください。
④ウォレット作成完了です。
Solidityとは?
Solidityは、スマートコントラクトを作成するためのオブジェクト指向プログラミング言語です。さまざまなブロックチェーン上、特にEthereumでスマートコントラクトを実装するために使用されます。Ethereum上で実行する場合は、Ethereumネットワーク上でコントラクトコードという状態でブロックチェーン上に記録されたコードが実行されることで処理することができます。
contract句を記述し、Contract名を宣言することで、ブロックチェーン上に書き込まれた際、このcontract内に書かれた処理が、イーサリアムネットワーク上で動作します。
「CryptZombies」というサイトで無料で文法は学べるのでぜひ興味のある方はやってみてください。
Solidityの開発環境 Remix
Remixというコントラクト開発用のIDEを使用します。
MetaMaskの導入を行い、テストコインを取得したら、このIDEを用いてコントラクトを定義していきます。
実際のIDEは以下のURLから試せます。
テストネット
Ethereumネットワークには、本物のETHを使用するMainnetをはじめ様々なネットワークが存在します。今回は実際のETHは使わず動作テストを目的としたテストネットで検証しようと思います。テストネットではテストネット用ETHを使用します。まずこれを取得しましょう。今回はコンセンサスアルゴリズムにPoAが採用されているGoerli テストネットワークを使用します。Faucetページでアカウントのアドレスを投稿することでEtherを取得することができます。
- Goerli
ウォレットアドレスをTwitterで投稿し、そのページのアドレスをサイトのインプットに入力するとEtherが取得できます。MetamaskのGoerli テストネットワークを選択し、実際に保有しているか確認して見てください。以下の画像のように取得できていたら成功です。
実際にTODOアプリを作っていく
コントラクト作成
実際にTODOアプリで必要なコントラクトを定義します。
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// コントラクト名を定義
contract TodoApp {
uint num = 0;
// struct型は構造体を格納するデータ型です。
// Todoに符号なし整数型のtaskidと文字列型のtask、bool型のflagを格納しています。
struct Todo {
uint taskid;
string task;
bool flag;
}
// publicは内部か、メッセージ経由で呼び出し可能。publicなstate変数の場合、自動的にgetter関数が生成されます。
Todo[] public todoList;
// mappingを利用した配列のような処理が可能
mapping (uint => address) public todoToOwner;
mapping (address => uint) public ownerTodoCount;
// ☆タスク追加の関数
function createTodo(string memory _task) public {
todoList.push(Todo(num,_task,true));
uint id = todoList.length - 1;
todoToOwner[id] = msg.sender;
ownerTodoCount[msg.sender]++;
num++;
}
// ☆ コントラクトアドレスを用いてタスクのリストを取得する関数
// memoryは、処理中だけ保持され、終わったら保持されない。
function getTodoListByOwner(address owner) external view returns(uint[] memory) {
uint[] memory result = new uint[](ownerTodoCount[owner]);
uint counter = 0;
for (uint i = 0; i < todoList.length; i++) {
if (todoToOwner[i] == owner){
result[counter] = i;
counter++;
}
}
return result;
}
}
コンパイルする
サイドメニューの赤矢印の項目を押下し、
- COMPILER
- LANGUAGE
- EVM VERSION
を設定し、先ほど書いたsolidityのファイルをコンパイルします。Errorが出ず、緑のチェックマークが表出すればコンパイルは成功です。これでEthereumネットワーク上にコントラクトをdeployする準備が整いました。
デプロイして実際に動かしてみる
テストネットの環境が整ったのでRemix上で実際に作成したコントラクトをブロックチェーン上にデプロイします。メニューから DEPLOY & RUN TRANSACTIONSを選択し移動します。
設定は上から順に
- ENVIROMENTは Injectred Web3
- ACCOUNT はすでに自分のウォレットアドレスがセットされているはずです。
- GAS LIMITは一旦defaultの3000000で大丈夫です。
- CONTRACTに自分が先ほど作成したコントラクトがセットされていることを確認してください。
ここまで問題なければ「DEPLOY」を押下してデプロイを実行してください。
上記のように支払われるGAS代と合計の使用ETHが表示されます。これで問題ないので「確認」を押下でETHを支払いデプロイします。
デプロイが成功しました。この表示になっていればOKです。Deployed Contractsの項目からRemix上でデプロイしたコントラクトに生えている関数をcallすることができますので是非試して見てください。次はフロントエンドのコードを書き、実際にUIからコントラクトの関数をcallできるようにしていきたいと思います。
フロントエンドからスマコンを呼ぶ準備
ABIの作成
ABIとはApplication Binary Interfaceの略称で、スマートコントラクトの実行や変数の取得に必要な情報をまとめたものです。コントラクトのコンパイル成功時にABIが生成されるので、生成されたABIをRemixから取得します。メニューからSOLIDITY COMPILERへ移動し、一番下にあるABIを押下するとコンパイルしたコントラクトのABIをコピーできます。以下のようなファイルを作成し、ペーストします。
[
{
"inputs": [
{
"internalType": "string",
"name": "_task",
"type": "string"
}
],
// ~省略~
}
]
コントラクトアドレスを .envファイルに記載する
.env.developmentを作成し、そこにRemixからコピーしてきたContract Addressを記載します。
NEXT_PUBLIC_CONTRACT_ADDRESS = [Contract Address]
web3-reactを使用する
web3-reactは、dApps(Web 3.0アプリケーション)に関連する特定の重要なデータ(例えば、ユーザーの現在の口座)が最新の状態に保たれるようにするステートマシンのような役割を担ってくれます。Contextを使用してデータを効率的に保存し、アプリケーション内の必要な場所に注入することでこの役割を実現しています。
ではこのweb3-reactを用いてフロントエンドからコントラクトへアクセスできるようにしていきます。
Providerを設置する
Web3ReactProviderをimportし、設置します。次にgetLibrary関数を作成します。この関数は Web3Providerのインスタンスを生成しているもので(低レベルのプロバイダーからweb3コンビニエンス・ライブラリ・オブジェクトをインスタンス化する役割を担う関数)これは、Ethersやweb3.jsといったライブラリのどれを使用しているかによって記法が異なります。
こちらをWeb3ReactProviderに渡してあげればOKです。
import React from "react";
import type { AppProps } from "next/app";
import { ChakraProvider, localStorageManager } from "@chakra-ui/react";
import { Web3ReactProvider } from "@web3-react/core";
import { Web3Provider } from "@ethersproject/providers";
function getLibrary(provider: ExternalProvider | JsonRpcFetchFunc) {
return new Web3Provider(provider);
}
const MyApp: React.VFC<AppProps> = ({ Component, pageProps }) => {
return (
<Web3ReactProvider getLibrary={getLibrary}>
<ChakraProvider colorModeManager={localStorageManager}>
<Component {...pageProps} />
</ChakraProvider>
</Web3ReactProvider>
);
};
フロントエンドからスマートコントラクトを呼び出す
以下実際にタスクの追加と表示のみ可能なTODOアプリのコードになります。
import Layout from "../components/Layout";
import {
Box,
FormControl,
FormLabel,
Textarea,
Button,
} from "@chakra-ui/react";
import { useState, useEffect } from "react";
import { useWeb3React } from "@web3-react/core";
import { Web3Provider } from "@ethersproject/providers";
import { injected } from "../lib/web3/connectors";
import TODOABI from "../lib/abi/todo.json";
import { ethers } from "ethers";
import { useEagerConnect, useInactiveListener } from "../hooks/hooks";
const contractAddress = process.env.NEXT_PUBLIC_CONTRACT_ADDRESS || "";
const IndexPage: React.FC = () => {
const { activate, account, library, connector } =
useWeb3React<Web3Provider>();
const [task, setTask] = useState("");
const [taskList, setTaskList] = useState([""]);
// 現在アクティブになっているコネクターを認識するためのハンドルロジック
const [activatingConnector, setActivatingConnector] = useState<any>();
useEffect(() => {
if (activatingConnector && activatingConnector === connector) {
setActivatingConnector(undefined);
}
// taskListの読み込み
activate(injected).then(async () => {
activate(injected).catch((e) => {
console.error(e);
});
if (library) {
const contract = new ethers.Contract(
contractAddress,
TODOABI,
library.getSigner()
);
const taskList = await contract.functions.getTodoListByOwner(account);
const hexList = taskList[0].map((num, index) => {
return num._hex;
});
// 格納されている配列を展開して、todoList()の引数として渡す
const todoList = await Promise.all(
hexList.map(async (num: number) => {
// コントラクトのtodoListを呼び出す
return await contract.functions.todoList(num);
})
);
const result = todoList.map((todo) => {
return todo.task;
});
// 取得した値を使って、stateを変更する
setTaskList(result);
} else return;
});
}, [activatingConnector, connector]);
// 注入されたイーサリアムプロバイダーが存在し、すでにアクセスを許可している場合、イーサリアムに接続するためのロジックを処理する
const triedEager = useEagerConnect();
// 注入されたイーサリアムプロバイダー上の特定のイベントが存在する場合、そのイベントに反応して接続するロジックを処理する
useInactiveListener(!triedEager || !!activatingConnector);
// タスク追加の関数
const submitHandler = async () => {
await activate(injected).catch((e) => {
console.error(e);
});
if (!injected.supportedChainIds) return;
activate(injected).then(async () => {
if (library) {
const contract = new ethers.Contract(
contractAddress,
TODOABI,
library.getSigner()
);
await contract.functions.createTodo(task);
} else return;
});
};
return (
<Layout>
<Box
display="flex"
width="600px"
margin="auto"
justifyContent="center"
alignContent="center"
flexDirection="column"
>
<FormLabel margin="auto">👋 TODO App👋</FormLabel>
<Box>
{taskList?.map((item) => (
<Box key={item}>
{item}
</Box>
))}
</Box>
<FormControl>
<Textarea
value={task}
onChange={(e) => setTask(e.target.value)}
></Textarea>
</FormControl>
<Button type="submit" onClick={submitHandler}>
ボタン
</Button>
</Box>
</Layout>
);
};
export default IndexPage;
実際に出来上がったもの
ソースコードはこちら。https://github.com/yukiorita1117/call-smart-contract
参考文献
Discussion