Build a Decentralised Exchange with moralis

Build a Decentralised Exchange with moralis

Trading involves the exchange of assets with value to others of the similar or

same value. More so, digital assets are also of value which means digital exchanges can be made possible through centralised or Decentralised exchanges (DEX).

So what is a DEX?

What is a Dex(Decentralised Exchange)

A decentralized exchange (DEX) is a type of cryptocurrency exchange that operates on a distributed ledger technology (DLT) or blockchain network, where users can trade cryptocurrencies directly without the need for a central authority or intermediary.

The most popular platforms for Trading digital currency are Uniswap, pancake swap,1inc e.t.c are various Decentralised platforms to trade cryptocurrency.

In this tutorial, you are going to build a full-stack Dex using moralis API for token price data.

Prerequisite

To follow along with this tutorial, you need to have a good understanding of the following:

  • Javascript

  • Nextjs

Let Get started

First off run the following command to install nextjs

npx create-next-app@latest

Do the following:

  • Rename your app

  • select no for typescript

  • no for ESLint

  • yes for the src/directory

  • no experimental app/ directory

Once the installation is complete, run the commands below after you have cd into the project folder to start the development server:

npm run dev

install the following packages

npm i axios moralis antd wagmi
  • Axios is a popular JavaScript library that is used to make HTTP requests from a web browser or Node.js. It provides an easy-to-use API that supports promises and async/await syntax for making requests and handling responses.

  • antd also known as Ant Design, is a popular UI library for React applications. It provides a set of reusable components that help developers to build user interfaces with ease.

  • wagmi is a collection of React Hooks containing everything you need to start working with Ethereum. wagmi makes it easy to "Connect Wallet," display ENS and balance information, sign messages, interact with contracts, and much more — all with caching, request deduplication, and persistence.

N/B : keep up on further tutorial on wagmi Hooks

Moralis

Moralis provides world-class APIs to developers across the globe, allowing companies and projects of all sizes to seamlessly integrate blockchain into their solutions stack and scale with ease.

Sign up for Moralis to get an API KEY

create a file .env to store your secret API key

Next under the page/api folder in your src directory create a file name tokenprice.js

Write the code

To make sure your api is working type on your browser to get token price

http://localhost:3000/api/tokenprice?addressOne=0xdac17f958d2ee523a2206206994597c13d831ec7&addressTwo=0x514910771af9ca656af840dff83e8264ecf986ca

Application setup

This is the price of Usdc to a Link token, to get all token addresses goto the GitHub link. To connect with Metamask we use wagmi Hook in the _app.js file to wrap the component's props and provide the supported chain network to connect to your application

Check out the wagmi supported chain for further information.

import '@/styles/globals.css'
import { configureChains, mainnet, WagmiConfig,goerli, createClient } from "wagmi";
import { publicProvider } from "wagmi/providers/public";

const { provider, webSocketProvider } = configureChains(
  [goerli,mainnet],
  [publicProvider()]
);

const client = createClient({
  autoConnect:true,
  provider,
  webSocketProvider
})

export default function App({ Component, pageProps }) {
  return (
      <WagmiConfig client={client}>

            <Component {...pageProps} />

      </WagmiConfig> 
)}

Next, create the file src/component/header.jsx and add the code below. The address and isconnected props get the address of the user's wallet and check to see if the user is connected to the app respectfully. The Header contains the Page icon, swap and token route and the address display that check if the wallet is connected to the application.

To get the image assets access the github link.

Now under the header file create a layer.jsx file to contain the navbar header.

import { useConnect, useAccount } from "wagmi";
import { MetaMaskConnector } from "wagmi/connectors/metaMask";
import Header from '@/components/Header'


const Layout = ({ children }) => {
    const { address, isConnected } = useAccount();
    const { connect } = useConnect({
        connector: new MetaMaskConnector(),
    });

    return (
      <div className="content">
        <Header connect={connect} isConnected={isConnected} address={address} />
        { children }
      </div>
    );
}

export default Layout

In the _app.jsx file wrap the component's props with the Layer Tag, this ensure the navigation bar is always present in any routes taken by the user.

    <WagmiConfig client={client}>
         <Layer>
            <Component {...pageProps} />
         </Layer>
    </WagmiConfig>

Add the following css code to the gobal.css in the style folder.

body {
  margin: 0;
  font-family: 'Clash Display', sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  height: 100vh;
  width: 100vw;
  background-color: rgb(25, 33, 52) !important;
  background: rgb(25, 33, 52);
  background: linear-gradient(
    180deg,
    rgba(25, 33, 52, 1) 28%,
    rgba(7, 8, 21, 1) 75%
  );
  color: white;
}

* {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
  font-family: 'Clash Display', sans-serif;
  font-weight: 500px;
}

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

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

header {
  height: 100px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-left: 50px;
  padding-right: 50px;
}

.logo{
  padding-right: 20px;
}

.eth{
  padding-right: 10px;
}

.leftH {
  display: flex;
  align-items: center;
  gap: 10px;
}

.centerH{
  display: flex;
}

.rightH {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
}

.headerItem {
  padding: 10px;
  padding-left: 15px;
  padding-right: 15px;
  border-radius: 5px;
  font-weight: 500;
  transition: 0.3s;
  display: flex;
  align-items: center;
}

.eth-box{
  background-color: #1748e8; 
  padding: 5px;
  padding-left: 10px;
  padding-right: 15px;
  border-radius: 5px;
  font-weight: 500;
  transition: 0.3s;
  display: flex;
  align-items: center;
}

.eth-box p{
  margin: 5px;
}

.connectButton {
  background-color: #243056; 
  padding: 10px;
  padding-right: 20px;
  padding-left: 20px;
  border-radius: 10px;
  color: #fff;
  font-weight: bold;
  transition: 0.3s;
}

.connectButton:hover {
  cursor: pointer;
  color: #3b4874;
  border: 2px solid white;
}
.mainWindow {
  margin-top: 40px;
  display: flex;
  justify-content: center;
}

Edit the code in the index.jsx file and add the mainWindow css class to the main tag.

import Head from 'next/head'
import Swap from '@/routes/Swap'
import { Inter } from '@next/font/google'


const inter = Inter({ subsets: ['latin'] })

export default function Home() {


  return (
    <>
      <Head>
        <title>Moralis Dex</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main className='mainWindow'>

      </main>
    </>
  )
}

Once you've finished editing the file, save it and you should see a nice-looking clean main screen.

Now it looks good well done reward yourself.

Swapping Page

Now that we are done with little part of our application we have connected the wallet to the application we have their wallet address, we can continue to the main function of the application in this part.

In this part, we are going to use Axios to fetch token prices and use the 1inch api to swap our token with the token address we download earlier.

create the folder routes/swap.jsx in the page directory and add the following code

import { useState , useEffect} from "react"
import { Input, Popover, Radio, Modal, message } from "antd";
import { useSendTransaction, useWaitForTransaction } from "wagmi";
import axios from "axios";
import {
    ArrowDownOutlined,
    DownOutlined,
    SettingOutlined,
  } from "@ant-design/icons";
import tokenList from '@/tokenList.json'
import Image from "next/image";

const Swap = (props) => {
return (
    <h1>SWap</h1>
);
}
export default Swap

Setting up the Trade functions

In the above code snippet, add to the code above the state component and their functionality below.

    const { address, isConnected } = props;
    const [slippage, setSlippage] = useState(2.5);
    const [tokenOneAmount, setTokenOneAmount] = useState(null);
    const [tokenTwoAmount, setTokenTwoAmount] = useState(null);
    const [tokenOne, setTokenOne] = useState(tokenList[0]);
    const [tokenTwo, setTokenTwo] = useState(tokenList[1]);
    const [isOpen, setIsOpen] = useState(false);
    const [changeToken, setChangeToken] = useState(1);
    const [prices, setPrices] = useState(null);
    const [messageApi, contextHolder] = message.useMessage();
    const [txDetails, setTxDetails] = useState({
        to:null,
        data: null,
        value: null,
    });

In the above code, we define the following state prop:

  • Address : The address uses the wagmi hook to get the user wallet address.

  • isconnected : using the wagmi react hook to check if the app is connected to metamask or a specified wallet the user chooses.

  • silage : is set to 2.5, which is the default value. Whenever the value of slippage changes, React will automatically re-render the component to reflect the new value.

  • tokenAmount: state gets token prices from the moralis API

  • tokenOne and tokenTwo set the token address from the tokenList.json provide in the GitHub

  • changeToken state set the token address to be exchange.

  • txDetails and messageApi uses the wagmi hook to get the transaction details and messages from the contract transactions

  • prices state props set the equivalent prices of the tokenOne to tokenTwo from the Moralis api call.

const {data, sendTransaction} = useSendTransaction({
        request: {
          from: address,
          to: String(txDetails.to),
          data: String(txDetails.data),
          value: String(txDetails.value),
        }
})

    const { isLoading, isSuccess } = useWaitForTransaction({
        hash: data?.hash,
    })

    function handleSlippageChange(e) {
        setSlippage(e.target.value);
      }


      function changeAmount(e) {
        setTokenOneAmount(e.target.value);
        if(e.target.value && prices){
          setTokenTwoAmount((e.target.value *prices.ratio).toFixed(2))
        }else{
          setTokenTwoAmount(null);
        }
      }

    // fetching price 
 async function fetchPrices(one,two){
    const res = await axios.get('http://localhost:3000/api/tokenprice'{params:{addressOne: one, addressTwo: two}})
    setPrices(res.data)
      }

      function openModal(asset) {
        setChangeToken(asset);
        setIsOpen(true);
      }

      function modifyToken(i){
        setPrices(null);
        setTokenOneAmount(null);
        setTokenTwoAmount(null);
        if (changeToken === 1) {
          setTokenOne(tokenList[i]);
          fetchPrices(tokenList[i].address, tokenTwo.address)
        } else {
          setTokenTwo(tokenList[i]);
          fetchPrices(tokenOne.address, tokenList[i].address)
        }
        setIsOpen(false);
      }

   async function fetchDexSwap(){

        const allowance = await axios.get(`https://api.1inch.io/v5.0/1/approve/allowance?tokenAddress=${tokenOne.address}&walletAddress=${address}`)

   if(allowance.data.allowance === "0"){
    const approve = await axios.get(`https://api.1inch.io/v5.0/1/approve/transaction?tokenAddress=${tokenOne.address}`)
    setTxDetails(approve.data);    
            console.log("not approved")
}

     const tx = await axios.get(
            `https://api.1inch.io/v5.0/1/swap?fromTokenAddress=${tokenOne.address}&toTokenAddress=${tokenTwo.address}&amount=${tokenOneAmount.padEnd(tokenOne.decimals+tokenOneAmount.length, '0')}&fromAddress=${address}&slippage=${slippage}`)

          let decimals = Number(`1E${tokenTwo.decimals}`)
      setTokenTwoAmount((Number(tx.data.toTokenAmount)/decimals).toFixed(2));

          setTxDetails(tx.data.tx);

      }

const settings = (
        <>
          <div>Slippage Tolerance</div>
          <div>
            <Radio.Group value={slippage} onChange={handleSlippageChange}>
              <Radio.Button value={0.5}>0.5%</Radio.Button>
              <Radio.Button value={2.5}>2.5%</Radio.Button>
              <Radio.Button value={5}>5.0%</Radio.Button>
            </Radio.Group>
          </div>
        </>
      );

useEffect(()=>{fetchPrices(tokenList[0].address,tokenList[1].address)}, [])

   useEffect(()=>{if(txDetails.to && isConnected){sendTransaction();
        }
    }, [txDetails])

    useEffect(()=>{

        messageApi.destroy();

        if(isLoading){
          messageApi.open({
            type: 'loading',
            content: 'Transaction is Pending...',
            duration: 0,
          })
        }    

      },[isLoading])

      useEffect(()=>{
        messageApi.destroy();
        if(isSuccess){
          messageApi.open({
            type: 'success',
            content: 'Transaction Successful',
            duration: 1.5,
          })
        }else if(txDetails.to){
          messageApi.open({
            type: 'error',
            content: 'Transaction Failed',
            duration: 1.50,
          })
        }

Here the functions :

  • useSendTransaction: hook is a custom hook provided by the wagmi library or module, and it is used to handle sending transactions in a blockchain environment.

    • from: The address of the sender of the transaction.

    • to: The address of the recipient of the transaction.

    • data: The data to be included in the transaction, which can include additional instructions or parameters.

    • value: The value to be transferred in the transaction, which is typically denominated in a cryptocurrency.

  • useWaitforTransaction: wagmi custom hook to check the status of the transactions.

  • handleslipage: the function that set the slippage ratio of the token prices.

  • changeAmount: the functionality takes an event object e as its parameter. The event handler that is called when the user inputs a value into a form field, and it is used to update the state of two variables, tokenOneAmount and tokenTwoAmount.

  • fetchPrices : asynchronous function used to retrieve the current price or exchange rate between the two tokens.

  • modifyToken: this function set the specified token to be traded or exchanged.

  • fetchDexSwap: An asynchronous function to swap between two tokens using the 1inch API.

  • settings function displays the token slippage percentage to select from an option of 0.5%, 2.5% and 5.0%

The useEffect loads the token prices when the application loads.

App UI

Now we done with the functionality of the application let focus on the UI components, copy the ui code below in the return swap function.

  <div className="tradeBox">
       <div className="tradeBoxHeader">
                <h4>Swap</h4>
                <Popover
                    content={settings}
                    title="Settings"
                    trigger="click"
                    placement="bottomRight">
                    <SettingOutlined className="cog" />
                </Popover>
            </div>
            <div className="inputs">
                <Input
                    placeholder="0"
                    value={tokenOneAmount}
                    onChange={changeAmount}
                    disabled={!prices}
                />
                <Input placeholder="0" value={tokenTwoAmount} disabled={true} />
                <div className="switchButton" >
                    <ArrowDownOutlined className="switchArrow" />
                </div>
               <div className="assetOne" onClick={() =>openModal(1)}>
                    <Image width={30} height={40} src={tokenOne.img} alt="assetOneLogo" className="assetLogo" />
                    {tokenOne.ticker}
                    <DownOutlined />
                </div>
                <div className="assetTwo" onClick={() => openModal(2)}>
                    <Image width={30} height={40} src={tokenTwo.img} alt="assetOneLogo" className="assetLogo" />
                    {tokenTwo.ticker}
                    <DownOutlined />
                </div>
            </div>
            <div className="swapButton" disabled={!tokenOneAmount || !isConnected} onClick={fetchDexSwap} >Swap</div>
        </div>

In the code above

  • First the div tag with the class tradeBox contains the exchanges ui where swap token prices are displayed and exchanged.

  • Secondly the Popover from antd display the slippage for the token price

  • Input allows the user to input the value amount of the token in which the user can trade.

  • The swap button functionality performs the swapping of tokens.

Add the modal component above the tradeBox class to handle the selection of token to swap and wrap the entire element with the react fragment.

<>
        <Modal
            open={isOpen}
            footer={null}
            onCancel={() => setIsOpen(false)}
            title="Select a token">
            <div className="modalContent">
            {tokenList?.map((e, i) => {
                return (
                <div
                    className="tokenChoice"
                    key={i}
                    onClick={() => modifyToken(i)}
                >
                    <Image src={e.img} width={30} height={40} alt={e.ticker} className="tokenLogo" />
                    <div className="tokenChoiceNames">
                    <div className="tokenName">{e.name}</div>
                    <div className="tokenTicker">{e.ticker}</div>
                    </div>
                </div>
                );
            })}
            </div>
        </Modal>

Now we are almost done with the application with need to add the css style to give it the flare it deserved.

.tradeBox {
  margin: 10px;
  width: 400px;
  background-color: #0E111B;
  border: 2px solid #21273a;
  min-height: 300px;
  border-radius: 15px;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  align-items: flex-start;
  padding-left: 30px;
  padding-right: 30px;
}

.inputs {
  position: relative;
}

/* ant */

.assetOne {
  position: absolute;
  min-width: 50px;
  height: 40px;
  background-color: #0b0b0c;
  top: 36px;
  right: 20px;
  border: 1px solid white;
  border-radius: 20px;
  display: flex;
  justify-content: flex-start;
  align-items: center;
  gap: 5px;
  font-weight: bold;
  font-size: 17px;
  padding: 10px;
}

.assetTwo {
  position: absolute;
  min-width: 50px;
  height: 40px;
  background-color: #0b0b0c;
  top: 135px;
  right: 20px;
  border: 1px solid white;
  border-radius: 20px;
  display: flex;
  justify-content: flex-start;
  align-items: center;
  gap: 5px;
  font-weight: bold;
  font-size: 17px;
  padding: 10px;
  transition: 0.4s ease-in;
}

.tradeBoxHeader {
  margin: 10px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 98%;
}

.assetOne:hover{
  cursor: pointer;
}

.assetTwo:hover{
  cursor: pointer;
}

.assetLogo {
  height: 22px;
  margin-left: 5px;
}
switchButton {
  background-color: #3a4157;
  width: 25px;
  height: 25px;
  align-items: center;
  justify-content: center;
  display: flex;
  border-radius: 8px;
  position: absolute;
  top: 86px;
  left: 180px;
  color: #5F6783;
  border: 3px solid #0E111B;
  font-size: 12px;
  transition: 0.3s
}

.swapButton {
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: #243056; 
  width: calc(100%);
  height: 55px;
  font-size: 20px;
  border-radius: 12px;
  color: #5981F3;
  font-weight: bold;
  transition: 0.3s;
  margin-bottom: 30px;
  margin-top: 8px;
}

.swapButton[disabled] {
  background-color: #243056;
  opacity: 0.4;
  color: #5982f39b;
}

.swapButton[disabled]:hover {
  cursor: not-allowed;
  background-color: #243056;
}

.swapButton:hover {
  cursor: pointer;
  background-color: #3b4874
}

.tokenLogo {
  height: 40px;
  width: 40px;
}

.modalContent {
  border-top: 1px solid #363e54;
  /* padding-top: 20px; */
  margin-top: 20px;
  display: flex;
  flex-direction: column;
  gap: 10px
}

.tokenChoice {
  display: flex;
  justify-content: flex-start;
  align-items: center;
  padding-left: 20px;
  padding-top: 10px;
  padding-bottom: 10px;
}

.tokenChoice:hover {
  cursor: pointer;
  background-color: #1f2639;
}

.tokenName {
  margin-left: 10px;
  font-size: 16px;
  font-weight: 500;
}

.tokenTicker {
  margin-left: 10px;
  font-size: 13px;
  font-weight: 300;
  color: #51596f;
}

Next, create a custom css from the antd which override the styling.

.ant-input {
  background-color: #1f2639 !important;
  color: white !important;
  border-width: 0px !important;
  height: 96px !important;
  margin-bottom: 5px;
  font-size: 35px;
  border-radius: 12px !important;
}

.ant-input::placeholder {
  color: #5f6783 !important;
}

.ant-popover-arrow {
  visibility: hidden;
}

.ant-popover-title {
  color: white !important;
}

.ant-popover-title {
  color: white !important;
}

.ant-popover-inner-content {
  color: white !important;
}

.ant-popover-inner {
  min-width: 260px !important;
  min-height: 140px !important;
  border: 1px solid #21273a;
  background-color: #0e111b !important;
}

Now back to index.jsx and add the swap component in the mainwindow class

<main className='mainWindow'>
        <Swap isConnected={isConnected} address={address} />
</main>

Great Job the Ui is looking good reward yourself for a job well done.

What’s Next?

So, you've made it this far! That's awesome and it tells me that you're enthusiastic about creating Web3 apps. Now, if you're feeling up for it, I have some ideas on how you can take your app to the next level:

  • How about giving users the ability to see the List of tokens the user have in their wallets? It could make your app more user-friendly and convenient to see the available token the user has to trade with.

  • You could also experiment with using other blockchain networks instead of Ethereum which gives the user the ability to make cross-chain transactions.

Conclusion

That is it for this article. I hope you found this article useful, if you need any help please let me know in the comment section or DM me on Twitter.

Let's connect on Twitter and LinkedIn.

👋 Thanks for reading, See you next time

Did you find this article valuable?

Support Emmanuel Philip by becoming a sponsor. Any amount is appreciated!