mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 09:50:01 +00:00
339 lines
12 KiB
TypeScript
339 lines
12 KiB
TypeScript
import { $ } from "bun";
|
|
import { getContainersMatchingImage, getPortFromKurtosis, logger } from "utils";
|
|
import { ParameterCollection } from "utils/parameters";
|
|
import { deployContracts } from "../../cli/handlers/launch/contracts";
|
|
import { launchLocalDataHavenSolochain } from "../datahaven";
|
|
import { getRunningKurtosisEnclaves, launchKurtosisNetwork } from "../kurtosis";
|
|
import { setDataHavenParameters } from "../parameters";
|
|
import { launchRelayers } from "../relayers";
|
|
import type { LaunchNetworkResult, NetworkLaunchOptions } from "../types";
|
|
import { LaunchedNetwork } from "../types/launchedNetwork";
|
|
import { checkBaseDependencies } from "../utils";
|
|
import { COMPONENTS } from "../utils/constants";
|
|
import { fundValidators, setupValidators } from "../validators";
|
|
|
|
// Authority IDs for test networks
|
|
const TEST_AUTHORITY_IDS = ["alice", "bob"] as const;
|
|
|
|
/**
|
|
* Validates that the network ID is unique and no resources with this ID exist.
|
|
* @throws {Error} if resources with the network ID already exist
|
|
*/
|
|
const validateNetworkIdUnique = async (networkId: string): Promise<void> => {
|
|
logger.info(`🔍 Validating network ID uniqueness: ${networkId}`);
|
|
|
|
// Check for existing DataHaven containers
|
|
const datahavenContainers = await getContainersMatchingImage(COMPONENTS.datahaven.imageName);
|
|
const conflictingDatahaven = datahavenContainers.filter((c) =>
|
|
c.Names.some((name) => name.includes(networkId))
|
|
);
|
|
if (conflictingDatahaven.length > 0) {
|
|
throw new Error(
|
|
`DataHaven containers with network ID '${networkId}' already exist. ` +
|
|
`Run 'bun cli stop --all' or remove containers manually.`
|
|
);
|
|
}
|
|
|
|
// Check for existing relayer containers
|
|
const relayerContainers = await getContainersMatchingImage(COMPONENTS.snowbridge.imageName);
|
|
const conflictingRelayers = relayerContainers.filter((c) =>
|
|
c.Names.some((name) => name.includes(networkId))
|
|
);
|
|
if (conflictingRelayers.length > 0) {
|
|
throw new Error(
|
|
`Relayer containers with network ID '${networkId}' already exist. ` +
|
|
`Run 'bun cli stop --all' or remove containers manually.`
|
|
);
|
|
}
|
|
|
|
// Check for existing Kurtosis enclaves
|
|
const enclaves = await getRunningKurtosisEnclaves();
|
|
const enclaveName = `eth-${networkId}`;
|
|
const conflictingEnclaves = enclaves.filter((e) => e.name === enclaveName);
|
|
if (conflictingEnclaves.length > 0) {
|
|
throw new Error(
|
|
`Kurtosis enclave '${enclaveName}' already exists. ` +
|
|
`Run 'kurtosis enclave rm ${enclaveName}' to remove it.`
|
|
);
|
|
}
|
|
|
|
// Check for existing Docker network
|
|
const dockerNetworkName = `datahaven-${networkId}`;
|
|
const networkOutput =
|
|
await $`docker network ls --filter "name=^${dockerNetworkName}$" --format "{{.Name}}"`.text();
|
|
if (networkOutput.trim()) {
|
|
throw new Error(
|
|
`Docker network '${dockerNetworkName}' already exists. ` +
|
|
`Run 'docker network rm ${dockerNetworkName}' to remove it.`
|
|
);
|
|
}
|
|
|
|
logger.success(`Network ID '${networkId}' is available`);
|
|
};
|
|
|
|
/**
|
|
* Creates a cleanup function for the test network.
|
|
*/
|
|
const createCleanupFunction = (networkId: string) => {
|
|
return async () => {
|
|
logger.info(`🧹 Cleaning up test network: ${networkId}`);
|
|
|
|
try {
|
|
// 1. Stop relayer containers
|
|
const relayerContainers = await getContainersMatchingImage(COMPONENTS.snowbridge.imageName);
|
|
const networkRelayers = relayerContainers.filter((c) =>
|
|
c.Names.some((name) => name.includes(networkId))
|
|
);
|
|
if (networkRelayers.length > 0) {
|
|
logger.info(`🔨 Stopping ${networkRelayers.length} relayer containers...`);
|
|
for (const container of networkRelayers) {
|
|
await $`docker stop ${container.Id}`.nothrow();
|
|
await $`docker rm ${container.Id}`.nothrow();
|
|
}
|
|
}
|
|
|
|
// 2. Stop DataHaven containers
|
|
const datahavenContainers = await getContainersMatchingImage(COMPONENTS.datahaven.imageName);
|
|
const networkDatahaven = datahavenContainers.filter((c) =>
|
|
c.Names.some((name) => name.includes(networkId))
|
|
);
|
|
if (networkDatahaven.length > 0) {
|
|
logger.info(`🔨 Stopping ${networkDatahaven.length} DataHaven containers...`);
|
|
for (const container of networkDatahaven) {
|
|
await $`docker stop ${container.Id}`.nothrow();
|
|
await $`docker rm ${container.Id}`.nothrow();
|
|
}
|
|
}
|
|
|
|
// 3. Remove Docker network
|
|
const dockerNetworkName = `datahaven-${networkId}`;
|
|
logger.info(`🔨 Removing Docker network: ${dockerNetworkName}`);
|
|
await $`docker network rm -f ${dockerNetworkName}`.nothrow();
|
|
|
|
// 4. Remove Kurtosis enclave
|
|
const enclaveName = `eth-${networkId}`;
|
|
logger.info(`🔨 Removing Kurtosis enclave: ${enclaveName}`);
|
|
await $`kurtosis enclave rm ${enclaveName} -f`.nothrow();
|
|
|
|
logger.success(`Cleanup completed for network: ${networkId}`);
|
|
} catch (error) {
|
|
logger.error(`❌ Cleanup failed for network ${networkId}:`, error);
|
|
// Continue cleanup, don't throw
|
|
}
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Launches a complete network stack for E2E testing.
|
|
*
|
|
* This function orchestrates the launch of all network components:
|
|
* 1. DataHaven blockchain nodes
|
|
* 2. Kurtosis Ethereum network
|
|
* 3. Smart contracts deployment
|
|
* 4. Validator setup
|
|
* 5. Runtime parameter configuration
|
|
* 6. Relayer services
|
|
* 7. Validator set update
|
|
*
|
|
* @param options - Configuration options for the network launch
|
|
* @returns NetworkConnectors with cleanup function
|
|
* @throws {Error} if network ID is not unique or any component fails to launch
|
|
*/
|
|
export const launchNetwork = async (
|
|
options: NetworkLaunchOptions
|
|
): Promise<LaunchNetworkResult> => {
|
|
const networkId = options.networkId;
|
|
const launchedNetwork = new LaunchedNetwork();
|
|
launchedNetwork.networkName = networkId;
|
|
|
|
let cleanup: (() => Promise<void>) | undefined;
|
|
|
|
try {
|
|
logger.info(`🚀 Launching complete network stack with ID: ${networkId}`);
|
|
const startTime = performance.now();
|
|
|
|
// Check base dependencies
|
|
await checkBaseDependencies();
|
|
|
|
// Validate network ID is unique
|
|
await validateNetworkIdUnique(networkId);
|
|
|
|
// Create cleanup function
|
|
cleanup = createCleanupFunction(networkId);
|
|
|
|
// Create parameter collection for use throughout the launch
|
|
const parameterCollection = new ParameterCollection();
|
|
|
|
// 1. Launch DataHaven network
|
|
logger.info("📦 Launching DataHaven network...");
|
|
await launchLocalDataHavenSolochain(
|
|
{
|
|
networkId,
|
|
datahavenImageTag: options.datahavenImageTag || "datahavenxyz/datahaven:local",
|
|
relayerImageTag: options.relayerImageTag || "datahavenxyz/snowbridge-relay:latest",
|
|
authorityIds: TEST_AUTHORITY_IDS,
|
|
buildDatahaven: options.buildDatahaven ?? !isCI, // if not specified, default to false for CI, true for local testing
|
|
datahavenBuildExtraArgs: options.datahavenBuildExtraArgs || "--features=fast-runtime"
|
|
},
|
|
launchedNetwork
|
|
);
|
|
|
|
// 2. Launch Ethereum/Kurtosis network
|
|
logger.info("⚡️ Launching Kurtosis Ethereum network...");
|
|
const kurtosisEnclaveName = `eth-${networkId}`;
|
|
await launchKurtosisNetwork(
|
|
{
|
|
kurtosisEnclaveName: kurtosisEnclaveName,
|
|
blockscout: options.blockscout ?? false,
|
|
slotTime: options.slotTime || 2,
|
|
kurtosisNetworkArgs: options.kurtosisNetworkArgs,
|
|
injectContracts: true // Forcing it to be true to run e2e tests
|
|
},
|
|
launchedNetwork
|
|
);
|
|
|
|
// 3. Deploy contracts
|
|
logger.info("📄 Deploying smart contracts...");
|
|
let blockscoutBackendUrl: string | undefined;
|
|
if (options.blockscout) {
|
|
const blockscoutPort = await getPortFromKurtosis("blockscout", "http", kurtosisEnclaveName);
|
|
blockscoutBackendUrl = `http://127.0.0.1:${blockscoutPort}`;
|
|
}
|
|
|
|
if (!launchedNetwork.elRpcUrl) {
|
|
throw new Error("Ethereum RPC URL not available");
|
|
}
|
|
|
|
await deployContracts({
|
|
rpcUrl: launchedNetwork.elRpcUrl,
|
|
verified: options.verified ?? false,
|
|
blockscoutBackendUrl,
|
|
parameterCollection,
|
|
deployContracts: false,
|
|
injectContracts: true // Because we are injecting contracts in kurtosis deployment
|
|
});
|
|
|
|
// 4. Fund validators
|
|
logger.info("💰 Funding validators...");
|
|
await fundValidators({
|
|
rpcUrl: launchedNetwork.elRpcUrl
|
|
});
|
|
|
|
// We are injecting contracts with already registers validator state
|
|
// // 5. Setup validators
|
|
// logger.info("🔐 Setting up validators...");
|
|
// await setupValidators({
|
|
// rpcUrl: launchedNetwork.elRpcUrl
|
|
// });
|
|
|
|
// We are injecting contracts but we still need the address
|
|
try {
|
|
const { parseDeploymentsFile, parseRewardsInfoFile } = await import("utils/contracts");
|
|
const deployments = await parseDeploymentsFile();
|
|
const gatewayAddress = deployments.Gateway;
|
|
const rewardsRegistryAddress = deployments.RewardsRegistry;
|
|
const rewardsInfo = await parseRewardsInfoFile();
|
|
const rewardsAgentOrigin = rewardsInfo.RewardsAgentOrigin;
|
|
const updateRewardsMerkleRootSelector = rewardsInfo.updateRewardsMerkleRootSelector;
|
|
|
|
if (gatewayAddress) {
|
|
logger.debug(
|
|
`📝 Reading EthereumGatewayAddress from existing deployment: ${gatewayAddress}`
|
|
);
|
|
parameterCollection.addParameter({
|
|
name: "EthereumGatewayAddress",
|
|
value: gatewayAddress
|
|
});
|
|
}
|
|
|
|
if (rewardsRegistryAddress) {
|
|
logger.debug(`📝 Adding RewardsRegistryAddress parameter: ${rewardsRegistryAddress}`);
|
|
parameterCollection.addParameter({
|
|
name: "RewardsRegistryAddress",
|
|
value: rewardsRegistryAddress
|
|
});
|
|
} else {
|
|
logger.warn("⚠️ RewardsRegistry address not found in deployments file");
|
|
}
|
|
|
|
if (updateRewardsMerkleRootSelector) {
|
|
logger.debug(
|
|
`📝 Adding RewardsUpdateSelector parameter: ${updateRewardsMerkleRootSelector}`
|
|
);
|
|
parameterCollection.addParameter({
|
|
name: "RewardsUpdateSelector",
|
|
value: updateRewardsMerkleRootSelector
|
|
});
|
|
} else {
|
|
logger.warn("⚠️ updateRewardsMerkleRootSelector not found in rewards info file");
|
|
}
|
|
|
|
if (rewardsAgentOrigin) {
|
|
logger.debug(`📝 Adding RewardsAgentOrigin parameter: ${rewardsAgentOrigin}`);
|
|
parameterCollection.addParameter({
|
|
name: "RewardsAgentOrigin",
|
|
value: rewardsAgentOrigin
|
|
});
|
|
} else {
|
|
logger.warn("⚠️ RewardsAgentOrigin not found in deployments file");
|
|
}
|
|
} catch (error) {
|
|
logger.error(`Failed to read Gateway address from deployments: ${error}`);
|
|
}
|
|
|
|
// 6. Set DataHaven runtime parameters
|
|
logger.info("⚙️ Setting DataHaven parameters...");
|
|
await setDataHavenParameters({
|
|
launchedNetwork,
|
|
collection: parameterCollection
|
|
});
|
|
|
|
// 7. Launch relayers
|
|
logger.info("❄️ Launching Snowbridge relayers...");
|
|
if (!options.relayerImageTag) {
|
|
throw new Error("Relayer image tag not specified");
|
|
}
|
|
|
|
await launchRelayers(
|
|
{
|
|
networkId,
|
|
relayerImageTag: options.relayerImageTag,
|
|
kurtosisEnclaveName
|
|
},
|
|
launchedNetwork
|
|
);
|
|
|
|
// Log success
|
|
const endTime = performance.now();
|
|
const minutes = ((endTime - startTime) / (1000 * 60)).toFixed(1);
|
|
logger.success(`Network launched successfully in ${minutes} minutes`);
|
|
|
|
// Validate required endpoints
|
|
if (!launchedNetwork.clEndpoint) {
|
|
throw new Error("Consensus layer endpoint not available");
|
|
}
|
|
|
|
// Return connectors
|
|
const aliceContainerName = `datahaven-alice-${networkId}`;
|
|
const wsPort = launchedNetwork.getContainerPort(aliceContainerName);
|
|
return {
|
|
launchedNetwork,
|
|
dataHavenRpcUrl: `http://127.0.0.1:${wsPort}`,
|
|
ethereumRpcUrl: launchedNetwork.elRpcUrl,
|
|
ethereumClEndpoint: launchedNetwork.clEndpoint,
|
|
cleanup
|
|
};
|
|
} catch (error) {
|
|
logger.error("❌ Failed to launch network", error);
|
|
|
|
// Run cleanup if we created it
|
|
if (cleanup) {
|
|
logger.info("🧹 Running cleanup due to launch failure...");
|
|
await cleanup();
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
export const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
|