How to interact with smart contracts deployed on Mode from a frontend using ethers.js
In this tutorial, we’ll walk through setting up a basic web app, deploying a smart contract on MODE Network’s testnet, and interacting with it using ethers.js.
Prerequisites
- Add MODE Testnet to your metamask using the network info from here:Mode Testnet Information
- Get test ETH sepolia here: Testnet Faucets
- Bridge to MODE testnet here: Bridging to Mode Testnet
- Have Node and NPM installed on your PC
- Have basic understanding of React.js and how blockchain works (You can still follow along if not ;) )
Getting Started
Now that we have the prerequisites, the first thing we need to do is to open our terminal and navigate into the folder where you want your project to be and create a simple react app by running the following command npx create-react-app bidboard-ui
It should create the whole react app for you.
Install Ether.js
Ethers.js is a sleek library facilitating the interaction between web applications and blockchain. In the App component, we are using ethers.js to interact with our smart contract and also listen to events being emitted from the smart contract. To install ethers.js, navigate into the project you just created and run the following command:
npm install ethers
Create Main App Component
You can open up the project in VSCode or any other code editor of your choice and then open the App.js
file
Replace all the code and paste this:
import React, { useState, useEffect } from "react";
import "./App.css";
import { ethers } from "ethers";
return (
<div className="app">
<h1>hello</h1>
</div>
);
};
export default App;
In this file are importing useState
and useEffect
which we'll be needing later in this tutorial, and we’re also importing ethers
to enable us make a connection to our smart contract. The next thing we need to do is to declare our contract address and ABI file, there are different ways get the contract ABI but if you deployed you smart contract using foundry, you can find your ABI check the following directory ./out/MyContract.sol/MyContract.json, e.g
On the frontend side of things, we’re going to update our App.js
adding these lines:
const contractAddress = "0x7215c347458DB86531f3c4Cae1c60c0B93e435Ce";
const abi = [ ABI JSON CODE ]
const [currentAd, setCurrentAd] = useState("Hello World!");
const [currentBid, setCurrentBid] = useState(0);
const [advertiser, setAdvertiser] = useState("0x0");
const [newAd, setNewAd] = useState("");
const [bidAmount, setBidAmount] = useState("");
const [provider, setProvider] = useState(null);
const [status, setStatus] = useState("");
Your App.js
should look like:
import React, { useState, useEffect } from "react";
import "./App.css";
import { ethers } from "ethers";
const contractAddress = "0x7215c347458DB86531f3c4Cae1c60c0B93e435Ce";
const abi = [ YOUR ABI JSON CODE HERE ]
const [currentAd, setCurrentAd] = useState("Hello World!");
const [currentBid, setCurrentBid] = useState(0);
const [advertiser, setAdvertiser] = useState("0x0");
const [newAd, setNewAd] = useState("");
const [bidAmount, setBidAmount] = useState("");
const [provider, setProvider] = useState(null);
const [status, setStatus] = useState("");
return (
<div className="app">
<h1>hello</h1>
</div>
);
};
export default App;
Set the Provider
As the page loads, we’ll set our provider to immediately fetch the current advertisement details from our smart contract, as demonstrated in the code below.
useEffect(() => {
if (typeof window !== "undefined") {
if (window.ethereum) {
setProvider(new ethers.BrowserProvider(window.ethereum));
// getCurrentAd()
} else {
console.error("Please install MetaMask!");
}
}
}, []);
This code is using the useEffect
hook in React to execute once after the component mounts. If the window
object and window.ethereum
are defined, it sets a new provider using ethers.BrowserProvider
to interact with the Ethereum blockchain. If window.ethereum
is not defined, it logs an error asking to install MetaMask, a browser extension for managing blockchain transactions.
Fetch Current Advertisement Data
Create a function to fetch the current advertisement data from our contract and call this function inside a useEffect
hook.
async function fetchCurrentAd() {
try {
// Get the provider, instantiate the contract and then call getCurrentAd
const provider = new ethers.BrowserProvider(window.ethereum);
const contract = new ethers.Contract(contractAddress, abi, provider);
const adData = await contract.getCurrentAd();
setCurrentAd(adData[0]);
setAdvertiser(adData[1]);
setCurrentBid(ethers.formatEther(adData[2]));
console.log(adData[0]);
} catch (error) {
console.error('Error fetching current ad:', error);
}
}
useEffect(() => {
fetchCurrentAd();
}, []);
In the above code we’re fetching the current ad by setting our provider like we did before, creating a new instance of our contract by passing in the contract address, contractABI and our provider and then invoking the getCurrentAd
function from our smart contract which returns the current adData. Then we pass the adData into our app state so we can render them in the frontend.
Update Our Smart Contract State
Now we’re able to fetch the current state of our contract we also need to be able to update the state and that’s what the below function does
const submitBid = async () => {
const contract = new ethers.Contract(contractAddress, abi, provider); // Instantiate the contract
const signer = provider.getSigner(); // Assumes Metamask or similar is injected in the browser
const contractWithSigner = contract.connect(await signer);
try {
const tx = await contractWithSigner.updateMessage(newAd, {
value: ethers.parseEther(bidAmount),
});
setStatus("Transaction sent, waiting for confirmation...");
await tx.wait();
setStatus("Transaction confirmed!");
} catch (err) {
console.error(err);
setStatus("Error: " + err.message);
}
};
In summary, the submitBid
function allows users to submit a new bid to the smart contract. It initializes a contract instance and a signer, creates a transaction to update the advertisement message with the new bid, and waits for the transaction to be confirmed on the blockchain.
If any errors occur during this process, they are caught and logged, and the user is informed about the error through a status message.
Listening to events on our Smart Contract
Consider you’re bidding for ad space, and someone else places a higher bid. It’s essential to know about this new bid immediately. This is where event listening helps: it can notify you about the latest bid in real time, keeping you updated on the bidding activity.
The following code sets up an event listener:
useEffect(() => {
const setupEventListener = async () => {
if (typeof window.ethereum !== "undefined") {
const provider = new ethers.BrowserProvider(window.ethereum);
const contract = new ethers.Contract(contractAddress, abi, provider);
contract.on(
"MessageUpdated",
(newMessage, newAdvertiser, newAmount, event) => {
// Update your state variables here
setCurrentAd(newMessage);
setCurrentBid(ethers.formatEther(newAmount));
setAdvertiser(newAdvertiser);
}
);
// contract.getEvent
console.log("Provider:", provider); // Debug line
} else {
console.error("Ethereum provider is not available");
}
};
setupEventListener();
// Cleanup the event listener
return () => {
if (typeof window.ethereum !== "undefined") {
const provider = new ethers.BrowserProvider(window.ethereum);
const contract = new ethers.Contract(contractAddress, abi, provider);
contract.removeAllListeners("MessageUpdated");
}
};
}, []);
This above code sets up an event listener using the useEffect hook to listen for the MessageUpdated
event emitted by the smart contract whenever the advertisement message is updated.
When such an event is detected, it updates the state variables currentAd
, currentBid
, and advertiser
with the new message, advertiser, and bid amount, respectively. This ensures that the displayed data on the web app remains synchronized with the blockchain.
Lastly, it provides a cleanup function to remove the event listener, preventing potential memory leaks when the component is unmounted or re-rendered.
Code The UI
That’s it, we’re done with the onchain functionalities, now let’s build UIs to test them out. How you choose to build your UI is entirely up to you, for our project we’re just going to add the following JSX into our App.js
code:
<div className="app">
{/* Landing Section */}
<section className="landing">
<h1>BidBoard</h1>
<p>Status: {status}</p>
</section>
<div className="container">
{/* Bid Section */}
<section className="bid-section">
<input
type="text"
value={newAd}
onChange={(e) => setNewAd(e.target.value)}
placeholder="Enter your advert message"
/>
<input
type="number"
value={bidAmount}
onChange={(e) => setBidAmount(e.target.value)}
placeholder="Enter your bid amount"
/>
<button onClick={submitBid}>Submit Bid</button>
</section>
{/* Advert Display Section */}
<section className="advert-display">
<div className="current-ad">"{currentAd}"</div>
<div className="card-details">
<div className="current-bid">
Current Bid: <br />
{currentBid} ETH
</div>
<div className="advertiser">
Advertiser: <br />
{advertiser}
</div>
</div>
</section>
</div>
{/* Footer Section */}
<footer>
<a
href="https://github.com/your-repo"
target="_blank"
rel="noopener noreferrer"
>
GitHub Repository
</a>
{/* Add more links as needed */}
</footer>
</div>
Running the project
To run the project open your terminal and run the following command in the project’s folder:
Npm start
Your console should look similar to this:
Open the URL shown in your browser and you are good to try your dApp!