├── .gitignore ├── contracts ├── NFTee.sol └── Lock.sol ├── README.md ├── hardhat.config.js ├── package.json ├── scripts └── deploy.js └── test └── Lock.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | coverage 4 | coverage.json 5 | typechain 6 | typechain-types 7 | 8 | # Hardhat files 9 | cache 10 | artifacts 11 | 12 | -------------------------------------------------------------------------------- /contracts/NFTee.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 5 | 6 | contract NFTee is ERC721 { 7 | 8 | constructor() ERC721("NFTee", "ITM") { 9 | _mint(msg.sender, 1); 10 | } 11 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sample Hardhat Project 2 | 3 | This project demonstrates a basic Hardhat use case. It comes with a sample contract, a test for that contract, and a script that deploys that contract. 4 | 5 | Try running some of the following tasks: 6 | 7 | ```shell 8 | npx hardhat help 9 | npx hardhat test 10 | REPORT_GAS=true npx hardhat test 11 | npx hardhat node 12 | npx hardhat run scripts/deploy.js 13 | ``` 14 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("@nomicfoundation/hardhat-toolbox"); 2 | require("dotenv").config({ path: ".env" }); 3 | 4 | const QUICKNODE_HTTP_URL = process.env.QUICKNODE_HTTP_URL; 5 | const PRIVATE_KEY = process.env.PRIVATE_KEY; 6 | 7 | module.exports = { 8 | solidity: "0.8.9", 9 | networks: { 10 | goerli: { 11 | url: QUICKNODE_HTTP_URL, 12 | accounts: [PRIVATE_KEY], 13 | }, 14 | }, 15 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nft-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@nomicfoundation/hardhat-toolbox": "^2.0.1", 14 | "hardhat": "^2.12.6" 15 | }, 16 | "dependencies": { 17 | "@openzeppelin/contracts": "^4.8.1", 18 | "dotenv": "^16.0.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /scripts/deploy.js: -------------------------------------------------------------------------------- 1 | // Import ethers from Hardhat package 2 | const { ethers } = require("hardhat"); 3 | 4 | async function main() { 5 | /* 6 | A ContractFactory in ethers.js is an abstraction used to deploy new smart contracts, 7 | so nftContract here is a factory for instances of our NFTee contract. 8 | */ 9 | const nftContract = await ethers.getContractFactory("NFTee"); 10 | 11 | // here we deploy the contract 12 | const deployedNFTContract = await nftContract.deploy(); 13 | 14 | // wait for the contract to deploy 15 | await deployedNFTContract.deployed(); 16 | 17 | // print the address of the deployed contract 18 | console.log("NFT Contract Address:", deployedNFTContract.address); 19 | } 20 | 21 | // Call the main function and catch if there is any error 22 | main() 23 | .then(() => process.exit(0)) 24 | .catch((error) => { 25 | console.error(error); 26 | process.exit(1); 27 | }); -------------------------------------------------------------------------------- /contracts/Lock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.9; 3 | 4 | // Uncomment this line to use console.log 5 | // import "hardhat/console.sol"; 6 | 7 | contract Lock { 8 | uint public unlockTime; 9 | address payable public owner; 10 | 11 | event Withdrawal(uint amount, uint when); 12 | 13 | constructor(uint _unlockTime) payable { 14 | require( 15 | block.timestamp < _unlockTime, 16 | "Unlock time should be in the future" 17 | ); 18 | 19 | unlockTime = _unlockTime; 20 | owner = payable(msg.sender); 21 | } 22 | 23 | function withdraw() public { 24 | // Uncomment this line, and the import of "hardhat/console.sol", to print a log in your terminal 25 | // console.log("Unlock time is %o and block timestamp is %o", unlockTime, block.timestamp); 26 | 27 | require(block.timestamp >= unlockTime, "You can't withdraw yet"); 28 | require(msg.sender == owner, "You aren't the owner"); 29 | 30 | emit Withdrawal(address(this).balance, block.timestamp); 31 | 32 | owner.transfer(address(this).balance); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/Lock.js: -------------------------------------------------------------------------------- 1 | const { 2 | time, 3 | loadFixture, 4 | } = require("@nomicfoundation/hardhat-network-helpers"); 5 | const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs"); 6 | const { expect } = require("chai"); 7 | 8 | describe("Lock", function () { 9 | // We define a fixture to reuse the same setup in every test. 10 | // We use loadFixture to run this setup once, snapshot that state, 11 | // and reset Hardhat Network to that snapshot in every test. 12 | async function deployOneYearLockFixture() { 13 | const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60; 14 | const ONE_GWEI = 1_000_000_000; 15 | 16 | const lockedAmount = ONE_GWEI; 17 | const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS; 18 | 19 | // Contracts are deployed using the first signer/account by default 20 | const [owner, otherAccount] = await ethers.getSigners(); 21 | 22 | const Lock = await ethers.getContractFactory("Lock"); 23 | const lock = await Lock.deploy(unlockTime, { value: lockedAmount }); 24 | 25 | return { lock, unlockTime, lockedAmount, owner, otherAccount }; 26 | } 27 | 28 | describe("Deployment", function () { 29 | it("Should set the right unlockTime", async function () { 30 | const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture); 31 | 32 | expect(await lock.unlockTime()).to.equal(unlockTime); 33 | }); 34 | 35 | it("Should set the right owner", async function () { 36 | const { lock, owner } = await loadFixture(deployOneYearLockFixture); 37 | 38 | expect(await lock.owner()).to.equal(owner.address); 39 | }); 40 | 41 | it("Should receive and store the funds to lock", async function () { 42 | const { lock, lockedAmount } = await loadFixture( 43 | deployOneYearLockFixture 44 | ); 45 | 46 | expect(await ethers.provider.getBalance(lock.address)).to.equal( 47 | lockedAmount 48 | ); 49 | }); 50 | 51 | it("Should fail if the unlockTime is not in the future", async function () { 52 | // We don't use the fixture here because we want a different deployment 53 | const latestTime = await time.latest(); 54 | const Lock = await ethers.getContractFactory("Lock"); 55 | await expect(Lock.deploy(latestTime, { value: 1 })).to.be.revertedWith( 56 | "Unlock time should be in the future" 57 | ); 58 | }); 59 | }); 60 | 61 | describe("Withdrawals", function () { 62 | describe("Validations", function () { 63 | it("Should revert with the right error if called too soon", async function () { 64 | const { lock } = await loadFixture(deployOneYearLockFixture); 65 | 66 | await expect(lock.withdraw()).to.be.revertedWith( 67 | "You can't withdraw yet" 68 | ); 69 | }); 70 | 71 | it("Should revert with the right error if called from another account", async function () { 72 | const { lock, unlockTime, otherAccount } = await loadFixture( 73 | deployOneYearLockFixture 74 | ); 75 | 76 | // We can increase the time in Hardhat Network 77 | await time.increaseTo(unlockTime); 78 | 79 | // We use lock.connect() to send a transaction from another account 80 | await expect(lock.connect(otherAccount).withdraw()).to.be.revertedWith( 81 | "You aren't the owner" 82 | ); 83 | }); 84 | 85 | it("Shouldn't fail if the unlockTime has arrived and the owner calls it", async function () { 86 | const { lock, unlockTime } = await loadFixture( 87 | deployOneYearLockFixture 88 | ); 89 | 90 | // Transactions are sent using the first signer by default 91 | await time.increaseTo(unlockTime); 92 | 93 | await expect(lock.withdraw()).not.to.be.reverted; 94 | }); 95 | }); 96 | 97 | describe("Events", function () { 98 | it("Should emit an event on withdrawals", async function () { 99 | const { lock, unlockTime, lockedAmount } = await loadFixture( 100 | deployOneYearLockFixture 101 | ); 102 | 103 | await time.increaseTo(unlockTime); 104 | 105 | await expect(lock.withdraw()) 106 | .to.emit(lock, "Withdrawal") 107 | .withArgs(lockedAmount, anyValue); // We accept any value as `when` arg 108 | }); 109 | }); 110 | 111 | describe("Transfers", function () { 112 | it("Should transfer the funds to the owner", async function () { 113 | const { lock, unlockTime, lockedAmount, owner } = await loadFixture( 114 | deployOneYearLockFixture 115 | ); 116 | 117 | await time.increaseTo(unlockTime); 118 | 119 | await expect(lock.withdraw()).to.changeEtherBalances( 120 | [owner, lock], 121 | [lockedAmount, -lockedAmount] 122 | ); 123 | }); 124 | }); 125 | }); 126 | }); 127 | --------------------------------------------------------------------------------