Using Wagmi with ZKsync

Learn how to use the Wagmi toolkit to create websites that interact with contracts on ZKsync

This guide outlines how to use the wagmi library to create websites that interact with ZKsync contracts. We provide some examples to help you get started.

What you'll learn:

  • How to use wagmi to interact with ZKsync
  • How to fetch data from ZKsync contracts
  • How to send transactions to ZKsync contracts
GitHub

Tools:

  • - wagmi
guidewagmifrontend
Last Updated: May 09, 2024

The @wagmi/core library provides a comprehensive VanillaJS toolkit for interacting with ZKsync Era. It simplifies wallet connections, balance retrieval, message signing, and contract interactions. For setup instructions, consult the official documentation here.

Wagmi hooks do not yet support Paymasters and native Account Abstraction; development is in progress.

Project Setup

Installing Dependencies

npm install @wagmi/core @wagmi/connectors viem@2.x

For React projects, you can also make use of the wagmi package:

npm install wagmi viem@2.x @tanstack/react-query

Configuration

Make a file called wagmi-config.ts and configure your preferred ZKsync network.

wagmi-config.ts
import { http, createConfig } from '@wagmi/core';
import { zksync, zksyncSepoliaTestnet, zksyncInMemoryNode, zksyncLocalNode } from '@wagmi/core/chains';
import { walletConnect } from '@wagmi/connectors';

export const config = createConfig({
  chains: [zksync, zksyncSepoliaTestnet, zksyncInMemoryNode, zksyncLocalNode],
  connectors: [
    walletConnect({
      projectId: 'd4a7167a6eed6a53c8364631aaeca861',
    }),
  ],
  transports: {
    [zksync.id]: http(),
    [zksyncSepoliaTestnet.id]: http(),
    [zksyncInMemoryNode.id]: http(),
    [zksyncLocalNode.id]: http(),
  },
});

Here are some common actions:

Connect Wallet

connect Method

connect.ts
import { connect } from '@wagmi/core';
import { injected } from '@wagmi/connectors';
import { config } from '../../wagmi-config';

export async function connectWallet() {
  try {
    await connect(config, { connector: injected() });
  } catch (error) {
    console.error('ERROR CONNECTING:', error);
  }
}

useConnect Hook

ConnectWallet.tsx
import { injected } from '@wagmi/connectors';
import { useConnect } from 'wagmi';

export function ConnectWallet() {
  const { connect } = useConnect();

  return (
    <div>
      <button onClick={() => connect({ connector: injected() })}>Connect Wallet</button>
    </div>
  );
}

Display Wallet Options

getConnectors Method

connectors.ts
import { getConnectors } from '@wagmi/core';
import { config } from '../../wagmi-config';

export function fetchConnectors() {
  const connectors = getConnectors(config);
  return connectors;
}

useConnectors Hook

ShowConnectors.tsx
import { connectWallet } from '@/utils/connect';
import { useConnectors } from 'wagmi';

export function ShowConnectors() {
  const connectors = useConnectors();

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
      {connectors.map((connector) => (
        <button
          key={connector.id}
          onClick={connectWallet}
        >
          {connector.name}
        </button>
      ))}
    </div>
  );
}

Fetch Account

getAccount Method

account.ts
import { getAccount } from '@wagmi/core';
import { config } from '../../wagmi-config';

export function fetchAccount() {
  const { address } = getAccount(config);
  return address;
}

useAccount Hook

Account.tsx
import { useAccount } from 'wagmi';

export function Account() {
  const { address } = useAccount();

  return <div>{address}</div>;
}

Fetch Balance

getBalance Method

balance.ts
import { getBalance } from '@wagmi/core';
import { config } from '../../wagmi-config';

export async function fetchBalance() {
  const address = '0xBC989fDe9e54cAd2aB4392Af6dF60f04873A033A';
  const balance = await getBalance(config, { address });
  return balance.value;
}

useBalance Hook

Balance.tsx
import { useBalance } from 'wagmi';

export function Balance() {
  const balance = useBalance({
    address: '0xBC989fDe9e54cAd2aB4392Af6dF60f04873A033A',
  });

  return <div>{balance && <p>Balance: {balance.data?.value.toString()}</p>}</div>;
}

Fetch Block Number

getBlockNumber Method

block.ts
import { getBlockNumber } from '@wagmi/core';
import { config } from '../../wagmi-config';

export async function fetchLatestBlockNumber() {
  const blockNumber = await getBlockNumber(config);
  return blockNumber;
}

useBlockNumber Hook

Block.tsx
import { useBlockNumber } from 'wagmi';

export function Block() {
  const block = useBlockNumber();

  return <div>{block && <p>Block: {block.data?.toString()}</p>}</div>;
}

Send Transaction

sendTransaction Method

sendTx.ts
import { sendTransaction } from '@wagmi/core';
import { parseEther } from 'viem';
import { config } from '../../wagmi-config';

export async function sendTx(address: `0x${string}`, value: `${number}`) {
  const result = await sendTransaction(config, {
    to: address,
    value: parseEther(value),
  });

  return result;
}

useSendTransaction Hook

SendTx.tsx
import { parseEther } from 'viem';
import { useSendTransaction } from 'wagmi';

export function SendTx() {
  const { sendTransaction, isError, isPending, isSuccess, data, error } = useSendTransaction();

  return (
    <>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          const formData = new FormData(e.target as HTMLFormElement);
          const address = formData.get('address') as `0x${string}`;
          const value = formData.get('value') as `${number}`;
          sendTransaction({
            to: address,
            value: parseEther(value),
          });
        }}
      >
        <input
          name="address"
          placeholder="address"
        />
        <input
          name="value"
          placeholder="value (ether)"
        />
        <button type="submit">Send</button>
      </form>

      {isPending && <div>Transaction pending...</div>}
      {isSuccess && <div>Transaction Hash: {data}</div>}
      {isError && <div>Error: {error?.message}</div>}
    </>
  );
}

Send Transaction (Prepared)

prepareTransactionRequest Method

prepareTx.ts
import { prepareTransactionRequest } from '@wagmi/core';
import { parseEther } from 'viem';
import { config } from '../../wagmi-config';

export async function prepareTx(to: `0x${string}`, value: `${number}`) {
  const tx = await prepareTransactionRequest(config, {
    to,
    value: parseEther(value),
  });

  return tx;
}

usePrepareTransactionRequest Hook

SendTxPrepared.tsx
import { useState } from 'react';
import { parseEther } from 'viem';
import { usePrepareTransactionRequest, useSendTransaction } from 'wagmi';

export function SendTxPrepared() {
  const [to, setTo] = useState<`0x${string}`>();
  const [value, setValue] = useState<string>('0.0');

  const { data: txRequest } = usePrepareTransactionRequest({
    to,
    value: parseEther(value as `${number}`),
  });

  const { sendTransaction, isError, isPending, isSuccess, data, error } = useSendTransaction();

  return (
    <>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          if (!txRequest) return;
          sendTransaction(txRequest);
        }}
      >
        <input
          placeholder="address"
          onChange={(e) => setTo(e.target.value as `0x${string}`)}
          value={to}
        />
        <input
          id="value"
          placeholder="value (ether)"
          onChange={(e) => setValue(e.target.value)}
          value={value?.toString()}
        />
        <button
          disabled={!txRequest}
          type="submit"
        >
          Send
        </button>
      </form>

      {isPending && <div>Transaction pending...</div>}
      {isSuccess && <div>Transaction Hash: {data}</div>}
      {isError && <div>Error: {error?.message}</div>}
    </>
  );
}

Sign Message

signMessage Method

message.ts
import { signMessage } from '@wagmi/core';
import { config } from '../../wagmi-config';

export async function getSignedMessage(message: string) {
  const signature = await signMessage(config, { message });
  return signature;
}

useSignMessage Hook

SignMessage.tsx
import { useEffect, useState } from 'react';
import { type Address, recoverMessageAddress } from 'viem';
import { useSignMessage } from 'wagmi';

export function SignMessage() {
  const [recoveredAddress, setRecoveredAddress] = useState<Address>();
  const { data: signature, variables, error, isPending, signMessage } = useSignMessage();

  useEffect(() => {
    (async () => {
      if (variables?.message && signature) {
        const recoveredAddress = await recoverMessageAddress({
          message: variables?.message,
          signature,
        });
        setRecoveredAddress(recoveredAddress);
      }
    })();
  }, [signature, variables?.message]);

  return (
    <>
      <form
        onSubmit={(event) => {
          event.preventDefault();
          const element = event.target as HTMLFormElement;
          const formData = new FormData(element);
          const message = formData.get('message') as string;
          signMessage({ message });
        }}
      >
        <input
          name="message"
          type="text"
          required
        />
        <button
          disabled={isPending}
          type="submit"
        >
          {isPending ? '...Pending' : 'Sign Message'}
        </button>
      </form>

      {signature && (
        <div>
          <div>Signature: {signature}</div>
          <div>Recovered address: {recoveredAddress}</div>
        </div>
      )}
      {error && <div>Error: {error?.message}</div>}
    </>
  );
}

Sign Typed Data

signTypedData Method

signTyped.ts
import { signTypedData, type SignTypedDataParameters } from '@wagmi/core';
import { config } from '../../wagmi-config';

export async function getSignedTypedData(data: SignTypedDataParameters) {
  const signature = await signTypedData(config, data);
  return signature;
}

useSignTypedData Hook

SignTypedData.tsx
import { useSignTypedData } from 'wagmi';

export function SignTypedData() {
  const { signTypedData, data } = useSignTypedData();

  return (
    <>
      <button
        onClick={() =>
          signTypedData({
            types: {
              Person: [
                { name: 'name', type: 'string' },
                { name: 'wallet', type: 'address' },
              ],
              Mail: [
                { name: 'from', type: 'Person' },
                { name: 'to', type: 'Person' },
                { name: 'contents', type: 'string' },
              ],
            },
            primaryType: 'Mail',
            message: {
              from: {
                name: 'Cow',
                wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
              },
              to: {
                name: 'Bob',
                wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
              },
              contents: 'Hello, Bob!',
            },
          })
        }
      >
        Sign message
      </button>
      <div>{data && <p>Signature: {data}</p>}</div>
    </>
  );
}

Read Contract

readContract Method

signTyped.ts
import { readContract } from '@wagmi/core';
import { config } from '../../wagmi-config';
import * as erc20TokenABI from '../../../frontend-paymaster/contracts/artifacts-zk/contracts/erc20/MyERC20Token.sol/MyERC20Token.json';

const CONTRACT_ADDRESS = '0x9c1a3d7C98dBF89c7f5d167F2219C29c2fe775A7';

export async function readERC20Contract(functionName: string) {
  const data = await readContract(config, {
    abi: erc20TokenABI.abi,
    address: CONTRACT_ADDRESS,
    functionName: functionName,
  });
  return data;
}

useReadContract Hook

ReadContract.tsx
import { useState } from 'react';
import type { Address, BaseError } from 'viem';
import { useReadContract } from 'wagmi';
import * as erc20TokenABI from '../../../frontend-paymaster/contracts/artifacts-zk/contracts/erc20/MyERC20Token.sol/MyERC20Token.json';

const CONTRACT_ADDRESS = '0x9c1a3d7C98dBF89c7f5d167F2219C29c2fe775A7';

export function ReadContract() {
  return (
    <div>
      <div>
        <BalanceOf />
        <br />
        <TotalSupply />
      </div>
    </div>
  );
}

function TotalSupply() {
  const { data, isRefetching, refetch } = useReadContract({
    abi: erc20TokenABI.abi,
    address: CONTRACT_ADDRESS,
    functionName: 'totalSupply',
  });

  console.log('data', data);

  return (
    <div>
      Total Supply: {data?.toString()}
      <button
        disabled={isRefetching}
        onClick={() => refetch()}
        style={{ marginLeft: 4 }}
      >
        {isRefetching ? 'loading...' : 'refetch'}
      </button>
    </div>
  );
}

function BalanceOf() {
  const [address, setAddress] = useState<Address>('0xBC989fDe9e54cAd2aB4392Af6dF60f04873A033A');
  const { data, error, isLoading, isSuccess } = useReadContract({
    abi: erc20TokenABI.abi,
    address: CONTRACT_ADDRESS,
    functionName: 'balanceOf',
    args: [address],
  });

  const [value, setValue] = useState<string>(address);

  return (
    <div>
      Token balance: {isSuccess && data?.toString()}
      <input
        onChange={(e) => setValue(e.target.value)}
        placeholder="wallet address"
        style={{ marginLeft: 4 }}
        value={value}
      />
      <button onClick={() => setAddress(value as Address)}>{isLoading ? 'fetching...' : 'fetch'}</button>
      {error && <div>{(error as BaseError).shortMessage}</div>}
    </div>
  );
}

Write Contract

writeContract Method

write.ts
import { readContract } from '@wagmi/core';
import { config } from '../../wagmi-config';
import * as greeterABI from '../../../frontend-paymaster/contracts/artifacts-zk/contracts/Greeter.sol/Greeter.json';

const CONTRACT_ADDRESS = '0xCeAB1fc2693930bbad33024D270598c620D7A52B';

export async function writeToGreeterContract(greeting: string) {
  const data = await readContract(config, {
    abi: greeterABI.abi,
    address: CONTRACT_ADDRESS,
    functionName: 'setGreeting',
    args: [greeting],
  });
  return data;
}

useWriteContract Hook

WriteContract.tsx
import { useState } from 'react';
import { type BaseError, useWriteContract } from 'wagmi';
import * as greeterABI from '../../../frontend-paymaster/contracts/artifacts-zk/contracts/Greeter.sol/Greeter.json';

const CONTRACT_ADDRESS = '0xCeAB1fc2693930bbad33024D270598c620D7A52B';

export function WriteContract() {
  const [greeting, setGreeting] = useState<string>();
  const { writeContract, isError, isPending, isSuccess, data, error } = useWriteContract();

  async function updateGreeting() {
    if (!greeting) return;
    writeContract({
      abi: greeterABI.abi,
      address: CONTRACT_ADDRESS,
      functionName: 'setGreeting',
      args: [greeting],
    });
  }

  return (
    <div>
      <input
        onChange={(e) => setGreeting(e.target.value)}
        placeholder="Hello, Zeek!"
        style={{ marginLeft: 4 }}
        value={greeting}
      />
      <button onClick={updateGreeting}>{isPending ? '...Pending' : 'Update Greeting'}</button>
      {isError && <div>{(error as BaseError).shortMessage}</div>}
      {isSuccess && <div>Transaction hash: {data}</div>}
    </div>
  );
}

Made with ❤️ by the ZKsync Community