Финансовое приложение традиционно умеет три вещи: показать баланс, принять платёж, отдать выписку. Десятилетиями за этими действиями стояли закрытые процессинги и внутренние бухгалтерии. Децентрализованные финансы переменили саму кухню: баланс живёт в общем реестре, платёж это подписанная транзакция, а выписку можно собрать из публичных событий контрактов. Для владельца финансового сайта интеграция с DeFi уже не экзотика, а насущный вопрос: аудитория держит кошельки и ждёт, что кнопки на странице говорят с блокчейном напрямую.
Хорошая новость в том, что технически это обычная фронтенд-разработка со своим набором инструментов: подключить кошелёк, поднять клиент к RPC узлу, описать ABI, вызвать метод. Дурная новость: на каждом шаге лежат грабли, от неверного числа десятичных знаков до молчаливо зависшей подписи. Дальше весь путь пройден по шагам, с кодом на viem и числами, которые сберегут не один вечер отладки.
Подключение кошелька через окно ethereum и стандарт EIP-1193 в браузере
Вход в блокчейн начинается не с контрактов, а с кошелька пользователя. Браузерные расширения и мобильные приложения встраивают в страницу объект window.ethereum, провайдер по стандарту EIP-1193. Единственный метод провайдера request закрывает все потребности: eth_requestAccounts запрашивает разрешение и возвращает адреса, eth_chainId сообщает сеть в шестнадцатеричном виде, wallet_switchEthereumChain предлагает сменить сеть. Значения легко запомнить: 0x1 это основная сеть Ethereum, 0x89 Polygon, 0xa4b1 Arbitrum One, 0x2105 Base, а тестовая Sepolia отвечает 0xaa36a7. Если сеть не совпадает с нужной, интерфейс предлагает переключение и блокирует опасные кнопки, а не надеется на внимательность человека. Пользователь подтверждает доступ один раз, дальше адрес читается без вопросов, но с интерфейса ответственность не снимается: события accountsChanged и chainChanged обязаны обрабатываться, потому что человек может сменить аккаунт или сеть посреди сессии, и страница, которая этого не заметила, продолжит показывать баланс чужого кошелька. Для мобильных браузеров без расширений работает протокол WalletConnect: сайт показывает QR-код, телефон сканирует его, и соединение живёт через мост.
У вежливого сайта два неписаных правила. Первое: не запрашивать адреса при первой загрузке страницы, шквал попапов убивает доверие ещё до первого клика, точка входа это всегда явная кнопка подключения. Второе: показывать адрес в контрольной сумме EIP-55, где регистр букв шифрует проверку на опечатку, потому что ручное сравнение двух похожих строк неизбежно ошибается.
const eth = window.ethereum;
const accounts = await eth.request({ method: "eth_requestAccounts" });
const chainId = await eth.request({ method: "eth_chainId" });
eth.on("accountsChanged", (accs) => updateAccounts(accs));
eth.on("chainChanged", (id) => reloadBalances(id));
RPC узлы и библиотеки ethers viem и wagmi как транспорт между сайтом и блокчейном
Блокчейн не открывает порты прямо в браузер, сайт общается с сетью через RPC узел, сервер, принимающий вызовы JSON-RPC. Методы знакомы поимённо: eth_getBalance вернёт баланс эфира, eth_call выполнит чтение контракта, eth_estimateGas оценит стоимость операции, eth_sendTransaction опубликует подписанную транзакцию. Узел держат свой или берут у провайдера: собственный полный узел это контроль и обязанность синхронизировать сотни гигабайт истории, общедоступные узлы подходят для чтения на старте, а платные тарифы дают ключ, стабильность и WebSocket. HTTP хорош для сценария запрос-ответ, WebSocket держит постоянное соединение и незаменим для мгновенных событий. Ответ узла не всегда успешен: запросы падают по таймауту и перегруженности, поэтому транспорт обязан уметь повтор и запасной узел, иначе живой баланс превратится в вечный спиннер.
Выбор библиотеки сведён к трём именам. ethers.js шестой версии проверен годами и встречается в большинстве старых кодовых баз. viem от авторов wagmi набрала популярность за строгую типизацию и модульность: чтение и запись разнесены по отдельным клиентам, каждый вызов типизирован, ошибки ловятся на этапе разработки. Сам wagmi это React хуки поверх viem, useAccount, useReadContract, useWriteContract, и с ними кошелёк, состояние сети и кэш запросов перестают быть ручной работой. Практичное решение: новый проект на React строить на wagmi, лёгкой странице без фреймворка хватит чистого viem. Отдельная забота это лимиты вызовов: публичные узлы режут частые запросы, поэтому чтения кэшируют, группируют через multicall и не устраивают водопад из десятков eth_call при каждом рендере.
Чтение состояния смарт-контракта через ABI и вызов eth_call без траты газа
Смарт-контракт живёт по адресу, а поговорить с ним можно только через меню функций, выписанное в ABI. ABI это JSON с именами методов, типами входов и выходов, и для чтения он не требует ни газа, ни подписей: eth_call выполняется прямо на узле и мгновенно возвращает результат. Читают баланс токена через balanceOf, текущий лимит через allowance, число знаков после запятой через decimals, тикер через symbol. Числа приходят целыми, в единицах контракта: у стейблкоина USDC шесть знаков, у большинства остальных токенов восемнадцать, и функция formatUnits переводит их в человеческий вид. Отдельная ловушка это точность: Number в JavaScript надёжно держит целые лишь до девяти квадриллионов, а единицы контрактов легко больше, поэтому суммы живут как bigint до самого вывода. Ошибка в знаках это классика жанра: интерфейс гордо показывает сумму с двенадцатью лишними нулями там, где должно быть 152,20.
Пример чтения баланса USDC на основной сети Ethereum, адрес контракта известен заранее и не меняется годами:
import { createPublicClient, http, formatUnits } from "viem";
import { mainnet } from "viem/chains";
const client = createPublicClient({
chain: mainnet,
transport: http("https://rpc.example.net/v3/key")
});
const balance = await client.readContract({
address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
abi: erc20Abi,
functionName: "balanceOf",
args: [userAddress]
});
console.log(formatUnits(balance, 6));
Запись в контракт газ EIP-1559 и путь транзакции до подтверждения
Запись меняет состояние реестра, за это платят газ и ставят подпись. Цепочка действий всегда одна: сайт собирает вызов, кошелёк показывает человеку сумму и оценку комиссии, подтверждение уходит, узел публикует транзакцию, интерфейс ждёт чек. Ориентиры по газу: пересылка эфира стоит 21000 единиц, перевод токена ERC-20 берёт порядка 50-65 тысяч, обмен на децентрализованной площадке легко уходит за 150-300 тысяч. После Лондонского обновления комиссия разделена: базовая часть сгорает, чаевые maxPriorityFeePerGas уходят валидатору, а maxFeePerGas это потолок, выше которого человек не платит. Блок в Ethereum выходит примерно каждые 12 секунд, поэтому честный интерфейс показывает статус по шагам: подписана, в мемпуле, подтверждена, отклонена с причиной, а не просто крутилка на полминуты. Зависшая транзакция лечится заменой с тем же nonce и повышенными чаевыми, именно это делает кнопка ускорения в кошельке.
import { createWalletClient, custom, parseUnits } from "viem";
const wallet = createWalletClient({
chain: mainnet,
transport: custom(window.ethereum)
});
const hash = await wallet.writeContract({
address: tokenAddress,
abi: erc20Abi,
functionName: "approve",
args: [spenderAddress, parseUnits("100", 6)],
account: userAddress
});
const receipt = await client.waitForTransactionReceipt({ hash });
Хороший тон: перед записью сверить, что адрес контракта и сеть совпадают с ожидаемыми, прогнать eth_estimateGas заранее и показать ожидаемое списание, а не отправлять человека в неизвестность.
Стандарты токенов ERC-20 ERC-721 и ERC-4626 и паттерн approve allowance в интерфейсе
Финансовому сайту чаще всего нужны три стандарта. ERC-20 описывает взаимозаменяемые токены и лежит в основе расчётов. ERC-721 и ERC-1155 покрывают уникальные активы, от билетов до долей произведений. ERC-4626 стандартизует токенизированные хранилища с долями участников, методы deposit, redeem и convertToShares позволяют показать человеку его долю без ручных вычислений.
Особая тема для финансового интерфейса это паттерн approve и allowance: чтобы контракт потратил чужие токены, владелец сначала выдаёт лимит через approve, а контракт затем списывает через transferFrom. Уважение к пользователю здесь выражается в мелочах: экран обязан показывать действующие лимиты, предлагать выдавать сумму под конкретную операцию, а не неограниченный лимит навсегда, и чистить использованные разрешения до нуля. Там, где токен поддерживает подпись без газа по EIP-2612 permit, транзакция approve экономится целиком: одна подпись вместо одной транзакции, и подтверждение в кошельке выглядит понятнее. Показ сумм подчиняется тем же правилам: шесть знаков после запятой у USDC не значат шесть знаков на экране, интерфейс округляет до двух и ведёт расчёты в целых до самого вывода, чтобы копейки не терялись.
Оракулы Chainlink и события контрактов как источник живых цен и действий
Свежая цена это дыхание финансового интерфейса, и брать её с потолка нельзя. Оракул Chainlink агрегирует значения с целого ряда площадок и пишет их в контракт-агрегатор, доступный для чтения без газа, технически это прокси EACAggregatorProxy поверх агрегатора. Метод latestRoundData возвращает массив: второй элемент это сама цена, четвёртый это время последнего обновления. Для пары ETH к доллару значение хранится с восемью знаками, то есть 245628000000 это 2456,28 доллара. Проверка свежести обязательна: устаревшая цена хуже отсутствующей, и сравнение времени обновления с текущим это две строки кода, которые уберегают от показа цифр во время сбоя агрегатора.
const feed = await client.readContract({
address: "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419",
abi: aggregatorAbi,
functionName: "latestRoundData"
});
const price = Number(feed[1]) / 1e8;
const updatedAt = Number(feed[3]);
События контрактов это второй живой канал. Контракт заявляет о действиях событиями, например Transfer при движении токена, и подписка через WebSocket или опрос eth_getLogs приносят их на сайт быстрее любой перезагрузки. Опрос в лоб годится для мелочи, подписка экономит запросы и реагирует мгновенно, а для сложных выборок по истории работают индексы вроде The Graph с GraphQL-запросами по готовым подграфам. Баланс, обновляющийся сам после каждой транзакции по адресу пользователя, выглядит как само собой разумеющееся, хотя за ним стоит аккуратная подписка на события.
Форки тестовые сети и симуляция транзакций перед подписью кошельком
Проверять интеграцию на основной сети дорого и неловко: каждая ошибка стоит реальных денег и репутации. Локальный форк Anvil из набора Foundry поднимает копию сети с живым состоянием одной командой anvil --fork-url, и фронтенд искренне считает, что работает с настоящим блокчейном, пока газ не стоит ничего. Hardhat делает то же через конфиг, а публичная сеть Sepolia раздаёт тестовый эфир через краны для сквозных проверок с внешними кошельками. Симуляция перед подписью это высшая форма заботы: сервисы вроде Tenderly прогоняют вызов по текущему состоянию и показывают, упадёт ли он и что именно изменит, и такой прогон стоит встроить прямо в экран подтверждения. Перед релизом адреса контрактов сверяют с контрольными суммами и белым списком, а верифицированный исходный код в обозревателе блоков закрывает вопрос, что именно исполняется по каждому адресу. Последовательность шагов при внедрении выглядит так:
- поднять публичный клиент viem на HTTP или WebSocket узле нужной сети;
- добавить подключение кошелька по EIP-1193 с реакцией на смену аккаунта и сети;
- выписать в ABI только нужные методы и читать состояние через eth_call;
- собрать экран подтверждения с оценкой газа и симуляцией до подписи;
- подключить цены из оракула с проверкой свежести раунда;
- подписаться на события контрактов для обновления интерфейса без перезагрузки;
- прогнать все сценарии на форке Anvil и в сети Sepolia.
Интеграция с DeFi давно стала ремеслом с ясной последовательностью швов: кошелёк, транспорт, контракт, подпись, события. Сайт, у которого собрана вся цепочка, вызывает у человека простое чувство, что всё работает, хотя за этой простотой стоят десятки решений, каждое из которых теперь описано и проверяемо.