Привет Хабр.

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

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

После проведенного небольшого исследования, мне удалось разбить изначальную задачу из абстрактного “создать NFT коллекцию” на более мелкие и конкретные, а именно:

  • с генерировать 10 000 уникальных изображений

  • с генерировать 10 000 метаданных к каждому изображению

  • загрузить 10 000 изображений вместе с метеданными в децентрализованное хранилище

  • создать смарт-контракт для NFT токенов

  • загрузить созданный смарт-контракт в mainnet Ethereum

  • создать сайт, который будет взаимодействовать с нашим смарт-контрактом с помощью web3, где собственно пользователи и смогут менять свои эфиры на наши NFT токены

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

Как с генерировать 10 000 уникальных изображений?

Почему именно 10 000? Ответ достаточно прост, большинство популярных NFT проектов, предлагают коллекции именно из 10 000 NFT токенов. Каждый создатель, сам волен решать сколько NFT токенов он хочет выпустить, но мы решили не отходить от канона и тоже сделали 10 000 токенов.

Итак, как же все таки с генерировать 10 000 уникальных изображений? Конечно же с помощью автоматического наложения слоев друг на друга. Немного поразмыслив, мы с художником пришли к выводу, что для нашего проекта нам нужны следующие слои:

  1. фон - 20 шт

  2. туловище персонажа - 25 шт

  3. голова - 15 шт

  4. эмоции - 20 шт

  5. одежда - 30 шт

  6. обувь - 25 шт

  7. аксессуары - 40 шт

В общем количестве, у нас получилось приблизительно 175 уникальных слоев в формате png, что более чем достаточно, чтобы получить 10 000 уникальных персонажей. Теперь осталось совсем ничего, а именно написать утилиту, которая на входе будет принимать заготовки в виде слоев, а на выходе будет отдавать готовых персонажей. 

Писать я буду на Golang, итак поехали. Для начала, нам нужно описать 2 структуры в пакете domain, одна для слоев, а другая для холста.

package domain

import (
	"image"
	"image/color"
)

// ImageLayer struct.
type ImageLayer struct {
	Image    image.Image
	Priotiry int
	XPos     int
	YPos     int
}

//BgProperty is background property struct.
type BgProperty struct {
	Width   int
	Length  int
	BgColor color.Color
}

Давайте более подробно рассмотрим обе структуры.

ImageLayer:

  • Image - изображение слоя

  • Priority - приоритет слоя, т.к. слои нужно накладывать в определенном порядке, сначала фон, потом туловище, потом голова, итд …

  • XPos, YPos - позиция слоя на холсте

BgProperty:

  • Width - ширина холста

  • Length - длина холста

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

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

package combiner
 
import (
   "bytes"
   "image"
   "image/draw"
   "image/png"
   "nft/internal/domain"
   "sort"
)
 
type service struct {
}
 
func NewBasicImageCombiner() domain.ImageCombiner {
   return &service{}
}
 
func (s *service) CombineLayers(layers []*domain.ImageLayer, bgProperty *domain.BgProperty) ([]byte, error) {
 
   // Sort list by position.
   layers = sortByPriotiry(layers)
 
   // Create image's background.
   bgImg := image.NewRGBA(image.Rect(0, 0, bgProperty.Width, bgProperty.Length))
 
   // Set the background color.
   draw.Draw(bgImg, bgImg.Bounds(), &image.Uniform{bgProperty.BgColor}, image.Point{}, draw.Src)
 
   // Looping image layers, higher position -> upper layer.
   for _, img := range layers {
 
       // Set the image offset.
       offset := image.Pt(img.XPos, img.YPos)
 
       // Combine the image.
       draw.Draw(bgImg, img.Image.Bounds().Add(offset), img.Image, image.Point{}, draw.Over)
   }
 
   // Encode image to buffer.
   buff := new(bytes.Buffer)
   if err := png.Encode(buff, bgImg); err != nil {
       return nil, err
   }
 
   return buff.Bytes(), nil
}
 
func sortByPriotiry(list []*domain.ImageLayer) []*domain.ImageLayer {
   sort.Slice(list, func(i, j int) bool {
       return list[i].Priotiry < list[j].Priotiry
   })
   return list
}

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

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

В каком формате должны быть метаданные для NFT токенов?

Т.к. NFT токены основаны на ERC-721 стандарте, а сам стандарт никак не описывает в каком формате должны быть метаданные, мы вольны использовать любой формат какой только захотим.

Но если мы хотим, чтобы наши NFT токены могли полноценно торговаться на таких пощадках как opensea, мы должны следовать следующему JSON формату:

{
   "image":"ipfs://QmPbxeGcXhYQQNgsC6a36dDyYUcHgMLnGKnF8pVFmGsvqi",
   "attributes":[
      {
         "trait_type":"Mouth",
         "value":"Grin"
      },
      {
         "trait_type":"Clothes",
         "value":"Vietnam Jacket"
      },
      {
         "trait_type":"Background",
         "value":"Orange"
      },
      {
         "trait_type":"Eyes",
         "value":"Blue Beams"
      },
      {
         "trait_type":"Fur",
         "value":"Robot"
      }
   ]
}

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

package domain

// ERC721Trait - ERC721 trait format.
type ERC721Trait struct {
	TraitType string `json:"trait_type"`
	Value     string `json:"value"`
}

// ERC721Metadata - metadata schema.
type ERC721Metadata struct {
	Image      string         `json:"image"`
	Attributes []*ERC721Trait `json:"attributes"`
}

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

P.S.Весь исходный код можно посмотреть на github.

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


  1. fedorro
    15.12.2021 13:54
    +4

    Ждем продолжения, в котором будет рассказано как продать весь этот мусор ????


  1. Bedrosova
    15.12.2021 16:23
    +3

    Есть же candy-machine, и только ленивый не написал еще гайда по нему, зачем изобретали этот велосипед?


    1. monomoto Автор
      15.12.2021 17:12

      candy-machine - отличный инструмент, но когда мы начинали свой проект, данного инструмента еше не было.


      1. Bedrosova
        15.12.2021 17:55

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


        1. peshkoff
          18.12.2021 09:47

          Да и хоть одну картинку как пример показывать , криптожадина :)