In my previous blog post, I put together a truffle project that could deploy ERC-721 token contracts to Polygon's Mumbai test network for compatibility for OpenSea. Today, I am ready to improve on this and show you devs and artists a couple of repositories that will help when deploying NFTs. Since my first post, I have been talking to a client about the power of regenerative art. Where artists can save time designing thousands of NFTs. I did some digging, and with some help from Moralis YT and HashLips GitHub, I conjured up a couple repos. Unfortunately, I have not configured an ERC-1155 example yet, but it is not too dissimilar from this code. So let's see what improvements I have made from the previous ERC-721 boilerplate.
The improvements
- Migration from Truffle to Hardhat.
- All of the benefits that come with Hardhat. Including straight-forward contract testing, more frequent updates, a larger community and much more.
- Enabled Meta-transactions to allow gas-less transactions when assets are bought on OpenSea.
- In addition to the Hardhat environment, I have also added a fork of HashLips to generate art metadata (purely optional)
- The HashLips art engine fork includes code for uploading NFT metadata to IPFS via a Moralis endpoint
The Repos:
My Working Example:
You will need funds on Polygon mainnet/testnet if you are to deploy any contracts (I have used mainnet to test meta tx's)
Firstly, I will show you the HashLips art engine fork to generate and upload NFTs. Lastly, I go through the Hardhat setup for deploying the contracts to Polygon.
HashLips art engine
To get started clone the HashLips fork. If you want to deploy contracts too, then get both. They have readme files that you can follow as well:
Moralis IPFS setup
You will need to an account at Moralis to get an API key. Then you may send your metadata in POST requests to https://deep-index.moralis.io/api/v2/ipfs/uploadFolder.
Once logged in, find the WEB3 API in the sidebar and copy the api key:
Paste it into new .env file for MORALIS
.
OpenSea Collection Metadata
If you want to test OpenSea Collection metadata, then replace (or use) the image and json files in moralisIPFS/collection#1
and use the names I have used to keep the scripts working. You can edit scripts/collectionImage.js
or scripts/collectionJson.js
to read the collection metadata on lines 15 or 17.
Then, run node moralisIPFS/scripts/collectionImage.js
in the console. If no error occurred, then copy your uri link from the console log. Mine looks like this:
{path:'https://ipfs.moralis.io:2053/ipfs/Qmbd46WBvCK33kuGcEb7LtQkWcXW3ygDEgBv5rdFvnJ7RX/collection-1/image.gif'}
You can change how your deployed IPFS url trails in the collectionImage.js
at line 18.
You will need this link in your collectionJson.js
scripts to complete the collection metadata. If you look in the json script at line 25, you can see the format of IPFS image link:
{
path: `collection-1/collection.json`,
content: {
...parsed,
//! here
image: `ipfs://Qmbd46WBvCK33kuGcEb7LtQkWcXW3ygDEgBv5rdFvnJ7RX/collection-1/image.gif`
}
}
So copy your link from the hash trail and up and swap mine for it: Qmbd46WBvCK33kuGcEb7LtQkWcXW3ygDEgBv5rdFvnJ7RX/collection-1/image.gif
Now you can run node moralisIPFS/scripts/collectionJson.js
to finalise your collection metadata. Copy the json uri link from the console and, open GameItem.sol
in your Hardhat project. Paste it here at line 232 after '/ipfs/':
function contractURI() public pure returns (string memory) {
return "https://ipfs.io/ipfs/yourhash/path/to/file.json";
}
NFT Metadata
Before generating your art, be sure to check moralisIPFS/scripts/numOfItems.js
. This is a variable for the upload scripts that we need to run shortly.
If you look in the project folder you can see the layers folder. Each folder in the layers folder is each layer/piece in the final output. When you inspect the layers folder further, you notice that each layer folder will need multiple variations of that layer, to have more combinations of NFTs. For editing the rarity, you can change the '#1-99' on each layer file for the percentage chance of it appearing in outputs. However, I shall be using the helpful boilerplate from HashLips.
If you navigate to the src/config.js file, you can see theres a few bits to configure here. One is the layersConfigurations
variable. Here you can set the number NFTs outputted, and the order of the layers that are placed in the image. I only minted 5 but, with the boilerplate layers you can make up to 15 or more.
Now, onto the NFTs. After running node index.js
the NFTs will be generated and you can see the console logs to confirm. These json/image outputs can be found in the moralisIPFS/build
folder.
Before going any further: Ensure to edit my moralisIPFS/scripts/numOfItems.js
to match the number of editions you have set in the HashLips config. This number will be read by the upload scripts to upload the correct amount.
You can now run node moralisIPFS/scripts/images721.js
. If successful, this will upload the files and log your image IPFS links. You will need the long hash from one of those image links:
[
{
path: 'https://ipfs.moralis.io:2053/ipfs/QmVgGeRfv1e4EMi6a4A7UEAAcZuESiNQbGBeX7kJaNLYKy/images/1.png'
}
],
[
{
path: 'https://ipfs.moralis.io:2053/ipfs/QmVgGeRfv1e4EMi6a4A7UEAAcZuESiNQbGBeX7kJaNLYKy/images/2.png'
}
]
//and so on
Just as you did with the collection metadata, you will paste the IPFS hash from the upload logs into your metadata721.js
script at line 23, replacing my dummy hash like so:
{
path: `metadata/${i}.json`,
content: {
...parsed,
image: `ipfs://PASTE-YOUR-HASH-HERE/images/${i}.png`
}
}
This metadata script will read the JSONs generated by HashLips, but replaces the image value to what you see above. So, now you have your metadata script ready, you should run node moralisIPFS/scripts/metadata721.js
to upload the metadata to IPFS. Copy full uri from the console log, open your Hardhat project, and navigate to scripts/mint.js
:
for (var i = 1; i <= NUM_ITEMS; i++) {
await gameItem.mintItem(OWNER_ADDRESS,`your-metadata-uri`);
}
Paste the uri in to the mintItem function parameters to match the following format:
https://ipfs.io/ipfs/bafybeickmuro374jjqidtotrxhvqubfdwpby3sm4k4ydurv4c3h4l4buni/metadata/${i}.json
With that done, our NFT metadata is ready and we can deploy and mint the NFTs.
Hardhat Contracts
For a quick start, you can check the readme file. In detail, you will need to run npm i
and add a .env
file with the following:
MNEMONIC=privatekey. not the seedphrase
MATIC_APP_ID=appid_from_maticvigil
POLYGONSCAN=apikey_from_polygonscan
We need the POLYGONSCAN var when running npx hardhat verify
to verify contracts on polygonscan. This part is optional but recommended if you need your source code verified.
You can set your own name and ticker in the scripts/deploy.js
file, if you need a specific name and ticker.
Before minting, be sure to find NUM_ITEMS in scripts/mint.js and set it to the number of JSONs you deployed.
After successfully running npx hardhat run --network matic scripts/deploy.js
, the console will log your contract address. Copy that, and paste it into line 8 contract.attach function in mint.js
.
In mint.js
you will see an OWNER_ADDRESS variable. Paste your deployer account address in there for the mintItem function to execute and mint to yourself.
Now that your minting script is ready, you can run that with npx hardhat run --network matic scripts/mint.js
.
And thats it. Now you should check PolygonScan and OpenSea for your .
Final notes
So there you go. You can now deploy ERC-721 regenerative NFTs for OpenSea on Polygon/Matic. With help from Filip at Moralis, I was able to add bulk IPFS uploads. The setup is still a bit cumbersome, because you are still required to do the images first then copy the uri for the json scripts.
Possible Improvements
- Automate the scripts so that the dev does less copying and hard-coding.
- Fill in the hardhat.config.js for other networks.
- Add external/proxy contracts for more flexibilty and upgradeability.
- Move the HashLips project into the Hardhat project for 1 repo.
Any more improvements? Go ahead and suggest them!
You can find my working example here on OpenSea and here on PolygonScan
Check these out:
Hope you learn't something new and enjoyed it. I am looking forward to seeing success & errors :D.
Top comments (10)
Nice tutorial Archie, for me I was focused on the meta-transactions piece as I've build my own NFT collectable generation program.
I'm stuck with Opensea unable to pull my metadata... I'm using Pinata for storage and a custom API on Heruko to serve up the metadata (this is all working fine).
Looking at how you're calling MintItem with the token ID as well, I think that might be the issue... Is this supposed to be the base URI for my token API, or does it expect the token number too?
For example I'm using my base URL in mintItem:
krazyphaces.herokuapp.com/api/token
But to serve each token's metadata requires the token ID too e.g.
krazyphaces.herokuapp.com/api/toke...
My verified contract code is here...
mumbai.polygonscan.com/address/0xb...
Cheers!
Hi Ben. I have looked at your APIs and they look fine as you say. So let's get down to it.
In the last post, I did not set a base uri, but just minted the item, passing in the full uri of the metadata - no prefix base. I did the same again here in this example. I have not overriden the virtual function
_baseURI
, this is where you would override it in the GameItem contract and add your base uri in the return statement. Although, there is more to it than that:An important part I did not mention, is that I picked inheritance/import of ERC721URIStorage.sol over ERC721.sol. This is because ERC721URIStorage.sol inherits ERC721, so that one can override the
tokenURI
function to get URIs and add a new function,setTokenURI
for setting the URIs (hence the name ERC721URIStorage).So when you are minting with the full URI,
setTokenURI
from ERC721URIStorage will expect a full working URI and not use a baseURI at all. Notice that this function is scoped as 'virtual', meaning it can be overridden to perhaps concatenate the baseURI with the ID you want to pass in when minting.With the above in mind, you can review the changes of the overridden tokenURI view (getter) function.
The function now checks if the baseURI has been changed from the original empty 0 string. If the
_baseURI()
call returns that empty string, thentokenURI
returns the URI from_tokenURIs
mapping, set when you call my specific mintItem function with the full URI, otherwisetokenURI
will concatenate the base uri with the token ID from_tokenURIs
mapping.So if you do override the
_baseURI
function to fit your base , when you mint, keep in mind that you can pass in the number of your token id (to complete with the base uri) but it will be stored as a string. Which leads me to yourmint.js
script. In case you did not know, in JS you can pass expressions into strings when using the backtick ` character. If your base URI is set like so: krazyphaces.herokuapp.com/api/token/then your js script could look like this
`
const hre = require("hardhat");
const NUM_ITEMS = 5;
const START_NUM = 0
const OWNER_ADDRESS = "";
async function main() {
}
... the main() call
`
Notice that i have added <= before
(START_NUM + NUM_ITEMS);
to include the final number ofNUM_ITEMS
, in this case 5.To clarify, I forgot to mention that I was using the ERC721URIStorage for URI storage and also not using a base.
To conclude this, I would encourage scanning through openzeppelins contracts that I linked above, noticing the scopes of some of these functions that you may be using.
I hope I have layed out the information you need, looking forward to your progress.
Excellent explanation as always Archie!
My workaround was to create my own counter and pass this each time I mint. Now the tokenIds are being returned correctly and the metadata is displaying...
My next challenge (and a suggestion for your next tutorial) is
1) Using the OpenSea SDK to sell the NFTs... When I try to sell using the OpenSea Creatures example sell script (github.com/ProjectOpenSea/opensea-...) I get this error:
return constants.DEPLOYED[network].WyvernExchange;
TypeError: Cannot read property 'WyvernExchange' of undefined
My theory right now is that the script (designed for mainnet, not matic/mumbai is expecting the WyvernExchange and I'm not sure if this is provided by matic/mumbai... but this is the first time I learned of the WyvernExchange protocol....
2) I tried to write a node.js script to force updates of my metadata by calling the URL of each NFT followed by ?force_update=true, however OpenSea's cloudflare didn't like the requests and blocked with a 403 error... Wish there was a way to force a metadata update for a whole collection without having to use a browser to open each page...
Keep up the great work, there's a NFT with your name on it (literally) once I'm done!
I found this one liner on why the OpenSea Creatures sell script fails on Polygon with TypeError: Cannot read property 'WyvernExchange' of undefined...
"OpenSea uses different order matching for ethereum and polygon. On Ethereum, Wyvern is used, but on Polygon, 0x v3 is used."
(ethereum.stackexchange.com/questio...)
So I guess I need a sell script that uses 0x v3 protocol instead, but can't see how on earth to do that!
Hey Ben, thankful for the great response.
I will be honest, I have never used the sell script, therefore never looked into it that much. With your introduction of it, I shall definitely have a look, seems interesting.
This is a bit of a problem when there are no OpenSea examples for PolygonMatic sell scripts. I can already tell by looking at the script on lines 36 and 49 that you would be relying on OpenSeaPort to have compatibility with a Matic.
So I was snooping around in the Opesea.js GitHub and OpenSeaPort docs and cant find 'Polygon' or 'Matic' anywhere. I will continue to search around.
Thanks, Archie
Thanks Archie, After a few hours of poking around, here's what I'm trying... It overcame the error I had before but there's very little out there about the sell.js and Polygon/Matic except a few comments suggesting that OpenSea's v1 API doesn't support it yet....
So I tried this... network name as Network.Matic....
const seaport = new OpenSeaPort(
providerEngine,
{
networkName: Network.Mumbai //,
// apiKey: API_KEY, /* i dont have (opensea) API_KEY */
},
(arg) => console.log(arg)
);
This got me past the previous error... But now I'm stuck with this new error...
FetchError: request to api.opensea.io/api/v1/asset/0xdbb6...? failed, reason: certificate has expired
Obviously I've checked all my keys (I'm using Alchemy with a key defined specifically for Polygon Mumbai)....
And I don't have the optional OpenSea API but have applied for one...
Ever onward! But really don't want to have to put thousands of Polygon NFTs up for sale individually by hand!
Hey Ben, I just got a response from opensea discord:
You should go join OpenSea, Polygon discords if you have not already.
Thanks, Archie
Oh and the collection is here: testnets.opensea.io/collection/kra...
After a bit more digging on Polygonscan it looks like the token URI isn't returning the token id when queried...
It seems the tokens ID counter isn't setting a newItemId here in _setTokenURI...?
function mintItem(address player, string memory tokenURI)
public
onlyOwner
returns (uint256)
{
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
_mint(player, newItemId);
_setTokenURI(newItemId, tokenURI);
return newItemId;
}
hi there mr. Archie. Is there a way you add me on discord @ ambasada gavioli#6145 . I am trying to start my nft project but have tons of question.
Your help would be really apricciated!