Skip to content

Conversation

@carletex
Copy link
Member

@carletex carletex commented Oct 29, 2025

Creating this draft so we can start tinkering & discussing:

For now:

  • Installed new deps (removed old ones)
  • Updated README with new NodeJS requirement (we might need to update in docs, etc)
  • yarn chain working
  • yarn deploy working
    • Using Ignition. Let's see if we don't need hardhat-deploy anymore (also available for HHv3)
    • Missing:
      • inheritedFunctions
      • deployedOnBlock

ToDo (will be adding more):

  • Fix GitHub lint workflow
  • Create new keystore system (move to custom one => hardhat-keystore / config Variables)
  • Make yarn deploy more generic. Before it deployed everything / tag-based. Now only the specified conttract.
  • --network for deploy (+ config networks on hardhat.config)
  • Verify / Etherscan API key
  • Tests (default should be solidity, TS or both?)
  • Mainnet Fork
  • TS Generation: get inherited functions + deployed block

Known Issues:

  • If I deploy, make a change to the contract, and deploy again, I get [ YourContract ] Nothing new to deploy based on previous execution stored in ./ignition/deployments/chain-31337

Fixes #1191

Copy link
Member Author

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

Copy link
Member Author

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)

Copy link
Collaborator

@technophile-04 technophile-04 Nov 10, 2025

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 contract

And 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")],
Copy link
Member Author

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";
Copy link
Member Author

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.

Copy link
Collaborator

@technophile-04 technophile-04 Nov 10, 2025

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

Copy link
Collaborator

@technophile-04 technophile-04 Nov 10, 2025

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hardhat V3

3 participants