フロントエンドのベースを作成しよう
それでは実際にコードを書いてフロントエンドのベースとなるものを作成していきます。これから先の作業は、AVAX-AMM/packages/client
ディレクトリ内のファイルを操作していきます。🙌
ここでは初期設定で存在すると想定されるファイルを削除・編集することがあります。 もし削除するファイルがあなたのフォルダ構成の中に無かった場合は、無視してください。 もし編集するファイルがあなたのフォルダ構成の中に無かった場合は、新たにファイルを作成し編集内容のコードをそのままコピーしてください。
📁 styles
ディレクトリ
styles
ディレクトリにはcssのコードが入っています。
全てのページに適用されるよう用意されたglobal.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;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
body {
color: white;
background: black;
}
}
Home.module.css
内を以下のコードに変更してください。
.pageBody {
height: 100vh;
background: linear-gradient(
20deg,
rgb(49, 62, 80) 0%,
rgb(122, 153, 182) 180%
);
}
.navBar {
height: 80px;
display: flex;
justify-content: flex-start;
align-items: center;
color: white;
padding: 0px 30px;
}
.rightHeader {
display: flex;
padding: 5px 10px 5px 10px;
}
.appName {
margin: 0 10px;
font-size: 28px;
font-weight: 800;
}
.connectBtn {
position: absolute;
right: 50px;
top: 20px;
background-color: #ff726e;
color: #0e0e10;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
padding: 5px 10px 5px 10px;
border-radius: 15px;
}
.connectBtn:hover {
color: white;
border: 2px solid #c8332e;
}
.connected {
position: absolute;
right: 50px;
top: 20px;
background-color: #4e4b56;
color: white;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
padding: 5px 10px 5px 10px;
border-radius: 15px;
}
styles
に関するフォルダ構成はこのようになります。
client
└── styles
├── Home.module.css
└── globals.css
📁 public
ディレクトリ
Next.js
はルートディレクトリ直下のpublic
ディレクトリを静的なリソース(画像やテキストデータなど)の配置場所と認識します。
そのためソースコード内で画像のURLを/image.png
と指定した場合、
Next.js
は自動的にpublic
ディレクトリをルートとしたプロジェクトルート/image.png
を参照してくれます。
ディレクトリ内のfavicon.ico
以外のファイルを全て削除してください。
そして新たに画像を追加します。
以下の画像をダウンロードするか、あなたのお好きな画像をbird.png
(または別の名前)という名前でpublic
ディレクトリ内に保存してください。
また、favicon.ico
を別の画像にすると、あなたのwebアプリケーションのファビコンが変わるので自由に変更してみてください。
public
に関するフォルダ構成はこのようになります。
client
└── public
├── bird.png
└── favicon.png
📁 utils
ディレクトリ
client
へ移動しutils
ディレクトリを作成してください。
その中にethereum.ts
、format.ts
、validAmount.ts
というファイルを作成してください。
client
└── utils
├── ethereum.ts
├── format.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
を使用するためには、window
にethereum
オブジェクトがあるということを明示する必要があります。
MetaMaskInpageProvider
は環境設定時にインストールした@metamask/providers
から取得したethereum
の型定義です。
📓
window.ethereum
とは Web アプリケーション上でユーザーがブロックチェーンネットワークと通信するためには、Web アプリケーションはユーザーのウォレット情報を取得する必要があります。
window.ethereum
は MetaMask がwindow
(JavaScript にデフォルトで存在するグローバル変数)の直下に用意するオブジェクトであり API です。 この API を使用して、ウェブサイトはユーザーのイーサリアムアカウントを要求し、ユーザーが接続しているブロックチェーンからデータを読み取り、ユーザーがメッセージや取引に署名するよう求めることができます。
また、getEthereum
関数を呼び出すとwindow
から取り出したethereum
オブジェクトを取得できるようにしています。
format.ts
の中に以下のコードを記述してください。
import { BigNumber } from "ethers";
// PRECISIONありのshareに変換します。
export const formatWithPrecision = (
share: string,
precision: BigNumber
): BigNumber => {
return BigNumber.from(share).mul(precision);
};
// PRECISIONなしのshareに変換します。
export const formatWithoutPrecision = (
share: BigNumber,
precision: BigNumber
): string => {
return share.div(precision).toString();
};
ここではコントラクトとshareの情報をやり取りする際に使用するutil関数を用意しています。
shareについては一度離れていた部分なので、再確認したい方はsection-1/lesson-2のシェアについて
部分を読み返してください。
基本的にフロントエンドでは、shareをPRECISIONなしでstring型で保持します。
フロントエンド -> コントラクトへshareを伝える際は、formatWithPrecision
を使用し
コントラクト -> フロントエンドへshareが伝えられた際は、formatWithoutPrecision
を使用して変換を行います。
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 { 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_requestAccounts
とeth_accounts
は、空の配列または単一のアカウントアドレスを含む配列を返す特別なメソッドです。
ユーザーがウォレットに複数のアカウントを持っている場合を考慮して、プログラムはユーザーの1つ目のアカウントアドレスを取得することにしています。
hooks
に関するフォルダ構成はこのようになります。
client
└── hooks
└── useWallet.ts