DEV Community

Daniel Brooks
Daniel Brooks

Posted on

How to Easily Call Smart Contracts Using Ethers.js and Next.js (BSC Testnet)

Calling smart contracts from a frontend is one of the most common Web3 tasks. At the same time, many tutorials either overcomplicate things or hide important steps behind abstractions.

This article keeps a balanced approach:

  • simple enough for developers new to Web3
  • explicit and transparent enough for experienced developers
  • no magic, no hidden logic
  • lots of real code, step by step

We will use Next.js, ethers.js, and BSC Testnet.


What You Will Build

By the end of this article, you will have:

  • a Next.js app
  • MetaMask wallet connection
  • a smart contract instance
  • a function to read data from the contract
  • a function to write data to the contract
  • a real transaction on BSC Testnet

Prerequisites

You need:

  • Node.js v16+
  • MetaMask installed in your browser
  • Basic React / JavaScript knowledge
  • No Solidity knowledge required

Get Test BNB (Required)

To send transactions on BSC Testnet, you need test BNB.

Use the official faucet:

https://testnet.bnbchain.org/faucet-smart

Paste your wallet address and request funds.


Create a Next.js Project

Create a new project and install ethers:

npx create-next-app@latest ethers-nextjs-bsc --typescript
cd ethers-nextjs-bsc
npm install ethers
Enter fullscreen mode Exit fullscreen mode

Start the dev server to verify everything works:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Wallet Connection with Ethers.js

Smart contracts are accessed through:

  • a provider (read-only)
  • a signer (for transactions)

Create a simple hook that exposes both.

// hooks/useWeb3.ts
import { useEffect, useState } from 'react'
import { ethers } from 'ethers'

export const useWeb3 = () => {
  const [provider, setProvider] =
    useState<ethers.BrowserProvider | null>(null)
  const [signer, setSigner] =
    useState<ethers.JsonRpcSigner | null>(null)
  const [account, setAccount] = useState<string | null>(null)

  const connectWallet = async () => {
    if (!window.ethereum) {
      alert('MetaMask is not installed')
      return
    }

    const browserProvider =
      new ethers.BrowserProvider(window.ethereum as any)

    await browserProvider.send('eth_requestAccounts', [])
    const signer = await browserProvider.getSigner()
    const address = await signer.getAddress()

    setProvider(browserProvider)
    setSigner(signer)
    setAccount(address)
  }

  useEffect(() => {
    if (!window.ethereum) return

    window.ethereum.on('accountsChanged', connectWallet)
    window.ethereum.on('chainChanged', () => window.location.reload())
  }, [])

  return { provider, signer, account, connectWallet }
}
Enter fullscreen mode Exit fullscreen mode

Nothing is hidden here:

  • MetaMask is requested explicitly
  • provider and signer are created manually
  • state is stored in React

Smart Contract Used in This Guide

We will use a simple contract deployed on BSC Testnet.

Contract address:

0xf22063aC68185A967eb71a2f5b877336b64bF9E1
Enter fullscreen mode Exit fullscreen mode

The contract exposes:

  • greet() → returns a string
  • setGreeting(string) → updates the greeting

Contract Helper File

Create a helper to reuse the contract instance.

// utils/contract.ts
import { ethers } from 'ethers'

export const CONTRACT_ADDRESS =
  '0xf22063aC68185A967eb71a2f5b877336b64bF9E1'

export const CONTRACT_ABI = [
  'function greet() view returns (string)',
  'function setGreeting(string _greeting)',
]

export const getContract = (
  signerOrProvider: ethers.Signer | ethers.Provider
) => {
  return new ethers.Contract(
    CONTRACT_ADDRESS,
    CONTRACT_ABI,
    signerOrProvider
  )
}
Enter fullscreen mode Exit fullscreen mode

This is exactly how ethers.js expects contracts to be instantiated.


Full Page Example (Read + Write)

Below is a complete page showing everything together.

// pages/index.tsx
import { useEffect, useState } from 'react'
import { useWeb3 } from '../hooks/useWeb3'
import { getContract } from '../utils/contract'

export default function Home() {
  const { provider, signer, account, connectWallet } = useWeb3()

  const [greeting, setGreeting] = useState('')
  const [newGreeting, setNewGreeting] = useState('')
  const [loading, setLoading] = useState(false)

  const loadGreeting = async () => {
    if (!provider) return
    const contract = getContract(provider)
    const value = await contract.greet()
    setGreeting(value)
  }

  const updateGreeting = async () => {
    if (!signer) {
      alert('Connect wallet first')
      return
    }

    try {
      setLoading(true)
      const contract = getContract(signer)
      const tx = await contract.setGreeting(newGreeting)
      await tx.wait()
      await loadGreeting()
    } finally {
      setLoading(false)
    }
  }

  useEffect(() => {
    loadGreeting()
  }, [provider])

  return (
    <main style={{ padding: 32 }}>
      {!account ? (
        <button onClick={connectWallet}>
          Connect Wallet
        </button>
      ) : (
        <p>Connected: {account}</p>
      )}

      <h2>Greeting from contract</h2>
      <p>{greeting || ''}</p>

      <input
        value={newGreeting}
        onChange={(e) => setNewGreeting(e.target.value)}
        placeholder="New greeting"
      />

      <button onClick={updateGreeting} disabled={loading}>
        {loading ? 'Sending...' : 'Update Greeting'}
      </button>
    </main>
  )
}
Enter fullscreen mode Exit fullscreen mode

This example:

  • reads contract state
  • sends a transaction
  • waits for confirmation
  • updates UI after mining

Network Reminder

This example works on BSC Testnet (chain ID 97).

If MetaMask is connected to a different network:

  • contract calls will fail
  • transactions will not be sent

Final Thoughts

Calling smart contracts with ethers.js does not require heavy abstractions.

Once you understand:

  • provider vs signer
  • contract address + ABI
  • read vs write calls

You can interact with almost any EVM smart contract from a frontend app.

This pattern works the same way on Ethereum, BSC, Polygon, and other EVM chains.

Top comments (0)