diff --git a/.changeset/eighty-ducks-tease.md b/.changeset/eighty-ducks-tease.md new file mode 100644 index 000000000..3be92c1a5 --- /dev/null +++ b/.changeset/eighty-ducks-tease.md @@ -0,0 +1,6 @@ +--- +"create-eth": patch +--- + +- reverse rpc fallback order (https://github.com/scaffold-eth/scaffold-eth-2/pull/1010) +- Encrypt deployer PK on .env file (when using hardhat) (https://github.com/scaffold-eth/scaffold-eth-2/pull/1008 ) diff --git a/templates/base/packages/nextjs/package.json b/templates/base/packages/nextjs/package.json index ad77e085f..ad08902ef 100644 --- a/templates/base/packages/nextjs/package.json +++ b/templates/base/packages/nextjs/package.json @@ -31,8 +31,8 @@ "react-dom": "~18.3.1", "react-hot-toast": "~2.4.0", "usehooks-ts": "~3.1.0", - "viem": "2.21.32", - "wagmi": "2.12.23", + "viem": "2.21.54", + "wagmi": "2.13.4", "zustand": "~5.0.0" }, "devDependencies": { diff --git a/templates/base/packages/nextjs/scaffold.config.ts.template.mjs b/templates/base/packages/nextjs/scaffold.config.ts.template.mjs index 22920768c..a380def17 100644 --- a/templates/base/packages/nextjs/scaffold.config.ts.template.mjs +++ b/templates/base/packages/nextjs/scaffold.config.ts.template.mjs @@ -11,6 +11,8 @@ export type ScaffoldConfig = { onlyLocalBurnerWallet: boolean; }; +export const DEFAULT_ALCHEMY_API_KEY = "oKxs-03sij-U_N0iOlrSsZFr29-IqbuF"; + const scaffoldConfig = { // The networks on which your DApp is live targetNetworks: [${chainName.map(chain => `chains.${chain}`).join(', ')}], @@ -23,7 +25,7 @@ const scaffoldConfig = { // You can get your own at https://dashboard.alchemyapi.io // It's recommended to store it in an env variable: // .env.local for local testing, and in the Vercel/system env config for live apps. - alchemyApiKey: process.env.NEXT_PUBLIC_ALCHEMY_API_KEY || "oKxs-03sij-U_N0iOlrSsZFr29-IqbuF", + alchemyApiKey: process.env.NEXT_PUBLIC_ALCHEMY_API_KEY || DEFAULT_ALCHEMY_API_KEY, // This is ours WalletConnect's default project ID. // You can get your own at https://cloud.walletconnect.com diff --git a/templates/base/packages/nextjs/services/web3/wagmiConfig.tsx b/templates/base/packages/nextjs/services/web3/wagmiConfig.tsx index 30fc4f9da..37253daf4 100644 --- a/templates/base/packages/nextjs/services/web3/wagmiConfig.tsx +++ b/templates/base/packages/nextjs/services/web3/wagmiConfig.tsx @@ -2,7 +2,7 @@ import { wagmiConnectors } from "./wagmiConnectors"; import { Chain, createClient, fallback, http } from "viem"; import { hardhat, mainnet } from "viem/chains"; import { createConfig } from "wagmi"; -import scaffoldConfig from "~~/scaffold.config"; +import scaffoldConfig, { DEFAULT_ALCHEMY_API_KEY } from "~~/scaffold.config"; import { getAlchemyHttpUrl } from "~~/utils/scaffold-eth"; const { targetNetworks } = scaffoldConfig; @@ -17,8 +17,14 @@ export const wagmiConfig = createConfig({ connectors: wagmiConnectors, ssr: true, client({ chain }) { + let rpcFallbacks = [http()]; + const alchemyHttpUrl = getAlchemyHttpUrl(chain.id); - const rpcFallbacks = alchemyHttpUrl ? [http(), http(alchemyHttpUrl)] : [http()]; + if (alchemyHttpUrl) { + const isUsingDefaultKey = scaffoldConfig.alchemyApiKey === DEFAULT_ALCHEMY_API_KEY; + // If using default Scaffold-ETH 2 API key, we prioritize the default RPC + rpcFallbacks = isUsingDefaultKey ? [http(), http(alchemyHttpUrl)] : [http(alchemyHttpUrl), http()]; + } return createClient({ chain, 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 716fec79e..6478bd694 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 @@ -15,8 +15,8 @@ const deployYourContract: DeployFunction = async function (hre: HardhatRuntimeEn 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 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(); diff --git a/templates/solidity-frameworks/foundry/packages/foundry/script/Deploy.s.sol.template.mjs b/templates/solidity-frameworks/foundry/packages/foundry/script/Deploy.s.sol.template.mjs index ea924b105..018e7ce21 100644 --- a/templates/solidity-frameworks/foundry/packages/foundry/script/Deploy.s.sol.template.mjs +++ b/templates/solidity-frameworks/foundry/packages/foundry/script/Deploy.s.sol.template.mjs @@ -17,6 +17,9 @@ contract DeployScript is ScaffoldETHDeploy { }`; export default withDefaults(content, { - deploymentsScriptsImports: "", - deploymentsLogic: "", + deploymentsScriptsImports: `import { DeployYourContract } from "./DeployYourContract.s.sol";`, + deploymentsLogic: ` + DeployYourContract deployYourContract = new DeployYourContract(); + deployYourContract.run(); + `, }); diff --git a/templates/solidity-frameworks/hardhat/package.json b/templates/solidity-frameworks/hardhat/package.json index 1d7f9c3fc..6008de639 100644 --- a/templates/solidity-frameworks/hardhat/package.json +++ b/templates/solidity-frameworks/hardhat/package.json @@ -1,13 +1,15 @@ { "scripts": { "account": "yarn workspace @se-2/hardhat account", + "account:import": "yarn workspace @se-2/hardhat account:import", + "account:generate": "yarn workspace @se-2/hardhat account:generate", "chain": "yarn workspace @se-2/hardhat chain", "fork": "yarn workspace @se-2/hardhat fork", "deploy": "yarn workspace @se-2/hardhat deploy", "verify": "yarn workspace @se-2/hardhat verify", "hardhat-verify": "yarn workspace @se-2/hardhat hardhat-verify", "compile": "yarn workspace @se-2/hardhat compile", - "generate": "yarn workspace @se-2/hardhat generate", + "generate": "yarn account:generate", "hardhat:lint": "yarn workspace @se-2/hardhat lint", "hardhat:lint-staged": "yarn workspace @se-2/hardhat lint-staged", "hardhat:format": "yarn workspace @se-2/hardhat format", diff --git a/templates/solidity-frameworks/hardhat/packages/hardhat/.env.example b/templates/solidity-frameworks/hardhat/packages/hardhat/.env.example index 86614fe5e..b12938318 100644 --- a/templates/solidity-frameworks/hardhat/packages/hardhat/.env.example +++ b/templates/solidity-frameworks/hardhat/packages/hardhat/.env.example @@ -7,5 +7,7 @@ # To access the values stored in this .env file you can use: process.env.VARIABLENAME ALCHEMY_API_KEY= -DEPLOYER_PRIVATE_KEY= ETHERSCAN_MAINNET_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/hardhat.config.ts.template.mjs b/templates/solidity-frameworks/hardhat/packages/hardhat/hardhat.config.ts.template.mjs index 134e8d39c..3e35b4a50 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 @@ -36,8 +36,9 @@ ${imports.filter(Boolean).join("\n")} // You can get your own at https://dashboard.alchemyapi.io const providerApiKey = process.env.ALCHEMY_API_KEY || "oKxs-03sij-U_N0iOlrSsZFr29-IqbuF"; // 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.DEPLOYER_PRIVATE_KEY ?? "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + process.env.__RUNTIME_DEPLOYER_PRIVATE_KEY ?? "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; // If not set, it uses our block explorers default API keys. const etherscanApiKey = process.env.ETHERSCAN_MAINNET_API_KEY || "DNXJA8RX2Q3VZ4URQIWP7Z68CJXQZSC6AW"; const etherscanOptimisticApiKey = process.env.ETHERSCAN_OPTIMISTIC_API_KEY || "RM62RDISS1RH448ZY379NX625ASG1N633R"; diff --git a/templates/solidity-frameworks/hardhat/packages/hardhat/package.json b/templates/solidity-frameworks/hardhat/packages/hardhat/package.json index 8f26aabff..7d4faf652 100644 --- a/templates/solidity-frameworks/hardhat/packages/hardhat/package.json +++ b/templates/solidity-frameworks/hardhat/packages/hardhat/package.json @@ -3,12 +3,14 @@ "version": "0.0.1", "scripts": { "account": "hardhat run scripts/listAccount.ts", + "account:import": "hardhat run scripts/importAccount.ts", + "account:generate": "hardhat run scripts/generateAccount.ts", "chain": "hardhat node --network hardhat --no-deploy", "check-types": "tsc --noEmit --incremental", "compile": "hardhat compile", - "deploy": "hardhat deploy", + "deploy": "ts-node scripts/runHardhatDeployWithPK.ts", "fork": "MAINNET_FORKING_ENABLED=true hardhat node --network hardhat --no-deploy", - "generate": "hardhat run scripts/generateAccount.ts", + "generate": "yarn account:generate", "flatten": "hardhat flatten", "lint": "eslint --config ./.eslintrc.json --ignore-path ./.eslintignore ./*.ts ./deploy/**/*.ts ./scripts/**/*.ts ./test/**/*.ts", "lint-staged": "eslint --config ./.eslintrc.json --ignore-path ./.eslintignore", @@ -49,6 +51,7 @@ "typescript": "<5.6.0" }, "dependencies": { + "@inquirer/password": "^4.0.2", "@openzeppelin/contracts": "~5.0.2", "@typechain/ethers-v6": "~0.5.1", "dotenv": "~16.4.5", diff --git a/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/generateAccount.ts b/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/generateAccount.ts index 5de8e458c..be55d0576 100644 --- a/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/generateAccount.ts +++ b/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/generateAccount.ts @@ -1,42 +1,55 @@ import { ethers } from "ethers"; import { parse, stringify } from "envfile"; import * as fs from "fs"; +import password from "@inquirer/password"; const envFilePath = "./.env"; -/** - * Generate a new random private key and write it to the .env file - */ -const setNewEnvConfig = (existingEnvConfig = {}) => { - console.log("šŸ‘› Generating new Wallet"); +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: randomWallet.privateKey, + DEPLOYER_PRIVATE_KEY_ENCRYPTED: encryptedJson, }; // Store in .env fs.writeFileSync(envFilePath, stringify(newEnvConfig)); - console.log("šŸ“„ Private Key saved to packages/hardhat/.env file"); - console.log("šŸŖ„ Generated wallet address:", randomWallet.address); + 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. - setNewEnvConfig(); + await setNewEnvConfig(); return; } - // .env file exists const existingEnvConfig = parse(fs.readFileSync(envFilePath).toString()); - if (existingEnvConfig.DEPLOYER_PRIVATE_KEY) { + if (existingEnvConfig.DEPLOYER_PRIVATE_KEY_ENCRYPTED) { console.log("āš ļø You already have a deployer account. Check the packages/hardhat/.env file"); return; } - setNewEnvConfig(existingEnvConfig); + await setNewEnvConfig(existingEnvConfig); } main().catch(error => { diff --git a/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/importAccount.ts b/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/importAccount.ts new file mode 100644 index 000000000..4af0815b7 --- /dev/null +++ b/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/importAccount.ts @@ -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; +}); diff --git a/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/listAccount.ts b/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/listAccount.ts index 4fc5f2da1..ae1aafd6f 100644 --- a/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/listAccount.ts +++ b/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/listAccount.ts @@ -3,17 +3,26 @@ dotenv.config(); import { ethers, Wallet } from "ethers"; import QRCode from "qrcode"; import { config } from "hardhat"; +import password from "@inquirer/password"; async function main() { - const privateKey = process.env.DEPLOYER_PRIVATE_KEY; + const encryptedKey = process.env.DEPLOYER_PRIVATE_KEY_ENCRYPTED; - if (!privateKey) { - console.log("šŸš«ļø You don't have a deployer account. Run `yarn generate` first"); + 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; } - // Get account from private key. - const wallet = new Wallet(privateKey); const address = wallet.address; console.log(await QRCode.toString(address, { type: "terminal", small: true })); console.log("Public address:", address, "\n"); diff --git a/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/runHardhatDeployWithPK.ts b/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/runHardhatDeployWithPK.ts new file mode 100644 index 000000000..7628b4313 --- /dev/null +++ b/templates/solidity-frameworks/hardhat/packages/hardhat/scripts/runHardhatDeployWithPK.ts @@ -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);