Всем привет! Представим, что вам нужен доступ к данным каких-либо смарт-контрактов на Ethereum (или Polygon, BSC и т.д.), например, Uniswap, SushiSwap, AAVE (или даже PEPE-coin) в реальном времени, чтобы анализировать их с помощью стандартных инструментов дата-аналитиков: Python, Pandas, Matplotlib и т.д. В этом туториале я покажу инструменты для доступа к данным на блокчейне, которые больше похожи на хирургический скальпель (сабграфы The Graph), чем на швейцарский нож (доступ к RPC ноде) или, скажем, молоток (готовые API от компаний-разработчиков). Надеюсь, мои неумелые метафоры вас не пугают. Кому интересно научиться, добро пожаловать под кат.

Вот несколько методов доступа к данным в Ethereum:

  • Использование команд RPC-ноды, таких как getBlockByNumber, для получения информации о блоке на низком уровне, затем доступ к данным смарт-контракта с помощью библиотек, таких как web3.py. Это позволяет получать данные блок за блоком и затем собирать их в своей собственной базе данных или CSV-файле. Этот способ не слишком быстрый, и обработка данных популярного смарт-контракта обычно занимает годы.

  • Использование провайдеров аналитических данных, таких как Dune, которые могут помочь с данными какого-нибудь популярного смарт-контракта, но это не вполне реальное время. Задержка может составлять несколько минут.

  • Использование некоторых готовых API, таких как NFT API/Token API/DeFi API. И это может быть отличным вариантом, поскольку обычно задержка в них обычно мала. Единственная проблема, с которой вы можете столкнуться, - это отсутствие необходимых вам данных. Например, не все переменные могут быть доступны как исторические временные ряды.

Что, если вам все еще нужны данные смарт-контракта в режиме реального времени, но предыдущие решения не удовлетворяют вас, потому что вы хотите все:

  • вам нужны данные с низкой задержкой (данные всегда актуальны сразу после появления нового блока)

  • вам нужны данные, которые недоступны ни в одном готовом API

  • вам не хочется заморачиваться с ручной обработкой данных блок за блоком, а также обработкой реорганизации блоков

Это отличный кейс для использования сабграфов стандарта The Graph. Фактически, The Graph - это децентрализованная сеть для доступа к данным смарт-контрактов децентрализованным способом (с оплатой в их токенах GRT). Но основная технология, называемая «сабграфами», позволяет вам преобразовывать несложное описание того, какие переменные из кода смарт-контракта необходимо сохранить (для доступа в реальном времени) в настоящий ETL (Extract Transform Load) процесс, который:

  • Извлекает данные из блокчейна

  • Сохраняет их в базе данных

  • Делает эти данные доступными через интерфейс GraphQL

  • Обновляет данные после добычи каждого нового блока в сети

  • Автоматически обрабатывает реорганизации блокчейна

А это довольно круто. Вам не нужно быть высококлассным дата инженером, опытным в EVM-compatible блокчейнах, чтобы настроить весь рабочий процесс.

Но давайте начнем с чего-нибудь готового к использованию. Что, если кто-то уже разработал сабграф, который помогает вам получить доступ к необходимым данным?

Вы можете перейти на сайт хостингового сервиса The Graph, найти раздел community сабграфов и попытаться найти существующие сабграфы по нужному вам протоколу. Например, давайте найдем сабграф для доступа к протоколу Lido (который позволяет пользователям стейкать свои эфиры без ограничения на минимальное значения в 32 эфира, как сейчас это требуется в нативном Ethereum).

Сейчас протокол Lido занимает первое место по общей стоимости заблокированных средств (Total Value Locked - метрика, используемая для измерения общей стоимости цифровых активов, которые заблокированы в той или иной DeFi или DApp платформе) согласно DeFiLlama.

И вуаля! Сабграф, разработанный командой протокола Lido готовый к использованию в наличии.

Теперь перейдем на страницу этого сабграфа:

Что мы можем здесь увидеть:

  1. CID IPFS этого сабграфа — это внутренний уникальный идентификатор этого сабграфа, указывающий на файл манифеста этого сабграфа, хранящегося в IPFS (p2p-протокол для поиска файла по хэшу — это упрощенное утверждение, но если нужно, там легко разобраться, как это работает).

  2. URL для запросов — это эндпоинт, который мы будем использовать в нашем коде на Python для доступа к данным смарт-контракта.

  3. Индикатор статуса синхронизации сабграфа. Когда сабграф засинхронизирован, вы можете получить "свежие" данные, но вы также должны понимать, что при развертывании нового сабграфа вам придется подождать некоторое время, чтобы засинхронизировать его. Во время этого процесса UI будет показывать номер текущего блока в обработке.

  4. GraphQL-запрос. У каждого сабграфа есть своя структура данных (список таблиц с данными), которую необходимо знать при создании GraphQL запроса. В целом, GraphQL довольно легко изучить, но если у вас возникнут проблемы с этим, вы можете попросить ChatGPT помочь с этим ????. Вот пример использования такого "помощника" в конце статьи.

  5. Кнопка, которая запускает запрос.

  6. Окно вывода. Как вы можете видеть, ответ GraphQL имеет структуру, похожую на JSON.

  7. Переключатель, который позволяет вам увидеть структуру данных этого сабграфа.

Итак, перейдем к делу (расчехлим наши jupyter-ноутбуки ????).

Получение исходных данных:

import pandas as pd
import requests

def run_query(uri, query):
    request = requests.post(uri, json={'query': query}, headers={"Content-Type": "application/json"})
    if request.status_code == 200:
        return request.json()
    else:
        raise Exception(f"Unexpected status code returned: {request.status_code}")

url = "https://api.thegraph.com/subgraphs/name/lidofinance/lido"
query = """{
  lidoTransfers(first: 50) {
    from
    to
    value
    block
    blockTime
    transactionHash
  }
}"""

result = run_query(url, query)

Значение переменной result будет выглядеть следующим образом:

Теперь создадим Pandas DataFrame для данных:

df = pd.DataFrame(result['data']['lidoTransfers'])
df.head()

Но как скачать все данные из таблицы? С помощью GraphQL есть разные варианты, и я выбираю следующий. Учитывая, что блоки увеличиваются, давайте просканируем с первого блока, запрашивая 1000 сущностей за раз (1000 - это ограничение для graph-node).

query = """{
  lidoTransfers(orderBy: block, orderDirection: asc, first: 1) {
     block
  }
}"""
# here we get the first block number to start with
first_block = int(run_query(url, query)['data']['lidoTransfers'][0]['block'])
current_last_block = 17379510

#query template to make consecutive queries
query_template = """{{
  lidoTransfers(where: {{block_gte: {block_x} }}, orderBy: block, orderDirection: asc, first: 1000) {{
    from
    to
    value
    block
    blockTime
    transactionHash
  }}
}}"""

result = [] # storing the response
offset = first_block # starting from the first found block

while True: 
    query = query_template.format(block_x=offset) # generate the query
    sub_result = run_query(url, query)['data']['lidoTransfers'] # get the data
    if len(sub_result)<=1: # break if finished
        break
    sh = int(sub_result[-1]['block']) - offset # calculate the shift
    offset = int(sub_result[-1]['block']) # calculate the new shift
    result.extend(sub_result) # append
    print(f"{(offset-first_block)/(current_last_block - first_block)* 100:.1f}%, got {len(sub_result)} lines, block shift {sh}" ) #show the log

# convert to the dataframe
df = pd.DataFrame(result)

Имейте в виду, что мы делаем перекрывающиеся запросы, потому что каждый раз мы используем последний номер блока из запроса, чтобы начать следующий. Мы делаем это, чтобы избежать пропуска записей из-за возможных нескольких транзакций в одном блоке (фильтруем же мы по блоку).

Как можно заметить, каждый запрос возвращает 1000 строк, но номер блока сдвигается на несколько десятков тысяч. Это означает, что не каждый блок содержит хотя бы одну транзакцию контракта Lido. Важным шагом здесь является избавление от дубликатов, которые мы собрали, чтобы избежать пропуска записей:

Как видно более 9 тыс записей оказались дубликатами.

Теперь сделаем простенький EDA (Exploratory data analysis)

col = "from"
df.groupby(col, as_index=False)\
    .agg({'transactionHash': 'count'})\
    .sort_values('transactionHash', ascending=False)\
    .head(5)

Если мы проверим наиболее часто встречающиеся адреса в поле "from", мы найдем адрес "0x0000000000000000000000000000000000000000". Обычно это означает выпуск новых токенов, поэтому мы можем найти транзакцию в Etherscan и проверить:

(df[df['from']=='0x0000000000000000000000000000000000000000'].iloc[1000].to,\
df[df['from']=='0x0000000000000000000000000000000000000000'].iloc[1000].transactionHash,
df[df['from']=='0x0000000000000000000000000000000000000000'].iloc[1000].value,)

Мы можем найти транзакцию с тем же значением "value" на Etherscan (поиском):

Еще интересно посмотреть, какие адреса наиболее часто встречаются в поле "to" (получатель)

col = "to"
df.groupby(col, as_index=False)\
    .agg({'transactionHash': 'count'})\
    .sort_values('transactionHash', ascending=False)\
    .head(5)

Адрес “0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0” можно также найти на Etherscan как "Lido: wrapped stETH Token" - ликвидный токен смарт-контракта Lido - типа застейканный эфир (звучит как тарабарщина, да?).

Давайте поанализируем что-нибудь попроще, например, количество транзакций в месяц:

import datetime
import matplotlib.pyplot as plt

df["blockTime_"] = df["blockTime"].apply(lambda x: datetime.datetime.fromtimestamp(int(x)))
df['ym'] = df['blockTime_'].dt.strftime("%Y-%m")
df_time = df.groupby('ym', as_index=False).agg({'transactionHash': 'count'}).sort_values('ym')

fig, ax = plt.subplots(figsize=(12,8))
ax.plot(df_time['ym'].iloc[:-1], df_time['transactionHash'].iloc[:-1])
plt.xticks(rotation=45)
plt.xlabel('month')
plt.ylabel('number of transactions')
plt.grid()
plt.show()

Количество транзакций в месяц постоянно растет, в том числе год к году.

При желании, можете продолжить анализ, перейдя к другим полям или делая запросы к другим табличкам в том же сабграфе:

Что еще можно делать с сабграфами:

Во-первых, если вам нужен доступ к данным любого другого смарт-контракта, который пока не имеет публично доступного сабграфа (или данных недостаточно), вы можете написать свой собственный сабграф, используя следующие туториалы:

  1. How to access the Tornado Cash data easily using The Graph’s subgraphs

  2. How to access transactions of PEPE ($PEPE) coin using The Graph subgraphs and ChatGPT prompts

  3. Indexing Uniswap data with subgraphs

  4. A beginner’s guide to getting started with The Graph

Во-вторых, если вы готовы к погружению в детали разработки сабграфов, рекомендую ознакомиться с этими подробными туториалами:

  1. Explaining Subgraph schemas

  2. Debugging subgraphs with a local Graph Node

  3. Fetching subgraph data using javascript

В-третьих, если вы собираетесь использовать данные сабграфов в своем продакшен приложении, вы можете развернуть свой сабграф на хостинге сабграфов Chainstack Subgraphs (в процессе разработки которого я на сабграфах собаку съел), который в несколько раз быстрее синхронизирует сабграфы и уже имеет 99,9% SLA.

Далее, как вы уже поняли, написать свой сабграф не является совсем уж простой задачей для начинающих. Поэтому телеграм-бот SubgraphGPT может быть полезен при изучении для получения ответов, основанных на базе знаний The Graph. Про то, как я его разрабатывал, если интересно, можно почитать в предыдущей статье на хабре.

Кроме того, возможно вы найдете что-то интересное среди ресурсов, собранных в репозитории-коллекции ссылок и примеров awesome-subgraphs на GitHub.

И наконец, если вы все еще не чувствуете себя уверенно с сабграфами, не стесняйтесь задавать любые вопросы в телеграм-чате "Subgraphs Experience Sharing".

Надеюсь, материал был полезен, если вы дочитали до сюда. В следующих статьях я хотел бы еще перевести несколько материалов непосредственно про разработку сабграфов.

Успехов в использовании и разработке сабграфов!

Идея статьи появилась в ходе обсуждения в сообществе разработчиков Ethereum Ru

Комментарии (1)


  1. anonymous
    00.00.0000 00:00

    НЛО прилетело и опубликовало эту надпись здесь