diff --git a/.github/workflows/ts-lint.yml b/.github/workflows/ts-lint.yml new file mode 100644 index 00000000..a0a40b45 --- /dev/null +++ b/.github/workflows/ts-lint.yml @@ -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 diff --git a/biome.json b/biome.json index f9f3dc82..eddea176 100644 --- a/biome.json +++ b/biome.json @@ -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"] diff --git a/test/TODO.md b/test/TODO.md index a8f85375..5ec3def5 100644 --- a/test/TODO.md +++ b/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 diff --git a/test/biome.json b/test/biome.json index 8381248f..03ae7f15 100644 --- a/test/biome.json +++ b/test/biome.json @@ -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"] } diff --git a/test/bun.lock b/test/bun.lock index e7f4b7c6..aa9b46b2 100644 --- a/test/bun.lock +++ b/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=="], } } diff --git a/test/cli/handlers/launch.ts b/test/cli/handlers/launch.ts deleted file mode 100644 index 47717f34..00000000 --- a/test/cli/handlers/launch.ts +++ /dev/null @@ -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 => { - 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 => { - 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 => { - 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 => { - const { exitCode, stderr, stdout } = await $`forge --version`.nothrow().quiet(); - if (exitCode !== 0) { - logger.error(stderr.toString()); - return false; - } - logger.debug(stdout.toString()); - return true; -}; diff --git a/test/cli/handlers/launch/checks.ts b/test/cli/handlers/launch/checks.ts new file mode 100644 index 00000000..8c1b8c0c --- /dev/null +++ b/test/cli/handlers/launch/checks.ts @@ -0,0 +1,56 @@ +import { $ } from "bun"; +import { logger } from "utils"; + +// ===== Checks ===== +export const checkDependencies = async (): Promise => { + 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 => { + 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 => { + 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 => { + const { exitCode, stderr, stdout } = await $`forge --version`.nothrow().quiet(); + if (exitCode !== 0) { + logger.error(stderr.toString()); + return false; + } + logger.debug(stdout.toString()); + return true; +}; diff --git a/test/cli/handlers/launch/datahaven.ts b/test/cli/handlers/launch/datahaven.ts new file mode 100644 index 00000000..92421a11 --- /dev/null +++ b/test/cli/handlers/launch/datahaven.ts @@ -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 +// /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 => { + 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; + } +}; diff --git a/test/cli/handlers/launch/index.ts b/test/cli/handlers/launch/index.ts new file mode 100644 index 00000000..a70a173d --- /dev/null +++ b/test/cli/handlers/launch/index.ts @@ -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"); + } +}; diff --git a/test/cli/handlers/launch/launchedNetwork.ts b/test/cli/handlers/launch/launchedNetwork.ts new file mode 100644 index 00000000..3216bdb4 --- /dev/null +++ b/test/cli/handlers/launch/launchedNetwork.ts @@ -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}`); + } + } + } +} diff --git a/test/cli/handlers/launch/relayer.ts b/test/cli/handlers/launch/relayer.ts new file mode 100644 index 00000000..339dc324 --- /dev/null +++ b/test/cli/handlers/launch/relayer.ts @@ -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"); +}; diff --git a/test/cli/handlers/launch/summary.ts b/test/cli/handlers/launch/summary.ts new file mode 100644 index 00000000..82edf164 --- /dev/null +++ b/test/cli/handlers/launch/summary.ts @@ -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; 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"); +}; diff --git a/test/cli/handlers/launch/validator.ts b/test/cli/handlers/launch/validator.ts new file mode 100644 index 00000000..b1b87351 --- /dev/null +++ b/test/cli/handlers/launch/validator.ts @@ -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"); + } +}; diff --git a/test/cli/index.ts b/test/cli/index.ts index f16d5d06..d3288e6a 100644 --- a/test/cli/index.ts +++ b/test/cli/index.ts @@ -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 ", + "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 ", + "Path to the relayer binary", + "tmp/bin/snowbridge-relay" + ) .hook("preAction", launchPreActionHook) .action(launch); diff --git a/test/package.json b/test/package.json index bf1cb45f..d10e024d 100644 --- a/test/package.json +++ b/test/package.json @@ -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", diff --git a/test/scripts/deploy-contracts.ts b/test/scripts/deploy-contracts.ts index 7d81f44f..09d2192f 100644 --- a/test/scripts/deploy-contracts.ts +++ b/test/scripts/deploy-contracts.ts @@ -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 diff --git a/test/scripts/launch-kurtosis.ts b/test/scripts/launch-kurtosis.ts index fdec2e37..6948842c 100644 --- a/test/scripts/launch-kurtosis.ts +++ b/test/scripts/launch-kurtosis.ts @@ -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> => { 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; }; /** diff --git a/test/scripts/send-txn.ts b/test/scripts/send-txn.ts index 6dd003c3..af2e4e40 100644 --- a/test/scripts/send-txn.ts +++ b/test/scripts/send-txn.ts @@ -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}`); } } diff --git a/test/scripts/setup-validators.ts b/test/scripts/setup-validators.ts index 6b2c3877..b834e0b0 100644 --- a/test/scripts/setup-validators.ts +++ b/test/scripts/setup-validators.ts @@ -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 diff --git a/test/tsconfig.json b/test/tsconfig.json index 7d41b24b..b8f20b13 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -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/"] } diff --git a/test/utils/constants.ts b/test/utils/constants.ts index b5941cb6..b2ac87c0 100644 --- a/test/utils/constants.ts +++ b/test/utils/constants.ts @@ -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; diff --git a/test/utils/docker.ts b/test/utils/docker.ts index 00b4f7b0..05698d8c 100644 --- a/test/utils/docker.ts +++ b/test/utils/docker.ts @@ -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 => { const docker = new Docker(); @@ -49,7 +9,7 @@ export const getServicesFromDocker = async (): Promise => { 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)) diff --git a/test/utils/index.ts b/test/utils/index.ts index 196aa70b..bfcf3632 100644 --- a/test/utils/index.ts +++ b/test/utils/index.ts @@ -5,3 +5,5 @@ export * from "./input"; export * from "./logger"; export * from "./rpc"; export * from "./viem"; +export * from "./kurtosis"; +export * from "./parser"; diff --git a/test/utils/input.ts b/test/utils/input.ts index 5cedf526..eb58c60d 100644 --- a/test/utils/input.ts +++ b/test/utils/input.ts @@ -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; +}; + +export const timeoutConfirm = createPrompt((cfg, done) => { + const [status, setStatus] = useState("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 => { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - - return new Promise((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) } - }); + } }); -}; diff --git a/test/utils/kurtosis.ts b/test/utils/kurtosis.ts new file mode 100644 index 00000000..8cb95e59 --- /dev/null +++ b/test/utils/kurtosis.ts @@ -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; + +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; + +export const getServiceFromKurtosis = async (service: string): Promise => { + 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 => { + 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> => { + 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 }), {}); +}; diff --git a/test/utils/logger.ts b/test/utils/logger.ts index 7a081e8d..05a7efad 100644 --- a/test/utils/logger.ts +++ b/test/utils/logger.ts @@ -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 diff --git a/test/utils/parser.ts b/test/utils/parser.ts new file mode 100644 index 00000000..18586c8a --- /dev/null +++ b/test/utils/parser.ts @@ -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; + +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; + +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); +}