Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .claude/agents/reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 `bun test`.
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.
Expand Down
2 changes: 1 addition & 1 deletion .claude/commands/babysit-pr.md
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ After all comments are addressed, run validation on affected packages:
```bash
pnpm --filter <affected-package> run typecheck
pnpm lint
bun test
pnpm test
pnpm format
```

Expand Down
6 changes: 3 additions & 3 deletions .claude/commands/review.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Comment thread
haydenshively marked this conversation as resolved.

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:
Expand Down Expand Up @@ -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.

Expand Down
8 changes: 1 addition & 7 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
@@ -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'
Comment thread
haydenshively marked this conversation as resolved.
Comment thread
haydenshively marked this conversation as resolved.
Comment thread
haydenshively marked this conversation as resolved.
inputs:
install:
description: 'Whether to install dependencies'
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ 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.
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
Expand Down
19 changes: 9 additions & 10 deletions .github/workflows/deploy-market-making-playground.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy-market-making-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

# Dependencies
node_modules
.pnpm-store

# Local env files
.env
Expand Down
9 changes: 5 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:**
Expand Down Expand Up @@ -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`).
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 projects plus Node playground suites
```

## Pointers
Expand Down
26 changes: 11 additions & 15 deletions bots/blue-liquidation/Dockerfile
Original file line number Diff line number Diff line change
@@ -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 —
Expand All @@ -22,16 +16,18 @@ 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.
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"]
9 changes: 5 additions & 4 deletions bots/blue-liquidation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -255,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
Expand Down
6 changes: 0 additions & 6 deletions bots/blue-liquidation/bunfig.toml

This file was deleted.

18 changes: 12 additions & 6 deletions bots/blue-liquidation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
"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",
"prestart": "pnpm --filter \"{.}...\" --if-present run build",
"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",
Comment thread
haydenshively marked this conversation as resolved.
"typecheck": "soltag && tsc --noEmit"
},
"dependencies": {
Expand All @@ -23,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:"
}
}
55 changes: 55 additions & 0 deletions bots/blue-liquidation/scripts/build.ts
Original file line number Diff line number Diff line change
@@ -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))
}
11 changes: 11 additions & 0 deletions bots/blue-liquidation/scripts/bundle-failed.error.ts
Original file line number Diff line number Diff line change
@@ -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'
}
}
Loading
Loading