При торговле любым активом нужно понимать какое-сейчас состояние рынка и для NFT это не исключение. В данном туториале я покажу, как собрать информацию об объеме продаж в разрезе коллекций за последние 24 часа в блокчейне TON используя Python.

Зачем это нужно? 

Прежде чем нырять в код и то, как работает индексатор dton.io поговорим, зачем нам это может понадобиться. Допустим мы хотим приобрести NFT для дальнейшей перепродажи, что грубо говоря нам важно: 

  1. Рост цены коллекции - чтобы перепродать дороже чем купили

  2. Ликвидность - чтобы при росте цены нашелся покупатель

  3. Примерное понимание, что первые два пункта не следствие мошеннических действий - например отмывочной торговли (как искать отмывочные сделки в коллекциях рассказывал в [другой статье](https://t.me/ton_learn/43))

Если отойти от общих слов про ликвидность и рост цены, то алгоритм мог бы быть следующий:

  1. Понять какое сейчас общее состояние рынка и конкретных блокчейнов

  2. После принятия решения об удачном состоянии рынка для покупки и выбора блокчейна, узнать какие коллекции популярны сейчас на этом блокчейне

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

  4. После выбора коллекции искать недооцененные элементы в них для покупки

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

Пример результата который мы получим, можно посмотреть здесь.

Как в поиске информации поможет dton.io?

Dton.io индексатор блокчейна, что значит, он собирает информацию из каждого нового блока в свою базу данных. В эту базу данных можно делать GraphQL запросы и таким образом собирать историческую информацию без парсинга всей цепочки блоков.

Две самых важных источника данных dton.io это таблица транзакций и представление последнего состояния кошельков/аккаунтов в сети. Делай запросы в данной представление и таблицу, можно собрать почти любую информацию. 

Важно отметить, что индексатор dton.io собирает не только сырые данные из блокчейна TON, но и обогащает данные - это очень удобно, что мы ниже увидим на примере.

Устанавливаем зависимости

Как мы знаем GraphQL запросы отправляются в теле HTTP POST-запроса, поэтому установим requests и прочие необходимые зависимости:

# install requriments

!pip install requests

import requests

import string 

import time

from itertools import groupby

Обозначим и эндпойнт dton.io:

endpoint = 'https://dton.io/graphql/'

Посчитаем сколько всего продаж NFT

Так как мы хотим посчитать количество транзакций за определенное время воспользуемся представлением lastTransactionCountSegments:

#how many sales were there in the last 24 hours

def getDaySaleNum(endpoint):

  query = '''

    query {

    lastTransactionCountSegments(

	segmentation: "hour"

    ) { cnt segment}

  }

Данное представление возвращает количество транзакций по сегментам, самое большее час. Так как данные можно получать страницами и указывать число данных на странице(максимум 150), то получить данные за 24 часа можно добавив `page_size: 24` Но ведь нам нужны не все транзакции, а только продажи…. .

Чтобы понимать как отсечь продажи, нужно разбираться в стандартах смарт-контрактов, конкретно здесь нужно понимать как работает продажа NFT в TON. У меня есть отдельный туториал с разбором смарт-контрактов продажи, а в данном туториале скажу, что главное для нас, это, то, что у смарт-контрактов продажи есть особый get-метод `get_sale_data`, который и позволит нам вытащить из транзакций все транзакции связанные с продажами.

Так как в продаже участвует несколько транзакций, нам нужна, так которая как бы закрывает продажу и здесь на помощь приходит, то, что в dton.io есть обогащение данных, понять закрывает ли транзакцию сделку, поможет поле `parsed_seller_is_closed: 1`. Получаем:

#how many sales were there in the last 24 hours

def getDaySaleNum(endpoint):

  query = '''

    query {

    lastTransactionCountSegments(

      segmentation: "hour"

      account_state_state_init_code_has_get_sale_data: 1

      parsed_seller_is_closed: 1

      page_size: 24

    ) { cnt segment}

  }

  '''

  response = requests.post(endpoint, json={'query': query})

  data = response.json()['data']['lastTransactionCountSegments']

  dayTxes = sum(item['cnt'] for item in data)

  return dayTxes

В конце суммируем и получаем число продаж NFT в сети TON за 24 часа. 

Опытный читатель, может сказать, что это не учитывает все возможные ситуации право передачи NFT, так как их три:

  • продажа

  • аукцион

  • оффер(предложение о покупке с последующей покупкой)

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

Соберем все продажи для последующей группировки 

Теперь мы знаем сколько транзакций нам нужно достать. Учтем, что так как на странице можно достать только 150 значений, то нам придется отправлять запросы партиями, конкатенируя их в один запрос. Сразу скажу лишние продажи отрезать не будет, чтобы не усложнять туториал:

day_sales_count = getDaySaleNum(endpoint)

page_size = 100

needed_queries = day_sales_count // page_size + 1

# собираем запрос

formatted_query =""

for batch_num in range(needed_queries):

  print(batch_num)

  template = string.Template("""

    q${page}: transactions(

      account_state_state_init_code_has_get_sale_data: 1

      parsed_seller_is_closed: 1

      workchain: 0

      page_size: ${page_size}

      page: ${page}

      ) {

      collection: parsed_seller_nft_collection_address_address

      price: parsed_seller_nft_price

      nft: parsed_seller_nft_address_address

      block_time:gen_utime

      }

  """)

  formatted = template.substitute(page=batch_num,page_size=page_size)

  formatted = formatted +"\n"

  formatted_query += formatted

formatted_query = """query {"""+formatted_query+"""}"""

formatted_query

response = requests.post(endpoint, json={'query': formatted_query})

res_arr = []

for v in response.json()['data'].values():

  for item in v:

    if(item['collection'] is not None):

      res_arr.append(item)

print("Collected txes: ",len(res_arr))

Вы можете заметить, что мы не берем NFT без коллекции(где None), да стандарт NFT в TON подразумевает возможность NFT без коллекции, так как в отличие от EVM сетей, NFT это не один контракт, хранящий некоторый базу записей, а один смарт-контракт коллекции и отдельные контракты для каждого элемента.

Также, хорошо знакомый с технологией блокчейн читатель может сказать, а зачем делать два запроса, сначала сколько, а потом уже транзакции, можно же достать с каждой транзакцией время генерации блока, и выгружая подряд все транзакции выйти из цикла на блоках, которые были через 24 часа. Да, это так, но для простоты читаемости кода и чтобы показать вам по больше про dton.io я решил делать именно через два запроса.

Двигаемся дальше.

Группируем и считаем количество транзакций и объем продаж

Относительно коллекции просуммируем продажи, это будет объем продажи, а также посчитаем количество транзакции. Зачем нам и количество и сумма? Одна сумма плохо отражает ситуацию в коллекции, одна крупная продажа в день для нас хуже чем много средних - для перепродажи важна ликвидность.

#We group the data by collection - collection, number of sales, sales volume and of course sort

# define a function for key

def key_func(k):

    return k['collection']

# sort INFO data by 'company' key.

INFO = sorted(res_arr, key=key_func)

count_arr=[]

for key, value in groupby(INFO, key_func):

    #print(key)

    #print(list(value))

    sum=0

    count=0

    for item in list(value):

      count = count + 1

      sum = sum + int(item['price'])

      #print(item['employee'])

    #print("Txes:",count)

    #print("Sum:",sum/1000000000)

    temp_d = {'col': key, 'txes': count, 'sum': round(sum/1000000000)}

    count_arr.append(temp_d)

newlist = sorted(count_arr, key=lambda d: d['sum'], reverse=True)

print("Size of sorted Arr: ",len(newlist))

Посмотрим, что у нас есть:

newlist[0:5]

[{'col': 'B774D95EB20543F186C06B371AB88AD704F7E256130CAF96189368A7D0CB6CCF',

  'txes': 59,

  'sum': 332419},

{'col': 'E06664290C96CEEA5687BEF89B6976173251006041FA2E752D039275F56D7C74',

  'txes': 2,

  'sum': 100098},

{'col': '28F760D832893182129CABE0A40864A4FCC817639168D523D6DB4824BD997BE6',

  'txes': 32,

  'sum': 12053},

{'col': 'D398C94E9AB5BE730AA5E4DBCEF4754F3FB1B6805B61BEB15B31052E6C0E838C',

  'txes': 106,

  'sum': 11737},

{'col': 'B3B928E4814343EB597B8081F736F55636D674457D61C73D6B5A7989A863E666',

  'txes': 3,

  'sum': 10052}]

Выглядит, не очень понятно, что за коллекция, да и что за громоздкий адрес.

Адреса в TON и небольшой трюк с эксплорером

Адреса смарт-контрактов, работающих в TON, выражаются в двух основных форматах:

  • Raw hex addresses: исходное полное представление адресов смарт-контрактов

  • User-friendly addresses: расширенные адреса для удобства пользователей

Подробнее про адреса в документации. Чтобы перевести адрес из одного вида в другой воспользуемся скриптом, который в данном туториале разбирать не будем, но в самом конце туториала будут все ссылки на код.

Также было бы круто получить данные о коллекции, название, картинку коллекции, а также вообще посмотреть, что за коллекция. Можно конечно достать все данные из блокчейна, но стандарт NFT в TON не ориентирован на быстрый доступ ко всей коллекции, поэтому для первого шага, предлагаю воспользоваться тем, что один из эксплореров позволяет посмотреть коллекции по адресу коллекции. Воспользуемся этим.

for row in newlist:

  row['explorer_link'] = "https://tonscan.org/nft/"+account_forms('0:'+row['col'])['bounceable']['b64url']

newlist[0:5]

Получим:

[{'col': 'D398C94E9AB5BE730AA5E4DBCEF4754F3FB1B6805B61BEB15B31052E6C0E838C',

  'txes': 108,

  'sum': 289703904,

  'explorer_link': 'https://tonscan.org/nft/EQDTmMlOmrW-cwql5NvO9HVPP7G2gFthvrFbMQUubA6DjH7-'},

{'col': 'B774D95EB20543F186C06B371AB88AD704F7E256130CAF96189368A7D0CB6CCF',

  'txes': 34,

  'sum': 510053,

  'explorer_link': 'https://tonscan.org/nft/EQC3dNlesgVD8YbAazcauIrXBPfiVhMMr5YYk2in0Mtsz0Bz'},

{'col': '80D78A35F955A14B679FAA887FF4CD5BFC0F43B4A4EEA2A7E6927F3701B273C2',

  'txes': 20,

  'sum': 301248,

  'explorer_link': 'https://tonscan.org/nft/EQCA14o1-VWhS2efqoh_9M1b_A9DtKTuoqfmkn83AbJzwnPi'},

{'col': 'A27781584DA7136F20D0D3BC1ECEC5D00BCC82C720E08BF0A81BD3063D704628',

  'txes': 13,

  'sum': 30868,

  'explorer_link': 'https://tonscan.org/nft/EQCid4FYTacTbyDQ07wezsXQC8yCxyDgi_CoG9MGPXBGKLJ_'},

{'col': '82AADDD0778EE241E576637F17D1DA83E85AA1CAC4D7B47D5B075D6A089CB218',

  'txes': 264,

  'sum': 17350,

  'explorer_link': 'https://tonscan.org/nft/EQCCqt3Qd47iQeV2Y38X0dqD6FqhysTXtH1bB11qCJyyGG8G'}]

Перейдя по ссылке мы можем посмотреть коллекцию, но что если, хочется самому достать данные.

Самый простой вариант получить данные о коллекции

Самый удобный вариант это получить данные по http протоколу. Поскольку узлы TON используют собственный транспортный протокол ADNL(о нём ниже), для HTTP-соединения необходим промежуточный сервис.

Toncenter  — это такой промежуточный сервис, получающий запросы по HTTP, он обращается к liteserver’s сети TON. У него есть метод /getTokenData,который возвращает информацию о токене в соответствии со стандартом данных токенов на TON.

API_ENDPOINT = 'https://toncenter.com/api/v2'

for row in newlist:

  try:

    payload = {'address': '0:'+row['col']}

    r = requests.get(API_ENDPOINT+'/getTokenData', params=payload)

    col_content = r.json()

    print(col_content)

    # limit because we dont use api key 

    time.sleep(1.1)

  except:

    print('some error')

Итого мы получаем проранжированные коллекции, готовые к просмотру, примерный результат можно посмотреть здесь.

Данные о коллекции напрямую из блокчейна без посредников

Выше упоминалось, что у TON есть свой транспортный протокол, но что мы должны запросить у Liteserver? Чтобы понять это, нужно разобраться, а что вообще представляет из себя коллекция, разбор есть тут.

Допустим мы разобрались и понимаем, что смарт-контракт является NFT коллекцией в TON если соответствует некоторым условиям, одно из которых, что должен быть метод get_collection_data(), который вернет эти данные.

Таким образом, нам надо вызвать GET-метод get_collection_data() у смарт-контракта коллекции и таким образом обогатить данные.

Сбор данных через ADNL, довольно объемная тема, которую я осветил здесь, и  через какое-то время опубликую это на habr.

Заключение 

Мне нравится блокчейн TON своей технической изящностью, как минимум это не очередная копия Ethereum, которую разгоняют с помощью большого капитала без оглядки, а вообще зачем это нужно пользователю. Если вы хотите узнать больше о блокчейне TON, у меня есть опенсорсные уроки, благодаря которым вы научитесь создавать полноценные приложения на TON.

https://github.com/romanovichim/TonFunClessons_ru

Новые туториалы и дата аналитику я кидаю сюда: https://t.me/ton_learn

Посмотреть, ранжирование коллекций из данного туториала в удобном виде можно здесь.

Код из данного туториала: ArticleNFTCollectionRanking.ipynb

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


  1. FreakII
    21.09.2023 10:28
    +5

    NFT разве ещё кому-то нужны?

    Your NFTs Are Actually — Finally — Totally Worthless


    1. IvanRomanovich Автор
      21.09.2023 10:28

      Судя по данным продажи есть, а вот нужны или не нужны, не подскажу, я по технической части - как достать данные или смарт-контракты как писать


    1. phillennium
      21.09.2023 10:28

      По моим ощущениям, 95% NFT действительно обесценились, и 95% людей смеются над заголовками об этом,

      но тем временем другие 5% зарабатывают на других 5% (вот и текст показывает, что продажи есть)

      Кому в таком случае стоит смеяться — по-моему, вопрос спорный)