UNPKG

tenderly-wizard-v6

Version:

A tool for managing virtual testnets using Tenderly

331 lines (283 loc) 9.71 kB
import fs from "fs"; import path from "path"; import config from "../env-config"; export class VirtualTestNet { async deleteVirtualTestNet(testnetId: string): Promise<void> { const { TENDERLY_ACCESS_TOKEN, TENDERLY_ACCOUNT, TENDERLY_PROJECT_ID } = config; if (!TENDERLY_ACCESS_TOKEN || !TENDERLY_ACCOUNT || !TENDERLY_PROJECT_ID) { throw new Error("Missing required Tenderly environment variables"); } // const url = `https://api.tenderly.co/api/v1/account/${TENDERLY_ACCOUNT}/project/${TENDERLY_PROJECT_ID}/testnet/container/${testnetId}`; const url = `https://api.tenderly.co/api/v1/account/${TENDERLY_ACCOUNT}/project/${TENDERLY_PROJECT_ID}/vnets/${testnetId}`; const headers = { Accept: "application/json", "X-Access-Key": TENDERLY_ACCESS_TOKEN!, }; try { const response = await fetch(url, { method: "DELETE", headers: headers, }); if (!response.ok) { const errorData = await response.json(); console.error("Error response:", errorData); throw new Error(`HTTP error! status: ${response.status}`); } console.log(`Virtual TestNet ${testnetId} deleted successfully.`); } catch (error) { console.error("Error deleting Virtual TestNet:", error); throw error; } } async createVirtualTestNet( testnetName: string, network_id: number = 1 ): Promise<{ admin_rpc: string; vnet_id: string } | void> { //get envs const { TENDERLY_ACCESS_TOKEN, TENDERLY_ACCOUNT, TENDERLY_PROJECT_ID } = config; const url = `https://api.tenderly.co/api/v1/account/${TENDERLY_ACCOUNT}/project/${TENDERLY_PROJECT_ID}/vnets`; const headers_ = { Accept: "application/json", "Content-Type": "application/json", "X-Access-Key": TENDERLY_ACCESS_TOKEN!, }; const data = { slug: testnetName, display_name: testnetName, fork_config: { network_id, }, virtual_network_config: { chain_config: { chain_id: 31337, }, }, sync_state_config: { enabled: false, }, explorer_page_config: { enabled: true, verification_visibility: "src", }, }; try { const response = await fetch(url, { method: "POST", headers: headers_, body: JSON.stringify(data), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } // this.removeEnvKeys(); const result: any = await response.json(); console.log("virtual testnet created"); const adminRpc: { name: string; url: string } = result.rpcs.find( (rpc: { name: string; url: string }) => rpc.name === "Admin RPC" ); this.addToEnvFile("VIRTUAL_MAINNET_RPC", adminRpc.url); this.addToEnvFile("TENDERLY_TESTNET_UUID", result.id); return { admin_rpc: adminRpc.url, vnet_id: result.id }; } catch (error) { console.error("Error creating Virtual TestNet:", error); } } async forkVirtualTestNet( sourceTestnetId: string, newTestnetName: string ): Promise<{ admin_rpc: string; vnet_id: string }> { const { TENDERLY_ACCESS_TOKEN, TENDERLY_ACCOUNT, TENDERLY_PROJECT_ID } = config; if (!TENDERLY_ACCESS_TOKEN || !TENDERLY_ACCOUNT || !TENDERLY_PROJECT_ID) { throw new Error("Missing required Tenderly environment variables"); } const url = `https://api.tenderly.co/api/v1/account/${TENDERLY_ACCOUNT}/project/${TENDERLY_PROJECT_ID}/testnet/clone`; const headers = { Accept: "application/json", "Content-Type": "application/json", "X-Access-Key": TENDERLY_ACCESS_TOKEN, }; const body = JSON.stringify({ srcContainerId: sourceTestnetId, dstContainerDisplayName: newTestnetName, }); try { const response = await fetch(url, { method: "POST", headers: headers, body: body, }); if (!response.ok) { const errorData = await response.json(); console.error("Error response:", errorData); throw new Error(`HTTP error! status: ${response.status}`); } const result: any = await response.json(); console.log( `Virtual TestNet forked successfully. New TestNet ID: ${result.id}` ); // Update .env file with new testnet information const admin_rpc = result.connectivityConfig.endpoints.find( (e: any) => e.transportProtocol == "HTTP" ).uri; await this.addToEnvFile("VIRTUAL_MAINNET_RPC", admin_rpc); await this.addToEnvFile("TENDERLY_TESTNET_UUID", result.id); return { admin_rpc, vnet_id: result.id, }; } catch (error) { console.error("Error forking Virtual TestNet:", error); throw error; } } async listVirtualTestnets(): Promise< { admin_rpc: string; vnet_id: string; displayName: string; network_id: number; }[] > { const { TENDERLY_ACCESS_TOKEN, TENDERLY_ACCOUNT, TENDERLY_PROJECT_ID } = config; if (!TENDERLY_ACCESS_TOKEN || !TENDERLY_ACCOUNT || !TENDERLY_PROJECT_ID) { throw new Error("Missing required Tenderly environment variables"); } const url = `https://api.tenderly.co/api/v1/account/${TENDERLY_ACCOUNT}/project/${TENDERLY_PROJECT_ID}/testnet/container?page=1&pageSize=20`; const headers = { Accept: "application/json", "Content-Type": "application/json", "X-Access-Key": TENDERLY_ACCESS_TOKEN, }; try { const response = await fetch(url, { method: "GET", headers: headers, }); if (!response.ok) { const errorData = await response.json(); console.error("Error response:", errorData); throw new Error(`HTTP error! status: ${response.status}`); } const result: ContainersResponse = await response.json(); return ( result?.containers?.map((container: Container) => { return { vnet_id: container.id, displayName: container.displayName, admin_rpc: container?.connectivityConfig?.endpoints?.find( (e: Endpoint) => e.transportProtocol == "HTTP" )?.uri || "", project: container?.metadata?.project_name || "", account: container?.metadata?.owner_name || "", fork_of: container?.metadata?.project_name || null, network_id: container?.networkConfig?.networkId || 0, }; }) || [] ); } catch (error) { console.error("Error Getting Virtual TestNets:", error); throw error; } } async getTestnet(name: string) { const vnets = (await this.listVirtualTestnets()) || []; return vnets.find(vnet => vnet?.displayName == name); } async addToEnvFile(key: string, value: string): Promise<void> { // write to .env file const envPath = path.resolve(process.cwd(), ".env"); let envContent = ""; if (fs.existsSync(envPath)) { envContent = fs.readFileSync(envPath, "utf8"); } const regex = new RegExp(`^${key}=.*$`, "m"); if (regex.test(envContent)) { envContent = envContent.replace(regex, `${key}=${value}`); } else { envContent += `\n${key}=${value}`; } fs.writeFileSync(envPath, envContent.trim() + "\n"); // change .env in current session process.env[key] = value; console.log( `Environment variable ${key} has been added/updated in .env file.` ); } // removeEnvKeys() { // const envPath = path.resolve(process.cwd(), ".env"); // const envConfig = dotenv.parse(fs.readFileSync(envPath)); // const keysToRemove = [ // "TENDERLY_SNAPSHOT", // "AC_SAFE", // "INV_SAFE", // "INV_ROLES", // "AC_ROLES", // ]; // keysToRemove.forEach((key) => { // delete envConfig[key]; // }); // const newEnvContent = Object.entries(envConfig) // .map(([key, value]) => `${key}=${value}`) // .join("\n"); // fs.writeFileSync(envPath, newEnvContent); // console.log("Specified keys removed from .env file"); // } } export default new VirtualTestNet(); interface Container { id: string; slug: string; displayName: string; description: string; templateSlugOrId: string; createdBy: string; owner: string; projectId: string; visibility: "TEAM" | string; containerType: "TEST_NET" | string; networkConfig: NetworkConfig; connectivityConfig: EndpointsConfig; maintenance: boolean; status: "RUNNING" | string; platformType: "VNET" | string; platformId: string; createdAt: string; updatedAt: string; executionStatus: "IN_PROGRESS" | string; explorer_config: object; metadata?: MetaData; // Optional as it doesn't appear in all objects } interface ContainersResponse { containers: Container[]; } type CommunicationProtocol = "JSON_RPC" | "REST" | "GRAPHQL" | string; // Add other possible protocols if known type TransportProtocol = "HTTP" | "HTTPS" | "WS" | "WSS" | string; // Add other possible transports if known interface Endpoint { id: string; slug: string; displayName: string; description: string; communicationProtocol: CommunicationProtocol; transportProtocol: TransportProtocol; private: boolean; isDefault: boolean; uri: string; createdAt: string; // or Date if you'll parse it updatedAt: string; // or Date if you'll parse it } interface EndpointsConfig { id: string; endpoints: Endpoint[]; } interface NetworkConfig { networkId: number; } interface MetaData { project_name: string; owner_name: string; }