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

lesson-4_フロントエンドを完成させよう

コンポーネントを追加しましょう

フロントエンドが完成に近づいてきました!

このレッスンでは残りのコンポーネントを実装してフロントエンドを完成させましょう。

各コンポーネント作成ごとにUIを確認していくので、AVAX-AMMディレクトリ直下にいることを確認しターミナル上で以下のコマンドを実行してください。

yarn client dev

📁 componentsディレクトリ

📁 SelectTabディレクトリ

componentsディレクトリ内にSelectTabというディレクトリを作成し、 その中にSelectTab.module.cssという名前のファイルを作成してください。

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

.tabBody {
margin: 0px auto;
width: 460px;
padding-top: 5px;
justify-content: center;
align-items: center;
background-color: #0e0e10;
border-radius: 0px 0px 19px 19px;
border-top: 0px;
}

.bottomDiv {
margin: 10px auto;
width: 30%;
padding: 5px;
justify-content: center;
align-items: center;
border-radius: 19px;
}

.btn {
background-color: #356c93;
margin: 10px 30px;
color: white;
display: flex;
justify-content: center;
align-items: center;
font-size: 18px;
width: 100px;
height: 40px;
border-radius: 9px;
cursor: pointer;
}

.btn:hover {
background: #57adea;
}

.swapIcon {
width: 5%;
text-align: center;
display: flex;
margin: 40px auto;
color: #ff726e;
}

.estimate {
height: 30px;
width: 75%;
margin: 0px auto;
margin-top: 10px;
margin-bottom: 30px;
color: white;
}

.error {
color: white;
display: flex;
justify-content: flex-start;
padding: 0px 20px;
}

🚰 Faucet

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

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

import { TokenType } from "../../hooks/useContract";
import { validAmount } from "../../utils/validAmount";
import InputNumberBox from "../InputBox/InputNumberBox";
import styles from "./SelectTab.module.css";

type Props = {
token0: TokenType | undefined;
token1: TokenType | undefined;
currentAccount: string | undefined;
updateDetails: () => void;
};

export default function Faucet({
token0,
token1,
currentAccount,
updateDetails,
}: Props) {
const [amountOfFunds, setAmountOfFunds] = useState("");
const [currentTokenIndex, setCurrentTokenIndex] = useState(0);

const [tokens, setTokens] = useState<TokenType[]>([]);

useEffect(() => {
if (!token0 || !token1) return;
setTokens([token0, token1]);
}, [token0, token1]);

// tokensの範囲内で、参照するインデックスを次に移動させます。
const onChangeToken = () => {
setCurrentTokenIndex((currentTokenIndex + 1) % tokens.length);
};

const onChangeAmountOfFunds = (amount: string) => {
setAmountOfFunds(amount);
};

async function onClickFund() {
if (!currentAccount) {
alert("connect wallet");
return;
}
if (tokens.length === 0) return;
if (!validAmount(amountOfFunds)) {
alert("Amount should be a valid number");
return;
}
try {
const contract = tokens[currentTokenIndex].contract;
const amountInWei = ethers.utils.parseEther(amountOfFunds);

const txn = await contract.faucet(currentAccount, amountInWei);
await txn.wait();
updateDetails(); // ユーザとammの情報を更新
alert("Success");
} catch (error) {
console.log(error);
}
}

return (
<div className={styles.tabBody}>
<div className={styles.bottomDiv}>
<div className={styles.btn} onClick={() => onChangeToken()}>
Change
</div>
</div>
<InputNumberBox
leftHeader={
"Amount of " +
(tokens[currentTokenIndex]
? tokens[currentTokenIndex].symbol
: "some token")
}
right={
tokens[currentTokenIndex] ? tokens[currentTokenIndex].symbol : ""
}
value={amountOfFunds}
onChange={(e) => onChangeAmountOfFunds(e.target.value)}
/>
<div className={styles.bottomDiv}>
<div className={styles.btn} onClick={() => onClickFund()}>
Fund
</div>
</div>
</div>
);
}

ここではあなたがデプロイした、USDCとJOEのfaucetを実装しています。

実装の中身を見る前にブラウザ上でUIを見てみましょう。

components/Container/Container.tsx内に以下の内容を追加してください。

import { useState } from 'react';

import { useContract } from '../../hooks/useContract';
import Details from '../Details/Details';
+ import Faucet from '../SelectTab/Faucet';
import styles from './Container.module.css';

type Props = {
currentAccount: string | undefined;
};

export default function Container({ currentAccount }: Props) {
// ...

return (
// ...

{activeTab === 'Swap' && <div>swap</div>}
{activeTab === 'Provide' && <div>provide</div>}
{activeTab === 'Withdraw' && <div>withdraw</div>}
+ {activeTab === 'Faucet' && (
+ <Faucet
+ token0={token0}
+ token1={token1}
+ currentAccount={currentAccount}
+ updateDetails={updateDetails}
+ />
+ )}
// ...
);
}

ブラウザでhttp://localhost:3000 へアクセスします。

Faucetタブをクリックすると以下のような表示がされます。

入力欄に10と入力し、Fundをクリックします。 トランザクションに署名し、しばらく待つと(ポップアップが表示されokを押した後)右側のYour DetailsのUSDCの部分が10増えているはずです。

ChangeボタンをクリックするとUSDC -> JOEへ変更されるため、JOEに関しても同じようにfaucetを利用することができます。

それではFaucet.tsxの中身を見ましょう。

使用する状態変数は以下の通りです。

const [amountOfFunds, setAmountOfFunds] = useState(""); // ユーザが指定した取得したいトークンの量を保持します。
const [currentTokenIndex, setCurrentTokenIndex] = useState(0); // 現在のtokens(この次にあります)のインデックスを保持します。

const [tokens, setTokens] = useState<TokenType[]>([]); // [token0, token1] のようにトークンオブジェクトが格納されます。

以下はユーザの操作によって動く関数の実装です。

// tokensの範囲内で、参照するインデックスを次に移動させます。
const onChangeToken = () => {
setCurrentTokenIndex((currentTokenIndex + 1) % tokens.length);
};

const onChangeAmountOfFunds = (amount: string) => {
setAmountOfFunds(amount);
};

async function onClickFund() {
if (!currentAccount) {
alert("connect wallet");
return;
}
if (tokens.length === 0) return;
if (!validAmount(amountOfFunds)) {
alert("Amount should be a valid number");
return;
}
try {
const contract = tokens[currentTokenIndex].contract;
const amountInWei = ethers.utils.parseEther(amountOfFunds);

const txn = await contract.faucet(currentAccount, amountInWei);
await txn.wait();
updateDetails(); // ユーザとammの情報を更新
alert("Success");
} catch (error) {
console.log(error);
}
}
  • onChangeToken: Changeボタンをクリックされた際にcurrentTokenIndexを変更します。 これによりUSDCとJOEの切り替えをします。
  • onChangeAmountOfFunds: ユーザの入力値をamountOfFundsにセットします。
  • onClickFund: Fundボタンをクリックされた際にcurrentTokenIndexのトークンのfaucet関数を呼び出します。 このコンポーエントの引数で渡されているupdateDetails()を実行することでdetailsを更新します。

🦜 Provide

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

import { BigNumber, ethers } from "ethers";
import { useCallback, useEffect, useState } from "react";
import { MdAdd } from "react-icons/md";

import { AmmType, TokenType } from "../../hooks/useContract";
import { validAmount } from "../../utils/validAmount";
import InputNumberBox from "../InputBox/InputNumberBox";
import styles from "./SelectTab.module.css";

type Props = {
token0: TokenType | undefined;
token1: TokenType | undefined;
amm: AmmType | undefined;
currentAccount: string | undefined;
updateDetails: () => void;
};

export default function Provide({
token0,
token1,
amm,
currentAccount,
updateDetails,
}: Props) {
const [amountOfToken0, setAmountOfToken0] = useState("");
const [amountOfToken1, setAmountOfToken1] = useState("");
const [activePool, setActivePool] = useState(true);

const checkLiquidity = useCallback(async () => {
if (!amm) return;
try {
const totalShare = await amm.contract.totalShare();
if (totalShare.eq(BigNumber.from(0))) {
setActivePool(false);
} else {
setActivePool(true);
}
} catch (error) {
alert(error);
}
}, [amm]);

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

const getProvideEstimate = async (
token: TokenType,
amount: string,
setPairTokenAmount: (amount: string) => void
) => {
if (!amm || !token0 || !token1) return;
if (!activePool) return;
if (!validAmount(amount)) return;
try {
const amountInWei = ethers.utils.parseEther(amount);
const pairAmountInWei = await amm.contract.getEquivalentToken(
token.contract.address,
amountInWei
);
const pairAmountInEther = ethers.utils.formatEther(pairAmountInWei);
setPairTokenAmount(pairAmountInEther);
} catch (error) {
alert(error);
}
};

const onChangeAmount = (
amount: string,
token: TokenType | undefined,
setAmount: (amount: string) => void,
setPairTokenAmount: (amount: string) => void
) => {
if (!token) return;
setAmount(amount);
getProvideEstimate(token, amount, setPairTokenAmount);
};

const onClickProvide = async () => {
if (!currentAccount) {
alert("connect wallet");
return;
}
if (!amm || !token0 || !token1) return;
if (!validAmount(amountOfToken0) || !validAmount(amountOfToken1)) {
alert("Amount should be a valid number");
return;
}
try {
const amountToken0InWei = ethers.utils.parseEther(amountOfToken0);
const amountToken1InWei = ethers.utils.parseEther(amountOfToken1);

const txn0 = await token0.contract.approve(
amm.contract.address,
amountToken0InWei
);
const txn1 = await token1.contract.approve(
amm.contract.address,
amountToken1InWei
);

await txn0.wait();
await txn1.wait();

const txn = await amm.contract.provide(
token0.contract.address,
amountToken0InWei,
token1.contract.address,
amountToken1InWei
);
await txn.wait();
setAmountOfToken0("");
setAmountOfToken1("");
checkLiquidity(); // プールの状態を確認
updateDetails(); // ユーザとammの情報を更新
alert("Success");
} catch (error) {
alert(error);
}
};

return (
<div className={styles.tabBody}>
<InputNumberBox
leftHeader={"Amount of " + (token0 ? token0.symbol : "some token")}
right={token0 ? token0.symbol : ""}
value={amountOfToken0}
onChange={(e) =>
onChangeAmount(
e.target.value,
token0,
setAmountOfToken0,
setAmountOfToken1
)
}
/>
<div className={styles.swapIcon}>
<MdAdd />
</div>
<InputNumberBox
leftHeader={"Amount of " + (token1 ? token1.symbol : "some token")}
right={token1 ? token1.symbol : ""}
value={amountOfToken1}
onChange={(e) =>
onChangeAmount(
e.target.value,
token1,
setAmountOfToken1,
setAmountOfToken0
)
}
/>
{!activePool && (
<div className={styles.error}>
Message: Empty pool. Set the initial conversion rate.
</div>
)}
<div className={styles.bottomDiv}>
<div className={styles.btn} onClick={() => onClickProvide()}>
Provide
</div>
</div>
</div>
);
}

ここではProvideタブを実装しています。

実装の中身を見る前にブラウザ上でUIを見てみましょう。

components/Container/Container.tsx内に以下の内容を追加してください。

import { useState } from 'react';

import { useContract } from '../../hooks/useContract';
import Details from '../Details/Details';
import Faucet from '../SelectTab/Faucet';
+ import Provide from '../SelectTab/Provide';
import styles from './Container.module.css';

type Props = {
currentAccount: string | undefined;
};

export default function Container({ currentAccount }: Props) {
// ...

return (
// ...

{activeTab === 'Swap' && <div>swap</div>}
+ {activeTab === 'Provide' && (
+ <Provide
+ token0={token0}
+ token1={token1}
+ amm={amm}
+ currentAccount={currentAccount}
+ updateDetails={updateDetails}
+ />
+ )}
{activeTab === 'Withdraw' && <div>withdraw</div>}
{activeTab === 'Faucet' && (
<Faucet
token0={token0}
token1={token1}
currentAccount={currentAccount}
updateDetails={updateDetails}
/>
)}
// ...
);
}

ブラウザでhttp://localhost:3000 へアクセスします。

Provideタブをクリックすると以下のような表示がされます。

入力欄にそれぞれ100と入力し、Provideをクリックします。

provideの実行では以下のトランザクションへの署名が必要です。

  • USDCのapprove
  • JOEのapprove
  • AMMのprovide

まず初めにMetamaskの署名画面が2度続けて表示されます。 それぞれに署名をし、しばらく待つと最後にprovideの署名について求められるので署名をします。 🙌 こちらのUI改善できる方がいらっしゃればUNCHAINにて共有して頂けると大変助かります!

しばらく待つと(ポップアップが表示されokを押した後)右側のYour Detailsが更新されます!

それではProvide.tsxの中身を見ましょう。

使用する状態変数は以下の通りです。

const [amountOfToken0, setAmountOfToken0] = useState(""); // ユーザが指定したtoken0の預けるトークンの量を保持します。
const [amountOfToken1, setAmountOfToken1] = useState(""); // ユーザが指定したtoken1の預けるトークンの量を保持します。
const [activePool, setActivePool] = useState(true); // プールに流動性があるのかをフラグで保持します。

次の2つの関数は、ユーザがどちらか一方(USDC or JOE)の入力欄に数値を入力した際に、もう片方に必要なトークンの量を表示するために必要です。 ※ 先ほどの挙動確認では最初の流動性提供であったためこの機能は確認できませんでした。もう1度Provideタブにて入力欄に数値を入力すると確認できるはずです!

const getProvideEstimate = async (
token: TokenType,
amount: string,
setPairTokenAmount: (amount: string) => void
) => {
if (!amm || !token0 || !token1) return;
if (!activePool) return;
if (!validAmount(amount)) return;
try {
const amountInWei = ethers.utils.parseEther(amount);
const pairAmountInWei = await amm.contract.getEquivalentToken(
token.contract.address,
amountInWei
);
const pairAmountInEther = ethers.utils.formatEther(pairAmountInWei);
setPairTokenAmount(pairAmountInEther);
} catch (error) {
alert(error);
}
};

const onChangeAmount = (
amount: string,
token: TokenType | undefined,
setAmount: (amount: string) => void,
setPairTokenAmount: (amount: string) => void
) => {
if (!token) return;
setAmount(amount);
getProvideEstimate(token, amount, setPairTokenAmount);
};

getProvideEstimateは内部で、ammのgetEquivalentTokenを引数で指定されたトークンとその量を併せて呼び出しています。 その返り値(指定したトークンと同価値のペアのトークンの量)をsetPairTokenAmountによりセットしています。

onChangeAmountはユーザが入力欄に数値を入力した際に動く関数です。 目的は入力されたトークンの量の状態変数の更新と、getProvideEstimateを呼び出すことにより同価値のペアのトークンの量の状態変数を更新することです。

続くonClickProvide関数はammの provideを呼び出す関数ですが、ポイントはprovideを実行する前に各トークンのapproveを呼び出している点です。

🦢 Swap

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

import { ethers } from "ethers";
import { useEffect, useState } from "react";
import { MdSwapVert } from "react-icons/md";

import { AmmType, TokenType } from "../../hooks/useContract";
import { validAmount } from "../../utils/validAmount";
import InputNumberBox from "../InputBox/InputNumberBox";
import styles from "./SelectTab.module.css";

type Props = {
token0: TokenType | undefined;
token1: TokenType | undefined;
amm: AmmType | undefined;
currentAccount: string | undefined;
updateDetails: () => void;
};

export default function Swap({
token0,
token1,
amm,
currentAccount,
updateDetails,
}: Props) {
// スワップ元とスワップ先のトークンを格納します。
const [tokenIn, setTokenIn] = useState<TokenType>();
const [tokenOut, setTokenOut] = useState<TokenType>();

const [amountIn, setAmountIn] = useState("");
const [amountOut, setAmountOut] = useState("");

useEffect(() => {
setTokenIn(token0);
setTokenOut(token1);
}, [token0, token1]);

const rev = () => {
// スワップ元とスワップ先のトークンを交換します。
const inCopy = tokenIn;
setTokenIn(tokenOut);
setTokenOut(inCopy);

// 交換後はソーストークンから推定量を再計算します。
getSwapEstimateOut(amountIn);
};

// スワップ元トークンに指定された量から、スワップ先トークンの受け取れる量を取得します。
const getSwapEstimateOut = async (amount: string) => {
if (!amm || !tokenIn) return;
if (!validAmount(amount)) return;
try {
const amountInInWei = ethers.utils.parseEther(amount);
const amountOutInWei = await amm.contract.getSwapEstimateOut(
tokenIn.contract.address,
amountInInWei
);
const amountOutInEther = ethers.utils.formatEther(amountOutInWei);
setAmountOut(amountOutInEther);
} catch (error) {
alert(error);
}
};

// スワップ先トークンに指定された量から、 スワップ元トークンに必要な量を取得します。
const getSwapEstimateIn = async (amount: string) => {
if (!amm || !tokenOut) return;
if (!validAmount(amount)) return;
if (amm) {
try {
const amountOutInWei = ethers.utils.parseEther(amount);
const amountInInWei = await amm.contract.getSwapEstimateIn(
tokenOut.contract.address,
amountOutInWei
);
const amountInInEther = ethers.utils.formatEther(amountInInWei);
setAmountIn(amountInInEther);
} catch (error) {
alert(error);
}
}
};

const onChangeIn = (amount: string) => {
setAmountIn(amount);
getSwapEstimateOut(amount);
};

const onChangeOut = (amount: string) => {
setAmountOut(amount);
getSwapEstimateIn(amount);
};

const onClickSwap = async () => {
if (!currentAccount) {
alert("Connect to wallet");
return;
}
if (!amm || !tokenIn || !tokenOut) return;
if (!validAmount(amountIn)) {
alert("Amount should be a valid number");
return;
}
try {
const amountInInWei = ethers.utils.parseEther(amountIn);

const txnIn = await tokenIn.contract.approve(
amm.contract.address,
amountInInWei
);
await txnIn.wait();

const txn = await amm.contract.swap(
tokenIn.contract.address,
tokenOut.contract.address,
amountInInWei
);
await txn.wait();
setAmountIn("");
setAmountOut("");
updateDetails(); // ユーザとammの情報を更新
alert("Success!");
} catch (error) {
alert(error);
}
};

return (
<div className={styles.tabBody}>
<InputNumberBox
leftHeader={"From"}
right={tokenIn ? tokenIn.symbol : ""}
value={amountIn}
onChange={(e) => onChangeIn(e.target.value)}
/>
<div className={styles.swapIcon} onClick={() => rev()}>
<MdSwapVert />
</div>
<InputNumberBox
leftHeader={"To"}
right={tokenOut ? tokenOut.symbol : ""}
value={amountOut}
onChange={(e) => onChangeOut(e.target.value)}
/>
<div className={styles.bottomDiv}>
<div className={styles.btn} onClick={() => onClickSwap()}>
Swap
</div>
</div>
</div>
);
}

ここではSwapタブを実装しています。

components/Container/Container.tsx内に以下の内容を追加し、UIを確認します。

import { useState } from 'react';

import { useContract } from '../../hooks/useContract';
import Details from '../Details/Details';
import Faucet from '../SelectTab/Faucet';
import Provide from '../SelectTab/Provide';
+ import Swap from '../SelectTab/Swap';
import styles from './Container.module.css';

type Props = {
currentAccount: string | undefined;
};

export default function Container({ currentAccount }: Props) {
// ...

return (
// ...

+ {activeTab === 'Swap' && (
+ <Swap
+ token0={token0}
+ token1={token1}
+ amm={amm}
+ currentAccount={currentAccount}
+ updateDetails={updateDetails}
+ />
+ )}
{activeTab === 'Provide' && (
<Provide
token0={token0}
token1={token1}
amm={amm}
currentAccount={currentAccount}
updateDetails={updateDetails}
/>
)}
{activeTab === 'Withdraw' && <div>withdraw</div>}
{activeTab === 'Faucet' && (
<Faucet
token0={token0}
token1={token1}
currentAccount={currentAccount}
updateDetails={updateDetails}
/>
)}
// ...
);
}

ブラウザでhttp://localhost:3000 へアクセスします。

Swapタブをクリックすると以下のような表示がされます。

USDC入力欄に50と入力すると、swapにより受けることができるJOEの量が表示されます。 Swapをクリックします。

swapの実行では以下のトランザクションへの署名が必要です。

  • USDCのapprove
  • AMMのswap

初めに表示されたapproveのトランザクションに署名し、しばらく待つとswapの署名について求められるので署名をします。

ここでは以下の図のようなことを行っています。

しばらく待つと(ポップアップが表示されokを押した後)右側のYour Detailsが更新されます!

それではSwap.tsxの中身を見ましょう。

使用する状態変数は以下の通りです。

// スワップ元とスワップ先のトークンを格納します。
const [tokenIn, setTokenIn] = useState<TokenType>();
const [tokenOut, setTokenOut] = useState<TokenType>();

// ユーザの入力値を保持します。
const [amountIn, setAmountIn] = useState("");
const [amountOut, setAmountOut] = useState("");

TokenInTokenOutはユーザの操作によって中身に入るトークンオブジェクトが変化します。 UIのSwapタブで、真ん中にある上下矢印のアイコンをクリックするとUSDCとJOEが切り替わるのは、このTokenIn/TokenOutの中身を入れ替えているためです。

以下のuseEffectで初期値を与え、rev関数により中身を入れ替えています。

useEffect(() => {
setTokenIn(token0);
setTokenOut(token1);
}, [token0, token1]);

const rev = () => {
// スワップ元とスワップ先のトークンを交換します。
const inCopy = tokenIn;
setTokenIn(tokenOut);
setTokenOut(inCopy);

// 交換後はソーストークンから推定量を再計算します。
getSwapEstimateOut(amountIn);
};

その他の関数はAMMコントラクトの関数を呼び出し、返り値を状態変数へ格納しています。

ポイントは以下です。

  • スワップ元のトークン(先ほどの挙動確認でのUSDC)の入力値に変化があった場合は、onChangeIngetSwapEstimateOutを実行することでスワップ先のトークン(先ほどの挙動確認でのJOE)の表示内容を更新します。
  • スワップ先のトークンの入力値に変化があった場合は、onChangeOutgetSwapEstimateInを実行することでスワップ元のトークンの表示内容を更新します。

🐃 Withdraw

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

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

import { AmmType, TokenType } from "../../hooks/useContract";
import {
formatWithoutPrecision,
formatWithPrecision,
} from "../../utils/format";
import { validAmount } from "../../utils/validAmount";
import InputNumberBox from "../InputBox/InputNumberBox";
import styles from "./SelectTab.module.css";

type Props = {
token0: TokenType | undefined;
token1: TokenType | undefined;
amm: AmmType | undefined;
currentAccount: string | undefined;
updateDetails: () => void;
};

export default function Withdraw({
token0,
token1,
amm,
currentAccount,
updateDetails,
}: Props) {
const [amountOfToken0, setAmountOfToken0] = useState("");
const [amountOfToken1, setAmountOfToken1] = useState("");
const [amountOfShare, setAmountOfShare] = useState("");
const [amountOfMaxShare, setAmountOfMaxShare] = useState<string>();

const getMaxShare = useCallback(async () => {
if (!amm || !currentAccount) return;
try {
const shareWithPrecision = await amm.contract.share(currentAccount);
const shareWithoutPrecision = formatWithoutPrecision(
shareWithPrecision,
amm.sharePrecision
);
setAmountOfMaxShare(shareWithoutPrecision);
} catch (error) {
alert(error);
}
}, [amm, currentAccount]);

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

const leftLessThanRightAsBigNumber = (
left: string,
right: string
): boolean => {
return BigNumber.from(left).lt(BigNumber.from(right));
};

const getEstimate = async (
token: TokenType | undefined,
amountOfShare: string,
setAmount: (amount: string) => void
) => {
if (!amm || !token || !amountOfMaxShare) return;
if (!validAmount(amountOfShare)) return;
if (leftLessThanRightAsBigNumber(amountOfMaxShare, amountOfShare)) {
alert("Amount should be less than your max share");
return;
}
try {
const shareWithPrecision = formatWithPrecision(
amountOfShare,
amm.sharePrecision
);
const estimateInWei = await amm.contract.getWithdrawEstimate(
token.contract.address,
shareWithPrecision
);
const estimateInEther = ethers.utils.formatEther(estimateInWei);
setAmount(estimateInEther);
} catch (error) {
alert(error);
}
};

const onClickMax = async () => {
if (!amountOfMaxShare) return;
setAmountOfShare(amountOfMaxShare);
getEstimate(token0, amountOfMaxShare, setAmountOfToken0);
getEstimate(token1, amountOfMaxShare, setAmountOfToken1);
};

const onChangeAmountOfShare = async (amount: string) => {
setAmountOfShare(amount);
getEstimate(token0, amount, setAmountOfToken0);
getEstimate(token1, amount, setAmountOfToken1);
};

const onClickWithdraw = async () => {
if (!currentAccount) {
alert("connect wallet");
return;
}
if (!amm || !amountOfMaxShare) return;
if (!validAmount(amountOfShare)) {
alert("Amount should be a valid number");
return;
}
if (leftLessThanRightAsBigNumber(amountOfMaxShare, amountOfShare)) {
alert("Amount should be less than your max share");
return;
}
try {
const txn = await amm.contract.withdraw(
formatWithPrecision(amountOfShare, amm.sharePrecision)
);
await txn.wait();
setAmountOfToken0("");
setAmountOfToken1("");
setAmountOfShare("");
updateDetails(); // ユーザとammの情報を更新
alert("Success!");
} catch (error) {
alert(error);
}
};

return (
<div className={styles.tabBody}>
<div className={styles.bottomDiv}>
<div className={styles.btn} onClick={() => onClickMax()}>
Max
</div>
</div>
<InputNumberBox
leftHeader={"Amount of share:"}
right=""
value={amountOfShare}
onChange={(e) => onChangeAmountOfShare(e.target.value)}
/>
{token0 && token1 && (
<div className={styles.estimate}>
<div>
<p>
Amount of {token0.symbol}: {amountOfToken0}
</p>
<p>
Amount of {token1.symbol}: {amountOfToken1}
</p>
</div>
</div>
)}
<div className={styles.bottomDiv}>
<div className={styles.btn} onClick={() => onClickWithdraw()}>
Withdraw
</div>
</div>
</div>
);
}

ここではWithdrawタブを実装しています。

components/Container/Container.tsx内に以下の内容を追加し、UIを確認します。

import { useState } from 'react';

import { useContract } from '../../hooks/useContract';
import Details from '../Details/Details';
import Faucet from '../SelectTab/Faucet';
import Provide from '../SelectTab/Provide';
import Swap from '../SelectTab/Swap';
+ import Withdraw from '../SelectTab/Withdraw';
import styles from './Container.module.css';

type Props = {
currentAccount: string | undefined;
};

export default function Container({ currentAccount }: Props) {
// ...

return (
// ...

{activeTab === 'Swap' && (
<Swap
token0={token0}
token1={token1}
amm={amm}
currentAccount={currentAccount}
updateDetails={updateDetails}
/>
)}
{activeTab === 'Provide' && (
<Provide
token0={token0}
token1={token1}
amm={amm}
currentAccount={currentAccount}
updateDetails={updateDetails}
/>
)}
+ {activeTab === 'Withdraw' && (
+ <Withdraw
+ token0={token0}
+ token1={token1}
+ amm={amm}
+ currentAccount={currentAccount}
+ updateDetails={updateDetails}
+ />
+ )}
{activeTab === 'Faucet' && (
<Faucet
token0={token0}
token1={token1}
currentAccount={currentAccount}
updateDetails={updateDetails}
/>
)}
// ...
);
}

ブラウザでhttp://localhost:3000へアクセスします。

Withdrawタブをクリックすると以下のような表示がされます。

Maxボタンをクリックするとユーザの保有するシェアが入力値となり、引き出すことのできるトークンの量がそれぞれ下に表示されます。 Withdrawをクリックします。

トランザクションに署名し、しばらく待つと(ポップアップが表示されokを押した後)右側のYour Detailsが更新されます!

今回は1つのアカウントで挙動を確かめているためMaxのシェアでトークンを引き出すとプールは空になり、ユーザの保有するトークンの量が元々保有していた量に戻ります。

Withdraw.tsxの中身はこれまでの実装に比べるとシンプルです。

主な処理は以下の流れで行われます。

  1. ユーザが入力欄に数値を入力するor Maxボタンをクリックするとそれぞれ onChangeAmountOfShareonClickMaxが実行されます。
  2. onChangeAmountOfShare/onClickMaxは状態変数を更新後、getEstimate関数を実行することでAMMコントラクトのgetWithdrawEstimateを呼び出します。
  3. getWithdrawEstimateの返り値を状態変数(amountOfToken0/amountOfToken1)にセットすることで、UIに表示するユーザが受け取れるトークンの量を更新します。

🌔 参考リンク

こちらに本プロジェクトの完成形のレポジトリがあります。

期待通り動かない場合は参考にしてみてください。

🙋‍♂️ 質問する

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

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

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

おめでとうございます! セクション3が終了しました!

次のセクションであなたのwebアプリをデプロイしましょう 🛫