Example Token Swap on Nexus

This guide provides a complete walkthrough for creating a decentralized token swap application (DEX) using the Nexus blockchain. Users will be able to swap two tokens Token A and Token B for one another.

Overview

This project demonstrates fundamental decentralized finance (DeFi) operations on Nexus. Users can:

  • Connect their crypto wallets.
  • Perform token swaps (Token A ⇄ Token B) at a fixed 1:1 ratio.
  • Understand the workflow of token approvals, transactions, and balance management.

This example serves as a starting point for developers interested in creating more sophisticated financial applications.

Project Context

Decentralized exchanges (DEXs) allow direct peer-to-peer cryptocurrency transactions without intermediaries, leveraging smart contracts for secure and trustless swaps. This Nexus token swap example illustrates blockchain concepts such as smart contract deployment, ERC20 token standards, wallet integrations, and real-time transaction management.

Decentralized exchanges (DEXs) allow direct peer-to-peer cryptocurrency transactions without intermediaries, leveraging smart contracts for secure and trustless swaps. This Nexus token swap example illustrates blockchain concepts such as smart contract deployment, ERC20 token standards, wallet integrations, and real-time transaction management.

Setting Up Your Development Environment

1. Install Required Tools

# Install Node.js and npm if not already installed
# Then install Hardhat
npm install --save-dev hardhat

Hardhat is a development environment and task runner for Ethereum that helps developers compile, deploy, test, and debug smart contracts. The —save-dev flag ensures Hardhat is installed as a development-only dependency, keeping it separate from production dependencies.

2. Create a New Project

# Create a new directory
mkdir contract
cd contract

# Initialize a Node.js project with package.json
npm init -y

# Initialize a new Hardhat project
npx hardhat

3. Configure Hardhat for Nexus

Within your project directory, create/update your config file hardhat.config.js which will define the network configuration, RPC endpoint for Hardhat to connect to, and chainID for signature verification:

require("dotenv").config();
require("@nomicfoundation/hardhat-toolbox");

module.exports = {
  solidity: "0.8.20",
  networks: {
    nexus: {
      url: "https://testnet3.rpc.nexus.xyz",
      chainId: 3940,
      accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : []
    }
  }
};

4. Configure your private key in .env file

  1. Get your private key from Nexus

    • Go to the Nexus Web App
    • Sign in to your account in the top right corner
    • Click the “Settings” tab
    • Navigate to “Account & Security”
    • Click on “Private Key”
    • Click “Reveal” to view your private key
  2. Create your .env file

    • Create a new file named .env in your project root directory
    • Add your private key in this format:
    PRIVATE_KEY=your_private_key_here
    
  3. Install dotenv package

    npm install dotenv
    
  4. Update hardhat.config.js

    • Add this line at the top of your hardhat.config.js if not already present:
    require("dotenv").config();
    

Important Security Notes:

  • Never share your private key
  • Never commit it to version control
  • Keep it secure and backed up
  • Use a test account for development

5. Receive test NEX from a faucet for contract deployment

To deploy your contract on Nexus Layer 1, you’ll need NEX tokens to pay for gas fees. Gas fees are the cost of executing transactions on the blockchain, similar to how you need fuel to drive a car.

There are two ways to receive NEX for testing:

  1. Earn NEX through Proving

    • Submit proofs to earn points and NEX in real-time
    • This is the recommended way as it helps secure the network
    • For detailed instructions on proving, visit our Proving Guide
  2. Use the Nexus Faucet

    • A faucet is a service that provides small amounts of cryptocurrency for testing.
    • It’s like a water faucet, but instead of water, it dispenses test tokens
    • Visit our Faucet to receive test NEX

Note: The amount of NEX you receive from the faucet is sufficient for deploying and testing basic contracts. For more extensive testing, consider earning NEX through proving.

The Smart Contracts

1. Create the Contract Files

This example includes three contracts: two ERC20 tokens and a swap contract that allows users to exchange one token for the other at a fixed 1:1 rate. Together, they demonstrate token creation, approval flows, and user-to-contract and contract-to-user transfers.

Token A Contract

Token A is a standard ERC20 token deployed with an initial supply minted to the deployer’s wallet.

Purpose: Represents one side of the swap pair. Token A is the token users must send into the TokenSwap contract in order to receive Token B.

Behavior:

  • Inherits from OpenZeppelin’s ERC20 implementation for standard compliance and security.
  • Accepts an initialSupply argument at deployment.
  • Mints the full supply to msg.sender, typically the deployer.
contract TokenA is ERC20 {
    constructor(uint256 initialSupply) ERC20("Token A", "TKNA") {
        _mint(msg.sender, initialSupply);
    }
}

Token B Contract

Token B is functionally identical to Token A, but represents the asset dispensed during a swap.

Purpose: Acts as the output token users receive after submitting Token A to the TokenSwap contract.

Behavior:

  • Same structure and logic as Token A.
  • Deployed independently with its own symbol, name, and supply.
  • Minted to the deployer, who is expected to fund the TokenSwap contract with Token B before users can perform swaps.
contract TokenB is ERC20 {
    constructor(uint256 initialSupply) ERC20("Token B", "TKNB") {
        _mint(msg.sender, initialSupply);
    }
}

Token Swap Contract

The TokenSwap contract performs the core logic of the application: exchanging Token A for Token B at a fixed 1:1 ratio.

Purpose: Enables users to trade Token A for Token B in a single-step transaction, after approval.

Constructor:

  • Accepts the addresses of the deployed Token A and Token B contracts.
  • Stores them as immutable IERC20 instances for interaction.

Swap Logic:

  • The swap(uint256 amount) function executes the exchange.
  • First, it transfers amount of Token A from the caller to the contract using transferFrom. This requires the user to have previously approved the contract to spend their tokens.
  • Then, it transfers the same amount of Token B from the contract to the user.

Assumptions:

  • The contract must be preloaded with a sufficient Token B balance (liquidity).
  • The exchange is strictly 1:1; no pricing, slippage, or fee logic is applied.
contract TokenSwap {
    IERC20 public tokenA;
    IERC20 public tokenB;

    constructor(address _tokenA, address _tokenB) {
        tokenA = IERC20(_tokenA);
        tokenB = IERC20(_tokenB);
    }

    function swap(uint256 amount) external returns (bool) {
        require(tokenA.transferFrom(msg.sender, address(this), amount), "Transfer Token A failed");
        require(tokenB.transfer(msg.sender, amount), "Transfer Token B failed");
        return true;
    }
}

2. Create a Deployment Script

Create scripts/deploy.js:

A deployment script is a programmatic way to deploy your smart contracts to a blockchain using Hardhat. Instead of manually interacting with the network through a UI or console, deployment scripts allow you to:

  • Automate contract deployment
  • Set constructor arguments dynamically
  • Log or save deployed addresses
  • Chain deployments in a predictable order
  • Integrate easily into testing, development, or CI/CD pipelines

Here we create a new folder entitled scripts and create a file entitled deploy.js, which will deploy our code to the Layer 1:

const hre = require("hardhat");
const fs = require("fs");
const path = require("path");

async function main() {
  console.log("Deploying contracts...");

  // Deploy TokenA (6-decimals)
  const TokenA = await hre.ethers.getContractFactory("TokenA");
  const tokenA = await TokenA.deploy();
  await tokenA.waitForDeployment();
  const tokenAAddress = await tokenA.getAddress();
  console.log(`TokenA deployed to: ${tokenAAddress}`);

  // Deploy TokenB (18-decimals)
  const TokenB = await hre.ethers.getContractFactory("TokenB");
  const tokenB = await TokenB.deploy();
  await tokenB.waitForDeployment();
  const tokenBAddress = await tokenB.getAddress();
  console.log(`TokenB deployed to: ${tokenBAddress}`);

  // Deploy TokenSwap
  const TokenSwap = await hre.ethers.getContractFactory("TokenSwap");
  const tokenSwap = await TokenSwap.deploy(tokenBAddress, tokenAAddress);
  await tokenSwap.waitForDeployment();
  const tokenSwapAddress = await tokenSwap.getAddress();
  console.log(`TokenSwap deployed to: ${tokenSwapAddress}`);

  // Save addresses to file
  const addresses = {
    TokenA: tokenAAddress,
    TokenB: tokenBAddress,
    TokenSwap: tokenSwapAddress,
  };

  const filePath = path.join(__dirname, "..", "deployedAddresses.json");
  fs.writeFileSync(filePath, JSON.stringify(addresses, null, 2));
  console.log(`Addresses saved to deployedAddresses.json`);
}

main().catch((error) => {
  console.error("Deployment failed:", error);
  process.exitCode = 1;
});

This script demonstrates a complete, real-world deployment pattern:

  • It manages asynchronous deployment logic with await
  • Provides helpful logs to track progress
  • Prepares for contract verification on Nexus Explorer

Deploying Your Contract

1. Deploy to Nexus

Run the following command to deploy your contract to the Nexus network:

npx hardhat run scripts/deploy.js --network nexus

You should see the following output in your terminal:

Deploying Counter contract...
Counter contract deployed to: 0x...  # Your contract's address will appear here

This means your contract has been successfully deployed to the Nexus network. Save the contract address - you’ll need it to interact with your contract later.

2. Examine Counter contract on the Nexus Explorer

Visit the Testnet III Nexus Explorer to view your deployed contract. The block explorer is a search engine for the blockchain that lets you:

  • View all transactions
  • Check contract code
  • Monitor contract interactions
  • Track token transfers
  • View account balances

To find your contract:

  1. Copy your contract’s address from the deployment output
  2. Paste it into the search bar at the top of the explorer
  3. Click on your contract to view its details

You’ll see:

  • Contract code (verified)
  • Transaction history
  • Contract balance
  • Recent interactions
  • Event logs

This is where you can verify that your contract was deployed correctly and monitor its activity on the network.

Building a Simple Frontend

The frontend uses ethers.js v6 to interact with the smart contract. Create a new Next.js project using the command

npx create-next-app@latest

On which, you will see the following prompts:

What is your project named? frontend
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like your code inside a `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to use Turbopack for `next dev`?  No / Yes
Would you like to customize the import alias (`@/*` by default)? No / Yes
What import alias would you like configured? @/*

Install ether.js in your Next.js project:

cd frontend
npm install ethers

Edit the src/app/page.tsx file to integrate the deployed smart contract with the frontend:

'use client';

import { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import deployedAddresses from '../../lib/deployedAddresses.json';

// Import ABIs
const TokenA = {
  abi: [
    "function balanceOf(address owner) view returns (uint256)",
    "function approve(address spender, uint256 amount) returns (bool)"
  ]
};

const TokenB = {
  abi: [
    "function balanceOf(address owner) view returns (uint256)"
  ]
};

const TokenSwap = {
  abi: [
    "function swap(uint256 amount) returns (bool)"
  ]
};

export default function Home() {
  const [account, setAccount] = useState('');
  const [tokenABalance, setTokenABalance] = useState('0');
  const [tokenBBalance, setTokenBBalance] = useState('0');
  const [swapAmount, setSwapAmount] = useState('');
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    checkIfWalletIsConnected();
  }, []);

  useEffect(() => {
    if (account) {
      updateBalances();
    }
  }, [account]);

  async function checkIfWalletIsConnected() {
    try {
      const { ethereum } = window as any;
      if (!ethereum) return;

      const accounts = await ethereum.request({ method: 'eth_accounts' });
      if (accounts.length > 0) {
        setAccount(accounts[0]);
      }
    } catch (error) {
      console.error(error);
    }
  }

  async function connectWallet() {
    try {
      const { ethereum } = window as any;
      if (!ethereum) return;

      const accounts = await ethereum.request({ method: 'eth_requestAccounts' });
      setAccount(accounts[0]);
    } catch (error) {
      console.error(error);
    }
  }

  async function updateBalances() {
    try {
      const { ethereum } = window as any;
      const provider = new ethers.BrowserProvider(ethereum);
      const signer = await provider.getSigner();

      const tokenAContract = new ethers.Contract(deployedAddresses.TokenA, TokenA.abi, signer);
      const tokenBContract = new ethers.Contract(deployedAddresses.TokenB, TokenB.abi, signer);

      const balanceA = await tokenAContract.balanceOf(account);
      const balanceB = await tokenBContract.balanceOf(account);

      setTokenABalance(ethers.formatEther(balanceA));
      setTokenBBalance(ethers.formatEther(balanceB));
    } catch (error) {
      console.error(error);
    }
  }

  async function handleSwap() {
    if (!swapAmount) return;
    setLoading(true);

    try {
      const { ethereum } = window as any;
      const provider = new ethers.BrowserProvider(ethereum);
      const signer = await provider.getSigner();

      const tokenAContract = new ethers.Contract(deployedAddresses.TokenA, TokenA.abi, signer);
      const swapContract = new ethers.Contract(deployedAddresses.TokenSwap, TokenSwap.abi, signer);

      const amount = ethers.parseEther(swapAmount);

      // First approve TokenSwap contract to spend TokenA
      const approveTx = await tokenAContract.approve(deployedAddresses.TokenSwap, amount);
      await approveTx.wait();

      // Then perform the swap
      const swapTx = await swapContract.swap(amount);
      await swapTx.wait();

      // Update balances after swap
      await updateBalances();
      setSwapAmount('');
    } catch (error) {
      console.error(error);
    } finally {
      setLoading(false);
    }
  }

  return (
    <div className="min-h-screen bg-white relative">
      {/* Connection status */}
      <div className="absolute top-6 right-6">
        <p className="text-sm text-gray-600">
          {account ? 'Connected' : 'Not Connected'}
        </p>
      </div>

      {/* Main content */}
      <div className="flex flex-col items-center justify-center min-h-screen">
        <h1 className="text-5xl font-normal mb-12 text-black">Nexus Swap</h1>
        
        {!account ? (
          <button
            onClick={connectWallet}
            className="bg-black text-white px-6 py-3 rounded-full text-sm font-medium hover:bg-gray-800 transition-colors"
          >
            Connect Wallet
          </button>
        ) : (
          <div className="space-y-8 w-full max-w-sm">
            <div className="grid grid-cols-2 gap-6">
              <div>
                <p className="text-sm text-gray-600 mb-1">Token A Balance</p>
                <p className="font-mono text-xl">{tokenABalance}</p>
              </div>
              <div>
                <p className="text-sm text-gray-600 mb-1">Token B Balance</p>
                <p className="font-mono text-xl">{tokenBBalance}</p>
              </div>
            </div>

            <div>
              <label className="block text-sm text-gray-600 mb-2">
                Swap Amount (Token A)
              </label>
              <input
                type="number"
                value={swapAmount}
                onChange={(e) => setSwapAmount(e.target.value)}
                placeholder="0.0"
                className="w-full bg-gray-50 border border-gray-200 px-4 py-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-black font-mono"
              />
            </div>

            <button
              onClick={handleSwap}
              disabled={loading || !swapAmount}
              className="w-full bg-black text-white py-3 rounded-full text-sm font-medium hover:bg-gray-800 transition-colors disabled:bg-gray-300"
            >
              {loading ? 'Processing...' : 'Swap'}
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

Project Structure

This repository is organized as follows

nexus-swap-example/
├── contracts/               # Hardhat project for deploying TokenA, TokenB, and TokenSwap
├── src/                    # Next.js frontend application
├── lib/                    # Shared frontend utilities (e.g., deployed contract addresses)
├── scripts/                # Hardhat scripts
├── test/                   # Contract tests
├── typechain-types/        # TypeChain-generated TypeScript bindings
├── .env                    # Environment variables for local setup
├── hardhat.config.ts       # Hardhat configuration file
├── tailwind.config.js      # Tailwind CSS config
├── vercel.json             # Vercel deployment configuration
├── README.md               # Project documentation

Key Features

Smart Contract Architecture

  • Utilizes robust ERC20 token standards provided by OpenZeppelin.
  • Straightforward 1:1 swap logic ensuring simplicity and transparency.
  • Secure approval and transaction processes.

Frontend Implementation

  • Modern UI leveraging Next.js and Tailwind CSS.
  • Dynamic real-time token balance updates.
  • User-friendly wallet connection and transaction status indicators.
  • Comprehensive error handling and notifications for a smooth user experience.

Demonstrated DeFi Concepts

  • Token approvals and transaction flow.
  • Multi-step DeFi transactions (approve and swap).
  • Real-time balance and contract interaction management.
  • Monitoring blockchain events for accurate transaction feedback.

Resources

Follow these steps to set up and run the application locally:

  1. Clone and install dependencies:
git clone https://github.com/nexus-xyz/nexus-swap-example
cd nexus-swap-example
npm install
  1. Deploy smart contracts:
cd contracts
npx hardhat run scripts/deploy.ts --network nexus
  1. Run frontend locally:
npm run dev

Using the dApp

Follow these simple steps to execute a token swap:

  1. Connect your cryptocurrency wallet (e.g., MetaMask).
  2. Verify your current balances of Token A and Token B.
  3. Enter the amount of Token A to swap.
  4. Click “Swap” and approve the transaction.
  5. Confirm the transaction in your wallet.
  6. Observe updated balances upon transaction completion.

Deployed Contract Addresses

  • Token A: 0x83FD4F92926AC4FB7a0b510aD5615Fe76e588cF8
  • Token B: 0x67bae4B1E5528Fb6C92E7E0BC243341dc71330db
  • Token Swap: 0xf18529580694Ff80e836132875a52505124358EC