Initial commit with 🏗️ create-eth @ 2.0.4

This commit is contained in:
han
2026-01-12 10:42:14 +07:00
commit fd53a8187a
126 changed files with 27771 additions and 0 deletions

View 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
View 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

View 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
}
}
]
}

View File

@@ -0,0 +1,43 @@
pragma solidity 0.8.20; //Do not change the solidity version as it negatively impacts submission grading
// SPDX-License-Identifier: MIT
import "@openzeppelin/contracts/access/Ownable.sol";
import "./YourToken.sol";
contract Vendor is Ownable {
/////////////////
/// Errors //////
/////////////////
// Errors go here...
//////////////////////
/// State Variables //
//////////////////////
YourToken public immutable yourToken;
////////////////
/// Events /////
////////////////
// Events go here...
///////////////////
/// Constructor ///
///////////////////
constructor(address tokenAddress) Ownable(msg.sender) {
yourToken = YourToken(tokenAddress);
}
///////////////////
/// Functions /////
///////////////////
function buyTokens() external payable {}
function withdraw() public onlyOwner {}
function sellTokens(uint256 amount) public {}
}

View File

@@ -0,0 +1,10 @@
pragma solidity 0.8.20; //Do not change the solidity version as it negatively impacts submission grading
// SPDX-License-Identifier: MIT
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
// learn more: https://docs.openzeppelin.com/contracts/5.x/erc20
contract YourToken is ERC20 {
constructor() ERC20("Gold", "GLD") {}
}

View File

@@ -0,0 +1,43 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
// import { Contract } from "ethers";
/**
* Deploys a contract named "YourToken" using the deployer account and
* constructor arguments set to the deployer address
*
* @param hre HardhatRuntimeEnvironment object.
*/
const deployYourToken: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
/*
On localhost, the deployer account is the one that comes with Hardhat, which is already funded.
When deploying to live networks (e.g `yarn deploy --network sepolia`), the deployer account
should have sufficient balance to pay for the gas fees for contract creation.
You can generate a random account with `yarn generate` which will fill DEPLOYER_PRIVATE_KEY
with a random private key in the .env file (then used on hardhat.config.ts)
You can run the `yarn account` command to check your balance in every network.
*/
const { deployer } = await hre.getNamedAccounts();
const { deploy } = hre.deployments;
await deploy("YourToken", {
from: deployer,
// Contract constructor arguments
args: [],
log: true,
// autoMine: can be passed to the deploy function to make the deployment process faster on local networks by
// automatically mining the contract deployment transaction. There is no effect on live networks.
autoMine: true,
});
// Get the deployed contract
// const yourToken = await hre.ethers.getContract<Contract>("YourToken", deployer);
};
export default deployYourToken;
// Tags are useful if you have multiple deploy files and only want to run one of them.
// e.g. yarn deploy --tags YourToken
deployYourToken.tags = ["YourToken"];

View File

@@ -0,0 +1,75 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import { Contract } from "ethers";
/**
* Deploys a contract named "Vendor" using the deployer account and
* constructor arguments set to the deployer address
*
* @param hre HardhatRuntimeEnvironment object.
*/
const deployVendor: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
/*
On localhost, the deployer account is the one that comes with Hardhat, which is already funded.
When deploying to live networks (e.g `yarn deploy --network sepolia`), the deployer account
should have sufficient balance to pay for the gas fees for contract creation.
You can generate a random account with `yarn generate` which will fill DEPLOYER_PRIVATE_KEY
with a random private key in the .env file (then used on hardhat.config.ts)
You can run the `yarn account` command to check your balance in every network.
*/
const { deployer } = await hre.getNamedAccounts();
const { deploy } = hre.deployments;
const yourToken = await hre.ethers.getContract<Contract>("YourToken", deployer);
/**
* Student TODO:
* - Put the address youre using in the frontend here (leave "" to default to the deployer)
*/
const FRONTEND_ADDRESS: string = "";
/**
* Mode switch:
* - If true: deploy Vendor and seed it with the token balance
* - If false: send tokens to your frontend address (or deployer if unset)
*/
const SEND_TOKENS_TO_VENDOR = false; // Don't switch until Checkpoint 2!
const recipientAddress = FRONTEND_ADDRESS && FRONTEND_ADDRESS.trim().length > 0 ? FRONTEND_ADDRESS : deployer;
if (!SEND_TOKENS_TO_VENDOR) {
// Send the entire initial supply to the wallet you use in the UI (useful when deployer != UI wallet).
// If FRONTEND_ADDRESS is "", this defaults to the deployer (no-op transfer).
if (recipientAddress != deployer) {
await yourToken.transfer(recipientAddress, hre.ethers.parseEther("1000"));
}
return;
} else {
// Deploy Vendor
const yourTokenAddress = await yourToken.getAddress();
await deploy("Vendor", {
from: deployer,
// Contract constructor arguments
args: [yourTokenAddress],
log: true,
// autoMine: can be passed to the deploy function to make the deployment process faster on local networks by
// automatically mining the contract deployment transaction. There is no effect on live networks.
autoMine: true,
});
const vendor = await hre.ethers.getContract<Contract>("Vendor", deployer);
const vendorAddress = await vendor.getAddress();
// Transfer tokens to Vendor (seed inventory)
await yourToken.transfer(vendorAddress, hre.ethers.parseEther("1000"));
// Make the UI wallet the owner (for withdraw(), etc). Defaults to deployer if unset.
await vendor.transferOwnership(recipientAddress);
}
};
export default deployVendor;
// Tags are useful if you have multiple deploy files and only want to run one of them.
// e.g. yarn deploy --tags Vendor
deployVendor.tags = ["Vendor"];

View 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",
},
],
},
},
]);

View File

@@ -0,0 +1,152 @@
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: {
// 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",
},
},
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],
},
},
// 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;

View 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"
}
}

View 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;
});

View 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;

View 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;
});

View 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;
});

View 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;
});

View 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);

View File

@@ -0,0 +1,2 @@
# Write tests for your smart contract in this directory
# Example: YourContract.ts

View File

@@ -0,0 +1,241 @@
//
// this script executes when you run 'yarn harhat:test'
//
import hre from "hardhat";
import { expect } from "chai";
import { impersonateAccount, stopImpersonatingAccount } from "@nomicfoundation/hardhat-network-helpers";
import { Vendor, YourToken } from "../typechain-types";
const { ethers } = hre;
describe("🚩 Challenge: 🏵 Token Vendor 🤖", function () {
// NOTE: The README expects tests grouped by checkpoint so you can run:
// yarn test --grep "Checkpoint1"
// yarn test --grep "Checkpoint2"
// yarn test --grep "Checkpoint3"
// yarn test --grep "Checkpoint4"
const contractAddress = process.env.CONTRACT_ADDRESS;
const getYourTokenArtifact = () => {
if (contractAddress) return "contracts/YourTokenAutograder.sol:YourToken";
return "contracts/YourToken.sol:YourToken";
};
const getVendorArtifact = () => {
if (process.env.CONTRACT_ADDRESS) return `contracts/download-${process.env.CONTRACT_ADDRESS}.sol:Vendor`;
return "contracts/Vendor.sol:Vendor";
};
const TOKENS_PER_ETH = 100n;
const INITIAL_SUPPLY = ethers.parseEther("1000");
async function deployYourTokenFixture() {
const [deployer, user] = await ethers.getSigners();
const YourTokenFactory = await ethers.getContractFactory(getYourTokenArtifact());
const yourToken = (await YourTokenFactory.deploy()) as YourToken;
await yourToken.waitForDeployment();
return { deployer, user, yourToken, yourTokenAddress: await yourToken.getAddress() };
}
async function deployVendorFixture(yourTokenAddress: string) {
const VendorFactory = await ethers.getContractFactory(getVendorArtifact());
const vendor = (await VendorFactory.deploy(yourTokenAddress)) as Vendor;
await vendor.waitForDeployment();
return { vendor, vendorAddress: await vendor.getAddress() };
}
describe("Checkpoint1: 🏵 Your Token (ERC20 mint + transfer)", function () {
it("Checkpoint1: mints exactly 1000 tokens (18 decimals) to the deployer", async function () {
const { deployer, yourToken } = await deployYourTokenFixture();
const totalSupply = await yourToken.totalSupply();
expect(totalSupply).to.equal(INITIAL_SUPPLY);
const deployerBalance = await yourToken.balanceOf(deployer.address);
expect(deployerBalance).to.equal(INITIAL_SUPPLY);
});
it("Checkpoint1: can transfer tokens and balanceOf updates correctly", async function () {
const { deployer, user, yourToken } = await deployYourTokenFixture();
const amount = ethers.parseEther("10");
await expect(yourToken.transfer(user.address, amount)).to.not.be.reverted;
expect(await yourToken.balanceOf(user.address)).to.equal(amount);
expect(await yourToken.balanceOf(deployer.address)).to.equal(INITIAL_SUPPLY - amount);
});
});
describe("Checkpoint2: ⚖️ Vendor buyTokens()", function () {
it("Checkpoint2: tokensPerEth constant is 100", async function () {
const { yourTokenAddress } = await deployYourTokenFixture();
const { vendor } = await deployVendorFixture(yourTokenAddress);
expect(await vendor.tokensPerEth()).to.equal(TOKENS_PER_ETH);
});
it("Checkpoint2: buyTokens reverts on 0 ETH with InvalidEthAmount", async function () {
const { yourTokenAddress } = await deployYourTokenFixture();
const { vendor } = await deployVendorFixture(yourTokenAddress);
await expect(vendor.buyTokens({ value: 0 })).to.be.revertedWithCustomError(vendor, "InvalidEthAmount");
});
it("Checkpoint2: can buy 10 tokens for 0.1 ETH (and emits BuyTokens)", async function () {
const { user, yourToken, yourTokenAddress } = await deployYourTokenFixture();
const { vendor, vendorAddress } = await deployVendorFixture(yourTokenAddress);
// Seed the Vendor with the full supply so it can sell.
await yourToken.transfer(vendorAddress, INITIAL_SUPPLY);
const ethToSpend = ethers.parseEther("0.1");
const expectedTokens = ethToSpend * TOKENS_PER_ETH; // 0.1 ETH * 100 = 10 tokens (18 decimals)
const startingBalance = await yourToken.balanceOf(user.address);
const tx = vendor.connect(user).buyTokens({ value: ethToSpend });
await expect(tx).to.emit(vendor, "BuyTokens").withArgs(user.address, ethToSpend, expectedTokens);
expect(await yourToken.balanceOf(user.address)).to.equal(startingBalance + expectedTokens);
});
it("Checkpoint2: reverts if Vendor does not have enough tokens (InsufficientVendorTokenBalance)", async function () {
const { yourTokenAddress } = await deployYourTokenFixture();
const { vendor } = await deployVendorFixture(yourTokenAddress);
const ethToSpend = ethers.parseEther("1");
const requiredTokens = ethToSpend * TOKENS_PER_ETH;
await expect(vendor.buyTokens({ value: ethToSpend }))
.to.be.revertedWithCustomError(vendor, "InsufficientVendorTokenBalance")
.withArgs(0, requiredTokens);
});
});
describe("Checkpoint3: 👑 Ownable + withdraw()", function () {
it("Checkpoint3: deployer is the owner", async function () {
const { deployer, yourTokenAddress } = await deployYourTokenFixture();
const { vendor } = await deployVendorFixture(yourTokenAddress);
expect(await vendor.owner()).to.equal(deployer.address);
});
it("Checkpoint3: only owner can withdraw (non-owner is rejected)", async function () {
const { user, yourTokenAddress } = await deployYourTokenFixture();
const { vendor } = await deployVendorFixture(yourTokenAddress);
await expect(vendor.connect(user).withdraw()).to.be.reverted;
});
it("Checkpoint3: withdraw sends all ETH in Vendor to the owner", async function () {
const { deployer, user, yourToken, yourTokenAddress } = await deployYourTokenFixture();
const { vendor, vendorAddress } = await deployVendorFixture(yourTokenAddress);
// Seed Vendor with tokens and buy tokens to fund Vendor with ETH.
await yourToken.transfer(vendorAddress, INITIAL_SUPPLY);
await vendor.connect(user).buyTokens({ value: ethers.parseEther("0.1") });
const vendorEthBefore = await ethers.provider.getBalance(vendorAddress);
expect(vendorEthBefore).to.be.gt(0);
const ownerEthBefore = await ethers.provider.getBalance(deployer.address);
const withdrawTx = await vendor.withdraw();
const receipt = await withdrawTx.wait();
expect(receipt?.status).to.equal(1);
const tx = await ethers.provider.getTransaction(withdrawTx.hash);
if (!tx || !receipt) throw new Error("Cannot resolve withdraw tx/receipt");
const gasPrice = (receipt as any).effectiveGasPrice ?? tx.gasPrice ?? 0n;
const gasCost = (receipt.gasUsed ?? 0n) * gasPrice;
const ownerEthAfter = await ethers.provider.getBalance(deployer.address);
const vendorEthAfter = await ethers.provider.getBalance(vendorAddress);
expect(vendorEthAfter).to.equal(0);
expect(ownerEthAfter).to.equal(ownerEthBefore + vendorEthBefore - gasCost);
});
it("Checkpoint3: withdraw reverts with EthTransferFailed if the owner can't receive ETH", async function () {
const { deployer, user, yourToken, yourTokenAddress } = await deployYourTokenFixture();
const { vendor, vendorAddress } = await deployVendorFixture(yourTokenAddress);
// Seed Vendor with tokens and fund Vendor with ETH.
await yourToken.transfer(vendorAddress, INITIAL_SUPPLY);
await vendor.connect(user).buyTokens({ value: ethers.parseEther("0.1") });
// Make the Vendor the owner of itself. Since Vendor has no receive()/payable fallback,
// sending ETH to it will revert by default, which should trigger EthTransferFailed.
await vendor.connect(deployer).transferOwnership(vendorAddress);
const vendorEthBefore = await ethers.provider.getBalance(vendorAddress);
await impersonateAccount(vendorAddress);
try {
const vendorAsOwner = await ethers.getSigner(vendorAddress);
// Use a simulation call (no gas is paid from vendorAddress), otherwise the tx gas would
// be deducted from vendorAddress and `address(this).balance` would be smaller than vendorEthBefore.
await expect(vendor.connect(vendorAsOwner).withdraw.staticCall())
.to.be.revertedWithCustomError(vendor, "EthTransferFailed")
.withArgs(vendorAddress, vendorEthBefore);
} finally {
await stopImpersonatingAccount(vendorAddress);
}
});
});
describe("Checkpoint4: 🤔 Vendor buyback (sellTokens + approve)", function () {
it("Checkpoint4: sellTokens rejects amount == 0 (InvalidTokenAmount)", async function () {
const { user, yourTokenAddress } = await deployYourTokenFixture();
const { vendor } = await deployVendorFixture(yourTokenAddress);
await expect(vendor.connect(user).sellTokens(0)).to.be.revertedWithCustomError(vendor, "InvalidTokenAmount");
});
it("Checkpoint4: sellTokens reverts if Vendor lacks ETH liquidity (InsufficientVendorEthBalance)", async function () {
const { user, yourToken, yourTokenAddress } = await deployYourTokenFixture();
const { vendor, vendorAddress } = await deployVendorFixture(yourTokenAddress);
// Give the user tokens directly so they can attempt to sell back.
await yourToken.transfer(user.address, ethers.parseEther("10"));
await yourToken.connect(user).approve(vendorAddress, ethers.parseEther("10"));
const amountToSell = ethers.parseEther("10"); // 10 tokens -> expects 0.1 ETH back
const expectedEth = amountToSell / TOKENS_PER_ETH;
await expect(vendor.connect(user).sellTokens(amountToSell))
.to.be.revertedWithCustomError(vendor, "InsufficientVendorEthBalance")
.withArgs(0, expectedEth);
});
it("Checkpoint4: approve + sellTokens returns correct ETH (and emits SellTokens)", async function () {
const { user, yourToken, yourTokenAddress } = await deployYourTokenFixture();
const { vendor, vendorAddress } = await deployVendorFixture(yourTokenAddress);
// Seed vendor with tokens and fund vendor with ETH by buying first.
await yourToken.transfer(vendorAddress, INITIAL_SUPPLY);
await vendor.connect(user).buyTokens({ value: ethers.parseEther("0.1") }); // user receives 10 tokens
const amountToSell = ethers.parseEther("10");
const expectedEth = amountToSell / TOKENS_PER_ETH; // 0.1 ETH
await yourToken.connect(user).approve(vendorAddress, amountToSell);
const userEthBefore = await ethers.provider.getBalance(user.address);
const tx = await vendor.connect(user).sellTokens(amountToSell);
await expect(tx).to.emit(vendor, "SellTokens").withArgs(user.address, amountToSell, expectedEth);
const receipt = await tx.wait();
expect(receipt?.status).to.equal(1);
const userEthAfter = await ethers.provider.getBalance(user.address);
// ethers v6 receipts expose `gasPrice` (effective gas price). Some toolchains expose `effectiveGasPrice`.
const effectiveGasPrice = ((receipt as any)?.gasPrice ?? (receipt as any)?.effectiveGasPrice ?? 0n) as bigint;
const gasCost = (receipt?.gasUsed ?? 0n) * effectiveGasPrice;
// ETH change = +expectedEth - gas
expect(userEthAfter).to.equal(userEthBefore + expectedEth - gasCost);
});
});
});

View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"esModuleInterop": true,
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}