DEV Community

Cover image for I Took My DApp Off My Laptop. The Internet Did Not Care, But I Was Thrilled.
Srashti
Srashti

Posted on

I Took My DApp Off My Laptop. The Internet Did Not Care, But I Was Thrilled.

Here's an uncomfortable truth about local Hardhat networks.

They feel real. They are not real.

Everything I'd built so far — the contract, the tests, the MetaMask connection — ran on a network that exists only on my laptop, resets every time I restart it, and that literally nobody else on Earth can access.

It was time to put this thing somewhere that actually counts.


Local network vs public testnet — the difference that actually matters

A local Hardhat network is basically a blockchain simulator. Fast, free, and fake. Perfect for testing. Useless for showing anyone else what you built.

A public testnet — I'm using Sepolia, Ethereum's main test network — is a real, persistent, publicly accessible blockchain. The ETH on it has no real value, but everything else behaves exactly like mainnet. Real gas costs (in test ETH), real block times, real network conditions.

If your contract works on Sepolia, you've actually proven something. If it only worked locally, you've proven... that it worked locally.


Getting test ETH

Before deploying anything, you need fake money to pay fake gas fees. Welcome to one of the stranger sentences in software development.

Sepolia faucets give out free test ETH. I used one tied to my Alchemy account, pasted in my wallet address, and waited a couple minutes. Free money, zero value, completely necessary.


Configuring Hardhat for Sepolia

Back to hardhat.config.js — except now it needs to know about a real network, not just the local simulator.

require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();

module.exports = {
  solidity: "0.8.20",
  networks: {
    sepolia: {
      url: process.env.SEPOLIA_RPC_URL,
      accounts: [process.env.PRIVATE_KEY],
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

Two things I had to get right here, and both involve secrets I should never, ever commit to GitHub.

SEPOLIA_RPC_URL — this is an endpoint that lets Hardhat actually talk to the Sepolia network. I got mine from Alchemy, free tier, no cost.

PRIVATE_KEY — my wallet's private key, used to sign and pay for the deployment transaction. This one terrified me a little the first time. If this leaks, anyone can drain that wallet. I put both of these in a .env file and added .env to .gitignore before I did anything else. Lesson learned before the mistake happened, for once.


Writing the deploy script

const hre = require("hardhat");

async function main() {
  const CrowdFund = await hre.ethers.getContractFactory("CrowdFund");
  const crowdFund = await CrowdFund.deploy();

  await crowdFund.waitForDeployment();

  console.log("CrowdFund deployed to:", await crowdFund.getAddress());
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
Enter fullscreen mode Exit fullscreen mode

Then actually run it:

npx hardhat run scripts/deploy.js --network sepolia
Enter fullscreen mode Exit fullscreen mode

This took noticeably longer than deploying locally — because it's a real network with real block times, not an instant simulation. Watching the terminal just sit there for the first time was its own small lesson in patience.

When it finally printed an address, that address was real. Anyone could look it up on Sepolia's block explorer and see my contract sitting there. That moment felt different from every local deploy before it.


Pointing the frontend at it

Small but easy-to-forget step — the React app's CONTRACT_ADDRESS had to change from the local Hardhat address to this new Sepolia one. And MetaMask had to actually be switched to the Sepolia network too, or none of this connects to anything.

That mismatch — frontend pointed one place, wallet connected to another — is its own special category of confusing bug. Everything looks fine. Nothing works. No error tells you why.


The bug that actually got me: events not updating the frontend

Here's the real problem I hit, and it's worth walking through honestly because the fix taught me something about how DApps are actually supposed to work.

My UI showed a campaign's amountRaised correctly right after I contributed — because I was calling the contract directly and re-fetching. But if someone else contributed while I had the page open, my screen had no idea. It just sat there, stale, until I manually refreshed.

The fix was adding events to the contract that I'd skipped back in the original version:

event ContributionMade(uint256 campaignId, address contributor, uint256 amount);

function contribute(uint256 _campaignId) public payable {
    Campaign storage campaign = campaigns[_campaignId];

    require(block.timestamp < campaign.deadline, "Campaign has ended");
    require(msg.value > 0, "Contribution must be greater than zero");

    campaign.amountRaised += msg.value;
    contributions[_campaignId][msg.sender] += msg.value;

    emit ContributionMade(_campaignId, msg.sender, msg.value);
}
Enter fullscreen mode Exit fullscreen mode

emit broadcasts this event onto the blockchain every time someone contributes. Anything listening can pick it up in real time.

Then on the frontend:

contract.on("ContributionMade", (campaignId, contributor, amount) => {
  console.log("New contribution detected:", campaignId, contributor, amount);
  refreshCampaignData(campaignId);
});
Enter fullscreen mode Exit fullscreen mode

This is the piece I genuinely didn't understand until I needed it. Smart contracts don't push updates to a frontend automatically just because something changed. The frontend has to explicitly listen for events — and if you don't emit them in your contract, there's nothing to listen to in the first place.

I had built a contract that worked perfectly and a frontend that had no way of knowing when things changed. Both pieces were "correct." Together, they were broken.


Why this bug actually matters beyond just fixing it

This is the difference between writing a contract that works and building a DApp that feels alive.

Without events, every user is stuck refreshing manually, never sure if what they're looking at is current. With events, the UI reacts the moment something happens on-chain — close to how a normal web app feels, except nothing here is coming from a server.

It's a small addition to the Solidity code. It changes the entire feel of the product.


Where things actually stand right now

Honest status update, because that's the whole point of writing this in public:

  • Contract is deployed on Sepolia, with a real address anyone can verify
  • Frontend connects to it, creates campaigns, accepts contributions
  • Events are now wired up, so the UI updates without manual refreshing
  • Still rough around the edges — no proper campaign list view yet, styling is minimal, error handling is basic

This isn't "the DApp is live and perfect." It's "the DApp is live, mostly works, and I know exactly what's left."

That gap is smaller than it was at the start of this whole series. That's the actual win.


What's left

A real campaign listing page. Better error messages when a transaction fails instead of a raw MetaMask error. And eventually, a proper look at whether this is even ready to show people outside of me testing it alone.

Getting something onto a public testnet didn't finish this project. It just moved the goalposts to something more honest.


I'm Srashti Gupta, building in the Web3 space. I write about real builds, real bugs, and blockchain development from scratch. Let's connect on LinkedIn.

Top comments (0)