Overview

In this guide, you’ll learn how to build and deploy a simple Counter smart contract on the Nexus Layer 1 blockchain. This hands-on tutorial is perfect for getting started with smart contract development and understanding the basics of state management on-chain. This project will also demonstrate a basic integration between a web application and the Nexus blockchain. It will feature a smart contract that maintains a counter which can only be incremented, showcasing fundamental blockchain interactions like:
  • Smart contract deployment
  • Wallet connection
  • Transaction signing
  • Event listening
  • State updates
What you’ll accomplish
  • Set up a full-stack development environment using Hardhat
  • Configure your wallet and acquire test NEX tokens
  • Write, compile, and deploy a counter smart contract
  • Verify the contract on the Nexus blockchain explorer
  • Interact with the contract to increment and retrieve the count value
What is a Counter application? A counter is one of the simplest smart contracts, yet it introduces you to critical blockchain concepts like state variables, public functions, and transactions. It keeps track of a number that can be incremented, demonstrating how on-chain data can change over time through user interaction. This kind of contract is useful for:
  • Learning how state changes are stored and managed on-chain
  • Tracking metrics like votes, visits, or events
  • Serving as a foundation for more complex decentralized applications
At the end of the tutorial, you would have build your very own Counter dApp just like: Nexus Counter App

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: {
      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.

Writing and Deploying The Counter Smart Contract

1. Create a Contract File

Create contracts/Counter.sol: This contract is a basic stateful Solidity example for learning blockchain development. It demonstrates essential concepts such as modifying contract storage, emitting events, and exposing read-only and state-changing functions. The contract implements a simple counter. When deployed, it starts at zero and exposes a method to increment the count. Each increment emits an event with the new value, allowing external apps or frontends to react to on-chain activity. This type of contract is useful for:
  • Learning on-chain state management
  • Demonstrating smart contract → frontend interactions
  • Testing web3 UX with simple function calls
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Counter {
    uint256 private count;

    event CountIncremented(uint256 newCount);

    function increment() public {
        count += 1;
        emit CountIncremented(count);
    }

    function getCount() public view returns (uint256) {
        return count;
    }
}
This contract defines a simple on-chain counter with two functions: one to increment the value and another to read it. It’s designed to demonstrate basic smart contract interactions including state mutation, event emission, and data retrieval.

State

uint256 private count;
The counter value is stored in a private uint256 variable. Making it private ensures that external contracts can’t modify or read it directly—access is only possible through the exposed functions.

Event

event CountIncremented(uint256 newCount);
This event is triggered every time the counter is incremented. Frontends or indexers can listen for it to react to changes or update UI state without having to call getCount() repeatedly.

Functions

increment()
function increment() public {
    count += 1;
    emit CountIncremented(count);
}
Increases the counter by one. Anyone can call this function. After updating the state, it emits an event with the new count so external listeners can track updates. getCount()
function getCount() public view returns (uint256) {
    return count;
}
Returns the current counter value. This is a view function, so it doesn’t consume gas when called from off-chain. It gives the frontend a way to fetch the latest state directly from the blockchain.

Usage

This contract is used to demonstrate how a frontend app can:
  • Read blockchain state (getCount)
  • Send a transaction to change it (increment)
  • Listen for changes via events (CountIncremented)

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");

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

  // Get the contract factory
  const Counter = await hre.ethers.getContractFactory("Counter");

  // Deploy the contract
  const counter = await Counter.deploy();

  // Wait for deployment to finish
  await counter.waitForDeployment();

  const counterAddress = await counter.getAddress();
  console.log(`Counter contract deployed to: ${counterAddress}`);

  // Wait for a few block confirmations before verifying
  console.log("Waiting for block confirmations...");
  await new Promise(resolve => setTimeout(resolve, 30000)); // Wait 30 seconds

  // Verify the contract
  console.log("Verifying contract...");
  try {
    await hre.run("verify:verify", {
      address: counterAddress,
      constructorArguments: [],
    });
    console.log("Contract verified successfully");
  } catch (error) {
    if (error.message.includes("Already Verified")) {
      console.log("Contract is already verified!");
    } else {
      console.error("Error verifying contract:", error);
    }
  }
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
  console.error(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, useCallback } from 'react'
import { BrowserProvider, JsonRpcSigner, Contract } from 'ethers'

const CONTRACT_ADDRESS = 'enter your deployed contract address'
const CONTRACT_ABI = [
  "function increment() public",
  "function getCount() public view returns (uint256)"
]

const NEXUS_CHAIN_ID = '0x3940'
const NEXUS_RPC_URL = "https://testnet3.rpc.nexus.xyz"
const EXPLORER_URL = 'https://testnet3.explorer.nexus.xyz'

export default function Home() {
  const [count, setCount] = useState<number>(0)
  const [isConnected, setIsConnected] = useState(false)
  const [isCorrectNetwork, setIsCorrectNetwork] = useState(false)
  const [signer, setSigner] = useState<JsonRpcSigner | null>(null)
  const [userAddress, setUserAddress] = useState<string>('')
  const [lastTxHash, setLastTxHash] = useState<string>('')

  useEffect(() => {
    checkWalletConnection()

    // Add network change listener
    if (window.ethereum) {
      window.ethereum.on('chainChanged', async () => {
        const networkCorrect = await checkNetwork()
        if (networkCorrect) {
          const provider = new BrowserProvider(window.ethereum)
          setSigner(await provider.getSigner())
          await getCount()
        }
      })
    }

    // Cleanup listener
    return () => {
      if (window.ethereum) {
        window.ethereum.removeListener('chainChanged', () => {
          console.log('Network change listener removed')
        })
      }
    }
  }, [])

  useEffect(() => {
    if (signer) {
      signer.getAddress().then(address => setUserAddress(address))
      getCount() // Fetch count whenever signer changes
    }
  }, [signer])

  const getCount = useCallback(async () => {
    if (!signer) return
    
    const contract = new Contract(CONTRACT_ADDRESS, CONTRACT_ABI, signer)
    try {
      const currentCount = await contract.getCount()
      setCount(Number(currentCount))
    } catch (error) {
      console.error('Error getting count:', error)
    }
  }, [signer])

  const checkNetwork = useCallback(async () => {
    const chainId = await window.ethereum.request({ method: 'eth_chainId' })
    setIsCorrectNetwork(chainId === NEXUS_CHAIN_ID)
    return chainId === NEXUS_CHAIN_ID
  }, [])

  const switchNetwork = async () => {
    try {
      await window.ethereum.request({
        method: 'wallet_switchEthereumChain',
        params: [{ chainId: NEXUS_CHAIN_ID }],
      })
      const networkCorrect = await checkNetwork()
      if (networkCorrect) {
        const provider = new BrowserProvider(window.ethereum)
        setSigner(await provider.getSigner())
        await getCount()
      }
      return true
    } catch (switchError: any) {
      if (switchError.code === 4902) {
        try {
          await window.ethereum.request({
            method: 'wallet_addEthereumChain',
            params: [{
              chainId: NEXUS_CHAIN_ID,
              rpcUrls: [NEXUS_RPC_URL],
              chainName: 'Nexus Testnet',
              nativeCurrency: {
                name: 'NEXUS',
                symbol: 'NEXUS',
                decimals: 18
              },
            }],
          })
          const networkCorrect = await checkNetwork()
          if (networkCorrect) {
            const provider = new BrowserProvider(window.ethereum)
            setSigner(await provider.getSigner())
            await getCount()
          }
          return true
        } catch (addError) {
          console.error('Error adding network:', addError)
          return false
        }
      }
      console.error('Error switching network:', switchError)
      return false
    }
  }

  const checkWalletConnection = useCallback(async () => {
    if (typeof window.ethereum !== 'undefined') {
      try {
        const provider = new BrowserProvider(window.ethereum)
        const accounts = await provider.listAccounts()
        if (accounts.length > 0) {
          const networkCorrect = await checkNetwork()
          setIsConnected(true)
          if (networkCorrect) {
            setSigner(await provider.getSigner())
          }
        }
      } catch (error) {
        console.error('Error checking wallet connection:', error)
      }
    }
  }, [checkNetwork])

  const connectWallet = async () => {
    if (typeof window.ethereum !== 'undefined') {
      try {
        const provider = new BrowserProvider(window.ethereum)
        await provider.send('eth_requestAccounts', [])
        const networkCorrect = await checkNetwork()
        setIsConnected(true)
        if (networkCorrect) {
          setSigner(await provider.getSigner())
        }
      } catch (error) {
        console.error('Error connecting wallet:', error)
      }
    }
  }

  const incrementCount = async () => {
    if (!signer) return
    
    const contract = new Contract(CONTRACT_ADDRESS, CONTRACT_ABI, signer)
    try {
      const tx = await contract.increment()
      setLastTxHash(tx.hash)
      await tx.wait()
      await getCount()
    } catch (error) {
      console.error('Error incrementing count:', error)
    }
  }

  const formatAddress = (address: string) => {
    if (!address) return ''
    return `${address.slice(0, 6)}...${address.slice(-4)}`
  }

  const formatHash = (hash: string) => {
    if (!hash) return ''
    return `${hash.slice(0, 6)}...${hash.slice(-4)}`
  }

  return (
    <main className="min-h-screen bg-white relative">
      <div className="absolute top-4 right-4 px-4 py-2 rounded-full border border-black/10">
        <p className="text-sm font-medium text-black/80">
          {isConnected ? formatAddress(userAddress) : 'Not Connected'}
        </p>
      </div>

      <div className="flex items-center justify-center min-h-screen">
        <div className="max-w-4xl w-full px-4">
          <div className="space-y-12 text-center">
            <h1 className="text-5xl font-light tracking-tight text-black">
              Nexus Counter
            </h1>
            
            <div className="space-y-8">
              {!isConnected ? (
                <button
                  onClick={connectWallet}
                  className="px-8 py-3 text-sm font-medium text-white bg-black rounded-full 
                           hover:bg-gray-800 transition-colors duration-200 
                           focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black"
                >
                  Connect Wallet
                </button>
              ) : !isCorrectNetwork ? (
                <button
                  onClick={switchNetwork}
                  className="px-8 py-3 text-sm font-medium text-black bg-transparent border-2 border-black rounded-full 
                           hover:bg-black/10 transition-colors duration-200
                           focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black"
                >
                  Switch to Nexus Network
                </button>
              ) : (
                <div className="space-y-8">
                  <div className="text-8xl font-light text-black">
                    {count}
                  </div>
                  <div className="flex flex-col items-center space-y-3">
                    <button
                      onClick={incrementCount}
                      className="px-8 py-3 text-sm font-medium text-white bg-black rounded-full 
                               hover:bg-gray-800 transition-colors duration-200
                               focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black"
                    >
                      Increment Counter
                    </button>
                    {lastTxHash && (
                      <a
                        href={`${EXPLORER_URL}/tx/${lastTxHash}`}
                        target="_blank"
                        rel="noopener noreferrer"
                        className="inline-flex items-center text-sm text-gray-500 hover:text-gray-800 
                                 transition-colors duration-200"
                        aria-label={`View transaction ${formatHash(lastTxHash)} on Nexus Explorer`}
                      >
                        <span className="mr-1">Latest tx:</span>
                        <span className="font-medium">{formatHash(lastTxHash)}</span>
                        <svg 
                          className="w-3.5 h-3.5 ml-1" 
                          fill="none" 
                          stroke="currentColor" 
                          viewBox="0 0 24 24"
                        >
                          <path 
                            strokeLinecap="round" 
                            strokeLinejoin="round" 
                            strokeWidth={2} 
                            d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" 
                          />
                        </svg>
                      </a>
                    )}
                  </div>
                </div>
              )}
            </div>
          </div>
        </div>
      </div>
    </main>
  )
} 

Project Structure

The project is separated into two packages:
  • contracts: A Hardhat project for deploying Counter.sol on Nexus
  • frontend: A NextJS project for interacting with Counter.sol on Nexus
nexus-counter/
|   ├── counter-contracts/    # Smart contract development
|   └── Lock.sol          # Solidity contract files
|   ├── scripts/              # Main Counter contract
|   └── deploy.js         # Deployment scripts
   ├── .env                  # TypeScript deployment script
   ├── hardhat.config.js     # Hardhat configuration
   └── package.json          # Next.js frontend application
├── frontend/
   ├── src/
   └── app/              # Next.js 13+ App Router packages
|   |       ├──global.css     # Styling for the frontend
|   |       ├──layout.tsx     # Components to be displayed in all pages in the web-app (eg. NavBar, SideBar)
|   |       └──page.tsx       # Components to be displayed in the homepage
   └── public/               # Static assets
├── package.json
└── README.md

Resources

To deploy and test the Counter dApp from the exisitng repo:
  1. Clone the repository and install dependencies:
git clone https://github.com/nexus-xyz/nexus-swap-example
cd nexus-swap-example
npm install
  1. Deploy the contracts:
cd contracts
npx hardhat run scripts/deploy.ts --network nexus
  1. Update the deplyed contract address in the frontend/src/app/page.tsx file.
  2. Start the frontend:
cd frontend
npm run dev
We will share more end-to-end guides in the future.