Вступление
Привет! Сегодня мы рассмотрим относительно новую библиотеку для интеграции с web3 - viem. Мы постараемся понять, способна ли эта библиотека заменить ethers и какие преимущества она предлагает.
Кому подойдет эта статья?
Если вы хорошо владеете javascript и знакомы с ethers или прочли мои статьи Ethers js - Основы и Ethers js - Транзакции, то можете смело читать дальше. В другом случае материал может быть сложен для восприятия.
Содержание
Что такое viem?
Преимущества и недостатки viem
Экосистема
Подключение кошельков
Взаимодействие со смарт-контрактами
Сравнение пакетов экосистемы
Вывод
Что такое viem?
Viem - это библиотека для работы с Ethereum совместимыми блокчейнами. Она предоставляет разработчикам удобные инструменты для создания децентрализованных приложений (dapp) на базе Ethereum.
Ранее сфера web3 интеграций часто контролировалась ethers, и большинство новых решений строились на его основе. Однако, с появлением аналога в виде viem, ситуация стала меняться, и теперь нашим следующим шагом будет рассмотрение основных отличий между этими двумя библиотеками.
ethers и viem - обе библиотеки предназначены для взаимодействия с Ethereum совместимыми блокчейнами. Их основные задачи схожи, но есть несколько ключевых отличий которые мы и рассмотрим дальше.
Преимущества и недостатки viem
Начнем с хорошего. Почему же стоит использовать viem?
Viem демонстрирует более высокую производительность по сравнению с ethers, особенно при работе с большим объемом данных. Например, при вызове метода isAddress на 100 000 итераций viem выполняется за 0.02 секунды, в то время как "ethers" требует 1.1 секунды. Это достигается благодаря оптимизации асинхронных задач и алгоритмов кодирования/анализа данных.
Viem имеет размер пакета почти в 5 раз меньше, чем ethers (27kb против 127kb). Это достигнуто благодаря минимизации компонентов для взаимодействия с блокчейном и разделению на модули. Таким образом, приложение может использовать только необходимый функционал, что обеспечивает эффективность и оптимизацию размера пакета.
Viem обладает отличной совместимостью с TypeScript и хорошим покрытием тестов, что делает ее надежной. Однако, следует упомянуть и некоторые недостатки по сравнению с ethers.
Несмотря на перечисленные преимущества, viem может быть менее удобным для начинающих web3 разработчиков из-за открытого подхода к реализации функционала, включая провайдеров и другие компоненты. В то время как ethers предоставляет готовые провайдеры для различных случаев (например, EtherscanProvider, FallbackProvider и т.д.), viem требует большей самостоятельной настройки и реализации функционала.
Также стоит упомянуть, что viem является относительно новой технологией, и на момент публикации она существует всего чуть более года. Ее сообщество разработчиков все еще развивается, и могут возникнуть сложности в поиске готовых решений из открытых источников.
В пользу viem можно сказать, что у нее хорошо написанная документация, что облегчает понимание и использование библиотеки. В сравнении с ethers, которая может оставлять вопросы после изучения, это является значительным преимуществом.
Экосистема
Viem - относительно новая технология, но уже обладает хорошей экосистемой для React и Vue, а также для создания ванильных JavaScript/TypeScript сервисов. Например, популярная библиотека интеграции мобильных крипто-кошельков WallecConnect начала предоставлять примеры кода с использованием Viem во второй версии. Для React и Vue появились удобные хуки для работы с блокчейном - wagmi и vagmi соответственно. Эти хуки предоставляют полный набор декларативных инструментов для решения различных задач, начиная с подключения кошелька и заканчивая оптимизацией вызовов методов смарт-контрактов.
При разработке ванильных JavaScript/TypeScript сервисов, разработчики могут использовать пакет wagmi/core, который также содержит функции обертки для эффективного решения разнообразных задач. В то время как Ethers не обладает такой развитой экосистемой библиотек, и для подключения кошельков остается использовать Web3Modal, который может быть ограничен в кастомизации или содержать слабо протестированные коннекторы. Из-за этого часто в продуктовых компаниях создаются свои собственные библиотеки, практически идентичные wagmi.
В данной статье мы предоставим примеры использования viem/wagmi в React приложениях, которые практически аналогичны и для Vue3 с использованием viem/vagmi. Также будет представлена таблица с методами для ванильных JavaScript/TypeScript сервисов, которые аналогичны методам, предоставляемыми хуками. Это позволит понять подход к решению задач в различных окружениях.
Подключение кошельков
Ранее, для подключения кошельков с помощью ethers, разработчикам приходилось использовать Web3Modal, открытые любительские коннекторы или создавать собственные библиотеки с нуля для компании. Однако viem в комбинации с wagmi предоставляет полный набор инструментов, который значительно ускоряет и структурирует данный процесс, при этом оставляя гибкость для различных потребностей.
Давайте рассмотрим как можно подключить различные кошельки на примере кода ниже:
import { useConnect, WagmiConfig, createConfig, configureChains } from 'wagmi'
import { polygon, mainnet } from '@wagmi/core/chains';
import { publicProvider } from 'wagmi/providers/public'
import { CoinbaseWalletConnector } from 'wagmi/connectors/coinbaseWallet'
import { InjectedConnector } from 'wagmi/connectors/injected'
import { MetaMaskConnector } from 'wagmi/connectors/metaMask'
import { WalletConnectConnector } from 'wagmi/connectors/walletConnect'
// 1. Настройка доступных сетей и провайдеров
const { chains, publicClient, webSocketPublicClient } = configureChains(
[mainnet, polygon],
[publicProvider()],
)
// 2. Настройка конфига
const config = createConfig({
autoConnect: true,
connectors: [
new MetaMaskConnector({ chains }),
new CoinbaseWalletConnector({
chains,
options: {
appName: 'educationApp',
},
}),
new WalletConnectConnector({
chains,
options: {
projectId: 'educationApp',
},
})
],
publicClient,
webSocketPublicClient,
})
// 3. Настройка интерфейса для подключения
function App() {
const { connect, connectors, error, isLoading, pendingConnector } =
useConnect()
return (
<WagmiConfig config={config}>
<div>
{connectors.map((connector) => (
<button
disabled={!connector.ready}
key={connector.id}
onClick={() => connect({ connector })}
>
{connector.name}
{!connector.ready && ' (unsupported)'}
{isLoading &&
connector.id === pendingConnector?.id &&
' (connecting)'}
</button>
))}
{error && <div>{error.message}</div>}
</div>
</WagmiConfig>
)
}
export default App;
Взамен инициализации JSONRpcProvider ethers приходит функция publicProvider, которая возвращает экземпляр JSON Rpc провайдера для получения ссылки на поставщика данных в разных сетях, в зависимости от переданных сетей. К слову, информация для основных сетей теперь также доступна для импорта, раньше же приходилось находить вручную и заводить под них константы.
В итоге для инициализации провайдера теперь необходимо вызвать configureChains и передавать туда:список поддерживаемых сетей, большую часть из которых также можно импортировать из wagmi
cписок провайдеров, которые будут использоваться в зависимости от их скорости и ответа. Например если передать 2 провайдера и один из них выйдет из строя, то запросы перенаправляются на второй.
В ethers есть похожий провайдеров FallbackProvider, но все же он ориентирован только на скорость ответа и если какой-то из провайдеров недоступен то прерывается инициализация всех. На выходе получаем объект в котором нас интересуют следующие поля:
chains - список всех поддерживаемых сетей
publicClient - провайдер, который отслеживает активную сеть и устанавливает нужного поставщика данных через JSONRPC
websocketPublicClient - аналогичный провайдер для установления веб сокет соединения
connect - функция для подключения, которая принимает объект с настройками для подключения. Основные это connector экземпляр класса для подключения к определенному кошельку и chainId идентификатор сети для подключения. Можем быть полезно если необходимо выбирать сеть для подключения непосредственно перед подключением.
connectors - список ранее настроенных в конфиге экземпляров класса для подключения к кошелькам
error - ошибка подключения, если такая возникнет
isLoading - флаг отображающий, что подключение происходит в данный момент
pendingConnector - экземпляр класса для подключения, подключение которого происходит в данный момент
Далее используя все полученные данные отображаем все доступные кошельки, вешаем обработчики клика и передаем туда вызов функции connect с аргументом в виде объекта с полем connector или другими словами с экземпляром класса для подключения. Отображаем данные при ожидании подключения.
Благодаря функции createConfig теперь есть возможность настроить поддерживаемые кошельки. Для реализации подобной задачи с помощью ethers можно было воспользоваться готовыми библиотеками по типу Web3Modal или snapshot/lock, которые имеют ряд недостатков и в большинстве случаев приходилось писать собственную библиотеку. Теперь же все удобно настраивается из коробки и функция createConfig принимает следующие аргументы:
autoConnect - флаг для автоматического подключения при перезагрузки приложения
connectors - список экземпляров класса для подключения к кошелькам, все экземпляры наследуются от одноименного класса Connector. wagmi реализует наиболее популярные кошельки и предоставляет их из коробки. Кроме того есть возможность создания собственного, остается лишь создать экземпляр класса наследуемого от класса Connector. В данном примере реализуется поддержка MetaMask, WalletConnect, CoinBase wallet.
в конце передаются ранее инициализированные провайдеры publicClient, websocketPublicClient
На выходе получаем config который в дальнейшем нужно сделать доступным во всем приложении.
Оборачиваем приложение в WagmiConfig и передаем туда ранее созданный config, это необходимо, чтобы все вспомогательные хуки в дальнейшем работали успешно. Теперь можно использовать вспомогательные хуки wagmi. Первый хук который нам понадобится useConnect. Он возвращает объект, основными свойствами которого являются:
Теперь подключение кошельков полностью готово, остается лишь отображать данные после подключения (адрес кошелька, активную сеть) и добавить возможность смены сети из интерфейса. Давайте разберемся как можно отображать необходимые данные. Для этого рассмотрим пример кода:
import { useAccount, useEnsName, useNetwork } from 'wagmi';
function AccountInfo () {
const { chain } = useNetwork();
const { address } = useAccount();
const { data: ensAvatar } = useEnsAvatar({ address });
const { data: ensName } = useEnsName({ address });
const { disconnect } = useDisconnect()
return (
<div>
<div>active chain: {chain?.name}</div>
{ensAvatar && (<img src={ensAvatar} alt="avatar" />)}
<div>account: {ensName || address}</div>
<button onClick={() => disconnect()}>Disconnect</button>
</div>
);
}
export default AccountInfo;
В данном компоненте мы отображаем информацию об аккаунте пользователе и предоставляем возможность отключиться. И все это с помощью хуков wagmi, давайте разберем подробнее каждый из них:
useNetwork - хук, который возвращает chain (активная сеть) и chains сети доступные для подключения.
useAccount - хук, который возвращает информацию о подключении аккаунта. Из этой информации наиболее полезные поля это address (собственно сам адрес кошелька), connector (instance того коннектора с помощью которого произошло подключение), isDisconnected, isConnected, isConnecting (состояния подключения в виде булейнов)
useEnsAvatar, useEnsName - хуки для получения аватара и ens имени аккаунта соответственно. Принимают в себя адрес кошелька и возвращают имя и аватар если такие существуют для данного адреса.
useDisconnect - хук для отключения подключенного аккаунта. возвращает функцию disconnect для синхронного отключения и disconnectAsync, если есть необходимость отслеживать promise. А также вспомогательные переменные о наличии ошибке или о состоянии процесса отключения.
Осталось реализовать только смену сетей. Давайте рассмотрим код ниже:
import { useSwitchNetwork, useNetwork } from 'wagmi';
export const ChainSwitcher = ({
open,
close,
}) => {
const { chains, switchNetwork } = useSwitchNetwork();
const { chain } = useNetwork();
return (
<div>
{chains.map((chainOption) => (
<button
onClick={() => switchNetwork?.(chainOption.id)}
className={
chain?.id === chainOption.id
? 'active-chain-classes'
: 'chain-classes',
}
key={chainOption.id}
>
{chainOption.name}
</button>
))}
</div>
);
};
export default ChainSwitcher;
Реализация переключения сети осуществлена с помощью хуков useSwitchNetwork и useNetwork импортируемых из wagmi.
Хук useSwitchNetwork предоставляет функцию для переключения сети - switchNetwork, а также асинхронную функцию switchNetworkAsync. Обратим внимание, что эти функции будут доступны только после успешного подключения аккаунта к сети. В случае, если аккаунт не подключен, значения этих функций будут равны undefined. Поэтому перед вызовом функции switchNetwork, мы проверяем ее доступность. Если функция доступна, мы передаем ей аргументом id сети, на которую хотим переключиться.
Хук useNetwork используется для получения текущей активной сети, на которой работает пользователь. Мы получаем объект chain, который содержит информацию о текущей сети, включая ее id.
Внутри компонента, мы используем map для отображения списка доступных сетей в виде кнопок. Каждая кнопка соответствует определенной сети. При клике на кнопку, мы вызываем функцию switchNetwork с переданным ей id сети, чтобы переключить аккаунт на выбранную сеть.
CSS-классы "active-chain-classes" и "chain-classes" используются для стилизации активной и неактивной сети соответственно.
Взаимодействие со смарт-контрактами
Давайте рассмотрим как можно взаимодействовать как можно обращаться к смарт-контрактам с помощью viem/wagmi на примере чтения баланса и выдачи разрешения смарт-контракту ERC-20 токена. Для этого разберем код ниже:
import {
useAccount,
useContractRead,
usePrepareContractWrite,
useContractWrite
} from 'wagmi'
import { erc20ABI } from '@wagmi/core'
function ApproveToken({
tokenAddress: string
}) {
const { address } = useAccount()
const { data: balance } = useContractRead({
address: tokenAddress,
abi: erc20ABI,
functionName: 'balanceOf',
args: [address],
watch: true,
})
const { config } = usePrepareContractWrite({
address: tokenAddress,
abi: erc20ABI,
functionName: 'approve',
args: [address, balance]
})
const { data, isLoading, isSuccess, write } = useContractWrite({
...config
})
const buttonLabel = isLoading ? 'Loading...' : `Approve ${balance?.toString()}`
return (
<div>
<button disabled={!write || isLoading} onClick={() => write?.()}>
{buttonLabel}
</button>
</div>
)
}
Компонент использует хук useAccount, чтобы получить текущий адрес Ethereum пользователя, который будет использоваться для проверки баланса токенов.
Хук useContractRead используется для чтения данных из контракта ERC-20. Мы передаем адрес контракта (tokenAddress), его ABI (erc20ABI), имя функции, которую необходимо вызвать (balanceOf), и аргументы функции (адрес пользователя address). Мы также устанавливаем watch: true
, чтобы автоматически обновлять данные при их изменении.
Хук usePrepareContractWrite используется для подготовки вызова функции контракта, которая изменит данные. В данном случае, это функция approve, которая позволяет одобрить определенное количество токенов для другого адреса. Мы передаем аргументы функции: адрес пользователя address и баланс токенов balance, полученный из предыдущего хука.
Хук useContractWrite выполняет транзакцию вызова функции контракта. Мы передаем ему config, полученный из предыдущего хука, и получаем статус выполнения транзакции (isLoading, isSuccess), а также данные, возвращенные функцией контракта.
Наконец, компонент отображает кнопку, которая позволяет пользователю одобрить токены. Кнопка будет отключена, если транзакция находится в процессе выполнения (isLoading) или если функция write недоступна. При нажатии кнопки происходит вызов функции write, выполняющей транзакцию.
Также нужно отметить что viem и wagmi из коробки предоставляют возможность работать с множественными вызовами. В моей предыдущей статье Ethers js - транзакции мы уже рассматривали как это можно реализовать с помощью ethers. Давайте посмотрим, как можно получить балансы сразу нескольких erc-20 токенов. Для этого разберем код ниже:
import { useAccount, useContractReads } from 'wagmi'
import { erc20ABI } from '@wagmi/core'
function TokensList({
tokenAddresses: string[]
}) {
const { address } = useAccount()
const contractCalls = tokenAddresses.map((tokenAddress) => ({
address: tokenAddress,
abi: erc20ABI,
functionName: 'balanceOf',
args: [address]
}))
const { data, isLoading } = useContractReads({
contracts: contractCalls
})
return (
<div>
{isLoading ? (
<div>Fetching balances...</div>
) : (
data?.map((balance, index) => (
<div>Token {tokenAddresses[index]}: {balance.toString()}</div>
))
)}
</div>
)
}
Итак хук useAccount нам уже знаком.
Затем мы подготавливаем вызовы смарт-контрактов:
Перед вызовом useContractReads, мы создаем массив contractCalls, используя map для каждого адреса токена из переданного массива tokenAddresses.
Каждый элемент contractCalls представляет собой объект с информацией о вызове контракта ERC-20 для функции balanceOf.
Этот объект включает адрес контракта (tokenAddress), ABI контракта (erc20ABI), имя функции для вызова (balanceOf) и аргументы функции (адрес пользователя address).
Хук useContractReads:
Этот хук используется для одновременного чтения данных из нескольких контрактов с помощью мультикола.
Мы передаем массив объектов contractCalls в хук, чтобы выполнить несколько параллельных вызовов контрактов ERC-20 для получения балансов токенов.
В ответе хука, мы получаем объект data, который содержит массив с балансами токенов, соответствующими вызовам контрактов и отображаем их.
Сравнение пакетов экосистемы
Теперь пора закрепить материал. В целом алгоритм по подключению кошелька работает одинаково и на vue3, react, js/ts сервисах, отличается только способ хранения данных в приложении. Хуки и composables хуки из wagmi и vagmi соответственно идентичны. Но в любом случае мы всегда можем написать собственные хуки или сервисы используя wagmi/core. Теперь рассмотрим таблицу основных хуков и аналогичных им методов для ванильного js/ts сервиса:
Хук (wagmi) | Функция (wagmi/core) | Назначение |
useConnect возвращает connect | connect | подключение к определенному кошельку (connector) и сети(chainId) |
useNetwork | getNetwork | получение chains(доступных сетей) и chain(активной сети) |
useSwitchNetwork | switchNetwork | переключение сети на нужную из доступных по id сети |
useEnsAvatar | fetchEnsAvatar | получение аватара по адресу кошелька, если он имеется |
useEnsName | fetchEnsName | получение ens имени по адресу кошелька, если оно имеется |
useDisconnect возвращает disconnect | disconnect | отключение от активного кошелька |
useAccount | getAccount | получение информации об аккаунте (address, connector и т д) |
useContractRead | readContract | чтение данных из смарт-контракта |
useContractReads | readContracts/multicall | чтение данных из нескольких смарт-контрактов |
usePrepareContractWrite | prepareWriteContract | подготовка вызова смарт-контракта для записи |
useContractWrite | writeContract | запись данных данных в смарт-контракт |
Вывод
Заключительно, хочу выразить радость от развития экосистемы dapp библиотек, которое дает разработчикам больше свободы выбора инструментов. В частности, я бы хотел отметить viem & wagmi - технологию, которая мне понравилась и которая, на мой взгляд, способна успешно конкурировать с ethers и предоставлять множество преимуществ.
Спасибо, что уделили время чтению этой статьи. Теперь вы знаете о еще одном способе создания dapp-приложений, и я надеюсь, что новая информация поможет вам разрабатывать интересные децентрализованные приложения. Удачи в вашем путешествии в мир блокчейна и разработки dapp-приложений!