DEV Community

Cover image for Building a DeFi Swap App with MetaMask, Moralis, and React
Muees A.
Muees A.

Posted on • Edited on

6 1 1

Building a DeFi Swap App with MetaMask, Moralis, and React

Introduction


Decentralized Finance (DeFi) is revolutionizing the financial ecosystem, offering users full control over their assets without intermediaries. This project aims to build a DeFi(DEX) swap application that enables seamless token swaps using MetaMask, Moralis, and React/Next.js

Join me as I build a fully functional Web3 Decentralized Exchange (DEX) from scratch! In this hands-on guide, I’ll walk you through every step of creating a platform that lets users seamlessly swap ERC-20 tokens on any EVM chain. Whether you’re new to DeFi or looking to refine your skills, let’s dive into smart contracts, liquidity pools, and real-world Web3 development — together!


Project Overview


Building a Decentralized Exchange (DEX) was an exciting challenge, and now I want to share everything I learned with you! In this guide, I’ll walk you through how I built a fully functional token-swapping platform that integrates blockchain transactions seamlessly.
Here’s what we’ll explore together:

✅ Connecting Wallets — How users authenticate with MetaMask.
🔄 Swapping Tokens — Selecting token pairs and executing trades.
📊 Fetching Real-Time Prices — Using Moralis API for live pricing.
⚡ Executing Transactions — Sending swaps to the blockchain.
⛽ Handling Gas & Errors — Ensuring smooth user feedback.

Throughout this article, I’ll share my approach, challenges I faced, and how I overcame them. Whether you’re looking to build your own DEX or just curious about the process, let’s dive in together!


🚀 Tech Stack: The Tools Behind the DEX


To bring this project to life, I used a modern Web3 stack that ensures seamless blockchain interaction and a smooth user experience. Here’s what powers it:

🎨 Frontend — Built with Next.js (React), Ant Design Components and styled using Tailwind CSS for a sleek and responsive UI.
🔗 Blockchain Interaction — Wallet authentication and transactions are handled via MetaMask ,Ethers.js, Wagmi, Viem,WalletConnect(reown)

📡 Backend & API Calls — Fetching real-time token prices and transaction data using Moralis API.
🧠 State Management — Managing app state efficiently with React Hooks & Context API.

Each of these tools played a crucial role in making the DEX functional and user-friendly. If you’re familiar with these technologies, you’ll feel right at home. If not, don’t worry — I’ll walk you through the key parts of the implementation! 🚀


Development Process


Before jumping into the code, let’s take a moment to talk through the development process. Understanding the steps will not only give you context but will also help you approach the project with a clear direction. Ready? Let’s break it down together!

1 Planning & Feature Definition
The first step is always to decide what you want to build. For me, the goal was clear: create a Web3-based platform that allows users to swap ERC-20 tokens with ease.

What features would you need to build a DEX?

  • Wallet Authentication: Users need a way to connect their wallets (we’re using MetaMask).
  • Token Selection: A way for users to pick the tokens they want to swap.
  • Real-Time Pricing: Fetch token prices using an API (Moralis API fits perfectly here).
  • Transaction Execution: Send the swap transactions to the blockchain.
  • Gas Estimation & Error Handling: Keep the user informed about gas fees and potential errors.

As you can see, this phase is about outlining the core features that will shape the user experience. Once you have a rough idea of the features, you’re ready for the next step: design and tech stack selection.

2 Choosing the Tech Stack
This is where things get fun! Based on the features I wanted to implement, I decided to use:

  • Next.js for the frontend, as it’s perfect for building React apps with great SEO and server-side rendering.
  • Tailwind CSS for styling because it’s super fast and flexible for building responsive UIs.
  • MetaMask & Ethers.js to handle all wallet interactions and blockchain transactions.
  • Moralis API to get live token prices and manage blockchain data efficiently.
  • React Hooks & Context API for state management to keep the app’s data in sync with the UI.

When choosing your tech stack, think about what will best support your project’s goals. Are you aiming for speed, flexibility, or scalability?

3 Setting Up the Project
With the tech stack chosen, the next step is setting up the project. Here’s where I:

  • Initialize a new Next.js project.
  • Install the necessary dependencies: MetaMask, Ethers.js, Tailwind, etc.
  • Set up my basic folder structure for clean, maintainable code.

Once that was ready, I moved on to building the core features — one by one.

4 Building the Core Features
This is where the magic happens, and where I had to overcome the toughest challenges. Let’s take a look at the features I built and the decisions I made:

  • Wallet Authentication: Setting up MetaMask was a bit tricky at first. You need to ensure the user’s wallet is connected, and that you handle the wallet’s network properly.
  • Token Selection & Price Fetching: This is where Moralis API comes in. You’ll need to fetch live prices for the selected tokens and display them in a user-friendly way.
  • Transaction Execution: Sending transactions to the blockchain using Ethers.js is straightforward, but I had to ensure users get proper feedback about their transaction status.
  • Gas Estimation & Error Handling: Showing users the estimated gas fees and potential errors is crucial for a smooth experience.

At each step, I learned more about how blockchain interactions work in a live environment — especially handling errors and ensuring the app is as user-friendly as possible.

5 Testing & Debugging
Once the core features were built, it was time to test everything. This is where things get really important. How do you ensure your DEX is working properly on every network?

  • Test with MetaMask: I made sure to test on different chains (Ethereum, Polygon) to ensure cross-chain compatibility.
  • Transaction Simulation: Before actually sending transactions, I ran simulations to see if they would execute properly.

Testing is key to a smooth user experience, especially in Web3 projects where real money could be involved.

6 UI/UX & Final Polish
With everything working, I focused on improving the user experience and refining the design. This is where Tailwind CSS really shines.

  • Responsive Design: I made sure the app worked well on both mobile and desktop.
  • User Feedback: I added loaders, success/failure messages, and transaction status updates so users always knew what was going on.

🎉 Ready to Dive into the Code?


Now that you know how the development process went, it’s time to dive into the code! Below, I’ll walk you through each core feature with snippets and explanations, so you can recreate the DEX for yourself.

1 Wallet Connection

To enable transactions, users need to connect their MetaMask wallet. This is implemented using ethers.js:

import React, { useState, useEffect } from "react";
import { ethers } from "ethers";
import { createWalletClient, custom } from "viem";
import { mainnet } from "viem/chains";
import { useAccount } from "wagmi";

const [messageApi, contextHolder] = message.useMessage();
const [client, setClient] = useState<ReturnType<
  typeof createWalletClient
> | null>(null);

useEffect(() => {
  if (typeof window !== "undefined" && window.ethereum) {
    try {
      const walletClient = createWalletClient({
        chain: mainnet,
        transport: custom(window.ethereum),
      });

      setClient(walletClient); // Update state with the initialized client
      console.log("Ethereum client created:", walletClient);
    } catch (error) {
      console.error("Error initializing wallet client:", error);
      messageApi.error("Failed to initialize wallet client.");
    }
  } else {
    messageApi.error(
      "Ethereum provider is not available. Please install MetaMask or another wallet."
    );
  }
}, [messageApi]);

Enter fullscreen mode Exit fullscreen mode

2 Fetching Swap Quotes from Moralis

To get real-time token prices, we use Moralis’ swap quote API:

const [tokenOneAmount, setTokenOneAmount] = useState("");
  const [tokenTwoAmount, setTokenTwoAmount] = useState("");
  const [tokenOne, setTokenOne] = useState(tokenList[0]);
  const [tokenTwo, setTokenTwo] = useState(tokenList[1]);
 const { address: walletAddress } = useAccount(); 

// Initiate Swap

  const initiateSwap = async () => {
    if (!tokenOneAmount || parseFloat(tokenOneAmount) <= 0) {
      messageApi.error("Please enter a valid amount greater than zero.");
      return;
    }
    if (tokenOne.address === tokenTwo.address) {
      messageApi.error("Cannot swap the same token.");
      return;
    }
    if (!walletAddress) {
      messageApi.error("Please connect your wallet to proceed.");
      return;
    }

    console.log("Initiating swap with the following data:");
    console.log("Token One Address:", tokenOne.address);
    console.log("Token Two Address:", tokenTwo.address);
    console.log("Amount:", tokenOneAmount);

const apiKey =
      process.env.NEXT_PUBLIC_MORALIS_API_KEY || "fallback_test_key";
    if (!apiKey || apiKey === "fallback_test_key") {
      console.error("API key is missing or not configured correctly.");
      messageApi.error("An unexpected error occurred. Please try again later.");
      return;
    }

    const swapUrl = `https://deep-index.moralis.io/api/v2.2/erc20/${tokenOne.address}/swaps?chain=eth&order=DESC`;

    const options = {
      method: "GET",
      headers: {
        accept: "application/json",
        "X-API-Key": apiKey,
      },
    };

    setIsLoading(true);

    try {
      const response = await fetch(swapUrl, options);
      const data = await response.json();

      console.log("API Response:", data);

      if (data.error) {
        throw new Error(data.error.message);
      }

      const transactionData = data.result?.[0]; // Safely access the first result
      if (!transactionData) {
        console.error("No transaction data found.");
        messageApi.error("No transaction data found.");
        return;
      }

      // Validate 'value' field

      const transactionValue = transactionData.totalValueUsd
        ? BigInt(Math.round(transactionData.totalValueUsd * 1e18)) // Convert to smallest unit if required
        : BigInt(0);

      // transactionValue Logic Check
      if (!transactionValue) {
        console.error(
          "Transaction value could not be determined. Full transaction data:",
          transactionData
        );
        messageApi.error(
          "Swap details could not be retrieved. Please try again later."
        );
        return;
      }

      console.log("Transaction value:", transactionValue);

      if (!client) {
        messageApi.error("Wallet client is not initialized.");
        return;
      }

      // Convert  Transaction Value to BigInt
      const txHash = await client.sendTransaction({
        account: walletAddress,
        to: transactionData.to,
        value: transactionValue, // Use validated transaction value
        data: transactionData.data ?? "0x", // Ensure data is valid
        chain: undefined,
      });

      console.log("Transaction Hash:", txHash);
      messageApi.success("Swap successful!");
    } catch (error) {
      console.error("Error initiating swap:", error);
      messageApi.error("Error initiating the swap. Please try again later.");
    } finally {
      setIsLoading(false);
    }
  };

Enter fullscreen mode Exit fullscreen mode

3 Handling Transactions with MetaMask
Once the user confirms a swap, we send the transaction through MetaMask:

const transactionValue = transactionData.totalValueUsd
        ? BigInt(Math.round(transactionData.totalValueUsd * 1e18)) // Convert to smallest unit if required
        : BigInt(0);

      // transactionValue Logic Check
      if (!transactionValue) {
        console.error(
          "Transaction value could not be determined. Full transaction data:",
          transactionData
        );
        messageApi.error(
          "Swap details could not be retrieved. Please try again later."
        );
        return;
      }

      console.log("Transaction value:", transactionValue);

      if (!client) {
        messageApi.error("Wallet client is not initialized.");
        return;
      }

      // Convert  Transaction Value to BigInt
      const txHash = await client.sendTransaction({
        account: walletAddress,
        to: transactionData.to,
        value: transactionValue, // Use validated transaction value
        data: transactionData.data ?? "0x", // Ensure data is valid
        chain: undefined,
      });

      console.log("Transaction Hash:", txHash);
      messageApi.success("Swap successful!");
    } catch (error) {
      console.error("Error initiating swap:", error);
      messageApi.error("Error initiating the swap. Please try again later.");
    } finally {
      setIsLoading(false);
    }
  };
Enter fullscreen mode Exit fullscreen mode
  • 4 ⚠️ Error Handling & Debugging: Learning from Mistakes

Building a Web3 DEX isn’t always smooth sailing. Throughout the development process, I encountered some tricky issues — like missing recipient addresses in swap transactions. But rather than getting frustrated, I took it as an opportunity to learn and improve the application. Here’s how I tackled it, and you can too!

I. Identifying the Issue: Missing Recipient Addresses
One of the first issues I faced was when swap transactions didn’t have the recipient address set correctly. This would result in failed transactions and confused users. If you’ve worked with smart contracts, you know how important it is to double-check every parameter before sending a transaction.

II. Fixing It: Logging & Setting Router Addresses
Here’s what I did to fix it:

  • Logged API Responses: I added logging to monitor the API responses for each transaction to check if the recipient address was being properly included. This helped me pinpoint where things were going wrong.
  • Manually Set Router Addresses: For DEXes like Uniswap and 1inch, I manually set the router addresses for token swaps. These addresses are crucial in ensuring the transaction executes properly, and knowing exactly where to route the swap helped eliminate errors.
  • Validated Inputs Before Submission: I implemented checks to validate inputs before submitting transactions. If the user didn’t select a valid token pair or the recipient address wasn’t correctly set, the app would show a clear error message instead of letting the transaction fail silently.

III. Takeaway: Debugging in Web3 Is a Learning Curve

If you’re building something similar, be prepared for bugs and unexpected issues. Debugging in Web3 development is different from traditional web apps since you’re dealing with external APIs, smart contracts, and blockchain networks. Logging, validating inputs, and handling potential errors with clear feedback will go a long way in building a stable app.

🔧 Pro Tip: Don’t Skip Error Handling

In Web3, errors can cost real money, so be sure to handle them upfront. Letting the user know exactly what went wrong helps maintain trust and confidence in your app.

💡What’s Coming Next? Unlocking the Magic of Gas Estimation!

Now that we’ve ironed out those pesky bugs and made sure our DEX is running smoothly, it’s time to step up our game. Ready to dive deeper into the magic of blockchain transactions? 🚀

In this next section, we’ll explore one of the most crucial (and often overlooked) aspects of any Web3 application — gas estimation. Think of gas fees as the fuel that powers blockchain transactions. If you don’t get it right, your users could end up paying more than they bargained for.

Here’s what we’re going to do:

I. Estimate Gas Fees Automatically — I’ll show you how to calculate gas costs dynamically for every transaction.

II. Optimize for Users — Learn how to give your users the most accurate gas price estimates, so they know exactly what to expect before confirming a transaction.

III. Smooth User Experience — We’ll make sure that everything from the gas fee to transaction confirmations is as seamless as
possible, eliminating confusion and boosting trust in your app.

Trust me, mastering this part is a game-changer for creating a truly user-friendly Web3 DEX. Ready to make your platform even more robust? Let’s dive in! 🔥

🚧 Challenges Faced & How We Overcame Them

Building a Web3 DEX is not without its challenges — but don’t let that discourage you! Every issue we faced was an opportunity to learn and grow. Here’s a look at the hurdles I encountered and how I overcame them — and how you can too!

I. CORS Errors
Ah, the infamous CORS errors. I’m sure you’ve run into them if you’ve worked with APIs before. At first, it seemed like a blocker, but after some digging, I discovered that the root cause was incorrect configuration in Next.js API routes and Moralis headers. Once I set up the appropriate headers and ensured the Next.js routes were properly handling the requests, those errors vanished.

  • Fix: Configured the Next.js API routes and added the required headers for Moralis requests.
  • Tip for You: When working with third-party APIs in Next.js, always check for CORS-related issues, especially when you’re handling authentication and blockchain interactions.

II. Incorrect Token Data
Another tricky issue was incorrect token data being pulled from the blockchain. It wasn’t uncommon for the token addresses to mismatch or for tokens to not appear correctly on the selected network. This meant that swaps would fail or tokens wouldn’t show up as expected.

  • Fix: I made sure to double-check token addresses against the correct network using the ethers.js library. It’s important to validate the network you’re interacting with to avoid mismatched data.
  • Tip for You: Always validate token addresses before calling swap functions, especially when working across different blockchains.

III. Gas Estimation Issues
Gas fees are an essential part of every blockchain transaction, but estimating them accurately can be tricky. I initially ran into some issues with gas estimation that resulted in transaction failures due to incorrect gas price calculations.

  • Fix: I leveraged ethers.js for precise gas estimation. With ethers.js, I was able to fetch the estimated gas and adjust it dynamically, ensuring that the user wasn’t overpaying for transactions.
  • Tip for You: Gas estimation can save your users a lot of money. Take the time to properly estimate gas using libraries like ethers.js to provide the best possible experience.

IV. UI Responsiveness
Finally, one of the most critical aspects of any modern web app — UI responsiveness. When dealing with Web3, it’s easy to focus too much on blockchain interactions and overlook the user interface. Initially, my DEX wasn’t looking great on mobile devices.

  • Fix: I used Tailwind CSS to fine-tune the responsiveness of the layout. Tailwind made it super easy to ensure that the UI adjusted well to different screen sizes, providing a seamless experience for users across all devices.
  • Tip for You: Don’t neglect mobile users. Tailwind CSS is a fantastic tool for building responsive layouts, so make sure your UI is optimized for mobile as well as desktop.

🌟 You’re Doing Great — Let’s Keep Building This Together!

I know diving into Web3 development can feel overwhelming at times, but trust me, you’re on the right path. The Web3 ecosystem is evolving fast, and by mastering key concepts like gas estimation, you’re not just building a project — you’re shaping the future of decentralized applications. 🚀

And don’t worry if things feel challenging! Every developer, no matter how experienced, hits bumps in the road. The great thing is, you’re learning — and this knowledge will empower you to build even more complex, user-friendly DEXes in the future. 💪

So take a deep breath, keep up the great work, and let’s continue building this decentralized exchange together. After all, every step forward is a win! 🌈

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (3)

Collapse
 
ayoashy profile image
Ayo Ashiru

Insightful read Muees! Really helpful

Collapse
 
muees99 profile image
Muees A.

I am glad you found it insightful.🚀

Collapse
 
mag_daleneee profile image
Ineh Udoka

Nice one Muees! totally worth reading.

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more