Build a DApp on MAGNE Testnets

Guide for developers to build decentralized applications on MAGNE L1 and M Hash L2 testnets using Ethers.js.

1 Overview

Testnet Only β€” This guide is for testnet DApp development only. MAGNE L1 Testnet and M Hash L2 Testnet are EVM-compatible environments. Testnet tokens have no monetary value. Network parameters may change as testing progresses.

  • Works with standard Ethereum development tools and libraries
  • Supports Ethers.js, Wagmi, Viem, and other Web3 libraries
  • Compatible with MetaMask, WalletConnect, and other wallets
  • Build frontend UI with React, Vue, or plain JavaScript

DApp Architecture

Frontend UI

React, Vue, or plain HTML/JS for user interface

Wallet Connection

MetaMask, WalletConnect, or other Web3 wallets

RPC Provider

Ethers.js or viem for blockchain interaction

Smart Contract

Your deployed contracts on testnet

Transaction Tracking

Block explorer for confirmation status

Network Constants

JavaScript Configuration

export const MAGNE_L1_TESTNET = {
  chainId: '0x' + (20250810).toString(16),
  chainName: "MAGNE L1 Testnet",
  nativeCurrency: {
    name: "MHA",
    symbol: "MHA",
    decimals: 18
  },
  rpcUrls: ["https://rpc.testnet.magicalhash.com"],
  blockExplorerUrls: ["https://explorer.testnet.magicalhash.com"]
};

export const M_HASH_L2_TESTNET = {
  chainId: '0x' + (20250827).toString(16),
  chainName: "M Hash L2 Testnet",
  nativeCurrency: {
    name: "MHA",
    symbol: "MHA",
    decimals: 18
  },
  rpcUrls: ["https://l2-rpc.testnet.magicalhash.com"],
  blockExplorerUrls: ["https://l2-explorer.testnet.magicalhash.com"]
};

Connect Wallet Example

Request Account Access

import { ethers } from 'ethers';

const provider = new ethers.BrowserProvider(window.ethereum);

async function connectWallet() {
  try {
    const accounts = await provider.send("eth_requestAccounts", []);
    console.log("Connected:", accounts[0]);
    return accounts[0];
  } catch (error) {
    console.error("Connection rejected:", error.message);
  }
}

Switch Network

async function switchToL1() {
  try {
    await window.ethereum.request({
      method: 'wallet_switchEthereumChain',
      params: [{ chainId: '0x' + (20250810).toString(16) }],
    });
  } catch (switchError) {
    console.error("Switch failed:", switchError.message);
  }
}

async function addNetwork() {
  try {
    await window.ethereum.request({
      method: 'wallet_addEthereumChain',
      params: [MAGNE_L1_TESTNET],
    });
  } catch (addError) {
    console.error("Add network failed:", addError.message);
  }
}

Tip: Call switchToL1 first. If it fails with chain not found error, call addNetwork to add the network to the wallet.

Read Contract Example

import { ethers } from 'ethers';

const CONTRACT_ADDRESS = "your_deployed_contract_address";
const ABI = [
  "function message() view returns (string)",
  "function setMessage(string newMessage)"
];

async function readContract() {
  const provider = new ethers.BrowserProvider(window.ethereum);
  const signer = await provider.getSigner();
  const contract = new ethers.Contract(CONTRACT_ADDRESS, ABI, signer);

  try {
    const message = await contract.message();
    console.log("Current message:", message);
    return message;
  } catch (error) {
    console.error("Read failed:", error.message);
  }
}

Write Contract Example

async function writeContract(newMessage) {
  const provider = new ethers.BrowserProvider(window.ethereum);
  const signer = await provider.getSigner();
  const contract = new ethers.Contract(CONTRACT_ADDRESS, ABI, signer);

  try {
    const tx = await contract.setMessage(newMessage);
    console.log("Transaction sent:", tx.hash);

    // Wait for confirmation
    const receipt = await tx.wait();
    console.log("Confirmed:", receipt.hash);

    return receipt;
  } catch (error) {
    if (error.message.includes("user rejected")) {
      console.log("Transaction rejected by user");
    } else {
      console.error("Transaction failed:", error.message);
    }
  }
}

User Flow: When calling write functions, the wallet will prompt the user to review and approve the transaction. Users can reject if they do not recognize the action.

Transaction Tracking

Track Transaction Status

function getExplorerUrl(chainId, txHash) {
  const explorers = {
    20250810: "https://explorer.testnet.magicalhash.com",
    20250827: "https://l2-explorer.testnet.magicalhash.com"
  };
  return `${explorers[chainId]}/tx/${txHash}`;
}

async function trackTransaction(tx) {
  console.log(`View on explorer: ${getExplorerUrl(20250810, tx.hash)}`);

  // Wait for 1 confirmation
  const receipt = await tx.wait(1);
  if (receipt.status === 1) {
    console.log("Transaction successful!");
  } else {
    console.log("Transaction failed!");
  }
}

UI / UX Notes

  • Show network name β€” Display the current network (MAGNE L1 or M Hash L2) in the UI so users know where they are
  • Show testnet warning β€” Display a clear banner indicating testnet environment
  • Display transaction status β€” Show pending, confirmed, or failed states
  • Provide explorer links β€” Link to transaction and contract pages on the block explorer
  • Handle wallet rejection β€” Gracefully handle when users reject transactions
  • Handle chain mismatches β€” Prompt user to switch to correct network
  • Show gas estimates β€” Display estimated transaction cost before confirmation

Security Notes

⚠️ Security Requirements

Never ask users for seed phrases or private keys. Legitimate DApps never need these credentials.

Never store private keys in frontend code, localStorage, or cookies.

Use dedicated test wallets for all testnet development.

Testnet tokens have no monetary value.

Smart contract and DApp examples are for testing and educational purposes only. Always audit code before production use.

Troubleshooting

Wallet not detected

Ensure the user has a Web3 wallet installed (MetaMask, Rabby, etc.). Check for window.ethereum availability.

Wrong network selected

Call wallet_switchEthereumChain with the correct chainId. Show users a prompt to switch if on wrong network.

User rejected request

Handle rejection gracefully. Check error.message.includes("user rejected") to identify this case.

RPC timeout

Check your internet connection. Try refreshing the page. If persistent, the RPC may be under maintenance.

Contract address not found

Verify the contract is deployed to the correct network. Check that you're connected to the expected network in the wallet.

Transaction pending

Check the block explorer for pending status. L2 transactions typically confirm faster than L1.

Gas estimation failed

This usually means the transaction would fail. Check contract state, parameters, and wallet balance.