tenderly-wizard-v6
Version:
A tool for managing virtual testnets using Tenderly
366 lines (318 loc) • 10.9 kB
text/typescript
import { Terminal, terminal } from "terminal-kit";
import { execSync, ExecSyncOptionsWithStringEncoding } from "child_process";
import VirtualTestNet from "./scripts/virtual-test-net";
import colors from "colors";
import { SingleColumnMenuResponse } from "terminal-kit/Terminal";
import { stripAnsi, updatePackageJson } from "./utils/file-manipulation";
import {
findPermissionsFiles,
} from "./utils/util";
import path from "path";
import fs from "fs";
async function getTestnetList() {
terminal.reset("========================\n");
terminal.black(" 🧙 TENDERLY WIZARD 🧙\n");
terminal.yellow(" Roles v2 / Ethers v6\n");
terminal.black("========================\n");
const vnets = await VirtualTestNet.listVirtualTestnets(); // Get the list of virtual testnets
const testnets = vnets.map(vnet => " 🌐 " + vnet.displayName);
testnets.unshift("\n");
testnets.unshift(colors.blue("Select an existing Testnet:"));
testnets.unshift("\n");
testnets.unshift("========================");
testnets.unshift(colors.green("➕ CREATE TESTNET"));
testnets.unshift(colors.green("➕ CREATE TESTNET & SETUP"));
testnets.push("\n");
testnets.push("========================");
testnets.push(colors.red("🛑 EXIT"));
testnets.push("========================");
// terminal.reset("========================");
let testnet: SingleColumnMenuResponse =
await terminal.singleColumnMenu(testnets).promise;
console.log(
(testnet.selectedText = testnet.selectedText.replace(" 🌐 ", ""))
);
if (
testnet.selectedText == colors.red("🛑 EXIT") ||
testnet.selectedText == "\n" ||
testnet.selectedText == "========================" ||
testnet.selectedText == colors.blue("Select Testnet:")
) {
terminal.processExit(0);
}
if (testnet.selectedText == colors.green("➕ CREATE TESTNET")) {
const result = await createNewTestnet(terminal);
testnet.selectedText = result.testnetName;
terminal.processExit(0);
}
if (testnet.selectedText == colors.green("➕ CREATE TESTNET & SETUP")) {
// create new testnet
const result = await createNewTestnet(terminal);
testnet.selectedText = result.testnetName;
// deploy safes
const outputSafes = execSync(`npm run deploy:safes`, {
stdio: "pipe",
}).toString();
console.log(outputSafes);
// apply whitelist
const outputWhitelist = execSync(`npm run deploy:whitelist`, {
stdio: "pipe",
}).toString();
console.log(outputWhitelist);
// save snapshot
const outputSnapshot = execSync(`npm run save:vnet-snapshot`, {
stdio: "pipe",
}).toString();
console.log(outputSnapshot);
terminal.processExit(0);
}
return testnet;
}
export async function start() {
// update target repo's package.json with scripts
if (!process.env.IS_DEV) {
updatePackageJson();
}
terminal.grabInput(true);
terminal.on("key", (name: any) => {
if (name === "ESCAPE") {
terminal.processExit(0);
}
});
let testnet: any;
testnet = await getTestnetList();
const vnets = await VirtualTestNet.listVirtualTestnets();
const vnet = vnets.find(vnet => vnet.displayName == testnet.selectedText);
terminal.reset();
console.log(`VIRTUAL_MAINNET_RPC=${vnet?.admin_rpc}`);
console.log(`TENDERLY_TESTNET_UUID=${vnet?.vnet_id}`);
console.log(`Select Action for ${testnet.selectedText}:`);
const action = await terminal.singleColumnMenu([
"Fork",
colors.red("Delete"),
"Snapshot",
"Activate",
"Deploy Safes",
"Apply Whitelist",
colors.blue("Back"),
]).promise;
// fork testnet
if (action.selectedIndex == 0) {
terminal.reset("Enter the name of the fork name: ");
const newTestnet = await terminal.inputField().promise;
console.log(`fork testnet: ${newTestnet}`);
const result = await VirtualTestNet.forkVirtualTestNet(
vnet?.vnet_id as string,
newTestnet as string
);
console.log(`Forked testnet: ${result.vnet_id}`);
}
// delete testnet
if (action.selectedIndex == 1) {
console.log(
"Are you sure you want to delete this testnet (Y/N):",
testnet.selectedText
);
const confirmDelete = await terminal.yesOrNo().promise;
if (confirmDelete?.valueOf()) {
await VirtualTestNet.deleteVirtualTestNet(vnet?.vnet_id as string);
console.log("Deleted testnet");
}
//go back to start,
goto: await start();
}
// save snapshot
if (action.selectedIndex == 2) {
// @note this function needs to be called from terminal in order to work (needs hardhat to fetch the snapshot)
const output = execSync(`npm run save:vnet-snapshot`, {
stdio: "pipe",
}).toString();
console.log(output);
}
// activate testnet
if (action.selectedIndex == 3) {
// get vnet details
const testNet = await VirtualTestNet.getTestnet(testnet.selectedText);
// // overwrite RPC, Testnet UUID and Fork ID in .env
VirtualTestNet.addToEnvFile(
"VIRTUAL_MAINNET_RPC",
testNet?.admin_rpc ?? ""
);
VirtualTestNet.addToEnvFile(
"TENDERLY_TESTNET_UUID",
testNet?.vnet_id ?? ""
);
VirtualTestNet.addToEnvFile(
"TENDERLY_FORK_ID",
testNet?.network_id?.toString() ?? "1"
);
// overwrite Snapshot in .env
const output = execSync(`npm run save:vnet-snapshot`, {
stdio: "pipe",
}).toString();
console.log(output);
console.log(`Testnet ${testnet.selectedText} activated successfully`);
}
// deploy safes
if (action.selectedIndex == 4) {
// set roles version to v2
await VirtualTestNet.addToEnvFile("ROLES_VERSION", "v2");
// confirmation
console.log(
"Are you sure you want to deploy default safes to this testnet (Y/N):",
testnet.selectedText
);
const confirmDeploy = await terminal.yesOrNo().promise;
if (confirmDeploy?.valueOf()) {
console.log("\nDeploying default safes...");
const output = execSync(`npm run deploy:safes`, {
stdio: "pipe",
}).toString();
console.log(output);
console.log("\nDeployed default safes successfully");
}
}
// deploy whitelist
if (action.selectedIndex == 5) {
// set roles version to v2
await VirtualTestNet.addToEnvFile("ROLES_VERSION", "v2");
let output: any;
// show menu to select whitelisting options
const whitelistOptions = ["Whitelist all", "Whitelist one"];
const whitelistSelection =
await terminal.singleColumnMenu(whitelistOptions).promise;
// whitelist all
if (whitelistSelection.selectedIndex == 0) {
console.log(`\nWhitelisting all...`);
output = executeWithLogs(
`BYPASS_APPROVALS=true npm run deploy:whitelist && npm run save:vnet-snapshot`
);
} else if (whitelistSelection.selectedIndex == 1) {
// whitelist one
console.log(`\nWhitelisting one...`);
const permissionsFiles = getWhitelistsV2();
if (permissionsFiles.length === 0) {
console.error("No permissions files found.");
return;
}
const rolesDirectory = path.resolve(
"../access-control-safes-v2/src/roles"
);
const whitelistNames = permissionsFiles.map(file =>
path.relative(rolesDirectory, file)
);
const permissionSelection =
await terminal.singleColumnMenu(whitelistNames).promise;
const selectedFile = permissionsFiles.find(
file =>
path.relative(rolesDirectory, file) ===
permissionSelection.selectedText
);
if (!selectedFile) {
console.error(
"Could not find matching permissions file for selection"
);
return;
}
process.env.SELECTED_PERMISSIONS_FILE_V2 = selectedFile;
output = executeWithLogs(
`npm run execute:whitelist-v2-one && npm run save:vnet-snapshot`
);
}
if (output) {
if (!output?.success) {
console.error("Error details:", output?.error);
console.error("Error output:", output?.output);
} else {
console.log(output);
console.log("\nApplied whitelist successfully");
}
}
}
if (action.selectedIndex == 6) {
goto: await start();
}
terminal.processExit(0);
}
function executeWithLogs(command: string, options = {}) {
try {
// Merge default options with user provided options
const defaultOptions: ExecSyncOptionsWithStringEncoding = {
encoding: "utf8",
stdio: "pipe",
maxBuffer: 1024 * 1024 * 10, // 10MB buffer
...options,
};
// Execute the command and capture output
const output = execSync(command, defaultOptions);
return {
success: true,
output: stripAnsi(output.toString()),
error: null,
};
} catch (error: any) {
// Capture detailed error information
return {
success: false,
output: error.output ? error.output.toString() : null,
error: {
message: error.message,
status: error.status,
signal: error.signal,
stderr: error.stderr ? error.stderr.toString() : null,
stdout: error.stdout ? error.stdout.toString() : null,
command: error.cmd,
},
};
}
}
async function createNewTestnet(terminal: Terminal) {
terminal.reset("Enter the name of the new testnet: ");
const newTestnet = await terminal.inputField().promise;
// select chain
// @audit add more chains
terminal.yellow("\n\nSelect a Chain: \n");
const chains = ["Ethereum", "Base", "Polygon"];
const chainSelection = await terminal.singleColumnMenu(chains).promise;
let chain = 1;
if (chainSelection.selectedIndex == 1) {
chain = 8453;
} else if (chainSelection.selectedIndex == 2) {
chain = 137;
}
// set roles version to v2
await VirtualTestNet.addToEnvFile("ROLES_VERSION", "v2");
console.log(`create testnet: ${newTestnet}`);
const result = await VirtualTestNet.createVirtualTestNet(
newTestnet as string,
chain
);
// set env variables
await VirtualTestNet.addToEnvFile("TENDERLY_FORK_ID", chain.toString());
// get snapshot
const outputSnapshot = execSync(`npm run save:vnet-snapshot`, {
stdio: "pipe",
}).toString();
console.log(outputSnapshot);
return {
testnetName: newTestnet as string,
result,
};
}
function getWhitelistsV2() {
const rolesDirectory = path.resolve("../access-control-safes-v2/src/roles");
try {
// Check if directory exists before trying to read it
if (fs.existsSync(rolesDirectory)) {
return findPermissionsFiles(rolesDirectory);
} else {
console.error(
`Error: The directory ${rolesDirectory} does not exist. Please make sure the 'access-control-safes-v2' repository is a sibling to this one.`
);
return [];
}
} catch (error) {
console.error("Error finding permissions files:", error);
return [];
}
}