Introduction
In the first part of this tutorial, we talked about what AMMs are, their importances, applications and relevant terms. We eventually wrote and deployed a smart contract to simulate the process. In this later part of the tutorial, we will build a front-end interface that implements our smart contract that way we can get a full mental picture of what an AMM should look like. This is going to be a sketchy UI, so feel free to tweak as you wish or find suitable. If you stumbled on this tutorial first, please read the first part of this tutorial for better context.
Prerequisites
- Celo Composer: While setting up Celo-Composer, we selected React as our front-end framework so we will be writing in React specifically Next JS.
Starter Guide
For a starter guide, you can check out the codebase here
Demo
You can find the demo of this tutorial here
Getting Started
Install the following dependencies to begin
npm i react-chartjs-2
npm i chart.js
npm i react-icons
Let’s start with creating a Sidebar to handle all of our features. We will be having three features: Liquidity Pool Creation, Liquidity Pool Explorer, Token Price Chart.
Navigate to the react-app folder, here is where we will be doing all frontend changes. In the components folder, create a Sidebar.tsx
file, in here input the following piece of code
import Link from "next/link";
import { useRouter } from "next/router";
import React, { useEffect, useRef } from "react";
import {
FaChartPie,
FaSwimmingPool,
FaOpera,
FaBalanceScale,
} from "react-icons/fa";
const Sidebar: React.FC = ({}) => {
const router = useRouter();
const ref = useRef<HTMLDivElement>(null);
return (
<>
<div className="h-screen flex justify-between shadow-sm scrollbar-change flex-col overflow-y-auto w-60 border-r border-r-[#545252] ml-0">
<div className="h-full flex flex-col justify-between">
<div>
<div className="flex items-center justify-center mx-auto py-4"></div>
<div className="nav__inner mt-5">
<div className="flex flex-col justify-between ">
<Link
href="/"
className={`nav__link hover:bg-white transition-all ease-in duration-200 ${
router.asPath === "/" ? "bg-white active" : ""
} `}
>
<div className="flex items-center gap-3 py-4 px-6 mx-auto border-b border-[#545252] text-sm relative">
<span>
<FaOpera />
</span>
<span className="nav_item">Liquidity Pool Creation</span>
<span className="right_border"></span>
</div>
</Link>
<Link
href="/pool"
className={`nav__link hover:bg-white transition-all ease-in duration-200 border-b border-[#545252] ${
router.asPath === "/pool" ||
router.pathname.startsWith("/pool")
? "bg-white active"
: ""
}`}
>
<div className="flex items-center gap-3 py-4 px-6 mx-auto text-sm relative">
<span>
<FaSwimmingPool />
</span>
<span className="nav_item">Liquidity Pool Explorer</span>
<span className="right_border"></span>
</div>
</Link>
<Link
href="/tokenchart"
className={`nav__link hover:bg-white transition-all ease-in duration-200 border-b border-[#545252] ${
router.asPath === "/tokenchart" ||
router.pathname.startsWith("/tokenchart")
? "bg-white active"
: ""
}`}
>
<div className="flex items-center gap-3 py-4 px-6 mx-auto text-sm relative">
<span>
<FaChartPie />
</span>
<span className="nav_item">Token Price Charts</span>
<span className="right_border"></span>
</div>
</Link>
</div>
</div>
</div>
<div className="about_btn hover:bg-white transition-all ease-in duration-200 ">
<Link href="/spaces">
<div className="flex items-center gap-3 py-4 px-6 mx-auto text-sm">
<span>
<FaBalanceScale />
</span>
<span className="nav_item">About</span>
</div>
</Link>
</div>
</div>
</div>
</>
);
};
export default Sidebar;
Edit your Layout component, Layout.tsx
by adding your SideBar menu. Here is the code
import { FC, ReactNode } from "react";
import Footer from "./Footer";
import Header from "./Header";
import Sidebar from "./Sidebar";
interface Props {
children: ReactNode;
}
const Layout: FC<Props> = ({ children }) => {
return (
<>
<div className="bg-gypsum overflow-hidden flex flex-col min-h-screen">
<Header />
<div className="flex flex-row">
<Sidebar />
{children}
</div>
<Footer />
</div>
</>
);
};
export default Layout;
Before we continue, let us create a helper folder that will hold our contract’s ABI and handle our contract’s connection. This is the contract you deployed and verified in the previous article. Go to the alfajores explorer to export your ABI and copy it in a json file. I called mine AmmSwap.json. Create another file which could be a javascript file and copy the following code. The code imports our ABI and returns our contract.
import abiJSON from "./AmmSwap.json";
import { ethers } from "ethers";
function connectContract() {
const contractAddress = "0x3E73365764fBBE4f58Eb0B6a62CA80FC706D851B";
const contractABI = abiJSON.abi;
let ammContract;
try {
const { ethereum } = window;
if (ethereum.chainId === "0xaef3") {
//checking for eth object in the window, see if they have wallet connected to Alfajeros network
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
console.log("contractABI", contractABI);
ammContract = new ethers.Contract(contractAddress, contractABI, signer); // instantiating new connection to the contract
} else {
throw new Error("Please connect to the Polygon Alfajeros network.");
}
} catch (error) {
console.log("ERROR:", error);
}
return ammContract;
}
export default connectContract;
For next steps, notice that from the Sidebar component, the index page will handle the liquidity pool creation, so we will be editing the index.tsx
page found in react-app/pages
import React, { useState } from "react";
import connectToContract from "../utils/connectContract";
import Alert from "../components/Alert";
import { ConnectButton } from "@rainbow-me/rainbowkit";
import { useAccount } from "wagmi";
export default function Home() {
const { address } = useAccount();
const [activeTab, setActiveTab] = useState("creation");
const [tokenA, setTokenA] = useState("");
const [tokenB, setTokenB] = useState("");
const [amountA, setAmountA] = useState("");
const [amountB, setAmountB] = useState("");
const [liquidityPoolId, setLiquidityPoolId] = useState("");
const [reserveA, setReserveA] = useState("");
const [reserveB, setReserveB] = useState("");
const [fees, setFees] = useState("");
const [success, setSuccess] = useState<boolean | null>(null);
const [message, setMessage] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean | null>(null);
return (
<div>
<div className="flex flex-col items-center min-h-screen mt-10 ml-10">
{loading && (
<Alert
alertType={"loading"}
alertBody={"Please wait"}
triggerAlert={true}
color={"white"}
/>
)}
{success && (
<Alert
alertType={"success"}
alertBody={message}
triggerAlert={true}
color={"palegreen"}
/>
)}
{success === false && (
<Alert
alertType={"failed"}
alertBody={message}
triggerAlert={true}
color={"palevioletred"}
/>
)}
<h1 className="text-3xl font-bold mb-4">Create Liquidity Pool</h1>
<form className="w-64" onSubmit={handleCreateLiquidityPool}>
<div className="mb-4">
<label htmlFor="tokenA" className="block mb-1 font-semibold">
Token A (Address):
</label>
<input
type="text"
id="tokenA"
className="w-full px-2 py-1 border border-gray-300 rounded"
value={tokenA}
onChange={(e) => setTokenA(e.target.value)}
/>
</div>
<div className="mb-4">
<label htmlFor="tokenB" className="block mb-1 font-semibold">
Token B (Address):
</label>
<input
type="text"
id="tokenB"
className="w-full px-2 py-1 border border-gray-300 rounded"
value={tokenB}
onChange={(e) => setTokenB(e.target.value)}
/>
</div>
<div className="mb-4">
<label htmlFor="amountA" className="block mb-1 font-semibold">
Amount A:
</label>
<input
type="number"
id="amountA"
className="w-full px-2 py-1 border border-gray-300 rounded"
value={amountA}
onChange={(e) => setAmountA(e.target.value)}
/>
</div>
<div className="mb-4">
<label htmlFor="amountB" className="block mb-1 font-semibold">
Amount B:
</label>
<input
type="number"
id="amountB"
className="w-full px-2 py-1 border border-gray-300 rounded"
value={amountB}
onChange={(e) => setAmountB(e.target.value)}
/>
</div>
<button
type="submit"
className="w-full bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded"
>
Create Liquidity Pool
</button>
</form>
{/* {liquidityPoolId && (
<div className="mt-8">
<h2 className="text-xl font-bold mb-2">Liquidity Pool Details</h2>
<p>Liquidity Pool ID: {liquidityPoolId}</p>
<p>Reserve A: {reserveA}</p>
<p>Reserve B: {reserveB}</p>
<p>Fees: {fees}</p>
</div>
)} */}
</div>
</div>
);
}
This is a form that will handle the create liquidity pool feature.
We need to call the createLiquidityPool
function from our contract so we can write an async function to do that
async function handleCreateLiquidityPool(
e: React.FormEvent<HTMLFormElement>
): Promise<void> {
e.preventDefault();
const body = {
tokenA: tokenA,
tokenB: tokenB,
amountA: amountA,
amountB: amountB,
};
try {
console.log("Form successfully submitted!");
await createLiquidityPool();
} catch (error) {
alert(
`Oops! Something went wrong. Please refresh and try again. Error ${error}`
);
}
}
const createLiquidityPool = async () => {
try {
const ammContract = connectToContract();
console.log(ammContract);
if (ammContract) {
let txn = await ammContract.createLiquidityPool(
tokenA,
tokenB,
amountA,
amountB,
{ gasLimit: 900000 }
);
console.log(await txn);
setLoading(true);
console.log("Creating pool...", txn.hash);
await txn.wait();
console.log("Created -- ", txn.hash);
setSuccess(true);
setLoading(false);
setMessage("Tokens have been added to liquidity pool successfully.");
} else {
console.log("Error getting contract.");
}
} catch (error) {
setSuccess(false);
setLoading(false);
console.log(error);
}
};
Our full index.tsx code will then look like this
import React, { useState } from "react";
import connectToContract from "../utils/connectContract";
import Alert from "../components/Alert";
import { ConnectButton } from "@rainbow-me/rainbowkit";
import { useAccount } from "wagmi";
export default function Home() {
const { address } = useAccount();
const [activeTab, setActiveTab] = useState("creation");
const [tokenA, setTokenA] = useState("");
const [tokenB, setTokenB] = useState("");
const [amountA, setAmountA] = useState("");
const [amountB, setAmountB] = useState("");
const [liquidityPoolId, setLiquidityPoolId] = useState("");
const [reserveA, setReserveA] = useState("");
const [reserveB, setReserveB] = useState("");
const [fees, setFees] = useState("");
const [success, setSuccess] = useState<boolean | null>(null);
const [message, setMessage] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean | null>(null);
// TODO: Perform input validation
async function handleCreateLiquidityPool(
e: React.FormEvent<HTMLFormElement>
): Promise<void> {
e.preventDefault();
const body = {
tokenA: tokenA,
tokenB: tokenB,
amountA: amountA,
amountB: amountB,
};
try {
console.log("Form successfully submitted!");
await createLiquidityPool();
} catch (error) {
alert(
`Oops! Something went wrong. Please refresh and try again. Error ${error}`
);
}
}
const createLiquidityPool = async () => {
try {
const ammContract = connectToContract();
console.log(ammContract);
if (ammContract) {
let txn = await ammContract.createLiquidityPool(
tokenA,
tokenB,
amountA,
amountB,
{ gasLimit: 900000 }
);
console.log(await txn);
setLoading(true);
console.log("Creating pool...", txn.hash);
await txn.wait();
console.log("Created -- ", txn.hash);
setSuccess(true);
setLoading(false);
setMessage("Tokens have been added to liquidity pool successfully.");
} else {
console.log("Error getting contract.");
}
} catch (error) {
setSuccess(false);
setLoading(false);
console.log(error);
}
};
// Mock data - replace with actual implementation
// const mockLiquidityPoolId = "123456789";
// const mockReserveA = "1000";
// const mockReserveB = "2000";
// const mockFees = "0.03%";
// // Update state with the created pool's details
// setLiquidityPoolId(mockLiquidityPoolId);
// setReserveA(mockReserveA);
// setReserveB(mockReserveB);
// setFees(mockFees);
return (
<div>
<div className="flex flex-col items-center min-h-screen mt-10 ml-10">
{loading && (
<Alert
alertType={"loading"}
alertBody={"Please wait"}
triggerAlert={true}
color={"white"}
/>
)}
{success && (
<Alert
alertType={"success"}
alertBody={message}
triggerAlert={true}
color={"palegreen"}
/>
)}
{success === false && (
<Alert
alertType={"failed"}
alertBody={message}
triggerAlert={true}
color={"palevioletred"}
/>
)}
<h1 className="text-3xl font-bold mb-4">Create Liquidity Pool</h1>
<form className="w-64" onSubmit={handleCreateLiquidityPool}>
<div className="mb-4">
<label htmlFor="tokenA" className="block mb-1 font-semibold">
Token A (Address):
</label>
<input
type="text"
id="tokenA"
className="w-full px-2 py-1 border border-gray-300 rounded"
value={tokenA}
onChange={(e) => setTokenA(e.target.value)}
/>
</div>
<div className="mb-4">
<label htmlFor="tokenB" className="block mb-1 font-semibold">
Token B (Address):
</label>
<input
type="text"
id="tokenB"
className="w-full px-2 py-1 border border-gray-300 rounded"
value={tokenB}
onChange={(e) => setTokenB(e.target.value)}
/>
</div>
<div className="mb-4">
<label htmlFor="amountA" className="block mb-1 font-semibold">
Amount A:
</label>
<input
type="number"
id="amountA"
className="w-full px-2 py-1 border border-gray-300 rounded"
value={amountA}
onChange={(e) => setAmountA(e.target.value)}
/>
</div>
<div className="mb-4">
<label htmlFor="amountB" className="block mb-1 font-semibold">
Amount B:
</label>
<input
type="number"
id="amountB"
className="w-full px-2 py-1 border border-gray-300 rounded"
value={amountB}
onChange={(e) => setAmountB(e.target.value)}
/>
</div>
<button
type="submit"
className="w-full bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded"
>
Create Liquidity Pool
</button>
</form>
{/* {liquidityPoolId && (
<div className="mt-8">
<h2 className="text-xl font-bold mb-2">Liquidity Pool Details</h2>
<p>Liquidity Pool ID: {liquidityPoolId}</p>
<p>Reserve A: {reserveA}</p>
<p>Reserve B: {reserveB}</p>
<p>Fees: {fees}</p>
</div>
)} */}
</div>
</div>
);
}
You can go to the terminal to run the code we already have
npm run dev
Your output would run on localhost:3000 and look similar to this
Let’s proceed to continue the other pages. Under the pages folder, create two new folders. You can name one pool
and the other one tokenchart
. In the pool folder, create a index file that will give the overview of all liquidity pools created. Your code could be like this
import React, { useState } from "react";
interface Pool {
id: number;
tokens: string;
reserves: string;
fees: number;
liquidity: number;
providers: number;
}
const LiquidityPoolExplorerPage: React.FC = () => {
const [pools, setPools] = useState<Pool[]>([
{
id: 1,
tokens: "Token A / Token B",
reserves: "1000 / 2000",
fees: 0.03,
liquidity: 5000,
providers: 10,
},
{
id: 2,
tokens: "Token C / Token D",
reserves: "500 / 1000",
fees: 0.02,
liquidity: 3000,
providers: 5,
},
// Add more pools here
]);
const [sortBy, setSortBy] = useState("liquidity"); // Default sort by liquidity
const [filterBy, setFilterBy] = useState(""); // Default no filter
const handleSortChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setSortBy(event.target.value);
};
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setFilterBy(event.target.value);
};
const filteredPools = pools.filter((pool) =>
pool.tokens.toLowerCase().includes(filterBy.toLowerCase())
);
const sortedPools = filteredPools.sort((a, b) => {
if (sortBy === "liquidity") {
return b.liquidity - a.liquidity;
} else if (sortBy === "fees") {
return b.fees - a.fees;
}
return 0;
});
return (
<div className="container mx-auto p-4 mt-10">
<h1 className="text-2xl font-bold mb-4">Liquidity Pool Explorer</h1>
<div className="mb-4">
<label htmlFor="sort" className="mr-2">
Sort By:
</label>
<select
id="sort"
className="p-2 border rounded"
value={sortBy}
onChange={handleSortChange}
>
<option value="liquidity">Liquidity</option>
<option value="fees">Fees</option>
</select>
</div>
<div className="mb-4">
<label htmlFor="filter" className="mr-2">
Filter By Token:
</label>
<input
type="text"
id="filter"
className="p-2 border rounded"
value={filterBy}
onChange={handleFilterChange}
/>
</div>
<table className="w-full border-collapse">
<thead>
<tr className="bg-gray-200">
<th className="p-4 font-bold text-left">Tokens</th>
<th className="p-4 font-bold text-left">Reserves</th>
<th className="p-4 font-bold text-left">Fees</th>
<th className="p-4 font-bold text-left">Liquidity</th>
<th className="p-4 font-bold text-left">Liquidity Providers</th>
</tr>
</thead>
<tbody>
{sortedPools.map((pool) => (
<tr key={pool.id} className="border-b">
<td className="p-4">{pool.tokens}</td>
<td className="p-4">{pool.reserves}</td>
<td className="p-4">{pool.fees}</td>
<td className="p-4">{pool.liquidity}</td>
<td className="p-4">{pool.providers}</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
export default LiquidityPoolExplorerPage;
Ideally, you can use subgraphs to fetch your created pools. I talked about subgraphs in my previous tutorial
Go back to your broswer to see your updated user interface.
Click on Liquidity Pool Explorer
There’s a filter function too that allows you to filter by the token you are searching for
For our last feature, in the tokenchart folder, create an index.tsx
file. We will be using chart.js to create couple of charts that indicates the supply of tokens, this will be useful in making decisions when a user comes to use your Amm. Here is a sample of how the code can look like
import React, { useRef, useEffect } from "react";
import Chart, { ChartType, ChartOptions } from "chart.js/auto";
import { Pie, Doughnut, Line } from "react-chartjs-2";
const TokenSupplyChart = () => {
const chartRef = useRef<HTMLCanvasElement | null>(null);
const chartInstanceRef = useRef<Chart | undefined>();
useEffect(() => {
const createChart = () => {
const chartCanvas = chartRef.current;
if (chartCanvas) {
// Destroy previous chart instance if it exists
if (chartInstanceRef.current) {
chartInstanceRef.current.destroy();
}
const chartOptions: ChartOptions = {
responsive: true,
maintainAspectRatio: false,
};
const chartData = {
labels: ["Token A", "Token B", "Token C", "Token D"],
datasets: [
{
label: "Total Supply",
data: [1000, 2000, 1500, 3000],
backgroundColor: "rgba(75, 192, 192, 0.2)",
borderColor: "rgba(75, 192, 192, 1)",
borderWidth: 2,
},
],
};
chartInstanceRef.current = new Chart(chartCanvas, {
type: "bar" as ChartType,
data: chartData,
options: chartOptions,
});
}
};
createChart();
return () => {
// Cleanup: Destroy chart instance
if (chartInstanceRef.current) {
chartInstanceRef.current.destroy();
chartInstanceRef.current = undefined;
}
};
}, []); // Empty dependency array
const chartData = {
labels: ["Token A", "Token B", "Token C", "Token D"],
datasets: [
{
label: "Token Distribution",
data: [30, 20, 25, 15],
backgroundColor: [
"rgba(255, 99, 132, 0.6)",
"rgba(54, 162, 235, 0.6)",
"rgba(255, 206, 86, 0.6)",
"rgba(75, 192, 192, 0.6)",
],
borderColor: [
"rgba(255, 99, 132, 1)",
"rgba(54, 162, 235, 1)",
"rgba(255, 206, 86, 1)",
"rgba(75, 192, 192, 1)",
],
borderWidth: 1,
},
],
};
const chartOptions: ChartOptions = {
responsive: true,
maintainAspectRatio: false,
};
return (
<>
<div className="mt-10 ml-10">
<h1>Token Supply Chart</h1>
<div className="mt-10" style={{ height: "400px" }}>
<canvas ref={chartRef}></canvas>
</div>
<div className="flex flex-wrap">
<div className="w-1/3 px-4">
<div className="h-400" style={{ height: "400px" }}>
<h2>Pie Chart</h2>
<Pie data={chartData} options={chartOptions} />
</div>
</div>
<div className="w-1/3 px-4">
<div className="h-400" style={{ height: "400px" }}>
<h2>Doughnut Chart</h2>
<Doughnut data={chartData} options={chartOptions} />
</div>
</div>
<div className="w-1/3 px-4">
<div className="h-400" style={{ height: "400px" }}>
<h2>Area Chart</h2>
<Line data={chartData} options={chartOptions} />
</div>
</div>
</div>
</div>
</>
);
};
export default TokenSupplyChart;
Ideally, you can use subgraphs to fetch your chart data. I talked about subgraphs in my previous tutorial
Again, let’s head back to our browser to see what this looks like. Here is what we see
Finally, we can go back to create a liquidity pool. Note that you are required to input the addresses of the tokens and their amounts
Conclusion
I hope this was a good starting point for you if you have been trying to understand and implement market makers on chain. There are several usecases with automated market makers and you should try and explore some of them maybe for a hackathon or a pet project. Kindly drop a comment if you have found my articles useful or want me to write anything on what might interest you.