This guide walks through creating and deploying a private Ethereum blockchain project using Solidity, Truffle, Ganache, and a simple Frontend.
Don't worry if you're new to this! You should have basic understanding of:
We're going to build something really simple - a Certificate Registry using blockchain technology. We're creating a system where:
Technologies We'll Use 🛠
# Install Truffle globally
npm install -g truffle
Check the Version to Verify Installation
npm -v # Check npm version
truffle version # Check Truffle version
ganache-cli --version # Check Ganache version
Example Output
Truffle v5.10.2 (core: 5.10.2)
Ganache v7.9.0
Solidity - ^0.8.0 (solc-js)
Node v18.17.1
Web3.js v1.10.0
If you get any permission errors on Mac/Linux, add sudo before the commands.
# Create project folder and navigate into it
mkdir CertificateRegistry
cd CertificateRegistry
truffle init
Setup a default Truffle project structure:
CertificateRegistry/
├── contracts/ # Smart contracts
├── migrations/ # Deployment scripts
├── client/ # Frontend
├── truffle-config.js # Configuration file
Ganache provides a local Ethereum blockchain with test accounts for development.
Download the Ganache App in you Computer
Default Settings
Let's break down what our smart contract does in simple terms:
struct Certificate {
string name;
string achievement;
uint256 timestamp;
}
Think of this like a digital certificate template with Name of the student, What they achieved & When they got it
contracts/CertificateRegistry.sol
This file contains the logic for managing certificates:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract CertificateRegistry {
struct Certificate {
string name;
string achievement;
uint256 timestamp;
bool isValid;
}
mapping(uint256 => Certificate) public certificates;
mapping(uint256 => address) public certificateOwners;
event CertificateRegistered(
uint256 indexed id,
string name,
string achievement,
uint256 timestamp
);
event CertificateVerified(
uint256 indexed id,
bool isValid,
string name,
string achievement,
uint256 timestamp
);
function registerCertificate(
uint256 _id,
string memory _name,
string memory _achievement
) public {
require(!certificates[_id].isValid, "Certificate ID already exists");
certificates[_id] = Certificate({
name: _name,
achievement: _achievement,
timestamp: block.timestamp,
isValid: true
});
certificateOwners[_id] = msg.sender;
emit CertificateRegistered(_id, _name, _achievement, block.timestamp);
}
function verifyCertificate(uint256 _id) public view returns (
bool,
string memory,
string memory,
uint256
) {
Certificate memory cert = certificates[_id];
return (
cert.isValid,
cert.name,
cert.achievement,
cert.timestamp
);
}
function invalidateCertificate(uint256 _id) public {
require(
msg.sender == certificateOwners[_id],
"Only certificate owner can invalidate"
);
require(
certificates[_id].isValid,
"Certificate already invalidated"
);
certificates[_id].isValid = false;
}
}
Save the CertificateRegistry.sol file in the contracts/
directory.
truffle compile
Expected output:
Compiling your contracts...
===========================
> Compiling .\contracts\CertificateRegistry.sol
> Artifacts written to Learnchain\build\contracts
> Compiled successfully using:
- solc: 0.8.0+commit.c7dfd78e.Emscripten.clang
This generates the build/contracts/CertificateRegistry.json
which provides ABI (Application Binary Interface) to the app.
migrations/1_deploy_certificates.js
This is created by default:
const CertificateRegistry = artifacts.require("CertificateRegistry");
module.exports = function(deployer) {
deployer.deploy(CertificateRegistry)
.then(() => {
console.log('Contract deployed to:', CertificateRegistry.address);
});
};
Run the migration:
truffle migrate --reset
Expected output:
Compiling your contracts...
===========================
> Compiling .\contracts\CertificateRegistry.sol
> Artifacts written to LearnChain\build\contracts
> Compiled successfully using:
- solc: 0.8.0+commit.c7dfd78e.Emscripten.clang
Starting migrations...
======================
> Network name: 'development'
> Network id: 5777
> Block gas limit: 6721975 (0x6691b7)
1_deploy_certificates.js
========================
Replacing 'CertificateRegistry'
-------------------------------
> transaction hash: 0xd844e904f4df435d7aab90729d60001b
> Blocks: 0 Seconds: 0
> contract address: 0xbCb41242aD19210D46E22E05A96D83eF76dA226e
> block number: 1
> block timestamp: 1732112465
> account: 0x268cb7Ca25f179c2dBf85E4c1aC07a1394Fd1cEF
> balance: 99.997319085625
> gas used: 794345 (0xc1ee9)
> gas price: 3.375 gwei
> value sent: 0 ETH
> total cost: 0.002680914375 ETH
Contract deployed to: 0xbCb41242aD19210D46E22E05A96D83eF76dA226e
> Saving artifacts
-------------------------------------
> Total cost: 0.002680914375 ETH
Summary
=======
> Total deployments: 1
> Final cost: 0.002680914375 ETH
Copy the Contract Address for future use
Here is added Block in Ganache
Here is how Transaction is added
client/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Certificate Registry DApp</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="d-flex justify-content-center align-items-center vh-100">
<div class="container">
<h1 class="text-center mb-4">Certificate Registry</h1>
<div id="connectionStatus" class="alert alert-info text-center">
Please connect your wallet...
</div>
<div class="card mb-4">
<div class="card-header text-center">
Register New Certificate
</div>
<div class="card-body">
<form id="registerForm">
<div class="mb-3">
<label for="certId" class="form-label">Certificate ID</label>
<input type="number" class="form-control" id="certId" required>
</div>
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" required>
</div>
<div class="mb-3">
<label for="achievement" class="form-label">Achievement</label>
<input type="text" class="form-control" id="achievement" required>
</div>
<button type="submit" class="btn btn-primary w-100">Register Certificate</button>
</form>
</div>
</div>
<div class="card">
<div class="card-header text-center">
Verify Certificate
</div>
<div class="card-body">
<div class="mb-3">
<label for="verifyCertId" class="form-label">Certificate ID</label>
<input type="number" class="form-control" id="verifyCertId">
</div>
<button id="verifyBtn" class="btn btn-success w-100">Verify Certificate</button>
<div id="verificationResult" class="mt-3"></div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/web3@1.7.4/dist/web3.min.js"></script>
<script src="script.js"></script>
</body>
</html>
client/script.js
// Global variables
let web3;
let contractInstance;
// Configuration - You'll need to update this address after deploying your contract
const CONFIG = {
contractAddress: '0xf8c107958940aA3741F6fca3504C7bB7EB2A658b' // Replace with your deployed contract address
};
// DOM Elements
const elements = {
connectionStatus: document.getElementById('connectionStatus'),
registerForm: document.getElementById('registerForm'),
verifyBtn: document.getElementById('verifyBtn'),
verifyCertId: document.getElementById('verifyCertId'),
verificationResult: document.getElementById('verificationResult')
};
// Load contract ABI from JSON file
async function loadContractABI() {
try {
const response = await fetch('/build/contracts/CertificateRegistry.json');
const contractJson = await response.json();
return contractJson.abi;
} catch (error) {
console.error('Error loading contract ABI:', error);
throw new Error('Failed to load contract ABI');
}
}
// Initialize the DApp
async function initDApp() {
if (typeof window.ethereum !== 'undefined') {
try {
// Request account access
await window.ethereum.request({ method: 'eth_requestAccounts' });
// Initialize Web3
web3 = new Web3(window.ethereum);
// Load contract ABI
const contractABI = await loadContractABI();
if (!CONFIG.contractAddress) {
throw new Error('Please set the contract address in CONFIG');
}
// Initialize contract instance
contractInstance = new web3.eth.Contract(contractABI, CONFIG.contractAddress);
// Get the current account
const accounts = await web3.eth.getAccounts();
const currentAccount = accounts[0];
// Update UI
updateConnectionStatus('Connected', currentAccount);
console.log('DApp initialized successfully');
// Setup event listeners for account changes
window.ethereum.on('accountsChanged', handleAccountChange);
window.ethereum.on('chainChanged', () => window.location.reload());
} catch (error) {
console.error("Error initializing DApp:", error);
updateConnectionStatus('Error', null, error.message);
}
} else {
const message = "Please install MetaMask to use this DApp!";
console.error(message);
updateConnectionStatus('Error', null, message);
}
}
// Handle account changes
async function handleAccountChange(accounts) {
if (accounts.length === 0) {
updateConnectionStatus('Disconnected');
} else {
updateConnectionStatus('Connected', accounts[0]);
}
}
// Update connection status in UI
function updateConnectionStatus(status, account = null, errorMessage = '') {
let statusHTML = '';
switch (status) {
case 'Connected':
statusHTML = `
<strong>Status:</strong> Connected
<br>
<strong>Account:</strong> ${account}
`;
elements.connectionStatus.className = 'alert alert-success';
break;
case 'Disconnected':
statusHTML = 'Wallet disconnected. Please connect your wallet.';
elements.connectionStatus.className = 'alert alert-warning';
break;
case 'Error':
statusHTML = `Error: ${errorMessage}`;
elements.connectionStatus.className = 'alert alert-danger';
break;
default:
statusHTML = 'Please connect your wallet...';
elements.connectionStatus.className = 'alert alert-info';
}
elements.connectionStatus.innerHTML = statusHTML;
}
// Register certificate
async function registerCertificate(id, name, achievement) {
if (!contractInstance) {
throw new Error('DApp not initialized');
}
const accounts = await web3.eth.getAccounts();
if (!accounts || accounts.length === 0) {
throw new Error('No account found. Please connect MetaMask.');
}
console.log('Registering certificate...', { id, name, achievement });
try {
const result = await contractInstance.methods
.registerCertificate(id, name, achievement)
.send({
from: accounts[0],
gas: 200000
});
console.log('Transaction result:', result);
return result;
} catch (error) {
console.error('Registration error:', error);
throw error;
}
}
// Verify certificate
async function verifyCertificate(id) {
if (!contractInstance) {
throw new Error('DApp not initialized');
}
try {
const result = await contractInstance.methods
.verifyCertificate(id)
.call();
return {
isValid: result[0],
name: result[1],
achievement: result[2],
timestamp: new Date(result[3] * 1000).toLocaleString()
};
} catch (error) {
console.error('Verification error:', error);
throw error;
}
}
// Form submission handler for registration
elements.registerForm.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = {
id: document.getElementById('certId').value,
name: document.getElementById('name').value,
achievement: document.getElementById('achievement').value
};
try {
await registerCertificate(formData.id, formData.name, formData.achievement);
alert('Certificate registered successfully!');
elements.registerForm.reset();
} catch (error) {
alert(`Error registering certificate: ${error.message}`);
}
});
// Verification button handler
elements.verifyBtn.addEventListener('click', async () => {
const certId = elements.verifyCertId.value;
try {
const certInfo = await verifyCertificate(certId);
let resultHTML = `
<div class="alert ${certInfo.isValid ? 'alert-success' : 'alert-warning'}">
<strong>Certificate Status:</strong> ${certInfo.isValid ? 'Valid' : 'Invalid'}
<br>
<strong>Name:</strong> ${certInfo.name}
<br>
<strong>Achievement:</strong> ${certInfo.achievement}
<br>
<strong>Timestamp:</strong> ${certInfo.timestamp}
</div>
`;
elements.verificationResult.innerHTML = resultHTML;
} catch (error) {
elements.verificationResult.innerHTML = `
<div class="alert alert-danger">
Error verifying certificate: ${error.message}
</div>
`;
}
});
// Connect wallet button (optional, for explicit connection)
function connectWallet() {
if (typeof window.ethereum !== 'undefined') {
window.ethereum.request({ method: 'eth_requestAccounts' })
.then(initDApp)
.catch(error => {
console.error('Failed to connect wallet:', error);
updateConnectionStatus('Error', null, error.message);
});
} else {
alert('Please install MetaMask!');
}
}
// Initialize DApp when page loads
document.addEventListener('DOMContentLoaded', initDApp);
Use a simple HTTP server to serve the client/
folder:
npx http-server client
Visit the URL (e.g., http://127.0.0.1:8080
).
Here is how Dapp Looks
Setup Metamask
Setup Private Network for Ganache
Open Ganache:
Get Network Details:
http://127.0.0.1:7545
8545 for CLI1337
Add Network in Metamask:
Ganache
http://127.0.0.1:7545
8545 for CLI1337
ETH
Connect our DApp to Metamask
When Metamask Popup appears click on Next and Continue with Transaction
You'll have your app connected to Metamask
Regiser the Certificate
Complete the Transaction
Registration Complete
Verify the Certificate
Blocks After Transaction
Now once you have learned how Smart Contracts & Web3 integration you can enhance this project and push it to your github repos.
Here are few enhancements you can try :
You can code at Github Repo from Github Link
Clone the Github Repo:
git clone https://github.com/bharathajjarapu/LearnChain.git
Install Node Packages:
cd Learnchain
npm i
Run the Project through above instructions.
This project incorporates :
Congratulations! You've built a real blockchain application!
Lets End this Course with a Quote
Remember Every developer starts somewhere. Keep building, keep learning, and most importantly - have fun building awesome projects! 🚀