Примерно раз в год у меня появляется неутолимая жажда накопать много данных и что-то с ними сделать. В этот раз мой выбор пал на маркетплейс NFT OpenSea. Меня осенило что блокчейн - это про открытые данные, а учитывая 1.2 миллиона транзакций в сети ETH каждый день - то это ещё и много данных, так что точно должно быть интересно.

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


Где достать данные?

Вопросов с тем, откуда доставать данные у меня практически не было. Наверняка, большинство из тех кто имел дело с ETH знает что такое Etherscan. Если кратко, то это платформа, на которой можно посмотреть детальную информацию по каждой транзакции и в целом краткую статистику про Etherium. Оставалось надеяться на то, что у них есть API.

Регистрируемся и забираем 100 тысяч бесплатных обращений в день, достаточно щедро, вряд ли нам понадобится больше.

Тарифы использования API Etherscan
Тарифы использования API Etherscan

Давайте разберёмся с тем, какие эндпоинты API нам понадобятся. Находим достаточно удобный, возвращающий логи исполнения смарт контрактов. Находим адрес смарт контракта OpenSea и пишем достаточно простой запрос.

OPENSEA_CONTRACT = '0x7Be8076f4EA4A4AD08075C2508e481d6C946D12b'
url = '<https://api.etherscan.io/api>'
params = {
  'module': 'logs',
  'action': 'getLogs',
  'fromBlock' : BLOCK_START,
  'toBlock': 'latest',
  'address': OPENSEA_CONTRACT,
  'apikey': API_KEY
}
r = requests.get(url, params=params)
json_data = json.loads(r.text)["result"]

Данный эндпоинт в качестве входных данных также принимает номер блока, начиная с которого он собирает логи смарт контракта. Узнать номер текущего последнего сгенерированного блока можно либо на сайте Etherscan, либо можно обратиться к ещё одному эндпоинту, который в качестве параметра принимает время, timestamp и возвращает номер блока ближайшего к этому времени. Передав текущее время получаем номер последнего сгенерированного блока.

url = '<https://api.etherscan.io/api>'
params = {
  'module': 'block',
  'action': 'getblocknobytime',
  'timestamp' : int(time.time() // 1),
  'closest': 'before',
  'apikey': API_KEY
}
r = requests.get(url, params=params)
json_data = json.loads(r.text)["result"]
LATEST_BLOCK = int(json_data)

В силу того, что первый эндпоинт возвращает максимум 1000 транзакций получить сразу все транзакции с OpenSea за один запрос у нас не получится. Путём экспериментов я установил, что примерно в 40 блоках набирается 600-900 транзакций с OpenSea. Таким образом:

BLOCK_START = LATEST_BLOCK - 40

Давайте разберёмся с тем, какие данные отдаёт нам наш эндпоинт. А отдаёт он нам массив примерно таких вот структур.

{ 'address': '0x7be8076f4ea4a4ad08075c2508e481d6c946d12b',
'topics': 
  ['0xc4109843e0b7d514e4c093114b863f8e7d8d9a458c372cd51bfe526b588006c9',
'0x000000000000000000000000c6ac8a42344b750b9de0e71289292b620c328131',
'0x0000000000000000000000007f61d795439562fa791c3d468e8738125f0a9866',
'0x0000000000000000000000000000000000000000000000000000000000000000'],
'data': '0x00000000000000000000000000000000000000000000000000000000000000007656e0665c6bdc38947d580d2e8c4c19ba8fa4019abb8fef31cfaa1b7645f00d000000000000000000000000000000000000000000000000027f7d0bdb920000',
'blockNumber': '0xd82788',
'timeStamp': '0x62027a71',
'gasPrice': '0xdb7c42e2a',
'gasUsed': '0x2f283',
'logIndex': '0x182',
'transactionHash': '0xcfff81e158623e66ab53be47f342e1cb170f05a1461c56b8c0523f96ea042409',
'transactionIndex': '0x116'}

Выглядит устрашающе, но давайте разбираться. Для начала отмечу, что все данные тут в hex формате, то есть в системе счисления с основанием 16. Самая вкуснятинка и нужные нам данные кроются в topics и  data, но что это такое и как это понимать?

Как устроены Логи транзакций

Во время своей работы смарт контракты создают события, таким образом они уведомляют о том что была вызвана какия-либо функция или метод. Лог транзакции состоит из событий, сгенерированных смарт контрактами, с которых было взаимодействие в рамках транзакции. В случае смарт контракта OpenSea при продаже NFT от одного пользователя другому создаётся событие с названием OrdersMatched.

Элемент кода смарт контракта OpenSea
Элемент кода смарт контракта OpenSea

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

Событие OrdersMatched
Событие OrdersMatched

Данные, передаваемые событием хранятся в объекте topics и data.

topics - это массив, который содержит в себе данные о событии и его параметрах. Максимальный размер массива topics - 4 элемента. topics[0], нулевой элемент массива - это “подписанное” название метода. Остальные элементы массива topics и “кусок даных” data содержат в себе более детальные данные о транзакции - продавца, покупателя и цену сделки.

Более детально рассмотрим эти данные, на базе нашего метода и примера ответа API:

Событие OrdersMatched вместе со всеми своими параметрами.

OrdersMatched (bytes32 buyHash, bytes32 sellHash, 
index_topic_1 address maker, index_topic_2 address taker, 
uint256 price, index_topic_3 bytes32 metadata)
{'topics':
['0xc4109843e0b7d514e4c093114b863f8e7d8d9a458c372cd51bfe526b588006c9',
'0x000000000000000000000000c6ac8a42344b750b9de0e71289292b620c328131',
'0x0000000000000000000000007f61d795439562fa791c3d468e8738125f0a9866',
'0x0000000000000000000000000000000000000000000000000000000000000000'],
'data': '0x00000000000000000000000000000000000000000000000000000000000000007656e0665c6bdc38947d580d2e8c4c19ba8fa4019abb8fef31cfaa1b7645f00d000000000000000000000000000000000000000000000000027f7d0bdb920000'
}
  • topics[0] - подписанное название метода и типов данных его параметров.

Таким образом подписывается название метода OrdersMatched, совместно с типами параметров метода:

from Crypto.Hash import keccak
method = b'OrdersMatched(bytes32,bytes32,address,address,uint256,bytes32)'
k = keccak.new(digest_bits=256)
k.update(method)
print('0x'+k.hexdigest())
Out[]: 0xc4109843e0b7d514e4c093114b863f8e7d8d9a458c372cd51bfe526b588006c9
  • topics[1] - index_topic_1 address maker адрес продавца, 20 байтов, который дополнен нулями до 32.

topics[1] = '0x000000000000000000000000c6ac8a42344b750b9de0e71289292b620c328131'
address_maker = '0xc6ac8a42344b750b9de0e71289292b620c328131'
  • topics[2] - index_topic_2 address taker адрес покупателя, также 20 байт, дополненные нулями до 32.

topics[2] = '0x0000000000000000000000007f61d795439562fa791c3d468e8738125f0a9866'
address_taker = '0x7f61d795439562fa791c3d468e8738125f0a9866'
  • topics[3] - index_topic_3 bytes32 metadata метаданные, по опыту скарпинга транзакций в большей части случаев - нулевые данные.

Все те параметры функции, которые “не влезли” в topics передаются в data. В случае рассматриваемой функции это будут bytes32 buyHashbytes32 sellHash и uint256 price. Все три этих параметра просто напросто конкатенированы в один кусок данных в порядке их указания в параметрах функции.

Наиболее интересный нам параметр, цена сделки, хранится в последних 256 битах куска данных (то есть 32 байта и 64 символа hex представления). Аккуратно взяв кусочек строки и конвертировав его в число мы сможем узнать цену:

data = "0x00000000000000000000000000000000000000000000000000000000000000007656e0665c6bdc38947d580d2e8c4c19ba8fa4019abb8fef31cfaa1b7645f00d000000000000000000000000000000000000000000000000027f7d0bdb920000"
price_wei = int("0x" + data[-64:], 16)
price_eth = price_wei / 10**18

Помним о том, что смарт контракты оперируют с wei - наименьшей делимой частью ETH.

Собирая всё вместе, получаем приблизительно такой код:

# Получаем последние транзакции как в прошлом разделе
transactions = get_latest_transactions() 

# Подпись нужного нам метода
OrdersMatchedSig = "0xc4109843e0b7d514e4c093114b863f8e7d8d9a458c372cd51bfe526b588006c9"

# Для конвертации wei в ETH
DIVIDER = 10**18

# тут будут уже обработанные транзакции
transactions_refactored = []

for tr in transactions:
		result = {}
		if tr['topics'][0] == OrdersMatchedSig:
        result['maker'] = '0x' + (sample['topics'][1])[26:]
        result['taker'] = '0x' + (sample['topics'][2])[26:]
        result['price'] = int('0x' + sample['data'][-32:], 16) / DIVIDER

        # .... Также можно добавить в result любые нужные данные из tr
        # .... По типу timeStamp, blockNumber, итд.

        transactions_refactored.append(result)

Более детально про транзакции - Определение валюты

У описанного выше метода есть один небольшой недостаток, с которым я столкнулся, проанализировав несколько тысяч транзакций. Недостаток этот заключается в том, что в параметрах события метода OrdersMatched не передаётся валюта, в которой была совершена покупка NFT. В большинстве случаев эта валюта, разумеется, ETH, однако иногда проскакивают и другие токены.

Давайте разберёмся с тем, как же определять валюту транзакции. Возьмём для примера две транзакции, одна была проведена в эфирах, другая в ASH. Транзакция раз, транзакция два.

150 ASH

Событие OrdersMatched для транзакции в 150 ASH
Событие OrdersMatched для транзакции в 150 ASH

115 ETH

Событие OrdersMatched для транзакции в 115 ETH
Событие OrdersMatched для транзакции в 115 ETH

В рамках того эндпоинта, который возвращает нам логи событий транзакций, эти две выглядят практически одинаково, не считая цены. Однако 115 ETH это куда больше чем 150 ASH. Нам нужен метод, который позволит надёжно определять валюту транзакций.

Более детально покопавшись в API Etherscan можно найти метод eth_getTransactionReceipt, который по хэшу транзакции возвращает более детальную сводку событий смарт контрактов, связанную с этой транзакцией.

В контексте Python обратиться к этому эндпоинту можно вот так:

def get_transaction_receipt(txn):
		url = '<https://api.etherscan.io/api>'
    params = {
      'module': 'proxy',
      'action': 'eth_getTransactionReceipt',
      'apikey': API_KEY,
      'txhash' : txn
    }
    r = requests.get(url, params=params)
    json_data = json.loads(r.text)["result"]
    return json_data

Для транзакции в 150 ASH ответ будет выглядеть вот так:

json_data
{'blockHash': '0x43cca3bae64e6d5dfaf9e682948349dfb66a1390c6f0d056007b30443912bbc9',
'blockNumber': '0xd831b2',
'contractAddress': None,
'cumulativeGasUsed': '0x1321b40',
'effectiveGasPrice': '0x1343f99739',
'from': '0xd4fc759d1dd10936438a5d4c5dc711a85f086c8c',
'gasUsed': '0x31a43',
'logs': [{'address': '0x64d91f12ece7362f91a6f8e7940cd55f05060b92',
'topics': ['0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
'0x000000000000000000000000d4fc759d1dd10936438a5d4c5dc711a85f086c8c',
'0x00000000000000000000000041f33855fe7bff95a6d8204cf93e8905cefa0485'],
'data': '0x00000000000000000000000000000000000000000000000821ab0d4414980000',
'blockNumber': '0xd831b2',
'transactionHash': '0xae0d18bec807c1968b501002502bd4a4494480de90b4caa027a84865713b8228',
'transactionIndex': '0xee',
'blockHash': '0x43cca3bae64e6d5dfaf9e682948349dfb66a1390c6f0d056007b30443912bbc9',
'logIndex': '0x192',
'removed': False},
{'address': '0x64d91f12ece7362f91a6f8e7940cd55f05060b92',
'topics': ['0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925',
'0x000000000000000000000000d4fc759d1dd10936438a5d4c5dc711a85f086c8c',
'0x000000000000000000000000e5c783ee536cf5e63e792988335c4255169be4e1'],
'data': '0xfffffffffffffffffffffffffffffffffffffffffffffff3a3dd47feeaefffff',
'blockNumber': '0xd831b2',
'transactionHash': '0xae0d18bec807c1968b501002502bd4a4494480de90b4caa027a84865713b8228',
'transactionIndex': '0xee',
'blockHash': '0x43cca3bae64e6d5dfaf9e682948349dfb66a1390c6f0d056007b30443912bbc9',
'logIndex': '0x193',
'removed': False},
{'address': '0x64d91f12ece7362f91a6f8e7940cd55f05060b92',
'topics': ['0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
'0x00000000000000000000000041f33855fe7bff95a6d8204cf93e8905cefa0485',
'0x0000000000000000000000005b3256965e7c3cf26e11fcaf296dfc8807c01073'],
'data': '0x000000000000000000000000000000000000000000000001043561a882930000',
'blockNumber': '0xd831b2',
'transactionHash': '0xae0d18bec807c1968b501002502bd4a4494480de90b4caa027a84865713b8228',
'transactionIndex': '0xee',
'blockHash': '0x43cca3bae64e6d5dfaf9e682948349dfb66a1390c6f0d056007b30443912bbc9',
'logIndex': '0x194',
'removed': False},
{'address': '0x64d91f12ece7362f91a6f8e7940cd55f05060b92',
'topics': ['0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925',
'0x00000000000000000000000041f33855fe7bff95a6d8204cf93e8905cefa0485',
'0x000000000000000000000000e5c783ee536cf5e63e792988335c4255169be4e1'],
'data': '0xfffffffffffffffffffffffffffffffffffffffffffffffaa59186332e2cffff',
'blockNumber': '0xd831b2',
'transactionHash': '0xae0d18bec807c1968b501002502bd4a4494480de90b4caa027a84865713b8228',
'transactionIndex': '0xee',
'blockHash': '0x43cca3bae64e6d5dfaf9e682948349dfb66a1390c6f0d056007b30443912bbc9',
'logIndex': '0x195',
'removed': False},
{'address': '0x495f947276749ce646f68ac8c248420045cb7b5e',
'topics': ['0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62',
'0x0000000000000000000000000474e1cff596114577cf641d3bf41617c48c2042',
'0x00000000000000000000000041f33855fe7bff95a6d8204cf93e8905cefa0485',
'0x000000000000000000000000d4fc759d1dd10936438a5d4c5dc711a85f086c8c'],
'data': '0xbc307bdf50fe05db053c7217e904f9c77e9d51250000000000000b000000022b0000000000000000000000000000000000000000000000000000000000000001',
'blockNumber': '0xd831b2',
'transactionHash': '0xae0d18bec807c1968b501002502bd4a4494480de90b4caa027a84865713b8228',
'transactionIndex': '0xee',
'blockHash': '0x43cca3bae64e6d5dfaf9e682948349dfb66a1390c6f0d056007b30443912bbc9',
'logIndex': '0x196',
'removed': False},
{'address': '0x7be8076f4ea4a4ad08075c2508e481d6c946d12b',
'topics': ['0xc4109843e0b7d514e4c093114b863f8e7d8d9a458c372cd51bfe526b588006c9',
'0x00000000000000000000000041f33855fe7bff95a6d8204cf93e8905cefa0485',
'0x000000000000000000000000d4fc759d1dd10936438a5d4c5dc711a85f086c8c',
'0x0000000000000000000000000000000000000000000000000000000000000000'],
'data': '0x0000000000000000000000000000000000000000000000000000000000000000bcf6a0125547a86ac1fa18dc1e9e3fa2b3826bd95aacf93545d4b79dc5009e6b00000000000000000000000000000000000000000000000821ab0d4414980000',
'blockNumber': '0xd831b2',
'transactionHash': '0xae0d18bec807c1968b501002502bd4a4494480de90b4caa027a84865713b8228',
'transactionIndex': '0xee',
'blockHash': '0x43cca3bae64e6d5dfaf9e682948349dfb66a1390c6f0d056007b30443912bbc9',
'logIndex': '0x197',
'removed': False}],
'logsBloom': '0x0000000000000000000000000000000100000000000000000000000400000000000000000000002000006080000000000200800000000000000000000020201000000000000001000000000800880000000000000000000000000000000000000000000002000000000000000000080000000000000000000000003000000010100000000000000000100000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000180000000000000240000000200000000000001000000000040a000000000000020020000010000000000000000400000000000000000000200000000000080000000000',
'status': '0x1',
'to': '0x7be8076f4ea4a4ad08075c2508e481d6c946d12b',
'transactionHash': '0xae0d18bec807c1968b501002502bd4a4494480de90b4caa027a84865713b8228',
'transactionIndex': '0xee',
'type': '0x2'}
Более читаемый вид

Ссылка на такой формат

Все события транзакции в 150 ASH
Все события транзакции в 150 ASH

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

Это значит, что в случае покупки NFT за ERC-20 токен, в транзакции отобразится взаимодействие со смарт контрактом данного ERC-20 токена.

В контексте упомянутой выше транзакции на 150 ASH это будут методы Approval и Transfer смарт контракта ASH.

 События транзакции в 150 ASH
События транзакции в 150 ASH

Можно заметить, что в методе Transfer в качестве одного из параметров присутствует цена транзакции в  150 * 10**18. Этого инсайта нам хватит для того, чтобы написать парсер, который будет также понимать валюту транзакции.

Функция определения валюты транзакции, реализованная на Python

WETH_CONTRACT = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'
OPENSEA_CONTRACT = '0x7be8076f4ea4a4ad08075c2508e481d6c946d12b'
TRANSFER_METHOD = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
# txn - хэш транзакции
def determine_currency(txn):
    # описанная ранее функция
    transaction_receipt = get_transaction_receipt(txn) 
    currency = 'ETH'
    # Лог события OrdersMatched будет последним с списке событий
    price_hex = transaction_receipt['logs'][-1]['data'][-64:]
    for s in transaction_receipt['logs']:
    		if s['topics'][0] == TRANSFER_METHOD:
              # Проверяем, есть ли цена сделки в логе события 
        		if price_hex in s['data']:
                 # Дополнительная проверка на Wrapped ETH
                if s['address'] == WETH_CONTRACT:
                    currency = 'WETH'
                else:
                    currency = 'NONETH-' +s['address']

    return currency

Логика работы этой функции заключается в том, что мы смотрим на все события смарт контрактов, связанные с нашей транзакцией. Если в данных одного из событий метода Transfer фигурирует цена сделки, то это будет обращение к смарт контракту ERC-20 токена. На базе адреса смарт контракта этого токена мы сможем установить, что это за валюта.

Более детально про транзакции - смарт контракты коллекции

Смотреть на то, как кто-то продаёт кому-то токены ERC-721 - это конечно же интересно. Но куда более интересно ещё и знать что это за токены: как называется их коллекции и каков порядковый номер токена.

Упомянутый в первом разделе статьи эндпоинт также не возвращает данные касательно токена ERC-721, фигурирующего в транзакции. Однако эндпоинт  eth_getTransactionReceipt возвращает все связанные с транзакцией события смарт контрактов, а значит там должны быть и детали касательно токенов ERC-721.

Действительно, при более детальном рассмотрении событий смарт контрактов можно понять что именно купили в транзакции. На примере транзакции в ETH можно заметить что помимо события OrdersMatched есть ещё два других события - Approval и Transfer. Так как в данной транзакции не было обращения к смарт контрактам ERC-20 токенов, можно предположить, что это события смарт контракта ERC-721 токена.

События транзакции покупки BoredApeYacthClub#8481
События транзакции покупки BoredApeYacthClub#8481

Более детально рассмотрев адрес смарт контракта можно понять, что это адрес коллекции BoredApeYachtClub (BAYC). А в методе Transfer фигурирует номер токена коллекции, который был переведён.

Таким образом, с помощью эндпоинта eth_getTransactionReceipt можно также получить более детальную информацию о токене NFT, который был куплен.

Оборачивая написанный текст в код, получим:

TRANSFER_METHOD = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
# txn - хэш транзакции
def determine_collection_contract(txn):
    # описанная ранее функция
    transaction_receipt = get_transaction_receipt(txn) 

    # Лог события OrdersMatched будет последним с списке событий
    price_hex = transaction_receipt['logs'][-1]['data'][-64:]

    collection_contract = ''
    token_number = -1
    for s in transaction_receipt['logs']:
        if (s['topics'][0] == TRANSFER_METHOD):
              # Исключаем ERC-20 контракты
            if price_hex not in s['data']:
                    collection_contract = s['address']
                    token_number = int(s['topics'][3], 16)

    return collection_contract, token_number

Логика работа функции похожая на логику функцию из предыдущего раздела, только в данном примере мы берём данные из метода Transfer, не связанного с ERC-20 смарт контрактами (где не фигурирует цена сделки).

Да, я знаю что тут есть редкий баг
  • В рамках данной функции может случиться такое, что номер токена коллекции совпадёт с ценой (это крайне маловероятно, но всё же). Данной проблемы можно избежать дополнительно проверяя параметры события Approval.

    Так выглядит событие Approval для ERC-721 токена

    Approval event для ERC-721 токена
    Approval event для ERC-721 токена

    А вот так для ERC-20

    Approval event для ERC-20 токена
    Approval event для ERC-20 токена

    С помощью такой дополнительной проверки можно исключить упомянутый выше баг.

Для того чтобы на базе адреса контракта коллекции можно было получить название мне пришлось воспользоваться API BlockScout. В целом API хорошее и удобное, даже есть GraphQL интерфейс, однако каким-то невероятным образом в базе данных этой системы нет части транзакций, причём целыми блоками, что делает этот сервис не самым надёжным основным источником данных.

Однако же данные касательно смарт контрактов в данном сервисе достаточно полноценны. Написанный ниже код на Python на базе адреса смарт контракта ERC-721 токена возвращает название коллекции.

def get_collection_name(contract_adress):
    url = '<https://blockscout.com/eth/mainnet/api>'
    params = {
        'module' : 'token',
        'action' : 'getToken',
        'contractaddress' : contract_adress
    }
    r = requests.get(url, params=params)
    if r.status_code == 200:
        json_data = json.loads(r.text)

        if (json_data != None) and (json_data['message'] == 'OK'):
            return json_data['result']['name']
        else:
            return "None"
    else:
        return "None"

Весь же ответ данного эндпоинта выглядит вот так:

{'message': 'OK',
'result': {
'cataloged': True,
'contractAddress': '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d',
'decimals': '',
'name': 'BoredApeYachtClub',
'symbol': 'BAYC',
'totalSupply': '10000',
'type': 'ERC-721'},
'status': '1'}

Пример для коллекции 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 это никак не отображается, разберётесь в чём загвоздка?

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


  1. v1000
    17.02.2022 13:33
    +2

    Cлышал интересную новость про то, что "накрутка" популярности NFT с помощью известных людей вызвана в том числе и их вложениями в сам OpenSea.

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


    1. trak
      17.02.2022 17:46

      ой прости, мисклик, попал в минус :(

      а вообще да, nft это firewall, а не вот это вот все...


  1. khis
    18.02.2022 10:31

    Кстати, у OpenSea есть же свой API NFT API Overview (opensea.io)

    Там чего-то не достаёт для работы с событиями?


    1. rawoak Автор
      18.02.2022 12:35

      В случае API OpenSea большая часть интересных эндпоинтов доступна только после регистрации, я отправил им заявку, а они так на неё и не ответили.

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