From de422b61b08cfa83acf6894f1d496ac6978c21da Mon Sep 17 00:00:00 2001 From: Salman Dabbakuti Date: Fri, 3 Nov 2023 11:47:04 +0530 Subject: [PATCH] updated: migrating to ts boilerplate with lock contract and tests --- .gitignore | 4 +- README.md | 8 +-- contracts/Greeter.sol | 18 ------ contracts/Lock.sol | 34 +++++++++++ hardhat.config.js | 80 -------------------------- hardhat.config.ts | 95 +++++++++++++++++++++++++++++++ package.json | 6 +- scripts/deploy.js | 25 -------- scripts/deploy.ts | 31 ++++++++++ test/Lock.ts | 129 ++++++++++++++++++++++++++++++++++++++++++ test/sample-test.js | 28 --------- tsconfig.json | 11 ++++ 12 files changed, 310 insertions(+), 159 deletions(-) delete mode 100644 contracts/Greeter.sol create mode 100644 contracts/Lock.sol delete mode 100644 hardhat.config.js create mode 100644 hardhat.config.ts delete mode 100644 scripts/deploy.js create mode 100644 scripts/deploy.ts create mode 100644 test/Lock.ts delete mode 100644 test/sample-test.js create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 3a5c6b9..5f82570 100644 --- a/.gitignore +++ b/.gitignore @@ -20,10 +20,12 @@ yarn.lock # Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties` # should NOT be excluded as they contain compiler settings and other important # information for Eclipse / Flash Builder. +.DS_Store #Hardhat files cache artifacts/ coverage/ coverage.json -.DS_Store +typechain/ +typechain-types/ diff --git a/README.md b/README.md index 57dd9bc..dc487a6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# hardhat-boilerplate +# Hardhat Boilerplate This project demonstrates a basic Hardhat use case. It comes with a sample contract, a test for that contract, a sample script that deploys that contract, and an example of a task implementation, which simply lists the available accounts with balances. @@ -8,7 +8,7 @@ This project demonstrates a basic Hardhat use case. It comes with a sample contr Try running some of the following tasks: -```shell +```bash npm install # starts local node @@ -26,8 +26,8 @@ npx hardhat compile # deploy contract defined in tasks on specified network npx hardhat deploy --network local -# deploy contract in scripts/deploy.js on specified network -npx hardhat run scripts/deploy.js --network local +# deploy contract in scripts/deploy.ts on specified network +npx hardhat run scripts/deploy.ts --network local #check linter issues using solhint plugin npx hardhat check diff --git a/contracts/Greeter.sol b/contracts/Greeter.sol deleted file mode 100644 index afbdc30..0000000 --- a/contracts/Greeter.sol +++ /dev/null @@ -1,18 +0,0 @@ -//SPDX-License-Identifier: MIT -pragma solidity 0.8.19; - -contract Greeter { - string private greeting; - - constructor(string memory _greeting) { - greeting = _greeting; - } - - function getGreeting() public view returns (string memory) { - return greeting; - } - - function setGreeting(string memory _greeting) public { - greeting = _greeting; - } -} diff --git a/contracts/Lock.sol b/contracts/Lock.sol new file mode 100644 index 0000000..50935f6 --- /dev/null +++ b/contracts/Lock.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.9; + +// Uncomment this line to use console.log +// import "hardhat/console.sol"; + +contract Lock { + uint public unlockTime; + address payable public owner; + + event Withdrawal(uint amount, uint when); + + constructor(uint _unlockTime) payable { + require( + block.timestamp < _unlockTime, + "Unlock time should be in the future" + ); + + unlockTime = _unlockTime; + owner = payable(msg.sender); + } + + function withdraw() public { + // Uncomment this line, and the import of "hardhat/console.sol", to print a log in your terminal + // console.log("Unlock time is %o and block timestamp is %o", unlockTime, block.timestamp); + + require(block.timestamp >= unlockTime, "You can't withdraw yet"); + require(msg.sender == owner, "You aren't the owner"); + + emit Withdrawal(address(this).balance, block.timestamp); + + owner.transfer(address(this).balance); + } +} diff --git a/hardhat.config.js b/hardhat.config.js deleted file mode 100644 index faf3e5a..0000000 --- a/hardhat.config.js +++ /dev/null @@ -1,80 +0,0 @@ -require('@nomicfoundation/hardhat-toolbox'); -require('dotenv').config(); - -// defining accounts to reuse. -const accounts = process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : []; - -// This is a sample Hardhat task. To learn how to create your own go to -// https://hardhat.org/guides/create-task.html -task("hello", "Prints Hello World", () => console.log("Hello World!")); - -task("accounts", "Prints the list of accounts with balances", async () => { - const accounts = await ethers.getSigners(); - const provider = await ethers.provider; - - for (const account of accounts) { - const balance = await provider.getBalance(account.address); - console.log(`${account.address} - ${ethers.formatEther(balance)} ETH`); - } -}); - -task("deploy", "Deploys Contract", async () => { - const contract = await ethers.deployContract("Greeter", ["Hello, Hardhat!"]); - await contract.waitForDeployment(); - console.log("contract deployed at:", contract.target); -}); - -task("balance", "Prints an account's balance") - .addParam("account", "The account's address") - .setAction(async ({ account }) => { - const provider = await ethers.provider; - const balance = await provider.getBalance(account); - console.log(ethers.formatEther(balance), "ETH"); - }); - - -module.exports = { - defaultNetwork: "local", - networks: { - hardhat: { - chainId: 1337 - }, - local: { - url: "http://127.0.0.1:8545", - }, - mumbai: { - url: process.env.POLYGON_MUMBAI_RPC_URL || "https://rpc-mumbai.maticvigil.com", - accounts - }, - polygon: { - url: process.env.POLYGON_MAINNET_RPC_URL || "https://rpc-mainnet.maticvigil.com", - accounts - } - }, - etherscan: { - // API key for Polygonscan - apiKey: process.env.ETHERSCAN_API_KEY - }, - gasReporter: { - enabled: true, - currency: "USD", - }, - solidity: { - version: "0.8.19", - settings: { - optimizer: { - enabled: true, - runs: 200 - } - } - }, - paths: { - sources: "./contracts", - tests: "./test", - cache: "./cache", - artifacts: "./artifacts" - }, - mocha: { - timeout: 20000 - } -}; \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts new file mode 100644 index 0000000..f0ccdea --- /dev/null +++ b/hardhat.config.ts @@ -0,0 +1,95 @@ +import { HardhatUserConfig, task } from "hardhat/config"; +import "@nomicfoundation/hardhat-toolbox"; +import "dotenv/config"; + +const accounts = process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : []; + +// https://hardhat.org/guides/create-task.html +task( + "accounts", + "Prints the list of accounts with balances", + async (_, hre) => { + const accounts = await hre.ethers.getSigners(); + const provider = hre.ethers.provider; + + for (const account of accounts) { + const balance = await provider.getBalance(account.address); + console.log( + `${account.address} - ${hre.ethers.formatEther(balance)} ETH` + ); + } + } +); + +task("deploy", "Deploys Contract", async (_, hre) => { + const currentTimestampInSeconds = Math.round(Date.now() / 1000); + const unlockTime = currentTimestampInSeconds + 300; + const lockedAmount = hre.ethers.parseEther("0.001"); + + const lockInstance = await hre.ethers.deployContract("Lock", [unlockTime], { + value: lockedAmount + }); + + await lockInstance.waitForDeployment(); + console.log("contract deployed at:", lockInstance.target); +}); + +task("balance", "Prints an account's balance") + .addParam("account", "The account's address") + .setAction(async ({ account }, hre) => { + const provider = hre.ethers.provider; + const balance = await provider.getBalance(account); + console.log(hre.ethers.formatEther(balance), "ETH"); + }); + +const config: HardhatUserConfig = { + defaultNetwork: "local", + networks: { + hardhat: { + chainId: 1337 + }, + local: { + url: "http://127.0.0.1:8545" + }, + mumbai: { + url: + process.env.POLYGON_MUMBAI_RPC_URL || + "https://rpc-mumbai.maticvigil.com", + accounts + }, + polygon: { + url: + process.env.POLYGON_MAINNET_RPC_URL || + "https://rpc-mainnet.maticvigil.com", + accounts + } + }, + etherscan: { + // API key for Polygonscan + apiKey: process.env.ETHERSCAN_API_KEY + }, + gasReporter: { + enabled: true, + currency: "USD" + }, + solidity: { + version: "0.8.19", + settings: { + optimizer: { + enabled: true, + runs: 200 + } + } + }, + paths: { + sources: "./contracts", + tests: "./test", + cache: "./cache", + artifacts: "./artifacts" + }, + mocha: { + timeout: 20000 + } +}; + +export default config; diff --git a/package.json b/package.json index b1dd211..3ceb591 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hardhat-boilerplate", - "version": "1.2.0", - "description": "hardhat and ethersjs boilerplate for Dapp development", + "version": "1.2.2", + "description": "Hardhat boilerplate for Dapp development", "private": true, "scripts": { "test": "npx hardhat test", @@ -15,6 +15,6 @@ "devDependencies": { "@nomicfoundation/hardhat-toolbox": "^3.0.0", "dotenv": "^16.3.1", - "hardhat": "^2.17.4" + "hardhat": "^2.19.0" } } diff --git a/scripts/deploy.js b/scripts/deploy.js deleted file mode 100644 index c1ccace..0000000 --- a/scripts/deploy.js +++ /dev/null @@ -1,25 +0,0 @@ -/* -* This script can only be run through Hardhat, and not through node directly. -* Since ethers or any other hardhat plugins are globally available to Hardhat Runtime Environment. So we are not importing them explicitly. -* So when running this script through node, we will get an error saying that ethers or any other plugins not defined error. -*/ - -async function main() { - const contract = await ethers.deployContract("Greeter", ["Hello, Hardhat!"]); - await contract.waitForDeployment(); - return contract; -} - -main() - .then(async (contract) => { - console.log("Contract deployed at:", contract.target); - // Write to contract - const tx = await contract.setGreeting("Hello Ethereum Devs!"); - await tx.wait(); - // Read from contract - const greeting = await contract.getGreeting(); - console.log('Greeting from contract:', greeting); - }) - .catch((error) => { - console.error(error); - }); diff --git a/scripts/deploy.ts b/scripts/deploy.ts new file mode 100644 index 0000000..ce2d70a --- /dev/null +++ b/scripts/deploy.ts @@ -0,0 +1,31 @@ +import { ethers } from "hardhat"; + +async function main() { + const currentTimestampInSeconds = Math.round(Date.now() / 1000); + const unlockTime = currentTimestampInSeconds + 300; + const lockedAmount = ethers.parseEther("0.001"); + + const lockInstance = await ethers.deployContract("Lock", [unlockTime], { + value: lockedAmount + }); + + await lockInstance.waitForDeployment(); + return lockInstance; +} + +main() + .then(async (lockInstance) => { + console.log("Lock Contract deployed to:", lockInstance.target); + // Read from the contract + const unlockTime = await lockInstance.unlockTime(); + console.log("Unlock time:", unlockTime.toString()); + + // Write to the contract + // const tx = await lockInstance.withdraw(); + // await tx.wait(); + // console.log("Withdrawn!"); + }) + .catch((error) => { + console.error(error); + process.exitCode = 1; + }); diff --git a/test/Lock.ts b/test/Lock.ts new file mode 100644 index 0000000..53e894b --- /dev/null +++ b/test/Lock.ts @@ -0,0 +1,129 @@ +import { + time, + loadFixture +} from "@nomicfoundation/hardhat-toolbox/network-helpers"; +import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; +import { expect } from "chai"; +import { ethers } from "hardhat"; + +describe("Lock Contract Tests", function () { + // We define a fixture to reuse the same setup in every test. + // We use loadFixture to run this setup once, snapshot that state, + // and reset Hardhat Network to that snapshot in every test. + async function deployOneYearLockFixture() { + const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60; + const ONE_GWEI = 1_000_000_000; + + const lockedAmount = ONE_GWEI; + const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS; + + // Contracts are deployed using the first signer/account by default + const [owner, otherAccount] = await ethers.getSigners(); + + const lock = await ethers.deployContract("Lock", [unlockTime], { + value: lockedAmount + }); + await lock.waitForDeployment(); + + return { lock, unlockTime, lockedAmount, owner, otherAccount }; + } + + describe("Deployment", function () { + it("Should set the right unlockTime", async function () { + const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture); + + expect(await lock.unlockTime()).to.equal(unlockTime); + }); + + it("Should set the right owner", async function () { + const { lock, owner } = await loadFixture(deployOneYearLockFixture); + + expect(await lock.owner()).to.equal(owner.address); + }); + + it("Should receive and store the funds to lock", async function () { + const { lock, lockedAmount } = await loadFixture( + deployOneYearLockFixture + ); + + expect(await ethers.provider.getBalance(lock.target)).to.equal( + lockedAmount + ); + }); + + it("Should fail if the unlockTime is not in the future", async function () { + // We don't use the fixture here because we want a different deployment + const latestTime = await time.latest(); + const Lock = await ethers.getContractFactory("Lock"); + await expect(Lock.deploy(latestTime, { value: 1 })).to.be.revertedWith( + "Unlock time should be in the future" + ); + }); + }); + + describe("Withdrawals", function () { + describe("Validations", function () { + it("Should revert with the right error if called too soon", async function () { + const { lock } = await loadFixture(deployOneYearLockFixture); + + await expect(lock.withdraw()).to.be.revertedWith( + "You can't withdraw yet" + ); + }); + + it("Should revert with the right error if called from another account", async function () { + const { lock, unlockTime, otherAccount } = await loadFixture( + deployOneYearLockFixture + ); + + // We can increase the time in Hardhat Network + await time.increaseTo(unlockTime); + + // We use lock.connect() to send a transaction from another account + await expect(lock.connect(otherAccount).withdraw()).to.be.revertedWith( + "You aren't the owner" + ); + }); + + it("Shouldn't fail if the unlockTime has arrived and the owner calls it", async function () { + const { lock, unlockTime } = await loadFixture( + deployOneYearLockFixture + ); + + // Transactions are sent using the first signer by default + await time.increaseTo(unlockTime); + + await expect(lock.withdraw()).not.to.be.reverted; + }); + }); + + describe("Events", function () { + it("Should emit an event on withdrawals", async function () { + const { lock, unlockTime, lockedAmount } = await loadFixture( + deployOneYearLockFixture + ); + + await time.increaseTo(unlockTime); + + await expect(lock.withdraw()) + .to.emit(lock, "Withdrawal") + .withArgs(lockedAmount, anyValue); // We accept any value as `when` arg + }); + }); + + describe("Transfers", function () { + it("Should transfer the funds to the owner", async function () { + const { lock, unlockTime, lockedAmount, owner } = await loadFixture( + deployOneYearLockFixture + ); + + await time.increaseTo(unlockTime); + + await expect(lock.withdraw()).to.changeEtherBalances( + [owner, lock], + [lockedAmount, -lockedAmount] + ); + }); + }); + }); +}); diff --git a/test/sample-test.js b/test/sample-test.js deleted file mode 100644 index b3355db..0000000 --- a/test/sample-test.js +++ /dev/null @@ -1,28 +0,0 @@ -/* -* This script can only be run through Hardhat, and not through node directly. -* Since ethers or any other hardhat plugins are globally available to Hardhat Runtime Environment. So we are not importing them explicitly. -* So when running this script through node, we will get an error saying that ethers or any other plugins not defined error. -*/ - -const { expect } = require("chai"); - -describe("Contract Tests", function () { - let accounts; - let greeterContract; - - // `before` will run only once, useful for deploying the contract and use it on every test - // It receives a callback, which can be async. - before(async () => { - accounts = await ethers.getSigners(); - greeterContract = await ethers.deployContract("Greeter", ["Hello, Hardhat!"], { gasLimit: 1000000 }); - await greeterContract.waitForDeployment(); - }); - - it("Should return the new greeting once it's changed", async function () { - expect(await greeterContract.getGreeting()).to.equal("Hello, Hardhat!"); - const setGreetingTx = await greeterContract.setGreeting("Hola, mundo!"); - // wait for the transaction to be mined - await setGreetingTx.wait(); - expect(await greeterContract.getGreeting()).to.equal("Hola, mundo!"); - }); -}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..574e785 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true + } +}