Hardhat is a popular development environment for building and deploying Ethereum smart contracts. In this guide, we’ll walk you through the steps to set up a simple Hardhat project, create a Dockerfile for containerization, and demonstrate how to launch the project using Docker Compose. Additionally, we’ll show you how to deploy a contract to the Goerli test network.
Step 1: Create a Hardhat Project
To create a new Hardhat project, follow these steps:
- Open your terminal and create a new directory for your project:
mkdir my-hardhat-project
cd my-hardhat-project
- Initialize a new Node.js project and install Hardhat as a development dependency:
npm init -y
npm install --save-dev hardhat
- Initialize Hardhat in your project:
npx hardhat
Follow the prompts to create a new Hardhat project configuration. You can choose to create a sample project or customize it according to your requirements.
Config the hardhat.config.js file and select the node (infura, manged, own, …), network, private key,…
require("@nomicfoundation/hardhat-toolbox");
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: "0.8.20",
defaultNetwork: "goerli",
networks: {
goerli: {
url: "https://goerli.infura.io/v3/<API>",
accounts: ["<PRIVATEKEY>"]
}
}
};
Write the main api.js code with the endopints:
const express = require('express')
require("dotenv").config()
const contract = require("./artifacts/contracts/CredentialNFT.sol/CredentialNFT.json");
var axios = require('axios');
const fs = require("fs");
const bodyParser = require('body-parser');
const { json } = require('body-parser');
const app = express()
const Web3 = require("web3")
const { ethers, JsonRpcProvider, BigNumber } = require('ethers');
app.use(bodyParser.json());
app.use(express.json());
const port = process.env.PORT || 5000
//:::::::::::::::::::SETUP NETWORK CONFIG:::::::::::::::::::::::::::::::::::::
const contractAddress = process.env.CONTRACT_ADDRESS
const contractAddressProfile = process.env.CONTRACT_ADDRESS_PROFILE
const abi = contract.abi
const hre = require("hardhat");
//:::::::::::::::::::API:::::::::::::::::::::::::::::::::::::
app.post("/mintToken", async (req, res) => {
if (!req.body.metadata || !req.body.publicKey) {
return res.status(400).json({ error: 'Missing parameter' });
}
try {
const [deployer] = await hre.ethers.getSigners();
const contract = new hre.ethers.Contract(contractAddress, abi, deployer);
let result = await contract.Mint(req.body.publicKey, req.body.metadata);
let receipt = await result.wait();
console.log(receipt)
let tokenID = await contract.getCount();
res.json({ tokenId: tokenID.toString(), txsHash: receipt.hash});
} catch (e) {
console.error(e);
res.writeHead(500);
res.end("internal problem with contract invocation");
return;
}
})
app.post("/mintTokenProfile", async (req, res) => {
if (!req.body.metadata || !req.body.publicKey) {
return res.status(400).json({ error: 'Missing parameter' });
}
try {
const [deployer] = await hre.ethers.getSigners();
const contract = new hre.ethers.Contract(contractAddressProfile, abi, deployer);
let result = await contract.Mint(req.body.publicKey, req.body.metadata);
let receipt = await result.wait(); // Attendiamo la conferma della transazione
console.log(receipt)
let tokenID = await contract.getCount();
res.json({ tokenId: tokenID.toString(), txsHash: receipt.hash});
} catch (e) {
console.error(e);
res.writeHead(500);
res.end("internal problem with contract invocation");
return;
}
})
app.post("/listNFT", async (req, res) => {
console.log(req)
if (!req.body.publicKey) {
return res.status(400).json({ error: 'Missing parameter' });
}
try {
const [deployer] = await hre.ethers.getSigners();
const contract = new hre.ethers.Contract(contractAddressProfile, abi, deployer);
let result = await contract.ListNFT(req.body.publicKey);
console.log(result)
res.send(result);
} catch (e) {
console.error(e);
res.writeHead(500);
res.end("internal problem with contract invocation");
return;
}
})
app.post("/burnToken", async (req, res) => {
if (!req.body.token) {
return res.status(400).json({ error: 'Missing parameter' });
}
try {
let result = await myNftContract.burnNFT(Web3.utils.toBigInt(req.body.token));
let receipt = await result.wait(); // Attendiamo la conferma della transazione
res.json({ message: "token burn"});
} catch (e) {
console.error(e);
res.writeHead(500);
res.end("internal problem with contract invocation");
return;
}
})
app.listen(port, () => {
console.log(`Server running on port ${port}`)
})
Step 2: Create a Dockerfile
To containerize your Hardhat project, create a Dockerfile
in the project root directory with the following content:
FROM node:18-alpine
COPY package.json package.json
COPY package-lock.json package-lock.json
RUN npm ci
COPY artifacts artifacts
COPY contracts contracts
COPY scripts scripts
COPY .env .env
COPY api.js api.js
COPY hardhat.config.js hardhat.config.js
CMD ["node", "api.js"]
This Dockerfile
sets up a Node.js environment, installs project dependencies, and specifies the command to run when the container starts. You may need to customize it based on your project’s requirements.
Step 3: Create a Docker Compose File
To simplify the deployment of your Hardhat project with Docker, create a docker-compose.yml
file in the project root directory with the following content:
version: '3'
services:
hardhat:
build: ./hardhat-project
container_name: sace-hardhat
network_mode: "host"
restart: always
This Docker Compose file defines a service named hardhat
using the Dockerfile in the current directory. The service name and container name can be customized as needed.
Step 4: Deploy a Contract
To deploy a contract, you can use a deploy script. for example in script/deploy.js:
// We require the Hardhat Runtime Environment explicitly here. This is optional
// but useful for running the script in a standalone fashion through `node <script>`.
//
// You can also run a script with `npx hardhat run <script>`. If you do that, Hardhat
// will compile your contracts, add the Hardhat Runtime Environment's members to the
// global scope, and execute the script.
const hre = require("hardhat");
async function main() {
const currentTimestampInSeconds = Math.round(Date.now() / 1000);
const unlockTime = currentTimestampInSeconds + 60;
const lockedAmount = hre.ethers.parseEther("0.001");
const lock = await hre.ethers.deployContract("CredentialNFT", [unlockTime], {
value: lockedAmount,
});
await lock.waitForDeployment();
console.log(
`Lock with ${ethers.formatEther(
lockedAmount
)}ETH and unlock timestamp ${unlockTime} deployed to ${lock.target}`
);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Will deploy the contract in contracts/ folder:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
error notOwner();
contract CredentialNFT is ERC721, ERC721URIStorage {
// uint256 public mintPrice;
uint256 private _tokenIdCounter;
address owner;
struct token {
uint256 id;
string uri;
}
constructor(string memory name, string memory symbol) ERC721(name, symbol) {
owner = msg.sender;
_tokenIdCounter = 0;
}
modifier _onlyOwner() {
if (msg.sender != owner) {
revert notOwner();
}
_;
}
function Mint(
address to,
string calldata uri
) public returns (uint256) {
_tokenIdCounter ++;
uint256 tokenId = _tokenIdCounter;
_safeMint(to, tokenId);
_setTokenURI(tokenId, uri);
return tokenId;
}
function burnNFT(uint256 tokenId) public _onlyOwner {
_burn(tokenId);
}
function tokenURI(
uint256 tokenId
) public view override(ERC721, ERC721URIStorage) returns (string memory) {
return super.tokenURI(tokenId);
}
function transfer(
address from,
address to,
uint256 tokenId
) public returns (bool) {
require(ownerOf(tokenId) == from);
require(to != from);
safeTransferFrom(from, to, tokenId);
return true;
}
function ListNFT(address addr) public view returns (token[] memory) {
require(balanceOf(addr)>0);
token[] memory list = new token[](balanceOf(addr));
uint count = 0;
for (uint i = 1; i <= _tokenIdCounter; i++) {
if (addr == ownerOf(i)) {
string memory _uri = tokenURI(i);
token memory t = token({
id: i,
uri: _uri
});
list[count] = t;
count++;
}
}
return list;
}
function getCount() public view returns (uint256) {
return _tokenIdCounter;
}
function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721URIStorage) returns (bool) {
return ERC721.supportsInterface(interfaceId) || ERC721URIStorage.supportsInterface(interfaceId);
}
}
Deploy to the Goerli test network using Hardhat, using the following command:
npx hardhat run scripts/deploy.js --network=goerli
Step 5: Build and Launch with Docker Compose
Now that you have set up the Dockerfile and Docker Compose configuration, you can build and launch your Hardhat project using Docker Compose:
- Build the Docker image from your project directory:
docker-compose build
- Launch your Hardhat project in a Docker container:
docker-compose up
Your Hardhat project will run inside the Docker container, and you can access it as specified in your Dockerfile
.
Conclusion
You’ve successfully set up a simple Hardhat project, created a Dockerfile for containerization, and demonstrated how to deploy a contract to the Goerli test network using Docker Compose. This setup allows you to develop, test, and deploy Ethereum smart contracts in a consistent and reproducible environment.