Building an Automated Market Maker (AMM) on the Celo Blockchain II

Building an Automated Market Maker (AMM) on the Celo Blockchain II https://celo.academy/uploads/default/optimized/2X/3/3ab6ad442b7f88db1a3afc6bf2dee45146c7864c_2_1024x576.png
none 0.0 0

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.

4 Likes

Well, I will also review for your part 2

1 Like

Just incase of next time try to keep thing straight and precise, You shouldn’t leave a review comment of one piece in another piece, when you haven’t even stated you’ll be reviewing the current piece :clinking_glasses:

1 Like

Actually I am doing the review of this one if I am not wrong

1 Like

I trust you did well at it too bro :clinking_glasses::handshake:
The piece looks nice and catchy Either way @busayo and @ishan.pathak2711

2 Likes