test: ️ CI Refactor (#59)

Eventually our CI will be required to run two private blockchains
locally plus associated relayers.

This PR is to prepare for this fate by improving run times and
refactoring our existing CIs so they are a bit easier to reason about.

### Refactors
- **_We now run ALL CIs on every PR!_** This is so that we decomplexify
the logic around conditional builds and fetching built binaries from
another source. This reduces the surface area of code we have to
maintain at the cost of execution time
- This penalty is ameliorated by a layered caching system. At best, it
will be less than a minute to complete a build since everything will be
cached. On GH runners this is about 6 minutes sadly.
- We will no longer be at risk of important CIs being skipped
erroneously which hide true failures.
- Caching is a low-risk approach because at worst it has to build from
scratch. A bad cache hit will never imply the wrong thing gets build
since cargo is smart enough to just throw away any inappropriate build
artefacts.
- `setup-rust` action created so we have a unified way of setting up
runner and unifying our approach to caching
- Use a unique caching key for different activities and it will fallback
to shared cache if no matches
- we are using `mainnet` kurtosis config so that it works with relayer
assumptions

### Additions
- We can specify the ethereum block time via a new cli arg `--slot-time
<seconds>`
- We can specify arbitrary network_param args which get passed into the
generated yaml
- e.g. giving `bun cli --kurtosis-network-args="pet=cat food=fish" will
add:

```yml
network_params:
  # existing params...
  pet: cat
  food: fish
```

- We now have the ability to programmatically modify the yaml
- This means we are back down to a single `minimal.yml` kurtosis config
so we dont have to maintain changes between them
- Flow is: `add new cli arg` -> `add if() block which mutates yaml` ->
`profit`

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
This commit is contained in:
Tim B 2025-05-06 21:20:02 +01:00 committed by GitHub
parent e16420f266
commit 3776d80a2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 633 additions and 461 deletions

38
.github/workflows/CI.yml vendored Normal file
View file

@ -0,0 +1,38 @@
#! Main CI Specification for DataHaven Repository
name: CI
on:
workflow_dispatch:
push:
branches:
- main
pull_request:
branches: [main]
concurrency:
group: pr-checks-${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
# First Tier
build-operator:
uses: ./.github/workflows/task-build-operator.yml
secrets: inherit # For demonstrative purposes, we don't use any secrets yet.
ts-build:
uses: ./.github/workflows/task-ts-build.yml
ts-lint:
uses: ./.github/workflows/task-ts-lint.yml
unit-tests:
uses: ./.github/workflows/task-rust-tests.yml
contract-tests:
uses: ./.github/workflows/task-foundry-tests.yml
rust-lint:
uses: ./.github/workflows/task-rust-lint.yml
# Second Tier
e2e-tests:
needs: [build-operator]
uses: ./.github/workflows/task-e2e.yml
with:
binary-hash: ${{ needs.build-operator.outputs.binary-hash }}

View file

@ -0,0 +1,59 @@
name: "Setup Rust Environment"
description: "Creates a Rust environment with the specified toolchain, cache, and dependencies"
inputs:
cache-key:
description: "Cache key used to retrieve built data. Usually matches the profile of the build"
required: false
default: "cache"
cache-targets:
description: "Whether to cache targets"
required: false
default: "true"
runs:
using: "composite"
steps:
- name: Set Rust version
shell: bash
run: |
echo "BUILD_RUST_VERSION=$(rustc --version)" >> $GITHUB_ENV
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.9
- name: Set Rust caching env vars
if: github.event_name != 'release' && github.event_name != 'workflow_dispatch'
shell: bash
run: |
echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV
sccache --show-stats
- name: Cache cargo + target + sccache2
uses: Swatinem/rust-cache@v2
with:
cache-targets: true
cache-all-crates: true
shared-key: ${{ runner.os }}-${{inputs.cache-key}}-${{ hashFiles('operator/rust-toolchain.toml') }}-${{ hashFiles('operator/Cargo.lock') }}
workspaces: |
operator -> target
cache-on-failure: true
cache-bin: true
cache-directories: |
~/.cache/sccache
~/.cargo/registry
~/.cargo/git
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: rui314/setup-mold@v1
- name: Install libpq-dev
shell: bash
run: sudo apt-get update && sudo apt-get install -y libpq-dev libclang-dev
- name: Install Protoc
uses: arduino/setup-protoc@v3

View file

@ -1,55 +0,0 @@
# Foundry Tests: CI for Foundry components (smart contracts for EigenLayer and Snowbridge interaction)
#
# Overview:
# 1. All Foundry Tests: Executes the full suite of Foundry tests found within the `./contracts` directory
name: Foundry AVS Smart Contract Tests
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
env:
FOUNDRY_PROFILE: ci
jobs:
test:
strategy:
fail-fast: false
matrix:
partition: [1]
name: Foundry Tests
runs-on: ubuntu-latest
defaults:
run:
working-directory: contracts
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
- name: Show Forge version
run: |
forge --version
- name: Run Forge fmt
run: |
forge fmt --check
id: fmt
- name: Run Forge build
run: |
forge build --sizes
id: build
- name: Run Forge tests
run: |
forge test -vvv
id: test

View file

@ -1,129 +0,0 @@
# Lint and Format: CI for Rust components (DataHaven runtime and node Rust tests)
#
# Overview:
# 1. Check Rust Format: Check that the Rust code is formatted correctly
# 2. Check Rust Lint: Check that the Rust code is linted correctly
name: Lint and Format
on:
pull_request:
push:
branches:
- main
- perm-*
workflow_dispatch:
inputs:
pull_request:
description: set to pull_request number to execute on external pr
required: false
env:
CARGO_TERM_COLOR: always
WORKING_DIR: operator
jobs:
setup:
runs-on: ubuntu-latest
outputs:
node_changed: ${{ steps.node_check.outputs.changed }}
env:
SKIP_BUILD_LABEL_PRESENT: ${{ contains(github.event.pull_request.labels.*.name, 'skip-node-build') }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check if Substrate Node needs rebuild
id: node_check
run: |
BASE_SHA="${{ github.event.pull_request.base.sha || github.event.before }}"
HEAD_SHA="${{ github.sha }}"
if [[ "${{ env.SKIP_BUILD_LABEL_PRESENT }}" != "true" ]] && git diff --name-only $BASE_SHA $HEAD_SHA | grep -E '^operator/(client|node|pallets|runtime)/|^operator/Cargo\.toml$'; then
echo "changed=true" >> $GITHUB_OUTPUT
else
echo "Comparing changes from $BASE_SHA to $HEAD_SHA"
echo "changed=false" >> $GITHUB_OUTPUT
fi
cargo-fmt:
needs: [ setup ]
if: needs.setup.outputs.node_changed == 'true'
name: "Check format with rustfmt"
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ env.WORKING_DIR }}
steps:
- uses: actions/checkout@v4
- name: Cache dependencies
uses: Swatinem/rust-cache@v2
# ! If this action starts failing, it may be because of this,
# ! For now this is not needed, and makes the workflow slower
# - name: Install protoc
# run: |
# sudo apt-get update
# sudo apt-get install -y protobuf-compiler
# protoc --version
- name: Run cargo fmt
run: cargo fmt --all -- --check
check-rust-lint:
needs: [ setup ]
if: needs.setup.outputs.node_changed == 'true'
name: "Check lint with clippy"
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ env.WORKING_DIR }}
steps:
- uses: actions/checkout@v4
- name: Install protoc
run: |
sudo apt-get update
sudo apt-get install -y protobuf-compiler
protoc --version
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Cache dependencies
uses: Swatinem/rust-cache@v2
- name: Install libpq-dev
run: sudo apt-get update && sudo apt-get install -y libpq-dev
- name: Install protoc
run: |
sudo apt-get update
sudo apt-get install -y protobuf-compiler
protoc --version
- name: Run cargo clippy
run: SKIP_WASM_BUILD=1 env -u RUSTFLAGS cargo clippy --features try-runtime,runtime-benchmarks --locked
env:
RUSTFLAGS: -D warnings
check-cargo-sort:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ env.WORKING_DIR }}
steps:
- name: Check out the repository to the runner
uses: actions/checkout@v4
- name: Make script executable
run: chmod +x scripts/sort-cargo-deps.sh
- name: Run the script on all Cargo.toml files
run: find . -name "Cargo.toml" -print0 | xargs -0 -n1 -I{} bash -c 'scripts/sort-cargo-deps.sh "{}" check' || exit 1

View file

@ -1,128 +0,0 @@
# Rust Tests: CI for Rust components (DataHaven runtime and node Rust tests)
#
# Overview:
# 1. Prepare: This job handles the setup phase where the cargo nextest archive is created
# and uploaded to the workflow for use in the subsequent jobs
# 2. All Rust Tests: Executes the full suite of Rust tests across two partitions to
# to reduce total execution time.
name: DataHave Operator Rust Tests
on:
pull_request:
push:
branches:
- main
workflow_dispatch:
jobs:
setup:
runs-on: ubuntu-latest
outputs:
node_changed: ${{ steps.node_check.outputs.changed }}
env:
SKIP_BUILD_LABEL_PRESENT: ${{ contains(github.event.pull_request.labels.*.name, 'skip-node-build') }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check if Substrate Node needs rebuild
id: node_check
run: |
BASE_SHA="${{ github.event.pull_request.base.sha || github.event.before }}"
HEAD_SHA="${{ github.sha }}"
if [[ "${{ env.SKIP_BUILD_LABEL_PRESENT }}" != "true" ]] && git diff --name-only $BASE_SHA $HEAD_SHA | grep -E '^operator/(client|node|pallets|runtime)/|^operator/Cargo\.toml$'; then
echo "changed=true" >> $GITHUB_OUTPUT
else
echo "Comparing changes from $BASE_SHA to $HEAD_SHA"
echo "changed=false" >> $GITHUB_OUTPUT
fi
prepare:
needs: [setup]
if: needs.setup.outputs.node_changed == 'true'
name: Prepare artifacts for Rust tests
runs-on: ubuntu-latest
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
CARGO_INCREMENTAL: "0"
CARGO_TERM_COLOR: always
defaults:
run:
working-directory: ./operator
steps:
- uses: actions/checkout@v4
with:
# By default actions/checkout checks out a merge commit. Check out the PR head instead.
# https://github.com/actions/checkout#checkout-pull-request-head-commit-instead-of-merge-commit
ref: ${{ github.event.pull_request.head.sha }}
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.9
- uses: rui314/setup-mold@v1
- name: Install nextest
uses: taiki-e/install-action@nextest
# Install libpq-dev
- name: Install libpq-dev
run: sudo apt-get update && sudo apt-get install -y libpq-dev
- name: Install Protoc
uses: arduino/setup-protoc@v3
- name: Build and archive tests
run: cargo nextest archive --archive-file nextest-archive.tar.zst
- name: Upload archive to workflow
uses: actions/upload-artifact@v4
with:
name: nextest-archive
path: ./operator/nextest-archive.tar.zst
all-rust-tests:
needs: [setup, prepare]
if: needs.setup.outputs.node_changed == 'true'
name: Run all Operator Rust tests (/w partitioning)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
partition: [1, 2]
defaults:
run:
working-directory: ./operator
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Download archive
uses: actions/download-artifact@v4
with:
name: nextest-archive
- name: Run Tests for All Projects!
run: |
~/.cargo/bin/cargo-nextest nextest run \
--archive-file ../nextest-archive.tar.zst \
--partition count:${{ matrix.partition }}/2
tests-result-checker:
name: Check tests were successful
needs: [setup, all-rust-tests]
if: always()
runs-on: ubuntu-latest
steps:
- name: Validate test results
run: |
echo "node_changed: ${{ needs.setup.outputs.node_changed }}"
echo "matrix result: ${{ needs.all-rust-tests.result }}"
if [ "${{ needs.setup.outputs.node_changed }}" == "true" ]; then
if [ "${{ needs.all-rust-tests.result }}" != "success" ]; then
echo "Rust tests failed or were cancelled"
exit 1
else
echo "Rust tests passed"
fi
else
echo "No relevant changes — skipping rust tests"
fi

View file

@ -0,0 +1,57 @@
# Build Operator: CI for building the operator binary
name: DataHave Operator Rust Tests
on:
workflow_dispatch:
workflow_call:
outputs:
binary-hash:
description: "The hash of the operator binary"
value: ${{ jobs.build-node.outputs.binary-hash }}
jobs:
build-node:
outputs:
binary-hash: ${{ steps.hash-binary.outputs.datahaven-node-hash }}
name: Build operator binary
runs-on: ubuntu-latest
env:
RUSTC_WRAPPER: "sccache"
CARGO_INCREMENTAL: "0"
CARGO_TERM_COLOR: always
RUSTFLAGS: "-C linker=clang -C link-arg=-fuse-ld=mold"
defaults:
run:
working-directory: ./operator
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 1
- uses: ./.github/workflows/actions/setup-env
with:
cache-key: CI
- name: Build node binary
run: cargo build --profile ci --locked
- name: Hash binary
id: hash-binary
run: |
TIMESTAMP=$(date +%s)
BINARY_PATH=./target/ci/datahaven-node
HASH=$(echo "$TIMESTAMP" | cat - $BINARY_PATH | sha256sum | awk '{ print $1 }')
echo "datahaven-node-hash=$HASH" >> $GITHUB_OUTPUT
echo "Hash of the datahaven-node is: $HASH (with timestamp: $TIMESTAMP)"
- name: Upload binary to workflow
uses: actions/upload-artifact@v4
with:
name: datahaven-node-${{ steps.hash-binary.outputs.datahaven-node-hash }}
path: operator/target/ci/datahaven-node
retention-days: 1
- name: Build Stats
run: |
sccache --show-stats

View file

@ -8,15 +8,13 @@
name: E2E - Kurtosis Deploy and Verify
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
concurrency:
group: "tests-${{ github.head_ref }}"
cancel-in-progress: true
workflow_call:
inputs:
binary-hash:
description: "The hash of the operator binary"
required: true
type: string
env:
FOUNDRY_PROFILE: ci
@ -50,12 +48,31 @@ jobs:
restore-keys: |
${{ runner.os }}-bun-
- uses: actions/cache@v4
- name: Cache Foundry libraries
uses: actions/cache/restore@v4
with:
path: ~/.foundry/cache
key: ${{ runner.os }}-foundry-${{ hashFiles('**/foundry.toml') }}
path: ../contracts/lib
key: ${{ runner.os }}-foundry-libs-${{ hashFiles('.gitmodules') }}
restore-keys: |
${{ runner.os }}-foundry-
${{ runner.os }}-foundry-libs-
- name: Cache Foundry build artifacts
uses: actions/cache/restore@v4
with:
path: |
../contracts/out
../contracts/cache
key: ${{ runner.os }}-foundry-build-${{ hashFiles('contracts/foundry.toml', 'contracts/**/*.sol') }}
restore-keys: |
${{ runner.os }}-foundry-build-
- name: Download operator binary
uses: actions/download-artifact@v4
with:
name: datahaven-node-${{ inputs.binary-hash }}
path: operator/target/release/
- run: chmod +x ../operator/target/release/datahaven-node
- run: ../operator/target/release/datahaven-node --help
- run: bun install
- run: bun start:e2e:ci

View file

@ -0,0 +1,56 @@
# Foundry Tests: CI for Foundry components (smart contracts for EigenLayer and Snowbridge interaction)
#
# Overview:
# 1. All Foundry Tests: Executes the full suite of Foundry tests found within the `./contracts` directory
name: Foundry AVS Smart Contract Tests
on:
workflow_dispatch:
workflow_call:
env:
FOUNDRY_PROFILE: ci
jobs:
test:
strategy:
fail-fast: false
matrix:
partition: [1]
name: Foundry Tests
runs-on: ubuntu-latest
defaults:
run:
working-directory: contracts
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
- name: Cache Foundry libraries
uses: actions/cache@v4
with:
path: contracts/lib
key: ${{ runner.os }}-foundry-libs-${{ hashFiles('contracts/.gitmodules') }}
restore-keys: |
${{ runner.os }}-foundry-libs-
- name: Cache Foundry build artifacts
uses: actions/cache@v4
with:
path: |
contracts/out
contracts/cache
key: ${{ runner.os }}-foundry-build-${{ hashFiles('contracts/foundry.toml', 'contracts/**/*.sol') }}
restore-keys: |
${{ runner.os }}-foundry-build-
- run: forge --version
- run: forge fmt --check
- run: forge build --sizes
- run: forge test -vvv

72
.github/workflows/task-rust-lint.yml vendored Normal file
View file

@ -0,0 +1,72 @@
# Lint and Format: CI for Rust components (DataHaven runtime and node Rust tests)
#
# Overview:
# 1. Check Rust Format: Check that the Rust code is formatted correctly
# 2. Check Rust Lint: Check that the Rust code is linted correctly
name: Lint and Format
on:
workflow_call:
workflow_dispatch:
inputs:
pull_request:
description: set to pull_request number to execute on external pr
required: false
env:
CARGO_TERM_COLOR: always
WORKING_DIR: operator
RUSTFLAGS: "-C linker=clang -C link-arg=-fuse-ld=mold"
jobs:
cargo-fmt:
name: "Check format with rustfmt"
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ env.WORKING_DIR }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/workflows/actions/setup-env
with:
cache-key: FMT
cache-targets: false
- name: Run cargo fmt
run: cargo fmt --all -- --check
check-rust-lint:
name: "Check lint with clippy"
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ env.WORKING_DIR }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/workflows/actions/setup-env
with:
cache-key: LINT
- name: Run cargo clippy
run: SKIP_WASM_BUILD=1 cargo clippy --features try-runtime,runtime-benchmarks --locked --release
check-cargo-sort:
name: "Check Cargo sort"
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ env.WORKING_DIR }}
steps:
- name: Check out the repository to the runner
uses: actions/checkout@v4
- name: Make script executable
run: chmod +x scripts/sort-cargo-deps.sh
- name: Run the script on all Cargo.toml files
run: find . -name "Cargo.toml" -print0 | xargs -0 -n1 -I{} bash -c 'scripts/sort-cargo-deps.sh "{}" check' || exit 1

88
.github/workflows/task-rust-tests.yml vendored Normal file
View file

@ -0,0 +1,88 @@
# Rust Tests: CI for Rust components (DataHaven runtime and node Rust tests)
#
# Overview:
# 1. Prepare: This job handles the setup phase where the cargo nextest archive is created
# and uploaded to the workflow for use in the subsequent jobs
# 2. All Rust Tests: Executes the full suite of Rust tests across two partitions to
# to reduce total execution time.
name: DataHave Operator Rust Tests
on:
workflow_dispatch:
workflow_call:
jobs:
prepare:
name: Prepare artifacts for Rust tests
runs-on: ubuntu-latest
env:
RUSTC_WRAPPER: "sccache"
CARGO_INCREMENTAL: "0"
CARGO_TERM_COLOR: always
RUSTFLAGS: "-C linker=clang -C link-arg=-fuse-ld=mold"
defaults:
run:
working-directory: ./operator
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- uses: ./.github/workflows/actions/setup-env
with:
cache-key: "TEST"
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Build and archive tests
run: cargo nextest archive --archive-file nextest-archive.tar.zst
- name: Upload archive to workflow
uses: actions/upload-artifact@v4
with:
name: nextest-archive
path: ./operator/nextest-archive.tar.zst
all-rust-tests:
name: Run all Operator Rust tests (/w partitioning)
needs: [prepare]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
partition: [1, 2]
defaults:
run:
working-directory: ./operator
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Download archive
uses: actions/download-artifact@v4
with:
name: nextest-archive
- name: Run Tests for All Projects!
run: |
~/.cargo/bin/cargo-nextest nextest run \
--archive-file ../nextest-archive.tar.zst \
--partition count:${{ matrix.partition }}/2
tests-result-checker:
name: Check tests were successful
needs: [all-rust-tests]
runs-on: ubuntu-latest
steps:
- name: Validate test results
run: |
echo "matrix result: ${{ needs.all-rust-tests.result }}"
if [ "${{ needs.all-rust-tests.result }}" != "success" ]; then
echo "Rust tests failed or were cancelled"
exit 1
else
echo "Rust tests passed"
fi

View file

@ -1,11 +1,8 @@
name: TS Build
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
workflow_call:
jobs:
generate-wagmi:

View file

@ -1,11 +1,8 @@
name: TS Lint & Format
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
workflow_call:
jobs:
typecheck:

View file

@ -6,6 +6,7 @@
name: Audit Rust dependencies
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * 0'
push:

View file

@ -0,0 +1,24 @@
// SPDX-License-Identifier: GPL-3.0
// This is a test contract for the DataHaven project
// It is deployed as pure bytecode in kurtosis
pragma solidity >=0.8.2 <0.9.0;
contract DataHavenTest {
uint256 public number;
address public owner;
constructor() {
number = 10;
owner = msg.sender;
}
function decrement() external {
require(number > 0, "Number should be greater than 0");
number = number - 1;
}
function reset() external {
require(msg.sender == owner, "Only callable by owner!");
number = 10;
}
}

View file

@ -206,3 +206,9 @@ fc-mapping-sync = { git = "https://github.com/polkadot-evm/frontier", branch = "
fc-rpc = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2412", default-features = false }
fc-rpc-core = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2412", default-features = false }
fc-storage = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2412", default-features = false }
[profile.ci]
inherits = "release"
incremental = false
codegen-units = 16
lto = false

View file

@ -23,6 +23,7 @@
"tiny-invariant": "^1.3.3",
"viem": "^2.28.0",
"wagmi": "^2.15.0",
"yaml": "^2.7.1",
"zod": "^3.24.3",
},
"devDependencies": {
@ -194,7 +195,7 @@
"@metamask/utils": ["@metamask/utils@8.5.0", "", { "dependencies": { "@ethereumjs/tx": "^4.2.0", "@metamask/superstruct": "^3.0.0", "@noble/hashes": "^1.3.1", "@scure/base": "^1.1.3", "@types/debug": "^4.1.7", "debug": "^4.3.4", "pony-cause": "^2.1.10", "semver": "^7.5.4", "uuid": "^9.0.1" } }, "sha512-I6bkduevXb72TIM9q2LRO63JSsF9EXduh3sBr9oybNX2hNNpr/j1tEjXrsG0Uabm4MJ1xkGAQEMwifvKZIkyxQ=="],
"@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="],
"@noble/ciphers": ["@noble/ciphers@1.2.1", "", {}, "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA=="],
"@noble/curves": ["@noble/curves@1.8.2", "", { "dependencies": { "@noble/hashes": "1.7.2" } }, "sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g=="],
@ -932,6 +933,8 @@
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
@ -942,6 +945,8 @@
"zustand": ["zustand@5.0.0", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ=="],
"@coinbase/wallet-sdk/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
"@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
@ -956,6 +961,8 @@
"@metamask/sdk-communication-layer/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
"@metamask/utils/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"@metamask/utils/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"@reown/appkit/@walletconnect/types": ["@walletconnect/types@2.19.2", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "events": "3.3.0" } }, "sha512-/LZWhkVCUN+fcTgQUxArxhn2R8DF+LSd/6Wh9FnpjeK/Sdupx1EPS8okWG6WPAqq2f404PRoNAfQytQ82Xdl3g=="],
@ -992,8 +999,6 @@
"@walletconnect/time/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
"@walletconnect/utils/@noble/ciphers": ["@noble/ciphers@1.2.1", "", {}, "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA=="],
"@walletconnect/utils/@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="],
"@walletconnect/utils/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="],
@ -1016,6 +1021,8 @@
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"eciesjs/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"engine.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
"engine.io-client/ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="],
@ -1038,6 +1045,8 @@
"obj-multiplex/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"ox/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
"restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
@ -1060,6 +1069,8 @@
"@metamask/eth-json-rpc-provider/@metamask/json-rpc-engine/@metamask/utils": ["@metamask/utils@8.5.0", "", { "dependencies": { "@ethereumjs/tx": "^4.2.0", "@metamask/superstruct": "^3.0.0", "@noble/hashes": "^1.3.1", "@scure/base": "^1.1.3", "@types/debug": "^4.1.7", "debug": "^4.3.4", "pony-cause": "^2.1.10", "semver": "^7.5.4", "uuid": "^9.0.1" } }, "sha512-I6bkduevXb72TIM9q2LRO63JSsF9EXduh3sBr9oybNX2hNNpr/j1tEjXrsG0Uabm4MJ1xkGAQEMwifvKZIkyxQ=="],
"@metamask/rpc-errors/@metamask/utils/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"@metamask/rpc-errors/@metamask/utils/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"@reown/appkit-controllers/@walletconnect/universal-provider/@walletconnect/sign-client": ["@walletconnect/sign-client@2.19.2", "", { "dependencies": { "@walletconnect/core": "2.19.2", "@walletconnect/events": "1.0.1", "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/logger": "2.1.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.19.2", "@walletconnect/utils": "2.19.2", "events": "3.3.0" } }, "sha512-a/K5PRIFPCjfHq5xx3WYKHAAF8Ft2I1LtxloyibqiQOoUtNLfKgFB1r8sdMvXM7/PADNPe4iAw4uSE6PrARrfg=="],
@ -1128,12 +1139,12 @@
"yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@metamask/eth-json-rpc-provider/@metamask/json-rpc-engine/@metamask/utils/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"@metamask/eth-json-rpc-provider/@metamask/json-rpc-engine/@metamask/utils/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"@reown/appkit-controllers/@walletconnect/universal-provider/@walletconnect/sign-client/@walletconnect/core": ["@walletconnect/core@2.19.2", "", { "dependencies": { "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/jsonrpc-ws-connection": "1.0.16", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "@walletconnect/relay-api": "1.0.11", "@walletconnect/relay-auth": "1.1.0", "@walletconnect/safe-json": "1.0.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.19.2", "@walletconnect/utils": "2.19.2", "@walletconnect/window-getters": "1.0.1", "es-toolkit": "1.33.0", "events": "3.3.0", "uint8arrays": "3.1.0" } }, "sha512-iu0mgLj51AXcKpdNj8+4EdNNBd/mkNjLEhZn6UMc/r7BM9WbmpPMEydA39WeRLbdLO4kbpmq4wTbiskI1rg+HA=="],
"@reown/appkit-controllers/@walletconnect/universal-provider/@walletconnect/utils/@noble/ciphers": ["@noble/ciphers@1.2.1", "", {}, "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA=="],
"@reown/appkit-controllers/@walletconnect/universal-provider/@walletconnect/utils/@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="],
"@reown/appkit-controllers/@walletconnect/universal-provider/@walletconnect/utils/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="],
@ -1142,8 +1153,6 @@
"@reown/appkit-utils/@walletconnect/universal-provider/@walletconnect/sign-client/@walletconnect/core": ["@walletconnect/core@2.19.2", "", { "dependencies": { "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/jsonrpc-ws-connection": "1.0.16", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "@walletconnect/relay-api": "1.0.11", "@walletconnect/relay-auth": "1.1.0", "@walletconnect/safe-json": "1.0.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.19.2", "@walletconnect/utils": "2.19.2", "@walletconnect/window-getters": "1.0.1", "es-toolkit": "1.33.0", "events": "3.3.0", "uint8arrays": "3.1.0" } }, "sha512-iu0mgLj51AXcKpdNj8+4EdNNBd/mkNjLEhZn6UMc/r7BM9WbmpPMEydA39WeRLbdLO4kbpmq4wTbiskI1rg+HA=="],
"@reown/appkit-utils/@walletconnect/universal-provider/@walletconnect/utils/@noble/ciphers": ["@noble/ciphers@1.2.1", "", {}, "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA=="],
"@reown/appkit-utils/@walletconnect/universal-provider/@walletconnect/utils/@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="],
"@reown/appkit-utils/@walletconnect/universal-provider/@walletconnect/utils/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="],
@ -1152,8 +1161,6 @@
"@reown/appkit/@walletconnect/universal-provider/@walletconnect/sign-client/@walletconnect/core": ["@walletconnect/core@2.19.2", "", { "dependencies": { "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/jsonrpc-ws-connection": "1.0.16", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "@walletconnect/relay-api": "1.0.11", "@walletconnect/relay-auth": "1.1.0", "@walletconnect/safe-json": "1.0.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.19.2", "@walletconnect/utils": "2.19.2", "@walletconnect/window-getters": "1.0.1", "es-toolkit": "1.33.0", "events": "3.3.0", "uint8arrays": "3.1.0" } }, "sha512-iu0mgLj51AXcKpdNj8+4EdNNBd/mkNjLEhZn6UMc/r7BM9WbmpPMEydA39WeRLbdLO4kbpmq4wTbiskI1rg+HA=="],
"@reown/appkit/@walletconnect/universal-provider/@walletconnect/utils/@noble/ciphers": ["@noble/ciphers@1.2.1", "", {}, "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA=="],
"@reown/appkit/@walletconnect/universal-provider/@walletconnect/utils/@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="],
"@reown/appkit/@walletconnect/universal-provider/@walletconnect/utils/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="],
@ -1162,7 +1169,7 @@
"@walletconnect/utils/viem/ox/@noble/curves": ["@noble/curves@1.8.2", "", { "dependencies": { "@noble/hashes": "1.7.2" } }, "sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g=="],
"@walletconnect/utils/viem/ox/@noble/hashes": ["@noble/hashes@1.7.2", "", {}, "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ=="],
"@walletconnect/utils/viem/ox/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"qrcode/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@ -1184,20 +1191,28 @@
"@reown/appkit/@walletconnect/universal-provider/@walletconnect/utils/viem/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
"@walletconnect/utils/viem/ox/@noble/curves/@noble/hashes": ["@noble/hashes@1.7.2", "", {}, "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ=="],
"qrcode/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"qrcode/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@reown/appkit-controllers/@walletconnect/universal-provider/@walletconnect/utils/viem/ox/@noble/curves": ["@noble/curves@1.8.2", "", { "dependencies": { "@noble/hashes": "1.7.2" } }, "sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g=="],
"@reown/appkit-controllers/@walletconnect/universal-provider/@walletconnect/utils/viem/ox/@noble/hashes": ["@noble/hashes@1.7.2", "", {}, "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ=="],
"@reown/appkit-controllers/@walletconnect/universal-provider/@walletconnect/utils/viem/ox/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"@reown/appkit-utils/@walletconnect/universal-provider/@walletconnect/utils/viem/ox/@noble/curves": ["@noble/curves@1.8.2", "", { "dependencies": { "@noble/hashes": "1.7.2" } }, "sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g=="],
"@reown/appkit-utils/@walletconnect/universal-provider/@walletconnect/utils/viem/ox/@noble/hashes": ["@noble/hashes@1.7.2", "", {}, "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ=="],
"@reown/appkit-utils/@walletconnect/universal-provider/@walletconnect/utils/viem/ox/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"@reown/appkit/@walletconnect/universal-provider/@walletconnect/utils/viem/ox/@noble/curves": ["@noble/curves@1.8.2", "", { "dependencies": { "@noble/hashes": "1.7.2" } }, "sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g=="],
"@reown/appkit/@walletconnect/universal-provider/@walletconnect/utils/viem/ox/@noble/hashes": ["@noble/hashes@1.7.2", "", {}, "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ=="],
"@reown/appkit/@walletconnect/universal-provider/@walletconnect/utils/viem/ox/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"@reown/appkit-controllers/@walletconnect/universal-provider/@walletconnect/utils/viem/ox/@noble/curves/@noble/hashes": ["@noble/hashes@1.7.2", "", {}, "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ=="],
"@reown/appkit-utils/@walletconnect/universal-provider/@walletconnect/utils/viem/ox/@noble/curves/@noble/hashes": ["@noble/hashes@1.7.2", "", {}, "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ=="],
"@reown/appkit/@walletconnect/universal-provider/@walletconnect/utils/viem/ox/@noble/curves/@noble/hashes": ["@noble/hashes@1.7.2", "", {}, "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ=="],
}
}

View file

@ -1,6 +1,5 @@
import type { Command } from "@commander-js/extra-typings";
import { deployContracts } from "scripts/deploy-contracts";
import { launchKurtosis } from "scripts/launch-kurtosis";
import sendTxn from "scripts/send-txn";
import invariant from "tiny-invariant";
import {
@ -12,6 +11,7 @@ import {
} from "utils";
import { checkDependencies } from "./checks";
import { performDatahavenOperations } from "./datahaven";
import { launchKurtosis } from "./kurtosis";
import { LaunchedNetwork } from "./launchedNetwork";
import { performRelayerOperations } from "./relayer";
import { performSummaryOperations } from "./summary";
@ -28,13 +28,16 @@ export interface LaunchOptions {
relayer?: boolean;
relayerBinPath?: string;
skipCleaning?: boolean;
alwaysClean?: boolean;
datahavenBinPath?: string;
datahaven?: boolean;
kurtosisNetworkArgs?: string;
slotTime?: number;
}
export const BASE_SERVICES = [
"cl-1-lighthouse-reth",
"cl-1-lighthouse-reth",
"cl-2-lighthouse-reth",
"el-1-reth-lighthouse",
"el-2-reth-lighthouse",
"dora"
@ -53,11 +56,7 @@ const launchFunction = async (options: LaunchOptions, launchedNetwork: LaunchedN
await checkDependencies();
logger.trace("Launching Kurtosis enclave");
await launchKurtosis({
launchKurtosis: options.launchKurtosis,
blockscout: options.blockscout,
skipCleaning: options.skipCleaning
});
await launchKurtosis(options);
logger.trace("Kurtosis enclave launched");
logger.trace("Send test transaction");
@ -116,7 +115,9 @@ const launchFunction = async (options: LaunchOptions, launchedNetwork: LaunchedN
printDivider();
performSummaryOperations(options, launchedNetwork);
logger.debug("Launch function completed successfully");
const fullEnd = performance.now();
const fullMinutes = ((fullEnd - timeStart) / (1000 * 60)).toFixed(1);
logger.info(`Launch function completed successfully in ${fullMinutes} minutes`);
};
export const launch = async (options: LaunchOptions) => {
@ -132,14 +133,7 @@ export const launch = async (options: LaunchOptions) => {
export const launchPreActionHook = (
thisCmd: Command<[], LaunchOptions & { [key: string]: any }>
) => {
const {
blockscout,
verified,
fundValidators,
setupValidators,
updateValidatorSet,
deployContracts
} = thisCmd.opts();
const { blockscout, verified, fundValidators, setupValidators, deployContracts } = thisCmd.opts();
if (verified && !blockscout) {
thisCmd.error("--verified requires --blockscout to be set");
}

View file

@ -1,4 +1,6 @@
import { $ } from "bun";
import type { LaunchOptions } from "cli/handlers";
import invariant from "tiny-invariant";
import {
type KurtosisService,
confirmWithTimeout,
@ -7,27 +9,18 @@ import {
printDivider,
printHeader
} from "utils";
import { parse, stringify } from "yaml";
/**
* Launches a Kurtosis Ethereum network enclave for testing.
*
* This function checks if a Kurtosis network is already running. If it is:
* - With `launchKurtosis: false` - keeps the existing enclave
* - With `launchKurtosis: true` - cleans and relaunches the enclave
* - With `launchKurtosis: undefined` - prompts the user to decide whether to relaunch
*
* If no network is running, it launches a new one.
*
* @param options - Configuration options
* @param options.launchKurtosis - Whether to forcibly launch Kurtosis (true), keep existing (false), or prompt user (undefined)
* @param options.blockscout - Whether to add Blockscout service (true/undefined) or not (false)
* @param options.skipCleaning - Whether to skip cleaning Kurtosis (true) or not (false)
* @returns Object containing success status and Docker services information
*/
export const launchKurtosis = async (
options: { launchKurtosis?: boolean; blockscout?: boolean; skipCleaning?: boolean } = {}
options: LaunchOptions = {}
): Promise<Record<string, KurtosisService>> => {
if (await checkKurtosisRunning()) {
if ((await checkKurtosisRunning()) && !options.alwaysClean) {
logger.info(" Kurtosis network is already running.");
logger.trace("Checking if launchKurtosis option was set via flags");
@ -73,10 +66,9 @@ export const launchKurtosis = async (
}
logger.info("🚀 Starting Kurtosis enclave...");
const configFile =
options.blockscout === true
? "configs/kurtosis/minimal-with-bs.yaml"
: "configs/kurtosis/minimal.yaml";
const configFile = await modifyConfig(options, "configs/kurtosis/minimal.yaml");
logger.info(`Using Kurtosis config file: ${configFile}`);
const { stderr, stdout, exitCode } =
@ -107,3 +99,42 @@ const checkKurtosisRunning = async (): Promise<boolean> => {
const text = await $`kurtosis enclave ls | grep "datahaven-ethereum" | grep RUNNING`.text();
return text.length > 0;
};
const modifyConfig = async (options: LaunchOptions, configFile: string) => {
const outputDir = "tmp/configs";
logger.debug(`Ensuring output directory exists: ${outputDir}`);
await $`mkdir -p ${outputDir}`.quiet();
const file = Bun.file(configFile);
invariant(file, `❌ Config file ${configFile} not found`);
const config = await file.text();
logger.debug(`Parsing config at ${configFile}`);
logger.trace(config);
const parsedConfig = parse(config);
if (options.blockscout) {
parsedConfig.additional_services.push("blockscout");
}
if (options.slotTime) {
parsedConfig.network_params.seconds_per_slot = options.slotTime;
}
if (options.kurtosisNetworkArgs) {
logger.debug(`Using custom Kurtosis network args: ${options.kurtosisNetworkArgs}`);
const args = options.kurtosisNetworkArgs.split(" ");
for (const arg of args) {
const [key, value] = arg.split("=");
parsedConfig.network_params[key] = value;
}
}
logger.trace(parsedConfig);
const outputFile = `${outputDir}/modified-config.yaml`;
logger.debug(`Modified config saving to ${outputFile}`);
await Bun.write(outputFile, stringify(parsedConfig));
return outputFile;
};

View file

@ -1,7 +1,16 @@
#!/usr/bin/env bun
import { Command } from "@commander-js/extra-typings";
import { Command, InvalidArgumentError } from "@commander-js/extra-typings";
import { launch, launchPreActionHook } from "./handlers";
// Function to parse integer
function parseIntValue(value: string): number {
const parsedValue = Number.parseInt(value, 10);
if (Number.isNaN(parsedValue)) {
throw new InvalidArgumentError("Not a number.");
}
return parsedValue;
}
// So far we only have the launch command
// we can expand this to more commands in the future
const program = new Command()
@ -14,13 +23,16 @@ const program = new Command()
.option("-u, --update-validator-set", "Update validator set")
.option("--no-update-validator-set", "Skip update validator set")
.option("-b, --blockscout", "Enable Blockscout")
.option("--slot-time <number>", "Set slot time in seconds", parseIntValue)
.option("--datahaven", "Enable Datahaven network to be launched")
.option("--kurtosis-network-args <value>", "CustomKurtosis network args")
.option(
"--datahaven-bin-path <value>",
"Path to the datahaven binary",
"../operator/target/release/datahaven-node"
)
.option("-v, --verified", "Verify smart contracts with Blockscout")
.option("--always-clean", "Always clean Kurtosis", false)
.option("-q, --skip-cleaning", "Skip cleaning Kurtosis")
.option("-r, --relayer", "Enable Relayer")
.option(

View file

@ -1,20 +0,0 @@
# Ethereum Private Testnet Configuration
# This configuration file is used with the ethPandaOps Ethereum Package for Kurtosis
# (https://github.com/ethpandaops/ethereum-package)
#
# Purpose: Sets up a minimal Ethereum testnet with multiple execution and consensus clients
# Usage: kurtosis run github.com/ethpandaops/ethereum-package --args-file configs/kurtosis/minimal-with-bs.yaml
participants:
- el_type: reth
cl_type: lighthouse
count: 2
additional_services:
- dora
- blockscout
network_params:
preset: minimal
num_validator_keys_per_node: 128
prefunded_accounts: '{"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266": {"balance": "10ETH"}, "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc": {"balance": "10ETH"}, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8": {"balance": "10ETH"},"0x976ea74026e726554db657fa54763abd0c3a0aa9": {"balance": "10ETH"}}'

View file

@ -1,10 +1,3 @@
# Ethereum Private Testnet Configuration
# This configuration file is used with the ethPandaOps Ethereum Package for Kurtosis
# (https://github.com/ethpandaops/ethereum-package)
#
# Purpose: Sets up a minimal Ethereum testnet with multiple execution and consensus clients
# Usage: kurtosis run github.com/ethpandaops/ethereum-package --args-file configs/kurtosis/minimal.yaml
participants:
- el_type: reth
cl_type: lighthouse
@ -14,6 +7,25 @@ additional_services:
- dora
network_params:
preset: minimal
preset: mainnet
seconds_per_slot: 2
num_validator_keys_per_node: 128
prefunded_accounts: '{"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266": {"balance": "10ETH"}, "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc": {"balance": "10ETH"}, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8": {"balance": "10ETH"},"0x976ea74026e726554db657fa54763abd0c3a0aa9": {"balance": "10ETH"}}'
prefunded_accounts: '{
"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266": {"balance": "10ETH"},
"0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc": {"balance": "10ETH"},
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8": {"balance": "10ETH"},
"0x976ea74026e726554db657fa54763abd0c3a0aa9": {"balance": "10ETH"}
}'
# Preloaded with DataHavenTest contract
additional_preloaded_contracts: |
{
"0x1111111111111111111111111111111111111111": {
"balance": "0ETH",
"code": "0x608060405234801561000f575f5ffd5b506004361061004a575f3560e01c80632baeceb71461004e5780638381f58a146100585780638da5cb5b14610073578063d826f88f1461009e575b5f5ffd5b6100566100a6565b005b6100605f5481565b6040519081526020015b60405180910390f35b600154610086906001600160a01b031681565b6040516001600160a01b03909116815260200161006a565b61005661010d565b5f5f54116100fb5760405162461bcd60e51b815260206004820152601f60248201527f4e756d6265722073686f756c642062652067726561746572207468616e20300060448201526064015b60405180910390fd5b60015f54610109919061016d565b5f55565b6001546001600160a01b031633146101675760405162461bcd60e51b815260206004820152601760248201527f4f6e6c792063616c6c61626c65206279206f776e65722100000000000000000060448201526064016100f2565b600a5f55565b8181038181111561018c57634e487b7160e01b5f52601160045260245ffd5b9291505056fea2646970667358221220ac5899491afd834afd223fd632497d1c0c7593961eda22f04c58db4b504999cf64736f6c634300081c0033",
"storage": {
"0x0000000000000000000000000000000000000000000000000000000000000000": "0x000000000000000000000000000000000000000000000000000000000000000a",
"0x0000000000000000000000000000000000000000000000000000000000000001": "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266"
},
"nonce": "1"
}
}

View file

@ -10,9 +10,8 @@
"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()\"",
"start:e2e:verified": "bun cli --verified --blockscout --deploy-contracts",
"start:e2e:minimal": "bun cli",
"start:e2e:ci": "bun cli -d --setup-validators --update-validator-set --fund-validators",
"start:e2e:verified": "bun cli --verified --blockscout --deploy-contracts --setup-validators --update-validator-set --fund-validators --slot-time 1",
"start:e2e:ci": "bun cli -d --setup-validators --update-validator-set --fund-validators --always-clean --slot-time 2",
"start:e2e:minrelayer": "bun cli --relayer -d --no-setup-validators --no-update-validator-set --no-fund-validators --datahaven",
"stop:e2e": "pkill datahaven ; kurtosis enclave stop datahaven-ethereum && kurtosis clean && kurtosis engine stop && docker container prune -f",
"stop:e2e:verified": "bun stop:e2e",
@ -48,6 +47,7 @@
"tiny-invariant": "^1.3.3",
"viem": "^2.28.0",
"wagmi": "^2.15.0",
"yaml": "^2.7.1",
"zod": "^3.24.3"
},
"trustedDependencies": [

View file

@ -34,15 +34,9 @@ The first step involves setting up the testing infrastructure using Kurtosis, a
```bash
# Start the E2E CLI environment with the minimal configuration
bun cli
# Alternative
bun start:e2e:minimal
# Start the E2E CLI environment with Blockscout and verified contracts
bun start:e2e:verified
# Behind the scenes both commands run:
bun run scripts/launch-kurtosis.ts
# And then continue setting up the environment with the next steps.
```
## 2. Ethereum-side Contract Deployment
@ -85,12 +79,14 @@ In this phase, we register validators as operators in EigenLayer and sync the va
### Steps
1. **Fund Validators with Tokens**
- Use `fund-validators.ts` script to fund validators with necessary tokens
- Transfers 5% of creator's tokens to each validator
- Transfers 1% of creator's ETH to validators with zero balance
- Ensures validators have sufficient funds for operations
2. **Register Operators in EigenLayer**
- Use `setup-validators.ts` script to register validators
- Deposits stake and registers for operator sets
- Sets up the validator set in the Ethereum side

View file

@ -1,6 +1,6 @@
import { $ } from "bun";
import invariant from "tiny-invariant";
import { confirmWithTimeout, logger, printHeader } from "utils";
import { confirmWithTimeout, logger, printHeader, runShellCommandWithLogger } from "utils";
interface DeployContractsOptions {
rpcUrl: string;
@ -63,12 +63,7 @@ export const deployContracts = async (options: DeployContractsOptions): Promise<
}
logger.debug(buildStdout.toString());
// Get forge path
const { stdout: forgePath } = await $`which forge`.quiet();
const forgeExecutable = forgePath.toString().trim();
// Prepare deployment command
let deployCommand = `${forgeExecutable} script script/deploy/DeployLocal.s.sol --rpc-url ${rpcUrl} --color never -vv --no-rpc-rate-limit --non-interactive --broadcast`;
let deployCommand = `forge script script/deploy/DeployLocal.s.sol --rpc-url ${rpcUrl} --color never -vv --no-rpc-rate-limit --non-interactive --broadcast`;
if (verified && blockscoutBackendUrl) {
deployCommand += ` --verify --verifier blockscout --verifier-url ${blockscoutBackendUrl}/api/ --delay 0`;
@ -77,14 +72,8 @@ export const deployContracts = async (options: DeployContractsOptions): Promise<
logger.info("⏳ Deploying contracts (this might take a few minutes)...");
const { exitCode: deployExitCode, stderr: deployStderr } = await $`sh -c ${deployCommand}`
.cwd("../contracts")
.nothrow();
if (deployExitCode !== 0) {
logger.error(deployStderr.toString());
throw Error("❌ Contracts have failed to deploy properly.");
}
// Using custom shell command to improve logging with forge's stdoutput
await runShellCommandWithLogger(deployCommand, { cwd: "../contracts" });
logger.success("Contracts deployed successfully");
return true;
@ -121,7 +110,6 @@ if (import.meta.main) {
}
}
// Check required parameters
if (!options.rpcUrl) {
console.error("Error: --rpc-url parameter is required");
process.exit(1);
@ -132,7 +120,6 @@ if (import.meta.main) {
process.exit(1);
}
// Run deployment
deployContracts({
rpcUrl: options.rpcUrl,
verified: options.verified,

View file

@ -1,9 +1,8 @@
import fs from "node:fs";
import path from "node:path";
// Setup of validators for DataHaven
import { $ } from "bun";
import invariant from "tiny-invariant";
import { confirmWithTimeout, logger, printHeader } from "../utils/index";
import { confirmWithTimeout, logger, printHeader, runShellCommandWithLogger } from "../utils/index";
interface SetupValidatorsOptions {
rpcUrl: string;
@ -113,16 +112,11 @@ export const setupValidators = async (options: SetupValidatorsOptions): Promise<
const validators = config.validators;
logger.info(`Found ${validators.length} validators to register`);
// Get forge path
const { stdout: forgePath } = await $`which forge`.quiet();
const forgeExecutable = forgePath.toString().trim();
// 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})`);
// Setting up the environment variables directly
const env = {
...process.env,
NETWORK: networkName,
@ -133,20 +127,10 @@ export const setupValidators = async (options: SetupValidatorsOptions): Promise<
};
// Prepare command to register validator
const signupCommand = `${forgeExecutable} script script/transact/SignUpValidator.s.sol --rpc-url ${rpcUrl} --broadcast --no-rpc-rate-limit --non-interactive`;
const signupCommand = `forge script script/transact/SignUpValidator.s.sol --rpc-url ${rpcUrl} --broadcast --no-rpc-rate-limit --non-interactive`;
logger.debug(`Running command: ${signupCommand}`);
// Run with environment variables directly passed to the environment
const { exitCode, stderr } = await $`sh -c ${signupCommand}`
.cwd("../contracts")
.env(env)
.nothrow();
if (exitCode !== 0) {
logger.error(`Failed to register validator ${validator.publicKey}: ${stderr.toString()}`);
continue;
}
await runShellCommandWithLogger(signupCommand, { env, cwd: "../contracts" });
logger.success(`Successfully registered validator ${validator.publicKey}`);
}

View file

@ -1,10 +1,11 @@
export * from "./blockscout";
export * from "./constants";
export * from "./contracts";
export * from "./docker";
export * from "./input";
export * from "./logger";
export * from "./rpc";
export * from "./viem";
export * from "./kurtosis";
export * from "./logger";
export * from "./parser";
export * from "./contracts";
export * from "./rpc";
export * from "./shell";
export * from "./viem";

View file

@ -40,7 +40,7 @@ export const timeoutConfirm = createPrompt<boolean, TimeoutConfirmConfig>((cfg,
clearInterval(id);
done(cfg.default ?? true);
}
}, 200);
}, 500);
return () => clearInterval(id);
}, []);
@ -64,7 +64,7 @@ export const timeoutConfirm = createPrompt<boolean, TimeoutConfirmConfig>((cfg,
const main = `${prefix} ${theme.style.message(cfg.message, status)} \
${defaultBadge} ${input}`;
const border = chalk.yellow("=".repeat(cfg.message.length + 40));
const border = chalk.yellow("=".repeat(80));
const hint = theme.style.help(
chalk.magenta(
`⏱ Will default to ${chalk.bold(cfg.default ? "YES" : "NO")} in ${chalk.bold((left / 1000).toFixed(0))}s`
@ -77,12 +77,14 @@ ${main}
${border}`;
});
export const confirmWithTimeout = (
export const confirmWithTimeout = async (
question: string,
defaultValue: boolean,
timeoutSeconds: number
) =>
timeoutConfirm({
) => {
await Bun.sleep(50); //debounce
return timeoutConfirm({
message: question,
default: defaultValue,
timeoutMs: timeoutSeconds * 1000,
@ -93,3 +95,4 @@ export const confirmWithTimeout = (
}
}
});
};

57
test/utils/shell.ts Normal file
View file

@ -0,0 +1,57 @@
import { existsSync } from "node:fs";
import { spawn } from "bun";
import { logger } from "./logger";
export const runShellCommandWithLogger = async (
command: string,
options?: { cwd?: string; env?: object }
) => {
const { cwd = ".", env = {} } = options || {};
try {
if (!existsSync(cwd)) {
logger.error("❌ CWD does not exist:", cwd);
throw new Error("❌ CWD does not exist");
}
const proc = spawn(["sh", "-c", command], {
cwd,
stdout: "pipe",
stderr: "pipe",
env: {
...process.env,
...env
}
});
const stdoutReader = proc.stdout.getReader();
const stderrReader = proc.stderr.getReader();
const readStream = async (
reader: typeof stdoutReader | typeof stderrReader,
streamName: string
) => {
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = new TextDecoder().decode(value);
const trimmedText = text.trim();
logger.info(trimmedText.includes("\n") ? `\n${trimmedText}` : trimmedText);
}
} catch (err) {
logger.error(`Error reading from ${streamName} stream:`, err);
} finally {
reader.releaseLock();
}
};
Promise.all([readStream(stdoutReader, "stdout"), readStream(stderrReader, "stderr")]);
await proc.exited;
} catch (err) {
logger.error("❌ Error running shell command:", command, "in", cwd);
logger.error(err);
throw err;
}
};