Skip to main content

Interact with smart contracts

Interact with smart contracts in your JavaScript dapp. With MM Connect, you can:

  • Read data from smart contracts.
  • Write data to smart contracts.
  • Handle contract events.
  • Manage transaction states.
  • Handle contract errors.

The following examples demonstrate how to use MM Connect with viem, web3.js, ethers.js, Ethereum APIs, or Wagmi to interact with Solidity smart contracts.

This simple Hello World contract allows anyone to read and write a message to it.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract HelloWorld {

string public message;

constructor(string memory initMessage) {
message = initMessage;
}

function update(string memory newMessage) public {
message = newMessage;
}
}

Read from contracts

import { createEVMClient } from '@metamask/connect/evm'
import { createPublicClient, custom } from 'viem'
import { sepolia } from 'viem/chains'

const evmClient = createEVMClient()
const provider = evmClient.getProvider()

const publicClient = createPublicClient({
chain: sepolia,
transport: custom(provider),
})

const contractABI = [
{
inputs: [{ internalType: 'string', name: 'initMessage', type: 'string' }],
stateMutability: 'nonpayable',
type: 'constructor',
},
{
inputs: [],
name: 'message',
outputs: [{ internalType: 'string', name: '', type: 'string' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [{ internalType: 'string', name: 'newMessage', type: 'string' }],
name: 'update',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
]

const contractAddress = '0x8AA6820B3F197384874fAdb355361758258cb981' // On Sepolia testnet, replace with your contract address

// Read message from smart contract
const message = await publicClient.readContract({
address: contractAddress,
abi: contractABI,
functionName: 'message',
})

Batch contract reads

With Wagmi, you can perform multiple contract read operations using the useReadContracts hook. This hook batches contract calls internally, returning the results as an array. For example:

import { useReadContracts } from "wagmi";

// Example contract definitions with their address and ABI
const contractA = {
address: "0xContractAddress1",
abi: contractABI1,
} as const;

const contractB = {
address: "0xContractAddress2",
abi: contractABI2,
} as const;

function MyBatchReadComponent() {
const { data, isError, isLoading } = useReadContracts({
contracts: [
{
...contractA,
functionName: "getValueA",
},
{
...contractA,
functionName: "getValueB",
},
{
...contractB,
functionName: "getValueX",
args: [42],
},
{
...contractB,
functionName: "getValueY",
args: [42],
},
],
});

if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error fetching data.</div>;

return (
<div>
<p>getValueA: {data?.[0]?.toString()}</p>
<p>getValueB: {data?.[1]?.toString()}</p>
<p>getValueX: {data?.[2]?.toString()}</p>
<p>getValueY: {data?.[3]?.toString()}</p>
</div>
);
}

In this example, four contract read calls are batched together. The results are returned as an array in the same order as the calls, allowing you to process each result accordingly.

info

"Batching" can also refer to batching JSON-RPC requests using MM Connect's metamask_batch method, or sending atomic batch transactions in MetaMask.

Write to contracts

import { createEVMClient } from '@metamask/connect/evm'
import { createPublicClient, custom } from 'viem'
import { sepolia } from 'viem/chains'

const evmClient = createEVMClient()
const provider = evmClient.getProvider()

const contractABI = [
{
inputs: [{ internalType: 'string', name: 'initMessage', type: 'string' }],
stateMutability: 'nonpayable',
type: 'constructor',
},
{
inputs: [],
name: 'message',
outputs: [{ internalType: 'string', name: '', type: 'string' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [{ internalType: 'string', name: 'newMessage', type: 'string' }],
name: 'update',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
]
const publicClient = createPublicClient({
chain: sepolia,
transport: custom(provider),
})

const walletClient = createWalletClient({
chain: sepolia,
transport: custom(provider),
})

const contractAddress = '0x8AA6820B3F197384874fAdb355361758258cb981' // On Sepolia, replace with your contract address
const address = await walletClient.getAddresses()

// Submit transaction to the blockchain
const hash = await walletClient.writeContract({
account: address[0],
address: contractAddress,
abi: JSON.parse(JSON.stringify(contractABI)),
functionName: 'update',
args: ['NEW_MESSAGE'],
})

// Send transaction to smart contract to update message
const receipt = await publicClient.waitForTransactionReceipt({ hash })

Best practices

Follow these best practices when interacting with smart contracts.

Contract validation

  • Always verify contract addresses.
  • Double check ABI correctness.
  • Validate input data before sending.
  • Use typed data when possible (for example, using Viem).

Error handling

  • Handle common errors like user rejection and contract reverts.
  • Provide clear error messages to users.
  • Implement proper error recovery flows.
  • Consider gas estimation failures.

User experience

  • Show clear loading states.
  • Display transaction progress.
  • Provide confirmation feedback.
  • Enable proper error recovery.

Common errors

Error codeDescriptionSolution
4001User rejected transactionShow a retry option and a clear error message.
-32000Invalid inputValidate the input data before sending.
-32603Contract execution revertedCheck the contract conditions and handle the error gracefully.
-32002Request already pendingPrevent multiple concurrent transactions.

Next steps

See the following guides to add more functionality to your dapp: