Note: the first part of the article doesn’t require any knowledge in programming but to follow the second part, you need to have a basic knowledge of JavaScript.
Note: the word “token” in this context means “a list of data that altogether represent something”. An NFT is a token because it is made of data related to something from the real world.
Note: in addition to these entrypoints and bigmaps, an NFT contract can implement other structures according to its use case, for example, you can have a bigmap with all the NFTs on sale and different entrypoints to set an NFT on sale, to purchase it or to withdraw it from the marketplace, you can have a burn entrypoint to destroy the NFTs you don’t want on the platform anymore, etc.
It is essential to understand the difference between “metadata” and “token_metadata”. The “metadata” bigmap holds information about the contract itself while the “token_metadata” bigmap holds information about every single token stored in the contract.
/mint
”, that will be called to create the NFT metadata and pin it on the IPFS with the associated picture. Before continuing with the code, you must set up an account with Pinata and get your API keys.process.env.NODE_ENV
variable. You can choose to have your API keys in a separate file, both in the development and production environment, but Heroku lets you define environment variables that are automatically injected in your build and stored securely, so this is generally the solution you would prefer, i.e having a separate file with your keys for development and having your keys in environment variables for production. Whichever solution you choose, the Pinata SDK can be easily instantiated by passing the API key and the secret key as parameters:corsOptions
variable, we indicate the URLs that are allowed to communicate with the server. During development, you should allow localhost
with the port you are using, then you can use the URL of your dapp.upload
is a middleware returnd by multer
that we set by passing an object whose dest
property is the path to the folder where we want to store the picture we will receivecors
with the options set up aboveexpress.json({ limit: “50mb” })
allows the app to receive up to 50 MB of JSON (which will be necessary to pass the picture)express.urlencoded({ limit: “50mb”, extended: true, parameterLimit: 50000 })
works in conjunction with the setting above and allows the server to receive a picture up to 50 MB in sizemint
endpoint!POST
endpoint (because of the picture we need to receive) that’s going to be called when a request comes to the /mint
route. We use the single
method of the upload
middleware from multer
with the “image”
parameter, which tells multer
that we are expecting to receive one image on this endpoint. We then store the request in a new variable cast to the any
type because TypeScript will raise an error later as it is unaware that the request has been modified by multer
.if(!multerReq.file)
, if there is none, the request fails with a 500 error code and a message. If a file was provided, we store the filename available at multerReq.file.filename
.testAuthentication
that verifies that you are properly authenticated. With that done, we can go ahead and pin the user’s picture in Pinata:Note: we have to pin the picture first before pinning the metadata to the IPFS because the metadata must include the hash of the picture.
createReadStream
method of the fs
package that you call with the path of the file that you want to convert to a readable stream. Remember that multer
automatically saved the image in the request in the uploads
folder, so this is where we will be looking for it.name
and keyvalues
of the pinataMetadata
property can be anything you want, the name
property is going to be displayed in the pin manager of the Pinata website.pinFileToIPFS
method of the Pinata SDK and pass as arguments the readable stream we created earlier and the options. This returns a promise that resolves with an object containing 2 properties we verify to make sure the pinning was successful: the IpfsHash
property holds the IPFS hash of the file we’ve just pinned and the PinSize
property holds the size of the file. If these 2 properties are defined and not equal to zero, we can assume the file was correctly pinned.unlinkSync
method of the fs
package and pass to it the path to the file.name
=> the name of the NFTdescription
=> a description of the NFTsymbol
=> the symbol will appear in wallets to represent your NFT, choose it wiselyartifactUri
=> the link to the asset formatted as ipfs://
+ the IPFS hashdisplayUri
=> the link to the picture formatted as ipfs://
+ the IPFS hashcreators
=> a list of the creators of the NFTdecimals
=> decimals are always set to 0
for NFTsthumbnailUri
=> the thumbnail to display for the NFT (for example, in wallets)is_transferable
=> whether the NFT can be transferred or notshouldPreferSymbol
=> allows wallets to decide whether or not a symbol should be displayed in place of a namepinJSONToIPFS
method to do what it says, pin JSON to the IPFS 😅 You can pass to it your JavaScript object directly (I assume the SDK converts it into JSON because passing a JSON string throws an error) and just like with the picture, you can set some metadata for the metadata! Once the promise resolves, we check if we got the IPFS hash back and that the data size is over 0. Now everything is pinned! We can send a simple response and attach the CID for the metadata and for the picture:Note: a reverse ledger is not a standard feature of NFT contracts and it may be absent from other platforms. If that’s the case, they may implement other ways of tracking token ids owned by a wallet address, for example, an external ledger file.
await Tezos.wallet.at(contractAddress)
creates an instance of the contract with different useful methods to interact with the contract or get details about, like the storage, that you can get using await contract.storage()
. After that, we have access to the whole storage.reverse_ledger
bigmap with the get
function:getTokenIds
is an array containing all the ids owned by the address
. We can simply loop through the array to get each individual id and look for the id in the ledger
bigmap:BigNumber
, so you have to call .toNumber()
first before being able to use it. Once we have the id, we can look for its metadata in the token_metadata
bigmap. The value returned is a Michelson map and the metadata path is going to be stored at the empty key. Because the path is stored as bytes, we use bytes2Char()
provided by the @taquito/utils
package to convert the returned bytes
into a string
. To finish, we return an object with 2 properties: the token id and the IPFS hash of the metadata.Note: although the standard requires us to store the IPFS hash in the following manner =>ipfs://IPFS_HASH
, there is no safeguard and any kind of data can be stored there, this is why we make a simple check withtokenInfo.slice(0, 7) === “ipfs://”
using the ternary operator to verify that at least this condition is fulfilled.
bind
attribute in Svelte makes it very easy to store the input in a variable that we can use later when we want to pin the NFT to the IPFS. A click on the upload
button will trigger the upload of the picture, its title and description to the server.pinningMetadata
and mintingToken
that we will update according to the result of the different steps of the upload to give some visual feedback to the users in the UI. Because we are not using a traditional, we must build the form data manually. After instantiating a new FormData
, we use the append
method to add the different details of the form, the picture, the title, the description and the creator of the NFT./mint
endpoint of our server app. The request should include the required headers and the form in the body
. The response from the server will include the hash for the picture and the hash for the metadata:response
comes, we can convert it to a usable JS object with the json
method. We check that the status
property is 200
and that the metadataHash
and imageHash
properties exist. If that’s the case, we can switch the UI from “pinning” to “minting” and send the transaction to the blockchain to save the NFT metadata:Tezos.wallet.at(contractAddress)
, then you call the mint
entrypoint in the contract.methods
property. Because the entrypoint expects bytes, we have to convert the IPFS hash into bytes without forgetting to prefix ipfs://
to make it valid. We pass the userAddress
at the same time to identify the owner of the NFT in the contract. After the NFT is minted and the minting is confirmed, we save the data of the NFT into newNft
to have it displayed in the interface, we reset the files, title and description variables to give the opportunity to the user to mint another NFT and we refresh the list of NFTs owned by the user by querying them (this is not absolutely necessary but getting up-to-date data from the contract never hurts).burn
endpoint: right now, your users can only create tokens, but you could also allow them to delete their NFTs.send({ amount: fee })
to monetize your service.