How to mint a music NFT: Dropping fire tunes with Chainstack IPFS storage
- Creates a Music NFT using a custom ERC721 contract (via OpenZeppelin Wizard) with metadata pointing to audio and cover files on IPFS
- Deploys contract with Hardhat to testnet; verifies it on Etherscan
- Pins the audio and cover image plus NFT metadata using Chainstack IPFS Storage
- Mints the NFT from your wallet, allowing you to see your music NFT on MetaMask/OpenSea with embedded audio
Main article
In the digital age, artists are constantly seeking innovative ways to share, monetize, and protect their work. Enter the world of non-fungible tokens (NFTs)—a revolutionary way for musicians to create, sell, and own their masterpieces. As the music industry evolves, NFTs are becoming the driving force behind a paradigm shift in how artists generate income and maintain control over their creations.
The music industry has always been a hotbed of innovation and change, from vinyl records to streaming services. Now, the advent of blockchain technology and the rise of NFTs are opening up an entirely new frontier for musicians and music enthusiasts alike. With the potential to revolutionize how we create, own, and trade music, NFTs are rapidly becoming an essential tool for artists looking to embrace the digital era.
But how does one go about minting a music NFT? If you’ve been wondering about this very question, you’ve come to the right place. In this comprehensive guide, we’ll walk you through the process step by step, demystifying the world of music NFTs and empowering you to join the revolution. So, whether you’re an aspiring musician, a dedicated collector, or simply curious about this exciting new development, read on and get ready to make some noise in the NFT space.
How to mint a music NFT?
Minting NFTs through coding can be slightly more challenging than relying on a marketplace platform to handle the process for you. However, it provides you with greater flexibility and allows you to engage with the core mechanics of minting. Let’s explore how you can get started:
Step 1: Get started with the basics
1.1: Obtain a node endpoint
Running your own node can be a time-consuming process. Instead, you can quickly and effortlessly deploy a node with Chainstack, saving both time and effort. To get started, visit the Chainstack website, create a free account, deploy a Sepolia node, and obtain its HTTPS endpoint.
Don’t worry about your testnet choice, the code base in this tutorial is structured in a way that will allow you to switch between Sepolia, and Mainnet freely, without having to rewrite everything. If you’re unsure about the process, follow the steps in our easy-to-understand quickstart guide.
1.2: Install core dependencies
If you haven’t installed node.js yet, go ahead and do so. Once that’s done, you’ll need to set up a workspace for your codebase. Create a new directory in your preferred location and initialize your node.js project by entering the following commands in your CLI (command line interface):
The -y
flag indicates that all the default values should be used without prompting the user for input.
And while you’re still on the topic of initializing your project, go ahead and do so for git, in case you haven’t done that already. To make this happen, download and install git, if it’s not present on your system yet, then execute the following in a freshly run CLI instance:
Next, install the Hardhat library with the web3
and hardhat-verify
plugins, which will provide you with the functionality required to interact with your node, as well as verify your deployed contract:
After the installation is complete, initialize your Hardhat project for JavaScript with:
Once the initialization is complete, you will find a hardhat.config.js
file in your project root
. Open it and replace the contents with the following:
1.3: Securely store your secrets
To securely store all values, which are best left away from prying eyes, such as your endpoint
you’ll utilize a .env
file. But before you set off to create such a file, go ahead and install the dotenv
package using npm
via CLI:
With the package installed, create a .env
file in your project root and transfer over your endpoint URL there as the first key-value pair. If you want to use all three Ethereum networks (Sepolia, and Mainnet) you can set it up like so:
This makes it possible for you to load the SEPOLIA
, or MAINNET
value from your .env
file in any script in your project that has the dotenv
dependency processed appropriately.
So, go ahead and break the ice by adding require('dotenv').config();
in your hardhat.config.js
script as early as possible. Referencing dotenv
before any and all other dependencies prevents possible conflicts, which may arise due to an inappropriate loading order.
Proceed by initializing each network with its corresponding endpoint in hardhat.config.js
as the values for the url
keys, using the dotenv process.env
method. You can also set a PRIVATE_KEY
value for the accounts
keys already, considering you will be getting a new one in the next section. Here’s how your hardhat.config.js
should look like, once you’ve set everything up correctly:
Before you continue, however, it is crucial to remember that you should NEVER upload your .env
file to any public repository, as it will serve as a container for all your sensitive information moving forward.
Apart from your endpoint URL, you will also store things like your private key inside the .env
file, which could expose your entire wallet if leaked.
Having this in mind, go ahead and create a .gitignore
file if you don’t have one already, so you can add .env
to it before you publish. Refer to the following example for how you can do that:
1.4: Create a new wallet and fund it
Once the initialization is complete, create a new /scripts
directory and add a new wallet.js
file in it. When you’ve created the new script, include the following lines of code to initialize the necessary dependencies:
Next, you will need to set up a function to create your wallet. To do this, you can use the web3.js method web3.eth.accounts.create();
, just make sure you return the address
and privateKey
values at the end of your async
function:
Once entered, you will also need to create another async
function that will fund your wallet with testnet ETH from the Chainstack faucet. To do this you will first need to take care of a few things:
-
Create a Chainstack API key and copy it, which will be similar to
Bearer y0urChainstackAPIkeyHer3
, then store it in your.env
file asCHAINSTACK
: -
Install axios library to be able to send HTTP requests to the faucet:
-
Require
axios
at the end of yourwallet.js
dependencies like so:
With that taken care of, you can move forward with your fundWallet
function, by adding address
and apiKey
as its required parameters. Next, create a new const
called apiUrl
, add the faucet API URL, and place ${network.name}
at the end of it to be able to select the right testnet version:
Using the Hardhat network.name
variable allows you to switch between networks with the --network
parameter whenever you are running a particular script with Hardhat.
Hopping back to your fundWallet
function, it is time for you to close it off with a try-catch
loop that will contain your axios
request to the Chainstack faucet. Inside the try
part of the loop, add a new const
called response
, where you will set the axios
settings and return the response:
This creates a POST
request to the Chainstack faucet apiURL
with your address
, your apiKey
as the Authorization
header, and application/json
as the Content-Type
.
And with the try
part of the loop taken care of, you can wrap up the entire thing by catching and then throwing an error
, should it occur. Here’s how your wallet.js
script should look like at this point:
But hey, hang on a minute! There is still something missing from your wallet.js
script—some of the variables are not set yet and neither are the calls that will run the two functions you have set. So, go ahead and create them but do so in a new async
function called main
.
And while you’re at it, why not add some visual feedback to make it easy for you to process their results. Since there is little new to learn with this function, let’s straight up recap with the full wallet.js
script until now:
With the wallet.js
script fully set up, the time has come for you to launch it via CLI using Hardhat:
You will get the following response if you have the additional visual feedback applied:
Congrats, you have now created a wallet and funded it successfully with the Chainstack faucet!
Your next step is to enter the two keys into your .env
file as values for the WALLET
and PRIVATE_KEY
keys. You can also use a pre-existing wallet address with the appropriate private key attached to it, by setting them in your .env
file instead.
1.5: Verify your wallet balance
Now it’s time to verify if your balance has been updated. Create a new file named balance.js
within the /scripts
directory and copy over the first two dependencies from your wallet.js
script, without adding the one for axios
.
Then, create a new address
constant and set its value to the WALLET
you just copied in your .env
file, using process.env.WALLET
:
With that taken care of, go ahead and create an asynchronous function that will use the address
constant as a parameter and within it call the web3.js getBalance
method.
Since it will take more time for your node to process this request than that of executing the rest of the function’s code locally, make sure you add await
prior to the method:
In doing so you will prevent the rest of the code from processing before the method’s promise is resolved once a response is returned.
By default, the getBalance
method will return a very barebones balance value in wei, so go ahead and add some extra visual feedback by calling the web3.js fromWei
method to convert the wei output to ETH units.
Once ready, go ahead and wrap things up by calling your getbal
function with the address
parameter at the end of your script:
Once you’re ready, go ahead and run the script via Hardhat in CLI using the --network
parameter:
Step 2: Prepare and deploy the smart contract
2.1: Draft an NFT smart contract
Now that you’ve completed all the necessary preparations, it’s time to create your smart contract. While this might seem intimidating at first, you can rest easy knowing that OpenZeppelin provides pre-built, security-audited contract templates.
The cherry on top? You can use their wizard to create a customized contract that perfectly fits your needs. For our sample project, we will be using the following settings:
Figure 1: music NFT project settings
Aside from the name and symbol, which are quite self-explanatory, we’ll include a Mintable
option with Auto Increment Ids
enabled. This feature allows privileged accounts (e.g., your account) to mint new tokens, which can represent new additions to your collection.
We also need to enable the URI Storage
option, as it allows us to attach media files like images to our NFTs. Additionally, we’ll incorporate the Ownable
option to enable administrative actions.
While there are more parameters available in the wizard, they fall outside the scope of this tutorial. However, don’t hesitate to experiment with them if you wish to explore additional functionalities beyond the basic ones we’ve implemented so far.
Finally, ensure that the OpenZeppelin dependencies are available in your project by installing its contracts
library with the following:
2.2: Compile the minter smart contract
With your contract ready, copy the code from the OpenZeppelin wizard into a new file, such as MyMusicNFT.sol
, or click Download in the top-right corner. Then, create a new directory called contracts
in your project root
and place your smart contract in it.
Hardhat automatically compiles a new contract when it needs it, without additional input from your end but you can also do that manually using npx hardhat compile
. This will compile all contracts located in the contracts
directory, so if you have more than one, they will all be process accordingly.
Before you move forward with deploying your compiled contract, however, there are some things to take care of first:
-
Create an Etherscan account and API key, so you can verify the contract once it is deployed.
-
Save the API key in your
.env
file as value for theETHERSCAN
key: -
Create a
deploy.js
script in your/scripts
directory and add the following:
Apart from referencing the hardhat-verify library you installed earlier, there are two packages you haven’t encountered in this tutorial yet—fs
and path
, both of which ship by default with node.js.
The former, fs
, is a file system module, that allows you to interact with local files, while the latter is used for handling and transforming file paths.
As the next step, create a few constants called contractName
, artifactPath
, contractArtifact
, contractABI
, and contractBIN
. The first one will be used by Hardhat to determine which contract you will be interacting with, while the second and third to locate and read the compiled contract artifact. This artifact contains your smart contract’s ABI
and its bytecode
, or BIN
.
ABI an BIN
The application binary interface (ABI) facilitates interaction between software modules, translating Solidity contract calls for Ethereum’s Virtual Machine (EVM) and decoding transaction data. On the other hand, bytecode (BIN) is the binary output of Solidity code and consists of machine-readable instructions including one-byte “opcodes”, hence the name.
The artifact, containing your smart contract’s ABI
and BIN
is automatically generated when you compile it with Hardhat as /artifacts/contracts/YourContractName.sol/YourContractName.json
, where YourContractName
is the name of your contract. In this tutorial’s case, it is MyFirstMusicNFT
, so go ahead and set the contractName
constant’s value to your smart contract’s actual name.
To help your code discover the location of the artifact, you can use the path.resolve
method with __dirname
as the first parameter, and the JSON path mentioned in the paragraph above, preceded by ..
to indicate the parent directory.
About __dirname
In node.js, __dirname
is an environment variable that gives the absolute path to the directory of the currently running file. Unlike ./
, which denotes the current directory of a file, or ../
, which refers to its parent directory, __dirname
always points to the precise directory where the executing file resides.
Then, go ahead and read the JSON file with the fs.readFileSync
method as the first parameter but make sure you have set utf-8
as the encoding type in the second. Lastly, pass the entire thing as a JSON object by wrapping it with JSON.parse
. Here’s how this part of your script should look like:
2.3: Deploy your smart contract
Next, it’s time for you to put together the rest of the deploy.js
script by adding a new function to handle the actual deployment process.
To do that, define a fresh asynchronous main
function first and create a new contract object constant contractNFT
by calling the web3.js Contract
method with contractABI
and address
as parameters, respectively.
Then, deploy the contractNFT
contract object as a constant contractTX
transaction object by applying the deploy
method to it with a data
parameter set to contractBIN
.
Once you’ve set this up, proceed by creating a function to estimate the gas of the contract deployment transaction contractTX
. To do that, just slap the web3.js estimateGas
method at the back of the contractTX
object, and add some relevant visual feedback.
Then, continue by calling the web3.js signTransaction
method with two parameters, the first being an object, and the second—privKey
. Compose the object parameter using the key from
set to address
, data
as contractTX
with the encodeABI
method applied to it, and gasCost
as value for thegas
one as the third.
With this taken care of, your next step is to ask for a receipt with a createReceipt
constant with value calling the web3.js sendSignedTransaction
method with a single parameter, preceded by an await
operator.
Set the createTransaction
constant as a value for it, apply the rawTransaction
method to it, and then add some visual feedback to display the receipt as the closing statement for the entire deploy
function. Don’t forget to run it either!
Now, it’s time to verify your contract with Hardhat after it has been deployed. But considering it takes roughly 5 blocks of confirmations before you are able to do that, let’s set up a function to wait for the appropriate time before launching the verification.
The function in itself just checks the current block using the web3.js getBlockNumber
method in a given interval, so just copy over the following outside the main
function loop:
Last, you need to set up the verification function itself, so create a new asynchronous verifyContract
function and inside it call the waitForBlocks
and run("verify:verify")
, while waiting for them to execute first.
Make sure to also add the address
and constructorArguments
parameters, with createReceipt.contractAddress
as value for the former and a blank array []
as one for the latter:
That’s it! Now all you have to do is run the deploy.js
script via CLI with npx hardhat run scripts/deploy.js --network $NETWORK
to have your very own smart contract deployed on the testnet of your choice.
But still, the steps that led you here were indeed a tad bit more complex than what you had done so far in this tutorial, so let’s recap by reviewing the entire deploy script’s code:
Congratulations! Should you have followed the steps so far correctly, your first NFT contract is now live on the testnet of your choice! With that milestone achieved, it is time to define the metadata properties of your NFT prior to minting.
Step 3: Pin the metadata and mint your NFTs
3.1: Getting started with Chainstack IPFS Storage
Before you can mint any NFT, you will first need to pin all relevant media files to the Interplanetary File System (IPFS). Thanks to this, you won’t have to rely on the availability of any centralized provider, instead having it permanently accessible. In IPFS, uploading is adding a file to the network, while pinning is ensuring its persistent availability by preventing its removal on a specific node.
To make the entire process a real walk in the park, you can use Chainstack IPFS Storage, which will provide a seamless interface and API for you to do that. You can use the interface to create the bucket and folder that you need to pin the files manually, or follow this process to do so via the API:
- Sign in to your Chainstack account via the console and select IPFS Storage from the navigation on the left.
- Click the Create bucket and enter a name of your choice.
- Inside the bucket, click New folder with a name of your choice.
- Open the folder and bucket and examine the URL in the address bar for each of them.
- Copy the bucket ID starting with
BUCK
, for example,BUCK-1337-8085-1337
. - Copy the folder ID starting with
FOLD
, for example,FOLD-1337-8085-1337
. - Paste the three values in your
.env
file for theBUCKET_ID
,FOLDER_ID
, andCHAINSTACK
keys like so:
3.2: Pin your NFT media with Chainstack IPFS Storage
With the prerequisites taken care of, it is time for you to pin the media files with Chainstack IPFS Storage. In the tutorial repo, you will find a tutorial audio file and cover image in the src
directory that you can use freely to test the waters.
Should you prefer to pin them with Chainstack IPFS Storage via the interface manually, you can do so already and jump to the next step, where you will set up the JSON metadata. Otherwise, it is time for you to create a new pin.js
file to do that via the API.
To get started with the new pin script, process the dependencies first. You will need the dotenv, fs, axios, and the FormData packages, the latter of which you can install with the following command:
Here’s how the start of the script should look like:
Next, define the location of the media files you will be pinning with Chainstack IPFS Storage via the API like so:
Once ready, it is time for you to define a new asynchronous addFiles
function as a constant with source
and single = false
as the two parameters it seeks:
The second parameter single = false
will serve to differentiate between pinning single and multiple files, as the API URLs for them are not the same. Set up the differentiation in the code like so:
Next, define a new data
constant with a value equal to new FormData()
and follow up with an if-else statement, checking for the state of the single
parameter:
Inside the if
statement, add some visual feedback to display which file you are attempting to pin by using the JSON.stringify
method with the source[0].title
value. Afterward, add the bucket_id
, folder_id
, file
, and title
to the data
object via the append
method.
You can load the bucket_id
and folder_id
values from your .env
file by setting them as the first parameter for the append
method and using process.env.BUCKET_ID
and process.env.FOLDER_ID
, respectively as the second.
In turn, do so for the file
and title
values but referencing source[0].file
and source[0].title
accordingly:
The process is relatively similar for the else
part of the statement, the only difference being that you need to use the forEach
method to make sure all entries are referenced correctly:
Once ready, proceed by creating a new config
constant, which will set up the axios
configuration. Here you will also make a reference to your Chainstack API authorization token in a fashion similar to this:
Next, create a new response
constant which will store the axios
response, once again differentiating between single and multiple files:
With the addFiles
function successfully set up, it is time to do so for the findCIDs
one. Its purpose will be to obtain the content IDs (CIDs) of the files you have pinned, which are unique identifiers that allow universal access via any IPFS client.
So, go ahead and create it as an asynchronous function by defining a new findCIDs
constant and set fileID
and single = false
as the two parameters it would accept. Then, proceed by removing possible excess characters like so:
It is quite possible that the CID
will not be ready when your script moves on to find them, so let’s set up a redundancy process that will automatically retry the search.
Start by defining a new maxRetries
constant, as well as a retryTimeout
one, setting the former’s value to 5
and the latter to 22000
. Simply put, this will make the function retry for a maximum of 3 times with an 11-second timeout between each retry.
Next, create an if-else
statement with the !single
parameter in the if
part. Inside it, create a new cid
and name
temporary array variables. After that, create a for
loop that will use the push
method to store the CID and title values of the freshly pinned files:
The rest of the function follows a similar logic but additionally features the retry loop, the axios configuration, as well as some error-checking code. Here’s how the entire findCIDs
function should look like in the end:
Once the CIDs are successfully fetched with the previous function, you will need to write them to a JSON file along with the rest of the metadata of your NFT. Said JSON file will then be used to mint an NFT with the appropriate contents.
So, go ahead and create a new asynchronous writeJSON
function as a constant with the pinCID
and pinName
parameters as the ones it should accept.
Create new temporary audioIPFS
and coverIPFS
variables and then an if-else
statement with pinCID && pinName
as its parameters.
Inside the if
statement, create a for
loop which will piece together the appropriate URLs of your media pins:
In this tutorial, the URLs are pieced together using the Chainstack IPFS Storage gateway ipfsgw.com
which will make your pins available faster, however, it is generally recommended to use ipfs://
for truly universal access, even if it takes much longer to propagate this way.
That being said, finish off the rest of the function by writing the metadata you collected earlier to a JSON file in the src
directory. The entire writeJSON
function should be set up in a fashion similar to this:
Lastly, create a new asynchronous pinNFT
function that will queue all the relevant functions in the correct order to have your media files and metadata JSON ready for minting:
That’s it! You should now have a fully working script that will pin your media files, write the relevant metadata to JSON, pin said JSON, and have everything ready for minting automatically, even if it takes a couple of retries to do so.
Don’t forget to run the pin.js
script via CLI, so you can watch the fireworks!
If the script returns Error during NFT pinning: Request failed with status code 400 as response.
If you encounter this error, there is some degree of duplication with existing files you have pinned previously.
To resolve it, you must either set new filenames and titles for the interrupted pins, or delete the previous ones.
Do note that once you delete them, any previously minted NFTs using them as source for metadata will no longer display correctly and will be left as blank containers permanently.
And considering this has been the most complex script you have set up until now (and the rest of the tutorial), it is certainly wise to recap with the full pin.js
code:
3.3: Create the script for minting
By the time you reach this step, you should have successfully pinned your NFT media files with Chainstack IPFS Storage, and have their CIDs referenced in a JSON file that was also pinned there.
The JSON file must contain an image
key to store the NFT cover, an animation_url
one for the audio file, name
for the track title, and optionally a description
, as well as an external_url
for a link to your profile for example. It should look similar to this:
If that is indeed the case, you can move forward by creating a new mint.js
file inside the scripts
directory for the minting script. Begin by processing the dependencies for the dotenv
, hardhat-web3
, and fs
modules.
Proceed by initializing your wallet address, private key, deployed smart contract ABI, and JSON metadata URL, as well as the appropriate deployed contract address for each network via a simple if
loop:
Then, create a new contract object and set the interactions origin to the owner address. This is crucial for the successful execution of the script, as earlier you set up the contract to allow minting only from the owner address. Without the correct from
parameter it will return an error.
Next, create a gas estimation function like the one in the scripts/deploy.js
using the web3.js estimateGas
method with the safeMint
method of your contract as its target:
With that taken care of, move forward by defining a new asynchronous startMint
function as a constant. Inside it, start by adding some visual feedback for the mint address target and the estimate response like so:
Continue the function by defining the transaction details and signing it with the web3.js signTransaction
method, using as the first parameter an object with the address
value for the from
key, the contractAdrs
one for to
.
Then, for data
, call the contract object’s safeMint
method with the encodeABI
method attached to it, and gas
set to gasCost
. Use the privKey
constant as the second parameter for a final result like this:
Lastly, make sure you get the transaction receipt by creating a createReceipt
function like the following and call the startMint
function to truly make the mix complete:
Congratulations! After running the mint.js
script with Hardhat using npx hardhat run scripts/mint.js --network NETWORK_NAME
, you should now have minted the first music NFT in your collection!
To add more NFTs, simply follow the process to create a new JSON file with different parameters and rerun the relevant functions, making sure to select the appropriate JSON file for the metadata variable in the mint.js
function.
3.4: View your NFTs on MetaMask and OpenSea
If you’ve managed to complete the previous steps, this one should be quite simple. Begin by opening your MetaMask wallet or downloading it if you haven’t already. Log in and choose the Sepolia network at the top.
Click the icon in the top right corner (not the MetaMask logo) and select Import Account. Opt for Private Key, as that’s how you initially set up your address, and paste it into the designated field.
Afterward, head to the NFTs tab and select Import NFTs at the bottom. Paste your music NFT contract’s address into the first field, and the token ID in the next. If it’s your first mint the token ID will be 0.
Finish the process by clicking Add, and there you have it—your music NFT is now visible in MetaMask!
Figure 2: Music NFT on MetaMask; Source:
Unfortunately, MetaMask does not permit you to play the audio associated with your music NFT. To accomplish this, visit OpenSea, specifically the Sepolia Testnet version found here. Use MetaMask to log in, and once successful, hover over your avatar and click Profile.
You should see your music NFT; click on it to reveal its details. If the featured image only shows a small preview, click on it once more and then a third time to open it in full screen, so your tune starts playing.
Figure 3: Music NFT on OpenSea; Source:
Access the tutorial repo
To make it even easier for you to follow and implement the concepts discussed in this tutorial, we have prepared a comprehensive code repository for your convenience. The repo contains all the necessary files, code snippets, and resources used throughout the tutorial.
You can access the full tutorial code at its dedicated GitHub repo here.
Feel free to download or clone the repository, and use it as a reference while working through the tutorial. This will help you save time and ensure that you have a complete understanding of the concepts presented.
Bringing it all together
With all of this taken care of, you have successfully dipped your feet and managed to explore the fascinating world of minting music NFTs. And thanks to this, you now have established a step-by-step process on how to create, deploy, and manage your very own digital music collectibles.
By leveraging blockchain technology, artists and collectors like yourself can take full advantage of this new avenue for monetization, creative expression, and secure ownership.
As you embark on your music NFT journey, remember that the possibilities are endless. Experiment with different parameters and smart contract functionalities to create NFTs that not only represent your unique artistic vision but also offer value to your audience.
Whether you’re an established musician or an emerging talent, music NFTs can open doors to new opportunities and reshape the way you engage with and appreciate the art of sound.
So go ahead, take the leap, and begin minting your very own music NFTs. Share your creations with the world, and witness the transformative power of this cutting-edge technology in the ever-evolving music industry. Happy minting!