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

lesson-2_フロントエンドのベースを作成しよう

🚅 フロントエンドのベースを実装しましょう

それでは実際にコードを書いてフロントエンドのベースとなるものを作成していきます。

ここでは初期設定で存在すると想定されるファイルを削除・編集することがあります。 もし削除するファイルがあなたのフォルダ構成の中に無かった場合は、 無視してください。 もし編集するファイルがあなたのフォルダ構成の中に無かった場合は、 新たにファイルを作成し編集内容のコードをそのままコピーしてください。

先に必要なファイルを用意する作業が少し長いですが、 最後にブラウザでUIを確認しますので、 コードが理解しづらかった部分はブラウザで表示した後に再度確認して頂ければと思います。

📁 stylesディレクトリ

stylesディレクトリにはcssのコードが入っています。 全てのページに適用されるよう用意されたglobal.cssと、 ホームページ用のHome.module.cssがあります。

Home.module.cssを削除してください。

global.css内に以下のコードを記述してください。

html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu,
Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
background-color: rgb(50, 158, 50);
color: white;
}

a {
color: inherit;
text-decoration: none;
}

* {
box-sizing: border-box;
}

@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}

stylesに関するフォルダ構成はこのようになります。

client
└── styles
└── globals.css

📁 publicディレクトリ

Next.jsはルートディレクトリ直下のpublicディレクトリを静的なリソース(画像やテキストデータなど)の配置場所と認識します。 そのためソースコード内で画像のURLを/image.pngと指定した場合、 Next.jsは自動的にpublicディレクトリをルートとしたプロジェクトルート/image.pngを参照してくれます。

ディレクトリ内のfavicon.ico以外のファイルを全て削除してください。

また、 あなたのアプリのファビコンを変更したい場合はお好きな画像をfavicon.icoという名前で保存してください。

publicに関するフォルダ構成はこのようになります。

client
└── public
└── favicon.png

📁 utilsディレクトリ

clientへ移動しutilsディレクトリを作成してください。 その中にethereum.tsformatter.tsvalidAmount.tsというファイルを作成してください。

client
└── utils
├── ethereum.ts
├── formatter.ts
└── validAmount.ts

ethereum.tsの中に以下のコードを記述してください。

import { MetaMaskInpageProvider } from "@metamask/providers";

// window に ethereum を追加します。
declare global {
interface Window {
ethereum?: MetaMaskInpageProvider;
}
}

export const getEthereum = (): MetaMaskInpageProvider | null => {
if (typeof window !== "undefined" && typeof window.ethereum !== "undefined") {
const { ethereum } = window;
return ethereum;
}
return null;
};

型定義に厳格なtypescriptでwindow.ethereumを使用するためには、 windowethereumオブジェクトがあるということを明示する必要があります。 MetaMaskInpageProviderは環境設定時にインストールした@metamask/providersから取得したethereumの型定義です。

📓 window.ethereumとは Web アプリケーション上でユーザーがブロックチェーンネットワークと通信するためには、 Web アプリケーションはユーザーのウォレット情報を取得する必要があります。

window.ethereumは MetaMask がwindow(JavaScript にデフォルトで存在するグローバル変数)の直下に用意するオブジェクトであり API です。 この API を使用して、 ウェブサイトはユーザーのイーサリアムアカウントを要求し、 ユーザーが接続しているブロックチェーンからデータを読み取り、 ユーザーがメッセージや取引に署名するよう求めることができます。

また、 getEthereum関数を呼び出すとwindowから取り出したethereumオブジェクトを取得できるようにしています。

formatter.tsの中に以下のコードを記述してください。

import { BigNumber, ethers } from "ethers";

export const weiToAvax = (wei: BigNumber) => {
return ethers.utils.formatEther(wei);
};

export const avaxToWei = (avax: string) => {
return ethers.utils.parseEther(avax);
};

export const blockTimeStampToDate = (timeStamp: BigNumber) => {
return new Date(timeStamp.toNumber() * 1000); // milliseconds to seconds
};

weiToAvax(or avaxToWei)はweiAVAXの単位変換を行なっています。 ※ APIでは「1 AVAX = 10^18 wei」で単位変換がされているため、 formatEther(or parseEther)を使用できます。

また、 blockTimeStampToDateはsolidity内のblock.timestampから、 フロントエンドで使用するDateへの変換を行なっています。 block.timestampは単位がミリ秒で、 Dateは秒単位の時間を元に作成するので* 1000を行なっています。

validAmount.tsの中に以下のコードを記述してください。

const regValidNumber = /^[0-9]+[.]?[0-9]*$/;

export const validAmount = (amount: string): boolean => {
if (amount === "") {
return false;
}
if (!regValidNumber.test(amount)) {
return false;
}
return true;
};

ここではユーザの入力をバリデートする関数を用意しています。

📁 hooksディレクトリ

clientディレクトリ直下にhooksというディレクトリを作成しましょう。 こちらにはウォレットやコントラクトの状態を扱うようなカスタムフック(独自で作ったフック)を実装したファイルを保存します。

hooksディレクトリ内にuseWallet.tsというファイルを作成し、 以下のコードを記述してください。

import { useCallback, useEffect, useState } from "react";

import { getEthereum } from "../utils/ethereum";

type ReturnUseWallet = {
currentAccount: string | undefined;
connectWallet: () => void;
};

export const useWallet = (): ReturnUseWallet => {
const [currentAccount, setCurrentAccount] = useState<string>();
const ethereum = getEthereum();

const connectWallet = async () => {
try {
if (!ethereum) {
alert("Get Wallet!");
return;
}
const accounts = await ethereum.request({
method: "eth_requestAccounts",
});
if (!Array.isArray(accounts)) return;
console.log("Connected: ", accounts[0]);
setCurrentAccount(accounts[0]); // 簡易実装のため, 配列の初めのアドレスを使用します。
} catch (error) {
console.log(error);
}
};

const checkIfWalletIsConnected = useCallback(async () => {
try {
if (!ethereum) {
console.log("Make sure you have Wallet!");
return;
} else {
console.log("We have the ethereum object", ethereum);
}
const accounts = await ethereum.request({ method: "eth_accounts" });
if (!Array.isArray(accounts)) return;
if (accounts.length !== 0) {
const account = accounts[0];
console.log("Found an authorized account:", account);
setCurrentAccount(account);
} else {
console.log("No authorized account found");
}
} catch (error) {
console.log(error);
}
}, [ethereum]);

useEffect(() => {
checkIfWalletIsConnected();
}, [checkIfWalletIsConnected]);

return {
currentAccount,
connectWallet,
};
};

ここでは、 ユーザがMetamaskを持っていることの確認とウォレットへの接続機能を実装します。

connectWalletはWebアプリがユーザのウォレットにアクセスすることを求める関数で、 この後の実装でUIにユーザのウォレット接続ボタンを用意し、 そのボタンとこの関数を連携します。 そのため外部で使用できるように返り値の中に含めています。

checkIfWalletIsConnectedは既にユーザのウォレットとWebアプリが接続しているかを確認する関数で、

また、 それぞれの関数内で使用しているeth_requestAccountseth_accountsは、空の配列または単一のアカウントアドレスを含む配列を返す特別なメソッドです。 ユーザーがウォレットに複数のアカウントを持っている場合を考慮して、 プログラムはユーザーの1つ目のアカウントアドレスを取得することにしています。

hooksに関するフォルダ構成はこのようになります。

client
└── hooks
└── useWallet.ts

📁 contextディレクトリ

clientディレクトリ直下にcontextというディレクトリを作成しましょう。

contextディレクトリ内にCurrentAccountProvider.tsxというファイルを作成し、 以下のコードを記述してください。

import { createContext, ReactNode } from "react";

import { useWallet } from "../hooks/useWallet";

const CurrentAccountContext = createContext<[string | undefined, () => void]>([
"",
() => {},
]);

export const CurrentAccountProvider = ({
children,
}: {
children: ReactNode;
}) => {
const { currentAccount, connectWallet } = useWallet();

return (
<CurrentAccountContext.Provider value={[currentAccount, connectWallet]}>
{children}
</CurrentAccountContext.Provider>
);
};

export default CurrentAccountContext;

ここでは、 コンテキストを用意しています。 コンテキストは複数のコンポーネント間を跨いで値を渡せるもので、 値にはstateも渡せます。

はじめに以下の部分で先ほど作成したuseWallet()を使用してウォレット接続に関わるオブジェクトを取得しています。

const { currentAccount, connectWallet } = useWallet();

重要なのは以下です。 valueに、 取得したオブジェクトを渡します。

<CurrentAccountContext.Provider value={[currentAccount, connectWallet]}>
{children}
</CurrentAccountContext.Provider>

するとこのコンポーネントの子となるコンポーネント(children)以下ではどのコンポーネントでもコンテキストからvalueを取得することができます。 複数のコンポーネント間でstateを共有できます。

client
└── context
└── CurrentAccountProvider.tsx

🚀 pagesディレクトリのファイルになってしまいますが、 作成したコンテキストに関わるのでここでpages/_app.tsxを編集しましょう。

_app.tsx内に以下のコードを記述してください。

import type { AppProps } from "next/app";

import { CurrentAccountProvider } from "../context/CurrentAccountProvider";
import "../styles/globals.css";

function MyApp({ Component, pageProps }: AppProps) {
return (
<CurrentAccountProvider>
<Component {...pageProps} />
</CurrentAccountProvider>
);
}

export default MyApp;

_app.tsxファイルは標準で全てのページの親コンポーネントとなります。

ここでCurrentAccountProviderを使用し、 全てのコンポーネントがCurrentAccountProviderの子コンポーネントとなります。

つまり本プロジェクトで作成する全てのコンポーネントで、 currentAccountの参照とconnectWalletの実行ができます。

📁 componentsディレクトリ

clientディレクトリ直下にcomponentsという名前のディレクトリを作成してください。 こちらにはコンポーネントを実装したファイルを保存していきます。

📓 コンポーネントとは UI(ユーザーインターフェイス)を形成する一つの部品のことです。 コンポーネントはボタンのような小さなものから、ページ全体のような大きなものまであります。 レゴブロックのようにコンポーネントのブロックで UI を作ることで、 機能の追加・削除などの変更を容易にすることができます。

📁 Buttonディレクトリ

ここでは貼り付けるコード量が多いので、 本プロジェクトの packages/client/componentsを参照します。

componentsディレクトリ内からButtonディレクトリをそのままコピーして貼り付けてください。 本レポジトリ自体をローカルにクローンしてからコピーしたほうがやりやすいかもしれません。

Buttonに関するフォルダ構成はこのようになります。

client
└── components
└── Button
├── ActionButton.module.css
├── ActionButton.tsx
├── LinkToPageButton.module.css
└── LinkToPageButton.tsx

ActionButton.tsxLinkToPageButton.tsxはボタンのコンポーネントになります。

~.module.cssはそれぞれのcssになります。

📓 ~.module.cssとは module.cssを css ファイルの語尾に付けることで、 CSSモジュールというNext.jsの仕組みを利用することができます。 CSSモジュールはファイル内のクラス名を元にユニークなクラス名を生成してくれます。 内部で自動的に行ってくれるので私たちがユニークなクラス名を直接使用することがありませんが、 クラス名の衝突を気にする必要がなくなります。 異なるファイルで同じ CSS クラス名を使用することができます。 詳しくはこちらをご覧ください。

この後のコンポーネントを使用します。

📁 Containerディレクトリ

同じく本プロジェクトの packages/client/componentsを参照します。

componentsディレクトリ内からContainerディレクトリをそのままコピーして貼り付けてください。

Containerに関するフォルダ構成はこのようになります。

client
└── components
└── Container
├── FarmerContainer.module.css
├── FarmerContainer.tsx
├── HomeContainer.module.css
└── HomeContainer.tsx

FarmerContainer.tsxでは農家が触るUIのベースとなるものが記載されています。 activeTabを変更することで表示する内容がTokenize or ViewBuyersのどちらかに変更できるようになっております。

HomeContainer.tsxではホームページのUIのベースとなるものが記載されています。 先ほど作成したButton/LinkToPageButtonのボタンにそれぞれのページへのパスを渡しリンクするようにしています。 ページはこの後作成します。

📁 Layoutディレクトリ

同じく本プロジェクトの packages/client/componentsを参照します。

componentsディレクトリ内からLayoutディレクトリをそのままコピーして貼り付けてください。

Layoutに関するフォルダ構成はこのようになります。

client
└── components
└── Layout
├── DefaultLayout.module.css
└── DefaultLayout.tsx

DefaultLayout.tsxでは全てのページのレイアウトとなるコンポーネントです。

はじめにCurrentAccountContextからcurrentAccountconnectWalletを取得しています。

currentAccountにユーザのアドレスが保存されていればUI画面右上にアドレスを表示し、 未定義ならconnectWalletを実行するボタンを表示します。

📁 Formディレクトリ

componentsディレクトリ内にFormディレクトリを作成してください。

Formディレクトリ内の実装は後のセクションで実装するのでここで簡易実装をします。

以下のファイルを作成してください。

  • ListNftForm.tsx
  • TokenizeForm.tsx
  • ViewBuyersForm.tsx

ListNftForm.tsx内に以下のコードを貼り付けてください。

export default function ListNftForm() {
return <div>ListNftForm</div>;
}

TokenizeForm.tsx内に以下のコードを貼り付けてください。

export default function TokenizeForm() {
return <div>TokenizeForm</div>;
}

ViewBuyersForm.tsx内に以下のコードを貼り付けてください。

export default function ViewBuyersForm() {
return <div>ViewBuyersForm</div>;
}

Formに関するフォルダ構成はこのようになります。

client
└── components
└── Form
├── ListNftForm.tsx
├── TokenizeForm.tsx
└── ViewBuyersForm.tsx

📁 pagesディレクトリ

最後にclientディレクトリ直下のpagesディレクトリを編集していきます。

まず初めにapiディレクトリは今回使用しないのでディレクトリごと削除してください。

_app.tsxは既にコンテキストのところで編集しています。

_app.tsx以外のファイルに関して、 こちらに以下の3つのファイルがあるのでコピーしてください。

  • BuyerPage.tsx
  • FarmerPage.tsx
  • index.tsx

pagesに関するフォルダ構成はこのようになります。

client
└── pages
├── BuyerPage.tsx
├── FarmerPage.tsx
├── _app.tsx
└── index.tsx

index.tsx(ホームページ)・BuyerPage.tsxFarmerPage.tsxはそれぞれ1つのページのコンポーネントです。

全てのページで先ほど作成したDefaultLayoutを使用しているので同じレイアウトになります。

🖥️ 画面で確認しましょう

ターミナル上で以下のコマンドを実行してください!

yarn client dev

そしてブラウザでhttp://localhost:3000 へアクセスしてください。

以下のような画面が表示されれば成功です!

画面右上のConnect to walletボタンを押下するとウォレットと接続することができます。 ⚠️ この先ウォレットを接続する場合は、 ネットワークにFujiを選択した状態で行ってください。

MetaMaskの承認が終わると、 Connect to walletボタンの部分があなたの接続しているウォレットのアドレスの表示に変更されます。

For Farmerボタンを押すとページが切り替わります。

TokenizeViewBuyersボタンを切り替えると表示も切り替わります。

右下のBack to homeを押すとホームページに戻ります。 For Buyersボタンを押すとまたページが切り替わります。

🌔 参考リンク

こちらに本プロジェクトの完成形のレポジトリがあります。 期待通り動かない場合は参考にしてみてください。

🙋‍♂️ 質問する

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

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

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

フロントエンドのベースとなるコードが出来ました! 次のレッスンではコントラクトとフロントエンドを連携する作業に入ります!