Примерно раз в год у меня появляется неутолимая жажда накопать много данных и что-то с ними сделать. В этот раз мой выбор пал на маркетплейс NFT OpenSea. Меня осенило что блокчейн - это про открытые данные, а учитывая 1.2 миллиона транзакций в сети ETH каждый день - то это ещё и много данных, так что точно должно быть интересно. В этом туториале я расскажу откуда можно достать данные о транзакциях блокчейна ETH, и как эти данные анализировать, в частности, как находить самые дорогие транзакции. И самое главное - бонус, небольшая игра в сыщиков в конце статьи. Вопросов с тем, откуда доставать данные у меня практически не было. Наверняка, большинство из тех кто имел дело с ETH знает что такое Etherscan. Если кратко, то это платформа, на которой можно посмотреть детальную информацию по каждой транзакции и в целом краткую статистику про Etherium. Оставалось надеяться на то, что у них есть API. Регистрируемся и забираем 100 тысяч бесплатных обращений в день, достаточно щедро, вряд ли нам понадобится больше. Давайте разберёмся с тем, какие эндпоинты API нам понадобятся. Находим достаточно удобный, возвращающий логи исполнения смарт контрактов. Находим адрес смарт контракта OpenSea и пишем достаточно простой запрос. Данный эндпоинт в качестве входных данных также принимает номер блока, начиная с которого он собирает логи смарт контракта. Узнать номер текущего последнего сгенерированного блока можно либо на сайте Etherscan, либо можно обратиться к ещё одному эндпоинту, который в качестве параметра принимает время, timestamp и возвращает номер блока ближайшего к этому времени. Передав текущее время получаем номер последнего сгенерированного блока. В силу того, что первый эндпоинт возвращает максимум 1000 транзакций получить сразу все транзакции с OpenSea за один запрос у нас не получится. Путём экспериментов я установил, что примерно в 40 блоках набирается 600-900 транзакций с OpenSea. Таким образом: Давайте разберёмся с тем, какие данные отдаёт нам наш эндпоинт. А отдаёт он нам массив примерно таких вот структур. Выглядит устрашающе, но давайте разбираться. Для начала отмечу, что все данные тут в hex формате, то есть в системе счисления с основанием 16. Самая вкуснятинка и нужные нам данные кроются в topics и  data, но что это такое и как это понимать? Во время своей работы смарт контракты создают события, таким образом они уведомляют о том что была вызвана какия-либо функция или метод. Лог транзакции состоит из событий, сгенерированных смарт контрактами, с которых было взаимодействие в рамках транзакции. В случае смарт контракта OpenSea при продаже NFT от одного пользователя другому создаётся событие с названием OrdersMatched. Вот так вот выглядит это событие, при просмотре деталей транзакции через Etherscan. Данные, передаваемые событием хранятся в объекте topics и data. topics - это массив, который содержит в себе данные о событии и его параметрах. Максимальный размер массива topics - 4 элемента. topics[0], нулевой элемент массива - это “подписанное” название метода. Остальные элементы массива topics и “кусок даных” data содержат в себе более детальные данные о транзакции - продавца, покупателя и цену сделки. Более детально рассмотрим эти данные, на базе нашего метода и примера ответа API: Событие OrdersMatched вместе со всеми своими параметрами. topics[0] - подписанное название метода и типов данных его параметров. Таким образом подписывается название метода OrdersMatched, совместно с типами параметров метода: topics[1] - index_topic_1 address maker адрес продавца, 20 байтов, который дополнен нулями до 32. topics[2] - index_topic_2 address taker адрес покупателя, также 20 байт, дополненные нулями до 32. topics[3] - index_topic_3 bytes32 metadata метаданные, по опыту скарпинга транзакций в большей части случаев - нулевые данные. Все те параметры функции, которые “не влезли” в topics передаются в data. В случае рассматриваемой функции это будут bytes32 buyHash, bytes32 sellHash и uint256 price. Все три этих параметра просто напросто конкатенированы в один кусок данных в порядке их указания в параметрах функции. Наиболее интересный нам параметр, цена сделки, хранится в последних 256 битах куска данных (то есть 32 байта и 64 символа hex представления). Аккуратно взяв кусочек строки и конвертировав его в число мы сможем узнать цену: Помним о том, что смарт контракты оперируют с wei - наименьшей делимой частью ETH. Собирая всё вместе, получаем приблизительно такой код: У описанного выше метода есть один небольшой недостаток, с которым я столкнулся, проанализировав несколько тысяч транзакций. Недостаток этот заключается в том, что в параметрах события метода OrdersMatched не передаётся валюта, в которой была совершена покупка NFT. В большинстве случаев эта валюта, разумеется, ETH, однако иногда проскакивают и другие токены. Давайте разберёмся с тем, как же определять валюту транзакции. Возьмём для примера две транзакции, одна была проведена в эфирах, другая в ASH. Транзакция раз, транзакция два. 150 ASH 115 ETH В рамках того эндпоинта, который возвращает нам логи событий транзакций, эти две выглядят практически одинаково, не считая цены. Однако 115 ETH это куда больше чем 150 ASH. Нам нужен метод, который позволит надёжно определять валюту транзакций. Более детально покопавшись в API Etherscan можно найти метод eth_getTransactionReceipt, который по хэшу транзакции возвращает более детальную сводку событий смарт контрактов, связанную с этой транзакцией. В контексте Python обратиться к этому эндпоинту можно вот так: Для транзакции в 150 ASH ответ будет выглядеть вот так: Ссылка на такой формат Самая приятная часть ответа данного эндпоинта заключается в том, что в нём содержатся все события, сгенерированные смарт контрактами, с которыми было взаимодействие в рамках данной транзакции. Это значит, что в случае покупки NFT за ERC-20 токен, в транзакции отобразится взаимодействие со смарт контрактом данного ERC-20 токена. В контексте упомянутой выше транзакции на 150 ASH это будут методы Approval и Transfer смарт контракта ASH. Можно заметить, что в методе Transfer в качестве одного из параметров присутствует цена транзакции в  150 * 10**18. Этого инсайта нам хватит для того, чтобы написать парсер, который будет также понимать валюту транзакции. Функция определения валюты транзакции, реализованная на Python Логика работы этой функции заключается в том, что мы смотрим на все события смарт контрактов, связанные с нашей транзакцией. Если в данных одного из событий метода Transfer фигурирует цена сделки, то это будет обращение к смарт контракту ERC-20 токена. На базе адреса смарт контракта этого токена мы сможем установить, что это за валюта. Смотреть на то, как кто-то продаёт кому-то токены ERC-721 - это конечно же интересно. Но куда более интересно ещё и знать что это за токены: как называется их коллекции и каков порядковый номер токена. Упомянутый в первом разделе статьи эндпоинт также не возвращает данные касательно токена ERC-721, фигурирующего в транзакции. Однако эндпоинт  eth_getTransactionReceipt возвращает все связанные с транзакцией события смарт контрактов, а значит там должны быть и детали касательно токенов ERC-721. Действительно, при более детальном рассмотрении событий смарт контрактов можно понять что именно купили в транзакции. На примере транзакции в ETH можно заметить что помимо события OrdersMatched есть ещё два других события - Approval и Transfer. Так как в данной транзакции не было обращения к смарт контрактам ERC-20 токенов, можно предположить, что это события смарт контракта ERC-721 токена. Более детально рассмотрев адрес смарт контракта можно понять, что это адрес коллекции BoredApeYachtClub (BAYC). А в методе Transfer фигурирует номер токена коллекции, который был переведён. Таким образом, с помощью эндпоинта eth_getTransactionReceipt можно также получить более детальную информацию о токене NFT, который был куплен. Оборачивая написанный текст в код, получим: Логика работа функции похожая на логику функцию из предыдущего раздела, только в данном примере мы берём данные из метода Transfer, не связанного с ERC-20 смарт контрактами (где не фигурирует цена сделки). В рамках данной функции может случиться такое, что номер токена коллекции совпадёт с ценой (это крайне маловероятно, но всё же). Данной проблемы можно избежать дополнительно проверяя параметры события Approval. Так выглядит событие Approval для ERC-721 токена А вот так для ERC-20 С помощью такой дополнительной проверки можно исключить упомянутый выше баг. Для того чтобы на базе адреса контракта коллекции можно было получить название мне пришлось воспользоваться API BlockScout. В целом API хорошее и удобное, даже есть GraphQL интерфейс, однако каким-то невероятным образом в базе данных этой системы нет части транзакций, причём целыми блоками, что делает этот сервис не самым надёжным основным источником данных. Однако же данные касательно смарт контрактов в данном сервисе достаточно полноценны. Написанный ниже код на Python на базе адреса смарт контракта ERC-721 токена возвращает название коллекции. Весь же ответ данного эндпоинта выглядит вот так: Пример для коллекции BoredApeYachtClub. Разобрались с тем как данные получать и анализировать, давайте это всё дело автоматизируем и красиво упакуем. На базе ранее написанного кода можно собрать полноценный скарпер и парсер транзакций, остально только написать скрипт по его периодическому запуску и сохранять данные в базу данных. В своих предыдущих проектах я зачастую прибегал к слегка костыльному подходу для запуска скриптов по расписанию - использовал crontab внутри докера. В этот раз захотелось сделать что-то понадёжнее и поинтереснее. Не буду опускаться в детали касательно реализации системы в рамках данного поста, но приведу краткую сводку касательно выбранной архитектуры. После небольшого ресёрча мой выбор пал на Celery + RabbitMQ для запуска проектов по расписанию. Результаты скарпинга я решил сохранять в Redis и каждый час через Celery запускать процесс по их анализу и сохранению результатов в MongoDB. Ну и для полной красоты процесса я ещё использовал Mongo-Express и Redisinsight для того чтобы следить что с базами данных всё в порядке. В рамках своего проекта я решил каждый час публиковать статистику по объёму торгов за час и несколько самых крупных транзакций с помощью телеграм бота и бота в твиттере. А сделал я это с помощью библиотек telebot и tweepy. В целом, хочется сказать, что у Твиттера оказалось на удивление простое и приятное API. В какой-то момент финальных стадий разработки этого проекта столкнулся с тем, что все крупные транзакции за час - была перепродажа одного и того же экземпляра NFT. Сначала я подумал что это баг, перепроверил весь свой код, но так и ничего не нашёл, пришлось внимательно изучать транзакции вокруг Auidioglyphs #1935. Предлагаю читателям включить режим сыщиков и разобраться что же не так с этими транзакциями.  Если кратко, то кто-то сознательно или бессознательно накручивал объём продаж вокруг данной коллекции. В среднем цена на OpenSea за один экземпляр данной коллекции колеблется около 0.01 - 0.1 ETH за штуку, а каждая транзация с Auidiogluphs #1935 была примерно на 180 - 190 ETH.  Интересно отметить, что OpenSea достаточно быстро заметили данную накрутку и перестали регистрировать похожие транзакции в статистике объёма примерно на следующий день. Более того, название коллекции подозрительно на коллекцию известной студии Larva Labs - Autoglyphs. Таким образом, некий аноним, вывев с биржи FTX порядка 200ETH в второй трети января промотал порядка 30 из них на такие вот транзакции (и знатно поспамил моим ботам). Поразбиравшись с транзакциями я решил в итоге забанить коллекцию в выдаче твиттер и телеграм ботов и побарагузил об этом в твиттере и телеграм канале.  P.S. Ещё одно интересное наблюдение, которое также можно сделать небольшим расследованием. На заставке данной статьи, как и на заставке моих ботов, изображения из коллекции Cryptopunks. Они торгуются на OpenSea, там же можно посмотреть статистику по их продажам, а вот на смарт контракте OpenSea это никак не отображается, разберётесь в чём загвоздка? Business Strategy & Operations at Revolut Crypto