mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 09:50:01 +00:00
test: ⚙️ Parse & Generate Relayer Configs (#54)
## Human Written Description
This PR adds the following to the E2E CLI:
- Relayer config generation for: `beacon-relay` `beefy-relay`
- The other two relayer types to be added later
- Relayers don't actually work yet
- By default turned off, this requires a binary to be present in:
`<repo_root>/operator/target/release` dir
- Datahaven network launching
- DH network is using default `local` network chain spec
- Launched with 5 nodes since our authority set is 6 large (and you need
2/3 + 1 of set size
> [!NOTE]
> Both the relayer and the DH node binaries are being run as local
processes TEMPORARILY. This means that logging is done in a very
rudimentary way (we pipe to a file whilst the CLI is running).
>
> This means that when the CLI finishes **the log files will no longer
be written to**.
> This is temporary since spawning binaries is a stop gap solution until
docker images available.
---
> [!IMPORTANT]
> The following is AI generated slop describing this PR's changes:
**Key Changes:**
* **CLI Enhancements (`test/cli/index.ts`):**
* Added options `--datahaven` and `--datahaven-bin-path` to enable
launching local DataHaven nodes.
* Added options `--relayer` and `--relayer-bin-path` to enable launching
Snowbridge relayers (Beefy and Beacon).
* Added negation flags (`--no-fund-validators`, `--no-setup-validators`,
`--no-update-validator-set`) for more granular control over validator
setup steps.
* Added `--skip-cleaning` option to preserve Kurtosis state between
runs.
* Added a pre-action hook (`launchPreActionHook`) to validate flag
combinations (e.g., `--verified` requires `--blockscout`).
* **New CLI Handlers (`test/cli/handlers/launch/`):**
* `datahaven.ts`: Logic for spawning DataHaven node processes using the
specified binary. Manages ports and process cleanup.
* `relayer.ts`: Logic for configuring and spawning Snowbridge relayer
processes (Beefy and Beacon). Reads contract deployment addresses,
updates relayer config templates, and uses specified private keys.
Manages log files and process cleanup.
* `summary.ts`: Generates and displays the table of running services
(including dynamically launched DataHaven nodes) and their endpoints.
* `validator.ts`: Extracted validator funding, setup, and set update
logic into its own handler.
* `index.ts`: Orchestrates the launch sequence based on CLI options,
calling the appropriate handlers. Includes a `LaunchedNetwork` class to
track spawned processes, file descriptors, and node ports for cleanup.
* **Updated `package.json` Scripts:**
* Added `start:e2e:minrelayer` script for a minimal setup including
relayers and DataHaven nodes.
* Modified `stop:e2e` to include `pkill datahaven` for proper cleanup.
* Added `stop:e2e:quick` to only stop the Kurtosis enclave without full
cleaning.
* **Updated `launch-kurtosis.ts`:** Modified to use new Kurtosis utility
functions and added a `skipCleaning` option.
* **New Utility Functions:**
* `test/utils/kurtosis.ts`: Functions to inspect Kurtosis services
(`getServiceFromKurtosis`, `getPortFromKurtosis`,
`getServicesFromKurtosis`).
* `test/utils/parser.ts`: Zod schemas and parsing functions for
Snowbridge relayer configurations.
* **Constants & Minor Updates:**
* Added `SUBSTRATE_FUNDED_ACCOUNTS` to `test/utils/constants.ts`.
* Updated `tsconfig.json` include paths.
* Refactored `test/utils/docker.ts` (though now largely superseded by
Kurtosis utils).
* Updated logging in `test/scripts/send-txn.ts`.
**Reasoning:**
This PR significantly expands the E2E testing capabilities by allowing
developers to easily launch and integrate local DataHaven nodes and
Snowbridge relayers into the test network, facilitating more
comprehensive integration testing. The CLI refactoring makes managing
these complex setups more robust and user-friendly.
This commit is contained in:
parent
f4a959f342
commit
95171a5e10
27 changed files with 1321 additions and 426 deletions
49
.github/workflows/ts-lint.yml
vendored
Normal file
49
.github/workflows/ts-lint.yml
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
name: TS Lint & Format
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
name: Check TypeScript Types
|
||||
defaults:
|
||||
run:
|
||||
working-directory: test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
- name: Check Types
|
||||
run: bun typecheck
|
||||
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
name: Check Formatting
|
||||
defaults:
|
||||
run:
|
||||
working-directory: test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
- name: Check Formatting
|
||||
run: bun fmt
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.8.2/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"files": {
|
||||
"include": ["*.js", "*.ts", "*.json", "*.yml", "*.md"],
|
||||
"ignore": ["./node_modules/*", "./target/*", "**/tmp/*", "*.spec.json"]
|
||||
|
|
|
|||
10
test/TODO.md
10
test/TODO.md
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
## E2E Script
|
||||
|
||||
- [ ] Refactor CLI to use commander (instead of manual arg passing)
|
||||
- [ ] Spin up a local DH network using command line args
|
||||
- [ ] Scrape the correct ports from kurtosis cli instead of docker
|
||||
- [x] Refactor CLI to use commander (instead of manual arg passing)
|
||||
- [x] Spin up a local DH network using command line args
|
||||
- [x] Scrape the correct ports from kurtosis cli instead of docker
|
||||
- [ ] Docker image generation for DH node
|
||||
|
||||
### Relayer
|
||||
|
||||
- [ ] Generate config files based on type
|
||||
- [x] Generate config files based on type
|
||||
- [ ] Pull relay docker image
|
||||
- [ ] Launch relayer with correct private key and endpoints
|
||||
- [x] Launch relayer with correct private key and endpoints
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.8.2/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"extends": ["../biome.json"]
|
||||
}
|
||||
|
|
|
|||
114
test/bun.lock
114
test/bun.lock
|
|
@ -7,6 +7,7 @@
|
|||
"@biomejs/biome": "^1.9.4",
|
||||
"@commander-js/extra-typings": "^13.1.0",
|
||||
"@dotenvx/dotenvx": "^1.41.0",
|
||||
"@inquirer/prompts": "^7.5.0",
|
||||
"@types/dockerode": "^3.3.38",
|
||||
"@types/node": "^22.14.1",
|
||||
"chalk": "^5.4.1",
|
||||
|
|
@ -14,6 +15,7 @@
|
|||
"dockerode": "^4.0.6",
|
||||
"dotenv": "^16.5.0",
|
||||
"octokit": "^4.1.3",
|
||||
"ora": "^8.2.0",
|
||||
"pino": "^9.6.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
|
|
@ -65,6 +67,34 @@
|
|||
|
||||
"@grpc/proto-loader": ["@grpc/proto-loader@0.7.15", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ=="],
|
||||
|
||||
"@inquirer/checkbox": ["@inquirer/checkbox@4.1.5", "", { "dependencies": { "@inquirer/core": "^10.1.10", "@inquirer/figures": "^1.0.11", "@inquirer/type": "^3.0.6", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-swPczVU+at65xa5uPfNP9u3qx/alNwiaykiI/ExpsmMSQW55trmZcwhYWzw/7fj+n6Q8z1eENvR7vFfq9oPSAQ=="],
|
||||
|
||||
"@inquirer/confirm": ["@inquirer/confirm@5.1.9", "", { "dependencies": { "@inquirer/core": "^10.1.10", "@inquirer/type": "^3.0.6" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w=="],
|
||||
|
||||
"@inquirer/core": ["@inquirer/core@10.1.10", "", { "dependencies": { "@inquirer/figures": "^1.0.11", "@inquirer/type": "^3.0.6", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw=="],
|
||||
|
||||
"@inquirer/editor": ["@inquirer/editor@4.2.10", "", { "dependencies": { "@inquirer/core": "^10.1.10", "@inquirer/type": "^3.0.6", "external-editor": "^3.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-5GVWJ+qeI6BzR6TIInLP9SXhWCEcvgFQYmcRG6d6RIlhFjM5TyG18paTGBgRYyEouvCmzeco47x9zX9tQEofkw=="],
|
||||
|
||||
"@inquirer/expand": ["@inquirer/expand@4.0.12", "", { "dependencies": { "@inquirer/core": "^10.1.10", "@inquirer/type": "^3.0.6", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-jV8QoZE1fC0vPe6TnsOfig+qwu7Iza1pkXoUJ3SroRagrt2hxiL+RbM432YAihNR7m7XnU0HWl/WQ35RIGmXHw=="],
|
||||
|
||||
"@inquirer/figures": ["@inquirer/figures@1.0.11", "", {}, "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw=="],
|
||||
|
||||
"@inquirer/input": ["@inquirer/input@4.1.9", "", { "dependencies": { "@inquirer/core": "^10.1.10", "@inquirer/type": "^3.0.6" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-mshNG24Ij5KqsQtOZMgj5TwEjIf+F2HOESk6bjMwGWgcH5UBe8UoljwzNFHqdMbGYbgAf6v2wU/X9CAdKJzgOA=="],
|
||||
|
||||
"@inquirer/number": ["@inquirer/number@3.0.12", "", { "dependencies": { "@inquirer/core": "^10.1.10", "@inquirer/type": "^3.0.6" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-7HRFHxbPCA4e4jMxTQglHJwP+v/kpFsCf2szzfBHy98Wlc3L08HL76UDiA87TOdX5fwj2HMOLWqRWv9Pnn+Z5Q=="],
|
||||
|
||||
"@inquirer/password": ["@inquirer/password@4.0.12", "", { "dependencies": { "@inquirer/core": "^10.1.10", "@inquirer/type": "^3.0.6", "ansi-escapes": "^4.3.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-FlOB0zvuELPEbnBYiPaOdJIaDzb2PmJ7ghi/SVwIHDDSQ2K4opGBkF+5kXOg6ucrtSUQdLhVVY5tycH0j0l+0g=="],
|
||||
|
||||
"@inquirer/prompts": ["@inquirer/prompts@7.5.0", "", { "dependencies": { "@inquirer/checkbox": "^4.1.5", "@inquirer/confirm": "^5.1.9", "@inquirer/editor": "^4.2.10", "@inquirer/expand": "^4.0.12", "@inquirer/input": "^4.1.9", "@inquirer/number": "^3.0.12", "@inquirer/password": "^4.0.12", "@inquirer/rawlist": "^4.1.0", "@inquirer/search": "^3.0.12", "@inquirer/select": "^4.2.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-tk8Bx7l5AX/CR0sVfGj3Xg6v7cYlFBkEahH+EgBB+cZib6Fc83dwerTbzj7f2+qKckjIUGsviWRI1d7lx6nqQA=="],
|
||||
|
||||
"@inquirer/rawlist": ["@inquirer/rawlist@4.1.0", "", { "dependencies": { "@inquirer/core": "^10.1.10", "@inquirer/type": "^3.0.6", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-6ob45Oh9pXmfprKqUiEeMz/tjtVTFQTgDDz1xAMKMrIvyrYjAmRbQZjMJfsictlL4phgjLhdLu27IkHNnNjB7g=="],
|
||||
|
||||
"@inquirer/search": ["@inquirer/search@3.0.12", "", { "dependencies": { "@inquirer/core": "^10.1.10", "@inquirer/figures": "^1.0.11", "@inquirer/type": "^3.0.6", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-H/kDJA3kNlnNIjB8YsaXoQI0Qccgf0Na14K1h8ExWhNmUg2E941dyFPrZeugihEa9AZNW5NdsD/NcvUME83OPQ=="],
|
||||
|
||||
"@inquirer/select": ["@inquirer/select@4.2.0", "", { "dependencies": { "@inquirer/core": "^10.1.10", "@inquirer/figures": "^1.0.11", "@inquirer/type": "^3.0.6", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KkXQ4aSySWimpV4V/TUJWdB3tdfENZUU765GjOIZ0uPwdbGIG6jrxD4dDf1w68uP+DVtfNhr1A92B+0mbTZ8FA=="],
|
||||
|
||||
"@inquirer/type": ["@inquirer/type@3.0.6", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA=="],
|
||||
|
||||
"@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
|
||||
|
||||
"@noble/ciphers": ["@noble/ciphers@1.2.1", "", {}, "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA=="],
|
||||
|
|
@ -163,7 +193,9 @@
|
|||
|
||||
"abitype": ["abitype@1.0.8", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3 >=3.22.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
"ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
|
|
@ -189,8 +221,16 @@
|
|||
|
||||
"chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
|
||||
|
||||
"chardet": ["chardet@0.7.0", "", {}, "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="],
|
||||
|
||||
"chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
|
||||
|
||||
"cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
|
||||
|
||||
"cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
|
||||
|
||||
"cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
|
||||
|
||||
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
|
@ -217,7 +257,7 @@
|
|||
|
||||
"eciesjs": ["eciesjs@0.4.14", "", { "dependencies": { "@ecies/ciphers": "^0.2.2", "@noble/ciphers": "^1.0.0", "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0" } }, "sha512-eJAgf9pdv214Hn98FlUzclRMYWF7WfoLlkS9nWMTm1qcCwn6Ad4EGD9lr9HXMBfSrZhYQujRE+p0adPRkctC6A=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
"emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
|
||||
|
||||
"end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="],
|
||||
|
||||
|
|
@ -227,6 +267,8 @@
|
|||
|
||||
"execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
|
||||
|
||||
"external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="],
|
||||
|
||||
"fast-content-type-parse": ["fast-content-type-parse@2.0.1", "", {}, "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q=="],
|
||||
|
||||
"fast-copy": ["fast-copy@3.0.2", "", {}, "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="],
|
||||
|
|
@ -241,12 +283,16 @@
|
|||
|
||||
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||
|
||||
"get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="],
|
||||
|
||||
"get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
|
||||
|
||||
"help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="],
|
||||
|
||||
"human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
|
@ -255,8 +301,12 @@
|
|||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
|
||||
|
||||
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
|
||||
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
|
||||
|
||||
"isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
|
||||
|
||||
"isows": ["isows@1.0.6", "", { "peerDependencies": { "ws": "*" } }, "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw=="],
|
||||
|
|
@ -265,18 +315,24 @@
|
|||
|
||||
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
|
||||
|
||||
"log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="],
|
||||
|
||||
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||
|
||||
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
|
||||
|
||||
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
|
||||
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
"mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="],
|
||||
|
||||
"nan": ["nan@2.22.2", "", {}, "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ=="],
|
||||
|
||||
"npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
|
||||
|
|
@ -291,6 +347,10 @@
|
|||
|
||||
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
||||
|
||||
"ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="],
|
||||
|
||||
"os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="],
|
||||
|
||||
"ox": ["ox@0.6.9", "", { "dependencies": { "@adraffy/ens-normalize": "^1.10.1", "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", "@scure/bip32": "^1.5.0", "@scure/bip39": "^1.4.0", "abitype": "^1.0.6", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
|
@ -319,6 +379,8 @@
|
|||
|
||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||
|
||||
"restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
|
||||
|
|
@ -341,11 +403,13 @@
|
|||
|
||||
"ssh2": ["ssh2@1.16.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.20.0" } }, "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg=="],
|
||||
|
||||
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
"stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="],
|
||||
|
||||
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
"strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||
|
||||
"strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
|
||||
|
||||
|
|
@ -359,10 +423,14 @@
|
|||
|
||||
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||
|
||||
"tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="],
|
||||
|
||||
"toad-cache": ["toad-cache@3.7.0", "", {}, "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw=="],
|
||||
|
||||
"tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],
|
||||
|
||||
"type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
|
@ -379,7 +447,7 @@
|
|||
|
||||
"which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
"wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
|
|
@ -391,16 +459,52 @@
|
|||
|
||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"yoctocolors-cjs": ["yoctocolors-cjs@2.1.2", "", {}, "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="],
|
||||
|
||||
"zod": ["zod@3.24.3", "", {}, "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg=="],
|
||||
|
||||
"@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||
|
||||
"@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"@types/ssh2/@types/node": ["@types/node@18.19.86", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-fifKayi175wLyKyc5qUfyENhQ1dCNI1UNjp653d8kuYcPQN5JhX3dGuP/XmvPTg/xRBn1VTLpbmi+H/Mr7tLfQ=="],
|
||||
|
||||
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="],
|
||||
|
||||
"restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||
|
||||
"restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
|
||||
"cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,290 +0,0 @@
|
|||
import type { Command } from "@commander-js/extra-typings";
|
||||
import { $ } from "bun";
|
||||
import { deployContracts } from "scripts/deploy-contracts";
|
||||
import { fundValidators } from "scripts/fund-validators";
|
||||
import { generateSnowbridgeConfigs } from "scripts/gen-snowbridge-cfgs";
|
||||
import { launchKurtosis } from "scripts/launch-kurtosis";
|
||||
import sendTxn from "scripts/send-txn";
|
||||
import { setupValidators } from "scripts/setup-validators";
|
||||
import { updateValidatorSet } from "scripts/update-validator-set";
|
||||
import invariant from "tiny-invariant";
|
||||
import { ANVIL_FUNDED_ACCOUNTS, logger, printDivider, printHeader, promptWithTimeout } from "utils";
|
||||
|
||||
interface LaunchOptions {
|
||||
verified?: boolean;
|
||||
launchKurtosis?: boolean;
|
||||
deployContracts?: boolean;
|
||||
fundValidators?: boolean;
|
||||
setupValidators?: boolean;
|
||||
updateValidatorSet?: boolean;
|
||||
blockscout?: boolean;
|
||||
relayer?: boolean;
|
||||
}
|
||||
|
||||
// ===== Launch Handler Functions =====
|
||||
|
||||
export const launch = async (options: LaunchOptions) => {
|
||||
logger.debug("Running with options:");
|
||||
logger.debug(options);
|
||||
|
||||
const timeStart = performance.now();
|
||||
|
||||
printHeader("Environment Checks");
|
||||
|
||||
await checkDependencies();
|
||||
|
||||
logger.trace("Launching Kurtosis enclave");
|
||||
const { services } = await launchKurtosis({
|
||||
launchKurtosis: options.launchKurtosis,
|
||||
blockscout: options.blockscout
|
||||
});
|
||||
logger.trace("Kurtosis enclave launched");
|
||||
|
||||
logger.trace("Send test transaction");
|
||||
printHeader("Setting Up Blockchain");
|
||||
logger.debug(`Using account ${ANVIL_FUNDED_ACCOUNTS[1].publicKey}`);
|
||||
const privateKey = ANVIL_FUNDED_ACCOUNTS[1].privateKey;
|
||||
const networkRpcUrl = services.find((s) => s.service === "reth-1-rpc")?.url;
|
||||
invariant(networkRpcUrl, "❌ Network RPC URL not found");
|
||||
|
||||
logger.info("💸 Sending test transaction...");
|
||||
await sendTxn(privateKey, networkRpcUrl);
|
||||
|
||||
printDivider();
|
||||
|
||||
logger.trace("Display service information in a clean table");
|
||||
printHeader("Service Endpoints");
|
||||
|
||||
logger.trace("Filter services to display based on blockscout option");
|
||||
const servicesToDisplay = services
|
||||
.filter((s) => ["reth-1-rpc", "reth-2-rpc", "dora"].includes(s.service))
|
||||
.concat([{ service: "kurtosis-web", port: "9711", url: "http://127.0.0.1:9711" }]);
|
||||
|
||||
logger.trace("Conditionally add blockscout services");
|
||||
if (options.blockscout !== false) {
|
||||
const blockscoutBackend = services.find((s) => s.service === "blockscout-backend");
|
||||
if (blockscoutBackend) {
|
||||
servicesToDisplay.push(blockscoutBackend);
|
||||
logger.trace("Adding blockscout frontend");
|
||||
servicesToDisplay.push({ service: "blockscout", port: "3000", url: "http://127.0.0.1:3000" });
|
||||
}
|
||||
}
|
||||
|
||||
console.table(servicesToDisplay);
|
||||
|
||||
printDivider();
|
||||
|
||||
logger.trace("Show completion information");
|
||||
const timeEnd = performance.now();
|
||||
const minutes = ((timeEnd - timeStart) / (1000 * 60)).toFixed(1);
|
||||
|
||||
logger.success(`Kurtosis network started successfully in ${minutes} minutes`);
|
||||
|
||||
printDivider();
|
||||
|
||||
logger.trace("Deploy contracts using the extracted function");
|
||||
let blockscoutBackendUrl: string | undefined = undefined;
|
||||
|
||||
if (options.blockscout !== false) {
|
||||
blockscoutBackendUrl = services.find((s) => s.service === "blockscout-backend")?.url;
|
||||
} else if (options.verified) {
|
||||
logger.warn(
|
||||
"⚠️ Contract verification (--verified) requested, but Blockscout is disabled (--no-blockscout). Verification will be skipped."
|
||||
);
|
||||
}
|
||||
|
||||
const contractsDeployed = await deployContracts({
|
||||
rpcUrl: networkRpcUrl,
|
||||
verified: options.verified,
|
||||
blockscoutBackendUrl,
|
||||
deployContracts: options.deployContracts
|
||||
});
|
||||
|
||||
logger.trace("Set up validators using the extracted function");
|
||||
if (contractsDeployed) {
|
||||
let shouldFundValidators = options.fundValidators;
|
||||
let shouldSetupValidators = options.setupValidators;
|
||||
let shouldUpdateValidatorSet = options.updateValidatorSet;
|
||||
|
||||
logger.trace("If not specified, prompt for funding");
|
||||
if (shouldFundValidators === undefined) {
|
||||
shouldFundValidators = await promptWithTimeout(
|
||||
"Do you want to fund validators with tokens and ETH?",
|
||||
true,
|
||||
10
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Using flag option: ${shouldFundValidators ? "will fund" : "will not fund"} validators`
|
||||
);
|
||||
}
|
||||
|
||||
logger.trace("If not specified, prompt for setup");
|
||||
if (shouldSetupValidators === undefined) {
|
||||
shouldSetupValidators = await promptWithTimeout(
|
||||
"Do you want to register validators in EigenLayer?",
|
||||
true,
|
||||
10
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Using flag option: ${shouldSetupValidators ? "will register" : "will not register"} validators`
|
||||
);
|
||||
}
|
||||
|
||||
logger.trace("If not specified, prompt for update");
|
||||
if (shouldUpdateValidatorSet === undefined) {
|
||||
shouldUpdateValidatorSet = await promptWithTimeout(
|
||||
"Do you want to update the validator set on the substrate chain?",
|
||||
true,
|
||||
10
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Using flag option: ${shouldUpdateValidatorSet ? "will update" : "will not update"} validator set`
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldFundValidators) {
|
||||
await fundValidators({
|
||||
rpcUrl: networkRpcUrl
|
||||
});
|
||||
} else {
|
||||
logger.info("Skipping validator funding");
|
||||
}
|
||||
|
||||
if (shouldSetupValidators) {
|
||||
await setupValidators({
|
||||
rpcUrl: networkRpcUrl
|
||||
});
|
||||
|
||||
if (shouldUpdateValidatorSet) {
|
||||
await updateValidatorSet({
|
||||
rpcUrl: networkRpcUrl
|
||||
});
|
||||
} else {
|
||||
logger.info("Skipping validator set update");
|
||||
}
|
||||
} else {
|
||||
logger.info("Skipping validator setup");
|
||||
}
|
||||
} else if (options.setupValidators || options.fundValidators) {
|
||||
logger.warn(
|
||||
"⚠️ Validator operations requested but contracts were not deployed. Skipping validator operations."
|
||||
);
|
||||
}
|
||||
|
||||
if (options.relayer) {
|
||||
printHeader("Starting Snowbridge Relayers");
|
||||
|
||||
// TODO - Replace this with our forked iamge when ready
|
||||
const dockerImage = "ronyang/snowbridge-relay";
|
||||
logger.info(`Pulling docker image ${dockerImage}`);
|
||||
|
||||
const { stdout, stderr, exitCode } =
|
||||
await $`sh -c docker pull --platform=linux/amd64 ${dockerImage}`.quiet().nothrow();
|
||||
|
||||
if (exitCode !== 0) {
|
||||
logger.error(`Failed to pull docker image ${dockerImage}: ${stderr.toString()}`);
|
||||
throw Error("❌ Failed to pull docker image");
|
||||
}
|
||||
logger.debug(stdout.toString());
|
||||
|
||||
const {
|
||||
stdout: stdout2,
|
||||
stderr: stderr2,
|
||||
exitCode: exitCode2
|
||||
} = await $`sh -c docker run --platform=linux/amd64 ${dockerImage}`.quiet().nothrow();
|
||||
|
||||
if (exitCode2 !== 0) {
|
||||
logger.error(`Failed to run docker image ${dockerImage}: ${stderr2.toString()}`);
|
||||
throw Error("❌ Failed to run docker image");
|
||||
}
|
||||
logger.debug(stdout2.toString());
|
||||
|
||||
logger.info("Preparing to generate configs");
|
||||
await generateSnowbridgeConfigs();
|
||||
logger.success("Snowbridge configs generated");
|
||||
|
||||
// TODO - Start Relayers here
|
||||
// For each relayer in array spawn in background relayer with appropriate private key, command and config param
|
||||
const relayersToStart = [
|
||||
{
|
||||
name: "relayer-1",
|
||||
type: "beefy",
|
||||
config: "beefy-relay.json"
|
||||
}
|
||||
];
|
||||
|
||||
logger.trace("Starting Snowbridge relayers");
|
||||
for (const relayer of relayersToStart) {
|
||||
await $`sh -c docker run --platform=linux/amd64 ${dockerImage}`.quiet().nothrow();
|
||||
}
|
||||
logger.success("Snowbridge relayers started");
|
||||
}
|
||||
|
||||
logger.success("Launch script completed successfully");
|
||||
};
|
||||
|
||||
export const launchPreActionHook = (
|
||||
thisCmd: Command<[], LaunchOptions & { [key: string]: any }>
|
||||
) => {
|
||||
const { blockscout, verified } = thisCmd.opts();
|
||||
if (verified && !blockscout) {
|
||||
thisCmd.error("--verified requires --blockscout to be set");
|
||||
}
|
||||
};
|
||||
|
||||
// ===== Checks =====
|
||||
const checkDependencies = async (): Promise<void> => {
|
||||
if (!(await checkKurtosisInstalled())) {
|
||||
logger.error("Kurtosis CLI is required to be installed: https://docs.kurtosis.com/install");
|
||||
throw Error("❌ Kurtosis CLI application not found.");
|
||||
}
|
||||
|
||||
logger.success("Kurtosis CLI found");
|
||||
|
||||
if (!(await checkDockerRunning())) {
|
||||
logger.error("Is Docker Running? Unable to make connection to docker daemon");
|
||||
throw Error("❌ Error connecting to Docker");
|
||||
}
|
||||
|
||||
logger.success("Docker is running");
|
||||
|
||||
if (!(await checkForgeInstalled())) {
|
||||
logger.error("Is foundry installed? https://book.getfoundry.sh/getting-started/installation");
|
||||
throw Error("❌ forge binary not found in PATH");
|
||||
}
|
||||
|
||||
logger.success("Forge is installed");
|
||||
};
|
||||
|
||||
const checkKurtosisInstalled = async (): Promise<boolean> => {
|
||||
const { exitCode, stderr, stdout } = await $`kurtosis version`.nothrow().quiet();
|
||||
if (exitCode !== 0) {
|
||||
logger.error(stderr.toString());
|
||||
return false;
|
||||
}
|
||||
logger.debug(stdout.toString());
|
||||
return true;
|
||||
};
|
||||
|
||||
const checkDockerRunning = async (): Promise<boolean> => {
|
||||
const { exitCode, stderr, stdout } = await $`docker system info`.nothrow().quiet();
|
||||
if (exitCode !== 0) {
|
||||
logger.error(stderr.toString());
|
||||
return false;
|
||||
}
|
||||
logger.debug(stdout.toString());
|
||||
return true;
|
||||
};
|
||||
|
||||
const checkForgeInstalled = async (): Promise<boolean> => {
|
||||
const { exitCode, stderr, stdout } = await $`forge --version`.nothrow().quiet();
|
||||
if (exitCode !== 0) {
|
||||
logger.error(stderr.toString());
|
||||
return false;
|
||||
}
|
||||
logger.debug(stdout.toString());
|
||||
return true;
|
||||
};
|
||||
56
test/cli/handlers/launch/checks.ts
Normal file
56
test/cli/handlers/launch/checks.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { $ } from "bun";
|
||||
import { logger } from "utils";
|
||||
|
||||
// ===== Checks =====
|
||||
export const checkDependencies = async (): Promise<void> => {
|
||||
if (!(await checkKurtosisInstalled())) {
|
||||
logger.error("Kurtosis CLI is required to be installed: https://docs.kurtosis.com/install");
|
||||
throw Error("❌ Kurtosis CLI application not found.");
|
||||
}
|
||||
|
||||
logger.success("Kurtosis CLI found");
|
||||
|
||||
if (!(await checkDockerRunning())) {
|
||||
logger.error("Is Docker Running? Unable to make connection to docker daemon");
|
||||
throw Error("❌ Error connecting to Docker");
|
||||
}
|
||||
|
||||
logger.success("Docker is running");
|
||||
|
||||
if (!(await checkForgeInstalled())) {
|
||||
logger.error("Is foundry installed? https://book.getfoundry.sh/getting-started/installation");
|
||||
throw Error("❌ forge binary not found in PATH");
|
||||
}
|
||||
|
||||
logger.success("Forge is installed");
|
||||
};
|
||||
|
||||
const checkKurtosisInstalled = async (): Promise<boolean> => {
|
||||
const { exitCode, stderr, stdout } = await $`kurtosis version`.nothrow().quiet();
|
||||
if (exitCode !== 0) {
|
||||
logger.error(stderr.toString());
|
||||
return false;
|
||||
}
|
||||
logger.debug(stdout.toString());
|
||||
return true;
|
||||
};
|
||||
|
||||
const checkDockerRunning = async (): Promise<boolean> => {
|
||||
const { exitCode, stderr, stdout } = await $`docker system info`.nothrow().quiet();
|
||||
if (exitCode !== 0) {
|
||||
logger.error(stderr.toString());
|
||||
return false;
|
||||
}
|
||||
logger.debug(stdout.toString());
|
||||
return true;
|
||||
};
|
||||
|
||||
const checkForgeInstalled = async (): Promise<boolean> => {
|
||||
const { exitCode, stderr, stdout } = await $`forge --version`.nothrow().quiet();
|
||||
if (exitCode !== 0) {
|
||||
logger.error(stderr.toString());
|
||||
return false;
|
||||
}
|
||||
logger.debug(stdout.toString());
|
||||
return true;
|
||||
};
|
||||
116
test/cli/handlers/launch/datahaven.ts
Normal file
116
test/cli/handlers/launch/datahaven.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { $ } from "bun";
|
||||
import invariant from "tiny-invariant";
|
||||
import { logger, printHeader } from "utils";
|
||||
import type { LaunchOptions } from ".";
|
||||
import type { LaunchedNetwork } from "./launchedNetwork";
|
||||
|
||||
const COMMON_LAUNCH_ARGS = [
|
||||
"--unsafe-force-node-key-generation",
|
||||
"--tmp",
|
||||
"--port=0",
|
||||
"--validator",
|
||||
"--no-prometheus",
|
||||
"--force-authoring",
|
||||
"--no-telemetry"
|
||||
];
|
||||
|
||||
// We need 5 since the (2/3 + 1) of 6 authority set is 5
|
||||
// <repo_root>/operator/runtime/src/genesis_config_presets.rs#L94
|
||||
const AUTHORITY_IDS = ["alice", "bob", "charlie", "dave", "eve"];
|
||||
|
||||
// TODO: This is very rough and will need something more substantial when we know what we want!
|
||||
export const performDatahavenOperations = async (
|
||||
options: LaunchOptions,
|
||||
launchedNetwork: LaunchedNetwork
|
||||
) => {
|
||||
printHeader("Starting Datahaven Network");
|
||||
|
||||
invariant(options.datahavenBinPath, "❌ Datahaven binary path not defined");
|
||||
invariant(
|
||||
await Bun.file(options.datahavenBinPath).exists(),
|
||||
"❌ Datahaven binary does not exist"
|
||||
);
|
||||
|
||||
const logsPath = `tmp/logs/${launchedNetwork.getRunId()}/`;
|
||||
logger.debug(`Ensuring logs directory exists: ${logsPath}`);
|
||||
await $`mkdir -p ${logsPath}`.quiet();
|
||||
|
||||
for (const id of AUTHORITY_IDS) {
|
||||
logger.info(`Starting ${id}...`);
|
||||
|
||||
const command: string[] = [options.datahavenBinPath, ...COMMON_LAUNCH_ARGS, `--${id}`];
|
||||
|
||||
const logFileName = `datahaven-${id}.log`;
|
||||
const logFilePath = path.join(logsPath, logFileName);
|
||||
logger.debug(`Writing logs to ${logFilePath}`);
|
||||
|
||||
const fd = fs.openSync(logFilePath, "a");
|
||||
launchedNetwork.addFileDescriptor(fd);
|
||||
|
||||
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 < 10; 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}`);
|
||||
}
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
logger.info("Waiting for datahaven to start...");
|
||||
|
||||
if (await isNetworkReady(9944)) {
|
||||
logger.success("Datahaven network started");
|
||||
return;
|
||||
}
|
||||
logger.debug("Node not ready, waiting 1 second...");
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
throw new Error("Datahaven network failed to start after 10 seconds");
|
||||
};
|
||||
|
||||
export const isNetworkReady = async (port: number): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`http://localhost:${port}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
method: "system_chain",
|
||||
params: []
|
||||
})
|
||||
});
|
||||
logger.debug(`isNodeReady check response: ${response.status}`);
|
||||
logger.trace(await response.json());
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
logger.debug(`isNodeReady check failed for port ${port}: ${error}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
152
test/cli/handlers/launch/index.ts
Normal file
152
test/cli/handlers/launch/index.ts
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
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 {
|
||||
ANVIL_FUNDED_ACCOUNTS,
|
||||
getPortFromKurtosis,
|
||||
logger,
|
||||
printDivider,
|
||||
printHeader
|
||||
} from "utils";
|
||||
import { checkDependencies } from "./checks";
|
||||
import { performDatahavenOperations } from "./datahaven";
|
||||
import { LaunchedNetwork } from "./launchedNetwork";
|
||||
import { performRelayerOperations } from "./relayer";
|
||||
import { performSummaryOperations } from "./summary";
|
||||
import { performValidatorOperations } from "./validator";
|
||||
|
||||
export interface LaunchOptions {
|
||||
verified?: boolean;
|
||||
launchKurtosis?: boolean;
|
||||
deployContracts?: boolean;
|
||||
fundValidators?: boolean;
|
||||
setupValidators?: boolean;
|
||||
updateValidatorSet?: boolean;
|
||||
blockscout?: boolean;
|
||||
relayer?: boolean;
|
||||
relayerBinPath?: string;
|
||||
skipCleaning?: boolean;
|
||||
datahavenBinPath?: string;
|
||||
datahaven?: boolean;
|
||||
}
|
||||
|
||||
export const BASE_SERVICES = [
|
||||
"cl-1-lighthouse-reth",
|
||||
"cl-1-lighthouse-reth",
|
||||
"el-1-reth-lighthouse",
|
||||
"el-2-reth-lighthouse",
|
||||
"dora"
|
||||
];
|
||||
|
||||
// ===== Launch Handler Functions =====
|
||||
|
||||
const launchFunction = async (options: LaunchOptions, launchedNetwork: LaunchedNetwork) => {
|
||||
logger.debug("Running with options:");
|
||||
logger.debug(options);
|
||||
|
||||
const timeStart = performance.now();
|
||||
|
||||
printHeader("Environment Checks");
|
||||
|
||||
await checkDependencies();
|
||||
|
||||
logger.trace("Launching Kurtosis enclave");
|
||||
await launchKurtosis({
|
||||
launchKurtosis: options.launchKurtosis,
|
||||
blockscout: options.blockscout,
|
||||
skipCleaning: options.skipCleaning
|
||||
});
|
||||
logger.trace("Kurtosis enclave launched");
|
||||
|
||||
logger.trace("Send test transaction");
|
||||
printHeader("Setting Up Blockchain");
|
||||
logger.debug(`Using account ${ANVIL_FUNDED_ACCOUNTS[1].publicKey}`);
|
||||
const privateKey = ANVIL_FUNDED_ACCOUNTS[1].privateKey;
|
||||
const rethPublicPort = await getPortFromKurtosis("el-1-reth-lighthouse", "rpc");
|
||||
const networkRpcUrl = `http://127.0.0.1:${rethPublicPort}`;
|
||||
invariant(networkRpcUrl, "❌ Network RPC URL not found");
|
||||
|
||||
logger.info("💸 Sending test transaction...");
|
||||
await sendTxn(privateKey, networkRpcUrl);
|
||||
|
||||
printDivider();
|
||||
|
||||
logger.trace("Show completion information");
|
||||
const timeEnd = performance.now();
|
||||
const minutes = ((timeEnd - timeStart) / (1000 * 60)).toFixed(1);
|
||||
|
||||
logger.success(`Kurtosis network started successfully in ${minutes} minutes`);
|
||||
|
||||
logger.trace("Deploy contracts using the extracted function");
|
||||
let blockscoutBackendUrl: string | undefined = undefined;
|
||||
|
||||
if (options.blockscout === true) {
|
||||
const blockscoutPublicPort = await getPortFromKurtosis("blockscout", "http");
|
||||
blockscoutBackendUrl = `http://127.0.0.1:${blockscoutPublicPort}`;
|
||||
} else if (options.verified) {
|
||||
logger.warn(
|
||||
"⚠️ Contract verification (--verified) requested, but Blockscout is disabled (--no-blockscout). Verification will be skipped."
|
||||
);
|
||||
}
|
||||
|
||||
const contractsDeployed = await deployContracts({
|
||||
rpcUrl: networkRpcUrl,
|
||||
verified: options.verified,
|
||||
blockscoutBackendUrl,
|
||||
deployContracts: options.deployContracts
|
||||
});
|
||||
|
||||
if (contractsDeployed) {
|
||||
await performValidatorOperations(options, networkRpcUrl);
|
||||
} else if (options.setupValidators || options.fundValidators) {
|
||||
logger.warn(
|
||||
"⚠️ Validator operations requested but contracts were not deployed. Skipping validator operations."
|
||||
);
|
||||
}
|
||||
if (options.datahaven) {
|
||||
await performDatahavenOperations(options, launchedNetwork);
|
||||
}
|
||||
|
||||
if (options.relayer) {
|
||||
await performRelayerOperations(options, launchedNetwork);
|
||||
}
|
||||
|
||||
printDivider();
|
||||
|
||||
performSummaryOperations(options, launchedNetwork);
|
||||
logger.debug("Launch function completed successfully");
|
||||
};
|
||||
|
||||
export const launch = async (options: LaunchOptions) => {
|
||||
const run = new LaunchedNetwork();
|
||||
try {
|
||||
await launchFunction(options, run);
|
||||
logger.success("Launch script completed successfully");
|
||||
} finally {
|
||||
await run.cleanup();
|
||||
}
|
||||
};
|
||||
|
||||
export const launchPreActionHook = (
|
||||
thisCmd: Command<[], LaunchOptions & { [key: string]: any }>
|
||||
) => {
|
||||
const {
|
||||
blockscout,
|
||||
verified,
|
||||
fundValidators,
|
||||
setupValidators,
|
||||
updateValidatorSet,
|
||||
deployContracts
|
||||
} = thisCmd.opts();
|
||||
if (verified && !blockscout) {
|
||||
thisCmd.error("--verified requires --blockscout to be set");
|
||||
}
|
||||
if (deployContracts === false && setupValidators) {
|
||||
thisCmd.error("--setupValidators requires --deployContracts to be set");
|
||||
}
|
||||
if (deployContracts === false && fundValidators) {
|
||||
thisCmd.error("--fundValidators requires --deployContracts to be set");
|
||||
}
|
||||
};
|
||||
59
test/cli/handlers/launch/launchedNetwork.ts
Normal file
59
test/cli/handlers/launch/launchedNetwork.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import fs from "node:fs";
|
||||
import invariant from "tiny-invariant";
|
||||
import { logger } from "utils";
|
||||
|
||||
export class LaunchedNetwork {
|
||||
protected runId: string;
|
||||
protected processes: Bun.Subprocess[];
|
||||
protected fileDescriptors: number[];
|
||||
protected DHNodes: { id: string; port: number }[];
|
||||
|
||||
constructor() {
|
||||
this.runId = crypto.randomUUID();
|
||||
this.processes = [];
|
||||
this.fileDescriptors = [];
|
||||
this.DHNodes = [];
|
||||
}
|
||||
|
||||
getRunId(): string {
|
||||
return this.runId;
|
||||
}
|
||||
|
||||
getDHNodes(): { id: string; port: number }[] {
|
||||
return [...this.DHNodes];
|
||||
}
|
||||
|
||||
getDHPort(id: string): number {
|
||||
const node = this.DHNodes.find((x) => x.id === id);
|
||||
invariant(node, `❌ Datahaven node ${id} not found`);
|
||||
return node.port;
|
||||
}
|
||||
|
||||
addFileDescriptor(fd: number) {
|
||||
this.fileDescriptors.push(fd);
|
||||
}
|
||||
|
||||
addProcess(process: Bun.Subprocess) {
|
||||
this.processes.push(process);
|
||||
}
|
||||
|
||||
addDHNode(id: string, port: number) {
|
||||
this.DHNodes.push({ id, port });
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
for (const process of this.processes) {
|
||||
logger.info(`Process is still running: ${process.pid}`);
|
||||
}
|
||||
|
||||
for (const fd of this.fileDescriptors) {
|
||||
try {
|
||||
fs.closeSync(fd);
|
||||
this.fileDescriptors = this.fileDescriptors.filter((x) => x !== fd);
|
||||
logger.debug(`Closed file descriptor ${fd}`);
|
||||
} catch (error) {
|
||||
logger.error(`Error closing file descriptor ${fd}: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
162
test/cli/handlers/launch/relayer.ts
Normal file
162
test/cli/handlers/launch/relayer.ts
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { $ } from "bun";
|
||||
import invariant from "tiny-invariant";
|
||||
import {
|
||||
ANVIL_FUNDED_ACCOUNTS,
|
||||
type RelayerType,
|
||||
SUBSTRATE_FUNDED_ACCOUNTS,
|
||||
getPortFromKurtosis,
|
||||
logger,
|
||||
parseRelayConfig,
|
||||
printHeader
|
||||
} from "utils";
|
||||
import type { LaunchOptions } from ".";
|
||||
import type { LaunchedNetwork } from "./launchedNetwork";
|
||||
|
||||
type RelayerSpec = {
|
||||
name: string;
|
||||
type: RelayerType;
|
||||
config: string;
|
||||
pk: { type: "ethereum" | "substrate"; value: string };
|
||||
};
|
||||
|
||||
export const performRelayerOperations = async (
|
||||
options: LaunchOptions,
|
||||
launchedNetwork: LaunchedNetwork
|
||||
) => {
|
||||
printHeader("Starting Snowbridge Relayers");
|
||||
logger.info("Preparing to generate configs");
|
||||
const anvilDeploymentsPath = "../contracts/deployments/anvil.json";
|
||||
const anvilDeploymentsFile = Bun.file(anvilDeploymentsPath);
|
||||
if (!(await anvilDeploymentsFile.exists())) {
|
||||
logger.error(`File ${anvilDeploymentsPath} does not exist`);
|
||||
throw new Error("Error reading anvil deployments file");
|
||||
}
|
||||
const anvilDeployments = await anvilDeploymentsFile.json();
|
||||
const beefyClientAddress = anvilDeployments.BeefyClient;
|
||||
const gatewayAddress = anvilDeployments.Gateway;
|
||||
invariant(beefyClientAddress, "❌ BeefyClient address not found in anvil.json");
|
||||
invariant(gatewayAddress, "❌ Gateway address not found in anvil.json");
|
||||
|
||||
const outputDir = "tmp/configs";
|
||||
logger.debug(`Ensuring output directory exists: ${outputDir}`);
|
||||
await $`mkdir -p ${outputDir}`.quiet();
|
||||
|
||||
const datastorePath = "tmp/datastore";
|
||||
logger.debug(`Ensuring datastore directory exists: ${datastorePath}`);
|
||||
await $`mkdir -p ${datastorePath}`.quiet();
|
||||
|
||||
const logsPath = `tmp/logs/${launchedNetwork.getRunId()}/`;
|
||||
logger.debug(`Ensuring logs directory exists: ${logsPath}`);
|
||||
await $`mkdir -p ${logsPath}`.quiet();
|
||||
|
||||
const relayersToStart: RelayerSpec[] = [
|
||||
{
|
||||
name: "relayer-🥩",
|
||||
type: "beefy",
|
||||
config: "beefy-relay.json",
|
||||
pk: {
|
||||
type: "ethereum",
|
||||
value: ANVIL_FUNDED_ACCOUNTS[1].privateKey
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "relayer-🥓",
|
||||
type: "beacon",
|
||||
config: "beacon-relay.json",
|
||||
pk: {
|
||||
type: "substrate",
|
||||
value: SUBSTRATE_FUNDED_ACCOUNTS.GOLIATH.privateKey
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
for (const { config: configFileName, type, name } of relayersToStart) {
|
||||
logger.debug(`Creating config for ${name}`);
|
||||
const templateFilePath = `configs/snowbridge/${configFileName}`;
|
||||
const outputFilePath = `tmp/configs/${configFileName}`;
|
||||
logger.debug(`Reading config file ${templateFilePath}`);
|
||||
const file = Bun.file(templateFilePath);
|
||||
|
||||
if (!(await file.exists())) {
|
||||
logger.error(`File ${templateFilePath} does not exist`);
|
||||
throw new Error("Error reading snowbridge config file");
|
||||
}
|
||||
const json = await file.json();
|
||||
|
||||
const ethWsPort = await getPortFromKurtosis("el-1-reth-lighthouse", "ws");
|
||||
const ethHttpPort = await getPortFromKurtosis("cl-1-lighthouse-reth", "http");
|
||||
const substrateWsPort = 9944;
|
||||
logger.debug(
|
||||
`Fetched ports: ETH WS=${ethWsPort}, ETH HTTP=${ethHttpPort}, Substrate WS=${substrateWsPort} (hardcoded)`
|
||||
);
|
||||
|
||||
if (type === "beacon") {
|
||||
const cfg = parseRelayConfig(json, type);
|
||||
cfg.source.beacon.endpoint = `http://127.0.0.1:${ethHttpPort}`;
|
||||
cfg.source.beacon.stateEndpoint = `http://127.0.0.1:${ethHttpPort}`;
|
||||
|
||||
cfg.source.beacon.datastore.location = datastorePath;
|
||||
|
||||
cfg.sink.parachain.endpoint = `ws://127.0.0.1:${substrateWsPort}`;
|
||||
await Bun.write(outputFilePath, JSON.stringify(cfg, null, 4));
|
||||
logger.success(`Updated beacon config written to ${outputFilePath}`);
|
||||
} else {
|
||||
const cfg = parseRelayConfig(json, type);
|
||||
cfg.source.polkadot.endpoint = `ws://127.0.0.1:${substrateWsPort}`;
|
||||
cfg.sink.ethereum.endpoint = `ws://127.0.0.1:${ethWsPort}`;
|
||||
cfg.sink.contracts.BeefyClient = beefyClientAddress;
|
||||
cfg.sink.contracts.Gateway = gatewayAddress;
|
||||
await Bun.write(outputFilePath, JSON.stringify(cfg, null, 4));
|
||||
logger.success(`Updated beefy config written to ${outputFilePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Spawning Snowbridge relayers processes");
|
||||
|
||||
invariant(options.relayerBinPath, "❌ Relayer binary path not defined");
|
||||
invariant(
|
||||
await Bun.file(options.relayerBinPath).exists(),
|
||||
`❌ Relayer binary does not exist at ${options.relayerBinPath}`
|
||||
);
|
||||
|
||||
for (const { config, name, type, pk } of relayersToStart) {
|
||||
try {
|
||||
logger.info(`Starting relayer ${name} ...`);
|
||||
const logFileName = `${type}-${name.replace(/[^a-zA-Z0-9-]/g, "")}.log`;
|
||||
const logFilePath = path.join(logsPath, logFileName);
|
||||
logger.debug(`Writing logs to ${logFilePath}`);
|
||||
|
||||
const fd = fs.openSync(logFilePath, "a");
|
||||
|
||||
const spawnCommand = [
|
||||
options.relayerBinPath,
|
||||
"run",
|
||||
type,
|
||||
"--config",
|
||||
path.join("tmp/configs", config),
|
||||
type === "beacon" ? "--substrate.private-key" : "--ethereum.private-key",
|
||||
pk.value
|
||||
];
|
||||
|
||||
logger.debug(`Spawning command: ${spawnCommand.join(" ")}`);
|
||||
|
||||
const process = Bun.spawn(spawnCommand, {
|
||||
stdout: fd,
|
||||
stderr: fd
|
||||
});
|
||||
|
||||
process.unref();
|
||||
|
||||
launchedNetwork.addFileDescriptor(fd);
|
||||
launchedNetwork.addProcess(process);
|
||||
logger.debug(`Started relayer ${name} with process ${process.pid}`);
|
||||
} catch (e) {
|
||||
logger.error(`Error starting relayer ${name}`);
|
||||
logger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
logger.success("Snowbridge relayers started");
|
||||
};
|
||||
110
test/cli/handlers/launch/summary.ts
Normal file
110
test/cli/handlers/launch/summary.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import invariant from "tiny-invariant";
|
||||
import { getServiceFromKurtosis, logger, printHeader } from "utils";
|
||||
import { BASE_SERVICES, type LaunchOptions } from ".";
|
||||
import type { LaunchedNetwork } from "./launchedNetwork";
|
||||
|
||||
export const performSummaryOperations = async (
|
||||
options: LaunchOptions,
|
||||
launchedNetwork: LaunchedNetwork
|
||||
) => {
|
||||
logger.trace("Display service information in a clean table");
|
||||
printHeader("Service Endpoints");
|
||||
|
||||
logger.trace("Filter services to display based on blockscout option");
|
||||
const servicesToDisplay = BASE_SERVICES;
|
||||
|
||||
if (options.blockscout === true) {
|
||||
servicesToDisplay.push(...["blockscout", "blockscout-frontend"]);
|
||||
}
|
||||
|
||||
if (options.datahaven === true) {
|
||||
const dhNodes = launchedNetwork.getDHNodes();
|
||||
for (const { id } of dhNodes) {
|
||||
servicesToDisplay.push(`datahaven-${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
const displayData: { service: string; ports: Record<string, number>; url: string }[] = [];
|
||||
for (const service of servicesToDisplay) {
|
||||
logger.debug(`Checking service: ${service}`);
|
||||
|
||||
const serviceInfo = service.startsWith("datahaven-")
|
||||
? undefined
|
||||
: await getServiceFromKurtosis(service);
|
||||
logger.trace("Service info", serviceInfo);
|
||||
switch (true) {
|
||||
case service.startsWith("cl-"): {
|
||||
invariant(serviceInfo, `❌ Service info for ${service} is not available`);
|
||||
const httpPort = serviceInfo.public_ports.http.number;
|
||||
displayData.push({
|
||||
service,
|
||||
ports: { http: httpPort },
|
||||
url: `http://127.0.0.1:${httpPort}`
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case service.startsWith("el-"): {
|
||||
invariant(serviceInfo, `❌ Service info for ${service} is not available`);
|
||||
const rpcPort = serviceInfo.public_ports.rpc.number;
|
||||
const wsPort = serviceInfo.public_ports.ws.number;
|
||||
displayData.push({
|
||||
service,
|
||||
ports: { rpc: rpcPort, ws: wsPort },
|
||||
url: `http://127.0.0.1:${rpcPort}`
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case service.startsWith("dora"): {
|
||||
invariant(serviceInfo, `❌ Service info for ${service} is not available`);
|
||||
const httpPort = serviceInfo.public_ports.http.number;
|
||||
displayData.push({
|
||||
service,
|
||||
ports: { http: httpPort },
|
||||
url: `http://127.0.0.1:${httpPort}`
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case service === "blockscout": {
|
||||
invariant(serviceInfo, `❌ Service info for ${service} is not available`);
|
||||
const httpPort = serviceInfo.public_ports.http.number;
|
||||
displayData.push({
|
||||
service,
|
||||
ports: { http: httpPort },
|
||||
url: `http://127.0.0.1:${httpPort}`
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case service === "blockscout-frontend": {
|
||||
invariant(serviceInfo, `❌ Service info for ${service} is not available`);
|
||||
const httpPort = serviceInfo.public_ports.http.number;
|
||||
displayData.push({
|
||||
service,
|
||||
ports: { http: httpPort },
|
||||
url: `http://127.0.0.1:${httpPort}`
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case service.startsWith("datahaven-"): {
|
||||
const port = launchedNetwork.getDHPort(service.split("datahaven-")[1]);
|
||||
displayData.push({
|
||||
service,
|
||||
ports: { http: port },
|
||||
url: `http://127.0.0.1:${port}`
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
logger.error(`Unknown service: ${service}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.table(displayData);
|
||||
logger.debug("Summary completed successfully");
|
||||
};
|
||||
75
test/cli/handlers/launch/validator.ts
Normal file
75
test/cli/handlers/launch/validator.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { fundValidators } from "scripts/fund-validators";
|
||||
import { setupValidators } from "scripts/setup-validators";
|
||||
import { updateValidatorSet } from "scripts/update-validator-set";
|
||||
import { confirmWithTimeout, logger } from "utils";
|
||||
import type { LaunchOptions } from "..";
|
||||
|
||||
export const performValidatorOperations = async (options: LaunchOptions, networkRpcUrl: string) => {
|
||||
logger.trace("Set up validators using the extracted function");
|
||||
let shouldFundValidators = options.fundValidators;
|
||||
let shouldSetupValidators = options.setupValidators;
|
||||
let shouldUpdateValidatorSet = options.updateValidatorSet;
|
||||
|
||||
logger.trace("If not specified, prompt for funding");
|
||||
if (shouldFundValidators === undefined) {
|
||||
shouldFundValidators = await confirmWithTimeout(
|
||||
"Do you want to fund validators with tokens and ETH?",
|
||||
true,
|
||||
10
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Using flag option: ${shouldFundValidators ? "will fund" : "will not fund"} validators`
|
||||
);
|
||||
}
|
||||
|
||||
logger.trace("If not specified, prompt for setup");
|
||||
if (shouldSetupValidators === undefined) {
|
||||
shouldSetupValidators = await confirmWithTimeout(
|
||||
"Do you want to register validators in EigenLayer?",
|
||||
true,
|
||||
10
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Using flag option: ${shouldSetupValidators ? "will register" : "will not register"} validators`
|
||||
);
|
||||
}
|
||||
|
||||
logger.trace("If not specified, prompt for update");
|
||||
if (shouldUpdateValidatorSet === undefined) {
|
||||
shouldUpdateValidatorSet = await confirmWithTimeout(
|
||||
"Do you want to update the validator set on the substrate chain?",
|
||||
true,
|
||||
10
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Using flag option: ${shouldUpdateValidatorSet ? "will update" : "will not update"} validator set`
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldFundValidators) {
|
||||
await fundValidators({
|
||||
rpcUrl: networkRpcUrl
|
||||
});
|
||||
} else {
|
||||
logger.info("Skipping validator funding");
|
||||
}
|
||||
|
||||
if (shouldSetupValidators) {
|
||||
await setupValidators({
|
||||
rpcUrl: networkRpcUrl
|
||||
});
|
||||
|
||||
if (shouldUpdateValidatorSet) {
|
||||
await updateValidatorSet({
|
||||
rpcUrl: networkRpcUrl
|
||||
});
|
||||
} else {
|
||||
logger.info("Skipping validator set update");
|
||||
}
|
||||
} else {
|
||||
logger.info("Skipping validator setup");
|
||||
}
|
||||
};
|
||||
|
|
@ -8,11 +8,26 @@ const program = new Command()
|
|||
.option("-l, --launch-kurtosis", "Launch Kurtosis")
|
||||
.option("-d, --deploy-contracts", "Deploy smart contracts")
|
||||
.option("-f, --fund-validators", "Fund validators")
|
||||
.option("-n, --no-fund-validators", "Skip funding validators")
|
||||
.option("-s, --setup-validators", "Setup validators")
|
||||
.option("--no-setup-validators", "Skip setup validators")
|
||||
.option("-u, --update-validator-set", "Update validator set")
|
||||
.option("--no-update-validator-set", "Skip update validator set")
|
||||
.option("-b, --blockscout", "Enable Blockscout")
|
||||
.option("--datahaven", "Enable Datahaven network to be launched")
|
||||
.option(
|
||||
"--datahaven-bin-path <value>",
|
||||
"Path to the datahaven binary",
|
||||
"../operator/target/release/datahaven-node"
|
||||
)
|
||||
.option("-v, --verified", "Verify smart contracts with Blockscout")
|
||||
.option("-q, --skip-cleaning", "Skip cleaning Kurtosis")
|
||||
.option("-r, --relayer", "Enable Relayer")
|
||||
.option(
|
||||
"-p, --relayer-bin-path <value>",
|
||||
"Path to the relayer binary",
|
||||
"tmp/bin/snowbridge-relay"
|
||||
)
|
||||
.hook("preAction", launchPreActionHook)
|
||||
.action(launch);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,18 +4,21 @@
|
|||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"cli":"bun run cli/index.ts",
|
||||
"cli": "bun run cli/index.ts",
|
||||
"fmt": "biome check .",
|
||||
"fmt:fix": "biome check --write .",
|
||||
"build:docker:relayer": "bun -e \"import build from './scripts/snowbridge-relayer.ts'; build()\"",
|
||||
"generate:snowbridge-cfgs": "bun -e \"import {generateSnowbridgeConfigs} from './scripts/gen-snowbridge-cfgs.ts'; await generateSnowbridgeConfigs()\"",
|
||||
"start:e2e:verified": "bun cli --verified --blockscout",
|
||||
"start:e2e:verified": "bun cli --verified --blockscout --deploy-contracts",
|
||||
"start:e2e:minimal": "bun cli",
|
||||
"stop:e2e": "kurtosis enclave stop datahaven-ethereum && kurtosis clean && kurtosis engine stop && docker container prune -f",
|
||||
"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",
|
||||
"stop:e2e:minimal": "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"
|
||||
"test:e2e": "bun test suites/e2e",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
|
|
@ -27,6 +30,7 @@
|
|||
"@biomejs/biome": "^1.9.4",
|
||||
"@commander-js/extra-typings": "^13.1.0",
|
||||
"@dotenvx/dotenvx": "^1.41.0",
|
||||
"@inquirer/prompts": "^7.5.0",
|
||||
"@types/dockerode": "^3.3.38",
|
||||
"@types/node": "^22.14.1",
|
||||
"chalk": "^5.4.1",
|
||||
|
|
@ -34,6 +38,7 @@
|
|||
"dockerode": "^4.0.6",
|
||||
"dotenv": "^16.5.0",
|
||||
"octokit": "^4.1.3",
|
||||
"ora": "^8.2.0",
|
||||
"pino": "^9.6.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { $ } from "bun";
|
||||
import invariant from "tiny-invariant";
|
||||
import { logger, printHeader, promptWithTimeout } from "utils";
|
||||
import { confirmWithTimeout, logger, printHeader } from "utils";
|
||||
|
||||
interface DeployContractsOptions {
|
||||
rpcUrl: string;
|
||||
|
|
@ -25,7 +25,7 @@ export const deployContracts = async (options: DeployContractsOptions): Promise<
|
|||
// Check if deployContracts option was set via flags, or prompt if not
|
||||
let shouldDeployContracts = deployContracts;
|
||||
if (shouldDeployContracts === undefined) {
|
||||
shouldDeployContracts = await promptWithTimeout(
|
||||
shouldDeployContracts = await confirmWithTimeout(
|
||||
"Do you want to deploy the smart contracts?",
|
||||
true,
|
||||
10
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
import { $ } from "bun";
|
||||
import { getServicesFromDocker, logger, printDivider, printHeader, promptWithTimeout } from "utils";
|
||||
import {
|
||||
type KurtosisService,
|
||||
confirmWithTimeout,
|
||||
getServicesFromKurtosis,
|
||||
logger,
|
||||
printDivider,
|
||||
printHeader
|
||||
} from "utils";
|
||||
|
||||
/**
|
||||
* Launches a Kurtosis Ethereum network enclave for testing.
|
||||
|
|
@ -14,25 +21,26 @@ import { getServicesFromDocker, logger, printDivider, printHeader, promptWithTim
|
|||
* @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 } = {}
|
||||
) => {
|
||||
options: { launchKurtosis?: boolean; blockscout?: boolean; skipCleaning?: boolean } = {}
|
||||
): Promise<Record<string, KurtosisService>> => {
|
||||
if (await checkKurtosisRunning()) {
|
||||
logger.info("ℹ️ Kurtosis network is already running.");
|
||||
|
||||
// Check if launchKurtosis option was set via flags
|
||||
logger.trace("Checking if launchKurtosis option was set via flags");
|
||||
if (options.launchKurtosis === false) {
|
||||
logger.info("Keeping existing Kurtosis enclave. Exiting...");
|
||||
return { success: true, services: await getServicesFromDocker() };
|
||||
return getServicesFromKurtosis();
|
||||
}
|
||||
|
||||
if (options.launchKurtosis === true) {
|
||||
logger.info("Proceeding to clean and relaunch the Kurtosis enclave...");
|
||||
} else {
|
||||
// Use promptWithTimeout if launchKurtosis is undefined
|
||||
const shouldRelaunch = await promptWithTimeout(
|
||||
// Use confirmWithTimeout if launchKurtosis is undefined
|
||||
const shouldRelaunch = await confirmWithTimeout(
|
||||
"Do you want to clean and relaunch the Kurtosis enclave?",
|
||||
true,
|
||||
10
|
||||
|
|
@ -40,24 +48,23 @@ export const launchKurtosis = async (
|
|||
|
||||
if (!shouldRelaunch) {
|
||||
logger.info("Keeping existing Kurtosis enclave. Exiting...");
|
||||
return { success: true, services: await getServicesFromDocker() };
|
||||
return getServicesFromKurtosis();
|
||||
}
|
||||
|
||||
logger.info("Proceeding to clean and relaunch the Kurtosis enclave...");
|
||||
}
|
||||
}
|
||||
|
||||
// Start Kurtosis network
|
||||
printHeader("Starting Kurtosis Network");
|
||||
|
||||
// Clean up Docker and Kurtosis
|
||||
logger.info("🧹 Cleaning up Docker and Kurtosis environments...");
|
||||
logger.debug(await $`kurtosis enclave stop datahaven-ethereum`.nothrow().text());
|
||||
logger.debug(await $`kurtosis clean`.text());
|
||||
logger.debug(await $`kurtosis engine stop`.text());
|
||||
logger.debug(await $`docker system prune -f`.nothrow().text());
|
||||
if (!options.skipCleaning) {
|
||||
logger.info("🧹 Cleaning up Docker and Kurtosis environments...");
|
||||
logger.debug(await $`kurtosis enclave stop datahaven-ethereum`.nothrow().text());
|
||||
logger.debug(await $`kurtosis clean`.text());
|
||||
logger.debug(await $`kurtosis engine stop`.text());
|
||||
logger.debug(await $`docker system prune -f`.nothrow().text());
|
||||
}
|
||||
|
||||
// Pull necessary Docker images
|
||||
if (process.platform === "darwin") {
|
||||
logger.debug("Detected macOS, pulling container images with linux/amd64 platform...");
|
||||
logger.debug(
|
||||
|
|
@ -65,9 +72,7 @@ export const launchKurtosis = async (
|
|||
);
|
||||
}
|
||||
|
||||
// Run Kurtosis
|
||||
logger.info("🚀 Starting Kurtosis enclave...");
|
||||
// Determine which config file to use based on the blockscout option
|
||||
const configFile =
|
||||
options.blockscout === true
|
||||
? "configs/kurtosis/minimal-with-bs.yaml"
|
||||
|
|
@ -85,13 +90,12 @@ export const launchKurtosis = async (
|
|||
}
|
||||
logger.debug(stdout.toString());
|
||||
|
||||
// Get service information from Docker
|
||||
logger.info("🔍 Detecting Docker container ports...");
|
||||
const services = await getServicesFromDocker();
|
||||
logger.info("🔍 Gathering Kurtosis public ports...");
|
||||
const services = await getServicesFromKurtosis();
|
||||
|
||||
printDivider();
|
||||
|
||||
return { success: true, services };
|
||||
return services;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { http, type Hex, createWalletClient, defineChain, parseEther, publicActions } from "viem";
|
||||
import { logger } from "utils";
|
||||
import { http, createWalletClient, defineChain, parseEther, publicActions } from "viem";
|
||||
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
|
||||
|
||||
export default async function main(privateKey: string, networkRpcUrl: string) {
|
||||
|
|
@ -22,7 +23,7 @@ export default async function main(privateKey: string, networkRpcUrl: string) {
|
|||
|
||||
const signer = privateKeyToAccount(privateKey as `0x${string}`);
|
||||
|
||||
console.log(`Using account: ${signer.address}`);
|
||||
logger.debug(`Using account: ${signer.address}`);
|
||||
const client = createWalletClient({
|
||||
account: signer,
|
||||
chain: datahaven,
|
||||
|
|
@ -38,15 +39,15 @@ export default async function main(privateKey: string, networkRpcUrl: string) {
|
|||
];
|
||||
|
||||
for (const address of addresses) {
|
||||
console.log(`Sending 1 ETH to address: ${address}`);
|
||||
logger.debug(`Sending 1 ETH to address: ${address}`);
|
||||
|
||||
const hash = await client.sendTransaction({
|
||||
to: address as `0x${string}`,
|
||||
value: parseEther("1.0")
|
||||
});
|
||||
|
||||
console.log(`Waiting for transaction ${hash} to be confirmed...`);
|
||||
logger.info(`Waiting for transaction ${hash} to be confirmed...`);
|
||||
const receipt = await client.waitForTransactionReceipt({ hash });
|
||||
console.log(`Transaction confirmed in block ${receipt.blockNumber}`);
|
||||
logger.info(`Transaction confirmed in block ${receipt.blockNumber}`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import path from "node:path";
|
|||
// Setup of validators for DataHaven
|
||||
import { $ } from "bun";
|
||||
import invariant from "tiny-invariant";
|
||||
import { logger, printHeader, promptWithTimeout } from "../utils/index";
|
||||
import { confirmWithTimeout, logger, printHeader } from "../utils/index";
|
||||
|
||||
interface SetupValidatorsOptions {
|
||||
rpcUrl: string;
|
||||
|
|
@ -46,7 +46,7 @@ export const setupValidators = async (options: SetupValidatorsOptions): Promise<
|
|||
// Check if executeSignup option was set via flags, or prompt if not
|
||||
let shouldExecuteSignup = executeSignup;
|
||||
if (shouldExecuteSignup === undefined) {
|
||||
shouldExecuteSignup = await promptWithTimeout(
|
||||
shouldExecuteSignup = await confirmWithTimeout(
|
||||
"Do you want to register validators in EigenLayer?",
|
||||
true,
|
||||
10
|
||||
|
|
|
|||
|
|
@ -31,6 +31,6 @@
|
|||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
},
|
||||
"include": ["utils/*.ts", "scripts/*.ts", "suites/**/*.ts", "cli/*.ts"],
|
||||
"include": ["utils/*.ts", "scripts/*.ts", "suites/**/*.ts", "cli/**/*.ts"],
|
||||
"exclude": ["node_modules/"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,3 +51,46 @@ export const CONTAINER_NAMES = {
|
|||
"blockscout-be": "blockscout--",
|
||||
"blockscout-fe": "blockscout-frontend--"
|
||||
} as const;
|
||||
|
||||
export const SUBSTRATE_FUNDED_ACCOUNTS = {
|
||||
ALITH: {
|
||||
publicKey: "0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac",
|
||||
privateKey: "0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133"
|
||||
},
|
||||
BALTATHAR: {
|
||||
publicKey: "0x3Cd0A705a2DC65e5b1E1205896BaA2be8A07c6e0",
|
||||
privateKey: "0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b"
|
||||
},
|
||||
CHARLETH: {
|
||||
publicKey: "0x798d4Ba9baf0064Ec19eB4F0a1a45785ae9D6DFc",
|
||||
privateKey: "0x0b6e18cafb6ed99687ec547bd28139cafdd2bffe70e6b688025de6b445aa5c5b"
|
||||
},
|
||||
DOROTHY: {
|
||||
publicKey: "0x773539d4Ac0e786233D90A233654ccEE26a613D9",
|
||||
privateKey: "0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68"
|
||||
},
|
||||
ETHAN: {
|
||||
publicKey: "0xFf64d3F6efE2317EE2807d223a0Bdc4c0c49dfDB",
|
||||
privateKey: "0x7dce9bc8babb68fec1409be38c8e1a52650206a7ed90ff956ae8a6d15eeaaef4"
|
||||
},
|
||||
FAITH: {
|
||||
publicKey: "0xC0F0f4ab324C46e55D02D0033343B4Be8A55532d",
|
||||
privateKey: "0xb9d2ea9a615f3165812e8d44de0d24da9bbd164b65c4f0573e1ce2c8dbd9c8df"
|
||||
},
|
||||
GOLIATH: {
|
||||
publicKey: "0x7BF369283338E12C90514468aa3868A551AB2929",
|
||||
privateKey: "0x96b8a38e12e1a31dee1eab2fffdf9d9990045f5b37e44d8cc27766ef294acf18"
|
||||
},
|
||||
HEATH: {
|
||||
publicKey: "0x931f3600a299fd9B24cEfB3BfF79388D19804BeA",
|
||||
privateKey: "0x0d6dcaaef49272a5411896be8ad16c01c35d6f8c18873387b71fbc734759b0ab"
|
||||
},
|
||||
IDA: {
|
||||
publicKey: "0xC41C5F1123ECCd5ce233578B2e7ebd5693869d73",
|
||||
privateKey: "0x4c42532034540267bf568198ccec4cb822a025da542861fcb146a5fab6433ff8"
|
||||
},
|
||||
JUDITH: {
|
||||
publicKey: "0x2898FE7a42Be376C8BC7AF536A940F7Fd5aDd423",
|
||||
privateKey: "0x94c49300a58d576011096bcb006aa06f5a91b34b4383891e8029c21dc39fbb8b"
|
||||
}
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -1,46 +1,6 @@
|
|||
import Docker from "dockerode";
|
||||
import invariant from "tiny-invariant";
|
||||
import { logger } from "utils";
|
||||
|
||||
interface ServiceMapping {
|
||||
service: string;
|
||||
containerPattern: string;
|
||||
internalPort: number;
|
||||
protocol: string;
|
||||
}
|
||||
|
||||
interface ServiceInfo {
|
||||
service: string;
|
||||
port: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const serviceMappings: ServiceMapping[] = [
|
||||
{
|
||||
service: "reth-1-rpc",
|
||||
containerPattern: "el-1-reth-lighthouse",
|
||||
internalPort: 8545,
|
||||
protocol: "tcp"
|
||||
},
|
||||
{
|
||||
service: "reth-2-rpc",
|
||||
containerPattern: "el-2-reth-lighthouse",
|
||||
internalPort: 8545,
|
||||
protocol: "tcp"
|
||||
},
|
||||
{
|
||||
service: "blockscout-backend",
|
||||
containerPattern: "blockscout--",
|
||||
internalPort: 4000,
|
||||
protocol: "tcp"
|
||||
},
|
||||
{
|
||||
service: "dora",
|
||||
containerPattern: "dora--",
|
||||
internalPort: 8080,
|
||||
protocol: "tcp"
|
||||
}
|
||||
];
|
||||
import { type ServiceInfo, type ServiceMapping, StandardServiceMappings, logger } from "utils";
|
||||
|
||||
export const getServicesFromDocker = async (): Promise<ServiceInfo[]> => {
|
||||
const docker = new Docker();
|
||||
|
|
@ -49,7 +9,7 @@ export const getServicesFromDocker = async (): Promise<ServiceInfo[]> => {
|
|||
|
||||
const services: ServiceInfo[] = [];
|
||||
|
||||
for (const mapping of serviceMappings) {
|
||||
for (const mapping of StandardServiceMappings) {
|
||||
try {
|
||||
const container = containers.find((container) =>
|
||||
container.Names.some((name) => name.includes(mapping.containerPattern))
|
||||
|
|
|
|||
|
|
@ -5,3 +5,5 @@ export * from "./input";
|
|||
export * from "./logger";
|
||||
export * from "./rpc";
|
||||
export * from "./viem";
|
||||
export * from "./kurtosis";
|
||||
export * from "./parser";
|
||||
|
|
|
|||
|
|
@ -1,51 +1,92 @@
|
|||
import readline from "node:readline";
|
||||
import {
|
||||
type Status,
|
||||
type Theme,
|
||||
createPrompt,
|
||||
isEnterKey,
|
||||
makeTheme,
|
||||
useEffect,
|
||||
useKeypress,
|
||||
usePrefix,
|
||||
useState
|
||||
} from "@inquirer/core";
|
||||
import type { PartialDeep } from "@inquirer/type";
|
||||
import chalk from "chalk";
|
||||
// Helper function to create an interactive prompt with timeout
|
||||
export const promptWithTimeout = async (
|
||||
|
||||
type TimeoutConfirmConfig = {
|
||||
message: string;
|
||||
default?: boolean;
|
||||
timeoutMs: number;
|
||||
theme?: PartialDeep<Theme>;
|
||||
};
|
||||
|
||||
export const timeoutConfirm = createPrompt<boolean, TimeoutConfirmConfig>((cfg, done) => {
|
||||
const [status, setStatus] = useState<Status>("loading");
|
||||
const [input, setInput] = useState("");
|
||||
const [left, setLeft] = useState(cfg.timeoutMs);
|
||||
|
||||
const theme = makeTheme(cfg.theme);
|
||||
const prefix = usePrefix({ status, theme });
|
||||
|
||||
useEffect(() => {
|
||||
const startTime = Date.now();
|
||||
const id = setInterval(() => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const newLeft = Math.max(0, cfg.timeoutMs - elapsed);
|
||||
|
||||
setLeft(newLeft);
|
||||
|
||||
if (newLeft <= 0) {
|
||||
setStatus("done");
|
||||
clearInterval(id);
|
||||
done(cfg.default ?? true);
|
||||
}
|
||||
}, 10);
|
||||
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
const finish = () => {
|
||||
const val = /^(y|yes)$/i.test(input)
|
||||
? true
|
||||
: /^(n|no)$/i.test(input)
|
||||
? false
|
||||
: (cfg.default ?? true);
|
||||
setStatus("done");
|
||||
done(val);
|
||||
};
|
||||
|
||||
useKeypress((key, rl) => {
|
||||
if (isEnterKey(key)) finish();
|
||||
else setInput(rl.line);
|
||||
});
|
||||
|
||||
const defaultBadge = theme.style.defaultAnswer(cfg.default === false ? "y/N" : "Y/n");
|
||||
|
||||
const main = `${prefix} ${theme.style.message(cfg.message, status)} \
|
||||
${defaultBadge} ${input}`;
|
||||
const border = chalk.yellow("=".repeat(cfg.message.length + 40));
|
||||
const hint = theme.style.help(
|
||||
chalk.magenta(
|
||||
`⏱ Will default to ${chalk.bold(cfg.default ? "YES" : "NO")} in ${chalk.bold((left / 1000).toFixed(0))}s`
|
||||
)
|
||||
);
|
||||
|
||||
return `${border}\n${hint}\n${main}\n${border}`;
|
||||
});
|
||||
|
||||
export const confirmWithTimeout = (
|
||||
question: string,
|
||||
defaultValue: boolean,
|
||||
timeoutSeconds: number
|
||||
): Promise<boolean> => {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const defaultText = defaultValue ? "Y/n" : "y/N";
|
||||
|
||||
// Create a visually striking prompt
|
||||
const border = chalk.yellow("=".repeat(question.length + 40));
|
||||
console.log("\n");
|
||||
console.log(border);
|
||||
console.log(chalk.yellow("▶ ") + chalk.bold.cyan(question));
|
||||
console.log(
|
||||
chalk.magenta(
|
||||
`⏱ Will default to ${chalk.bold(defaultValue ? "YES" : "NO")} in ${chalk.bold(timeoutSeconds)} seconds`
|
||||
)
|
||||
);
|
||||
console.log(border);
|
||||
const fullQuestion = chalk.green(`\n➤ Please enter your choice [${chalk.bold(defaultText)}]: `);
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
console.log(
|
||||
`\n${chalk.yellow("⏱")} ${chalk.bold("Timeout reached, using default:")} ${chalk.green(defaultValue ? "YES" : "NO")}\n`
|
||||
);
|
||||
rl.close();
|
||||
resolve(defaultValue);
|
||||
}, timeoutSeconds * 1000);
|
||||
|
||||
rl.question(fullQuestion, (answer) => {
|
||||
clearTimeout(timer);
|
||||
rl.close();
|
||||
|
||||
if (answer.trim() === "") {
|
||||
resolve(defaultValue);
|
||||
} else {
|
||||
const normalizedAnswer = answer.trim().toLowerCase();
|
||||
console.log("");
|
||||
resolve(normalizedAnswer === "y" || normalizedAnswer === "yes");
|
||||
) =>
|
||||
timeoutConfirm({
|
||||
message: question,
|
||||
default: defaultValue,
|
||||
timeoutMs: timeoutSeconds * 1000,
|
||||
theme: {
|
||||
style: {
|
||||
message: (text: string) => chalk.cyan(text),
|
||||
answer: (text: string) => chalk.green(text)
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
126
test/utils/kurtosis.ts
Normal file
126
test/utils/kurtosis.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import { $ } from "bun";
|
||||
import { z } from "zod";
|
||||
import { logger } from "./logger";
|
||||
|
||||
export interface ServiceMapping {
|
||||
service: string;
|
||||
containerPattern: string;
|
||||
internalPort: number;
|
||||
protocol: string;
|
||||
}
|
||||
|
||||
export interface ServiceInfo {
|
||||
service: string;
|
||||
port: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export type KurtosisServiceInfo = {
|
||||
name: string;
|
||||
portType: string;
|
||||
portNumber: number;
|
||||
};
|
||||
|
||||
export const standardKurtosisServices = [
|
||||
"el-1-reth-lighthouse",
|
||||
"el-2-reth-lighthouse",
|
||||
"vc-1-reth-lighthouse",
|
||||
"vc-2-reth-lighthouse",
|
||||
"dora"
|
||||
];
|
||||
|
||||
export const StandardServiceMappings: ServiceMapping[] = [
|
||||
{
|
||||
service: "reth-1-rpc",
|
||||
containerPattern: "el-1-reth-lighthouse",
|
||||
internalPort: 8545,
|
||||
protocol: "tcp"
|
||||
},
|
||||
{
|
||||
service: "reth-2-rpc",
|
||||
containerPattern: "el-2-reth-lighthouse",
|
||||
internalPort: 8545,
|
||||
protocol: "tcp"
|
||||
},
|
||||
{
|
||||
service: "blockscout-backend",
|
||||
containerPattern: "blockscout--",
|
||||
internalPort: 4000,
|
||||
protocol: "tcp"
|
||||
},
|
||||
{
|
||||
service: "dora",
|
||||
containerPattern: "dora--",
|
||||
internalPort: 8080,
|
||||
protocol: "tcp"
|
||||
}
|
||||
];
|
||||
|
||||
const portDetailSchema = z.object({
|
||||
number: z.number(),
|
||||
transport: z.number(), // Consider z.literal(0) | z.literal(2) if these are the only values
|
||||
maybe_application_protocol: z.string().optional()
|
||||
});
|
||||
|
||||
const portsListSchema = z.record(z.string(), portDetailSchema);
|
||||
type PortsList = z.infer<typeof portsListSchema>;
|
||||
|
||||
const serviceSchema = z.object({
|
||||
image: z.string(),
|
||||
ports: portsListSchema,
|
||||
public_ports: portsListSchema,
|
||||
files: z.record(z.string(), z.array(z.string())).optional(),
|
||||
entrypoint: z.array(z.string()).optional(),
|
||||
cmd: z.array(z.string()),
|
||||
env_vars: z.record(z.string(), z.string()),
|
||||
labels: z.record(z.string(), z.string()).optional(),
|
||||
tini_enabled: z.boolean()
|
||||
});
|
||||
|
||||
export type KurtosisService = z.infer<typeof serviceSchema>;
|
||||
|
||||
export const getServiceFromKurtosis = async (service: string): Promise<KurtosisService> => {
|
||||
logger.debug("Getting service from kurtosis", service);
|
||||
|
||||
const command = `kurtosis service inspect datahaven-ethereum ${service} -o json`;
|
||||
logger.debug(`Running command: ${command}`);
|
||||
|
||||
const { stdout, stderr, exitCode } = await $`sh -c ${command}`.nothrow().quiet();
|
||||
if (exitCode !== 0) {
|
||||
throw Error(`Failed to get port for ${service}: ${stderr.toString()}`);
|
||||
}
|
||||
|
||||
const output = stdout.toString();
|
||||
logger.trace(output);
|
||||
|
||||
return serviceSchema.parse(JSON.parse(output));
|
||||
};
|
||||
|
||||
export const getPortFromKurtosis = async (service: string, portName: string): Promise<number> => {
|
||||
logger.debug("Getting port for service", service, portName);
|
||||
|
||||
const command = `kurtosis service inspect datahaven-ethereum ${service} -o json`;
|
||||
logger.debug(`Running command: ${command}`);
|
||||
|
||||
const { stdout, stderr, exitCode } = await $`sh -c ${command}`.nothrow().quiet();
|
||||
if (exitCode !== 0) {
|
||||
throw Error(`Failed to get port for ${service} ${portName}: ${stderr.toString()}`);
|
||||
}
|
||||
|
||||
const output = stdout.toString();
|
||||
logger.debug(output);
|
||||
|
||||
const parsed = serviceSchema.parse(JSON.parse(output));
|
||||
|
||||
return parsed.public_ports[portName].number;
|
||||
};
|
||||
|
||||
export const getServicesFromKurtosis = async (): Promise<Record<string, KurtosisService>> => {
|
||||
const promises = standardKurtosisServices.map(async (serviceName) => {
|
||||
const serviceData = await getServiceFromKurtosis(serviceName);
|
||||
return { [serviceName]: serviceData };
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
return results.reduce((acc, current) => ({ ...acc, ...current }), {});
|
||||
};
|
||||
|
|
@ -5,7 +5,9 @@ import pinoPretty from "pino-pretty";
|
|||
const logLevel = process.env.LOG_LEVEL || "info";
|
||||
|
||||
const stream = pinoPretty({
|
||||
colorize: true
|
||||
colorize: true,
|
||||
// Log to STDERR so it doesn't interfere with CLI output
|
||||
destination: 2
|
||||
});
|
||||
|
||||
// Custom logger type with success method
|
||||
|
|
|
|||
103
test/utils/parser.ts
Normal file
103
test/utils/parser.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const BeaconRelayConfigSchema = z.object({
|
||||
source: z.object({
|
||||
beacon: z.object({
|
||||
endpoint: z.string(),
|
||||
stateEndpoint: z.string(),
|
||||
spec: z.object({
|
||||
syncCommitteeSize: z.number(),
|
||||
slotsInEpoch: z.number(),
|
||||
epochsPerSyncCommitteePeriod: z.number(),
|
||||
forkVersions: z.object({
|
||||
deneb: z.number(),
|
||||
electra: z.number()
|
||||
})
|
||||
}),
|
||||
datastore: z.object({
|
||||
location: z.string(),
|
||||
maxEntries: z.number()
|
||||
})
|
||||
})
|
||||
}),
|
||||
sink: z.object({
|
||||
parachain: z.object({
|
||||
endpoint: z.string(),
|
||||
maxWatchedExtrinsics: z.number(),
|
||||
headerRedundancy: z.number()
|
||||
}),
|
||||
updateSlotInterval: z.number()
|
||||
})
|
||||
});
|
||||
export type BeaconRelayConfig = z.infer<typeof BeaconRelayConfigSchema>;
|
||||
|
||||
export const BeefyRelayConfigSchema = z.object({
|
||||
source: z.object({
|
||||
polkadot: z.object({
|
||||
endpoint: z.string()
|
||||
})
|
||||
}),
|
||||
sink: z.object({
|
||||
ethereum: z.object({
|
||||
endpoint: z.string(),
|
||||
"gas-limit": z.string()
|
||||
}),
|
||||
"descendants-until-final": z.number(),
|
||||
contracts: z.object({
|
||||
BeefyClient: z.string(),
|
||||
Gateway: z.string()
|
||||
})
|
||||
}),
|
||||
"on-demand-sync": z.object({
|
||||
"max-tokens": z.number(),
|
||||
"refill-amount": z.number(),
|
||||
"refill-period": z.number()
|
||||
})
|
||||
});
|
||||
export type BeefyRelayConfig = z.infer<typeof BeefyRelayConfigSchema>;
|
||||
|
||||
export type RelayerType = "beefy" | "beacon";
|
||||
|
||||
/**
|
||||
* Parse beacon relay configuration
|
||||
*/
|
||||
function parseBeaconConfig(config: unknown): BeaconRelayConfig {
|
||||
const result = BeaconRelayConfigSchema.safeParse(config);
|
||||
if (result.success) {
|
||||
return result.data;
|
||||
}
|
||||
throw new Error(`Failed to parse config as BeaconRelayConfig: ${result.error.message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse beefy relay configuration
|
||||
*/
|
||||
function parseBeefyConfig(config: unknown): BeefyRelayConfig {
|
||||
const result = BeefyRelayConfigSchema.safeParse(config);
|
||||
if (result.success) {
|
||||
return result.data;
|
||||
}
|
||||
throw new Error(`Failed to parse config as BeefyRelayConfig: ${result.error.message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type Guard to check if a config object is a BeaconRelayConfig
|
||||
*/
|
||||
export function isBeaconConfig(
|
||||
config: BeaconRelayConfig | BeefyRelayConfig
|
||||
): config is BeaconRelayConfig {
|
||||
return "beacon" in config.source;
|
||||
}
|
||||
|
||||
export function parseRelayConfig(config: unknown, type: "beacon"): BeaconRelayConfig;
|
||||
export function parseRelayConfig(config: unknown, type: "beefy"): BeefyRelayConfig;
|
||||
export function parseRelayConfig(
|
||||
config: unknown,
|
||||
type: RelayerType
|
||||
): BeaconRelayConfig | BeefyRelayConfig;
|
||||
export function parseRelayConfig(
|
||||
config: unknown,
|
||||
type: RelayerType
|
||||
): BeaconRelayConfig | BeefyRelayConfig {
|
||||
return type === "beacon" ? parseBeaconConfig(config) : parseBeefyConfig(config);
|
||||
}
|
||||
Loading…
Reference in a new issue