Why Performance Matters in Web3 Why Performance Matters in Web3 In 2025, Web3 applications must compete with Web2's seamless experience. Slow transactions, laggy interfaces, and unreliable data plague many dApps, often owing to poorly optimized Remote Procedure Call (RPC) usage. Here, I tested GetBlock's free tier (5K daily requests, 5 RPS, and multi-chain support). Their paid plans have incredible features; however, for testing, the free tier still offers a decent opportunity to build production-ready dApps at zero cost—if you know how to optimize it. Here, I tested GetBlock's free tier production-ready dApps at zero cost I tested GetBlock’s incredible capabilities and attempted to organize the guide into easy-to-understand language. Essentially, this guide will walk you through building a cross-chain token bridge with real-time updates while maximizing GetBlock's free limits. cross-chain token bridge 📥 GetBlock RPC: Free Tier Capabilities & Strategic Advantages A. Free Plan Technical Specifications (2025 Update) A. Free Plan Technical Specifications (2025 Update) Throughput: 5 requests per second (RPS) Daily Limit: 5,000 requests Protocol Support: REST, JSON-RPC, WebSocket, gRPC, GraphQL Networks: 55+ blockchains (Ethereum, Solana, Polygon, BSC, etc.) Throughput: 5 requests per second (RPS) Throughput Daily Limit: 5,000 requests Daily Limit Protocol Support: REST, JSON-RPC, WebSocket, gRPC, GraphQL Protocol Support Networks: 55+ blockchains (Ethereum, Solana, Polygon, BSC, etc.) Networks This is a table with a more detailed description. Feature Free Tier Limit Ideal Use Case Requests/Day 5,000 Prototypes, MVP dApps Requests/Second 5 RPS Wallet integrations Chains Supported 55+ (EVM, Solana, etc.) Cross-chain bridges WebSocket Access ✅ Yes Real-time event tracking Feature Free Tier Limit Ideal Use Case Requests/Day 5,000 Prototypes, MVP dApps Requests/Second 5 RPS Wallet integrations Chains Supported 55+ (EVM, Solana, etc.) Cross-chain bridges WebSocket Access ✅ Yes Real-time event tracking Feature Free Tier Limit Ideal Use Case Feature Feature Feature Free Tier Limit Free Tier Limit Free Tier Limit Ideal Use Case Ideal Use Case Ideal Use Case Requests/Day 5,000 Prototypes, MVP dApps Requests/Day Requests/Day 5,000 5,000 Prototypes, MVP dApps Prototypes, MVP dApps Requests/Second 5 RPS Wallet integrations Requests/Second Requests/Second 5 RPS 5 RPS Wallet integrations Wallet integrations Chains Supported 55+ (EVM, Solana, etc.) Cross-chain bridges Chains Supported Chains Supported 55+ (EVM, Solana, etc.) 55+ (EVM, Solana, etc.) Cross-chain bridges Cross-chain bridges WebSocket Access ✅ Yes Real-time event tracking WebSocket Access WebSocket Access ✅ Yes ✅ Yes Real-time event tracking Real-time event tracking 📥Hidden Advantages for Performance Optimization Multi-Chain Single API Key – Reduces complexity by managing Ethereum, Polygon, and Solana via one endpoint. Testnet Faucets – Free MATIC, ETH for rapid iteration. WebSocket for Real-Time Data: Avoid polling and saving requests. Multi-Chain Single API Key – Reduces complexity by managing Ethereum, Polygon, and Solana via one endpoint. Multi-Chain Single API Key Testnet Faucets – Free MATIC, ETH for rapid iteration. Testnet Faucets WebSocket for Real-Time Data: Avoid polling and saving requests. WebSocket for Real-Time Data What We're Building We are building a cross-chain token bridge that allows users to transfer tokens between Ethereum and Binance Smart Chain using a multichain wallet dashboard. The solution will include efficient RPC usage, real-time balance updates, and many more. It is gonna be interesting, stay tuned😊. Tech Stack Component Technology Purpose Frontend React + Vite Fast UI rendering and state management Blockchain Ethers.js RPC interactions, smart contract calls Backend Node.js (optional) Caching and fallback layer for efficient use RPC Provider GetBlock Free Tier BSC and ETH RPC data Component Technology Purpose Frontend React + Vite Fast UI rendering and state management Blockchain Ethers.js RPC interactions, smart contract calls Backend Node.js (optional) Caching and fallback layer for efficient use RPC Provider GetBlock Free Tier BSC and ETH RPC data Component Technology Purpose Component Component Component Technology Technology Technology Purpose Purpose Purpose Frontend React + Vite Fast UI rendering and state management Frontend Frontend React + Vite React + Vite Fast UI rendering and state management Fast UI rendering and state management Blockchain Ethers.js RPC interactions, smart contract calls Blockchain Blockchain Ethers.js Ethers.js RPC interactions, smart contract calls RPC interactions, smart contract calls Backend Node.js (optional) Caching and fallback layer for efficient use Backend Backend Node.js (optional) Node.js (optional) Caching and fallback layer for efficient use Caching and fallback layer for efficient use RPC Provider GetBlock Free Tier BSC and ETH RPC data RPC Provider RPC Provider GetBlock Free Tier GetBlock Free Tier BSC and ETH RPC data BSC and ETH RPC data Great — let us build the complete, in-depth Web3 dApp project using GetBlock’s free RPC tier in 2025. 📁 Final Project Directory 📁 Final Project Directory Make sure that your final project’s directory looks like this. You can update your directory according to the following diagram stepwise. 💻 Project Setup 💻 Project Setup 👉Initialize the Project 👉Initialize the Project mkdir ETH-bsc-bridge && cd ETH-bsc-bridge npm init -y npm install --save-dev hardhat npx hardhat mkdir ETH-bsc-bridge && cd ETH-bsc-bridge npm init -y npm install --save-dev hardhat npx hardhat Subsequently, the following dependencies should be installed: npm install dotenv ethers npm install --save-dev @nomicfoundation/hardhat-toolbox npm install dotenv ethers npm install --save-dev @nomicfoundation/hardhat-toolbox 👉Get Your GetBlock API Key 👉Get Your GetBlock API Key Sign up at GetBlock.io Grab your free API key from the dashboard Note your endpoints: Sepolia RPC Testnet: https://go.getblock.io/YOUR_API_KEY (May not exhibit testnet balance in some cases if you are using Polygon Amoy ! ) Binance Smart Chain (BSC): https://go.getblock.io/YOUR_API_KEY Sign up at GetBlock.io GetBlock.io Grab your free API key from the dashboard Note your endpoints: Sepolia RPC Testnet: https://go.getblock.io/YOUR_API_KEY (May not exhibit testnet balance in some cases if you are using Polygon Amoy ! ) Binance Smart Chain (BSC): https://go.getblock.io/YOUR_API_KEY Sepolia RPC Testnet: https://go.getblock.io/YOUR_API_KEY (May not exhibit testnet balance in some cases if you are using Polygon Amoy ! ) Binance Smart Chain (BSC): https://go.getblock.io/YOUR_API_KEY Sepolia RPC Testnet: https://go.getblock.io/YOUR_API_KEY (May not exhibit testnet balance in some cases if you are using Polygon Amoy ! ) https://go.getblock.io/YOUR_API_KEY Binance Smart Chain (BSC): https://go.getblock.io/YOUR_API_KEY https://go.getblock.io/YOUR_API_KEY ⚛️Configure .env File ⚛️Configure .env File Create .env in root: MNEMONIC="your_12 words mnemonic phrase" BSC_RPC_URL="https://bsc-testnet.getblock.io/YOUR_API_KEY/jsonrpc" SEPOLIA_RPC_URL="https://eth-testnet.getblock.io/YOUR_API_KEY/jsonrpc" MNEMONIC="your_12 words mnemonic phrase" BSC_RPC_URL="https://bsc-testnet.getblock.io/YOUR_API_KEY/jsonrpc" SEPOLIA_RPC_URL="https://eth-testnet.getblock.io/YOUR_API_KEY/jsonrpc" ⚠️ Always use TEST ACCOUNTS for development.⚠️ Keep mnemonic safe — don’t share it publicly. ⚠️ Always use TEST ACCOUNTS for development.⚠️ Keep mnemonic safe — don’t share it publicly. ⚡Update hardhat.config.js ⚡Update hardhat.config.js 👉Please update pre-created hardhat.config.js in the root directory. 👉 hardhat.config.js require("@nomicfoundation/hardhat-toolbox"); require("dotenv").config(); const { MNEMONIC, SEPOLIA_RPC_URL, BSC_RPC_URL } = process.env; function getAccounts() { if (!MNEMONIC || MNEMONIC.split(" ").length !== 12) { console.warn("⚠️ Invalid MNEMONIC, no accounts loaded."); return []; } return { mnemonic: MNEMONIC }; } module.exports = { solidity: "0.8.20", defaultNetwork: "sepolia", networks: { sepolia: { url: SEPOLIA_RPC_URL, accounts: getAccounts(), gasPrice: 25_000_000_000, timeout: 60_000, }, bscTestnet: { url: BSC_RPC_URL, accounts: getAccounts(), gasPrice: 25_000_000_000, }, }, }; require("@nomicfoundation/hardhat-toolbox"); require("dotenv").config(); const { MNEMONIC, SEPOLIA_RPC_URL, BSC_RPC_URL } = process.env; function getAccounts() { if (!MNEMONIC || MNEMONIC.split(" ").length !== 12) { console.warn("⚠️ Invalid MNEMONIC, no accounts loaded."); return []; } return { mnemonic: MNEMONIC }; } module.exports = { solidity: "0.8.20", defaultNetwork: "sepolia", networks: { sepolia: { url: SEPOLIA_RPC_URL, accounts: getAccounts(), gasPrice: 25_000_000_000, timeout: 60_000, }, bscTestnet: { url: BSC_RPC_URL, accounts: getAccounts(), gasPrice: 25_000_000_000, }, }, }; 💡Smart Contracts – Lock & Mint 💡Smart Contracts – Lock & Mint We'll create three essential contracts here: ✅ Token.sol — ERC20 token deployed on both BSC & ETH ✅ BSCBridge.sol — Lock tokens & emit event on BSC ✅ ETHBridge.sol — Mint tokens on Ethereum (only by admin/relayer) ✅ Token.sol — ERC20 token deployed on both BSC & ETH Token.sol ✅ BSCBridge.sol — Lock tokens & emit event on BSC BSCBridge.sol ✅ ETHBridge.sol — Mint tokens on Ethereum (only by admin/relayer) ETHBridge.sol 📁 Location 📁 Location All smart contracts go into:/contracts/ /contracts/ Please re-check the directory structure in the Figure 1. 1️⃣ Token.sol – Shared Token Contract 1️⃣ Token.sol – Shared Token Contract Basic ERC20 token. 📄 contracts/Token.sol contracts/Token.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract BridgeToken is ERC20, Ownable { constructor(address initialOwner) ERC20("BridgeToken", "BRG") { _mint(initialOwner, 1_000_000 * 10 ** decimals()); transferOwnership(initialOwner); // This sets the initial owner (e.g., bridge contract later) } function mint(address to, uint256 amount) external onlyOwner { _mint(to, amount); } } // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract BridgeToken is ERC20, Ownable { constructor(address initialOwner) ERC20("BridgeToken", "BRG") { _mint(initialOwner, 1_000_000 * 10 ** decimals()); transferOwnership(initialOwner); // This sets the initial owner (e.g., bridge contract later) } function mint(address to, uint256 amount) external onlyOwner { _mint(to, amount); } } 2️⃣ BSCBridge.sol – Lock Contract on BSC 2️⃣ BSCBridge.sol – Lock Contract on BSC This locks tokens and emits TokenLocked event. 📄 contracts/BSCBridge.sol contracts/BSCBridge.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "./BridgeToken.sol"; contract BSCBridge { BridgeToken public token; address public admin; event TokenMinted(address indexed user, uint256 amount); constructor(address tokenAddress) { token = BridgeToken(tokenAddress); admin = msg.sender; } function mintTokens(address to, uint256 amount) external { require(msg.sender == admin, "Only admin can mint"); token.mint(to, amount); emit TokenMinted(to, amount); } } // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "./BridgeToken.sol"; contract BSCBridge { BridgeToken public token; address public admin; event TokenMinted(address indexed user, uint256 amount); constructor(address tokenAddress) { token = BridgeToken(tokenAddress); admin = msg.sender; } function mintTokens(address to, uint256 amount) external { require(msg.sender == admin, "Only admin can mint"); token.mint(to, amount); emit TokenMinted(to, amount); } } 🧠 How it works: Users approve and call lockTokens Contract emits TokenLocked Backend listens and triggers mint on BSC 🧠 How it works: How it works: Users approve and call lockTokens Contract emits TokenLocked Backend listens and triggers mint on BSC Users approve and call lockTokens lockTokens Contract emits TokenLocked TokenLocked Backend listens and triggers mint on BSC 3️⃣ ETHBridge.sol – Mint Contract on Ethereum 3️⃣ ETHBridge.sol – Mint Contract on Ethereum Only a trusted relayer (admin) can mint tokens. 📄 contracts/ETHBridge.sol contracts/ETHBridge.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "./BridgeToken.sol"; contract ETHBridge { BridgeToken public token; address public admin; event TokenLocked(address indexed user, uint256 amount, address targetAddress); constructor(address tokenAddress) { token = BridgeToken(tokenAddress); admin = msg.sender; } function lockTokens(uint256 amount, address targetAddress) external { require(amount > 0, "Amount must be greater than 0"); token.transferFrom(msg.sender, address(this), amount); emit TokenLocked(msg.sender, amount, targetAddress); } function withdraw(address to, uint256 amount) external { require(msg.sender == admin, "Only admin"); token.transfer(to, amount); } } // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "./BridgeToken.sol"; contract ETHBridge { BridgeToken public token; address public admin; event TokenLocked(address indexed user, uint256 amount, address targetAddress); constructor(address tokenAddress) { token = BridgeToken(tokenAddress); admin = msg.sender; } function lockTokens(uint256 amount, address targetAddress) external { require(amount > 0, "Amount must be greater than 0"); token.transferFrom(msg.sender, address(this), amount); emit TokenLocked(msg.sender, amount, targetAddress); } function withdraw(address to, uint256 amount) external { require(msg.sender == admin, "Only admin"); token.transfer(to, amount); } } 🧠 How it works: Backend (trusted relayer) listens to BSC events Calls mintTokens on Ethereum Sepolia via private key Tokens minted for user 🧠 How it works: How it works: Backend (trusted relayer) listens to BSC events Calls mintTokens on Ethereum Sepolia via private key Tokens minted for user Backend (trusted relayer) listens to BSC events Calls mintTokens on Ethereum Sepolia via private key mintTokens Tokens minted for user 4️⃣ Deploy Scripts 4️⃣ Deploy Scripts Wait, don’t forget to install dependency for Contracts: npm install @openzeppelin/contracts npm install @openzeppelin/contracts 👉Create scripts/deployBSC.js: scripts/deployBSC.js const hre = require("hardhat"); async function main() { const [deployer] = await hre.ethers.getSigners(); console.log("Deploying to BSC Testnet from:", deployer.address); // Deploy BridgeToken (owned by deployer for now) const BridgeToken = await hre.ethers.getContractFactory("BridgeToken"); const token = await BridgeToken.deploy(deployer.address); await token.waitForDeployment(); const tokenAddress = await token.getAddress(); console.log("✅ BridgeToken (BSC) deployed at:", tokenAddress); // Deploy BSCBridge const BSCBridge = await hre.ethers.getContractFactory("BSCBridge"); const bridge = await BSCBridge.deploy(tokenAddress); await bridge.waitForDeployment(); const bridgeAddress = await bridge.getAddress(); console.log("✅ BSCBridge deployed at:", bridgeAddress); // Transfer token ownership to BSCBridge so it can mint const tx = await token.transferOwnership(bridgeAddress); await tx.wait(); console.log("🔑 Token ownership transferred to BSCBridge."); } main().catch((error) => { console.error("🚨 Deployment failed:", error); process.exitCode = 1; }); const hre = require("hardhat"); async function main() { const [deployer] = await hre.ethers.getSigners(); console.log("Deploying to BSC Testnet from:", deployer.address); // Deploy BridgeToken (owned by deployer for now) const BridgeToken = await hre.ethers.getContractFactory("BridgeToken"); const token = await BridgeToken.deploy(deployer.address); await token.waitForDeployment(); const tokenAddress = await token.getAddress(); console.log("✅ BridgeToken (BSC) deployed at:", tokenAddress); // Deploy BSCBridge const BSCBridge = await hre.ethers.getContractFactory("BSCBridge"); const bridge = await BSCBridge.deploy(tokenAddress); await bridge.waitForDeployment(); const bridgeAddress = await bridge.getAddress(); console.log("✅ BSCBridge deployed at:", bridgeAddress); // Transfer token ownership to BSCBridge so it can mint const tx = await token.transferOwnership(bridgeAddress); await tx.wait(); console.log("🔑 Token ownership transferred to BSCBridge."); } main().catch((error) => { console.error("🚨 Deployment failed:", error); process.exitCode = 1; }); 👉Create scripts/deployETH.js: scripts/deployETH.js const hre = require("hardhat"); async function main() { const [deployer] = await hre.ethers.getSigners(); console.log("Deploying to Sepolia from:", deployer.address); // Deploy BridgeToken const BridgeToken = await hre.ethers.getContractFactory("BridgeToken"); const token = await BridgeToken.deploy(deployer.address); await token.waitForDeployment(); const tokenAddress = await token.getAddress(); console.log("✅ BridgeToken (Sepolia) deployed at:", tokenAddress); // Deploy ETHBridge const ETHBridge = await hre.ethers.getContractFactory("ETHBridge"); const bridge = await ETHBridge.deploy(tokenAddress); await bridge.waitForDeployment(); const bridgeAddress = await bridge.getAddress(); console.log("✅ ETHBridge deployed at:", bridgeAddress); } main().catch((error) => { console.error("🚨 Deployment failed:", error); process.exitCode = 1; }); const hre = require("hardhat"); async function main() { const [deployer] = await hre.ethers.getSigners(); console.log("Deploying to Sepolia from:", deployer.address); // Deploy BridgeToken const BridgeToken = await hre.ethers.getContractFactory("BridgeToken"); const token = await BridgeToken.deploy(deployer.address); await token.waitForDeployment(); const tokenAddress = await token.getAddress(); console.log("✅ BridgeToken (Sepolia) deployed at:", tokenAddress); // Deploy ETHBridge const ETHBridge = await hre.ethers.getContractFactory("ETHBridge"); const bridge = await ETHBridge.deploy(tokenAddress); await bridge.waitForDeployment(); const bridgeAddress = await bridge.getAddress(); console.log("✅ ETHBridge deployed at:", bridgeAddress); } main().catch((error) => { console.error("🚨 Deployment failed:", error); process.exitCode = 1; }); 5️⃣ Deploy the Contracts 5️⃣ Deploy the Contracts This time, we deploy our contracts to the blockchain. Before this, make sure to have testnet tokens for the BSC and Ethereum testnets in your MetaMask wallet. They can be obtained from free faucet sites such as the GetBlock, and BNB faucet. GetBlock BNB faucet First, let us try to deploy a contract to the BSC test; the run command npx hardhat compiles to compile, and then run the following command at the terminal: npx hardhat compiles npx hardhat run scripts/deployBSC.js --network bscTestnet npx hardhat run scripts/deployBSC.js --network bscTestnet Once you deploy the contract to the testnet, you will see the following output in the terminal: Second, the contract was deployed to the Ethereum Sepolia testnet. Run the following command at the terminal: npx hardhat run scripts/deployEth.js --network sepolia npx hardhat run scripts/deployEth.js --network sepolia The output of the terminal should be similar if everything is correct. Note: Please keep both the contract addresses from the BSC and Ethereum deployment to update.env files for the next step. The tokens deployed here are BRG and they can be tracked on BSC testnet and Ethereum Sepolia testnet explorers. Note: Please keep both the contract addresses from the BSC and Ethereum deployment to update.env files for the next step. The tokens deployed here are BRG and they can be tracked on BSC testnet and Ethereum Sepolia testnet explorers. .env ⚙️Backend Listener — Token Bridge Relayer ⚙️Backend Listener — Token Bridge Relayer The relayer service listens to events from the BSC Bridge and calls the mintTokens() function of the Ethereum Bridge. relayer service BSC Bridge Ethereum Bridge 📁 Directory Structure Update 📁 Directory Structure Update Create the backend/ directory in your root folder: backend/ mkdir backend && cd backend npm init -y npm install ethers dotenv mkdir backend && cd backend npm init -y npm install ethers dotenv 🔐 .env (Root-level) 🔐 .env (Root-level) Now, we update with the following details .env file. .env BSC_BRIDGE="0x...your deployed BSCBridge address" SEPOLIA_RPC_URL="0x...your deployed EthereumBridge address" SEPOLIA_TOKEN="0x...your ETH token address" BSC_BRIDGE="0x...your deployed BSCBridge address" SEPOLIA_RPC_URL="0x...your deployed EthereumBridge address" SEPOLIA_TOKEN="0x...your ETH token address" 🧠 You must replace the above values with real deployed addresses. 🧠 You must replace the above values with real deployed addresses. real deployed addresses 📄 backend/index.js — Main Relayer Logic 📄 backend/index.js — Main Relayer Logic require("dotenv").config({ path: __dirname + "/../.env" }); const { ethers } = require("ethers"); // Load environment variables const { MNEMONIC, SEPOLIA_WS_URL, SEPOLIA_BRIDGE, BSC_RPC_URL, BSC_BRIDGE } = process.env; // Validate env variables if (!MNEMONIC || !SEPOLIA_WS_URL || !SEPOLIA_BRIDGE || !BSC_RPC_URL || !BSC_BRIDGE) { console.error("❌ Missing environment variables. Check .env file."); process.exit(1); } // Create wallet from mnemonic let wallet; try { wallet = ethers.Wallet.fromPhrase(MNEMONIC); } catch (err) { console.error("❌ Invalid MNEMONIC:", err.message); process.exit(1); } // Providers const sepoliaProvider = new ethers.WebSocketProvider(SEPOLIA_WS_URL); const bscProvider = new ethers.JsonRpcProvider(BSC_RPC_URL); const bscSigner = wallet.connect(bscProvider); // Contract ABIs const ETH_BRIDGE_ABI = [ "event TokenLocked(address indexed user, uint256 amount, address targetAddress)" ]; const BSC_BRIDGE_ABI = [ "function mintTokens(address to, uint256 amount) external" ]; // Contract instances const ethBridge = new ethers.Contract(SEPOLIA_BRIDGE, ETH_BRIDGE_ABI, sepoliaProvider); const bscBridge = new ethers.Contract(BSC_BRIDGE, BSC_BRIDGE_ABI, bscSigner); // In-memory cache to avoid duplicate processing const processedTxs = new Set(); async function replayPastEvents(fromBlock = null, toBlock = "latest") { console.log("🔁 Scanning past events..."); if (!fromBlock) { const currentBlock = await sepoliaProvider.getBlockNumber(); fromBlock = currentBlock - 10000; // Lookback 10K blocks if (fromBlock < 0) fromBlock = 0; } if (toBlock === "latest") { toBlock = await sepoliaProvider.getBlockNumber(); } const step = 2000; for (let start = fromBlock; start <= toBlock; start += step) { let end = start + step - 1; if (end > toBlock) end = toBlock; console.log(`📦 Querying blocks: ${start} → ${end}`); try { const logs = await ethBridge.queryFilter("TokenLocked", start, end); for (const event of logs) { const { user, amount, targetAddress } = event.args; const txHash = event.transactionHash; if (processedTxs.has(txHash)) { console.log("⚠️ Already processed:", txHash); continue; } processedTxs.add(txHash); console.log(`🔒 Replaying: ${user} locked ${ethers.formatEther(amount)} BRG → ${targetAddress}`); try { const tx = await bscBridge.mintTokens(targetAddress, amount); await tx.wait(); console.log(`✅ Minted ${ethers.formatEther(amount)} BRG on BSC for ${targetAddress}`); } catch (err) { console.error("❌ Minting failed:", err.message); } } } catch (err) { console.error(`❌ Error querying blocks ${start} → ${end}:`, err.message); } } } async function startRelayer() { console.log("🚀 Relayer started. Listening for real-time TokenLocked events..."); ethBridge.on("TokenLocked", async (user, amount, targetAddress, event) => { const txHash = event.transactionHash; if (processedTxs.has(txHash)) { console.log("⚠️ Duplicate real-time event skipped:", txHash); return; } processedTxs.add(txHash); console.log(`🔒 Detected real-time lock: ${user} locked ${ethers.formatEther(amount)} BRG → ${targetAddress}`); try { const tx = await bscBridge.mintTokens(targetAddress, amount); await tx.wait(); console.log(`✅ Minted ${ethers.formatEther(amount)} BRG on BSC for ${targetAddress}`); } catch (err) { console.error("❌ Real-time minting failed:", err.message); } }); } // Start everything (async () => { await replayPastEvents(); await startRelayer(); })(); require("dotenv").config({ path: __dirname + "/../.env" }); const { ethers } = require("ethers"); // Load environment variables const { MNEMONIC, SEPOLIA_WS_URL, SEPOLIA_BRIDGE, BSC_RPC_URL, BSC_BRIDGE } = process.env; // Validate env variables if (!MNEMONIC || !SEPOLIA_WS_URL || !SEPOLIA_BRIDGE || !BSC_RPC_URL || !BSC_BRIDGE) { console.error("❌ Missing environment variables. Check .env file."); process.exit(1); } // Create wallet from mnemonic let wallet; try { wallet = ethers.Wallet.fromPhrase(MNEMONIC); } catch (err) { console.error("❌ Invalid MNEMONIC:", err.message); process.exit(1); } // Providers const sepoliaProvider = new ethers.WebSocketProvider(SEPOLIA_WS_URL); const bscProvider = new ethers.JsonRpcProvider(BSC_RPC_URL); const bscSigner = wallet.connect(bscProvider); // Contract ABIs const ETH_BRIDGE_ABI = [ "event TokenLocked(address indexed user, uint256 amount, address targetAddress)" ]; const BSC_BRIDGE_ABI = [ "function mintTokens(address to, uint256 amount) external" ]; // Contract instances const ethBridge = new ethers.Contract(SEPOLIA_BRIDGE, ETH_BRIDGE_ABI, sepoliaProvider); const bscBridge = new ethers.Contract(BSC_BRIDGE, BSC_BRIDGE_ABI, bscSigner); // In-memory cache to avoid duplicate processing const processedTxs = new Set(); async function replayPastEvents(fromBlock = null, toBlock = "latest") { console.log("🔁 Scanning past events..."); if (!fromBlock) { const currentBlock = await sepoliaProvider.getBlockNumber(); fromBlock = currentBlock - 10000; // Lookback 10K blocks if (fromBlock < 0) fromBlock = 0; } if (toBlock === "latest") { toBlock = await sepoliaProvider.getBlockNumber(); } const step = 2000; for (let start = fromBlock; start <= toBlock; start += step) { let end = start + step - 1; if (end > toBlock) end = toBlock; console.log(`📦 Querying blocks: ${start} → ${end}`); try { const logs = await ethBridge.queryFilter("TokenLocked", start, end); for (const event of logs) { const { user, amount, targetAddress } = event.args; const txHash = event.transactionHash; if (processedTxs.has(txHash)) { console.log("⚠️ Already processed:", txHash); continue; } processedTxs.add(txHash); console.log(`🔒 Replaying: ${user} locked ${ethers.formatEther(amount)} BRG → ${targetAddress}`); try { const tx = await bscBridge.mintTokens(targetAddress, amount); await tx.wait(); console.log(`✅ Minted ${ethers.formatEther(amount)} BRG on BSC for ${targetAddress}`); } catch (err) { console.error("❌ Minting failed:", err.message); } } } catch (err) { console.error(`❌ Error querying blocks ${start} → ${end}:`, err.message); } } } async function startRelayer() { console.log("🚀 Relayer started. Listening for real-time TokenLocked events..."); ethBridge.on("TokenLocked", async (user, amount, targetAddress, event) => { const txHash = event.transactionHash; if (processedTxs.has(txHash)) { console.log("⚠️ Duplicate real-time event skipped:", txHash); return; } processedTxs.add(txHash); console.log(`🔒 Detected real-time lock: ${user} locked ${ethers.formatEther(amount)} BRG → ${targetAddress}`); try { const tx = await bscBridge.mintTokens(targetAddress, amount); await tx.wait(); console.log(`✅ Minted ${ethers.formatEther(amount)} BRG on BSC for ${targetAddress}`); } catch (err) { console.error("❌ Real-time minting failed:", err.message); } }); } // Start everything (async () => { await replayPastEvents(); await startRelayer(); })(); Start Relayer From backend/ directory: node index.js If everything is set correctly, you should see: 🚀 Relayer started. Listening for BSC lock events... Now your relayer listens 24/7 to Ethereum Testnet, and triggers minting on Binance Smart Chain when locking happens. Also, don’t forget to install npm install ethers dotenv in the backend directory. In case you notice an error due to RPC endpoint, switch it to WebSocket on GetBlock and update your .env file SEPOLIA_WS_URL="wss://go.getblock.io/YOUR_API_KEY Start Relayer From backend/ directory: backend/ node index.js node index.js If everything is set correctly, you should see: 🚀 Relayer started. Listening for BSC lock events... 🚀 Relayer started. Listening for BSC lock events... Now your relayer listens 24/7 to Ethereum Testnet, and triggers minting on Binance Smart Chain when locking happens. Also, don’t forget to install npm install ethers dotenv in the backend directory. In case you notice an error due to RPC endpoint, switch it to WebSocket on GetBlock and update your .env file SEPOLIA_WS_URL="wss://go.getblock.io/YOUR_API_KEY npm install ethers dotenv backend .env SEPOLIA_WS_URL="wss://go.getblock.io/YOUR_API_KEY In our case, terminal exhibited something like as the following output: 🌐 Now, we are focused to build the Frontend Bridge dApp with React + MetaMask + Ethers.js Now, we are focused to build the Frontend Bridge dApp with React + MetaMask + Ethers.js 📁 Project Directory Update 📁 Project Directory Update npm create vite@latest frontend cd frontend npm install npm install -D tailwindcss@3.4.3 postcss autoprefixer web3@latest npx tailwindcss init -p npm create vite@latest frontend cd frontend npm install npm install -D tailwindcss@3.4.3 postcss autoprefixer web3@latest npx tailwindcss init -p Create another .env to /frontend/.env : .env /frontend/.env # Sepolia network VITE_ETH_BRIDGE=0xYourETHBridge VITE_TOKEN_ADDRESS=0xourETHToken VITE_ETH_CHAIN_ID=11155111 # BSC Testnet VITE_BSC_BRIDGE=0xYourBSCBridge VITE_BSC_TOKEN=0xYourBSCToken VITE_BSC_CHAIN_ID=97 # Sepolia network VITE_ETH_BRIDGE=0xYourETHBridge VITE_TOKEN_ADDRESS=0xourETHToken VITE_ETH_CHAIN_ID=11155111 # BSC Testnet VITE_BSC_BRIDGE=0xYourBSCBridge VITE_BSC_TOKEN=0xYourBSCToken VITE_BSC_CHAIN_ID=97 Note: You would have already created the ETH testnet token just include the contract address in the .env file. The BSC testnet-based token can be created easily from here without any difficulties if you wish further testings. Therefore, creating tokens in the blockchain is simple and does not require extra stress-coding expertise. Note: You would have already created the ETH testnet token just include the contract address in the .env file. The BSC testnet-based token can be created easily from here without any difficulties if you wish further testings. Therefore, creating tokens in the blockchain is simple and does not require extra stress-coding expertise. Note: .env here Therefore, creating tokens in the blockchain is simple and does not require extra stress-coding expertise. 1️⃣ frontend/src/bridge.js – Contract Logic 1️⃣ frontend/src/bridge.js – Contract Logic // frontend/src/bridge.js export const ETH_BRIDGE_ADDRESS = import.meta.env.VITE_ETH_BRIDGE; export const TOKEN_ADDRESS = import.meta.env.VITE_TOKEN_ADDRESS; // Full ABI from Hardhat artifact (for ETHBridge) export const ETH_BRIDGE_ABI = [ { "inputs": [ { "internalType": "address", "name": "tokenAddress", "type": "address" } ], "stateMutability": "nonpayable", "type": "constructor" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "user", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }, { "indexed": false, "internalType": "address", "name": "targetAddress", "type": "address" } ], "name": "TokenLocked", "type": "event" }, { "inputs": [ { "internalType": "uint256", "name": "amount", "type": "uint256" }, { "internalType": "address", "name": "targetAddress", "type": "address" } ], "name": "lockTokens", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "admin", "outputs": [{ "internalType": "address", "name": "", "type": "address" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "token", "outputs": [{ "internalType": "contract BridgeToken", "name": "", "type": "address" }], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "to", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" } ], "name": "withdraw", "outputs": [], "stateMutability": "nonpayable", "type": "function" } ]; // ERC20 ABI for approve and balanceOf export const ERC20_ABI = [ { "constant": true, "inputs": [{ "name": "_owner", "type": "address" }], "name": "balanceOf", "outputs": [{ "name": "balance", "type": "uint256" }], "type": "function" }, { "constant": false, "inputs": [ { "name": "_spender", "type": "address" }, { "name": "_value", "type": "uint256" } ], "name": "approve", "outputs": [{ "name": "success", "type": "bool" }], "type": "function" }, { "constant": true, "inputs": [], "name": "decimals", "outputs": [{ "name": "", "type": "uint8" }], "type": "function" } ]; // frontend/src/bridge.js export const ETH_BRIDGE_ADDRESS = import.meta.env.VITE_ETH_BRIDGE; export const TOKEN_ADDRESS = import.meta.env.VITE_TOKEN_ADDRESS; // Full ABI from Hardhat artifact (for ETHBridge) export const ETH_BRIDGE_ABI = [ { "inputs": [ { "internalType": "address", "name": "tokenAddress", "type": "address" } ], "stateMutability": "nonpayable", "type": "constructor" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "user", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }, { "indexed": false, "internalType": "address", "name": "targetAddress", "type": "address" } ], "name": "TokenLocked", "type": "event" }, { "inputs": [ { "internalType": "uint256", "name": "amount", "type": "uint256" }, { "internalType": "address", "name": "targetAddress", "type": "address" } ], "name": "lockTokens", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "admin", "outputs": [{ "internalType": "address", "name": "", "type": "address" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "token", "outputs": [{ "internalType": "contract BridgeToken", "name": "", "type": "address" }], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "to", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" } ], "name": "withdraw", "outputs": [], "stateMutability": "nonpayable", "type": "function" } ]; // ERC20 ABI for approve and balanceOf export const ERC20_ABI = [ { "constant": true, "inputs": [{ "name": "_owner", "type": "address" }], "name": "balanceOf", "outputs": [{ "name": "balance", "type": "uint256" }], "type": "function" }, { "constant": false, "inputs": [ { "name": "_spender", "type": "address" }, { "name": "_value", "type": "uint256" } ], "name": "approve", "outputs": [{ "name": "success", "type": "bool" }], "type": "function" }, { "constant": true, "inputs": [], "name": "decimals", "outputs": [{ "name": "", "type": "uint8" }], "type": "function" } ]; 2️⃣ frontend/src/App.jsx – React UI 2️⃣ frontend/src/App.jsx – React UI import React, { useEffect, useState } from "react"; import Web3 from "web3"; import detectEthereumProvider from "@metamask/detect-provider"; import { ETH_BRIDGE_ABI, ERC20_ABI, ETH_BRIDGE_ADDRESS, TOKEN_ADDRESS, } from "./bridge"; function App() { const [web3, setWeb3] = useState(null); const [account, setAccount] = useState(""); const [tokenBalance, setTokenBalance] = useState("0"); const [amount, setAmount] = useState(""); const [targetAddress, setTargetAddress] = useState(""); const [status, setStatus] = useState(""); // Debugging: Log env variables console.log("ENV ETH_BRIDGE_ADDRESS:", ETH_BRIDGE_ADDRESS); console.log("ENV TOKEN_ADDRESS:", TOKEN_ADDRESS); // Verify addresses on mount useEffect(() => { if (!ETH_BRIDGE_ADDRESS || !TOKEN_ADDRESS) { setStatus( "❌ Contract address not specified. Please check your .env file." ); } }, []); const connectWallet = async () => { const provider = await detectEthereumProvider(); if (!provider) { setStatus("❌ MetaMask not detected."); return; } try { await provider.request({ method: "eth_requestAccounts" }); const web3Instance = new Web3(provider); setWeb3(web3Instance); const accounts = await web3Instance.eth.getAccounts(); setAccount(accounts[0]); const chainId = await provider.request({ method: "eth_chainId" }); if (chainId !== "0xaa36a7") { setStatus("❌ Please switch to Ethereum Sepolia."); } else { setStatus("✅ Connected to Ethereum Sepolia."); } } catch (err) { console.error("MetaMask connection error:", err); setStatus("❌ Failed to connect MetaMask."); } }; const loadBalance = async () => { if (!web3 || !account) return; try { if (!TOKEN_ADDRESS) { setStatus("❌ Token contract address missing."); return; } const token = new web3.eth.Contract(ERC20_ABI, TOKEN_ADDRESS); const raw = await token.methods.balanceOf(account).call(); const formatted = web3.utils.fromWei(raw, "ether"); setTokenBalance(formatted); } catch (err) { console.error("Balance error:", err); setStatus("❌ Failed to fetch BRG balance."); } }; const handleBridge = async () => { setStatus(""); if (!ETH_BRIDGE_ADDRESS) { setStatus("❌ Bridge contract address not specified."); return; } if (!TOKEN_ADDRESS) { setStatus("❌ Token contract address not specified."); return; } if (!account) { setStatus("❗ Wallet not connected."); return; } if (!amount || isNaN(amount) || parseFloat(amount) <= 0) { setStatus("❗ Please enter a valid token amount."); return; } if (!targetAddress || !web3.utils.isAddress(targetAddress)) { setStatus("❗ Please enter a valid BSC wallet address."); return; } try { const weiAmount = web3.utils.toWei(amount.toString(), "ether"); const token = new web3.eth.Contract(ERC20_ABI, TOKEN_ADDRESS); const bridge = new web3.eth.Contract(ETH_BRIDGE_ABI, ETH_BRIDGE_ADDRESS); const checksumAddress = web3.utils.toChecksumAddress(targetAddress); setStatus("🔃 Approving token transfer..."); await token.methods.approve(ETH_BRIDGE_ADDRESS, weiAmount).send({ from: account }); setStatus("🔐 Locking tokens on Sepolia..."); await bridge.methods.lockTokens(weiAmount, checksumAddress).send({ from: account }); setStatus("✅ Tokens locked. Mint will happen on BSC."); setAmount(""); loadBalance(); } catch (err) { console.error("Bridge error:", err); setStatus("❌ Transaction failed: " + (err?.message || "Unknown error")); } }; useEffect(() => { connectWallet(); }, []); useEffect(() => { if (web3 && account) loadBalance(); }, [web3, account]); return ( <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-900 to-black text-white px-4 py-10"> <div className="bg-gray-900 border border-gray-700 shadow-2xl rounded-3xl max-w-md w-full p-8 space-y-6"> <h1 className="text-3xl font-bold text-center text-blue-400">🌉 ETH ➝ BSC Bridge</h1> <p className="text-center text-gray-400 text-sm"> Bridge your <span className="text-yellow-300 font-bold">BRG</span> tokens from <strong>Ethereum Sepolia</strong> to <strong>BSC Testnet</strong>. </p> <div className="text-center text-green-300 font-mono text-sm">💰 BRG Balance: {tokenBalance}</div> <input type="number" min="0" step="any" placeholder="Amount to bridge" value={amount} onChange={(e) => setAmount(e.target.value)} className="w-full p-3 rounded-lg bg-gray-800 border border-gray-600 placeholder-gray-500 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" /> <input type="text" placeholder="Your BSC wallet address" value={targetAddress} onChange={(e) => setTargetAddress(e.target.value)} className="w-full p-3 rounded-lg bg-gray-800 border border-gray-600 placeholder-gray-500 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" /> <button onClick={handleBridge} className="w-full py-3 bg-gradient-to-r from-green-500 to-blue-600 hover:from-green-600 hover:to-blue-700 font-bold rounded-xl shadow-md transition" > 🔐 Lock & Bridge </button> {status && ( <div className="text-center mt-4 text-yellow-300 font-mono text-sm"> {status} </div> )} <div className="text-xs text-center text-gray-500 pt-4 border-t border-gray-700"> Connected:{" "} <span className="text-gray-300 font-mono"> {account ? `${account.slice(0, 6)}...${account.slice(-4)}` : "Not connected"} </span> </div> </div> </div> ); } export default App; import React, { useEffect, useState } from "react"; import Web3 from "web3"; import detectEthereumProvider from "@metamask/detect-provider"; import { ETH_BRIDGE_ABI, ERC20_ABI, ETH_BRIDGE_ADDRESS, TOKEN_ADDRESS, } from "./bridge"; function App() { const [web3, setWeb3] = useState(null); const [account, setAccount] = useState(""); const [tokenBalance, setTokenBalance] = useState("0"); const [amount, setAmount] = useState(""); const [targetAddress, setTargetAddress] = useState(""); const [status, setStatus] = useState(""); // Debugging: Log env variables console.log("ENV ETH_BRIDGE_ADDRESS:", ETH_BRIDGE_ADDRESS); console.log("ENV TOKEN_ADDRESS:", TOKEN_ADDRESS); // Verify addresses on mount useEffect(() => { if (!ETH_BRIDGE_ADDRESS || !TOKEN_ADDRESS) { setStatus( "❌ Contract address not specified. Please check your .env file." ); } }, []); const connectWallet = async () => { const provider = await detectEthereumProvider(); if (!provider) { setStatus("❌ MetaMask not detected."); return; } try { await provider.request({ method: "eth_requestAccounts" }); const web3Instance = new Web3(provider); setWeb3(web3Instance); const accounts = await web3Instance.eth.getAccounts(); setAccount(accounts[0]); const chainId = await provider.request({ method: "eth_chainId" }); if (chainId !== "0xaa36a7") { setStatus("❌ Please switch to Ethereum Sepolia."); } else { setStatus("✅ Connected to Ethereum Sepolia."); } } catch (err) { console.error("MetaMask connection error:", err); setStatus("❌ Failed to connect MetaMask."); } }; const loadBalance = async () => { if (!web3 || !account) return; try { if (!TOKEN_ADDRESS) { setStatus("❌ Token contract address missing."); return; } const token = new web3.eth.Contract(ERC20_ABI, TOKEN_ADDRESS); const raw = await token.methods.balanceOf(account).call(); const formatted = web3.utils.fromWei(raw, "ether"); setTokenBalance(formatted); } catch (err) { console.error("Balance error:", err); setStatus("❌ Failed to fetch BRG balance."); } }; const handleBridge = async () => { setStatus(""); if (!ETH_BRIDGE_ADDRESS) { setStatus("❌ Bridge contract address not specified."); return; } if (!TOKEN_ADDRESS) { setStatus("❌ Token contract address not specified."); return; } if (!account) { setStatus("❗ Wallet not connected."); return; } if (!amount || isNaN(amount) || parseFloat(amount) <= 0) { setStatus("❗ Please enter a valid token amount."); return; } if (!targetAddress || !web3.utils.isAddress(targetAddress)) { setStatus("❗ Please enter a valid BSC wallet address."); return; } try { const weiAmount = web3.utils.toWei(amount.toString(), "ether"); const token = new web3.eth.Contract(ERC20_ABI, TOKEN_ADDRESS); const bridge = new web3.eth.Contract(ETH_BRIDGE_ABI, ETH_BRIDGE_ADDRESS); const checksumAddress = web3.utils.toChecksumAddress(targetAddress); setStatus("🔃 Approving token transfer..."); await token.methods.approve(ETH_BRIDGE_ADDRESS, weiAmount).send({ from: account }); setStatus("🔐 Locking tokens on Sepolia..."); await bridge.methods.lockTokens(weiAmount, checksumAddress).send({ from: account }); setStatus("✅ Tokens locked. Mint will happen on BSC."); setAmount(""); loadBalance(); } catch (err) { console.error("Bridge error:", err); setStatus("❌ Transaction failed: " + (err?.message || "Unknown error")); } }; useEffect(() => { connectWallet(); }, []); useEffect(() => { if (web3 && account) loadBalance(); }, [web3, account]); return ( <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-900 to-black text-white px-4 py-10"> <div className="bg-gray-900 border border-gray-700 shadow-2xl rounded-3xl max-w-md w-full p-8 space-y-6"> <h1 className="text-3xl font-bold text-center text-blue-400">🌉 ETH ➝ BSC Bridge</h1> <p className="text-center text-gray-400 text-sm"> Bridge your <span className="text-yellow-300 font-bold">BRG</span> tokens from <strong>Ethereum Sepolia</strong> to <strong>BSC Testnet</strong>. </p> <div className="text-center text-green-300 font-mono text-sm">💰 BRG Balance: {tokenBalance}</div> <input type="number" min="0" step="any" placeholder="Amount to bridge" value={amount} onChange={(e) => setAmount(e.target.value)} className="w-full p-3 rounded-lg bg-gray-800 border border-gray-600 placeholder-gray-500 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" /> <input type="text" placeholder="Your BSC wallet address" value={targetAddress} onChange={(e) => setTargetAddress(e.target.value)} className="w-full p-3 rounded-lg bg-gray-800 border border-gray-600 placeholder-gray-500 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" /> <button onClick={handleBridge} className="w-full py-3 bg-gradient-to-r from-green-500 to-blue-600 hover:from-green-600 hover:to-blue-700 font-bold rounded-xl shadow-md transition" > 🔐 Lock & Bridge </button> {status && ( <div className="text-center mt-4 text-yellow-300 font-mono text-sm"> {status} </div> )} <div className="text-xs text-center text-gray-500 pt-4 border-t border-gray-700"> Connected:{" "} <span className="text-gray-300 font-mono"> {account ? `${account.slice(0, 6)}...${account.slice(-4)}` : "Not connected"} </span> </div> </div> </div> ); } export default App; 3️⃣Create/Check index.html in frontend directory. 3️⃣Create/Check index.html in frontend directory. Make sure your index.html looks like this: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Token Bridge dApp</title> </head> <body class="bg-black text-white"> <div id="root"></div> <script type="module" src="/src/main.jsx"></script> </body> </html> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Token Bridge dApp</title> </head> <body class="bg-black text-white"> <div id="root"></div> <script type="module" src="/src/main.jsx"></script> </body> </html> 4️⃣Create main.jsx (or main.js) if using React in frontend/src directory: 4️⃣Create main.jsx (or main.js) if using React in frontend/src directory: import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App.jsx'; import './index.css'; ReactDOM.createRoot(document.getElementById('root')).render( <React.StrictMode> <App /> </React.StrictMode> ); import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App.jsx'; import './index.css'; ReactDOM.createRoot(document.getElementById('root')).render( <React.StrictMode> <App /> </React.StrictMode> ); Then edit Edit tailwind.config.js in the frontend directory: tailwind.config.js /** @type {import('tailwindcss').Config} */ export default { content: [ "./index.html", "./src/**/*.{js,ts,jsx,tsx}", ], theme: { extend: {}, }, plugins: [], } /** @type {import('tailwindcss').Config} */ export default { content: [ "./index.html", "./src/**/*.{js,ts,jsx,tsx}", ], theme: { extend: {}, }, plugins: [], } Don’t forget to paste the following code in frontend/src/index.css for the better styling. frontend/src/index.css @tailwind base; @tailwind components; @tailwind utilities; @tailwind base; @tailwind components; @tailwind utilities; 5️⃣Install dependencies 5️⃣Install dependencies Install React, ReactDOM and @metamask/detect-provider in the frontend directory by running the following commands in the terminal: @metamask/detect-provider npm install react react-dom npm install @metamask/detect-provider npm install react react-dom npm install @metamask/detect-provider After that, start the dev server: npm run dev npm run dev You will see your server starting at http://localhost:5173/. Once you visit the URL, dApp automatically ask you to connect MetaMask wallet by triggering a pop-up window. http://localhost:5173/ Once you connect wallet, you see the frontend of dApp exhibiting total BRG tokens and wallet connected status. You can check the following GIF of “Lock & Bridge” testing process. Lock & Bridge Once your transactions are confirmed, you see a notification from MetMask. However, one BRG token is located in the Ethereum Sepolia network, as can be seen in the Sepolia test net explorer. You will notice a message like , “✅ Tokens locked. Mint occurs in the BSC. How to check it in BSC.” Sepolia test net explorer The next step is crucial. This time, we have to run relayer from the backend directory using the following command. node index.js node index.js As you can see, 1 BRG token has been successfully minted on BSC testnet. You can track the transaction in BSC explorer here. BSC explorer here Finally, our 1 BRG token from Ethereum network is transferred to BSC testnet. You can find source code on GitHub here. GitHub here 🧪Final Testing, Deployment Guide & Advanced Enhancements 🧪Final Testing, Deployment Guide & Advanced Enhancements ✅ End-to-End Token Bridge Flow ✅ End-to-End Token Bridge Flow Step Action Chain Actor 1 User connects MetaMask BSC dApp 2 Approves + locks tokens BSC dApp (user signs) 3 TokenLocked emitted BSC Smart contract 4 Relayer catches event Node.js Backend 5 Relayer mints tokens Ethereum Calls smart contract 6 User sees tokens Ethereum On-chain Step Action Chain Actor 1 User connects MetaMask BSC dApp 2 Approves + locks tokens BSC dApp (user signs) 3 TokenLocked emitted BSC Smart contract 4 Relayer catches event Node.js Backend 5 Relayer mints tokens Ethereum Calls smart contract 6 User sees tokens Ethereum On-chain Step Action Chain Actor Step Step Step Action Action Action Chain Chain Chain Actor Actor Actor 1 User connects MetaMask BSC dApp 1 1 User connects MetaMask User connects MetaMask BSC BSC dApp dApp 2 Approves + locks tokens BSC dApp (user signs) 2 2 Approves + locks tokens Approves + locks tokens BSC BSC dApp (user signs) dApp (user signs) 3 TokenLocked emitted BSC Smart contract 3 3 TokenLocked emitted TokenLocked emitted BSC BSC Smart contract Smart contract 4 Relayer catches event Node.js Backend 4 4 Relayer catches event Relayer catches event Node.js Node.js Backend Backend 5 Relayer mints tokens Ethereum Calls smart contract 5 5 Relayer mints tokens Relayer mints tokens Ethereum Ethereum Calls smart contract Calls smart contract 6 User sees tokens Ethereum On-chain 6 6 User sees tokens User sees tokens Ethereum Ethereum On-chain On-chain During the test, make sure to have a look at the following checklist. ✅Manual Test Checklist ✅Manual Test Checklist 👉Preparation: 👉Preparation: Deploy both tokens and bridge contracts, as discussed above. Fund your wallet on: BSC testnet faucet Ethereum testnet faucet Configure .env in both root and /frontend Deploy both tokens and bridge contracts, as discussed above. Fund your wallet on: BSC testnet faucet Ethereum testnet faucet BSC testnet faucet Ethereum testnet faucet BSC testnet faucet Ethereum testnet faucet Configure .env in both root and /frontend /frontend ✅ Test Steps: ✅ Test Steps: # Test What to Expect 1 Launch dApp MetaMask connects 2 Approve & Lock 1 BRG (This is our test token) Tx completes, event logged 3 Backend prints log TokenLocked event 4 Tokens minted on Ethereum TokenMinted event 5 Check Ethereum token balance Matches bridged amount # Test What to Expect 1 Launch dApp MetaMask connects 2 Approve & Lock 1 BRG (This is our test token) Tx completes, event logged 3 Backend prints log TokenLocked event 4 Tokens minted on Ethereum TokenMinted event 5 Check Ethereum token balance Matches bridged amount # Test What to Expect # # # Test Test Test What to Expect What to Expect What to Expect 1 Launch dApp MetaMask connects 1 1 Launch dApp Launch dApp MetaMask connects MetaMask connects 2 Approve & Lock 1 BRG (This is our test token) Tx completes, event logged 2 2 Approve & Lock 1 BRG (This is our test token) Approve & Lock 1 BRG (This is our test token) Tx completes, event logged Tx completes, event logged 3 Backend prints log TokenLocked event 3 3 Backend prints log Backend prints log TokenLocked event TokenLocked event 4 Tokens minted on Ethereum TokenMinted event 4 4 Tokens minted on Ethereum Tokens minted on Ethereum TokenMinted event TokenMinted event 5 Check Ethereum token balance Matches bridged amount 5 5 Check Ethereum token balance Check Ethereum token balance Matches bridged amount Matches bridged amount ✅ You can check Ethereum and BSC balances using a custom block explorer by inputting your address. ✅ You can check Ethereum and BSC balances using a custom block explorer by inputting your address. 🧠 3. Advanced Features & Security Improvements 🧠 3. Advanced Features & Security Improvements Well, the demonstration so far in this tutorial was to safe to test the dApp in the testnet environment of the blockchain. If you are serious and want to go further, here are a few tips for you. 1️⃣ Nonce & Replay Protection 1️⃣ Nonce & Replay Protection Add a mapping of processed tx hashes in the backend to prevent the reprocessing of the same event: mapping of processed tx hashes const processedTxs = new Set(); // Use DB in production if (processedTxs.has(event.transactionHash)) return; processedTxs.add(event.transactionHash); const processedTxs = new Set(); // Use DB in production if (processedTxs.has(event.transactionHash)) return; processedTxs.add(event.transactionHash); 2️⃣ Event Indexing with GraphQL (Optional) 2️⃣ Event Indexing with GraphQL (Optional) You can use The Graph (when BSC indexing is needed) or integrate GetBlock’s webhooks for efficient event catching instead of polling. The Graph 3️⃣ Signature Verification for Minting (zkBridge-style) 3️⃣ Signature Verification for Minting (zkBridge-style) During production, the Ethereum Bridge should not trust the relayer blindly. Instead: not trust the relayer blindly Lock emits a message hash User signs that message off-chain Submit signature to mint (optional layer) Lock emits a message hash User signs that message off-chain Submit signature to mint (optional layer) 4️⃣ Support Multiple Chains 4️⃣ Support Multiple Chains You can make this bridge multichain using multichain Chain IDs for tracking bridge source/destination Adding multiple token contracts Abstracting the relayer to handle many chains (Polygon, ETH Sepolia, etc.) Chain IDs for tracking bridge source/destination Adding multiple token contracts Abstracting the relayer to handle many chains (Polygon, ETH Sepolia, etc.) 🚀 Deployment to Production (Optional) 🚀 Deployment to Production (Optional) If you are able to test the dApp in the testnet it is not hard for you to go mainnet-based dApp which you can deploy for the production. Here are some cool deployment options for you. Component Deployment Option Frontend UI Vercel / Netlify / Cloudflare Pages Backend Railway / Render / VPS Smart Contracts Deployed already via Hardhat on GetBlock RPC RPC Gateways GetBlock Dashboard Component Deployment Option Frontend UI Vercel / Netlify / Cloudflare Pages Backend Railway / Render / VPS Smart Contracts Deployed already via Hardhat on GetBlock RPC RPC Gateways GetBlock Dashboard Component Deployment Option Component Component Component Deployment Option Deployment Option Deployment Option Frontend UI Vercel / Netlify / Cloudflare Pages Frontend UI Frontend UI Vercel / Netlify / Cloudflare Pages Vercel / Netlify / Cloudflare Pages Backend Railway / Render / VPS Backend Backend Railway / Render / VPS Railway / Render / VPS Smart Contracts Deployed already via Hardhat on GetBlock RPC Smart Contracts Smart Contracts Deployed already via Hardhat on GetBlock RPC Deployed already via Hardhat on GetBlock RPC RPC Gateways GetBlock Dashboard RPC Gateways RPC Gateways GetBlock Dashboard GetBlock Dashboard GitHub actions are used to automatically trigger deployments from commits. GitHub actions are used to automatically trigger deployments from commits. GitHub actions 📥Conclusion: Summary of Completed Bridge dApp Using APIs 📥Conclusion: Summary of Completed Bridge dApp Using APIs GetBlocks offers battle-tested blockchain APIs. We have also attempted to prove this to some extent in this tutorial. To create new Web3 applications that are scalable and user-friendly, there is a need to overcome the constraints of public infrastructure. The blockchain API layer is not plumbing; it is the spinal cord of the dApp. It is essential to understand the engineering complexities that lie behind quick and dependable APIs, including node optimization and worldwide distribution, intelligent load balancing, and persistent caching. Putting their faith in a provider such as GetBlock, who has made investments at all these infrastructure levels and provides dedicated nodes, multi-chain support, and the reliability expected of multinational enterprises, is no longer a luxury but a strategic move on the part of dApps serious about performance, scalability, and customer retention. By paying appropriate attention to these strata, developers will be able to ensure that innovative applications are developed on a bulletproof infrastructure, which will meet the needs of the second surge of Web3 adoption. Its future is rapid, consistent, and established on elegant infrastructure, and it is important to select its API partner. We believe that you have successfully performed the following checklist: ✅ ERC-20 Tokendeployed on BSC + ETH✅Bridge contractsfor locking and minting✅Node.js backend relayerto bridge assets✅React frontend dAppwith wallet integration✅GetBlock endpointsused for seamless public testnet connection✅Production-ready architecture with upgrade paths for security & scaling ERC-20 Token Bridge contracts Node.js backend relayer React frontend dApp GetBlock endpoints Production-ready architecture If you have any question while testing this dApp, comment below. Thank You 🙏 !