From 82145b882b1ef0dc4ca1cd8345c1f62e5e9869f0 Mon Sep 17 00:00:00 2001 From: Tim B <79199034+timbrinded@users.noreply.github.com> Date: Fri, 16 May 2025 15:17:05 +0100 Subject: [PATCH] =?UTF-8?q?test:=20=F0=9F=90=B3=20Add=20docker=20support?= =?UTF-8?q?=20for=20datahaven=20nodes=20(#71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > [!NOTE] > This is `Part 3` of the ongoing _Docker Series._ ## New Additions: - Launching Datahaven network will spin up containers, as opposed to native binaries - `stop:docker` script to kill all dh containers - `e2e` test suite for datahaven solochain network - Contains reference test file that uses papi for storage queries, submitting exts, runtime calls (good job on that facu and tobi) - Added new utils: - `waitForLog()` to wait for log lines in docker container logs - `createPapiConnectors()` helper for test cases to build and connect to dh network - `getPapiSigner()` helper to return a papi compatible signer using our prefunded accounts (alith by default) - `sendTxn()` helper to submit txn and wait for block inclusion, instead of finalization, which std library provides ## Changes: > [!CAUTION] > Launching native binaries for datahaven no longer supported. - Datahaven binary location cli option changed to `-i, --datahaven-image-tag` - To locally run this you'll need a datahaven docker image handy, you'll need to either: - Point to remote dockerhub e.g. `moonsonglabs/datahaven:main` (must be logged in and have permission) - Build this locally with `bun build:docker:operator` ## Summary by CodeRabbit - **New Features** - Added end-to-end tests for the Datahaven solochain, including runtime API queries, storage lookups, extrinsic submissions, and event listening. - Introduced CLI option to specify the Datahaven Docker image tag, with a default value. - Added CLI option to disable the Relayer. - Provided new scripts to stop Docker containers associated with Datahaven. - Added utility functions for Docker log monitoring and container startup checks. - Introduced utilities for interacting with the Datahaven Polkadot API. - **Improvements** - Switched Datahaven network launch from local binaries to Docker containers. - Enhanced cache accuracy in build workflows by including Rust source files in cache keys. - Improved build performance with TypeScript incremental build options. - Increased timeout for end-to-end tests for better reliability. - Updated CLI version to 0.2.0. - Modified Dockerfile build to enable the `fast-runtime` feature. - Extended network launch summary to include relayer and container details. - **Bug Fixes** - Fixed cleanup logic by tracking and preparing for forced removal of Docker containers after tests. - **Chores** - Updated workflow steps for Docker image handling and network checks. - Adjusted scripts and workflow logic for improved Docker and test management. - Removed top-level disk usage summaries from cleanup workflow for streamlined reporting. - Enhanced shell command utility to support asynchronous wait during execution. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com> --- .../actions/cleanup-runner/action.yml | 6 - .github/workflows/task-docker.yml | 3 +- .github/workflows/task-e2e.yml | 15 +- biome.json | 5 + operator/Dockerfile | 7 +- test/cli/handlers/launch/datahaven.ts | 234 +++++++++++++----- test/cli/handlers/launch/index.ts | 6 +- test/cli/handlers/launch/kurtosis.ts | 6 +- test/cli/handlers/launch/launchedNetwork.ts | 103 ++++---- test/cli/handlers/launch/relayer.ts | 20 +- test/cli/handlers/launch/summary.ts | 40 ++- test/cli/index.ts | 8 +- test/package.json | 8 +- test/scripts/gen-snowbridge-cfgs.ts | 2 +- test/scripts/snowbridge-relayer.ts | 2 +- test/suites/e2e/datahaven-basic.test.ts | 74 ++++++ test/suites/e2e/service-manager.test.ts | 11 +- test/tsconfig.json | 6 +- test/utils/docker.ts | 97 +++++++- test/utils/kurtosis.ts | 1 - test/utils/papi.ts | 19 ++ test/utils/shell.ts | 11 +- test/utils/types.ts | 6 + 23 files changed, 522 insertions(+), 168 deletions(-) create mode 100644 test/suites/e2e/datahaven-basic.test.ts diff --git a/.github/workflows/actions/cleanup-runner/action.yml b/.github/workflows/actions/cleanup-runner/action.yml index 706dcee9..ff05f33a 100644 --- a/.github/workflows/actions/cleanup-runner/action.yml +++ b/.github/workflows/actions/cleanup-runner/action.yml @@ -10,9 +10,6 @@ runs: echo "Overall disk space before cleanup (df -h /):" df -h / echo "-------------------------------------------" - echo "Top-level directories before cleanup (du -h --max-depth=1 /):" - sudo du -h --max-depth=1 --exclude=/proc --exclude=/sys --exclude=/dev / | sort -rh | head -n 10 - echo "-------------------------------------------" echo "Detailed breakdown for /usr/ before cleanup:" sudo du -h --max-depth=1 /usr/ | sort -rh | head -n 10 echo "-------------------------------------------" @@ -51,9 +48,6 @@ runs: echo "Overall disk space after cleanup (df -h /):" df -h / echo "-------------------------------------------" - echo "Top-level directories after cleanup (du -h --max-depth=1 /):" - sudo du -h --max-depth=1 --exclude=/proc --exclude=/sys --exclude=/dev / | sort -rh | head -n 10 - echo "-------------------------------------------" echo "Detailed breakdown for /usr/ before cleanup:" sudo du -h --max-depth=1 /usr/ | sort -rh | head -n 10 echo "-------------------------------------------" diff --git a/.github/workflows/task-docker.yml b/.github/workflows/task-docker.yml index 0a217e7e..af97ad5e 100644 --- a/.github/workflows/task-docker.yml +++ b/.github/workflows/task-docker.yml @@ -65,8 +65,9 @@ jobs: cargo-registry cargo-git sccache - key: cache-mount-${{ hashFiles('./operator/Dockerfile', './operator/Cargo.lock') }} + key: cache-mount-${{ hashFiles('./operator/Dockerfile', './operator/Cargo.lock') }}-${{hashFiles('./operator/runtime/**/*.rs','./operator/pallets/**/*.rs', './operator/node/**/*.rs')}} restore-keys: | + cache-mount-${{ hashFiles('./operator/Dockerfile', './operator/Cargo.lock') }} cache-mount-${{ hashFiles('./operator/Dockerfile') }} cache-mount- - name: Inject cache into docker diff --git a/.github/workflows/task-e2e.yml b/.github/workflows/task-e2e.yml index 2edfd780..2723b5ad 100644 --- a/.github/workflows/task-e2e.yml +++ b/.github/workflows/task-e2e.yml @@ -84,14 +84,11 @@ jobs: chmod +x tmp/bin/snowbridge-relay docker rm temp - run: tmp/bin/snowbridge-relay --help - - name: Download datahaven binary - run: | - docker create --name temp ${{ inputs.image-tag }} - mkdir -p ../operator/target/release/ - docker cp temp:/usr/local/bin/datahaven-node ../operator/target/release/ - chmod +x ../operator/target/release/datahaven-node - docker rm temp - - run: ../operator/target/release/datahaven-node --help + - run: docker pull ${{ inputs.image-tag }} - run: bun install - - run: bun start:e2e:ci + - run: bun start:e2e:ci --datahaven-image-tag ${{ inputs.image-tag }} + - name: Check network + run: | + kurtosis enclave inspect datahaven-ethereum + docker container ls - run: bun test:e2e diff --git a/biome.json b/biome.json index 070f830b..8d5a285c 100644 --- a/biome.json +++ b/biome.json @@ -27,6 +27,7 @@ "trailingCommas": "none", "semicolons": "always", "indentStyle": "space", + "indentWidth": 2, "lineWidth": 100, "quoteStyle": "double" } @@ -35,6 +36,10 @@ "enabled": true, "rules": { "recommended": true, + "correctness": { + "noUnusedVariables": "warn", + "noUnusedImports": "warn" + }, "suspicious": { "noExplicitAny": "off", "noAsyncPromiseExecutor": "off" diff --git a/operator/Dockerfile b/operator/Dockerfile index ef25a170..6afdf771 100644 --- a/operator/Dockerfile +++ b/operator/Dockerfile @@ -3,6 +3,7 @@ FROM docker.io/paritytech/ci-unified:bullseye-1.85.0 AS base ARG MOLD_VERSION=2.39.0 ARG SCCACHE_VERSION=0.10.0 +ARG FAST_RUNTIME=TRUE RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ xz-utils \ @@ -43,7 +44,11 @@ COPY ./operator/ . RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/usr/local/cargo/git \ --mount=type=cache,target=/usr/local/sccache,sharing=locked \ - cargo build --locked --release + if [ "$FAST_RUNTIME" = "TRUE" ]; then \ + cargo build --locked --release --features fast-runtime; \ + else \ + cargo build --locked --release; \ + fi # --- Create final lightweight runtime image --- FROM docker.io/parity/base-bin:latest diff --git a/test/cli/handlers/launch/datahaven.ts b/test/cli/handlers/launch/datahaven.ts index 74ef6210..ce6ff396 100644 --- a/test/cli/handlers/launch/datahaven.ts +++ b/test/cli/handlers/launch/datahaven.ts @@ -7,26 +7,31 @@ import { type PolkadotClient, createClient } from "polkadot-api"; import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat"; import { getWsProvider } from "polkadot-api/ws-provider/web"; import invariant from "tiny-invariant"; +import { waitForContainerToStart } from "utils"; import { confirmWithTimeout, logger, printDivider, printHeader } from "utils"; import { type Hex, keccak256, toHex } from "viem"; -import { publicKeyToAddress } from "viem/utils"; +import { publicKeyToAddress } from "viem/accounts"; import type { LaunchOptions } from "."; import type { LaunchedNetwork } from "./launchedNetwork"; const COMMON_LAUNCH_ARGS = [ "--unsafe-force-node-key-generation", "--tmp", - "--port=0", "--validator", + "--discover-local", "--no-prometheus", + "--unsafe-rpc-external", + "--rpc-cors=all", "--force-authoring", "--no-telemetry", "--enable-offchain-indexing=true" ]; +const DEFAULT_PUBLIC_WS_PORT = 9944; + // We need 5 since the (2/3 + 1) of 6 authority set is 5 // /operator/runtime/src/genesis_config_presets.rs#L94 -const CLI_AUTHORITY_IDS = ["alice", "bob", "charlie", "dave", "eve"]; +const CLI_AUTHORITY_IDS = ["alice", "bob", "charlie", "dave", "eve"] as const; // 33-byte compressed public keys for DataHaven next validator set // These correspond to Alice, Bob, Charlie, Dave, Eve, Ferdie @@ -38,7 +43,7 @@ const FALLBACK_DATAHAVEN_AUTHORITY_PUBLIC_KEYS: Record = { dave: "0x0291f1217d5a04cb83312ee3d88a6e6b33284e053e6ccfc3a90339a0299d12967c", eve: "0x0389411795514af1627765eceffcbd002719f031604fadd7d188e2dc585b4e1afb", ferdie: "0x03bc9d0ca094bd5b8b3225d7651eac5d18c1c04bf8ae8f8b263eebca4e1410ed0c" -}; +} as const; /** * Prepares the configuration for DataHaven authorities by converting their @@ -51,7 +56,7 @@ export async function setupDataHavenValidatorConfig( logger.info(`๐Ÿ”ง Preparing DataHaven authorities configuration for network: ${networkName}...`); let authorityPublicKeys: string[] = []; - const dhNodes = launchedNetwork.getDHNodes(); + const dhNodes = launchedNetwork.containers.filter((x) => x.name.startsWith("datahaven-")); if (dhNodes.length === 0) { logger.warn( @@ -60,11 +65,11 @@ export async function setupDataHavenValidatorConfig( authorityPublicKeys = Object.values(FALLBACK_DATAHAVEN_AUTHORITY_PUBLIC_KEYS); } else { const firstNode = dhNodes[0]; - const wsUrl = `ws://127.0.0.1:${firstNode.port}`; + const wsUrl = `ws://127.0.0.1:${firstNode.publicPorts.ws}`; let papiClient: PolkadotClient | undefined; try { logger.info( - `๐Ÿ“ก Attempting to fetch BEEFY next authorities from node ${firstNode.id} (port ${firstNode.port})...` + `๐Ÿ“ก Attempting to fetch BEEFY next authorities from node ${firstNode.name} (port ${firstNode.publicPorts.ws})...` ); papiClient = createClient(withPolkadotSdkCompat(getWsProvider(wsUrl))); const dhApi = papiClient.getTypedApi(datahaven); @@ -84,14 +89,14 @@ export async function setupDataHavenValidatorConfig( ); authorityPublicKeys = Object.values(FALLBACK_DATAHAVEN_AUTHORITY_PUBLIC_KEYS); } - await papiClient.destroy(); + papiClient.destroy(); } catch (error) { logger.error( - `โŒ Error fetching BEEFY next authorities from node ${firstNode.id}: ${error}. Falling back to hardcoded authority set.` + `โŒ Error fetching BEEFY next authorities from node ${firstNode.name}: ${error}. Falling back to hardcoded authority set.` ); authorityPublicKeys = Object.values(FALLBACK_DATAHAVEN_AUTHORITY_PUBLIC_KEYS); if (papiClient) { - await papiClient.destroy(); // Ensure client is destroyed even on error + papiClient.destroy(); } } } @@ -151,7 +156,6 @@ export async function setupDataHavenValidatorConfig( } } -// TODO: This is very rough and will need something more substantial when we know what we want! /** * Launches a DataHaven solochain network for testing. * @@ -165,6 +169,41 @@ export const launchDataHavenSolochain = async ( printHeader("Starting DataHaven Network"); let shouldLaunchDataHaven = options.datahaven; + + if ((await checkDataHavenRunning()) && !options.alwaysClean) { + logger.info("โ„น๏ธ DataHaven network (Docker containers) is already running."); + + logger.trace("Checking if datahaven option was set via flags"); + if (options.datahaven === false) { + logger.info("Keeping existing DataHaven containers."); + + await registerNodes(launchedNetwork); + printDivider(); + return; + } + + if (options.datahaven === true) { + logger.info("Proceeding to clean and relaunch DataHaven containers..."); + await cleanDataHavenContainers(); + } else { + const shouldRelaunch = await confirmWithTimeout( + "Do you want to clean and relaunch the DataHaven containers?", + true, + 10 + ); + + if (!shouldRelaunch) { + logger.info("Keeping existing DataHaven containers."); + + await registerNodes(launchedNetwork); + printDivider(); + return; + } + logger.info("Proceeding to clean and relaunch DataHaven containers..."); + await cleanDataHavenContainers(); + } + } + if (shouldLaunchDataHaven === undefined) { shouldLaunchDataHaven = await confirmWithTimeout( "Do you want to launch the DataHaven network?", @@ -183,15 +222,9 @@ export const launchDataHavenSolochain = async ( return; } - // Kill any pre-existing datahaven processes if they exist - await $`pkill datahaven`.nothrow().quiet(); + invariant(options.datahavenImageTag, "โŒ Datahaven image tag not defined"); - invariant(options.datahavenBinPath, "โŒ DataHaven binary path not defined"); - - invariant( - await Bun.file(options.datahavenBinPath).exists(), - "โŒ DataHaven binary does not exist" - ); + await checkTagExists(options.datahavenImageTag); const logsPath = `tmp/logs/${launchedNetwork.getRunId()}/`; logger.debug(`Ensuring logs directory exists: ${logsPath}`); @@ -199,57 +232,46 @@ export const launchDataHavenSolochain = async ( for (const id of CLI_AUTHORITY_IDS) { logger.info(`Starting ${id}...`); + const containerName = `datahaven-${id}`; - const command: string[] = [options.datahavenBinPath, ...COMMON_LAUNCH_ARGS, `--${id}`]; + const command: string[] = [ + "docker", + "run", + "-d", + "--platform", + "linux/amd64", + "--name", + containerName, + ...(id === "alice" ? ["-p", `${DEFAULT_PUBLIC_WS_PORT}:9944`] : []), + options.datahavenImageTag, + `--${id}`, + ...COMMON_LAUNCH_ARGS + ]; - const logFileName = `datahaven-${id}.log`; - const logFilePath = path.join(logsPath, logFileName); - logger.debug(`Writing logs to ${logFilePath}`); + logger.debug($`sh -c "${command.join(" ")}"`.text()); - const fd = fs.openSync(logFilePath, "a"); - launchedNetwork.addFileDescriptor(fd); + await waitForContainerToStart(containerName); - logger.debug(`Spawning command: ${command.join(" ")}`); - const process = Bun.spawn(command, { - stdout: fd, - stderr: fd - }); - - process.unref(); - - let completed = false; - const file = Bun.file(logFilePath); - for (let i = 0; i < 60; i++) { - const pattern = "Running JSON-RPC server: addr=127.0.0.1:"; - const blob = await file.text(); - logger.debug(`Blob: ${blob}`); - if (blob.includes(pattern)) { - const port = blob.split(pattern)[1].split("\n")[0].replaceAll(",", ""); - launchedNetwork.addDHNode(id, Number.parseInt(port)); - logger.debug(`${id} started at port ${port}`); - completed = true; - break; - } - await new Promise((resolve) => setTimeout(resolve, 1000)); - } - invariant(completed, "โŒ Could not find 'Running JSON-RPC server:' in logs"); - - launchedNetwork.addProcess(process); - logger.debug(`Started ${id} at ${process.pid}`); + // TODO: Un-comment this when it doesn't stop process from hanging + // This is working on SH, but not here so probably a Bun defect + // + // const listeningLine = await waitForLog({ + // search: "Running JSON-RPC server: addr=0.0.0.0:", + // containerName, + // timeoutSeconds: 30 + // }); + // logger.debug(listeningLine); } - // Check if network is ready - for (let i = 0; i < 10; i++) { + for (let i = 0; i < 30; i++) { logger.info("Waiting for datahaven to start..."); - - // Get the port of the primary node (or default) - const primaryNodePort = launchedNetwork.getDHNodes()[0]?.port || 9944; - - if (await isNetworkReady(primaryNodePort)) { + if (await isNetworkReady(DEFAULT_PUBLIC_WS_PORT)) { logger.success( - `DataHaven network started, primary node accessible on port ${primaryNodePort}` + `DataHaven network started, primary node accessible on port ${DEFAULT_PUBLIC_WS_PORT}` ); + await registerNodes(launchedNetwork); + // Call setupDataHavenValidatorConfig now that nodes are up logger.info("Proceeding with DataHaven validator configuration setup..."); await setupDataHavenValidatorConfig(launchedNetwork); @@ -261,9 +283,47 @@ export const launchDataHavenSolochain = async ( await new Promise((resolve) => setTimeout(resolve, 1000)); } - throw new Error("DataHaven network failed to start after 10 seconds"); + throw new Error("Datahaven network failed to start after 30 seconds"); }; +/** + * Checks if any DataHaven containers are currently running. + * + * @returns True if any DataHaven containers are running, false otherwise. + */ +const checkDataHavenRunning = async (): Promise => { + // Check for any container whose name starts with "datahaven-" + const PIDS = await $`docker ps -q --filter "name=^datahaven-"`.text(); + return PIDS.trim().length > 0; +}; + +/** + * Stops and removes all DataHaven containers. + */ +const cleanDataHavenContainers = async (): Promise => { + 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()); + } + } + logger.info("โœ… Existing DataHaven containers stopped and removed."); +}; + +/** + * Checks if the DataHaven network is ready by sending a POST request to the system_chain method. + * + * @param port - The port number to check. + * @returns True if the network is ready, false otherwise. + */ export const isNetworkReady = async (port: number): Promise => { const wsUrl = `ws://127.0.0.1:${port}`; let client: PolkadotClient | undefined; @@ -272,17 +332,65 @@ export const isNetworkReady = async (port: number): Promise => { client = createClient(withPolkadotSdkCompat(getWsProvider(wsUrl))); const chainName = await client._request("system_chain", []); logger.debug(`isNetworkReady PAPI check successful for port ${port}, chain: ${chainName}`); - await client.destroy(); + client.destroy(); return !!chainName; // Ensure it's a boolean and chainName is truthy } catch (error) { logger.debug(`isNetworkReady PAPI check failed for port ${port}: ${error}`); if (client) { - await client.destroy(); + client.destroy(); } return false; } }; +/** + * Checks if an image exists locally or on Docker Hub. + * + * @param tag - The tag of the image to check. + * @returns A promise that resolves when the image is found. + */ +const checkTagExists = async (tag: string) => { + const cleaned = tag.trim(); + logger.debug(`Checking if image ${cleaned} is available locally`); + const { exitCode: localExists } = await $`docker image inspect ${cleaned}`.nothrow().quiet(); + + if (localExists !== 0) { + logger.debug(`Checking if image ${cleaned} is available on docker hub`); + const result = await $`docker manifest inspect ${cleaned}`.nothrow().quiet(); + invariant( + result.exitCode === 0, + `โŒ Image ${tag} not found.\n Does this image exist?\n Are you logged and have access to the repository?` + ); + } + + logger.success(`Image ${tag} found locally`); +}; + +const registerNodes = async (launchedNetwork: LaunchedNetwork) => { + const targetContainerName = "datahaven-alice"; + const aliceHostWsPort = 9944; // Standard host port for Alice's WS, as set during launch. + + logger.debug(`Checking Docker status for container: ${targetContainerName}`); + // Use ^ and $ for an exact name match in the filter. + const dockerPsOutput = await $`docker ps -q --filter "name=^${targetContainerName}$"`.text(); + const isContainerRunning = dockerPsOutput.trim().length > 0; + + if (!isContainerRunning) { + // If the target Docker container is not running, we cannot register it. + throw new Error( + `โŒ Docker container ${targetContainerName} is not running. Cannot register node.` + ); + } + + // 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}.` + ); + launchedNetwork.addContainer(targetContainerName, { ws: aliceHostWsPort }); + logger.success(`๐Ÿ‘ Node ${targetContainerName} successfully registered in launchedNetwork.`); +}; + // Function to convert compressed public key to Ethereum address export const compressedPubKeyToEthereumAddress = (compressedPubKey: string): string => { // Ensure the input is a hex string and remove "0x" prefix diff --git a/test/cli/handlers/launch/index.ts b/test/cli/handlers/launch/index.ts index b2372a43..1728aafe 100644 --- a/test/cli/handlers/launch/index.ts +++ b/test/cli/handlers/launch/index.ts @@ -21,8 +21,8 @@ export interface LaunchOptions { relayerBinPath?: string; skipCleaning?: boolean; alwaysClean?: boolean; - datahavenBinPath?: string; datahaven?: boolean; + datahavenImageTag?: string; kurtosisNetworkArgs?: string; slotTime?: number; } @@ -63,13 +63,13 @@ const launchFunction = async (options: LaunchOptions, launchedNetwork: LaunchedN } const contractsDeployed = await deployContracts({ - rpcUrl: launchedNetwork.getElRpcUrl(), + rpcUrl: launchedNetwork.elRpcUrl, verified: options.verified, blockscoutBackendUrl, deployContracts: options.deployContracts }); - await performValidatorOperations(options, launchedNetwork.getElRpcUrl(), contractsDeployed); + await performValidatorOperations(options, launchedNetwork.elRpcUrl, contractsDeployed); await launchRelayers(options, launchedNetwork); diff --git a/test/cli/handlers/launch/kurtosis.ts b/test/cli/handlers/launch/kurtosis.ts index 15cbab18..48c23e31 100644 --- a/test/cli/handlers/launch/kurtosis.ts +++ b/test/cli/handlers/launch/kurtosis.ts @@ -23,6 +23,7 @@ export const launchKurtosis = async ( logger.trace("Checking if launchKurtosis option was set via flags"); if (options.launchKurtosis === false) { logger.info("Keeping existing Kurtosis enclave."); + await registerServices(launchedNetwork); printDivider(); return; @@ -39,6 +40,7 @@ export const launchKurtosis = async ( if (!shouldRelaunch) { logger.info("Keeping existing Kurtosis enclave."); + await registerServices(launchedNetwork); printDivider(); return; @@ -146,7 +148,7 @@ const registerServices = async (launchedNetwork: LaunchedNetwork) => { 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.setElRpcUrl(elRpcUrl); + launchedNetwork.elRpcUrl = elRpcUrl; logger.info(`๐Ÿ‘ Execution Layer RPC URL configured: ${elRpcUrl}`); // Configure CL Endpoint @@ -156,6 +158,6 @@ const registerServices = async (launchedNetwork: LaunchedNetwork) => { clEndpoint, "โŒ CL Endpoint could not be determined from Kurtosis service cl-1-lighthouse-reth" ); - launchedNetwork.setClEndpoint(clEndpoint); + launchedNetwork.clEndpoint = clEndpoint; logger.info(`๐Ÿ‘ Consensus Layer Endpoint configured: ${clEndpoint}`); }; diff --git a/test/cli/handlers/launch/launchedNetwork.ts b/test/cli/handlers/launch/launchedNetwork.ts index fa612528..b36fe2d3 100644 --- a/test/cli/handlers/launch/launchedNetwork.ts +++ b/test/cli/handlers/launch/launchedNetwork.ts @@ -1,6 +1,10 @@ import fs from "node:fs"; import invariant from "tiny-invariant"; -import { logger } from "utils"; +import { type RelayerType, logger } from "utils"; + +type PipeOptions = number | "inherit" | "pipe" | "ignore"; +type BunProcess = Bun.Subprocess; +type ContainerSpec = { name: string; publicPorts: Record }; /** * Represents the state and associated resources of a launched network environment, @@ -8,21 +12,23 @@ import { logger } from "utils"; */ export class LaunchedNetwork { protected runId: string; - protected processes: Bun.Subprocess<"inherit" | "pipe" | "ignore", number, number>[]; + protected processes: BunProcess[]; + protected _containers: ContainerSpec[]; protected fileDescriptors: number[]; - protected DHNodes: { id: string; port: number }[]; + protected _activeRelayers: RelayerType[]; /** The RPC URL for the Ethereum Execution Layer (EL) client. */ - protected elRpcUrl?: string; + protected _elRpcUrl?: string; /** The HTTP endpoint for the Ethereum Consensus Layer (CL) client. */ - protected clEndpoint?: string; + protected _clEndpoint?: string; constructor() { this.runId = crypto.randomUUID(); this.processes = []; this.fileDescriptors = []; - this.DHNodes = []; - this.elRpcUrl = undefined; - this.clEndpoint = undefined; + this._containers = []; + this._activeRelayers = []; + this._elRpcUrl = undefined; + this._clEndpoint = undefined; } /** @@ -34,23 +40,17 @@ export class LaunchedNetwork { } /** - * Gets the list of launched DataHaven (DH) nodes. - * @returns An array of DH node objects, each with an id and port. + * Gets the port for a DataHaven RPC node. + * + * In reality, it just looks for the "ws" port of the + * `datahaven-alice` container, if it was registered. + * @returns The port number of the container, or -1 if ws port is not found. + * @throws If the container is not found. */ - getDHNodes(): { id: string; port: number }[] { - return [...this.DHNodes]; - } - - /** - * Gets the port for a specific DataHaven (DH) node by its ID. - * @param id - The ID of the DH node. - * @returns The port number of the DH node. - * @throws If the node with the given ID is not found. - */ - getDHPort(id: string): number { - const node = this.DHNodes.find((x) => x.id === id); - invariant(node, `โŒ DataHaven node ${id} not found`); - return node.port; + getContainerPort(id: string): number { + const container = this._containers.find((x) => x.name === id); + invariant(container, `โŒ Container ${id} not found`); + return container.publicPorts.ws ?? -1; } /** @@ -65,25 +65,42 @@ export class LaunchedNetwork { * Adds a running process to be managed and cleaned up. * @param process - The Bun subprocess object. */ - addProcess(process: Bun.Subprocess<"inherit" | "pipe" | "ignore", number, number>) { + addProcess(process: BunProcess) { this.processes.push(process); } - /** - * Adds a DataHaven (DH) node to the list of launched nodes. - * @param id - The ID of the DH node. - * @param port - The port number the DH node is running on. - */ - addDHNode(id: string, port: number) { - this.DHNodes.push({ id, port }); + addContainer(containerName: string, publicPorts: Record = {}) { + 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:"); + logger.debug(JSON.stringify(this.containers)); + const port = this.containers.map((x) => x.publicPorts.ws).find((x) => x !== -1); + invariant(port !== undefined, "โŒ No public port found in containers"); + 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. */ - setElRpcUrl(url: string) { - this.elRpcUrl = url; + public set elRpcUrl(url: string) { + this._elRpcUrl = url; } /** @@ -91,17 +108,17 @@ export class LaunchedNetwork { * @returns The EL RPC URL string. * @throws If the EL RPC URL has not been set. */ - getElRpcUrl(): string { - invariant(this.elRpcUrl, "โŒ EL RPC URL not set in LaunchedNetwork"); - return this.elRpcUrl; + public get elRpcUrl(): string { + invariant(this._elRpcUrl, "โŒ EL RPC URL not set in LaunchedNetwork"); + return this._elRpcUrl; } /** * Sets the HTTP endpoint for the Ethereum Consensus Layer (CL) client. * @param url - The CL HTTP endpoint string. */ - setClEndpoint(url: string) { - this.clEndpoint = url; + public set clEndpoint(url: string) { + this._clEndpoint = url; } /** @@ -109,14 +126,16 @@ export class LaunchedNetwork { * @returns The CL HTTP endpoint string. * @throws If the CL HTTP endpoint has not been set. */ - getClEndpoint(): string { - invariant(this.clEndpoint, "โŒ CL HTTP Endpoint not set in LaunchedNetwork"); - return this.clEndpoint; + public get clEndpoint(): string { + invariant(this._clEndpoint, "โŒ CL HTTP Endpoint not set in LaunchedNetwork"); + return this._clEndpoint; } async cleanup() { + logger.debug("Running cleanup"); for (const process of this.processes) { logger.debug(`Process is still running: ${process.pid}`); + process.unref(); } for (const fd of this.fileDescriptors) { diff --git a/test/cli/handlers/launch/relayer.ts b/test/cli/handlers/launch/relayer.ts index 50b9a3f1..6cad657f 100644 --- a/test/cli/handlers/launch/relayer.ts +++ b/test/cli/handlers/launch/relayer.ts @@ -69,7 +69,9 @@ export const launchRelayers = async (options: LaunchOptions, launchedNetwork: La } // Get DataHaven node port - const dhNodes = launchedNetwork.getDHNodes(); + const dhNodes = launchedNetwork.containers.filter((container) => + container.name.includes("datahaven") + ); let substrateWsPort: number; let substrateNodeId: string; @@ -81,8 +83,8 @@ export const launchRelayers = async (options: LaunchOptions, launchedNetwork: La substrateNodeId = "default (assumed)"; } else { const firstDhNode = dhNodes[0]; - substrateWsPort = firstDhNode.port; - substrateNodeId = firstDhNode.id; + substrateWsPort = firstDhNode.publicPorts.ws; + substrateNodeId = firstDhNode.name; logger.info( `๐Ÿ”Œ Using DataHaven node ${substrateNodeId} on port ${substrateWsPort} for relayers and BEEFY check.` ); @@ -238,7 +240,7 @@ const waitBeefyReady = async ( pollIntervalMs: number, timeoutMs: number ): Promise => { - const port = launchedNetwork.getDHNodes()[0]?.port ?? 9944; + const port = launchedNetwork.getPublicWsPort(); const wsUrl = `ws://127.0.0.1:${port}`; const maxAttempts = Math.floor(timeoutMs / pollIntervalMs); @@ -255,7 +257,7 @@ const waitBeefyReady = async ( if (finalizedHeadHex && finalizedHeadHex !== ZERO_HASH) { logger.success(`๐Ÿฅฉ BEEFY is ready. Finalized head: ${finalizedHeadHex}`); - await client.destroy(); + client.destroy(); return; } @@ -270,12 +272,12 @@ const waitBeefyReady = async ( } logger.error(`โŒ BEEFY failed to become ready after ${timeoutMs / 1000} seconds`); - if (client) await client.destroy(); + if (client) client.destroy(); throw new Error("BEEFY protocol not ready. Relayers cannot be launched."); } catch (error) { logger.error(`โŒ Failed to connect to DataHaven node for BEEFY check: ${error}`); if (client) { - await client.destroy(); + client.destroy(); } throw new Error("BEEFY protocol not ready. Relayers cannot be launched."); } @@ -317,7 +319,7 @@ export const initEthClientPallet = async ( logger.trace(initialCheckpoint.toJSON()); // Send the checkpoint to the Substrate runtime - const substrateRpcUrl = `http://127.0.0.1:${launchedNetwork.getDHNodes()[0].port}`; + const substrateRpcUrl = `http://127.0.0.1:${launchedNetwork.getPublicWsPort()}`; await sendCheckpointToSubstrate(substrateRpcUrl, initialCheckpoint); }; @@ -344,7 +346,7 @@ const waitBeaconChainReady = async ( while (keepPolling) { try { const response = await fetch( - `${launchedNetwork.getClEndpoint()}/eth/v1/beacon/states/head/finality_checkpoints` + `${launchedNetwork.clEndpoint}/eth/v1/beacon/states/head/finality_checkpoints` ); if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); diff --git a/test/cli/handlers/launch/summary.ts b/test/cli/handlers/launch/summary.ts index 4fbdddcd..2c5ee7f3 100644 --- a/test/cli/handlers/launch/summary.ts +++ b/test/cli/handlers/launch/summary.ts @@ -15,9 +15,13 @@ export const performSummaryOperations = async ( servicesToDisplay.push(...["blockscout", "blockscout-frontend"]); } - const dhNodes = launchedNetwork.getDHNodes(); - for (const { id } of dhNodes) { - servicesToDisplay.push(`datahaven-${id}`); + if (launchedNetwork.containers.find((c) => c.name === "datahaven-alice")) { + servicesToDisplay.push("datahaven-alice"); + } + + const activeRelayers = launchedNetwork.relayers; + for (const relayer of activeRelayers) { + servicesToDisplay.push(`${relayer}-relayer`); } logger.trace("Services to display", servicesToDisplay); @@ -87,21 +91,45 @@ export const performSummaryOperations = async ( break; } - case service.startsWith("datahaven-"): { - const port = launchedNetwork.getDHPort(service.split("datahaven-")[1]); + case service === "datahaven-alice": { + const port = launchedNetwork.getContainerPort(service); displayData.push({ service, - ports: { http: port }, + ports: { ws: port }, url: `http://127.0.0.1:${port}` }); 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}`); } } } + 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 }); + } + console.table(displayData); }; diff --git a/test/cli/index.ts b/test/cli/index.ts index 370a2047..d86fdded 100644 --- a/test/cli/index.ts +++ b/test/cli/index.ts @@ -35,9 +35,9 @@ const program = new Command() .option("--always-clean", "Always clean Kurtosis", false) .option("--skip-cleaning", "Skip cleaning Kurtosis") .option( - "--datahaven-bin-path ", - "Path to the datahaven binary", - "../operator/target/release/datahaven-node" + "-i, --datahaven-image-tag ", + "Tag of the datahaven image to use", + "moonsonglabs/datahaven:local" ) .option("--relayer-bin-path ", "Path to the relayer binary", "tmp/bin/snowbridge-relay") .hook("preAction", launchPreActionHook) @@ -45,7 +45,7 @@ const program = new Command() // ===== Program ===== program - .version("0.1.0") + .version("0.2.0") .name("bun cli") .summary("๐ŸซŽ DataHaven: Network Launcher CLI") .usage("[options]") diff --git a/test/package.json b/test/package.json index 73b0ddb5..4ff52b39 100644 --- a/test/package.json +++ b/test/package.json @@ -7,7 +7,7 @@ "cli": "bun run cli/index.ts", "fmt": "biome check .", "fmt:fix": "biome check --write .", - "build:docker:operator": "docker buildx build -t moonsonglabs/datahaven-node:local -f ../operator/Dockerfile ../.", + "build:docker:operator": "docker buildx build --platform=linux/amd64 -t moonsonglabs/datahaven:local -f ../operator/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()\"", @@ -15,12 +15,14 @@ "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:ci": "bun cli --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", "start:e2e:minimal:relayer": "bun cli --relayer --deploy-contracts --no-setup-validators --no-update-validator-set --no-fund-validators --datahaven", - "stop:e2e": "pkill datahaven ; pkill snowbridge-relay ; kurtosis enclave stop datahaven-ethereum && kurtosis clean && kurtosis engine stop && docker container prune -f", "stop:e2e:verified": "bun stop:e2e", "stop:e2e:quick": "kurtosis enclave stop datahaven-ethereum", "stop:kurtosis-engine": "kurtosis engine stop && docker container prune -f", - "test:e2e": "bun test suites/e2e --timeout 30000", + "test:e2e": "bun test suites/e2e --timeout 60000", "typecheck": "tsc --noEmit", "postinstall": "papi" }, diff --git a/test/scripts/gen-snowbridge-cfgs.ts b/test/scripts/gen-snowbridge-cfgs.ts index ccd429f6..e35b2f33 100644 --- a/test/scripts/gen-snowbridge-cfgs.ts +++ b/test/scripts/gen-snowbridge-cfgs.ts @@ -339,7 +339,7 @@ async function waitBeaconChainReady(options: SnowbridgeConfigOptions): Promise setTimeout(resolve, 1000)); diff --git a/test/scripts/snowbridge-relayer.ts b/test/scripts/snowbridge-relayer.ts index f43a7520..4ba714fb 100644 --- a/test/scripts/snowbridge-relayer.ts +++ b/test/scripts/snowbridge-relayer.ts @@ -3,7 +3,7 @@ import path from "node:path"; import { $ } from "bun"; import { Octokit } from "octokit"; import invariant from "tiny-invariant"; -import { logger, printDivider, printHeader } from "utils"; +import { logger, printHeader } from "utils"; const IMAGE_NAME = "snowbridge-relay:local"; const RELATIVE_DOCKER_FILE_PATH = "../../docker/SnowbridgeRelayer.dockerfile"; diff --git a/test/suites/e2e/datahaven-basic.test.ts b/test/suites/e2e/datahaven-basic.test.ts new file mode 100644 index 00000000..14ee360d --- /dev/null +++ b/test/suites/e2e/datahaven-basic.test.ts @@ -0,0 +1,74 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import type { PolkadotSigner } from "polkadot-api"; +import { + type DataHavenApi, + SUBSTRATE_FUNDED_ACCOUNTS, + createPapiConnectors, + generateRandomAccount, + getPapiSigner, + logger +} from "utils"; +import { isAddress, parseEther } from "viem"; + +describe("Datahaven solochain", () => { + let api: DataHavenApi; + let signer: PolkadotSigner; + + beforeAll(() => { + const { typedApi } = createPapiConnectors(); + api = typedApi; + signer = getPapiSigner(); + }); + + it("Can query runtime API", async () => { + const address = await api.apis.EthereumRuntimeRPCApi.author(); + logger.debug(`Author Address is: ${address.asHex()}`); + expect(isAddress(address.asHex())).toBeTrue(); + }); + + it("Can lookup storages ", async () => { + const { + data: { free: freeBalance } + } = await api.query.System.Account.getValue(SUBSTRATE_FUNDED_ACCOUNTS.ALITH.publicKey); + logger.debug(`Balance of ALITH on DH is ${freeBalance}`); + expect(freeBalance).toBeGreaterThan(0n); + }); + + it("Can submit extrinsics into finalized block", async () => { + const value = parseEther("1"); + const { address: dest } = generateRandomAccount(); + const ext = api.tx.Balances.transfer_allow_death({ + dest, + value + }); + + // This will wait until finalized block + const resp = await ext.signAndSubmit(signer, {}); + logger.debug(`Transaction in finalized block: ${resp.txHash}`); + }); + + // This is way faster and should be how we submit build tests + it("Can submit extrinsics into best block", async () => { + const value = parseEther("1"); + const { address: dest } = generateRandomAccount(); + const ext = api.tx.Balances.transfer_allow_death({ + dest, + value + }); + + const resp = await ext.signAndSubmit(signer, { at: "best" }); + logger.debug(`Transaction submitted: ${resp.txHash}`); + const { + data: { free: freeBalance } + } = await api.query.System.Account.getValue(dest, { at: "best" }); + logger.debug(`Balance of ${dest} on DH is ${freeBalance}`); + expect(freeBalance).toBeGreaterThan(0n); + }); + + it("Can listen to events", async () => { + const event = await api.event.System.ExtrinsicSuccess.pull(); + logger.debug(event[0]); + expect(event).not.toBeEmpty(); + expect(event[0].payload.dispatch_info.weight.ref_time).toBeGreaterThan(0n); + }); +}); diff --git a/test/suites/e2e/service-manager.test.ts b/test/suites/e2e/service-manager.test.ts index e3650f39..0b61402f 100644 --- a/test/suites/e2e/service-manager.test.ts +++ b/test/suites/e2e/service-manager.test.ts @@ -1,14 +1,5 @@ import { beforeAll, describe, expect, it } from "bun:test"; -import { beefyClientAbi } from "contract-bindings"; -import { - type AnvilDeployments, - type ContractInstance, - type ViemClientInterface, - createDefaultClient, - getContractInstance, - logger, - parseDeploymentsFile -} from "utils"; +import { type ContractInstance, getContractInstance, logger } from "utils"; import { isAddress } from "viem"; describe("BeefyClient contract", async () => { diff --git a/test/tsconfig.json b/test/tsconfig.json index e580d754..e8c43dc0 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -25,7 +25,11 @@ "baseUrl": "./", "preserveConstEnums": true, "esModuleInterop": true, - "resolveJsonModule": true + "resolveJsonModule": true, + + // Speed + "tsBuildInfoFile": "tmp/tsconfig.tsbuildinfo", + "assumeChangesOnlyAffectDirectDependencies": true }, "include": [ "utils/*.ts", diff --git a/test/utils/docker.ts b/test/utils/docker.ts index 05698d8c..52182bf1 100644 --- a/test/utils/docker.ts +++ b/test/utils/docker.ts @@ -1,12 +1,12 @@ +import { type Duplex, PassThrough, Transform } from "node:stream"; import Docker from "dockerode"; import invariant from "tiny-invariant"; -import { type ServiceInfo, type ServiceMapping, StandardServiceMappings, logger } from "utils"; +import { type ServiceInfo, StandardServiceMappings, logger } from "utils"; + +const docker = new Docker({}); export const getServicesFromDocker = async (): Promise => { - const docker = new Docker(); - const containers = await docker.listContainers(); - const services: ServiceInfo[] = []; for (const mapping of StandardServiceMappings) { @@ -83,3 +83,92 @@ export const getPublicPort = async ( invariant(portMappings, `โŒ port mapping not found for ${containerName}:${internalPort}`); return portMappings.PublicPort; }; + +export async function waitForLog(opts: { + search: string | RegExp; + containerName: string; + timeoutSeconds?: number; +}): Promise { + const container = docker.getContainer(opts.containerName); + await container.inspect(); + const timeoutMs = (opts.timeoutSeconds ?? 10) * 1_000; + + const rawStream = (await container.logs({ + stdout: true, + stderr: true, + follow: true, + since: 0 + })) as Duplex; + const pass = new PassThrough(); + container.modem.demuxStream(rawStream, pass, pass); + + const { readable } = Transform.toWeb(pass); + const decoder = new TextDecoder(); + const timer = setTimeout( + () => + pass.destroy( + new Error( + `Timed out after ${timeoutMs} ms waiting for โ€œ${opts.search}โ€ in ${opts.containerName}` + ) + ), + timeoutMs + ); + + try { + for await (const chunk of readable) { + const text = decoder.decode(chunk as Uint8Array, { stream: false }); + + const hit = + typeof opts.search === "string" ? text.includes(opts.search) : opts.search.test(text); + + if (hit) return text.trim(); + } + + throw new Error( + `Log stream ended before โ€œ${opts.search}โ€ appeared for container ${opts.containerName}` + ); + } finally { + if (timer) { + clearTimeout(timer); + } + + if (pass && typeof pass.destroy === "function" && !pass.destroyed) { + pass.destroy(); + } + + if (rawStream) { + if (typeof rawStream.destroy === "function" && !rawStream.destroyed) { + rawStream.destroy(); + } + const socket = (rawStream as any).socket; + if (socket && typeof socket.destroy === "function" && !socket.destroyed) { + socket.destroy(); + } + } + } +} + +export const waitForContainerToStart = async ( + containerName: string, + options?: { timeoutSeconds?: number } +) => { + logger.debug(`Waiting for container ${containerName} to start...`); + const docker = new Docker(); + const seconds = options?.timeoutSeconds ?? 30; + + for (let i = 0; i < seconds; i++) { + const containers = await docker.listContainers(); + const container = containers.find((container) => + container.Names.some((name) => name.includes(containerName)) + ); + if (container) { + logger.debug(`Container ${containerName} started after ${i} seconds`); + return; + } + await Bun.sleep(1000); + } + invariant( + false, + `โŒ container ${containerName} cannot be found in running container list after ${seconds} seconds` + ); +}; diff --git a/test/utils/kurtosis.ts b/test/utils/kurtosis.ts index 8cb95e59..5fd78368 100644 --- a/test/utils/kurtosis.ts +++ b/test/utils/kurtosis.ts @@ -63,7 +63,6 @@ const portDetailSchema = z.object({ }); const portsListSchema = z.record(z.string(), portDetailSchema); -type PortsList = z.infer; const serviceSchema = z.object({ image: z.string(), diff --git a/test/utils/papi.ts b/test/utils/papi.ts index 125ce46d..b020a57f 100644 --- a/test/utils/papi.ts +++ b/test/utils/papi.ts @@ -1,6 +1,12 @@ import { secp256k1 } from "@noble/curves/secp256k1"; import { keccak_256 } from "@noble/hashes/sha3"; +import { datahaven } from "@polkadot-api/descriptors"; +import { type PolkadotClient, type TypedApi, createClient } from "polkadot-api"; +import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat"; import { type PolkadotSigner, getPolkadotSigner } from "polkadot-api/signer"; +import { getWsProvider } from "polkadot-api/ws-provider/web"; +import { SUBSTRATE_FUNDED_ACCOUNTS } from "./constants"; +import type { Prettify } from "./types"; // A signer for EVM like chains that use AccountId20 as their public address export const getEvmEcdsaSigner = (privateKey: string): PolkadotSigner => { @@ -31,3 +37,16 @@ export const signEcdsa = ( return result; }; + +export const createPapiConnectors = ( + wsUrl?: string +): { client: PolkadotClient; typedApi: DataHavenApi } => { + const url = wsUrl ?? "ws://127.0.0.1:9944"; + const client = createClient(withPolkadotSdkCompat(getWsProvider(url))); + return { client, typedApi: client.getTypedApi(datahaven) }; +}; + +export const getPapiSigner = (person: keyof typeof SUBSTRATE_FUNDED_ACCOUNTS = "ALITH") => + getEvmEcdsaSigner(SUBSTRATE_FUNDED_ACCOUNTS[person].privateKey); + +export type DataHavenApi = Prettify>; diff --git a/test/utils/shell.ts b/test/utils/shell.ts index a379dc6b..bdc49dc7 100644 --- a/test/utils/shell.ts +++ b/test/utils/shell.ts @@ -6,7 +6,12 @@ export type LogLevel = "info" | "debug" | "error" | "warn"; export const runShellCommandWithLogger = async ( command: string, - options?: { cwd?: string; env?: object; logLevel?: LogLevel } + options?: { + cwd?: string; + env?: object; + logLevel?: LogLevel; + waitFor?: (...args: unknown[]) => Promise; + } ) => { const { cwd = ".", env = {}, logLevel = "info" as LogLevel } = options || {}; @@ -56,6 +61,10 @@ export const runShellCommandWithLogger = async ( readStream(stderrReader, "stderr", "error") ]); + if (options?.waitFor) { + await options.waitFor(); + } + await proc.exited; } catch (err) { logger.error("โŒ Error running shell command:", command, "in", cwd); diff --git a/test/utils/types.ts b/test/utils/types.ts index 6a51b50a..fd1103a7 100644 --- a/test/utils/types.ts +++ b/test/utils/types.ts @@ -219,3 +219,9 @@ const hexToUint8Array = (hex: string): Uint8Array => { } return Buffer.from(hexString, "hex"); }; + +// This squashes together the properties of the input type T +// making it easier to view in an IDE +export type Prettify = { + [K in keyof T]: T[K]; +} & {};