Финансовое приложение традиционно умеет три вещи: показать баланс, принять платёж, отдать выписку. Десятилетиями за этими действиями стояли закрытые процессинги и внутренние бухгалтерии. Децентрализованные финансы переменили саму кухню: баланс живёт в общем реестре, платёж это подписанная транзакция, а выписку можно собрать из публичных событий контрактов. Для владельца финансового сайта интеграция с 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 прогоняют вызов по текущему состоянию и показывают, упадёт ли он и что именно изменит, и такой прогон стоит встроить прямо в экран подтверждения. Перед релизом адреса контрактов сверяют с контрольными суммами и белым списком, а верифицированный исходный код в обозревателе блоков закрывает вопрос, что именно исполняется по каждому адресу. Последовательность шагов при внедрении выглядит так:

  1. поднять публичный клиент viem на HTTP или WebSocket узле нужной сети;
  2. добавить подключение кошелька по EIP-1193 с реакцией на смену аккаунта и сети;
  3. выписать в ABI только нужные методы и читать состояние через eth_call;
  4. собрать экран подтверждения с оценкой газа и симуляцией до подписи;
  5. подключить цены из оракула с проверкой свежести раунда;
  6. подписаться на события контрактов для обновления интерфейса без перезагрузки;
  7. прогнать все сценарии на форке Anvil и в сети Sepolia.

Интеграция с DeFi давно стала ремеслом с ясной последовательностью швов: кошелёк, транспорт, контракт, подпись, события. Сайт, у которого собрана вся цепочка, вызывает у человека простое чувство, что всё работает, хотя за этой простотой стоят десятки решений, каждое из которых теперь описано и проверяемо.