diff --git a/packages/hardhat/.gitignore b/packages/hardhat/.gitignore index 9722dbf..a191058 100644 --- a/packages/hardhat/.gitignore +++ b/packages/hardhat/.gitignore @@ -12,3 +12,7 @@ node_modules # solidity-coverage files /coverage /coverage.json + + +# Ignore lock files +*.lock diff --git a/packages/hardhat/README.md b/packages/hardhat/README.md index 7be82e5..2e95b94 100644 --- a/packages/hardhat/README.md +++ b/packages/hardhat/README.md @@ -1,13 +1,57 @@ -# Sample Hardhat Project +# Celo Composer - MiniPay Template | Hardhat -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. +## How to use -Try running some of the following tasks: +1. Create a copy of `.env.example` and rename it to `.env`. -```shell -npx hardhat help -npx hardhat test -REPORT_GAS=true npx hardhat test -npx hardhat node -npx hardhat run scripts/deploy.ts + 1. For the **smart contract deployment** you will need the `PRIVATE_KEY` set in `.env`. **Never** use a wallet with real funds for development. Always have a separate wallet for testing. + 2. For the **smart contract verification** you will need a [Celoscan API Key](https://celoscan.io/myapikey) `CELOSCAN_API_KEY` set in `.env`. + +2. Compile the contract + +```bash +npx hardhat compile +``` + +3. Deploy the contract + +Make sure your wallet is funded when deploying to testnet or mainnet. You can get test tokens for deploying it on Alfajores from the [Celo Faucet](https://faucet.celo.org/alfajores). + +```bash +npx hardhat ignition deploy ./ignition/modules/MiniPay.ts --network +``` + +On Alfajores + +```bash +npx hardhat ignition deploy ./ignition/modules/MiniPay.ts --network alfajores +``` + + +On Celo Mainnet + +```bash +npx hardhat ignition deploy ./ignition/modules/MiniPay.ts --network celo +``` + +4. Verify the contract + +For Alfajores (Testnet) Verification + +```bash +npx hardhat verify --network alfajores +``` + +For the Lock.sol contract that could look like this: + +```bash +npx hardhat verify 0xF9316Ce3E661D704000bCDDA925766Bf7F09fF5B 0x1724707c52de2fa65ad9c586b5d38507f52D3c06 --network alfajores +``` + +For Celo Mainnet Verification + +```bash +npx hardhat verify --network celo ``` + +Check the file `hardhat.config.js` for Celo specific hardhat configuration. diff --git a/packages/hardhat/contracts/Lock.sol b/packages/hardhat/contracts/Lock.sol deleted file mode 100644 index 50935f6..0000000 --- a/packages/hardhat/contracts/Lock.sol +++ /dev/null @@ -1,34 +0,0 @@ -// 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/packages/hardhat/contracts/ERC721.sol b/packages/hardhat/contracts/MiniPay.sol similarity index 100% rename from packages/hardhat/contracts/ERC721.sol rename to packages/hardhat/contracts/MiniPay.sol diff --git a/packages/hardhat/hardhat.config.ts b/packages/hardhat/hardhat.config.ts index 6074cca..ad2dc31 100644 --- a/packages/hardhat/hardhat.config.ts +++ b/packages/hardhat/hardhat.config.ts @@ -1,23 +1,49 @@ -import "@nomicfoundation/hardhat-toolbox"; -import dotenv from "dotenv"; -import { HardhatUserConfig } from "hardhat/config"; +import '@nomicfoundation/hardhat-toolbox'; +import '@nomicfoundation/hardhat-verify'; +import { config as dotEnvConfig } from 'dotenv'; +import { HardhatUserConfig } from 'hardhat/config'; -dotenv.config(); - -const PRIVATE_KEY = process.env.PRIVATE_KEY as string; - -if (!PRIVATE_KEY) { - throw new Error("PRIVATE_KEY is not set"); -} +dotEnvConfig(); const config: HardhatUserConfig = { - solidity: "0.8.20", networks: { alfajores: { - url: "https://alfajores-forno.celo-testnet.org", - accounts: [PRIVATE_KEY], + accounts: [process.env.PRIVATE_KEY ?? '0x0'], + url: 'https://alfajores-forno.celo-testnet.org', + }, + celo: { + accounts: [process.env.PRIVATE_KEY ?? '0x0'], + url: 'https://forno.celo.org', }, }, + etherscan: { + apiKey: { + alfajores: process.env.CELOSCAN_API_KEY ?? '', + celo: process.env.CELOSCAN_API_KEY ?? '', + }, + customChains: [ + { + chainId: 44_787, + network: 'alfajores', + urls: { + apiURL: 'https://api-alfajores.celoscan.io/api', + browserURL: 'https://alfajores.celoscan.io', + }, + }, + { + chainId: 42_220, + network: 'celo', + urls: { + apiURL: 'https://api.celoscan.io/api', + browserURL: 'https://celoscan.io/', + }, + }, + ], + }, + sourcify: { + enabled: false, + }, + solidity: '0.8.24', }; export default config; diff --git a/packages/hardhat/ignition/modules/MiniPay.ts b/packages/hardhat/ignition/modules/MiniPay.ts new file mode 100644 index 0000000..cbc9f88 --- /dev/null +++ b/packages/hardhat/ignition/modules/MiniPay.ts @@ -0,0 +1,17 @@ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +const MiniPayModule = buildModule("MiniPayModule", (m) => { + // Set up parameters if you want to make the contract address configurable + const initialOwner = m.getParameter( + "initialOwner", + "0x1724707c52de2fa65ad9c586b5d38507f52D3c06" + ); + + // Deploy the MiniPay contract with the specified parameters + const miniPayNFT = m.contract("MiniPay", [initialOwner]); + + return { miniPayNFT }; +}); + +export default MiniPayModule; + diff --git a/packages/hardhat/package.json b/packages/hardhat/package.json index 16b5ab2..8677e69 100644 --- a/packages/hardhat/package.json +++ b/packages/hardhat/package.json @@ -1,26 +1,40 @@ { "name": "hardhat-project", + "license": "MIT", "version": "1.0.0", "devDependencies": { "@nomicfoundation/hardhat-chai-matchers": "^2.0.0", - "@nomicfoundation/hardhat-network-helpers": "^1.0.0", - "@nomicfoundation/hardhat-toolbox": "^4.0.0", - "@nomicfoundation/hardhat-verify": "^2.0.0", + "@nomicfoundation/hardhat-ethers": "^3.0.0", + "@nomicfoundation/hardhat-ignition": "^0.15.7", + "@nomicfoundation/hardhat-ignition-ethers": "^0.15.0", + "@nomicfoundation/hardhat-network-helpers": "^1.0.11", + "@nomicfoundation/hardhat-toolbox": "^5.0.0", + "@nomicfoundation/hardhat-verify": "^2.0.11", + "@nomicfoundation/ignition-core": "^0.15.5", + "@nomiclabs/hardhat-ethers": "^2.2.3", + "@openzeppelin/contracts": "^5.1.0", "@typechain/ethers-v6": "^0.5.0", - "@typechain/hardhat": "^9.0.0", - "@types/chai": "^4.2.0", - "@types/mocha": ">=9.1.0", + "@typechain/hardhat": "^9.1.0", + "@types/chai": "^4.3.16", + "@types/mocha": "^10.0.7", "chai": "^4.2.0", - "hardhat": "^2.19.5", + "hardhat": "^2.22.15", "hardhat-gas-reporter": "^1.0.8", "solidity-coverage": "^0.8.1", "ts-node": "^10.9.2", - "typechain": "^8.3.0" + "typechain": "^8.3.2", + "typescript": "^5.5.3" }, "dependencies": { - "@nomicfoundation/hardhat-ethers": "^3.0.5", - "@openzeppelin/contracts": "^5.0.1", - "dotenv": "^16.4.3", - "ethers": "^6.11.0" + "dotenv": "^16.4.5" + }, + "scripts": { + "compile": "hardhat compile", + "tsc": "npx tsc -p . && cp typechain/*.d.ts dist/typechain/", + "build": "yarn compile && yarn tsc", + "clean": "hardhat clean", + "run:node": "hardhat node", + "test": "hardhat test", + "prettier": "prettier --write 'contracts/**/*.sol' '**/*.ts'" } } diff --git a/packages/hardhat/scripts/deploy.ts b/packages/hardhat/scripts/deploy.ts deleted file mode 100644 index 2e535cc..0000000 --- a/packages/hardhat/scripts/deploy.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ethers } from "hardhat"; - -async function main() { - const miniPayNFT = await ethers.deployContract("MiniPay", [ - "0x0D6Dc2f182Eafa687090F95466d5368726C1ca45", - ]); - - await miniPayNFT.waitForDeployment(); - - console.log("Minipay NFT address - " + (await miniPayNFT.getAddress())); -} - -// 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; -}); diff --git a/packages/hardhat/test/Lock.ts b/packages/hardhat/test/Lock.ts deleted file mode 100644 index a6e866b..0000000 --- a/packages/hardhat/test/Lock.ts +++ /dev/null @@ -1,127 +0,0 @@ -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", 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.getContractFactory("Lock"); - const lock = await Lock.deploy(unlockTime, { value: lockedAmount }); - - 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/packages/hardhat/test/MiniPay.ts b/packages/hardhat/test/MiniPay.ts new file mode 100644 index 0000000..a46ad86 --- /dev/null +++ b/packages/hardhat/test/MiniPay.ts @@ -0,0 +1,77 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { MiniPay, MiniPay__factory } from "../typechain-types"; + +describe("MiniPay Contract", function () { + let MiniPayFactory: MiniPay__factory; + let miniPay: MiniPay; + let owner: SignerWithAddress; + let addr1: SignerWithAddress; + let addr2: SignerWithAddress; + + beforeEach(async () => { + MiniPayFactory = (await ethers.getContractFactory("MiniPay")) as MiniPay__factory; + + // Retrieve the signers and cast them to `SignerWithAddress` + [owner, addr1, addr2] = (await ethers.getSigners()) as SignerWithAddress[]; + + // Use `owner.address` directly instead of `getAddress` + miniPay = await MiniPayFactory.deploy(owner.address); + await miniPay.deployed(); + }); + + it("should deploy with correct name and symbol", async () => { + expect(await miniPay.name()).to.equal("MiniPay"); + expect(await miniPay.symbol()).to.equal("MINI"); + }); + + it("should allow owner to mint NFTs and assign correct URI", async () => { + await miniPay.safeMint(addr1.address, "https://example.com/nft1"); + expect(await miniPay.ownerOf(0)).to.equal(addr1.address); + expect(await miniPay.tokenURI(0)).to.equal("https://example.com/nft1"); + + await miniPay.safeMint(addr2.address, "https://example.com/nft2"); + expect(await miniPay.ownerOf(1)).to.equal(addr2.address); + expect(await miniPay.tokenURI(1)).to.equal("https://example.com/nft2"); + }); + + it("should not allow non-owner to mint NFTs", async () => { + await expect( + miniPay.connect(addr1).safeMint(addr1.address, "https://example.com/nft3") + ).to.be.revertedWith("Ownable: caller is not the owner"); + }); + + it("should pause and unpause the contract", async () => { + await miniPay.pause(); + await expect( + miniPay.safeMint(addr1.address, "https://example.com/nft4") + ).to.be.revertedWith("Pausable: paused"); + + await miniPay.unpause(); + await miniPay.safeMint(addr1.address, "https://example.com/nft4"); + expect(await miniPay.ownerOf(0)).to.equal(addr1.address); + }); + + it("should allow owner to burn their NFTs", async () => { + await miniPay.safeMint(addr1.address, "https://example.com/nft5"); + expect(await miniPay.ownerOf(0)).to.equal(addr1.address); + + await miniPay.connect(addr1).burn(0); + await expect(miniPay.ownerOf(0)).to.be.revertedWith( + "ERC721: owner query for nonexistent token" + ); + }); + + it("should retrieve NFTs owned by a specific address", async () => { + await miniPay.safeMint(addr1.address, "https://example.com/nft6"); + await miniPay.safeMint(addr1.address, "https://example.com/nft7"); + await miniPay.safeMint(addr2.address, "https://example.com/nft8"); + + const addr1NFTs = await miniPay.getNFTsByAddress(addr1.address); + const addr2NFTs = await miniPay.getNFTsByAddress(addr2.address); + + expect(addr1NFTs.map((id) => id.toNumber())).to.deep.equal([0, 1]); + expect(addr2NFTs.map((id) => id.toNumber())).to.deep.equal([2]); + }); +});