-y
flag indicates that all the default values should be used without prompting the user for input.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:
hardhat.config.js
file in your project root
. Open it and replace the contents with the following:
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:
.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:
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:
.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:
/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:
web3.eth.accounts.create();
, just make sure you return the address
and privateKey
values at the end of your async
function:
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:
Bearer y0urChainstackAPIkeyHer3
, then store it in your .env
file as CHAINSTACK
:
axios
at the end of your wallet.js
dependencies like so:
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:
network.name
variable allows you to switch between networks with the --network
parameter whenever you are running a particular script with Hardhat.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:
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:
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:
wallet.js
script fully set up, the time has come for you to launch it via CLI using Hardhat:
.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.
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
:
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:
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:
--network
parameter:
Figure 1: music NFT project settings
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:
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.
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..env
file as value for the ETHERSCAN
key:
deploy.js
script in your /scripts
directory and add the following:
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
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.
__dirname
__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.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:
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
.
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.
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!
getBlockNumber
method in a given interval, so just copy over the following outside the main
function loop:
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:
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:
BUCK
, for example, BUCK-1337-8085-1337
.FOLD
, for example, FOLD-1337-8085-1337
..env
file for the BUCKET_ID
, FOLDER_ID
, and CHAINSTACK
keys like so: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:
addFiles
function as a constant with source
and single = false
as the two parameters it seeks:
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:
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:
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:
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:
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:
response
constant which will store the axios
response, once again differentiating between single and multiple files:
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:
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:
findCIDs
function should look like in the end:
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:
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:
pinNFT
function that will queue all the relevant functions in the correct order to have your media files and metadata JSON ready for minting:
pin.js
script via CLI, so you can watch the fireworks!
pin.js
code:
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:
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:
from
parameter it will return an error.
scripts/deploy.js
using the web3.js estimateGas
method with the safeMint
method of your contract as its target:
startMint
function as a constant. Inside it, start by adding some visual feedback for the mint address target and the estimate response like so:
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:
createReceipt
function like the following and call the startMint
function to truly make the mix complete:
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.
Figure 2: Music NFT on MetaMask; Source:
Figure 3: Music NFT on OpenSea; Source: