We are hiring!Join the brigade to usher in a new ERA of DIDs. Visit 'Careers' page to find out more.
Get in touch
Close

Contacts

HD-110, Cinnabar Hills, Embassy Golf Links Business Park, Challaghatta, Bengaluru,
Karnataka 560071

800 100 975 20 34
+ (123) 1800-234-5678

founder@sovereigntlabs.com

We are hiring! Join the brigade to usher in a new ERA of DIDs. Visit 'Careers' page to find out more.

Awesome ERC20 Token Swap With Solidity, Vanilla Javascript, EtherJs & Web3Js – 2

Screenshot of ERC20 token swap dApp built with Solidity & Vanilla Javascript, etherjs, web3js

In the part 1 of this guide we wrote the ERC20 Token Swap smart contract and now it is time to build its frontend. The HTML, CSS code for the frontend can be found at the bottom of this guide that can be used as reference to build the frontend for our ERC20 Token Swap.

Step 3. Writing scripts for our ERC20 Token Swap

Create a file called “abi.js” which will store the ABIs of our token contract and ERC20 token contract. Although we don’t need them separately as our token contract already includes the ERC20 interface. You can omit erc20abi and instead work around only with swapAbi inside abi.js. Make necessary changes accordingly to the script.js file.

//abi.js
const swapAbi = [{
		"inputs": [
			{
				"internalType": "address",
				"name": "spender",
				"type": "address"
			},
			{
				"internalType": "uint256",
				"name": "value",
				"type": "uint256"
			}
		],
		"name": "approve",
		"outputs": [
			{
				"internalType": "bool",
				"name": "",
				"type": "bool"
			}
		],
		"stateMutability": "nonpayable",
		"type": "function"
	},
.......
};

const erc20Abi = [{
        "constant": true,
        "inputs": [{ "name": "_owner", "type": "address" }],
        "name": "balanceOf",
        "outputs": [{ "name": "balance", "type": "uint256" }],
        "type": "function"
    },
.....
];

These ABIs include necessary functions that we will be calling from our frontend to fetch important data from smart contract and also allow EOAs to interact with our token contract for buying and selling of tokens.

Functions such as “approve” & “allowances” are important for ERC20 tokens as these allow spending of tokens via smart contracts.

Main Script for our ERC20 Token Swap

Lets list down the important functions we are going to create in our script.

  • Declare smart contracts for our swap & USDT
  • BSC Network parameters
  • State variables
  • Initialize Etherjs & ensure that user has metamask installed and is on Binance Test network
  • Initialze our contracts
  • connectWallet and fetch wallet info
  • QR Code for our token contract
  • Fetch & update reserve info
  • Handle account change if user switches wallets
  • Perfom Swap
  • Swap tokens positions when switching in the UI
  • Disconnect wallet
  • Show popup and event listeners


First we declare our deployed smart contract and USDT address on top along with BSC testnet parameters:

// Contract addresses - Replace with your actual addresses
const contractAddress = "0x96d60ef17f1e8724ac5ef91c0939587D39218878";//"0xc70C6d6f5dFE8761A70921d0754edca4d18BAa12"; // Replace with BSC testnet contract
const USDT_ADDRESS = "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd"; // "0x49561Eb00e1E2Ff7a3E2a7c9664cEAa2Ce365a10";   // Replace with BSC testnet token

const BSC_TESTNET_PARAMS = {
    chainId: "0x61", // 97 in decimal
    chainName: "BSC Testnet",
    nativeCurrency: {
        name: "Binance Coin",
        symbol: "BNB",
        decimals: 18,
    },
    rpcUrls: ["https://data-seed-prebsc-1-s1.binance.org:8545/"],
    blockExplorerUrls: ["https://testnet.bscscan.com/"],
};

Next up we will declare state variables declaring provider, signer, swapContract, our token contract, USDT contract & wallet address.

let provider;
let signer;
let swapContract;
let myTokenContract;
let usdtContract;
let walletAddress;

Now we initialize EtherJs, connectWallet, initialize contracts, and make sure that the user is connected to the right network, i.e. BSC Testnet

// Initialize Ethers and Contracts
async function initEthers() {
    if (typeof window.ethereum !== 'undefined') {
        try {
            provider = new ethers.providers.Web3Provider(window.ethereum, "any");
            await ensureBSCNetwork(); // Ensure user is on BSC testnet
            return true;
        } catch (error) {
            console.error("Failed to initialize Ethers:", error);
            return false;
        }
    } else {
        showPopup("Please install MetaMask!");
        return false;
    }
}

// Ensure user is connected to BSC Mainnet
async function ensureBSCNetwork() {
    const { chainId } = await provider.getNetwork();
    if (chainId !== 97) { // 97 is the BSC Mainnet chain ID
        try {
            await window.ethereum.request({
                method: "wallet_switchEthereumChain",
                params: [{ chainId: "0x61" }], // 987 in hex
            });
        } catch (switchError) {
            if (switchError.code === 4902) {
                // Chain not added to wallet, add it
                try {
                    await window.ethereum.request({
                        method: "wallet_addEthereumChain",
                        params: [BSC_TESTNET_PARAMS],
                    });
                } catch (addError) {
                    console.error("Failed to add BSC Mainnet:", addError);
                    showPopup("Failed to switch to BSC Mainnet. Please try again.");
                }
            } else {
                console.error("Failed to switch network:", switchError);
                showPopup("Failed to switch to BSC Mainnet. Please try again.");
            }
        }
    }
}

// Initialize Contracts
async function initializeContracts() {
    swapContract = new ethers.Contract(contractAddress, swapAbi, signer);
    myTokenContract = new ethers.Contract(contractAddress, swapAbi, signer);
    usdtContract = new ethers.Contract(USDT_ADDRESS, erc20Abi, signer);
}

// Connect Wallet
async function connectWallet() {
    try {
        const success = await initEthers();
        if (success) {
            await window.ethereum.request({ method: 'eth_requestAccounts' });

            signer = provider.getSigner();
            walletAddress = await signer.getAddress();

            await initializeContracts();
            await updateWalletInfo();


            if (!ethers.utils.isAddress(walletAddress)) {
                console.error("Invalid wallet address");
            }
            if (!ethers.utils.isAddress(contractAddress)) {
                console.error("Invalid contract address");
            }
            if (!ethers.utils.isAddress(USDT_ADDRESS)) {
                console.error("Invalid USDT address");
            }

            window.ethereum.on('accountsChanged', handleAccountsChanged);
            window.ethereum.on('chainChanged', () => window.location.reload());

            showPopup("Wallet connected successfully!");
        }
    } catch (error) {
        console.error("Failed to connect wallet:", error);
        showPopup("Failed to connect wallet. Please try again.");
    }
}

// Handle account changes
async function handleAccountsChanged(accounts) {
    if (accounts.length === 0) {
        await disconnectWallet();
    } else if (accounts[0] !== walletAddress) {
        walletAddress = accounts[0];
        signer = provider.getSigner();
        await initializeContracts();
        await updateWalletInfo();
        await updateReserveInfo();
    }
}

This next part of the code checks for token & USDT balances of the connected wallet and display those balances in the UI

// Update Wallet Information
async function updateWalletInfo() {
    const walletSection = document.querySelector('.wallet-section');

    try {
        const tokenBalance = await swapContract.balanceOf(walletAddress);
        const usdtBalance = await usdtContract.balanceOf(walletAddress);

        const tokenBalanceFormatted = ethers.utils.formatEther(tokenBalance);
        const usdtBalanceFormatted = ethers.utils.formatEther(usdtBalance);

        walletSection.innerHTML = `
            <div class="wallet-info">
                <div class="wallet-address">
                    <span>Connected: ${walletAddress.substring(0, 6)}...${walletAddress.substring(38)}</span>
                    <button id="disconnect-wallet" class="disconnect-btn">Disconnect</button>
                </div>
                <div class="token-balances">
                    <div class="balance">Token Balance: ${parseFloat(tokenBalanceFormatted).toFixed(2)}</div>
                    <div class="balance">USDT Balance: ${parseFloat(usdtBalanceFormatted).toFixed(2)}</div>
                </div>
            </div>
        `;

        document.getElementById('disconnect-wallet').addEventListener('click', disconnectWallet);
    } catch (error) {
        console.error("Failed to update wallet info:", error);
        showPopup("Failed to update wallet information");
    }
}

Next we configure the QR code generation for our token contract, which is an optional step. You can omit this step as it is not necessary for the dApp to work. It is just an additional feature allowing users to copy and import our token contract into their metamask and see their balance.

function showQrCodePopup(address) {
    const qrPopup = document.getElementById("qr-popup");
    const qrCodeContainer = document.getElementById("qr-code"); // Ensure this is a <canvas> element
    const reserveAddressText = document.getElementById("reserve-address");

    // Clear any existing QR code
    qrCodeContainer.innerHTML = "";

    // Check if the address is valid
    if (!ethers.utils.isAddress(address)) {
        console.error("Invalid contract address");
        showPopup("Invalid address for QR code generation.");
        return;
    }

    // Generate QR code to the canvas
    QRCode.toCanvas(qrCodeContainer, address, { width: 200, height: 200 }, function (error) {
        if (error) {
            console.error("QR Code generation failed:", error);
            showPopup("QR Code generation failed.");
        } else {
            // Display the contract address as text below the QR code
            reserveAddressText.textContent = `Reserve Address: ${address}`;
        }
    });

    // Show the popup
    qrPopup.classList.remove("hidden");
}

document.getElementById("close-qr-popup").addEventListener("click", () => {
    document.getElementById("qr-popup").classList.add("hidden");
});

document.getElementById("reserve-address-trigger").addEventListener("click", () => {
    showQrCodePopup(contractAddress); // Use your reserve address
});

Now we create a function to fetch the total token supply and USDT balance of our contract to show as reserve balance to ensure that contract has enough USDT tokens for sell actions.

// Update Reserve Information
async function updateReserveInfo() {
const reserveSection = document.querySelector(‘.reserve-section’);
const qrButton = document.getElementById(“reserve-address-trigger”); // Select the button element

try {
    const totalTokenSupply = await myTokenContract.totalSupply();
    const usdtReserveBalance = await usdtContract.balanceOf(swapContract.address);

    const totalTokenSupplyFormatted = ethers.utils.formatEther(totalTokenSupply);
    const usdtReserveBalanceFormatted = ethers.utils.formatEther(usdtReserveBalance);

    reserveSection.innerHTML = `
        <div class="reserve-info">
            <div class="reserve-balances">
            <div class="usdt-reserve-balance">USDT Reserve Balance: ${parseFloat(usdtReserveBalanceFormatted).toFixed(2)}</div>
                <div class="token-reserve-balance">Token Total Supply: ${parseFloat(totalTokenSupplyFormatted).toFixed(2)}</div>
            </div>
        </div>
    `;
    // Show the QR Code button
    qrButton.classList.remove("hidden");
} catch (error) {
    console.error("Failed to update reserve info:", error);
    showPopup("Failed to update reserve information");
}

}

It is time to write the central part of our script, which is to perfrom swaps between USDT and our own token. The dApp needs to check user balances and alert if user is trying to input amount more than the balance owned. It also checks for approval/allowances for the tokens being spent, followed by calling the “buyTokens” & “sellTokens” function on the smart contract depending on which tokens is being spent by the user (EOA).

// Perform Swap
async function performSwap() {
    if (!walletAddress) {
        showPopup("Please connect your wallet first.");
        return;
    }

    const sellAmount = document.getElementById("sell-amount").value;
    if (!sellAmount || sellAmount <= 0) {
        showPopup("Please enter a valid amount");
        return;
    }

    const sellTokenImg = document.getElementById("sell-token").querySelector("img");
    const buyTokenImg = document.getElementById("buy-token").querySelector("img");

    const sellToken = sellTokenImg.getAttribute("data-token");
    const buyToken = buyTokenImg.getAttribute("data-token");

    console.log(sellToken, buyToken);

    try {
        const amountInWei = ethers.utils.parseEther(sellAmount.toString());
        const tokenContract = sellToken === "Arrnaya" ? myTokenContract : usdtContract;

        // Verify contract addresses
        console.log("Token Contract Address:", tokenContract.address);
        console.log("Swap Contract Address:", swapContract.address);

        const userBalanceWhileSwapping = await tokenContract.balanceOf(walletAddress);

        if (sellAmount > userBalanceWhileSwapping) {
            showPopup("you don't have enough balance");
            return;
        }

        // Check current allowance
        const allowance = await tokenContract.allowance(walletAddress, swapContract.address);

        // Log the allowance for debugging
        console.log(`Allowance: ${ethers.utils.formatEther(allowance)} tokens`);

        if (allowance.lt(amountInWei)) { // Check if allowance is less than the required amount
            showPopup("Approving token transfer...");
            const approveTx = await tokenContract.approve(swapContract.address, amountInWei);
            await approveTx.wait();
            showPopup("Token transfer approved!");
        } else {
            console.log("Sufficient allowance already available. No need for approval.");
        }

        showPopup("Executing swap...");
        let swapTx;
        if (sellToken === "USDT" && buyToken === "Arrnaya") {
            swapTx = await swapContract.buyTokens(amountInWei);
        } else if (sellToken === "Arrnaya" && buyToken === "USDT") {
            swapTx = await swapContract.sellTokens(amountInWei);
        }

        await swapTx.wait();
        showPopup(`Swap successful! ${sellAmount} ${sellToken} to ${buyToken}`);

        // Clear inputs and update balances
        document.getElementById("sell-amount").value = "";
        document.getElementById("buy-amount").value = "";
        await updateWalletInfo();
        await updateReserveInfo();
    } catch (error) {
        console.error("Swap failed:", error);
        showPopup("Swap failed. Please try again.");
    }
}

This concludes our code along and we have a dApp ready for swapping. Make sure you have claimed some test USDT tokens from the facuet mentioned in the first part of this guide. All important links are also posted at the end of this guide.

Feel free to connect with us if any guidance is needed or post a comment here. Enjoy coding!

Link to Part 1

Source Code: https://github.com/sovereigntlabs/ERC20Token-Swap/

Demo: https://defi-swap-with-solidity.netlify.app/

BSC Tesnet Faucet: https://www.bnbchain.org/en/testnet-faucet

Leave a Comment

Your email address will not be published. Required fields are marked *