Пошаговая инструкция как создать NFT коллекцию на Golang + Ethereum (часть 2)

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Привет Хабр.

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

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

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

Итак, чтобы гарантировать, что наш контент останется сохраненным (закрепленным), мы должны запустить свои собственные IPFS узлы. Конечно мы можем настроить IPFS узлы сами, но гораздо удобней использовать готовый сервис, такой как Pinata. Далее статья будет посвящена тому, как работать с сервисом Pinata для публикаций NFT медиа файлов.

Согласно документации, для закрепления файлов мы должны вызвать ендпоинт pinFileToIPFS. Давайте напишем код, который собственно и будет это делать:

const (
	pinFileURL = "https://api.pinata.cloud/pinning/pinFileToIPFS"
)

func (s *service) pinFile(fileName string, data []byte, wrapWithDirectory bool) (string, error) {
   type pinataResponse struct {
       IPFSHash  string `json:"IpfsHash"`
       PinSize   int    `json:"PinSize"`
       Timestamp string `json:"Timestamp"`
   }
 
   bodyBuf := &bytes.Buffer{}
   bodyWriter := multipart.NewWriter(bodyBuf)
 
   // this step is very important
   fileWriter, err := bodyWriter.CreateFormFile("file", fileName)
   if err != nil {
       return "", err
   }
   if _, err := fileWriter.Write(data); err != nil {
       return "", err
   }
 
   // Wrap your content inside of a directory when adding to IPFS. 
   // This allows users to retrieve content via a filename instead of just a hash.
   if wrapWithDirectory {
       fileWriter, err = bodyWriter.CreateFormField("pinataOptions")
       if err != nil {
           return "", err
       }
       if _, err := fileWriter.Write([]byte(`{"wrapWithDirectory": true}`)); err != nil {
           return "", err
       }
   }
 
   contentType := bodyWriter.FormDataContentType()
   bodyWriter.Close()
 
   req, err := http.NewRequest("POST", pinFileURL, bodyBuf)
   if err != nil {
       return "", err
   }
 
   req.Header.Set("Content-Type", contentType)
   req.Header.Set("pinata_api_key", s.params.APIKey)
   req.Header.Set("pinata_secret_api_key", s.params.SecretKey)
 
   // Do request.
   var (
       retries = 3
       resp    *http.Response
   )
   for retries > 0 {
       resp, err = s.client.Do(req)
       if err != nil {
           retries -= 1
       } else {
           break
       }
   }
   if resp == nil {
       return "", fmt.Errorf("Failed to upload files to ipfs, err: %v", err)
   }
   defer resp.Body.Close()
   if resp.StatusCode != http.StatusOK {
       errMsg := make([]byte, resp.ContentLength)
       _, _ = resp.Body.Read(errMsg)
       return "", fmt.Errorf("Failed to upload file, response code %d, msg: %s", resp.StatusCode, string(errMsg))
   }
   body, err := ioutil.ReadAll(resp.Body)
   if err != nil {
       return "", err
   }
   pinataResp := pinataResponse{}
   err = json.NewDecoder(bytes.NewReader(body)).Decode(&pinataResp)
   if err != nil {
       return "", fmt.Errorf("Failed to decode json, err: %v", err)
   }
   if len(pinataResp.IPFSHash) == 0 {
       return "", errors.New("Ipfs hash not found in the response body")
   }
   return pinataResp.IPFSHash, nil
}

При детальном рассмотрении кода, можно заметить, что для вызовов ендпоинтов pinata, необходимо получить API key + secret key.

На успешно загруженный файл, мы получим IPFS-хэш файла, следующего вида: QmPbxeGcXhYQQNgsC6a36dDyYUcHgMLnGKnF8pVFmGsvqi

Теперь, когда у нас есть метод для загрузки файлов в IPFS -pinFile, нам нужно создать другой метод, который будет:

  1. считывать *.png файл с указанной директории

  2. загружать *.png файл в IPFS

  3. считывать *.json файл с описание аттрибутов (traits)

  4. создавать финальный *.json файл с описанием ERC-721 метаданных

Итак, давайте приступим к написанию кода:

func (s *service) uploadImage(p *uploadImageParam) error {

	// 1. Read the image along the specified path.
	imgBytes, err := ioutil.ReadFile(p.path)
	if err != nil {
		return err
	}

	// 2. Upload the image to the IPFS
	ipfsImageHash, err := s.pinFile(p.fileName, imgBytes, false)
	if err != nil {
		return err
	}

	// 3. Read NFT token attributes (traits)
	traitsBytes, err := ioutil.ReadFile(strings.ReplaceAll(p.path, ".png", ".json"))
	if err != nil {
		return err
	}
	traits := []*domain.ERC721Trait{}
	if err := json.Unmarshal(traitsBytes, &traits); err != nil {
		return err
	}

	// 4. Create ERC-721 metadata file.
	erc721Metadata := &domain.ERC721Metadata{
		Image:      fmt.Sprintf("ipfs://%s", ipfsImageHash),
		Attributes: traits,
	}
	erc721MetadataBytes, err := json.Marshal(erc721Metadata)
	if err != nil {
		return err
	}
	var (
		key = strings.TrimSuffix(p.fileName, filepath.Ext(p.fileName))
	)
	metadataFile, err := os.Create(fmt.Sprintf("%s/%d.%s.json", s.params.OutputDirectory, p.number, key))
	if err != nil {
		return err
	}
	defer metadataFile.Close()
	_, err = metadataFile.Write(erc721MetadataBytes)
	return err
}

Такие структуры как ERC721Trait и ERC721Metadata были подробно описаны в предыдущей статье.

На выходе у нас получиться файл для ERC-721 метаданных, следующего вида:

{
   "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"
      }
   ]
}

В следующей статье мы приступим к созданию смарт-контракта на Ethereum.

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

Источник: https://habr.com/ru/post/595763/


Интересные статьи

Интересные статьи

Все началось несколько лет назад со школьного проекта по Computer science. Моя идея была сделать компьютерную программу которая проанализирует историю рынка, определит комбинации из 4х свечей в класте...
У меня иногда появлялось желаение делать ботов для телеграм, так мой основной язык Java - выбор не велик и он меня не устраивает. Каждый раз нужно было придумывать какие-то схемы обработки приходящих ...
Добрый день всем желающим познакомиться с Flutter!У меня появилось горячее желание поделиться с вам моими знаниями, которые я накопил за несколько месяцев. Возможно мой о...
Мы уже рассказывали про Tarantool Cartridge, который позволяет разрабатывать распределенные приложения и паковать их. Осталось всего ничего: научиться деплоить эти приложения и управлять ими. Н...
В жизни каждого программиста бывали моменты, когда он мечтал сделать интересную игру. Многие программисты эти мечты реализовывают, и даже успешно, но речь сейчас не о них. Речь о тех, кто любит...