Every blockchain developer should know these Web3 and Metamask use cases

As a blockchain developer, sooner or later you‘ll need to integrate your decentralized application (dApp) with a browser-based Ethereum wallet like Metamask. Metamask is hugely popular as a way for users to interact with dApps right from their web browser. It injects a Web3 object into the window, allowing your JavaScript code to interact with the blockchain.

In this article, we‘ll walk through 10 essential use cases every blockchain developer needs to know when working with Web3 and Metamask. While the specific code will vary based on your dApp, the core concepts are relevant for any Web3 project. Let‘s dive in!

#1: Detecting Metamask and Instantiating Web3

The first step is to detect whether the user has Metamask installed and create a Web3 instance:

if (typeof window.ethereum !== ‘undefined‘) {
  console.log(‘Metamask detected‘)
  const web3 = new Web3(window.ethereum)
} else {
  console.error(‘Please install Metamask‘)
}

Here we check for the window.ethereum object that Metamask injects. If it‘s available, we know Metamask is installed so we create a Web3 instance using their provider. We want to use our version of Web3, not the outdated version injected by Metamask itself.

It‘s also good practice to handle cases where Metamask is not installed, directing the user to install it before using the dApp.

#2: Checking if Metamask is Locked

Even if Metamask is installed, the user may not be logged in. We must ensure Metamask is unlocked before trying to access user accounts or send transactions:

const isUnlocked = await web3.eth.getAccounts()
  .then(accounts => accounts.length > 0)
  .catch(e => {
    console.error(‘Failed to get accounts‘, e)
    return false
  })

if (!isUnlocked) {
  alert(‘Please unlock Metamask first‘)
}  

We simply try to retrieve the list of accounts. If we get an array with at least one account, then we know Metamask is unlocked. If the call fails or the array is empty, then it‘s locked. Be sure to handle any errors that occur.

#3: Checking the Current Network

Metamask allows users to connect to various networks, like the main Ethereum network or one of many test nets. Your dApp likely lives on one specific network, so you need to verify the user is connected to the correct one:

const chainId = await web3.eth.net.getId()
const expectedChainId = 1 // Ethereum mainnet

if (chainId !== expectedChainId) {
  alert(‘Please switch to the Ethereum mainnet in Metamask‘)
}

First we get the chain ID of the currently active network using web3.eth.net.getId(). Then we compare it to the expected chain ID for our dApp (Ethereum mainnet in this example). If they don‘t match, we prompt the user to switch networks.

It‘s essential to do this network check whenever the user loads or reloads your dApp. You don‘t want them accidentally performing actions on the wrong network.

#4: Getting the Current Account

The first account in the user‘s Metamask is considered their "active" account and is the one your dApp should interact with:

const accounts = await web3.eth.getAccounts()
const userAccount = accounts[0]

After confirming Metamask is unlocked, we can safely access web3.eth.getAccounts() which returns an array of account addresses. We grab the first one to get the user‘s active account.

Note that you should not cache or store this account address. Always retrieve it from Web3 whenever you need it, because the user could change their active account at any time!

#5: Getting the Account Balance

A common task is retrieving the ETH balance of the user‘s account:

const weiBalance = await web3.eth.getBalance(userAccount)
const ethBalance = web3.utils.fromWei(weiBalance, ‘ether‘)

console.log(`Account balance: ${ethBalance} ETH`) 

We pass the user‘s account address to web.eth.getBalance() to get their balance in wei (the smallest unit of ETH). To convert the balance to a more readable number of whole ETH, we use the utility function web3.utils.fromWei().

This is a simple way to display the user‘s current balance in your dApp‘s UI. Just remember balances can change as the user sends and receives ETH, so be sure to update it periodically or after key events.

#6: Detecting Account Changes

As mentioned earlier, the user can change their active account in Metamask at any time. We want to detect this and react accordingly:

function handleAccountChange(accounts) {
  if (accounts.length === 0) {
     // Metamask is locked or no accounts connected
     alert(‘Please connect to Metamask‘)
  } else if (accounts[0] !== userAccount) {
    userAccount = accounts[0]
    // Update UI, fetch new data, etc
  } 
}

window.ethereum.on(‘accountsChanged‘, handleAccountChange)

Metamask emits an accountsChanged event whenever the user‘s accounts change (new account imported, account removed, account switch). We listen for this event and define our handleAccountChange logic.

If the new accounts array is empty, that means Metamask was locked or the user disconnected all accounts. If the first account is different from userAccount, that means they switched to a different account. When this happens we update userAccount and perform any necessary UI updates and data fetching to reflect the new active account.

#7: Detecting when Metamask is Locked/Unlocked

The user might also decide to lock or unlock their Metamask wallet while using your dApp. You‘ll want to detect this and pause/resume activity accordingly:

function handleLockChange(isUnlocked) {
  if (isUnlocked) {
    console.log(‘Metamask is unlocked‘)
    // Re-instantiate web3 and/or get user accounts, resume app activity
  } else {
    console.log(‘Metamask is locked‘)
    // Clear user account, pause activity, display lock message
  }
}

window.ethereum.on(‘lock‘, () => handleLockChange(false))
window.ethereum.on(‘unlock‘, () => handleLockChange(true))

Here we define listeners for the lock and unlock events emitted by Metamask. Locking triggers our handler with isUnlocked set to false, while unlocking sets it to true.

When Metamask gets locked, the general practice is to pause any dApp activity, clear any stored user data (like accounts), and display a prompt for the user to log back in. When Metamask unlocks again, you can resume the dApp and refresh user data.

#8: Handling Transaction Confirmations

When you request a transaction signature from the user via Metamask, they‘ll be prompted with a confirmation popup showing the transaction details. You need to handle whether the user clicks confirm or cancel:

function handleConfirmation(err, txHash) {
  if (err) {
    if (err.message.includes(‘User denied transaction signature‘)) {
      console.log(‘User denied transaction, handle accordingly‘)
    } else {
      console.error(‘Error sending transaction‘, err)
    }
  } else {
    console.log(‘Transaction sent‘, txHash)
    // Update UI to pending state
  }
}

await web3.eth.sendTransaction(txData)
  .once(‘transactionHash‘, handleConfirmation)

We pass a callback function to web3.eth.sendTransaction() using the .once() listener for the transactionHash event. This callback gets invoked when the user approves or cancels the transaction.

If there‘s an error and the error message contains ‘User denied transaction signature‘, then we know the user clicked cancel. Otherwise it‘s a different error that we log.

If there‘s no error, the transaction was approved and sent! We get the transaction hash which we can display in the UI while waiting for the transaction to be mined and confirmed. It‘s important to update the UI immediately to a "pending" state so the user knows their action is being processed.

#9: Getting Transaction Receipts

After sending a transaction, you‘ll want to know when it has been successfully mined and confirmed on the blockchain. Unfortunately there‘s no push notification for this, we must poll for the transaction receipt:

async function getTransactionReceipt(txHash, interval = 5000) {
  try {
    const receipt = await web3.eth.getTransactionReceipt(txHash)
    if (receipt === null) {
      setTimeout(() => getTransactionReceipt(txHash, interval), interval)
    } else {
      // Receipt is available, transaction was mined
      return receipt
    }
  } catch (err) {
    console.error(‘Error getting transaction receipt‘, err)
  }
}

// Usage:
const receipt = await getTransactionReceipt(txHash)

Our getTransactionReceipt function takes a txHash and an optional polling interval (default 5 seconds). It calls web3.eth.getTransactionReceipt(txHash) to try retrieving the receipt.

If receipt is null, that means the transaction has not yet been mined, so we set a timeout to check again after the polling interval. If receipt exists, then the transaction was successfully mined and we return the receipt object.

The receipt contains data like the number of confirmations, gas used, logs emitted, and more. Once we have the receipt we can update our dApp‘s UI and state to reflect the completed action.

#10: Listening for Smart Contract Events

Most dApps interact with a smart contract in some way. Contracts can emit events when certain actions occur. We can subscribe to these events for a more responsive user experience:

const MyContract = new web3.eth.Contract(abi, contractAddress)

MyContract.events.MyEvent({}, (error, event) => {
  if (error) {
    console.error(‘Error on event‘, error)
    return
  }

  console.log(‘Event received‘, event)
  const eventData = event.returnValues
  // Update UI and state with event data
})
.on(‘error‘, console.error)

First we create a JavaScript object representing our contract using web3.eth.Contract and providing the contract‘s ABI and address.

Then we use the .events property to subscribe to a specific event by name, in this case MyEvent. The first argument is for optional event filters, which we leave empty. The second argument is our event handler callback.

This callback gets invoked whenever MyEvent is emitted by the contract. If there‘s an error we log it. Otherwise we get the event object which contains the event data in the returnValues property. We can use this data to update our dApp‘s state and UI reactively.

We also chain an additional .on(‘error‘) listener to log any Web3 subscription errors.

Listening to contract events this way is much more efficient and responsive than constantly polling for state changes. It‘s a best practice to use events wherever possible in your dApp.

Conclusion

Web3 and Metamask open the door for web developers to create interactive blockchain applications. But there are many important considerations to account for, from detecting accounts and networks to handling transaction lifecycles and events.

In this article we covered 10 essential use cases every blockchain developer should know when working with Web3 and Metamask. While the code examples here aren‘t an exhaustive reference, they demonstrate the key concepts you‘ll need to build robust dApps.

I encourage you to practice implementing these techniques in your own projects. Get familiar with the Web3 API reference and keep exploring more advanced topics like gas estimation, ENS resolution, and more. With a solid grasp of Web3 and Metamask interactions, you‘ll be ready to create the next generation of blockchain applications. Happy coding!

Similar Posts