#!/usr/bin/env tsx import { exec, execSync } from "child_process"; import fsSync from "fs"; import fs from "fs/promises"; import _ from "lodash"; import path from "path"; import shell from "shelljs"; import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const packageRoot = path.join(__dirname, ".."); // const shx = path.join(packageRoot, 'node_modules', '.bin', 'shx'); const v2DevnetIdlPath = path.join(packageRoot, "./idl/devnet.json"); const v2MainnetIdlPath = path.join(packageRoot, "./idl/mainnet.json"); const v2GeneratedPath = path.join( packageRoot, "./src/generated/oracle-program" ); const attestationDevnetIdlPath = path.join( packageRoot, "./idl/attestation-devnet.json" ); const attestationGeneratedPath = path.join( packageRoot, "./src/generated/attestation-program" ); const solanaProgramDir = path.join(packageRoot, "../../chains/solana"); const switchboardV2IdlPath = path.join( solanaProgramDir, "target/idl/switchboard_v2.json" ); const switchboardAttestationIdlPath = path.join( solanaProgramDir, "target/idl/switchboard_attestation_program.json" ); // Super hacky. Some files need to be reset to the previous git state and will be manually managed const ignoreFiles = [ `${v2GeneratedPath}/types/SwitchboardPermission.ts`, // we manually added NONE enumeration `${v2GeneratedPath}/types/SwitchboardDecimal.ts`, // added toBig methods `${v2GeneratedPath}/types/Lanes.ts`, // anchor-client-gen struggles with dual exports `${v2GeneratedPath}/types/index.ts`, // TODO: Need a better way to handle this. anchor-client-gen adds multiple, broken exports (for VRF builder) `${v2GeneratedPath}/errors/index.ts`, // need to revert the program ID check, `${v2GeneratedPath}/instructions/viewVersion.ts`, `${attestationGeneratedPath}/types/VerificationStatus.ts`, `${attestationGeneratedPath}/errors/index.ts`, `${attestationGeneratedPath}/types/SwitchboardAttestationPermission.ts`, `${attestationGeneratedPath}/instructions/functionDeactivateLookup.ts`, `${attestationGeneratedPath}/instructions/accountCloseOverride.ts`, `${attestationGeneratedPath}/instructions/viewVersion.ts`, `${attestationGeneratedPath}/instructions/index.ts`, // make sure to disable this if adding more attestation program instructions. used to avoid name conflict on viewVersion ixn // `${v2GeneratedPath}/types/VerificationStatus.ts`, ]; /** * Fetch a list of filepaths for a given directory and desired file extension * @param [dirPath] Filesystem path to a directory to search. * @param [arrayOfFiles] An array of existing file paths for recursive calls * @param [extensions] Optional, an array of desired extensions with the leading separator '.' * @throws {String} * @returns {string[]} */ const getAllFiles = async ( dirPath: string, arrayOfFiles: string[] = [], extensions: string[] = [] ): Promise => { const files = await fs.readdir(dirPath, "utf8"); arrayOfFiles = arrayOfFiles || []; for await (const file of files) { if (fsSync.statSync(dirPath + "/" + file).isDirectory()) { arrayOfFiles = await getAllFiles( dirPath + "/" + file, arrayOfFiles, extensions ); } else { const ext = path.extname(file); if (extensions && Array.isArray(extensions) && extensions.includes(ext)) { arrayOfFiles.push(path.join(dirPath, "/", file)); } else { arrayOfFiles.push(path.join(dirPath, "/", file)); } // if (!(extensions === undefined) || extensions.includes(ext)) { // arrayOfFiles.push(path.join(dirPath, '/', file)); // } } } return arrayOfFiles; }; async function main() { shell.cd(packageRoot); // generate IDL types from local directory let devMode = false; if (process.argv.slice(2).includes("--dev")) { devMode = true; } if (!shell.which("anchor")) { shell.echo( "Sorry, this script requires 'anchor' to be installed in your $PATH" ); shell.exit(1); } // Fetch IDLs and cleaning generated directories console.log(`Fetching anchor IDLs ...`); await Promise.all([ runCommandAsync( `anchor idl fetch -o ${v2MainnetIdlPath} SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f --provider.cluster mainnet`, { encoding: "utf-8", } ), runCommandAsync( `anchor idl fetch -o ${v2DevnetIdlPath} SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f --provider.cluster devnet`, { encoding: "utf-8", } ), runCommandAsync( `anchor idl fetch -o ${attestationDevnetIdlPath} sbattyXrzedoNATfc4L31wC9Mhxsi1BmFhTiN8gDshx --provider.cluster devnet`, { encoding: "utf-8", } ), fsSync.existsSync(v2GeneratedPath) ? fs.rm(v2GeneratedPath, { recursive: true }) : Promise.resolve(), fsSync.existsSync(attestationGeneratedPath) ? fs.rm(attestationGeneratedPath, { recursive: true }) : Promise.resolve(), ]); console.log(`Running anchor-client-gen ...`); if (devMode) { await Promise.all([ runCommandAsync( `npx anchor-client-gen --program-id SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f ${switchboardV2IdlPath} ${v2GeneratedPath}`, { encoding: "utf-8" } ), runCommandAsync( `npx anchor-client-gen --program-id sbattyXrzedoNATfc4L31wC9Mhxsi1BmFhTiN8gDshx ${switchboardAttestationIdlPath} ${attestationGeneratedPath}`, { encoding: "utf-8" } ), ]); } else { await Promise.all([ runCommandAsync( `npx anchor-client-gen --program-id SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f ${v2DevnetIdlPath} ${v2GeneratedPath}`, { encoding: "utf-8" } ), runCommandAsync( `npx anchor-client-gen --program-id sbattyXrzedoNATfc4L31wC9Mhxsi1BmFhTiN8gDshx ${attestationDevnetIdlPath} ${attestationGeneratedPath}`, { encoding: "utf-8" } ), ]); } await Promise.all([ fs.writeFile( `${v2GeneratedPath}/index.ts`, [ "export * from './accounts/index.js';", "export * from './errors/index.js';", "export * from './instructions/index.js';", "export * from './types/index.js';", ].join("\n") ), fs.writeFile( `${attestationGeneratedPath}/index.ts`, [ "export * from './accounts/index.js';", "export * from './errors/index.js';", "export * from './instructions/index.js';", "export * from './types/index.js';", ].join("\n") ), ]); // loop through directory and run regex replaces const allGeneratedFiles = _.flatten( await Promise.all([ getAllFiles(`${v2GeneratedPath}/accounts`), getAllFiles(`${v2GeneratedPath}/errors`), getAllFiles(`${v2GeneratedPath}/instructions`), getAllFiles(`${v2GeneratedPath}/types`), getAllFiles(`${attestationGeneratedPath}/accounts`), getAllFiles(`${attestationGeneratedPath}/errors`), getAllFiles(`${attestationGeneratedPath}/instructions`), getAllFiles(`${attestationGeneratedPath}/types`), ]) ); console.log(`Processing ${allGeneratedFiles.length} files ...`); await Promise.all(allGeneratedFiles.map((f) => processFile(f))); console.log(`Formatting generated files ...`); await Promise.all([ runCommandAsync(`npx prettier ${v2GeneratedPath} --write`, { encoding: "utf-8", }), runCommandAsync(`npx prettier ${attestationGeneratedPath} --write`, { encoding: "utf-8", }), ]); // reset ignored files for (const file of ignoreFiles) { execSync(`git restore ${file}`, { encoding: "utf-8" }); } // delete the extra VerifierAccountData structs await fs.rm(path.join(v2GeneratedPath, "types", "VerificationStatus.ts")); await fs.rm(path.join(v2GeneratedPath, "types", "VerifierAccountData.ts")); await fs.rm(path.join(v2GeneratedPath, "types", "Quote.ts")); // run auto fix for import ordering execSync(`pnpm fix`, { encoding: "utf-8" }); } main() .then(() => { // console.log("Executed successfully"); }) .catch((err) => { console.error(err); }); /** * Process the generated file and add the SwitchboardProgram class */ const processFile = async (file: string) => { return await fs .readFile(file, "utf-8") .then(async (fileString: string): Promise => { let updatedFileString = fileString; if (file.endsWith("errors/index.ts")) { return updatedFileString .replace( `import { PROGRAM_ID } from "../programId"`, `import { PROGRAM_ID } from "../programId.js"` ) .replace( `import * as anchor from "./anchor"`, `import * as anchor from "./anchor.js"` ) .replace( `import * as custom from "./custom"`, `import * as custom from "./custom.js"` ); } if (file.includes("index.ts")) { // add the .js extension to all local import/export paths return updatedFileString.replace( /((import|export)((\s\w+)?([^'"]*))from\s['"])([^'"]+)(['"])/gm, "$1$6.js$7" ); } updatedFileString = `import { SwitchboardProgram } from "../../../SwitchboardProgram.js"` + "\n" + fileString; // update the types import updatedFileString = updatedFileString // use full path .replace( `import * as types from "../types"`, `import * as types from "../types/index.js"` ) // use our library to avoid version conflicts .replace( `import BN from "bn.js"`, `import { BN } from "@switchboard-xyz/common"` ) // better .replace( `import * as borsh from "@project-serum/borsh"`, `import * as borsh from "@coral-xyz/borsh"` ) // remove this import, using SwitchboardProgram .replace(`import { PROGRAM_ID } from "../programId"`, ``) .replaceAll(`PROGRAM_ID`, `program.programId`) .replaceAll(`c: Connection,`, `program: SwitchboardProgram,`) .replaceAll(`c.getAccountInfo`, `program.connection.getAccountInfo`) .replaceAll( `c.getMultipleAccountsInfo`, `program.connection.getMultipleAccountsInfo` ); if (file.includes("/instructions/")) { updatedFileString = updatedFileString.replaceAll( `args:`, `program: SwitchboardProgram, args:` ); } if (file.includes("/attestation-program/")) { updatedFileString = updatedFileString.replaceAll( `program.programId`, `program.attestationProgramId` ); } return updatedFileString; }) .then(async (updatedFileString: string) => { return await fs.writeFile(file, updatedFileString, "utf-8"); }); }; async function runCommandAsync( command: string, options: Record ): Promise { return new Promise((resolve, reject) => { const cmd = exec(command, options); // cmd.stdout.on('data', data => { // console.log(data.toString()); // }); cmd?.stderr?.on("data", (data) => { console.error(data.toString()); }); // cmd.on("message", (data) => { // console.info(data.toString()); // }); cmd.on("error", (error) => { reject(error); }); cmd.on("close", (code) => { if (code !== 0) { reject(new Error(`Command exited with code ${code}`)); } else { resolve(undefined); } }); }); }