How to Connect a MetaMask Wallet to GenLayer Contracts: A Complete Guide

Connecting MetaMask to GenLayer contracts requires detecting the wallet, requesting account access, switching to the GenLayer network (chain ID 61999), and creating a GenLayer SDK client for signed transactions.

Integrating MetaMask with your GenLayer dApp enables users to sign transactions and interact with on-chain contracts directly from their browser wallet. The genlayer-project-boilerplate repository provides a complete, production-ready implementation that handles network detection, switching, and state persistence. This guide walks through the exact implementation found in the source code.

Detecting and Connecting MetaMask

The connection flow begins with verifying that MetaMask is installed and available in the browser. According to the GenLayer boilerplate source code, this check is handled by a dedicated utility function.

Verify MetaMask Installation

The isMetaMaskInstalled() helper in frontend/lib/genlayer/client.ts checks for the presence of the MetaMask provider:

// frontend/lib/genlayer/client.ts (lines 61-64)
export function isMetaMaskInstalled(): boolean {
  return typeof window !== "undefined" && window.ethereum?.isMetaMask === true;
}

This guard is essential before attempting any Ethereum provider calls. If window.ethereum is undefined or lacks the isMetaMask flag, the user either doesn't have MetaMask installed or is using a different wallet provider.

Request Account Access and Network Switch

Once MetaMask is confirmed, the connectMetaMask() function orchestrates the full connection sequence:

// frontend/lib/genlayer/client.ts (lines 10-30 excerpt)
export async function connectMetaMask(): Promise<string> {
  if (!isMetaMaskInstalled()) {
    throw new Error("MetaMask is not installed");
  }

  // Request account access
  const accounts = await window.ethereum.request({
    method: "eth_requestAccounts"
  });
  
  if (!accounts.length) {
    throw new Error("No accounts found");
  }

  // Ensure we're on the correct network
  if (!(await isOnGenLayerNetwork())) {
    await switchToGenLayerNetwork();
  }

  return accounts[0]; // Return the connected wallet address
}

This function performs three critical operations in sequence:

  • Prompts the user to unlock MetaMask and grant site access via eth_requestAccounts
  • Validates that at least one account is available
  • Switches networks if the user isn't already on GenLayer

Ensuring the Correct Network (GenLayer Chain ID 61999)

Network management is a core requirement for multi-chain dApps. The GenLayer boilerplate includes robust utilities to detect, validate, and switch networks automatically.

Reading the Current Chain ID

The getCurrentChainId() utility reads MetaMask's currently active network:

// frontend/lib/genlayer/client.ts (lines 22-34)
export async function getCurrentChainId(): Promise<string> {
  const chainId = await window.ethereum.request({
    method: "eth_chainId"
  });
  return chainId; // Returns hex string, e.g., "0xf23f" for 61999
}

Validating Against GenLayer

The isOnGenLayerNetwork() function compares the current chain ID against the expected GenLayer configuration:

// frontend/lib/genlayer/client.ts (lines 94-104 excerpt)
export async function isOnGenLayerNetwork(): Promise<boolean> {
  const currentChainId = await getCurrentChainId();
  const expectedChainId = getGenLayerChainId(); // From environment, default: 61999 (0xf23f)
  return currentChainId.toLowerCase() === expectedChainId.toLowerCase();
}

The default GenLayer chain ID is 61999 (hex: 0xf23f), defined in your environment configuration.

Switching or Adding the GenLayer Network

If the user is on a different network, switchToGenLayerNetwork() handles the transition. If GenLayer isn't configured in MetaMask, it falls back to addGenLayerNetwork():

// frontend/lib/genlayer/client.ts (lines 66-88 excerpt)
export async function switchToGenLayerNetwork(): Promise<void> {
  const chainId = getGenLayerChainId(); // "0xf23f"
  
  try {
    await window.ethereum.request({
      method: "wallet_switchEthereumChain",
      params: [{ chainId }]
    });
  } catch (switchError: any) {
    // Error 4902: Chain not added to MetaMask
    if (switchError.code === 4902) {
      await addGenLayerNetwork();
    } else {
      throw switchError;
    }
  }
}

async function addGenLayerNetwork(): Promise<void> {
  await window.ethereum.request({
    method: "wallet_addEthereumChain",
    params: [{
      chainId: "0xf23f",
      chainName: "GenLayer",
      nativeCurrency: {
        name: "GEN",
        symbol: "GEN",
        decimals: 18
      },
      rpcUrls: [getGenLayerRpcUrl()],
      blockExplorerUrls: [getGenLayerExplorerUrl()]
    }]
  });
}

This approach follows EIP-3085 and EIP-3326 standards for wallet network management, ensuring compatibility with MetaMask's latest APIs.

Managing Wallet State with React Context

The WalletProvider component in frontend/lib/genlayer/WalletProvider.tsx provides a centralized state management layer for wallet connections. It handles connection state, loading indicators, and persistence across page reloads.

Connection State and Persistence

The provider stores critical wallet data and syncs with localStorage:

// frontend/lib/genlayer/WalletProvider.tsx (lines 41-86 excerpt)
interface WalletState {
  address: string | null;
  chainId: string | null;
  isConnected: boolean;
  isLoading: boolean;
  isOnCorrectNetwork: boolean;
}

const DISCONNECT_FLAG = "genlayer-wallet-disconnected";

const connectWallet = useCallback(async () => {
  setState(prev => ({ ...prev, isLoading: true }));
  
  const address = await connectMetaMask();
  const chainId = await getCurrentChainId();
  const correctNetwork = await isOnGenLayerNetwork();
  
  // Remove disconnect flag to remember this connection
  localStorage.removeItem(DISCONNECT_FLAG);
  
  setState({
    address,
    chainId,
    isConnected: true,
    isLoading: false,
    isOnCorrectNetwork: correctNetwork,
  });
}, []);

const disconnectWallet = useCallback(() => {
  localStorage.setItem(DISCONNECT_FLAG, "true");
  setState({
    address: null,
    chainId: null,
    isConnected: false,
    isLoading: false,
    isOnCorrectNetwork: false,
  });
}, []);

The DISCONNECT_FLAG mechanism ensures that if a user explicitly disconnects, the app won't automatically reconnect on the next page load—respecting user intent while providing seamless reconnection otherwise.

Listening to MetaMask Events

The provider also subscribes to MetaMask's native events to keep state synchronized:

  • accountsChanged: Updates when the user switches accounts in MetaMask
  • chainChanged: Updates when the user switches networks
  • disconnect: Handles unexpected wallet disconnection

Creating a GenLayer Client for Contract Calls

Once connected, you need a GenLayer SDK client that uses the MetaMask provider for transaction signing. The createGenLayerClient(address) function builds this client:

// frontend/lib/genlayer/client.ts (lines 94-118 excerpt)
import { GenLayerClient, createClient } from "@genlayer/genlayer-js";
import { eip1193ProviderFromEthereum } from "@genlayer/genlayer-js/ethereum";

export async function createGenLayerClient(
  address: string
): Promise<GenLayerClient> {
  const provider = eip1193ProviderFromEthereum(window.ethereum);
  
  return createClient({
    provider,
    account: {
      address: address as `0x${string}`,
      type: "ethereum", // MetaMask uses Ethereum-style accounts
    },
    endpoint: getGenLayerEndpoint(),
  });
}

// Convenience function that auto-fetches connected account
export async function getClient(): Promise<GenLayerClient> {
  const [address] = await window.ethereum.request({
    method: "eth_accounts"
  });
  
  if (!address) {
    throw new Error("No connected account found");
  }
  
  return createGenLayerClient(address);
}

The eip1193ProviderFromEthereum adapter bridges MetaMask's EIP-1193 provider interface to the GenLayer SDK's expected format, enabling seamless transaction signing.

Using the Client in Your UI Components

With the provider and client utilities in place, your React components can trigger connections and interact with contracts. The AccountPanel.tsx component demonstrates this pattern:

// frontend/components/AccountPanel.tsx (lines 26-42 excerpt)
import { useWallet } from "@/lib/genlayer/WalletProvider";

export function AccountPanel() {
  const { 
    address, 
    isConnected, 
    isLoading, 
    connectWallet, 
    disconnectWallet 
  } = useWallet();

  if (!isMetaMaskInstalled()) {
    return <a href="https://metamask.io/download/">Install MetaMask</a>;
  }

  return isConnected ? (
    <div>
      <p>Connected: {address}</p>
      <button onClick={disconnectWallet}>Disconnect</button>
    </div>
  ) : (
    <button onClick={connectWallet} disabled={isLoading}>
      {isLoading ? "Connecting..." : "Connect MetaMask"}
    </button>
  );
}

Example: Calling a GenLayer Contract

Here's how to use the connected client to execute a contract write operation:

import { getClient } from "@/lib/genlayer/client";

async function placeBet(betAmount: number) {
  // Creates client with MetaMask account for signing
  const client = await getClient();
  
  // Target your deployed GenLayer contract
  const contract = client.contract({
    id: getContractAddress(), // Your contract's on-chain address
  });
  
  // Execute write method—MetaMask will prompt for signature
  const result = await contract.write.setBet(betAmount);
  
  return result;
}

The client.contract().write.* methods automatically route through MetaMask's signing flow, presenting the user with their familiar transaction confirmation interface.

Complete Integration Checklist

When implementing MetaMask connection in your GenLayer project:

  • Install detection: Always check window.ethereum?.isMetaMask before provider calls
  • Network configuration: Verify chain ID 61999 and provide fallback RPC endpoints
  • Error handling: Catch 4902 errors to trigger wallet_addEthereumChain
  • State persistence: Use localStorage to remember user disconnections
  • Event subscriptions: Listen for accountsChanged and chainChanged to keep UI synchronized
  • Client creation: Wrap MetaMask's provider with GenLayer SDK's eip1193ProviderFromEthereum

Summary

Connecting MetaMask to GenLayer contracts follows a structured flow implemented in the genlayer-project-boilerplate repository:

  • Detection: Verify MetaMask availability with isMetaMaskInstalled() in frontend/lib/genlayer/client.ts
  • Connection: Request accounts and auto-switch networks via connectMetaMask()
  • Network management: Validate chain ID 61999 and use switchToGenLayerNetwork() or addGenLayerNetwork() as needed
  • State management: Leverage WalletProvider for React state, persistence, and event handling
  • Transaction signing: Create a GenLayer SDK client with createGenLayerClient(address) for contract interactions

This architecture provides a production-ready foundation that handles edge cases like missing networks, user-initiated disconnections, and account switching.

Frequently Asked Questions

What chain ID does GenLayer use for MetaMask connections?

GenLayer uses chain ID 61999 (hex: 0xf23f) as the default network identifier. This value is configured in your environment variables and validated by isOnGenLayerNetwork() in frontend/lib/genlayer/client.ts. The switchToGenLayerNetwork() function automatically prompts users to add or switch to this network if they're on a different chain.

How do I handle users who don't have MetaMask installed?

Check isMetaMaskInstalled() before any connection attempt. This function returns false if window.ethereum?.isMetaMask is unavailable. Your UI should then display an installation prompt linking to https://metamask.io/download/, as shown in frontend/components/AccountPanel.tsx.

Can users switch accounts without reconnecting the dApp?

Yes. The WalletProvider subscribes to MetaMask's accountsChanged event and updates React state automatically. When a user switches accounts in the MetaMask extension, your dApp receives the new address via the event listener and updates the connected account without requiring a manual reconnect.

Where is the wallet connection state stored between page reloads?

The WalletProvider persists connection state using localStorage. A DISCONNECT_FLAG key tracks whether the user explicitly disconnected—if set, the app won't auto-reconnect. Otherwise, the provider can restore the connected state on page load by checking existing MetaMask permissions via eth_accounts.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →