How to create and deploy NFTs on Ethereum and OpenSea using python and solidity
Harry Thuku March 27, 2023, 9:28 a.m.
This tutorial is aimed at providing a step-by-step guide on how to create, host, and publish an NFT collection using python and solidity and deploying it on the Ethereum Blockchain and OpenSea marketplace.
Create and deploy NFTs on Ethereum and OpenSea using Python and Solidity | HtoStudios
Prerequisites
Before we dive into the tutorial, it's important to have a foundational understanding of Python and Solidity programming languages, as well as a basic knowledge of how blockchains operate. With these prerequisites, you'll be well-equipped to follow along and create your own NFT collection.
What are NFTs?
NFTs, or non-fungible tokens, are a type of cryptographic token that exists on a blockchain. Unlike fungible tokens, such as USDC, which are identical and interchangeable, NFTs are unique and cannot be replicated. This property makes NFTs ideal for representing assets that have a unique identity, such as artwork, real estate, vehicles, and more. By using NFTs, the buying and selling process of such assets becomes more streamlined compared to traditional financial systems. Additionally, NFTs provide an immutable and censorship-resistant record of ownership history, making them a more secure and efficient method for documenting the ownership of these assets than traditional systems
Getting started with Brownie
Before we get started, make sure you have python and brownie installed in your system. Brownie is a python based development and testing framework for smart contracts targeting the Ethereum Virtual Machine. Brownie supports both Solidity and Vyper, two of the most popular smart contract development languages. However, in this tutorial, we are going to use solidity.
To install brownie you will need to have pipx installed in your system. Pipx is a python tool used to install and run end-user applications written in python. It functions like npx in the JavaScript ecosystem.
To install pipx:
# https://github.com/htostudios/nft-collection
python3 -m pip install --user pipx
python3 -m pipx ensurepath
You may need to restart your terminal after installing pipx
To install Brownie:
# https://github.com/htostudios/nft-collection
pipx install eth-brownie
Once installation is complete, type brownie on the terminal to verify the version and whether the installation was successful.
Creating the project
Create a new folder for your project and cd into it. In this case, our project folder is called “nft-collection”. Initialize a new brownie project inside the folder using the “brownie init” command and open it with your favorite text editor.
# https://github.com/htostudios/nft-collection
$ mkdir nft-collection
$ cd nft-collection
$ brownie init
You will know everything is going on smoothly if you have a project structure that looks like this:
The NFT Smart Contract
Open the contracts folder and create a new Solidity file called “NFTCollection.sol”. This is going to be the contract that we will use to create and deploy the collection.
// https://github.com/htostudios/nft-collection
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
Open the “NFTCollection.sol” file and specify the License Identifier and the solidity version by typing the lines above.
Next, we are going to import two contracts from OpenZeppelin and Chainlink which are going to help us with writing our smart contract by taking care of a lot of abstraction for us hence reducing the amount of code that we will need to write.
// https://github.com/htostudios/nft-collection
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol";
From OpenZeppelin we import the ERC721 interface which is an implementation of the ERC-721 NFT token standard for Ethereum, and from chainlink, we will import the VRFConsumerBase Contract which will help us in generating random numbers for the purpose of uniqueness later on in the tutorial.
Configuring the import
Now that we are importing the two files from github, we need to tell brownie where to look by specifying the download location in a configuration file. In the root folder of your project, create a new file called “brownie-config.yaml”. This is where all of our project configurations and settings will go.
Open the config file and type:
# https://github.com/htostudios/nft-collection
project:
name: NFT-COLLECTION
version: 0.1.0
github:
token: ${GITHUB_TOKEN}
dependencies:
- smartcontractkit/chainlink-brownie-contracts@1.1.1
- OpenZeppelin/openzeppelin-contracts@3.4.0
compiler:
solc:
remappings:
- '@chainlink=smartcontractkit/chainlink-brownie-contracts@1.1.1'
- '@openzeppelin=OpenZeppelin/openzeppelin-contracts@3.4.0'
Now when Brownie sees @chainlik or @openzeppelin, it will know where to import the dependencies from. Alright! Let’s get back to our contract. You will also need to create a GitHub Access Token from your account so that brownie can make API calls to GitHub and import the dependencies. Once you are done, create a new “.env” file in the root directory of your project and add the GitHub API Token.
# https://github.com/htostudios/nft-collection
export GITHUB_TOKEN=”YOUR_GITHUB_API_TOKEN”
Remember to add the “.env” file to “.gitignore” so that you don’t accidentally push the GitHub token to a remote repository. Once we’ve completed the GitHub configurations successfully, we can get back to the contract.
// https://github.com/htostudios/nft-collection
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol";
contract NFTCollection is ERC721, VRFConsumerBase {
}
We initialize a new contract by typing the contract keyword followed by the contract name. In this case ,our contract name is “NFTCollection” and it extends the ERC721 and VRFConsumerBase contracts. The “contract” keyword functions like “class” in other programming languages like Java.
// https://github.com/htostudios/nft-collection
contract NFTCollection is ERC721, VRFConsumerBase {
uint256 public tokenCounter;
bytes32 public keyHash;
uint256 public fee;
}
We are going to initialize a few variables that we will use in our contract. The first is the “tokenCounter” which keeps a record of the total number of tokens that have been created, next is the “keyHash” which we will use for generating randomness later and lastly the “fee” which will be the fee that we will pay chainlink to generate a verifiable randomness function.
// https://github.com/htostudios/nft-collection
uint256 public tokenCounter;
bytes32 public keyHash;
uint256 public fee;
mapping(bytes32 => address) public requestIdToSender;
event requestedCollectible(bytes32 indexed requestId, address requester);
Now we will create a Mapping “requestIdToSender” which we will use to keep a record of all the NFTs that have been created, and an event that is emitted every time a new NFT is created. A mapping is a data type in the solidity that is used to store data in key-value pairs. It functions like a dictionary or a hash table in other programming languages. You can specify the data types that you want in this case we are going to map a “bytes32” to an “address”. Events give us an easy way of checking for state changes on the blockchain which is great for development and debugging purposes. You can still access all state changes that happen on a blockchain by looking at the transactions, but if you want access to specific data or actions you can use events.
Creating a constructor for our contract
Now we are going to create a constructor for our contract
// https://github.com/htostudios/nft-collection
constructor (address _vrfCoodinator, address _linkToken, bytes32 _keyHash,uint256 _fee)
VRFConsumerBase(_vrfCoodinator,_linkToken)
ERC721("2048 Shots", "2048") {
tokenCounter = 0;
keyHash = _keyHash;
fee = _fee;
}
In solidity, constructors are used for initializing state variables of the contract when the contract is deployed. In this case, we initialize the values of the “tokenCounter”, “keyHash”, and “fee” which are the static variables of our contract. We also initialize values of our parent contracts “VRFConsumerBase” and “ERC721”. For the “VRFConsumerbase” contract, we specify two values, the “_vrfCoodinator” which is a contract address that is used for the randomness function generation, and the “_linkToken” which is the contract address of the Link Tokens that we will use for the fee payments.
For the ERC721 contract, we specify the name of our collection “2048 Shots” and its symbol “2048”.
2048 Shots
For the purpose of this tutorial, we are going to use screenshots of a game called 2048 as the dummy images for our NFT Collection. I like the game because it has taught me a lot about consistency and you can create cool patterns with it. The images are screenshots of me playing the game. I hope in they are worth millions by the next Bull Run 😂😂. This is what they look like:
You can find the images inside the “img” folder of this repository. Download and store them in a similar folder in the root directory of your project as well.
Creating the NFTs
// https://github.com/htostudios/nft-collection
function createCollectible() public returns(bytes32) {
bytes32 requestId = requestRandomness(keyHash,fee);
requestIdToSender[requestId] = msg.sender;
emit requestedCollectible(requestId,msg.sender);
}
To create the NFTs we are going to implement two functions, first, the “createCollectible()” function which simply calls the “requestRandomness()” function a member of the “VRFConsumerBase” chainlink contract, to generate a truly verifiable randomness function. Since blockchains are deterministic systems, it is impossible to get a truly random value. When one calls a random generator function on a blockchain, all nodes call the function and each of them generates its own random value making it difficult to coordinate in returning the random value which is a big security vulnerability. Hence why we use optimal strategies like the chainlink VRF. Chainlink VRF is a way of generating verifiable randomness functions. Chainlink VRF follows the request-response pattern of data, hence why we call the “requestRandomness()” function with the “keyHash” and the fee that we pay to the chainlink oracle node which will provide us with a random number as the data response. The random number will be used as the “requestId” for the caller of the function. That’s why we map the “requestId” to the caller of the function “msg.sender”.
// https://github.com/htostudios/nft-collection
function fulfillRandomness(bytes32 requestId, uint256 randomNumber) internal override {
uint256 newTokenId = tokenCounter;
address nft_owner = requestIdToSender[requestId];
_safeMint(nft_owner,newTokenId);
tokenCounter = tokenCounter + 1;
}
The second function that we implement is the “fulfillRandomness()” function which is an internal function, meaning it can only be called by a smart contract. In this case, the “VRFConsumerBase” contract. When the “requestRandomness()” function is called as the request, the “fulfillRandomness()” function will be called as the response, and that’s why we call the “_safeMint()” function, which is a member of the ERC721 Interface, inside the “fulfillRandomness()” function with the owners address and a token ID as the parameters in order to create a new NFT. Finally, we increment the value of the “tokenCounter” in order to keep a record of the total number of NFTs that have been created.
// https://github.com/htostudios/nft-collection
function createCollectible() public returns(bytes32) {
bytes32 requestId = requestRandomness(keyHash,fee);
requestIdToSender[requestId] = msg.sender;
emit requestedCollectible(requestId,msg.sender);
}
function fulfillRandomness(bytes32 requestId, uint256 randomNumber) internal override {
uint256 newTokenId = tokenCounter;
address nft_owner = requestIdToSender[requestId];
_safeMint(nft_owner,newTokenId);
tokenCounter = tokenCounter + 1;
}
Before we start working with python, let’s implement a final function that will be used to set the token URIs for our NFTs.
// https://github.com/htostudios/nft-collection
function setTokenURI(uint256 tokenId, string memory _tokenURI) public {
require(_isApprovedOrOwner(_msgSender(),tokenId),"ERC721 caller is not owner nor approved");
_setTokenURI(tokenId, _tokenURI);
}
Our final contract should look like this:
// https://github.com/htostudios/nft-collection
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol";
contract NFTCollection is ERC721, VRFConsumerBase {
uint256 public tokenCounter;
bytes32 public keyHash;
uint256 public fee;
mapping(bytes32 => address) public requestIdToSender;
event requestedCollectible(bytes32 indexed requestId, address requester);
constructor (address _vrfCoodinator, address _linkToken, bytes32 _keyHash, uint256 _fee)
VRFConsumerBase(_vrfCoodinator,_linkToken)
ERC721("2048 Shots", "2048") {
tokenCounter = 0;
keyHash = _keyHash;
fee = _fee;
}
function createCollectible() public returns(bytes32) {
bytes32 requestId = requestRandomness(keyHash,fee);
requestIdToSender[requestId] = msg.sender;
emit requestedCollectible(requestId,msg.sender);
}
function fulfillRandomness(bytes32 requestId, uint256 randomNumber) internal override {
uint256 newTokenId = tokenCounter;
address nft_owner = requestIdToSender[requestId];
_safeMint(nft_owner,newTokenId);
tokenCounter = tokenCounter + 1;
}
function setTokenURI(uint256 tokenId, string memory _tokenURI) public {
require(_isApprovedOrOwner(_msgSender(),tokenId),"ERC721 caller is not owner nor approved");
_setTokenURI(tokenId, _tokenURI);
}
}
Compiling and deploying the contract
To compile the contract run “brownie compile” command from the root directory of your project in the terminal. If everything goes smoothly, you will find the compiled version of your contract stored in the build folder of your project.
Deploying the Contract
We are going to write python scripts that will automate the process of deploying and interacting with our contracts. Inside the scripts folder, create four files; “deploy.py” which will be used for deploying our contract, “helpful_scripts.py” which we will use to store some helpful utilities that we will use in our project globally, “create_collection.py” which will be used to create the collection, and finally “__init__.py” file to make python view our scripts folder as a package. Once that’s done, open the “helpful_scripts.py” file and paste the following code:
# https://github.com/htostudios/nft-collection
from brownie import accounts,config,network,interface,VRFCoordinatorMock,LinkToken,Contract
from web3 import Web3
FORKED_LOCAL_ENVIRONMENTS = ["mainnet-fork","mainnet-fork-dev"]
LOCAL_BLOCKCHAIN_ENVIRONMENTS = ["development","ganache-local"]
DECIMALS = 8
OPENSEA_URL = "https://testnets.opensea.io/assets/goerli/{}/{}"
STARTING_PRICE = 200000000000
OPENSEA_URL = "https://testnets.opensea.io/assets/goerli/{}/{}"
def get_account(index=None,id=None):
if index:
return accounts[index]
if id:
return accounts.load(id)
if network.show_active() in LOCAL_BLOCKCHAIN_ENVIRONMENTS or network.show_active() in FORKED_LOCAL_ENVIRONMENTS:
return accounts[0]
return accounts.add(config["wallets"]["from_key"])
contract_to_mock = {
"vrf_coordinator":VRFCoordinatorMock,
"link_token":LinkToken
}
def get_contract(contract_name):
contract_type = contract_to_mock[contract_name]
if network.show_active() in LOCAL_BLOCKCHAIN_ENVIRONMENTS:
if len(contract_type) <= 0:
deploy_mocks()
contract = contract_type[-1]
else:
contract_address = config["networks"][network.show_active()][contract_name]
contract = Contract.from_abi(contract_type._name, contract_address, contract_type.abi)
return contract
def deploy_mocks(decimals=DECIMALS,initial_value=STARTING_PRICE):
print("Deploying Mocks!")
account = get_account()
link_token = LinkToken.deploy({"from":account})
VRFCoordinatorMock.deploy(link_token.address,{"from":account})
print("Mocks Deployed!")
def fund_with_link(contract_address,account=None,link_token=None,amount=Web3.toWei(1,"ether")):
account = account if account else get_account()
link_token = link_token if link_token else get_contract("link_token")
link_token_contract = interface.LinkTokenInterface(link_token.address)
tx = link_token_contract.transfer(contract_address,amount,{"from":account})
tx.wait(1)
print("Funded contract with Link!")
return tx
The helpful Scripts
You can think of the helpful scripts as our project settings. The main function in this file is the “get_account()” function, which returns different accounts in different networks that will be used for signing transactions depending on the active network. If we are working on the local environments and forks, brownie will auto-generate ten accounts that we can use. In this case let’s use the first account for local and forked networks. If we are on a live blockchain network, the “get_account()” function returns our live private key which we will configure as an environment variable for security reasons.
Open the “.env” file and add the following lines:
# https://github.com/htostudios/nft-collection
export WEB3_INFURA_PROJECT_ID=”YOUR_INFURA_API_KEY”
export PRIVATE_KEY=”YOUR_METAMASK_PRIVATE_KEY”
Replace the PRIVATE_KEY value with your Metamask or any live private key and head on to Infura and generate an API Key. Infura is a Node provider for service that helps Decentralized Applications to easily connect to blockchains.
Once you complete, open the “brownie-config.yaml” file and the following lines which will tell brownie where to look for our environment variables and configure the default settings that we will need for working with Goerli since we are going to be deploying the NFTs on the Goerli testnet.
# https://github.com/htostudios/nft-collection
dotenv: .env
networks:
goerli:
vrf_coordinator: '0x2bce784e69d2Ff36c71edcB9F88358dB0DfB55b4'
link_token: '0x326C977E6efc84E512bB9C30f76E30c160eD06FB'
verify: True
keyhash: "0x0476f9a745b61ea5c0ab224d3a6e4c99f0b02fce4da01143a4f70aa80ae76e8a"
fee: 100000000000000000
wallets:
from_key: ${PRIVATE_KEY}
The helpful scripts also contains the “OPENSEA_URL” which we will use for constructing the urls of the NFTs to be viewed from Opensea. We will use it in our “create_collection.py” script.
The Deploying Scripts
Open the “deploy.py” file and paste the following code:
# https://github.com/htostudios/nft-collection
from scripts.helpful_scripts import get_account,get_contract
from brownie import NFTCollection,config,network
def deploy():
account = get_account()
nft_collection = NFTCollection.deploy(
get_contract("vrf_coordinator"),
get_contract("link_token"),
config["networks"][network.show_active()]["keyhash"],
config["networks"][network.show_active()]["fee"],
{
"from":account
},
)
print("advanced collectible contract has been deployed")
return nft_collection
def main():
deploy()
The above script will help us with deploying our “AdvancedCollectible” contract on any network that we may decide to work with. However, in this tutorial, we are going to deploy the contract on Goerli. Run the following command in the terminal to deploy:
# https://github.com/htostudios/nft-collection
$brownie run scripts/deploy.py --network goerli
You will know everything is successful if you get an output like this:
# https://github.com/htostudios/nft-collection
Running 'scripts\deploy.py::main'...
Transaction sent: 0x8baa920129682e29761cd7ec62db6403783a079ec6257755ba883b77a3eb4fef
Gas price: 1.0531e-05 gwei Gas limit: 1774421 Nonce: 79
NFTCollection.constructor confirmed Block: 8368191 Gas used: 1613110 (90.91%)
NFTCollection deployed at: 0x8a659eb7eeEcD5EdFeae5b01EDD08373E550888D
advanced collectible contract has been deployed
You will also find the compiled versions of your contracts inside the build folder. The purpose of this is for brownie to be able to access the contracts easily during the development and testing stages of our project.
Creating the collection
Open the “create_collection.py” and configure the following imports:
# https://github.com/htostudios/nft-collection
from brownie import NFTCollection, network, config
from scripts.helpful_scripts import fund_with_link, get_account,OPENSEA_URL
import os
import requests
import json
from pathlib import Path
from num2words import num2words
In the first line from brownie, we are importing the “NFTCollection” contract that we deployed, the “network” module which will help us with working with different networks, and the “config”, which is our “brownie-config.yaml” file. From our “helpful_scripts.py” file, we are going to import the “fund_with_link()” and “get_account()” functions and the “OPENSEA_URL” which we will use later to deploy our NFTs. The “fund_with_link()” function will help us with funding the chainlink oracle that will help us in generating truly verifiable randomness functions that we will use for the NFTs and the “get_account()” function will provide us with an account that will be used for signing the transactions.
Generating the Metadata
Using IPFS to store images and metadata associated with NFTs (non-fungible tokens) instead of storing them directly on-chain is a common practice in the NFT space. There are several reasons why this is necessary.
Firstly, scalability is a major concern when it comes to NFTs, as the sheer volume of data associated with these digital assets can quickly overwhelm the capacity of a blockchain. By using IPFS, NFT Images and metadata can be stored in a decentralized, peer-to-peer network, which allows for much greater scalability and ensures that the network can handle large amounts of data.
Secondly, cost-effectiveness is another important consideration when it comes to NFTs. Storing large amounts of data on-chain can be very expensive, due to the high costs associated with data storage and transfer. IPFS, on the other hand, is a free, open-source network that allows for data to be stored and transferred at a minimal cost.
Thirdly, data privacy is a major concern in the digital age, and IPFS provides a decentralized way of storing data that helps protect against data breaches and other security threats. With IPFS, there is no central point of failure, and data is spread across multiple nodes, making it much more difficult for hackers to access.
Lastly, performance is a key factor when it comes to NFTs, as buyers and sellers expect fast and reliable data transfer. IPFS allows for faster data transfer speeds, as it uses a content-addressable approach that allows for data to be retrieved directly from the closest node, rather than having to traverse the entire network.
In summary, using IPFS to store NFT images and metadata instead of on-chain is necessary for scalability, cost-effectiveness, data privacy, and performance reasons. We are going to create a template for generating our NFT metadata.
Create a new folder called "metadata" in the root directory of your project and within that folder, create two files named "__init__.py" and "sample_metadata.py". Open the “sample_metadata.py” file and paste the following code, which will be the template for the metadata of our NFTs:
# https://github.com/htostudios/nft-collection
metadata_template = {
"name":"",
"description":"",
"image":"",
"attributes":[{"trait_type":"dopeness","value":2048}],
}
Back in the “create_collection.py” script, import the “metadata_template” because we are going to use it here
# https://github.com/htostudios/nft-collection
from brownie import NFTCollection, network, config
from scripts.helpful_scripts import fund_with_link, get_account,OPENSEA_URL
import os
import requests
import json
from pathlib import Path
from num2words import num2words
# The Metadata template
from metadata.sample_metadata import metadata_template
Once that’s done paste the following code:
# https://github.com/htostudios/nft-collection
# Creating the NFT collection
def create_nft_collection(no_of_tokens):
account = get_account()
nft_collection = NFTCollection[-1]
for collection_index in range(no_of_tokens):
fund_with_link(nft_collection.address)
creation_transaction = nft_collection.createCollectible({"from":account})
creation_transaction.wait(1)
print(f"NFT #{collection_index} created successfully")
print(f"Creating Metadata for NFT #{collection_index}")
# Getting the TokenId of the recently created NFT
token_id = nft_collection.tokenCounter()
image_uri = create_metadata(nft_collection,token_id)
set_tokenURI(token_id,nft_collection,image_uri)
The function above creates an NFT (non-fungible token) collection. It starts by importing and initializing an account, which we will use for signing the transactions and also importing an “NFTCollection” contract (most recently deployed contract). It then creates a for loop that will execute for the number of tokens specified in the "no_of_tokens" parameter. This parameter determines the number of NFTs you want for your collection. Inside the for loop, the function "fund_with_link" is called to provide 1 $LINK to the “NFTCollection” contract. This is necessary as it will be used to pay the Chainlink oracle for generating a verifiable randomness function each time a new NFT is minted. The “NFTCollection” contract will utilize the random number, generated by the Chainlink oracle, as the requestId.Then, it calls the "createCollectible" function of the contract, passing in the account as the "from" parameter. The code then waits for the transaction to be mined, and prints a message indicating that the NFT has been created successfully. The code then creates metadata for the NFT by calling a "create_metadata" function and sets the token URI with the "set_tokenURI" function. The token URI is a unique identifier for the token and it allows access to additional information about the token.
Since we’ve called the “create_metadata()” function inside the “create_nft_collection()” function, we will need to create it.
# https://github.com/htostudios/nft-collection
# Generating Unique Metadata for the NFTs
def create_metadata(nft_contract,token_id):
number_of_collectibles = nft_contract.tokenCounter() + 1
print(f"You have created {number_of_collectibles} collectible(s)")
word = num2words(token_id)
metadata_file_name =f"./metadata/{network.show_active()}/{word}.json"
collectible_metadata = metadata_template
print(metadata_file_name)
if Path(metadata_file_name).exists():
print(f"{metadata_file_name} already exists! Please delete it to overwrite")
else:
print(f"Creating metadata file: {metadata_file_name}")
os.makedirs(metadata_file_name)
# Creating Metadata for the collection from the metadata template
collectible_metadata["name"] = f"2048 #{token_id}"
collectible_metadata["description"] = "A cool 2048 snapshot!"
image_path = f"./img/{token_id}.jpg"
filename = image_path.split("/")[-1:][0]
# Uploading Image to IPFS and generating URI from IPFS
print("Uploading to IPFS to generate ImageURI")
image_uri = upload_to_ipfs(image_path)
collectible_metadata["image"] = image_uri
# Creating the metadata file.
with open(metadata_file_name ,"w") as file:
json.dump(collectible_metadata, file)
print("Metadata created successfully")
return image_uri
The"create_metadata()" takes in two parameters, "nft_contract" and "token_id". The function first retrieves the number of collectibles that have been created from the nft_contract object and prints out the number of collectibles. It then creates a metadata file name using the active network and the token_id, and creates a collectible metadata using the “metadata_template”. It then checks if the metadata file already exists, and if it does, it prints a message to let the user know it already exists and should be deleted to overwrite. If the file does not exist, the function creates the metadata file by adding in a name and description, and opens a file with the metadata_file_name and writes the collectible_metadata to it in JSON format. Then it prints a message that metadata has been created successfully. Finally, it uploads the image file to IPFS by calling the “upload_to_ipfs” function and returns the image URI. Once that’s done, we define the “upload_to_ipfs()” function:
# https://github.com/htostudios/nft-collection
# Uploading the NFT Images to IPFS
def upload_to_ipfs(filepath):
with Path(filepath).open("rb") as fp:
image_binary = fp.read()
ipfs_url = "http://127.0.0.1:5001"
endpoint = "/api/v0/add"
response = requests.post(ipfs_url + endpoint, files={"file":image_binary})
ipfs_hash = response.json()["Hash"]
filename = filepath.split("/")[-1:][0]
image_uri = f"ipfs://ipfs/{ipfs_hash}?filename={filename}"
print(image_uri)
return image_uri
The "upload_to_ipfs()" function takes a single argument, "filepath", which is the filepath of an image that needs to be uploaded to IPFS. The function uses the "requests" library to make a POST request to the IPFS API endpoint for adding files. The image binary is read from the filepath and added as a file in the POST request. The response from the IPFS API contains the IPFS hash of the added image. The function then constructs an "image_uri" string in the format "ipfs://ipfs/{ipfs_hash}?filename={filename}", where "ipfs_hash" is the hash of the image and "filename" is the name of the image file. Finally, the function returns the image_uri.
Once that’s done we define the “set_tokenURI” function which we also called inside the “create_nft_collection” function.
# https://github.com/htostudios/nft-collection
# Set Token URI
def set_tokenURI(token_id,nft_contract,token_uri):
account = get_account()
tx = nft_contract.setTokenURI(token_id,token_uri,{"from":account})
tx.wait(1)
print(f"Added tokenURI for {token_id}")
print(f"You can view your NFT at {OPENSEA_URL.format(nft_contract.address,token_id)}")
print("Please wait up to 20 minutes and hit the refresh metadata button")
The "set_tokenURI()" function takes in three inputs: a token ID, an NFT contract, and a token URI. It starts by using the "get_account" function to retrieve the account that will be used to make the transaction. Then, it uses the "set_tokenURI()" function of the NFT contract to set the URI of the specified token ID. In this case our NFT contract will be the “AdvancedCollectible” contract. The transaction is sent with the "from" field set to the previously retrieved account. The function then waits for the transaction to be mined, and prints out a message indicating that the URI has been added for the specified token ID, along with a link to view the NFT on OpenSea. It also indicates that it might take up to 20 minutes for the metadata to be updated. Finally, we call the “create_nft_collection()” function inside our main function while specifying the number of tokens we want for our collection.
# https://www.github.com/htostudios/nft-collection
def main():
create_nft_collection(50)
The final “create_collection.py” script should look like this:
# https://github.com/htostudios/nft-collection
from brownie import NFTCollection, network, config
from scripts.helpful_scripts import fund_with_link, get_account,OPENSEA_URL
import os
import requests
import json
from pathlib import Path
from num2words import num2words
# The Metadata template
from metadata.sample_metadata import metadata_template
# Creating the NFT collection
def create_nft_collection(no_of_tokens):
account = get_account()
nft_collection = NFTCollection[-1]
for collection_index in range(no_of_tokens):
fund_with_link(nft_collection.address)
creation_transaction = nft_collection.createCollectible({"from":account})
creation_transaction.wait(1)
print(f"NFT #{collection_index} created successfully")
print(f"Creating Metadata for NFT #{collection_index}")
# Getting the TokenId of the recently created NFT
token_id = nft_collection.tokenCounter()
image_uri = create_metadata(nft_collection,token_id)
set_tokenURI(token_id,nft_collection,image_uri)
# Generating Unique Metadata for the NFTs
def create_metadata(nft_contract,token_id):
number_of_collectibles = nft_contract.tokenCounter() + 1
print(f"You have created {number_of_collectibles} collectible(s)")
word = num2words(token_id)
metadata_file_name =f"./metadata/{network.show_active()}/{word}.json"
collectible_metadata = metadata_template
print(metadata_file_name)
if Path(metadata_file_name).exists():
print(f"{metadata_file_name} already exists! Please delete it to overwrite")
else:
print(f"Creating metadata file: {metadata_file_name}")
os.makedirs(metadata_file_name)
# Creating Metadata for the collection from the metadata template
collectible_metadata["name"] = f"2048 #{token_id}"
collectible_metadata["description"] = "A cool 2048 snapshot!"
image_path = f"./img/{token_id}.jpg"
filename = image_path.split("/")[-1:][0]
# Uploading Image to IPFS and generating URI from IPFS
print("Uploading to IPFS to generate ImageURI")
image_uri = upload_to_ipfs(image_path)
collectible_metadata["image"] = image_uri
# Creating the metadata file.
with open(metadata_file_name ,"w") as file:
json.dump(collectible_metadata, file)
print("Metadata created successfully")
return image_uri
# Uploading the NFT Images to IPFS
def upload_to_ipfs(filepath):
with Path(filepath).open("rb") as fp:
image_binary = fp.read()
ipfs_url = "http://127.0.0.1:5001"
endpoint = "/api/v0/add"
response = requests.post(ipfs_url + endpoint, files={"file":image_binary})
ipfs_hash = response.json()["Hash"]
filename = filepath.split("/")[-1:][0]
image_uri = f"ipfs://ipfs/{ipfs_hash}?filename={filename}"
print(image_uri)
return image_uri
# Set Token URI
def set_tokenURI(token_id,nft_contract,token_uri):
account = get_account()
tx = nft_contract.setTokenURI(token_id,token_uri,{"from":account})
tx.wait(1)
print(f"Added tokenURI for {token_id}")
print(f"You can view your NFT at {OPENSEA_URL.format(nft_contract.address,token_id)}")
print("Please wait up to 20 minutes and hit the refresh metadata button")
def main():
create_nft_collection(50)
Running the Script
Before you run the script, you will need to install and configure IPFS in your system so that you can run an IPFS node which will be used for uploading the images to IPFS. You can run it on both the desktop and the terminal. In this project we are going to run it from the terminal. You can configure IPFS to be available globally across all of your terminals by registering it as an environment variable. To start an IPFS node instance, open a new terminal and cd into the root directory of this project and paste the following command:
# https://github.com/htostudios/nft-collection
$ipfs daemon
If everything runs successfully, you should get an output that looks like this:
# https://github.com/htostudios/nft-collection
Initializing daemon...
Kubo version: 0.16.0
Repo version: 12
System version: amd64/windows
Golang version: go1.19.1
Swarm listening on /ip4/127.0.0.1/tcp/4001
Swarm listening on /ip4/127.0.0.1/udp/4001/quic
Swarm listening on /ip4/169.254.2.167/tcp/4001
Swarm listening on /ip4/169.254.2.167/udp/4001/quic
Swarm listening on /ip4/169.254.45.246/tcp/4001
Swarm listening on /ip4/169.254.45.246/udp/4001/quic
Swarm listening on /ip4/169.254.47.127/tcp/4001
Swarm listening on /ip4/169.254.47.127/udp/4001/quic
Swarm listening on /ip4/169.254.98.4/tcp/4001
Swarm listening on /ip4/169.254.98.4/udp/4001/quic
Swarm listening on /ip4/192.168.43.234/tcp/4001
Swarm listening on /ip4/192.168.43.234/udp/4001/quic
Swarm listening on /ip6/::1/tcp/4001
Swarm listening on /ip6/::1/udp/4001/quic
Swarm listening on /p2p-circuit
Swarm announcing /ip4/102.166.28.139/udp/4001/quic
Swarm announcing /ip4/127.0.0.1/tcp/4001
Swarm announcing /ip4/127.0.0.1/udp/4001/quic
Swarm announcing /ip4/192.168.43.234/tcp/4001
Swarm announcing /ip4/192.168.43.234/udp/4001/quic
Swarm announcing /ip6/::1/tcp/4001
Swarm announcing /ip6/::1/udp/4001/quic
API server listening on /ip4/127.0.0.1/tcp/5001
WebUI: http://127.0.0.1:5001/webui
Gateway (readonly) server listening on /ip4/127.0.0.1/tcp/8080
Daemon is ready
Once that’s done, we can finally run the “create_collection.py” script.
# https://github.com/htostudios/nft-collection
brownie run scripts/deploy.py --network goerli
brownie run scripts/create_collection.py --network goerli
Final Thoughts
To sum up, creating and deploying NFTs using Ethereum and OpenSea requires a basic understanding of Python and Solidity programming languages and blockchain technology. NFTs are unique cryptographic tokens that are ideal for representing assets that have a unique identity. The tutorial recommends using Brownie, a Python-based development and testing framework for smart contracts, to create and deploy the NFT collection. In the tutorial, we create an NFT smart contract that implements two functions, “createCollectible()” and “fulfillRandomness()”, using the OpenZeppelin and Chainlink contracts to reduce the amount of code that needs to be written. By following this tutorial, you will be able to create your own NFT collection and deploy it on the OpenSea marketplace.
Additionally, when deploying NFTs on Ethereum, it is common practice to store the associated metadata, such as images and descriptions, on a decentralized file system like IPFS. This ensures that the metadata is available and cannot be tampered with, even if the hosting website or server goes offline. IPFS stands for InterPlanetary File System and is a peer-to-peer network for storing and sharing files in a distributed manner. It provides a reliable and decentralized way to store data and is often used in conjunction with blockchain technology to ensure the immutability and accessibility of data. By using IPFS to store the metadata of NFTs, creators can ensure that their assets remain unique and valuable over time.