In this tutorial, we will walk through creating and deploying an ERC-721 smart contract on the Opal test network using Hardhat and Solidity. Also, we will mint an NFT using a library.
You need to have the following installed and prepared to start this guide:
- Node.js, version > 14.
- npm or yarn.
- Metamask account.
At the moment, we have Opal Testnet. Its websocket URL is wss://ws-opal.unique.network (rpc endpoint - https://rpc-opal.unique.network).
In Polkadot apps, you can check it using this link.
You can use @unique2faucet_opal_bot in Telegram to get some OPL.
Create a folder and initialize a project.
mkdir unq-nft
cd unq-nft
npm init - y
yarn init -y
Add the Hardhat to your project and initialize it:
npm install --save-dev hardhat
yarn add -D hardhat
npx hardhat
yarn hardhat
To check if everything works properly, please run:
npx hardhat test
yarn hardhat test
This command will prompt to select the project type, please choose TypeScript and answer yes to all questions. Just in case, make sure that the additional plugins are installed by running these commands:
npm install --save-dev @nomicfoundation/hardhat-toolbox
yarn add --dev @nomicfoundation/hardhat-toolbox @nomicfoundation/hardhat-network-helpers @nomicfoundation/hardhat-chai-matchers @nomiclabs/hardhat-ethers @nomiclabs/hardhat-etherscan chai ethers hardhat-gas-reporter solidity-coverage @typechain/hardhat typechain @typechain/ethers-v5 @ethersproject/abi @ethersproject/providers
Please note that in Unqiue Network, there is no need to write smart contracts as it is usually required in Ethereum networks.
Unique Network provides emulated smart contracts. This means that if you call a specific address where a smart contract is supposed to be, Unique Network will pretend that it has a smart contact there. Thus, you can access our node using Ethereum technologies, and the node will respond.
This advantage allows using smart contracts just as libraries from any .js, .ts or even .sol file. The following guide will demonstrate how we call the smart contracts from the solidity-interfaces library.
Let's install the solidity-interfaces library that provides the smart contracts.
npm i @unique-nft/solidity-interfaces
yarn add @unique-nft/solidity-interfaces
After this, we will write a new smart contract that will use this library. Please pay attention that we just import a couple of .sol files and use them. We will create a new file in the /contracts folder with the CollectionManager.sol name.
pragma solidity ^0.8.17;
import {CollectionHelpers, CollectionHelpersEvents} from "@unique-nft/solidity-interfaces/contracts/CollectionHelpers.sol";
import {UniqueNFT, CrossAddress} from "@unique-nft/solidity-interfaces/contracts/UniqueNFT.sol";
contract CollectionManager is CollectionHelpersEvents {
CollectionHelpers helpers = CollectionHelpers(0x6C4E9fE1AE37a41E93CEE429e8E1881aBdcbb54F);
function createCollection(
address owner,
address managerContract,
string calldata name,
string calldata description,
string calldata symbol,
string calldata baseURI
) public payable virtual returns (address){
address collectionAddress = helpers.createNFTCollection{value: helpers.collectionCreationFee()}(name, description, symbol);
helpers.makeCollectionERC721MetadataCompatible(collectionAddress, baseURI);
UniqueNFT collection = UniqueNFT(collectionAddress);
collection.addCollectionAdminCross(CrossAddress(managerContract, 0));
collection.changeCollectionOwnerCross(CrossAddress(owner, 0));
return collectionAddress;
}
}
Install the dotenv package in your project directory by running:
npm install dotenv --save
yarn add dotenv --save
Then, create a .env file in the root directory of our project, and add your Metamask private key and the network RPC to it.
Follow these instructions to export your private key from Metamask.
Your .env should look like this:
RPC_OPAL="https://rpc-opal.unique.network"
PRIVATE_KEY = "your-metamask-private-key"
After this, update your hardhat.config.ts so that our project knows about all of these values.
import dotenv from 'dotenv'
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
dotenv.config()
const { RPC_OPAL, PRIVATE_KEY } = process.env;
const config: HardhatUserConfig = {
solidity: {
version: "0.8.17",
settings: {
metadata: {
// Not including the metadata hash
// https://github.com/paulrberg/solidity-template/issues/31
bytecodeHash: "none",
},
optimizer: {
enabled: true,
runs: 800,
},
viaIR : true,
},
},
networks: {
hardhat: {},
opal: {
url: RPC_OPAL,
accounts: [`${PRIVATE_KEY}`]
},
}
};
export default config;
Now, when our contract is written and our configuration file is ready, it is time to write the contract deploy script.
Create the file deploy.ts with the following:
const {ethers} = require('hardhat');
async function main() {
// Grab the contract factory
const CollectionManager = await ethers.getContractFactory("CollectionManager");
// Start deployment, returning a promise that resolves to a contract object
const collectionManager = await CollectionManager.deploy(); // Instance of the contract
console.log("Contract deployed to address:", collectionManager.address);
}
main()
.then(() => process.exit(0))
.catch(error => {
console.error(error);
process.exit(1);
});
We are finally ready to deploy our smart contract! For this, run the following command line:
npx hardhat run scripts/deploy.ts --network opal
yarn hardhat run scripts/deploy.ts --network opal
You should then see something like:
Contract deployed to address: 0xB07956E26FDF1b215aC89AE21F822F8AB9Be9A27
Now, we can create a new file in the /scripts folder called createCollection.ts.
import dotenv from 'dotenv'
import {ethers} from "hardhat"
import {CollectionHelpersFactory, UniqueNFTFactory} from "@unique-nft/solidity-interfaces"
import {CollectionManager__factory} from '../typechain-types'
import {Address} from "@unique-nft/utils";
dotenv.config()
const TOKEN_IPFS_CIDS = {
1: 'QmZ8Syn28bEhZJKYeZCxUTM5Ut74ccKKDbQCqk5AuYsEnp',
2: 'QmZVpSsjvxev3C8Dv4E44fSp8gGMP6aoLMp56HmZi5Wkxh',
3: 'QmZMo8JDB9isA7k7tr8sFLXYwNJNa51XjJinkLWcc9vnta',
4: 'QmV7fqfJBozrc7VtaHSd64GvwNYqoQE1QptaysenTJrbpL',
5: 'QmSK1Zr6u2f2b8VgaFgz9CY1NR3JEyygQPQjJZaAA496Bh',
6: 'QmafTK2uFRuLyir2zJpLSBMercq2nDfxtSiMWXL1dbqTDn',
7: 'QmXTMYJ3rKeTCaQ79QQPe2EYcpVFbHr3maqJCPGcUobS4B',
8: 'QmQa97BYq9se73AztVF4xG52fGSBVB1kZKtAtuhYLHE1NA',
}
async function main() {
const myEthAddress = '0xb4d6A98aa8CD5396069c2818Adf4ae1A0384b43a'
// Getting a substrate mirror. You must have tokes on it.
console.log(Address.mirror.ethereumToSubstrate(myEthAddress))
// define a provider
const provider = ethers.provider
// Creating a signer
const privateKey = process.env.PRIVATE_KEY
// @ts-ignore
const wallet = new ethers.Wallet(privateKey, provider)
const contractAddress = '0xaD2CEBe3768eE109bc0586E741E96130fc21134c'
// @ts-ignore
const collectionHelpers = await CollectionHelpersFactory(wallet, ethers)
// Creating a contract instance
const contract = await ethers.getContractFactory('CollectionManager')
const deployer = await contract.deploy()
const collectionManager = await deployer.deployed()
console.log(`Contract address found: ${collectionManager.address}`)
// Creating a new collection
let newCollection = await collectionManager.createCollection(
myEthAddress,
myEthAddress,
'My new collection',
'This collection is for testing purposes',
'CL',
'https://ipfs.unique.network/ipfs/',
{
value: await collectionHelpers.collectionCreationFee()
}
)
const transactionReceipt = await newCollection.wait()
const collectionAddress = transactionReceipt.events?.[0].args?.collectionId as string
const collectionId = Address.collection.addressToId(collectionAddress)
console.log(`Collection created!`)
console.log(`Address: ${collectionAddress} , id: ${collectionId}`)
// Minting NFTs
const collection = await UniqueNFTFactory(collectionAddress, wallet, ethers)
const txMintToken = await (await collection.mintWithTokenURI(
wallet.address,
'https://ipfs.unique.network/ipfs/' + TOKEN_IPFS_CIDS['1']
)).wait()
const tokenId = txMintToken.events?.[1].args?.tokenId.toNumber()
const tokenUri = await collection.tokenURI(tokenId)
console.log(`Successfully minted token #${tokenId}, it's URI is: ${tokenUri}`)
/* Minting NFTs for all cids
for (let cid in tokenIpfsCids) {
const txMintToken = await (await collection.mintWithTokenURI(wallet.address, cid)).wait()
const tokenId = txMintToken.events?.[0].args?.tokenId.toNumber()
const tokenUri = await collection.tokenURI(tokenId)
console.log(`Successfully minted token #${tokenId}, it's URI is: ${tokenUri}`)
}*/
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
To run the script, please execute the following commands:
npx hardhat run scripts/createCollection.ts --network opal
yarn hardhat run scripts/createCollection.ts --network opal
You should get the similar result, but with your values:
5DVgiNh1Go8ESa18xEirA4uV3zpra59awPGQjJTmCrqYMNtM
Contract address found: 0x3664F6c1178E19Bb775b597d6584CaA3B88a1C35
Collection created!
Address: 0x17c4e6453Cc49AAaAeACA894E6d9683E00000006 , id: 6
Cool! We created a collection and minted an NFT in it.
❗ We would like to draw your attention, that this example shows two use cases. First, that we created our own smart contact and deployed it, just like Ethereum users usually do. So, you can use the familiar approach to work with Unique Network. Second, we minted an NFT using a library that we imported. So, as we already mentioned in the beginning of this guide, you may not deploy your contracts, but just use ready ones.
At some point, you may face the issue with typings. For example, this error:
Error: Cannot find module './Lock_factory'
In this case, you can do the following to fix it:
npx hardhat clean
npx hardhat compile
yarn hardhat clean
yarn hardhat compile