diff --git a/.changeset/backmerge-20-05-26.md b/.changeset/backmerge-20-05-26.md new file mode 100644 index 0000000000..543d9e4790 --- /dev/null +++ b/.changeset/backmerge-20-05-26.md @@ -0,0 +1,7 @@ +--- +"create-eth": patch +--- + +- Deps: next 15 → 16 and minor frontend updates (https://github.com/scaffold-eth/scaffold-eth-2/pull/1280) +- new Alchemy key (https://github.com/scaffold-eth/scaffold-eth-2/pull/1287) +- hardhat v3 migration (https://github.com/scaffold-eth/scaffold-eth-2/pull/1272) diff --git a/.github/workflows/lint-instances.yml b/.github/workflows/lint-instances.yml index 48e2531891..364639aa42 100644 --- a/.github/workflows/lint-instances.yml +++ b/.github/workflows/lint-instances.yml @@ -30,10 +30,10 @@ jobs: git config --global user.name "Dummy User" git config --global user.email "dummy@example.com" - - name: Setup Node.js 20.x + - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: lts/* - name: Install Dependencies run: yarn diff --git a/README.md b/README.md index 3283080a25..e1c4a87da5 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ CLI to create decentralized applications (dapps) using Scaffold-ETH 2. Before you begin, you need to install the following tools: -- [Node (>= v20.18.3)](https://nodejs.org/en/download/) +- [Node (>= v22.10.0)](https://nodejs.org/en/download/) - Yarn ([v1](https://classic.yarnpkg.com/en/docs/install/) or [v2+](https://yarnpkg.com/getting-started/install)) - [Git](https://git-scm.com/downloads) diff --git a/templates/base/.lintstagedrc.js b/templates/base/.lintstagedrc.js index 941bf7888f..67201a9b6b 100644 --- a/templates/base/.lintstagedrc.js +++ b/templates/base/.lintstagedrc.js @@ -1,9 +1,9 @@ const path = require("path"); const buildNextEslintCommand = (filenames) => - `yarn next:lint --fix --file ${filenames + `yarn workspace @se-2/nextjs eslint --fix ${filenames .map((f) => path.relative(path.join("packages", "nextjs"), f)) - .join(" --file ")}`; + .join(" ")}`; const checkTypesNextCommand = () => "yarn next:check-types"; diff --git a/templates/base/packages/nextjs/.prettierrc.js b/templates/base/packages/nextjs/.prettierrc.js index c6d6d73800..ea724950e1 100644 --- a/templates/base/packages/nextjs/.prettierrc.js +++ b/templates/base/packages/nextjs/.prettierrc.js @@ -1,9 +1,9 @@ module.exports = { - "arrowParens": "avoid", - "printWidth": 120, - "tabWidth": 2, - "trailingComma": "all", - "importOrder": ["^react$", "^next/(.*)$", "", "^@heroicons/(.*)$", "^~~/(.*)$"], - "importOrderSortSpecifiers": true, - "plugins": [require.resolve("@trivago/prettier-plugin-sort-imports")], -} + arrowParens: "avoid", + printWidth: 120, + tabWidth: 2, + trailingComma: "all", + importOrder: ["^react$", "^next/(.*)$", "", "^@heroicons/(.*)$", "^~~/(.*)$"], + importOrderSortSpecifiers: true, + plugins: [require.resolve("@trivago/prettier-plugin-sort-imports")], +}; diff --git a/templates/base/packages/nextjs/app/blockexplorer/address/[address]/page.tsx.template.mjs b/templates/base/packages/nextjs/app/blockexplorer/address/[address]/page.tsx.template.mjs index d744880a81..3ec16089fc 100644 --- a/templates/base/packages/nextjs/app/blockexplorer/address/[address]/page.tsx.template.mjs +++ b/templates/base/packages/nextjs/app/blockexplorer/address/[address]/page.tsx.template.mjs @@ -50,8 +50,8 @@ ${ return { bytecode, assembly }; }` : `async function fetchByteCodeAndAssembly(buildInfoDirectory: string, contractName: string) { - const contractPath = \`contracts/\${contractName}.sol\`; - const buildInfoFiles = fs.readdirSync(buildInfoDirectory); + const contractPath = \`project/contracts/\${contractName}.sol\`; + const buildInfoFiles = fs.readdirSync(buildInfoDirectory).filter(f => f.endsWith(".output.json")); let bytecode = ""; let assembly = ""; @@ -60,7 +60,7 @@ ${ const buildInfo = JSON.parse(fs.readFileSync(filePath, "utf8")); - if (buildInfo.output.contracts[contractPath]) { + if (buildInfo.output?.contracts?.[contractPath]) { for (const contract in buildInfo.output.contracts[contractPath]) { bytecode = buildInfo.output.contracts[contractPath][contract].evm.bytecode.object; assembly = buildInfo.output.contracts[contractPath][contract].evm.bytecode.opcodes; @@ -86,13 +86,7 @@ const getContractData = async (address: Address) => { } const artifactsDirectory = path.join( - __dirname, - "..", - "..", - "..", - "..", - "..", - "..", + process.cwd(), "..", "${solidityFramework[0]}", "${artifactsDirName[0]}"${isFoundry ? "" : `, diff --git a/templates/base/packages/nextjs/components/ThemeProvider.tsx b/templates/base/packages/nextjs/components/ThemeProvider.tsx index 3a7798f267..7ec6aa8fa5 100644 --- a/templates/base/packages/nextjs/components/ThemeProvider.tsx +++ b/templates/base/packages/nextjs/components/ThemeProvider.tsx @@ -1,8 +1,7 @@ "use client"; import * as React from "react"; -import { ThemeProvider as NextThemesProvider } from "next-themes"; -import { type ThemeProviderProps } from "next-themes/dist/types"; +import { ThemeProvider as NextThemesProvider, type ThemeProviderProps } from "next-themes"; export const ThemeProvider = ({ children, ...props }: ThemeProviderProps) => { return {children}; diff --git a/templates/base/packages/nextjs/components/scaffold-eth/FaucetButton.tsx b/templates/base/packages/nextjs/components/scaffold-eth/FaucetButton.tsx index 350347ddff..4c49ff21a0 100644 --- a/templates/base/packages/nextjs/components/scaffold-eth/FaucetButton.tsx +++ b/templates/base/packages/nextjs/components/scaffold-eth/FaucetButton.tsx @@ -57,7 +57,7 @@ export const FaucetButton = () => { className={ !isBalanceZero ? "ml-1" - : "ml-1 tooltip tooltip-bottom tooltip-primary tooltip-open font-bold before:left-auto before:transform-none before:content-[attr(data-tip)] before:-translate-x-2/5" + : "ml-1 tooltip tooltip-bottom tooltip-primary tooltip-open font-bold before:left-auto before:right-0 before:transform-none before:translate-none before:content-[attr(data-tip)]" } data-tip="Grab funds from faucet" > diff --git a/templates/base/packages/nextjs/eslint.config.mjs b/templates/base/packages/nextjs/eslint.config.mjs index 466f632262..3a337f76ed 100644 --- a/templates/base/packages/nextjs/eslint.config.mjs +++ b/templates/base/packages/nextjs/eslint.config.mjs @@ -1,26 +1,21 @@ -import { FlatCompat } from "@eslint/eslintrc"; +import nextCoreWebVitals from "eslint-config-next/core-web-vitals"; +import nextTypescript from "eslint-config-next/typescript"; +import prettierConfig from "eslint-config-prettier"; import prettierPlugin from "eslint-plugin-prettier"; import { defineConfig } from "eslint/config"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const compat = new FlatCompat({ - baseDirectory: __dirname, -}); export default defineConfig([ + ...nextCoreWebVitals, + ...nextTypescript, + prettierConfig, { plugins: { prettier: prettierPlugin, }, - extends: compat.extends("next/core-web-vitals", "next/typescript", "prettier"), - rules: { "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/ban-ts-comment": "off", - + "react-hooks/set-state-in-effect": "off", "prettier/prettier": [ "warn", { diff --git a/templates/base/packages/nextjs/next-env.d.ts b/templates/base/packages/nextjs/next-env.d.ts index 830fb594ca..c4b7818fbb 100644 --- a/templates/base/packages/nextjs/next-env.d.ts +++ b/templates/base/packages/nextjs/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -/// +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/templates/base/packages/nextjs/next.config.ts.template.mjs b/templates/base/packages/nextjs/next.config.ts.template.mjs index 498d5a94d4..094effe2de 100644 --- a/templates/base/packages/nextjs/next.config.ts.template.mjs +++ b/templates/base/packages/nextjs/next.config.ts.template.mjs @@ -6,10 +6,6 @@ const defaultConfig = { typescript: { ignoreBuildErrors: '$$process.env.NEXT_PUBLIC_IGNORE_BUILD_ERROR === "true"$$', }, - eslint: { - ignoreDuringBuilds: '$$process.env.NEXT_PUBLIC_IGNORE_BUILD_ERROR === "true"$$', - }, - webpack: `$$config => { config.resolve.fallback = { fs: false, net: false, tls: false }; config.externals.push("pino-pretty", "lokijs", "encoding"); return config; }$$`, } const contents = ({ preContent, configOverrides, postConfigContent, finalNextConfigName }) => { diff --git a/templates/base/packages/nextjs/package.json b/templates/base/packages/nextjs/package.json index 8e2a435e79..5d38b7925c 100644 --- a/templates/base/packages/nextjs/package.json +++ b/templates/base/packages/nextjs/package.json @@ -3,11 +3,11 @@ "private": true, "version": "0.1.0", "scripts": { - "build": "next build", + "build": "yarn lint && next build", + "lint": "yarn eslint .", "check-types": "tsc --noEmit --incremental", "dev": "next dev", - "format": "prettier --write . '!(node_modules|.next|contracts)/**/*'", - "lint": "next lint", + "format": "prettier --write . '!(node_modules|.next)/**/*'", "serve": "next start", "start": "next dev", "vercel": "vercel --build-env YARN_ENABLE_IMMUTABLE_INSTALLS=false --build-env ENABLE_EXPERIMENTAL_COREPACK=1 --build-env VERCEL_TELEMETRY_DISABLED=1", @@ -16,47 +16,46 @@ "vercel:login": "vercel login" }, "dependencies": { - "@heroicons/react": "~2.1.5", + "@heroicons/react": "^2.2.0", "@rainbow-me/rainbowkit": "2.2.9", - "@react-native-async-storage/async-storage": "~2.2.0", + "@react-native-async-storage/async-storage": "^2.2.0", "@scaffold-ui/components": "^0.1.10", "@scaffold-ui/debug-contracts": "^0.1.9", "@scaffold-ui/hooks": "^0.1.8", - "@tanstack/react-query": "~5.59.15", - "blo": "~1.2.0", + "@tanstack/react-query": "^5.59.15", + "blo": "^2.0.0", "burner-connector": "0.0.20", - "daisyui": "5.0.9", - "kubo-rpc-client": "~5.0.2", - "next": "~15.2.8", - "next-nprogress-bar": "~2.3.13", - "next-themes": "~0.3.0", - "qrcode.react": "~4.0.1", - "react": "~19.2.3", - "react-dom": "~19.2.3", - "react-hot-toast": "~2.4.0", - "usehooks-ts": "~3.1.0", + "daisyui": "^5.5.19", + "kubo-rpc-client": "^6.1.0", + "next": "^16.2.4", + "next-nprogress-bar": "^2.4.7", + "next-themes": "^0.4.6", + "qrcode.react": "^4.2.0", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "react-hot-toast": "^2.6.0", + "usehooks-ts": "^3.1.1", "viem": "2.39.0", "wagmi": "2.19.5", - "zustand": "~5.0.0" + "zustand": "^5.0.12" }, "devDependencies": { - "@tailwindcss/postcss": "4.0.15", - "@trivago/prettier-plugin-sort-imports": "~4.3.0", - "@types/node": "~18.19.50", - "@types/react": "~19.0.7", + "@tailwindcss/postcss": "^4.2.4", + "@trivago/prettier-plugin-sort-imports": "^6.0.2", + "@types/node": "^18.19.130", + "@types/react": "^19.2.14", "abitype": "1.0.6", - "autoprefixer": "~10.4.20", - "bgipfs": "~0.0.12", - "eslint": "~9.23.0", - "eslint-config-next": "~15.2.3", - "eslint-config-prettier": "~10.1.1", - "eslint-plugin-prettier": "~5.2.4", - "postcss": "~8.4.45", - "prettier": "~3.5.3", - "tailwindcss": "4.1.3", - "type-fest": "~4.26.1", - "typescript": "~5.8.2", - "vercel": "~51.2.1" + "bgipfs": "^0.0.18", + "eslint": "^9.39.0", + "eslint-config-next": "^16.2.4", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.5", + "postcss": "^8.5.10", + "prettier": "^3.8.3", + "tailwindcss": "^4.2.4", + "type-fest": "^5.6.0", + "typescript": "^5.8.2", + "vercel": "~52.0.0" }, "packageManager": "yarn@4.13.0" } diff --git a/templates/base/packages/nextjs/postcss.config.js b/templates/base/packages/nextjs/postcss.config.js index e5640725a9..483f378543 100644 --- a/templates/base/packages/nextjs/postcss.config.js +++ b/templates/base/packages/nextjs/postcss.config.js @@ -1,5 +1,5 @@ module.exports = { plugins: { - '@tailwindcss/postcss': {}, + "@tailwindcss/postcss": {}, }, }; diff --git a/templates/base/packages/nextjs/tsconfig.json.template.mjs b/templates/base/packages/nextjs/tsconfig.json.template.mjs index c2cbcc2e0b..0b4f470038 100644 --- a/templates/base/packages/nextjs/tsconfig.json.template.mjs +++ b/templates/base/packages/nextjs/tsconfig.json.template.mjs @@ -14,7 +14,7 @@ const defaultTsConfig = { "moduleResolution": "Bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "incremental": true, "paths": { "~~/*": ["./*"] @@ -29,7 +29,8 @@ const defaultTsConfig = { "next-env.d.ts", "**/*.ts", "**/*.tsx", - ".next/types/**/*.ts" + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" ], "exclude": [ "node_modules" diff --git a/templates/base/packages/nextjs/utils/scaffold-eth/contract.ts b/templates/base/packages/nextjs/utils/scaffold-eth/contract.ts index bf819035f5..76866177ac 100644 --- a/templates/base/packages/nextjs/utils/scaffold-eth/contract.ts +++ b/templates/base/packages/nextjs/utils/scaffold-eth/contract.ts @@ -12,8 +12,7 @@ import { ExtractAbiFunction, } from "abitype"; import type { ExtractAbiFunctionNames } from "abitype"; -import type { Simplify } from "type-fest"; -import type { MergeDeepRecord } from "type-fest/source/merge-deep"; +import type { MergeDeep, Simplify } from "type-fest"; import { Address, Block, @@ -58,7 +57,7 @@ const deepMergeContracts = , E extends Record ); result[key] = { ...local[key], ...amendedExternal }; } - return result as MergeDeepRecord, AddExternalFlag, { arrayMergeMode: "replace" }>; + return result as MergeDeep, AddExternalFlag, { arrayMergeMode: "replace" }>; }; const contractsData = deepMergeContracts(deployedContractsData, externalContractsData); diff --git a/templates/example-contracts/hardhat/packages/hardhat/deploy/00_deploy_your_contract.ts b/templates/example-contracts/hardhat/packages/hardhat/deploy/00_deploy_your_contract.ts index 6478bd6948..ad5ed13c39 100644 --- a/templates/example-contracts/hardhat/packages/hardhat/deploy/00_deploy_your_contract.ts +++ b/templates/example-contracts/hardhat/packages/hardhat/deploy/00_deploy_your_contract.ts @@ -1,44 +1,40 @@ -import { HardhatRuntimeEnvironment } from "hardhat/types"; -import { DeployFunction } from "hardhat-deploy/types"; -import { Contract } from "ethers"; +import { deployScript } from "../rocketh/deploy.js"; +import * as artifacts from "../generated/artifacts/index.js"; /** * Deploys a contract named "YourContract" using the deployer account and * constructor arguments set to the deployer address * - * @param hre HardhatRuntimeEnvironment object. + * @param env Rocketh environment object. */ -const deployYourContract: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { - /* - On localhost, the deployer account is the one that comes with Hardhat, which is already funded. +export default deployScript( + async env => { + /* + 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. + 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` or `yarn account:import` to import your - existing PK which will fill DEPLOYER_PRIVATE_KEY_ENCRYPTED 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; + You can generate a random account with `yarn generate` or `yarn account:import` to import your + existing PK which will fill DEPLOYER_PRIVATE_KEY_ENCRYPTED 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 } = env.namedAccounts; - await deploy("YourContract", { - from: deployer, - // Contract constructor arguments - args: [deployer], - 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 yourContract = await env.deploy("YourContract", { + account: deployer, + artifact: artifacts.YourContract, + // Contract constructor arguments + args: [deployer], + }); - // Get the deployed contract to interact with it after deploying. - const yourContract = await hre.ethers.getContract("YourContract", deployer); - console.log("👋 Initial greeting:", await yourContract.greeting()); -}; - -export default deployYourContract; - -// Tags are useful if you have multiple deploy files and only want to run one of them. -// e.g. yarn deploy --tags YourContract -deployYourContract.tags = ["YourContract"]; + // Read back from the deployed contract + const greeting = await env.read(yourContract, { functionName: "greeting" }); + console.log("👋 Initial greeting:", greeting); + }, + { + // Tags are useful if you have multiple deploy files and only want to run some of them. + // e.g. yarn deploy --tags YourContract + tags: ["YourContract"], + }, +); diff --git a/templates/example-contracts/hardhat/packages/hardhat/test/YourContract.ts b/templates/example-contracts/hardhat/packages/hardhat/test/YourContract.ts index a44cf04e1d..5a9ffbe60c 100644 --- a/templates/example-contracts/hardhat/packages/hardhat/test/YourContract.ts +++ b/templates/example-contracts/hardhat/packages/hardhat/test/YourContract.ts @@ -1,24 +1,27 @@ import { expect } from "chai"; -import { ethers } from "hardhat"; -import { YourContract } from "../typechain-types"; +import { network } from "hardhat"; +import type { Abi_YourContract } from "../generated/abis/YourContract.js"; +import { loadAndExecuteDeploymentsFromFiles } from "../rocketh/environment.js"; -describe("YourContract", function () { - // We define a fixture to reuse the same setup in every test. +const { provider, networkHelpers, ethers } = await network.create(); - let yourContract: YourContract; - before(async () => { - const [owner] = await ethers.getSigners(); - const yourContractFactory = await ethers.getContractFactory("YourContract"); - yourContract = (await yourContractFactory.deploy(owner.address)) as YourContract; - await yourContract.waitForDeployment(); - }); +// We define a fixture to reuse the same setup in every test. +async function deployFixture() { + const env = await loadAndExecuteDeploymentsFromFiles({ provider }); + const { address, abi } = env.get("YourContract"); + const yourContract = await ethers.getContractAt(abi, address); + return { env, yourContract }; +} +describe("YourContract", function () { describe("Deployment", function () { it("Should have the right message on deploy", async function () { + const { yourContract } = await networkHelpers.loadFixture(deployFixture); expect(await yourContract.greeting()).to.equal("Building Unstoppable Apps!!!"); }); it("Should allow setting a new message", async function () { + const { yourContract } = await networkHelpers.loadFixture(deployFixture); const newGreeting = "Learn Scaffold-ETH 2! :)"; await yourContract.setGreeting(newGreeting); diff --git a/templates/solidity-frameworks/hardhat/package.json b/templates/solidity-frameworks/hardhat/package.json index 1ccb0bb4eb..77836a615c 100644 --- a/templates/solidity-frameworks/hardhat/package.json +++ b/templates/solidity-frameworks/hardhat/package.json @@ -27,6 +27,10 @@ "hardhat:verify": "yarn workspace @se-2/hardhat verify", "lint": "yarn next:lint && yarn hardhat:lint", "test": "yarn hardhat:test", - "verify": "yarn hardhat:verify" + "verify": "yarn hardhat:verify", + "hardhat-verify": "yarn hardhat:hardhat-verify" + }, + "engines": { + "node": ">=22.10.0" } } diff --git a/templates/solidity-frameworks/hardhat/packages/hardhat/.env.example b/templates/solidity-frameworks/hardhat/packages/hardhat/.env.example index df44996988..b377975414 100644 --- a/templates/solidity-frameworks/hardhat/packages/hardhat/.env.example +++ b/templates/solidity-frameworks/hardhat/packages/hardhat/.env.example @@ -7,7 +7,7 @@ # To access the values stored in this .env file you can use: process.env.VARIABLENAME ALCHEMY_API_KEY= -ETHERSCAN_V2_API_KEY= +ETHERSCAN_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= diff --git a/templates/solidity-frameworks/hardhat/packages/hardhat/.gitignore.template.mjs b/templates/solidity-frameworks/hardhat/packages/hardhat/.gitignore.template.mjs index 42e8da425a..31d2b02f46 100644 --- a/templates/solidity-frameworks/hardhat/packages/hardhat/.gitignore.template.mjs +++ b/templates/solidity-frameworks/hardhat/packages/hardhat/.gitignore.template.mjs @@ -11,9 +11,9 @@ node_modules coverage coverage.json -# typechain -typechain -typechain-types +# generated contract types +types +generated # hardhat files cache @@ -23,7 +23,8 @@ artifacts artifacts-zk cache-zk -# deployments +# local deployments +deployments/default deployments/localhost # typescript diff --git a/templates/solidity-frameworks/hardhat/packages/hardhat/eslint.config.mjs b/templates/solidity-frameworks/hardhat/packages/hardhat/eslint.config.mjs index 57edf3e012..a2700a52fd 100644 --- a/templates/solidity-frameworks/hardhat/packages/hardhat/eslint.config.mjs +++ b/templates/solidity-frameworks/hardhat/packages/hardhat/eslint.config.mjs @@ -14,7 +14,16 @@ const compat = new FlatCompat({ }); export default defineConfig([ - globalIgnores(["**/artifacts", "**/cache", "**/contracts", "**/node_modules/", "**/typechain-types", "**/*.json"]), + globalIgnores([ + "**/artifacts", + "**/cache", + "**/contracts", + "**/node_modules/", + "**/typechain-types", + "**/types", + "**/generated", + "**/*.json", + ]), { extends: compat.extends("plugin:@typescript-eslint/recommended", "prettier"), diff --git a/templates/solidity-frameworks/hardhat/packages/hardhat/hardhat.config.ts.template.mjs b/templates/solidity-frameworks/hardhat/packages/hardhat/hardhat.config.ts.template.mjs index de92c48a7a..bce814a3b8 100644 --- a/templates/solidity-frameworks/hardhat/packages/hardhat/hardhat.config.ts.template.mjs +++ b/templates/solidity-frameworks/hardhat/packages/hardhat/hardhat.config.ts.template.mjs @@ -1,9 +1,10 @@ import { withDefaults, stringify, deepMerge } from "../../../../utils.js"; const defaultConfig = { + plugins: '$$[hardhatToolbox, HardhatDeploy]$$', solidity: { - compilers: [ - { + profiles: { + default: { version: "0.8.30", settings: { optimizer: { @@ -12,154 +13,167 @@ const defaultConfig = { }, }, }, - ] + }, + }, + generateTypedArtifacts: { + destinations: [ + { + folder: "./generated", + mode: "typescript", + }, + ], }, - defaultNetwork: "localhost", - namedAccounts: { - deployer: { - default: 0, + verify: { + etherscan: { + apiKey: "$$etherscanApiKey$$", }, }, networks: { + default: { + type: "http", + url: "http://127.0.0.1:8545", + }, hardhat: { + type: "edr-simulated", forking: { url: `https://eth-mainnet.alchemyapi.io/v2/\${providerApiKey}`, enabled: '$$process.env.MAINNET_FORKING_ENABLED === "true"$$', }, }, mainnet: { + type: "http", url: "https://mainnet.rpc.buidlguidl.com", accounts: ["$$deployerPrivateKey$$"], }, sepolia: { + type: "http", 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: { + type: "http", url: `https://opt-mainnet.g.alchemy.com/v2/\${providerApiKey}`, - accounts: ["$$deployerPrivateKey$$"], + accounts: ["$$deployerPrivateKey$$"], }, optimismSepolia: { + type: "http", 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: { + type: "http", url: "https://mainnet.base.org", accounts: ["$$deployerPrivateKey$$"], }, baseSepolia: { + type: "http", url: "https://sepolia.base.org", accounts: ["$$deployerPrivateKey$$"], }, + arbitrum: { + type: "http", + url: `https://arb-mainnet.g.alchemy.com/v2/\${providerApiKey}`, + accounts: ["$$deployerPrivateKey$$"], + }, + arbitrumSepolia: { + type: "http", + url: `https://arb-sepolia.g.alchemy.com/v2/\${providerApiKey}`, + accounts: ["$$deployerPrivateKey$$"], + }, scrollSepolia: { + type: "http", url: "https://sepolia-rpc.scroll.io", accounts: ["$$deployerPrivateKey$$"], }, scroll: { + type: "http", url: "https://rpc.scroll.io", accounts: ["$$deployerPrivateKey$$"], }, celo: { + type: "http", url: "https://forno.celo.org", accounts: ["$$deployerPrivateKey$$"], }, celoSepolia: { + type: "http", url: "https://forno.celo-sepolia.celo-testnet.org/", accounts: ["$$deployerPrivateKey$$"], }, - }, - etherscan: { - apiKey: "$$etherscanApiKey$$", - }, - verify: { - etherscan: { - apiKey: "$$etherscanApiKey$$", + polygon: { + type: "http", + url: `https://polygon-mainnet.g.alchemy.com/v2/\${providerApiKey}`, + accounts: ["$$deployerPrivateKey$$"], + }, + polygonAmoy: { + type: "http", + url: `https://polygon-amoy.g.alchemy.com/v2/\${providerApiKey}`, + accounts: ["$$deployerPrivateKey$$"], + }, + gnosis: { + type: "http", + url: "https://rpc.gnosischain.com", + accounts: ["$$deployerPrivateKey$$"], + }, + chiado: { + type: "http", + url: "https://rpc.chiadochain.net", + accounts: ["$$deployerPrivateKey$$"], + }, + polygonZkEvm: { + type: "http", + url: `https://polygonzkevm-mainnet.g.alchemy.com/v2/\${providerApiKey}`, + accounts: ["$$deployerPrivateKey$$"], + }, + polygonZkEvmCardona: { + type: "http", + url: `https://polygonzkevm-cardona.g.alchemy.com/v2/\${providerApiKey}`, + accounts: ["$$deployerPrivateKey$$"], }, }, - sourcify: { - enabled: false, - }, + tasks: '$$deployTasks$$', }; const contents = ({ preContent, configOverrides }) => { // Merge the default config with any overrides const finalConfig = deepMerge(defaultConfig, configOverrides[0] || {}); - - return `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"; + + return `import "dotenv/config"; +import { defineConfig, overrideTask } from "hardhat/config"; +import hardhatToolbox from "@nomicfoundation/hardhat-toolbox-mocha-ethers"; +import HardhatDeploy from "hardhat-deploy"; +import generateTsAbis from "./scripts/generateTsAbis.js"; ${preContent[0] || ''} -// 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 || "IZYEU2cWBgnFmgiTAgpWD"; // 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 = ${stringify(finalConfig, { - // ".array" is a workaround to get the correct indentation for the property - "solidity.compilers.array.settings.optimizer.runs": "https://docs.soliditylang.org/en/latest/using-the-compiler.html#optimizer-options", - "namedAccounts.deployer.default": "By default, it will take the first Hardhat account as the deployer", - "networks.hardhat": "View the networks that are pre-configured.\nIf the network you are looking for is not here you can add new network settings", - "etherscan": "Configuration for harhdat-verify plugin", - "verify": "Configuration for etherscan-verify from hardhat-deploy plugin" -})}; +// 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 || "IZYEU2cWBgnFmgiTAgpWD"; -// 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 const etherscanApiKey = process.env.ETHERSCAN_API_KEY || "DNXJA8RX2Q3VZ4URQIWP7Z68CJXQZSC6AW"; + +const deployTasks = [ + overrideTask("deploy") + .setInlineAction(async (args, _hre, runSuper) => { + // Run the original deploy task + await runSuper(args); + // Force run the generateTsAbis script + await generateTsAbis(); + }) + .build(), +]; -export default config;`; +export default defineConfig(${stringify(finalConfig, { + "solidity.profiles.default.settings.optimizer.runs": + "https://docs.soliditylang.org/en/latest/using-the-compiler.html#optimizer-options", + "networks.default": + "View the networks that are pre-configured.\nIf the network you are looking for is not here you can add new network settings", + verify: "Configuration for hardhat-verify and rocketh-verify", + })});`; }; export default withDefaults(contents, { diff --git a/templates/solidity-frameworks/hardhat/packages/hardhat/package.json b/templates/solidity-frameworks/hardhat/packages/hardhat/package.json index 3c7df2fc00..4cfc2a2dac 100644 --- a/templates/solidity-frameworks/hardhat/packages/hardhat/package.json +++ b/templates/solidity-frameworks/hardhat/packages/hardhat/package.json @@ -1,63 +1,69 @@ { "name": "@se-2/hardhat", "version": "0.0.1", + "type": "module", "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", + "chain": "hardhat node --network hardhat", "check-types": "tsc --noEmit --incremental", - "compile": "hardhat compile", "clean": "hardhat clean", - "deploy": "ts-node scripts/runHardhatDeployWithPK.ts", + "compile": "hardhat compile", + "deploy": "hardhat compile && tsx scripts/runHardhatDeployWithPK.ts", "flatten": "hardhat flatten", - "fork": "MAINNET_FORKING_ENABLED=true hardhat node --network hardhat --no-deploy", + "fork": "MAINNET_FORKING_ENABLED=true hardhat node --network hardhat", "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" + "test": "hardhat test --network hardhat --gas-stats", + "verify": "tsx scripts/runVerify.ts" }, "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.28.2", - "hardhat-deploy": "^1.0.4", - "hardhat-deploy-ethers": "~0.4.2", - "hardhat-gas-reporter": "~2.2.1", + "@nomicfoundation/hardhat-ethers": "^4", + "@nomicfoundation/hardhat-ethers-chai-matchers": "^3", + "@nomicfoundation/hardhat-ignition": "^3", + "@nomicfoundation/hardhat-ignition-ethers": "^3", + "@nomicfoundation/hardhat-keystore": "^3", + "@nomicfoundation/hardhat-mocha": "^3", + "@nomicfoundation/hardhat-network-helpers": "^3", + "@nomicfoundation/hardhat-toolbox-mocha-ethers": "^3.0.4", + "@nomicfoundation/hardhat-typechain": "^3", + "@nomicfoundation/hardhat-verify": "^3", + "@nomicfoundation/ignition-core": "^3", + "@rocketh/deploy": "^0.19.6", + "@rocketh/node": "^0.19.7", + "@rocketh/read-execute": "^0.19.3", + "@rocketh/signer": "^0.19", + "@rocketh/verifier": "^0.19.4", + "@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": "^5", + "eslint": "^9.23.0", + "eslint-config-prettier": "^10.1.1", + "eslint-plugin-prettier": "^5.2.4", + "ethers": "^6.13.2", + "hardhat": "^3.4.5", + "hardhat-deploy": "^2.0.6", + "mocha": "^11", "prettier": "^3.5.3", - "prettier-plugin-solidity": "~1.4.1", - "solidity-coverage": "~0.8.13", - "ts-node": "~10.9.1", - "typechain": "~8.3.2", + "prettier-plugin-solidity": "^1.4.1", + "rocketh": "^0.19.7", + "tsx": "^4.21.0", "typescript": "^5.8.2" }, "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" + "@openzeppelin/contracts": "^5.0.2", + "dotenv": "^16.4.5", + "envfile": "^7.1.0", + "qrcode": "^1.5.4" } } diff --git a/templates/solidity-frameworks/hardhat/packages/hardhat/rocketh/config.ts b/templates/solidity-frameworks/hardhat/packages/hardhat/rocketh/config.ts new file mode 100644 index 0000000000..7c0b80864b --- /dev/null +++ b/templates/solidity-frameworks/hardhat/packages/hardhat/rocketh/config.ts @@ -0,0 +1,28 @@ +import type { UserConfig } from "rocketh/types"; +import { privateKey } from "@rocketh/signer"; +import * as deployExtension from "@rocketh/deploy"; +import * as readExecuteExtension from "@rocketh/read-execute"; + +export const config = { + accounts: { + deployer: { + default: 0, + }, + }, + data: {}, + signerProtocols: { + privateKey, + }, +} as const satisfies UserConfig; + +const extensions = { + ...readExecuteExtension, + ...deployExtension, +}; +export { extensions }; + +// Type exports for use in deploy.ts and environment.ts +type Extensions = typeof extensions; +type Accounts = typeof config.accounts; +type Data = typeof config.data; +export type { Extensions, Accounts, Data }; diff --git a/templates/solidity-frameworks/hardhat/packages/hardhat/rocketh/deploy.ts b/templates/solidity-frameworks/hardhat/packages/hardhat/rocketh/deploy.ts new file mode 100644 index 0000000000..442cc176a7 --- /dev/null +++ b/templates/solidity-frameworks/hardhat/packages/hardhat/rocketh/deploy.ts @@ -0,0 +1,5 @@ +import { type Accounts, type Data, type Extensions, extensions } from "./config.js"; +import { setupDeployScripts } from "rocketh"; + +const { deployScript } = setupDeployScripts(extensions); +export { deployScript }; diff --git a/templates/solidity-frameworks/hardhat/packages/hardhat/rocketh/environment.ts b/templates/solidity-frameworks/hardhat/packages/hardhat/rocketh/environment.ts new file mode 100644 index 0000000000..f471c3d7c2 --- /dev/null +++ b/templates/solidity-frameworks/hardhat/packages/hardhat/rocketh/environment.ts @@ -0,0 +1,13 @@ +import { type Accounts, type Data, type Extensions, extensions } from "./config.js"; +import { setupEnvironmentFromFiles } from "@rocketh/node"; +import { setupHardhatDeploy } from "hardhat-deploy/helpers"; + +const { loadEnvironmentFromFiles, loadAndExecuteDeploymentsFromFiles } = setupEnvironmentFromFiles< + Extensions, + Accounts, + Data +>(extensions); + +const { loadEnvironmentFromHardhat } = setupHardhatDeploy(extensions); + +export { loadEnvironmentFromFiles, loadAndExecuteDeploymentsFromFiles, loadEnvironmentFromHardhat }; diff --git a/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/generateTsAbis.ts b/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/generateTsAbis.ts index dfe91fed09..c9f19c8d3c 100644 --- a/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/generateTsAbis.ts +++ b/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/generateTsAbis.ts @@ -8,7 +8,6 @@ import * as fs from "fs"; import prettier from "prettier"; -import { DeployFunction } from "hardhat-deploy/types"; const generatedContractComment = ` /** @@ -18,7 +17,23 @@ const generatedContractComment = ` `; const DEPLOYMENTS_DIR = "./deployments"; -const ARTIFACTS_DIR = "./artifacts"; +const BUILD_INFO_DIR = "./artifacts/build-info"; + +function getBuildInfoContracts(): Record> { + if (!fs.existsSync(BUILD_INFO_DIR)) return {}; + const outputFiles = fs.readdirSync(BUILD_INFO_DIR).filter(f => f.endsWith(".output.json")); + const contracts: Record> = {}; + for (const file of outputFiles) { + const data = JSON.parse(fs.readFileSync(`${BUILD_INFO_DIR}/${file}`).toString()); + const compiledContracts = data.output?.contracts || {}; + for (const [sourcePath, contractMap] of Object.entries(compiledContracts) as [string, Record][]) { + contracts[sourcePath] = contractMap; + } + } + return contracts; +} + +const buildInfoContracts = getBuildInfoContracts(); function getDirectories(path: string) { return fs @@ -34,36 +49,37 @@ function getContractNames(path: string) { .map(dirent => dirent.name.split(".")[0]); } +// Resolve a contract by its NAME, not its .sol filename (handles multi-contract +// files and contracts inherited from npm packages). +function getSourcePathForContract(contractName: string) { + return Object.keys(buildInfoContracts).find(sourcePath => buildInfoContracts[sourcePath]?.[contractName]); +} + function getActualSourcesForContract(sources: Record, 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 []; + const sourcePath = getSourcePathForContract(contractName); + const contractContent = sourcePath ? (sources[sourcePath]?.content as string | undefined) : undefined; + if (!contractContent) return []; + + // Anchor to this contract so multi-contract files match the right declaration. + const escapedName = contractName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const match = contractContent.match(new RegExp(`contract\\s+${escapedName}\\s+is\\s+([^{}]+)\\{`)); + if (!match) return []; + + // Parent names, without any constructor args like `ERC20("n", "s")`. + return match[1] + .split(",") + .map(parent => parent.trim().split(/[\s(]/)[0]) + .filter(Boolean); } function getInheritedFunctions(sources: Record, contractName: string) { - const actualSources = getActualSourcesForContract(sources, contractName); + const parentContractNames = getActualSourcesForContract(sources, contractName); const inheritedFunctions = {} as Record; - 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 parentName of parentContractNames) { + const sourcePath = getSourcePathForContract(parentName); + const abi = sourcePath ? buildInfoContracts[sourcePath]?.[parentName]?.abi : undefined; + if (abi) { for (const functionAbi of abi) { if (functionAbi.type === "function") { inheritedFunctions[functionAbi.name] = sourcePath; @@ -84,10 +100,20 @@ function getContractDataFromDeployments() { for (const chainName of chainDirectories) { let chainId; try { - chainId = fs.readFileSync(`${DEPLOYMENTS_DIR}/${chainName}/.chainId`).toString(); + const chainFilePath = `${DEPLOYMENTS_DIR}/${chainName}/.chain`; + const chainIdFilePath = `${DEPLOYMENTS_DIR}/${chainName}/.chainId`; + if (fs.existsSync(chainFilePath)) { + const chainData = JSON.parse(fs.readFileSync(chainFilePath).toString()); + chainId = chainData.chainId; + } else if (fs.existsSync(chainIdFilePath)) { + chainId = fs.readFileSync(chainIdFilePath).toString().trim(); + } else { + console.log(`No chain file found for ${chainName}`); + continue; + } // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { - console.log(`No chainId file found for ${chainName}`); + console.log(`Error reading chain data for ${chainName}`); continue; } @@ -97,7 +123,10 @@ function getContractDataFromDeployments() { 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 }; + + // normalize the blockNumber to number, here we get it as hex + const blockNumber = receipt?.blockNumber != null ? Number(receipt.blockNumber) : undefined; + contracts[contractName] = { address, abi, inheritedFunctions, deployedOnBlock: blockNumber }; } output[chainId] = contracts; } @@ -108,7 +137,7 @@ function getContractDataFromDeployments() { * 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 () { +export default async function generateTsAbis() { const TARGET_DIR = "../nextjs/contracts/"; const allContractsData = getContractDataFromDeployments(); @@ -131,6 +160,12 @@ const generateTsAbis: DeployFunction = async function () { ); console.log(`📝 Updated TypeScript contract definition file on ${TARGET_DIR}deployedContracts.ts`); -}; +} -export default generateTsAbis; +// Run directly when executed as a script (e.g. `hardhat run scripts/generateTsAbis.ts`) +// but not when imported by hardhat.config.ts for the task override +const isDirectRun = + process.argv[1]?.includes("generateTsAbis") || process.argv.some(arg => arg.includes("generateTsAbis")); +if (isDirectRun) { + generateTsAbis(); +} diff --git a/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/listAccount.ts b/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/listAccount.ts index ae1aafd6f5..c2d6f909b0 100644 --- a/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/listAccount.ts +++ b/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/listAccount.ts @@ -33,7 +33,8 @@ async function main() { try { const network = availableNetworks[networkName]; if (!("url" in network)) continue; - const provider = new ethers.JsonRpcProvider(network.url); + const url = await network.url.getUrl(); + const provider = new ethers.JsonRpcProvider(url); await provider._detectNetwork(); const balance = await provider.getBalance(address); console.log("--", networkName, "-- 📡"); diff --git a/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/runHardhatDeployWithPK.ts b/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/runHardhatDeployWithPK.ts index 7628b43137..bd26693b85 100644 --- a/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/runHardhatDeployWithPK.ts +++ b/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/runHardhatDeployWithPK.ts @@ -1,58 +1,50 @@ -import * as dotenv from "dotenv"; -dotenv.config(); +import "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 + * Unencrypts the private key and runs the hardhat deploy command, + * then generates TypeScript ABIs for the frontend. */ 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 networkName = networkIndex !== -1 ? process.argv[networkIndex + 1] : "default"; - const encryptedKey = process.env.DEPLOYER_PRIVATE_KEY_ENCRYPTED; + const isLocalNetwork = networkName === "default" || networkName === "hardhat"; - if (!encryptedKey) { - console.log("đŸšĢī¸ You don't have a deployer account. Run `yarn generate` or `yarn account:import` first"); - return; - } + if (!isLocalNetwork) { + 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:" }); - 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); + try { + const wallet = await Wallet.fromEncryptedJson(encryptedKey, pass); + process.env.__RUNTIME_DEPLOYER_PRIVATE_KEY = wallet.privateKey; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (e) { + console.error("Failed to decrypt private key. Wrong password?"); + process.exit(1); + } } + + // Run hardhat deploy (compilation already handled by the npm script) + const deployArgs = ["deploy", "--no-compile", "--skip-prompts", ...process.argv.slice(2)]; + + const hardhat = spawn("hardhat", deployArgs, { + stdio: "inherit", + env: process.env, + shell: process.platform === "win32", + }); + + hardhat.on("exit", code => { + process.exit(code || 0); + }); } main().catch(console.error); diff --git a/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/runVerify.ts b/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/runVerify.ts new file mode 100644 index 0000000000..bb6afea8b5 --- /dev/null +++ b/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/runVerify.ts @@ -0,0 +1,55 @@ +import "dotenv/config"; +import { spawn } from "child_process"; +import { etherscanApiKey } from "../hardhat.config.js"; + +/** + * Forwards `yarn verify --network [args]` to the rocketh-verify CLI. + */ +async function main() { + const argv = process.argv.slice(2); + + if (argv.includes("--help") || argv.includes("-h")) { + console.log( + [ + "Usage: yarn verify --network [subcommand] [options]", + "", + "Subcommands: etherscan (default) | sourcify | blockscout | metadata", + "", + "Examples:", + " yarn verify --network optimismSepolia", + " yarn verify --network sepolia sourcify", + ].join("\n"), + ); + return; + } + + const networkIdx = argv.indexOf("--network"); + const network = networkIdx !== -1 ? argv[networkIdx + 1] : "default"; + const forwarded = argv.filter((_, i) => i !== networkIdx && i !== networkIdx + 1); + + const subcommands = ["etherscan", "sourcify", "blockscout", "metadata"]; + const hasSubcommand = forwarded.some(a => subcommands.includes(a)); + + const verifyArgs = ["-e", network, ...(hasSubcommand ? [] : ["etherscan"]), ...forwarded]; + + const env = { ...process.env }; + const userKey = process.env.ETHERSCAN_API_KEY; + env.ETHERSCAN_API_KEY = etherscanApiKey; + if (!userKey) { + console.log( + "â„šī¸ Using shared Etherscan API key (rate-limited). Set your own ETHERSCAN_API_KEY in .env for production.\n", + ); + } + + const child = spawn("rocketh-verify", verifyArgs, { + stdio: "inherit", + env, + shell: process.platform === "win32", + }); + + child.on("exit", code => { + process.exit(code || 0); + }); +} + +main().catch(console.error); diff --git a/templates/solidity-frameworks/hardhat/packages/hardhat/tsconfig.json.template.mjs b/templates/solidity-frameworks/hardhat/packages/hardhat/tsconfig.json.template.mjs index b1c23a227b..72b40cde95 100644 --- a/templates/solidity-frameworks/hardhat/packages/hardhat/tsconfig.json.template.mjs +++ b/templates/solidity-frameworks/hardhat/packages/hardhat/tsconfig.json.template.mjs @@ -2,8 +2,9 @@ import { withDefaults, deepMerge } from "../../../../utils.js"; const defaultTsConfig = { "compilerOptions": { - "target": "es2020", - "module": "commonjs", + "target": "es2022", + "module": "node16", + "moduleResolution": "node16", "esModuleInterop": true, "resolveJsonModule": true, "forceConsistentCasingInFileNames": true,