Building a dApp with Web3.js and EIP-6963

·

This tutorial guides you through building a decentralized application (dApp) using Web3.js that supports the EIP-6963 standard. EIP-6963 simplifies the process for dApp developers to allow users with multiple wallet browser extensions to interact with their applications seamlessly. Instead of relying on the global window.ethereum object, this standard introduces a mechanism for multiple wallet providers to announce their availability to a dApp. In this project, you'll create a dApp that enables users to transfer ether from one wallet account to another on the network.

Prerequisites and Setup

Before starting, ensure you have a basic understanding of command-line operations, React, and Node.js. You should also have Node.js and npm installed on your system. Verify your installations with the following commands:

node -v
npm -v

Install at least one EIP-6963 compliant wallet browser extension, such as MetaMask, Enkrypt, Exodus, or Trust Wallet. This tutorial uses MetaMask for demonstration purposes.

Project Initialization and Dependencies

Start by creating a new React project with TypeScript support:

npx create-react-app web3-intermediate-dapp --template typescript
cd web3-intermediate-dapp

Add Web3.js to your project:

npm install web3

Install Hardhat as a development dependency to set up a local network:

npm install --save-dev hardhat

Configuring the Hardhat Network

To ensure security, prevent the Hardhat configuration file from being tracked in version control. Add hardhat.config.js to your .gitignore file:

# .gitignore
hardhat.config.js

Create hardhat.config.js and configure it with your wallet's secret recovery phrase:

module.exports = {
  networks: {
    hardhat: {
      accounts: {
        mnemonic: 'your-secret-recovery-phrase',
      },
      chainId: 1337,
    },
  },
};

Start the Hardhat development network:

npx hardhat node

Note the HTTP and WebSocket JSON-RPC server URL (typically http://127.0.0.1:8545). Keep this terminal running and open a new one for subsequent commands.

Building the Provider Store

Delete unnecessary files (src/App.css, src/App.test.tsx, src/logo.svg). Create src/useProviders.ts to manage EIP-6963 provider discovery:

import { useSyncExternalStore } from 'react';
import {
  type EIP6963ProviderDetail,
  type EIP6963ProviderResponse,
  type EIP6963ProvidersMapUpdateEvent,
  Web3,
  web3ProvidersMapUpdated,
} from 'web3';

let providerList: EIP6963ProviderDetail[] = [];

const providerStore = {
  getSnapshot: () => providerList,
  subscribe: (callback: () => void) => {
    function setProviders(response: EIP6963ProviderResponse) {
      providerList = [];
      response.forEach((provider: EIP6963ProviderDetail) => {
        providerList.push(provider);
      });
      callback();
    }

    Web3.requestEIP6963Providers().then(setProviders);

    function updateProviders(providerEvent: EIP6963ProvidersMapUpdateEvent) {
      setProviders(providerEvent.detail);
    }

    Web3.onNewProviderDiscovered(updateProviders);

    return () => window.removeEventListener(web3ProvidersMapUpdated as any, updateProviders);
  },
};

export const useProviders = () =>
  useSyncExternalStore(providerStore.subscribe, providerStore.getSnapshot);

This hook provides a dynamic list of available EIP-6963 providers.

Creating the Main Application Component

Replace src/App.tsx with code to display available providers:

import type { EIP6963ProviderDetail } from 'web3';
import { useProviders } from './useProviders';

function App() {
  const providers = useProviders();

  return (
    <>
      {providers.map((provider: EIP6963ProviderDetail) => (
        <div key={provider.info.rdns}>
          {provider.info.name} [{provider.info.rdns}]
        </div>
      ))}
    </>
  );
}

export default App;

Start the development server:

npm start

Your browser should open and display all available EIP-6963 providers.

Integrating Web3.js with Provider Selection

Update src/App.tsx to handle provider selection and account management:

import { useEffect, useState } from 'react';
import { type EIP6963ProviderDetail, Web3 } from 'web3';
import { useProviders } from './useProviders';

function App() {
  const providers = useProviders();
  const [web3, setWeb3] = useState<Web3 | undefined>(undefined);
  const [accounts, setAccounts] = useState<string[]>([]);
  const [balances, setBalances] = useState<Map<string, number>>(new Map());

  function setProvider(provider: EIP6963ProviderDetail) {
    const web3Instance: Web3 = new Web3(provider.provider);
    setWeb3(web3Instance);
    web3Instance.eth.requestAccounts().then(setAccounts);
    provider.provider.on('accountsChanged', setAccounts);
    provider.provider.on('chainChanged', () => window.location.reload());
  }

  useEffect(() => {
    async function updateBalances(web3Instance: Web3) {
      const newBalances = new Map<string, number>();
      for (const account of accounts) {
        const balance = await web3Instance.eth.getBalance(account);
        newBalances.set(account, parseFloat(web3Instance.utils.fromWei(balance, 'ether')));
      }
      setBalances(newBalances);
    }

    if (web3 === undefined) return;

    updateBalances(web3);
    const subscription = web3.eth.subscribe('newBlockHeaders').then(subscription => {
      subscription.on('data', () => updateBalances(web3));
      return subscription;
    });

    return () => {
      subscription.then(subscription => subscription.unsubscribe());
    };
  }, [accounts, web3]);

  return (
    <>
      {web3 === undefined
        ? providers.map((provider: EIP6963ProviderDetail) => (
            <button
              key={provider.info.rdns}
              onClick={() => setProvider(provider)}
              style={{ display: 'inline-flex', alignItems: 'center' }}
            >
              <img src={provider.info.icon} alt={provider.info.name} width={24} height={24} />
              {provider.info.name}
            </button>
          ))
        : accounts.map((address: string, index: number) => (
            <div key={address}>
              Account: {address} <br />
              Balance: {`${balances.get(address)} ETH`}
              {index !== accounts.length - 1 && <hr />}
            </div>
          ))}
    </>
  );
}

export default App;

Configuring Your Wallet for the Hardhat Network

Add the Hardhat development network to your wallet:

Ensure your wallet is connected to the Hardhat network. Refresh your dApp and select your wallet provider. Your accounts should appear with 10,000 ETH balances.

Creating the Transfer Form

Create src/TransferForm.tsx for ether transfers:

import { type ChangeEvent, type FormEvent, useEffect, useState } from 'react';
import { type Address, Web3 } from 'web3';

function TransferForm({ address, web3 }: { address: Address; web3: Web3 }) {
  const [isFormValid, setIsFormValid] = useState<boolean>(false);
  const [transferTo, setTransferTo] = useState<string>('');
  const [transferAmount, setTransferAmount] = useState<string>('');

  function isValidAddress(address: string): boolean {
    return /^(0x)?[0-9a-fA-F]{40}$/.test(address);
  }

  useEffect(() => {
    const amount = parseFloat(transferAmount);
    setIsFormValid(isValidAddress(transferTo) && !isNaN(amount) && amount > 0);
  }, [transferTo, transferAmount]);

  function transferFormChange(e: ChangeEvent<HTMLInputElement>): void {
    const { name, value } = e.target;
    if (name === 'to') setTransferTo(value);
    else if (name === 'amount') setTransferAmount(value);
  }

  function transfer(e: FormEvent<HTMLFormElement>): void {
    e.preventDefault();
    if (web3 === undefined) return;

    const formData: FormData = new FormData(e.currentTarget);
    const to: FormDataEntryValue | null = formData.get('to');
    if (to === null || !isValidAddress(to as string)) return;

    const amount: FormDataEntryValue | null = formData.get('amount');
    if (amount === null) return;

    const value: number = parseFloat(amount as string);
    if (isNaN(value) || value <= 0) return;

    setTransferTo('');
    setTransferAmount('');

    web3.eth.sendTransaction({
      from: address,
      to: to as string,
      value: web3.utils.toWei(value, 'ether'),
    });
  }

  return (
    <form onSubmit={transfer}>
      <label>
        Transfer to:
        <input
          type="text"
          name="to"
          value={transferTo}
          onChange={transferFormChange}
          placeholder="0x..."
        />
      </label>
      <br />
      <label>
        Transfer amount in ether:
        <input
          type="number"
          name="amount"
          value={transferAmount}
          onChange={transferFormChange}
          placeholder="0.0"
          step="0.001"
        />
      </label>
      <br />
      <button type="submit" disabled={!isFormValid}>
        Transfer
      </button>
    </form>
  );
}

export default TransferForm;

Update src/App.tsx to include the transfer form:

// Add this import
import TransferForm from './TransferForm';

// Inside the return statement, modify the accounts mapping:
{accounts.map((address: string, index: number) => (
  <div key={address}>
    Account: {address} <br />
    Balance: {`${balances.get(address)} ETH`}
    <TransferForm address={address} web3={web3} />
    {index !== accounts.length - 1 && <hr />}
  </div>
))}

Testing the dApp

Return to your browser where the dApp is running. You should now see transfer forms below each account. Test the functionality by transferring ether between accounts. This will require confirming the transaction in your wallet.

Frequently Asked Questions

What is EIP-6963 and why is it important?
EIP-6963 is an Ethereum Improvement Proposal that standardizes how multiple wallet providers can announce their availability to dApps. This eliminates the reliance on the global window.ethereum object and provides a more flexible approach for users with multiple wallets.

How does Web3.js support EIP-6963?
Web3.js provides built-in helper functions like requestEIP6963Providers() and onNewProviderDiscovered() that simplify working with EIP-6963 compliant wallets. These utilities handle provider discovery and management automatically.

Can I use this dApp with other Ethereum networks?
Yes, simply configure your wallet to connect to other networks like Mainnet, Goerli, or Sepolia. The dApp will automatically detect the network change and refresh accordingly.

What security precautions should I take when developing with Hardhat?
Never commit your Hardhat configuration file with mnemonic phrases to version control. Use environment variables for sensitive information and ensure your development network is properly isolated from production environments.

How can I extend this dApp with additional functionality?
You can add features like token transfers, smart contract interactions, or transaction history tracking. 👉 Explore more strategies for advanced dApp development.

What should I do if my wallet doesn't appear in the provider list?
Ensure your wallet supports EIP-6963 and is properly installed. Refresh the dApp and check if your wallet appears. Some wallets may require additional permissions to be detected.

Conclusion

This tutorial demonstrated how to build a dApp using Web3.js with EIP-6963 support for multiple wallet providers. You learned to set up a development environment, configure a local Hardhat network, create a provider discovery system, and implement ether transfer functionality. Web3.js provides comprehensive utilities for working with Ethereum networks and wallet providers, making it an excellent choice for dApp development. 👉 Get advanced methods for optimizing your dApp development workflow.