sbv2-solana/javascript/solana.js/scripts/generate-client.ts

361 lines
11 KiB
TypeScript

#!/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<string[]> => {
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<string> => {
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<string, any>
): Promise<void> {
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);
}
});
});
}