Files

242 lines
7.8 KiB
TypeScript

import { execSync } from 'child_process';
import { promises as fs } from 'fs';
import { join } from 'path';
const debug = process.argv.includes('--debug');
/**
* Logger object for logging debug, info, and error messages. Debug messages are only logged when the --debug flag is provided.
*/
const logger = {
debug: debug ? console.debug : () => {},
info: console.info,
error: console.error,
};
logger.debug('Debugging enabled');
const GENERATED_FILE_HEADER = `// This file is generated by the generate-abis script. Do not modify it manually.\n\n`;
const ABI_DOC_STRING = (contractName: string) =>
`${GENERATED_FILE_HEADER}/**\n * Generated ABI for the ${contractName} contract.\n */\n`;
/**
* Generates the ABI for a contract and writes it to a file.
* @param filePath The path to the JSON file containing the contract.
* @param outputDir The directory to write the generated ABI file to.
* @returns The name of the generated contract.
*/
async function generateABI({ filePath, outputDir }: { filePath: string; outputDir: string }) {
try {
const json = await fs.readFile(filePath, 'utf8');
const { contractName, abi } = JSON.parse(json);
if (!contractName) {
throw new Error(`Contract name not found! ${filePath}`);
}
if (!abi) {
throw new Error(`Contract ABI not found! ${filePath} with name: ${contractName}`);
}
// Write the ABI to a file
logger.debug(`Generating ABI for ${contractName}...`);
const abiType = `${ABI_DOC_STRING(contractName)}export const ${contractName}Abi = ${JSON.stringify(abi, null, 2)} as const;`;
await fs.writeFile(join(outputDir, `${contractName}.ts`), abiType);
logger.info(`ABI for ${contractName} generated successfully!`);
return contractName;
} catch (error) {
logger.error('Error generating ABI:', error);
}
}
/**
* Downloads the repository from the specified GitHub URL to the destination path.
* If the repository is already downloaded, it uses the cache.
* @param url The GitHub URL of the repository to download.
* @param destinationPath The path to download the repository to.
*/
async function downloadRepository({
url,
destinationPath,
}: {
url: string;
destinationPath: string;
}) {
try {
// check if the repository is already downloaded
const isRepoDownloaded = await fs
.access(destinationPath)
.then(() => true)
.catch(() => false);
if (isRepoDownloaded) {
logger.info('Repository already downloaded. Using cache...');
return;
}
// Clone the repository
execSync(`git clone ${url} ${destinationPath}`);
logger.info('Repository downloaded successfully!');
execSync(`cd ${destinationPath} && git switch master`);
} catch (error) {
logger.error('Error downloading repository:', error);
}
}
/**
* Generates contracts.
* @param destinationPath The path to the downloaded repository.
* @returns A promise that resolves when the contracts are generated.
*/
async function generateContracts({ destinationPath }: { destinationPath: string }) {
execSync(`cd ${destinationPath} && pnpm yarn install && pnpm yarn build`, {
stdio: debug ? 'inherit' : 'pipe',
});
}
/**
* Generates contract ABIs for the given directory and its subdirectories.
*
* @param dir - The directory path to generate contract ABIs for.
* @param generatedAbis - An array to store the names of the generated ABIs.
* @param excludeDirs - An array of directories to exclude from ABI generation.
* @param outputDir - The directory to write the generated ABI files to.
* @returns A promise that resolves when all contract ABIs have been generated.
*/
async function generateContractABIs({
dir,
abis = [],
excludeDirs = [],
outputDir,
}: {
dir: string;
abis: Array<string>;
excludeDirs: Array<string>;
outputDir: string;
}) {
try {
// Get the list of files in the contracts directory
const files = await fs.readdir(dir);
// Generate ABIs for each contract
for (const file of files) {
// Check if the directory should be excluded
if (excludeDirs.includes(file)) {
logger.debug('Excluding directory:', file);
continue;
}
const filePath = join(dir, file);
if (file.endsWith('.dbg.json')) {
logger.debug('Skipping debug file:', file);
continue;
}
logger.debug('Checking file:', filePath);
if (file.endsWith('.json')) {
try {
const name = await generateABI({ filePath, outputDir });
abis.push(name);
} catch (error) {
logger.error(error);
}
continue;
}
// check if the file is a directory
const stats = await fs.lstat(filePath);
if (!stats.isDirectory()) {
logger.debug('File is not a directory. skipping');
continue;
}
await generateContractABIs({ dir: filePath, abis, excludeDirs, outputDir });
}
} catch (error) {
logger.error('Error generating contract ABIs:', error);
}
}
/**
* Generates an ABI index file based on the provided array of ABI names.
* @param abis An array of ABI names.
* @param outputDir The directory to write the generated index file to.
*/
async function generateAbiIndex({ abis, outputDir }: { abis: Array<string>; outputDir: string }) {
logger.debug('abis', abis);
const indexFile = abis
.map((abi) => `import { ${abi}Abi } from './${abi}';`)
.join('\n')
.concat(
`\n\nexport const Contracts = {\n${abis.map((abi) => `${abi}: ${abi}Abi`).join(',\n')}\n} as const;`
)
.concat(`export type ContractWithAbiName = keyof typeof Contracts;`)
.concat(`export type ContractAbis = typeof Contracts;`)
.concat(`export {\n${abis.map((abi) => `${abi}Abi,`).join('\n')}\n};`);
// Write the index file
await fs.writeFile(join(outputDir, 'index.ts'), GENERATED_FILE_HEADER.concat(indexFile));
logger.info('Index file generated successfully!');
}
/**
* This is the main function that generates the ABIs. It runs when the script is called.
*/
async function index() {
const githubUrl = 'https://github.com/oxen-io/eth-sn-contracts/';
const url = githubUrl.split('/').filter((str) => str);
const repoName = url.pop();
const destinationPath = `./.cache/${repoName}`;
const contractsPath = join(destinationPath, '/artifacts/contracts');
const outputDir = './abis';
const excludeDirs = ['interfaces', 'test'];
// Create the output directory
await fs.mkdir(outputDir, { recursive: true });
let cachedHash = '';
try {
await fs.access('.cache');
try {
cachedHash = await fs.readFile(`./.cache/${repoName}.hash`, 'utf8');
} catch (error) {
logger.error('Error reading cached hash:', error);
}
} catch (error) {
logger.debug('.cache directory not found. Skipping cache check');
}
// Download the repository
await downloadRepository({ url: githubUrl, destinationPath });
const commitHash = execSync('git rev-parse HEAD', { cwd: destinationPath }).toString().trim();
logger.debug('Commit hash:', commitHash);
await fs.writeFile(`${destinationPath}.hash`, commitHash);
// Check if ./abis directory exists and contains files
const files = await fs.readdir(outputDir);
if (files.length) {
if (files.length && commitHash === cachedHash) {
logger.info('No changes detected. Skipping ABI generation.');
return;
} else {
// write the hash to the cache
await fs.writeFile(`./.cache/${repoName}.hash`, commitHash);
}
}
// Generate contracts
await generateContracts({ destinationPath });
// Generate contract ABIs
const abis: Array<string> = [];
await generateContractABIs({ dir: contractsPath, abis, excludeDirs, outputDir });
// Generate the index file
await generateAbiIndex({ abis, outputDir });
logger.info('ABIs generated successfully!');
logger.info('Formatting files...');
execSync('pnpm format');
}
index();