From ce0106640639f2dd207436e54d8ca759651cbdcc Mon Sep 17 00:00:00 2001 From: Hayden Shively Date: Wed, 5 Aug 2026 09:26:10 -0400 Subject: [PATCH 01/14] refactor(repo): drop bun for vitest, node, and esbuild Completes the toolchain consolidation started by the pnpm migration. All 136 test files move from bun:test to vitest (soltag sol`` via its vite adapter, fork-suite env via loadEnv); bots bundle with esbuild and run plain node dist/src/index.js; images are node-slim with no bun binary; CI drops setup-bun and runs pnpm test. Bun APIs replaced: Bun.env->process.env (59), Bun.spawn->node:child_process, Bun.serve->node:http, Bun.file->readFile, Bun.sleep->node:timers/promises, Bun.which, Bun.argv->process.argv, import.meta.dir/main, bun's $ shell->execa, global confirm()->node:readline/promises, and the `bun` module type imports. Bun.build in @repo/contracts also moves to esbuild. Three latent bugs surfaced and are fixed: - 24 floating expect(...).rejects assertions that bun's typings hid. They never asserted anything; oxlint's no-floating-promises catches them under vitest typings. - Anvil fork ports collided once files ran in parallel. bun's runner was serial, so fixed ports were only deconflicted within a bot. Every fork suite now claims a distinct port and the registry is documented. - fetch.preconnect in http-json.utils.ts was a bun-only extension to fetch. Two bounds are raised because the interpreter genuinely changed: a tsx cold start costs ~1.3s against bun's ~0.1s, so the market-making subprocess tests get a 30s ceiling and the FIFO fail-closed probe a 10s bound. Both still prove what they were written to prove. Evidence: vitest collects exactly the same 136 files as bun (diffed, zero delta, including the one test outside test/); @repo/contracts' abis/*.json and dist/index.d.ts are byte-identical across Bun.build->esbuild; the secret path in deploy-railway still reaches stdin and never argv (probed); all 11 projects verified non-vacuous. Full suite green: 1415 pass, 1 skip. CRTR-2822 Co-Authored-By: Claude Opus 5 (1M context) --- .claude/agents/reviewer.md | 4 +- .claude/commands/review.md | 4 +- .github/actions/setup/action.yml | 8 +- .github/workflows/checks.yml | 2 +- CLAUDE.md | 9 +- README.md | 2 +- bots/blue-liquidation/Dockerfile | 26 +- bots/blue-liquidation/bunfig.toml | 6 - bots/blue-liquidation/package.json | 17 +- bots/blue-liquidation/scripts/build.ts | 55 + .../scripts/bundle-failed.error.ts | 11 + .../scripts/deploy-railway.ts | 69 +- .../scripts/probe-live-lens.ts | 2 +- bots/blue-liquidation/soltag.preload.ts | 30 - bots/blue-liquidation/src/config.ts | 2 +- bots/blue-liquidation/src/index.ts | 8 +- bots/blue-liquidation/test/config.test.ts | 2 +- .../test/discovery/borrowers.test.ts | 2 +- .../test/execution/encode-call.test.ts | 2 +- .../test/execution/swap-step.test.ts | 2 +- .../test/fork/harness.test.ts | 2 +- bots/blue-liquidation/test/fork/harness.ts | 57 +- .../test/fork/liquidation.test.ts | 2 +- bots/blue-liquidation/test/market.test.ts | 2 +- bots/blue-liquidation/test/quotes.test.ts | 2 +- .../test/runner/eligibility.test.ts | 2 +- .../blue-liquidation/test/runner/tick.test.ts | 2 +- bots/blue-liquidation/test/sizing/lif.test.ts | 2 +- .../blue-liquidation/test/sizing/math.test.ts | 2 +- .../blue-liquidation/test/sizing/plan.test.ts | 2 +- .../test/state/lens.sol.test.ts | 4 +- .../test/state/market-params.test.ts | 2 +- bots/blue-liquidation/tsconfig.json | 2 +- bots/blue-liquidation/vitest.config.ts | 23 + bots/market-making/docs/architecture.md | 2 +- bots/market-making/package.json | 40 +- bots/market-making/scripts/build.ts | 30 + .../scripts/bundle-failed.error.ts | 11 + .../market-making/scripts/check-jsdoc.test.ts | 40 +- bots/market-making/scripts/check-jsdoc.ts | 121 +- bots/market-making/src/bootstrap.ts | 2 +- .../src/config/config.service.ts | 2 +- bots/market-making/src/index.ts | 4 +- .../setup-state/http-json.utils.ts | 4 +- .../position-bootstrap.service.test.ts | 100 +- .../offer-invalidation.service.test.ts | 24 +- .../ladder-market-maker.service.test.ts | 6 +- .../market-making-mutation.utils.test.ts | 22 +- .../market-making.service.test.ts | 2 +- .../operator-error-name.utils.test.ts | 2 +- .../setup/setup-check.service.test.ts | 6 +- .../test/application/version.service.test.ts | 2 +- bots/market-making/test/bootstrap.test.ts | 31 +- .../test/config/config-loading.test.ts | 34 +- .../test/config/config-private-key.test.ts | 2 +- .../test/config/config.service.test.ts | 2 +- .../test/config/config.utils.test.ts | 2 +- .../test/config/ladder-config.test.ts | 2 +- .../bootstrap/position-bootstrap.test.ts | 4 +- .../test/domain/ladder/ladder.test.ts | 2 +- bots/market-making/test/e2e/anvil.ts | 26 +- bots/market-making/test/e2e/setup-api.ts | 65 +- .../test/e2e/setup-check.e2e.test.ts | 4 +- .../test/error-convention.test.ts | 19 +- .../bootstrap/bootstrap-make.service.test.ts | 200 +- ...bootstrap-mempool-validation.utils.test.ts | 2 +- .../bootstrap-pending-offer.utils.test.ts | 100 +- .../bootstrap-position.service.test.ts | 2 +- .../bootstrap-reference-rate.service.test.ts | 2 +- .../bootstrap/bootstrap-spread.utils.test.ts | 2 +- .../bootstrap/production-bootstrap.test.ts | 2 +- .../test/infrastructure/cli/cli.test.ts | 150 +- ...et-making-entrypoint-observability.test.ts | 18 +- .../offer-invalidation-group.utils.test.ts | 2 +- ...fer-invalidation-transaction.utils.test.ts | 2 +- .../production-offer-invalidation.test.ts | 2 +- .../ladder-active-publication.utils.test.ts | 2 +- .../ladder-cash-reservation.utils.test.ts | 2 +- .../ladder-group-ownership.utils.test.ts | 2 +- .../ladder/ladder-make.service.test.ts | 4 +- .../ladder/ladder-offer.utils.test.ts | 2 +- .../ladder/ladder-signature.utils.test.ts | 2 +- .../ladder/ladder-spread.utils.test.ts | 2 +- .../ladder/production-ladder.test.ts | 2 +- .../make/managed-maker-account.utils.test.ts | 2 +- .../make/read-only-make.service.test.ts | 10 +- .../viem-setup-state.service.test.ts | 87 +- bots/market-making/tsconfig.json | 4 +- bots/market-making/vitest.config.ts | 23 + bots/midnight-crossed-books/Dockerfile | 22 +- bots/midnight-crossed-books/README.md | 2 +- bots/midnight-crossed-books/package.json | 18 +- bots/midnight-crossed-books/scripts/build.ts | 30 + .../scripts/bundle-failed.error.ts | 11 + .../scripts/deploy-railway.ts | 47 +- bots/midnight-crossed-books/src/bootstrap.ts | 4 +- .../src/config/config.service.ts | 2 +- .../crossed-books-bot.service.test.ts | 18 +- .../test/config/config.service.test.ts | 2 +- .../test/domain/matching.service.test.ts | 2 +- .../infrastructure/morpho-api/service.test.ts | 26 +- .../infrastructure/openapi-result.test.ts | 2 +- .../resolver/resolver.encoder.test.ts | 2 +- .../resolver/resolver.service.test.ts | 10 +- .../infrastructure/router-api/service.test.ts | 12 +- .../test/scripts/railway.test.ts | 2 +- bots/midnight-crossed-books/tsconfig.json | 2 +- bots/midnight-crossed-books/vitest.config.ts | 7 + bots/midnight-liquidation/Dockerfile | 26 +- bots/midnight-liquidation/README.md | 8 +- bots/midnight-liquidation/bunfig.toml | 6 - bots/midnight-liquidation/package.json | 17 +- bots/midnight-liquidation/scripts/build.ts | 55 + .../scripts/bundle-failed.error.ts | 11 + .../scripts/deploy-railway.ts | 63 +- .../scripts/seed-liquidatable-positions.ts | 18 +- bots/midnight-liquidation/soltag.preload.ts | 30 - bots/midnight-liquidation/src/config.ts | 2 +- bots/midnight-liquidation/src/index.ts | 8 +- .../src/state/lens.sol.ts | 3 +- bots/midnight-liquidation/test/config.test.ts | 2 +- .../test/constants.test.ts | 2 +- .../test/contracts/executor.sol.test.ts | 10 +- .../test/discovery/borrowers.test.ts | 4 +- .../test/discovery/markets.test.ts | 2 +- .../test/execution/encode-call.test.ts | 2 +- .../test/execution/swap-step.test.ts | 2 +- .../midnight-liquidation/test/fork/harness.ts | 69 +- .../test/fork/liquidation.test.ts | 4 +- .../test/fork/queue.test.ts | 4 +- bots/midnight-liquidation/test/quotes.test.ts | 2 +- .../test/runner/eligibility.test.ts | 2 +- .../test/runner/tick.test.ts | 2 +- .../test/seed/offers.test.ts | 2 +- .../test/sizing/lif.test.ts | 2 +- .../test/sizing/plan.test.ts | 2 +- .../test/sizing/rcf.test.ts | 2 +- .../test/state/lens.sol.test.ts | 4 +- bots/midnight-liquidation/tsconfig.json | 2 +- bots/midnight-liquidation/vitest.config.ts | 23 + bunfig.toml | 9 - docs/CONVENTIONS.md | 7 +- knip.json | 9 +- package.json | 14 +- packages/bot-kit/package.json | 5 +- packages/bot-kit/test/balance.test.ts | 2 +- packages/bot-kit/test/client.test.ts | 10 +- packages/bot-kit/test/heartbeat.test.ts | 6 +- packages/bot-kit/test/logger.test.ts | 56 +- packages/bot-kit/test/policy.test.ts | 2 +- packages/bot-kit/test/queue/backoff.test.ts | 2 +- packages/bot-kit/test/queue/cooldown.test.ts | 2 +- .../bot-kit/test/queue/fee-policy.test.ts | 2 +- .../bot-kit/test/queue/pending-queue.test.ts | 2 +- packages/bot-kit/test/runner/runner.test.ts | 2 +- packages/bot-kit/test/runner/watcher.test.ts | 2 +- packages/bot-kit/test/signer.test.ts | 8 +- packages/bot-kit/test/simulate.test.ts | 2 +- packages/bot-kit/test/tx-error.test.ts | 2 +- packages/bot-kit/tsconfig.json | 2 +- packages/bot-kit/vitest.config.ts | 7 + packages/contracts/package.json | 12 +- packages/contracts/scripts/build.ts | 29 +- packages/contracts/scripts/codegen.ts | 5 +- packages/contracts/tsconfig.json | 2 +- packages/logging/package.json | 5 +- .../logging/test/cli-logger.utils.test.ts | 4 +- packages/logging/tsconfig.json | 2 +- packages/logging/vitest.config.ts | 7 + packages/monitoring/package.json | 5 +- .../monitoring/test/monitor.utils.test.ts | 2 +- packages/monitoring/tsconfig.json | 2 +- packages/monitoring/vitest.config.ts | 7 + packages/observability/package.json | 5 +- .../test/bot-observability.utils.test.ts | 21 +- .../test/verbose-argv.utils.test.ts | 2 +- packages/observability/tsconfig.json | 2 +- packages/observability/vitest.config.ts | 7 + packages/offers/package.json | 5 +- packages/offers/test/book.utils.test.ts | 2 +- packages/offers/tsconfig.json | 2 +- packages/offers/vitest.config.ts | 7 + packages/swaps/package.json | 5 +- packages/swaps/test/config.test.ts | 2 +- packages/swaps/test/constants.test.ts | 2 +- .../test/execution/executor-calls.test.ts | 2 +- packages/swaps/test/http-client.test.ts | 2 +- packages/swaps/test/quoting.test.ts | 2 +- .../swaps/test/unwrappers/erc4626.test.ts | 6 +- .../swaps/test/unwrappers/pendle-pt.test.ts | 16 +- .../swaps/test/unwrappers/resolve.test.ts | 4 +- packages/swaps/test/venue-selector.test.ts | 2 +- packages/swaps/test/venues/lifi.test.ts | 2 +- packages/swaps/test/venues/liquidswap.test.ts | 2 +- packages/swaps/test/venues/oneinch.test.ts | 2 +- packages/swaps/test/venues/uniswap-v3.test.ts | 2 +- packages/swaps/test/venues/zerox.test.ts | 2 +- packages/swaps/tsconfig.json | 2 +- packages/swaps/vitest.config.ts | 7 + packages/utils/package.json | 5 +- packages/utils/test/helpers/addresses.test.ts | 2 +- packages/utils/test/helpers/bigint.test.ts | 2 +- .../utils/test/helpers/deepFreeze.test.ts | 2 +- packages/utils/test/helpers/delay.test.ts | 2 +- .../helpers/deployless-batch-lens.test.ts | 2 +- packages/utils/test/helpers/errors.test.ts | 2 +- packages/utils/test/helpers/fetch.test.ts | 6 +- .../utils/test/helpers/formatters.test.ts | 2 +- packages/utils/test/helpers/json.test.ts | 2 +- packages/utils/test/helpers/map.test.ts | 2 +- packages/utils/test/helpers/promise.test.ts | 10 +- packages/utils/test/helpers/retry.test.ts | 34 +- packages/utils/test/helpers/schema.test.ts | 2 +- packages/utils/test/helpers/strings.test.ts | 2 +- packages/utils/test/helpers/time.test.ts | 2 +- .../utils/test/helpers/tokenBucket.test.ts | 2 +- packages/utils/test/helpers/tryCatch.test.ts | 2 +- .../utils/test/helpers/tryOrUndefined.test.ts | 2 +- packages/utils/test/helpers/wad.test.ts | 2 +- packages/utils/tsconfig.json | 2 +- packages/utils/vitest.config.ts | 7 + pnpm-lock.yaml | 2031 +++++++++++------ pnpm-workspace.yaml | 23 +- vitest.config.ts | 22 + 224 files changed, 2783 insertions(+), 1961 deletions(-) delete mode 100644 bots/blue-liquidation/bunfig.toml create mode 100644 bots/blue-liquidation/scripts/build.ts create mode 100644 bots/blue-liquidation/scripts/bundle-failed.error.ts delete mode 100644 bots/blue-liquidation/soltag.preload.ts create mode 100644 bots/blue-liquidation/vitest.config.ts create mode 100644 bots/market-making/scripts/build.ts create mode 100644 bots/market-making/scripts/bundle-failed.error.ts create mode 100644 bots/market-making/vitest.config.ts create mode 100644 bots/midnight-crossed-books/scripts/build.ts create mode 100644 bots/midnight-crossed-books/scripts/bundle-failed.error.ts create mode 100644 bots/midnight-crossed-books/vitest.config.ts delete mode 100644 bots/midnight-liquidation/bunfig.toml create mode 100644 bots/midnight-liquidation/scripts/build.ts create mode 100644 bots/midnight-liquidation/scripts/bundle-failed.error.ts delete mode 100644 bots/midnight-liquidation/soltag.preload.ts create mode 100644 bots/midnight-liquidation/vitest.config.ts delete mode 100644 bunfig.toml create mode 100644 packages/bot-kit/vitest.config.ts create mode 100644 packages/logging/vitest.config.ts create mode 100644 packages/monitoring/vitest.config.ts create mode 100644 packages/observability/vitest.config.ts create mode 100644 packages/offers/vitest.config.ts create mode 100644 packages/swaps/vitest.config.ts create mode 100644 packages/utils/vitest.config.ts create mode 100644 vitest.config.ts diff --git a/.claude/agents/reviewer.md b/.claude/agents/reviewer.md index b75a1c45..737bbd7c 100644 --- a/.claude/agents/reviewer.md +++ b/.claude/agents/reviewer.md @@ -43,7 +43,7 @@ hunks. Apply each focus area to the diff: 1. **Conventions compliance** — does the code follow `docs/CONVENTIONS.md`? Check type colocation, - naming, comment discipline, error handling via `tryCatch`, env-var access via `Bun.env.*`, + naming, comment discipline, error handling via `tryCatch`, env-var access via `process.env.*`, function organization, and code complexity. 2. **TypeScript correctness** — strict flags are on (`noImplicitReturns`, `noUncheckedIndexedAccess`). Flag `any`, unsafe casts, missing return types on exported @@ -57,7 +57,7 @@ Apply each focus area to the diff: `suggestion` if a close match could be adapted. If the new code is general-purpose and not tied to a specific bot's domain, suggest hoisting it into the appropriate shared package (`@repo/utils` for pure utilities, `@repo/abis` for ABI-adjacent helpers). -5. **Testing quality** — new behavior has a `{module}.test.ts` under the workspace's `test/` tree mirroring `src/`. Tests use `bun test`. +5. **Testing quality** — new behavior has a `{module}.test.ts` under the workspace's `test/` tree mirroring `src/`. Tests use `vitest`. Tests are non-vacuous (they actually fail if the implementation breaks). No mocking of on-chain behavior where a viem test client / anvil would give real evidence. 6. **Agent infrastructure** — if the diff touches `CLAUDE.md`, `.claude/`, `.mcp.json`, editor diff --git a/.claude/commands/review.md b/.claude/commands/review.md index 8761e668..706b9f3a 100644 --- a/.claude/commands/review.md +++ b/.claude/commands/review.md @@ -58,7 +58,7 @@ Review the provided PR thoroughly and interactively guide the user through each destructured object; omit inferable type annotations; helper-before-main ordering. - **Error Handling**: explicit handling, typed errors, structured logs with bot/operation/inputs context, `tryCatch` from `@repo/utils` for promise throws. - - **Environment Variables**: direct `Bun.env.VARIABLE_NAME` access; fail loudly at startup if a + - **Environment Variables**: direct `process.env.VARIABLE_NAME` access; fail loudly at startup if a required var is missing; never commit secrets. - **Performance Considerations**: batch on-chain reads (`readDeploylessBatchLens` for Lens-shaped data, `multicall` for heterogeneous reads); use explicit block tags for deterministic @@ -116,7 +116,7 @@ Review the provided PR thoroughly and interactively guide the user through each 5. **Use Context7 MCP**: When reviewing implementation details, use the Context7 MCP tools to verify against official documentation for: - viem (contract interactions, encoding, decoding, transports) - - pnpm (workspaces, catalog, lockfile semantics) and bun (test runner, runtime) + - pnpm (workspaces, catalog, lockfile semantics), vitest, esbuild bundles on Node 6. **TIB Consideration**: Check if the PR introduces changes that warrant a Technical Intent Brief (TIB) (see `docs/GUIDANCE.md`). Flag as "Minor" severity if the PR: diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index a94c9966..865908de 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -1,5 +1,5 @@ name: 'Setup' -description: 'Sets up the repository (pnpm + Node + Bun) and builds dist-emitting workspace packages' +description: 'Sets up the repository (pnpm + Node) and builds dist-emitting workspace packages' inputs: install: description: 'Whether to install dependencies' @@ -34,12 +34,6 @@ runs: with: node-version-file: .nvmrc - # bun remains the runtime and the test runner; pnpm only manages dependencies and tasks. - - name: Set up Bun - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 - with: - bun-version: 1.3.12 - - name: Install dependencies if: ${{ inputs.install == 'true' }} shell: bash diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 7434207e..c5915946 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -52,7 +52,7 @@ jobs: run: forge test --root packages/contracts -vv - name: Run unit tests - run: bun test + run: pnpm test env: # Base (chain 8453) archive RPC for the midnight-liquidation fork suite, which forks at a # pinned block and fails loud if this is unset. See test/fork/harness.ts. diff --git a/CLAUDE.md b/CLAUDE.md index baea86c0..3826ff22 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -88,7 +88,7 @@ commit/push): 2. **Lint**: Run `pnpm lint` from the repo root — zero warnings policy. Lint is a workspace-level concern; oxlint walks the whole tree and per-package `lint` scripts are deliberately omitted. 3. **Format**: Run `pnpm format` — auto-fixes formatting in place. -4. **Existing tests**: Run `bun test` — all must pass. +4. **Existing tests**: Run `pnpm test` — all must pass. **Escalation rule**: After 3 failed fix attempts for the same issue, STOP. Tell the user what you tried, what failed, and ask for guidance. Bad work is worse than no work — do not keep iterating @@ -118,7 +118,7 @@ addressing PR feedback, etc.). - During multi-step work, if you're iterating on earlier changes (bug fixes, feedback), existing verification tests catch if the fix broke earlier changes. - Follow existing test conventions: place tests under `test/` mirroring `src/` as - `{module}.test.ts`, use `bun test`, follow patterns from the nearest existing test file. + `{module}.test.ts`, use `vitest`, follow patterns from the nearest existing test file. - If a test file already exists for the module, add to it rather than creating a new one. **When NOT to write tests:** @@ -186,8 +186,9 @@ which supersedes the now-historical [TIB-2026-07-13-bot-architecture](./docs/decisions/TIB-2026-07-13-bot-architecture.md). **Key technologies**: pnpm 11.1.1 (package manager + workspace resolution + version catalog + task -runner; lifecycle scripts are default-denied via `allowBuilds` in `pnpm-workspace.yaml`), bun 1.3.12 -(runtime + test runner), Node.js 24.14.1, TypeScript 6.0, viem for Web3, oxlint + oxfmt for +runner; lifecycle scripts are default-denied via `allowBuilds` in `pnpm-workspace.yaml`), Node.js +24.14.1 (the only runtime — bots ship as esbuild bundles and run `node dist/src/index.js`), vitest as +the test runner, tsx for TypeScript scripts, TypeScript 6.0, viem for Web3, oxlint + oxfmt for lint/format, knip for dead-code detection. **Node.js requirement**: `24.14.1` (see `.nvmrc`). diff --git a/README.md b/README.md index b9d03420..5bd36774 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ pnpm run lint # oxlint, repo-wide pnpm run lint:fix # oxlint with --fix pnpm format # oxfmt, repo-wide pnpm run knip # dead-code detection -bun test # bun's built-in test runner +pnpm test # vitest, every workspace project ``` ## Pointers diff --git a/bots/blue-liquidation/Dockerfile b/bots/blue-liquidation/Dockerfile index 6d969723..c496f9de 100644 --- a/bots/blue-liquidation/Dockerfile +++ b/bots/blue-liquidation/Dockerfile @@ -1,15 +1,9 @@ # syntax=docker/dockerfile:1 -# pnpm-workspace image for the blue-liquidation bot. The build context MUST be the repo root so the -# workspace packages (packages/*) resolve — docker-compose.yml sets `context: ../..` and the Railway -# deploy runs `railway up` from the repo root. Borrower discovery polls the Morpho GraphQL API, so -# there is no indexer/database sidecar to build. -# -# pnpm owns installs, but bun is still the runtime, so this image carries both. The base is Node -# because pnpm is activated through corepack, which the oven/bun images do not provide; bun comes in -# as a single copied binary. Both pnpm and bun must stay on PATH in the FINAL image: `start` fires -# `prestart`, which shells out to `pnpm -r run build` to compile @repo/contracts' gitignored dist/. +# Image for the blue-liquidation bot. The build context MUST be the repo root so the workspace packages +# (packages/*) resolve — docker-compose.yml sets `context: ../..` and the Railway deploy runs +# `railway up` from the repo root. Borrower discovery polls the Morpho GraphQL API, so there is no +# indexer/database sidecar to build. Node only: pnpm installs, esbuild bundles, node runs. FROM node:24.14.1-slim -COPY --from=oven/bun:1.3.12-slim /usr/local/bin/bun /usr/local/bin/bun ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 # The container's environment holds a funded liquidator EOA key, so nothing here may run as root — @@ -22,8 +16,8 @@ WORKDIR /repo USER node # Manifests first, so the install layer is cached. `corepack install` pre-fetches the pnpm version -# pinned in package.json#packageManager, so no download happens at container start. -COPY --chown=node:node package.json pnpm-workspace.yaml pnpm-lock.yaml bunfig.toml ./ +# pinned in package.json#packageManager. +COPY --chown=node:node package.json pnpm-workspace.yaml pnpm-lock.yaml ./ RUN corepack install # All members' package.json are needed for pnpm to resolve the `workspace:*` links. @@ -31,7 +25,9 @@ COPY --chown=node:node packages ./packages COPY --chown=node:node bots ./bots RUN pnpm install --frozen-lockfile -# Run the bot from its package dir (`start` builds workspaces via `prestart`, then runs src/index.ts; -# the bot's own bunfig.toml preload compiles its soltag `sol``` lens templates at startup). +# Bundle at image-build time: workspace dists plus this bot's esbuild bundle (soltag `sol``` templates +# compiled in where present), so the container starts a plain `node` with no runtime transform. +RUN pnpm -r --if-present run build + WORKDIR /repo/bots/blue-liquidation -CMD ["bun", "run", "start"] +CMD ["node", "dist/src/index.js"] diff --git a/bots/blue-liquidation/bunfig.toml b/bots/blue-liquidation/bunfig.toml deleted file mode 100644 index 2a03c377..00000000 --- a/bots/blue-liquidation/bunfig.toml +++ /dev/null @@ -1,6 +0,0 @@ -# Compile soltag `sol``` lens templates at runtime (`bun run start` in the Docker image) and for -# `bun test` when run from this directory. See soltag.preload.ts for why the plugin is hand-rolled. -preload = ["./soltag.preload.ts"] - -[test] -preload = ["./soltag.preload.ts"] diff --git a/bots/blue-liquidation/package.json b/bots/blue-liquidation/package.json index a13fe1b1..c33bb8e0 100644 --- a/bots/blue-liquidation/package.json +++ b/bots/blue-liquidation/package.json @@ -5,10 +5,10 @@ "license": "Apache-2.0", "type": "module", "scripts": { - "deploy:railway": "bun run scripts/deploy-railway.ts", - "probe:lens": "bun run scripts/probe-live-lens.ts", - "prestart": "pnpm -r --parallel --if-present run build", - "start": "bun src/index.ts", + "build": "tsx scripts/build.ts", + "deploy:railway": "tsx scripts/deploy-railway.ts", + "probe:lens": "node --env-file-if-exists=.env dist/scripts/probe-live-lens.js", + "start": "node --env-file-if-exists=.env dist/src/index.js", "typecheck": "soltag && tsc --noEmit" }, "dependencies": { @@ -23,7 +23,12 @@ }, "devDependencies": { "@repo/typescript-config": "workspace:*", - "@types/bun": "catalog:", - "typescript": "catalog:" + "@types/node": "catalog:", + "esbuild": "catalog:", + "execa": "catalog:", + "tsx": "catalog:", + "typescript": "catalog:", + "vite": "catalog:", + "vitest": "catalog:" } } diff --git a/bots/blue-liquidation/scripts/build.ts b/bots/blue-liquidation/scripts/build.ts new file mode 100644 index 00000000..d78794d2 --- /dev/null +++ b/bots/blue-liquidation/scripts/build.ts @@ -0,0 +1,55 @@ +import type { Plugin } from 'esbuild' + +import { build as esbuild } from 'esbuild' +import { rmSync } from 'node:fs' +import { readFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { transformSolTemplates } from 'soltag/unplugin' + +import { BundleFailedError } from './bundle-failed.error' + +// Bundles the bot entrypoint (and the soltag-dependent operator script) to `dist/` with the +// `sol``` templates compiled to literal ABIs/bytecode, so production runs a plain `node` with no +// runtime transform. This replaces the bunfig `preload` that used to compile them at startup. + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..') +const DIST_DIR = join(ROOT, 'dist') +rmSync(DIST_DIR, { recursive: true, force: true }) + +const ESCAPED = ROOT.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +// This bot's own TS sources only — bundled workspace deps ship prebuilt dist and hold no templates. +const INCLUDE = new RegExp(`^${ESCAPED}/(?:src|scripts)/.*\\.tsx?$`) + +const soltagPlugin: Plugin = { + name: 'soltag', + setup(build) { + build.onLoad({ filter: INCLUDE }, async ({ path }) => { + const source = await readFile(path, 'utf8') + // Enable the optimizer — the lens's per-element computation has enough locals to hit + // "stack too deep" without it. + const transformed = transformSolTemplates(source, path, { + solc: { optimizer: { enabled: true, runs: 200 } } + }) + return { contents: transformed?.code ?? source, loader: 'ts' as const } + }) + } +} + +try { + await esbuild({ + entryPoints: [join(ROOT, 'src/index.ts'), join(ROOT, 'scripts/probe-live-lens.ts')], + outdir: DIST_DIR, + outbase: ROOT, + bundle: true, + platform: 'node', + format: 'esm', + // CJS deps reaching for require() inside an ESM bundle need a real require. + banner: { + js: "import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);" + }, + plugins: [soltagPlugin] + }) +} catch (error) { + throw new BundleFailedError(error instanceof Error ? error.message : String(error)) +} diff --git a/bots/blue-liquidation/scripts/bundle-failed.error.ts b/bots/blue-liquidation/scripts/bundle-failed.error.ts new file mode 100644 index 00000000..85b07b64 --- /dev/null +++ b/bots/blue-liquidation/scripts/bundle-failed.error.ts @@ -0,0 +1,11 @@ +/** Signals that the production bundle could not be produced. */ +export class BundleFailedError extends Error { + /** + * Creates a tooling failure from the bundler's own message, without retaining source contents. + * @param detail - Bundler-reported reason for the failure. + */ + constructor(readonly detail: string) { + super(`Bundle failed: ${detail}`) + this.name = 'BundleFailedError' + } +} diff --git a/bots/blue-liquidation/scripts/deploy-railway.ts b/bots/blue-liquidation/scripts/deploy-railway.ts index ea488e64..a67a9cab 100644 --- a/bots/blue-liquidation/scripts/deploy-railway.ts +++ b/bots/blue-liquidation/scripts/deploy-railway.ts @@ -36,16 +36,17 @@ * the variable key, never its value; variable values are never logged. */ import { delay, tryCatch } from '@repo/utils' -import { $ } from 'bun' -import { resolve } from 'node:path' +import { $ } from 'execa' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' // The target project is env-driven so no project identifier is baked into this (open-source) file. // RAILWAY_PROJECT_ID is required; RAILWAY_ENVIRONMENT defaults to the conventional `production`. -const PROJECT_ID = required(Bun.env, 'RAILWAY_PROJECT_ID') -const ENVIRONMENT = Bun.env.RAILWAY_ENVIRONMENT?.trim() || 'production' +const PROJECT_ID = required(process.env, 'RAILWAY_PROJECT_ID') +const ENVIRONMENT = process.env.RAILWAY_ENVIRONMENT?.trim() || 'production' const DOCKERFILE_PATH = 'bots/blue-liquidation/Dockerfile' // Repo root is three levels up from this file (scripts → blue-liquidation → bots → repo root). -const REPO_ROOT = resolve(import.meta.dir, '..', '..', '..') +const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..') type Env = Record type RailwayService = { id: string; name: string } @@ -75,8 +76,8 @@ function str(value: unknown): string { } // Surface a failed `railway` command's stderr so failures are actionable (the CLI writes the real -// reason — plan limits, auth, selection prompts — to stderr). Bun's ShellError carries `.stderr` as -// bytes; fall back to the generic message. Safe for non-secret commands; never used on setSecret. +// reason — plan limits, auth, selection prompts — to stderr). execa's error carries `.stderr` as a +// string; fall back to the generic message. Safe for non-secret commands; never used on setSecret. function stderrOf(error: unknown): string { if (isRecord(error) && 'stderr' in error) { const s = (error as { stderr: unknown }).stderr @@ -120,7 +121,7 @@ function parseLatestStatus(raw: string): string { } async function assertCli(): Promise { - const { error } = await tryCatch(Promise.resolve($`railway --version`.quiet())) + const { error } = await tryCatch($`railway --version`) if (error) throw new Error('Railway CLI not found. Install it: https://docs.railway.com/guides/cli') } @@ -128,13 +129,11 @@ async function assertCli(): Promise { // `railway add` has no --project/--environment flag, so it acts on the linked context. A project // token scopes every command implicitly; otherwise we link the project id once for this run. async function ensureContext(): Promise { - if (Bun.env.RAILWAY_TOKEN) { + if (process.env.RAILWAY_TOKEN) { console.log('Using RAILWAY_TOKEN for project context.') return } - const { error } = await tryCatch( - Promise.resolve($`railway link -p ${PROJECT_ID} -e ${ENVIRONMENT}`.quiet()) - ) + const { error } = await tryCatch($`railway link -p ${PROJECT_ID} -e ${ENVIRONMENT}`) if (error) { throw new Error( `Failed to link ${PROJECT_ID} (${ENVIRONMENT}). Set RAILWAY_TOKEN or run \`railway login\`.` @@ -144,9 +143,7 @@ async function ensureContext(): Promise { } async function listServices(): Promise { - const { data, error } = await tryCatch( - Promise.resolve($`railway service list --json`.quiet().text()) - ) + const { data, error } = await tryCatch($`railway service list --json`.then(r => r.stdout)) return error || typeof data !== 'string' ? [] : parseServices(data) } @@ -156,7 +153,7 @@ async function ensureService(name: string): Promise { return } console.log(`Creating service ${name}…`) - const { error } = await tryCatch(Promise.resolve($`railway add --service ${name} --json`.quiet())) + const { error } = await tryCatch($`railway add --service ${name} --json`) if (error) throw new Error(`Failed to create service ${name}: ${stderrOf(error)}`) } @@ -168,7 +165,7 @@ async function removeService(name: string): Promise { console.log(`Removing legacy service ${name}…`) const { error } = await tryCatch( Promise.resolve( - $`railway service delete --service ${name} --environment ${ENVIRONMENT} --yes --json`.quiet() + $`railway service delete --service ${name} --environment ${ENVIRONMENT} --yes --json` ) ) if (error) @@ -182,9 +179,7 @@ async function removeService(name: string): Promise { // Non-secret variable. `kv` is a single "KEY=VALUE" arg; only the key is logged. async function setVar(service: string, kv: string): Promise { const key = kv.split('=')[0] - const { error } = await tryCatch( - Promise.resolve($`railway variable set ${kv} -s ${service} --skip-deploys`.quiet()) - ) + const { error } = await tryCatch($`railway variable set ${kv} -s ${service} --skip-deploys`) if (error) throw new Error(`Failed to set ${key} on ${service}: ${stderrOf(error)}`) console.log(`Set ${key} on ${service}.`) } @@ -194,9 +189,9 @@ async function setVar(service: string, kv: string): Promise { async function listVarKeys(service: string): Promise> { const { data, error } = await tryCatch( Promise.resolve( - $`railway variable list -s ${service} -e ${ENVIRONMENT} -p ${PROJECT_ID} --json` - .quiet() - .text() + $`railway variable list -s ${service} -e ${ENVIRONMENT} -p ${PROJECT_ID} --json`.then( + r => r.stdout + ) ) ) if (error || typeof data !== 'string') return new Set() @@ -207,9 +202,7 @@ async function listVarKeys(service: string): Promise> { // Delete a variable (used to clear a stale venue secret). Fatal on failure: a stale key that // survives the delete silently keeps its venue enabled, which is exactly the drift this prevents. async function deleteVar(service: string, key: string): Promise { - const { error } = await tryCatch( - Promise.resolve($`railway variable delete ${key} -s ${service} --skip-deploys`.quiet()) - ) + const { error } = await tryCatch($`railway variable delete ${key} -s ${service} --skip-deploys`) if (error) throw new Error(`Failed to delete ${key} on ${service}: ${stderrOf(error)}`) console.log(`Deleted ${key} on ${service} (stale).`) } @@ -218,7 +211,7 @@ async function deleteVar(service: string, key: string): Promise { async function setSecret(service: string, key: string, value: string): Promise { const { error } = await tryCatch( Promise.resolve( - $`railway variable set ${key} --stdin -s ${service} --skip-deploys < ${Buffer.from(value, 'utf8')}`.quiet() + $({ input: value })`railway variable set ${key} --stdin -s ${service} --skip-deploys` ) ) if (error) throw new Error(`Failed to set ${key} on ${service}`) @@ -231,7 +224,7 @@ async function deployService(service: string): Promise { // subprocess, so `railway up` otherwise errors "No environment specified". Self-contained > ambient. const { error } = await tryCatch( Promise.resolve( - $`railway up -s ${service} -e ${ENVIRONMENT} -p ${PROJECT_ID} -d`.cwd(REPO_ROOT).quiet() + $({ cwd: REPO_ROOT })`railway up -s ${service} -e ${ENVIRONMENT} -p ${PROJECT_ID} -d` ) ) if (error) throw new Error(`Failed to start deploy for ${service}: ${stderrOf(error)}`) @@ -253,7 +246,7 @@ async function latestStatus(service: string): Promise { '1', '--json' ] - const { data, error } = await tryCatch(Promise.resolve($`${args}`.quiet().text())) + const { data, error } = await tryCatch($(args[0] ?? 'railway', args.slice(1)).then(r => r.stdout)) return error || typeof data !== 'string' ? 'UNKNOWN' : parseLatestStatus(data) } @@ -295,7 +288,7 @@ const LEGACY_BOT_SERVICE = 'bot' // GitHub Environment holds only RAILWAY_TOKEN + RAILWAY_PROJECT_ID, and the services/secrets were // provisioned once by a full (secret-bearing) run of this script. Skips the RPC/key requirements the // full path enforces, so it never needs those secrets in CI. Runs before `chainSecrets` is read. -if (/^(1|true)$/i.test(Bun.env.DEPLOY_ONLY?.trim() ?? '')) { +if (/^(1|true)$/i.test(process.env.DEPLOY_ONLY?.trim() ?? '')) { await ensureContext() const services = CHAINS.map(chain => chain.service) for (const service of services) await deployService(service) @@ -310,7 +303,7 @@ if (/^(1|true)$/i.test(Bun.env.DEPLOY_ONLY?.trim() ?? '')) { // Read a chainId-suffixed env var (e.g. RPC_URL_8453). RPC endpoints differ per chain so these are // effectively required per chain; the private key may instead fall back to a shared unsuffixed key. function suffixed(name: string, chainId: number): string | undefined { - return Bun.env[`${name}_${chainId}`]?.trim() || undefined + return process.env[`${name}_${chainId}`]?.trim() || undefined } function requiredSuffixed(name: string, chainId: number): string { const value = suffixed(name, chainId) @@ -319,7 +312,7 @@ function requiredSuffixed(name: string, chainId: number): string { } // A chainId-suffixed boolean flag with an unsuffixed fallback (e.g. ALLOW_DETECTION_ONLY_4663). function suffixedFlag(name: string, chainId: number): boolean { - const raw = suffixed(name, chainId) ?? Bun.env[name]?.trim() + const raw = suffixed(name, chainId) ?? process.env[name]?.trim() return /^(1|true)$/i.test(raw ?? '') } @@ -331,17 +324,17 @@ const chainSecrets = CHAINS.map(chain => { const rpcUrl = requiredSuffixed('RPC_URL', chain.chainId) // A single funded key may be reused across chains (unsuffixed fallback), or set one per chain. const liquidatorPrivateKey = - suffixed('LIQUIDATOR_PRIVATE_KEY', chain.chainId) ?? Bun.env.LIQUIDATOR_PRIVATE_KEY?.trim() + suffixed('LIQUIDATOR_PRIVATE_KEY', chain.chainId) ?? process.env.LIQUIDATOR_PRIVATE_KEY?.trim() if (!liquidatorPrivateKey) throw new Error( `Missing required env var: LIQUIDATOR_PRIVATE_KEY_${chain.chainId} (or a shared LIQUIDATOR_PRIVATE_KEY)` ) assertPrivateKey(liquidatorPrivateKey) // Venue keys enable their venue on this chain (no per-collateral routing file anymore). - const zeroxApiKey = suffixed('ZEROX_API_KEY', chain.chainId) ?? Bun.env.ZEROX_API_KEY?.trim() + const zeroxApiKey = suffixed('ZEROX_API_KEY', chain.chainId) ?? process.env.ZEROX_API_KEY?.trim() const oneInchApiKey = - suffixed('ONEINCH_API_KEY', chain.chainId) ?? Bun.env.ONEINCH_API_KEY?.trim() - const lifiApiKey = suffixed('LIFI_API_KEY', chain.chainId) ?? Bun.env.LIFI_API_KEY?.trim() + suffixed('ONEINCH_API_KEY', chain.chainId) ?? process.env.ONEINCH_API_KEY?.trim() + const lifiApiKey = suffixed('LIFI_API_KEY', chain.chainId) ?? process.env.LIFI_API_KEY?.trim() const enableLifi = suffixedFlag('ENABLE_LIFI', chain.chainId) || Boolean(lifiApiKey) const allowDetectionOnly = suffixedFlag('ALLOW_DETECTION_ONLY', chain.chainId) const hasVenue = Boolean(zeroxApiKey || oneInchApiKey || enableLifi) @@ -371,8 +364,8 @@ await ensureContext() // Optional BetterStack log shipping, one source for blue-liq shared across its chains (told apart by // the bot/chainId fields the logger stamps). Host is a plain var; token is a secret. Off when unset — // the bot's in-process loglayer transport stays inert, so the container behaves exactly as before. -const betterstackHost = Bun.env.BETTERSTACK_INGESTING_HOST?.trim() -const betterstackToken = Bun.env.BETTERSTACK_SOURCE_TOKEN?.trim() +const betterstackHost = process.env.BETTERSTACK_INGESTING_HOST?.trim() +const betterstackToken = process.env.BETTERSTACK_SOURCE_TOKEN?.trim() // --- bot-: one liquidation runner per chain. The in-container var names stay RPC_URL / // LIQUIDATOR_PRIVATE_KEY (the chainId suffix is only an operator-side convention). The whole venue diff --git a/bots/blue-liquidation/scripts/probe-live-lens.ts b/bots/blue-liquidation/scripts/probe-live-lens.ts index aecc8142..b358b0a7 100644 --- a/bots/blue-liquidation/scripts/probe-live-lens.ts +++ b/bots/blue-liquidation/scripts/probe-live-lens.ts @@ -34,7 +34,7 @@ const LOGS_MAX_CHUNKS = 12 const TARGET_PAIRS = 256 function required(name: string): string { - const value = Bun.env[name] + const value = process.env[name] if (!value?.trim()) throw new Error(`Missing required env var: ${name}`) return value.trim() } diff --git a/bots/blue-liquidation/soltag.preload.ts b/bots/blue-liquidation/soltag.preload.ts deleted file mode 100644 index cd982b58..00000000 --- a/bots/blue-liquidation/soltag.preload.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { transformSolTemplates } from 'soltag/unplugin' - -// soltag is a build-time-only transform: its `sol``` tagged templates throw at runtime unless a -// bundler/loader plugin compiles them (via solc). unplugin's own bun adapter (`unplugin.bun()`) is -// unusable under bun — it returns `undefined` from `onLoad` for files it doesn't transform, which -// bun rejects ("Expected module mock to return an object"). So we drive soltag's exported core -// transform from a bun loader plugin: it matches every TS source under this bot (where soltag is a -// dependency — other workspaces don't use it), and returns files unchanged when they hold no -// template to compile. Wired via bunfig `preload` for both `bun test` and `bun run`. - -const BOT_DIR = import.meta.dir -const ESCAPED = BOT_DIR.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') -// Matches `/**/*.{ts,tsx,mts,cts}`, excluding any nested node_modules. -const INCLUDE = new RegExp(`^${ESCAPED}/(?:(?!/node_modules/).)*\\.(?:m|c)?tsx?$`) - -Bun.plugin({ - name: 'soltag', - setup(build) { - build.onLoad({ filter: INCLUDE }, async ({ path }) => { - const source = await Bun.file(path).text() - const loader = path.endsWith('.tsx') ? 'tsx' : 'ts' - // Enable the optimizer — the lens's per-element computation has enough locals to hit - // "stack too deep" without it. - const transformed = transformSolTemplates(source, path, { - solc: { optimizer: { enabled: true, runs: 200 } } - }) - return { contents: transformed?.code ?? source, loader } - }) - } -}) diff --git a/bots/blue-liquidation/src/config.ts b/bots/blue-liquidation/src/config.ts index d35b4239..3a93d6fa 100644 --- a/bots/blue-liquidation/src/config.ts +++ b/bots/blue-liquidation/src/config.ts @@ -282,7 +282,7 @@ function addressListEnv(env: Env, name: string): Address[] { * singleton hold code) are performed in `index.ts` once a public client exists. */ export function loadConfig( - env: Env = Bun.env, + env: Env = process.env, deps: { chainMap?: Record } = {} ): Config { const chainMap = deps.chainMap ?? CHAIN_MAP diff --git a/bots/blue-liquidation/src/index.ts b/bots/blue-liquidation/src/index.ts index c4f98f24..422ece56 100644 --- a/bots/blue-liquidation/src/index.ts +++ b/bots/blue-liquidation/src/index.ts @@ -94,9 +94,9 @@ async function main() { // the no-key → detection-only opt-in). Keys are read HERE, at the point of use, and live only in // this closure — never on the (logged) Config object. const apiKeys: Partial> = {} - if (Bun.env.ZEROX_API_KEY) apiKeys['0x'] = Bun.env.ZEROX_API_KEY - if (Bun.env.ONEINCH_API_KEY) apiKeys['1inch'] = Bun.env.ONEINCH_API_KEY - if (Bun.env.LIFI_API_KEY) apiKeys.lifi = Bun.env.LIFI_API_KEY + if (process.env.ZEROX_API_KEY) apiKeys['0x'] = process.env.ZEROX_API_KEY + if (process.env.ONEINCH_API_KEY) apiKeys['1inch'] = process.env.ONEINCH_API_KEY + if (process.env.LIFI_API_KEY) apiKeys.lifi = process.env.LIFI_API_KEY const venues = config.venues.enabled if (venues.length === 0) { logger.warn('quoting.no_routes', { @@ -250,7 +250,7 @@ async function main() { logger }) const heartbeatMonitor = createHeartbeatMonitor({ - url: Bun.env.BETTERSTACK_HEARTBEAT_URL, + url: process.env.BETTERSTACK_HEARTBEAT_URL, logger }) void heartbeatMonitor.start() diff --git a/bots/blue-liquidation/test/config.test.ts b/bots/blue-liquidation/test/config.test.ts index 65300a29..9b9a814b 100644 --- a/bots/blue-liquidation/test/config.test.ts +++ b/bots/blue-liquidation/test/config.test.ts @@ -1,6 +1,6 @@ -import { describe, expect, it } from 'bun:test' import { getAddress } from 'viem' import { base } from 'viem/chains' +import { describe, expect, it } from 'vitest' import type { ChainConfig, Config, QuotingConfig } from '../src/config' diff --git a/bots/blue-liquidation/test/discovery/borrowers.test.ts b/bots/blue-liquidation/test/discovery/borrowers.test.ts index cd63696c..60380169 100644 --- a/bots/blue-liquidation/test/discovery/borrowers.test.ts +++ b/bots/blue-liquidation/test/discovery/borrowers.test.ts @@ -1,8 +1,8 @@ import type { Logger } from '@repo/bot-kit' import type { Address, Hex } from 'viem' -import { describe, expect, it } from 'bun:test' import { getAddress } from 'viem' +import { describe, expect, it } from 'vitest' import type { FetchPositionPage } from '../../src/discovery/borrowers' import type { MarketParams } from '../../src/market' diff --git a/bots/blue-liquidation/test/execution/encode-call.test.ts b/bots/blue-liquidation/test/execution/encode-call.test.ts index 27397dc3..e3f5f5e5 100644 --- a/bots/blue-liquidation/test/execution/encode-call.test.ts +++ b/bots/blue-liquidation/test/execution/encode-call.test.ts @@ -1,8 +1,8 @@ import type { SwapPlan, SwapStep } from '@repo/swaps' import type { Hex } from 'viem' -import { describe, expect, it } from 'bun:test' import { decodeAbiParameters, decodeFunctionData, getAddress } from 'viem' +import { describe, expect, it } from 'vitest' import type { MarketParams } from '../../src/market' diff --git a/bots/blue-liquidation/test/execution/swap-step.test.ts b/bots/blue-liquidation/test/execution/swap-step.test.ts index 2d10e30f..72578565 100644 --- a/bots/blue-liquidation/test/execution/swap-step.test.ts +++ b/bots/blue-liquidation/test/execution/swap-step.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import type { MarketParams } from '../../src/market' import type { LensOut } from '../../src/state/lens.sol' diff --git a/bots/blue-liquidation/test/fork/harness.test.ts b/bots/blue-liquidation/test/fork/harness.test.ts index c134d6f2..9a7a48e0 100644 --- a/bots/blue-liquidation/test/fork/harness.test.ts +++ b/bots/blue-liquidation/test/fork/harness.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'bun:test' import { getAddress } from 'viem' +import { describe, expect, it } from 'vitest' import { loadForkFixtureFromEnv } from './harness' diff --git a/bots/blue-liquidation/test/fork/harness.ts b/bots/blue-liquidation/test/fork/harness.ts index 28a44a17..3f7eea8c 100644 --- a/bots/blue-liquidation/test/fork/harness.ts +++ b/bots/blue-liquidation/test/fork/harness.ts @@ -1,8 +1,10 @@ -import type { Subprocess } from 'bun' +import type { ChildProcess } from 'node:child_process' import type { Address } from 'viem' import { Executor } from '@repo/contracts' import { ensureError } from '@repo/utils' +import { spawn } from 'node:child_process' +import { setTimeout as sleep } from 'node:timers/promises' import { createTestClient, createWalletClient, @@ -119,7 +121,22 @@ const DEPLOYER_KEY = '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b export const LIQUIDATOR = privateKeyToAccount(LIQUIDATOR_KEY).address export type TestClient = ReturnType -export type ForkHandle = Subprocess +export type ForkHandle = { + kill: (signal: NodeJS.Signals) => void + /** Resolves with the exit code once the child has actually gone (Node has no `.exited`). */ + exited: Promise +} + +// Wraps a ChildProcess in the tiny surface the teardown needs. The `exited` promise is built here, +// at spawn time, so its `exit` listener is attached before the child can possibly exit. +const toForkHandle = (child: ChildProcess): ForkHandle => ({ + kill: signal => { + child.kill(signal) + }, + exited: new Promise(resolve => { + child.once('exit', code => resolve(code)) + }) +}) /** Polls the JSON-RPC endpoint until it answers `eth_blockNumber`, so callers see a ready node. */ async function waitForRpc(url: string, timeoutMs = 30_000): Promise { @@ -139,15 +156,21 @@ async function waitForRpc(url: string, timeoutMs = 30_000): Promise { } catch (error) { lastError = ensureError(error) } - await Bun.sleep(100) + await sleep(100) } const detail = lastError ? `: ${lastError.message}` : '' throw new Error(`anvil RPC at ${url} not ready within ${timeoutMs}ms${detail}`) } +// Anvil port registry — vitest runs test FILES IN PARALLEL (bun's runner was serial, so fixed ports +// used to be safe within a bot only). Every fork suite in the repo must claim a distinct port: +// 8545 bots/blue-liquidation fork/liquidation +// 8546 bots/market-making e2e/setup-check +// 8547 bots/midnight-liquidation fork/liquidation +// 8548 bots/midnight-liquidation fork/queue /** * Boots an anvil instance forking Base at `forkBlock` (chain id pinned to Base so signatures match). - * `port` is explicit so multiple fork test files can run in one `bun test` process without colliding. + * `port` is explicit so parallel fork test files never collide — see the registry above. * Requires the `anvil` binary on PATH (Foundry locally; foundry-toolchain in CI) and `FORK_URL`. */ export async function startFork( @@ -155,19 +178,21 @@ export async function startFork( port = 8545 ): Promise<{ anvil: ForkHandle; rpcUrl: string }> { if (!FORK_URL) throw new Error('RPC_URL_8453 is required to start the fork') - const anvil = Bun.spawn( - [ + const anvil = toForkHandle( + spawn( 'anvil', - '--fork-url', - FORK_URL, - '--fork-block-number', - String(forkBlock), - '--chain-id', - String(base.id), - '--port', - String(port) - ], - { stdout: 'ignore', stderr: 'ignore' } + [ + '--fork-url', + FORK_URL, + '--fork-block-number', + String(forkBlock), + '--chain-id', + String(base.id), + '--port', + String(port) + ], + { stdio: 'ignore' } + ) ) const rpcUrl = `http://127.0.0.1:${port}` await waitForRpc(rpcUrl) diff --git a/bots/blue-liquidation/test/fork/liquidation.test.ts b/bots/blue-liquidation/test/fork/liquidation.test.ts index 7dfa3e81..da0e3adc 100644 --- a/bots/blue-liquidation/test/fork/liquidation.test.ts +++ b/bots/blue-liquidation/test/fork/liquidation.test.ts @@ -10,9 +10,9 @@ import { } from '@repo/bot-kit' import { quoteUniswapV3 } from '@repo/swaps' import { lensKey } from '@repo/utils' -import { afterAll, beforeAll, describe, expect, it } from 'bun:test' import { erc20Abi, parseGwei } from 'viem' import { base } from 'viem/chains' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' import type { ForkFixture, ForkHandle, TestClient } from './harness' diff --git a/bots/blue-liquidation/test/market.test.ts b/bots/blue-liquidation/test/market.test.ts index 685bf4fb..d5fe860f 100644 --- a/bots/blue-liquidation/test/market.test.ts +++ b/bots/blue-liquidation/test/market.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'bun:test' import { encodeAbiParameters, keccak256 } from 'viem' +import { describe, expect, it } from 'vitest' import type { MarketParams } from '../src/market' diff --git a/bots/blue-liquidation/test/quotes.test.ts b/bots/blue-liquidation/test/quotes.test.ts index 1d282f49..511fb497 100644 --- a/bots/blue-liquidation/test/quotes.test.ts +++ b/bots/blue-liquidation/test/quotes.test.ts @@ -1,8 +1,8 @@ import type { Logger } from '@repo/bot-kit' import type { RateLimitedClient, VenuePair, VenueQuoteEstimate, VenueSelector } from '@repo/swaps' -import { describe, expect, it } from 'bun:test' import { getAddress } from 'viem' +import { describe, expect, it } from 'vitest' import type { MarketParams } from '../src/market' import type { LiquidationPlan } from '../src/sizing/plan' diff --git a/bots/blue-liquidation/test/runner/eligibility.test.ts b/bots/blue-liquidation/test/runner/eligibility.test.ts index b394ad2c..4d1dccd1 100644 --- a/bots/blue-liquidation/test/runner/eligibility.test.ts +++ b/bots/blue-liquidation/test/runner/eligibility.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import type { MarketParams } from '../../src/market' import type { LensOut } from '../../src/state/lens.sol' diff --git a/bots/blue-liquidation/test/runner/tick.test.ts b/bots/blue-liquidation/test/runner/tick.test.ts index ae936ff8..b2f236e5 100644 --- a/bots/blue-liquidation/test/runner/tick.test.ts +++ b/bots/blue-liquidation/test/runner/tick.test.ts @@ -5,8 +5,8 @@ import type { Address } from 'viem' import { createBackoff, createCooldownStore } from '@repo/bot-kit' import { lensKey } from '@repo/utils' -import { describe, expect, it } from 'bun:test' import { getAddress } from 'viem' +import { describe, expect, it } from 'vitest' import type { BorrowerCandidate } from '../../src/discovery/borrowers' import type { MarketParams } from '../../src/market' diff --git a/bots/blue-liquidation/test/sizing/lif.test.ts b/bots/blue-liquidation/test/sizing/lif.test.ts index a652342e..c3488bba 100644 --- a/bots/blue-liquidation/test/sizing/lif.test.ts +++ b/bots/blue-liquidation/test/sizing/lif.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import { LIQUIDATION_CURSOR, MAX_LIQUIDATION_INCENTIVE_FACTOR, WAD } from '../../src/constants' import { lifFromLltv } from '../../src/sizing/lif' diff --git a/bots/blue-liquidation/test/sizing/math.test.ts b/bots/blue-liquidation/test/sizing/math.test.ts index 4c002889..fba9c652 100644 --- a/bots/blue-liquidation/test/sizing/math.test.ts +++ b/bots/blue-liquidation/test/sizing/math.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import { VIRTUAL_ASSETS, VIRTUAL_SHARES, WAD } from '../../src/constants' import { diff --git a/bots/blue-liquidation/test/sizing/plan.test.ts b/bots/blue-liquidation/test/sizing/plan.test.ts index a14b1669..50d797a4 100644 --- a/bots/blue-liquidation/test/sizing/plan.test.ts +++ b/bots/blue-liquidation/test/sizing/plan.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import type { PlanInput } from '../../src/sizing/plan' diff --git a/bots/blue-liquidation/test/state/lens.sol.test.ts b/bots/blue-liquidation/test/state/lens.sol.test.ts index 8a23c595..32b8cc6f 100644 --- a/bots/blue-liquidation/test/state/lens.sol.test.ts +++ b/bots/blue-liquidation/test/state/lens.sol.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'bun:test' import { decodeFunctionResult, encodeFunctionResult, getAddress } from 'viem' +import { describe, expect, it } from 'vitest' import { BlueLiquidationLens } from '../../src/state/lens.sol' @@ -7,7 +7,7 @@ const MORPHO = getAddress('0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb') describe('BlueLiquidationLens', () => { it('compiles via soltag and binds the Morpho address into the factory call', () => { - // Proves the soltag bun preload compiled the inline Solidity (sol``` would otherwise throw) + // Proves the soltag/vite transform compiled the inline Solidity (sol``` would otherwise throw) // and that constructor binding produced a deployless factory call. const compiled = BlueLiquidationLens.with(MORPHO) expect(compiled.factoryData.length).toBeGreaterThan(2) diff --git a/bots/blue-liquidation/test/state/market-params.test.ts b/bots/blue-liquidation/test/state/market-params.test.ts index 3e45a00e..589b2f40 100644 --- a/bots/blue-liquidation/test/state/market-params.test.ts +++ b/bots/blue-liquidation/test/state/market-params.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'bun:test' import { getAddress, type Hex } from 'viem' +import { describe, expect, it } from 'vitest' import type { MarketParams } from '../../src/market' import type { MarketParamsResolver } from '../../src/state/market-params' diff --git a/bots/blue-liquidation/tsconfig.json b/bots/blue-liquidation/tsconfig.json index 7ad64bb1..dccc6c59 100644 --- a/bots/blue-liquidation/tsconfig.json +++ b/bots/blue-liquidation/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@repo/typescript-config/base", "compilerOptions": { - "types": ["bun"], + "types": ["node"], "plugins": [{ "name": "soltag/plugin" }] }, "include": ["src", "test", "scripts", ".soltag/types.d.ts"], diff --git a/bots/blue-liquidation/vitest.config.ts b/bots/blue-liquidation/vitest.config.ts new file mode 100644 index 00000000..827c62aa --- /dev/null +++ b/bots/blue-liquidation/vitest.config.ts @@ -0,0 +1,23 @@ +import { fileURLToPath } from 'node:url' +import soltag from 'soltag/vite' +import { loadEnv } from 'vite' +import { defineConfig } from 'vitest/config' + +const BOT_DIR = fileURLToPath(new URL('.', import.meta.url)) + +// soltag is a build-time-only transform: its `sol``` tagged templates throw at runtime unless a +// plugin compiles them (via solc). The vite plugin transforms this project's TS sources and leaves +// files without templates unchanged — it replaces the hand-rolled bun loader plugin that used to be +// wired through bunfig `preload`. Enable the optimizer: the lens's per-element computation has +// enough locals to hit "stack too deep" without it. +export default defineConfig({ + plugins: [soltag({ solc: { optimizer: { enabled: true, runs: 200 } } })], + test: { + name: 'blue-liquidation', + // bun auto-loaded .env files; vitest does not. The fork suite reads RPC_URL_8453 from + // .env.test.local and skips when it is unset. `loadEnv` with an empty prefix passes an + // already-exported value straight through (so CI's secret survives) and omits the key entirely + // when no file supplies it — it never substitutes an empty string, so CI cannot silently skip. + env: loadEnv('test', BOT_DIR, '') + } +}) diff --git a/bots/market-making/docs/architecture.md b/bots/market-making/docs/architecture.md index 4b0c8e95..4cee7300 100644 --- a/bots/market-making/docs/architecture.md +++ b/bots/market-making/docs/architecture.md @@ -149,7 +149,7 @@ historical block and requires `anvil` on `PATH` plus an archive-capable `RPC_URL Run package-focused checks with: ```sh -bun test bots/market-making/test +pnpm --filter @morpho-org/market-making-bot exec vitest run pnpm --filter @morpho-org/market-making-bot run test:e2e pnpm --filter @morpho-org/market-making-bot run typecheck pnpm --filter @morpho-org/market-making-bot run jsdoc:build diff --git a/bots/market-making/package.json b/bots/market-making/package.json index b2f9a03e..627deba2 100644 --- a/bots/market-making/package.json +++ b/bots/market-making/package.json @@ -4,52 +4,38 @@ "private": true, "license": "Apache-2.0", "bin": { - "mm": "./src/index.ts" + "mm": "./dist/src/index.js" }, "type": "module", "scripts": { - "deploy:railway": "bun run scripts/deploy-railway.ts", - "start": "bun src/index.ts", - "playground:build": "node scripts/playground-build.mjs", - "playground:build:dev": "bun build playground/index.html --outdir playground/dist --target browser --watch", - "playground:serve": "node scripts/playground-serve.mjs", - "playground:serve:test": "node --test scripts/playground-atomic-publish.test.mjs scripts/playground-build.test.mjs scripts/playground-process.test.mjs scripts/playground-serve.test.mjs", - "playground:smoke": "node scripts/playground-smoke.mjs", - "playground:smoke:test": "node --test scripts/playground-smoke.browser.mjs", - "playground:smoke:output-probe": "node scripts/playground-smoke-output-probe.mjs", - "test:e2e": "bun test test/e2e", - "typecheck": "tsc --noEmit", - "jsdoc:check": "bun scripts/check-jsdoc.ts", - "jsdoc:build": "pnpm run jsdoc:check && typedoc --options typedoc.json" + "build": "tsx scripts/build.ts", + "jsdoc:build": "pnpm run jsdoc:check && typedoc --options typedoc.json", + "jsdoc:check": "tsx scripts/check-jsdoc.ts", + "start": "node --env-file-if-exists=.env dist/src/index.js", + "test:e2e": "vitest run test/e2e", + "typecheck": "tsc --noEmit" }, "dependencies": { - "@aws-sdk/client-kms": "3.1101.0", - "@ethereumjs/wallet": "^10.0.0", "@morpho-org/midnight-sdk": "catalog:", "@morpho-org/morpho-sdk": "5.4.1", "@morpho-org/morpho-ts": "catalog:", - "@noble/curves": "1.9.1", - "@repo/bot-kit": "workspace:*", "@repo/logging": "workspace:*", "@repo/monitoring": "workspace:*", "@repo/observability": "workspace:*", "@repo/offers": "workspace:*", "@repo/utils": "workspace:*", - "@tanstack/react-form": "catalog:", - "@tanstack/react-table": "catalog:", - "asn1js": "3.0.6", "commander": "^14.0.3", - "react": "catalog:", - "react-dom": "catalog:", "viem": "catalog:", "yaml": "2.8.3" }, "devDependencies": { "@repo/typescript-config": "workspace:*", - "@types/bun": "catalog:", - "@types/react": "catalog:", - "@types/react-dom": "catalog:", + "@types/node": "catalog:", + "esbuild": "catalog:", + "execa": "catalog:", + "tsx": "catalog:", "typedoc": "0.28.20", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/bots/market-making/scripts/build.ts b/bots/market-making/scripts/build.ts new file mode 100644 index 00000000..5a44eee5 --- /dev/null +++ b/bots/market-making/scripts/build.ts @@ -0,0 +1,30 @@ +import { build as esbuild } from 'esbuild' +import { rmSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +import { BundleFailedError } from './bundle-failed.error' + +// Bundles the bot entrypoint to `dist/` so production runs a plain `node` with no TypeScript +// transform at startup. This bot holds no soltag `sol``` templates, so no transform plugin is wired. + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..') +const DIST_DIR = join(ROOT, 'dist') +rmSync(DIST_DIR, { recursive: true, force: true }) + +try { + await esbuild({ + entryPoints: [join(ROOT, 'src/index.ts')], + outdir: DIST_DIR, + outbase: ROOT, + bundle: true, + platform: 'node', + format: 'esm', + // CJS deps reaching for require() inside an ESM bundle need a real require. + banner: { + js: "import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);" + } + }) +} catch (error) { + throw new BundleFailedError(error instanceof Error ? error.message : String(error)) +} diff --git a/bots/market-making/scripts/bundle-failed.error.ts b/bots/market-making/scripts/bundle-failed.error.ts new file mode 100644 index 00000000..85b07b64 --- /dev/null +++ b/bots/market-making/scripts/bundle-failed.error.ts @@ -0,0 +1,11 @@ +/** Signals that the production bundle could not be produced. */ +export class BundleFailedError extends Error { + /** + * Creates a tooling failure from the bundler's own message, without retaining source contents. + * @param detail - Bundler-reported reason for the failure. + */ + constructor(readonly detail: string) { + super(`Bundle failed: ${detail}`) + this.name = 'BundleFailedError' + } +} diff --git a/bots/market-making/scripts/check-jsdoc.test.ts b/bots/market-making/scripts/check-jsdoc.test.ts index 045d6f91..77fa423c 100644 --- a/bots/market-making/scripts/check-jsdoc.test.ts +++ b/bots/market-making/scripts/check-jsdoc.test.ts @@ -1,9 +1,6 @@ -import { describe, expect, test } from 'bun:test' -import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join, relative } from 'node:path' +import { describe, expect, test } from 'vitest' -import { discoverJSDocSourceFiles, inspectJSDocSource } from './check-jsdoc' +import { inspectJSDocSource } from './check-jsdoc' import { JSDocValidationError } from './js-doc-validation.error' const inspect = (source: string) => inspectJSDocSource('fixture.ts', source) @@ -22,39 +19,6 @@ export interface Reader { ` describe('JSDoc contract checker', () => { - test('discovers relevant TypeScript files in deterministic order', async () => { - const packageRoot = await mkdtemp(join(tmpdir(), 'market-making-jsdoc-')) - try { - await Promise.all([ - mkdir(join(packageRoot, 'src/nested'), { recursive: true }), - mkdir(join(packageRoot, 'scripts'), { recursive: true }), - mkdir(join(packageRoot, 'build/generated'), { recursive: true }), - mkdir(join(packageRoot, 'node_modules/dependency'), { recursive: true }) - ]) - await Promise.all([ - writeFile(join(packageRoot, 'src/z.ts'), ''), - writeFile(join(packageRoot, 'src/nested/a.ts'), ''), - writeFile(join(packageRoot, 'src/index.ts'), ''), - writeFile(join(packageRoot, 'scripts/check-jsdoc.ts'), ''), - writeFile(join(packageRoot, 'scripts/js-doc-validation.error.ts'), ''), - writeFile(join(packageRoot, 'scripts/check-jsdoc.test.ts'), ''), - writeFile(join(packageRoot, 'build/generated/output.ts'), ''), - writeFile(join(packageRoot, 'node_modules/dependency/index.ts'), '') - ]) - - const files = await discoverJSDocSourceFiles(packageRoot) - - expect(files.map(file => relative(packageRoot, file))).toEqual([ - 'scripts/check-jsdoc.ts', - 'scripts/js-doc-validation.error.ts', - 'src/nested/a.ts', - 'src/z.ts' - ]) - } finally { - await rm(packageRoot, { recursive: true, force: true }) - } - }) - test('uses a stable typed tooling failure with a safe violation count', () => { const error = new JSDocValidationError(3) diff --git a/bots/market-making/scripts/check-jsdoc.ts b/bots/market-making/scripts/check-jsdoc.ts index fe8e3f6c..d3a97ef4 100644 --- a/bots/market-making/scripts/check-jsdoc.ts +++ b/bots/market-making/scripts/check-jsdoc.ts @@ -1,4 +1,6 @@ -import { relative, resolve } from 'node:path' +import { readFile } from 'node:fs/promises' +import { dirname, relative, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' import ts from 'typescript' import { JSDocValidationError } from './js-doc-validation.error' @@ -295,31 +297,104 @@ export const inspectJSDocSource = (file: string, source: string): JSDocInspectio return { declarations, failures } } -const packageRoot = resolve(import.meta.dir, '..') - -/** - * Discovers the TypeScript files that define the documented market-making surface. - * @param root - Absolute market-making package directory to scan. - * @returns Relevant source and checker files in deterministic package-relative order. - * @remarks The scan is read-only and excludes the executable entrypoint and test files. - */ -export const discoverJSDocSourceFiles = async (root: string) => { - const glob = new Bun.Glob('{src,scripts}/**/*.ts') - const files: string[] = [] - for await (const file of glob.scan({ cwd: root, absolute: true, onlyFiles: true })) { - const packagePath = relative(root, file) - if (packagePath === 'src/index.ts' || packagePath.endsWith('.test.ts')) continue - files.push(file) - } - return files.toSorted() -} +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const sourceRoot = resolve(packageRoot, 'src') +const sourceFiles = [ + 'application/operator-error-name.utils.ts', + 'application/bootstrap/bootstrap-ownership-cleanup.error.ts', + 'application/bootstrap/position-bootstrap-halted.error.ts', + 'application/bootstrap/position-bootstrap-monitor-halted.error.ts', + 'application/bootstrap/position-bootstrap-verbose.ts', + 'application/bootstrap/position-bootstrap.service.ts', + 'application/invalidation/offer-invalidation-failed.error.ts', + 'application/invalidation/offer-invalidation.service.ts', + 'application/ladder/ladder-cycle-halted.error.ts', + 'application/ladder/ladder-market-maker.service.ts', + 'application/ladder/ladder-market-maker.utils.ts', + 'application/ladder/ladder-monitor-halted.error.ts', + 'application/ladder/ladder-ownership-cleanup.error.ts', + 'application/ladder/ladder-verbose.ts', + 'application/market-making/market-making-monitor-halted.error.ts', + 'application/market-making/market-making-mutation.utils.ts', + 'application/market-making/market-making.service.ts', + 'application/setup/setup-check.service.ts', + 'application/setup/setup-check.utils.ts', + 'application/setup/safe-provider.error.ts', + 'application/setup/setup-failed.error.ts', + 'application/setup/setup-monitor-configuration.error.ts', + 'application/setup/setup-monitor-halted.error.ts', + 'application/version.service.ts', + 'bootstrap.ts', + 'config/config-file.error.ts', + 'config/config-source.utils.ts', + 'config/config-validation.error.ts', + 'config/config.service.ts', + 'config/config.utils.ts', + 'domain/bootstrap/bootstrap-configuration.error.ts', + 'domain/bootstrap/position-bootstrap.ts', + 'domain/ladder/ladder-configuration.error.ts', + 'domain/ladder/ladder.ts', + 'infrastructure/bootstrap/bootstrap-hard-halt.error.ts', + 'infrastructure/bootstrap/bootstrap-exposure.utils.ts', + 'infrastructure/bootstrap/bootstrap-make.service.ts', + 'infrastructure/bootstrap/bootstrap-mempool-validation.error.ts', + 'infrastructure/bootstrap/bootstrap-mempool-validation.utils.ts', + 'infrastructure/bootstrap/bootstrap-offer.utils.ts', + 'infrastructure/bootstrap/bootstrap-pending-offer.utils.ts', + 'infrastructure/bootstrap/bootstrap-adapter.error.ts', + 'infrastructure/bootstrap/bootstrap-group-ownership.utils.ts', + 'infrastructure/bootstrap/bootstrap-groups.utils.ts', + 'infrastructure/bootstrap/bootstrap-position.service.ts', + 'infrastructure/bootstrap/bootstrap-reference-rate.service.ts', + 'infrastructure/bootstrap/bootstrap-requirement-client.utils.ts', + 'infrastructure/bootstrap/bootstrap-requirements.utils.ts', + 'infrastructure/bootstrap/bootstrap-spread.utils.ts', + 'infrastructure/bootstrap/bootstrap-transaction.utils.ts', + 'infrastructure/bootstrap/production-bootstrap.ts', + 'infrastructure/cli/cli-usage.error.ts', + 'infrastructure/cli/cli.ts', + 'infrastructure/cli/market-making-entrypoint.ts', + 'infrastructure/cli/offer-invalidation-argument.utils.ts', + 'infrastructure/invalidation/offer-invalidation-adapter.error.ts', + 'infrastructure/invalidation/offer-invalidation-group.utils.ts', + 'infrastructure/invalidation/offer-invalidation-transaction.utils.ts', + 'infrastructure/invalidation/production-offer-invalidation.ts', + 'infrastructure/ladder/ladder-adapter.error.ts', + 'infrastructure/ladder/ladder-active-publication.utils.ts', + 'infrastructure/ladder/ladder-bootstrap-offer.utils.ts', + 'infrastructure/ladder/ladder-cash-reservation.utils.ts', + 'infrastructure/ladder/ladder-group-ownership.utils.ts', + 'infrastructure/ladder/ladder-hard-halt.error.ts', + 'infrastructure/ladder/ladder-make.service.ts', + 'infrastructure/ladder/ladder-offer.utils.ts', + 'infrastructure/ladder/ladder-ratification.utils.ts', + 'infrastructure/ladder/ladder-signature.utils.ts', + 'infrastructure/ladder/ladder-spread.utils.ts', + 'infrastructure/ladder/ladder-transaction.utils.ts', + 'infrastructure/ladder/production-ladder.ts', + 'infrastructure/make/managed-maker-account.utils.ts', + 'infrastructure/make/read-only-bootstrap-make.service.ts', + 'infrastructure/make/read-only-ladder-make.service.ts', + 'infrastructure/make/read-only-make.utils.ts', + 'infrastructure/reference/blue-reference-reader.utils.ts', + 'infrastructure/setup-state/chain-reader.utils.ts', + 'infrastructure/reference/reference-adapter.error.ts', + 'infrastructure/setup-state/http-json.utils.ts', + 'infrastructure/setup-state/provider-pagination.error.ts', + 'infrastructure/setup-state/provider-read.error.ts', + 'infrastructure/setup-state/provider-read.utils.ts', + 'infrastructure/setup-state/provider-response.error.ts', + 'infrastructure/setup-state/viem-setup-state.service.ts', + 'infrastructure/setup-state/viem-setup-state.utils.ts' +].map(path => resolve(sourceRoot, path)) +sourceFiles.push(resolve(packageRoot, 'scripts/js-doc-validation.error.ts')) +sourceFiles.push(resolve(packageRoot, 'scripts/check-jsdoc.ts')) const run = async () => { - const sourceFiles = await discoverJSDocSourceFiles(packageRoot) const failures: JSDocFailure[] = [] const declarations: string[] = [] for (const file of sourceFiles) { - const source = await Bun.file(file).text() + const source = await readFile(file, 'utf8') const inspection = inspectJSDocSource(relative(packageRoot, file), source) declarations.push( ...inspection.declarations.map(item => `${relative(packageRoot, file)} ${item}`) @@ -340,4 +415,6 @@ const run = async () => { } } -if (import.meta.main) await run() +// bun's `import.meta.main`; under Node (and under an esbuild bundle, where it is unreliable) compare +// the resolved entry path instead. +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await run() diff --git a/bots/market-making/src/bootstrap.ts b/bots/market-making/src/bootstrap.ts index df705adf..1c3f2a66 100644 --- a/bots/market-making/src/bootstrap.ts +++ b/bots/market-making/src/bootstrap.ts @@ -153,7 +153,7 @@ const defaultState = async (config: ConfigService) => { * cannot race signer nonces. */ export const createApplication = ( - environment: Environment = Bun.env, + environment: Environment = process.env, dependencies: Dependencies = {} ): { /** diff --git a/bots/market-making/src/config/config.service.ts b/bots/market-making/src/config/config.service.ts index 200ba944..ecbefa96 100644 --- a/bots/market-making/src/config/config.service.ts +++ b/bots/market-making/src/config/config.service.ts @@ -58,7 +58,7 @@ export class ConfigService { * permits fallback; unsafe entries fail closed. No file means env-only loading. Read-only mode * omits YAML and environment private-key values before typed validation. */ - static async load(environment: Environment = Bun.env, options: ConfigurationLoadOptions = {}) { + static async load(environment: Environment = process.env, options: ConfigurationLoadOptions = {}) { const source = await loadConfigurationSources(environment, options) const declaredMethod = source.values.KEY_STORAGE_METHOD?.toString().trim() const keystorePath = source.values.KEYSTORE_PATH?.toString().trim() diff --git a/bots/market-making/src/index.ts b/bots/market-making/src/index.ts index 82ebbded..21b7d900 100644 --- a/bots/market-making/src/index.ts +++ b/bots/market-making/src/index.ts @@ -36,9 +36,9 @@ await observability.start() try { process.exitCode = await runMarketMakingEntrypoint( createApplication(), - enhanceVerboseArgv(Bun.argv.slice(2), { + enhanceVerboseArgv(process.argv.slice(2), { commands: MARKET_MAKING_VERBOSE_COMMANDS, - env: Bun.env + env: process.env }), { writeOut: value => console.log(value), diff --git a/bots/market-making/src/infrastructure/setup-state/http-json.utils.ts b/bots/market-making/src/infrastructure/setup-state/http-json.utils.ts index 48bc864a..73d9770d 100644 --- a/bots/market-making/src/infrastructure/setup-state/http-json.utils.ts +++ b/bots/market-making/src/infrastructure/setup-state/http-json.utils.ts @@ -61,5 +61,7 @@ export const booksJsonRequestFetch = (request: JsonRequest, timeoutMs: number): const value = await request(inputUrl, 'morpho-api', timeoutMs) return Response.json(value) } - return Object.assign(adapter, { preconnect: fetch.preconnect }) + // bun's `fetch` carried a `preconnect` property that had to be copied to satisfy `typeof fetch`; + // Node's `fetch` type has no such member, so the adapter alone is assignable. + return adapter } diff --git a/bots/market-making/test/application/bootstrap/position-bootstrap.service.test.ts b/bots/market-making/test/application/bootstrap/position-bootstrap.service.test.ts index b99c98f1..4ea51cd3 100644 --- a/bots/market-making/test/application/bootstrap/position-bootstrap.service.test.ts +++ b/bots/market-making/test/application/bootstrap/position-bootstrap.service.test.ts @@ -1,6 +1,6 @@ import type { Hex } from 'viem' -import { describe, expect, mock, test } from 'bun:test' +import { describe, expect, test, vi } from 'vitest' import type { BootstrapMakeService, @@ -40,7 +40,7 @@ const setup = ({ configs?: BootstrapConfig[] credit?: bigint } = {}) => { - const readPosition = mock(async () => ({ + const readPosition = vi.fn(async () => ({ credit, debt: 0n, cashBalance: 2_000n, @@ -48,14 +48,14 @@ const setup = ({ totalExposure: 0n, activeOffer: undefined })) - const readRate = mock(async () => ({ + const readRate = vi.fn(async () => ({ mode: 'static' as const, rateBps: 500n, observationId: 'static:500' })) - const reconcile = mock(async () => undefined) - const hardHalt = mock(async () => undefined) - const cleanup = mock(async () => undefined) + const reconcile = vi.fn(async () => undefined) + const hardHalt = vi.fn(async () => undefined) + const cleanup = vi.fn(async () => undefined) const positions: BootstrapPositionService = { readPosition } const rates: BootstrapReferenceRateService = { readRate } const make: BootstrapMakeService = { reconcile, hardHalt, cleanup } @@ -79,10 +79,10 @@ describe('PositionBootstrapService', () => { const events: string[] = [] const controller = new AbortController() const { service, make } = setup() - make.reconcile = mock(async () => { + make.reconcile = vi.fn(async () => { events.push('reconcile') }) - const cleanup = mock(async () => { + const cleanup = vi.fn(async () => { events.push('cleanup') }) make.cleanup = cleanup @@ -150,10 +150,10 @@ describe('PositionBootstrapService', () => { test('stops monitoring on a handled failed cycle and still cleans owned groups', async () => { const controller = new AbortController() const { service, make } = setup() - make.reconcile = mock(async () => { + make.reconcile = vi.fn(async () => { throw new Error('publication unavailable') }) - const cleanup = mock(async () => 'logged' as const) + const cleanup = vi.fn(async () => 'logged' as const) make.cleanup = cleanup const report = await service.runContinuously({ @@ -174,7 +174,7 @@ describe('PositionBootstrapService', () => { test('reports a sanitized cleanup failure after a stop signal', async () => { const controller = new AbortController() const { service, make } = setup() - make.cleanup = mock(async () => { + make.cleanup = vi.fn(async () => { const error = new Error('provider https://rpc.example/?key=secret') error.name = 'https://rpc.example/?key=secret' throw error @@ -279,7 +279,7 @@ describe('PositionBootstrapService', () => { const { service, make, readPosition, readRate, reconcile } = setup({ configs: [{ ...config(), maximumTotalExposure: 0n }] }) - const hardHalt = mock(async () => { + const hardHalt = vi.fn(async () => { throw new RangeError('cleanup reverted') }) make.hardHalt = hardHalt @@ -331,7 +331,7 @@ describe('PositionBootstrapService', () => { const submittedEvents: unknown[] = [] const transactionOrder: string[] = [] let read = 0 - const readPosition = mock(async () => { + const readPosition = vi.fn(async () => { read += 1 return { credit: read === 1 ? 0n : 100n, @@ -352,7 +352,7 @@ describe('PositionBootstrapService', () => { } }) positions.readPosition = readPosition - make.reconcile = mock(async parameters => { + make.reconcile = vi.fn(async parameters => { await parameters.onTransactionSubmitted?.({ operation: 'publish', txHash: publicationHash @@ -492,7 +492,7 @@ describe('PositionBootstrapService', () => { test('keeps an applied result when only its verbose after-state read fails', async () => { const { service, positions } = setup() let read = 0 - positions.readPosition = mock(async () => { + positions.readPosition = vi.fn(async () => { read += 1 if (read === 2) throw new TypeError('after-state provider unavailable') return { @@ -521,7 +521,7 @@ describe('PositionBootstrapService', () => { test('reports verbose monitor cycles and confirmed shutdown cancellation hashes', async () => { const controller = new AbortController() const { service, make } = setup({ credit: 900n }) - make.cleanup = mock(async parameters => { + make.cleanup = vi.fn(async parameters => { await parameters?.onTransactionSubmitted?.({ operation: 'cancel', txHash: cancellationHash @@ -625,7 +625,7 @@ describe('PositionBootstrapService', () => { const { service, positions, reconcile } = setup({ configs: [capped, { ...capped, marketId: secondMarketId }] }) - positions.readPosition = mock(async id => ({ + positions.readPosition = vi.fn(async id => ({ credit: 0n, debt: 0n, cashBalance: id === marketId ? 1_000n : 500n, @@ -667,7 +667,7 @@ describe('PositionBootstrapService', () => { const { service, positions, reconcile } = setup({ configs: [capped, { ...capped, marketId: secondMarketId }] }) - positions.readPosition = mock(async id => ({ + positions.readPosition = vi.fn(async id => ({ credit: id === marketId ? 900n : 0n, debt: 0n, cashBalance: id === marketId ? 1_000n : 500n, @@ -703,7 +703,7 @@ describe('PositionBootstrapService', () => { test('invalidates at target and stays observational after completion when auto-refill is off', async () => { const { service, positions, reconcile } = setup() let cycle = 0 - positions.readPosition = mock(async () => { + positions.readPosition = vi.fn(async () => { cycle += 1 return { credit: cycle === 1 ? 900n : 500n, @@ -761,7 +761,7 @@ describe('PositionBootstrapService', () => { configs: [config(), config(secondMarketId, true)] }) let preparingCompletion = warmup - positions.readPosition = mock(async id => { + positions.readPosition = vi.fn(async id => { if (preparingCompletion) { return { credit: 900n, @@ -795,7 +795,7 @@ describe('PositionBootstrapService', () => { reconcile.mockClear() hardHalt.mockClear() } - const failedReconcile = mock(async request => { + const failedReconcile = vi.fn(async request => { if (request.marketId === marketId && request.desiredOffer === undefined) { throw new RangeError('market invalidation reverted') } @@ -823,7 +823,7 @@ describe('PositionBootstrapService', () => { const { service, positions, make } = setup({ configs: [config(), config(secondMarketId)] }) - positions.readPosition = mock(async id => ({ + positions.readPosition = vi.fn(async id => ({ credit: id === marketId ? 900n : 0n, debt: 0n, cashBalance: 2_000n, @@ -839,7 +839,7 @@ describe('PositionBootstrapService', () => { } : undefined })) - const failedReconcile = mock(async request => { + const failedReconcile = vi.fn(async request => { if (request.marketId === marketId) { const hostileInvalidation = new Error('market invalidation reverted') hostileInvalidation.name = 'https://invalidator.example/?token=secret-invalidation' @@ -847,7 +847,7 @@ describe('PositionBootstrapService', () => { } }) make.reconcile = failedReconcile - const hardHalt = mock(async () => { + const hardHalt = vi.fn(async () => { const hostileCleanup = new Error('hard halt reverted') hostileCleanup.name = 'https://cleanup.example/?token=secret-cleanup' throw hostileCleanup @@ -878,7 +878,7 @@ describe('PositionBootstrapService', () => { const { service, positions, reconcile, hardHalt } = setup({ configs: [config(), config(secondMarketId)] }) - positions.readPosition = mock(async id => ({ + positions.readPosition = vi.fn(async id => ({ credit: id === marketId ? 900n : 0n, debt: 0n, cashBalance: 2_000n, @@ -907,7 +907,7 @@ describe('PositionBootstrapService', () => { const { service, positions, reconcile } = setup({ configs: [config(), config(secondMarketId)] }) - positions.readPosition = mock(async id => { + positions.readPosition = vi.fn(async id => { if (id === marketId) throw new Error('provider unavailable') return { credit: 0n, @@ -950,10 +950,10 @@ describe('PositionBootstrapService', () => { test('halts after market invalidation fails while preserving the read failure', async () => { const { service, positions, make, hardHalt } = setup() - positions.readPosition = mock(async () => { + positions.readPosition = vi.fn(async () => { throw new TypeError('position unavailable') }) - make.reconcile = mock(async () => { + make.reconcile = vi.fn(async () => { throw new RangeError('invalidation reverted') }) @@ -974,7 +974,7 @@ describe('PositionBootstrapService', () => { const { service, positions, make } = setup({ configs: [config(), config(secondMarketId)] }) - positions.readPosition = mock(async id => { + positions.readPosition = vi.fn(async id => { if (id === marketId) throw new TypeError('position unavailable') return { credit: 0n, @@ -985,7 +985,7 @@ describe('PositionBootstrapService', () => { activeOffer: undefined } }) - const failedReconcile = mock(async request => { + const failedReconcile = vi.fn(async request => { if (request.marketId === marketId) throw new RangeError('invalidation reverted') }) make.reconcile = failedReconcile @@ -1005,13 +1005,13 @@ describe('PositionBootstrapService', () => { test('preserves market invalidation and hard-halt failure classifications', async () => { const { service, positions, make } = setup() - positions.readPosition = mock(async () => { + positions.readPosition = vi.fn(async () => { throw new TypeError('position unavailable') }) - make.reconcile = mock(async () => { + make.reconcile = vi.fn(async () => { throw new RangeError('invalidation reverted') }) - make.hardHalt = mock(async () => { + make.hardHalt = vi.fn(async () => { throw new URIError('hard halt reverted') }) @@ -1032,7 +1032,7 @@ describe('PositionBootstrapService', () => { const { service, positions } = setup() const hostile = new Error('provider failed') hostile.name = 'https://rpc.example/?token=secret-token' - positions.readPosition = mock(async () => { + positions.readPosition = vi.fn(async () => { throw hostile }) @@ -1053,7 +1053,7 @@ describe('PositionBootstrapService', () => { 'hard-halts an out-of-bounds rate with zero capacity (%s BPS)', async rateBps => { const { service, positions, rates, reconcile, hardHalt } = setup() - positions.readPosition = mock(async () => ({ + positions.readPosition = vi.fn(async () => ({ credit: 0n, debt: 0n, cashBalance: 0n, @@ -1061,7 +1061,7 @@ describe('PositionBootstrapService', () => { totalExposure: 0n, activeOffer: undefined })) - rates.readRate = mock(async () => ({ + rates.readRate = vi.fn(async () => ({ mode: 'static' as const, rateBps, observationId: `static:${rateBps}` @@ -1085,7 +1085,7 @@ describe('PositionBootstrapService', () => { const { service, rates, reconcile, hardHalt } = setup({ configs: [config(), config(secondMarketId)] }) - rates.readRate = mock(async () => { + rates.readRate = vi.fn(async () => { throw new TypeError('stale reference') }) @@ -1110,7 +1110,7 @@ describe('PositionBootstrapService', () => { const { service, rates, reconcile, hardHalt } = setup({ configs: [config(), config(secondMarketId)] }) - rates.readRate = mock(async id => { + rates.readRate = vi.fn(async id => { if (id === secondMarketId) throw new TypeError('stale reference') return { mode: 'static' as const, rateBps: 500n, observationId: 'static:500' } }) @@ -1130,10 +1130,10 @@ describe('PositionBootstrapService', () => { test('preserves the reference failure classification when strategy cleanup also fails', async () => { const { service, rates, make } = setup() - rates.readRate = mock(async () => { + rates.readRate = vi.fn(async () => { throw new TypeError('stale reference') }) - make.hardHalt = mock(async () => { + make.hardHalt = vi.fn(async () => { throw new RangeError('cleanup reverted') }) @@ -1151,7 +1151,7 @@ describe('PositionBootstrapService', () => { test('halts and invalidates the strategy when the bootstrap decision rejects active offers', async () => { const { service, positions, rates, hardHalt } = setup() - positions.readPosition = mock(async () => ({ + positions.readPosition = vi.fn(async () => ({ credit: 0n, debt: 0n, cashBalance: 2_000n, @@ -1164,7 +1164,7 @@ describe('PositionBootstrapService', () => { referenceObservationId: 'static:500' } })) - rates.readRate = mock(async () => ({ + rates.readRate = vi.fn(async () => ({ mode: 'static' as const, rateBps: 100n, observationId: 'static:100' @@ -1186,7 +1186,7 @@ describe('PositionBootstrapService', () => { const { service, positions, readRate, reconcile, hardHalt } = setup({ configs: [{ ...config(), acceptanceAssets: -1n }, config(secondMarketId)] }) - const readPosition = mock(async (id: Hex) => ({ + const readPosition = vi.fn(async (id: Hex) => ({ credit: 0n, debt: 0n, cashBalance: 2_000n, @@ -1221,7 +1221,7 @@ describe('PositionBootstrapService', () => { const { service, positions, make, readRate, reconcile } = setup({ configs: [{ ...config(), acceptanceAssets: 1_001n }, config(secondMarketId)] }) - const readPosition = mock(async (id: Hex) => ({ + const readPosition = vi.fn(async (id: Hex) => ({ credit: 0n, debt: 0n, cashBalance: 2_000n, @@ -1235,7 +1235,7 @@ describe('PositionBootstrapService', () => { } })) positions.readPosition = readPosition - const hardHalt = mock(async () => { + const hardHalt = vi.fn(async () => { throw new RangeError('cleanup reverted') }) make.hardHalt = hardHalt @@ -1260,7 +1260,7 @@ describe('PositionBootstrapService', () => { test('completes before a failed reference read and stays stopped with auto-refill disabled', async () => { const { service, positions, rates, reconcile } = setup() let cycle = 0 - positions.readPosition = mock(async () => { + positions.readPosition = vi.fn(async () => { cycle += 1 return { credit: cycle === 1 ? 900n : 500n, @@ -1279,7 +1279,7 @@ describe('PositionBootstrapService', () => { : undefined } }) - const failedReadRate = mock(async () => { + const failedReadRate = vi.fn(async () => { throw new TypeError('reference unavailable') }) rates.readRate = failedReadRate @@ -1306,7 +1306,7 @@ describe('PositionBootstrapService', () => { test('stops dependent plans after a make failure', async () => { const { service, make } = setup({ configs: [config(), config(secondMarketId)] }) - const failedReconcile = mock(async request => { + const failedReconcile = vi.fn(async request => { if (request.marketId === marketId) throw new RangeError('publish rejected') }) make.reconcile = failedReconcile @@ -1327,7 +1327,7 @@ describe('PositionBootstrapService', () => { test('reports a sanitized Mempool asset floor after publication validation fails', async () => { const { service, make } = setup() - make.reconcile = mock(async () => { + make.reconcile = vi.fn(async () => { throw new BootstrapMempoolValidationError([ { rule: 'min_offer_assets_usd', minimumAssets: 100_000_000n } ]) @@ -1348,7 +1348,7 @@ describe('PositionBootstrapService', () => { test('resumes after initial completion when auto-refill is enabled', async () => { const { service, positions, reconcile } = setup({ configs: [config(marketId, true)] }) let cycle = 0 - positions.readPosition = mock(async () => { + positions.readPosition = vi.fn(async () => { cycle += 1 return { credit: cycle === 1 ? 900n : 500n, diff --git a/bots/market-making/test/application/invalidation/offer-invalidation.service.test.ts b/bots/market-making/test/application/invalidation/offer-invalidation.service.test.ts index a13290de..d7afac13 100644 --- a/bots/market-making/test/application/invalidation/offer-invalidation.service.test.ts +++ b/bots/market-making/test/application/invalidation/offer-invalidation.service.test.ts @@ -1,6 +1,6 @@ import type { Hex } from 'viem' -import { describe, expect, mock, test } from 'bun:test' +import { describe, expect, test, vi } from 'vitest' import type { OfferInvalidationPort } from '../../../src/application/invalidation/offer-invalidation.service' @@ -49,8 +49,8 @@ const subject = ( describe('OfferInvalidationService', () => { test('uses the mandatory batch capability once for maker-wide read-only rendering', async () => { - const invalidateBatch = mock(async () => undefined) - const invalidate = mock(async () => txB) + const invalidateBatch = vi.fn(async () => undefined) + const invalidate = vi.fn(async () => txB) const { service } = subject({ invalidateBatch, invalidate }, 'readonly') const report = await service.run() @@ -67,15 +67,15 @@ describe('OfferInvalidationService', () => { }) test('reports one confirmed batch hash for every selected group and forgets them together', async () => { - const invalidateBatch = mock( + const invalidateBatch = vi.fn( async (groupIds: readonly Hex[], observer?: (hash: Hex) => unknown) => { expect(groupIds).toEqual([groupA, groupB]) await observer?.(txA) return txA } ) - const invalidate = mock(async () => txB) - const forgetGroups = mock(async () => undefined) + const invalidate = vi.fn(async () => txB) + const forgetGroups = vi.fn(async () => undefined) const { service } = subject({ invalidateBatch, invalidate, forgetGroups }) const submitted: unknown[] = [] @@ -98,7 +98,7 @@ describe('OfferInvalidationService', () => { }) test('does not fall back after a submitted batch transaction fails', async () => { - const invalidate = mock(async () => txB) + const invalidate = vi.fn(async () => txB) const { service } = subject({ invalidateBatch: async (_groupIds, observer) => { await observer?.(txA) @@ -122,7 +122,7 @@ describe('OfferInvalidationService', () => { }) test('retains the shared batch hash for every group when ownership cleanup fails', async () => { - const forgetGroups = mock(async () => { + const forgetGroups = vi.fn(async () => { throw new OfferInvalidationAdapterError('ownership-cleanup') }) const { service } = subject({ @@ -165,8 +165,8 @@ describe('OfferInvalidationService', () => { }) test('directly invalidates an explicit group without consulting the active-group API', async () => { - const listActiveGroupIds = mock(async () => [groupB]) - const invalidateBatch = mock(async () => txB) + const listActiveGroupIds = vi.fn(async () => [groupB]) + const invalidateBatch = vi.fn(async () => txB) const { service } = subject({ listActiveGroupIds, invalidateBatch }) const report = await service.run({ groupId: groupA }) @@ -256,8 +256,8 @@ describe('OfferInvalidationService', () => { }) test('reports preflight failure without enumerating or invalidating groups', async () => { - const listActiveGroupIds = mock(async () => [groupA]) - const invalidate = mock(async () => txA) + const listActiveGroupIds = vi.fn(async () => [groupA]) + const invalidate = vi.fn(async () => txA) const { service } = subject({ preflight: async () => { throw new OfferInvalidationAdapterError('preflight') diff --git a/bots/market-making/test/application/ladder/ladder-market-maker.service.test.ts b/bots/market-making/test/application/ladder/ladder-market-maker.service.test.ts index db7a38dd..7bf5ee5b 100644 --- a/bots/market-making/test/application/ladder/ladder-market-maker.service.test.ts +++ b/bots/market-making/test/application/ladder/ladder-market-maker.service.test.ts @@ -1,6 +1,6 @@ import type { Hex } from 'viem' -import { describe, expect, mock, test } from 'bun:test' +import { describe, expect, test, vi } from 'vitest' import type { LadderMakeService, @@ -75,7 +75,7 @@ const harness = (configs: readonly LadderConfig[] = [config()]) => { return { rateBps: rate, observationId } } } - const cleanup = mock(async () => { + const cleanup = vi.fn(async () => { liveDesired.clear() }) const make: LadderMakeService = { @@ -197,7 +197,7 @@ describe('LadderMarketMakerService', () => { const cancellationHash: Hex = `0x${'bb'.repeat(32)}` const desired = new Map() const controller = new AbortController() - const readMarket = mock(async () => state()) + const readMarket = vi.fn(async () => state()) const make: LadderMakeService = { readActive: async id => desired.get(id), reconcile: async parameters => { diff --git a/bots/market-making/test/application/market-making/market-making-mutation.utils.test.ts b/bots/market-making/test/application/market-making/market-making-mutation.utils.test.ts index d409e51a..323cbaff 100644 --- a/bots/market-making/test/application/market-making/market-making-mutation.utils.test.ts +++ b/bots/market-making/test/application/market-making/market-making-mutation.utils.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, mock, test } from 'bun:test' +import { describe, expect, test, vi } from 'vitest' import type { BootstrapMakeService } from '../../../src/application/bootstrap/position-bootstrap.service' import type { LadderMakeService } from '../../../src/application/ladder/ladder-market-maker.service' @@ -9,28 +9,28 @@ const marketId = `0x${'11'.repeat(32)}` as const const createServices = (events: string[]) => { const bootstrap: BootstrapMakeService = { - reconcile: mock(async () => { + reconcile: vi.fn(async () => { events.push('bootstrap:reconcile') }), - hardHalt: mock(async () => { + hardHalt: vi.fn(async () => { events.push('bootstrap:halt') }), - cleanup: mock(async () => { + cleanup: vi.fn(async () => { events.push('bootstrap:cleanup') }) } const ladder: LadderMakeService = { - readActive: mock(async () => { + readActive: vi.fn(async () => { events.push('ladder:read') return undefined }), - reconcile: mock(async () => { + reconcile: vi.fn(async () => { events.push('ladder:reconcile') }), - hardHalt: mock(async () => { + hardHalt: vi.fn(async () => { events.push('ladder:halt') }), - cleanup: mock(async () => { + cleanup: vi.fn(async () => { events.push('ladder:cleanup') }) } @@ -42,7 +42,7 @@ describe('serializeMarketMakingWrites', () => { const events: string[] = [] let releaseBootstrap: (() => void) | undefined const services = createServices(events) - services.bootstrap.reconcile = mock( + services.bootstrap.reconcile = vi.fn( () => new Promise(resolve => { events.push('bootstrap:start') @@ -64,7 +64,7 @@ describe('serializeMarketMakingWrites', () => { test('continues draining mutations after a preceding operation rejects', async () => { const events: string[] = [] const services = createServices(events) - services.bootstrap.cleanup = mock(async () => { + services.bootstrap.cleanup = vi.fn(async () => { events.push('bootstrap:failed') throw new TypeError('publication failed') }) @@ -82,7 +82,7 @@ describe('serializeMarketMakingWrites', () => { const events: string[] = [] let releaseBootstrap: (() => void) | undefined const services = createServices(events) - services.bootstrap.hardHalt = mock( + services.bootstrap.hardHalt = vi.fn( () => new Promise(resolve => { events.push('bootstrap:start') diff --git a/bots/market-making/test/application/market-making/market-making.service.test.ts b/bots/market-making/test/application/market-making/market-making.service.test.ts index 0cfc76a3..8df49f3c 100644 --- a/bots/market-making/test/application/market-making/market-making.service.test.ts +++ b/bots/market-making/test/application/market-making/market-making.service.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import type { MarketMakingBootstrapMonitor, diff --git a/bots/market-making/test/application/operator-error-name.utils.test.ts b/bots/market-making/test/application/operator-error-name.utils.test.ts index 74b0f34e..16723c70 100644 --- a/bots/market-making/test/application/operator-error-name.utils.test.ts +++ b/bots/market-making/test/application/operator-error-name.utils.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import { MarketMakingMonitorHaltedError } from '../../src/application/market-making/market-making-monitor-halted.error' import { diff --git a/bots/market-making/test/application/setup/setup-check.service.test.ts b/bots/market-making/test/application/setup/setup-check.service.test.ts index 630c55e3..e104841f 100644 --- a/bots/market-making/test/application/setup/setup-check.service.test.ts +++ b/bots/market-making/test/application/setup/setup-check.service.test.ts @@ -1,6 +1,6 @@ import type { Hex } from 'viem' -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import { SetupCheckService, @@ -323,8 +323,8 @@ describe('SetupCheckService', () => { const readiness = new SetupCheckService(state, config).assertReady() - expect(readiness).rejects.toBeInstanceOf(SetupFailedError) - expect(readiness).rejects.toMatchObject({ + await expect(readiness).rejects.toBeInstanceOf(SetupFailedError) + await expect(readiness).rejects.toMatchObject({ report: { ready: false, checks: expect.arrayContaining([ diff --git a/bots/market-making/test/application/version.service.test.ts b/bots/market-making/test/application/version.service.test.ts index a9cfb1d6..f2352dc7 100644 --- a/bots/market-making/test/application/version.service.test.ts +++ b/bots/market-making/test/application/version.service.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import { VersionService } from '../../src/application/version.service' diff --git a/bots/market-making/test/bootstrap.test.ts b/bots/market-making/test/bootstrap.test.ts index dc4a72bb..415b5113 100644 --- a/bots/market-making/test/bootstrap.test.ts +++ b/bots/market-making/test/bootstrap.test.ts @@ -1,9 +1,10 @@ import type { Address, Hex } from 'viem' -import { describe, expect, mock, spyOn, test } from 'bun:test' import { mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' +import { setTimeout as sleep } from 'node:timers/promises' +import { describe, expect, test, vi } from 'vitest' import type { SetupCheckReport, @@ -643,8 +644,8 @@ describe('createApplication', () => { }) test('routes --readonly bootstrap make operations to terminal output', async () => { - const reconcile = mock(async () => {}) - const hardHalt = mock(async () => {}) + const reconcile = vi.fn(async () => {}) + const hardHalt = vi.fn(async () => {}) const events: unknown[] = [] const bootstrapEnvironment = { ...environment, @@ -751,7 +752,7 @@ describe('createApplication', () => { await writeStarted.promise const state = await Promise.race([ run.then(() => 'completed' as const), - Bun.sleep(20).then(() => 'pending' as const) + sleep(20).then(() => 'pending' as const) ]) releaseWrite.resolve() @@ -760,9 +761,9 @@ describe('createApplication', () => { }) test('routes --readonly ladder make operations to terminal output', async () => { - const reconcile = mock(async () => {}) - const hardHalt = mock(async () => {}) - const validateReconcile = mock(async () => {}) + const reconcile = vi.fn(async () => {}) + const hardHalt = vi.fn(async () => {}) + const validateReconcile = vi.fn(async () => {}) const events: unknown[] = [] const application = createApplication( { @@ -826,7 +827,7 @@ describe('createApplication', () => { } ) - expect( + await expect( application.run(['--readonly', 'ladder'], { writeEvent: async () => { throw writeError @@ -839,10 +840,10 @@ describe('createApplication', () => { }) test('runs the exact read-only ladder monitor surface without loading or invoking a signer', async () => { - const reconcile = mock(async () => {}) - const hardHalt = mock(async () => {}) - const cleanup = mock(async () => {}) - const terminal = spyOn(console, 'log').mockImplementation(() => {}) + const reconcile = vi.fn(async () => {}) + const hardHalt = vi.fn(async () => {}) + const cleanup = vi.fn(async () => {}) + const terminal = vi.spyOn(console, 'log').mockImplementation(() => {}) const controller = new AbortController() controller.abort() const application = createApplication( @@ -889,8 +890,8 @@ describe('createApplication', () => { }) test('composes the combined start lifecycle and drains both writer cleanups', async () => { - const bootstrapCleanup = mock(async () => {}) - const ladderCleanup = mock(async () => {}) + const bootstrapCleanup = vi.fn(async () => {}) + const ladderCleanup = vi.fn(async () => {}) const controller = new AbortController() controller.abort() const application = createApplication( @@ -1146,6 +1147,6 @@ setup: state.getChainId = async () => 1 const application = createApplication(environment, { createState: () => state }) - expect(application.run(['setup-check'])).rejects.toBeInstanceOf(SetupFailedError) + await expect(application.run(['setup-check'])).rejects.toBeInstanceOf(SetupFailedError) }) }) diff --git a/bots/market-making/test/config/config-loading.test.ts b/bots/market-making/test/config/config-loading.test.ts index aa8d108c..7502d0c5 100644 --- a/bots/market-making/test/config/config-loading.test.ts +++ b/bots/market-making/test/config/config-loading.test.ts @@ -1,9 +1,10 @@ import type { Hex } from 'viem' -import { afterEach, describe, expect, test } from 'bun:test' +import { $ } from 'execa' import { mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' +import { afterEach, describe, expect, test } from 'vitest' import { ConfigFileError } from '../../src/config/config-file.error' import { @@ -41,9 +42,8 @@ const temporaryDirectory = async () => { } const createFifo = async (path: string) => { - const process = Bun.spawn(['mkfifo', path], { stderr: 'pipe' }) - const exitCode = await process.exited - if (exitCode !== 0) throw new Error('Unable to create FIFO test fixture') + const { failed } = await $({ reject: false })`mkfifo ${path}` + if (failed) throw new Error('Unable to create FIFO test fixture') } const loadInBoundedSubprocess = async (options: { configPath?: string; cwd: string }) => { @@ -57,21 +57,19 @@ const loadInBoundedSubprocess = async (options: { configPath?: string; cwd: stri console.log(JSON.stringify({ reason: error?.reason ?? 'unexpected' })) } ` - const process = Bun.spawn([Bun.which('bun') ?? 'bun', '-e', script], { - stdout: 'pipe', - stderr: 'pipe' - }) - let timedOut = false - const timeout = setTimeout(() => { - timedOut = true - process.kill() - }, 1_000) - const exitCode = await process.exited - clearTimeout(timeout) + // `node -e` cannot import TypeScript, so tsx is registered as a loader for the inline module, and + // execa's own timeout enforces the bound instead of a manual kill. The bound is what this suite is + // really asserting: reading a FIFO would block forever, so any finite completion proves the loader + // fails closed. It is 10s rather than the 1s bun used because a tsx cold start alone costs ~1.3s — + // well under the bound, but not under one sized for bun's interpreter startup. + const result = await $({ + reject: false, + timeout: 10_000 + })`node --import tsx --input-type=module -e ${script}` return { - exitCode, - timedOut, - stdout: await new Response(process.stdout).text() + exitCode: result.exitCode ?? null, + timedOut: result.timedOut, + stdout: result.stdout } } diff --git a/bots/market-making/test/config/config-private-key.test.ts b/bots/market-making/test/config/config-private-key.test.ts index da1e8789..6655ef54 100644 --- a/bots/market-making/test/config/config-private-key.test.ts +++ b/bots/market-making/test/config/config-private-key.test.ts @@ -1,6 +1,6 @@ import type { Address, Hex } from 'viem' -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import { ConfigValidationError } from '../../src/config/config-validation.error' import { ConfigService } from '../../src/config/config.service' diff --git a/bots/market-making/test/config/config.service.test.ts b/bots/market-making/test/config/config.service.test.ts index cf38a184..4bf7fdde 100644 --- a/bots/market-making/test/config/config.service.test.ts +++ b/bots/market-making/test/config/config.service.test.ts @@ -1,7 +1,7 @@ import type { Address, Hex } from 'viem' -import { describe, expect, test } from 'bun:test' import { bytesToHex, hexToBytes } from 'viem' +import { describe, expect, test } from 'vitest' import { ConfigValidationError } from '../../src/config/config-validation.error' import { ConfigService } from '../../src/config/config.service' diff --git a/bots/market-making/test/config/config.utils.test.ts b/bots/market-making/test/config/config.utils.test.ts index b0eafe5b..b61fb5c1 100644 --- a/bots/market-making/test/config/config.utils.test.ts +++ b/bots/market-making/test/config/config.utils.test.ts @@ -1,7 +1,7 @@ import type { Hex } from 'viem' -import { describe, expect, test } from 'bun:test' import { bytesToHex, getAddress, hexToBytes } from 'viem' +import { describe, expect, test } from 'vitest' import { parseAddress, parseBytes32 } from '../../src/config/config.utils' diff --git a/bots/market-making/test/config/ladder-config.test.ts b/bots/market-making/test/config/ladder-config.test.ts index 02d6a961..1ba7373d 100644 --- a/bots/market-making/test/config/ladder-config.test.ts +++ b/bots/market-making/test/config/ladder-config.test.ts @@ -1,9 +1,9 @@ import type { Hex } from 'viem' -import { afterEach, describe, expect, test } from 'bun:test' import { mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' +import { afterEach, describe, expect, test } from 'vitest' import { ConfigValidationError } from '../../src/config/config-validation.error' import { ConfigService } from '../../src/config/config.service' diff --git a/bots/market-making/test/domain/bootstrap/position-bootstrap.test.ts b/bots/market-making/test/domain/bootstrap/position-bootstrap.test.ts index 4c02bcc8..8fcc2e76 100644 --- a/bots/market-making/test/domain/bootstrap/position-bootstrap.test.ts +++ b/bots/market-making/test/domain/bootstrap/position-bootstrap.test.ts @@ -1,6 +1,6 @@ import type { Hex } from 'viem' -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import { BootstrapConfigurationError } from '../../../src/domain/bootstrap/bootstrap-configuration.error' import { @@ -39,7 +39,7 @@ const parameters = { } describe('validateBootstrapConfig', () => { - test.each(['0x12', `0x${'gg'.repeat(32)}`, `0x${'11'.repeat(31)}`])( + test.each<`0x${string}`>(['0x12', `0x${'gg'.repeat(32)}`, `0x${'11'.repeat(31)}`])( 'rejects malformed market id %s', malformedMarketId => { expect(() => diff --git a/bots/market-making/test/domain/ladder/ladder.test.ts b/bots/market-making/test/domain/ladder/ladder.test.ts index 27e25679..89dea49d 100644 --- a/bots/market-making/test/domain/ladder/ladder.test.ts +++ b/bots/market-making/test/domain/ladder/ladder.test.ts @@ -1,6 +1,6 @@ import type { Hex } from 'viem' -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import { generateLadder, diff --git a/bots/market-making/test/e2e/anvil.ts b/bots/market-making/test/e2e/anvil.ts index d9f28316..ebc724c3 100644 --- a/bots/market-making/test/e2e/anvil.ts +++ b/bots/market-making/test/e2e/anvil.ts @@ -1,11 +1,15 @@ -import type { Subprocess } from 'bun' +import type { ChildProcess } from 'node:child_process' +import { spawn } from 'node:child_process' +import { setTimeout as sleep } from 'node:timers/promises' import { createTestClient, http, publicActions } from 'viem' import { base } from 'viem/chains' import { AnvilStartupError } from './anvil-startup.error' const BASE_FORK_BLOCK = 48_900_000n +// 8546 is this suite's slot in the repo-wide anvil port registry (see the liquidation bots' +// fork harnesses); vitest runs test files in parallel, so the ports must not overlap. const DEFAULT_ANVIL_PORT = 8546 const STARTUP_POLL_INTERVAL_MS = 100 const STARTUP_TIMEOUT_MS = 10_000 @@ -29,8 +33,10 @@ const requireForkUrl = () => { export type AnvilHandle = { client: ReturnType - process: Subprocess + process: ChildProcess rpcUrl: string + /** Resolves once the child has actually exited. Node has no `.exited`, so it is built at spawn. */ + exited: Promise } const waitForAnvil = async (handle: AnvilHandle) => { @@ -51,7 +57,7 @@ const waitForAnvil = async (handle: AnvilHandle) => { cause = error } - await Bun.sleep(STARTUP_POLL_INTERVAL_MS) + await sleep(STARTUP_POLL_INTERVAL_MS) } throw new AnvilStartupError(`Anvil was not ready within ${STARTUP_TIMEOUT_MS}ms`, { cause }) @@ -68,7 +74,7 @@ export const stopAnvil = async (handle: AnvilHandle | undefined) => { if (!handle) return if (handle.process.exitCode === null) handle.process.kill('SIGKILL') - await handle.process.exited + await handle.exited } /** @@ -83,9 +89,9 @@ export const stopAnvil = async (handle: AnvilHandle | undefined) => { */ export const startAnvil = async (port = DEFAULT_ANVIL_PORT): Promise => { const forkUrl = requireForkUrl() - const process = Bun.spawn( + const child = spawn( + 'anvil', [ - Bun.which('anvil') ?? 'anvil', '--fork-url', forkUrl, '--fork-block-number', @@ -99,10 +105,14 @@ export const startAnvil = async (port = DEFAULT_ANVIL_PORT): Promise(resolve => { + child.once('exit', () => resolve()) + }) + const handle = { client: createAnvilClient(rpcUrl), process: child, rpcUrl, exited } try { await waitForAnvil(handle) diff --git a/bots/market-making/test/e2e/setup-api.ts b/bots/market-making/test/e2e/setup-api.ts index 943afc3d..99ef259a 100644 --- a/bots/market-making/test/e2e/setup-api.ts +++ b/bots/market-making/test/e2e/setup-api.ts @@ -1,4 +1,7 @@ -import type { Server } from 'bun' +import type { IncomingMessage, Server, ServerResponse } from 'node:http' +import type { AddressInfo } from 'node:net' + +import { createServer } from 'node:http' import { ECRECOVER_RATIFIER, MARKET, MARKET_ID } from './constants' @@ -58,19 +61,61 @@ const route = (request: Request) => { export type SetupApiHandle = { baseUrl: string - server: Server + server: Server } +// bun's `Bun.serve` took a Web-standard `(Request) => Response` handler directly. Node's http server +// speaks IncomingMessage/ServerResponse, so this adapts between the two and leaves `route` untouched. +// Node's request listener is void-returning, so the async work is wrapped: the promise is consumed +// here and a handler failure answers 500 rather than surfacing as an unhandled rejection. +const toNodeListener = + (handle: (incoming: IncomingMessage, outgoing: ServerResponse) => Promise) => + (incoming: IncomingMessage, outgoing: ServerResponse): void => { + void handle(incoming, outgoing).catch(() => { + if (!outgoing.headersSent) outgoing.writeHead(500) + outgoing.end() + }) + } + +const toNodeHandler = + (handler: (request: Request) => Response) => + async ( + incoming: import('node:http').IncomingMessage, + outgoing: import('node:http').ServerResponse + ) => { + const url = `http://${incoming.headers.host ?? '127.0.0.1'}${incoming.url ?? '/'}` + const headers = new Headers() + for (const [key, value] of Object.entries(incoming.headers)) { + if (typeof value === 'string') headers.set(key, value) + else if (Array.isArray(value)) for (const v of value) headers.append(key, v) + } + const method = incoming.method ?? 'GET' + const body = + method === 'GET' || method === 'HEAD' + ? undefined + : await new Promise(resolve => { + const chunks: Buffer[] = [] + incoming.on('data', chunk => chunks.push(chunk as Buffer)) + incoming.on('end', () => resolve(Buffer.concat(chunks))) + }) + const response = handler(new Request(url, { method, headers, body })) + outgoing.writeHead(response.status, Object.fromEntries(response.headers)) + outgoing.end(Buffer.from(await response.arrayBuffer())) + } + /** * Starts deterministic Morpho and Router API fixtures for setup-check provider reads. * - * @returns A loopback API origin and the running Bun server. + * @returns A loopback API origin and the running Node http server. * @remarks The responses mirror the pinned market's immutable Base state and intentionally contain - * no maker offers. The caller must pass the result to {@link stopSetupApi}. + * no maker offers. Binds port 0 so parallel suites cannot collide. The caller must pass the result to + * {@link stopSetupApi}. */ -export const startSetupApi = (): SetupApiHandle => { - const server = Bun.serve({ hostname: '127.0.0.1', port: 0, fetch: route }) - return { baseUrl: server.url.origin, server } +export const startSetupApi = async (): Promise => { + const server = createServer(toNodeListener(toNodeHandler(route))) + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) + const { port } = server.address() as AddressInfo + return { baseUrl: `http://127.0.0.1:${port}`, server } } /** @@ -80,5 +125,9 @@ export const startSetupApi = (): SetupApiHandle => { * @returns A promise that resolves after active connections close. */ export const stopSetupApi = async (handle: SetupApiHandle | undefined) => { - if (handle) await handle.server.stop(true) + if (!handle) return + handle.server.closeAllConnections() + await new Promise((resolve, reject) => { + handle.server.close(error => (error ? reject(error) : resolve())) + }) } diff --git a/bots/market-making/test/e2e/setup-check.e2e.test.ts b/bots/market-making/test/e2e/setup-check.e2e.test.ts index 75634349..d8f00673 100644 --- a/bots/market-making/test/e2e/setup-check.e2e.test.ts +++ b/bots/market-making/test/e2e/setup-check.e2e.test.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeAll, describe, expect, test } from 'bun:test' +import { afterAll, beforeAll, describe, expect, test } from 'vitest' import type { SetupCheckReport } from '../../src/application/setup/setup-check.service' import type { AnvilHandle } from './anvil' @@ -35,7 +35,7 @@ describe('market-making setup check on a pinned Base fork', () => { beforeAll(async () => { anvil = await startAnvil() - api = startSetupApi() + api = await startSetupApi() const setup = await setupMaker(anvil) expect(setup.balance).toBe(MAKER_USDC_BALANCE) diff --git a/bots/market-making/test/error-convention.test.ts b/bots/market-making/test/error-convention.test.ts index 5bb0adbc..8e7649ac 100644 --- a/bots/market-making/test/error-convention.test.ts +++ b/bots/market-making/test/error-convention.test.ts @@ -1,23 +1,16 @@ -import { describe, expect, test } from 'bun:test' import { readdirSync, readFileSync } from 'node:fs' -import { basename, extname, join, relative } from 'node:path' +import { basename, dirname, extname, join, relative } from 'node:path' +import { fileURLToPath } from 'node:url' import ts from 'typescript' +import { describe, expect, test } from 'vitest' -const packageRoot = join(import.meta.dir, '..') -const roots = [ - join(packageRoot, 'src'), - join(packageRoot, 'scripts'), - join(packageRoot, 'playground') -] +const packageRoot = join(dirname(fileURLToPath(import.meta.url)), '..') +const roots = [join(packageRoot, 'src'), join(packageRoot, 'scripts')] const sourceFiles = roots.flatMap(root => { const walk = (directory: string): string[] => readdirSync(directory, { withFileTypes: true }).flatMap(entry => { const path = join(directory, entry.name) - return entry.isDirectory() - ? walk(path) - : ['.ts', '.tsx'].includes(extname(path)) - ? [path] - : [] + return entry.isDirectory() ? walk(path) : extname(path) === '.ts' ? [path] : [] }) return walk(root) }) diff --git a/bots/market-making/test/infrastructure/bootstrap/bootstrap-make.service.test.ts b/bots/market-making/test/infrastructure/bootstrap/bootstrap-make.service.test.ts index aeb240a1..4c255c6b 100644 --- a/bots/market-making/test/infrastructure/bootstrap/bootstrap-make.service.test.ts +++ b/bots/market-making/test/infrastructure/bootstrap/bootstrap-make.service.test.ts @@ -1,6 +1,6 @@ import type { Hex } from 'viem' -import { describe, expect, mock, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import type { BootstrapSubmittedTransaction } from '../../../src/application/bootstrap/position-bootstrap-verbose' @@ -76,204 +76,6 @@ describe('MidnightBootstrapMakeService', () => { expect(events).toEqual(['book', 'publish']) }) - test('retains an active group when raw rates resolve to the same protocol tick', async () => { - const metadataUpdates: string[] = [] - const preparePublication = mock(async () => ({ - groupId: publishedGroupId, - publish: async () => publicationHash - })) - const invalidate = mock(async () => cancellationHash) - const service = new MidnightBootstrapMakeService({ - listActiveGroups: async () => [ - { - id: groupId, - marketId, - assets: 100n, - maximumAssets: 140n, - rateBps: 499n, - tick: 100n, - offerCount: 1, - continuousFeeCap: 17n - } - ], - listBookOffers: async () => [], - toProspectiveBookOffer: async () => ({ - marketId, - buy: true, - tick: 100n, - continuousFeeCap: 17n - }), - preparePublication, - reserveGroup: async (id, offer) => { - metadataUpdates.push( - `reserve:${id}:${offer.assets}:${offer.rateBps}:${offer.tick}:${offer.continuousFeeCap}` - ) - }, - confirmPublishedGroup: async id => { - metadataUpdates.push(`confirm:${id}`) - }, - releaseGroupReservation: async () => {}, - invalidate - }) - - const result = await service.reconcile({ marketId, desiredOffer, reason: 'replace' }) - - expect(result).toBe('unchanged') - expect(preparePublication).not.toHaveBeenCalled() - expect(invalidate).not.toHaveBeenCalled() - expect(metadataUpdates).toEqual([`reserve:${groupId}:140:500:100:17`, `confirm:${groupId}`]) - }) - - test('replaces a matching offer after the market continuous fee changes', async () => { - const events: string[] = [] - const service = new MidnightBootstrapMakeService({ - listActiveGroups: async () => [ - { - id: groupId, - marketId, - assets: 100n, - maximumAssets: 100n, - rateBps: 500n, - tick: 100n, - offerCount: 1, - continuousFeeCap: 16n - } - ], - listBookOffers: async () => [], - toProspectiveBookOffer: async () => ({ - marketId, - buy: true, - tick: 100n, - continuousFeeCap: 17n - }), - preparePublication: async () => ({ - groupId: publishedGroupId, - tick: 100n, - publish: async () => { - events.push('publish') - } - }), - reserveGroup: async (id, offer) => { - events.push(`reserve:${id}:${offer.continuousFeeCap}`) - }, - confirmPublishedGroup: async id => { - events.push(`confirm:${id}`) - }, - releaseGroupReservation: async () => {}, - invalidate: async id => { - events.push(`invalidate:${id}`) - } - }) - - await service.reconcile({ marketId, desiredOffer, reason: 'replace' }) - - expect(events).toEqual([ - `reserve:${publishedGroupId}:17`, - `invalidate:${groupId}`, - 'publish', - `confirm:${publishedGroupId}` - ]) - }) - - test('replaces a matching projection when its group contains multiple offers', async () => { - const events: string[] = [] - const service = new MidnightBootstrapMakeService({ - listActiveGroups: async () => [ - { - id: groupId, - marketId, - assets: 100n, - maximumAssets: 100n, - rateBps: 500n, - tick: 100n, - offerCount: 2 - } - ], - listBookOffers: async () => [], - toProspectiveBookOffer: async () => ({ marketId, buy: true, tick: 100n }), - preparePublication: async () => ({ - groupId: publishedGroupId, - tick: 100n, - publish: async () => { - events.push('publish') - } - }), - reserveGroup: async id => { - events.push(`reserve:${id}`) - }, - confirmPublishedGroup: async id => { - events.push(`confirm:${id}`) - }, - releaseGroupReservation: async () => {}, - invalidate: async id => { - events.push(`invalidate:${id}`) - } - }) - - await service.reconcile({ marketId, desiredOffer, reason: 'replace' }) - - expect(events).toEqual([ - `reserve:${publishedGroupId}`, - `invalidate:${groupId}`, - 'publish', - `confirm:${publishedGroupId}` - ]) - }) - - test('preserves duplicate cancellation evidence when retained metadata refresh fails', async () => { - const service = new MidnightBootstrapMakeService({ - listActiveGroups: async () => [ - { - id: groupId, - marketId, - assets: 100n, - maximumAssets: 100n, - rateBps: 500n, - tick: 100n, - offerCount: 1, - continuousFeeCap: 17n - }, - { - id: publishedGroupId, - marketId, - assets: 100n, - rateBps: 500n, - tick: 100n, - offerCount: 1, - continuousFeeCap: 17n - } - ], - listBookOffers: async () => [], - toProspectiveBookOffer: async () => ({ - marketId, - buy: true, - tick: 100n, - continuousFeeCap: 17n - }), - preparePublication: async () => ({ - groupId: publishedGroupId, - publish: async () => publicationHash - }), - reserveGroup: async () => { - throw new BootstrapAdapterError('group-ownership-state') - }, - confirmPublishedGroup: async () => {}, - releaseGroupReservation: async () => {}, - forgetGroups: async () => {}, - invalidate: async () => cancellationHash - }) - - const error = await service - .reconcile({ marketId, desiredOffer, reason: 'replace' }) - .catch(value => value) - - expect(error).toBeInstanceOf(BootstrapAdapterError) - expect(error).toMatchObject({ - operation: 'group-ownership-state', - confirmedTransactions: [{ operation: 'cancel', txHash: cancellationHash }] - }) - }) - test('returns confirmed cancellation and publication hashes in submission order', async () => { const submitted: BootstrapSubmittedTransaction[] = [] const service = new MidnightBootstrapMakeService({ diff --git a/bots/market-making/test/infrastructure/bootstrap/bootstrap-mempool-validation.utils.test.ts b/bots/market-making/test/infrastructure/bootstrap/bootstrap-mempool-validation.utils.test.ts index 6d5ce85d..9f4ce2cc 100644 --- a/bots/market-making/test/infrastructure/bootstrap/bootstrap-mempool-validation.utils.test.ts +++ b/bots/market-making/test/infrastructure/bootstrap/bootstrap-mempool-validation.utils.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import { BootstrapAdapterError } from '../../../src/infrastructure/bootstrap/bootstrap-adapter.error' import { BootstrapMempoolValidationError } from '../../../src/infrastructure/bootstrap/bootstrap-mempool-validation.error' diff --git a/bots/market-making/test/infrastructure/bootstrap/bootstrap-pending-offer.utils.test.ts b/bots/market-making/test/infrastructure/bootstrap/bootstrap-pending-offer.utils.test.ts index 36944f77..9064e334 100644 --- a/bots/market-making/test/infrastructure/bootstrap/bootstrap-pending-offer.utils.test.ts +++ b/bots/market-making/test/infrastructure/bootstrap/bootstrap-pending-offer.utils.test.ts @@ -1,26 +1,23 @@ import type { Address, Hex } from 'viem' -import { describe, expect, mock, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import type { BootstrapRawGroup } from '../../../src/infrastructure/bootstrap/bootstrap-groups.utils' import { - pendingBootstrapOffers, - readLivePendingBootstrapOffers + pendingBootstrapGroups, + pendingBootstrapOffers } from '../../../src/infrastructure/bootstrap/bootstrap-pending-offer.utils' const marketId: Hex = `0x${'11'.repeat(32)}` const groupId: Hex = `0x${'22'.repeat(32)}` -const pendingGroupId: Hex = `0x${'44'.repeat(32)}` const maker: Address = '0x3333333333333333333333333333333333333333' const offer = { groupId, marketId, assets: 100n, rateBps: 450n, - referenceObservationId: 'blocks:100-200', - tick: 123n, - continuousFeeCap: 17n + referenceObservationId: 'blocks:100-200' } const indexedGroup = (consumed: bigint): BootstrapRawGroup => ({ @@ -33,9 +30,18 @@ const indexedGroup = (consumed: bigint): BootstrapRawGroup => ({ offers: [{ marketId, maker, buy: true, tick: 1n }] }) -describe('pendingBootstrapOffers', () => { - test('selects persisted publication intent while API indexing is pending', () => { +describe('pendingBootstrapGroups', () => { + test('projects persisted publication intent while API indexing is pending', () => { expect(pendingBootstrapOffers([], [offer])).toEqual([offer]) + expect(pendingBootstrapGroups([], [offer])).toEqual([ + { + id: groupId, + marketId, + assets: 100n, + rateBps: 450n, + referenceObservationId: 'blocks:100-200' + } + ]) }) test.each([0n, 100n])( @@ -43,81 +49,7 @@ describe('pendingBootstrapOffers', () => { consumed => { const groups = [indexedGroup(consumed)] expect(pendingBootstrapOffers(groups, [offer])).toEqual([]) + expect(pendingBootstrapGroups(groups, [offer])).toEqual([]) } ) }) - -describe('readLivePendingBootstrapOffers', () => { - test('resolves an indexed owned group without persisted intent when offers are empty', async () => { - const readGroupConsumed = mock(async () => 0n) - - const resolution = readLivePendingBootstrapOffers({ - groups: [indexedGroup(0n)], - ownedGroupIds: [groupId], - offers: [], - readGroupConsumed - }) - - // Indexed groups are provider-projected, so they are absent from pending reconstruction output. - await expect(resolution).resolves.toEqual([]) - expect(readGroupConsumed).not.toHaveBeenCalled() - }) - - test('accepts a provider-indexed owned group without persisted intent while resolving pending offers', async () => { - const readGroupConsumed = mock(async () => 40n) - const pendingOffer = { ...offer, groupId: pendingGroupId } - - const result = await readLivePendingBootstrapOffers({ - groups: [indexedGroup(0n)], - ownedGroupIds: [groupId, pendingGroupId], - offers: [pendingOffer], - readGroupConsumed - }) - - expect(result).toEqual([{ ...pendingOffer, maximumAssets: 100n, assets: 60n }]) - expect(readGroupConsumed).toHaveBeenCalledTimes(1) - expect(readGroupConsumed).toHaveBeenCalledWith(pendingGroupId) - }) - - test('fails closed when an owned group is API-missing without persisted offer intent', async () => { - const readGroupConsumed = mock(async () => 0n) - - await expect( - readLivePendingBootstrapOffers({ - groups: [], - ownedGroupIds: [groupId], - offers: [], - readGroupConsumed - }) - ).rejects.toMatchObject({ operation: 'missing-owned-group-intent' }) - expect(readGroupConsumed).not.toHaveBeenCalled() - }) - - test('retains the remaining capacity of an API-missing partially consumed offer', async () => { - const readGroupConsumed = mock(async () => 40n) - - const result = await readLivePendingBootstrapOffers({ - groups: [], - ownedGroupIds: [groupId], - offers: [offer], - readGroupConsumed - }) - - expect(result).toEqual([{ ...offer, maximumAssets: 100n, assets: 60n }]) - expect(readGroupConsumed).toHaveBeenCalledWith(groupId) - }) - - test('omits an API-missing offer that is fully consumed on-chain', async () => { - const readGroupConsumed = mock(async () => 100n) - - const result = await readLivePendingBootstrapOffers({ - groups: [], - ownedGroupIds: [groupId], - offers: [offer], - readGroupConsumed - }) - - expect(result).toEqual([]) - expect(readGroupConsumed).toHaveBeenCalledWith(groupId) - }) -}) diff --git a/bots/market-making/test/infrastructure/bootstrap/bootstrap-position.service.test.ts b/bots/market-making/test/infrastructure/bootstrap/bootstrap-position.service.test.ts index dab2b9b6..22b1effe 100644 --- a/bots/market-making/test/infrastructure/bootstrap/bootstrap-position.service.test.ts +++ b/bots/market-making/test/infrastructure/bootstrap/bootstrap-position.service.test.ts @@ -1,6 +1,6 @@ import type { Address, Hex } from 'viem' -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import { MidnightBootstrapPositionService } from '../../../src/infrastructure/bootstrap/bootstrap-position.service' diff --git a/bots/market-making/test/infrastructure/bootstrap/bootstrap-reference-rate.service.test.ts b/bots/market-making/test/infrastructure/bootstrap/bootstrap-reference-rate.service.test.ts index 7c67e3b6..64e14694 100644 --- a/bots/market-making/test/infrastructure/bootstrap/bootstrap-reference-rate.service.test.ts +++ b/bots/market-making/test/infrastructure/bootstrap/bootstrap-reference-rate.service.test.ts @@ -1,6 +1,6 @@ import type { Hex } from 'viem' -import { afterEach, describe, expect, setSystemTime, test } from 'bun:test' +import { afterEach, describe, expect, setSystemTime, test } from 'vitest' import { BootstrapAdapterError } from '../../../src/infrastructure/bootstrap/bootstrap-adapter.error' import { diff --git a/bots/market-making/test/infrastructure/bootstrap/bootstrap-spread.utils.test.ts b/bots/market-making/test/infrastructure/bootstrap/bootstrap-spread.utils.test.ts index e924ad4b..a7b62dc4 100644 --- a/bots/market-making/test/infrastructure/bootstrap/bootstrap-spread.utils.test.ts +++ b/bots/market-making/test/infrastructure/bootstrap/bootstrap-spread.utils.test.ts @@ -1,6 +1,6 @@ import type { Hex } from 'viem' -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import { BootstrapAdapterError } from '../../../src/infrastructure/bootstrap/bootstrap-adapter.error' import { diff --git a/bots/market-making/test/infrastructure/bootstrap/production-bootstrap.test.ts b/bots/market-making/test/infrastructure/bootstrap/production-bootstrap.test.ts index 9431c82b..b545130f 100644 --- a/bots/market-making/test/infrastructure/bootstrap/production-bootstrap.test.ts +++ b/bots/market-making/test/infrastructure/bootstrap/production-bootstrap.test.ts @@ -10,11 +10,11 @@ import { type IMarketParams, setterRatifierAbi } from '@morpho-org/midnight-sdk' -import { describe, expect, test } from 'bun:test' import { mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { encodeFunctionData } from 'viem' +import { describe, expect, test } from 'vitest' import { ConfigService } from '../../../src/config/config.service' import { BootstrapAdapterError } from '../../../src/infrastructure/bootstrap/bootstrap-adapter.error' diff --git a/bots/market-making/test/infrastructure/cli/cli.test.ts b/bots/market-making/test/infrastructure/cli/cli.test.ts index ebaa97ac..1d6fb2bc 100644 --- a/bots/market-making/test/infrastructure/cli/cli.test.ts +++ b/bots/market-making/test/infrastructure/cli/cli.test.ts @@ -1,4 +1,7 @@ -import { describe, expect, mock, test } from 'bun:test' +import { $ } from 'execa' +import { dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +import { describe, expect, test, vi } from 'vitest' import type { LadderTransactionSubmittedEvent } from '../../../src/application/ladder/ladder-verbose' @@ -37,21 +40,26 @@ const cli = (assertReady = async () => readyReport) => { ) } +const REPO_ROOT = `${dirname(fileURLToPath(import.meta.url))}/../../../../..` + +// The entrypoint is TypeScript, so the subprocess runs it through tsx rather than a bare node. `reject: +// false` keeps a non-zero exit as a value (these tests assert on exit codes), and the env is passed +// explicitly so no ambient config leaks in — only PATH plus whatever the case sets. +const runEntrypointWith = async ( + argv: readonly string[], + env: Record = {} +) => { + const { exitCode, stdout, stderr } = await $({ + cwd: REPO_ROOT, + env: { PATH: process.env.PATH, ...env }, + extendEnv: false, + reject: false + })`tsx bots/market-making/src/index.ts ${argv}` + return { exitCode: exitCode ?? 0, stdout, stderr } +} + const runEntrypoint = async (argv: readonly string[]) => { - const process = Bun.spawn( - [Bun.which('bun') ?? 'bun', 'bots/market-making/src/index.ts', ...argv], - { - cwd: `${import.meta.dir}/../../../../..`, - env: { PATH: Bun.env.PATH }, - stdout: 'pipe', - stderr: 'pipe' - } - ) - const [exitCode, stdout, stderr] = await Promise.all([ - process.exited, - new Response(process.stdout).text(), - new Response(process.stderr).text() - ]) + const { exitCode, stdout, stderr } = await runEntrypointWith(argv) return { exitCode, output: stdout + stderr } } @@ -68,20 +76,7 @@ describe('Cli', () => { }) test('entrypoint --version succeeds without loading runtime setup environment', async () => { - const process = Bun.spawn( - [Bun.which('bun') ?? 'bun', 'bots/market-making/src/index.ts', '--version'], - { - cwd: `${import.meta.dir}/../../../../..`, - env: { PATH: Bun.env.PATH }, - stdout: 'pipe', - stderr: 'pipe' - } - ) - const [exitCode, stdout, stderr] = await Promise.all([ - process.exited, - new Response(process.stdout).text(), - new Response(process.stderr).text() - ]) + const { exitCode, stdout, stderr } = await runEntrypointWith(['--version']) expect(exitCode).toBe(0) expect(stdout.trim()).toBe('0.0.0') @@ -89,20 +84,7 @@ describe('Cli', () => { }) test('entrypoint --json --version emits a valid JSON string', async () => { - const process = Bun.spawn( - [Bun.which('bun') ?? 'bun', 'bots/market-making/src/index.ts', '--json', '--version'], - { - cwd: `${import.meta.dir}/../../../../..`, - env: { PATH: Bun.env.PATH }, - stdout: 'pipe', - stderr: 'pipe' - } - ) - const [exitCode, stdout, stderr] = await Promise.all([ - process.exited, - new Response(process.stdout).text(), - new Response(process.stderr).text() - ]) + const { exitCode, stdout, stderr } = await runEntrypointWith(['--json', '--version']) expect(exitCode).toBe(0) expect(JSON.parse(stdout)).toBe('0.0.0') @@ -127,37 +109,23 @@ describe('Cli', () => { 'fragment', '"origin"' ] - const process = Bun.spawn( - [Bun.which('bun') ?? 'bun', 'bots/market-making/src/index.ts', 'setup-check'], - { - cwd: `${import.meta.dir}/../../../../..`, - env: { - PATH: Bun.env.PATH, - CHAIN_ID: '8453', - RPC_URL: `https://${markers[0]}:${markers[1]}@127.0.0.1:1/rpc?key=${markers[2]}#fragment`, - REFERENCE_RPC_URL: `http://127.0.0.1:1/archive?token=${markers[3]}`, - MAKER_PRIVATE_KEY: `0x${'11'.repeat(32)}`, - MAKER_ADDRESS: '0x19E7E376E7C213B7E7e7e46cc70A5dD086DAff2A', - MIDNIGHT_ADDRESS: '0x2222222222222222222222222222222222222222', - LOAN_ASSET_ADDRESS: '0x3333333333333333333333333333333333333333', - RATIFIER_ADDRESS: '0xd6e70365C8E8DDa9a4ca662C07bbE663b017755E', - MARKET_IDS: `0x${'55'.repeat(32)}`, - REFERENCE_MARKET_ID: `0x${'77'.repeat(32)}`, - NATIVE_RESERVE_WEI: '10', - MAXIMUM_LEND_EXPOSURE_ASSETS: '100', - MORPHO_API_BASE_URL: `http://127.0.0.1:1/morpho?key=${markers[4]}`, - ROUTER_API_BASE_URL: `http://127.0.0.1:1/router?key=${markers[5]}`, - REQUEST_TIMEOUT_MS: '50' - }, - stdout: 'pipe', - stderr: 'pipe' - } - ) - const [exitCode, stdout, stderr] = await Promise.all([ - process.exited, - new Response(process.stdout).text(), - new Response(process.stderr).text() - ]) + const { exitCode, stdout, stderr } = await runEntrypointWith(['setup-check'], { + CHAIN_ID: '8453', + RPC_URL: `https://${markers[0]}:${markers[1]}@127.0.0.1:1/rpc?key=${markers[2]}#fragment`, + REFERENCE_RPC_URL: `http://127.0.0.1:1/archive?token=${markers[3]}`, + MAKER_PRIVATE_KEY: `0x${'11'.repeat(32)}`, + MAKER_ADDRESS: '0x19E7E376E7C213B7E7e7e46cc70A5dD086DAff2A', + MIDNIGHT_ADDRESS: '0x2222222222222222222222222222222222222222', + LOAN_ASSET_ADDRESS: '0x3333333333333333333333333333333333333333', + RATIFIER_ADDRESS: '0xd6e70365C8E8DDa9a4ca662C07bbE663b017755E', + MARKET_IDS: `0x${'55'.repeat(32)}`, + REFERENCE_MARKET_ID: `0x${'77'.repeat(32)}`, + NATIVE_RESERVE_WEI: '10', + MAXIMUM_LEND_EXPOSURE_ASSETS: '100', + MORPHO_API_BASE_URL: `http://127.0.0.1:1/morpho?key=${markers[4]}`, + ROUTER_API_BASE_URL: `http://127.0.0.1:1/router?key=${markers[5]}`, + REQUEST_TIMEOUT_MS: '50' + }) const output = stdout + stderr expect(exitCode).toBe(1) @@ -272,8 +240,8 @@ describe('Cli', () => { reason: 'signal' as const, cycles: 1 } - const assertReady = mock(async () => readyReport) - const runContinuously = mock( + const assertReady = vi.fn(async () => readyReport) + const runContinuously = vi.fn( async (parameters: { signal: AbortSignal onCycle?: (result: typeof readyReport) => void | Promise @@ -328,7 +296,7 @@ describe('Cli', () => { }) test('mm bootstrap triggers one explicit position-bootstrap run', async () => { - const runOnce = mock(async () => [ + const runOnce = vi.fn(async () => [ { marketId: `0x${'11'.repeat(32)}`, status: 'applied', action: 'publish', assets: 10n } ]) const application = new Cli( @@ -347,7 +315,7 @@ describe('Cli', () => { test('mm bootstrap --verbose requests expanded bootstrap diagnostics', async () => { const txHash = `0x${'aa'.repeat(32)}` as const - const runOnce = mock( + const runOnce = vi.fn( async (parameters?: { verbose?: boolean onTransactionSubmitted?: (event: { @@ -397,8 +365,8 @@ describe('Cli', () => { cycles: 1, cleanup: { status: 'applied' as const } } - const runOnce = mock(async () => []) - const runContinuously = mock( + const runOnce = vi.fn(async () => []) + const runContinuously = vi.fn( async (parameters: { signal: AbortSignal onCycle?: (result: readonly { status: string; [key: string]: unknown }[]) => void @@ -432,7 +400,7 @@ describe('Cli', () => { test('mm bootstrap --monitor --verbose forwards verbose monitoring', async () => { const controller = new AbortController() - const runContinuously = mock( + const runContinuously = vi.fn( async (_parameters: { signal: AbortSignal; verbose?: boolean }) => ({ status: 'stopped' as const, reason: 'signal' as const, @@ -490,9 +458,9 @@ describe('Cli', () => { }) test('mm ladder is exposed alongside setup-check and bootstrap', async () => { - const assertReady = mock(async () => readyReport) - const bootstrap = mock(async () => []) - const runOnce = mock(async () => [ + const assertReady = vi.fn(async () => readyReport) + const bootstrap = vi.fn(async () => []) + const runOnce = vi.fn(async () => [ { marketId: `0x${'11'.repeat(32)}`, status: 'observed', action: 'rest' } ]) const application = new Cli( @@ -519,7 +487,7 @@ describe('Cli', () => { matchedGroups: 1, invalidatedGroups: [{ groupId, txHash }] } - const run = mock( + const run = vi.fn( async (parameters?: { groupId?: `0x${string}` onTransactionSubmitted?: (event: { @@ -561,7 +529,7 @@ describe('Cli', () => { test('mm invalidate canonicalizes one explicit group and forwards read-only mode', async () => { const groupId = `0x${'ab'.repeat(32)}` as const let readOnly: boolean | undefined - const run = mock(async (_parameters?: { groupId?: `0x${string}` }) => ({ + const run = vi.fn(async (_parameters?: { groupId?: `0x${string}` }) => ({ status: 'logged' as const, scope: 'group' as const, matchedGroups: 1, @@ -585,7 +553,7 @@ describe('Cli', () => { }) test('mm invalidate rejects a malformed group before constructing the writer', async () => { - const invalidation = mock(() => ({ run: async () => undefined as never })) + const invalidation = vi.fn(() => ({ run: async () => undefined as never })) const application = new Cli( new VersionService(), () => ({ assertReady: async () => readyReport }), @@ -611,7 +579,7 @@ describe('Cli', () => { cleanup: { status: 'logged' as const } } let readOnly: boolean | undefined - const runContinuously = mock( + const runContinuously = vi.fn( async (parameters: { signal: AbortSignal verbose?: boolean @@ -730,7 +698,7 @@ describe('Cli', () => { } let readOnly: boolean | undefined let factoryWriteEvent: ((value: unknown) => void | Promise) | undefined - const runContinuously = mock( + const runContinuously = vi.fn( async (parameters: { signal: AbortSignal verbose?: boolean @@ -1219,6 +1187,8 @@ describe('Cli', () => { test('propagates a readiness failure for a deterministic non-zero entrypoint exit', async () => { const failure = new Error('Setup check failed: chain') - expect(cli(async () => Promise.reject(failure)).run(['setup-check'])).rejects.toBe(failure) + await expect(cli(async () => Promise.reject(failure)).run(['setup-check'])).rejects.toBe( + failure + ) }) }) diff --git a/bots/market-making/test/infrastructure/cli/market-making-entrypoint-observability.test.ts b/bots/market-making/test/infrastructure/cli/market-making-entrypoint-observability.test.ts index fec937ba..2b56a34f 100644 --- a/bots/market-making/test/infrastructure/cli/market-making-entrypoint-observability.test.ts +++ b/bots/market-making/test/infrastructure/cli/market-making-entrypoint-observability.test.ts @@ -1,5 +1,5 @@ import { enhanceVerboseArgv } from '@repo/observability' -import { describe, expect, mock, test } from 'bun:test' +import { describe, expect, test, vi } from 'vitest' import { ConfigFileError } from '../../../src/config/config-file.error' import { @@ -12,7 +12,7 @@ describe('runMarketMakingEntrypoint observability', () => { test('mirrors streamed actions and terminal results while preserving stdout', async () => { const stdout: string[] = [] const stderr: string[] = [] - const record = mock((_value: unknown) => undefined) + const record = vi.fn((_value: unknown) => undefined) const cycle = { event: 'ladder.cycle', status: 'resting', activeOffers: [{ assets: 9n }] } const result = { status: 'stopped', cycles: 1 } @@ -26,7 +26,7 @@ describe('runMarketMakingEntrypoint observability', () => { ['ladder', '--monitor'], { writeOut: value => stdout.push(value), writeError: value => stderr.push(value) }, {}, - { record, unexpected: mock(() => undefined) } + { record, unexpected: vi.fn(() => undefined) } ) expect(exitCode).toBe(0) @@ -41,7 +41,7 @@ describe('runMarketMakingEntrypoint observability', () => { test('observes unknown failures by name without leaking their message', async () => { const stdout: string[] = [] const stderr: string[] = [] - const unexpected = mock((_error: unknown, _origin: 'entrypoint') => undefined) + const unexpected = vi.fn((_error: unknown, _origin: 'entrypoint') => undefined) const error = new Error('raw provider payload with api credential') error.name = 'ProviderFailureError' @@ -50,7 +50,7 @@ describe('runMarketMakingEntrypoint observability', () => { ['start'], { writeOut: value => stdout.push(value), writeError: value => stderr.push(value) }, {}, - { record: mock(() => undefined), unexpected } + { record: vi.fn(() => undefined), unexpected } ) expect(exitCode).toBe(1) @@ -82,7 +82,7 @@ describe('runMarketMakingEntrypoint observability', () => { test('suppresses report payloads from errors outside the audited allowlist', async () => { const stdout: string[] = [] const stderr: string[] = [] - const unexpected = mock((_error: unknown, _origin: 'entrypoint') => undefined) + const unexpected = vi.fn((_error: unknown, _origin: 'entrypoint') => undefined) const error = Object.assign(new Error('raw provider payload'), { report: { secret: 'hostile provider response body' } }) @@ -93,7 +93,7 @@ describe('runMarketMakingEntrypoint observability', () => { ['start'], { writeOut: value => stdout.push(value), writeError: value => stderr.push(value) }, {}, - { record: mock(() => undefined), unexpected } + { record: vi.fn(() => undefined), unexpected } ) expect(exitCode).toBe(1) @@ -122,14 +122,14 @@ describe('runMarketMakingEntrypoint observability', () => { test('preserves audited configuration diagnostics while still classifying the failure', async () => { const stderr: string[] = [] - const unexpected = mock((_error: unknown, _origin: 'entrypoint') => undefined) + const unexpected = vi.fn((_error: unknown, _origin: 'entrypoint') => undefined) const exitCode = await runMarketMakingEntrypoint( { run: async () => Promise.reject(new ConfigFileError('malformed')) }, ['start'], { writeOut: () => undefined, writeError: value => stderr.push(value) }, {}, - { record: mock(() => undefined), unexpected } + { record: vi.fn(() => undefined), unexpected } ) expect(exitCode).toBe(1) diff --git a/bots/market-making/test/infrastructure/invalidation/offer-invalidation-group.utils.test.ts b/bots/market-making/test/infrastructure/invalidation/offer-invalidation-group.utils.test.ts index 516030ca..ec0e9f97 100644 --- a/bots/market-making/test/infrastructure/invalidation/offer-invalidation-group.utils.test.ts +++ b/bots/market-making/test/infrastructure/invalidation/offer-invalidation-group.utils.test.ts @@ -1,6 +1,6 @@ import type { Hex } from 'viem' -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import { offerInvalidationGroupIds } from '../../../src/infrastructure/invalidation/offer-invalidation-group.utils' diff --git a/bots/market-making/test/infrastructure/invalidation/offer-invalidation-transaction.utils.test.ts b/bots/market-making/test/infrastructure/invalidation/offer-invalidation-transaction.utils.test.ts index cb73468a..0fe47390 100644 --- a/bots/market-making/test/infrastructure/invalidation/offer-invalidation-transaction.utils.test.ts +++ b/bots/market-making/test/infrastructure/invalidation/offer-invalidation-transaction.utils.test.ts @@ -1,8 +1,8 @@ import type { Address, Hex } from 'viem' import { MAX_OFFER_CAP, midnightAbi } from '@morpho-org/midnight-sdk' -import { describe, expect, test } from 'bun:test' import { encodeFunctionData } from 'viem' +import { describe, expect, test } from 'vitest' import { OfferInvalidationAdapterError } from '../../../src/infrastructure/invalidation/offer-invalidation-adapter.error' import { assertOfferInvalidationTransaction } from '../../../src/infrastructure/invalidation/offer-invalidation-transaction.utils' diff --git a/bots/market-making/test/infrastructure/invalidation/production-offer-invalidation.test.ts b/bots/market-making/test/infrastructure/invalidation/production-offer-invalidation.test.ts index 850c2985..5eb24fdc 100644 --- a/bots/market-making/test/infrastructure/invalidation/production-offer-invalidation.test.ts +++ b/bots/market-making/test/infrastructure/invalidation/production-offer-invalidation.test.ts @@ -1,6 +1,6 @@ import type { Address, Hex } from 'viem' -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import { ConfigService } from '../../../src/config/config.service' import { createProductionOfferInvalidationPort } from '../../../src/infrastructure/invalidation/production-offer-invalidation' diff --git a/bots/market-making/test/infrastructure/ladder/ladder-active-publication.utils.test.ts b/bots/market-making/test/infrastructure/ladder/ladder-active-publication.utils.test.ts index 7389271d..0fa69564 100644 --- a/bots/market-making/test/infrastructure/ladder/ladder-active-publication.utils.test.ts +++ b/bots/market-making/test/infrastructure/ladder/ladder-active-publication.utils.test.ts @@ -1,6 +1,6 @@ import type { Hex } from 'viem' -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import type { BootstrapRawGroup } from '../../../src/infrastructure/bootstrap/bootstrap-groups.utils' import type { OwnedLadderPublication } from '../../../src/infrastructure/ladder/ladder-group-ownership.utils' diff --git a/bots/market-making/test/infrastructure/ladder/ladder-cash-reservation.utils.test.ts b/bots/market-making/test/infrastructure/ladder/ladder-cash-reservation.utils.test.ts index 6efbbab2..d2d72940 100644 --- a/bots/market-making/test/infrastructure/ladder/ladder-cash-reservation.utils.test.ts +++ b/bots/market-making/test/infrastructure/ladder/ladder-cash-reservation.utils.test.ts @@ -1,6 +1,6 @@ import type { Hex } from 'viem' -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import type { OwnedLadderPublication } from '../../../src/infrastructure/ladder/ladder-group-ownership.utils' diff --git a/bots/market-making/test/infrastructure/ladder/ladder-group-ownership.utils.test.ts b/bots/market-making/test/infrastructure/ladder/ladder-group-ownership.utils.test.ts index e6efab8c..88ede75d 100644 --- a/bots/market-making/test/infrastructure/ladder/ladder-group-ownership.utils.test.ts +++ b/bots/market-making/test/infrastructure/ladder/ladder-group-ownership.utils.test.ts @@ -1,9 +1,9 @@ import type { Address, Hex } from 'viem' -import { describe, expect, test } from 'bun:test' import { mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' +import { describe, expect, test } from 'vitest' import type { LadderQuoteSet } from '../../../src/domain/ladder/ladder' diff --git a/bots/market-making/test/infrastructure/ladder/ladder-make.service.test.ts b/bots/market-making/test/infrastructure/ladder/ladder-make.service.test.ts index 2c3d7325..0a33fb0a 100644 --- a/bots/market-making/test/infrastructure/ladder/ladder-make.service.test.ts +++ b/bots/market-making/test/infrastructure/ladder/ladder-make.service.test.ts @@ -1,6 +1,6 @@ import type { Hex } from 'viem' -import { describe, expect, mock, test } from 'bun:test' +import { describe, expect, test, vi } from 'vitest' import type { LadderQuoteSet } from '../../../src/domain/ladder/ladder' import type { LadderOfferTransport } from '../../../src/infrastructure/ladder/ladder-make.service' @@ -344,7 +344,7 @@ describe('MidnightLadderMakeService', () => { test('attempts every active group before reporting aggregate hard-halt failure', async () => { const subject = harness() - const invalidate = mock(async (groupId: Hex) => { + const invalidate = vi.fn(async (groupId: Hex) => { subject.events.push(`cancel:${groupId}`) if (groupId === oldGroup) throw new TypeError('provider detail') }) diff --git a/bots/market-making/test/infrastructure/ladder/ladder-offer.utils.test.ts b/bots/market-making/test/infrastructure/ladder/ladder-offer.utils.test.ts index 503e315e..6b1aff14 100644 --- a/bots/market-making/test/infrastructure/ladder/ladder-offer.utils.test.ts +++ b/bots/market-making/test/infrastructure/ladder/ladder-offer.utils.test.ts @@ -1,7 +1,7 @@ import type { IMarket } from '@morpho-org/midnight-sdk' import type { Address, Hex } from 'viem' -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import type { LadderQuoteSet } from '../../../src/domain/ladder/ladder' diff --git a/bots/market-making/test/infrastructure/ladder/ladder-signature.utils.test.ts b/bots/market-making/test/infrastructure/ladder/ladder-signature.utils.test.ts index f8125d8e..a2e27a58 100644 --- a/bots/market-making/test/infrastructure/ladder/ladder-signature.utils.test.ts +++ b/bots/market-making/test/infrastructure/ladder/ladder-signature.utils.test.ts @@ -1,10 +1,10 @@ import type { Address } from 'viem' import { Offer, Tree } from '@morpho-org/midnight-sdk' -import { describe, expect, test } from 'bun:test' import { createWalletClient, custom, isHex, zeroAddress } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { base } from 'viem/chains' +import { describe, expect, test } from 'vitest' import { signLadderTree } from '../../../src/infrastructure/ladder/ladder-signature.utils' diff --git a/bots/market-making/test/infrastructure/ladder/ladder-spread.utils.test.ts b/bots/market-making/test/infrastructure/ladder/ladder-spread.utils.test.ts index 2404ca69..244c1f36 100644 --- a/bots/market-making/test/infrastructure/ladder/ladder-spread.utils.test.ts +++ b/bots/market-making/test/infrastructure/ladder/ladder-spread.utils.test.ts @@ -1,6 +1,6 @@ import type { Hex } from 'viem' -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import { LadderAdapterError } from '../../../src/infrastructure/ladder/ladder-adapter.error' import { assertLadderProspectiveSpread } from '../../../src/infrastructure/ladder/ladder-spread.utils' diff --git a/bots/market-making/test/infrastructure/ladder/production-ladder.test.ts b/bots/market-making/test/infrastructure/ladder/production-ladder.test.ts index 2ba1e5c7..23ad2286 100644 --- a/bots/market-making/test/infrastructure/ladder/production-ladder.test.ts +++ b/bots/market-making/test/infrastructure/ladder/production-ladder.test.ts @@ -1,6 +1,6 @@ import type { Address, Hex } from 'viem' -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import type { LadderQuoteSet } from '../../../src/domain/ladder/ladder' diff --git a/bots/market-making/test/infrastructure/make/managed-maker-account.utils.test.ts b/bots/market-making/test/infrastructure/make/managed-maker-account.utils.test.ts index 4ff40435..0fa0b886 100644 --- a/bots/market-making/test/infrastructure/make/managed-maker-account.utils.test.ts +++ b/bots/market-making/test/infrastructure/make/managed-maker-account.utils.test.ts @@ -1,6 +1,6 @@ -import { describe, expect, test } from 'bun:test' import { createPublicClient, custom } from 'viem' import { base } from 'viem/chains' +import { describe, expect, test } from 'vitest' import { createManagedMakerAccount } from '../../../src/infrastructure/make/managed-maker-account.utils' diff --git a/bots/market-making/test/infrastructure/make/read-only-make.service.test.ts b/bots/market-making/test/infrastructure/make/read-only-make.service.test.ts index aad981f0..b7365b95 100644 --- a/bots/market-making/test/infrastructure/make/read-only-make.service.test.ts +++ b/bots/market-making/test/infrastructure/make/read-only-make.service.test.ts @@ -1,6 +1,6 @@ import type { Hex } from 'viem' -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import type { LadderQuoteSet } from '../../../src/domain/ladder/ladder' @@ -69,8 +69,8 @@ describe('read-only make adapters', () => { throw writeError }) - expect(service.hardHalt({ reason: 'bootstrap-decision-failed' })).rejects.toBe(writeError) - expect(service.cleanup()).rejects.toBe(writeError) + await expect(service.hardHalt({ reason: 'bootstrap-decision-failed' })).rejects.toBe(writeError) + await expect(service.cleanup()).rejects.toBe(writeError) }) test('validates a read-only bootstrap reconcile before logging it', async () => { @@ -154,8 +154,8 @@ describe('read-only make adapters', () => { } ) - expect(service.hardHalt({ reason: 'ladder-decision-failed' })).rejects.toBe(writeError) - expect(service.cleanup()).rejects.toBe(writeError) + await expect(service.hardHalt({ reason: 'ladder-decision-failed' })).rejects.toBe(writeError) + await expect(service.cleanup()).rejects.toBe(writeError) }) test('validates a read-only ladder reconcile before logging it', async () => { diff --git a/bots/market-making/test/infrastructure/setup-state/viem-setup-state.service.test.ts b/bots/market-making/test/infrastructure/setup-state/viem-setup-state.service.test.ts index 82a3346e..4edf0888 100644 --- a/bots/market-making/test/infrastructure/setup-state/viem-setup-state.service.test.ts +++ b/bots/market-making/test/infrastructure/setup-state/viem-setup-state.service.test.ts @@ -1,18 +1,51 @@ +import type { AddressInfo } from 'node:net' import type { Address, Hex } from 'viem' import { setterRatifierAbi } from '@morpho-org/midnight-sdk' import { blueAbi } from '@morpho-org/morpho-sdk/abis' -import { describe, expect, mock, test } from 'bun:test' +import { readFile } from 'node:fs/promises' +import { createServer } from 'node:http' +import { setTimeout as sleep } from 'node:timers/promises' import { bytesToHex, hexToBytes, keccak256, zeroHash } from 'viem' +import { describe, expect, test } from 'vitest' import { SafeProviderError } from '../../../src/application/setup/safe-provider.error' -import { MakerAccountError } from '../../../src/infrastructure/make/maker-account.error' import { requestJson } from '../../../src/infrastructure/setup-state/http-json.utils' import { ProviderPaginationError } from '../../../src/infrastructure/setup-state/provider-pagination.error' import { ProviderReadError } from '../../../src/infrastructure/setup-state/provider-read.error' import { ProviderResponseError } from '../../../src/infrastructure/setup-state/provider-response.error' import { ViemSetupStateService } from '../../../src/infrastructure/setup-state/viem-setup-state.service' +// bun's `Bun.serve` accepted a Web-standard fetch handler and exposed `.port`/`.stop()`. These tests +// only need a throwaway loopback server, so this keeps that shape over node:http. +const startFixtureServer = async ( + handler: () => Response | Promise +): Promise<{ port: number; stop: () => Promise }> => { + // Node's request listener is void-returning; consume the promise here so a handler failure cannot + // escape as an unhandled rejection. + const server = createServer((_incoming, outgoing): void => { + void (async () => { + const response = await handler() + outgoing.writeHead(response.status, Object.fromEntries(response.headers)) + outgoing.end(Buffer.from(await response.arrayBuffer())) + })().catch(() => { + if (!outgoing.headersSent) outgoing.writeHead(500) + outgoing.end() + }) + }) + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) + const { port } = server.address() as AddressInfo + return { + port, + stop: async () => { + server.closeAllConnections() + await new Promise((resolve, reject) => { + server.close(error => (error ? reject(error) : resolve())) + }) + } + } +} + const maker: Address = '0x1111111111111111111111111111111111111111' const midnight: Address = '0x2222222222222222222222222222222222222222' const loanAsset: Address = '0x3333333333333333333333333333333333333333' @@ -24,12 +57,12 @@ const knownGroup: Hex = `0x${'66'.repeat(32)}` const unknownGroup: Hex = `0x${'77'.repeat(32)}` const removedMarketId: Hex = `0x${'ab'.repeat(32)}` const authoritativeRatifierRuntime = ( - await Bun.file(new URL('../../fixtures/ecrecover-ratifier-base.hex', import.meta.url)).text() + await readFile(new URL('../../fixtures/ecrecover-ratifier-base.hex', import.meta.url), 'utf8') ).trim() as Hex const authoritativeRatifierRuntimeHash = '0xcce1e0dd38ae831e81a9270627af2c24c208409ec03d5654a28a33ead53b1ac1' const authoritativeSetterRatifierRuntime = ( - await Bun.file(new URL('../../fixtures/setter-ratifier-base.hex', import.meta.url)).text() + await readFile(new URL('../../fixtures/setter-ratifier-base.hex', import.meta.url), 'utf8') ).trim() as Hex const createState = ( @@ -191,26 +224,6 @@ describe('ViemSetupStateService', () => { expect(await state.getDerivedMaker()).toBeUndefined() }) - test('defers remote signer derivation until the captured maker check runs', async () => { - const failure = new MakerAccountError('kms-public-key') - const deriveSignerAddress = mock(async (): Promise
=> Promise.reject(failure)) - const state = new ViemSetupStateService({} as never, {} as never, async () => ({}), { - deriveSignerAddress, - midnight, - loanAsset, - morphoApiBaseUrl: 'https://api.example', - routerApiBaseUrl: 'https://router.example', - marketIds: [marketId], - v0OfferGroupIds: [knownGroup], - readOwnedGroupIds: async () => [], - referenceMarketId - }) - - expect(deriveSignerAddress).toHaveBeenCalledTimes(0) - await expect(state.getDerivedMaker()).rejects.toBe(failure) - expect(deriveSignerAddress).toHaveBeenCalledTimes(1) - }) - test.each([ [ 'getChainId', @@ -359,10 +372,7 @@ describe('ViemSetupStateService', () => { ) test('reports HTTP failures with a fixed provider id and no URL fields', async () => { - const server = Bun.serve({ - port: 0, - fetch: () => new Response('private body', { status: 503 }) - }) + const server = await startFixtureServer(() => new Response('private body', { status: 503 })) const secret = 'provider-api-key' try { @@ -388,17 +398,14 @@ describe('ViemSetupStateService', () => { expect(serialized).not.toContain('/private') expect(serialized).not.toContain('private body') } finally { - await server.stop(true) + await server.stop() } }) test('classifies a bounded request timeout without exposing URL credentials', async () => { - const server = Bun.serve({ - port: 0, - fetch: async () => { - await Bun.sleep(100) - return Response.json({ ok: true }) - } + const server = await startFixtureServer(async () => { + await sleep(100) + return Response.json({ ok: true }) }) const secret = 'timeout-provider-key' @@ -423,7 +430,7 @@ describe('ViemSetupStateService', () => { expect(serialized).not.toContain(String(server.port)) expect(serialized).not.toContain('/slow') } finally { - await server.stop(true) + await server.stop() } }) @@ -987,7 +994,7 @@ describe('ViemSetupStateService', () => { ) responses['/v0/midnight/users/'] = { cursor: page(0), data: [] } - expect(createState(responses).state.inspectOffers(maker)).rejects.toThrow( + await expect(createState(responses).state.inspectOffers(maker)).rejects.toThrow( 'Morpho API offer page limit exceeded' ) }) @@ -1000,7 +1007,9 @@ describe('ViemSetupStateService', () => { } }) - expect(state.inspectOffers(maker)).rejects.toThrow('Morpho API offer-group page size exceeded') + await expect(state.inspectOffers(maker)).rejects.toThrow( + 'Morpho API offer-group page size exceeded' + ) }) test('fails closed when Morpho API exceeds the offer item limit', async () => { @@ -1017,6 +1026,6 @@ describe('ViemSetupStateService', () => { } }) - expect(state.inspectOffers(maker)).rejects.toThrow('Morpho API offer item limit exceeded') + await expect(state.inspectOffers(maker)).rejects.toThrow('Morpho API offer item limit exceeded') }) }) diff --git a/bots/market-making/tsconfig.json b/bots/market-making/tsconfig.json index d3faa43b..8a06a316 100644 --- a/bots/market-making/tsconfig.json +++ b/bots/market-making/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "@repo/typescript-config/base", - "compilerOptions": { "types": ["bun"], "lib": ["ESNext", "DOM"], "jsx": "react-jsx" }, - "include": ["src", "test", "scripts", "playground/*.ts", "playground/*.tsx"], + "compilerOptions": { "types": ["node"] }, + "include": ["src", "test", "scripts"], "exclude": ["node_modules"] } diff --git a/bots/market-making/vitest.config.ts b/bots/market-making/vitest.config.ts new file mode 100644 index 00000000..eb7a4c1b --- /dev/null +++ b/bots/market-making/vitest.config.ts @@ -0,0 +1,23 @@ +import { fileURLToPath } from 'node:url' +import { loadEnv } from 'vite' +import { defineConfig } from 'vitest/config' + +const BOT_DIR = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + test: { + name: 'market-making', + // This bot is the one member with a test outside `test/`: scripts/check-jsdoc.test.ts. Without + // `scripts/` in `include`, vitest's default glob silently collects one file fewer and the suite + // still reports green — so both roots are listed explicitly. + include: ['test/**/*.test.ts', 'scripts/**/*.test.ts'], + // Several cases here spawn the CLI as a real subprocess through tsx. A tsx cold start is ~1.3s + // against bun's ~0.1s, and with projects running in parallel the slowest of them exceeded the 5s + // default under CPU contention. The subprocess work is genuinely slower now, so the ceiling is + // raised rather than the parallelism reduced. + testTimeout: 30_000, + // The e2e suite forks Base and reads RPC_URL_8453; see the liquidation bots' configs for why + // loadEnv is safe when the file is absent. + env: loadEnv('test', BOT_DIR, '') + } +}) diff --git a/bots/midnight-crossed-books/Dockerfile b/bots/midnight-crossed-books/Dockerfile index bf0bce41..eb154cf5 100644 --- a/bots/midnight-crossed-books/Dockerfile +++ b/bots/midnight-crossed-books/Dockerfile @@ -1,13 +1,9 @@ # syntax=docker/dockerfile:1 -# pnpm-workspace image for the midnight-crossed-books bot. The build context MUST be the repo root so -# the workspace packages (packages/*) resolve — docker-compose.yml sets `context: ../..`. -# -# pnpm owns installs, but bun is still the runtime, so this image carries both. The base is Node -# because pnpm is activated through corepack, which the oven/bun images do not provide; bun comes in -# as a single copied binary. Both pnpm and bun must stay on PATH in the FINAL image: `start` fires -# `prestart`, which runs `generate:api` and then `pnpm -r run build` for @repo/contracts' dist/. +# Image for the midnight-crossed-books bot. The build context MUST be the repo root so the workspace +# packages (packages/*) resolve — docker-compose.yml sets `context: ../..`. The bot's `build` script +# runs `generate:api` first, so the OpenAPI types are generated during the image build. +# Node only: pnpm installs, esbuild bundles, node runs. FROM node:24.14.1-slim -COPY --from=oven/bun:1.3.12-slim /usr/local/bin/bun /usr/local/bin/bun ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 # The container's environment holds a funded EOA key, so nothing here may run as root — the previous @@ -20,8 +16,8 @@ WORKDIR /repo USER node # Manifests first, so the install layer is cached. `corepack install` pre-fetches the pnpm version -# pinned in package.json#packageManager, so no download happens at container start. -COPY --chown=node:node package.json pnpm-workspace.yaml pnpm-lock.yaml bunfig.toml ./ +# pinned in package.json#packageManager. +COPY --chown=node:node package.json pnpm-workspace.yaml pnpm-lock.yaml ./ RUN corepack install # All members' package.json are needed for pnpm to resolve the `workspace:*` links. @@ -29,5 +25,9 @@ COPY --chown=node:node packages ./packages COPY --chown=node:node bots ./bots RUN pnpm install --frozen-lockfile +# Bundle at image-build time: workspace dists plus this bot's esbuild bundle (soltag `sol``` templates +# compiled in where present), so the container starts a plain `node` with no runtime transform. +RUN pnpm -r --if-present run build + WORKDIR /repo/bots/midnight-crossed-books -CMD ["bun", "run", "start"] +CMD ["node", "dist/src/index.js"] diff --git a/bots/midnight-crossed-books/README.md b/bots/midnight-crossed-books/README.md index d0253a12..a7c2eb39 100644 --- a/bots/midnight-crossed-books/README.md +++ b/bots/midnight-crossed-books/README.md @@ -85,7 +85,7 @@ bot runtime secrets remain on Railway. ## Test ```sh -bun test bots/midnight-crossed-books/test +pnpm --filter @morpho-org/midnight-crossed-books exec vitest run forge test --root packages/contracts -vv ``` diff --git a/bots/midnight-crossed-books/package.json b/bots/midnight-crossed-books/package.json index d9d47745..ea4149cb 100644 --- a/bots/midnight-crossed-books/package.json +++ b/bots/midnight-crossed-books/package.json @@ -5,11 +5,11 @@ "license": "Apache-2.0", "type": "module", "scripts": { - "prestart": "pnpm run generate:api && pnpm -r --parallel --if-present run build", - "start": "bun src/index.ts", - "typecheck": "pnpm run generate:api && tsc --noEmit", - "deploy:railway": "bun run scripts/deploy-railway.ts", - "generate:api": "openapi-typescript morpho-api.json -o src/infrastructure/morpho-api/generated/morpho-api.types.ts && openapi-typescript router-api.json -o src/infrastructure/router-api/generated/router-api.types.ts" + "build": "pnpm run generate:api && tsx scripts/build.ts", + "deploy:railway": "tsx scripts/deploy-railway.ts", + "generate:api": "openapi-typescript morpho-api.json -o src/infrastructure/morpho-api/generated/morpho-api.types.ts && openapi-typescript router-api.json -o src/infrastructure/router-api/generated/router-api.types.ts", + "start": "node --env-file-if-exists=.env dist/src/index.js", + "typecheck": "pnpm run generate:api && tsc --noEmit" }, "dependencies": { "@repo/bot-kit": "workspace:*", @@ -20,8 +20,12 @@ }, "devDependencies": { "@repo/typescript-config": "workspace:*", - "@types/bun": "catalog:", + "@types/node": "catalog:", + "esbuild": "catalog:", + "execa": "catalog:", "openapi-typescript": "catalog:", - "typescript": "catalog:" + "tsx": "catalog:", + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/bots/midnight-crossed-books/scripts/build.ts b/bots/midnight-crossed-books/scripts/build.ts new file mode 100644 index 00000000..5a44eee5 --- /dev/null +++ b/bots/midnight-crossed-books/scripts/build.ts @@ -0,0 +1,30 @@ +import { build as esbuild } from 'esbuild' +import { rmSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +import { BundleFailedError } from './bundle-failed.error' + +// Bundles the bot entrypoint to `dist/` so production runs a plain `node` with no TypeScript +// transform at startup. This bot holds no soltag `sol``` templates, so no transform plugin is wired. + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..') +const DIST_DIR = join(ROOT, 'dist') +rmSync(DIST_DIR, { recursive: true, force: true }) + +try { + await esbuild({ + entryPoints: [join(ROOT, 'src/index.ts')], + outdir: DIST_DIR, + outbase: ROOT, + bundle: true, + platform: 'node', + format: 'esm', + // CJS deps reaching for require() inside an ESM bundle need a real require. + banner: { + js: "import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);" + } + }) +} catch (error) { + throw new BundleFailedError(error instanceof Error ? error.message : String(error)) +} diff --git a/bots/midnight-crossed-books/scripts/bundle-failed.error.ts b/bots/midnight-crossed-books/scripts/bundle-failed.error.ts new file mode 100644 index 00000000..85b07b64 --- /dev/null +++ b/bots/midnight-crossed-books/scripts/bundle-failed.error.ts @@ -0,0 +1,11 @@ +/** Signals that the production bundle could not be produced. */ +export class BundleFailedError extends Error { + /** + * Creates a tooling failure from the bundler's own message, without retaining source contents. + * @param detail - Bundler-reported reason for the failure. + */ + constructor(readonly detail: string) { + super(`Bundle failed: ${detail}`) + this.name = 'BundleFailedError' + } +} diff --git a/bots/midnight-crossed-books/scripts/deploy-railway.ts b/bots/midnight-crossed-books/scripts/deploy-railway.ts index ddd35ae3..64b40b0a 100644 --- a/bots/midnight-crossed-books/scripts/deploy-railway.ts +++ b/bots/midnight-crossed-books/scripts/deploy-railway.ts @@ -5,17 +5,18 @@ * re-ship the already-provisioned service without copying any bot secrets into GitHub. */ import { delay, tryCatch } from '@repo/utils' -import { $ } from 'bun' -import { resolve } from 'node:path' +import { $ } from 'execa' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' import { parseLatestStatus, parseServices } from './railway' -const PROJECT_ID = required(Bun.env, 'RAILWAY_PROJECT_ID') -const ENVIRONMENT = Bun.env.RAILWAY_ENVIRONMENT?.trim() || 'production' +const PROJECT_ID = required(process.env, 'RAILWAY_PROJECT_ID') +const ENVIRONMENT = process.env.RAILWAY_ENVIRONMENT?.trim() || 'production' const SERVICE = ENVIRONMENT === 'production' ? 'bot' : `${ENVIRONMENT}-bot` const DOCKERFILE_PATH = 'bots/midnight-crossed-books/Dockerfile' -const REPO_ROOT = resolve(import.meta.dir, '..', '..', '..') -const DEPLOY_ONLY = /^(1|true)$/i.test(Bun.env.DEPLOY_ONLY?.trim() || '') +const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..') +const DEPLOY_ONLY = /^(1|true)$/i.test(process.env.DEPLOY_ONLY?.trim() || '') const TERMINAL_STATUSES = new Set([ 'SUCCESS', 'FAILED', @@ -58,21 +59,19 @@ function assertPrivateKey(key: string) { } async function assertCli() { - const { error } = await tryCatch(Promise.resolve($`railway --version`.quiet())) + const { error } = await tryCatch($`railway --version`) if (error) { throw new Error('Railway CLI not found. Install it: https://docs.railway.com/guides/cli') } } async function ensureContext() { - if (Bun.env.RAILWAY_TOKEN) { + if (process.env.RAILWAY_TOKEN) { console.log('Using RAILWAY_TOKEN for project context.') return } - const { error } = await tryCatch( - Promise.resolve($`railway link -p ${PROJECT_ID} -e ${ENVIRONMENT}`.quiet()) - ) + const { error } = await tryCatch($`railway link -p ${PROJECT_ID} -e ${ENVIRONMENT}`) if (error) { throw new Error( `Failed to link ${PROJECT_ID} (${ENVIRONMENT}). Set RAILWAY_TOKEN or run \`railway login\`.` @@ -82,9 +81,7 @@ async function ensureContext() { } async function listServices() { - const { data, error } = await tryCatch( - Promise.resolve($`railway service list --json`.quiet().text()) - ) + const { data, error } = await tryCatch($`railway service list --json`.then(r => r.stdout)) return error || typeof data !== 'string' ? [] : parseServices(data) } @@ -94,18 +91,14 @@ async function ensureService() { return } - const { error } = await tryCatch( - Promise.resolve($`railway add --service ${SERVICE} --json`.quiet()) - ) + const { error } = await tryCatch($`railway add --service ${SERVICE} --json`) if (error) throw new Error(`Failed to create service ${SERVICE}: ${errorDetails(error)}`) console.log(`Created service ${SERVICE}.`) } async function setVariable(value: string) { const key = value.split('=')[0] - const { error } = await tryCatch( - Promise.resolve($`railway variable set ${value} -s ${SERVICE} --skip-deploys`.quiet()) - ) + const { error } = await tryCatch($`railway variable set ${value} -s ${SERVICE} --skip-deploys`) if (error) throw new Error(`Failed to set ${key} on ${SERVICE}: ${errorDetails(error)}`) console.log(`Set ${key} on ${SERVICE}.`) } @@ -113,7 +106,7 @@ async function setVariable(value: string) { async function setSecret(name: string, value: string) { const { error } = await tryCatch( Promise.resolve( - $`railway variable set ${name} --stdin -s ${SERVICE} --skip-deploys < ${Buffer.from(value, 'utf8')}`.quiet() + $({ input: value })`railway variable set ${name} --stdin -s ${SERVICE} --skip-deploys` ) ) if (error) throw new Error(`Failed to set ${name} on ${SERVICE}`) @@ -124,9 +117,9 @@ async function deployService() { const message = `deploy midnight crossed-books ${ENVIRONMENT}` const { error } = await tryCatch( Promise.resolve( - $`railway up -s ${SERVICE} -p ${PROJECT_ID} -e ${ENVIRONMENT} -d -m ${message}` - .cwd(REPO_ROOT) - .quiet() + $({ + cwd: REPO_ROOT + })`railway up -s ${SERVICE} -p ${PROJECT_ID} -e ${ENVIRONMENT} -d -m ${message}` ) ) if (error) throw new Error(`Failed to start deploy for ${SERVICE}: ${errorDetails(error)}`) @@ -147,7 +140,7 @@ async function latestStatus() { '1', '--json' ] - const { data, error } = await tryCatch(Promise.resolve($`${args}`.quiet().text())) + const { data, error } = await tryCatch($(args[0] ?? 'railway', args.slice(1)).then(r => r.stdout)) return error || typeof data !== 'string' ? 'UNKNOWN' : parseLatestStatus(data) } @@ -175,8 +168,8 @@ if (DEPLOY_ONLY) { await deployService() reportStatus(await waitForDeploy()) } else { - const rpcUrl = required(Bun.env, 'RPC_URL') - const resolverPrivateKey = required(Bun.env, 'RESOLVER_PRIVATE_KEY') + const rpcUrl = required(process.env, 'RPC_URL') + const resolverPrivateKey = required(process.env, 'RESOLVER_PRIVATE_KEY') assertPrivateKey(resolverPrivateKey) await ensureContext() diff --git a/bots/midnight-crossed-books/src/bootstrap.ts b/bots/midnight-crossed-books/src/bootstrap.ts index 017579d3..ba18d3ce 100644 --- a/bots/midnight-crossed-books/src/bootstrap.ts +++ b/bots/midnight-crossed-books/src/bootstrap.ts @@ -32,7 +32,9 @@ function resolverSelector() { return toFunctionSelector(resolveAbi) } -export async function createApplication(environment: Record = Bun.env) { +export async function createApplication( + environment: Record = process.env +) { const config = ConfigService.from(environment) const logger = createLogger('info', { context: { diff --git a/bots/midnight-crossed-books/src/config/config.service.ts b/bots/midnight-crossed-books/src/config/config.service.ts index 91da7426..3d058426 100644 --- a/bots/midnight-crossed-books/src/config/config.service.ts +++ b/bots/midnight-crossed-books/src/config/config.service.ts @@ -24,7 +24,7 @@ function unsignedDecimal(environment: Environment, name: string, fallback?: stri } export class ConfigService { - static from(environment: Environment = Bun.env) { + static from(environment: Environment = process.env) { const chainId = Number(unsignedDecimal(environment, 'CHAIN_ID')) if (chainId !== base.id) { throw new Error(`Unsupported CHAIN_ID ${chainId}; supported: ${base.id}`) diff --git a/bots/midnight-crossed-books/test/application/crossed-books-bot.service.test.ts b/bots/midnight-crossed-books/test/application/crossed-books-bot.service.test.ts index 300e2a99..437d424e 100644 --- a/bots/midnight-crossed-books/test/application/crossed-books-bot.service.test.ts +++ b/bots/midnight-crossed-books/test/application/crossed-books-bot.service.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, mock, test } from 'bun:test' +import { describe, expect, test, vi } from 'vitest' import type { ListedMarketsService, @@ -39,27 +39,27 @@ function setup( maxMatches?: number } = {} ) { - const listListedActiveMarkets = mock(async () => overrides.markets ?? [MARKET]) - const getTakeableBook = mock(async () => { + const listListedActiveMarkets = vi.fn(async () => overrides.markets ?? [MARKET]) + const getTakeableBook = vi.fn(async () => { if (overrides.booksError) throw overrides.booksError return { asks: [MATCH.ask], bids: [MATCH.bid] } }) - const match = mock(() => overrides.matches ?? [MATCH, SECOND_MATCH]) - const simulate = mock( + const match = vi.fn(() => overrides.matches ?? [MATCH, SECOND_MATCH]) + const simulate = vi.fn( async (): Promise => overrides.simulation ?? { status: 'ok', prepared: { marketId: MARKET_ID, data: '0x1234', profit: 1n } } ) - const submit = mock(async () => undefined) + const submit = vi.fn(async () => undefined) const markets: ListedMarketsService = { listListedActiveMarkets } const books: OrderBookService = { getTakeableBook } const matching: MatchingServicePort = { match } const resolver: ResolverService = { simulate, submit } const logger = { - info: mock(() => undefined), - warn: mock(() => undefined) + info: vi.fn(() => undefined), + warn: vi.fn(() => undefined) } const service = new CrossedBooksBotService( markets, @@ -164,7 +164,7 @@ describe('CrossedBooksBotService', () => { test('isolates a book failure and continues to the next market', async () => { let calls = 0 const { service, books, submit } = setup({ markets: [MARKET, OTHER_MARKET] }) - books.getTakeableBook = mock(async marketId => { + books.getTakeableBook = vi.fn(async marketId => { calls += 1 if (marketId === MARKET_ID) throw new Error('router unavailable') return { diff --git a/bots/midnight-crossed-books/test/config/config.service.test.ts b/bots/midnight-crossed-books/test/config/config.service.test.ts index e6abad3f..d162b482 100644 --- a/bots/midnight-crossed-books/test/config/config.service.test.ts +++ b/bots/midnight-crossed-books/test/config/config.service.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import { ConfigService } from '../../src/config/config.service' diff --git a/bots/midnight-crossed-books/test/domain/matching.service.test.ts b/bots/midnight-crossed-books/test/domain/matching.service.test.ts index 042a1e2f..07b3ff05 100644 --- a/bots/midnight-crossed-books/test/domain/matching.service.test.ts +++ b/bots/midnight-crossed-books/test/domain/matching.service.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import { MatchingService } from '../../src/domain/matching.service' import { MARKET_ID, OTHER_MARKET_ID, makeOffer } from '../fixtures/offers' diff --git a/bots/midnight-crossed-books/test/infrastructure/morpho-api/service.test.ts b/bots/midnight-crossed-books/test/infrastructure/morpho-api/service.test.ts index 648834a9..d9df3627 100644 --- a/bots/midnight-crossed-books/test/infrastructure/morpho-api/service.test.ts +++ b/bots/midnight-crossed-books/test/infrastructure/morpho-api/service.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, mock, test } from 'bun:test' +import { describe, expect, test, vi } from 'vitest' import { MorphoApiService } from '../../../src/infrastructure/morpho-api/service' import { MorphoApiError } from '../../../src/infrastructure/openapi/error' @@ -13,7 +13,7 @@ function response(body: unknown, status = 200) { describe('MorphoApiService', () => { test('requests only listed active markets on the configured chain', async () => { - const GET = mock(async () => ({ + const GET = vi.fn(async () => ({ data: { cursor: null, data: [{ market_id: MARKET_ID }] }, response: response({}) })) @@ -35,18 +35,20 @@ describe('MorphoApiService', () => { }) test('drains cursor pagination in order', async () => { - const GET = mock(async (_path: string, request: { params: { query: { cursor?: string } } }) => { - if (!request.params.query.cursor) { + const GET = vi.fn( + async (_path: string, request: { params: { query: { cursor?: string } } }) => { + if (!request.params.query.cursor) { + return { + data: { cursor: 'next', data: [{ market_id: MARKET_ID }] }, + response: response({}) + } + } return { - data: { cursor: 'next', data: [{ market_id: MARKET_ID }] }, + data: { cursor: null, data: [{ market_id: OTHER_MARKET_ID }] }, response: response({}) } } - return { - data: { cursor: null, data: [{ market_id: OTHER_MARKET_ID }] }, - response: response({}) - } - }) + ) const service = new MorphoApiService({ GET } as never, 8453) const markets = await service.listListedActiveMarkets() @@ -56,7 +58,7 @@ describe('MorphoApiService', () => { }) test('wraps a non-success response in MorphoApiError', async () => { - const GET = mock(async () => ({ + const GET = vi.fn(async () => ({ error: { code: 'SERVICE_UNAVAILABLE' }, response: response({}, 503) })) @@ -66,7 +68,7 @@ describe('MorphoApiService', () => { }) test('wraps a rejected fetch in MorphoApiError', async () => { - const GET = mock(async () => { + const GET = vi.fn(async () => { throw new Error('network down') }) const service = new MorphoApiService({ GET } as never, 8453) diff --git a/bots/midnight-crossed-books/test/infrastructure/openapi-result.test.ts b/bots/midnight-crossed-books/test/infrastructure/openapi-result.test.ts index e669e750..3caa553e 100644 --- a/bots/midnight-crossed-books/test/infrastructure/openapi-result.test.ts +++ b/bots/midnight-crossed-books/test/infrastructure/openapi-result.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import { MorphoApiError } from '../../src/infrastructure/openapi/error' import { diff --git a/bots/midnight-crossed-books/test/infrastructure/resolver/resolver.encoder.test.ts b/bots/midnight-crossed-books/test/infrastructure/resolver/resolver.encoder.test.ts index 238a7bd4..b8a87d59 100644 --- a/bots/midnight-crossed-books/test/infrastructure/resolver/resolver.encoder.test.ts +++ b/bots/midnight-crossed-books/test/infrastructure/resolver/resolver.encoder.test.ts @@ -1,6 +1,6 @@ import { CrossedBooksResolver } from '@repo/contracts' -import { describe, expect, test } from 'bun:test' import { decodeFunctionData, encodeFunctionResult } from 'viem' +import { describe, expect, test } from 'vitest' import { ViemResolverEncoder } from '../../../src/infrastructure/resolver/resolver.encoder' import { makeOffer } from '../../fixtures/offers' diff --git a/bots/midnight-crossed-books/test/infrastructure/resolver/resolver.service.test.ts b/bots/midnight-crossed-books/test/infrastructure/resolver/resolver.service.test.ts index d9510b9e..7f323f8b 100644 --- a/bots/midnight-crossed-books/test/infrastructure/resolver/resolver.service.test.ts +++ b/bots/midnight-crossed-books/test/infrastructure/resolver/resolver.service.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, mock, test } from 'bun:test' +import { describe, expect, test, vi } from 'vitest' import type { PreparedResolution } from '../../../src/domain/order-book' import type { ResolverTransport } from '../../../src/infrastructure/resolver/resolver.transport' @@ -14,12 +14,12 @@ const MATCH = { const MATCHES = [MATCH] function setup(result: Awaited>) { - const simulate = mock(async () => result) - const submit = mock(async () => undefined) + const simulate = vi.fn(async () => result) + const submit = vi.fn(async () => undefined) const transport: ResolverTransport = { simulate, submit } const encoder = { - encode: mock(() => '0x1234' as const), - decodeProfit: mock(() => 42n) + encode: vi.fn(() => '0x1234' as const), + decodeProfit: vi.fn(() => 42n) } const service = new ResolverExecutionService(transport, encoder, 10n) diff --git a/bots/midnight-crossed-books/test/infrastructure/router-api/service.test.ts b/bots/midnight-crossed-books/test/infrastructure/router-api/service.test.ts index 65dced3d..a73ce8cf 100644 --- a/bots/midnight-crossed-books/test/infrastructure/router-api/service.test.ts +++ b/bots/midnight-crossed-books/test/infrastructure/router-api/service.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, mock, test } from 'bun:test' +import { describe, expect, test, vi } from 'vitest' import { RouterApiError } from '../../../src/infrastructure/openapi/error' import { RouterApiService } from '../../../src/infrastructure/router-api/service' @@ -49,7 +49,7 @@ function response(body: unknown, status = 200) { describe('RouterApiService', () => { test('requests asks and bids concurrently through typed paths', async () => { - const GET = mock(async (_path: string, request: { params: { path: { side: string } } }) => ({ + const GET = vi.fn(async (_path: string, request: { params: { path: { side: string } } }) => ({ data: { data: [wireOffer(MARKET_ID, request.params.path.side === 'bids')] }, response: response({}) })) @@ -67,7 +67,7 @@ describe('RouterApiService', () => { }) test('maps generated snake-case offer fields to the domain model', async () => { - const GET = mock(async (_path: string, request: { params: { path: { side: string } } }) => ({ + const GET = vi.fn(async (_path: string, request: { params: { path: { side: string } } }) => ({ data: { data: [wireOffer(MARKET_ID, request.params.path.side === 'bids')] }, response: response({}) })) @@ -89,7 +89,7 @@ describe('RouterApiService', () => { }) test('drops zero-sized and wrong-market rows', async () => { - const GET = mock(async (_path: string, request: { params: { path: { side: string } } }) => ({ + const GET = vi.fn(async (_path: string, request: { params: { path: { side: string } } }) => ({ data: { data: [ wireOffer(MARKET_ID, request.params.path.side === 'bids'), @@ -108,7 +108,7 @@ describe('RouterApiService', () => { }) test('wraps one-side HTTP failures as RouterApiError', async () => { - const GET = mock(async (_path: string, request: { params: { path: { side: string } } }) => + const GET = vi.fn(async (_path: string, request: { params: { path: { side: string } } }) => request.params.path.side === 'asks' ? { error: { code: 'SERVICE_UNAVAILABLE' }, response: response({}, 503) } : { data: { data: [] }, response: response({}) } @@ -119,7 +119,7 @@ describe('RouterApiService', () => { }) test('wraps rejected requests as RouterApiError', async () => { - const GET = mock(async () => { + const GET = vi.fn(async () => { throw new Error('timeout') }) const service = new RouterApiService({ GET } as never) diff --git a/bots/midnight-crossed-books/test/scripts/railway.test.ts b/bots/midnight-crossed-books/test/scripts/railway.test.ts index e8a57177..5cc52429 100644 --- a/bots/midnight-crossed-books/test/scripts/railway.test.ts +++ b/bots/midnight-crossed-books/test/scripts/railway.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import { parseLatestStatus, parseServices } from '../../scripts/railway' diff --git a/bots/midnight-crossed-books/tsconfig.json b/bots/midnight-crossed-books/tsconfig.json index f1fe093b..8a06a316 100644 --- a/bots/midnight-crossed-books/tsconfig.json +++ b/bots/midnight-crossed-books/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "@repo/typescript-config/base", - "compilerOptions": { "types": ["bun"] }, + "compilerOptions": { "types": ["node"] }, "include": ["src", "test", "scripts"], "exclude": ["node_modules"] } diff --git a/bots/midnight-crossed-books/vitest.config.ts b/bots/midnight-crossed-books/vitest.config.ts new file mode 100644 index 00000000..8b0f1d57 --- /dev/null +++ b/bots/midnight-crossed-books/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + name: 'midnight-crossed-books' + } +}) diff --git a/bots/midnight-liquidation/Dockerfile b/bots/midnight-liquidation/Dockerfile index cbec722d..d4284f4f 100644 --- a/bots/midnight-liquidation/Dockerfile +++ b/bots/midnight-liquidation/Dockerfile @@ -1,15 +1,9 @@ # syntax=docker/dockerfile:1 -# pnpm-workspace image for the midnight-liquidation bot. The build context MUST be the repo root so the -# workspace packages (packages/*) resolve — docker-compose.yml sets `context: ../..` and the Railway -# deploy runs `railway up` from the repo root. Borrower discovery polls the markets -# liquidation-candidates HTTP API, so there is no indexer/database sidecar to build. -# -# pnpm owns installs, but bun is still the runtime, so this image carries both. The base is Node -# because pnpm is activated through corepack, which the oven/bun images do not provide; bun comes in -# as a single copied binary. Both pnpm and bun must stay on PATH in the FINAL image: `start` fires -# `prestart`, which shells out to `pnpm -r run build` to compile @repo/contracts' gitignored dist/. +# Image for the midnight-liquidation bot. The build context MUST be the repo root so the workspace +# packages (packages/*) resolve — docker-compose.yml sets `context: ../..` and the Railway deploy runs +# `railway up` from the repo root. Borrower discovery polls the markets liquidation-candidates HTTP +# API, so there is no indexer/database sidecar to build. Node only: pnpm installs, esbuild bundles. FROM node:24.14.1-slim -COPY --from=oven/bun:1.3.12-slim /usr/local/bin/bun /usr/local/bin/bun ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 # The container's environment holds a funded liquidator EOA key, so nothing here may run as root — @@ -22,8 +16,8 @@ WORKDIR /repo USER node # Manifests first, so the install layer is cached. `corepack install` pre-fetches the pnpm version -# pinned in package.json#packageManager, so no download happens at container start. -COPY --chown=node:node package.json pnpm-workspace.yaml pnpm-lock.yaml bunfig.toml ./ +# pinned in package.json#packageManager. +COPY --chown=node:node package.json pnpm-workspace.yaml pnpm-lock.yaml ./ RUN corepack install # All members' package.json are needed for pnpm to resolve the `workspace:*` links. @@ -31,7 +25,9 @@ COPY --chown=node:node packages ./packages COPY --chown=node:node bots ./bots RUN pnpm install --frozen-lockfile -# Run the bot from its package dir (`start` builds workspaces via `prestart`, then runs src/index.ts; -# the bot's own bunfig.toml preload compiles its soltag `sol``` lens templates at startup). +# Bundle at image-build time: workspace dists plus this bot's esbuild bundle (soltag `sol``` templates +# compiled in where present), so the container starts a plain `node` with no runtime transform. +RUN pnpm -r --if-present run build + WORKDIR /repo/bots/midnight-liquidation -CMD ["bun", "run", "start"] +CMD ["node", "dist/src/index.js"] diff --git a/bots/midnight-liquidation/README.md b/bots/midnight-liquidation/README.md index 2367967c..19b3ee36 100644 --- a/bots/midnight-liquidation/README.md +++ b/bots/midnight-liquidation/README.md @@ -29,7 +29,7 @@ This package is operational code, but it is still intentionally narrow: ## Prerequisites - Node.js `24.14.1` (`nvm use` from the repo root). -- Bun `1.3.12`. +- pnpm `11.1.1` (via corepack), Node `24.14.1`. - A Base RPC URL. - A funded liquidator EOA private key. - A deployed permissionless Executor contract. If `EXECUTOOOR_ADDRESS` is unset, the bot uses the @@ -148,12 +148,12 @@ Useful validation commands while developing: ```sh pnpm --filter @morpho-org/midnight-liquidation run typecheck -bun test bots/midnight-liquidation/test +pnpm --filter @morpho-org/midnight-liquidation exec vitest run ``` ## Testing -- `bun test bots/midnight-liquidation/test` — unit tests for the sizing planner, the deployless lens, +- `pnpm --filter @morpho-org/midnight-liquidation exec vitest run` — unit tests for the sizing planner, the deployless lens, discovery (the candidate + markets APIs and their shared retry loop), quoting / venue selection, the pending queue, eligibility, and the exec encoder. - **Fork suite** (`test/fork/`) — end-to-end against a real Base fork. Unlike a fixture-gated suite it @@ -251,7 +251,7 @@ it runs the same locally or in CI. Railway service names are project-wide: production uses `bot`, while non-production environments use an environment prefix (for example, `staging-bot`). -The [Dockerfile](./Dockerfile) is a single-stage Node image carrying both pnpm and bun; +The [Dockerfile](./Dockerfile) is a single-stage Node image (pnpm installs, esbuild bundles, node runs); `RAILWAY_DOCKERFILE_PATH` points Railway at it and `railway up` runs from the repo root so the pnpm workspace resolves. diff --git a/bots/midnight-liquidation/bunfig.toml b/bots/midnight-liquidation/bunfig.toml deleted file mode 100644 index 2a03c377..00000000 --- a/bots/midnight-liquidation/bunfig.toml +++ /dev/null @@ -1,6 +0,0 @@ -# Compile soltag `sol``` lens templates at runtime (`bun run start` in the Docker image) and for -# `bun test` when run from this directory. See soltag.preload.ts for why the plugin is hand-rolled. -preload = ["./soltag.preload.ts"] - -[test] -preload = ["./soltag.preload.ts"] diff --git a/bots/midnight-liquidation/package.json b/bots/midnight-liquidation/package.json index 384a4eb5..3e11f08b 100644 --- a/bots/midnight-liquidation/package.json +++ b/bots/midnight-liquidation/package.json @@ -5,10 +5,10 @@ "license": "Apache-2.0", "type": "module", "scripts": { - "deploy:railway": "bun run scripts/deploy-railway.ts", - "seed:positions": "bun run scripts/seed-liquidatable-positions.ts", - "prestart": "pnpm -r --parallel --if-present run build", - "start": "bun src/index.ts", + "build": "tsx scripts/build.ts", + "deploy:railway": "tsx scripts/deploy-railway.ts", + "seed:positions": "node --env-file-if-exists=.env dist/scripts/seed-liquidatable-positions.js", + "start": "node --env-file-if-exists=.env dist/src/index.js", "typecheck": "soltag && tsc --noEmit" }, "dependencies": { @@ -24,7 +24,12 @@ }, "devDependencies": { "@repo/typescript-config": "workspace:*", - "@types/bun": "catalog:", - "typescript": "catalog:" + "@types/node": "catalog:", + "esbuild": "catalog:", + "execa": "catalog:", + "tsx": "catalog:", + "typescript": "catalog:", + "vite": "catalog:", + "vitest": "catalog:" } } diff --git a/bots/midnight-liquidation/scripts/build.ts b/bots/midnight-liquidation/scripts/build.ts new file mode 100644 index 00000000..aaa1e81a --- /dev/null +++ b/bots/midnight-liquidation/scripts/build.ts @@ -0,0 +1,55 @@ +import type { Plugin } from 'esbuild' + +import { build as esbuild } from 'esbuild' +import { rmSync } from 'node:fs' +import { readFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { transformSolTemplates } from 'soltag/unplugin' + +import { BundleFailedError } from './bundle-failed.error' + +// Bundles the bot entrypoint (and the soltag-dependent operator script) to `dist/` with the +// `sol``` templates compiled to literal ABIs/bytecode, so production runs a plain `node` with no +// runtime transform. This replaces the bunfig `preload` that used to compile them at startup. + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..') +const DIST_DIR = join(ROOT, 'dist') +rmSync(DIST_DIR, { recursive: true, force: true }) + +const ESCAPED = ROOT.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +// This bot's own TS sources only — bundled workspace deps ship prebuilt dist and hold no templates. +const INCLUDE = new RegExp(`^${ESCAPED}/(?:src|scripts)/.*\\.tsx?$`) + +const soltagPlugin: Plugin = { + name: 'soltag', + setup(build) { + build.onLoad({ filter: INCLUDE }, async ({ path }) => { + const source = await readFile(path, 'utf8') + // Enable the optimizer — the lens's per-element computation has enough locals to hit + // "stack too deep" without it. + const transformed = transformSolTemplates(source, path, { + solc: { optimizer: { enabled: true, runs: 200 } } + }) + return { contents: transformed?.code ?? source, loader: 'ts' as const } + }) + } +} + +try { + await esbuild({ + entryPoints: [join(ROOT, 'src/index.ts'), join(ROOT, 'scripts/seed-liquidatable-positions.ts')], + outdir: DIST_DIR, + outbase: ROOT, + bundle: true, + platform: 'node', + format: 'esm', + // CJS deps reaching for require() inside an ESM bundle need a real require. + banner: { + js: "import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);" + }, + plugins: [soltagPlugin] + }) +} catch (error) { + throw new BundleFailedError(error instanceof Error ? error.message : String(error)) +} diff --git a/bots/midnight-liquidation/scripts/bundle-failed.error.ts b/bots/midnight-liquidation/scripts/bundle-failed.error.ts new file mode 100644 index 00000000..85b07b64 --- /dev/null +++ b/bots/midnight-liquidation/scripts/bundle-failed.error.ts @@ -0,0 +1,11 @@ +/** Signals that the production bundle could not be produced. */ +export class BundleFailedError extends Error { + /** + * Creates a tooling failure from the bundler's own message, without retaining source contents. + * @param detail - Bundler-reported reason for the failure. + */ + constructor(readonly detail: string) { + super(`Bundle failed: ${detail}`) + this.name = 'BundleFailedError' + } +} diff --git a/bots/midnight-liquidation/scripts/deploy-railway.ts b/bots/midnight-liquidation/scripts/deploy-railway.ts index 8dd9687b..97dbef8d 100644 --- a/bots/midnight-liquidation/scripts/deploy-railway.ts +++ b/bots/midnight-liquidation/scripts/deploy-railway.ts @@ -14,7 +14,7 @@ * RAILWAY_PROJECT_ID=… RPC_URL=… LIQUIDATOR_PRIVATE_KEY=0x… \ * pnpm --filter @morpho-org/midnight-liquidation run deploy:railway * - * The build context MUST be the repo root so the bun workspace (packages/*) resolves — the script + * The build context MUST be the repo root so the pnpm workspace (packages/*) resolves — the script * runs `railway up` with cwd set to the repo root (mirrors the Dockerfile header + compose context), * and passes `-p/-e` explicitly so the deploy targets this project/environment regardless of whatever * the repo-root directory happens to be linked to (a sibling bot's deploy leaves it linked elsewhere). @@ -26,16 +26,17 @@ * its value; variable values are never logged. */ import { delay, tryCatch } from '@repo/utils' -import { $ } from 'bun' -import { resolve } from 'node:path' +import { $ } from 'execa' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' // The target project is env-driven so no project identifier is baked into this (open-source) file. // RAILWAY_PROJECT_ID is required; RAILWAY_ENVIRONMENT defaults to the conventional `production`. -const PROJECT_ID = required(Bun.env, 'RAILWAY_PROJECT_ID') -const ENVIRONMENT = Bun.env.RAILWAY_ENVIRONMENT?.trim() || 'production' +const PROJECT_ID = required(process.env, 'RAILWAY_PROJECT_ID') +const ENVIRONMENT = process.env.RAILWAY_ENVIRONMENT?.trim() || 'production' const DOCKERFILE_PATH = 'bots/midnight-liquidation/Dockerfile' // Repo root is three levels up from this file (scripts → midnight-liquidation → bots → repo root). -const REPO_ROOT = resolve(import.meta.dir, '..', '..', '..') +const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..') type Env = Record type RailwayService = { id: string; name: string } @@ -63,8 +64,8 @@ function str(value: unknown): string { } // Surface a failed `railway` command's stderr so failures are actionable (the CLI writes the real -// reason — plan limits, auth, selection prompts — to stderr). Bun's ShellError carries `.stderr` as -// bytes; fall back to the generic message. Safe for non-secret commands; never used on setSecret. +// reason — plan limits, auth, selection prompts — to stderr). execa's error carries `.stderr` as a +// string; fall back to the generic message. Safe for non-secret commands; never used on setSecret. function stderrOf(error: unknown): string { if (isRecord(error) && 'stderr' in error) { const s = (error as { stderr: unknown }).stderr @@ -108,7 +109,7 @@ function parseLatestStatus(raw: string): string { } async function assertCli(): Promise { - const { error } = await tryCatch(Promise.resolve($`railway --version`.quiet())) + const { error } = await tryCatch($`railway --version`) if (error) throw new Error('Railway CLI not found. Install it: https://docs.railway.com/guides/cli') } @@ -116,13 +117,11 @@ async function assertCli(): Promise { // `railway add` has no --project/--environment flag, so it acts on the linked context. A project // token scopes every command implicitly; otherwise we link the project id once for this run. async function ensureContext(): Promise { - if (Bun.env.RAILWAY_TOKEN) { + if (process.env.RAILWAY_TOKEN) { console.log('Using RAILWAY_TOKEN for project context.') return } - const { error } = await tryCatch( - Promise.resolve($`railway link -p ${PROJECT_ID} -e ${ENVIRONMENT}`.quiet()) - ) + const { error } = await tryCatch($`railway link -p ${PROJECT_ID} -e ${ENVIRONMENT}`) if (error) { throw new Error( `Failed to link ${PROJECT_ID} (${ENVIRONMENT}). Set RAILWAY_TOKEN or run \`railway login\`.` @@ -132,9 +131,7 @@ async function ensureContext(): Promise { } async function listServices(): Promise { - const { data, error } = await tryCatch( - Promise.resolve($`railway service list --json`.quiet().text()) - ) + const { data, error } = await tryCatch($`railway service list --json`.then(r => r.stdout)) return error || typeof data !== 'string' ? [] : parseServices(data) } @@ -144,16 +141,14 @@ async function ensureService(name: string): Promise { return } console.log(`Creating service ${name}…`) - const { error } = await tryCatch(Promise.resolve($`railway add --service ${name} --json`.quiet())) + const { error } = await tryCatch($`railway add --service ${name} --json`) if (error) throw new Error(`Failed to create service ${name}: ${stderrOf(error)}`) } // Non-secret variable. `kv` is a single "KEY=VALUE" arg; only the key is logged. async function setVar(service: string, kv: string): Promise { const key = kv.split('=')[0] - const { error } = await tryCatch( - Promise.resolve($`railway variable set ${kv} -s ${service} --skip-deploys`.quiet()) - ) + const { error } = await tryCatch($`railway variable set ${kv} -s ${service} --skip-deploys`) if (error) throw new Error(`Failed to set ${key} on ${service}: ${stderrOf(error)}`) console.log(`Set ${key} on ${service}.`) } @@ -162,7 +157,7 @@ async function setVar(service: string, kv: string): Promise { async function setSecret(service: string, key: string, value: string): Promise { const { error } = await tryCatch( Promise.resolve( - $`railway variable set ${key} --stdin -s ${service} --skip-deploys < ${Buffer.from(value, 'utf8')}`.quiet() + $({ input: value })`railway variable set ${key} --stdin -s ${service} --skip-deploys` ) ) if (error) throw new Error(`Failed to set ${key} on ${service}`) @@ -172,13 +167,13 @@ async function setSecret(service: string, key: string, value: string): Promise { console.log(`Deploying ${service} from repo root…`) // `railway up` runs with cwd = REPO_ROOT (build context), but the script only ever links the - // *package* dir (ensureContext, which is where bun runs it). Railway links are per-directory, so + // *package* dir (ensureContext, which is where pnpm runs it). Railway links are per-directory, so // without explicit flags `up` would inherit whatever REPO_ROOT happens to be linked to — e.g. a // sibling bot's project after its last deploy, which fails with "No environment specified" or, worse, // targets the wrong project. Scope the deploy explicitly so it never depends on ambient link state. const { error } = await tryCatch( Promise.resolve( - $`railway up -s ${service} -p ${PROJECT_ID} -e ${ENVIRONMENT} -d`.cwd(REPO_ROOT).quiet() + $({ cwd: REPO_ROOT })`railway up -s ${service} -p ${PROJECT_ID} -e ${ENVIRONMENT} -d` ) ) if (error) throw new Error(`Failed to start deploy for ${service}: ${stderrOf(error)}`) @@ -200,7 +195,7 @@ async function latestStatus(service: string): Promise { '1', '--json' ] - const { data, error } = await tryCatch(Promise.resolve($`${args}`.quiet().text())) + const { data, error } = await tryCatch($(args[0] ?? 'railway', args.slice(1)).then(r => r.stdout)) return error || typeof data !== 'string' ? 'UNKNOWN' : parseLatestStatus(data) } @@ -228,7 +223,7 @@ const BOT_SERVICE = serviceName('bot') // stage GitHub Environment holds only RAILWAY_TOKEN + RAILWAY_PROJECT_ID, and the service/secrets // were provisioned once by a full (secret-bearing) run of this script. Skips the RPC/key/venue // requirements the full path enforces, so it never needs those secrets in CI. -if (/^(1|true)$/i.test(Bun.env.DEPLOY_ONLY?.trim() ?? '')) { +if (/^(1|true)$/i.test(process.env.DEPLOY_ONLY?.trim() ?? '')) { await ensureContext() await deployService(BOT_SERVICE) // `railway up` rebuilds it server-side const status = await waitForDeploy(BOT_SERVICE) @@ -239,17 +234,17 @@ if (/^(1|true)$/i.test(Bun.env.DEPLOY_ONLY?.trim() ?? '')) { } // Secrets / config from this process's env (fail loud before mutating any Railway state). -const rpcUrl = required(Bun.env, 'RPC_URL') -const liquidatorPrivateKey = required(Bun.env, 'LIQUIDATOR_PRIVATE_KEY') +const rpcUrl = required(process.env, 'RPC_URL') +const liquidatorPrivateKey = required(process.env, 'LIQUIDATOR_PRIVATE_KEY') assertPrivateKey(liquidatorPrivateKey) // Venues are enabled by the presence of their API key. The bot hard-fails at boot with no key unless // ALLOW_BAD_DEBT_ONLY=true — so require the operator to pass a venue key (pushed as a secret) or // explicitly opt into bad-debt-only here, rather than deploying a service that crash-loops. -const zeroxKey = Bun.env.ZEROX_API_KEY?.trim() -const oneinchKey = Bun.env.ONEINCH_API_KEY?.trim() -const lifiKey = Bun.env.LIFI_API_KEY?.trim() -const allowBadDebtOnly = Bun.env.ALLOW_BAD_DEBT_ONLY?.trim().toLowerCase() === 'true' +const zeroxKey = process.env.ZEROX_API_KEY?.trim() +const oneinchKey = process.env.ONEINCH_API_KEY?.trim() +const lifiKey = process.env.LIFI_API_KEY?.trim() +const allowBadDebtOnly = process.env.ALLOW_BAD_DEBT_ONLY?.trim().toLowerCase() === 'true' if (!zeroxKey && !oneinchKey && !lifiKey && !allowBadDebtOnly) { throw new Error( 'Set LIFI_API_KEY, ZEROX_API_KEY, and/or ONEINCH_API_KEY, or ALLOW_BAD_DEBT_ONLY=true to deploy bad-debt-only.' @@ -258,9 +253,9 @@ if (!zeroxKey && !oneinchKey && !lifiKey && !allowBadDebtOnly) { // Optional BetterStack log shipping: host is a plain var, token is a secret. Off when unset — the // bot's in-process loglayer transport stays inert, so the container behaves exactly as before. -const betterstackHost = Bun.env.BETTERSTACK_INGESTING_HOST?.trim() -const betterstackToken = Bun.env.BETTERSTACK_SOURCE_TOKEN?.trim() -const betterstackHeartbeatUrl = Bun.env.BETTERSTACK_HEARTBEAT_URL?.trim() +const betterstackHost = process.env.BETTERSTACK_INGESTING_HOST?.trim() +const betterstackToken = process.env.BETTERSTACK_SOURCE_TOKEN?.trim() +const betterstackHeartbeatUrl = process.env.BETTERSTACK_HEARTBEAT_URL?.trim() await ensureContext() diff --git a/bots/midnight-liquidation/scripts/seed-liquidatable-positions.ts b/bots/midnight-liquidation/scripts/seed-liquidatable-positions.ts index 9e9b1883..96e9d41b 100644 --- a/bots/midnight-liquidation/scripts/seed-liquidatable-positions.ts +++ b/bots/midnight-liquidation/scripts/seed-liquidatable-positions.ts @@ -21,7 +21,7 @@ * tool's OWN swap-route file (it needs a WETH route to fund the seed swaps); it is unrelated to the * bot's runtime, which no longer uses a swap-config file: * RPC_URL=... PRIVATE_KEY_LENDER=0x... PRIVATE_KEY_BORROWER=0x... \ - * bun scripts/seed-liquidatable-positions.ts \ + * pnpm --filter @morpho-org/midnight-liquidation run seed:positions -- \ * --config ./swap.config.json --pair WETH/USDC --count 100 --drawdown-bps 0 --dry-run * * Never prints secrets (keys, full RPC URL). @@ -33,6 +33,7 @@ import { MidnightAbi } from '@repo/contracts' import { parseSwapConfig } from '@repo/swaps' import { delay as sleep, lensKey, tryCatch } from '@repo/utils' import { readFileSync } from 'node:fs' +import { createInterface } from 'node:readline/promises' import { parseArgs } from 'node:util' import { createPublicClient, @@ -64,6 +65,19 @@ import { encodeRatifierData, hashOffer, isLeaf, signOfferTree, toId } from './se import { DEFAULT_TICK_SPACING, priceToTick, tickToPrice } from './seed/price-tick' import { priceDropToLiquidateBps, sizePosition } from './seed/sizing' +// bun exposed a global synchronous `confirm()`; Node does not. Same contract: anything other than an +// explicit y/yes is a decline, and a non-interactive stdin declines rather than hanging a CI run. +const confirmPrompt = async (question: string): Promise => { + if (!process.stdin.isTTY) return false + const rl = createInterface({ input: process.stdin, output: process.stdout }) + try { + const answer = await rl.question(`${question} [y/N] `) + return /^y(es)?$/i.test(answer.trim()) + } finally { + rl.close() + } +} + const CHAIN_ID = 8453 const MIDNIGHT = getAddress('0xAdedD8ab6dE832766Fedf0FaC4992E5C4D3EA18A') const PRIVATE_KEY_HEX_LENGTH = 66 @@ -768,7 +782,7 @@ async function main() { logger.info('seed.dry_run_complete', { detail: 'self-checks passed; no transactions sent' }) return } - if (!args.yes && !confirm('Proceed to send REAL transactions on Base mainnet?')) { + if (!args.yes && !(await confirmPrompt('Proceed to send REAL transactions on Base mainnet?'))) { logger.warn('seed.aborted', { detail: 'user declined' }) return } diff --git a/bots/midnight-liquidation/soltag.preload.ts b/bots/midnight-liquidation/soltag.preload.ts deleted file mode 100644 index cd982b58..00000000 --- a/bots/midnight-liquidation/soltag.preload.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { transformSolTemplates } from 'soltag/unplugin' - -// soltag is a build-time-only transform: its `sol``` tagged templates throw at runtime unless a -// bundler/loader plugin compiles them (via solc). unplugin's own bun adapter (`unplugin.bun()`) is -// unusable under bun — it returns `undefined` from `onLoad` for files it doesn't transform, which -// bun rejects ("Expected module mock to return an object"). So we drive soltag's exported core -// transform from a bun loader plugin: it matches every TS source under this bot (where soltag is a -// dependency — other workspaces don't use it), and returns files unchanged when they hold no -// template to compile. Wired via bunfig `preload` for both `bun test` and `bun run`. - -const BOT_DIR = import.meta.dir -const ESCAPED = BOT_DIR.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') -// Matches `/**/*.{ts,tsx,mts,cts}`, excluding any nested node_modules. -const INCLUDE = new RegExp(`^${ESCAPED}/(?:(?!/node_modules/).)*\\.(?:m|c)?tsx?$`) - -Bun.plugin({ - name: 'soltag', - setup(build) { - build.onLoad({ filter: INCLUDE }, async ({ path }) => { - const source = await Bun.file(path).text() - const loader = path.endsWith('.tsx') ? 'tsx' : 'ts' - // Enable the optimizer — the lens's per-element computation has enough locals to hit - // "stack too deep" without it. - const transformed = transformSolTemplates(source, path, { - solc: { optimizer: { enabled: true, runs: 200 } } - }) - return { contents: transformed?.code ?? source, loader } - }) - } -}) diff --git a/bots/midnight-liquidation/src/config.ts b/bots/midnight-liquidation/src/config.ts index c3987edf..e06af5b8 100644 --- a/bots/midnight-liquidation/src/config.ts +++ b/bots/midnight-liquidation/src/config.ts @@ -285,7 +285,7 @@ function addressListEnv(env: Env, name: string): Address[] { * run separately at startup in `index.ts` once a public client exists. */ export function loadConfig( - env: Env = Bun.env, + env: Env = process.env, deps: { chainMap?: Record } = {} ): Config { const chainMap = deps.chainMap ?? CHAIN_MAP diff --git a/bots/midnight-liquidation/src/index.ts b/bots/midnight-liquidation/src/index.ts index 3e3f8f72..f26da88c 100644 --- a/bots/midnight-liquidation/src/index.ts +++ b/bots/midnight-liquidation/src/index.ts @@ -98,9 +98,9 @@ async function main() { // no-key → bad-debt-only opt-in). Keys are read HERE, at the point of use, and live only in this // closure — never on the (logged) Config object. const apiKeys: Partial> = {} - if (Bun.env.ZEROX_API_KEY) apiKeys['0x'] = Bun.env.ZEROX_API_KEY - if (Bun.env.ONEINCH_API_KEY) apiKeys['1inch'] = Bun.env.ONEINCH_API_KEY - if (Bun.env.LIFI_API_KEY) apiKeys.lifi = Bun.env.LIFI_API_KEY + if (process.env.ZEROX_API_KEY) apiKeys['0x'] = process.env.ZEROX_API_KEY + if (process.env.ONEINCH_API_KEY) apiKeys['1inch'] = process.env.ONEINCH_API_KEY + if (process.env.LIFI_API_KEY) apiKeys.lifi = process.env.LIFI_API_KEY const venues = config.venues.enabled if (venues.length === 0) { logger.warn('quoting.no_routes', { @@ -293,7 +293,7 @@ async function main() { logger }) const heartbeatMonitor = createHeartbeatMonitor({ - url: Bun.env.BETTERSTACK_HEARTBEAT_URL, + url: process.env.BETTERSTACK_HEARTBEAT_URL, logger }) void heartbeatMonitor.start() diff --git a/bots/midnight-liquidation/src/state/lens.sol.ts b/bots/midnight-liquidation/src/state/lens.sol.ts index df92fb87..3db84d95 100644 --- a/bots/midnight-liquidation/src/state/lens.sol.ts +++ b/bots/midnight-liquidation/src/state/lens.sol.ts @@ -15,7 +15,8 @@ import type { Market } from '../execution/encode-call' // struct, so no off-chain market input or `id == toId(market)` re-check is needed — then composes // liquidatability + the sizing inputs (maxDebt/badDebt/best-collateral) the way liquidate() does and // returns the Market so the caller can encode the liquidate call. Compiled to a deployless factory by -// the soltag bun preload (see ../../soltag.preload.ts); `sol``` throws if not active. +// the soltag transform — the esbuild plugin in scripts/build.ts for the shipped bundle, and +// soltag/vite for tests; `sol``` throws if neither is active. export const MidnightLiquidationLens = sol('MidnightLiquidationLens')` // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.19; diff --git a/bots/midnight-liquidation/test/config.test.ts b/bots/midnight-liquidation/test/config.test.ts index 2b670a06..1e91c954 100644 --- a/bots/midnight-liquidation/test/config.test.ts +++ b/bots/midnight-liquidation/test/config.test.ts @@ -1,9 +1,9 @@ import type { Address } from 'viem' import { Executor } from '@repo/contracts' -import { describe, expect, it } from 'bun:test' import { getAddress, parseGwei } from 'viem' import { mainnet } from 'viem/chains' +import { describe, expect, it } from 'vitest' import type { ChainConfig, Config } from '../src/config' diff --git a/bots/midnight-liquidation/test/constants.test.ts b/bots/midnight-liquidation/test/constants.test.ts index c029c111..9ffd597c 100644 --- a/bots/midnight-liquidation/test/constants.test.ts +++ b/bots/midnight-liquidation/test/constants.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'bun:test' import { keccak256, parseEther, stringToBytes } from 'viem' +import { describe, expect, it } from 'vitest' import { CALLBACK_SUCCESS, diff --git a/bots/midnight-liquidation/test/contracts/executor.sol.test.ts b/bots/midnight-liquidation/test/contracts/executor.sol.test.ts index 5fa242bc..508c6078 100644 --- a/bots/midnight-liquidation/test/contracts/executor.sol.test.ts +++ b/bots/midnight-liquidation/test/contracts/executor.sol.test.ts @@ -1,10 +1,11 @@ import { Executor } from '@repo/contracts' -import { describe, expect, test } from 'bun:test' import { executorAbi as upstreamExecutorAbi } from 'executooor-viem' import { readFileSync } from 'node:fs' -import { join } from 'node:path' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' import solc from 'solc' import { toFunctionSelector } from 'viem' +import { describe, expect, test } from 'vitest' // Verifies the vendored Executor singleton (@repo/contracts/solidity/Executor.sol — a single // self-contained file with IExecutor/Placeholder inlined): it must compile clean for Cancun, its @@ -13,7 +14,10 @@ import { toFunctionSelector } from 'viem' // constructor, same call surface). solc runs once per file (~seconds), so all assertions share one // compile. -const CONTRACTS_DIR = join(import.meta.dir, '../../../../packages/contracts/solidity') +const CONTRACTS_DIR = join( + dirname(fileURLToPath(import.meta.url)), + '../../../../packages/contracts/solidity' +) function compile() { const input = { diff --git a/bots/midnight-liquidation/test/discovery/borrowers.test.ts b/bots/midnight-liquidation/test/discovery/borrowers.test.ts index ec7eb2a7..3b03effe 100644 --- a/bots/midnight-liquidation/test/discovery/borrowers.test.ts +++ b/bots/midnight-liquidation/test/discovery/borrowers.test.ts @@ -1,8 +1,8 @@ import type { Logger } from '@repo/bot-kit' import type { Hex } from 'viem' -import { describe, expect, it } from 'bun:test' import { getAddress } from 'viem' +import { describe, expect, it } from 'vitest' import type { CandidatePage, FetchCandidatePage } from '../../src/discovery/borrowers' @@ -213,6 +213,6 @@ describe('createApiCandidateSource', () => { healthFactorLte: 1.02, fetchImpl }) - expect(source('bad-cursor')).rejects.toThrow('HTTP 400') + await expect(source('bad-cursor')).rejects.toThrow('HTTP 400') }) }) diff --git a/bots/midnight-liquidation/test/discovery/markets.test.ts b/bots/midnight-liquidation/test/discovery/markets.test.ts index a5ccf7fe..024732c4 100644 --- a/bots/midnight-liquidation/test/discovery/markets.test.ts +++ b/bots/midnight-liquidation/test/discovery/markets.test.ts @@ -1,7 +1,7 @@ import type { Logger } from '@repo/bot-kit' import type { Hex } from 'viem' -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import { createListedMarketFilter } from '../../src/discovery/markets' diff --git a/bots/midnight-liquidation/test/execution/encode-call.test.ts b/bots/midnight-liquidation/test/execution/encode-call.test.ts index 5f767af5..175e976a 100644 --- a/bots/midnight-liquidation/test/execution/encode-call.test.ts +++ b/bots/midnight-liquidation/test/execution/encode-call.test.ts @@ -2,7 +2,6 @@ import type { SwapPlan } from '@repo/swaps' import type { Hex } from 'viem' import { MidnightAbi } from '@repo/contracts' -import { describe, expect, it } from 'bun:test' import { executorAbi } from 'executooor-viem' import { decodeAbiParameters, @@ -12,6 +11,7 @@ import { isAddressEqual, zeroAddress } from 'viem' +import { describe, expect, it } from 'vitest' import type { CollateralParams, Market } from '../../src/execution/encode-call' diff --git a/bots/midnight-liquidation/test/execution/swap-step.test.ts b/bots/midnight-liquidation/test/execution/swap-step.test.ts index 604bfe68..255a510e 100644 --- a/bots/midnight-liquidation/test/execution/swap-step.test.ts +++ b/bots/midnight-liquidation/test/execution/swap-step.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'bun:test' import { getAddress, zeroAddress } from 'viem' +import { describe, expect, it } from 'vitest' import type { LiquidationPlan } from '../../src/sizing/plan' import type { LensOut } from '../../src/state/lens.sol' diff --git a/bots/midnight-liquidation/test/fork/harness.ts b/bots/midnight-liquidation/test/fork/harness.ts index 40a6f42d..3b4adc04 100644 --- a/bots/midnight-liquidation/test/fork/harness.ts +++ b/bots/midnight-liquidation/test/fork/harness.ts @@ -1,8 +1,10 @@ -import type { Subprocess } from 'bun' +import type { ChildProcess } from 'node:child_process' import type { Address, Hex } from 'viem' import { Executor } from '@repo/contracts' import { ensureError } from '@repo/utils' +import { spawn } from 'node:child_process' +import { setTimeout as sleep } from 'node:timers/promises' import { createTestClient, createWalletClient, http, parseEther, publicActions } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { base } from 'viem/chains' @@ -14,7 +16,7 @@ import { base } from 'viem/chains' // end-to-end), then the suite warps past `maturity` to make it post-maturity liquidatable. FORK_BLOCK // is a fixed block shortly after the deploy so the WETH oracle price and the WETH/USDC pool are // deterministic; RPC_URL_8453 must be an archive endpoint that serves it (CI secret; locally set it in -// .env.test.local — bun does NOT load .env.local under NODE_ENV=test). +// .env.test.local — loaded explicitly by vitest.config.ts via vite's loadEnv). const FORK_BLOCK = 48_300_000n export const MIDNIGHT = '0xAdedD8ab6dE832766Fedf0FaC4992E5C4D3EA18A' as Address @@ -68,8 +70,23 @@ export type TestClient = ReturnType // instances: when the process is slow to exit (its keep-alive sockets stall the SIGTERM it sends), an // internal `stopTimeout` promise escapes as an unhandled "Anvil failed to stop in time" rejection // that bun's test runner fails the suite on, and the child is left orphaned (port bound for the next -// run). A bare `Bun.spawn` gives us `.kill('SIGKILL')` + `.exited`, which terminate deterministically. -export type ForkHandle = Subprocess +// run). A bare child_process spawn gives us SIGKILL + an exit promise, which terminate deterministically. +export type ForkHandle = { + kill: (signal: NodeJS.Signals) => void + /** Resolves with the exit code once the child has actually gone (Node has no `.exited`). */ + exited: Promise +} + +// Wraps a ChildProcess in the tiny surface the teardown needs. The `exited` promise is built here, +// at spawn time, so its `exit` listener is attached before the child can possibly exit. +const toForkHandle = (child: ChildProcess): ForkHandle => ({ + kill: signal => { + child.kill(signal) + }, + exited: new Promise(resolve => { + child.once('exit', code => resolve(code)) + }) +}) /** Polls the JSON-RPC endpoint until it answers `eth_blockNumber`, so callers see a ready node. */ async function waitForRpc(url: string, timeoutMs = 30_000): Promise { @@ -89,35 +106,43 @@ async function waitForRpc(url: string, timeoutMs = 30_000): Promise { } catch (error) { lastError = ensureError(error) } - await Bun.sleep(100) + await sleep(100) } const detail = lastError ? `: ${lastError.message}` : '' throw new Error(`anvil RPC at ${url} not ready within ${timeoutMs}ms${detail}`) } +// Anvil port registry — vitest runs test FILES IN PARALLEL (bun's runner was serial, so fixed ports +// used to be safe within a bot only). Every fork suite in the repo must claim a distinct port: +// 8545 bots/blue-liquidation fork/liquidation +// 8546 bots/market-making e2e/setup-check +// 8547 bots/midnight-liquidation fork/liquidation +// 8548 bots/midnight-liquidation fork/queue /** * Boots an anvil instance forking Base at FORK_BLOCK (chain id pinned to Base so signatures match). - * `port` is explicit so multiple fork test files can run in one `bun test` process without colliding. + * `port` is explicit so parallel fork test files never collide — see the registry above. * Requires the `anvil` binary on PATH (Foundry locally; foundry-toolchain in CI). */ export async function startFork(port = 8545): Promise<{ anvil: ForkHandle; rpcUrl: string }> { - const anvil = Bun.spawn( - [ + const anvil = toForkHandle( + spawn( 'anvil', - '--fork-url', - FORK_URL, - '--fork-block-number', - String(FORK_BLOCK), - '--chain-id', - String(base.id), - // The deployed Midnight is compiled for the `osaka` EVM and uses the CLZ opcode (EIP-7939) in - // liquidate; anvil's default hardfork lacks CLZ, so it would revert with empty data. Pin osaka. - '--hardfork', - 'osaka', - '--port', - String(port) - ], - { stdout: 'ignore', stderr: 'ignore' } + [ + '--fork-url', + FORK_URL, + '--fork-block-number', + String(FORK_BLOCK), + '--chain-id', + String(base.id), + // The deployed Midnight is compiled for the `osaka` EVM and uses the CLZ opcode (EIP-7939) in + // liquidate; anvil's default hardfork lacks CLZ, so it would revert with empty data. Pin osaka. + '--hardfork', + 'osaka', + '--port', + String(port) + ], + { stdio: 'ignore' } + ) ) const rpcUrl = `http://127.0.0.1:${port}` await waitForRpc(rpcUrl) diff --git a/bots/midnight-liquidation/test/fork/liquidation.test.ts b/bots/midnight-liquidation/test/fork/liquidation.test.ts index d0993332..4839f43f 100644 --- a/bots/midnight-liquidation/test/fork/liquidation.test.ts +++ b/bots/midnight-liquidation/test/fork/liquidation.test.ts @@ -10,9 +10,9 @@ import { } from '@repo/bot-kit' import { quoteUniswapV3 } from '@repo/swaps' import { lensKey } from '@repo/utils' -import { afterAll, beforeAll, describe, expect, it } from 'bun:test' import { erc20Abi, parseGwei } from 'viem' import { base } from 'viem/chains' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { encodeLiquidationExec } from '../../src/execution/encode-call' import { expectedLoanOut } from '../../src/execution/swap-step' @@ -60,7 +60,7 @@ describe('fork: end-to-end liquidation against a real Base position', () => { } beforeAll(async () => { - const fork = await startFork() + const fork = await startFork(8547) // see the port registry in harness.ts anvil = fork.anvil test = testClient(fork.rpcUrl) await fundEth(test, LIQUIDATOR) diff --git a/bots/midnight-liquidation/test/fork/queue.test.ts b/bots/midnight-liquidation/test/fork/queue.test.ts index ded9a91f..6135916e 100644 --- a/bots/midnight-liquidation/test/fork/queue.test.ts +++ b/bots/midnight-liquidation/test/fork/queue.test.ts @@ -1,7 +1,7 @@ import { createLogger, createPendingQueue, createSigner, initialFees } from '@repo/bot-kit' -import { afterAll, beforeAll, describe, expect, it } from 'bun:test' import { parseGwei } from 'viem' import { base } from 'viem/chains' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { type ForkHandle, @@ -24,7 +24,7 @@ describe('fork: pending-queue bump + replacement against a real node', () => { let rpcUrl: string beforeAll(async () => { - const fork = await startFork(8546) // distinct port from the liquidation suite + const fork = await startFork(8548) // see the port registry in harness.ts anvil = fork.anvil rpcUrl = fork.rpcUrl test = testClient(rpcUrl) diff --git a/bots/midnight-liquidation/test/quotes.test.ts b/bots/midnight-liquidation/test/quotes.test.ts index 729aaf4e..861feeb7 100644 --- a/bots/midnight-liquidation/test/quotes.test.ts +++ b/bots/midnight-liquidation/test/quotes.test.ts @@ -1,8 +1,8 @@ import type { Logger } from '@repo/bot-kit' import type { RateLimitedClient, VenuePair, VenueQuoteEstimate, VenueSelector } from '@repo/swaps' -import { describe, expect, it } from 'bun:test' import { getAddress } from 'viem' +import { describe, expect, it } from 'vitest' import type { Market } from '../src/execution/encode-call' import type { LiquidationPlan } from '../src/sizing/plan' diff --git a/bots/midnight-liquidation/test/runner/eligibility.test.ts b/bots/midnight-liquidation/test/runner/eligibility.test.ts index 99ba0d72..cf4872d9 100644 --- a/bots/midnight-liquidation/test/runner/eligibility.test.ts +++ b/bots/midnight-liquidation/test/runner/eligibility.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'bun:test' import { getAddress } from 'viem' +import { describe, expect, it } from 'vitest' import type { LensOut } from '../../src/state/lens.sol' diff --git a/bots/midnight-liquidation/test/runner/tick.test.ts b/bots/midnight-liquidation/test/runner/tick.test.ts index 748c6ef1..ee826b01 100644 --- a/bots/midnight-liquidation/test/runner/tick.test.ts +++ b/bots/midnight-liquidation/test/runner/tick.test.ts @@ -5,8 +5,8 @@ import type { Address, Hex } from 'viem' import { createBackoff, createCooldownStore } from '@repo/bot-kit' import { lensKey } from '@repo/utils' -import { describe, expect, it } from 'bun:test' import { getAddress } from 'viem' +import { describe, expect, it } from 'vitest' import type { BorrowerCandidate } from '../../src/discovery/borrowers' import type { LensInput, LensOut } from '../../src/state/lens.sol' diff --git a/bots/midnight-liquidation/test/seed/offers.test.ts b/bots/midnight-liquidation/test/seed/offers.test.ts index fc098586..24bdb2d7 100644 --- a/bots/midnight-liquidation/test/seed/offers.test.ts +++ b/bots/midnight-liquidation/test/seed/offers.test.ts @@ -6,8 +6,8 @@ import type { Address, Hex } from 'viem' -import { describe, expect, it } from 'bun:test' import { keccak256, toHex, zeroAddress } from 'viem' +import { describe, expect, it } from 'vitest' import type { Offer } from '../../scripts/seed/offers' import type { CollateralParams, Market } from '../../src/execution/encode-call' diff --git a/bots/midnight-liquidation/test/sizing/lif.test.ts b/bots/midnight-liquidation/test/sizing/lif.test.ts index d5273ef6..770923e6 100644 --- a/bots/midnight-liquidation/test/sizing/lif.test.ts +++ b/bots/midnight-liquidation/test/sizing/lif.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import { lifAt } from '../../src/sizing/lif' diff --git a/bots/midnight-liquidation/test/sizing/plan.test.ts b/bots/midnight-liquidation/test/sizing/plan.test.ts index 436161e1..126e8649 100644 --- a/bots/midnight-liquidation/test/sizing/plan.test.ts +++ b/bots/midnight-liquidation/test/sizing/plan.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import type { LiquidationPlan, PlanInput } from '../../src/sizing/plan' diff --git a/bots/midnight-liquidation/test/sizing/rcf.test.ts b/bots/midnight-liquidation/test/sizing/rcf.test.ts index 63f32179..b80f1071 100644 --- a/bots/midnight-liquidation/test/sizing/rcf.test.ts +++ b/bots/midnight-liquidation/test/sizing/rcf.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'bun:test' import { maxUint256 } from 'viem' +import { describe, expect, it } from 'vitest' import { ORACLE_PRICE_SCALE, WAD } from '../../src/constants' import { isRcfExempt, maxRepaidPreMaturity } from '../../src/sizing/rcf' diff --git a/bots/midnight-liquidation/test/state/lens.sol.test.ts b/bots/midnight-liquidation/test/state/lens.sol.test.ts index 991f4e05..1f8e515e 100644 --- a/bots/midnight-liquidation/test/state/lens.sol.test.ts +++ b/bots/midnight-liquidation/test/state/lens.sol.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'bun:test' import { decodeFunctionResult, encodeFunctionResult, getAddress } from 'viem' +import { describe, expect, it } from 'vitest' import type { Market } from '../../src/execution/encode-call' import type { LensOut } from '../../src/state/lens.sol' @@ -31,7 +31,7 @@ const MARKET: Market = { describe('MidnightLiquidationLens', () => { it('compiles via soltag and binds the Midnight address into the factory call', () => { - // Proves the soltag bun preload compiled the inline Solidity (sol``` would otherwise throw) + // Proves the soltag/vite transform compiled the inline Solidity (sol``` would otherwise throw) // and that constructor binding produced a deployless factory call. const compiled = MidnightLiquidationLens.with(MIDNIGHT) expect(compiled.factoryData.length).toBeGreaterThan(2) diff --git a/bots/midnight-liquidation/tsconfig.json b/bots/midnight-liquidation/tsconfig.json index 7ad64bb1..dccc6c59 100644 --- a/bots/midnight-liquidation/tsconfig.json +++ b/bots/midnight-liquidation/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@repo/typescript-config/base", "compilerOptions": { - "types": ["bun"], + "types": ["node"], "plugins": [{ "name": "soltag/plugin" }] }, "include": ["src", "test", "scripts", ".soltag/types.d.ts"], diff --git a/bots/midnight-liquidation/vitest.config.ts b/bots/midnight-liquidation/vitest.config.ts new file mode 100644 index 00000000..c6e9c1fb --- /dev/null +++ b/bots/midnight-liquidation/vitest.config.ts @@ -0,0 +1,23 @@ +import { fileURLToPath } from 'node:url' +import soltag from 'soltag/vite' +import { loadEnv } from 'vite' +import { defineConfig } from 'vitest/config' + +const BOT_DIR = fileURLToPath(new URL('.', import.meta.url)) + +// soltag is a build-time-only transform: its `sol``` tagged templates throw at runtime unless a +// plugin compiles them (via solc). The vite plugin transforms this project's TS sources and leaves +// files without templates unchanged — it replaces the hand-rolled bun loader plugin that used to be +// wired through bunfig `preload`. Enable the optimizer: the lens's per-element computation has +// enough locals to hit "stack too deep" without it. +export default defineConfig({ + plugins: [soltag({ solc: { optimizer: { enabled: true, runs: 200 } } })], + test: { + name: 'midnight-liquidation', + // bun auto-loaded .env files; vitest does not. The fork suite reads RPC_URL_8453 from + // .env.test.local and fails loud when it is unset. `loadEnv` with an empty prefix passes an + // already-exported value straight through (so CI's secret survives) and omits the key entirely + // when no file supplies it — it never substitutes an empty string, so CI cannot silently skip. + env: loadEnv('test', BOT_DIR, '') + } +}) diff --git a/bunfig.toml b/bunfig.toml deleted file mode 100644 index d7a00e63..00000000 --- a/bunfig.toml +++ /dev/null @@ -1,9 +0,0 @@ -# bun remains the runtime + test runner; pnpm owns installs (see pnpm-workspace.yaml). - -# Compile the liquidation bots' soltag `sol``` lens templates during `bun test` (each plugin -# self-scopes to its own bot's files, so both can be preloaded — other workspaces are untouched). -[test] -preload = [ - "./bots/midnight-liquidation/soltag.preload.ts", - "./bots/blue-liquidation/soltag.preload.ts" -] diff --git a/docs/CONVENTIONS.md b/docs/CONVENTIONS.md index 338af872..7e930040 100644 --- a/docs/CONVENTIONS.md +++ b/docs/CONVENTIONS.md @@ -54,7 +54,7 @@ ### Environment Variables -- **Direct `Bun.env` access**: Bots read `Bun.env.VARIABLE_NAME` directly at the point of use. +- **Direct `process.env` access**: Bots read `process.env.VARIABLE_NAME` directly at the point of use. There is no helper wrapper and no runtime schema layer — if a required variable is missing, fail loudly at startup (throw and exit), don't silently degrade. - **Never committed**: Secrets and runtime config live in local `.env` files or deploy-time @@ -99,8 +99,9 @@ - **Assertion Precision**: Use exact matchers (`toBe`, `toEqual`, `toStrictEqual`); only use approximate matchers (e.g., floating-point arithmetic, time-dependent values) with a comment explaining why exact matching is not feasible. -- **Bun test runner**: Tests run under `bun test`. The runner is Vitest-compatible; existing - Vitest-style assertions and spies carry over. +- **Vitest test runner**: Tests run under `vitest` (`pnpm test` at the root, which drives every + workspace project listed in the root `vitest.config.ts`). Mocks and spies come from `vi` + (`vi.fn`, `vi.spyOn`, `vi.restoreAllMocks`). ### Testing Anti-Patterns diff --git a/knip.json b/knip.json index 724a355f..b0580dc1 100644 --- a/knip.json +++ b/knip.json @@ -2,7 +2,7 @@ "$schema": "https://unpkg.com/knip@5/schema.json", "exclude": ["enumMembers", "classMembers"], "ignore": [".claude/**"], - "ignoreBinaries": ["forge", "railway"], + "ignoreBinaries": ["forge", "mkfifo", "railway"], "workspaces": { ".": {}, "packages/typescript-config": {}, @@ -15,9 +15,12 @@ "packages/offers": {}, "packages/swaps": {}, "bots/midnight-liquidation": { - "ignore": ["src/generated/**"] + "ignore": ["src/generated/**"], + "entry": ["src/index.ts", "scripts/seed-liquidatable-positions.ts"] + }, + "bots/blue-liquidation": { + "entry": ["src/index.ts", "scripts/probe-live-lens.ts"] }, - "bots/blue-liquidation": {}, "bots/midnight-crossed-books": { "ignore": ["src/infrastructure/*/generated/**"] }, diff --git a/package.json b/package.json index d2411dc7..85ccd160 100644 --- a/package.json +++ b/package.json @@ -6,24 +6,24 @@ "format": "oxfmt", "format:check": "oxfmt --check", "knip": "knip", - "lint": "oxlint --no-error-on-unmatched-pattern && bun run lint:playground-smoke", + "lint": "oxlint --no-error-on-unmatched-pattern", "lint:fix": "oxlint --fix --no-error-on-unmatched-pattern", - "lint:playground-smoke": "oxlint --config bots/market-making/scripts/playground-smoke.oxlintrc.json bots/market-making/scripts/playground-smoke*.mjs", - "market-making:playground": "node bots/market-making/scripts/playground-serve.mjs", "prepare": "husky", - "test": "bun test", - "test:browser": "bun run --filter @morpho-org/market-making-bot playground:smoke:test" + "test": "vitest run" }, "devDependencies": { "@repo/typescript-config": "workspace:*", - "esbuild": "0.25.12", + "@types/node": "catalog:", "husky": "catalog:", "knip": "catalog:", "lint-staged": "catalog:", "oxfmt": "catalog:", "oxlint": "catalog:", "oxlint-tsgolint": "catalog:", - "typescript": "catalog:" + "tsx": "catalog:", + "typescript": "catalog:", + "vite": "catalog:", + "vitest": "catalog:" }, "engines": { "node": "^24.14.1" diff --git a/packages/bot-kit/package.json b/packages/bot-kit/package.json index ff657d77..3eb1c8f5 100644 --- a/packages/bot-kit/package.json +++ b/packages/bot-kit/package.json @@ -31,7 +31,8 @@ }, "devDependencies": { "@repo/typescript-config": "workspace:*", - "@types/bun": "catalog:", - "typescript": "catalog:" + "@types/node": "catalog:", + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/bot-kit/test/balance.test.ts b/packages/bot-kit/test/balance.test.ts index 1f36e14b..288b33b9 100644 --- a/packages/bot-kit/test/balance.test.ts +++ b/packages/bot-kit/test/balance.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'bun:test' import { parseEther } from 'viem' +import { describe, expect, it } from 'vitest' import type { Logger, LogLevel } from '../src/logger' diff --git a/packages/bot-kit/test/client.test.ts b/packages/bot-kit/test/client.test.ts index ddb77423..2c59b942 100644 --- a/packages/bot-kit/test/client.test.ts +++ b/packages/bot-kit/test/client.test.ts @@ -1,8 +1,8 @@ import type { Client } from 'viem' -import { describe, expect, it } from 'bun:test' import { getAddress } from 'viem' import { base } from 'viem/chains' +import { describe, expect, it } from 'vitest' import { assertContractDeployed, createDeploylessClient } from '../src/client' @@ -32,10 +32,10 @@ describe('createDeploylessClient', () => { }) describe('assertContractDeployed', () => { - it('throws when the address holds no code', () => { - expect(assertContractDeployed(stubClient('0x'), ADDRESS, 'EXECUTOOOR_ADDRESS')).rejects.toThrow( - /EXECUTOOOR_ADDRESS/ - ) + it('throws when the address holds no code', async () => { + await expect( + assertContractDeployed(stubClient('0x'), ADDRESS, 'EXECUTOOOR_ADDRESS') + ).rejects.toThrow(/EXECUTOOOR_ADDRESS/) }) it('resolves when the address holds code', async () => { diff --git a/packages/bot-kit/test/heartbeat.test.ts b/packages/bot-kit/test/heartbeat.test.ts index b81ee88a..a16a2c72 100644 --- a/packages/bot-kit/test/heartbeat.test.ts +++ b/packages/bot-kit/test/heartbeat.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, mock, spyOn } from 'bun:test' +import { afterEach, describe, expect, it, vi } from 'vitest' import type { Logger, LogLevel } from '../src/logger' @@ -20,7 +20,7 @@ function captureLogger() { } describe('createHeartbeatMonitor', () => { - afterEach(() => mock.restore()) + afterEach(() => vi.restoreAllMocks()) it('is inert when the URL is unset', async () => { const { logger } = captureLogger() @@ -62,7 +62,7 @@ describe('createHeartbeatMonitor', () => { const { logger } = captureLogger() const urls: string[] = [] let callback: (() => void) | undefined - const setIntervalSpy = spyOn(globalThis, 'setInterval').mockImplementation((( + const setIntervalSpy = vi.spyOn(globalThis, 'setInterval').mockImplementation((( ...args: Parameters ) => { const [handler, delay] = args diff --git a/packages/bot-kit/test/logger.test.ts b/packages/bot-kit/test/logger.test.ts index b0e4c0a4..735b5f4f 100644 --- a/packages/bot-kit/test/logger.test.ts +++ b/packages/bot-kit/test/logger.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, mock, spyOn } from 'bun:test' +import { afterEach, describe, expect, it, vi } from 'vitest' import type { Logger } from '../src/logger' @@ -17,12 +17,12 @@ function parseLine(raw: unknown) { describe('createLogger', () => { // Restore console spies even when an assertion throws first, so a failure in one test // cannot leak its captured calls into the next. - afterEach(() => mock.restore()) + afterEach(() => vi.restoreAllMocks()) it('drops lines below the configured minimum level', () => { const logger: Logger = createLogger('warn') - const log = spyOn(console, 'log').mockImplementation(() => undefined) - const err = spyOn(console, 'error').mockImplementation(() => undefined) + const log = vi.spyOn(console, 'log').mockImplementation(() => undefined) + const err = vi.spyOn(console, 'error').mockImplementation(() => undefined) logger.debug('rindexer.lag') logger.info('block.new') @@ -35,8 +35,8 @@ describe('createLogger', () => { it('routes every level to stderr', () => { const logger = createLogger('debug') - const log = spyOn(console, 'log').mockImplementation(() => undefined) - const err = spyOn(console, 'error').mockImplementation(() => undefined) + const log = vi.spyOn(console, 'log').mockImplementation(() => undefined) + const err = vi.spyOn(console, 'error').mockImplementation(() => undefined) logger.debug('a') logger.info('b') @@ -49,7 +49,7 @@ describe('createLogger', () => { it('stamps the current utc time onto every line', () => { const logger = createLogger('debug') - const err = spyOn(console, 'error').mockImplementation(() => undefined) + const err = vi.spyOn(console, 'error').mockImplementation(() => undefined) const fmt = (date: Date) => `${date.toISOString().slice(5, 10)} ${date.toISOString().slice(11, 19)}` @@ -64,7 +64,7 @@ describe('createLogger', () => { it('serializes bigint fields as decimal strings', () => { const logger = createLogger('debug') - const err = spyOn(console, 'error').mockImplementation(() => undefined) + const err = vi.spyOn(console, 'error').mockImplementation(() => undefined) logger.info('tx.sent', { nonce: 7n, maxFee: 300_000_000_000n }) @@ -78,7 +78,7 @@ describe('createLogger', () => { it('recurses into nested bigint fields', () => { const logger = createLogger('debug') - const err = spyOn(console, 'error').mockImplementation(() => undefined) + const err = vi.spyOn(console, 'error').mockImplementation(() => undefined) logger.info('tx.bumped', { tx: { nonce: 7n }, attempts: [1n, 2n] }) @@ -92,7 +92,7 @@ describe('createLogger', () => { it('stamps bound context onto every line', () => { const logger = createLogger('debug', { context: { bot: 'blue-liquidation', chainId: 8453 } }) - const err = spyOn(console, 'error').mockImplementation(() => undefined) + const err = vi.spyOn(console, 'error').mockImplementation(() => undefined) logger.info('block.new', { height: 42n }) logger.warn('state.reset') @@ -117,17 +117,17 @@ describe('createLogger', () => { }) describe('createLogger BetterStack opt-in contract', () => { - afterEach(() => mock.restore()) + afterEach(() => vi.restoreAllMocks()) it('stays fully silent when BOTH env vars are unset', () => { - const err = spyOn(console, 'error').mockImplementation(() => undefined) + const err = vi.spyOn(console, 'error').mockImplementation(() => undefined) createLogger('info', { env: {} }) // Both unset is the opt-out: no warning line at construction. expect(err).not.toHaveBeenCalled() }) it('token-only fails loud (names the missing host)', () => { - const err = spyOn(console, 'error').mockImplementation(() => undefined) + const err = vi.spyOn(console, 'error').mockImplementation(() => undefined) createLogger('info', { env: { BETTERSTACK_SOURCE_TOKEN: 'tok' } }) expect(err).toHaveBeenCalledTimes(1) @@ -138,7 +138,7 @@ describe('createLogger BetterStack opt-in contract', () => { }) it('host-only fails loud (names the missing token)', () => { - const err = spyOn(console, 'error').mockImplementation(() => undefined) + const err = vi.spyOn(console, 'error').mockImplementation(() => undefined) createLogger('info', { env: { BETTERSTACK_INGESTING_HOST: 's1.betterstackdata.com' } }) expect(err).toHaveBeenCalledTimes(1) @@ -148,7 +148,7 @@ describe('createLogger BetterStack opt-in contract', () => { }) it('treats blank/whitespace as unset — a blank token with a host still fails loud', () => { - const err = spyOn(console, 'error').mockImplementation(() => undefined) + const err = vi.spyOn(console, 'error').mockImplementation(() => undefined) // Blank/whitespace does not count as set: this is token-unset + host-set → partial config. createLogger('info', { env: { BETTERSTACK_SOURCE_TOKEN: ' ', BETTERSTACK_INGESTING_HOST: 'h' } @@ -178,12 +178,14 @@ describe('createLogger BetterStack opt-in contract', () => { }) describe('createLogger BetterStack path', () => { - afterEach(() => mock.restore()) + afterEach(() => vi.restoreAllMocks()) it('performs zero network activity when the env vars are unset', () => { - const fetchSpy = spyOn(globalThis, 'fetch').mockImplementation((() => - Promise.reject(new Error('network must not be touched'))) as unknown as typeof fetch) - spyOn(console, 'error').mockImplementation(() => undefined) + const fetchSpy = vi + .spyOn(globalThis, 'fetch') + .mockImplementation((() => + Promise.reject(new Error('network must not be touched'))) as unknown as typeof fetch) + vi.spyOn(console, 'error').mockImplementation(() => undefined) const logger = createLogger('debug', { env: {} }) logger.info('tx.sent', { nonce: 1n }) @@ -192,9 +194,11 @@ describe('createLogger BetterStack path', () => { }) it('warns and performs zero network activity under partial (token-only) config', () => { - const fetchSpy = spyOn(globalThis, 'fetch').mockImplementation((() => - Promise.reject(new Error('network must not be touched'))) as unknown as typeof fetch) - const err = spyOn(console, 'error').mockImplementation(() => undefined) + const fetchSpy = vi + .spyOn(globalThis, 'fetch') + .mockImplementation((() => + Promise.reject(new Error('network must not be touched'))) as unknown as typeof fetch) + const err = vi.spyOn(console, 'error').mockImplementation(() => undefined) const logger = createLogger('debug', { env: { BETTERSTACK_SOURCE_TOKEN: 'tok' } }) logger.info('tx.sent', { nonce: 1n }) @@ -207,9 +211,11 @@ describe('createLogger BetterStack path', () => { it('serializes nested bigints without throwing when BetterStack is enabled (HTTP mocked)', () => { // Mock the HTTP layer so no real request can escape even if a batch were to flush. - const fetchSpy = spyOn(globalThis, 'fetch').mockImplementation((() => - Promise.resolve(new Response(null, { status: 202 }))) as unknown as typeof fetch) - const err = spyOn(console, 'error').mockImplementation(() => undefined) + const fetchSpy = vi + .spyOn(globalThis, 'fetch') + .mockImplementation((() => + Promise.resolve(new Response(null, { status: 202 }))) as unknown as typeof fetch) + const err = vi.spyOn(console, 'error').mockImplementation(() => undefined) const logger = createLogger('debug', { env: { diff --git a/packages/bot-kit/test/policy.test.ts b/packages/bot-kit/test/policy.test.ts index 78776ee9..55e28891 100644 --- a/packages/bot-kit/test/policy.test.ts +++ b/packages/bot-kit/test/policy.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'bun:test' import { getAddress } from 'viem' +import { describe, expect, it } from 'vitest' import type { Policy, PolicyTx } from '../src/policy' diff --git a/packages/bot-kit/test/queue/backoff.test.ts b/packages/bot-kit/test/queue/backoff.test.ts index 6cdb5d00..5df627dc 100644 --- a/packages/bot-kit/test/queue/backoff.test.ts +++ b/packages/bot-kit/test/queue/backoff.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import { createBackoff } from '../../src/queue/backoff' diff --git a/packages/bot-kit/test/queue/cooldown.test.ts b/packages/bot-kit/test/queue/cooldown.test.ts index a001972e..aa1790c2 100644 --- a/packages/bot-kit/test/queue/cooldown.test.ts +++ b/packages/bot-kit/test/queue/cooldown.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import { createCooldownStore } from '../../src/queue/cooldown' diff --git a/packages/bot-kit/test/queue/fee-policy.test.ts b/packages/bot-kit/test/queue/fee-policy.test.ts index 71aab5d5..93eb48e4 100644 --- a/packages/bot-kit/test/queue/fee-policy.test.ts +++ b/packages/bot-kit/test/queue/fee-policy.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import { bumpFees, initialFees } from '../../src/queue/fee-policy' diff --git a/packages/bot-kit/test/queue/pending-queue.test.ts b/packages/bot-kit/test/queue/pending-queue.test.ts index 3c093175..d965a58f 100644 --- a/packages/bot-kit/test/queue/pending-queue.test.ts +++ b/packages/bot-kit/test/queue/pending-queue.test.ts @@ -1,7 +1,7 @@ import type { Address, Hex } from 'viem' -import { describe, expect, it } from 'bun:test' import { ExecutionRevertedError } from 'viem' +import { describe, expect, it } from 'vitest' import type { Logger, LogLevel } from '../../src/logger' import type { diff --git a/packages/bot-kit/test/runner/runner.test.ts b/packages/bot-kit/test/runner/runner.test.ts index a4e83080..5fc154f8 100644 --- a/packages/bot-kit/test/runner/runner.test.ts +++ b/packages/bot-kit/test/runner/runner.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import type { Logger } from '../../src/logger' diff --git a/packages/bot-kit/test/runner/watcher.test.ts b/packages/bot-kit/test/runner/watcher.test.ts index f3c4c820..2efd3187 100644 --- a/packages/bot-kit/test/runner/watcher.test.ts +++ b/packages/bot-kit/test/runner/watcher.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import type { Logger } from '../../src/logger' diff --git a/packages/bot-kit/test/signer.test.ts b/packages/bot-kit/test/signer.test.ts index 3110b9cb..b0da8c17 100644 --- a/packages/bot-kit/test/signer.test.ts +++ b/packages/bot-kit/test/signer.test.ts @@ -1,8 +1,8 @@ import type { Hex } from 'viem' -import { afterEach, describe, expect, it, mock, spyOn } from 'bun:test' import { parseTransaction } from 'viem' import { base } from 'viem/chains' +import { afterEach, describe, expect, it, vi } from 'vitest' import type { Policy } from '../src/policy' @@ -44,11 +44,11 @@ function mockRpc(results: Record) { const result = typeof value === 'function' ? await value(body) : value return Response.json({ jsonrpc: '2.0', id: body.id, result }) } - spyOn(globalThis, 'fetch').mockImplementation(handler as unknown as typeof fetch) + vi.spyOn(globalThis, 'fetch').mockImplementation(handler as unknown as typeof fetch) } describe('createSigner', () => { - afterEach(() => mock.restore()) + afterEach(() => vi.restoreAllMocks()) it('send returns the signer-assigned nonce and the tx hash', async () => { mockRpc({ @@ -141,7 +141,7 @@ describe('createSigner', () => { it('getBaseFee throws when the chain reports no base fee', async () => { mockRpc({ eth_getBlockByNumber: {} }) - expect(createSigner(CONFIG).getBaseFee()).rejects.toThrow(/baseFeePerGas/) + await expect(createSigner(CONFIG).getBaseFee()).rejects.toThrow(/baseFeePerGas/) }) it('consumedNonce reads the latest (mined) transaction count', async () => { diff --git a/packages/bot-kit/test/simulate.test.ts b/packages/bot-kit/test/simulate.test.ts index ee49375a..2c72319b 100644 --- a/packages/bot-kit/test/simulate.test.ts +++ b/packages/bot-kit/test/simulate.test.ts @@ -1,8 +1,8 @@ import type { Hex } from 'viem' -import { describe, expect, it } from 'bun:test' import { createPublicClient, custom, getAddress } from 'viem' import { base } from 'viem/chains' +import { describe, expect, it } from 'vitest' import { simulateLiquidationExec } from '../src/simulate' diff --git a/packages/bot-kit/test/tx-error.test.ts b/packages/bot-kit/test/tx-error.test.ts index 515b1045..7ad643cc 100644 --- a/packages/bot-kit/test/tx-error.test.ts +++ b/packages/bot-kit/test/tx-error.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'bun:test' import { BaseError, encodeErrorResult, ExecutionRevertedError } from 'viem' +import { describe, expect, it } from 'vitest' import { abiRevertDecoder, isExecutionRevert, revertReason, TxSendError } from '../src/tx-error' diff --git a/packages/bot-kit/tsconfig.json b/packages/bot-kit/tsconfig.json index 812897d6..47eee2db 100644 --- a/packages/bot-kit/tsconfig.json +++ b/packages/bot-kit/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@repo/typescript-config/base", "compilerOptions": { - "types": ["bun"] + "types": ["node"] }, "include": ["src", "test"], "exclude": ["node_modules"] diff --git a/packages/bot-kit/vitest.config.ts b/packages/bot-kit/vitest.config.ts new file mode 100644 index 00000000..b0a9b3e6 --- /dev/null +++ b/packages/bot-kit/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + name: 'bot-kit' + } +}) diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 3ac49b31..b6f6e04d 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -11,19 +11,21 @@ } }, "scripts": { - "generate": "bun run scripts/codegen.ts && oxfmt src/abis.ts src/contracts.ts", - "build": "soltag && bun run scripts/build.ts && tsc -p tsconfig.build.json --emitDeclarationOnly", - "deploy:executor": "pnpm run build && bun run scripts/deploy-executor.ts", + "generate": "tsx scripts/codegen.ts && oxfmt src/abis.ts src/contracts.ts", + "build": "soltag && tsx scripts/build.ts && tsc -p tsconfig.build.json --emitDeclarationOnly", + "deploy:executor": "pnpm run build && tsx scripts/deploy-executor.ts", "typecheck": "soltag && tsc --noEmit", "format:sol": "forge fmt", "format:sol:check": "forge fmt --check", - "deploy:crossed-books-resolver": "pnpm run build && bun run scripts/deploy-crossed-books-resolver.ts" + "deploy:crossed-books-resolver": "pnpm run build && tsx scripts/deploy-crossed-books-resolver.ts" }, "devDependencies": { "@repo/typescript-config": "workspace:*", - "@types/bun": "catalog:", + "@types/node": "catalog:", + "esbuild": "catalog:", "solc": "catalog:", "soltag": "catalog:", + "tsx": "catalog:", "typescript": "catalog:", "viem": "catalog:" } diff --git a/packages/contracts/scripts/build.ts b/packages/contracts/scripts/build.ts index 4313da7a..419eb7e1 100644 --- a/packages/contracts/scripts/build.ts +++ b/packages/contracts/scripts/build.ts @@ -1,17 +1,20 @@ +import { build as esbuild } from 'esbuild' import { mkdirSync, rmSync, writeFileSync } from 'node:fs' -import { join } from 'node:path' +import { readFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' import { transformSolTemplates } from 'soltag/unplugin' // Bundles `src/{index,v2/index}.ts` to `dist/` with the soltag `sol``` templates in `src/abis.ts` // and `src/contracts.ts` compiled to literal ABIs (and, for contracts, bytecode + a deployless // factory). The narrowed `.d.ts` is emitted separately by // `tsc -p tsconfig.build.json --emitDeclarationOnly` (see the `build` script), which reads the -// `.soltag/types.d.ts` augmentation cache the `soltag` CLI writes first. We bundle (rather than -// shell out to tsdown) to stay bun-native; the soltag onLoad plugin is the same transform the -// repo's preloads use, scoped here to this package's `src/` so bundled deps are untouched. +// `.soltag/types.d.ts` augmentation cache the `soltag` CLI writes first. We bundle with esbuild +// (rather than shell out to tsdown) to keep the toolchain to one bundler; the soltag onLoad plugin +// is scoped to this package's `src/` so bundled deps are untouched. // This script lives in `scripts/`, so paths resolve against the package root one level up. -const ROOT = join(import.meta.dir, '..') +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..') const DIST_DIR = join(ROOT, 'dist') // Clean first so renamed/removed sources don't leave stale `dist/*.{js,d.ts}` behind (the `tsc` @@ -22,18 +25,19 @@ const SRC_DIR = join(ROOT, 'src') const ESCAPED = SRC_DIR.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') const INCLUDE = new RegExp(`^${ESCAPED}/.*\\.tsx?$`) -const result = await Bun.build({ - entrypoints: [join(SRC_DIR, 'index.ts')], +await esbuild({ + entryPoints: [join(SRC_DIR, 'index.ts')], outdir: DIST_DIR, - root: SRC_DIR, - target: 'node', + outbase: SRC_DIR, + bundle: true, + platform: 'node', format: 'esm', plugins: [ { name: 'soltag-contracts', setup(build) { build.onLoad({ filter: INCLUDE }, async ({ path }) => { - const source = await Bun.file(path).text() + const source = await readFile(path, 'utf8') const transformed = transformSolTemplates(source, path, { solc: { optimizer: { enabled: true, runs: 200 } } }) @@ -44,11 +48,6 @@ const result = await Bun.build({ ] }) -if (!result.success) { - for (const log of result.logs) console.error(log) - process.exit(1) -} - // Emit each interface ABI as JSON under `abis/`, for tools that need ABIs on disk rather than the // TS `as const` exports (e.g. rindexer, which the midnight-liquidation image feeds from here). The // ABIs are materialized in the freshly-bundled `dist/index.js` above, so we read them back from it. diff --git a/packages/contracts/scripts/codegen.ts b/packages/contracts/scripts/codegen.ts index 25461c73..998a7431 100644 --- a/packages/contracts/scripts/codegen.ts +++ b/packages/contracts/scripts/codegen.ts @@ -11,10 +11,11 @@ * The output is checked in — we don't run codegen on every build. */ import { readdirSync, writeFileSync } from 'node:fs' -import { resolve } from 'node:path' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' // This script lives in `scripts/`, so paths resolve against the package root one level up. -const ROOT = resolve(import.meta.dir, '..') +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const SOLIDITY_DIR = resolve(ROOT, 'solidity') const INTERFACES_DIR = resolve(SOLIDITY_DIR, 'interfaces') diff --git a/packages/contracts/tsconfig.json b/packages/contracts/tsconfig.json index c9bdf12a..23684b26 100644 --- a/packages/contracts/tsconfig.json +++ b/packages/contracts/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@repo/typescript-config/base", "compilerOptions": { - "types": ["bun"], + "types": ["node"], "plugins": [{ "name": "soltag/plugin" }] }, "include": ["src", "scripts", ".soltag/types.d.ts"], diff --git a/packages/logging/package.json b/packages/logging/package.json index c300f01b..819039c7 100644 --- a/packages/logging/package.json +++ b/packages/logging/package.json @@ -16,7 +16,8 @@ }, "devDependencies": { "@repo/typescript-config": "workspace:*", - "@types/bun": "catalog:", - "typescript": "catalog:" + "@types/node": "catalog:", + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/logging/test/cli-logger.utils.test.ts b/packages/logging/test/cli-logger.utils.test.ts index 51629ccd..ad3d39f6 100644 --- a/packages/logging/test/cli-logger.utils.test.ts +++ b/packages/logging/test/cli-logger.utils.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import { createCliLogger } from '../src/cli-logger.utils' @@ -34,7 +34,7 @@ describe('createCliLogger', () => { logger.result({ status: 'published', assets: 7n }) expect(out).toHaveLength(1) - expect(out[0]).not.toInclude('\n') + expect(out[0]).not.toContain('\n') expect(JSON.parse(out[0]!)).toEqual({ status: 'published', assets: '7' }) }) diff --git a/packages/logging/tsconfig.json b/packages/logging/tsconfig.json index 812897d6..47eee2db 100644 --- a/packages/logging/tsconfig.json +++ b/packages/logging/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@repo/typescript-config/base", "compilerOptions": { - "types": ["bun"] + "types": ["node"] }, "include": ["src", "test"], "exclude": ["node_modules"] diff --git a/packages/logging/vitest.config.ts b/packages/logging/vitest.config.ts new file mode 100644 index 00000000..2230bbbc --- /dev/null +++ b/packages/logging/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + name: 'logging' + } +}) diff --git a/packages/monitoring/package.json b/packages/monitoring/package.json index ef024e5f..77b1517a 100644 --- a/packages/monitoring/package.json +++ b/packages/monitoring/package.json @@ -16,7 +16,8 @@ }, "devDependencies": { "@repo/typescript-config": "workspace:*", - "@types/bun": "catalog:", - "typescript": "catalog:" + "@types/node": "catalog:", + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/monitoring/test/monitor.utils.test.ts b/packages/monitoring/test/monitor.utils.test.ts index 6ea5ed92..50b6fd56 100644 --- a/packages/monitoring/test/monitor.utils.test.ts +++ b/packages/monitoring/test/monitor.utils.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import { createOperationQueue, cycleHasFailure, waitForMonitorInterval } from '../src/monitor.utils' diff --git a/packages/monitoring/tsconfig.json b/packages/monitoring/tsconfig.json index 812897d6..47eee2db 100644 --- a/packages/monitoring/tsconfig.json +++ b/packages/monitoring/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@repo/typescript-config/base", "compilerOptions": { - "types": ["bun"] + "types": ["node"] }, "include": ["src", "test"], "exclude": ["node_modules"] diff --git a/packages/monitoring/vitest.config.ts b/packages/monitoring/vitest.config.ts new file mode 100644 index 00000000..27cd4697 --- /dev/null +++ b/packages/monitoring/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + name: 'monitoring' + } +}) diff --git a/packages/observability/package.json b/packages/observability/package.json index d9f996fd..57db64a7 100644 --- a/packages/observability/package.json +++ b/packages/observability/package.json @@ -19,7 +19,8 @@ }, "devDependencies": { "@repo/typescript-config": "workspace:*", - "@types/bun": "catalog:", - "typescript": "catalog:" + "@types/node": "catalog:", + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/observability/test/bot-observability.utils.test.ts b/packages/observability/test/bot-observability.utils.test.ts index 802683ac..94ef26ed 100644 --- a/packages/observability/test/bot-observability.utils.test.ts +++ b/packages/observability/test/bot-observability.utils.test.ts @@ -1,6 +1,7 @@ import type { Logger, LogLevel } from '@repo/bot-kit' -import { describe, expect, mock, spyOn, test } from 'bun:test' +import { setTimeout as sleep } from 'node:timers/promises' +import { describe, expect, test, vi } from 'vitest' import { createBotObservability, installProcessObservers } from '../src/bot-observability.utils' @@ -24,7 +25,7 @@ const identity = { bot: 'test-bot', chainId: 8453, errorName } describe('createBotObservability', () => { test('ships lifecycle, heartbeat, and record events without consuming CLI output', async () => { const { logger, records } = captureLogger() - const heartbeat = { start: mock(async () => undefined), stop: mock(() => undefined) } + const heartbeat = { start: vi.fn(async () => undefined), stop: vi.fn(() => undefined) } const observability = createBotObservability({ ...identity, logger, heartbeat }) const cycle = { event: 'market-making.cycle', @@ -89,7 +90,7 @@ describe('createBotObservability', () => { test('records a fresh lifecycle start after a clean stop so process restarts remain queryable', async () => { const { logger, records } = captureLogger() - const heartbeat = { start: mock(async () => undefined), stop: mock(() => undefined) } + const heartbeat = { start: vi.fn(async () => undefined), stop: vi.fn(() => undefined) } const observability = createBotObservability({ ...identity, logger, heartbeat }) await observability.start() @@ -150,9 +151,9 @@ describe('createBotObservability', () => { test('sanitizes heartbeat transport failures before logging', async () => { const { logger, records } = captureLogger() - const fetchSpy = spyOn(globalThis, 'fetch').mockRejectedValue( - new Error('heartbeat secret URL and raw network response') - ) + const fetchSpy = vi + .spyOn(globalThis, 'fetch') + .mockRejectedValue(new Error('heartbeat secret URL and raw network response')) try { const observability = createBotObservability({ ...identity, @@ -161,7 +162,7 @@ describe('createBotObservability', () => { }) await observability.start() - await Bun.sleep(0) + await sleep(0) observability.stop('completed') expect(records).toContainEqual({ @@ -179,20 +180,20 @@ describe('createBotObservability', () => { describe('installProcessObservers', () => { test('observes fatal exceptions and rethrows unhandled rejections after recording', () => { - const unexpected = mock( + const unexpected = vi.fn( (_error: unknown, _origin: 'uncaughtException' | 'unhandledRejection') => undefined ) let exceptionListener: ((error: Error, origin: string) => void) | undefined let rejectionListener: ((reason: unknown) => void) | undefined const target = { - on: mock((event: string, value: (...args: never[]) => void) => { + on: vi.fn((event: string, value: (...args: never[]) => void) => { if (event === 'uncaughtExceptionMonitor') { exceptionListener = value as typeof exceptionListener } else { rejectionListener = value as typeof rejectionListener } }), - removeListener: mock((_event: string, _value: (...args: never[]) => void) => undefined) + removeListener: vi.fn((_event: string, _value: (...args: never[]) => void) => undefined) } const cleanup = installProcessObservers({ unexpected }, target) diff --git a/packages/observability/test/verbose-argv.utils.test.ts b/packages/observability/test/verbose-argv.utils.test.ts index e0c8fbf6..ada0b4c9 100644 --- a/packages/observability/test/verbose-argv.utils.test.ts +++ b/packages/observability/test/verbose-argv.utils.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import { enhanceVerboseArgv } from '../src/verbose-argv.utils' diff --git a/packages/observability/tsconfig.json b/packages/observability/tsconfig.json index 812897d6..47eee2db 100644 --- a/packages/observability/tsconfig.json +++ b/packages/observability/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@repo/typescript-config/base", "compilerOptions": { - "types": ["bun"] + "types": ["node"] }, "include": ["src", "test"], "exclude": ["node_modules"] diff --git a/packages/observability/vitest.config.ts b/packages/observability/vitest.config.ts new file mode 100644 index 00000000..81e0d46c --- /dev/null +++ b/packages/observability/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + name: 'observability' + } +}) diff --git a/packages/offers/package.json b/packages/offers/package.json index 76c02379..c8764e48 100644 --- a/packages/offers/package.json +++ b/packages/offers/package.json @@ -19,7 +19,8 @@ }, "devDependencies": { "@repo/typescript-config": "workspace:*", - "@types/bun": "catalog:", - "typescript": "catalog:" + "@types/node": "catalog:", + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/offers/test/book.utils.test.ts b/packages/offers/test/book.utils.test.ts index ab8605c9..5a21c4b4 100644 --- a/packages/offers/test/book.utils.test.ts +++ b/packages/offers/test/book.utils.test.ts @@ -1,6 +1,6 @@ import type { Hex } from 'viem' -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import { batchProspectiveBook, crossedMarketIds, hasNegativeSpread } from '../src/book.utils' diff --git a/packages/offers/tsconfig.json b/packages/offers/tsconfig.json index 812897d6..47eee2db 100644 --- a/packages/offers/tsconfig.json +++ b/packages/offers/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@repo/typescript-config/base", "compilerOptions": { - "types": ["bun"] + "types": ["node"] }, "include": ["src", "test"], "exclude": ["node_modules"] diff --git a/packages/offers/vitest.config.ts b/packages/offers/vitest.config.ts new file mode 100644 index 00000000..2cc2833f --- /dev/null +++ b/packages/offers/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + name: 'offers' + } +}) diff --git a/packages/swaps/package.json b/packages/swaps/package.json index fc4a952f..b6983df3 100644 --- a/packages/swaps/package.json +++ b/packages/swaps/package.json @@ -22,7 +22,8 @@ }, "devDependencies": { "@repo/typescript-config": "workspace:*", - "@types/bun": "catalog:", - "typescript": "catalog:" + "@types/node": "catalog:", + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/swaps/test/config.test.ts b/packages/swaps/test/config.test.ts index 580b9d5b..3589b589 100644 --- a/packages/swaps/test/config.test.ts +++ b/packages/swaps/test/config.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import { parseSwapConfig } from '../src/config' diff --git a/packages/swaps/test/constants.test.ts b/packages/swaps/test/constants.test.ts index 28486ee5..2ce4cf81 100644 --- a/packages/swaps/test/constants.test.ts +++ b/packages/swaps/test/constants.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'bun:test' import { getAddress } from 'viem' +import { describe, expect, it } from 'vitest' import { ONEINCH_ROUTER, PENDLE_CHAIN_IDS } from '../src/constants' diff --git a/packages/swaps/test/execution/executor-calls.test.ts b/packages/swaps/test/execution/executor-calls.test.ts index 0b60ba39..da919896 100644 --- a/packages/swaps/test/execution/executor-calls.test.ts +++ b/packages/swaps/test/execution/executor-calls.test.ts @@ -1,7 +1,7 @@ import type { Hex } from 'viem' -import { describe, expect, it } from 'bun:test' import { decodeFunctionData, getAddress } from 'viem' +import { describe, expect, it } from 'vitest' import type { SwapStep } from '../../src/types' diff --git a/packages/swaps/test/http-client.test.ts b/packages/swaps/test/http-client.test.ts index fdb10ac6..cf8c023a 100644 --- a/packages/swaps/test/http-client.test.ts +++ b/packages/swaps/test/http-client.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import { createRateLimitedClient } from '../src/http-client' import { QuoteError } from '../src/types' diff --git a/packages/swaps/test/quoting.test.ts b/packages/swaps/test/quoting.test.ts index 2a4026c0..529cc2bd 100644 --- a/packages/swaps/test/quoting.test.ts +++ b/packages/swaps/test/quoting.test.ts @@ -1,7 +1,7 @@ import type { Address } from 'viem' -import { describe, expect, it } from 'bun:test' import { getAddress, isAddressEqual } from 'viem' +import { describe, expect, it } from 'vitest' import type { HttpVenue, RateLimitedClient } from '../src/http-client' import type { QuoteLogger, QuoteRequest } from '../src/quoting' diff --git a/packages/swaps/test/unwrappers/erc4626.test.ts b/packages/swaps/test/unwrappers/erc4626.test.ts index ee664a64..4ed9ee21 100644 --- a/packages/swaps/test/unwrappers/erc4626.test.ts +++ b/packages/swaps/test/unwrappers/erc4626.test.ts @@ -1,7 +1,7 @@ import type { Hex } from 'viem' -import { describe, expect, it } from 'bun:test' import { createClient, custom, decodeFunctionData, encodeAbiParameters, getAddress } from 'viem' +import { describe, expect, it } from 'vitest' import type { QuoteLogger } from '../../src/quoting' @@ -115,7 +115,9 @@ describe('createErc4626Unwrapper', () => { }) const unwrapper = createErc4626Unwrapper({ client, logger: NOOP_LOGGER }) - expect(unwrapper.resolve({ token: VAULT, amountIn: 1n, executor: EXECUTOR })).rejects.toThrow() + await expect( + unwrapper.resolve({ token: VAULT, amountIn: 1n, executor: EXECUTOR }) + ).rejects.toThrow() // The outage was not cached as "not a vault": once the RPC recovers, the same token resolves. healthy = true diff --git a/packages/swaps/test/unwrappers/pendle-pt.test.ts b/packages/swaps/test/unwrappers/pendle-pt.test.ts index ab04112e..82998c56 100644 --- a/packages/swaps/test/unwrappers/pendle-pt.test.ts +++ b/packages/swaps/test/unwrappers/pendle-pt.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'bun:test' import { getAddress } from 'viem' +import { describe, expect, it } from 'vitest' import type { HttpVenue, RateLimitedClient } from '../../src/http-client' import type { QuoteLogger } from '../../src/quoting' @@ -203,12 +203,14 @@ describe('createPendlePtUnwrapper', () => { } }) - expect(unwrapper.resolve({ token: PT, amountIn: 1n, executor: EXECUTOR })).rejects.toThrow( - 'pendle down' - ) + await expect( + unwrapper.resolve({ token: PT, amountIn: 1n, executor: EXECUTOR }) + ).rejects.toThrow('pendle down') // The failure was not cached: the next resolve fetches again. - expect(unwrapper.resolve({ token: PT, amountIn: 1n, executor: EXECUTOR })).rejects.toThrow() + await expect( + unwrapper.resolve({ token: PT, amountIn: 1n, executor: EXECUTOR }) + ).rejects.toThrow() expect(calls).toHaveLength(2) }) @@ -224,7 +226,7 @@ describe('createPendlePtUnwrapper', () => { markets: () => marketsBody(FUTURE), convert: () => ({ tx: { to: ROUTER, data: 'nothex' }, data: { amountOut: '1' } }) }) - expect( + await expect( badData.unwrapper.resolve({ token: PT, amountIn: 1n, executor: EXECUTOR }) ).rejects.toThrow(/malformed swap tx/) @@ -232,7 +234,7 @@ describe('createPendlePtUnwrapper', () => { markets: () => marketsBody(FUTURE), convert: () => ({ tx: { to: ROUTER, data: '0xabc1' }, data: { amountOut: 'NaN' } }) }) - expect( + await expect( badAmount.unwrapper.resolve({ token: PT, amountIn: 1n, executor: EXECUTOR }) ).rejects.toThrow(/malformed amountOut/) }) diff --git a/packages/swaps/test/unwrappers/resolve.test.ts b/packages/swaps/test/unwrappers/resolve.test.ts index 114f3cee..549a952a 100644 --- a/packages/swaps/test/unwrappers/resolve.test.ts +++ b/packages/swaps/test/unwrappers/resolve.test.ts @@ -1,7 +1,7 @@ import type { Address } from 'viem' -import { describe, expect, it } from 'bun:test' import { getAddress, isAddressEqual } from 'viem' +import { describe, expect, it } from 'vitest' import type { Unwrapper } from '../../src/unwrappers/resolve' @@ -110,7 +110,7 @@ describe('resolveUnwraps', () => { throw new Error('probe exploded') } } - expect( + await expect( resolveUnwraps([broken], { token: A, amountIn: 1n, executor: EXECUTOR, stopToken: LOAN }) ).rejects.toThrow('probe exploded') }) diff --git a/packages/swaps/test/venue-selector.test.ts b/packages/swaps/test/venue-selector.test.ts index c0656604..27bd12c2 100644 --- a/packages/swaps/test/venue-selector.test.ts +++ b/packages/swaps/test/venue-selector.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'bun:test' import { getAddress, parseUnits } from 'viem' +import { describe, expect, it } from 'vitest' import type { QuoteLogger } from '../src/quoting' import type { PriceParameters, Venue } from '../src/types' diff --git a/packages/swaps/test/venues/lifi.test.ts b/packages/swaps/test/venues/lifi.test.ts index e5016829..89f772cc 100644 --- a/packages/swaps/test/venues/lifi.test.ts +++ b/packages/swaps/test/venues/lifi.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'bun:test' import { getAddress } from 'viem' +import { describe, expect, it } from 'vitest' import type { RateLimitedClient } from '../../src/http-client' import type { PriceParameters, QuoteParameters } from '../../src/types' diff --git a/packages/swaps/test/venues/liquidswap.test.ts b/packages/swaps/test/venues/liquidswap.test.ts index f50bcfd0..512627e0 100644 --- a/packages/swaps/test/venues/liquidswap.test.ts +++ b/packages/swaps/test/venues/liquidswap.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'bun:test' import { getAddress } from 'viem' +import { describe, expect, it } from 'vitest' import type { RateLimitedClient } from '../../src/http-client' import type { PriceParameters, QuoteParameters } from '../../src/types' diff --git a/packages/swaps/test/venues/oneinch.test.ts b/packages/swaps/test/venues/oneinch.test.ts index f0659ac1..a09bad24 100644 --- a/packages/swaps/test/venues/oneinch.test.ts +++ b/packages/swaps/test/venues/oneinch.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'bun:test' import { getAddress } from 'viem' +import { describe, expect, it } from 'vitest' import type { RateLimitedClient } from '../../src/http-client' import type { QuoteParameters } from '../../src/types' diff --git a/packages/swaps/test/venues/uniswap-v3.test.ts b/packages/swaps/test/venues/uniswap-v3.test.ts index 12fbe832..358cbe66 100644 --- a/packages/swaps/test/venues/uniswap-v3.test.ts +++ b/packages/swaps/test/venues/uniswap-v3.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'bun:test' import { decodeFunctionData, getAddress } from 'viem' +import { describe, expect, it } from 'vitest' import type { QuoteParameters } from '../../src/types' diff --git a/packages/swaps/test/venues/zerox.test.ts b/packages/swaps/test/venues/zerox.test.ts index ed1ad6ac..366bcfff 100644 --- a/packages/swaps/test/venues/zerox.test.ts +++ b/packages/swaps/test/venues/zerox.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'bun:test' import { getAddress } from 'viem' +import { describe, expect, it } from 'vitest' import type { RateLimitedClient } from '../../src/http-client' import type { QuoteParameters } from '../../src/types' diff --git a/packages/swaps/tsconfig.json b/packages/swaps/tsconfig.json index 812897d6..47eee2db 100644 --- a/packages/swaps/tsconfig.json +++ b/packages/swaps/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@repo/typescript-config/base", "compilerOptions": { - "types": ["bun"] + "types": ["node"] }, "include": ["src", "test"], "exclude": ["node_modules"] diff --git a/packages/swaps/vitest.config.ts b/packages/swaps/vitest.config.ts new file mode 100644 index 00000000..c208cc92 --- /dev/null +++ b/packages/swaps/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + name: 'swaps' + } +}) diff --git a/packages/utils/package.json b/packages/utils/package.json index 17fef8c7..8bb2b0aa 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -29,8 +29,9 @@ }, "devDependencies": { "@repo/typescript-config": "workspace:*", - "@types/bun": "catalog:", "@types/lodash-es": "catalog:", - "typescript": "catalog:" + "@types/node": "catalog:", + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/utils/test/helpers/addresses.test.ts b/packages/utils/test/helpers/addresses.test.ts index 69764a53..fc78f24f 100644 --- a/packages/utils/test/helpers/addresses.test.ts +++ b/packages/utils/test/helpers/addresses.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import { abbreviateAddress } from '../../src/helpers/addresses' diff --git a/packages/utils/test/helpers/bigint.test.ts b/packages/utils/test/helpers/bigint.test.ts index f1d5f85f..0d8c43ff 100644 --- a/packages/utils/test/helpers/bigint.test.ts +++ b/packages/utils/test/helpers/bigint.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import { bigintAbs, bigintMin, mulDivDown, mulDivUp, zeroFloorSub } from '../../src/helpers/bigint' diff --git a/packages/utils/test/helpers/deepFreeze.test.ts b/packages/utils/test/helpers/deepFreeze.test.ts index b27cdc5b..87f403b9 100644 --- a/packages/utils/test/helpers/deepFreeze.test.ts +++ b/packages/utils/test/helpers/deepFreeze.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import { deepFreeze } from '../../src/helpers/deepFreeze' diff --git a/packages/utils/test/helpers/delay.test.ts b/packages/utils/test/helpers/delay.test.ts index aaf4acec..8e84f66b 100644 --- a/packages/utils/test/helpers/delay.test.ts +++ b/packages/utils/test/helpers/delay.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import { delay } from '../../src/helpers/delay' diff --git a/packages/utils/test/helpers/deployless-batch-lens.test.ts b/packages/utils/test/helpers/deployless-batch-lens.test.ts index fd9e7455..99dfe333 100644 --- a/packages/utils/test/helpers/deployless-batch-lens.test.ts +++ b/packages/utils/test/helpers/deployless-batch-lens.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'bun:test' import { getAddress } from 'viem' +import { describe, expect, it } from 'vitest' import { lensKey } from '../../src/helpers/deployless-batch-lens' diff --git a/packages/utils/test/helpers/errors.test.ts b/packages/utils/test/helpers/errors.test.ts index d01705ef..da2ae221 100644 --- a/packages/utils/test/helpers/errors.test.ts +++ b/packages/utils/test/helpers/errors.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import { assertNever, ensureError } from '../../src/helpers/errors' diff --git a/packages/utils/test/helpers/fetch.test.ts b/packages/utils/test/helpers/fetch.test.ts index ed130808..394a4041 100644 --- a/packages/utils/test/helpers/fetch.test.ts +++ b/packages/utils/test/helpers/fetch.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { fetchJsonResponse, fetchWithRetry, parseJsonResponse } from '../../src/helpers/fetch' @@ -168,10 +168,10 @@ describe('fetchWithRetry', () => { describe('fetchJsonResponse', () => { const originalFetch = globalThis.fetch - let fetchMock: ReturnType + let fetchMock: ReturnType beforeEach(() => { - fetchMock = mock(() => Promise.reject(new Error('fetch mock not configured'))) + fetchMock = vi.fn(() => Promise.reject(new Error('fetch mock not configured'))) globalThis.fetch = fetchMock as unknown as typeof fetch }) diff --git a/packages/utils/test/helpers/formatters.test.ts b/packages/utils/test/helpers/formatters.test.ts index 76ee5f06..f8634dcc 100644 --- a/packages/utils/test/helpers/formatters.test.ts +++ b/packages/utils/test/helpers/formatters.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import { formatBPSPercent, diff --git a/packages/utils/test/helpers/json.test.ts b/packages/utils/test/helpers/json.test.ts index fad4186e..11687c66 100644 --- a/packages/utils/test/helpers/json.test.ts +++ b/packages/utils/test/helpers/json.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'bun:test' import { Address } from 'viem' +import { describe, expect, it } from 'vitest' import { bigintReplacer, bigIntReviver, parse, stringify } from '../../src/helpers/json' diff --git a/packages/utils/test/helpers/map.test.ts b/packages/utils/test/helpers/map.test.ts index d903eb4c..97199ecb 100644 --- a/packages/utils/test/helpers/map.test.ts +++ b/packages/utils/test/helpers/map.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import { filterNull, mapObjValues } from '../../src/helpers/map' diff --git a/packages/utils/test/helpers/promise.test.ts b/packages/utils/test/helpers/promise.test.ts index b13d0ec3..2a5afdac 100644 --- a/packages/utils/test/helpers/promise.test.ts +++ b/packages/utils/test/helpers/promise.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, mock } from 'bun:test' +import { describe, expect, it, vi } from 'vitest' import { allFulfilled } from '../../src/helpers/promise' @@ -18,7 +18,7 @@ describe('allFulfilled', () => { }) it('calls onRejected for each rejected promise with reason and index', async () => { - const onRejected = mock() + const onRejected = vi.fn() const error = new Error('boom') await allFulfilled([Promise.resolve(1), Promise.reject(error), Promise.resolve(3)], onRejected) @@ -28,7 +28,7 @@ describe('allFulfilled', () => { }) it('calls onRejected for multiple rejected promises with correct indices', async () => { - const onRejected = mock() + const onRejected = vi.fn() const error1 = new Error('first') const error2 = new Error('second') @@ -44,7 +44,7 @@ describe('allFulfilled', () => { }) it('calls onRejected with non-Error rejection reasons', async () => { - const onRejected = mock() + const onRejected = vi.fn() await allFulfilled([Promise.reject('string reason'), Promise.reject(42)], onRejected) @@ -54,7 +54,7 @@ describe('allFulfilled', () => { }) it('does not call onRejected when all promises fulfill', async () => { - const onRejected = mock() + const onRejected = vi.fn() await allFulfilled([Promise.resolve(1), Promise.resolve(2)], onRejected) diff --git a/packages/utils/test/helpers/retry.test.ts b/packages/utils/test/helpers/retry.test.ts index 0897d7b4..146d9fb3 100644 --- a/packages/utils/test/helpers/retry.test.ts +++ b/packages/utils/test/helpers/retry.test.ts @@ -1,10 +1,10 @@ -import { describe, expect, it, mock } from 'bun:test' +import { describe, expect, it, vi } from 'vitest' import { retryUntilDefined } from '../../src/helpers/retry' describe('retryUntilDefined', () => { it('should return value immediately if defined on first try', async () => { - const fn = mock(() => 'success') + const fn = vi.fn(() => 'success') const result = await retryUntilDefined(fn) @@ -13,7 +13,8 @@ describe('retryUntilDefined', () => { }) it('should retry until value is defined', async () => { - const fn = mock<() => string | undefined>(() => undefined) + const fn = vi + .fn<() => string | undefined>(() => undefined) .mockReturnValueOnce(undefined) .mockReturnValueOnce(undefined) .mockReturnValue('success') @@ -25,7 +26,7 @@ describe('retryUntilDefined', () => { }) it('should throw error after max retries', async () => { - const fn = mock<() => string | undefined>(() => undefined) + const fn = vi.fn<() => string | undefined>(() => undefined) await expect(retryUntilDefined(fn, { maxRetries: 3, retryDelay: 1 })).rejects.toThrow( 'Value not defined after 3 retries' @@ -34,11 +35,12 @@ describe('retryUntilDefined', () => { }) it('should call onRetry callback with attempt number', async () => { - const fn = mock<() => string | undefined>(() => undefined) + const fn = vi + .fn<() => string | undefined>(() => undefined) .mockReturnValueOnce(undefined) .mockReturnValueOnce(undefined) .mockReturnValue('success') - const onRetry = mock(() => {}) + const onRetry = vi.fn(() => {}) await retryUntilDefined(fn, { retryDelay: 1, onRetry }) @@ -48,10 +50,10 @@ describe('retryUntilDefined', () => { }) it('should handle different return types', async () => { - const fnNumber = mock(() => 42) - const fnBoolean = mock(() => false) - const fnObject = mock(() => ({ foo: 'bar' })) - const fnArray = mock(() => [1, 2, 3]) + const fnNumber = vi.fn(() => 42) + const fnBoolean = vi.fn(() => false) + const fnObject = vi.fn(() => ({ foo: 'bar' })) + const fnArray = vi.fn(() => [1, 2, 3]) const [num, bool, obj, arr] = await Promise.all([ retryUntilDefined(fnNumber), @@ -67,7 +69,7 @@ describe('retryUntilDefined', () => { }) it('should treat null as defined value', async () => { - const fn = mock(() => null) + const fn = vi.fn(() => null) const result = await retryUntilDefined(fn) @@ -76,8 +78,8 @@ describe('retryUntilDefined', () => { }) it('should not call onRetry on last failed attempt', async () => { - const fn = mock<() => string | undefined>(() => undefined) - const onRetry = mock(() => {}) + const fn = vi.fn<() => string | undefined>(() => undefined) + const onRetry = vi.fn(() => {}) await expect(retryUntilDefined(fn, { maxRetries: 3, retryDelay: 1, onRetry })).rejects.toThrow() expect(onRetry).toHaveBeenCalledTimes(2) @@ -85,7 +87,7 @@ describe('retryUntilDefined', () => { }) it('should handle functions that throw errors', async () => { - const fn = mock<() => string | undefined>(() => { + const fn = vi.fn<() => string | undefined>(() => { throw new Error('First error') }) @@ -94,7 +96,7 @@ describe('retryUntilDefined', () => { }) it('should handle zero as a defined value', async () => { - const fn = mock(() => 0) + const fn = vi.fn(() => 0) const result = await retryUntilDefined(fn) @@ -103,7 +105,7 @@ describe('retryUntilDefined', () => { }) it('should handle empty string as a defined value', async () => { - const fn = mock(() => '') + const fn = vi.fn(() => '') const result = await retryUntilDefined(fn) diff --git a/packages/utils/test/helpers/schema.test.ts b/packages/utils/test/helpers/schema.test.ts index 522f3e9d..0652edd9 100644 --- a/packages/utils/test/helpers/schema.test.ts +++ b/packages/utils/test/helpers/schema.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'bun:test' import { maxUint256 } from 'viem' +import { describe, expect, it } from 'vitest' import { addressSchema, createOnchainAmountSchema } from '../../src/helpers/schema' diff --git a/packages/utils/test/helpers/strings.test.ts b/packages/utils/test/helpers/strings.test.ts index 09f26f14..718131e9 100644 --- a/packages/utils/test/helpers/strings.test.ts +++ b/packages/utils/test/helpers/strings.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import { arrayToSentence, diff --git a/packages/utils/test/helpers/time.test.ts b/packages/utils/test/helpers/time.test.ts index b7f63706..e77bb1c6 100644 --- a/packages/utils/test/helpers/time.test.ts +++ b/packages/utils/test/helpers/time.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import { dateLabelForUnixTimestamp, diff --git a/packages/utils/test/helpers/tokenBucket.test.ts b/packages/utils/test/helpers/tokenBucket.test.ts index e4e4bd0a..271cce9b 100644 --- a/packages/utils/test/helpers/tokenBucket.test.ts +++ b/packages/utils/test/helpers/tokenBucket.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import { createTokenBucket } from '../../src/helpers/tokenBucket' diff --git a/packages/utils/test/helpers/tryCatch.test.ts b/packages/utils/test/helpers/tryCatch.test.ts index 81bdbdce..19ed825c 100644 --- a/packages/utils/test/helpers/tryCatch.test.ts +++ b/packages/utils/test/helpers/tryCatch.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import { tryCatch } from '../../src/helpers/tryCatch' diff --git a/packages/utils/test/helpers/tryOrUndefined.test.ts b/packages/utils/test/helpers/tryOrUndefined.test.ts index 4f992fb6..0f57ce31 100644 --- a/packages/utils/test/helpers/tryOrUndefined.test.ts +++ b/packages/utils/test/helpers/tryOrUndefined.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import { tryOrUndefined } from '../../src/helpers/tryOrUndefined' diff --git a/packages/utils/test/helpers/wad.test.ts b/packages/utils/test/helpers/wad.test.ts index 9a4cae06..38a2c722 100644 --- a/packages/utils/test/helpers/wad.test.ts +++ b/packages/utils/test/helpers/wad.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it } from 'vitest' import { wadToWholePercent, wholePercentToWAD } from '../../src/helpers/wad' diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json index 812897d6..47eee2db 100644 --- a/packages/utils/tsconfig.json +++ b/packages/utils/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@repo/typescript-config/base", "compilerOptions": { - "types": ["bun"] + "types": ["node"] }, "include": ["src", "test"], "exclude": ["node_modules"] diff --git a/packages/utils/vitest.config.ts b/packages/utils/vitest.config.ts new file mode 100644 index 00000000..bb311490 --- /dev/null +++ b/packages/utils/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + name: 'utils' + } +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec1fcc80..5a24be7d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,27 +21,21 @@ catalogs: '@morpho-org/viem-dlc': specifier: 0.0.11 version: 0.0.11 - '@tanstack/react-form': - specifier: 1.20.0 - version: 1.20.0 - '@tanstack/react-table': - specifier: 8.21.3 - version: 8.21.3 - '@types/bun': - specifier: 1.3.13 - version: 1.3.13 '@types/lodash-es': specifier: ^4.17.12 version: 4.17.12 - '@types/react': - specifier: 19.2.18 - version: 19.2.18 - '@types/react-dom': - specifier: 19.2.4 - version: 19.2.4 + '@types/node': + specifier: 24.7.0 + version: 24.7.0 date-fns: specifier: 4.1.0 version: 4.1.0 + esbuild: + specifier: 0.28.1 + version: 0.28.1 + execa: + specifier: 9.6.0 + version: 9.6.0 executooor-viem: specifier: ^1.3.3 version: 1.3.3 @@ -75,24 +69,27 @@ catalogs: oxlint-tsgolint: specifier: ^0.22.1 version: 0.22.1 - react: - specifier: 19.2.8 - version: 19.2.8 - react-dom: - specifier: 19.2.8 - version: 19.2.8 solc: specifier: 0.8.35 version: 0.8.35 soltag: specifier: ^0.0.17 version: 0.0.17 + tsx: + specifier: 4.21.0 + version: 4.21.0 typescript: specifier: 6.0.2 version: 6.0.2 viem: specifier: 2.47.17 version: 2.47.17 + vite: + specifier: 7.3.6 + version: 7.3.6 + vitest: + specifier: 4.1.2 + version: 4.1.2 zod: specifier: 3.25.76 version: 3.25.76 @@ -104,15 +101,15 @@ importers: '@repo/typescript-config': specifier: workspace:* version: link:packages/typescript-config - esbuild: - specifier: 0.25.12 - version: 0.25.12 + '@types/node': + specifier: 'catalog:' + version: 24.7.0 husky: specifier: 'catalog:' version: 9.1.7 knip: specifier: 'catalog:' - version: 5.88.1(@types/node@25.8.0)(typescript@6.0.2) + version: 5.88.1(@types/node@24.7.0)(typescript@6.0.2) lint-staged: specifier: 'catalog:' version: 16.4.0 @@ -125,9 +122,18 @@ importers: oxlint-tsgolint: specifier: 'catalog:' version: 0.22.1 + tsx: + specifier: 'catalog:' + version: 4.21.0 typescript: specifier: 'catalog:' version: 6.0.2 + vite: + specifier: 'catalog:' + version: 7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0) + vitest: + specifier: 'catalog:' + version: 4.1.2(@types/node@24.7.0)(vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) bots/blue-liquidation: dependencies: @@ -151,7 +157,7 @@ importers: version: 0.8.35 soltag: specifier: 'catalog:' - version: 0.0.17(esbuild@0.25.12)(solc@0.8.35)(typescript@6.0.2)(viem@2.47.17(typescript@6.0.2)(zod@4.4.3)) + version: 0.0.17(esbuild@0.28.1)(rollup@4.62.4)(solc@0.8.35)(typescript@6.0.2)(viem@2.47.17(typescript@6.0.2)(zod@4.4.3))(vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) viem: specifier: 'catalog:' version: 2.47.17(typescript@6.0.2)(zod@4.4.3) @@ -159,21 +165,30 @@ importers: '@repo/typescript-config': specifier: workspace:* version: link:../../packages/typescript-config - '@types/bun': + '@types/node': + specifier: 'catalog:' + version: 24.7.0 + esbuild: specifier: 'catalog:' - version: 1.3.13 + version: 0.28.1 + execa: + specifier: 'catalog:' + version: 9.6.0 + tsx: + specifier: 'catalog:' + version: 4.21.0 typescript: specifier: 'catalog:' version: 6.0.2 + vite: + specifier: 'catalog:' + version: 7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0) + vitest: + specifier: 'catalog:' + version: 4.1.2(@types/node@24.7.0)(vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) bots/market-making: dependencies: - '@aws-sdk/client-kms': - specifier: 3.1101.0 - version: 3.1101.0 - '@ethereumjs/wallet': - specifier: ^10.0.0 - version: 10.0.0 '@morpho-org/midnight-sdk': specifier: 'catalog:' version: 1.3.0(@morpho-org/morpho-ts@2.8.0)(viem@2.47.17(typescript@6.0.2)(zod@4.4.3)) @@ -183,12 +198,6 @@ importers: '@morpho-org/morpho-ts': specifier: 'catalog:' version: 2.8.0 - '@noble/curves': - specifier: 1.9.1 - version: 1.9.1 - '@repo/bot-kit': - specifier: workspace:* - version: link:../../packages/bot-kit '@repo/logging': specifier: workspace:* version: link:../../packages/logging @@ -204,24 +213,9 @@ importers: '@repo/utils': specifier: workspace:* version: link:../../packages/utils - '@tanstack/react-form': - specifier: 'catalog:' - version: 1.20.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@tanstack/react-table': - specifier: 'catalog:' - version: 8.21.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - asn1js: - specifier: 3.0.6 - version: 3.0.6 commander: specifier: ^14.0.3 version: 14.0.3 - react: - specifier: 'catalog:' - version: 19.2.8 - react-dom: - specifier: 'catalog:' - version: 19.2.8(react@19.2.8) viem: specifier: 'catalog:' version: 2.47.17(typescript@6.0.2)(zod@4.4.3) @@ -232,21 +226,27 @@ importers: '@repo/typescript-config': specifier: workspace:* version: link:../../packages/typescript-config - '@types/bun': + '@types/node': specifier: 'catalog:' - version: 1.3.13 - '@types/react': + version: 24.7.0 + esbuild: + specifier: 'catalog:' + version: 0.28.1 + execa: specifier: 'catalog:' - version: 19.2.18 - '@types/react-dom': + version: 9.6.0 + tsx: specifier: 'catalog:' - version: 19.2.4(@types/react@19.2.18) + version: 4.21.0 typedoc: specifier: 0.28.20 version: 0.28.20(typescript@6.0.2) typescript: specifier: 'catalog:' version: 6.0.2 + vitest: + specifier: 'catalog:' + version: 4.1.2(@types/node@24.7.0)(vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.8.3)) bots/midnight-crossed-books: dependencies: @@ -269,15 +269,27 @@ importers: '@repo/typescript-config': specifier: workspace:* version: link:../../packages/typescript-config - '@types/bun': + '@types/node': specifier: 'catalog:' - version: 1.3.13 + version: 24.7.0 + esbuild: + specifier: 'catalog:' + version: 0.28.1 + execa: + specifier: 'catalog:' + version: 9.6.0 openapi-typescript: specifier: 'catalog:' version: 7.13.0(typescript@6.0.2) + tsx: + specifier: 'catalog:' + version: 4.21.0 typescript: specifier: 'catalog:' version: 6.0.2 + vitest: + specifier: 'catalog:' + version: 4.1.2(@types/node@24.7.0)(vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) bots/midnight-liquidation: dependencies: @@ -304,7 +316,7 @@ importers: version: 0.8.35 soltag: specifier: 'catalog:' - version: 0.0.17(esbuild@0.25.12)(solc@0.8.35)(typescript@6.0.2)(viem@2.47.17(typescript@6.0.2)(zod@4.4.3)) + version: 0.0.17(esbuild@0.28.1)(rollup@4.62.4)(solc@0.8.35)(typescript@6.0.2)(viem@2.47.17(typescript@6.0.2)(zod@4.4.3))(vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) viem: specifier: 'catalog:' version: 2.47.17(typescript@6.0.2)(zod@4.4.3) @@ -312,12 +324,27 @@ importers: '@repo/typescript-config': specifier: workspace:* version: link:../../packages/typescript-config - '@types/bun': + '@types/node': + specifier: 'catalog:' + version: 24.7.0 + esbuild: + specifier: 'catalog:' + version: 0.28.1 + execa: specifier: 'catalog:' - version: 1.3.13 + version: 9.6.0 + tsx: + specifier: 'catalog:' + version: 4.21.0 typescript: specifier: 'catalog:' version: 6.0.2 + vite: + specifier: 'catalog:' + version: 7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0) + vitest: + specifier: 'catalog:' + version: 4.1.2(@types/node@24.7.0)(vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) packages/bot-kit: dependencies: @@ -340,27 +367,36 @@ importers: '@repo/typescript-config': specifier: workspace:* version: link:../typescript-config - '@types/bun': + '@types/node': specifier: 'catalog:' - version: 1.3.13 + version: 24.7.0 typescript: specifier: 'catalog:' version: 6.0.2 + vitest: + specifier: 'catalog:' + version: 4.1.2(@types/node@24.7.0)(vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) packages/contracts: devDependencies: '@repo/typescript-config': specifier: workspace:* version: link:../typescript-config - '@types/bun': + '@types/node': + specifier: 'catalog:' + version: 24.7.0 + esbuild: specifier: 'catalog:' - version: 1.3.13 + version: 0.28.1 solc: specifier: 'catalog:' version: 0.8.35 soltag: specifier: 'catalog:' - version: 0.0.17(esbuild@0.25.12)(solc@0.8.35)(typescript@6.0.2)(viem@2.47.17(typescript@6.0.2)(zod@4.4.3)) + version: 0.0.17(esbuild@0.28.1)(rollup@4.62.4)(solc@0.8.35)(typescript@6.0.2)(viem@2.47.17(typescript@6.0.2)(zod@4.4.3))(vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) + tsx: + specifier: 'catalog:' + version: 4.21.0 typescript: specifier: 'catalog:' version: 6.0.2 @@ -373,24 +409,30 @@ importers: '@repo/typescript-config': specifier: workspace:* version: link:../typescript-config - '@types/bun': + '@types/node': specifier: 'catalog:' - version: 1.3.13 + version: 24.7.0 typescript: specifier: 'catalog:' version: 6.0.2 + vitest: + specifier: 'catalog:' + version: 4.1.2(@types/node@24.7.0)(vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) packages/monitoring: devDependencies: '@repo/typescript-config': specifier: workspace:* version: link:../typescript-config - '@types/bun': + '@types/node': specifier: 'catalog:' - version: 1.3.13 + version: 24.7.0 typescript: specifier: 'catalog:' version: 6.0.2 + vitest: + specifier: 'catalog:' + version: 4.1.2(@types/node@24.7.0)(vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) packages/observability: dependencies: @@ -401,12 +443,15 @@ importers: '@repo/typescript-config': specifier: workspace:* version: link:../typescript-config - '@types/bun': + '@types/node': specifier: 'catalog:' - version: 1.3.13 + version: 24.7.0 typescript: specifier: 'catalog:' version: 6.0.2 + vitest: + specifier: 'catalog:' + version: 4.1.2(@types/node@24.7.0)(vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) packages/offers: dependencies: @@ -417,12 +462,15 @@ importers: '@repo/typescript-config': specifier: workspace:* version: link:../typescript-config - '@types/bun': + '@types/node': specifier: 'catalog:' - version: 1.3.13 + version: 24.7.0 typescript: specifier: 'catalog:' version: 6.0.2 + vitest: + specifier: 'catalog:' + version: 4.1.2(@types/node@24.7.0)(vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) packages/swaps: dependencies: @@ -442,12 +490,15 @@ importers: '@repo/typescript-config': specifier: workspace:* version: link:../typescript-config - '@types/bun': + '@types/node': specifier: 'catalog:' - version: 1.3.13 + version: 24.7.0 typescript: specifier: 'catalog:' version: 6.0.2 + vitest: + specifier: 'catalog:' + version: 4.1.2(@types/node@24.7.0)(vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) packages/typescript-config: {} @@ -478,88 +529,24 @@ importers: '@repo/typescript-config': specifier: workspace:* version: link:../typescript-config - '@types/bun': - specifier: 'catalog:' - version: 1.3.13 '@types/lodash-es': specifier: 'catalog:' version: 4.17.12 + '@types/node': + specifier: 'catalog:' + version: 24.7.0 typescript: specifier: 'catalog:' version: 6.0.2 + vitest: + specifier: 'catalog:' + version: 4.1.2(@types/node@24.7.0)(vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) packages: '@adraffy/ens-normalize@1.11.1': resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} - '@aws-sdk/client-kms@3.1101.0': - resolution: {integrity: sha512-qczxakJlFYG9E6nE2yWIJ2nXq0HbMzHxMSWIsyubPZqx1TANHd2CSJWDTkg+IsNKq+94tG+e+EJo3uADwZ6cdw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/core@3.977.5': - resolution: {integrity: sha512-O5otOc1c6UZh5HsHAaPdYBcUUR9HL6mtnKqvc8nxN/CKDGUBUpsdh0q8K04Uz/dd1i0TaGyIQuQNqoO7+ad2TQ==} - engines: {node: '>=20.0.0'} - deprecated: |- - Deprecated due to Document number parsing bug in JSON, see - https://github.com/aws/aws-sdk-js-v3/issues/8246. Newer version available. - - '@aws-sdk/credential-provider-env@3.972.66': - resolution: {integrity: sha512-bOzP2+zdJ0XrghywB4FaJXtGZCx9yS0AGps+VJ5yEgg30wVyHNmVDBwVDXcRypzQY5iLGCS3NSn0nsuISqjFCQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-http@3.972.68': - resolution: {integrity: sha512-lkunS8X+H6V76WE+t/uGQm/U8v0JXK5mLfNFTUAMlE1kqaCjwlmqKJrgCVtqjK/vqnlrSWsLK4Lr4NANBWlfTQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-ini@3.973.11': - resolution: {integrity: sha512-KoDEolYtLHG/8C+IiZpXbJWyBOMkrHV+j66Kb9PBXmLv5euGb7aELvuCmLenoGAV6gBW2wM7TsG/1e5iulH4kA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-login@3.972.73': - resolution: {integrity: sha512-tjsxMkTAFkmiV9ycmymapb9nLECWVOwFs0bZMQ9gB9bnbY8/HwfukHZlWbXZZp7qkPU6EXAfOcMm3DioFFEywA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-node@3.972.77': - resolution: {integrity: sha512-l4nitYCN/Ls57vtUfdextCjTjW41JD7lQiAnuR0RTbdByFc/6OmEAzwGd+lrp6CUtiXGQL1FCaYiamfHASrwBw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-process@3.972.66': - resolution: {integrity: sha512-YOnX6bIhdjx0QfaENu2PB0eFm5MEc9ft8XNGQ+NxMfeLSq9aE+XjWCwDupEnV4UWv5ZFpBLJbTREIx7KNOoqpQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-sso@3.973.10': - resolution: {integrity: sha512-IsXnQ35j5VE+3ZK6aIhT5ypB+Jim3zRwVz0nYuVwyBKZyu/SYx+O2/LQpng8c2EiuwyqceabsDlYrICHDlJPsA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-web-identity@3.972.72': - resolution: {integrity: sha512-nj9Zlsy7ya+fy+jhWTJwgfr7YdtDM4xHyZvgKuftuny0UgROVx9lxwvsWJSLvpKk4lig0m0tHng3k1fEnt0LeA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/nested-clients@3.997.40': - resolution: {integrity: sha512-hEdHT0PBR4fkGxWhwKG5EtEYKnAM7HKkp0vD10ufk4YcXejH4r4q6G/XhPzjUc6Yxo5kBS2vHg7llj4ViR9VTQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/signature-v4-multi-region@3.996.43': - resolution: {integrity: sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/token-providers@3.1102.0': - resolution: {integrity: sha512-Ua700vVvM1q105yABSUQWkCK6FeTrNfU6ORGetJe5BzkZWY7QhkF7SVTOlmDGWRDNd6jbyY0Dv5e+E4bMBEmLg==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/types@3.974.2': - resolution: {integrity: sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/xml-builder@3.972.37': - resolution: {integrity: sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg==} - engines: {node: '>=20.0.0'} - - '@aws/lambda-invoke-store@0.3.0': - resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} - engines: {node: '>=18.0.0'} - '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -577,174 +564,317 @@ packages: '@emnapi/wasi-threads@1.2.2': resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} - '@esbuild/aix-ppc64@0.25.12': - resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.12': - resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.12': - resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.12': - resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.12': - resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.12': - resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.12': - resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.12': - resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.12': - resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.12': - resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.12': - resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.12': - resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.12': - resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.12': - resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.12': - resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.12': - resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.12': - resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.12': - resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.12': - resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.12': - resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.12': - resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.12': - resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.12': - resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.12': - resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.12': - resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} - cpu: [ia32] + cpu: [arm64] os: [win32] - '@esbuild/win32-x64@0.25.12': - resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} engines: {node: '>=18'} - cpu: [x64] + cpu: [ia32] os: [win32] - '@ethereumjs/rlp@10.1.2': - resolution: {integrity: sha512-T5Zt6C2pd02Wd88Q9A5/UX+He1Q2Y1LntHxz/038tfbUMiqby4fYSSTLEDx+TEfJqw1BsJSBY/TSu6goUzlk+w==} - engines: {node: '>=20'} - hasBin: true + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] - '@ethereumjs/util@10.1.2': - resolution: {integrity: sha512-UPBgXtHHfQugoXOSAoeG3jdmPbl37cwV9y3XqTPAnw8tJj8np14TPV2uc5lOs7C2LMF9Ubn66zyaiYxgwGppng==} - engines: {node: '>=20'} + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] - '@ethereumjs/wallet@10.0.0': - resolution: {integrity: sha512-ayRmV8uL1YEaRxUacr5fcy/Z/03K8NtVP7NIq3X4y5wDaNx7e+6hk40sa7cbkBhzOEK4j3JPpGohVhrOw5I03A==} + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} + cpu: [x64] + os: [win32] '@gerrit0/mini-shiki@3.23.0': resolution: {integrity: sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==} @@ -834,6 +964,13 @@ packages: '@vercel/blob': optional: true + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + '@napi-rs/wasm-runtime@1.2.2': resolution: {integrity: sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==} engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} @@ -845,18 +982,10 @@ packages: resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} engines: {node: ^14.21.3 || >=16} - '@noble/curves@1.9.0': - resolution: {integrity: sha512-7YDlXiNMdO1YZeH6t/kvopHHbIZzlxrCV9WLqCY6QhcXOoXiNCMDqJIglZ9Yjx5+w7Dz30TITFrlTjnRg7sKEg==} - engines: {node: ^14.21.3 || >=16} - '@noble/curves@1.9.1': resolution: {integrity: sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==} engines: {node: ^14.21.3 || >=16} - '@noble/curves@2.2.0': - resolution: {integrity: sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==} - engines: {node: '>= 20.19.0'} - '@noble/hashes@1.8.0': resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} @@ -1264,6 +1393,144 @@ packages: resolution: {integrity: sha512-UyKIm0wTPw5BcY7Z2PkbK1Ma260um96LSBWXHrdSMe+ZV0EPMyDfAcUcjjm3qEiGST9OK/1TriekdPCZkn4Q3A==} engines: {node: '>=18.17.0', npm: '>=9.5.0'} + '@rollup/rollup-android-arm-eabi@4.62.4': + resolution: {integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.4': + resolution: {integrity: sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.4': + resolution: {integrity: sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.4': + resolution: {integrity: sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.4': + resolution: {integrity: sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.4': + resolution: {integrity: sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.62.4': + resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.62.4': + resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.62.4': + resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.62.4': + resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.62.4': + resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.62.4': + resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.62.4': + resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.62.4': + resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.62.4': + resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.62.4': + resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.62.4': + resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.62.4': + resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.62.4': + resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.4': + resolution: {integrity: sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.4': + resolution: {integrity: sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.4': + resolution: {integrity: sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.4': + resolution: {integrity: sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.4': + resolution: {integrity: sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==} + cpu: [x64] + os: [win32] + '@scure/base@1.2.6': resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} @@ -1273,6 +1540,9 @@ packages: '@scure/bip39@1.6.0': resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@shikijs/engine-oniguruma@3.23.0': resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==} @@ -1288,70 +1558,27 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} - '@smithy/core@3.31.1': - resolution: {integrity: sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg==} - engines: {node: '>=18.0.0'} - - '@smithy/credential-provider-imds@4.4.16': - resolution: {integrity: sha512-QfuLWAkLzptffFW980AFeHZFdqds2B64rpEd3uJ6lgs3xVn9QegGMUgUcj+4d7dRrAsya3r58ZKpku97WcFb4w==} - engines: {node: '>=18.0.0'} - - '@smithy/fetch-http-handler@5.6.13': - resolution: {integrity: sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A==} - engines: {node: '>=18.0.0'} - - '@smithy/node-http-handler@4.9.13': - resolution: {integrity: sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw==} - engines: {node: '>=18.0.0'} - - '@smithy/signature-v4@5.6.12': - resolution: {integrity: sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ==} - engines: {node: '>=18.0.0'} + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} - '@smithy/types@4.16.1': - resolution: {integrity: sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==} - engines: {node: '>=18.0.0'} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} '@swc/helpers@0.5.23': resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} - '@tanstack/form-core@1.20.0': - resolution: {integrity: sha512-FGlKvcsusOf4756vtN1EoDI4h50r4/11eTcpF3NcnE04N/bSn2gP7cdhG6tYA0lJWzM9H1pNIzZ86uZ4MHB9eA==} - - '@tanstack/react-form@1.20.0': - resolution: {integrity: sha512-1UfWqEYRnHr4cooGbHiTQqoqus8soNUH+RLD6UyhIQEvomOSQMX0JgX+zGSl08tIugrnWcAnh50n5T9IIs/Evw==} - peerDependencies: - '@tanstack/react-start': ^1.130.10 - react: ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@tanstack/react-start': - optional: true - - '@tanstack/react-store@0.7.7': - resolution: {integrity: sha512-qqT0ufegFRDGSof9D/VqaZgjNgp4tRPHZIJq2+QIHkMUtHjaJ0lYrrXjeIUJvjnTbgPfSD1XgOMEt0lmANn6Zg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - '@tanstack/react-table@8.21.3': - resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} - engines: {node: '>=12'} - peerDependencies: - react: '>=16.8' - react-dom: '>=16.8' - - '@tanstack/store@0.7.7': - resolution: {integrity: sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==} - - '@tanstack/table-core@8.21.3': - resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} - engines: {node: '>=12'} - '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} - '@types/bun@1.3.13': - resolution: {integrity: sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} '@types/hast@3.0.5': resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} @@ -1362,19 +1589,40 @@ packages: '@types/lodash@4.17.25': resolution: {integrity: sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==} - '@types/node@25.8.0': - resolution: {integrity: sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==} + '@types/node@24.7.0': + resolution: {integrity: sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - '@types/react-dom@19.2.4': - resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==} + '@vitest/expect@4.1.2': + resolution: {integrity: sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==} + + '@vitest/mocker@4.1.2': + resolution: {integrity: sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==} peerDependencies: - '@types/react': ^19.2.0 + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true - '@types/react@19.2.18': - resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} + '@vitest/pretty-format@4.1.2': + resolution: {integrity: sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==} - '@types/unist@3.0.3': - resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@vitest/runner@4.1.2': + resolution: {integrity: sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==} + + '@vitest/snapshot@4.1.2': + resolution: {integrity: sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==} + + '@vitest/spy@4.1.2': + resolution: {integrity: sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==} + + '@vitest/utils@4.1.2': + resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==} abitype@1.2.3: resolution: {integrity: sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==} @@ -1410,9 +1658,9 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - asn1js@3.0.6: - resolution: {integrity: sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==} - engines: {node: '>=12.0.0'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1421,9 +1669,6 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - bowser@2.14.1: - resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} - brace-expansion@2.1.4: resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} @@ -1435,8 +1680,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - bun-types@1.3.13: - resolution: {integrity: sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} change-case@5.4.4: resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} @@ -1466,8 +1712,12 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} @@ -1481,12 +1731,6 @@ packages: supports-color: optional: true - decode-formdata@0.9.0: - resolution: {integrity: sha512-q5uwOjR3Um5YD+ZWPOF/1sGHVW9A5rCrRwITQChRXlmPkxDFBqCm4jNTIVdGHNH9OnR+V9MoZVgRhsFb+ARbUw==} - - devalue@5.9.0: - resolution: {integrity: sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==} - emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -1498,14 +1742,21 @@ packages: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} - esbuild@0.25.12: - resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} hasBin: true - ethereum-cryptography@3.2.0: - resolution: {integrity: sha512-Urr5YVsalH+Jo0sYkTkv1MyI9bLYZwW8BENZCeE1QYaTHETEYx0Nv/SVsWkSqpYrzweg6d8KMY1wTjH/1m/BIg==} - engines: {node: ^14.21.3 || >=16, npm: '>=9'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} @@ -1517,12 +1768,20 @@ packages: resolution: {integrity: sha512-rMFIq/xfsNpR1JGkXPnLP1iXLBCtVr1N4tErzdEcKJoqREpKOZswCwNSljN/Sf1QZjv+KGCamh4E4FQcVaJekA==} engines: {node: '>=12.0'} + execa@9.6.0: + resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==} + engines: {node: ^18.19.0 || >=20.5.0} + executooor-viem@1.3.3: resolution: {integrity: sha512-8npMgRIDerwxeyWonqsTSrw28CIU4EDUz2JSXRz5hEIF6pxNbRls6uS2wR2lf74J2fxAgtF6+Ltin5/bMlFiyw==} peerDependencies: evm-maths: ^7.0.0 viem: ^2.0.0 + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1536,6 +1795,19 @@ packages: fd-package-json@2.0.0: resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -1554,10 +1826,22 @@ packages: engines: {node: '>=18.3.0'} hasBin: true + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + get-east-asian-width@1.6.0: resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} engines: {node: '>=18'} + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1566,6 +1850,10 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} @@ -1591,6 +1879,21 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isows@1.0.7: resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==} peerDependencies: @@ -1604,9 +1907,6 @@ packages: resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} engines: {node: '>=0.10.0'} - js-md5@0.8.3: - resolution: {integrity: sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==} - js-sha3@0.8.0: resolution: {integrity: sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==} @@ -1654,6 +1954,9 @@ packages: lunr@2.3.9: resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + markdown-it@14.3.0: resolution: {integrity: sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==} hasBin: true @@ -1691,6 +1994,19 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} @@ -1745,6 +2061,21 @@ packages: resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} engines: {node: '>=18'} + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1760,33 +2091,28 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + postcss@8.5.25: + resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} + engines: {node: ^10 || ^12 || >=14} + + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} - pvtsutils@1.3.6: - resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} - - pvutils@1.1.5: - resolution: {integrity: sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==} - engines: {node: '>=16.0.0'} - queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - react-dom@19.2.8: - resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} - peerDependencies: - react: ^19.2.8 - - react@19.2.8: - resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} - engines: {node: '>=0.10.0'} - require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} @@ -1798,16 +2124,29 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rollup@4.62.4: + resolution: {integrity: sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -1840,6 +2179,16 @@ packages: typescript: optional: true + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -1856,6 +2205,10 @@ packages: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + strip-json-comments@5.0.3: resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} engines: {node: '>=14.16'} @@ -1864,14 +2217,25 @@ packages: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@1.3.0: resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} engines: {node: '>=18'} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + tinypool@2.1.0: resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} engines: {node: ^20.0.0 || >=22.0.0} + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -1883,6 +2247,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + type-fest@4.41.0: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} @@ -1906,8 +2275,12 @@ packages: resolution: {integrity: sha512-X2wH19RAPZE3+ldGicOkoj/SIA83OIxcJ6Cuaw23hf8Xc6fQpvZXY0SftE2JgS0QhYLUG4uwodSI3R53keyh7w==} engines: {node: '>=14'} - undici-types@7.24.6: - resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + undici-types@7.14.0: + resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} unplugin@3.3.0: resolution: {integrity: sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==} @@ -1945,15 +2318,6 @@ packages: uri-js-replace@1.0.1: resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} - use-sync-external-store@1.6.0: - resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - uuid@11.1.1: - resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} - hasBin: true - valibot@1.4.2: resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==} peerDependencies: @@ -1970,6 +2334,81 @@ packages: typescript: optional: true + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.2: + resolution: {integrity: sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.2 + '@vitest/browser-preview': 4.1.2 + '@vitest/browser-webdriverio': 4.1.2 + '@vitest/ui': 4.1.2 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + walk-up-path@4.0.0: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} @@ -1977,6 +2416,16 @@ packages: webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} @@ -2010,160 +2459,19 @@ packages: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yoctocolors@2.2.0: + resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==} + engines: {node: '>=18'} + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} -snapshots: - - '@adraffy/ens-normalize@1.11.1': {} - - '@aws-sdk/client-kms@3.1101.0': - dependencies: - '@aws-sdk/core': 3.977.5 - '@aws-sdk/credential-provider-node': 3.972.77 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/fetch-http-handler': 5.6.13 - '@smithy/node-http-handler': 4.9.13 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/core@3.977.5': - dependencies: - '@aws-sdk/types': 3.974.2 - '@aws-sdk/xml-builder': 3.972.37 - '@aws/lambda-invoke-store': 0.3.0 - '@smithy/core': 3.31.1 - '@smithy/signature-v4': 5.6.12 - '@smithy/types': 4.16.1 - bowser: 2.14.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-env@3.972.66': - dependencies: - '@aws-sdk/core': 3.977.5 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-http@3.972.68': - dependencies: - '@aws-sdk/core': 3.977.5 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/fetch-http-handler': 5.6.13 - '@smithy/node-http-handler': 4.9.13 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-ini@3.973.11': - dependencies: - '@aws-sdk/core': 3.977.5 - '@aws-sdk/credential-provider-env': 3.972.66 - '@aws-sdk/credential-provider-http': 3.972.68 - '@aws-sdk/credential-provider-login': 3.972.73 - '@aws-sdk/credential-provider-process': 3.972.66 - '@aws-sdk/credential-provider-sso': 3.973.10 - '@aws-sdk/credential-provider-web-identity': 3.972.72 - '@aws-sdk/nested-clients': 3.997.40 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/credential-provider-imds': 4.4.16 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-login@3.972.73': - dependencies: - '@aws-sdk/core': 3.977.5 - '@aws-sdk/nested-clients': 3.997.40 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-node@3.972.77': - dependencies: - '@aws-sdk/credential-provider-env': 3.972.66 - '@aws-sdk/credential-provider-http': 3.972.68 - '@aws-sdk/credential-provider-ini': 3.973.11 - '@aws-sdk/credential-provider-process': 3.972.66 - '@aws-sdk/credential-provider-sso': 3.973.10 - '@aws-sdk/credential-provider-web-identity': 3.972.72 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/credential-provider-imds': 4.4.16 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-process@3.972.66': - dependencies: - '@aws-sdk/core': 3.977.5 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-sso@3.973.10': - dependencies: - '@aws-sdk/core': 3.977.5 - '@aws-sdk/nested-clients': 3.997.40 - '@aws-sdk/token-providers': 3.1102.0 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-web-identity@3.972.72': - dependencies: - '@aws-sdk/core': 3.977.5 - '@aws-sdk/nested-clients': 3.997.40 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/nested-clients@3.997.40': - dependencies: - '@aws-sdk/core': 3.977.5 - '@aws-sdk/signature-v4-multi-region': 3.996.43 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/fetch-http-handler': 5.6.13 - '@smithy/node-http-handler': 4.9.13 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/signature-v4-multi-region@3.996.43': - dependencies: - '@aws-sdk/types': 3.974.2 - '@smithy/signature-v4': 5.6.12 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/token-providers@3.1102.0': - dependencies: - '@aws-sdk/core': 3.977.5 - '@aws-sdk/nested-clients': 3.997.40 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/types@3.974.2': - dependencies: - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/xml-builder@3.972.37': - dependencies: - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws/lambda-invoke-store@0.3.0': {} +snapshots: + + '@adraffy/ens-normalize@1.11.1': {} '@babel/code-frame@7.29.7': dependencies: @@ -2189,99 +2497,161 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.25.12': + '@esbuild/aix-ppc64@0.27.7': optional: true - '@esbuild/android-arm64@0.25.12': + '@esbuild/aix-ppc64@0.28.1': optional: true - '@esbuild/android-arm@0.25.12': + '@esbuild/android-arm64@0.27.7': optional: true - '@esbuild/android-x64@0.25.12': + '@esbuild/android-arm64@0.28.1': optional: true - '@esbuild/darwin-arm64@0.25.12': + '@esbuild/android-arm@0.27.7': optional: true - '@esbuild/darwin-x64@0.25.12': + '@esbuild/android-arm@0.28.1': optional: true - '@esbuild/freebsd-arm64@0.25.12': + '@esbuild/android-x64@0.27.7': optional: true - '@esbuild/freebsd-x64@0.25.12': + '@esbuild/android-x64@0.28.1': optional: true - '@esbuild/linux-arm64@0.25.12': + '@esbuild/darwin-arm64@0.27.7': optional: true - '@esbuild/linux-arm@0.25.12': + '@esbuild/darwin-arm64@0.28.1': optional: true - '@esbuild/linux-ia32@0.25.12': + '@esbuild/darwin-x64@0.27.7': optional: true - '@esbuild/linux-loong64@0.25.12': + '@esbuild/darwin-x64@0.28.1': optional: true - '@esbuild/linux-mips64el@0.25.12': + '@esbuild/freebsd-arm64@0.27.7': optional: true - '@esbuild/linux-ppc64@0.25.12': + '@esbuild/freebsd-arm64@0.28.1': optional: true - '@esbuild/linux-riscv64@0.25.12': + '@esbuild/freebsd-x64@0.27.7': optional: true - '@esbuild/linux-s390x@0.25.12': + '@esbuild/freebsd-x64@0.28.1': optional: true - '@esbuild/linux-x64@0.25.12': + '@esbuild/linux-arm64@0.27.7': optional: true - '@esbuild/netbsd-arm64@0.25.12': + '@esbuild/linux-arm64@0.28.1': optional: true - '@esbuild/netbsd-x64@0.25.12': + '@esbuild/linux-arm@0.27.7': optional: true - '@esbuild/openbsd-arm64@0.25.12': + '@esbuild/linux-arm@0.28.1': optional: true - '@esbuild/openbsd-x64@0.25.12': + '@esbuild/linux-ia32@0.27.7': optional: true - '@esbuild/openharmony-arm64@0.25.12': + '@esbuild/linux-ia32@0.28.1': optional: true - '@esbuild/sunos-x64@0.25.12': + '@esbuild/linux-loong64@0.27.7': optional: true - '@esbuild/win32-arm64@0.25.12': + '@esbuild/linux-loong64@0.28.1': optional: true - '@esbuild/win32-ia32@0.25.12': + '@esbuild/linux-mips64el@0.27.7': optional: true - '@esbuild/win32-x64@0.25.12': + '@esbuild/linux-mips64el@0.28.1': optional: true - '@ethereumjs/rlp@10.1.2': {} + '@esbuild/linux-ppc64@0.27.7': + optional: true - '@ethereumjs/util@10.1.2': - dependencies: - '@ethereumjs/rlp': 10.1.2 - '@noble/curves': 2.2.0 - '@noble/hashes': 2.2.0 + '@esbuild/linux-ppc64@0.28.1': + optional: true - '@ethereumjs/wallet@10.0.0': - dependencies: - '@ethereumjs/util': 10.1.2 - '@scure/base': 1.2.6 - ethereum-cryptography: 3.2.0 - js-md5: 0.8.3 - uuid: 11.1.1 + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true '@gerrit0/mini-shiki@3.23.0': dependencies: @@ -2382,6 +2752,9 @@ snapshots: transitivePeerDependencies: - typescript + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + '@napi-rs/wasm-runtime@1.2.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)': dependencies: '@emnapi/core': 1.11.2 @@ -2391,18 +2764,10 @@ snapshots: '@noble/ciphers@1.3.0': {} - '@noble/curves@1.9.0': - dependencies: - '@noble/hashes': 1.8.0 - '@noble/curves@1.9.1': dependencies: '@noble/hashes': 1.8.0 - '@noble/curves@2.2.0': - dependencies: - '@noble/hashes': 2.2.0 - '@noble/hashes@1.8.0': {} '@noble/hashes@2.2.0': {} @@ -2635,6 +3000,81 @@ snapshots: transitivePeerDependencies: - supports-color + '@rollup/rollup-android-arm-eabi@4.62.4': + optional: true + + '@rollup/rollup-android-arm64@4.62.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.4': + optional: true + + '@rollup/rollup-darwin-x64@4.62.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.4': + optional: true + '@scure/base@1.2.6': {} '@scure/bip32@1.7.0': @@ -2648,6 +3088,8 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 + '@sec-ant/readable-stream@0.4.1': {} + '@shikijs/engine-oniguruma@3.23.0': dependencies: '@shikijs/types': 3.23.0 @@ -2668,106 +3110,92 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} - '@smithy/core@3.31.1': - dependencies: - '@smithy/types': 4.16.1 - tslib: 2.8.1 + '@sindresorhus/merge-streams@4.0.0': {} - '@smithy/credential-provider-imds@4.4.16': - dependencies: - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 - tslib: 2.8.1 + '@standard-schema/spec@1.1.0': {} - '@smithy/fetch-http-handler@5.6.13': + '@swc/helpers@0.5.23': dependencies: - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 tslib: 2.8.1 - '@smithy/node-http-handler@4.9.13': + '@tybys/wasm-util@0.10.3': dependencies: - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 tslib: 2.8.1 + optional: true - '@smithy/signature-v4@5.6.12': + '@types/chai@5.2.3': dependencies: - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 - tslib: 2.8.1 + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 - '@smithy/types@4.16.1': - dependencies: - tslib: 2.8.1 + '@types/deep-eql@4.0.2': {} - '@swc/helpers@0.5.23': - dependencies: - tslib: 2.8.1 + '@types/estree@1.0.9': {} - '@tanstack/form-core@1.20.0': + '@types/hast@3.0.5': dependencies: - '@tanstack/store': 0.7.7 + '@types/unist': 3.0.3 - '@tanstack/react-form@1.20.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@types/lodash-es@4.17.12': dependencies: - '@tanstack/form-core': 1.20.0 - '@tanstack/react-store': 0.7.7(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - decode-formdata: 0.9.0 - devalue: 5.9.0 - react: 19.2.8 - transitivePeerDependencies: - - react-dom + '@types/lodash': 4.17.25 - '@tanstack/react-store@0.7.7(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': - dependencies: - '@tanstack/store': 0.7.7 - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) - use-sync-external-store: 1.6.0(react@19.2.8) + '@types/lodash@4.17.25': {} - '@tanstack/react-table@8.21.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@types/node@24.7.0': dependencies: - '@tanstack/table-core': 8.21.3 - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) - - '@tanstack/store@0.7.7': {} + undici-types: 7.14.0 - '@tanstack/table-core@8.21.3': {} + '@types/unist@3.0.3': {} - '@tybys/wasm-util@0.10.3': + '@vitest/expect@4.1.2': dependencies: - tslib: 2.8.1 - optional: true + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + chai: 6.2.2 + tinyrainbow: 3.1.1 - '@types/bun@1.3.13': + '@vitest/mocker@4.1.2(vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - bun-types: 1.3.13 + '@vitest/spy': 4.1.2 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.8.3) - '@types/hast@3.0.5': + '@vitest/mocker@4.1.2(vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: - '@types/unist': 3.0.3 + '@vitest/spy': 4.1.2 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0) - '@types/lodash-es@4.17.12': + '@vitest/pretty-format@4.1.2': dependencies: - '@types/lodash': 4.17.25 - - '@types/lodash@4.17.25': {} + tinyrainbow: 3.1.1 - '@types/node@25.8.0': + '@vitest/runner@4.1.2': dependencies: - undici-types: 7.24.6 + '@vitest/utils': 4.1.2 + pathe: 2.0.3 - '@types/react-dom@19.2.4(@types/react@19.2.18)': + '@vitest/snapshot@4.1.2': dependencies: - '@types/react': 19.2.18 + '@vitest/pretty-format': 4.1.2 + '@vitest/utils': 4.1.2 + magic-string: 0.30.21 + pathe: 2.0.3 - '@types/react@19.2.18': - dependencies: - csstype: 3.2.3 + '@vitest/spy@4.1.2': {} - '@types/unist@3.0.3': {} + '@vitest/utils@4.1.2': + dependencies: + '@vitest/pretty-format': 4.1.2 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 abitype@1.2.3(typescript@6.0.2)(zod@3.25.76): optionalDependencies: @@ -2793,18 +3221,12 @@ snapshots: argparse@2.0.1: {} - asn1js@3.0.6: - dependencies: - pvtsutils: 1.3.6 - pvutils: 1.1.5 - tslib: 2.8.1 + assertion-error@2.0.1: {} balanced-match@1.0.2: {} balanced-match@4.0.4: {} - bowser@2.14.1: {} - brace-expansion@2.1.4: dependencies: balanced-match: 1.0.2 @@ -2817,9 +3239,7 @@ snapshots: dependencies: fill-range: 7.1.1 - bun-types@1.3.13: - dependencies: - '@types/node': 25.8.0 + chai@6.2.2: {} change-case@5.4.4: {} @@ -2842,7 +3262,13 @@ snapshots: commander@8.3.0: {} - csstype@3.2.3: {} + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 date-fns@4.1.0: {} @@ -2852,52 +3278,75 @@ snapshots: optionalDependencies: supports-color: 10.2.2 - decode-formdata@0.9.0: {} - - devalue@5.9.0: {} - emoji-regex@10.6.0: {} entities@4.5.0: {} environment@1.1.0: {} - esbuild@0.25.12: + es-module-lexer@2.1.0: {} + + esbuild@0.27.7: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.12 - '@esbuild/android-arm': 0.25.12 - '@esbuild/android-arm64': 0.25.12 - '@esbuild/android-x64': 0.25.12 - '@esbuild/darwin-arm64': 0.25.12 - '@esbuild/darwin-x64': 0.25.12 - '@esbuild/freebsd-arm64': 0.25.12 - '@esbuild/freebsd-x64': 0.25.12 - '@esbuild/linux-arm': 0.25.12 - '@esbuild/linux-arm64': 0.25.12 - '@esbuild/linux-ia32': 0.25.12 - '@esbuild/linux-loong64': 0.25.12 - '@esbuild/linux-mips64el': 0.25.12 - '@esbuild/linux-ppc64': 0.25.12 - '@esbuild/linux-riscv64': 0.25.12 - '@esbuild/linux-s390x': 0.25.12 - '@esbuild/linux-x64': 0.25.12 - '@esbuild/netbsd-arm64': 0.25.12 - '@esbuild/netbsd-x64': 0.25.12 - '@esbuild/openbsd-arm64': 0.25.12 - '@esbuild/openbsd-x64': 0.25.12 - '@esbuild/openharmony-arm64': 0.25.12 - '@esbuild/sunos-x64': 0.25.12 - '@esbuild/win32-arm64': 0.25.12 - '@esbuild/win32-ia32': 0.25.12 - '@esbuild/win32-x64': 0.25.12 - - ethereum-cryptography@3.2.0: - dependencies: - '@noble/ciphers': 1.3.0 - '@noble/curves': 1.9.0 - '@noble/hashes': 1.8.0 - '@scure/bip32': 1.7.0 - '@scure/bip39': 1.6.0 + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 eventemitter3@5.0.1: {} @@ -2905,6 +3354,21 @@ snapshots: evm-maths@7.0.1: {} + execa@9.6.0: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.2.0 + executooor-viem@1.3.3(evm-maths@7.0.1)(viem@2.47.17(typescript@6.0.2)(zod@3.25.76)): dependencies: evm-maths: 7.0.1 @@ -2915,6 +3379,8 @@ snapshots: evm-maths: 7.0.1 viem: 2.47.17(typescript@6.0.2)(zod@4.4.3) + expect-type@1.4.0: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -2933,6 +3399,14 @@ snapshots: dependencies: walk-up-path: 4.0.0 + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -2943,8 +3417,20 @@ snapshots: dependencies: fd-package-json: 2.0.0 + fsevents@2.3.3: + optional: true + get-east-asian-width@1.6.0: {} + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -2956,6 +3442,8 @@ snapshots: transitivePeerDependencies: - supports-color + human-signals@8.0.1: {} + husky@9.1.7: {} index-to-position@1.2.0: {} @@ -2972,6 +3460,14 @@ snapshots: is-number@7.0.0: {} + is-plain-obj@4.1.0: {} + + is-stream@4.0.1: {} + + is-unicode-supported@2.1.0: {} + + isexe@2.0.0: {} + isows@1.0.7(ws@8.18.3): dependencies: ws: 8.18.3 @@ -2980,8 +3476,6 @@ snapshots: js-levenshtein@1.1.6: {} - js-md5@0.8.3: {} - js-sha3@0.8.0: {} js-tokens@4.0.0: {} @@ -2992,10 +3486,10 @@ snapshots: json-schema-traverse@1.0.0: {} - knip@5.88.1(@types/node@25.8.0)(typescript@6.0.2): + knip@5.88.1(@types/node@24.7.0)(typescript@6.0.2): dependencies: '@nodelib/fs.walk': 1.2.8 - '@types/node': 25.8.0 + '@types/node': 24.7.0 fast-glob: 3.3.3 formatly: 0.3.0 jiti: 2.7.0 @@ -3052,6 +3546,10 @@ snapshots: lunr@2.3.9: {} + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + markdown-it@14.3.0: dependencies: argparse: 2.0.1 @@ -3086,6 +3584,15 @@ snapshots: ms@2.1.3: {} + nanoid@3.3.16: {} + + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + + obug@2.1.4: {} + onetime@7.0.0: dependencies: mimic-function: 5.0.1 @@ -3222,6 +3729,14 @@ snapshots: index-to-position: 1.2.0 type-fest: 4.41.0 + parse-ms@4.0.0: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + pathe@2.0.3: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -3230,25 +3745,24 @@ snapshots: pluralize@8.0.0: {} - punycode.js@2.3.1: {} + postcss@8.5.25: + dependencies: + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 - pvtsutils@1.3.6: + pretty-ms@9.3.0: dependencies: - tslib: 2.8.1 + parse-ms: 4.0.0 - pvutils@1.1.5: {} + punycode.js@2.3.1: {} queue-microtask@1.2.3: {} - react-dom@19.2.8(react@19.2.8): - dependencies: - react: 19.2.8 - scheduler: 0.27.0 - - react@19.2.8: {} - require-from-string@2.0.2: {} + resolve-pkg-maps@1.0.0: {} + restore-cursor@5.1.0: dependencies: onetime: 7.0.0 @@ -3258,14 +3772,52 @@ snapshots: rfdc@1.4.1: {} + rollup@4.62.4: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.62.4 + '@rollup/rollup-android-arm64': 4.62.4 + '@rollup/rollup-darwin-arm64': 4.62.4 + '@rollup/rollup-darwin-x64': 4.62.4 + '@rollup/rollup-freebsd-arm64': 4.62.4 + '@rollup/rollup-freebsd-x64': 4.62.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.4 + '@rollup/rollup-linux-arm-musleabihf': 4.62.4 + '@rollup/rollup-linux-arm64-gnu': 4.62.4 + '@rollup/rollup-linux-arm64-musl': 4.62.4 + '@rollup/rollup-linux-loong64-gnu': 4.62.4 + '@rollup/rollup-linux-loong64-musl': 4.62.4 + '@rollup/rollup-linux-ppc64-gnu': 4.62.4 + '@rollup/rollup-linux-ppc64-musl': 4.62.4 + '@rollup/rollup-linux-riscv64-gnu': 4.62.4 + '@rollup/rollup-linux-riscv64-musl': 4.62.4 + '@rollup/rollup-linux-s390x-gnu': 4.62.4 + '@rollup/rollup-linux-x64-gnu': 4.62.4 + '@rollup/rollup-linux-x64-musl': 4.62.4 + '@rollup/rollup-openbsd-x64': 4.62.4 + '@rollup/rollup-openharmony-arm64': 4.62.4 + '@rollup/rollup-win32-arm64-msvc': 4.62.4 + '@rollup/rollup-win32-ia32-msvc': 4.62.4 + '@rollup/rollup-win32-x64-gnu': 4.62.4 + '@rollup/rollup-win32-x64-msvc': 4.62.4 + fsevents: 2.3.3 + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 - scheduler@0.27.0: {} - semver@5.7.2: {} + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + signal-exit@4.1.0: {} slice-ansi@7.1.2: @@ -3292,10 +3844,10 @@ snapshots: transitivePeerDependencies: - debug - soltag@0.0.17(esbuild@0.25.12)(solc@0.8.35)(typescript@6.0.2)(viem@2.47.17(typescript@6.0.2)(zod@4.4.3)): + soltag@0.0.17(esbuild@0.28.1)(rollup@4.62.4)(solc@0.8.35)(typescript@6.0.2)(viem@2.47.17(typescript@6.0.2)(zod@4.4.3))(vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)): dependencies: solc: 0.8.35 - unplugin: 3.3.0(esbuild@0.25.12) + unplugin: 3.3.0(esbuild@0.28.1)(rollup@4.62.4)(vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) viem: 2.47.17(typescript@6.0.2)(zod@4.4.3) optionalDependencies: typescript: 6.0.2 @@ -3310,6 +3862,12 @@ snapshots: - vite - webpack + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@4.2.0: {} + string-argv@0.3.2: {} string-width@7.2.0: @@ -3327,14 +3885,25 @@ snapshots: dependencies: ansi-regex: 6.2.2 + strip-final-newline@4.0.0: {} + strip-json-comments@5.0.3: {} supports-color@10.2.2: {} + tinybench@2.9.0: {} + tinyexec@1.3.0: {} + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + tinypool@2.1.0: {} + tinyrainbow@3.1.1: {} + tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 @@ -3345,6 +3914,13 @@ snapshots: tslib@2.8.1: {} + tsx@4.21.0: + dependencies: + esbuild: 0.27.7 + get-tsconfig: 4.14.0 + optionalDependencies: + fsevents: 2.3.3 + type-fest@4.41.0: {} typedoc@0.28.20(typescript@6.0.2): @@ -3362,24 +3938,22 @@ snapshots: unbash@2.2.0: {} - undici-types@7.24.6: {} + undici-types@7.14.0: {} - unplugin@3.3.0(esbuild@0.25.12): + unicorn-magic@0.3.0: {} + + unplugin@3.3.0(esbuild@0.28.1)(rollup@4.62.4)(vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)): dependencies: '@jridgewell/remapping': 2.3.5 picomatch: 4.0.5 webpack-virtual-modules: 0.6.2 optionalDependencies: - esbuild: 0.25.12 + esbuild: 0.28.1 + rollup: 4.62.4 + vite: 7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0) uri-js-replace@1.0.1: {} - use-sync-external-store@1.6.0(react@19.2.8): - dependencies: - react: 19.2.8 - - uuid@11.1.1: {} - valibot@1.4.2(typescript@6.0.2): optionalDependencies: typescript: 6.0.2 @@ -3418,10 +3992,103 @@ snapshots: - utf-8-validate - zod + vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.8.3): + dependencies: + esbuild: 0.28.1 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + postcss: 8.5.25 + rollup: 4.62.4 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.7.0 + fsevents: 2.3.3 + jiti: 2.7.0 + tsx: 4.21.0 + yaml: 2.8.3 + + vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0): + dependencies: + esbuild: 0.28.1 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + postcss: 8.5.25 + rollup: 4.62.4 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.7.0 + fsevents: 2.3.3 + jiti: 2.7.0 + tsx: 4.21.0 + yaml: 2.9.0 + + vitest@4.1.2(@types/node@24.7.0)(vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.2 + '@vitest/mocker': 4.1.2(vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.2 + '@vitest/runner': 4.1.2 + '@vitest/snapshot': 4.1.2 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + es-module-lexer: 2.1.0 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.7.0 + transitivePeerDependencies: + - msw + + vitest@4.1.2(@types/node@24.7.0)(vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)): + dependencies: + '@vitest/expect': 4.1.2 + '@vitest/mocker': 4.1.2(vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.2 + '@vitest/runner': 4.1.2 + '@vitest/snapshot': 4.1.2 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + es-module-lexer: 2.1.0 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.7.0 + transitivePeerDependencies: + - msw + walk-up-path@4.0.0: {} webpack-virtual-modules@0.6.2: {} + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 @@ -3438,6 +4105,8 @@ snapshots: yargs-parser@21.1.1: {} + yoctocolors@2.2.0: {} + zod@3.25.76: {} zod@4.4.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 868a2bfc..625342d7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -16,17 +16,17 @@ minimumReleaseAge: 4320 minimumReleaseAgeExclude: - '@morpho-org/*' -# No dependency lifecycle script runs unless opted in here — add entries deliberately. esbuild's -# platform binary is required by the root build dependency introduced on main. +# No dependency lifecycle script runs unless opted in here — add entries deliberately. esbuild is +# the only dependency in the tree that needs one; any future addition is a reviewable change. allowBuilds: esbuild: true # Make an un-opted-in build script a hard install failure, not a warning. Without this the # default-deny above is advisory, which defeats the point of the migration. strictDepBuilds: true -# pnpm does not run npm-style pre/post scripts by default. `prestart` is load-bearing here: it -# builds @repo/contracts' gitignored dist/ (and runs generate:api for midnight-crossed-books), so -# the bots cannot resolve their own dependency without it. +# pnpm does not run npm-style pre/post scripts by default. Nothing depends on them today (the bots +# build explicitly, at image-build time), but keep npm-compatible behavior so a future `pre*`/`post*` +# script does not silently never run. enablePrePostScripts: true # Migrating the lockfile re-resolves every caret range, so entries that bun.lock had already @@ -34,17 +34,15 @@ enablePrePostScripts: true # dependency versions, per the TIB's non-goal; relaxing any of these is a separate, deliberate bump. catalog: '@internationalized/date': 3.12.1 - '@tanstack/react-form': 1.20.0 - '@tanstack/react-table': 8.21.3 '@loglayer/transport-betterstack': 2.2.0 '@morpho-org/midnight-sdk': 1.3.0 '@morpho-org/morpho-ts': 2.8.0 '@morpho-org/viem-dlc': 0.0.11 - '@types/bun': 1.3.13 '@types/lodash-es': ^4.17.12 - '@types/react': 19.2.18 - '@types/react-dom': 19.2.4 + '@types/node': 24.7.0 date-fns: 4.1.0 + esbuild: 0.28.1 + execa: 9.6.0 executooor-viem: ^1.3.3 husky: ^9.1.5 knip: ^5.86.0 @@ -53,8 +51,6 @@ catalog: loglayer: 9.3.0 openapi-fetch: 0.17.0 openapi-typescript: ^7.13.0 - react: 19.2.8 - react-dom: 19.2.8 oxfmt: ^0.36.0 # Pinned to what bun.lock resolved for `^1.46.0`. Newer minors change rule behavior and want # oxlint-tsgolint >= 0.24 — bump both together, deliberately, in a separate change. @@ -65,6 +61,9 @@ catalog: # settings (0.8.35, optimizer runs=200). solc: 0.8.35 soltag: ^0.0.17 + tsx: 4.21.0 typescript: 6.0.2 viem: 2.47.17 + vite: 7.3.6 + vitest: 4.1.2 zod: 3.25.76 diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..425721ce --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from 'vitest/config' + +// Workspace runner: every member that carries tests is a project. The two liquidation bots hold +// their own vitest.config.ts (soltag `sol``` transform + fork-suite env files); the rest run with +// defaults rooted at their own directory. +export default defineConfig({ + test: { + projects: [ + 'packages/utils', + 'packages/bot-kit', + 'packages/swaps', + 'packages/logging', + 'packages/monitoring', + 'packages/observability', + 'packages/offers', + 'bots/blue-liquidation', + 'bots/midnight-liquidation', + 'bots/midnight-crossed-books', + 'bots/market-making' + ] + } +}) From 93ce48d64ce4d80a83dea3e8c5b7db00ba615ee2 Mon Sep 17 00:00:00 2001 From: Hayden Shively Date: Wed, 5 Aug 2026 13:41:26 -0400 Subject: [PATCH 02/14] fix(repo): make tryCatch thenable-safe and restore the local build step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review on #130. - tryCatch branched on `fn instanceof Promise`, so an execa subprocess — a thenable that is NOT a native Promise, yet typed `extends Promise` — fell through to the sync path and got *called*. Every deploy would have reported "Railway CLI not found" from assertCli() before reaching Railway. It now branches on callable and assimilates the thenable via Promise.resolve, which also lets the ad-hoc Promise.resolve() wrappers at the other execa call sites go away so all of them read the same. - Restored `prestart` on all four bots. `start` runs `dist/src/index.js`, which a clean checkout does not have; the images build at image-build time and their CMD never fires a pre-script, so this only affects the documented local path. `{.}...` scopes the build to the bot plus its workspace deps. - market-making declares `vite` (its vitest.config.ts imports loadEnv) instead of relying on root hoisting, matching the two liquidation bots. - Registered scripts/bundle-failed.error.ts in check-jsdoc.ts, typedoc.json, and the build-jsdoc skill inventory. - .claude/commands: babysit-pr's validate block runs `pnpm test`, review.md's stack list names vitest/esbuild/Node. (CLAUDE.md's remaining "bun" is a verbatim past commit title used as a format example, so it stays.) - Declaring vite in market-making shifted knip's peer attribution and exposed root `tsx` as unused. It is not: `node --import tsx` and the CLI subprocess tests resolve it from the root, and dropping it fails two market-making tests. knip now ignores it explicitly, with the reason recorded at the usage. Verified: 1417 pass / 1 skip / 136 files (fork suites included, RPC_URL_8453 supplied); the two new tryCatch tests reproduce `fn is not a function` against the old implementation; a real execa call through tryCatch now succeeds for a present binary and still errors for a missing one; all four bots build from wiped dists via prestart and reach their fail-loud config check; all three images build, run as uid=1000(node), carry no bun binary, and reach that same check; pnpm lint 0/0, knip clean, 12/12 typecheck, jsdoc:build exits 0. Co-Authored-By: Claude Opus 5 (1M context) --- .agents/skills/build-jsdoc/SKILL.md | 62 ++++++++++++++++--- .claude/commands/babysit-pr.md | 2 +- .claude/commands/review.md | 2 +- bots/blue-liquidation/README.md | 5 +- bots/blue-liquidation/package.json | 1 + .../scripts/deploy-railway.ts | 18 ++---- bots/market-making/package.json | 2 + bots/market-making/scripts/check-jsdoc.ts | 1 + .../test/config/config-loading.test.ts | 6 +- bots/market-making/typedoc.json | 6 +- bots/midnight-crossed-books/package.json | 1 + .../scripts/deploy-railway.ts | 12 ++-- bots/midnight-liquidation/package.json | 1 + .../scripts/deploy-railway.ts | 8 +-- .../TIB-2026-07-20-migrate-to-pnpm.md | 8 +++ knip.json | 4 +- packages/utils/src/helpers/tryCatch.ts | 21 ++++--- packages/utils/test/helpers/tryCatch.test.ts | 36 +++++++++++ pnpm-lock.yaml | 3 + 19 files changed, 146 insertions(+), 53 deletions(-) diff --git a/.agents/skills/build-jsdoc/SKILL.md b/.agents/skills/build-jsdoc/SKILL.md index 07f48b85..ec89406a 100644 --- a/.agents/skills/build-jsdoc/SKILL.md +++ b/.agents/skills/build-jsdoc/SKILL.md @@ -68,13 +68,59 @@ Completion criterion: the AST inventory exits zero, TypeDoc emits zero warnings/ ## Public-Surface Inventory -The coverage script dynamically discovers TypeScript files under `src/**` and `scripts/**` in -deterministic package-relative order. It excludes the executable `src/index.ts` entrypoint and test -files; generated output and dependencies are outside the scanned roots. Newly added source files -therefore enter the JSDoc check without a manually maintained path list. +The coverage script inventories these market-making boundary and utility files: + +- `src/application/operator-error-name.utils.ts` +- `src/application/bootstrap/position-bootstrap-halted.error.ts` +- `src/application/bootstrap/position-bootstrap.service.ts` +- `src/application/ladder/ladder-cycle-halted.error.ts` +- `src/application/ladder/ladder-market-maker.service.ts` +- `src/application/ladder/ladder-market-maker.utils.ts` +- `src/application/setup/setup-check.service.ts` +- `src/application/setup/setup-check.utils.ts` +- `src/application/setup/safe-provider.error.ts` +- `src/application/setup/setup-failed.error.ts` +- `src/application/version.service.ts` +- `src/bootstrap.ts` +- `src/config/config-file.error.ts` +- `src/config/config-source.utils.ts` +- `src/config/config-validation.error.ts` +- `src/config/config.service.ts` +- `src/config/config.utils.ts` +- `src/domain/bootstrap/bootstrap-configuration.error.ts` +- `src/domain/bootstrap/position-bootstrap.ts` +- `src/domain/ladder/ladder-configuration.error.ts` +- `src/domain/ladder/ladder.ts` +- `src/infrastructure/bootstrap/bootstrap-hard-halt.error.ts` +- `src/infrastructure/bootstrap/bootstrap-exposure.utils.ts` +- `src/infrastructure/bootstrap/bootstrap-make.service.ts` +- `src/infrastructure/bootstrap/bootstrap-offer.utils.ts` +- `src/infrastructure/bootstrap/bootstrap-adapter.error.ts` +- `src/infrastructure/bootstrap/bootstrap-group-ownership.utils.ts` +- `src/infrastructure/bootstrap/bootstrap-groups.utils.ts` +- `src/infrastructure/bootstrap/bootstrap-position.service.ts` +- `src/infrastructure/bootstrap/bootstrap-reference-rate.service.ts` +- `src/infrastructure/bootstrap/bootstrap-requirements.utils.ts` +- `src/infrastructure/bootstrap/bootstrap-transaction.utils.ts` +- `src/infrastructure/bootstrap/production-bootstrap.ts` +- `src/infrastructure/cli/cli-usage.error.ts` +- `src/infrastructure/cli/cli.ts` +- `src/infrastructure/cli/market-making-entrypoint.ts` +- `src/infrastructure/make/read-only-bootstrap-make.service.ts` +- `src/infrastructure/make/read-only-ladder-make.service.ts` +- `src/infrastructure/make/read-only-make.utils.ts` +- `src/infrastructure/setup-state/http-json.utils.ts` +- `src/infrastructure/setup-state/provider-pagination.error.ts` +- `src/infrastructure/setup-state/provider-read.error.ts` +- `src/infrastructure/setup-state/provider-read.utils.ts` +- `src/infrastructure/setup-state/provider-response.error.ts` +- `src/infrastructure/setup-state/viem-setup-state.service.ts` +- `src/infrastructure/setup-state/viem-setup-state.utils.ts` +- `scripts/js-doc-validation.error.ts` +- `scripts/bundle-failed.error.ts` It checks exported functions/classes/interfaces/type aliases, interface methods, callable members of -exported type literals, and public constructors/methods/accessors. Every discovered declaration needs a +exported type literals, and public constructors/methods/accessors. Every listed declaration needs a substantive `/** ... */` block. The checker requires a non-filler summary, exact `@param` names, `@returns` for non-void callables, and `@throws` at its provider-boundary rule. Scoped rules also enforce read-only, aggregate-deadline, and `Promise.all` concurrency semantics. Do not satisfy @@ -109,9 +155,9 @@ For the current market-making implementation, preserve these verified package bo Completion criterion: package source/exports still support each imported symbol, the active-offer source remains complete, and every local exception has a concrete missing-SDK-export justification. -When adding another boundary file, confirm the dynamic check discovers it and add it to -`typedoc.json` in the same change. Completion criterion: the command's printed inventory contains -every added public callable exactly once. +When adding another boundary file, add it to both `scripts/check-jsdoc.ts` and `typedoc.json` in the +same change. Completion criterion: the command's printed inventory contains every added public +callable exactly once. ## Inspect Generated Docs diff --git a/.claude/commands/babysit-pr.md b/.claude/commands/babysit-pr.md index 82239eaa..9605dd27 100644 --- a/.claude/commands/babysit-pr.md +++ b/.claude/commands/babysit-pr.md @@ -440,7 +440,7 @@ After all comments are addressed, run validation on affected packages: ```bash pnpm --filter run typecheck pnpm lint -bun test +pnpm test pnpm format ``` diff --git a/.claude/commands/review.md b/.claude/commands/review.md index 706b9f3a..17bc14ef 100644 --- a/.claude/commands/review.md +++ b/.claude/commands/review.md @@ -194,7 +194,7 @@ Review the provided PR thoroughly and interactively guide the user through each `--sandbox danger-full-access`, `git push --force`)? Flag any escalation of tool access or sandbox permissions. - **Evaluation completeness**: For review-type commands or checklists, are there gaps in - coverage given the codebase's stack (viem, multi-chain, pnpm, bun, oxlint)? + coverage given the codebase's stack (viem, multi-chain, pnpm, vitest, esbuild, Node, oxlint)? - **Cross-reference accuracy**: If the file references other files (TIBs, CONVENTIONS.md sections, other commands), verify those references are valid and up to date. diff --git a/bots/blue-liquidation/README.md b/bots/blue-liquidation/README.md index 9a0e788e..e2f75037 100644 --- a/bots/blue-liquidation/README.md +++ b/bots/blue-liquidation/README.md @@ -89,8 +89,9 @@ export ZEROX_API_KEY=… # or ENABLE_LIFI=true, or ALLOW_DETECTION_ON pnpm --filter @morpho-org/blue-liquidation run start ``` -`prestart` builds the workspace packages (soltag-compiles `@repo/contracts` and materializes the ABI). -Discovery hits the public Morpho GraphQL API — no indexer or database to run. +`prestart` builds this bot and its workspace dependencies (soltag-compiles `@repo/contracts` and +materializes the ABI, then esbuild-bundles `dist/`), so `start` runs a plain `node` against a +freshly built bundle. Discovery hits the public Morpho GraphQL API — no indexer or database to run. ## Running With Docker Compose diff --git a/bots/blue-liquidation/package.json b/bots/blue-liquidation/package.json index c33bb8e0..6ba6e135 100644 --- a/bots/blue-liquidation/package.json +++ b/bots/blue-liquidation/package.json @@ -7,6 +7,7 @@ "scripts": { "build": "tsx scripts/build.ts", "deploy:railway": "tsx scripts/deploy-railway.ts", + "prestart": "pnpm --filter \"{.}...\" --if-present run build", "probe:lens": "node --env-file-if-exists=.env dist/scripts/probe-live-lens.js", "start": "node --env-file-if-exists=.env dist/src/index.js", "typecheck": "soltag && tsc --noEmit" diff --git a/bots/blue-liquidation/scripts/deploy-railway.ts b/bots/blue-liquidation/scripts/deploy-railway.ts index a67a9cab..e861da44 100644 --- a/bots/blue-liquidation/scripts/deploy-railway.ts +++ b/bots/blue-liquidation/scripts/deploy-railway.ts @@ -164,9 +164,7 @@ async function removeService(name: string): Promise { if (!(await listServices()).some(service => service.name === name)) return console.log(`Removing legacy service ${name}…`) const { error } = await tryCatch( - Promise.resolve( - $`railway service delete --service ${name} --environment ${ENVIRONMENT} --yes --json` - ) + $`railway service delete --service ${name} --environment ${ENVIRONMENT} --yes --json` ) if (error) console.warn( @@ -188,10 +186,8 @@ async function setVar(service: string, kv: string): Promise { // CLI's JSON includes raw values, so it is parsed in-memory and only key NAMES ever leave here. async function listVarKeys(service: string): Promise> { const { data, error } = await tryCatch( - Promise.resolve( - $`railway variable list -s ${service} -e ${ENVIRONMENT} -p ${PROJECT_ID} --json`.then( - r => r.stdout - ) + $`railway variable list -s ${service} -e ${ENVIRONMENT} -p ${PROJECT_ID} --json`.then( + r => r.stdout ) ) if (error || typeof data !== 'string') return new Set() @@ -210,9 +206,7 @@ async function deleteVar(service: string, key: string): Promise { // Secret variable: value piped via stdin (never argv), `--json` omitted (it echoes raw values). async function setSecret(service: string, key: string, value: string): Promise { const { error } = await tryCatch( - Promise.resolve( - $({ input: value })`railway variable set ${key} --stdin -s ${service} --skip-deploys` - ) + $({ input: value })`railway variable set ${key} --stdin -s ${service} --skip-deploys` ) if (error) throw new Error(`Failed to set ${key} on ${service}`) console.log(`Set ${key} on ${service} (secret).`) @@ -223,9 +217,7 @@ async function deployService(service: string): Promise { // Pass -p/-e explicitly: `railway link` doesn't reliably carry the environment into this non-TTY // subprocess, so `railway up` otherwise errors "No environment specified". Self-contained > ambient. const { error } = await tryCatch( - Promise.resolve( - $({ cwd: REPO_ROOT })`railway up -s ${service} -e ${ENVIRONMENT} -p ${PROJECT_ID} -d` - ) + $({ cwd: REPO_ROOT })`railway up -s ${service} -e ${ENVIRONMENT} -p ${PROJECT_ID} -d` ) if (error) throw new Error(`Failed to start deploy for ${service}: ${stderrOf(error)}`) } diff --git a/bots/market-making/package.json b/bots/market-making/package.json index 627deba2..f73578b3 100644 --- a/bots/market-making/package.json +++ b/bots/market-making/package.json @@ -11,6 +11,7 @@ "build": "tsx scripts/build.ts", "jsdoc:build": "pnpm run jsdoc:check && typedoc --options typedoc.json", "jsdoc:check": "tsx scripts/check-jsdoc.ts", + "prestart": "pnpm --filter \"{.}...\" --if-present run build", "start": "node --env-file-if-exists=.env dist/src/index.js", "test:e2e": "vitest run test/e2e", "typecheck": "tsc --noEmit" @@ -36,6 +37,7 @@ "tsx": "catalog:", "typedoc": "0.28.20", "typescript": "catalog:", + "vite": "catalog:", "vitest": "catalog:" } } diff --git a/bots/market-making/scripts/check-jsdoc.ts b/bots/market-making/scripts/check-jsdoc.ts index d3a97ef4..fc671b64 100644 --- a/bots/market-making/scripts/check-jsdoc.ts +++ b/bots/market-making/scripts/check-jsdoc.ts @@ -388,6 +388,7 @@ const sourceFiles = [ 'infrastructure/setup-state/viem-setup-state.utils.ts' ].map(path => resolve(sourceRoot, path)) sourceFiles.push(resolve(packageRoot, 'scripts/js-doc-validation.error.ts')) +sourceFiles.push(resolve(packageRoot, 'scripts/bundle-failed.error.ts')) sourceFiles.push(resolve(packageRoot, 'scripts/check-jsdoc.ts')) const run = async () => { diff --git a/bots/market-making/test/config/config-loading.test.ts b/bots/market-making/test/config/config-loading.test.ts index 7502d0c5..27cc9220 100644 --- a/bots/market-making/test/config/config-loading.test.ts +++ b/bots/market-making/test/config/config-loading.test.ts @@ -57,8 +57,10 @@ const loadInBoundedSubprocess = async (options: { configPath?: string; cwd: stri console.log(JSON.stringify({ reason: error?.reason ?? 'unexpected' })) } ` - // `node -e` cannot import TypeScript, so tsx is registered as a loader for the inline module, and - // execa's own timeout enforces the bound instead of a manual kill. The bound is what this suite is + // `node -e` cannot import TypeScript, so tsx is registered as a loader for the inline module. That + // specifier resolves from the workspace root, which is why `tsx` stays a root devDependency (knip + // cannot see a usage inside `--import`, so the root workspace ignores it explicitly). execa's own + // timeout enforces the bound instead of a manual kill. The bound is what this suite is // really asserting: reading a FIFO would block forever, so any finite completion proves the loader // fails closed. It is 10s rather than the 1s bun used because a tsx cold start alone costs ~1.3s — // well under the bound, but not under one sized for bun's interpreter startup. diff --git a/bots/market-making/typedoc.json b/bots/market-making/typedoc.json index 7ca6de67..244a3a3c 100644 --- a/bots/market-making/typedoc.json +++ b/bots/market-making/typedoc.json @@ -31,10 +31,8 @@ "src/config/config-validation.error.ts", "src/config/config.service.ts", "src/config/config.utils.ts", - "src/config/market-collections.ts", "src/domain/bootstrap/bootstrap-configuration.error.ts", "src/domain/bootstrap/position-bootstrap.ts", - "src/domain/bytes32.ts", "src/domain/ladder/ladder-configuration.error.ts", "src/domain/ladder/ladder.ts", "src/infrastructure/bootstrap/bootstrap-hard-halt.error.ts", @@ -64,6 +62,7 @@ "src/infrastructure/invalidation/production-offer-invalidation.ts", "src/infrastructure/ladder/ladder-adapter.error.ts", "src/infrastructure/ladder/ladder-active-publication.utils.ts", + "src/infrastructure/ladder/ladder-bootstrap-offer.utils.ts", "src/infrastructure/ladder/ladder-cash-reservation.utils.ts", "src/infrastructure/ladder/ladder-group-ownership.utils.ts", "src/infrastructure/ladder/ladder-hard-halt.error.ts", @@ -89,8 +88,7 @@ "src/infrastructure/reference/blue-reference-reader.utils.ts", "src/infrastructure/reference/reference-adapter.error.ts", "scripts/js-doc-validation.error.ts", - "scripts/railway-deployment.error.ts", - "scripts/railway.utils.ts" + "scripts/bundle-failed.error.ts" ], "tsconfig": "tsconfig.json", "out": "build/jsdoc", diff --git a/bots/midnight-crossed-books/package.json b/bots/midnight-crossed-books/package.json index ea4149cb..4fb5ca68 100644 --- a/bots/midnight-crossed-books/package.json +++ b/bots/midnight-crossed-books/package.json @@ -8,6 +8,7 @@ "build": "pnpm run generate:api && tsx scripts/build.ts", "deploy:railway": "tsx scripts/deploy-railway.ts", "generate:api": "openapi-typescript morpho-api.json -o src/infrastructure/morpho-api/generated/morpho-api.types.ts && openapi-typescript router-api.json -o src/infrastructure/router-api/generated/router-api.types.ts", + "prestart": "pnpm --filter \"{.}...\" --if-present run build", "start": "node --env-file-if-exists=.env dist/src/index.js", "typecheck": "pnpm run generate:api && tsc --noEmit" }, diff --git a/bots/midnight-crossed-books/scripts/deploy-railway.ts b/bots/midnight-crossed-books/scripts/deploy-railway.ts index 64b40b0a..eb7760ea 100644 --- a/bots/midnight-crossed-books/scripts/deploy-railway.ts +++ b/bots/midnight-crossed-books/scripts/deploy-railway.ts @@ -105,9 +105,7 @@ async function setVariable(value: string) { async function setSecret(name: string, value: string) { const { error } = await tryCatch( - Promise.resolve( - $({ input: value })`railway variable set ${name} --stdin -s ${SERVICE} --skip-deploys` - ) + $({ input: value })`railway variable set ${name} --stdin -s ${SERVICE} --skip-deploys` ) if (error) throw new Error(`Failed to set ${name} on ${SERVICE}`) console.log(`Set ${name} on ${SERVICE} (secret).`) @@ -116,11 +114,9 @@ async function setSecret(name: string, value: string) { async function deployService() { const message = `deploy midnight crossed-books ${ENVIRONMENT}` const { error } = await tryCatch( - Promise.resolve( - $({ - cwd: REPO_ROOT - })`railway up -s ${SERVICE} -p ${PROJECT_ID} -e ${ENVIRONMENT} -d -m ${message}` - ) + $({ + cwd: REPO_ROOT + })`railway up -s ${SERVICE} -p ${PROJECT_ID} -e ${ENVIRONMENT} -d -m ${message}` ) if (error) throw new Error(`Failed to start deploy for ${SERVICE}: ${errorDetails(error)}`) } diff --git a/bots/midnight-liquidation/package.json b/bots/midnight-liquidation/package.json index 3e11f08b..02d7a7df 100644 --- a/bots/midnight-liquidation/package.json +++ b/bots/midnight-liquidation/package.json @@ -7,6 +7,7 @@ "scripts": { "build": "tsx scripts/build.ts", "deploy:railway": "tsx scripts/deploy-railway.ts", + "prestart": "pnpm --filter \"{.}...\" --if-present run build", "seed:positions": "node --env-file-if-exists=.env dist/scripts/seed-liquidatable-positions.js", "start": "node --env-file-if-exists=.env dist/src/index.js", "typecheck": "soltag && tsc --noEmit" diff --git a/bots/midnight-liquidation/scripts/deploy-railway.ts b/bots/midnight-liquidation/scripts/deploy-railway.ts index 97dbef8d..e16b58be 100644 --- a/bots/midnight-liquidation/scripts/deploy-railway.ts +++ b/bots/midnight-liquidation/scripts/deploy-railway.ts @@ -156,9 +156,7 @@ async function setVar(service: string, kv: string): Promise { // Secret variable: value piped via stdin (never argv), `--json` omitted (it echoes raw values). async function setSecret(service: string, key: string, value: string): Promise { const { error } = await tryCatch( - Promise.resolve( - $({ input: value })`railway variable set ${key} --stdin -s ${service} --skip-deploys` - ) + $({ input: value })`railway variable set ${key} --stdin -s ${service} --skip-deploys` ) if (error) throw new Error(`Failed to set ${key} on ${service}`) console.log(`Set ${key} on ${service} (secret).`) @@ -172,9 +170,7 @@ async function deployService(service: string): Promise { // sibling bot's project after its last deploy, which fails with "No environment specified" or, worse, // targets the wrong project. Scope the deploy explicitly so it never depends on ambient link state. const { error } = await tryCatch( - Promise.resolve( - $({ cwd: REPO_ROOT })`railway up -s ${service} -p ${PROJECT_ID} -e ${ENVIRONMENT} -d` - ) + $({ cwd: REPO_ROOT })`railway up -s ${service} -p ${PROJECT_ID} -e ${ENVIRONMENT} -d` ) if (error) throw new Error(`Failed to start deploy for ${service}: ${stderrOf(error)}`) } diff --git a/docs/decisions/TIB-2026-07-20-migrate-to-pnpm.md b/docs/decisions/TIB-2026-07-20-migrate-to-pnpm.md index df94597f..22b50d4a 100644 --- a/docs/decisions/TIB-2026-07-20-migrate-to-pnpm.md +++ b/docs/decisions/TIB-2026-07-20-migrate-to-pnpm.md @@ -181,6 +181,14 @@ pre/post scripts by default. `prestart` is load-bearing: it builds `@repo/contra `dist/` for the liquidators and runs `generate:api` for `midnight-crossed-books`. Without this setting the bots cannot resolve their own workspace dependency at container start. +**`prestart` survives PR 2, but only for the local path.** PR 2 moves the build to image-build time +(`RUN pnpm -r --if-present run build`) and its `CMD` is a bare `node dist/src/index.js`, which never +fires a pre-script — so the container builds exactly once and does not rebuild at start. An operator +running the documented `pnpm --filter run start` from a clean checkout has no `dist/` at all, +though, so each bot keeps `prestart: pnpm --filter "{.}..." --if-present run build` — the `{.}...` +selector scopes it to that bot plus its workspace dependencies, so one bot's start does not build the +other three. + **PR 1's Docker intermediate state uses a Node base with a copied bun binary.** Keeping `FROM oven/bun` was not viable: pnpm is activated through corepack, which those images do not provide, and `bun install --frozen-lockfile` is not a fallback once `bun.lock` is deleted. Both pnpm diff --git a/knip.json b/knip.json index b0580dc1..52448463 100644 --- a/knip.json +++ b/knip.json @@ -4,7 +4,9 @@ "ignore": [".claude/**"], "ignoreBinaries": ["forge", "mkfifo", "railway"], "workspaces": { - ".": {}, + ".": { + "ignoreDependencies": ["tsx"] + }, "packages/typescript-config": {}, "packages/utils": {}, "packages/contracts": {}, diff --git a/packages/utils/src/helpers/tryCatch.ts b/packages/utils/src/helpers/tryCatch.ts index 3452b0a9..c82c4db1 100644 --- a/packages/utils/src/helpers/tryCatch.ts +++ b/packages/utils/src/helpers/tryCatch.ts @@ -14,13 +14,20 @@ export function tryCatch(fn: Promise | (() => T)) return { data: null, error: error as E } } - if (fn instanceof Promise) { - return fn.then(data => ({ data, error: null })).catch(formatError) + // Branch on callable, not on `instanceof Promise`: a thenable that is not a native Promise — most + // notably execa's subprocess, which its own typings declare as `extends Promise` — would + // otherwise fall through to the sync path and be *called*, turning every such await into a + // `fn is not a function` failure that reads like the awaited operation itself failed. + // `Promise.resolve` assimilates a thenable and passes a real Promise straight through. + if (typeof fn === 'function') { + try { + return { data: fn(), error: null } + } catch (e) { + return formatError(e) + } } - try { - return { data: fn(), error: null } - } catch (e) { - return formatError(e) - } + return Promise.resolve(fn) + .then(data => ({ data, error: null })) + .catch(formatError) } diff --git a/packages/utils/test/helpers/tryCatch.test.ts b/packages/utils/test/helpers/tryCatch.test.ts index 19ed825c..9e5d143b 100644 --- a/packages/utils/test/helpers/tryCatch.test.ts +++ b/packages/utils/test/helpers/tryCatch.test.ts @@ -139,6 +139,42 @@ describe('tryCatch', () => { }) }) + // The deploy scripts pass execa subprocesses here. Those are thenables that are NOT native + // Promises, yet execa's typings declare them `extends Promise` — so a check of + // `fn instanceof Promise` type-checks fine and then takes the sync path at runtime, calling the + // subprocess object and reporting `fn is not a function` as if the command itself had failed. + describe('thenable (non-Promise) form', () => { + const thenable = (value: T) => ({ + // oxlint-disable-next-line unicorn/no-thenable -- a non-Promise thenable IS the subject here + then: (resolve: (value: T) => void) => { + resolve(value) + } + }) + const rejecting = (reason: unknown) => ({ + // oxlint-disable-next-line unicorn/no-thenable -- as above; stands in for execa's subprocess + then: (_resolve: (value: never) => void, reject: (reason: unknown) => void) => { + reject(reason) + } + }) + + it('should resolve a thenable that is not a Promise instance', async () => { + const subject = thenable(42) + expect(subject).not.toBeInstanceOf(Promise) + + const result = await tryCatch(subject as unknown as Promise) + + expect(result).toEqual({ data: 42, error: null }) + }) + + it('should capture a thenable rejection as an error rather than calling it', async () => { + const result = await tryCatch(rejecting(new Error('boom')) as unknown as Promise) + + expect(result.data).toBeNull() + expect(result.error?.message).toBe('boom') + expect(result.error?.message).not.toContain('is not a function') + }) + }) + describe('custom error types', () => { class CustomError extends Error { code: number diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a24be7d..334be90b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -244,6 +244,9 @@ importers: typescript: specifier: 'catalog:' version: 6.0.2 + vite: + specifier: 'catalog:' + version: 7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' version: 4.1.2(@types/node@24.7.0)(vite@7.3.6(@types/node@24.7.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.8.3)) From 3eeb3e8e7ecd4602ec0cc4e1fe1fc5f5cc264386 Mon Sep 17 00:00:00 2001 From: Hayden Shively Date: Wed, 5 Aug 2026 14:16:24 -0400 Subject: [PATCH 03/14] refactor(market-making): port main's new test files off bun:test main gained #121/#124/#125 after this branch's merge-base, adding test files that still import bun:test and call bun's mock(). Linearizing the stack dropped these conversions along with the merge commit that carried them, so they are restored here as their own commit: 7 files moved to vitest imports and mock() -> vi.fn(). Tree is byte-identical to the verified pre-rebase state. Co-Authored-By: Claude Opus 5 (1M context) --- .../bootstrap/position-bootstrap.service.test.ts | 2 +- .../application/ladder/ladder-market-maker.service.test.ts | 2 +- .../bootstrap/bootstrap-requirement-client.utils.test.ts | 2 +- .../batch-offer-invalidation-transaction.utils.test.ts | 2 +- .../invalidation/batch-offer-invalidation.utils.test.ts | 6 +++--- .../infrastructure/ladder/ladder-ratification.utils.test.ts | 2 +- .../infrastructure/ladder/ladder-transaction.utils.test.ts | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/bots/market-making/test/application/bootstrap/position-bootstrap.service.test.ts b/bots/market-making/test/application/bootstrap/position-bootstrap.service.test.ts index 4ea51cd3..1ec24553 100644 --- a/bots/market-making/test/application/bootstrap/position-bootstrap.service.test.ts +++ b/bots/market-making/test/application/bootstrap/position-bootstrap.service.test.ts @@ -456,7 +456,7 @@ describe('PositionBootstrapService', () => { test('retains a confirmed ratification hash when publication later fails', async () => { const { service, make } = setup() - make.reconcile = mock(async () => { + make.reconcile = vi.fn(async () => { throw new BootstrapAdapterError( 'publication-transaction-reverted-after-ratification' ).recordConfirmedTransactions([{ operation: 'ratify', txHash: ratificationHash }]) diff --git a/bots/market-making/test/application/ladder/ladder-market-maker.service.test.ts b/bots/market-making/test/application/ladder/ladder-market-maker.service.test.ts index 7bf5ee5b..4b203b6a 100644 --- a/bots/market-making/test/application/ladder/ladder-market-maker.service.test.ts +++ b/bots/market-making/test/application/ladder/ladder-market-maker.service.test.ts @@ -371,7 +371,7 @@ describe('LadderMarketMakerService', () => { test('retains a confirmed ratification hash when publication later fails', async () => { const subject = harness() - subject.make.reconcile = mock(async () => { + subject.make.reconcile = vi.fn(async () => { throw new LadderAdapterError( 'publication-transaction-reverted-after-ratification' ).recordConfirmedTransactions([{ operation: 'ratify', txHash: ratificationHash }]) diff --git a/bots/market-making/test/infrastructure/bootstrap/bootstrap-requirement-client.utils.test.ts b/bots/market-making/test/infrastructure/bootstrap/bootstrap-requirement-client.utils.test.ts index 66fa4dab..85381394 100644 --- a/bots/market-making/test/infrastructure/bootstrap/bootstrap-requirement-client.utils.test.ts +++ b/bots/market-making/test/infrastructure/bootstrap/bootstrap-requirement-client.utils.test.ts @@ -1,10 +1,10 @@ import type { Address } from 'viem' import { EcrecoverRatifierUtils, Offer, Tree } from '@morpho-org/midnight-sdk' -import { describe, expect, test } from 'bun:test' import { isHex, zeroAddress } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { base } from 'viem/chains' +import { describe, expect, test } from 'vitest' import { createBootstrapRequirementClient } from '../../../src/infrastructure/bootstrap/bootstrap-requirement-client.utils' diff --git a/bots/market-making/test/infrastructure/invalidation/batch-offer-invalidation-transaction.utils.test.ts b/bots/market-making/test/infrastructure/invalidation/batch-offer-invalidation-transaction.utils.test.ts index a2a50c6f..8f029329 100644 --- a/bots/market-making/test/infrastructure/invalidation/batch-offer-invalidation-transaction.utils.test.ts +++ b/bots/market-making/test/infrastructure/invalidation/batch-offer-invalidation-transaction.utils.test.ts @@ -1,8 +1,8 @@ import type { Address, Hex } from 'viem' import { MAX_OFFER_CAP, midnightAbi } from '@morpho-org/midnight-sdk' -import { describe, expect, test } from 'bun:test' import { encodeFunctionData } from 'viem' +import { describe, expect, test } from 'vitest' import { OfferInvalidationAdapterError } from '../../../src/infrastructure/invalidation/offer-invalidation-adapter.error' import { assertBatchOfferInvalidationTransaction } from '../../../src/infrastructure/invalidation/offer-invalidation-transaction.utils' diff --git a/bots/market-making/test/infrastructure/invalidation/batch-offer-invalidation.utils.test.ts b/bots/market-making/test/infrastructure/invalidation/batch-offer-invalidation.utils.test.ts index e854f64a..0950f3f2 100644 --- a/bots/market-making/test/infrastructure/invalidation/batch-offer-invalidation.utils.test.ts +++ b/bots/market-making/test/infrastructure/invalidation/batch-offer-invalidation.utils.test.ts @@ -1,8 +1,8 @@ import type { Address, Hex } from 'viem' import { MAX_OFFER_CAP, midnightAbi } from '@morpho-org/midnight-sdk' -import { describe, expect, mock, test } from 'bun:test' import { decodeFunctionData } from 'viem' +import { describe, expect, test, vi } from 'vitest' import { invalidateOffersBatch } from '../../../src/infrastructure/invalidation/batch-offer-invalidation.utils' import { OfferInvalidationAdapterError } from '../../../src/infrastructure/invalidation/offer-invalidation-adapter.error' @@ -14,10 +14,10 @@ const groupIds = [`0x${'44'.repeat(32)}`, `0x${'55'.repeat(32)}`] as const const txHash: Hex = `0x${'aa'.repeat(32)}` const subject = (status = 'success', account = maker) => { - const sendTransaction = mock( + const sendTransaction = vi.fn( async (_transaction: { to: Address; data: Hex; value: bigint }) => txHash ) - const waitForTransactionReceipt = mock(async () => ({ status })) + const waitForTransactionReceipt = vi.fn(async () => ({ status })) return { wallet: { account: { address: account }, sendTransaction, waitForTransactionReceipt }, sendTransaction, diff --git a/bots/market-making/test/infrastructure/ladder/ladder-ratification.utils.test.ts b/bots/market-making/test/infrastructure/ladder/ladder-ratification.utils.test.ts index 5f6e406d..c96c15fd 100644 --- a/bots/market-making/test/infrastructure/ladder/ladder-ratification.utils.test.ts +++ b/bots/market-making/test/infrastructure/ladder/ladder-ratification.utils.test.ts @@ -1,10 +1,10 @@ import type { Address } from 'viem' import { Offer, SetterRatifierUtils, Tree, setterRatifierAbi } from '@morpho-org/midnight-sdk' -import { describe, expect, test } from 'bun:test' import { createWalletClient, custom, decodeFunctionData, zeroAddress } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { base } from 'viem/chains' +import { describe, expect, test } from 'vitest' import { configuredRatifierType, diff --git a/bots/market-making/test/infrastructure/ladder/ladder-transaction.utils.test.ts b/bots/market-making/test/infrastructure/ladder/ladder-transaction.utils.test.ts index 92164920..ba02eaaa 100644 --- a/bots/market-making/test/infrastructure/ladder/ladder-transaction.utils.test.ts +++ b/bots/market-making/test/infrastructure/ladder/ladder-transaction.utils.test.ts @@ -7,8 +7,8 @@ import { Tree, setterRatifierAbi } from '@morpho-org/midnight-sdk' -import { describe, expect, test } from 'bun:test' import { encodeFunctionData } from 'viem' +import { describe, expect, test } from 'vitest' import { assertLadderPublicationTransaction, From 96d901fc89f6e7242ebe7823bb2e6808e7443ffc Mon Sep 17 00:00:00 2001 From: Hayden Shively Date: Fri, 7 Aug 2026 11:35:44 -0500 Subject: [PATCH 04/14] fix(repo): reconcile migration with main --- bots/market-making/package.json | 18 + bots/market-making/playground/model.ts | 12 +- bots/market-making/scripts/check-jsdoc.ts | 16 + bots/market-making/scripts/deploy-railway.ts | 66 +-- .../src/config/config.service.ts | 9 +- .../position-bootstrap.service.test.ts | 2 +- .../test/config/key-storage.test.ts | 15 +- bots/market-making/test/e2e/setup-api.ts | 4 +- .../bootstrap-pending-offer.utils.test.ts | 17 +- .../cli/key-storage-cli.test.ts | 29 +- ...et-making-entrypoint-observability.test.ts | 4 +- .../cli/password-prompt.test.ts | 4 +- .../infrastructure/make/maker-account.test.ts | 12 +- .../test/playground/artifact.test.ts | 25 +- .../test/playground/model.test.ts | 11 +- .../test/playground/module-graph.test.ts | 7 +- .../test/playground/pages-workflow.test.ts | 2 +- .../playground/playground-error.utils.test.ts | 2 +- .../test/playground/react-contract.test.ts | 2 +- .../shared-market-collections.test.ts | 9 +- .../test/playground/tsconfig-coverage.test.ts | 7 +- .../test/scripts/railway.utils.test.ts | 9 +- bots/market-making/tsconfig.json | 8 +- bots/market-making/typedoc.json | 1 - knip.json | 3 +- packages/bot-kit/test/heartbeat.test.ts | 12 +- packages/bot-kit/test/logger.test.ts | 2 +- pnpm-lock.yaml | 540 ++++++++++++++++++ pnpm-workspace.yaml | 6 + 29 files changed, 711 insertions(+), 143 deletions(-) diff --git a/bots/market-making/package.json b/bots/market-making/package.json index f73578b3..174d69ef 100644 --- a/bots/market-making/package.json +++ b/bots/market-making/package.json @@ -9,29 +9,47 @@ "type": "module", "scripts": { "build": "tsx scripts/build.ts", + "deploy:railway": "tsx scripts/deploy-railway.ts", "jsdoc:build": "pnpm run jsdoc:check && typedoc --options typedoc.json", "jsdoc:check": "tsx scripts/check-jsdoc.ts", "prestart": "pnpm --filter \"{.}...\" --if-present run build", + "playground:build": "node scripts/playground-build.mjs", + "playground:serve": "node scripts/playground-serve.mjs", + "playground:serve:test": "node --test scripts/playground-atomic-publish.test.mjs scripts/playground-build.test.mjs scripts/playground-process.test.mjs scripts/playground-serve.test.mjs", + "playground:smoke": "node scripts/playground-smoke.mjs", + "playground:smoke:output-probe": "node scripts/playground-smoke-output-probe.mjs", + "playground:smoke:test": "node --test scripts/playground-smoke.browser.mjs", "start": "node --env-file-if-exists=.env dist/src/index.js", "test:e2e": "vitest run test/e2e", "typecheck": "tsc --noEmit" }, "dependencies": { + "@aws-sdk/client-kms": "3.1101.0", + "@ethereumjs/wallet": "^10.0.0", "@morpho-org/midnight-sdk": "catalog:", "@morpho-org/morpho-sdk": "5.4.1", "@morpho-org/morpho-ts": "catalog:", + "@noble/curves": "1.9.1", + "@repo/bot-kit": "workspace:*", "@repo/logging": "workspace:*", "@repo/monitoring": "workspace:*", "@repo/observability": "workspace:*", "@repo/offers": "workspace:*", "@repo/utils": "workspace:*", + "@tanstack/react-form": "catalog:", + "@tanstack/react-table": "catalog:", + "asn1js": "3.0.6", "commander": "^14.0.3", + "react": "catalog:", + "react-dom": "catalog:", "viem": "catalog:", "yaml": "2.8.3" }, "devDependencies": { "@repo/typescript-config": "workspace:*", "@types/node": "catalog:", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", "esbuild": "catalog:", "execa": "catalog:", "tsx": "catalog:", diff --git a/bots/market-making/playground/model.ts b/bots/market-making/playground/model.ts index 60393f0e..76fec4e6 100644 --- a/bots/market-making/playground/model.ts +++ b/bots/market-making/playground/model.ts @@ -601,6 +601,12 @@ export const parseCollectionsImport = (text: string): CollectionsImport => { } export const COLLECTION_FRAGMENT_VERSION = 1 +type PlaygroundLocation = { + origin: string + pathname: string + search: string +} + export const encodePlaygroundFragment = (state: PlaygroundState) => { const validation = validatePlaygroundState(state) if (!validation.valid) throw new FragmentCodecError('Fragment state is invalid') @@ -624,10 +630,8 @@ export const encodePlaygroundFragment = (state: PlaygroundState) => { * @returns A canonical absolute URL containing the exact encoded playground fragment. * @throws When either collection is runtime-invalid or the encoded fragment exceeds its size bound. */ -export const createPlaygroundShareUrl = ( - state: PlaygroundState, - location: Pick -) => `${location.origin}${location.pathname}${location.search}${encodePlaygroundFragment(state)}` +export const createPlaygroundShareUrl = (state: PlaygroundState, location: PlaygroundLocation) => + `${location.origin}${location.pathname}${location.search}${encodePlaygroundFragment(state)}` export const decodePlaygroundFragment = (fragment: string): PlaygroundState => { const encoded = fragment.startsWith('#') ? fragment.slice(1) : fragment diff --git a/bots/market-making/scripts/check-jsdoc.ts b/bots/market-making/scripts/check-jsdoc.ts index fc671b64..5420f8b5 100644 --- a/bots/market-making/scripts/check-jsdoc.ts +++ b/bots/market-making/scripts/check-jsdoc.ts @@ -391,7 +391,23 @@ sourceFiles.push(resolve(packageRoot, 'scripts/js-doc-validation.error.ts')) sourceFiles.push(resolve(packageRoot, 'scripts/bundle-failed.error.ts')) sourceFiles.push(resolve(packageRoot, 'scripts/check-jsdoc.ts')) +/** + * Discovers the TypeScript files that define the documented market-making surface. + * @param root - Absolute market-making package directory to scan. + * @returns Relevant source and checker files in deterministic package-relative order. + * @remarks The scan is read-only and excludes the executable entrypoint and test files. + */ +export const discoverJSDocSourceFiles = async (root: string) => + ts.sys + .readDirectory(root, ['.ts'], ['node_modules'], ['src', 'scripts']) + .filter(file => { + const packagePath = relative(root, file) + return packagePath !== 'src/index.ts' && !packagePath.endsWith('.test.ts') + }) + .toSorted() + const run = async () => { + sourceFiles.splice(0, sourceFiles.length, ...(await discoverJSDocSourceFiles(packageRoot))) const failures: JSDocFailure[] = [] const declarations: string[] = [] for (const file of sourceFiles) { diff --git a/bots/market-making/scripts/deploy-railway.ts b/bots/market-making/scripts/deploy-railway.ts index e7390765..ba176187 100644 --- a/bots/market-making/scripts/deploy-railway.ts +++ b/bots/market-making/scripts/deploy-railway.ts @@ -7,8 +7,9 @@ * reach a terminal state and succeed only on Railway `SUCCESS`. */ import { delay, tryCatch } from '@repo/utils' -import { $ } from 'bun' -import { resolve } from 'node:path' +import { $ } from 'execa' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' import { RailwayDeploymentError } from './railway-deployment.error' import { @@ -26,10 +27,10 @@ import { const SERVICE = 'market-making' const DOCKERFILE_PATH = 'bots/market-making/Dockerfile' const STATE_MOUNT_PATH = '/state' -const REPO_ROOT = resolve(import.meta.dir, '..', '..', '..') -const ENVIRONMENT = Bun.env.RAILWAY_ENVIRONMENT?.trim() || 'production' -const DEPLOY_ONLY = /^(1|true)$/i.test(Bun.env.DEPLOY_ONLY?.trim() || '') -const PROJECT_ID = Bun.env.RAILWAY_PROJECT_ID?.trim() +const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..') +const ENVIRONMENT = process.env.RAILWAY_ENVIRONMENT?.trim() || 'production' +const DEPLOY_ONLY = /^(1|true)$/i.test(process.env.DEPLOY_ONLY?.trim() || '') +const PROJECT_ID = process.env.RAILWAY_PROJECT_ID?.trim() if (!PROJECT_ID) { throw new RailwayDeploymentError('Missing required environment variable: RAILWAY_PROJECT_ID') } @@ -54,7 +55,7 @@ type RequiredRuntimeVariableName = (typeof requiredRuntimeVariableNames)[number] type RuntimeVariable = readonly [name: string, value: string] const required = (name: RequiredRuntimeVariableName) => { - const value = Bun.env[name]?.trim() + const value = process.env[name]?.trim() if (!value) throw new RailwayDeploymentError(`Missing required environment variable: ${name}`) if ((name === 'BOOTSTRAP_MARKETS' || name === 'LADDER_MARKETS') && !isNonEmptyJsonArray(value)) { throw new RailwayDeploymentError(`${name} must be a non-empty JSON array`) @@ -67,9 +68,10 @@ const runtimeVariables = (): RuntimeVariable[] => { const requiredVariables = requiredRuntimeVariableNames.map( name => [name, required(name)] as const ) - const optionalVariables = synchronizedOptionalRailwayVariables(Bun.env) + const optionalVariables = synchronizedOptionalRailwayVariables(process.env) const method = - Bun.env.KEY_STORAGE_METHOD?.trim() || (Bun.env.MAKER_PRIVATE_KEY?.trim() ? 'private-key' : '') + process.env.KEY_STORAGE_METHOD?.trim() || + (process.env.MAKER_PRIVATE_KEY?.trim() ? 'private-key' : '') if (!['private-key', 'keystore', 'aws'].includes(method)) { throw new RailwayDeploymentError('KEY_STORAGE_METHOD must select exactly one signer') } @@ -90,7 +92,7 @@ const runtimeVariables = (): RuntimeVariable[] => { ? ['KEYSTORE_PATH', 'KEYSTORE_PASSWORD'] : ['AWS_KMS_KEY_ID', 'AWS_REGION'] for (const name of signerRequired) { - const value = Bun.env[name]?.trim() + const value = process.env[name]?.trim() if (!value) throw new RailwayDeploymentError(`Missing required environment variable: ${name}`) signerValues[name] = value } @@ -100,25 +102,23 @@ const runtimeVariables = (): RuntimeVariable[] => { } const assertCli = async () => { - const { error } = await tryCatch(Promise.resolve($`railway --version`.quiet())) + const { error } = await tryCatch($`railway --version`) if (error) throw new RailwayDeploymentError('Railway CLI is unavailable') } const ensureContext = async () => { - if (Bun.env.RAILWAY_TOKEN) return + if (process.env.RAILWAY_TOKEN) return const { error } = await tryCatch( - Promise.resolve($`railway link --project ${PROJECT_ID} --environment ${ENVIRONMENT}`.quiet()) + $`railway link --project ${PROJECT_ID} --environment ${ENVIRONMENT}` ) if (error) throw new RailwayDeploymentError('Failed to select the Railway project environment') } const listServices = async () => { const { data, error } = await tryCatch( - Promise.resolve( - $`railway service list --project ${PROJECT_ID} --environment ${ENVIRONMENT} --json` - .quiet() - .text() + $`railway service list --project ${PROJECT_ID} --environment ${ENVIRONMENT} --json`.then( + result => result.stdout ) ) if (error || typeof data !== 'string') { @@ -135,7 +135,7 @@ const ensureService = async () => { assertFreshRailwayReferenceProvisioning(Bun.env, true) const { data, error } = await tryCatch( - Promise.resolve($`railway add --service ${SERVICE} --json`.quiet().text()) + $`railway add --service ${SERVICE} --json`.then(result => result.stdout) ) if (error || typeof data !== 'string') { throw new RailwayDeploymentError('Failed to create the Railway service') @@ -151,10 +151,8 @@ const ensureService = async () => { const listVolumes = async () => { const { data, error } = await tryCatch( - Promise.resolve( - $`railway volume list --service ${SERVICE} --project ${PROJECT_ID} --environment ${ENVIRONMENT} --json` - .quiet() - .text() + $`railway volume list --service ${SERVICE} --project ${PROJECT_ID} --environment ${ENVIRONMENT} --json`.then( + result => result.stdout ) ) if (error || typeof data !== 'string') { @@ -189,9 +187,7 @@ const ensureStateVolume = async (serviceId: string | undefined) => { } const { error } = await tryCatch( - Promise.resolve( - $`railway volume add --service ${serviceId} --project ${PROJECT_ID} --environment ${ENVIRONMENT} --mount-path ${STATE_MOUNT_PATH} --json`.quiet() - ) + $`railway volume add --service ${serviceId} --project ${PROJECT_ID} --environment ${ENVIRONMENT} --mount-path ${STATE_MOUNT_PATH} --json` ) if (error) throw new RailwayDeploymentError('Failed to create the Railway state volume') @@ -205,9 +201,9 @@ const ensureStateVolume = async (serviceId: string | undefined) => { const setRuntimeVariable = async ([name, value]: RuntimeVariable) => { const { error } = await tryCatch( - Promise.resolve( - $`railway variable set ${name} --stdin --service ${SERVICE} --environment ${ENVIRONMENT} --skip-deploys < ${Buffer.from(value, 'utf8')}`.quiet() - ) + $({ + input: value + })`railway variable set ${name} --stdin --service ${SERVICE} --environment ${ENVIRONMENT} --skip-deploys` ) if (error) throw new RailwayDeploymentError(`Failed to set Railway variable: ${name}`) @@ -216,10 +212,8 @@ const setRuntimeVariable = async ([name, value]: RuntimeVariable) => { const latestDeploymentJson = async () => { const { data, error } = await tryCatch( - Promise.resolve( - $`railway deployment list --service ${SERVICE} --project ${PROJECT_ID} --environment ${ENVIRONMENT} --limit 1 --json` - .quiet() - .text() + $`railway deployment list --service ${SERVICE} --project ${PROJECT_ID} --environment ${ENVIRONMENT} --limit 1 --json`.then( + result => result.stdout ) ) if (error || typeof data !== 'string') { @@ -232,11 +226,9 @@ const latestDeploymentJson = async () => { const startDeployment = async () => { const message = `deploy market-making ${ENVIRONMENT}` const { error } = await tryCatch( - Promise.resolve( - $`railway up --service ${SERVICE} --project ${PROJECT_ID} --environment ${ENVIRONMENT} --detach --message ${message}` - .cwd(REPO_ROOT) - .quiet() - ) + $({ + cwd: REPO_ROOT + })`railway up --service ${SERVICE} --project ${PROJECT_ID} --environment ${ENVIRONMENT} --detach --message ${message}` ) if (error) throw new RailwayDeploymentError('Failed to start the Railway deployment') } diff --git a/bots/market-making/src/config/config.service.ts b/bots/market-making/src/config/config.service.ts index ecbefa96..27c9fcbd 100644 --- a/bots/market-making/src/config/config.service.ts +++ b/bots/market-making/src/config/config.service.ts @@ -31,14 +31,14 @@ export type { MakerIdentity } from './signer-identity.utils' export class ConfigService { /** * Parses the existing environment-only representation through the shared validation path. - * @param environment - Environment map; defaults to `Bun.env` at the runtime boundary. + * @param environment - Environment map; defaults to `process.env` at the runtime boundary. * @param options - Runtime mode; read-only operation accepts only the configured maker address. * @returns An immutable configuration with checksummed addresses and narrowed IDs. * @throws `ConfigValidationError` on missing, malformed, duplicated, unsupported, or out-of-range * values; rejected values and secrets are not retained by the error. */ static from( - environment: Environment = Bun.env, + environment: Environment = process.env, options: Pick = {} ) { return ConfigService.fromSource( @@ -58,7 +58,10 @@ export class ConfigService { * permits fallback; unsafe entries fail closed. No file means env-only loading. Read-only mode * omits YAML and environment private-key values before typed validation. */ - static async load(environment: Environment = process.env, options: ConfigurationLoadOptions = {}) { + static async load( + environment: Environment = process.env, + options: ConfigurationLoadOptions = {} + ) { const source = await loadConfigurationSources(environment, options) const declaredMethod = source.values.KEY_STORAGE_METHOD?.toString().trim() const keystorePath = source.values.KEYSTORE_PATH?.toString().trim() diff --git a/bots/market-making/test/application/bootstrap/position-bootstrap.service.test.ts b/bots/market-making/test/application/bootstrap/position-bootstrap.service.test.ts index 1ec24553..14fabcdc 100644 --- a/bots/market-making/test/application/bootstrap/position-bootstrap.service.test.ts +++ b/bots/market-making/test/application/bootstrap/position-bootstrap.service.test.ts @@ -482,7 +482,7 @@ describe('PositionBootstrapService', () => { test('reports canonical protocol no-ops as observed resting offers', async () => { const { service, make } = setup() - make.reconcile = mock(async () => 'unchanged' as const) + make.reconcile = vi.fn(async () => 'unchanged' as const) const result = await service.runOnce() diff --git a/bots/market-making/test/config/key-storage.test.ts b/bots/market-making/test/config/key-storage.test.ts index 50aab460..f3f32647 100644 --- a/bots/market-making/test/config/key-storage.test.ts +++ b/bots/market-making/test/config/key-storage.test.ts @@ -1,7 +1,8 @@ -import { afterEach, describe, expect, mock, test } from 'bun:test' import { mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' +import { inspect } from 'node:util' +import { afterEach, describe, expect, test, vi } from 'vitest' import { ConfigValidationError } from '../../src/config/config-validation.error' import { ConfigService } from '../../src/config/config.service' @@ -28,7 +29,7 @@ const configurationYaml = (identity: string) => `chain:\n id: 8453\n rpcUrl: https://yaml-rpc.example\n archiveRpcUrl: https://archive.example\nidentity:\n${identity}\ncontracts:\n midnightAddress: 0x2222222222222222222222222222222222222222\n loanAssetAddress: 0x3333333333333333333333333333333333333333\n ratifierAddress: 0x4444444444444444444444444444444444444444\napis:\n morphoBaseUrl: https://api.example\n routerBaseUrl: https://router.example\nmarkets:\n allowlist: [0x${'55'.repeat(32)}]\n referenceMarketId: 0x${'77'.repeat(32)}\nsetup:\n nativeReserveWei: 10\n maximumLendExposureAssets: 100\n` afterEach(async () => { - mock.restore() + vi.restoreAllMocks() await Promise.all(directories.splice(0).map(path => rm(path, { recursive: true }))) }) @@ -38,7 +39,7 @@ describe('maker key storage configuration', () => { expect(config.keyStorageMethod).toBe('private-key') expect(config.identity).toMatchObject({ method: 'private-key', privateKey }) expect(JSON.stringify(config.identity)).not.toContain(privateKey) - expect(Bun.inspect(config.identity)).not.toContain(privateKey) + expect(inspect(config.identity)).not.toContain(privateKey) }) test('loads a keystore with a direct password without exposing the password through serialization', () => { @@ -56,9 +57,9 @@ describe('maker key storage configuration', () => { password }) expect(JSON.stringify(config)).not.toContain(password) - expect(Bun.inspect(config)).not.toContain(password) + expect(inspect(config)).not.toContain(password) expect(JSON.stringify(config.identity)).not.toContain(password) - expect(Bun.inspect(config.identity)).not.toContain(password) + expect(inspect(config.identity)).not.toContain(password) }) test('rejects only a truly empty keystore password while preserving whitespace byte-for-byte', () => { @@ -82,7 +83,7 @@ describe('maker key storage configuration', () => { }) test('loads a keystore password from the interactive reader', async () => { - const readPassword = mock(async () => ' prompted-秘密🔐 ') + const readPassword = vi.fn(async () => ' prompted-秘密🔐 ') const config = await ConfigService.load( { ...baseEnvironment, @@ -97,7 +98,7 @@ describe('maker key storage configuration', () => { }) test('normalizes keystore selectors before resolving an empty interactive password', async () => { - const readPassword = mock(async () => 'prompted-password') + const readPassword = vi.fn(async () => 'prompted-password') const config = await ConfigService.load( { ...baseEnvironment, diff --git a/bots/market-making/test/e2e/setup-api.ts b/bots/market-making/test/e2e/setup-api.ts index 99ef259a..4730e511 100644 --- a/bots/market-making/test/e2e/setup-api.ts +++ b/bots/market-making/test/e2e/setup-api.ts @@ -98,7 +98,9 @@ const toNodeHandler = incoming.on('data', chunk => chunks.push(chunk as Buffer)) incoming.on('end', () => resolve(Buffer.concat(chunks))) }) - const response = handler(new Request(url, { method, headers, body })) + const response = handler( + new Request(url, { method, headers, body: body ? new Uint8Array(body) : undefined }) + ) outgoing.writeHead(response.status, Object.fromEntries(response.headers)) outgoing.end(Buffer.from(await response.arrayBuffer())) } diff --git a/bots/market-making/test/infrastructure/bootstrap/bootstrap-pending-offer.utils.test.ts b/bots/market-making/test/infrastructure/bootstrap/bootstrap-pending-offer.utils.test.ts index 9064e334..7cc972a6 100644 --- a/bots/market-making/test/infrastructure/bootstrap/bootstrap-pending-offer.utils.test.ts +++ b/bots/market-making/test/infrastructure/bootstrap/bootstrap-pending-offer.utils.test.ts @@ -4,10 +4,7 @@ import { describe, expect, test } from 'vitest' import type { BootstrapRawGroup } from '../../../src/infrastructure/bootstrap/bootstrap-groups.utils' -import { - pendingBootstrapGroups, - pendingBootstrapOffers -} from '../../../src/infrastructure/bootstrap/bootstrap-pending-offer.utils' +import { pendingBootstrapOffers } from '../../../src/infrastructure/bootstrap/bootstrap-pending-offer.utils' const marketId: Hex = `0x${'11'.repeat(32)}` const groupId: Hex = `0x${'22'.repeat(32)}` @@ -30,18 +27,9 @@ const indexedGroup = (consumed: bigint): BootstrapRawGroup => ({ offers: [{ marketId, maker, buy: true, tick: 1n }] }) -describe('pendingBootstrapGroups', () => { +describe('pendingBootstrapOffers', () => { test('projects persisted publication intent while API indexing is pending', () => { expect(pendingBootstrapOffers([], [offer])).toEqual([offer]) - expect(pendingBootstrapGroups([], [offer])).toEqual([ - { - id: groupId, - marketId, - assets: 100n, - rateBps: 450n, - referenceObservationId: 'blocks:100-200' - } - ]) }) test.each([0n, 100n])( @@ -49,7 +37,6 @@ describe('pendingBootstrapGroups', () => { consumed => { const groups = [indexedGroup(consumed)] expect(pendingBootstrapOffers(groups, [offer])).toEqual([]) - expect(pendingBootstrapGroups(groups, [offer])).toEqual([]) } ) }) diff --git a/bots/market-making/test/infrastructure/cli/key-storage-cli.test.ts b/bots/market-making/test/infrastructure/cli/key-storage-cli.test.ts index f6909afa..f912dc52 100644 --- a/bots/market-making/test/infrastructure/cli/key-storage-cli.test.ts +++ b/bots/market-making/test/infrastructure/cli/key-storage-cli.test.ts @@ -1,4 +1,7 @@ -import { describe, expect, test } from 'bun:test' +import { execa } from 'execa' +import { dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +import { describe, expect, test } from 'vitest' import { VersionService } from '../../../src/application/version.service' import { Cli } from '../../../src/infrastructure/cli/cli' @@ -113,23 +116,15 @@ describe('maker key storage CLI options', () => { test('entrypoint --help emits both argv warnings without exposing an ambient secret', async () => { const secret = 'must-not-appear-in-help-output' - const process = Bun.spawn( - [Bun.which('bun') ?? 'bun', 'bots/market-making/src/index.ts', '--help'], - { - cwd: `${import.meta.dir}/../../../../..`, - env: { PATH: Bun.env.PATH, MAKER_PRIVATE_KEY: secret, KEYSTORE_PASSWORD: secret }, - stdout: 'pipe', - stderr: 'pipe' - } - ) - const [exitCode, stdout, stderr] = await Promise.all([ - process.exited, - new Response(process.stdout).text(), - new Response(process.stderr).text() - ]) - const output = stdout + stderr + const repoRoot = dirname(fileURLToPath(new URL('../../../../../package.json', import.meta.url))) + const process = await execa('tsx', ['bots/market-making/src/index.ts', '--help'], { + cwd: repoRoot, + env: { MAKER_PRIVATE_KEY: secret, KEYSTORE_PASSWORD: secret }, + reject: false + }) + const output = process.stdout + process.stderr - expect(exitCode).toBe(0) + expect(process.exitCode).toBe(0) expect(output).toContain('--private-key ') expect(output).toContain('MAKER_PRIVATE_KEY') expect(output).toContain('--password ') diff --git a/bots/market-making/test/infrastructure/cli/market-making-entrypoint-observability.test.ts b/bots/market-making/test/infrastructure/cli/market-making-entrypoint-observability.test.ts index 2b56a34f..581fb379 100644 --- a/bots/market-making/test/infrastructure/cli/market-making-entrypoint-observability.test.ts +++ b/bots/market-making/test/infrastructure/cli/market-making-entrypoint-observability.test.ts @@ -63,7 +63,7 @@ describe('runMarketMakingEntrypoint observability', () => { test('surfaces sanitized maker-account failures without reporting them as unexpected', async () => { const stderr: string[] = [] - const unexpected = mock((_error: unknown, _origin: 'entrypoint') => undefined) + const unexpected = vi.fn((_error: unknown, _origin: 'entrypoint') => undefined) const error = new MakerAccountError('keystore-decrypt') const exitCode = await runMarketMakingEntrypoint( @@ -71,7 +71,7 @@ describe('runMarketMakingEntrypoint observability', () => { ['setup-check'], { writeOut: () => undefined, writeError: value => stderr.push(value) }, {}, - { record: mock(() => undefined), unexpected } + { record: vi.fn(() => undefined), unexpected } ) expect(exitCode).toBe(1) diff --git a/bots/market-making/test/infrastructure/cli/password-prompt.test.ts b/bots/market-making/test/infrastructure/cli/password-prompt.test.ts index ced2dbc4..2d06f11f 100644 --- a/bots/market-making/test/infrastructure/cli/password-prompt.test.ts +++ b/bots/market-making/test/infrastructure/cli/password-prompt.test.ts @@ -1,6 +1,6 @@ -import { describe, expect, test } from 'bun:test' import { EventEmitter } from 'node:events' import { readFile } from 'node:fs/promises' +import { describe, expect, test } from 'vitest' import { readPasswordInteractively } from '../../../src/infrastructure/cli/password-prompt.utils' @@ -70,7 +70,7 @@ const expectWiped = (secretBytes: number[], length: number) => { describe('hidden keystore password input', () => { test('defaults prompts and trailing newlines to stderr', async () => { const source = await readFile( - `${import.meta.dir}/../../../src/infrastructure/cli/password-prompt.utils.ts`, + new URL('../../../src/infrastructure/cli/password-prompt.utils.ts', import.meta.url), 'utf8' ) diff --git a/bots/market-making/test/infrastructure/make/maker-account.test.ts b/bots/market-making/test/infrastructure/make/maker-account.test.ts index e1182044..e323e7d3 100644 --- a/bots/market-making/test/infrastructure/make/maker-account.test.ts +++ b/bots/market-making/test/infrastructure/make/maker-account.test.ts @@ -2,7 +2,6 @@ import type { TransactionSerializedLegacy } from 'viem' import { Wallet } from '@ethereumjs/wallet' import { secp256k1 } from '@noble/curves/secp256k1' -import { describe, expect, mock, test } from 'bun:test' import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -14,6 +13,7 @@ import { recoverTransactionAddress } from 'viem' import { privateKeyToAccount } from 'viem/accounts' +import { describe, expect, test, vi } from 'vitest' import { createMakerAccount } from '../../../src/infrastructure/make/maker-account.utils' @@ -23,7 +23,7 @@ const maker = privateKeyToAccount(privateKey).address describe('maker signer selection', () => { test('reuses one AWS KMS client per region across public-key and signing calls', async () => { const source = await readFile( - `${import.meta.dir}/../../../src/infrastructure/make/maker-account.utils.ts`, + new URL('../../../src/infrastructure/make/maker-account.utils.ts', import.meta.url), 'utf8' ) @@ -62,8 +62,8 @@ describe('maker signer selection', () => { test('decrypts a keystore only at the signer boundary', async () => { const password = ' keystore-秘密🔐 ' - const readFile = mock(async () => '{"encrypted":true}') - const decryptKeystore = mock(async (json: string, suppliedPassword: string) => { + const readFile = vi.fn(async () => '{"encrypted":true}') + const decryptKeystore = vi.fn(async (json: string, suppliedPassword: string) => { expect(json).toContain('encrypted') expect(suppliedPassword).toBe(password) return privateKey @@ -122,8 +122,8 @@ describe('maker signer selection', () => { ...Buffer.from('3056301006072a8648ce3d020106052b8104000a034200', 'hex'), ...publicKey ]) - const getPublicKey = mock(async () => spki) - const signDigest = mock(async (_keyId: string, _region: string, digest: Uint8Array) => + const getPublicKey = vi.fn(async () => spki) + const signDigest = vi.fn(async (_keyId: string, _region: string, digest: Uint8Array) => secp256k1.sign(digest, secret, { lowS: false }).toDERRawBytes() ) const account = await createMakerAccount( diff --git a/bots/market-making/test/playground/artifact.test.ts b/bots/market-making/test/playground/artifact.test.ts index 59f196b2..2795949f 100644 --- a/bots/market-making/test/playground/artifact.test.ts +++ b/bots/market-making/test/playground/artifact.test.ts @@ -1,10 +1,12 @@ -import { afterEach, describe, expect, test } from 'bun:test' import { build } from 'esbuild' +import { execa } from 'execa' import { readdir, readFile, rm } from 'node:fs/promises' -import { join } from 'node:path' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' import { gzipSync } from 'node:zlib' +import { afterEach, describe, expect, test } from 'vitest' -const packageRoot = join(import.meta.dir, '../..') +const packageRoot = dirname(fileURLToPath(new URL('../../package.json', import.meta.url))) let temporaryDirectory = '' afterEach(async () => { @@ -13,21 +15,16 @@ afterEach(async () => { }) const productionBuild = async () => { - const child = Bun.spawn( - [Bun.which('node')!, join(packageRoot, 'scripts/playground-build.mjs'), '--temporary'], + const child = await execa( + process.execPath, + [join(packageRoot, 'scripts/playground-build.mjs'), '--temporary'], { cwd: packageRoot, - env: { ...Bun.env, BUN_EXE: Bun.which('bun')! }, - stdout: 'pipe', - stderr: 'pipe' + reject: false } ) - const [code, stdout, stderr] = await Promise.all([ - child.exited, - new Response(child.stdout).text(), - new Response(child.stderr).text() - ]) - expect(code, `${stdout}\n${stderr}`).toBe(0) + expect(child.exitCode, `${child.stdout}\n${child.stderr}`).toBe(0) + const { stdout } = child const record = stdout.split(/\r?\n/).flatMap(line => { try { const value = JSON.parse(line) diff --git a/bots/market-making/test/playground/model.test.ts b/bots/market-making/test/playground/model.test.ts index 28f24a77..4ab51ca3 100644 --- a/bots/market-making/test/playground/model.test.ts +++ b/bots/market-making/test/playground/model.test.ts @@ -1,4 +1,5 @@ -import { describe, expect, test } from 'bun:test' +import { readFile } from 'node:fs/promises' +import { describe, expect, test } from 'vitest' import { CollectionImportError } from '../../playground/collection-import.error' import { CollectionValidationError } from '../../playground/collection-validation.error' @@ -33,9 +34,9 @@ describe('bootstrap + ladder only playground follow-up', () => { test('does not expose key-storage configuration in the collection-only playground', async () => { const [model, application, document] = await Promise.all([ - Bun.file(new URL('../../playground/model.ts', import.meta.url)).text(), - Bun.file(new URL('../../playground/app.tsx', import.meta.url)).text(), - Bun.file(new URL('../../playground/index.html', import.meta.url)).text() + readFile(new URL('../../playground/model.ts', import.meta.url), 'utf8'), + readFile(new URL('../../playground/app.tsx', import.meta.url), 'utf8'), + readFile(new URL('../../playground/index.html', import.meta.url), 'utf8') ]) const exposedPlaygroundSurface = `${JSON.stringify(createDefaultPlaygroundState())}\n${model}\n${application}\n${document}` @@ -50,7 +51,7 @@ describe('bootstrap + ladder only playground follow-up', () => { }) test('documents argv secret exposure without literal private keys or passwords', async () => { - const readme = await Bun.file('bots/market-making/README.md').text() + const readme = await readFile(new URL('../../README.md', import.meta.url), 'utf8') expect(readme).toContain('`--private-key `') expect(readme).toContain('`MAKER_PRIVATE_KEY`') diff --git a/bots/market-making/test/playground/module-graph.test.ts b/bots/market-making/test/playground/module-graph.test.ts index 73817922..fcbf5eff 100644 --- a/bots/market-making/test/playground/module-graph.test.ts +++ b/bots/market-making/test/playground/module-graph.test.ts @@ -1,9 +1,10 @@ -import { describe, expect, test } from 'bun:test' import { build } from 'esbuild' -import { join } from 'node:path' +import { dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +import { describe, expect, test } from 'vitest' const normalize = (value: string) => value.replaceAll('\\', '/') -const packageRoot = join(import.meta.dir, '../..') +const packageRoot = dirname(fileURLToPath(new URL('../../package.json', import.meta.url))) describe('playground browser module graph', () => { test('uses an explicit browser-safe local allowlist and excludes runtime capabilities', async () => { diff --git a/bots/market-making/test/playground/pages-workflow.test.ts b/bots/market-making/test/playground/pages-workflow.test.ts index 855be618..baa21f90 100644 --- a/bots/market-making/test/playground/pages-workflow.test.ts +++ b/bots/market-making/test/playground/pages-workflow.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, test } from 'bun:test' import { readFile } from 'node:fs/promises' +import { describe, expect, test } from 'vitest' import { parse } from 'yaml' const workflowPath = new URL( diff --git a/bots/market-making/test/playground/playground-error.utils.test.ts b/bots/market-making/test/playground/playground-error.utils.test.ts index afbfb434..aedf3eb0 100644 --- a/bots/market-making/test/playground/playground-error.utils.test.ts +++ b/bots/market-making/test/playground/playground-error.utils.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import { CollectionImportError } from '../../playground/collection-import.error' import { CollectionValidationError } from '../../playground/collection-validation.error' diff --git a/bots/market-making/test/playground/react-contract.test.ts b/bots/market-making/test/playground/react-contract.test.ts index 18460b34..89f28703 100644 --- a/bots/market-making/test/playground/react-contract.test.ts +++ b/bots/market-making/test/playground/react-contract.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, test } from 'bun:test' import { readFile } from 'node:fs/promises' +import { describe, expect, test } from 'vitest' const read = (path: string) => readFile(new URL(`../../${path}`, import.meta.url), 'utf8') diff --git a/bots/market-making/test/playground/shared-market-collections.test.ts b/bots/market-making/test/playground/shared-market-collections.test.ts index 7f7095bf..4a8a4892 100644 --- a/bots/market-making/test/playground/shared-market-collections.test.ts +++ b/bots/market-making/test/playground/shared-market-collections.test.ts @@ -1,4 +1,5 @@ -import { describe, expect, test } from 'bun:test' +import { readFile } from 'node:fs/promises' +import { describe, expect, test } from 'vitest' import { createDefaultBootstrap, createDefaultLadder } from '../../playground/model' import { @@ -33,9 +34,9 @@ describe('shared market collection boundary', () => { test('keeps the browser model graph off runtime configuration and secret parsing modules', async () => { const [model, shared, runtime] = await Promise.all([ - Bun.file(new URL('../../playground/model.ts', import.meta.url)).text(), - Bun.file(new URL('../../src/config/market-collections.ts', import.meta.url)).text(), - Bun.file(new URL('../../src/config/config.service.ts', import.meta.url)).text() + readFile(new URL('../../playground/model.ts', import.meta.url), 'utf8'), + readFile(new URL('../../src/config/market-collections.ts', import.meta.url), 'utf8'), + readFile(new URL('../../src/config/config.service.ts', import.meta.url), 'utf8') ]) expect(model).not.toContain('src/config/config.utils') diff --git a/bots/market-making/test/playground/tsconfig-coverage.test.ts b/bots/market-making/test/playground/tsconfig-coverage.test.ts index b57ff3b8..3282385e 100644 --- a/bots/market-making/test/playground/tsconfig-coverage.test.ts +++ b/bots/market-making/test/playground/tsconfig-coverage.test.ts @@ -1,8 +1,9 @@ -import { describe, expect, test } from 'bun:test' -import { resolve } from 'node:path' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' import ts from 'typescript' +import { describe, expect, test } from 'vitest' -const packageRoot = resolve(import.meta.dir, '../..') +const packageRoot = dirname(fileURLToPath(new URL('../../package.json', import.meta.url))) const configPath = resolve(packageRoot, 'tsconfig.json') const appPath = resolve(packageRoot, 'playground/app.tsx') diff --git a/bots/market-making/test/scripts/railway.utils.test.ts b/bots/market-making/test/scripts/railway.utils.test.ts index f86549e0..96defedb 100644 --- a/bots/market-making/test/scripts/railway.utils.test.ts +++ b/bots/market-making/test/scripts/railway.utils.test.ts @@ -1,6 +1,5 @@ -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import { readFileSync } from 'node:fs' -import { resolve } from 'node:path' import { assertFreshRailwayReferenceProvisioning, @@ -103,12 +102,12 @@ describe('Railway CLI output parsing', () => { }) test('checks fresh-service references before Railway can create the service', () => { - const deploy = readFileSync(resolve(import.meta.dir, '../../scripts/deploy-railway.ts'), 'utf8') + const deploy = readFileSync(new URL('../../scripts/deploy-railway.ts', import.meta.url), 'utf8') expect( - deploy.indexOf('assertFreshRailwayReferenceProvisioning(Bun.env, true)') + deploy.indexOf('assertFreshRailwayReferenceProvisioning(process.env, true)') ).toBeGreaterThan(-1) - expect(deploy.indexOf('assertFreshRailwayReferenceProvisioning(Bun.env, true)')).toBeLessThan( + expect(deploy.indexOf('assertFreshRailwayReferenceProvisioning(process.env, true)')).toBeLessThan( deploy.indexOf('railway add --service') ) }) diff --git a/bots/market-making/tsconfig.json b/bots/market-making/tsconfig.json index 8a06a316..5d7900f2 100644 --- a/bots/market-making/tsconfig.json +++ b/bots/market-making/tsconfig.json @@ -1,6 +1,10 @@ { "extends": "@repo/typescript-config/base", - "compilerOptions": { "types": ["node"] }, - "include": ["src", "test", "scripts"], + "compilerOptions": { + "jsx": "react-jsx", + "lib": ["ESNext", "DOM"], + "types": ["node", "react", "react-dom"] + }, + "include": ["playground", "src", "test", "scripts"], "exclude": ["node_modules"] } diff --git a/bots/market-making/typedoc.json b/bots/market-making/typedoc.json index 244a3a3c..7803bdd9 100644 --- a/bots/market-making/typedoc.json +++ b/bots/market-making/typedoc.json @@ -62,7 +62,6 @@ "src/infrastructure/invalidation/production-offer-invalidation.ts", "src/infrastructure/ladder/ladder-adapter.error.ts", "src/infrastructure/ladder/ladder-active-publication.utils.ts", - "src/infrastructure/ladder/ladder-bootstrap-offer.utils.ts", "src/infrastructure/ladder/ladder-cash-reservation.utils.ts", "src/infrastructure/ladder/ladder-group-ownership.utils.ts", "src/infrastructure/ladder/ladder-hard-halt.error.ts", diff --git a/knip.json b/knip.json index 52448463..0418b4b9 100644 --- a/knip.json +++ b/knip.json @@ -27,8 +27,7 @@ "ignore": ["src/infrastructure/*/generated/**"] }, "bots/market-making": { - "entry": ["playground/app.tsx"], - "ignore": ["scripts/playground-smoke-support.mjs"], + "entry": ["src/index.ts", "playground/app.tsx"], "ignoreDependencies": ["@repo/bot-kit"] } } diff --git a/packages/bot-kit/test/heartbeat.test.ts b/packages/bot-kit/test/heartbeat.test.ts index a16a2c72..c3f66543 100644 --- a/packages/bot-kit/test/heartbeat.test.ts +++ b/packages/bot-kit/test/heartbeat.test.ts @@ -39,7 +39,7 @@ describe('createHeartbeatMonitor', () => { it('is inert without warning or scheduling when the URL contains only whitespace', async () => { const { logger, events } = captureLogger() - const setIntervalSpy = spyOn(globalThis, 'setInterval') + const setIntervalSpy = vi.spyOn(globalThis, 'setInterval') let pings = 0 const monitor = createHeartbeatMonitor({ url: ' \t\r\n ', @@ -119,9 +119,11 @@ describe('createHeartbeatMonitor', () => { ['http://example.test:8080/heartbeat', 'http://example.test:8080/heartbeat'] ])('uses the exact trimmed HTTP(S) URL %s', async (url, expectedUrl) => { const { logger } = captureLogger() - const setIntervalSpy = spyOn(globalThis, 'setInterval').mockImplementation( - (() => 1 as unknown as ReturnType) as typeof setInterval - ) + const setIntervalSpy = vi + .spyOn(globalThis, 'setInterval') + .mockImplementation( + (() => 1 as unknown as ReturnType) as typeof setInterval + ) const urls: string[] = [] const monitor = createHeartbeatMonitor({ url, @@ -149,7 +151,7 @@ describe('createHeartbeatMonitor', () => { 'https://example.test:bad-port/heartbeat' ])('warns once and stays inert for invalid URL %s', async url => { const { logger, events } = captureLogger() - const setIntervalSpy = spyOn(globalThis, 'setInterval') + const setIntervalSpy = vi.spyOn(globalThis, 'setInterval') let pings = 0 const monitor = createHeartbeatMonitor({ url, diff --git a/packages/bot-kit/test/logger.test.ts b/packages/bot-kit/test/logger.test.ts index 735b5f4f..7b71be40 100644 --- a/packages/bot-kit/test/logger.test.ts +++ b/packages/bot-kit/test/logger.test.ts @@ -159,7 +159,7 @@ describe('createLogger BetterStack opt-in contract', () => { }) it('accepts a nonblank malformed paired host without synchronous validation', () => { - const err = spyOn(console, 'error').mockImplementation(() => undefined) + const err = vi.spyOn(console, 'error').mockImplementation(() => undefined) expect(() => createLogger('info', { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 334be90b..52a53c7f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,12 +21,24 @@ catalogs: '@morpho-org/viem-dlc': specifier: 0.0.11 version: 0.0.11 + '@tanstack/react-form': + specifier: 1.20.0 + version: 1.20.0 + '@tanstack/react-table': + specifier: 8.21.3 + version: 8.21.3 '@types/lodash-es': specifier: ^4.17.12 version: 4.17.12 '@types/node': specifier: 24.7.0 version: 24.7.0 + '@types/react': + specifier: 19.2.18 + version: 19.2.18 + '@types/react-dom': + specifier: 19.2.4 + version: 19.2.4 date-fns: specifier: 4.1.0 version: 4.1.0 @@ -69,6 +81,12 @@ catalogs: oxlint-tsgolint: specifier: ^0.22.1 version: 0.22.1 + react: + specifier: 19.2.8 + version: 19.2.8 + react-dom: + specifier: 19.2.8 + version: 19.2.8 solc: specifier: 0.8.35 version: 0.8.35 @@ -189,6 +207,12 @@ importers: bots/market-making: dependencies: + '@aws-sdk/client-kms': + specifier: 3.1101.0 + version: 3.1101.0 + '@ethereumjs/wallet': + specifier: ^10.0.0 + version: 10.0.0 '@morpho-org/midnight-sdk': specifier: 'catalog:' version: 1.3.0(@morpho-org/morpho-ts@2.8.0)(viem@2.47.17(typescript@6.0.2)(zod@4.4.3)) @@ -198,6 +222,12 @@ importers: '@morpho-org/morpho-ts': specifier: 'catalog:' version: 2.8.0 + '@noble/curves': + specifier: 1.9.1 + version: 1.9.1 + '@repo/bot-kit': + specifier: workspace:* + version: link:../../packages/bot-kit '@repo/logging': specifier: workspace:* version: link:../../packages/logging @@ -213,9 +243,24 @@ importers: '@repo/utils': specifier: workspace:* version: link:../../packages/utils + '@tanstack/react-form': + specifier: 'catalog:' + version: 1.20.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@tanstack/react-table': + specifier: 'catalog:' + version: 8.21.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + asn1js: + specifier: 3.0.6 + version: 3.0.6 commander: specifier: ^14.0.3 version: 14.0.3 + react: + specifier: 'catalog:' + version: 19.2.8 + react-dom: + specifier: 'catalog:' + version: 19.2.8(react@19.2.8) viem: specifier: 'catalog:' version: 2.47.17(typescript@6.0.2)(zod@4.4.3) @@ -229,6 +274,12 @@ importers: '@types/node': specifier: 'catalog:' version: 24.7.0 + '@types/react': + specifier: 'catalog:' + version: 19.2.18 + '@types/react-dom': + specifier: 'catalog:' + version: 19.2.4(@types/react@19.2.18) esbuild: specifier: 'catalog:' version: 0.28.1 @@ -550,6 +601,73 @@ packages: '@adraffy/ens-normalize@1.11.1': resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} + '@aws-sdk/client-kms@3.1101.0': + resolution: {integrity: sha512-qczxakJlFYG9E6nE2yWIJ2nXq0HbMzHxMSWIsyubPZqx1TANHd2CSJWDTkg+IsNKq+94tG+e+EJo3uADwZ6cdw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.977.5': + resolution: {integrity: sha512-O5otOc1c6UZh5HsHAaPdYBcUUR9HL6mtnKqvc8nxN/CKDGUBUpsdh0q8K04Uz/dd1i0TaGyIQuQNqoO7+ad2TQ==} + engines: {node: '>=20.0.0'} + deprecated: |- + Deprecated due to Document number parsing bug in JSON, see + https://github.com/aws/aws-sdk-js-v3/issues/8246. Newer version available. + + '@aws-sdk/credential-provider-env@3.972.66': + resolution: {integrity: sha512-bOzP2+zdJ0XrghywB4FaJXtGZCx9yS0AGps+VJ5yEgg30wVyHNmVDBwVDXcRypzQY5iLGCS3NSn0nsuISqjFCQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.68': + resolution: {integrity: sha512-lkunS8X+H6V76WE+t/uGQm/U8v0JXK5mLfNFTUAMlE1kqaCjwlmqKJrgCVtqjK/vqnlrSWsLK4Lr4NANBWlfTQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.973.11': + resolution: {integrity: sha512-KoDEolYtLHG/8C+IiZpXbJWyBOMkrHV+j66Kb9PBXmLv5euGb7aELvuCmLenoGAV6gBW2wM7TsG/1e5iulH4kA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.73': + resolution: {integrity: sha512-tjsxMkTAFkmiV9ycmymapb9nLECWVOwFs0bZMQ9gB9bnbY8/HwfukHZlWbXZZp7qkPU6EXAfOcMm3DioFFEywA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.77': + resolution: {integrity: sha512-l4nitYCN/Ls57vtUfdextCjTjW41JD7lQiAnuR0RTbdByFc/6OmEAzwGd+lrp6CUtiXGQL1FCaYiamfHASrwBw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.66': + resolution: {integrity: sha512-YOnX6bIhdjx0QfaENu2PB0eFm5MEc9ft8XNGQ+NxMfeLSq9aE+XjWCwDupEnV4UWv5ZFpBLJbTREIx7KNOoqpQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.973.10': + resolution: {integrity: sha512-IsXnQ35j5VE+3ZK6aIhT5ypB+Jim3zRwVz0nYuVwyBKZyu/SYx+O2/LQpng8c2EiuwyqceabsDlYrICHDlJPsA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.72': + resolution: {integrity: sha512-nj9Zlsy7ya+fy+jhWTJwgfr7YdtDM4xHyZvgKuftuny0UgROVx9lxwvsWJSLvpKk4lig0m0tHng3k1fEnt0LeA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.997.40': + resolution: {integrity: sha512-hEdHT0PBR4fkGxWhwKG5EtEYKnAM7HKkp0vD10ufk4YcXejH4r4q6G/XhPzjUc6Yxo5kBS2vHg7llj4ViR9VTQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.43': + resolution: {integrity: sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1102.0': + resolution: {integrity: sha512-Ua700vVvM1q105yABSUQWkCK6FeTrNfU6ORGetJe5BzkZWY7QhkF7SVTOlmDGWRDNd6jbyY0Dv5e+E4bMBEmLg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.974.2': + resolution: {integrity: sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.37': + resolution: {integrity: sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.3.0': + resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -879,6 +997,19 @@ packages: cpu: [x64] os: [win32] + '@ethereumjs/rlp@10.1.2': + resolution: {integrity: sha512-T5Zt6C2pd02Wd88Q9A5/UX+He1Q2Y1LntHxz/038tfbUMiqby4fYSSTLEDx+TEfJqw1BsJSBY/TSu6goUzlk+w==} + engines: {node: '>=20'} + hasBin: true + + '@ethereumjs/util@10.1.2': + resolution: {integrity: sha512-UPBgXtHHfQugoXOSAoeG3jdmPbl37cwV9y3XqTPAnw8tJj8np14TPV2uc5lOs7C2LMF9Ubn66zyaiYxgwGppng==} + engines: {node: '>=20'} + + '@ethereumjs/wallet@10.0.0': + resolution: {integrity: sha512-ayRmV8uL1YEaRxUacr5fcy/Z/03K8NtVP7NIq3X4y5wDaNx7e+6hk40sa7cbkBhzOEK4j3JPpGohVhrOw5I03A==} + engines: {node: '>=18'} + '@gerrit0/mini-shiki@3.23.0': resolution: {integrity: sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==} @@ -985,10 +1116,18 @@ packages: resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} engines: {node: ^14.21.3 || >=16} + '@noble/curves@1.9.0': + resolution: {integrity: sha512-7YDlXiNMdO1YZeH6t/kvopHHbIZzlxrCV9WLqCY6QhcXOoXiNCMDqJIglZ9Yjx5+w7Dz30TITFrlTjnRg7sKEg==} + engines: {node: ^14.21.3 || >=16} + '@noble/curves@1.9.1': resolution: {integrity: sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==} engines: {node: ^14.21.3 || >=16} + '@noble/curves@2.2.0': + resolution: {integrity: sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==} + engines: {node: '>= 20.19.0'} + '@noble/hashes@1.8.0': resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} @@ -1565,12 +1704,68 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@smithy/core@3.31.1': + resolution: {integrity: sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.4.16': + resolution: {integrity: sha512-QfuLWAkLzptffFW980AFeHZFdqds2B64rpEd3uJ6lgs3xVn9QegGMUgUcj+4d7dRrAsya3r58ZKpku97WcFb4w==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.6.13': + resolution: {integrity: sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.9.13': + resolution: {integrity: sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.6.12': + resolution: {integrity: sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.16.1': + resolution: {integrity: sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==} + engines: {node: '>=18.0.0'} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} '@swc/helpers@0.5.23': resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + '@tanstack/form-core@1.20.0': + resolution: {integrity: sha512-FGlKvcsusOf4756vtN1EoDI4h50r4/11eTcpF3NcnE04N/bSn2gP7cdhG6tYA0lJWzM9H1pNIzZ86uZ4MHB9eA==} + + '@tanstack/react-form@1.20.0': + resolution: {integrity: sha512-1UfWqEYRnHr4cooGbHiTQqoqus8soNUH+RLD6UyhIQEvomOSQMX0JgX+zGSl08tIugrnWcAnh50n5T9IIs/Evw==} + peerDependencies: + '@tanstack/react-start': ^1.130.10 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@tanstack/react-start': + optional: true + + '@tanstack/react-store@0.7.7': + resolution: {integrity: sha512-qqT0ufegFRDGSof9D/VqaZgjNgp4tRPHZIJq2+QIHkMUtHjaJ0lYrrXjeIUJvjnTbgPfSD1XgOMEt0lmANn6Zg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/react-table@8.21.3': + resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + '@tanstack/store@0.7.7': + resolution: {integrity: sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==} + + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} @@ -1595,6 +1790,14 @@ packages: '@types/node@24.7.0': resolution: {integrity: sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==} + '@types/react-dom@19.2.4': + resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.18': + resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -1661,6 +1864,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + asn1js@3.0.6: + resolution: {integrity: sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==} + engines: {node: '>=12.0.0'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1672,6 +1879,9 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + brace-expansion@2.1.4: resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} @@ -1722,6 +1932,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} @@ -1734,6 +1947,12 @@ packages: supports-color: optional: true + decode-formdata@0.9.0: + resolution: {integrity: sha512-q5uwOjR3Um5YD+ZWPOF/1sGHVW9A5rCrRwITQChRXlmPkxDFBqCm4jNTIVdGHNH9OnR+V9MoZVgRhsFb+ARbUw==} + + devalue@5.9.0: + resolution: {integrity: sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -1761,6 +1980,10 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + ethereum-cryptography@3.2.0: + resolution: {integrity: sha512-Urr5YVsalH+Jo0sYkTkv1MyI9bLYZwW8BENZCeE1QYaTHETEYx0Nv/SVsWkSqpYrzweg6d8KMY1wTjH/1m/BIg==} + engines: {node: ^14.21.3 || >=16, npm: '>=9'} + eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} @@ -1910,6 +2133,9 @@ packages: resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} engines: {node: '>=0.10.0'} + js-md5@0.8.3: + resolution: {integrity: sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==} + js-sha3@0.8.0: resolution: {integrity: sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==} @@ -2106,9 +2332,25 @@ packages: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} + pvtsutils@1.3.6: + resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} + + pvutils@1.1.5: + resolution: {integrity: sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==} + engines: {node: '>=16.0.0'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + react-dom@19.2.8: + resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} + peerDependencies: + react: ^19.2.8 + + react@19.2.8: + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} + engines: {node: '>=0.10.0'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2135,6 +2377,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -2321,6 +2566,15 @@ packages: uri-js-replace@1.0.1: resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + uuid@11.1.1: + resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} + hasBin: true + valibot@1.4.2: resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==} peerDependencies: @@ -2476,6 +2730,151 @@ snapshots: '@adraffy/ens-normalize@1.11.1': {} + '@aws-sdk/client-kms@3.1101.0': + dependencies: + '@aws-sdk/core': 3.977.5 + '@aws-sdk/credential-provider-node': 3.972.77 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/fetch-http-handler': 5.6.13 + '@smithy/node-http-handler': 4.9.13 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/core@3.977.5': + dependencies: + '@aws-sdk/types': 3.974.2 + '@aws-sdk/xml-builder': 3.972.37 + '@aws/lambda-invoke-store': 0.3.0 + '@smithy/core': 3.31.1 + '@smithy/signature-v4': 5.6.12 + '@smithy/types': 4.16.1 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.66': + dependencies: + '@aws-sdk/core': 3.977.5 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.68': + dependencies: + '@aws-sdk/core': 3.977.5 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/fetch-http-handler': 5.6.13 + '@smithy/node-http-handler': 4.9.13 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.973.11': + dependencies: + '@aws-sdk/core': 3.977.5 + '@aws-sdk/credential-provider-env': 3.972.66 + '@aws-sdk/credential-provider-http': 3.972.68 + '@aws-sdk/credential-provider-login': 3.972.73 + '@aws-sdk/credential-provider-process': 3.972.66 + '@aws-sdk/credential-provider-sso': 3.973.10 + '@aws-sdk/credential-provider-web-identity': 3.972.72 + '@aws-sdk/nested-clients': 3.997.40 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/credential-provider-imds': 4.4.16 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.73': + dependencies: + '@aws-sdk/core': 3.977.5 + '@aws-sdk/nested-clients': 3.997.40 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.77': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.66 + '@aws-sdk/credential-provider-http': 3.972.68 + '@aws-sdk/credential-provider-ini': 3.973.11 + '@aws-sdk/credential-provider-process': 3.972.66 + '@aws-sdk/credential-provider-sso': 3.973.10 + '@aws-sdk/credential-provider-web-identity': 3.972.72 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/credential-provider-imds': 4.4.16 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.66': + dependencies: + '@aws-sdk/core': 3.977.5 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.973.10': + dependencies: + '@aws-sdk/core': 3.977.5 + '@aws-sdk/nested-clients': 3.997.40 + '@aws-sdk/token-providers': 3.1102.0 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.72': + dependencies: + '@aws-sdk/core': 3.977.5 + '@aws-sdk/nested-clients': 3.997.40 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.40': + dependencies: + '@aws-sdk/core': 3.977.5 + '@aws-sdk/signature-v4-multi-region': 3.996.43 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/fetch-http-handler': 5.6.13 + '@smithy/node-http-handler': 4.9.13 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.43': + dependencies: + '@aws-sdk/types': 3.974.2 + '@smithy/signature-v4': 5.6.12 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1102.0': + dependencies: + '@aws-sdk/core': 3.977.5 + '@aws-sdk/nested-clients': 3.997.40 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/types@3.974.2': + dependencies: + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.37': + dependencies: + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.3.0': {} + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -2656,6 +3055,22 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true + '@ethereumjs/rlp@10.1.2': {} + + '@ethereumjs/util@10.1.2': + dependencies: + '@ethereumjs/rlp': 10.1.2 + '@noble/curves': 2.2.0 + '@noble/hashes': 2.2.0 + + '@ethereumjs/wallet@10.0.0': + dependencies: + '@ethereumjs/util': 10.1.2 + '@scure/base': 1.2.6 + ethereum-cryptography: 3.2.0 + js-md5: 0.8.3 + uuid: 11.1.1 + '@gerrit0/mini-shiki@3.23.0': dependencies: '@shikijs/engine-oniguruma': 3.23.0 @@ -2767,10 +3182,18 @@ snapshots: '@noble/ciphers@1.3.0': {} + '@noble/curves@1.9.0': + dependencies: + '@noble/hashes': 1.8.0 + '@noble/curves@1.9.1': dependencies: '@noble/hashes': 1.8.0 + '@noble/curves@2.2.0': + dependencies: + '@noble/hashes': 2.2.0 + '@noble/hashes@1.8.0': {} '@noble/hashes@2.2.0': {} @@ -3115,12 +3538,76 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} + '@smithy/core@3.31.1': + dependencies: + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.4.16': + dependencies: + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.6.13': + dependencies: + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.9.13': + dependencies: + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/signature-v4@5.6.12': + dependencies: + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/types@4.16.1': + dependencies: + tslib: 2.8.1 + '@standard-schema/spec@1.1.0': {} '@swc/helpers@0.5.23': dependencies: tslib: 2.8.1 + '@tanstack/form-core@1.20.0': + dependencies: + '@tanstack/store': 0.7.7 + + '@tanstack/react-form@1.20.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@tanstack/form-core': 1.20.0 + '@tanstack/react-store': 0.7.7(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + decode-formdata: 0.9.0 + devalue: 5.9.0 + react: 19.2.8 + transitivePeerDependencies: + - react-dom + + '@tanstack/react-store@0.7.7(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@tanstack/store': 0.7.7 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + use-sync-external-store: 1.6.0(react@19.2.8) + + '@tanstack/react-table@8.21.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@tanstack/table-core': 8.21.3 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@tanstack/store@0.7.7': {} + + '@tanstack/table-core@8.21.3': {} + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 @@ -3149,6 +3636,14 @@ snapshots: dependencies: undici-types: 7.14.0 + '@types/react-dom@19.2.4(@types/react@19.2.18)': + dependencies: + '@types/react': 19.2.18 + + '@types/react@19.2.18': + dependencies: + csstype: 3.2.3 + '@types/unist@3.0.3': {} '@vitest/expect@4.1.2': @@ -3224,12 +3719,20 @@ snapshots: argparse@2.0.1: {} + asn1js@3.0.6: + dependencies: + pvtsutils: 1.3.6 + pvutils: 1.1.5 + tslib: 2.8.1 + assertion-error@2.0.1: {} balanced-match@1.0.2: {} balanced-match@4.0.4: {} + bowser@2.14.1: {} + brace-expansion@2.1.4: dependencies: balanced-match: 1.0.2 @@ -3273,6 +3776,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + csstype@3.2.3: {} + date-fns@4.1.0: {} debug@4.4.3(supports-color@10.2.2): @@ -3281,6 +3786,10 @@ snapshots: optionalDependencies: supports-color: 10.2.2 + decode-formdata@0.9.0: {} + + devalue@5.9.0: {} + emoji-regex@10.6.0: {} entities@4.5.0: {} @@ -3351,6 +3860,14 @@ snapshots: dependencies: '@types/estree': 1.0.9 + ethereum-cryptography@3.2.0: + dependencies: + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.0 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + eventemitter3@5.0.1: {} eventemitter3@5.0.4: {} @@ -3479,6 +3996,8 @@ snapshots: js-levenshtein@1.1.6: {} + js-md5@0.8.3: {} + js-sha3@0.8.0: {} js-tokens@4.0.0: {} @@ -3760,8 +4279,21 @@ snapshots: punycode.js@2.3.1: {} + pvtsutils@1.3.6: + dependencies: + tslib: 2.8.1 + + pvutils@1.1.5: {} + queue-microtask@1.2.3: {} + react-dom@19.2.8(react@19.2.8): + dependencies: + react: 19.2.8 + scheduler: 0.27.0 + + react@19.2.8: {} + require-from-string@2.0.2: {} resolve-pkg-maps@1.0.0: {} @@ -3811,6 +4343,8 @@ snapshots: dependencies: queue-microtask: 1.2.3 + scheduler@0.27.0: {} + semver@5.7.2: {} shebang-command@2.0.0: @@ -3957,6 +4491,12 @@ snapshots: uri-js-replace@1.0.1: {} + use-sync-external-store@1.6.0(react@19.2.8): + dependencies: + react: 19.2.8 + + uuid@11.1.1: {} + valibot@1.4.2(typescript@6.0.2): optionalDependencies: typescript: 6.0.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 625342d7..9d08733d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -34,12 +34,16 @@ enablePrePostScripts: true # dependency versions, per the TIB's non-goal; relaxing any of these is a separate, deliberate bump. catalog: '@internationalized/date': 3.12.1 + '@tanstack/react-form': 1.20.0 + '@tanstack/react-table': 8.21.3 '@loglayer/transport-betterstack': 2.2.0 '@morpho-org/midnight-sdk': 1.3.0 '@morpho-org/morpho-ts': 2.8.0 '@morpho-org/viem-dlc': 0.0.11 '@types/lodash-es': ^4.17.12 '@types/node': 24.7.0 + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4 date-fns: 4.1.0 esbuild: 0.28.1 execa: 9.6.0 @@ -51,6 +55,8 @@ catalog: loglayer: 9.3.0 openapi-fetch: 0.17.0 openapi-typescript: ^7.13.0 + react: 19.2.8 + react-dom: 19.2.8 oxfmt: ^0.36.0 # Pinned to what bun.lock resolved for `^1.46.0`. Newer minors change rule behavior and want # oxlint-tsgolint >= 0.24 — bump both together, deliberately, in a separate change. From 6c9dac2f3654360627be0b930e9b70942720cfd3 Mon Sep 17 00:00:00 2001 From: Hayden Shively Date: Fri, 7 Aug 2026 11:45:01 -0500 Subject: [PATCH 05/14] fix(market-making): build playground with vite --- .../scripts/playground-build-arguments.mjs | 21 ++++++++----------- .../scripts/playground-build.mjs | 4 ++-- .../scripts/playground-build.test.mjs | 8 +++---- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/bots/market-making/scripts/playground-build-arguments.mjs b/bots/market-making/scripts/playground-build-arguments.mjs index 25b071fa..2631938e 100644 --- a/bots/market-making/scripts/playground-build-arguments.mjs +++ b/bots/market-making/scripts/playground-build-arguments.mjs @@ -1,17 +1,14 @@ export const productionPlaygroundBuildArguments = outdir => [ 'build', - 'playground/index.html', - '--outdir', + 'playground', + '--outDir', outdir, '--target', - 'browser', - '--production', - '--entry-naming', - '[name].[ext]', - '--asset-naming', - '[name].[ext]', - '--define', - 'process.env.NODE_ENV="production"', - '--sourcemap=none', - '--env=disable' + 'es2022', + '--minify', + 'esbuild', + '--assetsDir', + '.', + '--sourcemap', + 'false' ] diff --git a/bots/market-making/scripts/playground-build.mjs b/bots/market-making/scripts/playground-build.mjs index 436f5536..d025b863 100644 --- a/bots/market-making/scripts/playground-build.mjs +++ b/bots/market-making/scripts/playground-build.mjs @@ -103,8 +103,8 @@ const buildIdentity = { dev: Number(created.dev), ino: Number(created.ino) } let keepTemporary = false try { - const bun = process.env.BUN_EXE || 'bun' - const child = spawn(bun, productionPlaygroundBuildArguments(buildOutdir), { + const vite = process.env.BUN_EXE || 'vite' + const child = spawn(vite, productionPlaygroundBuildArguments(buildOutdir), { cwd: packageRoot, env: { ...process.env, NODE_ENV: 'production' }, shell: false, diff --git a/bots/market-making/scripts/playground-build.test.mjs b/bots/market-making/scripts/playground-build.test.mjs index df366453..6766a64d 100644 --- a/bots/market-making/scripts/playground-build.test.mjs +++ b/bots/market-making/scripts/playground-build.test.mjs @@ -107,7 +107,7 @@ test('--temporary creates a private owned OS-temp directory, reports its exact p const { writeFileSync } = require('node:fs') const { tmpdir } = require('node:os') const { basename, dirname } = require('node:path') -const outdir = process.argv[process.argv.indexOf('--outdir') + 1] +const outdir = process.argv[process.argv.indexOf('--outDir') + 1] if (dirname(outdir) !== tmpdir()) throw new Error('not under OS temp') if (!basename(outdir).startsWith('market-making-playground-dist-')) throw new Error('wrong prefix') writeFileSync(outdir + '/index.html', '') @@ -133,7 +133,7 @@ test('failed temporary build removes its internally-created partial output', asy ) const fakeBun = await makeFakeBun(` const { writeFileSync } = require('node:fs') -const outdir = process.argv[process.argv.indexOf('--outdir') + 1] +const outdir = process.argv[process.argv.indexOf('--outDir') + 1] writeFileSync(outdir + '/partial.bin', 'partial') process.exit(23) `) @@ -153,7 +153,7 @@ test('canonical build stages in owned OS temp, publishes finalized files, and re const { writeFileSync } = require('node:fs') const { tmpdir } = require('node:os') const { basename, dirname } = require('node:path') -const outdir = process.argv[process.argv.indexOf('--outdir') + 1] +const outdir = process.argv[process.argv.indexOf('--outDir') + 1] if (dirname(outdir) !== tmpdir()) throw new Error('canonical staging not in OS temp') if (!basename(outdir).startsWith('market-making-playground-staging-')) throw new Error('wrong staging prefix') writeFileSync(outdir + '/index.html', '') @@ -182,7 +182,7 @@ test('failed canonical build preserves the prior index and removes OS-temp stagi ) const fakeBun = await makeFakeBun(` const { writeFileSync } = require('node:fs') -const outdir = process.argv[process.argv.indexOf('--outdir') + 1] +const outdir = process.argv[process.argv.indexOf('--outDir') + 1] writeFileSync(outdir + '/partial', 'never publish me') process.exit(29) `) From 2f09bd5b2b53197b906279642447825d47801f6a Mon Sep 17 00:00:00 2001 From: Hayden Shively Date: Fri, 7 Aug 2026 14:14:40 -0500 Subject: [PATCH 06/14] ci(checks): run browser smoke with pnpm --- .github/workflows/checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index c5915946..80444f50 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -59,7 +59,7 @@ jobs: RPC_URL_8453: ${{ secrets.RPC_URL_8453 }} - name: Run browser smoke tests - run: bun test:browser + run: pnpm --filter @morpho-org/market-making-bot run playground:smoke:test Dead-Code: runs-on: ubuntu-latest From 0acd366ff19ec52185f87065fe13c1c904821615 Mon Sep 17 00:00:00 2001 From: Hayden Shively Date: Fri, 7 Aug 2026 14:46:40 -0500 Subject: [PATCH 07/14] fix(market-making): preserve relative playground assets --- bots/market-making/scripts/playground-build-arguments.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bots/market-making/scripts/playground-build-arguments.mjs b/bots/market-making/scripts/playground-build-arguments.mjs index 2631938e..8d3e0266 100644 --- a/bots/market-making/scripts/playground-build-arguments.mjs +++ b/bots/market-making/scripts/playground-build-arguments.mjs @@ -9,6 +9,8 @@ export const productionPlaygroundBuildArguments = outdir => [ 'esbuild', '--assetsDir', '.', + '--base', + './', '--sourcemap', 'false' ] From bede1929cfad7fd1ce142f01cfb26e6370c7ad09 Mon Sep 17 00:00:00 2001 From: Hayden Shively Date: Fri, 7 Aug 2026 14:52:30 -0500 Subject: [PATCH 08/14] test(market-making): stabilize node playground smoke --- bots/market-making/scripts/playground-smoke.browser.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bots/market-making/scripts/playground-smoke.browser.mjs b/bots/market-making/scripts/playground-smoke.browser.mjs index 33af5bd1..b754e76b 100644 --- a/bots/market-making/scripts/playground-smoke.browser.mjs +++ b/bots/market-making/scripts/playground-smoke.browser.mjs @@ -95,7 +95,7 @@ const assertPortClosed = port => const spawnSmoke = ({ env = process.env } = {}) => { const child = spawnOwnedProcess(process.execPath, [smokeScript], { - env, + env: { ...env, NODE_DISABLE_COMPILE_CACHE: '1' }, stdio: ['ignore', 'pipe', 'pipe'] }) child.stdout.setEncoding('utf8') @@ -444,6 +444,8 @@ setInterval(() => {}, 1000) ...process.env, CHROMIUM_PATH: chromiumPath, PATH: `${bin}${delimiter}${process.env.PATH ?? ''}`, + BUN_EXE: fakeBun, + NODE_DISABLE_COMPILE_CACHE: '1', PLAYGROUND_SMOKE_BUILD_CAPTURE_BARRIER_FILE: captureBarrierFile, PLAYGROUND_SMOKE_BUILD_CAPTURE_FILE: captureFile, PLAYGROUND_SMOKE_BUILD_TIMEOUT_MS: String(timeoutMs), From c4bc9c0ab0aa7f4ed276049b676b2010730844a7 Mon Sep 17 00:00:00 2001 From: Hayden Shively Date: Tue, 11 Aug 2026 09:28:08 -0500 Subject: [PATCH 09/14] fix(market-making): port main additions off bun --- bots/market-making/scripts/deploy-railway.ts | 2 +- bots/market-making/test/bootstrap.test.ts | 14 +++++++------- .../bootstrap-reference-rate.service.test.ts | 8 ++++---- .../test/playground/field-visibility.utils.test.ts | 2 +- .../test/scripts/railway.utils.test.ts | 10 +++++----- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/bots/market-making/scripts/deploy-railway.ts b/bots/market-making/scripts/deploy-railway.ts index ba176187..fa04f9d3 100644 --- a/bots/market-making/scripts/deploy-railway.ts +++ b/bots/market-making/scripts/deploy-railway.ts @@ -133,7 +133,7 @@ const ensureService = async () => { const existingService = services.find(service => service.name === SERVICE) if (existingService) return { service: existingService, isFreshService: false } - assertFreshRailwayReferenceProvisioning(Bun.env, true) + assertFreshRailwayReferenceProvisioning(process.env, true) const { data, error } = await tryCatch( $`railway add --service ${SERVICE} --json`.then(result => result.stdout) ) diff --git a/bots/market-making/test/bootstrap.test.ts b/bots/market-making/test/bootstrap.test.ts index 415b5113..35b3817c 100644 --- a/bots/market-making/test/bootstrap.test.ts +++ b/bots/market-making/test/bootstrap.test.ts @@ -334,7 +334,7 @@ describe('createApplication', () => { test('starts a hardcoded-only bootstrap workflow without Blue reference readiness', async () => { const state = readyState() - const checkReference = mock(async () => { + const checkReference = vi.fn(async () => { throw new Error('Blue archive unavailable') }) state.checkReference = checkReference @@ -387,7 +387,7 @@ describe('createApplication', () => { test('keeps Blue reference readiness fail-closed for variable-rate bootstrap workflows', async () => { const state = readyState() - const checkReference = mock(async () => { + const checkReference = vi.fn(async () => { throw new Error('Blue archive unavailable') }) state.checkReference = checkReference @@ -410,10 +410,10 @@ describe('createApplication', () => { test('keeps Blue reference readiness fail-closed for variable-rate ladder workflows', async () => { const state = readyState() - const checkReference = mock(async () => { + const checkReference = vi.fn(async () => { throw new Error('Blue archive unavailable') }) - const createLadderAdapters = mock(() => { + const createLadderAdapters = vi.fn(() => { throw new Error('ladder adapters must not start') }) state.checkReference = checkReference @@ -438,7 +438,7 @@ describe('createApplication', () => { test('starts a hardcoded-only ladder workflow without Blue reference readiness', async () => { const state = readyState() - const checkReference = mock(async () => { + const checkReference = vi.fn(async () => { throw new Error('Blue archive unavailable') }) state.checkReference = checkReference @@ -482,7 +482,7 @@ describe('createApplication', () => { test('setup-check composes hardcoded bootstrap and ladder strategies without Blue reference readiness', async () => { const state = readyState() - const checkReference = mock(async () => { + const checkReference = vi.fn(async () => { throw new Error('Blue archive unavailable') }) state.checkReference = checkReference @@ -971,7 +971,7 @@ describe('createApplication', () => { }) test('combined start composes hardcoded bootstrap and ladder workflows without Blue reference readiness', async () => { - const checkReference = mock(async () => { + const checkReference = vi.fn(async () => { throw new Error('Blue archive unavailable') }) const state = readyState() diff --git a/bots/market-making/test/infrastructure/bootstrap/bootstrap-reference-rate.service.test.ts b/bots/market-making/test/infrastructure/bootstrap/bootstrap-reference-rate.service.test.ts index 64e14694..0e25f497 100644 --- a/bots/market-making/test/infrastructure/bootstrap/bootstrap-reference-rate.service.test.ts +++ b/bots/market-making/test/infrastructure/bootstrap/bootstrap-reference-rate.service.test.ts @@ -1,6 +1,6 @@ import type { Hex } from 'viem' -import { afterEach, describe, expect, setSystemTime, test } from 'vitest' +import { afterEach, describe, expect, test, vi } from 'vitest' import { BootstrapAdapterError } from '../../../src/infrastructure/bootstrap/bootstrap-adapter.error' import { @@ -11,7 +11,7 @@ import { const marketId = `0x${'11'.repeat(32)}` as const const secondMarketId = `0x${'22'.repeat(32)}` as const -afterEach(() => setSystemTime()) +afterEach(() => vi.useRealTimers()) describe('StrategyBootstrapReferenceRateService', () => { test('uses a configured hardcoded target without reading the Blue variable-rate average', async () => { @@ -68,9 +68,9 @@ describe('StrategyBootstrapReferenceRateService', () => { { readRate: async () => ({ mode: 'variable', rateBps: 500n, observationId: 'hour:1' }) } ) - setSystemTime(new Date(3_599_000)) + vi.setSystemTime(new Date(3_599_000)) const first = await service.readRate(marketId) - setSystemTime(new Date(3_600_000)) + vi.setSystemTime(new Date(3_600_000)) const second = await service.readRate(marketId) expect(first.observationId).toBe('static:400:hour:0') diff --git a/bots/market-making/test/playground/field-visibility.utils.test.ts b/bots/market-making/test/playground/field-visibility.utils.test.ts index 5ce2311b..0169b47c 100644 --- a/bots/market-making/test/playground/field-visibility.utils.test.ts +++ b/bots/market-making/test/playground/field-visibility.utils.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from 'bun:test' +import { describe, expect, test } from 'vitest' import type { TargetRateInput } from '../../playground/model' diff --git a/bots/market-making/test/scripts/railway.utils.test.ts b/bots/market-making/test/scripts/railway.utils.test.ts index 96defedb..a8d236f7 100644 --- a/bots/market-making/test/scripts/railway.utils.test.ts +++ b/bots/market-making/test/scripts/railway.utils.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, test } from 'vitest' import { readFileSync } from 'node:fs' +import { describe, expect, test } from 'vitest' import { assertFreshRailwayReferenceProvisioning, @@ -107,9 +107,9 @@ describe('Railway CLI output parsing', () => { expect( deploy.indexOf('assertFreshRailwayReferenceProvisioning(process.env, true)') ).toBeGreaterThan(-1) - expect(deploy.indexOf('assertFreshRailwayReferenceProvisioning(process.env, true)')).toBeLessThan( - deploy.indexOf('railway add --service') - ) + expect( + deploy.indexOf('assertFreshRailwayReferenceProvisioning(process.env, true)') + ).toBeLessThan(deploy.indexOf('railway add --service')) }) test('synchronizes every optional variable with explicit safe defaults', () => { @@ -175,7 +175,7 @@ describe('Railway CLI output parsing', () => { }) test('allows Compose deployments to omit inactive reference configuration', () => { - const compose = readFileSync(resolve(import.meta.dir, '../../docker-compose.yml'), 'utf8') + const compose = readFileSync(new URL('../../docker-compose.yml', import.meta.url), 'utf8') expect(compose).toContain('REFERENCE_RPC_URL: ${REFERENCE_RPC_URL:-}') expect(compose).toContain('REFERENCE_MARKET_ID: ${REFERENCE_MARKET_ID:-}') From 058c2767e4f8a57a606103d06e39f2deba8c6b24 Mon Sep 17 00:00:00 2001 From: Hayden Shively Date: Tue, 11 Aug 2026 10:55:10 -0500 Subject: [PATCH 10/14] fix(repo): cover playground checks with pnpm --- bots/blue-liquidation/package.json | 2 +- bots/market-making/package.json | 1 + .../scripts/playground-build.mjs | 2 +- .../scripts/playground-build.test.mjs | 15 +++++--- .../scripts/playground-serve-support.mjs | 4 +-- .../scripts/playground-serve.test.mjs | 8 +++-- .../scripts/playground-smoke-suite.test.mjs | 35 +++++++++++-------- .../scripts/playground-smoke.browser.mjs | 2 +- .../scripts/playground-smoke.test.mjs | 4 +++ bots/midnight-liquidation/package.json | 2 +- package.json | 5 +-- 11 files changed, 50 insertions(+), 30 deletions(-) diff --git a/bots/blue-liquidation/package.json b/bots/blue-liquidation/package.json index 6ba6e135..b76fda2b 100644 --- a/bots/blue-liquidation/package.json +++ b/bots/blue-liquidation/package.json @@ -8,7 +8,7 @@ "build": "tsx scripts/build.ts", "deploy:railway": "tsx scripts/deploy-railway.ts", "prestart": "pnpm --filter \"{.}...\" --if-present run build", - "probe:lens": "node --env-file-if-exists=.env dist/scripts/probe-live-lens.js", + "probe:lens": "pnpm --filter \"{.}...\" --if-present run build && node --env-file-if-exists=.env dist/scripts/probe-live-lens.js", "start": "node --env-file-if-exists=.env dist/src/index.js", "typecheck": "soltag && tsc --noEmit" }, diff --git a/bots/market-making/package.json b/bots/market-making/package.json index 174d69ef..d1d41758 100644 --- a/bots/market-making/package.json +++ b/bots/market-making/package.json @@ -19,6 +19,7 @@ "playground:smoke": "node scripts/playground-smoke.mjs", "playground:smoke:output-probe": "node scripts/playground-smoke-output-probe.mjs", "playground:smoke:test": "node --test scripts/playground-smoke.browser.mjs", + "playground:test": "node --test scripts/playground-atomic-publish.test.mjs scripts/playground-build.test.mjs scripts/playground-process.test.mjs scripts/playground-serve.test.mjs scripts/playground-smoke-suite.test.mjs scripts/playground-smoke.test.mjs", "start": "node --env-file-if-exists=.env dist/src/index.js", "test:e2e": "vitest run test/e2e", "typecheck": "tsc --noEmit" diff --git a/bots/market-making/scripts/playground-build.mjs b/bots/market-making/scripts/playground-build.mjs index d025b863..5f4a2dd1 100644 --- a/bots/market-making/scripts/playground-build.mjs +++ b/bots/market-making/scripts/playground-build.mjs @@ -103,7 +103,7 @@ const buildIdentity = { dev: Number(created.dev), ino: Number(created.ino) } let keepTemporary = false try { - const vite = process.env.BUN_EXE || 'vite' + const vite = process.env.VITE_EXE || 'vite' const child = spawn(vite, productionPlaygroundBuildArguments(buildOutdir), { cwd: packageRoot, env: { ...process.env, NODE_ENV: 'production' }, diff --git a/bots/market-making/scripts/playground-build.test.mjs b/bots/market-making/scripts/playground-build.test.mjs index 6766a64d..e115ef31 100644 --- a/bots/market-making/scripts/playground-build.test.mjs +++ b/bots/market-making/scripts/playground-build.test.mjs @@ -14,7 +14,7 @@ import { } from 'node:fs/promises' import { tmpdir } from 'node:os' import { basename, dirname, join } from 'node:path' -import { afterEach, test } from 'node:test' +import { afterEach, beforeEach, test } from 'node:test' import { fileURLToPath } from 'node:url' import { CANONICAL_PUBLISH_TEMP_MARKER } from './playground-atomic-publish.mjs' @@ -28,6 +28,11 @@ const buildScript = join(packageRoot, 'scripts/playground-build.mjs') const canonical = join(packageRoot, 'playground', 'dist') const staleCanonicalAsset = join(canonical, 'offline-clean-only.stale') const owned = new Set() + +beforeEach(t => { + if (process.platform !== 'linux') t.skip('requires Linux canonical-path semantics') +}) + const makeCanonicalChain = async () => { const container = await mkdtemp(join(tmpdir(), 'playground-path-chain-')) owned.add(container) @@ -113,7 +118,7 @@ if (!basename(outdir).startsWith('market-making-playground-dist-')) throw new Er writeFileSync(outdir + '/index.html', '') writeFileSync(outdir + '/index.js', 'globalThis.temporary = true') `) - const result = await runBuild(['--temporary'], { BUN_EXE: fakeBun }) + const result = await runBuild(['--temporary'], { VITE_EXE: fakeBun }) assert.equal(result.code, 0, result.stderr) const record = outputRecord(result.stdout) assert.deepEqual(Object.keys(record).sort(), ['kind', 'mode', 'path']) @@ -137,7 +142,7 @@ const outdir = process.argv[process.argv.indexOf('--outDir') + 1] writeFileSync(outdir + '/partial.bin', 'partial') process.exit(23) `) - const result = await runBuild(['--temporary'], { BUN_EXE: fakeBun }) + const result = await runBuild(['--temporary'], { VITE_EXE: fakeBun }) assert.notEqual(result.code, 0) assert.match(result.stderr, /Production playground build failed with exit code 23/) const after = (await readdir(tmpdir())).filter( @@ -160,7 +165,7 @@ writeFileSync(outdir + '/index.html', ' { export const ensureFrozenDependencies = async ({ repoRoot, packageRoot, - executable = 'bun', + executable = 'pnpm', env = process.env, chmodFile = chmod, platform = process.platform, @@ -185,7 +185,7 @@ export const ensureFrozenDependencies = async ({ const snapshots = await snapshotWorkspaceBinModes(repoRoot, { packageRoot, platform }) let installError try { - console.log('Checking workspace dependencies with bun install --frozen-lockfile...') + console.log(`Checking workspace dependencies with ${executable} install --frozen-lockfile...`) const result = await processRunner({ executable, args: ['install', '--frozen-lockfile'], diff --git a/bots/market-making/scripts/playground-serve.test.mjs b/bots/market-making/scripts/playground-serve.test.mjs index 2ba075fc..be3826c2 100644 --- a/bots/market-making/scripts/playground-serve.test.mjs +++ b/bots/market-making/scripts/playground-serve.test.mjs @@ -30,6 +30,10 @@ import { } from './playground-serve-support.mjs' import { prepareFreshDist, startStaticServer } from './playground-smoke-support.mjs' +test.beforeEach(t => { + if (process.platform !== 'linux') t.skip('requires Linux process and socket semantics') +}) + const root = fileURLToPath(new URL('..', import.meta.url)) const launcher = fileURLToPath(new URL('./playground-serve.mjs', import.meta.url)) const temporaryDirectories = [] @@ -162,8 +166,8 @@ test('installed package dependencies still run the fast frozen lockfile check', } }) assert.deepEqual( - calls.map(call => call.args), - [['install', '--frozen-lockfile']] + calls.map(call => [call.executable, call.args]), + [['pnpm', ['install', '--frozen-lockfile']]] ) }) diff --git a/bots/market-making/scripts/playground-smoke-suite.test.mjs b/bots/market-making/scripts/playground-smoke-suite.test.mjs index 613cde4b..c4052a0c 100644 --- a/bots/market-making/scripts/playground-smoke-suite.test.mjs +++ b/bots/market-making/scripts/playground-smoke-suite.test.mjs @@ -11,7 +11,7 @@ const scriptsDirectory = fileURLToPath(new URL('.', import.meta.url)) const readJson = async path => JSON.parse(await readFile(path, 'utf8')) -test('CI keeps real-browser tests out of Bun discovery and runs them explicitly after Bun', async () => { +test('CI runs every Node playground suite after the Vitest unit suite', async () => { const [rootPackage, marketMakingPackage, workflow, scriptNames] = await Promise.all([ readJson(join(root, 'package.json')), readJson(join(root, 'bots/market-making/package.json')), @@ -19,30 +19,35 @@ test('CI keeps real-browser tests out of Bun discovery and runs them explicitly readdir(scriptsDirectory) ]) - assert.equal(rootPackage.scripts.test, 'bun test') assert.equal( - rootPackage.scripts['test:browser'], - 'bun run --filter @morpho-org/market-making-bot playground:smoke:test' + rootPackage.scripts.test, + 'vitest run && pnpm --filter @morpho-org/market-making-bot run playground:test' + ) + assert.equal( + marketMakingPackage.scripts['playground:test'], + 'node --test scripts/playground-atomic-publish.test.mjs scripts/playground-build.test.mjs scripts/playground-process.test.mjs scripts/playground-serve.test.mjs scripts/playground-smoke-suite.test.mjs scripts/playground-smoke.test.mjs' ) assert.equal( marketMakingPackage.scripts['playground:smoke:test'], 'node --test scripts/playground-smoke.browser.mjs' ) - const unitStep = workflow.indexOf('- name: Run unit tests\n run: bun test') + const unitStep = workflow.indexOf('- name: Run unit tests\n run: pnpm test') const browserStep = workflow.indexOf( - '- name: Run browser smoke tests\n run: bun test:browser' + '- name: Run browser smoke tests\n run: pnpm --filter @morpho-org/market-making-bot run playground:smoke:test' ) - assert.notEqual(unitStep, -1) - assert.ok(browserStep > unitStep, 'browser smoke command must run after the Bun suite') + assert.ok(unitStep >= 0) + assert.ok(browserStep > unitStep, 'browser smoke command must run after the unit suite') - const bunDiscoveredTests = scriptNames.filter( - name => /\.test\.[cm]?[jt]s$/.test(name) && name !== 'playground-smoke-suite.test.mjs' + const nodeTests = scriptNames.filter( + name => /\.test\.mjs$/.test(name) && name !== 'playground-smoke-suite.test.mjs' ) - const bunSources = await Promise.all( - bunDiscoveredTests.map(name => readFile(join(scriptsDirectory, name), 'utf8')) + for (const name of nodeTests) + assert.ok(marketMakingPackage.scripts['playground:test'].includes(name)) + const nodeSources = await Promise.all( + nodeTests.map(name => readFile(join(scriptsDirectory, name), 'utf8')) ) - for (const source of bunSources) { + for (const source of nodeSources) { assert.doesNotMatch(source, /after Chromium readiness/) assert.doesNotMatch(source, /two complete smoke runs/) assert.doesNotMatch(source, /test\.skip/) @@ -115,7 +120,7 @@ test('browser lifecycle uses separate bounded build, startup, body, UI, CDP, and assert.match(smokeSource, /panel: 'panel-ladder-string'/) }) -test('the declared root lint path effectively checks only the playground smoke mjs files', async () => { +test('the root lint command explicitly checks the playground smoke mjs files', async () => { const [rootPackage, workflow] = await Promise.all([ readJson(join(root, 'package.json')), readFile(join(root, '.github/workflows/checks.yml'), 'utf8') @@ -125,7 +130,7 @@ test('the declared root lint path effectively checks only the playground smoke m rootPackage.scripts['lint:playground-smoke'], 'oxlint --config bots/market-making/scripts/playground-smoke.oxlintrc.json bots/market-making/scripts/playground-smoke*.mjs' ) - assert.match(rootPackage.scripts.lint, /bun run lint:playground-smoke/) + assert.match(rootPackage.scripts.lint, /pnpm run lint:playground-smoke/) assert.match(workflow, /- name: Run lint\n run: pnpm lint/) }) diff --git a/bots/market-making/scripts/playground-smoke.browser.mjs b/bots/market-making/scripts/playground-smoke.browser.mjs index b754e76b..8967a72a 100644 --- a/bots/market-making/scripts/playground-smoke.browser.mjs +++ b/bots/market-making/scripts/playground-smoke.browser.mjs @@ -444,7 +444,7 @@ setInterval(() => {}, 1000) ...process.env, CHROMIUM_PATH: chromiumPath, PATH: `${bin}${delimiter}${process.env.PATH ?? ''}`, - BUN_EXE: fakeBun, + VITE_EXE: fakeBun, NODE_DISABLE_COMPILE_CACHE: '1', PLAYGROUND_SMOKE_BUILD_CAPTURE_BARRIER_FILE: captureBarrierFile, PLAYGROUND_SMOKE_BUILD_CAPTURE_FILE: captureFile, diff --git a/bots/market-making/scripts/playground-smoke.test.mjs b/bots/market-making/scripts/playground-smoke.test.mjs index d8615c5c..d9b59562 100644 --- a/bots/market-making/scripts/playground-smoke.test.mjs +++ b/bots/market-making/scripts/playground-smoke.test.mjs @@ -35,6 +35,10 @@ import { waitForReadiness } from './playground-smoke-support.mjs' +test.beforeEach(t => { + if (process.platform !== 'linux') t.skip('requires Linux /proc process inspection') +}) + const temporaryDirectories = [] const smokeScript = fileURLToPath(new URL('./playground-smoke.mjs', import.meta.url)) const temporaryDirectory = async prefix => { diff --git a/bots/midnight-liquidation/package.json b/bots/midnight-liquidation/package.json index 02d7a7df..15a52ead 100644 --- a/bots/midnight-liquidation/package.json +++ b/bots/midnight-liquidation/package.json @@ -8,7 +8,7 @@ "build": "tsx scripts/build.ts", "deploy:railway": "tsx scripts/deploy-railway.ts", "prestart": "pnpm --filter \"{.}...\" --if-present run build", - "seed:positions": "node --env-file-if-exists=.env dist/scripts/seed-liquidatable-positions.js", + "seed:positions": "pnpm --filter \"{.}...\" --if-present run build && node --env-file-if-exists=.env dist/scripts/seed-liquidatable-positions.js", "start": "node --env-file-if-exists=.env dist/src/index.js", "typecheck": "soltag && tsc --noEmit" }, diff --git a/package.json b/package.json index 85ccd160..ff175e30 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,11 @@ "format": "oxfmt", "format:check": "oxfmt --check", "knip": "knip", - "lint": "oxlint --no-error-on-unmatched-pattern", + "lint": "oxlint --no-error-on-unmatched-pattern && pnpm run lint:playground-smoke", "lint:fix": "oxlint --fix --no-error-on-unmatched-pattern", + "lint:playground-smoke": "oxlint --config bots/market-making/scripts/playground-smoke.oxlintrc.json bots/market-making/scripts/playground-smoke*.mjs", "prepare": "husky", - "test": "vitest run" + "test": "vitest run && pnpm --filter @morpho-org/market-making-bot run playground:test" }, "devDependencies": { "@repo/typescript-config": "workspace:*", From 4e85f0ca6d4bed55102a7f04dd891622edf95d70 Mon Sep 17 00:00:00 2001 From: Hayden Shively Date: Tue, 11 Aug 2026 10:59:19 -0500 Subject: [PATCH 11/14] test(market-making): rename migrated tool fixtures --- .../scripts/playground-build.test.mjs | 22 +++++++++---------- .../scripts/playground-serve.test.mjs | 20 ++++++++--------- .../scripts/playground-smoke.browser.mjs | 22 +++++++++---------- .../scripts/playground-smoke.test.mjs | 6 ++--- 4 files changed, 35 insertions(+), 35 deletions(-) diff --git a/bots/market-making/scripts/playground-build.test.mjs b/bots/market-making/scripts/playground-build.test.mjs index e115ef31..207b0ec0 100644 --- a/bots/market-making/scripts/playground-build.test.mjs +++ b/bots/market-making/scripts/playground-build.test.mjs @@ -42,10 +42,10 @@ const makeCanonicalChain = async () => { await mkdir(playground, { recursive: true }) return { container, packageRoot, playground, repoRoot } } -const makeFakeBun = async source => { - const root = await mkdtemp(join(tmpdir(), 'market-making-fake-bun-')) +const makeFakeVite = async source => { + const root = await mkdtemp(join(tmpdir(), 'market-making-fake-vite-')) owned.add(root) - const executable = join(root, 'bun') + const executable = join(root, 'vite') await writeFile(executable, `#!/usr/bin/env node\n${source}`) await chmod(executable, 0o755) return executable @@ -108,7 +108,7 @@ test('CLI exposes only canonical default and --temporary; arbitrary output flags }) test('--temporary creates a private owned OS-temp directory, reports its exact path, and leaves cleanup to caller', async () => { - const fakeBun = await makeFakeBun(` + const fakeVite = await makeFakeVite(` const { writeFileSync } = require('node:fs') const { tmpdir } = require('node:os') const { basename, dirname } = require('node:path') @@ -118,7 +118,7 @@ if (!basename(outdir).startsWith('market-making-playground-dist-')) throw new Er writeFileSync(outdir + '/index.html', '') writeFileSync(outdir + '/index.js', 'globalThis.temporary = true') `) - const result = await runBuild(['--temporary'], { VITE_EXE: fakeBun }) + const result = await runBuild(['--temporary'], { VITE_EXE: fakeVite }) assert.equal(result.code, 0, result.stderr) const record = outputRecord(result.stdout) assert.deepEqual(Object.keys(record).sort(), ['kind', 'mode', 'path']) @@ -136,13 +136,13 @@ test('failed temporary build removes its internally-created partial output', asy const before = new Set( (await readdir(tmpdir())).filter(name => name.startsWith('market-making-playground-dist-')) ) - const fakeBun = await makeFakeBun(` + const fakeVite = await makeFakeVite(` const { writeFileSync } = require('node:fs') const outdir = process.argv[process.argv.indexOf('--outDir') + 1] writeFileSync(outdir + '/partial.bin', 'partial') process.exit(23) `) - const result = await runBuild(['--temporary'], { VITE_EXE: fakeBun }) + const result = await runBuild(['--temporary'], { VITE_EXE: fakeVite }) assert.notEqual(result.code, 0) assert.match(result.stderr, /Production playground build failed with exit code 23/) const after = (await readdir(tmpdir())).filter( @@ -154,7 +154,7 @@ process.exit(23) test('canonical build stages in owned OS temp, publishes finalized files, and retains old assets', async () => { await mkdir(canonical, { recursive: true }) await writeFile(staleCanonicalAsset, 'retained until an explicit offline clean') - const fakeBun = await makeFakeBun(` + const fakeVite = await makeFakeVite(` const { writeFileSync } = require('node:fs') const { tmpdir } = require('node:os') const { basename, dirname } = require('node:path') @@ -165,7 +165,7 @@ writeFileSync(outdir + '/index.html', ' name.startsWith('market-making-playground-staging-')) ) - const fakeBun = await makeFakeBun(` + const fakeVite = await makeFakeVite(` const { writeFileSync } = require('node:fs') const outdir = process.argv[process.argv.indexOf('--outDir') + 1] writeFileSync(outdir + '/partial', 'never publish me') process.exit(29) `) - const result = await runBuild([], { VITE_EXE: fakeBun }) + const result = await runBuild([], { VITE_EXE: fakeVite }) assert.notEqual(result.code, 0) assert.match(result.stderr, /exit code 29/) assert.deepEqual(await readFile(join(canonical, 'index.html')), beforeIndex) diff --git a/bots/market-making/scripts/playground-serve.test.mjs b/bots/market-making/scripts/playground-serve.test.mjs index be3826c2..59457d06 100644 --- a/bots/market-making/scripts/playground-serve.test.mjs +++ b/bots/market-making/scripts/playground-serve.test.mjs @@ -97,8 +97,8 @@ const rawHttpRequest = (server, method, path = '/malformed%') => }) }) -const writeBlockingBun = async root => { - const executable = join(root, 'blocking-bun') +const writeBlockingPnpm = async root => { + const executable = join(root, 'blocking-pnpm') await writeFile( executable, `#!/bin/sh\ntrap '' TERM\nsh -c 'trap "" TERM; echo $$ > "$PID_FILE"; while :; do sleep 1; done' &\nwait\n`, @@ -136,7 +136,7 @@ test('a frozen install runs unconditionally and resolves partial workspace depen await writeResolvableDependencies(packageRoot, ['viem']) await mkdir(bin) await writeFile( - join(bin, 'bun'), + join(bin, 'pnpm'), `#!/usr/bin/env node\nconst { mkdirSync, writeFileSync } = require('node:fs')\nconst { join } = require('node:path')\nwriteFileSync(process.env.INSTALL_LOG, JSON.stringify(process.argv.slice(2)))\nfor (const name of ['viem', '@repo/bot-kit']) { const root = join(process.env.PACKAGE_ROOT, 'node_modules', name); mkdirSync(root, { recursive: true }); writeFileSync(join(root, 'package.json'), JSON.stringify({ name, main: 'index.js' })); writeFileSync(join(root, 'index.js'), '') }\n`, { mode: 0o755 } ) @@ -144,7 +144,7 @@ test('a frozen install runs unconditionally and resolves partial workspace depen await ensureFrozenDependencies({ repoRoot, packageRoot, - executable: join(bin, 'bun'), + executable: join(bin, 'pnpm'), env: { ...process.env, INSTALL_LOG: log, PACKAGE_ROOT: packageRoot } }) @@ -186,7 +186,7 @@ for (const [platform, originalMode] of [ const repoRoot = await temporaryDirectory(`playground-clean-install-${platform}-`) const packageRoot = join(repoRoot, 'bots/market-making') const entrypoint = join(packageRoot, 'src/index.ts') - const executable = join(repoRoot, 'fake-bun') + const executable = join(repoRoot, 'fake-pnpm') const source = 'console.log("package bin content must stay unchanged")\n' await writeResolvableDependencies(packageRoot) await mkdir(join(packageRoot, 'src'), { recursive: true }) @@ -352,12 +352,12 @@ test('workspace bin discovery skips escaping and symlink targets', async () => { test('frozen install failures report the exact command and exit code', async () => { const repoRoot = await temporaryDirectory('playground-install-failure-') const packageRoot = join(repoRoot, 'bots/market-making') - const executable = join(repoRoot, 'bun-failure') + const executable = join(repoRoot, 'pnpm-failure') await writeResolvableDependencies(packageRoot, []) await writeFile(executable, '#!/usr/bin/env node\nprocess.exit(23)\n', { mode: 0o755 }) await assert.rejects(ensureFrozenDependencies({ repoRoot, packageRoot, executable }), error => { - assert.match(error.message, /bun-failure install --frozen-lockfile failed with exit code 23/) + assert.match(error.message, /pnpm-failure install --frozen-lockfile failed with exit code 23/) return true }) }) @@ -365,7 +365,7 @@ test('frozen install failures report the exact command and exit code', async () test('successful install clearly lists dependencies that remain unresolved', async () => { const repoRoot = await temporaryDirectory('playground-unresolved-') const packageRoot = join(repoRoot, 'bots/market-making') - const executable = join(repoRoot, 'bun-noop') + const executable = join(repoRoot, 'pnpm-noop') await writeResolvableDependencies(packageRoot, []) await writeFile(executable, '#!/usr/bin/env node\n', { mode: 0o755 }) @@ -403,7 +403,7 @@ test('signal during frozen install kills its descendant tree', { timeout: 10_000 const packageRoot = join(repoRoot, 'bots/market-making') const pidFile = join(repoRoot, 'descendant.pid') await writeResolvableDependencies(packageRoot) - const executable = await writeBlockingBun(repoRoot) + const executable = await writeBlockingPnpm(repoRoot) const controller = new AbortController() const pending = ensureFrozenDependencies({ repoRoot, @@ -459,7 +459,7 @@ test( async () => { const packageRoot = await temporaryDirectory('playground-build-signal-') const pidFile = join(packageRoot, 'descendant.pid') - const executable = await writeBlockingBun(packageRoot) + const executable = await writeBlockingPnpm(packageRoot) const controller = new AbortController() let reported = false const runner = createPortableProcessRunner({ terminationGraceMs: 25, forceKillGraceMs: 250 }) diff --git a/bots/market-making/scripts/playground-smoke.browser.mjs b/bots/market-making/scripts/playground-smoke.browser.mjs index 8967a72a..98a462d9 100644 --- a/bots/market-making/scripts/playground-smoke.browser.mjs +++ b/bots/market-making/scripts/playground-smoke.browser.mjs @@ -184,7 +184,7 @@ for (const signal of ['SIGTERM', 'SIGINT']) { test(testName, { timeout: browserTestTimeout }, async () => { const isolatedTmp = await temporaryDirectory(`playground-browser-${signal.toLowerCase()}-`) const bin = join(isolatedTmp, 'bin') - const fakeBun = join(bin, 'bun') + const fakeVite = join(bin, 'vite') const chromiumLink = join(bin, 'chromium') const wrapper = join(isolatedTmp, 'chromium-wrapper') const wrapperPidFile = join(isolatedTmp, 'chromium-wrapper-pid') @@ -197,7 +197,7 @@ for (const signal of ['SIGTERM', 'SIGINT']) { 'fake build must mount a non-empty React root before declaring playground readiness' ) await writeFile( - fakeBun, + fakeVite, `#!/usr/bin/env node const { mkdirSync, writeFileSync } = require('node:fs') const outdir = process.argv[process.argv.indexOf('--outdir') + 1] @@ -206,7 +206,7 @@ mkdirSync(outdir, { recursive: true }) writeFileSync(outdir + '/index.html', ${JSON.stringify(fakeBuiltHtml)}) ` ) - await chmod(fakeBun, 0o755) + await chmod(fakeVite, 0o755) await writeFile( wrapper, `#!/usr/bin/env python3 @@ -286,12 +286,12 @@ test( async () => { const isolatedTmp = await temporaryDirectory('playground-browser-timeout-before-marker-') const bin = join(isolatedTmp, 'bin') - const fakeBun = join(bin, 'bun') + const fakeVite = join(bin, 'vite') const wrapper = join(isolatedTmp, 'chromium-no-devtools') const chromePidFile = join(isolatedTmp, 'chrome-pid') await mkdir(bin) await writeFile( - fakeBun, + fakeVite, `#!/usr/bin/env node const { mkdirSync, writeFileSync } = require('node:fs') const outdir = process.argv[process.argv.indexOf('--outdir') + 1] @@ -299,7 +299,7 @@ mkdirSync(outdir, { recursive: true }) writeFileSync(outdir + '/index.html', 'timeout cleanup') ` ) - await chmod(fakeBun, 0o755) + await chmod(fakeVite, 0o755) await writeFile( wrapper, `#!/usr/bin/env node @@ -413,14 +413,14 @@ test( const runBuildTimeoutCase = async ({ publishFixture, timeoutMs, waitForFixture }) => { const isolatedTmp = await temporaryDirectory('playground-browser-build-timeout-') const bin = join(isolatedTmp, 'bin') - const fakeBun = join(bin, 'bun') + const fakeVite = join(bin, 'vite') const buildPidFile = join(isolatedTmp, 'build-pid') const descendantPidFile = join(isolatedTmp, 'build-descendant-pid') const captureBarrierFile = join(isolatedTmp, 'build-capture-ready') const captureFile = join(isolatedTmp, 'build-capture.json') await mkdir(bin) await writeFile( - fakeBun, + fakeVite, `#!/usr/bin/env node const { spawn } = require('node:child_process') const { writeFileSync } = require('node:fs') @@ -438,13 +438,13 @@ child.on('close', () => process.exit(0)) setInterval(() => {}, 1000) ` ) - await chmod(fakeBun, 0o755) + await chmod(fakeVite, 0o755) const run = spawnSmoke({ env: { ...process.env, CHROMIUM_PATH: chromiumPath, PATH: `${bin}${delimiter}${process.env.PATH ?? ''}`, - VITE_EXE: fakeBun, + VITE_EXE: fakeVite, NODE_DISABLE_COMPILE_CACHE: '1', PLAYGROUND_SMOKE_BUILD_CAPTURE_BARRIER_FILE: captureBarrierFile, PLAYGROUND_SMOKE_BUILD_CAPTURE_FILE: captureFile, @@ -479,7 +479,7 @@ setInterval(() => {}, 1000) assert.equal(recordedBuildTree.rootPid, recordedBuildTree.processes[0]?.pid) assert.ok( recordedBuildTree.processes.length >= 4, - `expected the build subreaper, build runner, fake Bun, and descendant; got ${JSON.stringify(recordedBuildTree)}` + `expected the build subreaper, build runner, fake Vite, and descendant; got ${JSON.stringify(recordedBuildTree)}` ) captured = [ ...captured, diff --git a/bots/market-making/scripts/playground-smoke.test.mjs b/bots/market-making/scripts/playground-smoke.test.mjs index d9b59562..d0e93bdc 100644 --- a/bots/market-making/scripts/playground-smoke.test.mjs +++ b/bots/market-making/scripts/playground-smoke.test.mjs @@ -941,9 +941,9 @@ for (const signal of ['SIGTERM', 'SIGINT']) { const forwardingFile = join(isolatedTmp, 'build-forwarding') const prematureSignalFile = join(isolatedTmp, 'build-descendant-signalled-before-parent') await mkdir(bin) - const fakeBun = join(bin, 'bun') + const fakeVite = join(bin, 'vite') await writeFile( - fakeBun, + fakeVite, `#!/usr/bin/env node const { spawn } = require('node:child_process') const { writeFileSync } = require('node:fs') @@ -967,7 +967,7 @@ process.on('SIGTERM', () => { child.on('close', () => process.exit(0)) ` ) - await chmod(fakeBun, 0o755) + await chmod(fakeVite, 0o755) const smoke = spawn(process.execPath, [smokeScript], { env: { From c1c85dbd7354bfb980be10181397271f9f51bc77 Mon Sep 17 00:00:00 2001 From: Hayden Shively Date: Tue, 11 Aug 2026 11:11:37 -0500 Subject: [PATCH 12/14] fix(market-making): restore dynamic jsdoc inventory --- .agents/skills/build-jsdoc/SKILL.md | 62 ++------------- bots/market-making/scripts/check-jsdoc.ts | 94 +---------------------- 2 files changed, 9 insertions(+), 147 deletions(-) diff --git a/.agents/skills/build-jsdoc/SKILL.md b/.agents/skills/build-jsdoc/SKILL.md index ec89406a..07f48b85 100644 --- a/.agents/skills/build-jsdoc/SKILL.md +++ b/.agents/skills/build-jsdoc/SKILL.md @@ -68,59 +68,13 @@ Completion criterion: the AST inventory exits zero, TypeDoc emits zero warnings/ ## Public-Surface Inventory -The coverage script inventories these market-making boundary and utility files: - -- `src/application/operator-error-name.utils.ts` -- `src/application/bootstrap/position-bootstrap-halted.error.ts` -- `src/application/bootstrap/position-bootstrap.service.ts` -- `src/application/ladder/ladder-cycle-halted.error.ts` -- `src/application/ladder/ladder-market-maker.service.ts` -- `src/application/ladder/ladder-market-maker.utils.ts` -- `src/application/setup/setup-check.service.ts` -- `src/application/setup/setup-check.utils.ts` -- `src/application/setup/safe-provider.error.ts` -- `src/application/setup/setup-failed.error.ts` -- `src/application/version.service.ts` -- `src/bootstrap.ts` -- `src/config/config-file.error.ts` -- `src/config/config-source.utils.ts` -- `src/config/config-validation.error.ts` -- `src/config/config.service.ts` -- `src/config/config.utils.ts` -- `src/domain/bootstrap/bootstrap-configuration.error.ts` -- `src/domain/bootstrap/position-bootstrap.ts` -- `src/domain/ladder/ladder-configuration.error.ts` -- `src/domain/ladder/ladder.ts` -- `src/infrastructure/bootstrap/bootstrap-hard-halt.error.ts` -- `src/infrastructure/bootstrap/bootstrap-exposure.utils.ts` -- `src/infrastructure/bootstrap/bootstrap-make.service.ts` -- `src/infrastructure/bootstrap/bootstrap-offer.utils.ts` -- `src/infrastructure/bootstrap/bootstrap-adapter.error.ts` -- `src/infrastructure/bootstrap/bootstrap-group-ownership.utils.ts` -- `src/infrastructure/bootstrap/bootstrap-groups.utils.ts` -- `src/infrastructure/bootstrap/bootstrap-position.service.ts` -- `src/infrastructure/bootstrap/bootstrap-reference-rate.service.ts` -- `src/infrastructure/bootstrap/bootstrap-requirements.utils.ts` -- `src/infrastructure/bootstrap/bootstrap-transaction.utils.ts` -- `src/infrastructure/bootstrap/production-bootstrap.ts` -- `src/infrastructure/cli/cli-usage.error.ts` -- `src/infrastructure/cli/cli.ts` -- `src/infrastructure/cli/market-making-entrypoint.ts` -- `src/infrastructure/make/read-only-bootstrap-make.service.ts` -- `src/infrastructure/make/read-only-ladder-make.service.ts` -- `src/infrastructure/make/read-only-make.utils.ts` -- `src/infrastructure/setup-state/http-json.utils.ts` -- `src/infrastructure/setup-state/provider-pagination.error.ts` -- `src/infrastructure/setup-state/provider-read.error.ts` -- `src/infrastructure/setup-state/provider-read.utils.ts` -- `src/infrastructure/setup-state/provider-response.error.ts` -- `src/infrastructure/setup-state/viem-setup-state.service.ts` -- `src/infrastructure/setup-state/viem-setup-state.utils.ts` -- `scripts/js-doc-validation.error.ts` -- `scripts/bundle-failed.error.ts` +The coverage script dynamically discovers TypeScript files under `src/**` and `scripts/**` in +deterministic package-relative order. It excludes the executable `src/index.ts` entrypoint and test +files; generated output and dependencies are outside the scanned roots. Newly added source files +therefore enter the JSDoc check without a manually maintained path list. It checks exported functions/classes/interfaces/type aliases, interface methods, callable members of -exported type literals, and public constructors/methods/accessors. Every listed declaration needs a +exported type literals, and public constructors/methods/accessors. Every discovered declaration needs a substantive `/** ... */` block. The checker requires a non-filler summary, exact `@param` names, `@returns` for non-void callables, and `@throws` at its provider-boundary rule. Scoped rules also enforce read-only, aggregate-deadline, and `Promise.all` concurrency semantics. Do not satisfy @@ -155,9 +109,9 @@ For the current market-making implementation, preserve these verified package bo Completion criterion: package source/exports still support each imported symbol, the active-offer source remains complete, and every local exception has a concrete missing-SDK-export justification. -When adding another boundary file, add it to both `scripts/check-jsdoc.ts` and `typedoc.json` in the -same change. Completion criterion: the command's printed inventory contains every added public -callable exactly once. +When adding another boundary file, confirm the dynamic check discovers it and add it to +`typedoc.json` in the same change. Completion criterion: the command's printed inventory contains +every added public callable exactly once. ## Inspect Generated Docs diff --git a/bots/market-making/scripts/check-jsdoc.ts b/bots/market-making/scripts/check-jsdoc.ts index 5420f8b5..968ff5d4 100644 --- a/bots/market-making/scripts/check-jsdoc.ts +++ b/bots/market-making/scripts/check-jsdoc.ts @@ -298,98 +298,6 @@ export const inspectJSDocSource = (file: string, source: string): JSDocInspectio } const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') -const sourceRoot = resolve(packageRoot, 'src') -const sourceFiles = [ - 'application/operator-error-name.utils.ts', - 'application/bootstrap/bootstrap-ownership-cleanup.error.ts', - 'application/bootstrap/position-bootstrap-halted.error.ts', - 'application/bootstrap/position-bootstrap-monitor-halted.error.ts', - 'application/bootstrap/position-bootstrap-verbose.ts', - 'application/bootstrap/position-bootstrap.service.ts', - 'application/invalidation/offer-invalidation-failed.error.ts', - 'application/invalidation/offer-invalidation.service.ts', - 'application/ladder/ladder-cycle-halted.error.ts', - 'application/ladder/ladder-market-maker.service.ts', - 'application/ladder/ladder-market-maker.utils.ts', - 'application/ladder/ladder-monitor-halted.error.ts', - 'application/ladder/ladder-ownership-cleanup.error.ts', - 'application/ladder/ladder-verbose.ts', - 'application/market-making/market-making-monitor-halted.error.ts', - 'application/market-making/market-making-mutation.utils.ts', - 'application/market-making/market-making.service.ts', - 'application/setup/setup-check.service.ts', - 'application/setup/setup-check.utils.ts', - 'application/setup/safe-provider.error.ts', - 'application/setup/setup-failed.error.ts', - 'application/setup/setup-monitor-configuration.error.ts', - 'application/setup/setup-monitor-halted.error.ts', - 'application/version.service.ts', - 'bootstrap.ts', - 'config/config-file.error.ts', - 'config/config-source.utils.ts', - 'config/config-validation.error.ts', - 'config/config.service.ts', - 'config/config.utils.ts', - 'domain/bootstrap/bootstrap-configuration.error.ts', - 'domain/bootstrap/position-bootstrap.ts', - 'domain/ladder/ladder-configuration.error.ts', - 'domain/ladder/ladder.ts', - 'infrastructure/bootstrap/bootstrap-hard-halt.error.ts', - 'infrastructure/bootstrap/bootstrap-exposure.utils.ts', - 'infrastructure/bootstrap/bootstrap-make.service.ts', - 'infrastructure/bootstrap/bootstrap-mempool-validation.error.ts', - 'infrastructure/bootstrap/bootstrap-mempool-validation.utils.ts', - 'infrastructure/bootstrap/bootstrap-offer.utils.ts', - 'infrastructure/bootstrap/bootstrap-pending-offer.utils.ts', - 'infrastructure/bootstrap/bootstrap-adapter.error.ts', - 'infrastructure/bootstrap/bootstrap-group-ownership.utils.ts', - 'infrastructure/bootstrap/bootstrap-groups.utils.ts', - 'infrastructure/bootstrap/bootstrap-position.service.ts', - 'infrastructure/bootstrap/bootstrap-reference-rate.service.ts', - 'infrastructure/bootstrap/bootstrap-requirement-client.utils.ts', - 'infrastructure/bootstrap/bootstrap-requirements.utils.ts', - 'infrastructure/bootstrap/bootstrap-spread.utils.ts', - 'infrastructure/bootstrap/bootstrap-transaction.utils.ts', - 'infrastructure/bootstrap/production-bootstrap.ts', - 'infrastructure/cli/cli-usage.error.ts', - 'infrastructure/cli/cli.ts', - 'infrastructure/cli/market-making-entrypoint.ts', - 'infrastructure/cli/offer-invalidation-argument.utils.ts', - 'infrastructure/invalidation/offer-invalidation-adapter.error.ts', - 'infrastructure/invalidation/offer-invalidation-group.utils.ts', - 'infrastructure/invalidation/offer-invalidation-transaction.utils.ts', - 'infrastructure/invalidation/production-offer-invalidation.ts', - 'infrastructure/ladder/ladder-adapter.error.ts', - 'infrastructure/ladder/ladder-active-publication.utils.ts', - 'infrastructure/ladder/ladder-bootstrap-offer.utils.ts', - 'infrastructure/ladder/ladder-cash-reservation.utils.ts', - 'infrastructure/ladder/ladder-group-ownership.utils.ts', - 'infrastructure/ladder/ladder-hard-halt.error.ts', - 'infrastructure/ladder/ladder-make.service.ts', - 'infrastructure/ladder/ladder-offer.utils.ts', - 'infrastructure/ladder/ladder-ratification.utils.ts', - 'infrastructure/ladder/ladder-signature.utils.ts', - 'infrastructure/ladder/ladder-spread.utils.ts', - 'infrastructure/ladder/ladder-transaction.utils.ts', - 'infrastructure/ladder/production-ladder.ts', - 'infrastructure/make/managed-maker-account.utils.ts', - 'infrastructure/make/read-only-bootstrap-make.service.ts', - 'infrastructure/make/read-only-ladder-make.service.ts', - 'infrastructure/make/read-only-make.utils.ts', - 'infrastructure/reference/blue-reference-reader.utils.ts', - 'infrastructure/setup-state/chain-reader.utils.ts', - 'infrastructure/reference/reference-adapter.error.ts', - 'infrastructure/setup-state/http-json.utils.ts', - 'infrastructure/setup-state/provider-pagination.error.ts', - 'infrastructure/setup-state/provider-read.error.ts', - 'infrastructure/setup-state/provider-read.utils.ts', - 'infrastructure/setup-state/provider-response.error.ts', - 'infrastructure/setup-state/viem-setup-state.service.ts', - 'infrastructure/setup-state/viem-setup-state.utils.ts' -].map(path => resolve(sourceRoot, path)) -sourceFiles.push(resolve(packageRoot, 'scripts/js-doc-validation.error.ts')) -sourceFiles.push(resolve(packageRoot, 'scripts/bundle-failed.error.ts')) -sourceFiles.push(resolve(packageRoot, 'scripts/check-jsdoc.ts')) /** * Discovers the TypeScript files that define the documented market-making surface. @@ -407,7 +315,7 @@ export const discoverJSDocSourceFiles = async (root: string) => .toSorted() const run = async () => { - sourceFiles.splice(0, sourceFiles.length, ...(await discoverJSDocSourceFiles(packageRoot))) + const sourceFiles = await discoverJSDocSourceFiles(packageRoot) const failures: JSDocFailure[] = [] const declarations: string[] = [] for (const file of sourceFiles) { From 97afb1b0d83b6fed8fedd06a330aa7c72cd69562 Mon Sep 17 00:00:00 2001 From: Hayden Shively Date: Tue, 11 Aug 2026 11:31:03 -0500 Subject: [PATCH 13/14] test(market-making): tolerate reaped install children --- .../scripts/playground-serve.test.mjs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/bots/market-making/scripts/playground-serve.test.mjs b/bots/market-making/scripts/playground-serve.test.mjs index 59457d06..e872d80c 100644 --- a/bots/market-making/scripts/playground-serve.test.mjs +++ b/bots/market-making/scripts/playground-serve.test.mjs @@ -72,13 +72,17 @@ const waitFor = async operation => { throw lastError } -const assertProcessNotLive = async pid => { - let state = 'missing' +const readProcessState = async (pid, readProcessStat = readFile) => { try { - state = (await readFile(`/proc/${pid}/stat`, 'utf8')).split(' ')[2] + return (await readProcessStat(`/proc/${pid}/stat`, 'utf8')).split(' ')[2] } catch (error) { - if (error.code !== 'ENOENT') throw error + if (error.code === 'ENOENT' || error.code === 'ESRCH') return 'missing' + throw error } +} + +const assertProcessNotLive = async pid => { + const state = await readProcessState(pid) assert.ok(state === 'missing' || state === 'Z', `process ${pid} remains live in state ${state}`) } @@ -419,6 +423,11 @@ test('signal during frozen install kills its descendant tree', { timeout: 10_000 await assertProcessNotLive(descendantPid) }) +test('process liveness treats an already-reaped child as missing', async () => { + const error = Object.assign(new Error('no such process'), { code: 'ESRCH' }) + assert.equal(await readProcessState(42, async () => Promise.reject(error)), 'missing') +}) + test('fresh build preserves canonical dist, uses only temporary output, and validates index', async () => { const packageRoot = await temporaryDirectory('playground-fresh-injected-') const stale = join(packageRoot, 'playground/dist') From adda072fe7f6fe8ffc07f1b013626776d77cddb6 Mon Sep 17 00:00:00 2001 From: Hayden Shively Date: Tue, 11 Aug 2026 13:34:43 -0500 Subject: [PATCH 14/14] fix(repo): finish bun removal migration --- .claude/agents/reviewer.md | 7 +-- .../deploy-market-making-playground.yml | 19 ++++--- .../deploy-market-making-production.yml | 2 +- .gitignore | 1 + README.md | 2 +- bots/blue-liquidation/README.md | 4 +- .../scripts/deploy-railway.ts | 2 +- .../scripts/probe-live-lens.ts | 2 +- bots/blue-liquidation/src/state/lens.sol.ts | 4 +- bots/market-making/Dockerfile | 28 +++++++--- bots/market-making/README.md | 22 ++++---- .../scripts/playground-process.test.mjs | 10 ++-- .../scripts/playground-smoke.browser.mjs | 51 +++++++++++-------- .../scripts/playground-smoke.test.mjs | 10 ++-- .../test/playground/pages-workflow.test.ts | 21 +++++--- .../scripts/seed-liquidatable-positions.ts | 6 +-- docs/CONVENTIONS.md | 6 +-- package.json | 1 + .../solidity/interfaces/IMidnight.sol | 2 +- .../contracts/solidity/interfaces/IMorpho.sol | 2 +- .../src/bot-observability.utils.ts | 2 +- pnpm-workspace.yaml | 4 -- 22 files changed, 116 insertions(+), 92 deletions(-) diff --git a/.claude/agents/reviewer.md b/.claude/agents/reviewer.md index 737bbd7c..c8f890ef 100644 --- a/.claude/agents/reviewer.md +++ b/.claude/agents/reviewer.md @@ -57,9 +57,10 @@ Apply each focus area to the diff: `suggestion` if a close match could be adapted. If the new code is general-purpose and not tied to a specific bot's domain, suggest hoisting it into the appropriate shared package (`@repo/utils` for pure utilities, `@repo/abis` for ABI-adjacent helpers). -5. **Testing quality** — new behavior has a `{module}.test.ts` under the workspace's `test/` tree mirroring `src/`. Tests use `vitest`. - Tests are non-vacuous (they actually fail if the implementation breaks). No mocking of - on-chain behavior where a viem test client / anvil would give real evidence. +5. **Testing quality** — new TypeScript behavior has a `{module}.test.ts` under the workspace's + `test/` tree mirroring `src/` and uses Vitest. The playground's JavaScript harness uses Node + `*.test.mjs` suites. Tests are non-vacuous (they actually fail if the implementation breaks). No + mocking of on-chain behavior where a viem test client / anvil would give real evidence. 6. **Agent infrastructure** — if the diff touches `CLAUDE.md`, `.claude/`, `.mcp.json`, editor configs, or agent definitions, mirror the change across any documented counterpart (`AGENTS.md`, `.cursorrules`) and flag inconsistencies. diff --git a/.github/workflows/deploy-market-making-playground.yml b/.github/workflows/deploy-market-making-playground.yml index b552966e..a900517e 100644 --- a/.github/workflows/deploy-market-making-playground.yml +++ b/.github/workflows/deploy-market-making-playground.yml @@ -13,9 +13,11 @@ on: - packages/utils/** - packages/typescript-config/** - package.json - - bun.lock - - bunfig.toml + - pnpm-lock.yaml + - pnpm-workspace.yaml - .npmrc + - .nvmrc + - .github/actions/setup/action.yml - .github/workflows/deploy-market-making-playground.yml workflow_dispatch: @@ -39,19 +41,16 @@ jobs: with: persist-credentials: false - - name: Set up Bun - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + - name: Setup + uses: ./.github/actions/setup with: - bun-version: 1.3.12 - - - name: Install dependencies - run: bun install --frozen-lockfile + build-contracts: 'false' - name: Test playground at the GitHub Pages subpath - run: PLAYGROUND_SMOKE_BASE_PATH=/morpho-bots/ bun run --filter @morpho-org/market-making-bot playground:smoke:test + run: PLAYGROUND_SMOKE_BASE_PATH=/morpho-bots/ pnpm --filter @morpho-org/market-making-bot run playground:smoke:test - name: Build playground - run: bun run --filter @morpho-org/market-making-bot playground:build + run: pnpm --filter @morpho-org/market-making-bot run playground:build - name: Upload GitHub Pages artifact uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 diff --git a/.github/workflows/deploy-market-making-production.yml b/.github/workflows/deploy-market-making-production.yml index 2224891e..61fe4438 100644 --- a/.github/workflows/deploy-market-making-production.yml +++ b/.github/workflows/deploy-market-making-production.yml @@ -42,4 +42,4 @@ jobs: RAILWAY_ENVIRONMENT: production RAILWAY_PROJECT_ID: ${{ vars.RAILWAY_PROJECT_ID }} RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }} - run: bun run --filter @morpho-org/market-making-bot deploy:railway + run: pnpm --filter @morpho-org/market-making-bot run deploy:railway diff --git a/.gitignore b/.gitignore index 76200c10..fa326487 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # Dependencies node_modules +.pnpm-store # Local env files .env diff --git a/README.md b/README.md index 5bd36774..2af687bc 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ pnpm run lint # oxlint, repo-wide pnpm run lint:fix # oxlint with --fix pnpm format # oxfmt, repo-wide pnpm run knip # dead-code detection -pnpm test # vitest, every workspace project +pnpm test # vitest projects plus Node playground suites ``` ## Pointers diff --git a/bots/blue-liquidation/README.md b/bots/blue-liquidation/README.md index e2f75037..e9739cdc 100644 --- a/bots/blue-liquidation/README.md +++ b/bots/blue-liquidation/README.md @@ -19,7 +19,7 @@ position (none exist while the market is healthy), `RPC_URL_8453`, and ## Prerequisites -- **pnpm** `11.1.1` (via corepack), **bun** `1.3.12`, **Node** `24.14.1` (`.nvmrc`). +- **pnpm** `11.1.1` (via corepack) and **Node** `24.14.1` (`.nvmrc`). - A chain RPC that both reads and _relays_ transactions — **not** `rpc.morpho.dev/realtime`, which acknowledges sends but never broadcasts them. - A **funded EOA** (native gas) — the liquidator and the recipient of the end-of-exec token sweeps. @@ -256,7 +256,7 @@ the nonce from `getTransactionCount('pending')`. ## Testing -- `bun test` — unit tests for the math, LIF, seize-exact planner (incl. the underflow-safety sweep), +- `pnpm test` — unit tests for the math, LIF, seize-exact planner (incl. the underflow-safety sweep), the id derivation, GraphQL discovery (parsing, pagination, retry semantics), config (incl. venue inference and the zero-venue gate), eligibility, quoting, venues, the queue, and the exec encoder. - **Live read-path probe** — `pnpm --filter @morpho-org/blue-liquidation run probe:lens` (needs diff --git a/bots/blue-liquidation/scripts/deploy-railway.ts b/bots/blue-liquidation/scripts/deploy-railway.ts index e861da44..028f1e4e 100644 --- a/bots/blue-liquidation/scripts/deploy-railway.ts +++ b/bots/blue-liquidation/scripts/deploy-railway.ts @@ -21,7 +21,7 @@ * RAILWAY_PROJECT_ID=… RPC_URL_8453=… RPC_URL_4663=… LIQUIDATOR_PRIVATE_KEY=0x… \ * ALLOW_DETECTION_ONLY_4663=true pnpm --filter @morpho-org/blue-liquidation run deploy:railway * - * The build context MUST be the repo root so the bun workspace (packages/*) resolves — the script + * The build context MUST be the repo root so the pnpm workspace (packages/*) resolves — the script * runs `railway up` with cwd set to the repo root (mirrors the Dockerfile header + compose context). * * Idempotent: existing services / variables are reused; each run redeploys every bot. The venue diff --git a/bots/blue-liquidation/scripts/probe-live-lens.ts b/bots/blue-liquidation/scripts/probe-live-lens.ts index b358b0a7..dbfb8ceb 100644 --- a/bots/blue-liquidation/scripts/probe-live-lens.ts +++ b/bots/blue-liquidation/scripts/probe-live-lens.ts @@ -9,7 +9,7 @@ * `ForkFixture` you can paste into `test/fork/liquidation.test.ts` to run the end-to-end suite. * * Usage (needs a Base RPC; no anvil required): - * RPC_URL=https://… bun run bots/blue-liquidation/scripts/probe-live-lens.ts + * RPC_URL=https://… pnpm --filter @morpho-org/blue-liquidation run probe:lens */ import type { Address, Hex } from 'viem' diff --git a/bots/blue-liquidation/src/state/lens.sol.ts b/bots/blue-liquidation/src/state/lens.sol.ts index 65759a5f..141ae6b2 100644 --- a/bots/blue-liquidation/src/state/lens.sol.ts +++ b/bots/blue-liquidation/src/state/lens.sol.ts @@ -24,8 +24,8 @@ import { marketId } from '../market' // re-derives the id from the supplied params on-chain, so mismatched params read an uncreated market // and return valid=false. // -// Compiled to a deployless factory by the soltag bun preload (see ../../soltag.preload.ts); `sol``` -// throws if not active. +// Compiled to a deployless factory by soltag's Vitest/esbuild integrations; `sol``` throws if the +// transform is not active. export const BlueLiquidationLens = sol('BlueLiquidationLens')` // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.19; diff --git a/bots/market-making/Dockerfile b/bots/market-making/Dockerfile index ca5a3477..20ac61ec 100644 --- a/bots/market-making/Dockerfile +++ b/bots/market-making/Dockerfile @@ -1,12 +1,26 @@ # syntax=docker/dockerfile:1 -# The build context is the repository root so every workspace dependency remains available. -FROM oven/bun:1.3.12-slim +# Image for the market-making bot. The build context MUST be the repository root so workspace +# packages resolve. Node only: pnpm installs, esbuild bundles, node runs. +FROM node:24.14.1-slim +ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 + +# Runtime configuration may include a funded maker key, so the process must not run as root. +# Corepack writes its pnpm shim to /usr/local/bin; enable it before dropping privileges. +RUN corepack enable pnpm +RUN mkdir -p /repo && chown node:node /repo WORKDIR /repo +USER node + +# Manifests first keep the install layer cacheable. `corepack install` fetches the pnpm version pinned +# in package.json#packageManager. +COPY --chown=node:node package.json pnpm-workspace.yaml pnpm-lock.yaml ./ +RUN corepack install -COPY package.json bun.lock bunfig.toml ./ -COPY packages ./packages -COPY bots ./bots -RUN bun install --frozen-lockfile +# All workspace manifests and sources are needed to resolve links and build the bot bundle. +COPY --chown=node:node packages ./packages +COPY --chown=node:node bots ./bots +RUN pnpm install --frozen-lockfile +RUN pnpm -r --if-present run build WORKDIR /repo/bots/market-making -CMD ["bun", "run", "start", "--", "start", "--verbose"] +CMD ["node", "dist/src/index.js", "start", "--verbose"] diff --git a/bots/market-making/README.md b/bots/market-making/README.md index f876851e..d5005d1a 100644 --- a/bots/market-making/README.md +++ b/bots/market-making/README.md @@ -15,7 +15,7 @@ structure. Run the stateless parameter playground locally from the repository root: ```sh -bun run market-making:playground +pnpm run market-making:playground ``` The launcher performs a frozen-lockfile dependency check before every build, then prints the local @@ -23,7 +23,7 @@ server URL after building the browser artifact. For a production-equivalent build without starting a server, run: ```sh -bun run --filter @morpho-org/market-making-bot playground:build +pnpm --filter @morpho-org/market-making-bot run playground:build ``` After this pull request is merged to `main`, relevant playground, browser-safe bot-kit, package, lock, @@ -43,16 +43,16 @@ pnpm --filter @morpho-org/market-making-bot run start -- setup-check pnpm --filter @morpho-org/market-making-bot run start -- --readonly setup-check # Explicit signer sources (root options precede the command). -bun run --filter @morpho-org/market-making-bot start -- --private-key '' setup-check -bun run --filter @morpho-org/market-making-bot start -- --keystore ./maker.json --interactive setup-check -bun run --filter @morpho-org/market-making-bot start -- --aws setup-check +pnpm --filter @morpho-org/market-making-bot run start -- --private-key '' setup-check +pnpm --filter @morpho-org/market-making-bot run start -- --keystore ./maker.json --interactive setup-check +pnpm --filter @morpho-org/market-making-bot run start -- --aws setup-check ``` For unattended keystore operation, provision `KEYSTORE_PASSWORD` separately through the deployment secret manager or process environment, then run the keystore command without an inline value: ```sh -bun run --filter @morpho-org/market-making-bot start -- --keystore ./maker.json setup-check +pnpm --filter @morpho-org/market-making-bot run start -- --keystore ./maker.json setup-check ``` `--private-key ` and `--password ` remain available for explicit automation, but they @@ -254,8 +254,8 @@ pnpm --filter @morpho-org/market-making-bot run start -- --version The package owns its production [Dockerfile](./Dockerfile), local [docker-compose.yml](./docker-compose.yml), and idempotent [`scripts/deploy-railway.ts`](./scripts/deploy-railway.ts) entrypoint. The Docker build context is the -repository root so Bun can resolve every workspace dependency; the image starts the combined setup, -bootstrap, and ladder monitor. +repository root so pnpm can resolve every workspace dependency; the image builds the workspace and +starts the combined setup, bootstrap, and ladder monitor as an unprivileged Node process. A full deployment creates the `market-making` Railway service, selects the package Dockerfile, provisions a persistent volume at `/state`, and writes the effective environment configuration @@ -263,7 +263,7 @@ through stdin so values never appear in process arguments or logs: ```sh RAILWAY_PROJECT_ID=... \ -bun run --filter @morpho-org/market-making-bot deploy:railway +pnpm --filter @morpho-org/market-making-bot run deploy:railway ``` Provide the required values from [`.env.example`](./.env.example) in the invoking environment. @@ -752,12 +752,12 @@ collections are rejected atomically. The four outputs are Bootstrap JSON, the co `BOOTSTRAP_MARKETS` value, Ladder JSON, and the compact exact `LADDER_MARKETS` value; each collection validates independently. -From the repository root, one command runs `bun install --frozen-lockfile` (also on already-installed +From the repository root, one command runs `pnpm install --frozen-lockfile` (also on already-installed workspaces, where it is fast), creates a fresh isolated build, and serves it on loopback. It does not run test assertions or require Chromium: ```sh -bun run market-making:playground +pnpm run market-making:playground ``` Open the exact URL printed by the command (default `http://127.0.0.1:4173`). Override the listener diff --git a/bots/market-making/scripts/playground-process.test.mjs b/bots/market-making/scripts/playground-process.test.mjs index 13698d9e..3803c5f4 100644 --- a/bots/market-making/scripts/playground-process.test.mjs +++ b/bots/market-making/scripts/playground-process.test.mjs @@ -26,7 +26,7 @@ test('pre-aborted commands never spawn', async () => { } }) await assert.rejects( - run({ executable: 'bun', args: [], signal: controller.signal }), + run({ executable: 'tool', args: [], signal: controller.signal }), /already stopped/ ) assert.equal(spawns, 0) @@ -49,7 +49,7 @@ test('an abort raised synchronously inside spawn is caught by the registered lis } }) await assert.rejects( - run({ executable: 'bun', args: [], signal: controller.signal }), + run({ executable: 'tool', args: [], signal: controller.signal }), /spawn-window abort/ ) assert.deepEqual(kills, [ @@ -77,7 +77,7 @@ for (const platform of ['linux', 'darwin']) { } }) const controller = new AbortController() - const result = run({ executable: 'bun', args: ['install'], signal: controller.signal }) + const result = run({ executable: 'tool', args: ['install'], signal: controller.signal }) controller.abort(new Error('cancelled')) await assert.rejects(result, /cancelled/) assert.equal(spawnOptions.detached, true) @@ -97,7 +97,7 @@ test('Windows runner terminates a task tree with argument arrays and no shell', terminationGraceMs: 1, forceKillGraceMs: 10, spawnProcess(executable, args, options) { - if (executable === 'bun.exe') return child + if (executable === 'tool.exe') return child commands.push({ executable, args, options }) const taskkill = fakeChild(999) queueMicrotask(() => taskkill.emit('close', 0, null)) @@ -106,7 +106,7 @@ test('Windows runner terminates a task tree with argument arrays and no shell', } }) const controller = new AbortController() - const result = run({ executable: 'bun.exe', args: ['build'], signal: controller.signal }) + const result = run({ executable: 'tool.exe', args: ['build'], signal: controller.signal }) controller.abort(new Error('cancelled')) await assert.rejects(result, /cancelled/) assert.deepEqual( diff --git a/bots/market-making/scripts/playground-smoke.browser.mjs b/bots/market-making/scripts/playground-smoke.browser.mjs index 98a462d9..920968f8 100644 --- a/bots/market-making/scripts/playground-smoke.browser.mjs +++ b/bots/market-making/scripts/playground-smoke.browser.mjs @@ -1,11 +1,14 @@ import assert from 'node:assert/strict' +import { execFile } from 'node:child_process' import { chmod, mkdtemp, mkdir, readFile, readdir, rm, symlink, writeFile } from 'node:fs/promises' import { connect } from 'node:net' import { tmpdir } from 'node:os' import { delimiter, join } from 'node:path' import test from 'node:test' import { fileURLToPath } from 'node:url' +import { promisify } from 'node:util' +import { productionPlaygroundBuildArguments } from './playground-build-arguments.mjs' import { closeOwnedProcessTreeGracefully, discoverChromium, @@ -19,6 +22,7 @@ import { const temporaryDirectories = [] const harnessRuns = new Set() +const execFileAsync = promisify(execFile) const smokeScript = fileURLToPath(new URL('./playground-smoke.mjs', import.meta.url)) const chromiumPath = await discoverChromium() const { cleanupTimeout, outerReadinessTimeout, browserTestTimeout } = smokeBudgets(process.env) @@ -27,6 +31,19 @@ const temporaryDirectory = async prefix => { temporaryDirectories.push(directory) return directory } +const writeFakeVite = async (path, html) => { + await writeFile( + path, + `#!/usr/bin/env node +const { mkdirSync, writeFileSync } = require('node:fs') +const outdir = process.argv[process.argv.indexOf('--outDir') + 1] +if (!outdir) throw new Error('missing --outDir') +mkdirSync(outdir, { recursive: true }) +writeFileSync(outdir + '/index.html', ${JSON.stringify(html)}) +` + ) + await chmod(path, 0o755) +} test.after(async () => { await Promise.allSettled([...harnessRuns].map(run => cleanupHarnessRun(run))) @@ -35,6 +52,17 @@ test.after(async () => { ) }) +test('fake Vite lifecycle builder follows the production --outDir contract', async () => { + const directory = await temporaryDirectory('playground-fake-vite-contract-') + const executable = join(directory, 'vite') + const outdir = join(directory, 'dist') + await writeFakeVite(executable, 'contract') + + await execFileAsync(executable, productionPlaygroundBuildArguments(outdir)) + + assert.equal(await readFile(join(outdir, 'index.html'), 'utf8'), 'contract') +}) + const processIdentity = async pid => { try { const stat = await readFile(`/proc/${pid}/stat`, 'utf8') @@ -196,17 +224,7 @@ for (const signal of ['SIGTERM', 'SIGINT']) { /
[^<]+<\/div><\/div>