Understanding MetaMask RPC methods and errors
MetaMask is a popular cryptocurrency wallet that sends, receives, and signs transactions on the blockchain. It is the preferred cryptocurrency wallet used by web3 developers due to its flexible, cutting-edge, and innovative features.
As a developer working with MetaMask and its RPC, you must have already noticed that catching MetaMask errors isn’t possible with a try-and-catch block. A situation that can be frustrating, especially when you can see the error clearly in your browser console.
Before we go further into catching these errors, we must first understand what exactly an RPC is and how MetaMask uses it for its methods.
What is an RPC?
A Remote Procedure Call (RPC) is an action-oriented protocol that interacts with external systems and allows an application to execute programs in a different location.
In the case of web3, an RPC allows an application to access blockchain data, create transactions, and interact with smart contract functions through a server node. Wallets like MetaMask have an internal RPC built in known as a JSON-RPC. It is the middleware between front-end applications and smart contract functions. This type of RPC is a stateless and lightweight protocol used to agnostically transport data over WebSockets or HTTP.
MetaMask RPC methods
When users download the MetaMask extension, the wallet injects the Ethereum object into the browser window (window.ethereum). The object contains all the RPC methods of MetaMask, which are isConnected
, on
, and request
.
ethereum.isConnected
: This method checks the connection status between MetaMask's node server and the blockchain. It returns a boolean value. If it returns true, the node server can make RPC requests to the Ethereum or other Ethereum Virtual Machine (EVM) compatible chain.ethereum.on
: Metamask implements Node.js' event emitter API into this method to track blockchain events, chain changes, and user account changes. The method uses arguments containing the event's name to track and a function containing what your React application does when the event happens. Some of MetaMask's default events are "accountsChanged":
window.ethereum.on('accountsChanged',(accounts)=>{
console.log("You switched your account to: ", accounts[0])
})
And “chainChanged”:
window.ethereum.on('chainChanged',(chains) => {
console.log("You switched chains to: ", chains[0])
})
ethereum.request
: This method contains MetaMask’s RPC API and exposes its caller to external methods of the node server connected to MetaMask. It takes a single JSON object containing an external method to call and its parameters (params).
window.ethereum.request({
method: 'eth_sendTransaction',
params: [
{
from: '0x21ab...', //sender address
to: '0x1432ba...', //receiver address
gas: '0x5208', //hex representation of 21000
gasPrice: '0x9184e72a000', // hex of 1e13 wei
value: '0xDE0B6B3A7640000', // hex for 1e18 wei or 1 Ether
}
]
})
Calling a function
Calling the function of a smart contract can be done with one of MetaMask’s RPC methods. However, a better way to call functions and establish a connection with the MetaMask wallet and other wallets is by using web3 library packages such as web3modal and ether.js. To install these packages, run the following command in your terminal:
npm install --save web3modal ethers
We will call a function from a basic erc20 smart contract called “approve”. This smart contract function enables us to give a third-party address permission to use erc20 tokens on our behalf. But first, let’s get the ABI. The Application Binary Interface (ABI) is the standard way of interacting with smart contracts on the Ethereum network.
The ABI for the “approve” function looks like function approve(address _spender, uint256 _value) public returns (bool success)
.
Now that we have the ABI, we can easily call the function with the help of the installed web3modal and ethers dependencies.
The code below establishes a secure connection to MetaMask with a cache provider from web3modal. By doing this, whenever the page gets refreshed, the app remains connected to MetaMask. The secure connection is wrapped into ethers and is used to call the transaction signer (owner of the MetaMask wallet).
import Web3Modal from "web3modal";
import { ethers } from "ethers";
export const Approve = () => {
const abi = ["function approve(address _spender, uint256 value) public returns (bool success)",];
const usdtAddress = "0xdac17f958d2ee523a2206206994597c13d831ec7";
const thirdPartyAddress = "0xdac17f958d2ee523a2206206994597c13d831ec7";
const web3Modal =
typeof window !== "undefined" && new Web3Modal({ cacheProvider: true });
async function handleApproveSubmit() {
const connection = web3Modal && (await web3Modal.connect());
const provider = new ethers.providers.Web3Provider(connection);
const contract = new ethers.Contract(
usdtAddress,
abi,
provider.getSigner()
);
contract.approve(thirdPartyAddress, 20);
}
return <button onClick={handleApproveSubmit}>Approve</button>;
};
The handleApproveSubmit()
function will open up MetaMask with a transaction to be signed.
Session Replay for Developers
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — an open-source session replay suite for developers. It can be self-hosted in minutes, giving you complete control over your customer data
Happy debugging! Try using OpenReplay today.
Catching function call errors
A try-and-catch block is sufficient to catch all errors from calling the smart contract function.
try{
const connection = web3Modal && (await web3Modal.connect());
const provider = new ethers.providers.Web3Provider(connection);
const contract = new ethers.Contract(
usdtAddress,
abi,
provider.getSigner()
);
contract.approve(thirdPartyAddress, 20);
}
catch(error){
console.log(error.message);
}
However, it is unable to detect errors from MetaMask’s RPC.
Catching MetaMask errors
To catch MetaMask errors, we have to add a promise after the function call. This promise anticipates the hash of the transaction about to be broadcast to the blockchain. If the transaction gets broadcasted successfully, let us log a success message and the transaction hash into our console; if not, let us log the error.
contract.approve(thirdPartyAddress, 20)
.then((tx) => {
provider.waitForTransaction(tx.hash)
.then(()=>{
console.log("success");
console.log(tx.hash);
})
})
.catch((error) => {
console.log(error.message);
})
This approach catches all possible errors from MetaMask’s RPC, including 4001: “User Denied Transaction Signature”.
Conclusion
With the approach covered in this article, you can handle MetaMask errors more efficiently in your React application. The approach also applies to other wallets with similar RPCs, like TrustWallet, Exodus, or MyEtherWallet(MEW).
Originally published at blog.openreplay.com on January 24, 2023.