Build a Simple Fraternity Registration DApp
You’ll deploy a FraternityRegistration contract and build a minimal Web UI to register members and check membership via MetaMask → Ganache.
Tip: Use only local networks for this project (Ganache). Do not deploy on mainnet.
Reference
This guide matches your provided spec and fills in missing details (ABI, network config, and front‑end wiring).
Fraternity DApp – Project Structure
fraternity-dapp/
├─ contracts/
│ └─ FraternityRegistration.sol
├─ migrations/
│ └─ 2_deploy_contracts.js
├─ build/
│ └─ contracts/
│ └─ FraternityRegistration.json # generated after compile
├─ frontend/
│ ├─ index.html
│ ├─ app.js
│ ├─ package.json
│ └─ bs-config.json # (added) serve parent build/ to the browser
├─ truffle-config.js
└─ README.md
1) Initialize Truffle Project
mkdir fraternity-dapp
cd fraternity-dapp
truffle init
Configure truffle-config.js
for Ganache GUI (port 7545
):
module.exports = {
networks: {
development: {
host: "127.0.0.1",
port: 7545,
network_id: "*",
},
},
compilers: {
solc: { version: "0.8.20" }
}
};
2) Smart Contract
Create contracts/FraternityRegistration.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract FraternityRegistration {
address public owner;
uint256 public totalMembers;
struct Member { string name; bool isRegistered; }
mapping(address => Member) public members;
event MemberRegistered(address indexed memberAddress, string name);
event MemberRemoved(address indexed memberAddress);
modifier onlyOwner(){ require(msg.sender == owner, "Only owner"); _; }
constructor(){ owner = msg.sender; }
function registerMember(address _memberAddress, string memory _name) public onlyOwner {
require(!members[_memberAddress].isRegistered, "Already registered");
members[_memberAddress] = Member({ name:_name, isRegistered:true });
totalMembers++;
emit MemberRegistered(_memberAddress, _name);
}
function isMember(address _memberAddress) public view returns (bool) {
return members[_memberAddress].isRegistered;
}
function getMemberDetails(address _memberAddress) public view returns (string memory) {
require(members[_memberAddress].isRegistered, "Not registered");
return members[_memberAddress].name;
}
function removeMember(address _memberAddress) public onlyOwner {
require(members[_memberAddress].isRegistered, "Not registered");
delete members[_memberAddress];
totalMembers--;
emit MemberRemoved(_memberAddress);
}
function transferOwnership(address _newOwner) public onlyOwner {
require(_newOwner != address(0), "Invalid owner");
owner = _newOwner;
}
}
Compile:
truffle compile
3) Migration & Deploy
Create migrations/2_deploy_contracts.js
:
const FraternityRegistration = artifacts.require("FraternityRegistration");
module.exports = function (deployer) {
deployer.deploy(FraternityRegistration);
};
Start Ganache (GUI quickstart → RPC http://127.0.0.1:7545
) and deploy:
truffle migrate --network development
# Confirm address in the output
# (Optional) Get it again in the console:
truffle console --network development
(Finality) const d = await FraternityRegistration.deployed(); d.address
Save the deployed contract address for the front‑end.
4) Front‑End (Web3 + lite‑server)
4.1 Create front‑end folder & packages
mkdir frontend
cd frontend
npm init -y
npm install web3 lite-server --save-dev
4.2 Add package.json
scripts
{
"name": "fraternity-dapp",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "lite-server"
},
"devDependencies": {
"lite-server": "^2.6.1"
},
"dependencies": {
"web3": "^1.10.4"
}
}
We serve Web3 via CDN below; the npm install is just to keep dependencies tracked.
4.3 bs-config.json
(lets browser read ../build/
)
{
"server": { "baseDir": ["./", "../"] }
}
4.4 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>JU Baseball & Cross Team – Decentralized Registration</title>
</head>
<body style="font-family: Arial, sans-serif; background:#f4f4f4; color:#333">
<h1 style="text-align:center; color:navy">Jacksonville University Baseball and Cross Team Decentralized Registration</h1>
<div style="max-width:720px; margin:auto; padding:20px; background:#fff; border-radius:10px; box-shadow:0 0 10px rgba(0,0,0,.1)">
<button id="connect" style="width:100%; padding:10px; background:teal; color:#fff; border:none; border-radius:6px; cursor:pointer">Connect MetaMask</button>
<h2 style="color:darkblue; margin-top:16px">Register a New Member</h2>
<label>Member Address</label>
<input type="text" id="memberAddress" placeholder="0x..." style="width:100%; padding:10px; margin-bottom:10px; border:1px solid #ccc; border-radius:5px" />
<label>Member Name</label>
<input type="text" id="memberName" placeholder="Full name" style="width:100%; padding:10px; margin-bottom:10px; border:1px solid #ccc; border-radius:5px" />
<button id="registerBtn" style="width:100%; padding:10px; background:darkblue; color:#fff; border:none; border-radius:5px; cursor:pointer">Register Member</button>
<h2 style="color:darkblue; margin-top:20px">Check Membership</h2>
<button id="checkBtn" style="width:100%; padding:10px; background:navy; color:#fff; border:none; border-radius:5px; cursor:pointer">Check Membership</button>
<div id="result" style="margin-top:20px; color:darkred"></div>
<div id="owner" style="margin-top:8px; color:green"></div>
<div id="stats" style="margin-top:8px; color:#444"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/web3@1.10.4/dist/web3.min.js"></script>
<script src="app.js"></script>
</body>
</html>
4.5 app.js
// Auto-load ABI and address from Truffle build, fallback to manual address
let web3, accounts, contract;
let contractAddress = ""; // OPTIONAL: paste manual address if fetch fails
const ABI_URL = "../build/contracts/FraternityRegistration.json";
async function connect(){
if(!window.ethereum){ alert('Please install MetaMask'); return; }
web3 = new Web3(window.ethereum);
accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
document.getElementById('result').textContent = `Connected as ${accounts[0]}`;
await loadContract();
}
async function loadContract(){
try{
const res = await fetch(ABI_URL);
const json = await res.json();
const abi = json.abi;
// Try to derive address from networks key
const nets = json.networks || {};
const netIds = Object.keys(nets);
if(!contractAddress && netIds.length){ contractAddress = nets[netIds[netIds.length-1]].address; }
if(!contractAddress) throw new Error('Set contractAddress in app.js');
contract = new web3.eth.Contract(abi, contractAddress);
document.getElementById('owner').textContent = 'Contract: ' + contractAddress;
// Show owner & totalMembers
const [owner, total] = await Promise.all([
contract.methods.owner().call(),
contract.methods.totalMembers().call()
]);
document.getElementById('stats').textContent = `Owner: ${owner} • Total Members: ${total}`;
}catch(e){
console.error(e); document.getElementById('result').textContent = 'Load contract failed: ' + e.message;
}
}
async function registerMember(){
const addr = document.getElementById('memberAddress').value.trim();
const name = document.getElementById('memberName').value.trim();
if(!web3.utils.isAddress(addr)) return (document.getElementById('result').textContent='Invalid address');
const btn = document.getElementById('registerBtn'); btn.disabled = true;
try{
const gas = await contract.methods.registerMember(addr, name).estimateGas({ from: accounts[0] });
await contract.methods.registerMember(addr, name).send({ from: accounts[0], gas });
document.getElementById('result').textContent = 'Member registered ✅';
const total = await contract.methods.totalMembers().call();
document.getElementById('stats').textContent = `Owner: ${(await contract.methods.owner().call())} • Total Members: ${total}`;
}catch(e){ document.getElementById('result').textContent = 'Error: ' + e.message; }
finally{ btn.disabled=false; }
}
async function checkMembership(){
const addr = document.getElementById('memberAddress').value.trim();
if(!web3.utils.isAddress(addr)) return (document.getElementById('result').textContent='Invalid address');
try{
const isM = await contract.methods.isMember(addr).call();
document.getElementById('result').textContent = isM ? '✅ This address is a member.' : '❌ Not a member.';
if(isM){
try{ const name = await contract.methods.getMemberDetails(addr).call();
document.getElementById('result').textContent += ` Name: ${name}`;
}catch(_){ /* ignore */ }
}
}catch(e){ document.getElementById('result').textContent = 'Error: ' + e.message; }
}
document.getElementById('connect').addEventListener('click', connect);
window.registerMember = registerMember;
window.checkMembership = checkMembership;
Prefer a hard‑coded ABI? (inline example)
Instead of fetching from ../build/
, you may paste the ABI + set contractAddress
manually. Ensure it includes isMember and getMemberDetails (the earlier snippet missed them).
5) Run & Test
- Start Ganache (GUI quickstart).
- In a terminal at
fraternity-dapp/frontend
:npm start
- Open
http://localhost:3000
→ click Connect MetaMask and switch to your Ganache network. - Optionally import a Ganache account private key into MetaMask so you can act as the owner (the deployer).
- Register a member (owner only), then check membership.
Troubleshooting
- Wrong address / ABI: Make sure the front‑end uses the deployed address (
truffle migrate
output) and ABI frombuild/contracts/FraternityRegistration.json
. Ourbs-config.json
lets the browser fetch it. - Not owner: Only the deployer (contract owner) can register/remove. Import that Ganache account into MetaMask.
- MetaMask network: Switch to Ganache RPC (e.g.,
http://127.0.0.1:7545
, Chain ID 1337/5777). - Pending tx / nonce issues: In MetaMask → Settings → Advanced → Reset account (dev only) or restart Ganache workspace.
- Port conflicts: If 3000 is busy, change
lite-server
port viaBS_PORT
env or use another port. - image009.jpg: If your handout references a diagram, place it in
frontend/
and embed inindex.html
for students.
Security note: This demo has no access controls beyond
onlyOwner
. Don’t use on public networks. Avoid storing sensitive data on‑chain.