tenderly-wizard-v6
Version:
A tool for managing virtual testnets using Tenderly
331 lines (283 loc) • 9.71 kB
text/typescript
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;
}