DEV Community

Cover image for MetaMask, Wallets, and Why Ownership Actually Matters
Srashti
Srashti

Posted on

MetaMask, Wallets, and Why Ownership Actually Matters

Up until this post, my entire DApp lived in a terminal.

Tests passed. Contract compiled. Everything technically worked. And none of it looked like anything a real human would ever want to use.

Time to give it a face.


Why MetaMask is the whole game here

In a normal web app, you log in with an email and password. The server checks a database, decides who you are, and trusts that decision.

There's no login here. There's no database deciding who you are.

Instead, there's MetaMask — a wallet that holds your private key and lets you sign transactions. When you "connect" your wallet to a DApp, you're not logging into anything. You're just letting the website see your public address and asking your permission before any transaction goes out.

Nobody can fake being you. Nobody can reset your password and lock you out. Your wallet is your identity here, and only you control it.

That's the part that actually matters. Not the UI. The ownership.


Connecting the wallet

In my React app, I'm using ethers.js (Web3.js's sibling library — similar purpose, cleaner API) to talk to MetaMask.

import { ethers } from "ethers";
import { useState } from "react";

function App() {
  const [account, setAccount] = useState(null);
  const [provider, setProvider] = useState(null);

  async function connectWallet() {
    if (!window.ethereum) {
      alert("MetaMask not found. Please install it.");
      return;
    }

    const browserProvider = new ethers.BrowserProvider(window.ethereum);
    const accounts = await browserProvider.send("eth_requestAccounts", []);

    setProvider(browserProvider);
    setAccount(accounts[0]);
  }

  return (
    <div>
      {account ? (
        <p>Connected: {account.slice(0, 6)}...{account.slice(-4)}</p>
      ) : (
        <button onClick={connectWallet}>Connect Wallet</button>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

window.ethereum is the object MetaMask injects into every page you visit, automatically, the moment the extension is installed. If it's not there, MetaMask isn't installed — that's the first thing to check.

eth_requestAccounts is what actually triggers that familiar MetaMask popup — "this site wants to connect to your wallet." The user has to approve it. Nothing happens silently.

I'm slicing the address for display (0x71C7...976F instead of the full 42 characters) because nobody wants to read a full wallet address on a button. Small detail, makes the UI feel less like a database dump.


Getting the contract talking to React

Connecting a wallet is step one. Actually calling our contract's functions is step two.

import CrowdFundABI from "./CrowdFund.json";

const CONTRACT_ADDRESS = "0xYourDeployedContractAddress";

async function getContract(provider) {
  const signer = await provider.getSigner();
  return new ethers.Contract(CONTRACT_ADDRESS, CrowdFundABI.abi, signer);
}
Enter fullscreen mode Exit fullscreen mode

Remember that ABI file Hardhat generated back when we first compiled the contract? This is exactly where it gets used. The ABI tells ethers.js what functions exist on our contract and what arguments they expect — without it, JavaScript has no idea our contract can even do anything.

provider.getSigner() gets the actual connected account, the one capable of signing and sending transactions — not just reading data.

CONTRACT_ADDRESS is wherever our CrowdFund contract actually got deployed. For now, that's a testnet address from Hardhat's local network or somewhere like Sepolia.


Creating a campaign from the UI

async function createCampaign(goalInEth, durationInSeconds) {
  const contract = await getContract(provider);

  const tx = await contract.createCampaign(
    ethers.parseEther(goalInEth),
    durationInSeconds
  );

  await tx.wait();
  alert("Campaign created!");
}
Enter fullscreen mode Exit fullscreen mode

Two steps, and the gap between them matters.

contract.createCampaign(...) sends the transaction — MetaMask pops up, the user approves it, and it gets broadcast to the network. But sending a transaction and it actually being confirmed are different moments.

tx.wait() pauses until the transaction is actually mined into a block. Skip this step and your UI might say "Campaign created!" before the campaign genuinely exists on-chain — which is exactly the kind of bug that makes a DApp feel broken even when the contract is fine.


Contributing to a campaign

async function contributeToCampaign(campaignId, amountInEth) {
  const contract = await getContract(provider);

  const tx = await contract.contribute(campaignId, {
    value: ethers.parseEther(amountInEth),
  });

  await tx.wait();
  alert("Contribution sent!");
}
Enter fullscreen mode Exit fullscreen mode

Same pattern as the test we wrote in the last post — value in the call is what actually attaches real ETH to this transaction. The only difference now is a real human is clicking a real button, MetaMask is popping up with a real gas estimate, and real money is moving.

This is the moment the whole project stops feeling like an exercise.


What it actually feels like end to end

Here's the full user journey, now that all of it connects:

  1. Open the app, click Connect Wallet — MetaMask pops up, user approves
  2. Fill in a goal and duration, click Create Campaign — MetaMask pops up again asking to confirm the transaction
  3. Wait a few seconds for confirmation, campaign appears
  4. Someone else opens the app, connects their own wallet, contributes ETH to that campaign — another MetaMask popup, another confirmation
  5. Refresh, and the campaign's raised amount has actually updated — read directly from the blockchain, not from any database

No login. No backend tracking sessions. No "forgot password" flow. Just wallets, signatures, and a contract that doesn't care who you are beyond your address.


The bug that taught me something

First time I tested this flow, I created a campaign, switched MetaMask to a different test account, and tried to contribute — except the contribution amount field still showed the old account's input because I hadn't reset my component state on account change.

MetaMask lets users switch accounts anytime, and your app needs to actually listen for that:

window.ethereum.on("accountsChanged", (accounts) => {
  setAccount(accounts[0]);
});
Enter fullscreen mode Exit fullscreen mode

Without this listener, your UI can silently show stale data tied to an account the user already switched away from. Small thing. Easy to miss. Definitely the kind of bug that erodes trust fast if a real user hits it.


Why this matters beyond just "it works now"

Stepping back for a second — what we just built is genuinely different from a normal web app's login flow.

There's no "Srashti's account" sitting in a database somewhere that the platform can suspend, ban, or quietly modify. There's a wallet address, and whatever that address has done on-chain, visible to anyone, controlled by nobody but its owner.

That's not a minor technical detail. That's the entire pitch of what we're building, finally made visible instead of theoretical.


What's next

Wallet connects. Campaigns get created. Contributions go through. The full loop actually works, end to end, with a real face on it.

What's left is polish and reality — proper UI for displaying all active campaigns, progress bars toward each goal, and eventually pushing this from a local Hardhat network onto a real public testnet where anyone, anywhere, could actually try it.

That's where this story is headed next.


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)