mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 09:50:01 +00:00
245 lines
7.3 KiB
TypeScript
245 lines
7.3 KiB
TypeScript
|
|
/**
|
||
|
|
* DataHaven utility functions for launching and managing validator nodes
|
||
|
|
*
|
||
|
|
* This module provides utilities for launching individual DataHaven validator nodes
|
||
|
|
* on demand, checking their status, and managing their lifecycle.
|
||
|
|
*
|
||
|
|
* @example
|
||
|
|
* ```typescript
|
||
|
|
* import { launchDatahavenValidator, TestAccounts } from "utils";
|
||
|
|
*
|
||
|
|
* // Launch a new Charlie validator node
|
||
|
|
* const charlieNode = await launchDatahavenValidator(TestAccounts.Charlie, {
|
||
|
|
* launchedNetwork: suite.getLaunchedNetwork()
|
||
|
|
* });
|
||
|
|
*
|
||
|
|
* console.log(`Charlie node launched on port ${charlieNode.publicPort}`);
|
||
|
|
* console.log(`WebSocket URL: ${charlieNode.wsUrl}`);
|
||
|
|
* ```
|
||
|
|
*
|
||
|
|
* @example
|
||
|
|
* ```typescript
|
||
|
|
* // Check if a node is already running before launching
|
||
|
|
* if (await isValidatorNodeRunning("charlie", "test-network")) {
|
||
|
|
* console.log("Charlie node is already running");
|
||
|
|
* } else {
|
||
|
|
* // Launch the node
|
||
|
|
* const node = await launchDatahavenValidator(TestAccounts.Charlie, options);
|
||
|
|
* }
|
||
|
|
* ```
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { $ } from "bun";
|
||
|
|
import { dataHavenServiceManagerAbi } from "contract-bindings";
|
||
|
|
import { logger, waitForContainerToStart } from "utils";
|
||
|
|
import { DEFAULT_SUBSTRATE_WS_PORT } from "utils/constants";
|
||
|
|
import { getPublicPort } from "utils/docker";
|
||
|
|
import { privateKeyToAccount } from "viem/accounts";
|
||
|
|
import type { LaunchedNetwork } from "../launcher/types/launchedNetwork";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Enum for test account names that are prefunded in substrate
|
||
|
|
*/
|
||
|
|
export enum TestAccounts {
|
||
|
|
Alice = "alice",
|
||
|
|
Bob = "bob",
|
||
|
|
Charlie = "charlie",
|
||
|
|
Dave = "dave",
|
||
|
|
Eve = "eve",
|
||
|
|
Ferdie = "ferdie"
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface ValidatorInfo {
|
||
|
|
publicKey: string;
|
||
|
|
privateKey: string;
|
||
|
|
solochainAddress: string;
|
||
|
|
solochainPrivateKey: string;
|
||
|
|
solochainAuthorityName: string;
|
||
|
|
isActive: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Information about a launched DataHaven validator node
|
||
|
|
*/
|
||
|
|
export interface LaunchedValidatorInfo {
|
||
|
|
nodeId: string;
|
||
|
|
containerName: string;
|
||
|
|
rpcUrl: string;
|
||
|
|
wsUrl: string;
|
||
|
|
publicPort: number;
|
||
|
|
internalPort: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Options for launching a DataHaven validator
|
||
|
|
*/
|
||
|
|
export interface LaunchValidatorOptions {
|
||
|
|
datahavenImageTag?: string;
|
||
|
|
launchedNetwork: LaunchedNetwork;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const COMMON_LAUNCH_ARGS = [
|
||
|
|
"--unsafe-force-node-key-generation",
|
||
|
|
"--tmp",
|
||
|
|
"--validator",
|
||
|
|
"--discover-local",
|
||
|
|
"--no-prometheus",
|
||
|
|
"--unsafe-rpc-external",
|
||
|
|
"--rpc-cors=all",
|
||
|
|
"--force-authoring",
|
||
|
|
"--no-telemetry",
|
||
|
|
"--enable-offchain-indexing=true"
|
||
|
|
];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Checks if a DataHaven validator node is already running
|
||
|
|
* @param nodeId - The node identifier (e.g., "alice", "bob")
|
||
|
|
* @param networkId - The network identifier
|
||
|
|
* @returns True if the node is running, false otherwise
|
||
|
|
*/
|
||
|
|
export const isValidatorNodeRunning = async (
|
||
|
|
nodeId: string,
|
||
|
|
networkId: string
|
||
|
|
): Promise<boolean> => {
|
||
|
|
const containerName = `datahaven-${nodeId}-${networkId}`;
|
||
|
|
const dockerPsOutput = await $`docker ps -q --filter "name=^${containerName}"`.text();
|
||
|
|
return dockerPsOutput.trim().length > 0;
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Launches a single DataHaven validator node on demand
|
||
|
|
* @param name - The test account name to launch
|
||
|
|
* @param options - Configuration options for launching the node
|
||
|
|
* @returns Information about the launched node
|
||
|
|
*/
|
||
|
|
export const launchDatahavenValidator = async (
|
||
|
|
name: TestAccounts,
|
||
|
|
options: LaunchValidatorOptions
|
||
|
|
): Promise<LaunchedValidatorInfo> => {
|
||
|
|
const nodeId = name.toLowerCase();
|
||
|
|
const networkId = options.launchedNetwork.networkId;
|
||
|
|
const datahavenImageTag = options.datahavenImageTag || "datahavenxyz/datahaven:local";
|
||
|
|
const containerName = `datahaven-${nodeId}-${networkId}`;
|
||
|
|
|
||
|
|
// Check if node is already running
|
||
|
|
if (await isValidatorNodeRunning(nodeId, networkId)) {
|
||
|
|
logger.warn(`⚠️ Node ${nodeId} is already running in network ${networkId}`);
|
||
|
|
|
||
|
|
// Get existing node info
|
||
|
|
const publicPort = await getPublicPort(containerName, DEFAULT_SUBSTRATE_WS_PORT);
|
||
|
|
return {
|
||
|
|
nodeId,
|
||
|
|
containerName,
|
||
|
|
rpcUrl: `http://127.0.0.1:${publicPort}`,
|
||
|
|
wsUrl: `ws://127.0.0.1:${publicPort}`,
|
||
|
|
publicPort,
|
||
|
|
internalPort: DEFAULT_SUBSTRATE_WS_PORT
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.info(`🚀 Launching DataHaven validator node: ${nodeId}...`);
|
||
|
|
|
||
|
|
// Get port mapping for the node
|
||
|
|
const portMapping = getPortMappingForNode(nodeId, networkId);
|
||
|
|
|
||
|
|
const command: string[] = [
|
||
|
|
"docker",
|
||
|
|
"run",
|
||
|
|
"-d",
|
||
|
|
"--name",
|
||
|
|
containerName,
|
||
|
|
"--network",
|
||
|
|
options.launchedNetwork.networkName,
|
||
|
|
...portMapping,
|
||
|
|
datahavenImageTag,
|
||
|
|
`--${nodeId}`,
|
||
|
|
...COMMON_LAUNCH_ARGS
|
||
|
|
];
|
||
|
|
|
||
|
|
logger.debug(await $`sh -c "${command.join(" ")}"`.text());
|
||
|
|
|
||
|
|
await waitForContainerToStart(containerName);
|
||
|
|
|
||
|
|
// Get the dynamic port and register in the network
|
||
|
|
const publicPort = await getPublicPort(containerName, DEFAULT_SUBSTRATE_WS_PORT);
|
||
|
|
|
||
|
|
// Add container to the launched network
|
||
|
|
options.launchedNetwork.addContainer(
|
||
|
|
containerName,
|
||
|
|
{ ws: publicPort },
|
||
|
|
{ ws: DEFAULT_SUBSTRATE_WS_PORT }
|
||
|
|
);
|
||
|
|
|
||
|
|
logger.success(`DataHaven validator node ${nodeId} launched successfully on port ${publicPort}`);
|
||
|
|
|
||
|
|
return {
|
||
|
|
nodeId,
|
||
|
|
containerName,
|
||
|
|
rpcUrl: `http://127.0.0.1:${publicPort}`,
|
||
|
|
wsUrl: `ws://127.0.0.1:${publicPort}`,
|
||
|
|
publicPort,
|
||
|
|
internalPort: DEFAULT_SUBSTRATE_WS_PORT
|
||
|
|
};
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Determines the port mapping for a DataHaven node based on the network type.
|
||
|
|
* Reused from launcher/datahaven.ts
|
||
|
|
* @param nodeId - The node identifier (e.g., "alice", "bob")
|
||
|
|
* @param networkId - The network identifier
|
||
|
|
* @returns Array of port mapping arguments for Docker run command
|
||
|
|
*/
|
||
|
|
const getPortMappingForNode = (nodeId: string, networkId: string): string[] => {
|
||
|
|
const isCliLaunch = networkId === "cli-launch";
|
||
|
|
|
||
|
|
if (isCliLaunch && nodeId === "alice") {
|
||
|
|
// For CLI-launch networks, only alice gets the fixed port mapping
|
||
|
|
return ["-p", `${DEFAULT_SUBSTRATE_WS_PORT}:${DEFAULT_SUBSTRATE_WS_PORT}`];
|
||
|
|
}
|
||
|
|
|
||
|
|
// For other networks or non-alice nodes, only expose internal port
|
||
|
|
// Docker will assign a random external port
|
||
|
|
return ["-p", `${DEFAULT_SUBSTRATE_WS_PORT}`];
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get node info by account name from validator set JSON
|
||
|
|
* @param validatorSetJson - Validator set JSON
|
||
|
|
* @param account - Test account name
|
||
|
|
* @returns Node info
|
||
|
|
*/
|
||
|
|
export const getValidatorInfoByName = (
|
||
|
|
validatorSetJson: any,
|
||
|
|
account: TestAccounts
|
||
|
|
): ValidatorInfo => {
|
||
|
|
const validatorsRaw = validatorSetJson.validators as Array<ValidatorInfo>;
|
||
|
|
const node = validatorsRaw.find((v) => v.solochainAuthorityName === account.toLowerCase());
|
||
|
|
if (!node) {
|
||
|
|
throw new Error(`Node ${account} not found in validator set`);
|
||
|
|
}
|
||
|
|
return node;
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Adds a validator to the EigenLayer allowlist
|
||
|
|
* @param connectors - The connectors to use
|
||
|
|
* @param validator - The validator to add to the allowlist
|
||
|
|
*/
|
||
|
|
export const addValidatorToAllowlist = async (
|
||
|
|
connectors: any,
|
||
|
|
validator: ValidatorInfo,
|
||
|
|
deployments: any
|
||
|
|
) => {
|
||
|
|
logger.info(`Adding validator ${validator.publicKey} to allowlist...`);
|
||
|
|
const hash = await connectors.walletClient.writeContract({
|
||
|
|
address: deployments.ServiceManager as `0x${string}`,
|
||
|
|
abi: dataHavenServiceManagerAbi,
|
||
|
|
functionName: "addValidatorToAllowlist",
|
||
|
|
args: [validator.publicKey as `0x${string}`],
|
||
|
|
account: privateKeyToAccount(validator.privateKey as `0x${string}`),
|
||
|
|
chain: null
|
||
|
|
});
|
||
|
|
await connectors.publicClient.waitForTransactionReceipt({ hash });
|
||
|
|
logger.info(`✅ Validator ${validator.publicKey} added to allowlist`);
|
||
|
|
};
|