DEV Community

Karthi Keyan
Karthi Keyan

Posted on

Building Mystic Vault: A Step-by-Step Guide to Creating a Simple Ethereum DApp from Scratch 🔮✨ ( 30 min )

MysticVault Showcase Image

Embark on a mystical journey to build **Mystic Vault, an enchanting Ethereum Decentralized Application (DApp) that allows users to store and retrieve their secret spells. In this comprehensive guide, we’ll integrate the provided HTML, CSS, JavaScript, and Solidity code into a fully functional DApp. Let’s weave some magic!


Table of Contents

  1. Introduction
  2. Prerequisites
  3. Project Structure
  4. Setting Up the Development Environment
  5. Writing the Smart Contract
  6. Compiling and Deploying the Smart Contract
  7. Building the Frontend
  8. Connecting Frontend to Smart Contract
  9. Testing the DApp
  10. Conclusion
  11. Clone the Repository

Introduction

Decentralized Applications (DApps) harness the power of blockchain technology to create secure and transparent platforms. Mystic Vault is a simple yet magical DApp that allows users to store and retrieve their secret spells on the Ethereum blockchain. This guide will walk you through every step, from writing the smart contract to building an interactive frontend.

Ready to conjure up some blockchain magic? Let’s get started! 🪄

Prerequisites

Before we begin, ensure you have the following installed:

With these tools in place, you’re all set to begin your mystical development journey!

Project Structure

Here’s an overview of the project structure we’ll be working with:

MysticVault/
├── contracts/
│   └── SimpleStorage.sol
├── frontend/
│   ├── index.html
│   ├── app.js
│   ├── styles.css
├── hardhat.config.js
├── package.json
└── artifacts/
Enter fullscreen mode Exit fullscreen mode

Setting Up the Development Environment

1. Create and Navigate to the Project Directory

mkdir MysticVault
cd MysticVault
Enter fullscreen mode Exit fullscreen mode

2. Initialize npm and Install Dependencies

npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox web3
Enter fullscreen mode Exit fullscreen mode

3. Initialize Hardhat Project

npx hardhat init
Enter fullscreen mode Exit fullscreen mode
  • Select “Create a JavaScript project” when prompted.
  • Accept default settings and install dependencies.

4. Update hardhat.config.js

require("@nomicfoundation/hardhat-toolbox");
require("@nomicfoundation/hardhat-ignition");

module.exports = {
  solidity: "^0.8.27",
  networks: {
    hardhat: {
      chainId: 1337,
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

Writing the Smart Contract

Create SimpleStorage.sol in the contracts Directory

// SPDX-License-Identifier: MIT
pragma solidity 0.8.27;

contract SimpleStorage {
    string private data;

    // Function to set the secret spell
    function setSecretSpell(string memory _data) public {
        data = _data;
    }

    // Function to get the secret spell
    function getSecretSpell() public view returns (string memory) {
        return data;
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • State Variable: data stores the secret spell as a private string.
  • setSecretSpell Function: Allows users to store a new secret spell.
  • getSecretSpell Function: Retrieves the stored secret spell.

Compiling and Deploying the Smart Contract

1. Compile the Contract

npx hardhat compile
Enter fullscreen mode Exit fullscreen mode

2. Deploy the Contract Using Hardhat

Add the Deployment Code

Open ignition/modules/SimpleStorage.js and add the following code:

// This setup uses Hardhat Ignition to manage smart contract deployments.
// Learn more about it at https://hardhat.org/ignition

const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");

module.exports = buildModule("SimpleStorageModule", (m) => {
  const simpleStorage = m.contract("SimpleStorage", []);
  return { simpleStorage };
});
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • Import buildModule: This function helps define deployment modules.
  • Define Module Name: "SimpleStorageModule" is an arbitrary name for the module.
  • Deploy Contract: m.contract("SimpleStorage", []) deploys the SimpleStorage contract without constructor arguments.

(Insert this image screenshot here)

Deploying the Smart Contract Using Hardhat Ignition

  1. Start the Hardhat Local Node

    In one terminal window, run:

    npx hardhat node
    

    This starts a local Ethereum node provided by Hardhat, running on http://127.0.0.1:8545.

Image description

  1. Deploy Using Ignition

    Open a new terminal window and execute the deployment command:

    npx hardhat ignition deploy ./ignition/modules/SimpleStorage.js --network localhost
    

    then press y and wait for output similar to this

Image description


Interacting with the Smart Contract

Now that our smart contract is deployed, let’s interact with it to ensure it’s functioning correctly.

  1. Connect to the Hardhat Network

    Since Hardhat provides JSON-RPC endpoints, we can use ethers.js in a script or use the Hardhat console.

  2. Using Hardhat Console

    Run the Hardhat console:

    npx hardhat console --network localhost
    

    (Insert this image screenshot here)

  3. Interact with SimpleStorage

    a. Retrieve the Contract Instance

    const SimpleStorage = await ethers.getContractFactory("SimpleStorage");
    const simpleStorage = await SimpleStorage.attach("0xYourContractAddress");
    

    Replace 0xYourContractAddress with the actual address output from the deployment step.

    b. Get Initial Data

    const initialData = await simpleStorage.getSecretSpell();
    console.log(initialData); // Should output an empty string
    

    c. Set New Data

    await simpleStorage.setSecretSpell("Abra Kadabra");
    

    d. Retrieve Updated Data

    const updatedData = await simpleStorage.getSecretSpell();
    console.log(updatedData); // Should output "Abra Kadabra"
    

    (Insert this image screenshot here)

    This confirms that our smart contract can store and retrieve data as intended.

  4. Exit Hardhat Console

    .exit
    
  5. Create a folder ‘test’ and create a file with name ‘debugInteract.js’ and past the following code

import Web3 from 'web3';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

// Resolve __dirname in ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Load ABI from the JSON file
const abiPath = path.resolve(__dirname, '../artifacts/contracts/SimpleStorage.sol/SimpleStorage.json');
const abiFile = JSON.parse(fs.readFileSync(abiPath, 'utf-8'));
const contractABI = abiFile.abi;

// Contract address (replace with your deployed address)
const contractAddress = '0x5FbDB2315678afecb367f032d93F642f64180aa3';

const main = async () => {
  try {
    console.log("Setting up Web3 provider...");

    // Initialize Web3 with your provider (e.g., local node)
    const web3 = new Web3('http://localhost:8545'); // Update if using a different provider

    console.log("Fetching accounts...");
    const accounts = await web3.eth.getAccounts();
    console.log("Available Accounts:", accounts);

    if (accounts.length === 0) {
      throw new Error("No accounts available. Ensure your node has unlocked accounts.");
    }

    console.log("Creating contract instance...");
    const contract = new web3.eth.Contract(contractABI, contractAddress);
    console.log(`Contract deployed at: ${contract.options.address}`);

    console.log("Calling getData()...");
    const data = await contract.methods.getSecretSpell().call({ from: accounts[0] });
    console.log("Data retrieved from contract:", data);

    // Example: Setting new data (uncomment if needed)
    /*
    const newData = "Hello, Mystic Vault!";
    console.log(`Sending transaction to setData("${newData}")...`);
    const receipt = await contract.methods.setData(newData).send({ from: accounts[0], gas: 300000 });
    console.log("Transaction successful with receipt:", receipt);

    // Verify the update
    const updatedData = await contract.methods.getData().call({ from: accounts[0] });
    console.log("Updated Data:", updatedData);
    */

  } catch (error) {
    console.error("An error occurred during contract interaction:", error);
  }
};

main();
Enter fullscreen mode Exit fullscreen mode

add { ... "type": "module", ... } inside package.json then run

node test/debugInteract.js 
Enter fullscreen mode Exit fullscreen mode

Building the Frontend

1. Create the Frontend Directory and Files

Navigate to the project root and create the frontend directory:

mkdir frontend
cd frontend
Enter fullscreen mode Exit fullscreen mode

Create the following files inside frontend:

  • index.html
  • app.js
  • styles.css

2. Write the HTML Structure (index.html)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Mystic Vault</title>

  <!-- Include Magic Animations CSS -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/magic/1.1.0/magic.css">

  <!-- Include Google Fonts -->
  <link href="https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@400;700&family=Roboto:wght@400;700&display=swap" rel="stylesheet">

  <!-- Include Font Awesome -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">

  <!-- Link to External CSS -->
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <!-- Sparkles Background -->
  <div class="sparkles"></div>

  <!-- Header with Padlock and Sparkles Icons -->
  <h1 class="magictime puffIn">
    <i class="fas fa-lock icon" aria-label="Padlock"></i>
    Mystic Vault
    <i class="fas fa-magic icon" aria-label="Sparkles"></i>
  </h1>

  <!-- Store Secret Spell Section with Wand Icon -->
  <div class="store-container">
    <input type="text" id="inputData" placeholder="Enter your secret spell here..." aria-label="Secret Spell Input" />
    <button id="setData" class="magictime puffIn" aria-label="Store Secret Spell">
      <i class="fas fa-wand-magic icon"></i>Store Spell
    </button>
  </div>

  <!-- Retrieve Secret Spell Section with Orb Icon -->
  <div class="retrieve-container">
    <button id="getData" class="magictime puffIn" aria-label="Retrieve Secret Spell">
      <i class="fas fa-circle-notch icon"></i>Retrieve Secret Spell
    </button>
    <div id="displayData" class="magictime fadeIn" aria-live="polite">
      <i class="fas fa-magic"></i> Your secret spell will appear here...
    </div>
    <button id="clearDisplay" aria-label="Clear Retrieved Spell">Clear</button>
  </div>

  <!-- Loading Spinner -->
  <div id="loading" style="display: none;" class="magictime puffIn" aria-live="assertive">
    <i class="fas fa-spinner fa-spin"></i> Processing...
  </div>

  <!-- Notification Message -->
  <div id="notification" class="magictime fadeIn" role="alert" aria-live="assertive"></div>

  <!-- Include Web3.js -->
  <script src="https://cdn.jsdelivr.net/npm/web3/dist/web3.min.js"></script>
  <!-- Link to External JavaScript -->
  <script src="app.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Connecting Frontend to Smart Contract

1. Update the ABI in app.js

First, extract the ABI from the compiled contract:

  • Locate the ABI in artifacts/contracts/SimpleStorage.sol/SimpleStorage.json.
  • Copy the contents of the "abi" field.

Updated ABI:

const contractABI = [
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "_data",
        "type": "string"
      }
    ],
    "name": "setSecretSpell",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "getSecretSpell",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
];
Enter fullscreen mode Exit fullscreen mode

2. Update the Contract Address in app.js

Replace '0xYourContractAddress' with the actual contract address from the deployment output.

const contractAddress = '0xYourContractAddress'; // Replace with your contract's address
Enter fullscreen mode Exit fullscreen mode

3. Write the JavaScript Logic (app.js)

// Function to display notifications
function showNotification(message, isSuccess = true) {
  const notification = document.getElementById('notification');
  notification.innerText = message;
  notification.style.backgroundColor = isSuccess ? 'rgba(46, 204, 113, 0.9)' : 'rgba(231, 76, 60, 0.9)';
  notification.classList.add('show');

  // Animate the notification
  animateCSS('#notification', 'fadeIn');

  // Hide after 3 seconds
  setTimeout(() => {
    notification.classList.remove('show');
  }, 3000);
}

// Function to show or hide the loading spinner
function showLoading(show) {
  const loading = document.getElementById('loading');
  if (show) {
    loading.style.display = 'flex';
  } else {
    loading.style.display = 'none';
  }
}

// Function to add and remove animation classes
function animateCSS(element, animationName, callback) {
  const node = document.querySelector(element);
  node.classList.add('magictime', animationName);

  function handleAnimationEnd() {
      node.classList.remove('magictime', animationName);
      node.removeEventListener('animationend', handleAnimationEnd);

      if (typeof callback === 'function') callback();
  }

  node.addEventListener('animationend', handleAnimationEnd);
}

// Initialize the DApp
window.addEventListener('load', async () => {
  let web3;
  let accounts;
  let simpleStorage;

  // Modern dapp browsers...
  if (window.ethereum) {
    web3 = new Web3(window.ethereum);
    try {
      // Request account access
      await window.ethereum.request({ method: 'eth_requestAccounts' });
    } catch (error) {
      console.error("User denied account access");
      alert("Please allow access to your Ethereum wallet to use this DApp.");
      return;
    }
  }
  // Legacy dapp browsers...
  else if (window.web3) {
    web3 = new Web3(web3.currentProvider);
  }
  // Non-dapp browsers...
  else {
    alert('No Ethereum wallet detected. Please install MetaMask, Brave Wallet, or use a compatible browser.');
    return;
  }

  // Instantiate the contract
  simpleStorage = new web3.eth.Contract(contractABI, contractAddress);

  // Get the user's accounts
  accounts = await web3.eth.getAccounts();

  // Handle Store Secret Spell
  document.getElementById('setData').onclick = async () => {
    const data = document.getElementById('inputData').value;
    if (data.trim() === "") {
      showNotification("Please enter a secret spell before storing.", false);
      return;
    }
    showLoading(true);
    try {
      await simpleStorage.methods.setSecretSpell(data).send({ from: accounts[0] });
      showLoading(false);
      showNotification('✨ Secret Spell Stored Successfully! ✨', true);
    } catch (error) {
      console.error(error);
      showLoading(false);
      showNotification('❌ Failed to Store Secret Spell.', false);
    }
  };

  // Handle Retrieve Secret Spell
  document.getElementById('getData').onclick = async () => {
    showLoading(true);
    try {
      const result = await simpleStorage.methods.getSecretSpell().call();
      document.getElementById('displayData').innerHTML = `<i class="fas fa-magic"></i> ${result}`;
      showLoading(false);
      showNotification('🔮 Secret Spell Retrieved Successfully! 🔮', true);
    } catch (error) {
      console.error(error);
      showLoading(false);
      showNotification('❌ Failed to Retrieve Secret Spell.', false);
    }
  };

  // Handle Clear Retrieved Spell
  document.getElementById('clearDisplay').onclick = () => {
    document.getElementById('displayData').innerHTML = `<i class="fas fa-magic"></i> Your secret spell will appear here...`;
    showNotification('🧹 Retrieved spell cleared.', true);
  };
});
Enter fullscreen mode Exit fullscreen mode

Styling the Frontend (styles.css)

/* 1. General Styles */
body {
  background: linear-gradient(135deg, #141E30, #243B55);
  color: #ecf0f1;
  font-family: 'Roboto', sans-serif;
  text-align: center;
  padding: 50px;
  margin: 0;
  height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  position: relative;
  overflow: hidden;
}

/* 2. Header with Padlock and Sparkles Icons */
h1 {
  font-family: 'Roboto Slab', serif;
  font-size: 3em;
  margin-bottom: 40px;
  color: #fff;
  text-shadow: 2px 2px 8px rgba(0,0,0,0.5);
  animation: fadeInDown 1s ease forwards;
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* 3. Adding Sparkles in the Background */
.sparkles {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: radial-gradient(circle, rgba(255,255,255,0.05) 1px, transparent 1px),
              radial-gradient(circle, rgba(255,255,255,0.05) 1px, transparent 1px);
  background-position: 0 0, 25px 25px;
  background-size: 50px 50px;
  z-index: 0;
}

/* 4. Container for Store Secret Spell */
.store-container {
  display: flex;
  align-items: center;
  margin-bottom: 30px;
  width: 400px;
  position: relative;
  z-index: 1;
}

.store-container input[type="text"] {
  flex: 1;
  padding: 12px 20px;
  border: 2px solid #6c5ce7;
  border-right: none;
  border-radius: 5px 0 0 5px;
  font-size: 1em;
  background-color: #34495e;
  color: #ecf0f1;
  transition: border-color 0.3s;
}

.store-container input[type="text"]::placeholder {
  color: #bdc3c7;
}

.store-container input[type="text"]:focus {
  outline: none;
  border-color: #a29bfe;
}

.store-container button {
  padding: 12px 20px;
  border: 2px solid #6c5ce7;
  border-left: none;
  border-radius: 0 5px 5px 0;
  background-color: #6c5ce7;
  color: #ecf0f1;
  font-size: 1em;
  cursor: pointer;
  transition: background-color 0.3s, transform 0.3s;
  display: flex;
  align-items: center;
  justify-content: center;
}

.store-container button:hover {
  background-color: #a29bfe;
  transform: scale(1.05);
}

/* 5. Retrieve and Display Section */
.retrieve-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 400px;
  position: relative;
  z-index: 1;
}

.retrieve-container button {
  background-color: #e67e22;
  border: none;
  padding: 12px 24px;
  border-radius: 5px;
  color: #ecf0f1;
  font-size: 1em;
  cursor: pointer;
  transition: background-color 0.3s, transform 0.3s;
  margin-bottom: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.retrieve-container button:hover {
  background-color: #f39c12;
  transform: scale(1.05);
}

#displayData {
  background-color: #34495e;
  padding: 20px;
  border-radius: 5px;
  width: 100%;
  font-size: 1.2em;
  color: #ecf0f1;
  min-height: 50px;
  box-shadow: 0 4px 6px rgba(0,0,0,0.3);
  animation: fadeIn 1s ease forwards;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* 6. Magic Animations Overrides */
.magictime.puffIn {
  animation-duration: 1s;
  animation-fill-mode: both;
}

/* 7. Custom Animations */
@keyframes fadeInDown {
  from {
    opacity: 0;
    transform: translateY(-20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* 8. Responsive Design */
@media (max-width: 500px) {
  .store-container, .retrieve-container {
    width: 90%;
  }

  h1 {
    font-size: 2.5em;
  }
}

/* 9. Decorative Icons Styles */
.icon {
  margin: 0 8px;
}

/* 10. Loading Spinner Styles */
#loading {
  position: fixed;
  top: 20px;
  right: 20px;
  background-color: rgba(52, 73, 94, 0.9);
  padding: 10px 20px;
  border-radius: 5px;
  color: #ecf0f1;
  font-size: 1em;
  box-shadow: 0 4px 6px rgba(0,0,0,0.3);
  z-index: 2;
  display: flex;
  align-items: center;
}

/* 11. Notification Message Styles */
#notification {
  position: fixed;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  background-color: rgba(52, 73, 94, 0.9); /* Default background color */
  padding: 10px 20px;
  border-radius: 5px;
  color: #ecf0f1;
  font-size: 1em;
  box-shadow: 0 4px 6px rgba(0,0,0,0.3);
  z-index: 2;
  display: none; /* Hidden by default */
  opacity: 0;
  transition: opacity 0.5s ease, visibility 0.5s ease;
}

#notification.show {
  display: flex;
  opacity: 1;
  visibility: visible;
}

/* 12. Clear Button for Retrieved Message */
#clearDisplay {
  margin-top: 10px;
  background-color: #95a5a6;
  border: none;
  padding: 8px 16px;
  border-radius: 5px;
  color: #ecf0f1;
  font-size: 0.9em;
  cursor: pointer;
  transition: background-color 0.3s, transform 0.3s;
}

#clearDisplay:hover {
  background-color: #7f8c8d;
  transform: scale(1.05);
}
Enter fullscreen mode Exit fullscreen mode

Testing the DApp

1. Serve the Frontend

In the frontend directory, start a simple HTTP server:

cd frontend
npx http-server . -c-1 -p 8080
Enter fullscreen mode Exit fullscreen mode

press ‘y’ for package installation if asked

Navigate to http://localhost:8080 in your browser.

2. Connect Your Wallet

  • Ensure your wallet (MetaMask, Brave Wallet) is connected to the Hardhat network.
  • Network Details:

3. Import an Account

  • From the Hardhat node output, copy one of the private keys.
  • Import it into your wallet.

4. Interact with the DApp

  • Store a Secret Spell:

    • Enter a spell in the input field.
    • Click “Store Spell”.
    • Confirm the transaction in your wallet.
    • A notification will confirm success.
  • Retrieve the Secret Spell:

    • Click “Retrieve Secret Spell”.
    • The stored spell will appear.
    • A notification will confirm success.
  • Clear the Retrieved Spell:

    • Click “Clear”.
    • The display resets.
    • A notification will confirm the action.

(Insert screenshots of each interaction step here)

Conclusion

🎉 Congratulations! You’ve successfully built Mystic Vault, a magical Ethereum DApp that allows users to store and retrieve their secret spells. This journey has taken you through smart contract development, frontend integration, and blockchain interaction.

Key Learnings:

  • Smart Contract Development: Writing and deploying Solidity contracts.
  • Blockchain Interaction: Using Web3.js to interact with smart contracts.
  • Frontend Development: Building an interactive and animated user interface.
  • Troubleshooting: Debugging common issues in DApp development.

With these skills, you’re ready to explore more complex and exciting blockchain projects. The possibilities are endless!

Clone the Repository

Ready to explore the code and see Mystic Vault in action? Clone the repository from GitHub and get started!

git clone https://github.com/KarthiDreamr/MysticVault-Dapp-Simple-Storage.git
Enter fullscreen mode Exit fullscreen mode

Feel free to customize and expand upon this project to suit your needs. Happy coding! 🪄✨

If you found this guide helpful, share it with fellow blockchain enthusiasts and stay tuned for more tutorials on decentralized application development!

🔗 Connect with me on GitHub | Linkedin | Twitter

Happy Coding! 🪄✨

By following this guide, you’ve built a fully functional Ethereum DApp with an engaging user interface. Keep experimenting and learning—the blockchain world awaits your magical creations! 🚀

Feel free to reach out if you have any questions or need further assistance. Happy coding! 🪄✨

Top comments (0)