diff --git a/package.json b/package.json index b0673bb15..1c49c957f 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,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", "flatten": "yarn workspace @se-2/hardhat flatten", "hardhat:lint": "yarn workspace @se-2/hardhat lint", "hardhat:lint-staged": "yarn workspace @se-2/hardhat lint-staged", diff --git a/packages/hardhat/.env.example b/packages/hardhat/.env.example index 86614fe5e..b12938318 100644 --- a/packages/hardhat/.env.example +++ b/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/packages/hardhat/deploy/00_deploy_your_contract.ts b/packages/hardhat/deploy/00_deploy_your_contract.ts index 716fec79e..6478bd694 100644 --- a/packages/hardhat/deploy/00_deploy_your_contract.ts +++ b/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/packages/hardhat/hardhat.config.ts b/packages/hardhat/hardhat.config.ts index fd4a6d41a..0ea0bdf47 100644 --- a/packages/hardhat/hardhat.config.ts +++ b/packages/hardhat/hardhat.config.ts @@ -13,8 +13,9 @@ import { task } from "hardhat/config"; import generateTsAbis from "./scripts/generateTsAbis"; // 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/packages/hardhat/package.json b/packages/hardhat/package.json index 2a17b3ebe..22184d753 100644 --- a/packages/hardhat/package.json +++ b/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/packages/hardhat/scripts/generateAccount.ts b/packages/hardhat/scripts/generateAccount.ts index 5de8e458c..be55d0576 100644 --- a/packages/hardhat/scripts/generateAccount.ts +++ b/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/packages/hardhat/scripts/importAccount.ts b/packages/hardhat/scripts/importAccount.ts new file mode 100644 index 000000000..4af0815b7 --- /dev/null +++ b/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/packages/hardhat/scripts/listAccount.ts b/packages/hardhat/scripts/listAccount.ts index 4fc5f2da1..ae1aafd6f 100644 --- a/packages/hardhat/scripts/listAccount.ts +++ b/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/packages/hardhat/scripts/runHardhatDeployWithPK.ts b/packages/hardhat/scripts/runHardhatDeployWithPK.ts new file mode 100644 index 000000000..7628b4313 --- /dev/null +++ b/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); diff --git a/yarn.lock b/yarn.lock index 69eb84490..0a857132b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -894,6 +894,52 @@ __metadata: languageName: node linkType: hard +"@inquirer/core@npm:^10.1.0": + version: 10.1.0 + resolution: "@inquirer/core@npm:10.1.0" + dependencies: + "@inquirer/figures": ^1.0.8 + "@inquirer/type": ^3.0.1 + ansi-escapes: ^4.3.2 + cli-width: ^4.1.0 + mute-stream: ^2.0.0 + signal-exit: ^4.1.0 + strip-ansi: ^6.0.1 + wrap-ansi: ^6.2.0 + yoctocolors-cjs: ^2.1.2 + checksum: c52be9ef04497a2b82ed6b1258ebd24ad0950b4b83a96e6fbde1a801eeced4e4b32ed5b2217eac98e504cc1d16ddc8d9d39243c96bdb5390ff13629b28c96591 + languageName: node + linkType: hard + +"@inquirer/figures@npm:^1.0.8": + version: 1.0.8 + resolution: "@inquirer/figures@npm:1.0.8" + checksum: 24c5c70f49a5f0e9d38f5552fb6936c258d2fc545f6a4944b17ba357c9ca4a729e8cffd77666971554ebc2a57948cfe5003331271a259c406b3f2de0e9c559b7 + languageName: node + linkType: hard + +"@inquirer/password@npm:^4.0.2": + version: 4.0.2 + resolution: "@inquirer/password@npm:4.0.2" + dependencies: + "@inquirer/core": ^10.1.0 + "@inquirer/type": ^3.0.1 + ansi-escapes: ^4.3.2 + peerDependencies: + "@types/node": ">=18" + checksum: 69dc3986098cdfb4ed73653b690f7db7d88e483fdd9026b7308d1f6ff1208e5a7599f8c49910735e1fd8008fb367a9bf8f0ff80e7551831c6de15733980277af + languageName: node + linkType: hard + +"@inquirer/type@npm:^3.0.1": + version: 3.0.1 + resolution: "@inquirer/type@npm:3.0.1" + peerDependencies: + "@types/node": ">=18" + checksum: af412f1e7541d43554b02199ae71a2039a1bff5dc51ceefd87de9ece55b199682733b28810fb4b6cb3ed4a159af4cc4a26d4bb29c58dd127e7d9dbda0797d8e7 + languageName: node + linkType: hard + "@ioredis/commands@npm:^1.1.1": version: 1.2.0 resolution: "@ioredis/commands@npm:1.2.0" @@ -2274,6 +2320,7 @@ __metadata: dependencies: "@ethersproject/abi": ^5.7.0 "@ethersproject/providers": ^5.7.2 + "@inquirer/password": ^4.0.2 "@nomicfoundation/hardhat-chai-matchers": ^2.0.7 "@nomicfoundation/hardhat-ethers": ^3.0.8 "@nomicfoundation/hardhat-network-helpers": ^1.0.11 @@ -4104,7 +4151,7 @@ __metadata: languageName: node linkType: hard -"ansi-escapes@npm:^4.3.0": +"ansi-escapes@npm:^4.3.0, ansi-escapes@npm:^4.3.2": version: 4.3.2 resolution: "ansi-escapes@npm:4.3.2" dependencies: @@ -5097,6 +5144,13 @@ __metadata: languageName: node linkType: hard +"cli-width@npm:^4.1.0": + version: 4.1.0 + resolution: "cli-width@npm:4.1.0" + checksum: 0a79cff2dbf89ef530bcd54c713703ba94461457b11e5634bd024c78796ed21401e32349c004995954e06f442d82609287e7aabf6a5f02c919a1cf3b9b6854ff + languageName: node + linkType: hard + "client-only@npm:0.0.1": version: 0.0.1 resolution: "client-only@npm:0.0.1" @@ -9958,6 +10012,13 @@ __metadata: languageName: node linkType: hard +"mute-stream@npm:^2.0.0": + version: 2.0.0 + resolution: "mute-stream@npm:2.0.0" + checksum: d2e4fd2f5aa342b89b98134a8d899d8ef9b0a6d69274c4af9df46faa2d97aeb1f2ce83d867880d6de63643c52386579b99139801e24e7526c3b9b0a6d1e18d6c + languageName: node + linkType: hard + "mz@npm:^2.7.0": version: 2.7.0 resolution: "mz@npm:2.7.0" @@ -14345,6 +14406,13 @@ __metadata: languageName: node linkType: hard +"yoctocolors-cjs@npm:^2.1.2": + version: 2.1.2 + resolution: "yoctocolors-cjs@npm:2.1.2" + checksum: 1c474d4b30a8c130e679279c5c2c33a0d48eba9684ffa0252cc64846c121fb56c3f25457fef902edbe1e2d7a7872130073a9fc8e795299d75e13fa3f5f548f1b + languageName: node + linkType: hard + "zksync-ethers@npm:^5.0.0": version: 5.9.2 resolution: "zksync-ethers@npm:5.9.2"