In this tutorial, you'll learn how to build a simple decentralized application (dApp) using the Core blockchain TestNet. The focus will be on creating a "Hello World" dApp that interacts with a smart contract. This dApp will allow users to store, retrieve, and update a message on the blockchain. By following the steps in this guide, you will gain hands-on experience with MetaMask wallet integration, smart contract development, and front-end interaction using the Ethers.js library.
In this tutorial, we'll develop a simple Hello World decentralized application (dApp) that stores a message in a smart contract deployed on the Core blockchain TestNet. The dApp also has the functionality to retrieve and display the stored message.
This tutorial will help you gain knowledge on the following learning points:
- MetaMask Wallet connectivity to Core Testnet;
- Smart contract development and deployment on Core Testnet;
- Front-end integration with the smart contract using Ethers.js library;
- Read data from a smart contract;
- Write data to a smart contract;
- Git v2.44.0
- Node.js v20.11.1
- npm v10.2.4
- Hardhat v2.22.7
- MetaMask Web Wallet Extension
- Create a new directory for the project and navigate into it
mkdir hello-world-dapp
cd hello-world-dapp- Install Hardhat
npm init --yes
npm install --save-dev hardhat- Initialize Hardhat project by running the following command
npx hardhat init- Once this project is initialized, you'll find the following project structure:
dapp-tutorial.
| .gitignore
| hardhat-config.js
| package-lock.json
| package.json
| README.md
|
+---contracts
| Lock.sol
|
+---ignition
| \---modules
| Lock.js
|
+---node_modules
|
+---test
| Lock.js
| -
Install and configure MetaMask Chrome Extension to use with Core Testnet. Refer here for a detailed guide.
-
Create a secret.json file in the root folder and store the private key of your MetaMask wallet in it. Refer here for details on how to get MetaMask account's private key.
{"PrivateKey":"you private key, do not leak this file, do keep it absolutely safe"}Do not forget to add this file to the
.gitignorefile in the root folder of your project so that you don't accidentally check your private keys/secret phrases into a public repository. Make sure you keep this file in an absolutely safe place!
- Copy the following into your
hardhat.config.jsfile
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();
/** @type import('hardhat/config').HardhatUserConfig */
const PRIVATE_KEY = process.env.PRIVATE_KEY;
module.exports = {
defaultNetwork: 'core_testnet',
networks: {
hardhat: {
},
core_testnet: {
url: 'https://rpc.test2.btcs.network',
accounts: [PRIVATE_KEY],
chainId: 1115,
}
},
solidity: {
compilers: [
{
version: '0.8.26',
settings: {
evmVersion: 'paris',
optimizer: {
enabled: true,
runs: 200,
},
},
},
],
},
paths: {
sources: './contracts',
cache: './cache',
artifacts: './artifacts',
},
mocha: {
timeout: 60000,
},
};- Navigate to the
contractsfolder in the root directory of your project. - Delete the
Lock.solfile; create a new fileHelloWorld.soland paste the following contents into it.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract HelloWorld {
string public message;
constructor(string memory _message) {
message = _message;
}
function setMessage(string memory _message) public {
message = _message;
}
}The HelloWorld Solidity contract stores a message in a public string variable. It initializes this message through the constructor when the contract is deployed. The setMessage function allows anyone to update the stored message. The public keyword makes the message variable readable by anyone.
string public message: Public string variable to store and read a message.
constructor(string memory _message) { message = _message; }: Initializes the message state variable with a value provided during deployment.
function setMessage(string memory _message) public { message = _message; }: Allows updating the message state variable.
- To compile the
HelloWorldsmart contract defined in theHelloWorld.sol, from the root directory run the following command
npx hardhat compile-
Before deploying your smart contract on the Core blockchain, it is best adviced to first run a series of tests making sure that the smart contract is working as desired.
-
Inside the
testfolder, delete theLock.jsfile, create a new fileHelloWorld.jsand update it with the following:
const { expect } = require("chai");
describe("HelloWorld contract", function () {
let HelloWorld;
let helloWorld;
let owner;
beforeEach(async function () {
HelloWorld = await ethers.getContractFactory("HelloWorld");
[owner] = await ethers.getSigners();
helloWorld = await HelloWorld.deploy("Hello, world!");
await helloWorld.waitForDeployment();
});
it("Should return the initial message", async function () {
expect(await helloWorld.message()).to.equal("Hello, world!");
});
it("Should update the message", async function () {
const tx =await helloWorld.setMessage("Hello, Hardhat!");
await tx.wait(); // Wait for the transaction to be mined
expect(await helloWorld.message()).to.equal("Hello, Hardhat!");
});
});This test script is designed to validate the behavior of the HelloWorld smart contract. In the setup phase (beforeEach), it deploys a new instance of the contract with the initial message "Hello, world!" and waits for the deployment to complete.
The first test, "Should return the initial message," checks that the contract correctly returns the initial message stored during deployment. It ensures that the message is set to "Hello, world!" as expected.
The second test, "Should update the message," updates the message to "Hello, Hardhat!" and then verifies that the message has been updated successfully by checking its new value. This ensures that the setMessage function works correctly and the contract's state is updated as intended.
This test script checks the functionality of the HelloWorld contract:
- Setup: It deploys the HelloWorld contract with the initial message "Hello, world!" before each test.
- Test Initial Message: It verifies that the contract's initial message is correctly set to "Hello, world!".
- Test Update Message: It updates the message to "Hello, Hardhat!" and confirms that the change is successfully reflected.
Overall, these tests confirm that the contract's functionality for setting and retrieving messages operates correctly.
- Run the test scripts using the command
npx hardhat test
- Create a
scriptsfolder in the root directory of your project. Inside this folder, create a filedeploy.js; paste the following script into it.
const { ethers } = require("hardhat");
async function main() {
const [deployer] = await ethers.getSigners();
console.log("Deploying contract with the account:", deployer.address);
const HelloWorld = await ethers.getContractFactory("HelloWorld");
const helloWorld = await HelloWorld.deploy("Hello, World");
console.log("HelloWorld Contract Address:", await helloWorld.getAddress());
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});-
Make sure your MetaMask wallet has tCORE test tokens for the Core Testnet. Refer here for details on how to get tCORE tokens from Core Faucet.
-
Run the following command from the root directory of your project, to deploy your smart contract on the Core Chain.
npx hardhat run scripts/deploy.jsIf succesfully deployed, you will get the following output
- Make sure to save the Address of HelloWorld Contract at which is deployed, as obtained above, this will be used for interacting with smart contract from the dApp's frontend.
🎉 Congratulations! You have successfully learned how to create, compile, and deploy a smart contract on the Core Chain Testnet using the Hardhat.
⚡️ Let's create a frontend interface for interacting with the HelloWorld smart contract.
- Run the following commands to create a react project and install the required depencendy of
ethers.jsto communicate with the HelloWorld smart contract.
npx create-react-app frontend
cd frontend
npm install ethers- Copy the
HelloWorld.jsonfile fromartifacts/contracts/HelloWorld.sol/to thefrontend/src/Contract-ABIdirectory.
- Inside
src, create a file namedHelloWorld.js. Ensure you replace placeholders likeYOUR_CONTRACT_ADDRESSwith actual values from your deployment.
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import { toast, ToastContainer } from 'react-toastify'; // Import ToastContainer and toast
import 'react-toastify/dist/ReactToastify.css'; // Import the CSS for toast notifications
import './HelloWorld.css'; // Import the CSS file
// Import ABI from JSON file
import HelloWorldABI from './Contract-ABI/HelloWorld.json';
// Import the logo image
import logo from './core-dao-logo.png'; // Adjust the path as needed
// Replace with your contract's address
const contractAddress = "0xBF46BAA6210Ae6c9050F5453B996070209f69830";
function HelloWorld() {
const [message, setMessage] = useState('');
const [newMessage, setNewMessage] = useState('');
const [provider, setProvider] = useState(null);
const [contract, setContract] = useState(null);
const [showMessage, setShowMessage] = useState(false); // State to control message visibility
useEffect(() => {
async function init() {
if (!window.ethereum) {
toast.error("No crypto wallet found. Please install MetaMask.");
return;
}
try {
// Prompt user to connect MetaMask
await window.ethereum.request({ method: 'eth_requestAccounts' });
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const contract = new ethers.Contract(contractAddress, HelloWorldABI.abi, signer);
setProvider(provider);
setContract(contract);
} catch (error) {
toast.error("Failed to connect to MetaMask. Please try again.");
console.error("Error initializing MetaMask:", error);
}
}
init();
}, []);
const updateMessage = async () => {
if (!contract) return;
try {
const tx = await contract.setMessage(newMessage);
await tx.wait();
setNewMessage(''); // Clear the input field after updating
toast.success("Message updated successfully!");
} catch (error) {
toast.error("Failed to update message. Please try again.");
console.error("Error updating message:", error);
}
};
const retrieveMessage = async () => {
if (!contract) return;
try {
const currentMessage = await contract.message();
setMessage(currentMessage);
setShowMessage(true); // Show the message after retrieval
toast.success("Message retrieved successfully!");
} catch (error) {
toast.error("Failed to retrieve message. Please try again.");
console.error("Error retrieving message:", error);
}
};
return (
<div className="container">
<ToastContainer /> {/* Add ToastContainer to display notifications */}
<img src={logo} alt="Core DAO Logo" className="logo" />
{showMessage && (
<div className="message-display">{message}</div>
)}
<button className="retrieve-button" onClick={retrieveMessage}>Retrieve Current Message</button>
<br/>
<input
type="text"
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
className="input-field"
placeholder="Enter new message"
/>
<button className="update-button" onClick={updateMessage}>Set New Message</button>
</div>
);
}
export default HelloWorld;- Inside
src, create a file namedHelloWorld.css
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
width: 100vw;
background-color: #2e2e2e;
color: #e0e0e0;
padding: 20px;
box-sizing: border-box;
}
.logo {
width: 200px;
margin-bottom: 40px;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
}
.retrieve-button, .update-button {
background-color: #ff920f;
color: white;
border: none;
padding: 14px 28px;
margin: 10px;
cursor: pointer;
border-radius: 8px;
font-size: 18px;
transition: background-color 0.3s ease, transform 0.2s ease;
}
.retrieve-button:hover, .update-button:hover {
background-color: #e67e22;
transform: scale(1.05);
}
.message-display {
font-size: 28px;
margin-bottom: 20px;
font-weight: bold;
color: #ffd54f;
}
.input-field {
padding: 14px;
border: 1px solid #555;
border-radius: 8px;
width: 80%;
max-width: 500px;
margin-bottom: 20px;
font-size: 18px;
background-color: #424242;
color: #fff;
transition: background-color 0.3s ease, border-color 0.3s ease;
}
.retrieve-button {
margin-top: 20px;
}
.input-field:focus {
background-color: #616161;
border-color: #ff920f;
outline: none;
}- Replace the contents of App.js with the following
import React from 'react';
import './App.css';
import HelloWorld from './HelloWorld';
function App() {
return (
<div className="App">
<header className="App-header">
<HelloWorld />
</header>
</div>
);
}
export default App;- Start the React Development Server using the command
npm run start
npm start
Your application should now be accessible at http://localhost:3000.Open your React app in the browser. You should be able to retrieve and set messages using your deployed contract.

🎉 Congratulations! You've just interacted with your newly-deployed contract using your dApp's front end! You can build on the codebase by deploying and interacting with different contracts, and by adding new UI components to the website for your users.


