Для любой технической задачи на TON, вам необходимо использовать индексаторы. Индексаторы это сервисы, которые агрегируют внутри себя транзакции блокчейна, обогащают данные и позволяют получить эти данные в необходимом виде.
Без использования таких сервисов, для каждого запроса информации, вам бы пришлось парсить кучу блоков блокчейна, чтобы вернуть данные. В данной статье, я покажу вам как делать GraphQL запросы в dton.io на блокчейне TON. Возьмем простую задачу и пройдем весь путь формирования запроса и параллельно рассмотрим основные возможности индексатора.
Что такое dton.io?
Dton.io индексатор блокчейна, что значит, он собирает информацию из каждого нового блока в свою базу данных. В эту базу данных можно делать GraphQL запросы и таким образом собирать историческую информацию без парсинга всей цепочки блоков.
Две самых важных источника данных dton.io это таблица транзакций и представление последнего состояния кошельков/аккаунтов в сети. Делая запросы в данной представление и таблицу, можно собрать почти любую информацию.
Большим плюсом dton.io, является обогащение данных, помимо нативных данных транзакций и блоков, dton.io собирает информацию из регистра данных смарт-контрактов, а также обогащает данными популярные стандарты токенов на TON, такие как NFT - стандарт не взаимозаменяемых токенов и Jetton - стандарт взаимозаменяемых токенов.
Обогащение данных позволяет уменьшить количество необходимых запросов.
Как происходит процесс формирования запроса
Верхнеуровневый алгоритм всегда следующий:
понять как задача выглядит на уровне смарт-контрактов
иттеративный сбор запроса + борьба с возникающими проблемами(вроде таймаута на очень крупных запросах), валидация правильности данных
В силу специфики архитектуры смарт-контрактов на TON, первая часть зачастую является самой сложной - одной логической операции, например поставка ликвидности в пул, может участвовать несколько контрактов и конечно же искать, где там среди 5 контрактов передается нужная вам информация получиться, только погружаясь в смарт-контракты.
Кстати запросы, которые мы будем рассматривать ниже, вы можете запутить тут - https://dton.io/graphql/
Формулируем задачу
В рамках данного туториала, мы возьмем понятную широкому кругу задачу: найти самых крупных покупателей NFT для выбранной коллекции.
Предпосылкой такого запроса может быть, желание найти, кому продать свою нфт. Но в данном туториале это просто удобная задача, через которую можно показать большую часть функционала dton.io.
Что это значит с точки зрения смарт-контрактов
Прежде чем прыгать с головой в поля доступные в индексаторе и думать, как нам агрегировать данные по сумме и количеству сделок, давайте поймем как вообще происходят продажи NFT в TON.
Почти любая сделка подразумевает некий контракт продажи. на который “перемещают NFT”, данный контракт реализует логику продажи, это может быть как просто продажа любому желающему заплатить сумму, так и сложная логика в виде аукциона.
Соответственно с точки зрения данных продажей можно назвать транзакцию в которой меняется собственник NFT. Посмотреть примеры разных контрактов продаж можно тут - https://github.com/ton-blockchain/token-contract/blob/main/nft/nft-sale.fc. Это простейший контракт продажи написанный на языке FUNC.
Теперь попробуем сделать первый запрос.
Достанем последние транзакции с продажами nft
Итак, нас интересуют транзакции с передачей права собственности NFT, значит будем использовать представление транзакций:
{
raw_transactions(
) {
}
}
Теперь попробуем наполнить запрос, посмотреть все доступные поля можно в документации https://docs.dton.io/transactions-and-account-states .
Возьмем для примера коллекцию NFT Telegram Username, а также уже законченные сделки:
{
raw_transactions(
parsed_seller_is_closed: 1
parsed_nft_collection_address_address: "80D78A35F955A14B679FAA887FF4CD5BFC0F43B4A4EEA2A7E6927F3701B273C2"
) {
nft_address: address
col_address: parsed_nft_collection_address_address
}
}
Поля начинающиеся с parsed это обогащенные, а не нативные поля. Теперь, нам важно убрать транзакции в которые происходят не с NFT, для этого добавим поле account_state_state_init_code_has_get_nft_data: 1 . Здесь может возникнуть вопрос что за get_nft_data, самым простым способ отличать разные типы смарт-контрактов, является их сигнатура, смарт-контракты определенных стандартов, должны содержать определенные функции и вызовы. Стандарт NFT в TON предполагает наличие GET метода get_nft_data.
{
raw_transactions(
parsed_seller_is_closed: 1
account_state_state_init_code_has_get_nft_data: 1
workchain: 0
page: 0
page_size: 10
parsed_nft_collection_address_address: "80D78A35F955A14B679FAA887FF4CD5BFC0F43B4A4EEA2A7E6927F3701B273C2"
) {
prev_owner: parsed_seller_nft_prev_owner_address_address
new_owner: parsed_seller_nft_new_owner_address_address
nft_address: address
col_address: parsed_nft_collection_address_address
price: parsed_seller_nft_price
}
}
Также сразу добавим parsed значения цены и предыдущего и нового владельца. Предлагаю выполнить запрос.
Проблема таймаут
После выполнения запроса мы увидим:
{
"data": {},
"errors": [
"Timeout exceeded, simplify your query (https://docs.dton.io/query-speed-optimization-timeout)"
]
}
Проблема заключается в том, что наш запрос, отрабатывает по всей базе индексатора. Возможные варианты решения проблемы описнны тут https://docs.dton.io/query-speed-optimization-timeout. Самым простым вариантом явлется добавления фильтра по времени.
Для этого нам понадобятся фильтры.
Фильтруем запрос по времени
Фильтры в dton.io на поля можно добавлть с помощью нижнего подчеркивания. Фильтр на время генерации один из самых распространенных - он позволяет “не бегать” запросом по всей базе.
Сузим нашу задачу и будем искать только за последний месяц:
{
raw_transactions(
parsed_seller_is_closed: 1
account_state_state_init_code_has_get_nft_data: 1
workchain: 0
page: 0
page_size: 10
gen_utime__gt: "2024-12-04 00:00:00"
parsed_nft_collection_address_address: "80D78A35F955A14B679FAA887FF4CD5BFC0F43B4A4EEA2A7E6927F3701B273C2"
) {
prev_owner: parsed_seller_nft_prev_owner_address_address
new_owner: parsed_seller_nft_new_owner_address_address
nft_address: address
col_address: parsed_nft_collection_address_address
price: parsed_seller_nft_price
}
}
Список доступных фильтров здесь - https://docs.dton.io/filters
В запросе вы можете видеть page и page_size, это обычная пагинация. Отображения транзакции и состояний аккаунтов поддерживают максимум 150 записей для страницы.
Попробуем запустить запрос, получим примерно следующее:
{
"data": {
"raw_transactions": [
{
"prev_owner": "CF01D60CC8924D3C34C54A5787B9175BCD8D45D9ADA91371F93318DF2B76EBDB",
"new_owner": "69B8D2CA45C14F056FED73236A6A0CB7AB5BA2B1A0F2E1AD784D84F7B4454D81",
"nft_address": "8C08EBCDDEDB79E6FF3AA1B4F0A65388D16FE2F1D86BFBA46E25833A24A659F4",
"col_address": "80D78A35F955A14B679FAA887FF4CD5BFC0F43B4A4EEA2A7E6927F3701B273C2",
"price": 4746195291
},
{
"prev_owner": "50FC81C8CFEA8780CEF8B636D3643ED9F7E5ADD3D4E8045510FD19AF2EAC337D",
"new_owner": "0E4A5829EEBC85FB049DD63B5CCF801D3DC411780ABEC22F8F8918B66B852337",
"nft_address": "E20696CD521BF88D729E06827587D2908E0ED94ED5910A79D26069C4F9F261AB",
"col_address": "80D78A35F955A14B679FAA887FF4CD5BFC0F43B4A4EEA2A7E6927F3701B273C2",
"price": 8479134769
}
…
]
},
"errors": []
}
Убираем отмывочную торговлю
Как я и упоминал в начале статьи, сборка запроса итеративный процесс, а значит пришло время вернуться к бизнес задаче. Для интересующей нас статистики, нам важно убрать отмывочную торговлю, например, когда перепродажа идет между своим же кошельком.
В этом нам помогут фильтры состоящие из логических операторов, в данном случае not. Уберем из запроса транзакции, в которых старый и новый владельце одинаковые.
{
raw_transactions(
parsed_seller_is_closed: 1
account_state_state_init_code_has_get_nft_data: 1
workchain: 0
page: 0
page_size: 10
gen_utime__gt: "2024-12-04 00:00:00"
not__: {parsed_seller_nft_prev_owner_address_address: new_owner}
parsed_nft_collection_address_address: "80D78A35F955A14B679FAA887FF4CD5BFC0F43B4A4EEA2A7E6927F3701B273C2"
) {
prev_owner: parsed_seller_nft_prev_owner_address_address
new_owner: parsed_seller_nft_new_owner_address_address
nft_address: address
col_address: parsed_nft_collection_address_address
price: parsed_seller_nft_price
}
}
Удобные фичи - преобразуем адреса
Как и в других блокчейнах в TON существуют разные представления адресов, чтобы привести адреса в запросе в чаще всего употребимый вид, нужно поменять поля на такие же поля но с приставкой friendly.
В нашем случае получиться примерно вот так:
{
raw_transactions(
parsed_seller_is_closed: 1
account_state_state_init_code_has_get_nft_data: 1
workchain: 0
page: 0
page_size: 10
gen_utime__gt: "2024-12-04 00:00:00"
not__: {parsed_seller_nft_prev_owner_address_address: new_owner}
order_by: "parsed_seller_nft_price",
order_desc: true,
parsed_nft_collection_address_address: "80D78A35F955A14B679FAA887FF4CD5BFC0F43B4A4EEA2A7E6927F3701B273C2"
) {
prev_owner: parsed_seller_nft_prev_owner_address_address__friendly
new_owner: parsed_seller_nft_new_owner_address_address__friendly
nft_address: address__friendly
col_address: parsed_nft_collection_address_address__friendly
price: parsed_seller_nft_price
}
}
Если вам интересно почитать про разные представления адрессов, в TON есть отдельный стандарт :https://github.com/ton-blockchain/TEPs/blob/master/text/0002-address.md
Агрегация данных
После того как мы собрали простой запрос, посмотрели, что в ответе на запрос мы получаем те транзакции, которые ожидали, можно перейти к сборке агрегированного запроса.
Для агрегации можно использовать endpoint’ы groupTransactions и groupAccountStates для выполнения операций агрегации. Эти endpoint’ы поддерживают различные функции агрегации и позволяют группировать, сортировать и фильтровать результаты.
В агрегационных запросов, довольно много параметров, именно поэтому изначально я советую собирать простые запросы и только потом из просто собирать агрегацию.
Поля, по которым мы фильтровали запрос кладем в filter. остальные же поля заполняем в соответствии с логикой агрегации:
{
groupTransactions(
by: ["parsed_seller_nft_new_owner_address_address__friendly"],
aggregations: [
{field: "parsed_seller_nft_price", operation: "sum"},
{operation: "count"}
],
order_by: "parsed_seller_nft_price__sum",
order_desc: true,
page: 0,
page_size: 10,
filter_by: {or__: [
{not__: {
parsed_seller_nft_prev_owner_address_address: parsed_seller_nft_new_owner_address_address
}},
{and__: {
parsed_seller_is_closed: 1
account_state_state_init_code_has_get_nft_data: 1
workchain: 0
gen_utime__gt: "2024-12-04 00:00:00"
parsed_nft_collection_address_address: "80D78A35F955A14B679FAA887FF4CD5BFC0F43B4A4EEA2A7E6927F3701B273C2"
}}]}
) {
trader_new_address:parsed_seller_nft_new_owner_address_address__friendly
deal_count: count
deal_sum: parsed_seller_nft_price__sum
}
}
С помощью by агрегируем по адресу нового владельца, в остальных полях прописываем логику подсчета по цене. Для репрезентативности посчитаем и сумму и количество сделок. Подробнее об агрегации здесь - https://docs.dton.io/aggregations.
Отмечу, что для выполения крупных агрегаций, необходим платный план dton.io
Заключение
Мне нравится блокчейн TON своей технической изящностью, как минимум это не очередная копия Ethereum, которую разгоняют с помощью большого капитала без оглядки, а вообще зачем это нужно пользователю. Если вы хотите узнать больше о блокчейне TON, у меня есть опенсорсные уроки, благодаря которым вы научитесь создавать полноценные приложения на TON.
https://github.com/romanovichim/TonFunClessons_ru
Новые туториалы и дата аналитику я кидаю сюда: https://t.me/ton_learn