May 2, 2022

Свой NFT Rarity на Django

короче превью было краисвое но чето пошло не так и получились артефакты. Похуй блять

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

В данной статье рассмотрен Эфир, Солана же работает ПОЧТИ также окда

🔧 Настройка проекта

Для чекера мы будем использовать шаблон Cookiecutter-Django для начальной загрузки проекта Django. Вы можете настроить проект либо для локального запуска, либо с помощью Docker. Поскольку мы будем использовать Celery, то настроим свой проект на использование Docker

Загрузите проект с помощью команды:
cookiecutter gh:cookiecutter/cookiecutter-django

Если что-то будет не так, то читайте документацию Cookiecutter-Django

🎭 Создаем приложение

Создайте новое приложение для хранения всей логики чекера рарности:

docker-compose -f local.yml run --rm django python manage.py startapp sniper

🗿 Модели

Данный проект будет иметь несколько моделей для хранения проекта NFT, атрибутов NFT и уникальных NFT. Внутри нового приложения models.py добавьте следующий код:

from django.db import models


class NFTProject(models.Model):
    contract_address = models.CharField(max_length=100)
    contract_abi = models.TextField()
    name = models.CharField(max_length=50)  # e.g BAYC
    number_of_nfts = models.PositiveIntegerField()

    def __str__(self):
        return self.name


class NFT(models.Model):
    project = models.ForeignKey(
        NFTProject, on_delete=models.CASCADE, related_name="nfts"
    )
    rarity_score = models.FloatField(null=True)
    nft_id = models.PositiveIntegerField()
    image_url = models.CharField(max_length=200)
    rank = models.PositiveIntegerField(null=True)
    
    def __str__(self):
        return f"{self.project.name}: {self.nft_id}"


class NFTAttribute(models.Model):
    project = models.ForeignKey(
        NFTProject, on_delete=models.CASCADE, related_name="attributes"
    )
    name = models.CharField(max_length=50)
    value = models.CharField(max_length=100)

    def __str__(self):
        return f"{self.name}: {self.value}"


class NFTTrait(models.Model):
    nft = models.ForeignKey(
        NFT, on_delete=models.CASCADE, related_name="nft_attributes"
    )
    attribute = models.ForeignKey(
        NFTAttribute, on_delete=models.CASCADE, related_name="traits"
    )
    rarity_score = models.FloatField(null=True)

    def __str__(self):
        return f"{self.attribute.name}: {self.attribute.value}"

🔗 Web3

Одним из самых популярных пакетов Python для взаимодействия с блокчейном Ethereum является web3.py. Используя этот пакет, мы можем взаимодействовать с существующими смарт-контрактами.

Начните с установки пакета web3 вместе с pip install web3 образами Docker и перестройте их.

1️⃣ Экспериментируем с коллекцией BAYC

В этом уроке мы сосредоточимся на одном проекте NFT — яхт-клубе Bored Ape

Чтобы найти смарт-контракт для любого проекта, выполните поиск BAYC в списке токенов Etherscan ERC721. Вы можете просмотреть токен BAYC здесь. Обратите внимание на значение Contract, указанную в Profile Summary.

Следующий шаг — просмотреть код контракта, что вы можете сделать, нажав на пункт меню Contract. Это приведет вас на эту страницу. Затем вы можете просмотреть все методы, доступные в смарт-контракте. Большинство методов в контракте используются для чтения данных, понятненько?

Метод, который нас интересует, - это tokenURI метод, который вы можете найти 20 по счету. Метод принимает tokenId качестве параметра и ищет информацию для этого конкретного идентификатора NFT.

Например, вы можете указать значение 7575, и оно вернет информацию для NFT BAYC # 7575:

2️⃣ Адрес смарт-контракта и ABI

Вам нужно две вещи, чтобы взаимодействовать со смарт-контрактом:

  1. Адрес смарт-контракта
  2. Двоичный интерфейс приложения (ABI)

Обе эти вещи можно найти на Etherscan на странице адреса контракта BAYC

Адрес контракта указан в URL-адресе ссылки выше, а также в заголовке страницы. Адрес контракта: 0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D

Чтобы получить ABI, перейдите в Contract и прокрутите вниз до раздела Contract ABI. Там вы увидите текстовый ввод с длинным значением данных JSON следующим образом:

[{"inputs":[{"internalType":"string","name":"name","type":"string"},{"internalType":"string","name":"symbol","type":"string"},{"internalType":"uint256","name":"maxNftSupply","type":"uint256"},{"internalType":"uint256","name":"saleStart","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs": .....

Этот длинный текст является значением ABI. Мы будем использовать адрес контракта и ABI в коде

3️⃣ Выполнение запросов к блокчейну Ethereum

Чтобы сделать запрос, вам нужно будет настроить провайдера web3.

Следуя из web3.py документации:

Провайдер — это то, как web3 разговаривает с блокчейном. Провайдеры принимают запросы JSON-RPC и возвращают ответ. Обычно это делается путем отправки запроса на сервер на основе сокетов HTTP или IPC.

Одним из вариантов является запуск собственного узла Ethereum, но это выходит за рамки этого урока, поэтому вместо этого мы будем использовать сервис.

► Infura

Infura предоставляет инструменты, помогающие в разработке блокчейна.

Делаем учетную запись (это бесплатно), переходим на панель мониторинга и создаем новый проект. В настройках проекта вы найдете PROJECT_ID. Вы будете использовать это значение для подключения к своему собственному провайдеру web3. Теперь можно скамить!

4️⃣ Извлечение данных признаков из NFT

Классная вещь о NFT заключается в том, что вы можете прикреплять данные к NFT, такие как текст и изображения. Эти данные называются метаданными.

Теперь мы напишем код с использованием пакета web3 Python для взаимодействия с контрактом BAYC и получения метаданных для BAYC # 7575.

Вот команда управления Django, которую можно запустить с python manage.py fetch_nfts помощью или docker-compose -f local.yml run --rm django python manage.py fetch_nfts

from django.core.management.base import BaseCommand
from web3.main import Web3

INFURA_PROJECT_ID = "<your_project_id>"
INFURA_ENDPOINT = f"https://mainnet.infura.io/v3/{INFURA_PROJECT_ID}"


class Command(BaseCommand):
    def handle(self, *args, **options):
        self.fetch_nfts(7575)

    def fetch_nfts(self, token_id):

        contract_address = "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D"
        contract_abi = '<paste the ABI text in here>'

        w3 = Web3(Web3.HTTPProvider(INFURA_ENDPOINT))
        contract_instance = w3.eth.contract(address=contract_address, abi=contract_abi)

        print(f"Fetching NFT #{token_id}")
        data = contract_instance.functions.tokenURI(token_id).call()
        print(data)

В этом скрипте мы настраиваем INFURA_ENDPOINT значение, которое указывает на наш проект Infura. Обязательно <your_project_id>замените его своим собственным значением из панели мониторинга проекта Infura. Затем мы используем пакет web3 Python для установки соединения с блокчейном Ethereum через Infura.

С адресом контракта и ABI мы можем позвонитьw3.eth.contract, чтобы подключиться к контракту. После подключения мы можем выполнять методы, доступные в контракте, как мы видели в коде контракта Etherscan.

В частности, мы вызываем tokenURI метод.

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

data = contract_instance.functions.tokenURI(token_id).call()

Эта строка вызывает tokenURI функцию. Обратите внимание, что мы используем.call(). Это происходит потому, что мы считываем данные из контракта. Если бы мы хотели записать данные в контракт, мы бы их использовали.transact(). Подробнее об этом вы можете прочитать в документах web3.

После запуска команды управления в терминале должно появиться следующее:

ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/7575

Это значение является URL-адресом IPFS.

InterPlanetary File System (IPFS) — это распределенная файловая система. Для просмотра данных IPFS вы можете использовать такую услугу, как ipfs.io.

Перейдите к https://ipfs.io/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/7575 и вы получите следующий ответ JSON:

{"image":"ipfs://QmTE1TK15CcmETgc6wwSNDNwMgF7PvH714GGq33ShcWjR7","attributes":[{"trait_type":"Eyes","value":"Sleepy"},{"trait_type":"Mouth","value":"Bored Unshaven"},{"trait_type":"Background","value":"Orange"},{"trait_type":"Hat","value":"Prussian Helmet"},{"trait_type":"Fur","value":"Black"},{"trait_type":"Clothes","value":"Sleeveless T"}]}

Теперь вы можете видеть, что данные JSON включают URL-адрес изображения и список признаков. Некоторые из черт - это рот, фон, шляпа и т. д. Вы также можете увидеть значение для каждой черты. Эти черты мы будем использовать для расчета редкости NFT.

Для просмотра изображения вам нужно будет использовать значение URL IPFS. Опять же, вы можете использовать ipfs.io и просмотрите изображение здесь.

5️⃣ Хранение данных NFT

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

Внутри sniper/admin.py обязательно зарегистрируйте все модели:

from django.contrib import admin
from .models import NFTProject, NFT, NFTTrait, NFTAttribute


class NFTAdmin(admin.ModelAdmin):
    list_display = ["nft_id", "rank", "rarity_score"]
    search_fields = ["nft_id__exact"]


class NFTAttributeAdmin(admin.ModelAdmin):
    list_display = ["name", "value"]
    list_filter = ["name"]


admin.site.register(NFTProject)
admin.site.register(NFTTrait)
admin.site.register(NFT, NFTAdmin)
admin.site.register(NFTAttribute, NFTAttributeAdmin)

Перейдите к администратору Django http://127.0.0.1:8000/admin и создайте новый NFTProject со всеми данными BAYC:

Мы собираемся изменить наш скрипт Django так, чтобы он создавал все NFT и связывал их с только что созданным NFTProject:

from django.core.management.base import BaseCommand
import requests
from web3.main import Web3
from djsniper.sniper.models import NFTProject, NFT, NFTAttribute, NFTTrait

INFURA_PROJECT_ID = "<your_project_id>"
INFURA_ENDPOINT = f"https://mainnet.infura.io/v3/{INFURA_PROJECT_ID}"


class Command(BaseCommand):
    def handle(self, *args, **options):
        self.fetch_nfts(1)

    def fetch_nfts(self, project_id):
        project = NFTProject.objects.get(id=project_id)

        w3 = Web3(Web3.HTTPProvider(INFURA_ENDPOINT))
        contract_instance = w3.eth.contract(
            address=project.contract_address, abi=project.contract_abi
        )

        # Hardcoding only 10 NFTs otherwise it takes long
        for i in range(0, 10):
            ipfs_uri = contract_instance.functions.tokenURI(i).call()
            data = requests.get(
                f"https://ipfs.io/ipfs/{ipfs_uri.split('ipfs://')[1]}"
            ).json()
            nft = NFT.objects.create(nft_id=i, project=project, image_url=data["image"].split('ipfs://')[1])
            attributes = data["attributes"]
            for attribute in attributes:
                nft_attribute, created = NFTAttribute.objects.get_or_create(
                    project=project,
                    name=attribute["trait_type"],
                    value=attribute["value"],
                )
                NFTTrait.objects.create(nft=nft, attribute=nft_attribute)

После запуска скрипта вы должны увидеть 10 NFT внутри администратора Django. Вы также должны увидеть 48 атрибутов nft, которые вы можете фильтровать с помощью фильтра списка сбоку

6️⃣ Вычисление NFT Rarity

Rarity Tools был одним из первых проектов, которые вы могли использовать для расчета NFT rarity. Мы будем вычислять редкость, используя ту же формулу, что и инструменты редкости. Вы также можете прочитать больше о том, как рассчитывается оценка редкости

Формула для расчета редкости выглядит следующим образом:

[Оценка редкости для значения признака] = 1 / ([Количество элементов с этим значением признака] / [Общее количество элементов в коллекции])

Теперь мы напишем второй скрипт для вычисления редкости и ранга каждого NFT:

from django.core.management.base import BaseCommand
from django.db.models import OuterRef, Func, Subquery
from djsniper.sniper.models import NFTProject, NFTAttribute, NFTTrait


class Command(BaseCommand):
    def handle(self, *args, **options):
        self.rank_nfts(1)

    def rank_nfts(self, project_id):
        project = NFTProject.objects.get(id=project_id)

        # calculate sum of NFT trait types
        trait_count_subquery = (
            NFTTrait.objects.filter(attribute=OuterRef("id"))
            .order_by()
            .annotate(count=Func("id", function="Count"))
            .values("count")
        )

        attributes = NFTAttribute.objects.all().annotate(
            trait_count=Subquery(trait_count_subquery)
        )

        # Group traits under each type
        trait_type_map = {}
        for i in attributes:
            if i.name in trait_type_map.keys():
                trait_type_map[i.name][i.value] = i.trait_count
            else:
                trait_type_map[i.name] = {i.value: i.trait_count}

        # Calculate rarity
        """
        [Rarity Score for a Trait Value] = 1 / ([Number of Items with that Trait Value] / [Total Number of Items in Collection])
        """

        for nft in project.nfts.all():
            # fetch all traits for NFT
            total_score = 0

            for nft_attribute in nft.nft_attributes.all():
                trait_name = nft_attribute.attribute.name
                trait_value = nft_attribute.attribute.value

                # Number of Items with that Trait Value
                trait_sum = trait_type_map[trait_name][trait_value]

                rarity_score = 1 / (trait_sum / project.number_of_nfts)

                nft_attribute.rarity_score = rarity_score
                nft_attribute.save()

                total_score += rarity_score

            nft.rarity_score = total_score
            nft.save()

        # Rank NFTs
        for index, nft in enumerate(project.nfts.all().order_by("-rarity_score")):
            nft.rank = index + 1
            nft.save()

В этом скрипте происходит несколько вещей

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

trait_count_subquery = (
    NFTTrait.objects.filter(attribute=OuterRef("id"))
	.order_by()
	.annotate(count=Func("id", function="Count"))
	.values("count")
)

attributes = NFTAttribute.objects.all().annotate(
    trait_count=Subquery(trait_count_subquery)
)

Данные, возвращаемые из этого запроса, выглядят следующим образом:

('Earring', 'Silver Hoop', 1)
('Background', 'Orange', 2)
('Fur', 'Robot', 3)
('Clothes', 'Striped Tee', 1)
('Mouth', 'Discomfort', 1)
('Eyes', 'X Eyes', 2)
('Mouth', 'Grin', 1)
('Clothes', 'Vietnam Jacket', 1)
...

Затем мы группируем данные в каждую категорию атрибутов, чтобы с ними было легче работать:

trait_type_map = {}
for i in attributes:
    if i.name in trait_type_map.keys():
        trait_type_map[i.name][i.value] = i.trait_count
    else:
        trait_type_map[i.name] = {i.value: i.trait_count}

trait_type_map Выглядит так:

{'Background': {'Aquamarine': 2,
                'Army Green': 1,
                'Blue': 1,
                'Gray': 1,
                'Orange': 2,
                'Purple': 2,
                'Yellow': 1},
 'Clothes': {'Bayc T Red': 1,
             'Bone Necklace': 1,
             'Navy Striped Tee': 1,
             'Striped Tee': 1,
             'Stunt Jacket': 1,
             'Tweed Suit': 1,
             'Vietnam Jacket': 1,
             'Wool Turtleneck': 1},
...

Теперь мы можем легко получить доступ к каждому типу признака (например, фон) и значению признака (например, синий).

Затем мы вычисляем редкость , используя формулу редкости , и , наконец , вычисляем ранг каждого NFT, упорядочивая NFT в соответствии с rarity_score.

С нашими 10 NFT вы должны получить следующий рейтинг и оценку редкости в администраторе Django:

🔚 Заключение

Поздравляем, теперь у вас есть рабочий чекер редкости NFT. На данный момент вы можете улучшить проект следующими способами:

  1. Используйте Celery для извлечения данных NFT, потому что если вы получите все 10000 NFT, он будет тайм-аут, если выполняется синхронно.
  2. Создайте пользовательский интерфейс вокруг моделей - добавьте гламура, чтобы можно было продать за ахуенные бабки свой чекер

Канал: https://t.me/in_crypto_Info

Чат: https://t.me/crypto_chat_rus

Автор (за заказами по коду): @asynco