In my subjective opinion, the main message of NFT tokens is that no one can ever change the token and its contents, and accordingly, during development, we should be as abstract as possible from centralized systems. That is why we will publish all our files in decentralized storage – IPFS.
So, to ensure that our content stays persisted (pinned), we need to run our own IPFS nodes. Of course, we can set up IPFS nodes ourselves, but it is much more convenient to use a ready-made service, such as Pinata. The following article will focus on how to work with the Pinata service for publishing NFT media files.
According to the documentation, we need to call the pinFileToIPFS endpoint to pin the files. Let’s write the code that actually does this:
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
}
Looking at the code in detail, you can see that in order to call pinata endpoints, you need to get an API key + secret key.
On a successfully uploaded file, we will get an IPFS hash of the file, of the following form: QmPbxeGcXhYQQNgsC6a36dDyYUcHgMLnGKnF8pVFmGsvqi
Now that we have a method for uploading files to IPFS – pinFile
we need to create another method that will:
- read *.png file from the specified directory
- upload *.png file to IPFS
- read *.json file with a description of attributes (traits)
- create final *.json file with the description of ERC-721 metadata
So let’s get started writing the code: