Initial commit with 🏗️ create-eth @ 2.0.4
This commit is contained in:
13
packages/hardhat/.env.example
Normal file
13
packages/hardhat/.env.example
Normal file
@@ -0,0 +1,13 @@
|
||||
# Template for Hardhat environment variables.
|
||||
|
||||
# To use this template, copy this file, rename it .env, and fill in the values.
|
||||
|
||||
# If not set, we provide default values (check `hardhat.config.ts`) so developers can start prototyping out of the box,
|
||||
# but we recommend getting your own API Keys for Production Apps.
|
||||
|
||||
# To access the values stored in this .env file you can use: process.env.VARIABLENAME
|
||||
ALCHEMY_API_KEY=
|
||||
ETHERSCAN_V2_API_KEY=
|
||||
|
||||
# Don't fill this value manually, run yarn generate to generate a new account or yarn account:import to import an existing PK.
|
||||
DEPLOYER_PRIVATE_KEY_ENCRYPTED=
|
||||
30
packages/hardhat/.gitignore
vendored
Normal file
30
packages/hardhat/.gitignore
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# dependencies
|
||||
node_modules
|
||||
|
||||
# env files
|
||||
.env
|
||||
|
||||
# coverage
|
||||
coverage
|
||||
coverage.json
|
||||
|
||||
# typechain
|
||||
typechain
|
||||
typechain-types
|
||||
|
||||
# hardhat files
|
||||
cache
|
||||
artifacts
|
||||
|
||||
# zkSync files
|
||||
artifacts-zk
|
||||
cache-zk
|
||||
|
||||
# deployments
|
||||
deployments/localhost
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
# other
|
||||
temp
|
||||
18
packages/hardhat/.prettierrc.json
Normal file
18
packages/hardhat/.prettierrc.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"plugins": ["prettier-plugin-solidity"],
|
||||
"arrowParens": "avoid",
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.sol",
|
||||
"options": {
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"singleQuote": false,
|
||||
"bracketSpacing": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
61
packages/hardhat/contracts/CrowdFund.sol
Normal file
61
packages/hardhat/contracts/CrowdFund.sol
Normal file
@@ -0,0 +1,61 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity 0.8.20; // Do not change the solidity version as it negatively impacts submission grading
|
||||
|
||||
import "hardhat/console.sol";
|
||||
import "./FundingRecipient.sol";
|
||||
|
||||
contract CrowdFund {
|
||||
/////////////////
|
||||
/// Errors //////
|
||||
/////////////////
|
||||
|
||||
// Errors go here...
|
||||
|
||||
//////////////////////
|
||||
/// State Variables //
|
||||
//////////////////////
|
||||
|
||||
FundingRecipient public fundingRecipient;
|
||||
|
||||
////////////////
|
||||
/// Events /////
|
||||
////////////////
|
||||
|
||||
// Events go here...
|
||||
|
||||
///////////////////
|
||||
/// Modifiers /////
|
||||
///////////////////
|
||||
|
||||
modifier notCompleted() {
|
||||
_;
|
||||
}
|
||||
|
||||
///////////////////
|
||||
/// Constructor ///
|
||||
///////////////////
|
||||
|
||||
constructor(address fundingRecipientAddress) {
|
||||
fundingRecipient = FundingRecipient(fundingRecipientAddress);
|
||||
}
|
||||
|
||||
///////////////////
|
||||
/// Functions /////
|
||||
///////////////////
|
||||
|
||||
function contribute() public payable {}
|
||||
|
||||
function withdraw() public {}
|
||||
|
||||
function execute() public {}
|
||||
|
||||
receive() external payable {}
|
||||
|
||||
////////////////////////
|
||||
/// View Functions /////
|
||||
////////////////////////
|
||||
|
||||
function timeLeft() public view returns (uint256) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
10
packages/hardhat/contracts/FundingRecipient.sol
Normal file
10
packages/hardhat/contracts/FundingRecipient.sol
Normal file
@@ -0,0 +1,10 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity 0.8.20; // Do not change the solidity version as it negatively impacts submission grading
|
||||
|
||||
contract FundingRecipient {
|
||||
bool public completed;
|
||||
|
||||
function complete() public payable {
|
||||
completed = true;
|
||||
}
|
||||
}
|
||||
22
packages/hardhat/deploy/00_deploy_funding_recipient.ts
Normal file
22
packages/hardhat/deploy/00_deploy_funding_recipient.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||
import { DeployFunction } from "hardhat-deploy/types";
|
||||
|
||||
/**
|
||||
* Deploys a contract named "FundingRecipient" using the deployer account.
|
||||
*
|
||||
* @param hre HardhatRuntimeEnvironment object.
|
||||
*/
|
||||
const deployFundingRecipient: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
|
||||
const { deployer } = await hre.getNamedAccounts();
|
||||
const { deploy } = hre.deployments;
|
||||
|
||||
await deploy("FundingRecipient", {
|
||||
from: deployer,
|
||||
log: true,
|
||||
autoMine: true,
|
||||
});
|
||||
};
|
||||
|
||||
export default deployFundingRecipient;
|
||||
|
||||
deployFundingRecipient.tags = ["FundingRecipient"];
|
||||
25
packages/hardhat/deploy/01_deploy_crowdfund.ts
Normal file
25
packages/hardhat/deploy/01_deploy_crowdfund.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||
import { DeployFunction } from "hardhat-deploy/types";
|
||||
|
||||
/**
|
||||
* Deploys a contract named "CrowdFund" using the deployer account.
|
||||
*
|
||||
* @param hre HardhatRuntimeEnvironment object.
|
||||
*/
|
||||
const deployCrowdFund: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
|
||||
const { deployer } = await hre.getNamedAccounts();
|
||||
const { deploy, get } = hre.deployments;
|
||||
|
||||
const fundingRecipient = await get("FundingRecipient");
|
||||
|
||||
await deploy("CrowdFund", {
|
||||
from: deployer,
|
||||
args: [fundingRecipient.address],
|
||||
log: true,
|
||||
autoMine: true,
|
||||
});
|
||||
};
|
||||
|
||||
export default deployCrowdFund;
|
||||
|
||||
deployCrowdFund.tags = ["CrowdFund"];
|
||||
44
packages/hardhat/eslint.config.mjs
Normal file
44
packages/hardhat/eslint.config.mjs
Normal file
@@ -0,0 +1,44 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import globals from "globals";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import prettierPlugin from "eslint-plugin-prettier";
|
||||
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(["**/artifacts", "**/cache", "**/contracts", "**/node_modules/", "**/typechain-types", "**/*.json"]),
|
||||
{
|
||||
extends: compat.extends("plugin:@typescript-eslint/recommended", "prettier"),
|
||||
|
||||
plugins: {
|
||||
prettier: prettierPlugin,
|
||||
},
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
},
|
||||
|
||||
parser: tsParser,
|
||||
},
|
||||
|
||||
rules: {
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
|
||||
"prettier/prettier": [
|
||||
"warn",
|
||||
{
|
||||
endOfLine: "auto",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
156
packages/hardhat/hardhat.config.ts
Normal file
156
packages/hardhat/hardhat.config.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import * as dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import { HardhatUserConfig } from "hardhat/config";
|
||||
import "@nomicfoundation/hardhat-ethers";
|
||||
import "@nomicfoundation/hardhat-chai-matchers";
|
||||
import "@typechain/hardhat";
|
||||
import "hardhat-gas-reporter";
|
||||
import "solidity-coverage";
|
||||
import "@nomicfoundation/hardhat-verify";
|
||||
import "hardhat-deploy";
|
||||
import "hardhat-deploy-ethers";
|
||||
import { task } from "hardhat/config";
|
||||
import generateTsAbis from "./scripts/generateTsAbis";
|
||||
|
||||
// If not set, it uses ours Alchemy's default API key.
|
||||
// You can get your own at https://dashboard.alchemyapi.io
|
||||
const providerApiKey = process.env.ALCHEMY_API_KEY || "cR4WnXePioePZ5fFrnSiR";
|
||||
// If not set, it uses the hardhat account 0 private key.
|
||||
// You can generate a random account with `yarn generate` or `yarn account:import` to import your existing PK
|
||||
const deployerPrivateKey =
|
||||
process.env.__RUNTIME_DEPLOYER_PRIVATE_KEY ?? "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
|
||||
// If not set, it uses our block explorers default API keys.
|
||||
const etherscanApiKey = process.env.ETHERSCAN_V2_API_KEY || "DNXJA8RX2Q3VZ4URQIWP7Z68CJXQZSC6AW";
|
||||
|
||||
const config: HardhatUserConfig = {
|
||||
solidity: {
|
||||
compilers: [
|
||||
{
|
||||
version: "0.8.20",
|
||||
settings: {
|
||||
optimizer: {
|
||||
enabled: true,
|
||||
// https://docs.soliditylang.org/en/latest/using-the-compiler.html#optimizer-options
|
||||
runs: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultNetwork: "localhost",
|
||||
namedAccounts: {
|
||||
deployer: {
|
||||
// By default, it will take the first Hardhat account as the deployer
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
networks: {
|
||||
mainnet: {
|
||||
url: "https://mainnet.rpc.buidlguidl.com",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
sepolia: {
|
||||
url: `https://eth-sepolia.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
arbitrum: {
|
||||
url: `https://arb-mainnet.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
arbitrumSepolia: {
|
||||
url: `https://arb-sepolia.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
optimism: {
|
||||
url: `https://opt-mainnet.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
optimismSepolia: {
|
||||
url: `https://opt-sepolia.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
polygon: {
|
||||
url: `https://polygon-mainnet.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
polygonAmoy: {
|
||||
url: `https://polygon-amoy.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
polygonZkEvm: {
|
||||
url: `https://polygonzkevm-mainnet.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
polygonZkEvmCardona: {
|
||||
url: `https://polygonzkevm-cardona.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
gnosis: {
|
||||
url: "https://rpc.gnosischain.com",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
chiado: {
|
||||
url: "https://rpc.chiadochain.net",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
base: {
|
||||
url: "https://mainnet.base.org",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
baseSepolia: {
|
||||
url: "https://sepolia.base.org",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
scrollSepolia: {
|
||||
url: "https://sepolia-rpc.scroll.io",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
scroll: {
|
||||
url: "https://rpc.scroll.io",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
celo: {
|
||||
url: "https://forno.celo.org",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
celoSepolia: {
|
||||
url: "https://forno.celo-sepolia.celo-testnet.org/",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
// View the networks that are pre-configured.
|
||||
// If the network you are looking for is not here you can add new network settings
|
||||
hardhat: {
|
||||
forking: {
|
||||
url: `https://eth-mainnet.alchemyapi.io/v2/${providerApiKey}`,
|
||||
enabled: process.env.MAINNET_FORKING_ENABLED === "true",
|
||||
},
|
||||
mining: {
|
||||
auto: true,
|
||||
interval: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
// Configuration for harhdat-verify plugin
|
||||
etherscan: {
|
||||
apiKey: etherscanApiKey,
|
||||
},
|
||||
// Configuration for etherscan-verify from hardhat-deploy plugin
|
||||
verify: {
|
||||
etherscan: {
|
||||
apiKey: etherscanApiKey,
|
||||
},
|
||||
},
|
||||
sourcify: {
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
// Extend the deploy task
|
||||
task("deploy").setAction(async (args, hre, runSuper) => {
|
||||
// Run the original deploy task
|
||||
await runSuper(args);
|
||||
// Force run the generateTsAbis script
|
||||
await generateTsAbis(hre);
|
||||
});
|
||||
|
||||
export default config;
|
||||
63
packages/hardhat/package.json
Normal file
63
packages/hardhat/package.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "@se-2/hardhat",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"account": "hardhat run scripts/listAccount.ts",
|
||||
"account:generate": "hardhat run scripts/generateAccount.ts",
|
||||
"account:import": "hardhat run scripts/importAccount.ts",
|
||||
"account:reveal-pk": "hardhat run scripts/revealPK.ts",
|
||||
"chain": "hardhat node --network hardhat --no-deploy",
|
||||
"check-types": "tsc --noEmit --incremental",
|
||||
"clean": "hardhat clean",
|
||||
"compile": "hardhat compile",
|
||||
"deploy": "ts-node scripts/runHardhatDeployWithPK.ts",
|
||||
"flatten": "hardhat flatten",
|
||||
"fork": "MAINNET_FORKING_ENABLED=true hardhat node --network hardhat --no-deploy",
|
||||
"format": "prettier --write './**/*.(ts|sol)'",
|
||||
"generate": "yarn account:generate",
|
||||
"hardhat-verify": "hardhat verify",
|
||||
"lint": "eslint",
|
||||
"lint-staged": "eslint",
|
||||
"test": "REPORT_GAS=true hardhat test --network hardhat",
|
||||
"verify": "hardhat etherscan-verify"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inquirer/password": "^4.0.2",
|
||||
"@openzeppelin/contracts": "~5.0.2",
|
||||
"@typechain/ethers-v6": "~0.5.1",
|
||||
"dotenv": "~16.4.5",
|
||||
"envfile": "~7.1.0",
|
||||
"qrcode": "~1.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ethersproject/abi": "~5.7.0",
|
||||
"@ethersproject/providers": "~5.7.2",
|
||||
"@nomicfoundation/hardhat-chai-matchers": "~2.0.7",
|
||||
"@nomicfoundation/hardhat-ethers": "~3.0.8",
|
||||
"@nomicfoundation/hardhat-network-helpers": "~1.0.11",
|
||||
"@nomicfoundation/hardhat-verify": "~2.0.10",
|
||||
"@typechain/ethers-v5": "~11.1.2",
|
||||
"@typechain/hardhat": "~9.1.0",
|
||||
"@types/eslint": "~9.6.1",
|
||||
"@types/mocha": "~10.0.10",
|
||||
"@types/prettier": "~3.0.0",
|
||||
"@types/qrcode": "~1.5.5",
|
||||
"@typescript-eslint/eslint-plugin": "~8.27.0",
|
||||
"@typescript-eslint/parser": "~8.27.0",
|
||||
"chai": "~4.5.0",
|
||||
"eslint": "~9.23.0",
|
||||
"eslint-config-prettier": "~10.1.1",
|
||||
"eslint-plugin-prettier": "~5.2.4",
|
||||
"ethers": "~6.13.2",
|
||||
"hardhat": "~2.22.10",
|
||||
"hardhat-deploy": "^1.0.4",
|
||||
"hardhat-deploy-ethers": "~0.4.2",
|
||||
"hardhat-gas-reporter": "~2.2.1",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-solidity": "~1.4.1",
|
||||
"solidity-coverage": "~0.8.13",
|
||||
"ts-node": "~10.9.1",
|
||||
"typechain": "~8.3.2",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
58
packages/hardhat/scripts/generateAccount.ts
Normal file
58
packages/hardhat/scripts/generateAccount.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { ethers } from "ethers";
|
||||
import { parse, stringify } from "envfile";
|
||||
import * as fs from "fs";
|
||||
import password from "@inquirer/password";
|
||||
|
||||
const envFilePath = "./.env";
|
||||
|
||||
const getValidatedPassword = async () => {
|
||||
while (true) {
|
||||
const pass = await password({ message: "Enter a password to encrypt your private key:" });
|
||||
const confirmation = await password({ message: "Confirm password:" });
|
||||
|
||||
if (pass === confirmation) {
|
||||
return pass;
|
||||
}
|
||||
console.log("❌ Passwords don't match. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
const setNewEnvConfig = async (existingEnvConfig = {}) => {
|
||||
console.log("👛 Generating new Wallet\n");
|
||||
const randomWallet = ethers.Wallet.createRandom();
|
||||
|
||||
const pass = await getValidatedPassword();
|
||||
const encryptedJson = await randomWallet.encrypt(pass);
|
||||
|
||||
const newEnvConfig = {
|
||||
...existingEnvConfig,
|
||||
DEPLOYER_PRIVATE_KEY_ENCRYPTED: encryptedJson,
|
||||
};
|
||||
|
||||
// Store in .env
|
||||
fs.writeFileSync(envFilePath, stringify(newEnvConfig));
|
||||
console.log("\n📄 Encrypted Private Key saved to packages/hardhat/.env file");
|
||||
console.log("🪄 Generated wallet address:", randomWallet.address, "\n");
|
||||
console.log("⚠️ Make sure to remember your password! You'll need it to decrypt the private key.");
|
||||
};
|
||||
|
||||
async function main() {
|
||||
if (!fs.existsSync(envFilePath)) {
|
||||
// No .env file yet.
|
||||
await setNewEnvConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
const existingEnvConfig = parse(fs.readFileSync(envFilePath).toString());
|
||||
if (existingEnvConfig.DEPLOYER_PRIVATE_KEY_ENCRYPTED) {
|
||||
console.log("⚠️ You already have a deployer account. Check the packages/hardhat/.env file");
|
||||
return;
|
||||
}
|
||||
|
||||
await setNewEnvConfig(existingEnvConfig);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
136
packages/hardhat/scripts/generateTsAbis.ts
Normal file
136
packages/hardhat/scripts/generateTsAbis.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* DON'T MODIFY OR DELETE THIS SCRIPT (unless you know what you're doing)
|
||||
*
|
||||
* This script generates the file containing the contracts Abi definitions.
|
||||
* These definitions are used to derive the types needed in the custom scaffold-eth hooks, for example.
|
||||
* This script should run as the last deploy script.
|
||||
*/
|
||||
|
||||
import * as fs from "fs";
|
||||
import prettier from "prettier";
|
||||
import { DeployFunction } from "hardhat-deploy/types";
|
||||
|
||||
const generatedContractComment = `
|
||||
/**
|
||||
* This file is autogenerated by Scaffold-ETH.
|
||||
* You should not edit it manually or your changes might be overwritten.
|
||||
*/
|
||||
`;
|
||||
|
||||
const DEPLOYMENTS_DIR = "./deployments";
|
||||
const ARTIFACTS_DIR = "./artifacts";
|
||||
|
||||
function getDirectories(path: string) {
|
||||
return fs
|
||||
.readdirSync(path, { withFileTypes: true })
|
||||
.filter(dirent => dirent.isDirectory())
|
||||
.map(dirent => dirent.name);
|
||||
}
|
||||
|
||||
function getContractNames(path: string) {
|
||||
return fs
|
||||
.readdirSync(path, { withFileTypes: true })
|
||||
.filter(dirent => dirent.isFile() && dirent.name.endsWith(".json"))
|
||||
.map(dirent => dirent.name.split(".")[0]);
|
||||
}
|
||||
|
||||
function getActualSourcesForContract(sources: Record<string, any>, contractName: string) {
|
||||
for (const sourcePath of Object.keys(sources)) {
|
||||
const sourceName = sourcePath.split("/").pop()?.split(".sol")[0];
|
||||
if (sourceName === contractName) {
|
||||
const contractContent = sources[sourcePath].content as string;
|
||||
const regex = /contract\s+(\w+)\s+is\s+([^{}]+)\{/;
|
||||
const match = contractContent.match(regex);
|
||||
|
||||
if (match) {
|
||||
const inheritancePart = match[2];
|
||||
// Split the inherited contracts by commas to get the list of inherited contracts
|
||||
const inheritedContracts = inheritancePart.split(",").map(contract => `${contract.trim()}.sol`);
|
||||
|
||||
return inheritedContracts;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function getInheritedFunctions(sources: Record<string, any>, contractName: string) {
|
||||
const actualSources = getActualSourcesForContract(sources, contractName);
|
||||
const inheritedFunctions = {} as Record<string, any>;
|
||||
|
||||
for (const sourceContractName of actualSources) {
|
||||
const sourcePath = Object.keys(sources).find(key => key.includes(`/${sourceContractName}`));
|
||||
if (sourcePath) {
|
||||
const sourceName = sourcePath?.split("/").pop()?.split(".sol")[0];
|
||||
const { abi } = JSON.parse(fs.readFileSync(`${ARTIFACTS_DIR}/${sourcePath}/${sourceName}.json`).toString());
|
||||
for (const functionAbi of abi) {
|
||||
if (functionAbi.type === "function") {
|
||||
inheritedFunctions[functionAbi.name] = sourcePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return inheritedFunctions;
|
||||
}
|
||||
|
||||
function getContractDataFromDeployments() {
|
||||
if (!fs.existsSync(DEPLOYMENTS_DIR)) {
|
||||
throw Error("At least one other deployment script should exist to generate an actual contract.");
|
||||
}
|
||||
const output = {} as Record<string, any>;
|
||||
const chainDirectories = getDirectories(DEPLOYMENTS_DIR);
|
||||
for (const chainName of chainDirectories) {
|
||||
let chainId;
|
||||
try {
|
||||
chainId = fs.readFileSync(`${DEPLOYMENTS_DIR}/${chainName}/.chainId`).toString();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (error) {
|
||||
console.log(`No chainId file found for ${chainName}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const contracts = {} as Record<string, any>;
|
||||
for (const contractName of getContractNames(`${DEPLOYMENTS_DIR}/${chainName}`)) {
|
||||
const { abi, address, metadata, receipt } = JSON.parse(
|
||||
fs.readFileSync(`${DEPLOYMENTS_DIR}/${chainName}/${contractName}.json`).toString(),
|
||||
);
|
||||
const inheritedFunctions = metadata ? getInheritedFunctions(JSON.parse(metadata).sources, contractName) : {};
|
||||
contracts[contractName] = { address, abi, inheritedFunctions, deployedOnBlock: receipt?.blockNumber };
|
||||
}
|
||||
output[chainId] = contracts;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the TypeScript contract definition file based on the json output of the contract deployment scripts
|
||||
* This script should be run last.
|
||||
*/
|
||||
const generateTsAbis: DeployFunction = async function () {
|
||||
const TARGET_DIR = "../nextjs/contracts/";
|
||||
const allContractsData = getContractDataFromDeployments();
|
||||
|
||||
const fileContent = Object.entries(allContractsData).reduce((content, [chainId, chainConfig]) => {
|
||||
return `${content}${parseInt(chainId).toFixed(0)}:${JSON.stringify(chainConfig, null, 2)},`;
|
||||
}, "");
|
||||
|
||||
if (!fs.existsSync(TARGET_DIR)) {
|
||||
fs.mkdirSync(TARGET_DIR);
|
||||
}
|
||||
fs.writeFileSync(
|
||||
`${TARGET_DIR}deployedContracts.ts`,
|
||||
await prettier.format(
|
||||
`${generatedContractComment} import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract"; \n\n
|
||||
const deployedContracts = {${fileContent}} as const; \n\n export default deployedContracts satisfies GenericContractsDeclaration`,
|
||||
{
|
||||
parser: "typescript",
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
console.log(`📝 Updated TypeScript contract definition file on ${TARGET_DIR}deployedContracts.ts`);
|
||||
};
|
||||
|
||||
export default generateTsAbis;
|
||||
72
packages/hardhat/scripts/importAccount.ts
Normal file
72
packages/hardhat/scripts/importAccount.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { ethers } from "ethers";
|
||||
import { parse, stringify } from "envfile";
|
||||
import * as fs from "fs";
|
||||
import password from "@inquirer/password";
|
||||
|
||||
const envFilePath = "./.env";
|
||||
|
||||
const getValidatedPassword = async () => {
|
||||
while (true) {
|
||||
const pass = await password({ message: "Enter a password to encrypt your private key:" });
|
||||
const confirmation = await password({ message: "Confirm password:" });
|
||||
|
||||
if (pass === confirmation) {
|
||||
return pass;
|
||||
}
|
||||
console.log("❌ Passwords don't match. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
const getWalletFromPrivateKey = async () => {
|
||||
while (true) {
|
||||
const privateKey = await password({ message: "Paste your private key:" });
|
||||
try {
|
||||
const wallet = new ethers.Wallet(privateKey);
|
||||
return wallet;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
console.log("❌ Invalid private key format. Please try again.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const setNewEnvConfig = async (existingEnvConfig = {}) => {
|
||||
console.log("👛 Importing Wallet\n");
|
||||
|
||||
const wallet = await getWalletFromPrivateKey();
|
||||
|
||||
const pass = await getValidatedPassword();
|
||||
const encryptedJson = await wallet.encrypt(pass);
|
||||
|
||||
const newEnvConfig = {
|
||||
...existingEnvConfig,
|
||||
DEPLOYER_PRIVATE_KEY_ENCRYPTED: encryptedJson,
|
||||
};
|
||||
|
||||
// Store in .env
|
||||
fs.writeFileSync(envFilePath, stringify(newEnvConfig));
|
||||
console.log("\n📄 Encrypted Private Key saved to packages/hardhat/.env file");
|
||||
console.log("🪄 Imported wallet address:", wallet.address, "\n");
|
||||
console.log("⚠️ Make sure to remember your password! You'll need it to decrypt the private key.");
|
||||
};
|
||||
|
||||
async function main() {
|
||||
if (!fs.existsSync(envFilePath)) {
|
||||
// No .env file yet.
|
||||
await setNewEnvConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
const existingEnvConfig = parse(fs.readFileSync(envFilePath).toString());
|
||||
if (existingEnvConfig.DEPLOYER_PRIVATE_KEY_ENCRYPTED) {
|
||||
console.log("⚠️ You already have a deployer account. Check the packages/hardhat/.env file");
|
||||
return;
|
||||
}
|
||||
|
||||
await setNewEnvConfig(existingEnvConfig);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
52
packages/hardhat/scripts/listAccount.ts
Normal file
52
packages/hardhat/scripts/listAccount.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import * as dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import { ethers, Wallet } from "ethers";
|
||||
import QRCode from "qrcode";
|
||||
import { config } from "hardhat";
|
||||
import password from "@inquirer/password";
|
||||
|
||||
async function main() {
|
||||
const encryptedKey = process.env.DEPLOYER_PRIVATE_KEY_ENCRYPTED;
|
||||
|
||||
if (!encryptedKey) {
|
||||
console.log("🚫️ You don't have a deployer account. Run `yarn generate` or `yarn account:import` first");
|
||||
return;
|
||||
}
|
||||
|
||||
const pass = await password({ message: "Enter your password to decrypt the private key:" });
|
||||
let wallet: Wallet;
|
||||
try {
|
||||
wallet = (await Wallet.fromEncryptedJson(encryptedKey, pass)) as Wallet;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
console.log("❌ Failed to decrypt private key. Wrong password?");
|
||||
return;
|
||||
}
|
||||
|
||||
const address = wallet.address;
|
||||
console.log(await QRCode.toString(address, { type: "terminal", small: true }));
|
||||
console.log("Public address:", address, "\n");
|
||||
|
||||
// Balance on each network
|
||||
const availableNetworks = config.networks;
|
||||
for (const networkName in availableNetworks) {
|
||||
try {
|
||||
const network = availableNetworks[networkName];
|
||||
if (!("url" in network)) continue;
|
||||
const provider = new ethers.JsonRpcProvider(network.url);
|
||||
await provider._detectNetwork();
|
||||
const balance = await provider.getBalance(address);
|
||||
console.log("--", networkName, "-- 📡");
|
||||
console.log(" balance:", +ethers.formatEther(balance));
|
||||
console.log(" nonce:", +(await provider.getTransactionCount(address)));
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
console.log("Can't connect to network", networkName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
31
packages/hardhat/scripts/revealPK.ts
Normal file
31
packages/hardhat/scripts/revealPK.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import * as dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import { Wallet } from "ethers";
|
||||
import password from "@inquirer/password";
|
||||
|
||||
async function main() {
|
||||
const encryptedKey = process.env.DEPLOYER_PRIVATE_KEY_ENCRYPTED;
|
||||
|
||||
if (!encryptedKey) {
|
||||
console.log("🚫️ You don't have a deployer account. Run `yarn generate` or `yarn account:import` first");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("👀 This will reveal your private key on the console.\n");
|
||||
|
||||
const pass = await password({ message: "Enter your password to decrypt the private key:" });
|
||||
let wallet: Wallet;
|
||||
try {
|
||||
wallet = (await Wallet.fromEncryptedJson(encryptedKey, pass)) as Wallet;
|
||||
} catch {
|
||||
console.log("❌ Failed to decrypt private key. Wrong password?");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("\n🔑 Private key:", wallet.privateKey);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
58
packages/hardhat/scripts/runHardhatDeployWithPK.ts
Normal file
58
packages/hardhat/scripts/runHardhatDeployWithPK.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import * as dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import { Wallet } from "ethers";
|
||||
import password from "@inquirer/password";
|
||||
import { spawn } from "child_process";
|
||||
import { config } from "hardhat";
|
||||
|
||||
/**
|
||||
* Unencrypts the private key and runs the hardhat deploy command
|
||||
*/
|
||||
async function main() {
|
||||
const networkIndex = process.argv.indexOf("--network");
|
||||
const networkName = networkIndex !== -1 ? process.argv[networkIndex + 1] : config.defaultNetwork;
|
||||
|
||||
if (networkName === "localhost" || networkName === "hardhat") {
|
||||
// Deploy command on the localhost network
|
||||
const hardhat = spawn("hardhat", ["deploy", ...process.argv.slice(2)], {
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
|
||||
hardhat.on("exit", code => {
|
||||
process.exit(code || 0);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const encryptedKey = process.env.DEPLOYER_PRIVATE_KEY_ENCRYPTED;
|
||||
|
||||
if (!encryptedKey) {
|
||||
console.log("🚫️ You don't have a deployer account. Run `yarn generate` or `yarn account:import` first");
|
||||
return;
|
||||
}
|
||||
|
||||
const pass = await password({ message: "Enter password to decrypt private key:" });
|
||||
|
||||
try {
|
||||
const wallet = await Wallet.fromEncryptedJson(encryptedKey, pass);
|
||||
process.env.__RUNTIME_DEPLOYER_PRIVATE_KEY = wallet.privateKey;
|
||||
|
||||
const hardhat = spawn("hardhat", ["deploy", ...process.argv.slice(2)], {
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
|
||||
hardhat.on("exit", code => {
|
||||
process.exit(code || 0);
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
console.error("Failed to decrypt private key. Wrong password?");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
2
packages/hardhat/test/.gitkeep
Normal file
2
packages/hardhat/test/.gitkeep
Normal file
@@ -0,0 +1,2 @@
|
||||
# Write tests for your smart contract in this directory
|
||||
# Example: YourContract.ts
|
||||
368
packages/hardhat/test/CrowdFund.ts
Normal file
368
packages/hardhat/test/CrowdFund.ts
Normal file
@@ -0,0 +1,368 @@
|
||||
//
|
||||
// This script executes when you run 'yarn test'
|
||||
//
|
||||
import { ethers, network } from "hardhat";
|
||||
import { expect } from "chai";
|
||||
import { FundingRecipient, CrowdFund } from "../typechain-types";
|
||||
|
||||
describe("🚩 Challenge: 📣 Crowdfunding App", function () {
|
||||
let fundingRecipient: FundingRecipient;
|
||||
let crowdFundContract: CrowdFund;
|
||||
|
||||
describe("CrowdFund", function () {
|
||||
const contractAddress = process.env.CONTRACT_ADDRESS;
|
||||
|
||||
let contractArtifact: string;
|
||||
if (contractAddress) {
|
||||
// For the autograder.
|
||||
contractArtifact = `contracts/download-${contractAddress}.sol:CrowdFund`;
|
||||
} else {
|
||||
contractArtifact = "contracts/CrowdFund.sol:CrowdFund";
|
||||
}
|
||||
|
||||
const deployContracts = async () => {
|
||||
const FundingRecipientFactory = await ethers.getContractFactory("FundingRecipient");
|
||||
fundingRecipient = (await FundingRecipientFactory.deploy()) as FundingRecipient;
|
||||
|
||||
const CrowdFundFactory = await ethers.getContractFactory(contractArtifact);
|
||||
crowdFundContract = (await CrowdFundFactory.deploy(await fundingRecipient.getAddress())) as CrowdFund;
|
||||
};
|
||||
|
||||
describe("Checkpoint 1: 🤝 Contributing 💵", function () {
|
||||
beforeEach(async function () {
|
||||
await deployContracts();
|
||||
});
|
||||
|
||||
const getContributionEventsFromReceipt = (receipt: any) => {
|
||||
// We avoid chai matchers like `.to.emit` so this works in minimal environments.
|
||||
const parsed = receipt.logs
|
||||
.map((log: any) => {
|
||||
try {
|
||||
return crowdFundContract.interface.parseLog(log);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
return parsed.filter((p: any) => p.name === "Contribution");
|
||||
};
|
||||
|
||||
it("Checkpoint1: balances should go up when you contribute()", async function () {
|
||||
const [owner] = await ethers.getSigners();
|
||||
|
||||
const startingBalance = await crowdFundContract.balances(owner.address);
|
||||
|
||||
const amount = ethers.parseEther("0.001");
|
||||
const contributeTx = await crowdFundContract.contribute({ value: amount });
|
||||
const receipt = await contributeTx.wait();
|
||||
expect(receipt?.status).to.equal(1);
|
||||
|
||||
const newBalance = await crowdFundContract.balances(owner.address);
|
||||
expect(newBalance).to.equal(startingBalance + amount);
|
||||
});
|
||||
|
||||
it("Checkpoint1: should emit a Contribution event with contributor + amount", async function () {
|
||||
const [owner] = await ethers.getSigners();
|
||||
|
||||
const amount = ethers.parseEther("0.001");
|
||||
const tx = await crowdFundContract.contribute({ value: amount });
|
||||
const receipt = await tx.wait();
|
||||
expect(receipt?.status).to.equal(1);
|
||||
|
||||
const contributionEvents = getContributionEventsFromReceipt(receipt);
|
||||
expect(contributionEvents.length).to.equal(1);
|
||||
|
||||
const evt = contributionEvents[0];
|
||||
expect(evt.args[0]).to.equal(owner.address);
|
||||
expect(evt.args[1]).to.equal(amount);
|
||||
});
|
||||
|
||||
it("Checkpoint1: should accumulate multiple contributions from the same user", async function () {
|
||||
const [owner] = await ethers.getSigners();
|
||||
|
||||
const starting = await crowdFundContract.balances(owner.address);
|
||||
|
||||
const a1 = ethers.parseEther("0.001");
|
||||
const a2 = ethers.parseEther("0.002");
|
||||
await (await crowdFundContract.contribute({ value: a1 })).wait();
|
||||
await (await crowdFundContract.contribute({ value: a2 })).wait();
|
||||
|
||||
const ending = await crowdFundContract.balances(owner.address);
|
||||
expect(ending).to.equal(starting + a1 + a2);
|
||||
});
|
||||
|
||||
it("Checkpoint1: should track balances independently per contributor", async function () {
|
||||
const [owner, secondAccount] = await ethers.getSigners();
|
||||
|
||||
const a1 = ethers.parseEther("0.001");
|
||||
const a2 = ethers.parseEther("0.002");
|
||||
|
||||
await (await crowdFundContract.connect(owner).contribute({ value: a1 })).wait();
|
||||
await (await crowdFundContract.connect(secondAccount).contribute({ value: a2 })).wait();
|
||||
|
||||
expect(await crowdFundContract.balances(owner.address)).to.equal(a1);
|
||||
expect(await crowdFundContract.balances(secondAccount.address)).to.equal(a2);
|
||||
});
|
||||
|
||||
it("Checkpoint1: contract ETH balance should increase when someone contributes", async function () {
|
||||
const startContractBal = await ethers.provider.getBalance(await crowdFundContract.getAddress());
|
||||
|
||||
const amount = ethers.parseEther("0.001");
|
||||
await (await crowdFundContract.contribute({ value: amount })).wait();
|
||||
|
||||
const endContractBal = await ethers.provider.getBalance(await crowdFundContract.getAddress());
|
||||
expect(endContractBal).to.equal(startContractBal + amount);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Checkpoint 2: 📤 Withdrawing Funds", function () {
|
||||
beforeEach(async function () {
|
||||
await deployContracts();
|
||||
});
|
||||
|
||||
const setOpenToWithdrawTrue = async () => {
|
||||
// Checkpoint 2 doesn't include a setter for `openToWithdraw`, so we toggle it directly in storage.
|
||||
//
|
||||
// Don't worry if you don't understand the wizardry that happens here.
|
||||
//
|
||||
// Important: `openToWithdraw` might be:
|
||||
// - in its own storage slot (bool uses the least-significant byte of the slot), OR
|
||||
// - packed into an existing slot (e.g. declared right after an `address`, so it shares slot 0),
|
||||
// in which case flipping `0x...01` for the whole slot DOES NOT necessarily flip the bool.
|
||||
//
|
||||
// So we probe (slot, byteOffset) by mutating ONE byte at a time and checking which mutation makes
|
||||
// `openToWithdraw()` return true. This effectively answers: "which storage location impacts this variable?"
|
||||
const target = await crowdFundContract.getAddress();
|
||||
|
||||
// If it's already open, nothing to do.
|
||||
if (await crowdFundContract.openToWithdraw()) return;
|
||||
|
||||
const writeStorageAt = async (slot: bigint, value: string) => {
|
||||
const slotHex = ethers.zeroPadValue(ethers.toBeHex(slot), 32);
|
||||
await network.provider.send("hardhat_setStorageAt", [target, slotHex, value]);
|
||||
};
|
||||
|
||||
const hexToBytes32 = (hex: string) => {
|
||||
// Expect 0x-prefixed 32-byte hex from `getStorage`.
|
||||
const normalized = hex.startsWith("0x") ? hex.slice(2) : hex;
|
||||
const padded = normalized.padStart(64, "0");
|
||||
const out: number[] = [];
|
||||
for (let i = 0; i < 64; i += 2) out.push(parseInt(padded.slice(i, i + 2), 16));
|
||||
if (out.length !== 32) throw new Error("Expected 32 bytes");
|
||||
return out;
|
||||
};
|
||||
|
||||
const bytes32ToHex = (bytes: number[]) => {
|
||||
if (bytes.length !== 32) throw new Error("Expected 32 bytes");
|
||||
const hex = bytes.map(b => b.toString(16).padStart(2, "0")).join("");
|
||||
return "0x" + hex;
|
||||
};
|
||||
|
||||
// Probe a reasonable number of slots so re-ordering / adding variables doesn't break tests.
|
||||
// (Mapping `balances` lives at a slot too, but since we always revert unsuccessful writes,
|
||||
// it's safe to probe it.)
|
||||
for (let s = 0n; s <= 20n; s++) {
|
||||
const original = await ethers.provider.getStorage(target, s);
|
||||
const originalBytes = hexToBytes32(original);
|
||||
|
||||
try {
|
||||
// Try flipping each byte to 0x01 (leaving all other bytes unchanged).
|
||||
// Storage words are represented big-endian in hex, but since we probe ALL 32 bytes,
|
||||
// we don't need to reason about endianness/packing direction here.
|
||||
for (let byteIdx = 0; byteIdx < 32; byteIdx++) {
|
||||
const mutated = [...originalBytes];
|
||||
mutated[byteIdx] = 0x01;
|
||||
await writeStorageAt(s, bytes32ToHex(mutated));
|
||||
|
||||
const isOpen = await crowdFundContract.openToWithdraw();
|
||||
if (isOpen === true) {
|
||||
// Found the (slot, byteIdx) that impacts `openToWithdraw`.
|
||||
// We intentionally keep this storage mutation for the test.
|
||||
return;
|
||||
}
|
||||
|
||||
// Not the right byte → restore original before continuing.
|
||||
await writeStorageAt(s, original);
|
||||
}
|
||||
} catch {
|
||||
// If the function doesn't exist yet, nothing to do here.
|
||||
}
|
||||
// Ensure we leave storage exactly as we found it before moving to the next slot.
|
||||
await writeStorageAt(s, original);
|
||||
}
|
||||
|
||||
throw new Error("Could not locate `openToWithdraw` storage slot to toggle it for tests.");
|
||||
};
|
||||
|
||||
it("Checkpoint2: withdraw should revert with NotOpenToWithdraw() when withdrawals are not open", async function () {
|
||||
await expect(crowdFundContract.withdraw()).to.be.revertedWithCustomError(
|
||||
crowdFundContract,
|
||||
"NotOpenToWithdraw",
|
||||
);
|
||||
});
|
||||
|
||||
it("Checkpoint2: withdraw should send your full balance and zero-out your recorded balance", async function () {
|
||||
const [, contributor] = await ethers.getSigners();
|
||||
|
||||
const amount = ethers.parseEther("0.001");
|
||||
await (await crowdFundContract.connect(contributor).contribute({ value: amount })).wait();
|
||||
expect(await crowdFundContract.balances(contributor.address)).to.equal(amount);
|
||||
|
||||
// We modify storage directly to set openToWithdraw to true since we have not implemented the setter yet.
|
||||
await setOpenToWithdrawTrue();
|
||||
|
||||
const startingBalance = await ethers.provider.getBalance(contributor.address);
|
||||
const withdrawTx = await crowdFundContract.connect(contributor).withdraw();
|
||||
|
||||
const tx = await ethers.provider.getTransaction(withdrawTx.hash);
|
||||
if (!tx) throw new Error("Cannot resolve transaction");
|
||||
const receipt = await ethers.provider.getTransactionReceipt(withdrawTx.hash);
|
||||
if (!receipt) throw new Error("Cannot resolve receipt");
|
||||
|
||||
const gasCost = tx.gasPrice * receipt.gasUsed;
|
||||
const endingBalance = await ethers.provider.getBalance(contributor.address);
|
||||
|
||||
expect(endingBalance).to.equal(startingBalance + amount - gasCost);
|
||||
expect(await crowdFundContract.balances(contributor.address)).to.equal(0n);
|
||||
});
|
||||
|
||||
it("Checkpoint2: withdrawing twice should not let you withdraw more than you contributed", async function () {
|
||||
const [, contributor] = await ethers.getSigners();
|
||||
|
||||
const amount = ethers.parseEther("0.001");
|
||||
await (await crowdFundContract.connect(contributor).contribute({ value: amount })).wait();
|
||||
|
||||
// We modify storage directly to set openToWithdraw to true since we have not implemented the setter yet.
|
||||
await setOpenToWithdrawTrue();
|
||||
|
||||
await (await crowdFundContract.connect(contributor).withdraw()).wait();
|
||||
const balanceAfterFirst = await ethers.provider.getBalance(contributor.address);
|
||||
|
||||
// Second withdraw should refund 0; only gas should change wallet balance.
|
||||
const secondTx = await crowdFundContract.connect(contributor).withdraw();
|
||||
const tx = await ethers.provider.getTransaction(secondTx.hash);
|
||||
if (!tx) throw new Error("Cannot resolve transaction");
|
||||
const receipt = await ethers.provider.getTransactionReceipt(secondTx.hash);
|
||||
if (!receipt) throw new Error("Cannot resolve receipt");
|
||||
const gasCost = tx.gasPrice * receipt.gasUsed;
|
||||
|
||||
const balanceAfterSecond = await ethers.provider.getBalance(contributor.address);
|
||||
expect(balanceAfterSecond).to.equal(balanceAfterFirst - gasCost);
|
||||
expect(await crowdFundContract.balances(contributor.address)).to.equal(0n);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Checkpoint 3: 🔬 State Machine / Timing ⏱", function () {
|
||||
beforeEach(async function () {
|
||||
await deployContracts();
|
||||
});
|
||||
|
||||
it("Checkpoint3: execute() should revert with TooEarly() if called before the deadline", async function () {
|
||||
await expect(crowdFundContract.execute()).to.be.revertedWithCustomError(crowdFundContract, "TooEarly");
|
||||
});
|
||||
|
||||
it("Checkpoint3: timeLeft should decrease as time moves forward (until it hits 0)", async function () {
|
||||
const t1 = await crowdFundContract.timeLeft();
|
||||
expect(Number(t1)).to.be.greaterThan(0);
|
||||
|
||||
await network.provider.send("evm_increaseTime", [5]);
|
||||
await network.provider.send("evm_mine");
|
||||
|
||||
const t2 = await crowdFundContract.timeLeft();
|
||||
expect(Number(t2)).to.be.lessThan(Number(t1));
|
||||
expect(Number(t2)).to.be.greaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it("Checkpoint3: if enough is contributed and time has passed, execute() should complete()", async function () {
|
||||
const timeLeft1 = await crowdFundContract.timeLeft();
|
||||
expect(
|
||||
Number(timeLeft1),
|
||||
"timeLeft not greater than 0. Did you implement the timeLeft() function correctly?",
|
||||
).to.greaterThan(0);
|
||||
|
||||
const amount = ethers.parseEther("1");
|
||||
await crowdFundContract.contribute({ value: amount });
|
||||
|
||||
await network.provider.send("evm_increaseTime", [72 * 3600]);
|
||||
await network.provider.send("evm_mine");
|
||||
|
||||
const timeLeft2 = await crowdFundContract.timeLeft();
|
||||
expect(
|
||||
Number(timeLeft2),
|
||||
"timeLeft not equal to 0. Did you implement the timeLeft() function correctly?",
|
||||
).to.equal(0);
|
||||
|
||||
const startRecipientBal = await ethers.provider.getBalance(await fundingRecipient.getAddress());
|
||||
const startContractBal = await ethers.provider.getBalance(await crowdFundContract.getAddress());
|
||||
|
||||
await crowdFundContract.execute();
|
||||
|
||||
const result = await fundingRecipient.completed();
|
||||
expect(result).to.equal(true);
|
||||
|
||||
const endRecipientBal = await ethers.provider.getBalance(await fundingRecipient.getAddress());
|
||||
const endContractBal = await ethers.provider.getBalance(await crowdFundContract.getAddress());
|
||||
|
||||
// Funds should have moved into the FundingRecipient via `complete{value: ...}()`
|
||||
expect(endRecipientBal).to.equal(startRecipientBal + startContractBal);
|
||||
expect(endContractBal).to.equal(0n);
|
||||
});
|
||||
|
||||
it("Checkpoint3: if not enough is contributed and time has passed, execute() should enable withdraw", async function () {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [owner, secondAccount] = await ethers.getSigners();
|
||||
|
||||
await crowdFundContract.connect(secondAccount).contribute({
|
||||
value: ethers.parseEther("0.001"),
|
||||
});
|
||||
|
||||
await network.provider.send("evm_increaseTime", [72 * 3600]);
|
||||
await network.provider.send("evm_mine");
|
||||
|
||||
await crowdFundContract.execute();
|
||||
|
||||
const result = await fundingRecipient.completed();
|
||||
expect(result).to.equal(false);
|
||||
|
||||
// If `openToWithdraw` is implemented (Checkpoint 2/3), it should now be true.
|
||||
// We use `as any` so earlier checkpoints can still compile this test file.
|
||||
if ((crowdFundContract as any).openToWithdraw) {
|
||||
const isOpen = await (crowdFundContract as any).openToWithdraw();
|
||||
expect(isOpen).to.equal(true);
|
||||
}
|
||||
|
||||
const startingBalance = await ethers.provider.getBalance(secondAccount.address);
|
||||
const withdrawTx = await crowdFundContract.connect(secondAccount).withdraw();
|
||||
|
||||
const tx = await ethers.provider.getTransaction(withdrawTx.hash);
|
||||
if (!tx) throw new Error("Cannot resolve transaction");
|
||||
|
||||
const receipt = await ethers.provider.getTransactionReceipt(withdrawTx.hash);
|
||||
if (!receipt) throw new Error("Cannot resolve receipt");
|
||||
|
||||
const gasCost = tx.gasPrice * receipt.gasUsed;
|
||||
const endingBalance = await ethers.provider.getBalance(secondAccount.address);
|
||||
|
||||
expect(endingBalance).to.equal(startingBalance + ethers.parseEther("0.001") - gasCost);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Checkpoint 4: 💵 Receive Function / UX 🙎", function () {
|
||||
beforeEach(async function () {
|
||||
await deployContracts();
|
||||
});
|
||||
|
||||
it("Checkpoint4: sending ETH directly to the contract should behave like contribute()", async function () {
|
||||
const [owner] = await ethers.getSigners();
|
||||
const startingBalance = await crowdFundContract.balances(owner.address);
|
||||
|
||||
const amount = ethers.parseEther("0.001");
|
||||
const sendTx = await owner.sendTransaction({ to: await crowdFundContract.getAddress(), value: amount });
|
||||
await sendTx.wait();
|
||||
|
||||
const newBalance = await crowdFundContract.balances(owner.address);
|
||||
expect(newBalance).to.equal(startingBalance + amount);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
11
packages/hardhat/tsconfig.json
Normal file
11
packages/hardhat/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user