mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 01:38:32 +00:00
test: 🐳 Add Docker relay support to CLI (#74)
## Changes
- Latest changes to have working relayer 🎉 component
- Changed spawning snowbridge relayers to docker containers
- Small logging output changes
- Refactoring to `LaunchedNetwork` class
- new flag `--bd` `--build-datahaven` which will build a local docker
container which is **much** quicker than the proper CI build (which uses
a controlled build enviroment)
- new bun script `start:e2e:local`, which is everything that
`start:e2e:ci` has, but with building local docker container and
log_level debug set
---
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
## Summary by CodeRabbit
- **New Features**
- Added support for launching and managing relayer and DataHaven
services using Docker containers and networks.
- Introduced a CLI option to specify the relayer Docker image tag
instead of a binary path.
- **Improvements**
- Enhanced log messages with clearer text and expressive emojis for
better user feedback.
- Improved summary display by removing relayer services from the output.
- Updated build scripts to consistently enable the "fast-runtime"
feature for cross-platform builds.
- Refined validation and error reporting for checkpoint data parsing.
- **Bug Fixes**
- Improved Docker container cleanup and network management during
service launch and teardown.
- **Chores**
- Updated and refactored npm scripts for Docker operations and
end-to-end test cleanup.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
This commit is contained in:
parent
a86791ec1c
commit
431d1f7181
18 changed files with 304 additions and 222 deletions
|
|
@ -8,13 +8,21 @@ import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat";
|
|||
import { getWsProvider } from "polkadot-api/ws-provider/web";
|
||||
import { cargoCrossbuild } from "scripts/cargo-crossbuild";
|
||||
import invariant from "tiny-invariant";
|
||||
import { waitForContainerToStart } from "utils";
|
||||
import { confirmWithTimeout, logger, printDivider, printHeader } from "utils";
|
||||
import {
|
||||
confirmWithTimeout,
|
||||
killExistingContainers,
|
||||
logger,
|
||||
printDivider,
|
||||
printHeader,
|
||||
waitForContainerToStart
|
||||
} from "utils";
|
||||
import { type Hex, keccak256, toHex } from "viem";
|
||||
import { publicKeyToAddress } from "viem/accounts";
|
||||
import type { LaunchOptions } from ".";
|
||||
import type { LaunchedNetwork } from "./launchedNetwork";
|
||||
|
||||
const DOCKER_NETWORK_NAME = "datahaven-net";
|
||||
|
||||
const LOG_LEVEL = Bun.env.LOG_LEVEL || "info";
|
||||
|
||||
const COMMON_LAUNCH_ARGS = [
|
||||
|
|
@ -75,8 +83,7 @@ export const launchDataHavenSolochain = async (
|
|||
}
|
||||
|
||||
if (options.datahaven === true) {
|
||||
logger.info("Proceeding to clean and relaunch DataHaven containers...");
|
||||
await cleanDataHavenContainers();
|
||||
await cleanDataHavenContainers(options);
|
||||
} else {
|
||||
const shouldRelaunch = await confirmWithTimeout(
|
||||
"Do you want to clean and relaunch the DataHaven containers?",
|
||||
|
|
@ -91,8 +98,7 @@ export const launchDataHavenSolochain = async (
|
|||
printDivider();
|
||||
return;
|
||||
}
|
||||
logger.info("Proceeding to clean and relaunch DataHaven containers...");
|
||||
await cleanDataHavenContainers();
|
||||
await cleanDataHavenContainers(options);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -104,7 +110,7 @@ export const launchDataHavenSolochain = async (
|
|||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Using flag option: ${shouldLaunchDataHaven ? "will launch" : "will not launch"} DataHaven network`
|
||||
`🏳️ Using flag option: ${shouldLaunchDataHaven ? "will launch" : "will not launch"} DataHaven network`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -114,13 +120,20 @@ export const launchDataHavenSolochain = async (
|
|||
return;
|
||||
}
|
||||
|
||||
logger.info(`⛓️💥 Creating Docker network: ${DOCKER_NETWORK_NAME}`);
|
||||
logger.debug(await $`docker network rm ${DOCKER_NETWORK_NAME} -f`.text());
|
||||
logger.debug(await $`docker network create ${DOCKER_NETWORK_NAME}`.text());
|
||||
|
||||
invariant(options.datahavenImageTag, "❌ DataHaven image tag not defined");
|
||||
|
||||
await buildLocalImage(options);
|
||||
await checkTagExists(options.datahavenImageTag);
|
||||
|
||||
launchedNetwork.networkName = DOCKER_NETWORK_NAME;
|
||||
logger.success(`DataHaven nodes will use Docker network: ${DOCKER_NETWORK_NAME}`);
|
||||
|
||||
for (const id of CLI_AUTHORITY_IDS) {
|
||||
logger.info(`Starting ${id}...`);
|
||||
logger.info(`🚀 Starting ${id}...`);
|
||||
const containerName = `datahaven-${id}`;
|
||||
|
||||
const command: string[] = [
|
||||
|
|
@ -129,13 +142,15 @@ export const launchDataHavenSolochain = async (
|
|||
"-d",
|
||||
"--name",
|
||||
containerName,
|
||||
"--network",
|
||||
DOCKER_NETWORK_NAME,
|
||||
...(id === "alice" ? ["-p", `${DEFAULT_PUBLIC_WS_PORT}:9944`] : []),
|
||||
options.datahavenImageTag,
|
||||
`--${id}`,
|
||||
...COMMON_LAUNCH_ARGS
|
||||
];
|
||||
|
||||
logger.debug($`sh -c "${command.join(" ")}"`.text());
|
||||
logger.debug(await $`sh -c "${command.join(" ")}"`.text());
|
||||
|
||||
await waitForContainerToStart(containerName);
|
||||
|
||||
|
|
@ -151,7 +166,7 @@ export const launchDataHavenSolochain = async (
|
|||
}
|
||||
|
||||
for (let i = 0; i < 30; i++) {
|
||||
logger.info("Waiting for datahaven to start...");
|
||||
logger.info("⌛️ Waiting for datahaven to start...");
|
||||
if (await isNetworkReady(DEFAULT_PUBLIC_WS_PORT)) {
|
||||
logger.success(
|
||||
`DataHaven network started, primary node accessible on port ${DEFAULT_PUBLIC_WS_PORT}`
|
||||
|
|
@ -160,7 +175,7 @@ export const launchDataHavenSolochain = async (
|
|||
await registerNodes(launchedNetwork);
|
||||
|
||||
// Call setupDataHavenValidatorConfig now that nodes are up
|
||||
logger.info("Proceeding with DataHaven validator configuration setup...");
|
||||
logger.info("🔧 Proceeding with DataHaven validator configuration setup...");
|
||||
await setupDataHavenValidatorConfig(launchedNetwork);
|
||||
|
||||
printDivider();
|
||||
|
|
@ -180,29 +195,42 @@ export const launchDataHavenSolochain = async (
|
|||
*/
|
||||
const checkDataHavenRunning = async (): Promise<boolean> => {
|
||||
// Check for any container whose name starts with "datahaven-"
|
||||
const PIDS = await $`docker ps -q --filter "name=^datahaven-"`.text();
|
||||
return PIDS.trim().length > 0;
|
||||
const containerIds = await $`docker ps -q --filter "name=^datahaven-"`.text();
|
||||
const networkOutput =
|
||||
await $`docker network ls --filter "name=^${DOCKER_NETWORK_NAME}$" --format "{{.Name}}"`.text();
|
||||
|
||||
// Check if containerIds has any actual IDs (not just whitespace)
|
||||
const containersExist = containerIds.trim().length > 0;
|
||||
// Check if networkOutput has any network names (not just whitespace or empty lines)
|
||||
const networksExist =
|
||||
networkOutput
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter((line) => line.trim().length > 0).length > 0;
|
||||
|
||||
return containersExist || networksExist;
|
||||
};
|
||||
|
||||
/**
|
||||
* Stops and removes all DataHaven containers.
|
||||
*/
|
||||
const cleanDataHavenContainers = async (): Promise<void> => {
|
||||
const cleanDataHavenContainers = async (options: LaunchOptions): Promise<void> => {
|
||||
logger.info("🧹 Stopping and removing existing DataHaven containers...");
|
||||
const containerIds = (await $`docker ps -a -q --filter "name=^datahaven-"`.text()).trim();
|
||||
logger.debug(`Container IDs: ${containerIds}`);
|
||||
if (containerIds.length > 0) {
|
||||
const idsArray = containerIds
|
||||
.split("\n")
|
||||
.map((id) => id.trim())
|
||||
.filter((id) => id.length > 0);
|
||||
for (const id of idsArray) {
|
||||
logger.debug(`Stopping container ${id}`);
|
||||
logger.debug(await $`docker stop ${id}`.nothrow().text());
|
||||
logger.debug(await $`docker rm ${id}`.nothrow().text());
|
||||
}
|
||||
|
||||
invariant(options.datahavenImageTag, "❌ DataHaven image tag not defined");
|
||||
await killExistingContainers(options.datahavenImageTag);
|
||||
|
||||
if (options.relayerImageTag) {
|
||||
logger.info(
|
||||
"🧹 Stopping and removing existing relayer containers (relayers depend on DataHaven nodes)..."
|
||||
);
|
||||
await killExistingContainers(options.relayerImageTag);
|
||||
}
|
||||
|
||||
logger.info("✅ Existing DataHaven containers stopped and removed.");
|
||||
|
||||
logger.debug(await $`docker network rm -f ${DOCKER_NETWORK_NAME}`.text());
|
||||
logger.info("✅ DataHaven Docker network removed.");
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -241,12 +269,12 @@ const buildLocalImage = async (options: LaunchOptions) => {
|
|||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Using flag option: ${shouldBuildDataHaven ? "will build" : "will not build"} DataHaven node local Docker image`
|
||||
`🏳️ Using flag option: ${shouldBuildDataHaven ? "will build" : "will not build"} DataHaven node local Docker image`
|
||||
);
|
||||
}
|
||||
|
||||
if (!shouldBuildDataHaven) {
|
||||
logger.info("Skipping DataHaven node local Docker image build. Done!");
|
||||
logger.info("👍 Skipping DataHaven node local Docker image build. Done!");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -302,11 +330,11 @@ const registerNodes = async (launchedNetwork: LaunchedNetwork) => {
|
|||
|
||||
// If the Docker container is running, proceed to register it in launchedNetwork.
|
||||
// We use the standard host WS port that "datahaven-alice" is expected to use.
|
||||
logger.info(
|
||||
`✅ Docker container ${targetContainerName} is running. Registering with WS port ${aliceHostWsPort}.`
|
||||
logger.debug(
|
||||
`Docker container ${targetContainerName} is running. Registering with WS port ${aliceHostWsPort}.`
|
||||
);
|
||||
launchedNetwork.addContainer(targetContainerName, { ws: aliceHostWsPort });
|
||||
logger.success(`👍 Node ${targetContainerName} successfully registered in launchedNetwork.`);
|
||||
logger.info(`📝 Node ${targetContainerName} successfully registered in launchedNetwork.`);
|
||||
};
|
||||
|
||||
// Function to convert compressed public key to Ethereum address
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export interface LaunchOptions {
|
|||
updateValidatorSet?: boolean;
|
||||
blockscout?: boolean;
|
||||
relayer?: boolean;
|
||||
relayerBinPath?: string;
|
||||
relayerImageTag?: string;
|
||||
skipCleaning?: boolean;
|
||||
alwaysClean?: boolean;
|
||||
datahaven?: boolean;
|
||||
|
|
@ -75,7 +75,7 @@ const launchFunction = async (options: LaunchOptions, launchedNetwork: LaunchedN
|
|||
|
||||
await launchRelayers(options, launchedNetwork);
|
||||
|
||||
performSummaryOperations(options, launchedNetwork);
|
||||
await performSummaryOperations(options, launchedNetwork);
|
||||
const fullEnd = performance.now();
|
||||
const fullMinutes = ((fullEnd - timeStart) / (1000 * 60)).toFixed(1);
|
||||
logger.success(`Launch function completed successfully in ${fullMinutes} minutes`);
|
||||
|
|
|
|||
|
|
@ -22,16 +22,14 @@ export const launchKurtosis = async (
|
|||
|
||||
logger.trace("Checking if launchKurtosis option was set via flags");
|
||||
if (options.launchKurtosis === false) {
|
||||
logger.info("Keeping existing Kurtosis enclave.");
|
||||
logger.info("👍 Keeping existing Kurtosis enclave.");
|
||||
|
||||
await registerServices(launchedNetwork);
|
||||
printDivider();
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.launchKurtosis === true) {
|
||||
logger.info("Proceeding to clean and relaunch the Kurtosis enclave...");
|
||||
} else {
|
||||
if (options.launchKurtosis !== true) {
|
||||
const shouldRelaunch = await confirmWithTimeout(
|
||||
"Do you want to clean and relaunch the Kurtosis enclave?",
|
||||
true,
|
||||
|
|
@ -39,14 +37,12 @@ export const launchKurtosis = async (
|
|||
);
|
||||
|
||||
if (!shouldRelaunch) {
|
||||
logger.info("Keeping existing Kurtosis enclave.");
|
||||
logger.info("👍 Keeping existing Kurtosis enclave.");
|
||||
|
||||
await registerServices(launchedNetwork);
|
||||
printDivider();
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("Proceeding to clean and relaunch the Kurtosis enclave...");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -142,14 +138,14 @@ const modifyConfig = async (options: LaunchOptions, configFile: string) => {
|
|||
* @param launchedNetwork - The LaunchedNetwork instance to store network details.
|
||||
*/
|
||||
const registerServices = async (launchedNetwork: LaunchedNetwork) => {
|
||||
logger.info("⚙️ Registering Kurtosis service endpoints...");
|
||||
logger.info("📝 Registering Kurtosis service endpoints...");
|
||||
|
||||
// Configure EL RPC URL
|
||||
const rethPublicPort = await getPortFromKurtosis("el-1-reth-lighthouse", "rpc");
|
||||
invariant(rethPublicPort && rethPublicPort > 0, "❌ Could not find EL RPC port");
|
||||
const elRpcUrl = `http://127.0.0.1:${rethPublicPort}`;
|
||||
launchedNetwork.elRpcUrl = elRpcUrl;
|
||||
logger.info(`👍 Execution Layer RPC URL configured: ${elRpcUrl}`);
|
||||
logger.info(`📝 Execution Layer RPC URL configured: ${elRpcUrl}`);
|
||||
|
||||
// Configure CL Endpoint
|
||||
const lighthousePublicPort = await getPortFromKurtosis("cl-1-lighthouse-reth", "http");
|
||||
|
|
@ -159,5 +155,5 @@ const registerServices = async (launchedNetwork: LaunchedNetwork) => {
|
|||
"❌ CL Endpoint could not be determined from Kurtosis service cl-1-lighthouse-reth"
|
||||
);
|
||||
launchedNetwork.clEndpoint = clEndpoint;
|
||||
logger.info(`👍 Consensus Layer Endpoint configured: ${clEndpoint}`);
|
||||
logger.info(`📝 Consensus Layer Endpoint configured: ${clEndpoint}`);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export class LaunchedNetwork {
|
|||
protected processes: BunProcess[];
|
||||
protected _containers: ContainerSpec[];
|
||||
protected fileDescriptors: number[];
|
||||
protected _networkName: string;
|
||||
protected _activeRelayers: RelayerType[];
|
||||
/** The RPC URL for the Ethereum Execution Layer (EL) client. */
|
||||
protected _elRpcUrl?: string;
|
||||
|
|
@ -27,10 +28,20 @@ export class LaunchedNetwork {
|
|||
this.fileDescriptors = [];
|
||||
this._containers = [];
|
||||
this._activeRelayers = [];
|
||||
this._networkName = "";
|
||||
this._elRpcUrl = undefined;
|
||||
this._clEndpoint = undefined;
|
||||
}
|
||||
|
||||
public set networkName(name: string) {
|
||||
invariant(name.trim().length > 0, "❌ networkName cannot be empty");
|
||||
this._networkName = name.trim();
|
||||
}
|
||||
|
||||
public get networkName(): string {
|
||||
return this._networkName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the unique ID for this run of the launched network.
|
||||
* @returns The run ID string.
|
||||
|
|
@ -73,12 +84,6 @@ export class LaunchedNetwork {
|
|||
this._containers.push({ name: containerName, publicPorts });
|
||||
}
|
||||
|
||||
registerRelayerType(type: RelayerType): void {
|
||||
if (!this._activeRelayers.includes(type)) {
|
||||
this._activeRelayers.push(type);
|
||||
}
|
||||
}
|
||||
|
||||
public getPublicWsPort(): number {
|
||||
logger.debug("Getting public WebSocket port for LaunchedNetwork");
|
||||
logger.debug("Containers:");
|
||||
|
|
@ -88,13 +93,6 @@ export class LaunchedNetwork {
|
|||
return port;
|
||||
}
|
||||
|
||||
public get containers(): ContainerSpec[] {
|
||||
return this._containers;
|
||||
}
|
||||
|
||||
public get relayers(): RelayerType[] {
|
||||
return [...this._activeRelayers];
|
||||
}
|
||||
/**
|
||||
* Sets the RPC URL for the Ethereum Execution Layer (EL) client.
|
||||
* @param url - The EL RPC URL string.
|
||||
|
|
@ -131,6 +129,20 @@ export class LaunchedNetwork {
|
|||
return this._clEndpoint;
|
||||
}
|
||||
|
||||
registerRelayerType(type: RelayerType): void {
|
||||
if (!this._activeRelayers.includes(type)) {
|
||||
this._activeRelayers.push(type);
|
||||
}
|
||||
}
|
||||
|
||||
public get containers(): ContainerSpec[] {
|
||||
return this._containers;
|
||||
}
|
||||
|
||||
public get relayers(): RelayerType[] {
|
||||
return [...this._activeRelayers];
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
logger.debug("Running cleanup");
|
||||
for (const process of this.processes) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { datahaven } from "@polkadot-api/descriptors";
|
||||
import { $ } from "bun";
|
||||
|
|
@ -13,11 +12,14 @@ import {
|
|||
confirmWithTimeout,
|
||||
getEvmEcdsaSigner,
|
||||
getPortFromKurtosis,
|
||||
killExistingContainers,
|
||||
logger,
|
||||
parseDeploymentsFile,
|
||||
parseRelayConfig,
|
||||
printDivider,
|
||||
printHeader
|
||||
printHeader,
|
||||
runShellCommandWithLogger,
|
||||
waitForContainerToStart
|
||||
} from "utils";
|
||||
import type { BeaconCheckpoint, FinalityCheckpointsResponse } from "utils/types";
|
||||
import { parseJsonToBeaconCheckpoint } from "utils/types";
|
||||
|
|
@ -38,7 +40,9 @@ const RELAYER_CONFIG_PATHS = {
|
|||
BEACON: path.join(RELAYER_CONFIG_DIR, "beacon-relay.json"),
|
||||
BEEFY: path.join(RELAYER_CONFIG_DIR, "beefy-relay.json")
|
||||
};
|
||||
const INITIAL_CHECKPOINT_PATH = "./dump-initial-checkpoint.json";
|
||||
const INITIAL_CHECKPOINT_FILE = "dump-initial-checkpoint.json";
|
||||
const INITIAL_CHECKPOINT_DIR = "tmp/beacon-checkpoint";
|
||||
const INITIAL_CHECKPOINT_PATH = path.join(INITIAL_CHECKPOINT_DIR, INITIAL_CHECKPOINT_FILE);
|
||||
|
||||
/**
|
||||
* Launches Snowbridge relayers for the DataHaven network.
|
||||
|
|
@ -58,7 +62,7 @@ export const launchRelayers = async (options: LaunchOptions, launchedNetwork: La
|
|||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Using flag option: ${shouldLaunchRelayers ? "will launch" : "will not launch"} Snowbridge relayers`
|
||||
`🏳️ Using flag option: ${shouldLaunchRelayers ? "will launch" : "will not launch"} Snowbridge relayers`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -90,8 +94,8 @@ export const launchRelayers = async (options: LaunchOptions, launchedNetwork: La
|
|||
);
|
||||
}
|
||||
|
||||
// Kill any pre-existing relayer processes if they exist
|
||||
await $`pkill snowbridge-relay`.nothrow().quiet();
|
||||
invariant(options.relayerImageTag, "❌ relayerImageTag is required");
|
||||
await killExistingContainers(options.relayerImageTag);
|
||||
|
||||
// Check if BEEFY is ready before proceeding
|
||||
await waitBeefyReady(launchedNetwork, 2000, 60000);
|
||||
|
|
@ -109,10 +113,6 @@ export const launchRelayers = async (options: LaunchOptions, launchedNetwork: La
|
|||
logger.debug(`Ensuring datastore directory exists: ${datastorePath}`);
|
||||
await $`mkdir -p ${datastorePath}`.quiet();
|
||||
|
||||
const logsPath = `tmp/logs/${launchedNetwork.getRunId()}/`;
|
||||
logger.debug(`Ensuring logs directory exists: ${logsPath}`);
|
||||
await $`mkdir -p ${logsPath}`.quiet();
|
||||
|
||||
const relayersToStart: RelayerSpec[] = [
|
||||
{
|
||||
name: "relayer-🥩",
|
||||
|
|
@ -139,7 +139,7 @@ export const launchRelayers = async (options: LaunchOptions, launchedNetwork: La
|
|||
|
||||
logger.debug(`Creating config for ${name}`);
|
||||
const templateFilePath = `configs/snowbridge/${configFileName}`;
|
||||
const outputFilePath = `tmp/configs/${configFileName}`;
|
||||
const outputFilePath = path.resolve(RELAYER_CONFIG_DIR, configFileName);
|
||||
logger.debug(`Reading config file ${templateFilePath}`);
|
||||
const file = Bun.file(templateFilePath);
|
||||
|
||||
|
|
@ -157,18 +157,17 @@ export const launchRelayers = async (options: LaunchOptions, launchedNetwork: La
|
|||
|
||||
if (type === "beacon") {
|
||||
const cfg = parseRelayConfig(json, type);
|
||||
cfg.source.beacon.endpoint = `http://127.0.0.1:${ethHttpPort}`;
|
||||
cfg.source.beacon.stateEndpoint = `http://127.0.0.1:${ethHttpPort}`;
|
||||
cfg.source.beacon.endpoint = `http://host.docker.internal:${ethHttpPort}`;
|
||||
cfg.source.beacon.stateEndpoint = `http://host.docker.internal:${ethHttpPort}`;
|
||||
cfg.source.beacon.datastore.location = "/data";
|
||||
cfg.sink.parachain.endpoint = `ws://${substrateNodeId}:${substrateWsPort}`;
|
||||
|
||||
cfg.source.beacon.datastore.location = datastorePath;
|
||||
|
||||
cfg.sink.parachain.endpoint = `ws://127.0.0.1:${substrateWsPort}`;
|
||||
await Bun.write(outputFilePath, JSON.stringify(cfg, null, 4));
|
||||
logger.success(`Updated beacon config written to ${outputFilePath}`);
|
||||
} else {
|
||||
const cfg = parseRelayConfig(json, type);
|
||||
cfg.source.polkadot.endpoint = `ws://127.0.0.1:${substrateWsPort}`;
|
||||
cfg.sink.ethereum.endpoint = `ws://127.0.0.1:${ethWsPort}`;
|
||||
cfg.source.polkadot.endpoint = `ws://${substrateNodeId}:${substrateWsPort}`;
|
||||
cfg.sink.ethereum.endpoint = `ws://host.docker.internal:${ethWsPort}`;
|
||||
cfg.sink.contracts.BeefyClient = beefyClientAddress;
|
||||
cfg.sink.contracts.Gateway = gatewayAddress;
|
||||
await Bun.write(outputFilePath, JSON.stringify(cfg, null, 4));
|
||||
|
|
@ -176,27 +175,43 @@ export const launchRelayers = async (options: LaunchOptions, launchedNetwork: La
|
|||
}
|
||||
}
|
||||
|
||||
logger.info("Spawning Snowbridge relayers processes");
|
||||
|
||||
invariant(options.relayerBinPath, "❌ Relayer binary path not defined");
|
||||
invariant(
|
||||
await Bun.file(options.relayerBinPath).exists(),
|
||||
`❌ Relayer binary does not exist at ${options.relayerBinPath}`
|
||||
);
|
||||
invariant(options.relayerImageTag, "❌ Relayer image tag not defined");
|
||||
|
||||
await initEthClientPallet(options, launchedNetwork);
|
||||
|
||||
for (const { config, name, type, pk } of relayersToStart) {
|
||||
try {
|
||||
logger.info(`Starting relayer ${name} ...`);
|
||||
const logFileName = `${type}-${name.replace(/[^a-zA-Z0-9-]/g, "")}.log`;
|
||||
const logFilePath = path.join(logsPath, logFileName);
|
||||
logger.debug(`Writing logs to ${logFilePath}`);
|
||||
const containerName = `snowbridge-${type}-relay`;
|
||||
logger.info(`🚀 Starting relayer ${containerName} ...`);
|
||||
|
||||
const fd = fs.openSync(logFilePath, "a");
|
||||
const hostConfigFilePath = path.resolve(config);
|
||||
const containerConfigFilePath = `/${config}`;
|
||||
const networkName = launchedNetwork.networkName;
|
||||
invariant(networkName, "❌ Docker network name not found in LaunchedNetwork instance");
|
||||
|
||||
const spawnCommand = [
|
||||
options.relayerBinPath,
|
||||
const commandBase: string[] = [
|
||||
"docker",
|
||||
"run",
|
||||
"-d",
|
||||
"--platform",
|
||||
"linux/amd64",
|
||||
"--add-host",
|
||||
"host.docker.internal:host-gateway",
|
||||
"--name",
|
||||
containerName,
|
||||
"--network",
|
||||
networkName
|
||||
];
|
||||
|
||||
const volumeMounts: string[] = ["-v", `${hostConfigFilePath}:${containerConfigFilePath}`];
|
||||
|
||||
if (type === "beacon") {
|
||||
const hostDatastorePath = path.resolve(datastorePath);
|
||||
const containerDatastorePath = "/data";
|
||||
volumeMounts.push("-v", `${hostDatastorePath}:${containerDatastorePath}`);
|
||||
}
|
||||
|
||||
const relayerCommandArgs: string[] = [
|
||||
"run",
|
||||
type,
|
||||
"--config",
|
||||
|
|
@ -205,17 +220,28 @@ export const launchRelayers = async (options: LaunchOptions, launchedNetwork: La
|
|||
pk.value
|
||||
];
|
||||
|
||||
logger.debug(`Spawning command: ${spawnCommand.join(" ")}`);
|
||||
const command: string[] = [
|
||||
...commandBase,
|
||||
...volumeMounts,
|
||||
options.relayerImageTag,
|
||||
...relayerCommandArgs
|
||||
];
|
||||
|
||||
const process = Bun.spawn(spawnCommand, {
|
||||
stdout: fd,
|
||||
stderr: fd
|
||||
});
|
||||
logger.debug(`Running command: ${command.join(" ")}`);
|
||||
await runShellCommandWithLogger(command.join(" "), { logLevel: "debug" });
|
||||
|
||||
process.unref();
|
||||
launchedNetwork.addContainer(containerName);
|
||||
|
||||
await waitForContainerToStart(containerName);
|
||||
|
||||
// TODO: Re-enable when we know what we want to tail for
|
||||
// await waitForLog({
|
||||
// searchString: "<LOG LINE TO WAIT FOR>",
|
||||
// containerName,
|
||||
// timeoutSeconds: 30,
|
||||
// tail: 1
|
||||
// });
|
||||
|
||||
launchedNetwork.addFileDescriptor(fd);
|
||||
launchedNetwork.addProcess(process);
|
||||
logger.debug(`Started relayer ${name} with process ${process.pid}`);
|
||||
} catch (e) {
|
||||
logger.error(`Error starting relayer ${name}`);
|
||||
|
|
@ -244,7 +270,7 @@ const waitBeefyReady = async (
|
|||
const wsUrl = `ws://127.0.0.1:${port}`;
|
||||
const maxAttempts = Math.floor(timeoutMs / pollIntervalMs);
|
||||
|
||||
logger.info(`Waiting for BEEFY to be ready on port ${port}...`);
|
||||
logger.info(`⌛️ Waiting for BEEFY to be ready on port ${port}...`);
|
||||
|
||||
let client: PolkadotClient | undefined;
|
||||
try {
|
||||
|
|
@ -256,7 +282,7 @@ const waitBeefyReady = async (
|
|||
const finalizedHeadHex = await client._request<string>("beefy_getFinalizedHead", []);
|
||||
|
||||
if (finalizedHeadHex && finalizedHeadHex !== ZERO_HASH) {
|
||||
logger.success(`🥩 BEEFY is ready. Finalized head: ${finalizedHeadHex}`);
|
||||
logger.info(`🥩 BEEFY is ready. Finalized head: ${finalizedHeadHex}`);
|
||||
client.destroy();
|
||||
return;
|
||||
}
|
||||
|
|
@ -296,24 +322,40 @@ export const initEthClientPallet = async (
|
|||
options: LaunchOptions,
|
||||
launchedNetwork: LaunchedNetwork
|
||||
) => {
|
||||
logger.debug("Initialising eth client pallet");
|
||||
// Poll the beacon chain until it's ready every 10 seconds for 5 minutes
|
||||
await waitBeaconChainReady(launchedNetwork, 10000, 300000);
|
||||
|
||||
// Generate the initial checkpoint for the CL client in Substrate
|
||||
const { stdout, stderr, exitCode } =
|
||||
await $`${options.relayerBinPath} generate-beacon-checkpoint --config ${RELAYER_CONFIG_PATHS.BEACON} --export-json`
|
||||
.nothrow()
|
||||
.quiet();
|
||||
if (exitCode !== 0) {
|
||||
logger.error(stderr);
|
||||
throw new Error("Error generating beacon checkpoint");
|
||||
}
|
||||
logger.trace(`Beacon checkpoint stdout: ${stdout}`);
|
||||
const beaconConfigHostPath = path.resolve(RELAYER_CONFIG_PATHS.BEACON);
|
||||
const beaconConfigContainerPath = `/app/${RELAYER_CONFIG_PATHS.BEACON}`;
|
||||
const checkpointHostPath = path.resolve(INITIAL_CHECKPOINT_PATH);
|
||||
const checkpointContainerPath = `/app/${INITIAL_CHECKPOINT_FILE}`;
|
||||
|
||||
logger.debug("Generating beacon checkpoint");
|
||||
// Pre-create the checkpoint file so that Docker doesn't interpret it as a directory
|
||||
await Bun.write(INITIAL_CHECKPOINT_PATH, "");
|
||||
|
||||
logger.debug("Removing 'generate-beacon-checkpoint' container if it exists");
|
||||
logger.debug(await $`docker rm -f generate-beacon-checkpoint`.text());
|
||||
|
||||
logger.debug("Generating beacon checkpoint");
|
||||
const command = `docker run \
|
||||
-v ${beaconConfigHostPath}:${beaconConfigContainerPath}:ro \
|
||||
-v ${checkpointHostPath}:${checkpointContainerPath} \
|
||||
--name generate-beacon-checkpoint \
|
||||
--workdir /app \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--network ${launchedNetwork.networkName} \
|
||||
${options.relayerImageTag} \
|
||||
generate-beacon-checkpoint --config ${RELAYER_CONFIG_PATHS.BEACON} --export-json`;
|
||||
logger.debug(`Running command: ${command}`);
|
||||
logger.debug(await $`sh -c "${command}"`.text());
|
||||
|
||||
// Load the checkpoint into a JSON object and clean it up
|
||||
const initialCheckpointRaw = fs.readFileSync(INITIAL_CHECKPOINT_PATH, "utf-8");
|
||||
const initialCheckpointFile = Bun.file(INITIAL_CHECKPOINT_PATH);
|
||||
const initialCheckpointRaw = await initialCheckpointFile.text();
|
||||
const initialCheckpoint = parseJsonToBeaconCheckpoint(JSON.parse(initialCheckpointRaw));
|
||||
fs.unlinkSync(INITIAL_CHECKPOINT_PATH);
|
||||
await initialCheckpointFile.delete();
|
||||
|
||||
logger.trace("Initial checkpoint:");
|
||||
logger.trace(initialCheckpoint.toJSON());
|
||||
|
|
@ -321,6 +363,7 @@ export const initEthClientPallet = async (
|
|||
// Send the checkpoint to the Substrate runtime
|
||||
const substrateRpcUrl = `http://127.0.0.1:${launchedNetwork.getPublicWsPort()}`;
|
||||
await sendCheckpointToSubstrate(substrateRpcUrl, initialCheckpoint);
|
||||
logger.success("Ethereum Beacon Client pallet initialised");
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -19,11 +19,6 @@ export const performSummaryOperations = async (
|
|||
servicesToDisplay.push("datahaven-alice");
|
||||
}
|
||||
|
||||
const activeRelayers = launchedNetwork.relayers;
|
||||
for (const relayer of activeRelayers) {
|
||||
servicesToDisplay.push(`${relayer}-relayer`);
|
||||
}
|
||||
|
||||
logger.trace("Services to display", servicesToDisplay);
|
||||
|
||||
const displayData: { service: string; ports: Record<string, number>; url: string }[] = [];
|
||||
|
|
@ -101,24 +96,6 @@ export const performSummaryOperations = async (
|
|||
break;
|
||||
}
|
||||
|
||||
case service === "beefy-relayer": {
|
||||
displayData.push({
|
||||
service,
|
||||
ports: {},
|
||||
url: "Background process (connects to other services)"
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case service === "beacon-relayer": {
|
||||
displayData.push({
|
||||
service,
|
||||
ports: {},
|
||||
url: "Background process (connects to other services)"
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
logger.error(`Unknown service: ${service}`);
|
||||
}
|
||||
|
|
@ -127,8 +104,10 @@ export const performSummaryOperations = async (
|
|||
|
||||
const containers = launchedNetwork.containers.filter((c) => !c.name.startsWith("datahaven-"));
|
||||
for (const { name, publicPorts } of containers) {
|
||||
const url = "ws" in publicPorts ? `ws://127.0.0.1:${publicPorts.ws}` : "un-exposed";
|
||||
displayData.push({ service: name, ports: publicPorts, url });
|
||||
const url = "ws" in publicPorts ? `ws://127.0.0.1:${publicPorts.ws}` : undefined;
|
||||
if (url) {
|
||||
displayData.push({ service: name, ports: publicPorts, url });
|
||||
}
|
||||
}
|
||||
|
||||
console.table(displayData);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export const performValidatorOperations = async (
|
|||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Using flag option: ${shouldFundValidators ? "will fund" : "will not fund"} validators`
|
||||
`🏳️ Using flag option: ${shouldFundValidators ? "will fund" : "will not fund"} validators`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ export const performValidatorOperations = async (
|
|||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Using flag option: ${shouldSetupValidators ? "will register" : "will not register"} validators`
|
||||
`🏳️ Using flag option: ${shouldSetupValidators ? "will register" : "will not register"} validators`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -73,7 +73,7 @@ export const performValidatorOperations = async (
|
|||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Using flag option: ${shouldUpdateValidatorSet ? "will update" : "will not update"} validator set`
|
||||
`🏳️ Using flag option: ${shouldUpdateValidatorSet ? "will update" : "will not update"} validator set`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,11 @@ const program = new Command()
|
|||
"Tag of the datahaven image to use",
|
||||
"moonsonglabs/datahaven:local"
|
||||
)
|
||||
.option("--relayer-bin-path <value>", "Path to the relayer binary", "tmp/bin/snowbridge-relay")
|
||||
.option(
|
||||
"-p, --relayer-image-tag <value>",
|
||||
"Tag of the relayer",
|
||||
"moonsonglabs/snowbridge-relayer:latest"
|
||||
)
|
||||
.hook("preAction", launchPreActionHook)
|
||||
.action(launch);
|
||||
|
||||
|
|
|
|||
|
|
@ -8,16 +8,19 @@
|
|||
"fmt": "biome check .",
|
||||
"fmt:fix": "biome check --write .",
|
||||
"build:docker:operator": "docker build -t moonsonglabs/datahaven:local -f ./docker/datahaven-node-local.dockerfile ../.",
|
||||
"build:docker:operator:timbo": "docker build -t moonsonglabs/datahaven:local -f ./docker/Local.Dockerfile ../.",
|
||||
"build:docker:relayer": "bun -e \"import build from './scripts/snowbridge-relayer.ts'; build()\"",
|
||||
"generate:wagmi": "wagmi generate",
|
||||
"generate:snowbridge-cfgs": "bun -e \"import {generateSnowbridgeConfigs} from './scripts/gen-snowbridge-cfgs.ts'; await generateSnowbridgeConfigs()\"",
|
||||
"generate:types": "(cd ../operator && cargo build --release) && bun x papi add --wasm \"../operator/target/release/wbuild/datahaven-stagenet-runtime/datahaven_stagenet_runtime.wasm\" datahaven",
|
||||
"start:e2e:verified": "bun cli --verified --blockscout --deploy-contracts --setup-validators --update-validator-set --fund-validators",
|
||||
"start:e2e:verified:relayers": "bun cli --verified --blockscout --deploy-contracts --setup-validators --update-validator-set --fund-validators --slot-time 1 --relayer --datahaven",
|
||||
"start:e2e:local": "LOG_LEVEL=debug bun start:e2e:ci --bd",
|
||||
"start:e2e:ci": "bun cli --datahaven --no-build-datahaven --launch-kurtosis --deploy-contracts --fund-validators --setup-validators --update-validator-set --relayer --always-clean",
|
||||
"start:e2e:minrelayer": "bun cli --relayer --deploy-contracts --no-setup-validators --no-update-validator-set --no-fund-validators --datahaven",
|
||||
"stop:docker": "docker ps -a --filter 'ancestor=moonsonglabs/datahaven:local' -q | xargs -r docker rm -f",
|
||||
"stop:e2e": "bun stop:docker ;pkill datahaven ; pkill snowbridge-relay ; kurtosis enclave stop datahaven-ethereum && kurtosis clean && kurtosis engine stop && docker container prune -f",
|
||||
"stop:docker:datahaven": "docker rm -f $(docker ps -aq --filter name='^datahaven-') 2>/dev/null || true",
|
||||
"stop:docker:relayer": "docker rm -f $(docker ps -aq --filter name='^snowbridge-relayer-') 2>/dev/null || true",
|
||||
"stop:e2e": "bun stop:docker:datahaven ; bun stop:docker:relayer ; (kurtosis enclave stop datahaven-ethereum || true) && kurtosis clean && kurtosis engine stop && docker container prune -f",
|
||||
"start:e2e:minimal:relayer": "bun cli --relayer --deploy-contracts --no-setup-validators --no-update-validator-set --no-fund-validators --datahaven",
|
||||
"stop:e2e:verified": "bun stop:e2e",
|
||||
"stop:e2e:quick": "kurtosis enclave stop datahaven-ethereum",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ const LOG_LEVEL = Bun.env.LOG_LEVEL || "info";
|
|||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const RUNTIME_FEATURES = ["fast-runtime"];
|
||||
|
||||
export const cargoCrossbuild = async (options: {
|
||||
datahavenBuildExtraArgs?: string;
|
||||
|
|
@ -40,7 +41,7 @@ export const cargoCrossbuild = async (options: {
|
|||
// Get additional arguments from command line
|
||||
const additionalArgs = options.datahavenBuildExtraArgs ?? "";
|
||||
|
||||
const command = `cargo zigbuild --target ${target} --release ${additionalArgs}`;
|
||||
const command = `cargo zigbuild --target ${target} --release ${additionalArgs} --features ${RUNTIME_FEATURES.join(",")}`;
|
||||
logger.debug(`Running build command: ${command}`);
|
||||
|
||||
if (LOG_LEVEL === "debug") {
|
||||
|
|
@ -53,7 +54,9 @@ export const cargoCrossbuild = async (options: {
|
|||
} else if (ARCH === "x86_64" && OS === "Linux") {
|
||||
logger.info("🖥️ Linux AMD64 detected. Proceeding with cross-building...");
|
||||
|
||||
const command = "cargo build --release";
|
||||
const target = "x86_64-unknown-linux-gnu";
|
||||
addRustupTarget(target);
|
||||
const command = `cargo build --target ${target} --release --features ${RUNTIME_FEATURES.join(",")}`;
|
||||
logger.debug(`Running build command: ${command}`);
|
||||
|
||||
if (LOG_LEVEL === "debug") {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export const deployContracts = async (options: DeployContractsOptions): Promise<
|
|||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Using flag option: ${shouldDeployContracts ? "will deploy" : "will not deploy"} smart contracts`
|
||||
`🏳️ Using flag option: ${shouldDeployContracts ? "will deploy" : "will not deploy"} smart contracts`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ export const deployContracts = async (options: DeployContractsOptions): Promise<
|
|||
logger.info("🔍 Contract verification enabled");
|
||||
}
|
||||
|
||||
logger.info("⏳ Deploying contracts (this might take a few minutes)...");
|
||||
logger.info("⌛️ Deploying contracts (this might take a few minutes)...");
|
||||
|
||||
// Using custom shell command to improve logging with forge's stdoutput
|
||||
await runShellCommandWithLogger(deployCommand, { cwd: "../contracts" });
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ export const fundValidators = async (options: FundValidatorsOptions): Promise<bo
|
|||
}
|
||||
|
||||
const validators = config.validators;
|
||||
logger.info(`Found ${validators.length} validators to fund`);
|
||||
logger.info(`🔎 Found ${validators.length} validators to fund`);
|
||||
|
||||
// Get cast path for transactions
|
||||
const { stdout: castPath } = await $`which cast`.quiet();
|
||||
|
|
@ -127,7 +127,7 @@ export const fundValidators = async (options: FundValidatorsOptions): Promise<bo
|
|||
logger.debug(`Found ${deployments.DeployedStrategies.length} strategies with token information`);
|
||||
|
||||
// We need to ensure all operators to be registered have the necessary tokens
|
||||
logger.info("Funding validators with tokens...");
|
||||
logger.info("💸 Funding validators with tokens...");
|
||||
|
||||
// Iterate through the strategies, using the embedded token information to fund validators
|
||||
for (const strategy of deployments.DeployedStrategies) {
|
||||
|
|
|
|||
|
|
@ -96,12 +96,12 @@ export const setupValidators = async (options: SetupValidatorsOptions): Promise<
|
|||
}
|
||||
|
||||
const validators = config.validators;
|
||||
logger.info(`Found ${validators.length} validators to register`);
|
||||
logger.info(`🔎 Found ${validators.length} validators to register`);
|
||||
|
||||
// Iterate through all validators to register them
|
||||
for (let i = 0; i < validators.length; i++) {
|
||||
const validator = validators[i];
|
||||
logger.info(`Setting up validator ${i} (${validator.publicKey})`);
|
||||
logger.info(`🔧 Setting up validator ${i} (${validator.publicKey})`);
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ async function downloadRelayBinary() {
|
|||
repo: "snowbridge"
|
||||
});
|
||||
const tagName = latestRelease.data.tag_name;
|
||||
logger.info(`Found latest release: ${tagName}`);
|
||||
logger.info(`🔎 Found latest release: ${tagName}`);
|
||||
|
||||
const relayAsset = latestRelease.data.assets.find((asset) => asset.name === "snowbridge-relay");
|
||||
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ export async function waitForLog(opts: {
|
|||
() =>
|
||||
pass.destroy(
|
||||
new Error(
|
||||
`Timed out after ${timeoutMs} ms waiting for “${opts.search}” in ${opts.containerName}`
|
||||
`Timed out after ${timeoutMs} ms waiting for "${opts.search}" in ${opts.containerName}`
|
||||
)
|
||||
),
|
||||
timeoutMs
|
||||
|
|
@ -125,7 +125,7 @@ export async function waitForLog(opts: {
|
|||
}
|
||||
|
||||
throw new Error(
|
||||
`Log stream ended before “${opts.search}” appeared for container ${opts.containerName}`
|
||||
`Log stream ended before "${opts.search}" appeared for container ${opts.containerName}`
|
||||
);
|
||||
} finally {
|
||||
if (timer) {
|
||||
|
|
@ -172,3 +172,21 @@ export const waitForContainerToStart = async (
|
|||
`❌ container ${containerName} cannot be found in running container list after ${seconds} seconds`
|
||||
);
|
||||
};
|
||||
|
||||
export const killExistingContainers = async (imageName: string) => {
|
||||
logger.debug(`Searching for containers with image ${imageName}...`);
|
||||
const docker = new Docker();
|
||||
const containerInfos = (await docker.listContainers({ all: true })).filter((container) =>
|
||||
container.Image.includes(imageName)
|
||||
);
|
||||
|
||||
if (containerInfos.length === 0) {
|
||||
logger.debug(`No containers found with image ${imageName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const promises = containerInfos.map(({ Id }) => docker.getContainer(Id).remove({ force: true }));
|
||||
await Promise.all(promises);
|
||||
|
||||
logger.debug(`${containerInfos.length} containers with image ${imageName} killed`);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -46,7 +46,9 @@ export const runShellCommandWithLogger = async (
|
|||
const text = new TextDecoder().decode(value);
|
||||
const trimmedText = text.trim();
|
||||
if (trimmedText) {
|
||||
logger[logLevel](trimmedText.includes("\n") ? `\n${trimmedText}` : trimmedText);
|
||||
logger[logLevel](
|
||||
trimmedText.includes("\n") ? `>_ \n${trimmedText}` : `>_ ${trimmedText}`
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { type FixedSizeArray, FixedSizeBinary } from "polkadot-api";
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* The type of the response from the `/eth/v1/beacon/states/head/finality_checkpoints`
|
||||
|
|
@ -49,28 +50,6 @@ export interface BeaconCheckpoint {
|
|||
toJSON: () => JsonBeaconCheckpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the structure of the BeaconCheckpoint as it might be after JSON.parse
|
||||
* before specific type coercions (e.g., to BigInt).
|
||||
*/
|
||||
interface RawBeaconCheckpoint {
|
||||
header: {
|
||||
slot: number | string | bigint; // JSON.parse will yield number or string for big numbers
|
||||
proposer_index: number | string | bigint; // Same as above
|
||||
parent_root: string; // Assuming hex string
|
||||
state_root: string; // Assuming hex string
|
||||
body_root: string; // Assuming hex string
|
||||
};
|
||||
current_sync_committee: {
|
||||
pubkeys: string[]; // Assuming array of hex strings
|
||||
aggregate_pubkey: string; // Assuming hex string
|
||||
};
|
||||
current_sync_committee_branch: string[]; // Assuming array of hex strings
|
||||
validators_root: string; // Assuming hex string
|
||||
block_roots_root: string; // Assuming hex string
|
||||
block_roots_branch: string[]; // Assuming array of hex strings
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the structure of a BeaconCheckpoint when serialized to JSON.
|
||||
* BigInts are converted to strings, and FixedSizeBinary types are converted to hex strings.
|
||||
|
|
@ -93,51 +72,35 @@ interface JsonBeaconCheckpoint {
|
|||
block_roots_branch: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a JSON object into a BeaconCheckpoint.
|
||||
*
|
||||
* @param jsonInput - The JSON object to parse.
|
||||
* @returns The parsed BeaconCheckpoint.
|
||||
*/
|
||||
export const parseJsonToBeaconCheckpoint = (jsonInput: any): BeaconCheckpoint => {
|
||||
const raw = jsonInput as RawBeaconCheckpoint;
|
||||
// Zod schema for hex strings, ensuring they start with "0x" if not empty
|
||||
const hexStringSchema = z.union([
|
||||
z.string().regex(/^0x[0-9a-fA-F]*$/, {
|
||||
message: "Invalid hex string"
|
||||
}),
|
||||
z.literal("")
|
||||
]);
|
||||
|
||||
// Basic validation
|
||||
if (!raw || typeof raw.header !== "object" || raw.header === null) {
|
||||
throw new Error("Invalid JSON structure for BeaconCheckpoint: missing or invalid header");
|
||||
}
|
||||
if (typeof raw.header.slot === "undefined" || typeof raw.header.proposer_index === "undefined") {
|
||||
throw new Error(
|
||||
"Invalid JSON structure for BeaconCheckpoint: header missing slot or proposer_index"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!raw.current_sync_committee?.pubkeys ||
|
||||
!raw.current_sync_committee.aggregate_pubkey ||
|
||||
!Array.isArray(raw.current_sync_committee.pubkeys) ||
|
||||
!Array.isArray(raw.current_sync_committee_branch) ||
|
||||
!raw.validators_root ||
|
||||
!raw.block_roots_root ||
|
||||
!Array.isArray(raw.block_roots_branch)
|
||||
) {
|
||||
throw new Error(
|
||||
"Invalid JSON structure for BeaconCheckpoint: missing sync-committee or root fields"
|
||||
);
|
||||
}
|
||||
|
||||
if (raw.current_sync_committee.pubkeys.length !== 512) {
|
||||
throw new Error(
|
||||
`Invalid sync-committee size. Expected 512 pubkeys, got ${raw.current_sync_committee.pubkeys.length}`
|
||||
);
|
||||
}
|
||||
|
||||
// Map pubkeys to FixedSizeBinary<48>
|
||||
const pubkeys = new Array<FixedSizeBinary<48>>(512);
|
||||
for (let i = 0; i < raw.current_sync_committee.pubkeys.length; i++) {
|
||||
pubkeys[i] = new FixedSizeBinary<48>(hexToUint8Array(raw.current_sync_committee.pubkeys[i]));
|
||||
}
|
||||
// Zod schema for the RawBeaconCheckpoint structure
|
||||
const rawBeaconCheckpointSchema = z.object({
|
||||
header: z.object({
|
||||
slot: z.union([z.number(), z.string(), z.bigint()]),
|
||||
proposer_index: z.union([z.number(), z.string(), z.bigint()]),
|
||||
parent_root: hexStringSchema,
|
||||
state_root: hexStringSchema,
|
||||
body_root: hexStringSchema
|
||||
}),
|
||||
current_sync_committee: z.object({
|
||||
pubkeys: z.array(hexStringSchema).length(512),
|
||||
aggregate_pubkey: hexStringSchema
|
||||
}),
|
||||
current_sync_committee_branch: z.array(hexStringSchema),
|
||||
validators_root: hexStringSchema,
|
||||
block_roots_root: hexStringSchema,
|
||||
block_roots_branch: z.array(hexStringSchema)
|
||||
});
|
||||
|
||||
// Zod schema for transforming RawBeaconCheckpoint to BeaconCheckpoint
|
||||
const beaconCheckpointSchema = rawBeaconCheckpointSchema.transform((raw) => {
|
||||
const checkpointData: Omit<BeaconCheckpoint, "toJSON"> = {
|
||||
header: {
|
||||
slot: BigInt(raw.header.slot),
|
||||
|
|
@ -147,7 +110,12 @@ export const parseJsonToBeaconCheckpoint = (jsonInput: any): BeaconCheckpoint =>
|
|||
body_root: new FixedSizeBinary<32>(hexToUint8Array(raw.header.body_root))
|
||||
},
|
||||
current_sync_committee: {
|
||||
pubkeys: asFixedSizeArray(pubkeys, 512),
|
||||
pubkeys: asFixedSizeArray(
|
||||
raw.current_sync_committee.pubkeys.map(
|
||||
(pk) => new FixedSizeBinary<48>(hexToUint8Array(pk))
|
||||
),
|
||||
512
|
||||
),
|
||||
aggregate_pubkey: new FixedSizeBinary<48>(
|
||||
hexToUint8Array(raw.current_sync_committee.aggregate_pubkey)
|
||||
)
|
||||
|
|
@ -186,6 +154,30 @@ export const parseJsonToBeaconCheckpoint = (jsonInput: any): BeaconCheckpoint =>
|
|||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Parses a JSON object into a BeaconCheckpoint.
|
||||
*
|
||||
* @param jsonInput - The JSON object to parse.
|
||||
* @returns The parsed BeaconCheckpoint.
|
||||
*/
|
||||
export const parseJsonToBeaconCheckpoint = (jsonInput: any): BeaconCheckpoint => {
|
||||
try {
|
||||
return beaconCheckpointSchema.parse(jsonInput);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
// You can customize error handling here, e.g., throw a more specific error
|
||||
// or log the validation issues.
|
||||
throw new Error(
|
||||
`Invalid JSON structure for BeaconCheckpoint: ${error.errors
|
||||
.map((e) => `${e.path.join(".")} - ${e.message}`)
|
||||
.join(", ")}`
|
||||
);
|
||||
}
|
||||
// Re-throw other errors
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -217,6 +209,9 @@ const hexToUint8Array = (hex: string): Uint8Array => {
|
|||
if (hexString.startsWith("0x")) {
|
||||
hexString = hexString.slice(2);
|
||||
}
|
||||
if (hexString.length % 2 !== 0) {
|
||||
throw new Error("Hex string must have an even number of characters");
|
||||
}
|
||||
return Buffer.from(hexString, "hex");
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { ANVIL_FUNDED_ACCOUNTS, CHAIN_ID, getRPCUrl, getWSUrl } from "utils";
|
||||
import { http, createWalletClient, defineChain, publicActions } from "viem";
|
||||
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
|
||||
import type { Prettify } from "./types";
|
||||
|
||||
export const createChainConfig = async () =>
|
||||
defineChain({
|
||||
|
|
@ -37,8 +38,6 @@ export const createDefaultClient = async () =>
|
|||
transport: http()
|
||||
}).extend(publicActions);
|
||||
|
||||
// export interface ViemClientInterface extends WalletClient, PublicActions {}
|
||||
|
||||
export type ViemClientInterface = Awaited<ReturnType<typeof createDefaultClient>>;
|
||||
export type ViemClientInterface = Prettify<Awaited<ReturnType<typeof createDefaultClient>>>;
|
||||
|
||||
export const generateRandomAccount = () => privateKeyToAccount(generatePrivateKey());
|
||||
|
|
|
|||
Loading…
Reference in a new issue