メインコンテンツにスキップ
Time to read: 1 min

Interacting with Rootstock using Viem

Viem offers a minimalist, lightweight, and more efficient Typescript interface alternative to Ethers.js and Web3.js for interacting with Ethereum nodes. It maintains a bundle size of 35KB compared to Ethers and Web3.js, which are over 300KB and 600KB, respectively. Smaller bundle size implies faster page loads and better performance on mobile. Viem also offers more granular control over APIs in contrast to Ethers.js, which abstracts APIs for more functionalities.

This tutorial will teach you how to use Viem to interact with your contracts on Rootstock.

Installation and Setup

In this guide, you will learn how to interact with an existing lending contract on the Rootstock Explorer, rLending RBTC. The full code of the working demo is available on GitHub.

You can use the following command to install the Viem npm package:

npm install viem

For more installation options, you can check out the official installation guide.

Using the Public Client

You need to set up your Client in Viem, which would have been your Provider in a typical Ethers.js project. A Client allows you to access Viem actions for interacting with the blockchain. One type of Client is the Public Client which provides access to public actions like the getBlockNumber and getBalance. The following snippet indicates how you would use the Public Client in an existing project.

import { createPublicClient, http } from 'viem'
import { rootstockTestnet } from 'viem/chains';

const RPC_URL = process.env.RPC_URL;

const publicClient = createPublicClient({
chain: rootstockTestnet,
transport: http(RPC_URL),
})

In the above code:

  • You need to define a Chain and Transport for Rootstock-specific values.
  • Chain values for the Rootstock chain could be rootstock for the mainnet or rootstockTestnet for the Testnet
  • It is recommended to set your RPC_URL as an environmental variable whose value can be obtained from the Rootstock RPC API dashboard.
RPC_URL='https://rpc.testnet.rootstock.io/<API_KEY>'
CONTRACT_ADDRESS='0x...' // You can also add the contract address as an environment variable

NB: If you are working on a Next.js project, environment variables must be prepended with NEXT_PUBLIC_ as NEXT_PUBLIC_RPC_URL for the RPC URL variable.

To access a deployed contract with its ABI using the Viem's getContract method.

import { profileContractAbi } from './contractABI';

const lendingContract = getContract({
abi: profileContractAbi,
address: getAddress(CONTRACT_ADDRESS),
client: publicClient
})

Then, read the borrow rate:

export async function fetchBorrowRatePerBlock(): Promise<bigint> {
const result = await publicClient.readContract({
address: getAddress(CONTRACT_ADDRESS),
abi: profileContractAbi,
functionName: 'borrowRatePerBlock',
})
return result as bigint;
}

You would get a response like the following in bigint data type:

67627655645n

To use some other read contract functions like fetchTotalSupply and fetchTotalBorrows:

export async function fetchTotalSupply(): Promise<bigint> {
return await lendingContract.read.totalSupply() as bigint;
}

export async function fetchTotalBorrows(): Promise<bigint> {
return await lendingContract.read.totalBorrows() as bigint;
}

export async function fetchSymbol(): Promise<string> {
return await lendingContract.read.symbol() as string;
}

Using the Wallet Client

You can also use the wallet client to access the user account and public address using the createWalletClient.

import { createWalletClient } from 'viem';

if (typeof window === 'undefined' || typeof window.ethereum === 'undefined') {
throw new Error('MetaMask not detected');
}

export const walletClient = createWalletClient({
chain: rootstockTestnet,
transport: custom(window.ethereum!)
})

const [walletAddresses] = await walletClient.getAddresses()

You have to explicitly make TypeScript aware that window.ethereum exists. It is not part of the standard Window library but is injected by browser wallets like MetaMask. Create a file called global.d.ts in the utils folder and add the following code to it.

interface Window {
ethereum?: any
}

Then, you can use the writeContract method to make state-changing function calls like the borrow function to borrow tokens from the lending protocol:

export async function borrow(amount: bigint): Promise<`0x${string}`> {
if (!walletClient) throw new Error("Wallet client not available.");
const account = await getConnectedAccount();

const txHash = await walletClient.writeContract({
address: CONTRACT_ADDRESS,
abi: profileContractAbi,
functionName: 'borrow',
args: [amount],
account,
});

return txHash;
}

Simulating Transactions

It helps to do a dry run of your state-changing functions first to catch errors and see if something fails before the real transaction. You can use the simulateContract() feature, which does not spend gas. Performing a simulation prevents sending a transaction that could cost more gas fees. It also helps you to detect and return human-readable revert reasons if provided in the smart contract via require() or revert(). The following code snippet shows how to simulate a transaction before performing the actual transaction.

// Simulate transaction
const { request } = await publicClient.simulateContract({
address: getAddress(CONTRACT_ADDRESS),
abi: profileContractAbi,
functionName: 'borrow',
args: [amount],
account,
});

// Send real transaction only if simulation succeeded
const txHash = await walletClient.writeContract(request);

return txHash;

In the above code snippet, a transaction was simulated with the smart contract Abi which you have to provide in your project. Then, a real transaction is allowed to happen after a successful simulation. We used another type of client in the snippet, the Wallet Client. The Wallet Client allows you to use wallet actions like signing a message or sending a transaction.

The simulated transaction looks like the following:

{
"abi": [
{
"constant": false,
"inputs": [
{
"internalType": "uint256",
"name": "borrowAmount",
"type": "uint256"
}
],
"name": "borrow",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
}
],
"address": "0xc19F0882bf318C9f8767C7d520018888E878417b",
"args": [
"100000000000000000"
],
"functionName": "borrow",
"account": {
"address": "0xfcdF314daed7E8c39E8591870e20A8c25D138a5C",
"type": "json-rpc"
}
}

Then, the returned transaction hash is in the following form:

0x61c7f5cc54dd2c9cfa10c10eb50f212c40ba62d78e63c683b9e9d0db20d9630b

Resources:

最終更新 作成者: jkaylight

フィードバック