-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Migration to Hardhat v3 #1192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Migration to Hardhat v3 #1192
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Saving this for easier access :D
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Went with this solution for now (naming it generic, so people can deploy everything here by default.... since the module path is hardcoded on package.json)
e.g. This works:
export default buildModule("ScaffoldEthDeployModule", m => {
const yourContract = m.contract("YourContract", ["0x0000000000000000000000000000000000000000"]);
const yourContract2 = m.contract("YourContract2", ["0x0000000000000000000000000000000000000000"]);
return { yourContract, yourContract };
});they say in their docs
You can create multiple modules in a single file, but each must have a unique ID. To deploy a module, you must export it using module.exports = or export default. As a best practice, we suggest maintaining one module per file, naming the file after the module ID.
But I couldn't make it work (creating more modules in the file + exporting both)
Note: they also have https://hardhat.org/ignition/docs/guides/scripts, where you can do more "complex" logic (e.g. console.logs of contract read data)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In foundry we have similar case. So what we have is insted of hardcoding path infront of forge deploy <file_name> we have created a proxy javascript script.
So if the user run yarn deploy => node parseArgs.js. And if you have just ran yarn deploy it will run default all in one deploy script hardhat ignition deploy ignition/modules/ScaffoldEthDeployModule.ts but if you pass extra --file arg then it run only that particualr deploy script.
on this case people could run yarn deploy --file YourContract.ts (we tell people not to include the whole path, it shall be relative to ignition/modules/). And then we ask people to follow the following structure:
ignition/
└── modules/
├── MyContract.ts // each module of each contract
├── ScaffoldEthDeployModule.ts
└── YourContract.ts // each module of each contractAnd in ScaffoldEthDeployModule.ts we do:
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
import YourContractModule from "./YourContract.js";
import MyContractModule from "./MyContract.js";
export default buildModule("ScaffoldEthDeployModule", m => {
const { yourContract } = m.useModule(YourContractModule);
const { myContract } = m.useModule(MyContractModule);
return { yourContract, myContract };
});Lol also we shall discuss this only if we are sure we are gonna use ignition, if we are gonna use hardhat-deploy, then probably not worth the discussion
| chainType: "l1", | ||
| url: "https://mainnet.rpc.buidlguidl.com", | ||
| accounts: [deployerPrivateKey], | ||
| accounts: [configVariable("DEPLOYER_PRIVATE_KEY")], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yarn hardhat keystore set DEPLOYER_PRIVATE_KEY
We'd need to abstract this to the user for yarn generate etc.
Also: for the keystore you need a 8 character password. Unless you use the --dev flag, which doesn't ask for a password at all https://hardhat.org/docs/guides/configuration-variables#improving-ux-when-using-keystore-values-during-the-dev-process
|
|
||
| // 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"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can use regular env vars for non-critical data (like this key) and then the keystore (configVariable) for PKs, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can use regular env vars for non-critical data (like this key)
Agree!! Let's have etherscan and alchemy api key in .env only instead of keystore
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just grabbed some stuff from #644 and did couple of iteration with Claude sonnet to make inherited work.
here is the file with working inherited funcitons:
generateTsAbis.ts
/**
* 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";
const generatedContractComment = `
/**
* This file is autogenerated by Scaffold-ETH.
* You should not edit it manually or your changes might be overwritten.
*/
`;
const DEPLOYMENTS_DIR = "./ignition/deployments";
const ARTIFACTS_DIR = "./artifacts";
function getDirectories(path: string) {
return fs
.readdirSync(path, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
}
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,
compiledContracts: Record<string, any>,
) {
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) {
// Extract the actual contract name without .sol extension
const cleanContractName = sourceContractName.replace(".sol", "");
// Try to find the contract in the compiled output
const compiledContract = compiledContracts[sourcePath]?.[cleanContractName];
if (compiledContract?.abi) {
for (const functionAbi of compiledContract.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>;
for (const dirName of getDirectories(DEPLOYMENTS_DIR)) {
// exmaple dirName = chain-31337
const chainId = dirName.split("-")[1];
const arfifactsPath = `${DEPLOYMENTS_DIR}/${dirName}/artifacts`;
const fileNames = new Set<string>();
for (const fileName of fs.readdirSync(arfifactsPath)) {
const actualFile = fileName.split(".")[0];
fileNames.add(actualFile);
}
const contracts = {} as Record<string, any>;
for (const fileName of fileNames) {
const JsonFilePath = `${arfifactsPath}/${fileName}.json`;
const JsonFileContent = fs.readFileSync(JsonFilePath).toString();
const { abi, contractName, buildInfoId } = JSON.parse(JsonFileContent);
// In Hardhat Ignition, build info is referenced by buildInfoId
// The input is in the ignition build-info, but we need the output from artifacts build-info
const ignitionBuildInfoPath = `${DEPLOYMENTS_DIR}/${dirName}/build-info/${buildInfoId}.json`;
const ignitionBuildInfo = JSON.parse(fs.readFileSync(ignitionBuildInfoPath).toString());
const { input } = ignitionBuildInfo;
// Read the output from the artifacts build-info (has .output.json extension)
const artifactsBuildInfoPath = `${ARTIFACTS_DIR}/build-info/${buildInfoId}.output.json`;
let compiledContracts = {};
if (fs.existsSync(artifactsBuildInfoPath)) {
const artifactsBuildInfo = JSON.parse(fs.readFileSync(artifactsBuildInfoPath).toString());
compiledContracts = artifactsBuildInfo.output?.contracts || {};
}
const inheritedFunctions = getInheritedFunctions(input.sources, contractName, compiledContracts);
const deployedAddresses = fs.readFileSync(`${DEPLOYMENTS_DIR}/${dirName}/deployed_addresses.json`).toString();
const deployedAddressesJson = JSON.parse(deployedAddresses);
const address = deployedAddressesJson[fileName];
contracts[contractName] = { address, abi, inheritedFunctions };
}
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 = 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;Seems to work for me, but we can for sure do some more cleaning up.
note: You need to pass actual address in ScaffoldEthDeployModule otherwise openzeppling Owable contracts reverts because of zero address
Creating this draft so we can start tinkering & discussing:
For now:
yarn chainworkingyarn deployworkingToDo (will be adding more):
yarn deploymore generic. Before it deployed everything / tag-based. Now only the specified conttract.Known Issues:
[ YourContract ] Nothing new to deploy based on previous execution stored in ./ignition/deployments/chain-31337Fixes #1191