Files
2025-12-27 21:16:53 -08:00

186 lines
6.4 KiB
TypeScript

/**
* 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";
import { DeployFunction } from "hardhat-deploy/types";
import * as path from "path";
const generatedContractComment = `
/**
* This file is autogenerated by Scaffold-ETH.
* You should not edit it manually or your changes might be overwritten.
*/
`;
const DEPLOYMENTS_DIR = "./deployments";
const ARTIFACTS_DIR = "./artifacts";
const TARGET_DIR = "../nextjs/contracts/";
function getDirectories(path: string) {
return fs
.readdirSync(path, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
}
function getContractNames(path: string) {
return fs
.readdirSync(path, { withFileTypes: true })
.filter(dirent => dirent.isFile() && dirent.name.endsWith(".json"))
.map(dirent => dirent.name.split(".")[0]);
}
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) {
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) {
const sourceName = sourcePath?.split("/").pop()?.split(".sol")[0];
const { abi } = JSON.parse(fs.readFileSync(`${ARTIFACTS_DIR}/${sourcePath}/${sourceName}.json`).toString());
for (const functionAbi of 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 chainName of getDirectories(DEPLOYMENTS_DIR)) {
const chainId = fs.readFileSync(`${DEPLOYMENTS_DIR}/${chainName}/.chainId`).toString();
const contracts = {} as Record<string, any>;
for (const contractName of getContractNames(`${DEPLOYMENTS_DIR}/${chainName}`)) {
const { abi, address, metadata } = JSON.parse(
fs.readFileSync(`${DEPLOYMENTS_DIR}/${chainName}/${contractName}.json`).toString(),
);
const inheritedFunctions = metadata ? getInheritedFunctions(JSON.parse(metadata).sources, contractName) : {};
contracts[contractName] = { address, abi, inheritedFunctions };
}
output[chainId] = contracts;
}
return output;
}
function getExistingDeployedContracts() {
const filePath = path.resolve(`${TARGET_DIR}deployedContracts.ts`);
if (!fs.existsSync(filePath)) {
return {};
}
try {
// Create a temporary directory for our approach
const tempDir = path.join(process.cwd(), "temp");
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir);
}
// Create a temporary JS file that exports the content of deployedContracts.ts
const tempFile = path.join(tempDir, "temp.js");
const targetContent = fs.readFileSync(filePath, "utf8");
// Transform the TypeScript file into a JavaScript module
const jsContent = targetContent
.replace(/import[^;]*;/, "")
.replace(/export default[^;]*;/, "module.exports = deployedContracts;")
.replace(/as const/, "")
.replace(/satisfies GenericContractsDeclaration/, "");
fs.writeFileSync(tempFile, jsContent);
// Now we can require this file
try {
// Clear require cache to ensure fresh load
delete require.cache[require.resolve(tempFile)];
const contracts = require(tempFile);
// Clean up
fs.unlinkSync(tempFile);
return contracts;
} catch (importError) {
console.error("Error requiring the temporary contracts file:", importError);
// Fallback: try a simpler approach with less features
console.log("Falling back to simpler approach...");
return {};
}
} catch (error) {
console.error("Error reading existing deployedContracts.ts file:", error);
return {};
}
}
/**
* 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 () {
// Get locally deployed contracts
const localContractsData = getContractDataFromDeployments();
// Get existing deployed contracts from the file
const existingContractsData = getExistingDeployedContracts();
// Merge local deployments with existing data (local deployments take precedence)
const mergedContractsData = { ...existingContractsData, ...localContractsData };
const fileContent = Object.entries(mergedContractsData).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;