Skip to content

🎉 substreams: initial implementation#627

Merged
cruzdanilo merged 20 commits intomainfrom
substreams
Jan 9, 2026
Merged

🎉 substreams: initial implementation#627
cruzdanilo merged 20 commits intomainfrom
substreams

Conversation

@cruzdanilo
Copy link
Member

@cruzdanilo cruzdanilo commented Jan 8, 2026

this is the first step towards #585. it purposefully lets out the final deployment and usage in the server for later, in a following pr.

Summary by CodeRabbit

  • New Features

    • Substreams integration: handlers to discover, track, and export EXA accounts and plugins.
  • Database

    • New sub-schema and tables to persist substreams data (plugins, cursors, history, blocks).
  • Infrastructure

    • Substreams service, container image, and CI workflow added; refined ignore patterns and CI cache paths.
  • Developer Tools

    • Rust/protobuf toolchain, codegen/build scripts, formatting/lint configs, and editor recommendations.
  • Tests

    • Improved CI/test orchestration and runtime test process management.
  • Chores

    • Multiple changesets added for package versioning.

✏️ Tip: You can customize this high-level summary in your review settings.

@changeset-bot
Copy link

changeset-bot bot commented Jan 8, 2026

🦋 Changeset detected

Latest commit: 89f6bb0

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@exactly/substreams Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented Jan 8, 2026

Walkthrough

Adds a new Substreams Rust/WASM package (codegen, proto, handlers, manifest, Docker), integrates substreams DB schema and drizzle config, adds a Firehose line-processor and test orchestration changes, updates CI/workflows, workspace/NX targets, ignores, editor recommendations, and four changeset files.

Changes

Cohort / File(s) Summary
Changesets
​.changeset/breezy-planes-repeat.md, ​.changeset/rare-readers-tell.md, ​.changeset/red-toys-pump.md, ​.changeset/violet-poems-heal.md
Four new changeset files declaring minor bumps for @exactly/substreams with short notes about mapping/storing/inserting exa accounts/plugins.
Substreams core & manifest
substreams/Cargo.toml, substreams/package.json, substreams/src/lib.rs, substreams/substreams.yaml, substreams/build.rs, substreams/proto/exa.proto, substreams/rust-toolchain.toml, substreams/rustfmt.toml, substreams/buf.gen.yaml
New cdylib crate and NX package, codegen build script producing contract/proto bindings, proto messages (Accounts/Plugins), four Substreams handlers (map/store/map->db), and Substreams manifest with modules, networks, and sink configuration.
Substreams packaging & runtime
substreams/Dockerfile, substreams/.gitignore, substreams/.cargo/config.toml
New multi-stage Dockerfile for build/runtime, added gitignore entries, and cargo config targeting wasm32-unknown-unknown.
Database & Drizzle
server/database/schema.ts, server/drizzle.config.ts
Adds substreams pgSchema and tables (cursors, substreams_history, blocks, exa_plugins), updates index declaration parameter destructuring, and limits drizzle schemaFilter to ["public","substreams"].
Server scripts & tests
server/script/firehose.ts, server/test/database.ts, server/test/anvil.ts, server/test/e2e.ts
New Firehose line-processor (protobuf timestamp extraction/patching); test harness now launches firehose and substreams with AbortController and tcp waits; forge invocation narrowed to a specific test case; Sentry mocked in tests.
CI / Workflows / Matchers
.github/workflows/substreams-build.yaml, .github/workflows/server-build.yaml, .github/workflows/test.yaml, .github/workflows/docs.yaml, .github/workflows/mobile-expo.yaml, .github/workflows/version.yaml, .github/matcher.json
New tag-triggered substreams build (build/push/attest), server build specifies Dockerfile and adds GHA cache, test workflow downloads CLI binaries and sets NX_DAEMON=false, multiple workflows switch NX cache path to .nx/cache, and added Rust/rust-formatter matchers.
Workspace / NX / packages
pnpm-workspace.yaml, common/package.json, server/package.json, docs/package.json, contracts/package.json
Adds substreams to workspace; expands NX inputs/outputs (generate:wagmi, server generate:broadcasts, docs build); server adds devDependency @exactly/substreams; contracts reorganizes test targets and sets parallelism false for several targets.
Contracts tests / mocks
contracts/test/mocks/BobExecute.s.sol
Replaces account creation usage with factory.getAddress(...), adds BatchDeploy contract and deploy step to create multiple accounts.
Tooling & editor config
.dockerignore, cspell.json, .vscode/extensions.json
Replaces broad dockerignore patterns with granular ignores/negations, updates cspell ignore paths and vocabulary, and adds VS Code recommendations for Protobuf and Rust analyzer.

Sequence Diagram

sequenceDiagram
    participant Block as Blockchain Block
    participant MapAccounts as map_exa_accounts
    participant StoreAccounts as store_exa_accounts
    participant MapPlugins as map_exa_plugins
    participant DbOut as db_out
    participant Database as PostgreSQL

    Block->>MapAccounts: deliver block + params (factories)
    MapAccounts->>MapAccounts: parse events -> Accounts
    MapAccounts-->>StoreAccounts: emit Accounts

    StoreAccounts->>StoreAccounts: persist flags for accounts

    Block->>MapPlugins: deliver block
    StoreAccounts-->>MapPlugins: provide account lookup
    MapPlugins->>MapPlugins: scan PluginInstalled, filter by new/store -> Plugins
    MapPlugins-->>DbOut: emit Plugins

    DbOut->>DbOut: build DatabaseChanges (upsert exa_plugins, insert block)
    DbOut->>Database: apply DatabaseChanges
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title '🎉 substreams: initial implementation' accurately reflects the main change: introducing the substreams workspace as an initial implementation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch substreams

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cursor
Copy link

cursor bot commented Jan 8, 2026

PR Summary

Implements on-chain indexing of EXA accounts and plugins via Substreams and persists results for server use.

  • New Rust @exactly/substreams package with handlers: map_exa_accounts, store_exa_accounts, map_exa_plugins, and db_out (writes exa_plugins and blocks)
  • Adds substreams Postgres schema in server/database/schema.ts (exa_plugins, blocks, cursors, substreams_history) and includes it in Drizzle schemaFilter
  • Ships Docker image (substreams/Dockerfile) and GitHub workflow substreams-build.yaml; updates other workflows to use .nx/cache
  • Test infra: local Firehose bridge (server/script/firehose.ts), spins up sink in server/test/database.ts, and e2e wiring; minor contract test script tweak
  • Repo/config: .dockerignore additions, cspell words, VSCode extensions, workspace updates, and changesets for @exactly/substreams

Written by Cursor Bugbot for commit 89f6bb0. This will update automatically on new commits. Configure here.

@gemini-code-assist
Copy link

Summary of Changes

Hello @cruzdanilo, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request marks the initial implementation of substreams, laying the groundwork for a robust data processing pipeline. It establishes the core Rust project, defines the logic for extracting and storing critical blockchain data related to Exa accounts and plugins, and integrates the necessary database schemas and development tools. This foundational work is a prerequisite for subsequent deployment and full integration into the server, as outlined in the linked issue.

Highlights

  • Substreams Project Initialization: Introduced a new substreams Rust project, setting up the foundational structure for event-driven data processing from the Ethereum blockchain.
  • Data Extraction and Storage Logic: Implemented Rust-based substream handlers to map and store Exa accounts and their associated plugins, preparing this data for database persistence.
  • Database Schema and Integration: Extended the Drizzle ORM schema with new tables (cursors, substreams_history, blocks, exa_plugins) under a dedicated substreams schema, and configured Drizzle to recognize it.
  • Development Environment and Tooling: Integrated Rust toolchain, Protobuf generation, and ABI binding generation for smart contracts, along with new VS Code extensions and GitHub Actions problem matchers to support Rust development.
  • Testing Infrastructure Update: Modified test setups to incorporate Firehose and substreams-sink-sql services, ensuring the new substream components are properly tested within the existing environment.
Ignored Files
  • Ignored by pattern: .github/workflows/** (3)
    • .github/workflows/server-build.yaml
    • .github/workflows/substreams-build.yaml
    • .github/workflows/test.yaml
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the initial implementation of substreams to index on-chain data. It adds a new Rust-based substreams package, along with the necessary configuration for building, testing, and deploying it as a Docker service. The changes also include updates to the database schema, test setup, and CI/CD configurations to integrate this new service. My review focuses on the new substreams implementation and its integration. I've found a critical logic bug in the Rust code and several medium-severity issues related to maintainability, reproducibility, and potential correctness in the supporting scripts and Dockerfile. Overall, this is a great first step, and addressing these points will improve the robustness and maintainability of the new service.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is being reviewed by Cursor Bugbot

Details

You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🤖 Fix all issues with AI agents
In @.github/workflows/substreams-build.yaml:
- Line 2: Convert compact inline mappings to expanded multi-line YAML for
readability and to satisfy yamllint: replace the single-line "on: { push: {
tags: [\"@exactly/substreams@*\"] }, workflow_dispatch: {} }" with an expanded
mapping using "on:" as a parent key and nested "push:" with "tags:" array and a
separate "workflow_dispatch:" mapping; apply the same transformation to other
compact mappings in this file (the occurrences using { ... } syntax such as the
ones around push/tags and workflow_dispatch) so each mapping is written in
standard block form.

In @.github/workflows/test.yaml:
- Around line 37-41: Update the workflow run step that downloads binaries so it
(1) adds $HOME/.local/bin to the GitHub Actions PATH by appending
"$HOME/.local/bin" to $GITHUB_PATH immediately after installation, (2) enables
fail-fast behavior (e.g., set -e) so download/extract errors fail the job, (3)
replaces the "latest" GitHub API query with pinned release tags or specific
asset URLs for the three repos (firehose-ethereum, substreams,
substreams-sink-sql) to ensure reproducible builds, and (4) split the long
for-loop into explicit download/extract commands per repo to simplify debugging
and error reporting; update the run step that currently creates and populates
$HOME/.local/bin and uses curl/jq/tar accordingly to implement these changes.

In @contracts/test/mocks/BobExecute.s.sol:
- Around line 61-71: Add Natspec comments to the BatchDeploy contract and its
constructor: include a @notice describing the contract's purpose
(batch-deploying ExaAccounts for tests), a @dev explaining the internal behavior
(creates `count` accounts by populating owners array and calling
`factory.createAccount`), and @param tags for the constructor parameters
`factory` (ExaAccountFactory used to create accounts) and `count` (number of
accounts to create). Place the comments immediately above the contract
declaration and the constructor signature using @notice, @dev, and @param tags
referencing `factory` and `count`.

In @server/database/schema.ts:
- Around line 70-100: The new tables cursors, substreamsHistory, blocks, and
exaPlugins are missing explicit Drizzle relation declarations; add relations for
each table in server/database/schema.ts (like the existing
credentials/cards/transactions pattern) so Drizzle can generate typed query
results—locate the table symbols cursors, substreamsHistory, blocks, and
exaPlugins and add a relations callback/object that declares their foreign keys
and navigation properties (e.g., which column references which parent table and
any one-to-many or one-to-one mappings), keeping the existing primaryKey
callback for exaPlugins.

In @substreams/Dockerfile:
- Line 13: Replace the non-deterministic "releases/latest" fetch in the
Dockerfile RUN line with a pinned release tag (e.g., v1.10.0); specifically,
update the curl URL used in the RUN line to query the GitHub release for that
tag (replace "releases/latest" with "releases/tag/v1.10.0" or use the
releases/download/v1.10.0 endpoint) so the asset selection and tar extraction
always pull the exact desired substreams binary version.
- Around line 1-33: Add a HEALTHCHECK instruction to the Dockerfile to enable
container monitoring: insert a HEALTHCHECK line before ENTRYPOINT that
periodically probes the running process or endpoint (for example, check the
substreams process or an HTTP/status endpoint if available) and set sensible
intervals, timeouts, retries and a failure action; reference the existing
ENTRYPOINT and CMD symbols (ENTRYPOINT ["dumb-init", "/app/substreams-sink-sql",
"run"] and CMD ["postgres://...","substreams.yaml"]) to place the HEALTHCHECK
just above ENTRYPOINT and ensure the check uses the same command/port that
indicates the app is healthy.
- Line 33: The CMD entry currently hardcodes
"postgres://host.docker.internal:5432/postgres?schemaName=substreams&sslmode=disable"
which is not portable; change the Dockerfile CMD so the Postgres connection
string is configurable (e.g., read from an environment variable like
POSTGRES_DSN or use a clear local-dev placeholder such as
postgres://localhost:5432/... ), and keep the second argument "substreams.yaml"
as-is; update the CMD invocation (the CMD array that sets the two arguments) to
reference the env var or the new portable default so the container works outside
Docker Desktop.

In @substreams/package.json:
- Around line 43-49: The test target's dependsOn references a non-existent
"test:proto" task while the actual script/target is named "test:protolint";
update the dependsOn array for the "test" target to reference "test:protolint"
(or rename the script to "test:proto" if you prefer that name) so the dependency
matches the defined target name and Nx can resolve it.

In @substreams/proto/exa.proto:
- Line 3: The protobuf package declaration "package exa;" should be placed in a
file that matches its package directory; move the proto file so the package and
directory align (e.g., rename/move exa.proto into a directory named exa and use
exa/exa.proto) and then update any build/config references that point to the old
location (for example entries in substreams.yaml and related build scripts) so
tooling like buf sees package and path consistency.

In @substreams/rust-toolchain.toml:
- Around line 1-4: The toolchain is set to unpinned "nightly" while Cargo.toml
claims an invalid or future edition; either change the [toolchain] channel to
"stable" if Cargo.toml uses a valid edition like edition = "2021", or if you
truly require edition = "2024" keep nightly but pin it to a reproducible
snapshot (e.g., channel = "nightly-YYYY-MM-DD"); update rust-toolchain.toml's
channel accordingly and ensure Cargo.toml's edition field matches a
valid/intentional value so builds are reproducible.

In @substreams/src/lib.rs:
- Around line 54-71: The map_exa_plugins function inverts dedup logic for
new.accounts: the branch uses !seen.insert(address.to_vec()) which rejects the
first occurrence and accepts duplicates; change this so the function keeps the
first plugin from a new account and filters later duplicates by removing the
negation (use seen.insert(address.to_vec()) to allow first insertion and return
false for duplicates). Keep the existing behavior for existing accounts using
accounts.get_last(Hex(&address).to_string()) == 1 and ensure the overall closure
still returns Plugin { address: event.plugin, account: address.to_vec(),
ordinal: log.ordinal() } when appropriate.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7574bd5 and 16db65f.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • substreams/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (35)
  • .changeset/breezy-planes-repeat.md
  • .changeset/rare-readers-tell.md
  • .changeset/red-toys-pump.md
  • .changeset/violet-poems-heal.md
  • .dockerignore
  • .github/matcher.json
  • .github/workflows/server-build.yaml
  • .github/workflows/substreams-build.yaml
  • .github/workflows/test.yaml
  • .vscode/extensions.json
  • common/package.json
  • contracts/test/mocks/BobExecute.s.sol
  • cspell.json
  • docs/package.json
  • pnpm-workspace.yaml
  • server/Dockerfile
  • server/database/schema.ts
  • server/drizzle.config.ts
  • server/package.json
  • server/script/firehose.ts
  • server/test/anvil.ts
  • server/test/database.ts
  • server/test/e2e.ts
  • substreams/.cargo/config.toml
  • substreams/.gitignore
  • substreams/Cargo.toml
  • substreams/Dockerfile
  • substreams/buf.gen.yaml
  • substreams/build.rs
  • substreams/package.json
  • substreams/proto/exa.proto
  • substreams/rust-toolchain.toml
  • substreams/rustfmt.toml
  • substreams/src/lib.rs
  • substreams/substreams.yaml
🧰 Additional context used
📓 Path-based instructions (11)
server/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/server.mdc)

server/**/*.ts: Use c.var object to pass strongly-typed data between Hono middleware and route handlers; do not use c.set
All request validation (headers, body, params) must be handled by @hono/valibot-validator middleware; do not perform manual validation inside route handlers
Use Hono's built-in error handling by throwing new HTTPException() for expected errors; unhandled errors will be caught and logged automatically
Enforce Node.js best practices using ESLint plugin:n/recommended configuration
Enforce Drizzle ORM best practices using ESLint plugin:drizzle/all configuration, including requiring where clauses for update and delete operations
Use Drizzle ORM query builder for all database interactions; do not write raw SQL queries unless absolutely unavoidable
All authentication and authorization logic must be implemented in Hono middleware
Do not access process.env directly in application code; load all configuration and secrets once at startup and pass them through dependency injection or context
Avoid long-running, synchronous operations; use async/await correctly and be mindful of CPU-intensive tasks to prevent blocking the event loop

Files:

  • server/test/anvil.ts
  • server/test/e2e.ts
  • server/test/database.ts
  • server/drizzle.config.ts
  • server/database/schema.ts
  • server/script/firehose.ts
**/*.{js,ts,tsx,jsx,sol}

📄 CodeRabbit inference engine (AGENTS.md)

Follow linter/formatter (eslint, prettier, solhint) strictly with high strictness level. No any type.

Files:

  • server/test/anvil.ts
  • server/test/e2e.ts
  • server/test/database.ts
  • server/drizzle.config.ts
  • contracts/test/mocks/BobExecute.s.sol
  • server/database/schema.ts
  • server/script/firehose.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Omit redundant type names in variable declarations - let the type system explain itself

**/*.{ts,tsx}: Use PascalCase for TypeScript types and interfaces
Use valibot for all runtime validation of API inputs, environment variables, and other data; define schemas once and reuse them
Infer TypeScript types from valibot schemas using type User = v.Input<typeof UserSchema> instead of manually defining interfaces

Files:

  • server/test/anvil.ts
  • server/test/e2e.ts
  • server/test/database.ts
  • server/drizzle.config.ts
  • server/database/schema.ts
  • server/script/firehose.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Omit contextual names - don't repeat class/module names in members
Omit meaningless words like 'data', 'state', 'manager', 'engine', 'value' from variable and function names unless they add disambiguation

**/*.{ts,tsx,js,jsx}: Prefer function declarations for all multi-line functions; use function expressions or arrow functions only for single-line implementations
Prefer const for all variable declarations by default; only use let if the variable's value will be reassigned
Declare each variable on its own line with its own const or let keyword, not multiple declarations on one line
Use camelCase for TypeScript variables and functions
Always use import type { ... } for type imports
Use relative paths for all imports within the project; avoid tsconfig path aliases
Follow eslint-plugin-import order: react, external libraries, then relative paths
Use object and array destructuring to access and use properties
Use object method shorthand syntax when a function is a property of an object
Prefer optional chaining (?.), nullish coalescing (??), object and array spreading (...), and for...of loops over traditional syntax
Do not use abbreviations or cryptic names; write out full words like error, parameters, request instead of err, params, req
Use Number.parseInt() instead of the global parseInt() function when parsing numbers
All classes called with new must use PascalCase
Use Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe() instead of the deprecated new Buffer()
Use @ts-expect-error instead of @ts-ignore; follow it immediately with a single-line lowercase comment explaining why the error is expected, without separators like - or :
Do not include the type in a variable's name; let the static type system do its job (e.g., use const user: User not const userObject: User)
Do not repeat the name of a class or module within its members; omit contextual names (e.g., use `class User { getProfil...

Files:

  • server/test/anvil.ts
  • server/test/e2e.ts
  • server/test/database.ts
  • server/drizzle.config.ts
  • server/database/schema.ts
  • server/script/firehose.ts
server/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

server/**/*.{ts,tsx}: Server API: implement schema-first approach using OpenAPI via hono with validation via valibot middleware
Server database: drizzle schema is source of truth. Migrations required. No direct database access in handlers - use c.var.db

Files:

  • server/test/anvil.ts
  • server/test/e2e.ts
  • server/test/database.ts
  • server/drizzle.config.ts
  • server/database/schema.ts
  • server/script/firehose.ts
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

For files with a single default export, name the file identically to the export; for files with multiple exports, use camelCase with a strong preference for a single word

Files:

  • server/test/anvil.ts
  • server/test/e2e.ts
  • server/test/database.ts
  • server/drizzle.config.ts
  • server/database/schema.ts
  • server/script/firehose.ts
**/.changeset/*.md

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

Use a lowercase sentence in the imperative present tense for changeset summaries

Files:

  • .changeset/violet-poems-heal.md
  • .changeset/breezy-planes-repeat.md
  • .changeset/rare-readers-tell.md
  • .changeset/red-toys-pump.md
**/+(app|src)/**

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

Use kebab-case for directory names and public Expo router routes

Files:

  • substreams/src/lib.rs
contracts/**/*.sol

📄 CodeRabbit inference engine (.cursor/rules/contracts.mdc)

contracts/**/*.sol: Import order must be sorted alphabetically and grouped by type (openzeppelin, solady, internal, etc.), enforced by forge fmt
All functions and state variables must have explicit visibility modifiers (public, private, internal, external)
Use custom errors instead of require() statements with string messages; require() with messages is forbidden
console.log is forbidden in contract code
All external and public functions that modify state must be protected by a reentrancy guard, unless explicitly proven unnecessary
Use modifiers like onlyOwner or function-level checks for privileged functions; default to strictest access level (private > internal > external > public)
Use named parameters in mappings for clarity (e.g., mapping(address user => uint256 balance))
All code must pass pnpm --filter plugin test:slither and pnpm --filter plugin test:solhint with zero warnings; do not ignore warnings
Follow standard Solidity import order: external libraries (openzeppelin, solady) first, then project-internal contracts
All public and external functions and state variables must have complete natspec comments (@notice, @dev, @param, @return)
Use custom errors instead of require statements with string messages for better gas efficiency and error information
Internal and private variables should be prefixed with an underscore (_); function arguments should not be prefixed
Use constant keyword for true constants; use immutable for configurable values like contract addresses
Minimize writes to storage; read data into memory or calldata whenever possible for gas optimization
Use smallest integer size that is safe for the variable (e.g., uint32 for timestamps) and be aware of struct packing for gas efficiency
Prefer using the solady library over openzeppelin where possible, as it is highly optimized for gas
Correctly label functions as view or pure when they do not modify state

contracts/**/*.sol: Smart contracts: use custom errors error MyError(); instead of s...

Files:

  • contracts/test/mocks/BobExecute.s.sol
**/*.sol

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

**/*.sol: Use camelCase for Solidity variables and functions
Use PascalCase for Solidity contracts and structs

Files:

  • contracts/test/mocks/BobExecute.s.sol
server/database/schema.ts

📄 CodeRabbit inference engine (.cursor/rules/server.mdc)

server/database/schema.ts: Database schema must be defined in server/database/schema.ts as the single source of truth for database structure
Define relations explicitly in the schema file to enable Drizzle to generate typed query results including related entities

Files:

  • server/database/schema.ts
🧠 Learnings (16)
📚 Learning: 2025-12-31T00:23:55.034Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 610
File: .changeset/ready-experts-fly.md:1-2
Timestamp: 2025-12-31T00:23:55.034Z
Learning: In the exactly/exa repository, allow and require empty changeset files (containing only --- separators) when changes are not user-facing and do not warrant a version bump. This is needed because CI runs changeset status --since origin/main and requires a changeset file to exist. Ensure such empty changesets are used only for non-user-facing changes and document the rationale in the commit or changelog notes.

Applied to files:

  • .changeset/violet-poems-heal.md
  • .changeset/breezy-planes-repeat.md
  • .changeset/rare-readers-tell.md
  • .changeset/red-toys-pump.md
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Zero config local dev environment: no `.env` files, mock all external services

Applied to files:

  • server/test/e2e.ts
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Applies to {src/generated/**,server/generated/**,contracts/broadcast/**,contracts/cache/**} : Do not manually edit files in generated directories: `src/generated/`, `server/generated/`, `contracts/broadcast/`, and `contracts/cache/`

Applied to files:

  • substreams/.gitignore
  • .dockerignore
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Avoid long-running, synchronous operations; use `async/await` correctly and be mindful of CPU-intensive tasks to prevent blocking the event loop

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Applies to server/**/*.{ts,tsx} : Server database: drizzle schema is source of truth. Migrations required. No direct database access in handlers - use `c.var.db`

Applied to files:

  • server/test/database.ts
  • server/drizzle.config.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Server stack: use hono, node.js, drizzle orm, postgres

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Use Drizzle ORM query builder for all database interactions; do not write raw SQL queries unless absolutely unavoidable

Applied to files:

  • server/test/database.ts
  • server/drizzle.config.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/database/schema.ts : Define relations explicitly in the schema file to enable Drizzle to generate typed query results including related entities

Applied to files:

  • server/test/database.ts
  • server/drizzle.config.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/database/migrations/** : All schema changes must be accompanied by a migration file generated by `drizzle-kit`; never edit the database schema manually

Applied to files:

  • server/test/database.ts
  • server/drizzle.config.ts
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Server (exactly/server) uses Hono (framework), Node.js, Drizzle ORM (database), and PostgreSQL

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Enforce Drizzle ORM best practices using ESLint `plugin:drizzle/all` configuration, including requiring `where` clauses for `update` and `delete` operations

Applied to files:

  • server/test/database.ts
  • server/drizzle.config.ts
📚 Learning: 2025-12-30T15:03:28.449Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Omit meaningless words from names like `data`, `state`, `manager`, `engine`, `object`, `entity`, and `instance`

Applied to files:

  • cspell.json
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Omit meaningless words like 'data', 'state', 'manager', 'engine', 'value' from variable and function names unless they add disambiguation

Applied to files:

  • cspell.json
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/database/schema.ts : Database schema must be defined in `server/database/schema.ts` as the single source of truth for database structure

Applied to files:

  • server/drizzle.config.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Use `nx` for running tasks (build, lint, test, e2e) instead of underlying tooling directly

Applied to files:

  • common/package.json
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Prepare the environment by running `pnpm prepare`, which handles git hooks, code generation (wagmi), and versioning

Applied to files:

  • common/package.json
🪛 Buf (1.62.1)
substreams/proto/exa.proto

3-3: Files with package "exa" must be within a directory "exa" relative to root but were in directory "substreams/proto".

(PACKAGE_DIRECTORY_MATCH)

🪛 Checkov (3.2.334)
substreams/Dockerfile

[low] 1-33: Ensure that HEALTHCHECK instructions have been added to container images

(CKV_DOCKER_2)

🪛 YAMLlint (1.37.1)
.github/workflows/substreams-build.yaml

[warning] 2-2: truthy value should be one of [false, true]

(truthy)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 6-6: too many spaces inside braces

(braces)


[error] 6-6: too many spaces inside braces

(braces)


[error] 11-11: too many spaces inside braces

(braces)


[error] 11-11: too many spaces inside braces

(braces)


[error] 25-25: too many spaces inside braces

(braces)


[error] 25-25: too many spaces inside braces

(braces)

🔇 Additional comments (36)
.changeset/breezy-planes-repeat.md (1)

1-5: LGTM! Changeset follows the guidelines.

The changeset summary correctly uses lowercase and imperative present tense as required by the coding guidelines.

.changeset/red-toys-pump.md (1)

1-5: LGTM! Changeset follows the guidelines.

The changeset summary correctly uses lowercase and imperative present tense as required by the coding guidelines.

substreams/.gitignore (1)

1-8: LGTM! Appropriate ignore rules for generated artifacts.

The ignore rules correctly exclude generated directories (src/contracts, src/proto) and build artifacts (target, abi), which aligns with the project's code generation workflow.

pnpm-workspace.yaml (1)

7-7: LGTM! Workspace package addition is correct.

The addition of the substreams package to the workspace is appropriate and necessary for the new substreams project integration.

.vscode/extensions.json (1)

6-6: LGTM! Extensions align with new Substreams development.

The addition of Protobuf and Rust analyzer extensions appropriately supports the new Rust-based Substreams package and Protobuf definitions introduced in this PR.

Also applies to: 16-16

substreams/.cargo/config.toml (1)

1-2: LGTM! Standard WASM build configuration.

This configuration correctly sets the default build target for WebAssembly, which is required for Substreams module compilation.

.github/workflows/server-build.yaml (1)

36-37: LGTM! Correct Dockerfile path configuration.

The explicit file parameter correctly points to the relocated Dockerfile while maintaining the root context for Docker build access.

common/package.json (1)

43-50: LGTM! Enhanced build dependency tracking.

The additional environment input and dependent task output files improve NX's incremental build accuracy by ensuring wagmi regenerates when contract artifacts or deployment broadcasts change.

.changeset/violet-poems-heal.md (1)

5-5: Remove emoji to follow changeset guidelines.

As per coding guidelines, changeset summaries should use "a lowercase sentence in the imperative present tense" without decorative elements.

📝 Suggested fix
-✨ insert exa plugins into database
+insert exa plugins into database
⛔ Skipped due to learnings
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/.changeset/*.md : Use a lowercase sentence in the imperative present tense for changeset summaries
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/git.mdc:0-0
Timestamp: 2025-12-23T19:57:14.465Z
Learning: Select the most precise gitmoji that best communicates the intent of the change (e.g., ✨ for new feature, 🐛 for bug fix, ♻️ for refactoring, ⚡️ for performance improvement, 📝 for documentation, 🚀 for deployment)
Learnt from: cruzdanilo
Repo: exactly/exa PR: 610
File: .changeset/ready-experts-fly.md:1-2
Timestamp: 2025-12-31T00:24:05.835Z
Learning: In the exactly/exa repository, empty changeset files (containing only `---` separators) are acceptable and required for CI to pass when changes are not user-facing and don't warrant a version bump, as the CI runs `changeset status --since origin/main` which requires a changeset file to be present.
server/drizzle.config.ts (1)

8-8: LGTM! Schema filter correctly configured for multi-schema support.

The addition of schemaFilter enables drizzle-kit to introspect both the public and substreams PostgreSQL schemas, which aligns with the new substreams infrastructure introduced in this PR.

cspell.json (2)

6-9: LGTM! Ignore paths correctly added for generated files.

The ignore paths for .github/matcher.json and substreams/target appropriately exclude configuration and build artifacts from spell-checking.


14-138: LGTM! Dictionary appropriately expanded for Substreams tooling.

The additions cover Rust tooling (clippy, rustup, serde, prost), Protobuf tools (bufbuild, protogen, protolint), and Substreams ecosystem terms (fireeth, streamingfast, substreams, spkg), all relevant to the new substreams implementation.

substreams/buf.gen.yaml (1)

1-8: LGTM! Buf configuration correctly set up for Rust code generation.

The configuration uses appropriate prost plugins for generating Rust bindings from Protobuf definitions, with sensible options for a substreams project.

substreams/Dockerfile (1)

9-9: The Rust toolchain is properly specified via rust-toolchain.toml (located in the substreams directory), which configures the nightly channel with required components (clippy, rustfmt) and the wasm32-unknown-unknown target. Using --default-toolchain none in the Dockerfile is correct—rustup will automatically detect and apply the toolchain from the configuration file when the build runs.

Likely an incorrect or invalid review comment.

.github/matcher.json (1)

63-92: LGTM! Well-structured Rust problem matchers.

The new problem matchers correctly parse Rust compiler warnings/errors and formatter diffs, with proper ANSI escape code handling for CI environments.

.dockerignore (1)

6-25: LGTM! Comprehensive Docker build optimization.

The ignore patterns appropriately exclude build artifacts, generated files, and development-only files while preserving necessary runtime artifacts like run-latest.json.

docs/package.json (1)

37-44: LGTM! Proper Nx cache tracking.

The inputs configuration correctly tracks production sources and the dependent openapi.json output for accurate build caching and invalidation.

contracts/test/mocks/BobExecute.s.sol (2)

39-39: LGTM! Semantic change to address retrieval.

Changing from createAccount to getAddress correctly retrieves the account address rather than creating it, which aligns with the test's intent.


55-55: LGTM! Batch deployment pattern for testing.

Instantiating BatchDeploy for its constructor side effects is an acceptable pattern in test scripts for batch account creation.

substreams/package.json (1)

20-62: LGTM! Well-configured Nx build target.

The build target correctly tracks Rust source files, Cargo manifests, environment variables, and dependent task outputs for proper cache invalidation. The WASM output path is appropriate for the target platform.

server/database/schema.ts (1)

36-36: LGTM: Updated index definition follows Drizzle ORM patterns.

The function-based selector for the unique index is a valid Drizzle ORM pattern and functionally equivalent to the previous definition.

server/script/firehose.ts (2)

4-65: LGTM: ProtoCursor implementation is correct.

The minimal Protobuf parser correctly implements varint reading, tag parsing, and field skipping. The non-null assertion at line 20 is safe due to the preceding bounds check.


67-107: LGTM: Timestamp extraction logic is sound.

The nested cursor pattern correctly navigates the Protobuf structure (block → header → timestamp → seconds/nanoseconds) and gracefully returns null when fields are missing.

.changeset/rare-readers-tell.md (1)

1-5: LGTM: Changeset follows formatting guidelines.

The changeset summary uses lowercase imperative present tense as required by the coding guidelines.

server/test/anvil.ts (1)

159-159: LGTM: Added test filter for targeted execution.

The --tc BobExecuteScript flag narrows the forge script execution to a specific test contract, improving test precision and potentially performance.

server/test/e2e.ts (2)

7-7: LGTM: Type import for Sentry mock typing.

Correctly uses import type for the type-only import needed by the mock implementation.


58-67: LGTM: Sentry mock enhances test observability.

The mock correctly wraps captureException to log error details during tests while preserving the original behavior. The implementation is properly typed and follows the established mocking pattern in this file.

substreams/rustfmt.toml (1)

1-6: LGTM: Standard rustfmt configuration.

The rustfmt settings are appropriate and follow common Rust formatting practices. The 2-space indentation aligns with project conventions, and the 120-character line width provides good readability.

substreams/Cargo.toml (3)

11-21: LGTM: Dependencies are appropriate for Ethereum substreams.

The dependency list includes standard crates for Ethereum contract interaction, protobuf handling, and substreams processing. Versions appear current and compatible.


32-35: LGTM: Release profile optimizations are appropriate.

The release profile correctly optimizes for size with LTO enabled and debug info stripped, which is suitable for a production substreams library that may be deployed in size-constrained environments.


1-9: Edition 2024 implicitly enables resolver version 3—verify dependency resolution is working correctly.

The Cargo manifest specifies edition = "2024", which was stabilized in Rust 1.85.0 (February 20, 2025). While dependencies like substreams (0.7) and substreams-ethereum (0.11) are compatible with this edition, Rust 2024 implicitly enables resolver version 3, which can cause subtle dependency resolution differences. The nightly toolchain is appropriate, but ensure cargo check and local builds pass without resolver-related warnings or version conflicts, particularly with the implicit resolver upgrade.

substreams/proto/exa.proto (1)

1-17: LGTM: Protobuf definitions are clear and appropriate.

The message structures are straightforward and suitable for representing accounts and plugins in the substreams context. The use of bytes for addresses and accounts is standard for Ethereum data.

server/package.json (2)

64-64: The @exactly/substreams devDependency is actively used in server/test/database.ts for test setup and initialization of substreams services. The dependency is referenced in test commands and as a working directory path for shell execution, making it a necessary development dependency.

Likely an incorrect or invalid review comment.


143-146: No actionable concern — the pattern out/**/*.json is correct. It properly tracks the build outputs from @exactly/plugin that the generate:broadcasts task depends on. The broadcast output files are a separate, downstream output of this target, not part of its input dependencies.

server/test/database.ts (1)

51-71: Verify error handling during teardown doesn't mask legitimate failures.

The catch blocks check controller.signal.aborted and silently return, which correctly avoids re-throwing cancellation errors during teardown. However, this pattern could also mask legitimate process failures that occur after abort() is called but before the signal is checked.

Consider whether non-abort errors during teardown should be logged or handled differently to aid debugging.

substreams/substreams.yaml (1)

78-82: The anvil factory address is hardcoded and must be kept in sync with the external plugin deployment.

The factory address Eb2454D494082f2294Fc5d4Cd67096f13E64306f in substreams.yaml line 82 is static, while the test setup (server/test/database.ts line 84) extracts the actual deployed address from node_modules/@exactly/plugin/broadcast/ExaAccountFactory.s.sol/31337/run-latest.json. If the external plugin is updated or redeployed, the hardcoded manifest address could become stale and cause the test environment to use the wrong factory. Consider extracting this address dynamically from the plugin at build time or adding a validation step to ensure they remain synchronized.

@sentry
Copy link

sentry bot commented Jan 8, 2026

Codecov Report

❌ Patch coverage is 22.07792% with 120 lines in your changes missing coverage. Please review.
✅ Project coverage is 53.69%. Comparing base (06ab3e8) to head (89f6bb0).
⚠️ Report is 20 commits behind head on main.

Files with missing lines Patch % Lines
server/script/firehose.ts 0.00% 119 Missing ⚠️
server/drizzle.config.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #627      +/-   ##
==========================================
- Coverage   54.12%   53.69%   -0.43%     
==========================================
  Files          50       51       +1     
  Lines        7912     8072     +160     
  Branches      641      651      +10     
==========================================
+ Hits         4282     4334      +52     
- Misses       3615     3723     +108     
  Partials       15       15              
Flag Coverage Δ
github 53.67% <22.07%> (-0.45%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 22

🤖 Fix all issues with AI agents
In @.github/workflows/test.yaml:
- Around line 37-41: The workflow currently downloads and extracts binaries in
the for loop using curl and tar without integrity checks; modify the loop that
iterates over repos (firehose-ethereum, substreams, substreams-sink-sql) so that
for each release you (a) resolve a pinned release/tag or fetch the release
assets URL and also fetch the associated checksum file or signature, (b)
download both the binary and its checksum/signature, (c) verify the binary with
sha256sum (or gpg verify if a signature is provided) before running tar zxf, and
fail the job if verification fails; ensure the curl + tar sequence is replaced
with a download-then-verify-then-extract flow and prefer pinning a specific
release version instead of using "latest".
- Around line 37-41: The workflow step that downloads binaries in
.github/workflows/test.yaml (the for loop iterating repos: firehose-ethereum,
substreams, substreams-sink-sql) should use actions/cache to avoid
re-downloading unchanged release assets: add a cache restore step before the
curl/tar loop that computes a key from each repository's latest release
identifier (e.g., the GitHub API release tag or asset URL) and the runner
architecture, run the existing download loop only if the cache is missing, and
then add a cache-save step after the downloads to store $HOME/.local/bin under
the same computed key so subsequent runs restore the cached binaries instead of
fetching them again.
- Around line 37-41: Wrap the download loop in a robust shell block that enables
strict failure handling (set -euo pipefail), use the GITHUB_TOKEN for
authenticated GitHub API requests by adding an Authorization header to curl,
split the nested curl into two steps: first query the release API to capture
ASSET_URL into a variable (using uname mapping for arch), validate ASSET_URL is
non-empty and fail with a clear message if not, then download with curl -fL
"$ASSET_URL" and extract with tar; ensure each repo iteration
(firehose-ethereum, substreams, substreams-sink-sql) checks curl exit codes and
aborts on error so failures are reported clearly.

In @.vscode/extensions.json:
- Line 6: Replace the invalid VS Code extension ID "drblury.protobuf-vsc" in the
extensions.json recommendations with a maintained protobuf extension; update the
string to one of the suggested IDs such as "bufbuild.vscode-buf" (recommended),
"zxh404.vscode-proto3", or "peterj.vscode-protobuf" so the recommendation
resolves correctly in the Marketplace.

In @common/package.json:
- Around line 44-50: Update the three "dependentTasksOutputFiles" entries in
common/package.json so they point to Foundry's actual broadcast outputs: replace
"out/**/*.json" and "broadcast/*/31337/*.json" with a single pattern that
matches contracts/broadcast locations (e.g., "contracts/broadcast/**/*.json");
edit the array item(s) containing the "dependentTasksOutputFiles" key to use the
corrected "contracts/broadcast/**/*.json" pattern so NX change detection for the
generate:wagmi task will pick up Foundry script broadcasts.

In @contracts/test/mocks/BobExecute.s.sol:
- Around line 61-71: The BatchDeploy contract constructor lacks natspec and uses
an unexplained magic number; add NatSpec comments for the contract and its
constructor (include @notice and @param for factory and count and, if added,
startAddr) and replace or document the hardcoded 420 by either introducing a
constructor parameter (e.g., startAddr) and using that instead of 420 in the
loop or adding a clear inline comment explaining why 420 is chosen; update the
constructor signature in BatchDeploy and its NatSpec accordingly so the intent
and the starting address are explicit.
- Line 39: bobAccount is only computed via factory.getAddress() but never
deployed, so calls to bobAccount.executeProposal() in run() will fail; replace
the address-only call with an actual deployment (use factory.createAccount(...)
with the same salt and owners.toPublicKeys() or otherwise ensure the account is
created before run()), and update the BatchDeploy usage so it deploys Bob’s
account (salt 0 with owner 0xb0b / owners index 0) rather than creating accounts
for salts 0/owners 420–423; ensure the deployed account reference (bobAccount)
points to the real deployed contract prior to invoking run() and its
executeProposal() calls.

In @server/database/schema.ts:
- Around line 79-86: The audit table definition for substreamsHistory currently
leaves critical audit columns nullable; update the column definitions on the
substreamsHistory table so that op, tableName, and pk are marked .notNull()
(e.g., change char("op", { length: 1 }) → char(...).notNull(), and
text("table_name")/text("pk") → text(...).notNull()), while leaving prevValue
nullable to allow INSERTs with no previous value; ensure the serial id and
blockNum definitions remain unchanged.

In @server/script/firehose.ts:
- Line 127: The variable name payloadB64 in the array destructuring (const [, ,
num, hash, libNum, parentHash, payloadB64] = parts) uses an abbreviation; rename
it to payloadBase64 and update every subsequent reference in this module to the
new identifier (e.g., any use in parsing, decoding, or function calls) so the
code compiles and consistently uses the full word "Base64" instead of "B64".
- Around line 59-64: readBytes() does not validate that the length read via
this.readVarint() keeps this.offset within this.buffer bounds, risking
out-of-range reads; update readBytes() to check that length is non-negative and
that (this.offset + length) <= this.buffer.length before advancing this.offset,
throw or return a clear error if the check fails, and use the same symbols
(readBytes, this.readVarint, this.offset, this.buffer) so callers get a
deterministic failure instead of corrupted data.

In @server/test/database.ts:
- Around line 92-95: The teardown currently calls controller.abort() then
immediately awaits postgres.stop(), which can race with running sub-processes
(forceKillAfterDelay: 33_333) and leave connections open; update the setup to
capture the started process promises (e.g., store process/child promises in an
array or variables when launching substreams-sink-sql) and in the teardown
function do controller.abort(); await Promise.all(processPromises) (or await
each process termination promise) before calling await postgres.stop();
alternatively, if capturing promises is not possible, insert a small await delay
after controller.abort() to give processes time to exit before calling
postgres.stop().

In @substreams/build.rs:
- Around line 22-35: The current contracts.iter().try_for_each block uses
assert! on the Command::new("bash") .status() which panics without context when
jq/bash fails; change this to run the command with .output(), check the exit
status, and return a descriptive Err (or use anyhow::bail!) including mod_name
and contract plus stderr/stdout content so failures are propagated rather than
panicked; update the Command invocation site and its error handling (the closure
passed to try_for_each and the Command::new("bash") call) to use the command
output for richer error context before calling Abigen::new.

In @substreams/Dockerfile:
- Around line 32-33: The Dockerfile's CMD hardcodes a Postgres URL with
host.docker.internal (ENTRYPOINT ["dumb-init", "/app/substreams-sink-sql",
"run"] and CMD
["postgres://host.docker.internal:5432/postgres?schemaName=substreams&sslmode=disable",
"substreams.yaml"]), which only works on Docker Desktop; change CMD to use an
environment variable (e.g., POSTGRES_URL) or switch to a shell/wrapper script
that expands ${POSTGRES_URL} (falling back to a sensible local default) and
document that production deployments must override POSTGRES_URL via
orchestration; ensure the ENTRYPOINT remains the exec form and add README notes
about local vs production usage and overriding CMD/env.
- Around line 12-13: The Dockerfile currently fetches the "latest" release which
makes builds non-deterministic; update the RUN that calls GitHub API and curl to
pin the substreams CLI to v1.15.6 (replace the "releases/latest" URL or the
queried tag with "releases/tag/v1.15.6" and ensure the jq filter still selects
the linux_$arch asset), so the WORKDIR /usr/local/bin RUN curl ... command
downloads and extracts the v1.15.6 asset instead of the latest.

In @substreams/package.json:
- Around line 43-50: The workspace "test" target depends on a non-existent
target named "test:proto"; update the NX targets so the dependency matches the
actual script/target name (either add a new target named "test:proto" or change
the dependency to "test:protolint"), ensuring the "test" target's dependsOn
array references the existing "test:protolint" target or create a corresponding
target that runs the protolint script; update the dependsOn entry for "test"
(and/or add the missing target definition) to eliminate the naming mismatch
between "test:proto" and "test:protolint".

In @substreams/proto/exa.proto:
- Around line 1-7: The proto file declares package "exa" (message Accounts) but
lives at substreams/proto/exa.proto which triggers Buf's
package-directory-mismatch lint; either move the file into a matching directory
(substreams/proto/exa/exa.proto) and update substreams.yaml importPaths
accordingly, or if the layout is intentional add/adjust buf.yaml to disable or
override the package-directory-mismatch rule for this file; locate the file
defining package exa and message Accounts and apply one of these two fixes so
Buf no longer warns.
- Around line 5-17: Add concise proto comments for the messages and fields to
document expected formats: annotate message Accounts and its field accounts with
a comment like "repeated 20-byte Ethereum addresses" (or the correct
byte-length/format), annotate message Plugins and nested Plugin plus fields
address, account with descriptions (e.g., "20-byte Ethereum address",
"associated account bytes/format"), and annotate ordinal with its semantic
meaning (e.g., "plugin ordering index"). Update comments inline above symbols
Accounts, accounts, Plugins, Plugin, address, account, and ordinal so future
readers understand the expected byte formats and semantics.

In @substreams/rust-toolchain.toml:
- Around line 1-4: Replace the toolchain channel setting from "nightly" to
"stable" in the [toolchain] block (change channel = "nightly" to channel =
"stable") so the file uses the stable Rust toolchain while keeping components =
["clippy", "rustfmt"] and targets = ["wasm32-unknown-unknown"] unchanged.

In @substreams/src/lib.rs:
- Around line 53-71: The deduplication in map_exa_plugins is inverted because
the negation on !seen.insert(address.to_vec()) causes first occurrences to be
dropped and duplicates kept; change the logic to use
seen.insert(address.to_vec()) (remove the !) so that the first time an address
from new.accounts is encountered it returns true (included) and subsequent
duplicates return false (excluded); keep the rest of the conditional (the
accounts.get_last(...) branch and construction of Plugin with event.plugin,
account: address.to_vec(), ordinal: log.ordinal()) unchanged.
- Around line 27-44: The nested iterator chain in map_exa_accounts (parsing
params, decoding Hex::decode, collecting into Vec, then mapping to slices)
reduces readability; refactor by extracting the parsed factories and their
decoded byte vectors into temporary variables (e.g., parse
serde_qs::from_str::<Factories>(&params)? to a factories variable, decode into a
Vec<Vec<u8>> via Hex::decode and ? for errors, then create a Vec<&[u8]> by
mapping Vec::as_slice) and pass that slice vector to
block.events::<ExaAccountInitialized>(&slices) and proceed to map and collect as
before to simplify the flow and avoid deeply nested iterator expressions.

In @substreams/substreams.yaml:
- Around line 43-48: The sink configuration in substreams.yaml points schema:
../server/generated/schema.sql which doesn't match the Dockerfile copy
destination; update the sink.config.schema value to ../server/schema.sql (or
alternatively change the Dockerfile to copy into ../server/generated/schema.sql)
so the path referenced by the sink.module db_out (type
sf.substreams.sink.sql.v1.Service) matches the actual file location at runtime.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7574bd5 and 284cc90.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • substreams/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (36)
  • .changeset/breezy-planes-repeat.md
  • .changeset/rare-readers-tell.md
  • .changeset/red-toys-pump.md
  • .changeset/violet-poems-heal.md
  • .dockerignore
  • .github/matcher.json
  • .github/workflows/server-build.yaml
  • .github/workflows/substreams-build.yaml
  • .github/workflows/test.yaml
  • .vscode/extensions.json
  • common/package.json
  • contracts/package.json
  • contracts/test/mocks/BobExecute.s.sol
  • cspell.json
  • docs/package.json
  • pnpm-workspace.yaml
  • server/Dockerfile
  • server/database/schema.ts
  • server/drizzle.config.ts
  • server/package.json
  • server/script/firehose.ts
  • server/test/anvil.ts
  • server/test/database.ts
  • server/test/e2e.ts
  • substreams/.cargo/config.toml
  • substreams/.gitignore
  • substreams/Cargo.toml
  • substreams/Dockerfile
  • substreams/buf.gen.yaml
  • substreams/build.rs
  • substreams/package.json
  • substreams/proto/exa.proto
  • substreams/rust-toolchain.toml
  • substreams/rustfmt.toml
  • substreams/src/lib.rs
  • substreams/substreams.yaml
🧰 Additional context used
📓 Path-based instructions (11)
server/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/server.mdc)

server/**/*.ts: Use c.var object to pass strongly-typed data between Hono middleware and route handlers; do not use c.set
All request validation (headers, body, params) must be handled by @hono/valibot-validator middleware; do not perform manual validation inside route handlers
Use Hono's built-in error handling by throwing new HTTPException() for expected errors; unhandled errors will be caught and logged automatically
Enforce Node.js best practices using ESLint plugin:n/recommended configuration
Enforce Drizzle ORM best practices using ESLint plugin:drizzle/all configuration, including requiring where clauses for update and delete operations
Use Drizzle ORM query builder for all database interactions; do not write raw SQL queries unless absolutely unavoidable
All authentication and authorization logic must be implemented in Hono middleware
Do not access process.env directly in application code; load all configuration and secrets once at startup and pass them through dependency injection or context
Avoid long-running, synchronous operations; use async/await correctly and be mindful of CPU-intensive tasks to prevent blocking the event loop

Files:

  • server/drizzle.config.ts
  • server/test/anvil.ts
  • server/script/firehose.ts
  • server/test/e2e.ts
  • server/test/database.ts
  • server/database/schema.ts
**/*.{js,ts,tsx,jsx,sol}

📄 CodeRabbit inference engine (AGENTS.md)

Follow linter/formatter (eslint, prettier, solhint) strictly with high strictness level. No any type.

Files:

  • server/drizzle.config.ts
  • server/test/anvil.ts
  • server/script/firehose.ts
  • server/test/e2e.ts
  • contracts/test/mocks/BobExecute.s.sol
  • server/test/database.ts
  • server/database/schema.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Omit redundant type names in variable declarations - let the type system explain itself

**/*.{ts,tsx}: Use PascalCase for TypeScript types and interfaces
Use valibot for all runtime validation of API inputs, environment variables, and other data; define schemas once and reuse them
Infer TypeScript types from valibot schemas using type User = v.Input<typeof UserSchema> instead of manually defining interfaces

Files:

  • server/drizzle.config.ts
  • server/test/anvil.ts
  • server/script/firehose.ts
  • server/test/e2e.ts
  • server/test/database.ts
  • server/database/schema.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Omit contextual names - don't repeat class/module names in members
Omit meaningless words like 'data', 'state', 'manager', 'engine', 'value' from variable and function names unless they add disambiguation

**/*.{ts,tsx,js,jsx}: Prefer function declarations for all multi-line functions; use function expressions or arrow functions only for single-line implementations
Prefer const for all variable declarations by default; only use let if the variable's value will be reassigned
Declare each variable on its own line with its own const or let keyword, not multiple declarations on one line
Use camelCase for TypeScript variables and functions
Always use import type { ... } for type imports
Use relative paths for all imports within the project; avoid tsconfig path aliases
Follow eslint-plugin-import order: react, external libraries, then relative paths
Use object and array destructuring to access and use properties
Use object method shorthand syntax when a function is a property of an object
Prefer optional chaining (?.), nullish coalescing (??), object and array spreading (...), and for...of loops over traditional syntax
Do not use abbreviations or cryptic names; write out full words like error, parameters, request instead of err, params, req
Use Number.parseInt() instead of the global parseInt() function when parsing numbers
All classes called with new must use PascalCase
Use Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe() instead of the deprecated new Buffer()
Use @ts-expect-error instead of @ts-ignore; follow it immediately with a single-line lowercase comment explaining why the error is expected, without separators like - or :
Do not include the type in a variable's name; let the static type system do its job (e.g., use const user: User not const userObject: User)
Do not repeat the name of a class or module within its members; omit contextual names (e.g., use `class User { getProfil...

Files:

  • server/drizzle.config.ts
  • server/test/anvil.ts
  • server/script/firehose.ts
  • server/test/e2e.ts
  • server/test/database.ts
  • server/database/schema.ts
server/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

server/**/*.{ts,tsx}: Server API: implement schema-first approach using OpenAPI via hono with validation via valibot middleware
Server database: drizzle schema is source of truth. Migrations required. No direct database access in handlers - use c.var.db

Files:

  • server/drizzle.config.ts
  • server/test/anvil.ts
  • server/script/firehose.ts
  • server/test/e2e.ts
  • server/test/database.ts
  • server/database/schema.ts
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

For files with a single default export, name the file identically to the export; for files with multiple exports, use camelCase with a strong preference for a single word

Files:

  • server/drizzle.config.ts
  • server/test/anvil.ts
  • server/script/firehose.ts
  • server/test/e2e.ts
  • server/test/database.ts
  • server/database/schema.ts
**/.changeset/*.md

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

Use a lowercase sentence in the imperative present tense for changeset summaries

Files:

  • .changeset/rare-readers-tell.md
  • .changeset/breezy-planes-repeat.md
  • .changeset/red-toys-pump.md
  • .changeset/violet-poems-heal.md
**/+(app|src)/**

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

Use kebab-case for directory names and public Expo router routes

Files:

  • substreams/src/lib.rs
contracts/**/*.sol

📄 CodeRabbit inference engine (.cursor/rules/contracts.mdc)

contracts/**/*.sol: Import order must be sorted alphabetically and grouped by type (openzeppelin, solady, internal, etc.), enforced by forge fmt
All functions and state variables must have explicit visibility modifiers (public, private, internal, external)
Use custom errors instead of require() statements with string messages; require() with messages is forbidden
console.log is forbidden in contract code
All external and public functions that modify state must be protected by a reentrancy guard, unless explicitly proven unnecessary
Use modifiers like onlyOwner or function-level checks for privileged functions; default to strictest access level (private > internal > external > public)
Use named parameters in mappings for clarity (e.g., mapping(address user => uint256 balance))
All code must pass pnpm --filter plugin test:slither and pnpm --filter plugin test:solhint with zero warnings; do not ignore warnings
Follow standard Solidity import order: external libraries (openzeppelin, solady) first, then project-internal contracts
All public and external functions and state variables must have complete natspec comments (@notice, @dev, @param, @return)
Use custom errors instead of require statements with string messages for better gas efficiency and error information
Internal and private variables should be prefixed with an underscore (_); function arguments should not be prefixed
Use constant keyword for true constants; use immutable for configurable values like contract addresses
Minimize writes to storage; read data into memory or calldata whenever possible for gas optimization
Use smallest integer size that is safe for the variable (e.g., uint32 for timestamps) and be aware of struct packing for gas efficiency
Prefer using the solady library over openzeppelin where possible, as it is highly optimized for gas
Correctly label functions as view or pure when they do not modify state

contracts/**/*.sol: Smart contracts: use custom errors error MyError(); instead of s...

Files:

  • contracts/test/mocks/BobExecute.s.sol
**/*.sol

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

**/*.sol: Use camelCase for Solidity variables and functions
Use PascalCase for Solidity contracts and structs

Files:

  • contracts/test/mocks/BobExecute.s.sol
server/database/schema.ts

📄 CodeRabbit inference engine (.cursor/rules/server.mdc)

server/database/schema.ts: Database schema must be defined in server/database/schema.ts as the single source of truth for database structure
Define relations explicitly in the schema file to enable Drizzle to generate typed query results including related entities

Files:

  • server/database/schema.ts
🧠 Learnings (21)
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/database/schema.ts : Define relations explicitly in the schema file to enable Drizzle to generate typed query results including related entities

Applied to files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Applies to server/**/*.{ts,tsx} : Server database: drizzle schema is source of truth. Migrations required. No direct database access in handlers - use `c.var.db`

Applied to files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/database/schema.ts : Database schema must be defined in `server/database/schema.ts` as the single source of truth for database structure

Applied to files:

  • server/drizzle.config.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/database/migrations/** : All schema changes must be accompanied by a migration file generated by `drizzle-kit`; never edit the database schema manually

Applied to files:

  • server/drizzle.config.ts
  • server/test/database.ts
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Applies to {src/generated/**,server/generated/**,contracts/broadcast/**,contracts/cache/**} : Do not manually edit files in generated directories: `src/generated/`, `server/generated/`, `contracts/broadcast/`, and `contracts/cache/`

Applied to files:

  • substreams/.gitignore
  • .dockerignore
📚 Learning: 2025-12-31T00:23:55.034Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 610
File: .changeset/ready-experts-fly.md:1-2
Timestamp: 2025-12-31T00:23:55.034Z
Learning: In the exactly/exa repository, allow and require empty changeset files (containing only --- separators) when changes are not user-facing and do not warrant a version bump. This is needed because CI runs changeset status --since origin/main and requires a changeset file to exist. Ensure such empty changesets are used only for non-user-facing changes and document the rationale in the commit or changelog notes.

Applied to files:

  • .changeset/rare-readers-tell.md
  • .changeset/breezy-planes-repeat.md
  • .changeset/red-toys-pump.md
  • .changeset/violet-poems-heal.md
📚 Learning: 2025-12-30T15:03:28.449Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Omit meaningless words from names like `data`, `state`, `manager`, `engine`, `object`, `entity`, and `instance`

Applied to files:

  • cspell.json
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Omit meaningless words like 'data', 'state', 'manager', 'engine', 'value' from variable and function names unless they add disambiguation

Applied to files:

  • cspell.json
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Zero config local dev environment: no `.env` files, mock all external services

Applied to files:

  • server/test/e2e.ts
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Run all tests using `pnpm test`, which includes TypeScript compilation, ESLint, spell checking, markdown linting, and contract tests

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Run contract tests using `pnpm --filter plugin test` and check formatting using `pnpm --filter plugin test:fmt`

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Use `nx` for running tasks (build, lint, test, e2e) instead of underlying tooling directly

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.t.sol : Test files must end with .t.sol and mirror the name of the contract they test (e.g., ExaPlugin.sol -> ExaPlugin.t.sol)

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.sol : All code must pass pnpm --filter plugin test:slither and pnpm --filter plugin test:solhint with zero warnings; do not ignore warnings

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Avoid long-running, synchronous operations; use `async/await` correctly and be mindful of CPU-intensive tasks to prevent blocking the event loop

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Do not access `process.env` directly in application code; load all configuration and secrets once at startup and pass them through dependency injection or context

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Server stack: use hono, node.js, drizzle orm, postgres

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Use Drizzle ORM query builder for all database interactions; do not write raw SQL queries unless absolutely unavoidable

Applied to files:

  • server/test/database.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Server (exactly/server) uses Hono (framework), Node.js, Drizzle ORM (database), and PostgreSQL

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Enforce Drizzle ORM best practices using ESLint `plugin:drizzle/all` configuration, including requiring `where` clauses for `update` and `delete` operations

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-30T15:03:28.449Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/.changeset/*.md : Use a lowercase sentence in the imperative present tense for changeset summaries

Applied to files:

  • .changeset/violet-poems-heal.md
🪛 Buf (1.62.1)
substreams/proto/exa.proto

3-3: Files with package "exa" must be within a directory "exa" relative to root but were in directory "substreams/proto".

(PACKAGE_DIRECTORY_MATCH)

🪛 Checkov (3.2.334)
substreams/Dockerfile

[low] 1-33: Ensure that HEALTHCHECK instructions have been added to container images

(CKV_DOCKER_2)

🪛 YAMLlint (1.37.1)
.github/workflows/substreams-build.yaml

[warning] 2-2: truthy value should be one of [false, true]

(truthy)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 6-6: too many spaces inside braces

(braces)


[error] 6-6: too many spaces inside braces

(braces)


[error] 11-11: too many spaces inside braces

(braces)


[error] 11-11: too many spaces inside braces

(braces)


[error] 25-25: too many spaces inside braces

(braces)


[error] 25-25: too many spaces inside braces

(braces)

🔇 Additional comments (42)
contracts/package.json (1)

60-101: Consider whether the sequential test chain is optimal for CI performance.

The new dependency structure creates a fully sequential execution path: test:sizestest:gastest:slithertest:coverage. While this provides fail-fast behavior, it may significantly increase total test time compared to running independent tasks in parallel.

Specifically:

  • test:slither (static analysis) does not inherently require test:gas (snapshot validation) to complete first
  • test:coverage does not inherently require test:slither to complete first

Both could potentially run in parallel after the build step, unless:

  • The sequential chain is intentional to fail fast and avoid wasting CI resources when earlier checks fail
  • Shared output directories ({projectRoot}/out, {projectRoot}/cache) would cause conflicts with parallel execution
  • There are resource contention concerns in the CI environment

If the linear execution was not an intentional optimization, consider restructuring to:

"test:slither": {
  "dependsOn": ["test:sizes"],
  "outputs": ["{projectRoot}/out", "{projectRoot}/cache"]
},
"test:coverage": {
  "dependsOn": ["test:gas"],
  "outputs": ["{projectRoot}/out", "{projectRoot}/cache", "{projectRoot}/lcov.info"]
}

This would allow test:slither and test:coverage to run in parallel after their respective prerequisites complete, potentially halving the test suite duration.

server/test/e2e.ts (2)

7-7: LGTM!

Correct use of import type for the type-only import, following the coding guidelines.


58-67: LGTM!

The Sentry mock correctly wraps captureException to provide test observability while preserving the original behavior. The destructuring pattern cleanly separates the function being wrapped from the rest of the module exports.

server/test/anvil.ts (1)

159-160: LGTM!

Adding the --tc BobExecuteScript test case selector makes the forge script invocation more explicit, ensuring only the intended contract is executed.

server/test/database.ts (3)

6-6: LGTM!

Import of $ from execa is appropriate for managing external processes.


45-46: LGTM!

Cleaning up the .firehose directory before starting ensures a fresh state for the firehose processes.


49-72: Verify substreams-sink-sql readiness before tests run if they depend on substreams data.

Currently, fireeth waits for port 10016 (line 60), but substreams-sink-sql starts without a readiness check. If tests depend on substreams data, add a wait that polls the database (e.g., check the substreams.cursors table) rather than a port check, since substreams-sink-sql writes to the database, not a port. However, this may not be necessary if tests don't immediately query substreams tables.

The teardown already properly awaits postgres.stop() with a 33-second graceful shutdown window before force kill, so no changes needed there.

.github/workflows/test.yaml (1)

37-41: Remove the binary download step or clarify its purpose.

The workflow downloads three binaries (firehose-ethereum, substreams, substreams-sink-sql) and extracts them to ~/.local/bin, but these binaries are not referenced or invoked anywhere in the repository. If they are not needed for the test suite, this step should be removed to reduce workflow overhead. If they are intended for future use or debugging, add a comment explaining their purpose.

Likely an incorrect or invalid review comment.

docs/package.json (1)

37-44: LGTM! Proper NX incremental build configuration.

The inputs array correctly tracks the OpenAPI file dependency, ensuring the docs build invalidates its cache when the generated OpenAPI file changes. This aligns well with the existing dependsOn configuration and follows NX best practices.

server/package.json (2)

142-146: LGTM! Consistent dependency tracking configuration.

The additions properly track the CHAIN_ID environment variable and contract build artifacts (out/**/*.json) as inputs to the broadcasts generation. This is consistent with similar patterns in common/package.json and ensures proper incremental builds.

Note: The out/**/*.json pattern should be verified as mentioned in the review of common/package.json.


64-64: The substreams package is correctly configured in the workspace.

The @exactly/substreams package exists at substreams/package.json with version 0.0.1, which correctly matches the workspace dependency reference workspace:^0.0.1 in server/package.json.

contracts/test/mocks/BobExecute.s.sol (1)

55-55: LGTM: Side-effect constructor pattern.

The BatchDeploy contract is instantiated solely for its constructor side effects (creating 4 accounts). The discarded contract address is intentional, as the contract serves no purpose after deployment. This pattern is acceptable for deployment scripts.

pnpm-workspace.yaml (1)

7-7: LGTM!

The addition of the substreams package to the workspace is consistent with the PR's introduction of the substreams module and aligns with related files (substreams/package.json, Dockerfile, etc.).

substreams/.cargo/config.toml (1)

1-2: LGTM!

Standard Cargo configuration for WebAssembly builds. The wasm32-unknown-unknown target is appropriate for the substreams WASM module architecture.

.changeset/violet-poems-heal.md (1)

1-5: LGTM!

The changeset summary correctly follows the coding guidelines: lowercase, imperative present tense ("insert"), and clearly describes the change.

substreams/.gitignore (1)

1-8: LGTM!

The ignore patterns are appropriate for the Rust/WASM build environment, covering generated artifacts (src/contracts, src/proto), build outputs (target, *.spkg), test data (firehose-data), and logs.

.changeset/red-toys-pump.md (1)

1-5: LGTM! Changeset follows the guideline.

The summary uses lowercase imperative present tense as required.

.changeset/rare-readers-tell.md (1)

1-5: LGTM! Changeset follows the guideline.

The summary uses lowercase imperative present tense as required.

substreams/rustfmt.toml (1)

1-6: LGTM! Standard Rust formatting configuration.

The rustfmt settings are reasonable and follow common Rust project conventions.

cspell.json (1)

6-6: LGTM! Appropriate spell-check updates for substreams.

The added ignore paths and dictionary words are legitimate technical terms related to Rust tooling, blockchain infrastructure, and substreams functionality.

Also applies to: 9-9, 14-138

.github/workflows/substreams-build.yaml (1)

1-38: LGTM! Workflow follows security best practices.

The workflow correctly implements:

  • Least-privilege permissions with granular grants
  • Build provenance attestation for supply chain security
  • Proper tag generation patterns for semantic versioning
  • Secure authentication using GITHUB_TOKEN

The YAMLlint style warnings about inline braces are purely stylistic preferences—the YAML syntax is valid and appears consistent with the project's formatting style.

substreams/Cargo.toml (2)

1-35: Remaining configuration looks appropriate for a substreams cdylib.

The dependencies, build-dependencies, and release profile are well-suited for a WASM-targeted substreams module. LTO and opt-level = 's' are sensible choices for size optimization in WASM builds.


4-4: Edition 2024 is valid and stable — this will compile correctly.

Rust edition 2024 was stabilized and released in Rust 1.85.0 on February 20, 2025. As of January 2026, the latest stable release is Rust 1.92.0. Using edition = "2024" in Cargo.toml is correct and will compile without issues.

Likely an incorrect or invalid review comment.

.github/matcher.json (1)

63-92: Well-structured problem matchers for Rust tooling.

The regex patterns correctly handle ANSI escape codes and capture the relevant groups for both compiler diagnostics (warning/error with optional code, file location on second line) and rustfmt diffs. The two-pattern array for "rust" properly matches the multi-line compiler output format.

server/script/firehose.ts (2)

1-65: Clean protobuf cursor implementation.

The ProtoCursor class provides a well-structured minimal protobuf parser. The varint decoding, tag extraction, and field skipping logic are correctly implemented for the supported wire types.


67-148: Timestamp extraction and line processing look correct.

The nested field navigation (field 5 → field 12 → fields 1,2) correctly extracts the timestamp. The FIRE INIT normalization and FIRE BLOCK transformation with error handling are well-implemented.

server/database/schema.ts (2)

70-100: New substreams schema tables are well-structured.

The schema design with separate tables for cursors, history, blocks, and plugins is appropriate. The composite primary key on exaPlugins correctly enforces uniqueness on the address-account pair.

Note: No relations are defined for these tables. If cross-schema queries with typed results are needed, consider adding relations. Based on learnings, relations should be defined explicitly to enable Drizzle typed query results.


36-36: LGTM!

The destructured parameter syntax for the unique index callback is cleaner and equivalent to the previous implementation.

server/drizzle.config.ts (1)

8-8: LGTM!

The schemaFilter correctly includes the new substreams schema alongside public, ensuring drizzle-kit processes both schemas for migrations and introspection.

.changeset/breezy-planes-repeat.md (1)

1-5: LGTM!

The changeset correctly declares a minor version bump for the new substreams functionality. The summary follows the imperative present tense as required.

.github/workflows/server-build.yaml (1)

36-37: LGTM!

Explicitly specifying the Dockerfile path while keeping the build context at the repository root is the correct approach when the Dockerfile needs access to files outside its directory (e.g., shared dependencies or configs at the repo root).

substreams/buf.gen.yaml (1)

1-8: LGTM!

The Buf configuration is well-structured with pinned plugin versions for reproducibility. The output path aligns with the build.rs protogen configuration and the options are appropriate for the Rust/WASM target.

substreams/package.json (1)

1-42: Well-structured NX configuration.

The build configuration properly tracks all relevant inputs (Cargo files, source files, environment variables) and outputs (WASM binary). The dependency chain with rustup ensures the toolchain is available before building.

substreams/Dockerfile (1)

20-30: Good security practices in the runtime stage.

The runtime stage correctly:

  • Uses dumb-init as PID 1 for proper signal handling
  • Creates and switches to a non-root substreams user
  • Sets appropriate file ownership with --chown

Regarding the static analysis hint about HEALTHCHECK: While useful for container orchestration, it's optional if health checks are configured at the orchestrator level (e.g., Kubernetes liveness/readiness probes).

substreams/build.rs (1)

12-18: Good use of rerun-if-changed directives.

The build script correctly declares its dependencies for incremental rebuilds, ensuring Cargo re-runs the script when proto files, configuration, or source dependencies change.

.dockerignore (2)

21-21: Verify intent: **/*.md excludes all Markdown including README files.

This pattern will exclude README.md and other documentation from Docker builds. If any README content is needed in containers (e.g., for reference or license compliance), consider a more targeted pattern like **/CHANGELOG.md or use negation for specific files.

If intentional (to reduce image size), this is fine. Just confirming documentation isn't needed in the built images.


6-18: Good refinement of ignore patterns for substreams artifacts.

The patterns correctly exclude:

  • Generated contracts/proto code that build.rs regenerates
  • Build artifacts (target, abi directories)
  • Broadcast artifacts except run-latest.json needed for deployment references

This reduces Docker context size and ensures reproducible builds.

substreams/substreams.yaml (2)

54-82: Verify factory addresses match deployed contracts.

The factory addresses for each network are critical configuration. Ensure these addresses are verified against actual deployments:

  • optimism: 4 factories starting at block 124,728,176
  • base: 1 factory starting at block 38,172,919
  • op-sepolia: 1 factory starting at block 26,170,889
  • base-sepolia: 1 factory starting at block 29,999,489
  • anvil: 1 factory starting at block 0

Note: The factory addresses appear to be checksummed (mixed case) for mainnet but lowercase for testnets. Consider using consistent casing (preferably checksummed) for all addresses to catch copy-paste errors.


1-42: Well-structured Substreams manifest.

The module pipeline is logically organized:

  1. map_exa_accounts → filters account creation events
  2. store_exa_accounts → persists accounts for lookups
  3. map_exa_plugins → maps plugin installations using account store
  4. db_out → transforms to database changes

The dependency chain and input/output types align with the Rust implementation in src/lib.rs.

substreams/src/lib.rs (3)

1-1: Verify the necessity of this clippy expectation.

No unsafe pointer dereferences are visible in the source code. This expectation likely relates to code generated by the #[substreams::handlers::*] macros. Confirm that this directive is required and consider adding a comment explaining why.


46-51: LGTM!

The store logic is straightforward and correct.


73-88: LGTM!

The database change logic correctly upserts plugin records and creates a corresponding blocks row when changes exist. The unwrap_or_default() on line 85 safely handles missing timestamps.

@cruzdanilo cruzdanilo force-pushed the substreams branch 2 times, most recently from 8421280 to 8b6cd00 Compare January 8, 2026 20:19
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🤖 Fix all issues with AI agents
In @contracts/package.json:
- Around line 73-95: The scripts test:gas, test:slither, and test:coverage are
chained sequentially but only need test:sizes; change their dependsOn so each
depends directly on "test:sizes" instead of chaining (i.e., set
test:slither.dependsOn and test:coverage.dependsOn to ["test:sizes"]), confirm
outputs remain the same, and ensure there are no shared-directory concurrency
issues before enabling parallel execution.
- Around line 83-91: The "test:slither" npm task declares outputs that Slither
doesn't produce; open the package.json entry for the "test:slither" script and
remove "{projectRoot}/out" and "{projectRoot}/cache" from the "outputs" array
(or remove the "outputs" key entirely if no outputs remain) so that only real
Slither outputs are declared and Forge-managed directories aren't incorrectly
marked as produced by Slither.
- Around line 60-101: Remove "{projectRoot}/out" and "{projectRoot}/cache" from
the "outputs" array of the "test:slither" script (slither is static analysis and
doesn't produce those artifacts), and restructure dependencies so "test:sizes"
is the single prerequisite: make "test:gas", "test:slither", and "test:coverage"
each list "test:sizes" in their "dependsOn" (instead of chaining test:sizes →
test:gas → test:slither → test:coverage) so they can run in parallel after sizes
completes.

In @server/database/schema.ts:
- Around line 70-100: The drizzle config is missing the "out" output directory
so migrations for the new substreams tables aren't generated; update your
drizzle.config.ts (the defineConfig export) to include an out path, then run
drizzle-kit generate:pg to produce migration files for the new tables (cursors,
substreams_history, blocks, exa_plugins corresponding to the substreams schema
objects substreams.table entries: cursors, substreamsHistory, blocks,
exaPlugins), review the generated SQL, and commit the migration files to version
control.

In @server/script/firehose.ts:
- Around line 100-104: The timestamp parsing loop in server/script/firehose.ts
assumes fields 1 (seconds) and 2 (nanoseconds) are varints and calls
tsCursor.readVarint() unconditionally; validate tag.wireType === 0 before
calling readVarint() and only assign seconds/nanoseconds when the wire type
matches, otherwise call tsCursor.skip(tag.wireType) (or handle/log the
unexpected wire type) to avoid misreading malformed messages; update the loop
that uses tsCursor.readTag(), tag.fieldNumber, tag.wireType,
tsCursor.readVarint(), and tsCursor.skip to perform this check.
- Around line 59-64: readBytes() advances this.offset by a length decoded from
readVarint() without checking remaining bytes, which can cause silent
truncation; add a bounds check before advancing (compute length =
Number(this.readVarint()), ensure length >= 0 and this.offset + length <=
this.buffer.length), and if the check fails throw a clear Error (do not modify
this.offset). Apply the same pre-check pattern to the skip() implementation(s)
that advance this.offset (use the same validation and error behavior) so no
offset is advanced when the requested skip/length exceeds remaining buffer.
- Around line 130-131: The local variable named "ts" is an abbreviation; rename
it to a descriptive name like "extractedTimestamp" (or "rawTimestamp") where it
is defined from extractTimestamp(buffer) and update its usage in the ternary
that assigns to the existing "timestamp" variable so the code reads e.g. const
extractedTimestamp = extractTimestamp(buffer); const timestamp =
extractedTimestamp === null ? "0" : extractedTimestamp.toString(); ensure all
references to "ts" in the scope are replaced accordingly.

In @substreams/buf.gen.yaml:
- Around line 1-8: Replace the nonexistent plugin versions by updating the two
plugin remote strings: change "buf.build/community/neoeinstein-prost:v0.5.0" to
"buf.build/community/neoeinstein-prost:v0.2.3" and change
"buf.build/community/neoeinstein-prost-crate:v0.5.0" to
"buf.build/community/neoeinstein-prost-crate:v0.3.1" in the buf.gen.yaml plugin
entries so the config references valid releases.

In @substreams/build.rs:
- Around line 48-57: The block that invokes Command::new("substreams") uses
assert! so failures panic instead of propagating an error; replace the assert!
by executing the command, using ? to propagate the Result from .status() (remove
the .expect("protogen failed")), then inspect the returned ExitStatus and return
an Err (e.g., via anyhow::anyhow or a suitable Err variant) if
!status.success(), including context such as the command and its exit code; keep
the same env construction (canonicalize("node_modules/.bin") and var("PATH"))
and span the change around the Command::new("substreams").arg("protogen")...
.status() call so the function returns a Result instead of panicking.
- Around line 59-65: The code uses assert! around the rustfmt Command invocation
which panics instead of returning an error; change the build step that runs
Command::new("rustfmt") (the block currently wrapped by assert!) to capture the
Command status via .status()? and return a Result-based error on failure (e.g.,
if !status.success() { return Err(anyhow::format_err!("rustfmt failed: {:?}",
status)); }) so it matches the protogen pattern that uses ? and early returns;
also update the surrounding function signature to return Result<()> if it does
not already.

In @substreams/Dockerfile:
- Line 13: The Dockerfile currently downloads the Substreams CLI from the
repository's "releases/latest" which makes builds non-reproducible; modify the
RUN curl command that fetches the asset (the long command starting with RUN curl
-L "$(curl -s
https://api.github.com/repos/streamingfast/substreams/releases/latest ...)") to
target a specific release tag instead of /releases/latest (e.g., replace the
latest URL with one that includes a pinned tag like /releases/download/vX.Y.Z)
so the tarball URL is deterministic and repeatable.

In @substreams/package.json:
- Around line 25-41: The package.json build target's "inputs" array is missing
the protobuf sources so changes to .proto files won't trigger rebuilds; update
the inputs list used for the substreams build (the JSON object containing
"inputs" and "outputs" in substreams/package.json) to include the proto sources
by adding an entry for "{projectRoot}/proto/**" (or equivalent glob) alongside
the existing entries so that modifications to .proto files invalidate the cache
and trigger a rebuild of generated code in src/proto.

In @substreams/proto/exa.proto:
- Around line 1-17: Proto package declaration "package exa" in exa.proto does
not match the repository directory layout expected by Buf; either move the file
into an exa/ directory or change the package name to match the current
directory. Update the filesystem by moving substreams/proto/exa.proto into
substreams/proto/exa/exa.proto (or rename the package in the file) so the
package exa aligns with the directory, keeping the message definitions Accounts
and Plugins (and nested Plugin) unchanged.

In @substreams/rust-toolchain.toml:
- Around line 1-4: Update the toolchain specification in rust-toolchain.toml to
use the stable channel instead of nightly: change the [toolchain] channel value
from "nightly" to a stable pin such as "stable" or a specific version like
"1.85" and keep the existing components ("clippy","rustfmt") and targets
("wasm32-unknown-unknown") unchanged to ensure reproducible builds.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 284cc90 and 8b6cd00.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • substreams/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (36)
  • .changeset/breezy-planes-repeat.md
  • .changeset/rare-readers-tell.md
  • .changeset/red-toys-pump.md
  • .changeset/violet-poems-heal.md
  • .dockerignore
  • .github/matcher.json
  • .github/workflows/server-build.yaml
  • .github/workflows/substreams-build.yaml
  • .github/workflows/test.yaml
  • .vscode/extensions.json
  • common/package.json
  • contracts/package.json
  • contracts/test/mocks/BobExecute.s.sol
  • cspell.json
  • docs/package.json
  • pnpm-workspace.yaml
  • server/Dockerfile
  • server/database/schema.ts
  • server/drizzle.config.ts
  • server/package.json
  • server/script/firehose.ts
  • server/test/anvil.ts
  • server/test/database.ts
  • server/test/e2e.ts
  • substreams/.cargo/config.toml
  • substreams/.gitignore
  • substreams/Cargo.toml
  • substreams/Dockerfile
  • substreams/buf.gen.yaml
  • substreams/build.rs
  • substreams/package.json
  • substreams/proto/exa.proto
  • substreams/rust-toolchain.toml
  • substreams/rustfmt.toml
  • substreams/src/lib.rs
  • substreams/substreams.yaml
🧰 Additional context used
📓 Path-based instructions (11)
server/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/server.mdc)

server/**/*.ts: Use c.var object to pass strongly-typed data between Hono middleware and route handlers; do not use c.set
All request validation (headers, body, params) must be handled by @hono/valibot-validator middleware; do not perform manual validation inside route handlers
Use Hono's built-in error handling by throwing new HTTPException() for expected errors; unhandled errors will be caught and logged automatically
Enforce Node.js best practices using ESLint plugin:n/recommended configuration
Enforce Drizzle ORM best practices using ESLint plugin:drizzle/all configuration, including requiring where clauses for update and delete operations
Use Drizzle ORM query builder for all database interactions; do not write raw SQL queries unless absolutely unavoidable
All authentication and authorization logic must be implemented in Hono middleware
Do not access process.env directly in application code; load all configuration and secrets once at startup and pass them through dependency injection or context
Avoid long-running, synchronous operations; use async/await correctly and be mindful of CPU-intensive tasks to prevent blocking the event loop

Files:

  • server/test/e2e.ts
  • server/drizzle.config.ts
  • server/test/anvil.ts
  • server/test/database.ts
  • server/script/firehose.ts
  • server/database/schema.ts
**/*.{js,ts,tsx,jsx,sol}

📄 CodeRabbit inference engine (AGENTS.md)

Follow linter/formatter (eslint, prettier, solhint) strictly with high strictness level. No any type.

Files:

  • server/test/e2e.ts
  • server/drizzle.config.ts
  • server/test/anvil.ts
  • server/test/database.ts
  • contracts/test/mocks/BobExecute.s.sol
  • server/script/firehose.ts
  • server/database/schema.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Omit redundant type names in variable declarations - let the type system explain itself

**/*.{ts,tsx}: Use PascalCase for TypeScript types and interfaces
Use valibot for all runtime validation of API inputs, environment variables, and other data; define schemas once and reuse them
Infer TypeScript types from valibot schemas using type User = v.Input<typeof UserSchema> instead of manually defining interfaces

Files:

  • server/test/e2e.ts
  • server/drizzle.config.ts
  • server/test/anvil.ts
  • server/test/database.ts
  • server/script/firehose.ts
  • server/database/schema.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Omit contextual names - don't repeat class/module names in members
Omit meaningless words like 'data', 'state', 'manager', 'engine', 'value' from variable and function names unless they add disambiguation

**/*.{ts,tsx,js,jsx}: Prefer function declarations for all multi-line functions; use function expressions or arrow functions only for single-line implementations
Prefer const for all variable declarations by default; only use let if the variable's value will be reassigned
Declare each variable on its own line with its own const or let keyword, not multiple declarations on one line
Use camelCase for TypeScript variables and functions
Always use import type { ... } for type imports
Use relative paths for all imports within the project; avoid tsconfig path aliases
Follow eslint-plugin-import order: react, external libraries, then relative paths
Use object and array destructuring to access and use properties
Use object method shorthand syntax when a function is a property of an object
Prefer optional chaining (?.), nullish coalescing (??), object and array spreading (...), and for...of loops over traditional syntax
Do not use abbreviations or cryptic names; write out full words like error, parameters, request instead of err, params, req
Use Number.parseInt() instead of the global parseInt() function when parsing numbers
All classes called with new must use PascalCase
Use Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe() instead of the deprecated new Buffer()
Use @ts-expect-error instead of @ts-ignore; follow it immediately with a single-line lowercase comment explaining why the error is expected, without separators like - or :
Do not include the type in a variable's name; let the static type system do its job (e.g., use const user: User not const userObject: User)
Do not repeat the name of a class or module within its members; omit contextual names (e.g., use `class User { getProfil...

Files:

  • server/test/e2e.ts
  • server/drizzle.config.ts
  • server/test/anvil.ts
  • server/test/database.ts
  • server/script/firehose.ts
  • server/database/schema.ts
server/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

server/**/*.{ts,tsx}: Server API: implement schema-first approach using OpenAPI via hono with validation via valibot middleware
Server database: drizzle schema is source of truth. Migrations required. No direct database access in handlers - use c.var.db

Files:

  • server/test/e2e.ts
  • server/drizzle.config.ts
  • server/test/anvil.ts
  • server/test/database.ts
  • server/script/firehose.ts
  • server/database/schema.ts
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

For files with a single default export, name the file identically to the export; for files with multiple exports, use camelCase with a strong preference for a single word

Files:

  • server/test/e2e.ts
  • server/drizzle.config.ts
  • server/test/anvil.ts
  • server/test/database.ts
  • server/script/firehose.ts
  • server/database/schema.ts
**/.changeset/*.md

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

Use a lowercase sentence in the imperative present tense for changeset summaries

Files:

  • .changeset/breezy-planes-repeat.md
  • .changeset/rare-readers-tell.md
  • .changeset/red-toys-pump.md
  • .changeset/violet-poems-heal.md
contracts/**/*.sol

📄 CodeRabbit inference engine (.cursor/rules/contracts.mdc)

contracts/**/*.sol: Import order must be sorted alphabetically and grouped by type (openzeppelin, solady, internal, etc.), enforced by forge fmt
All functions and state variables must have explicit visibility modifiers (public, private, internal, external)
Use custom errors instead of require() statements with string messages; require() with messages is forbidden
console.log is forbidden in contract code
All external and public functions that modify state must be protected by a reentrancy guard, unless explicitly proven unnecessary
Use modifiers like onlyOwner or function-level checks for privileged functions; default to strictest access level (private > internal > external > public)
Use named parameters in mappings for clarity (e.g., mapping(address user => uint256 balance))
All code must pass pnpm --filter plugin test:slither and pnpm --filter plugin test:solhint with zero warnings; do not ignore warnings
Follow standard Solidity import order: external libraries (openzeppelin, solady) first, then project-internal contracts
All public and external functions and state variables must have complete natspec comments (@notice, @dev, @param, @return)
Use custom errors instead of require statements with string messages for better gas efficiency and error information
Internal and private variables should be prefixed with an underscore (_); function arguments should not be prefixed
Use constant keyword for true constants; use immutable for configurable values like contract addresses
Minimize writes to storage; read data into memory or calldata whenever possible for gas optimization
Use smallest integer size that is safe for the variable (e.g., uint32 for timestamps) and be aware of struct packing for gas efficiency
Prefer using the solady library over openzeppelin where possible, as it is highly optimized for gas
Correctly label functions as view or pure when they do not modify state

contracts/**/*.sol: Smart contracts: use custom errors error MyError(); instead of s...

Files:

  • contracts/test/mocks/BobExecute.s.sol
**/*.sol

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

**/*.sol: Use camelCase for Solidity variables and functions
Use PascalCase for Solidity contracts and structs

Files:

  • contracts/test/mocks/BobExecute.s.sol
**/+(app|src)/**

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

Use kebab-case for directory names and public Expo router routes

Files:

  • substreams/src/lib.rs
server/database/schema.ts

📄 CodeRabbit inference engine (.cursor/rules/server.mdc)

server/database/schema.ts: Database schema must be defined in server/database/schema.ts as the single source of truth for database structure
Define relations explicitly in the schema file to enable Drizzle to generate typed query results including related entities

Files:

  • server/database/schema.ts
🧠 Learnings (30)
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Zero config local dev environment: no `.env` files, mock all external services

Applied to files:

  • server/test/e2e.ts
  • contracts/test/mocks/BobExecute.s.sol
📚 Learning: 2025-12-31T00:23:55.034Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 610
File: .changeset/ready-experts-fly.md:1-2
Timestamp: 2025-12-31T00:23:55.034Z
Learning: In the exactly/exa repository, allow and require empty changeset files (containing only --- separators) when changes are not user-facing and do not warrant a version bump. This is needed because CI runs changeset status --since origin/main and requires a changeset file to exist. Ensure such empty changesets are used only for non-user-facing changes and document the rationale in the commit or changelog notes.

Applied to files:

  • .changeset/breezy-planes-repeat.md
  • .changeset/rare-readers-tell.md
  • .changeset/red-toys-pump.md
  • .changeset/violet-poems-heal.md
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/database/schema.ts : Define relations explicitly in the schema file to enable Drizzle to generate typed query results including related entities

Applied to files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Applies to server/**/*.{ts,tsx} : Server database: drizzle schema is source of truth. Migrations required. No direct database access in handlers - use `c.var.db`

Applied to files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/database/migrations/** : All schema changes must be accompanied by a migration file generated by `drizzle-kit`; never edit the database schema manually

Applied to files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/database/schema.ts : Database schema must be defined in `server/database/schema.ts` as the single source of truth for database structure

Applied to files:

  • server/drizzle.config.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Enforce Drizzle ORM best practices using ESLint `plugin:drizzle/all` configuration, including requiring `where` clauses for `update` and `delete` operations

Applied to files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Use Drizzle ORM query builder for all database interactions; do not write raw SQL queries unless absolutely unavoidable

Applied to files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • server/database/schema.ts
📚 Learning: 2026-01-08T19:54:26.885Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: substreams/rust-toolchain.toml:1-4
Timestamp: 2026-01-08T19:54:26.885Z
Learning: Rust edition 2024 was released as stable on February 20, 2025 with Rust 1.85.0 and does not require nightly channel.

Applied to files:

  • substreams/rust-toolchain.toml
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Avoid long-running, synchronous operations; use `async/await` correctly and be mindful of CPU-intensive tasks to prevent blocking the event loop

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Do not access `process.env` directly in application code; load all configuration and secrets once at startup and pass them through dependency injection or context

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Server stack: use hono, node.js, drizzle orm, postgres

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Server (exactly/server) uses Hono (framework), Node.js, Drizzle ORM (database), and PostgreSQL

Applied to files:

  • server/test/database.ts
📚 Learning: 2026-01-08T19:45:38.852Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: .github/workflows/substreams-build.yaml:2-2
Timestamp: 2026-01-08T19:45:38.852Z
Learning: In the exactly/exa repository, prefer concise inline YAML syntax (e.g., `on: { push: { tags: ["pattern"] }, workflow_dispatch: {} }`) over expanded multi-line format in GitHub workflow files, even if it triggers YAMLlint warnings about formatting.

Applied to files:

  • .github/workflows/substreams-build.yaml
📚 Learning: 2025-12-30T15:03:28.449Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/.changeset/*.md : Use a lowercase sentence in the imperative present tense for changeset summaries

Applied to files:

  • .changeset/rare-readers-tell.md
  • .changeset/violet-poems-heal.md
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.sol : Use custom errors instead of require statements with string messages for better gas efficiency and error information

Applied to files:

  • substreams/build.rs
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Applies to {src/generated/**,server/generated/**,contracts/broadcast/**,contracts/cache/**} : Do not manually edit files in generated directories: `src/generated/`, `server/generated/`, `contracts/broadcast/`, and `contracts/cache/`

Applied to files:

  • substreams/.gitignore
  • .dockerignore
  • common/package.json
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.sol : All public and external functions and state variables must have complete natspec comments (notice, dev, param, return)

Applied to files:

  • contracts/test/mocks/BobExecute.s.sol
📚 Learning: 2026-01-08T19:46:37.829Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: contracts/test/mocks/BobExecute.s.sol:61-71
Timestamp: 2026-01-08T19:46:37.829Z
Learning: Natspec documentation requirements do not apply to test/mock/dev scripts (files in contracts/test/ or with .s.sol extension). Only production contract code in contracts/src/ requires complete natspec comments.

Applied to files:

  • contracts/test/mocks/BobExecute.s.sol
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.t.sol : Use vm.prank to simulate calls from different addresses; avoid changing msg.sender through other means

Applied to files:

  • contracts/test/mocks/BobExecute.s.sol
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Use `nx` for running tasks (build, lint, test, e2e) instead of underlying tooling directly

Applied to files:

  • substreams/package.json
  • contracts/package.json
📚 Learning: 2026-01-08T20:26:33.396Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: .vscode/extensions.json:6-6
Timestamp: 2026-01-08T20:26:33.396Z
Learning: For validating VS Code extension IDs in this repository, check both the official VS Code Marketplace (marketplace.visualstudio.com) and the Open VSX registry (open-vsx.org) before flagging an extension as invalid. Open VSX is a widely-used alternative registry, so a valid ID may appear in either registry. If an ID is not present in both registries, flag it as invalid. Apply this check specifically when validating the IDs listed in .vscode/extensions.json.

Applied to files:

  • .vscode/extensions.json
📚 Learning: 2025-12-30T15:03:28.449Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Do not use abbreviations or cryptic names; write out full words like `error`, `parameters`, `request` instead of `err`, `params`, `req`

Applied to files:

  • server/script/firehose.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Omit meaningless words like 'data', 'state', 'manager', 'engine', 'value' from variable and function names unless they add disambiguation

Applied to files:

  • server/script/firehose.ts
  • cspell.json
📚 Learning: 2026-01-08T19:54:10.673Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: substreams/src/lib.rs:54-71
Timestamp: 2026-01-08T19:54:10.673Z
Learning: In substreams/src/lib.rs in the map_exa_plugins function, the deduplication logic using `!seen.insert(address.to_vec())` is intentional. The first plugin installed for a new account is an owner plugin (not an exa plugin), so it must be filtered out. The negation ensures the first plugin is skipped and subsequent plugins (the actual exa plugins) are kept.

Applied to files:

  • substreams/src/lib.rs
📚 Learning: 2025-12-30T15:03:28.449Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Omit meaningless words from names like `data`, `state`, `manager`, `engine`, `object`, `entity`, and `instance`

Applied to files:

  • cspell.json
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Run contract tests using `pnpm --filter plugin test` and check formatting using `pnpm --filter plugin test:fmt`

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Run all tests using `pnpm test`, which includes TypeScript compilation, ESLint, spell checking, markdown linting, and contract tests

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.t.sol : Test files must end with .t.sol and mirror the name of the contract they test (e.g., ExaPlugin.sol -> ExaPlugin.t.sol)

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.sol : All code must pass pnpm --filter plugin test:slither and pnpm --filter plugin test:solhint with zero warnings; do not ignore warnings

Applied to files:

  • contracts/package.json
🪛 Buf (1.62.1)
substreams/proto/exa.proto

3-3: Files with package "exa" must be within a directory "exa" relative to root but were in directory "substreams/proto".

(PACKAGE_DIRECTORY_MATCH)

🪛 Checkov (3.2.334)
substreams/Dockerfile

[low] 1-33: Ensure that HEALTHCHECK instructions have been added to container images

(CKV_DOCKER_2)

🪛 YAMLlint (1.37.1)
.github/workflows/substreams-build.yaml

[warning] 2-2: truthy value should be one of [false, true]

(truthy)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 6-6: too many spaces inside braces

(braces)


[error] 6-6: too many spaces inside braces

(braces)


[error] 11-11: too many spaces inside braces

(braces)


[error] 11-11: too many spaces inside braces

(braces)


[error] 25-25: too many spaces inside braces

(braces)


[error] 25-25: too many spaces inside braces

(braces)

🔇 Additional comments (47)
server/script/firehose.ts (1)

135-135: Verify intentional use of libNum twice in output.

The output format uses libNum in both its original position and again before timestamp. The input has 7 tokens but the output structure differs:

  • Input: FIRE BLOCK num hash libNum parentHash payloadB64
  • Output: FIRE BLOCK num hash libNum parentHash libNum timestamp newPayload

Is the second libNum intentional, or should a different value be used there?

.github/workflows/test.yaml (1)

37-41: [Your rewritten review comment text here]
[Exactly ONE classification tag]

substreams/.cargo/config.toml (1)

1-2: LGTM! Standard WASM build configuration.

The build target is correctly set to wasm32-unknown-unknown, which is the standard target for Substreams modules that compile to WebAssembly.

substreams/.gitignore (1)

1-8: LGTM! Appropriate ignore patterns for Substreams artifacts.

The gitignore correctly excludes:

  • Generated code directories (abi, src/contracts, src/proto)
  • Build outputs (target)
  • Runtime data (firehose-data)
  • Build artifacts (*.log, *.spkg)

This aligns with the project's practice of not committing generated or build artifacts.

.changeset/breezy-planes-repeat.md (1)

1-5: LGTM! Changeset follows the required format.

The changeset correctly:

  • Uses lowercase imperative present tense ("map exa accounts")
  • Targets the appropriate package with a minor version bump
  • Follows the standard changeset file structure
.changeset/red-toys-pump.md (1)

1-5: LGTM! Changeset follows the required format.

The changeset correctly:

  • Uses lowercase imperative present tense ("map exa plugins")
  • Targets the appropriate package with a minor version bump
  • Follows the standard changeset file structure
server/drizzle.config.ts (1)

8-8: LGTM! Correct schema filter for Substreams integration.

The addition of "substreams" to the schema filter enables drizzle-kit to manage migrations for the new Substreams database schema alongside the existing public schema.

.changeset/rare-readers-tell.md (1)

1-5: LGTM!

The changeset follows the required format with a lowercase imperative present tense summary. The emoji prefix is consistent with common changeset conventions, and the description accurately reflects the PR's objective of storing Exa accounts.

cspell.json (1)

6-6: LGTM!

The additions to ignore paths and the word list appropriately support the new Rust/Substreams tooling introduced in this PR. The ignored paths (substreams/target for Rust build artifacts and .github/matcher.json for error patterns) and technical terms (Rust ecosystem tools like clippy, rustup, serde, and Substreams-related terms) are all contextually appropriate.

Also applies to: 9-9, 14-138

server/test/e2e.ts (2)

7-7: LGTM!

Correctly uses import type for the type-only import, following TypeScript best practices.


58-67: LGTM!

The Sentry mock implementation correctly preserves the original captureException behavior while adding console logging for improved e2e test debugging. The approach of destructuring the original module and wrapping the function is sound.

.github/workflows/server-build.yaml (1)

36-37: Dockerfile path verified. The explicit file: server/Dockerfile parameter is appropriate for the multi-component repository structure and the file exists at the specified location.

.vscode/extensions.json (1)

6-6: No action needed. The extension ID drblury.protobuf-vsc is valid and available in both the VS Code Marketplace and Open VSX registry.

Likely an incorrect or invalid review comment.

docs/package.json (1)

38-43: LGTM! NX inputs properly track OpenAPI generation.

The inputs array correctly wires the OpenAPI output from @exactly/server into the docs build's incremental tracking, ensuring the docs rebuild when the API spec changes.

server/package.json (2)

64-64: LGTM! Substreams dependency added.

The addition of @exactly/substreams as a dev dependency aligns with the PR objectives for initial substreams implementation.


142-146: LGTM! Enhanced incremental build tracking.

The expanded inputs properly track the CHAIN_ID environment variable and dependent task outputs (out/**/*.json), ensuring correct incremental builds for broadcast generation.

substreams/Cargo.toml (2)

32-35: LGTM! Release profile optimized for WASM.

The release profile configuration with LTO, size optimization ('s'), and stripped debug info is appropriate for WASM targets in a Substreams context.


1-4: Fix invalid Rust edition.

Rust edition "2024" does not exist. Valid Rust editions are 2015, 2018, and 2021. Use 2021 as it's the latest stable edition.

🐛 Proposed fix
 [package]
 name = "exactly-substreams"
 version = "0.1.0"
-edition = "2024"
+edition = "2021"
 repository = "https://github.com/exactly/exa"
⛔ Skipped due to learnings
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: substreams/rust-toolchain.toml:1-4
Timestamp: 2026-01-08T19:54:26.885Z
Learning: Rust edition 2024 was released as stable on February 20, 2025 with Rust 1.85.0 and does not require nightly channel.
contracts/test/mocks/BobExecute.s.sol (2)

61-71: LGTM! BatchDeploy helper simplifies multi-account creation.

The BatchDeploy contract provides a clean way to create multiple test accounts in a single transaction. The implementation is straightforward and appropriate for test scaffolding.


39-39: Document script execution order dependency.

This script calls executeProposal on bobAccount (lines 50-53), which requires the account to already exist. The change from createAccount to getAddress assumes Bob's account with salt 0 has been created in the prior Bob.s.sol script. Ensure this dependency is documented or consider embedding the account creation here to avoid failures if scripts run out of order.

server/test/anvil.ts (1)

159-159: LGTM! Test case selector disambiguates script execution.

Adding --tc BobExecuteScript is appropriate given that BobExecute.s.sol now contains multiple contracts. This ensures only BobExecuteScript.run() is executed as the entry point.

common/package.json (1)

44-50: LGTM!

The additional dependentTasksOutputFiles inputs correctly wire contract compilation outputs (out/**/*.json) and local chain broadcast artifacts (broadcast/*/31337/*.json) into the wagmi generation target, ensuring proper cache invalidation when these artifacts change.

.dockerignore (1)

6-25: LGTM!

The granular ignore patterns appropriately reduce Docker build context while preserving necessary artifacts like run-latest.json. The substreams-specific ignores (abi, target, src/contracts, src/proto) correctly exclude generated and build directories.

.github/matcher.json (1)

64-92: LGTM!

The Rust problem matchers are well-structured. The two-pattern approach for the rust owner correctly handles the multi-line compiler output format (message line followed by location line). The ANSI escape sequence handling (\x1b\[[\d;]+m) ensures colored terminal output is properly parsed.

server/database/schema.ts (1)

36-36: LGTM!

The destructured syntax ({ account }) => [uniqueIndex(...).on(account)] is cleaner and aligns with the pattern used in the new exaPlugins table definition.

server/test/database.ts (1)

49-72: The URL concatenation on line 66 is correct. The pattern ${postgresURL}&schemaName=substreams properly follows standard URL syntax: the first query parameter uses ? (in postgresURL ending with ?sslmode=disable), and subsequent parameters use &. The absence of whitespace in the template literal is not an error—it's standard practice and produces the correct URL string postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable&schemaName=substreams. No changes needed.

Likely an incorrect or invalid review comment.

pnpm-workspace.yaml (1)

7-7: LGTM!

The workspace is correctly expanded to include the new substreams package, maintaining the existing ordering convention.

.changeset/violet-poems-heal.md (1)

1-5: LGTM!

The changeset summary follows the coding guidelines: lowercase sentence in imperative present tense. Based on coding guidelines.

substreams/rustfmt.toml (1)

1-6: LGTM!

The rustfmt configuration is well-suited for the Rust nightly toolchain specified in this PR. The settings promote consistent formatting with sensible defaults.

substreams/Dockerfile (1)

20-33: Runtime stage setup looks good.

The runtime stage follows security best practices: runs as non-root user, uses dumb-init for proper signal handling, and pins the base image version. The default CMD with host.docker.internal is appropriate for local development; production deployments will override this.

Regarding the static analysis hint about missing HEALTHCHECK: for a sink process that may not expose HTTP endpoints, a healthcheck would need to be tailored to the specific runtime behavior. Consider adding one if the sink exposes a health endpoint.

.github/workflows/substreams-build.yaml (2)

1-38: LGTM!

The workflow is well-structured with proper permissions for container registry operations and build attestation. The inline YAML syntax follows the repository conventions. Based on learnings.


20-20: Verify the tag pattern intent.

The pattern ^(?!@)(.*)$ matches strings that don't start with @, but the workflow only triggers on tags matching @exactly/substreams@*. This pattern may never produce matches given the trigger constraint. Was this intended for a different trigger scenario?

substreams/package.json (1)

51-62: LGTM on test targets.

The test orchestration via NX is well-structured with proper dependency ordering: clippy runs first (with prepare dependencies), then fmt, and protolint runs in parallel via the aggregate test target.

substreams/build.rs (3)

14-18: LGTM: Cargo rebuild triggers are correctly configured.

The rerun triggers appropriately cover proto definitions, configuration files, and contract sources in node_modules.


20-34: LGTM: ABI extraction and binding generation logic is correct.

The code correctly:

  • Extracts ABI from Forge artifacts at the expected path
  • Generates Rust bindings using Abigen
  • Handles errors appropriately with ? operator

36-46: LGTM: Generated module file creation is correct.

The code properly generates a module file with:

  • // @generated marker for tooling
  • #[expect(clippy::style, clippy::complexity)] to suppress lints on generated code
  • Proper module declarations
substreams/substreams.yaml (5)

1-9: LGTM: Package and protobuf configuration is correct.


10-13: LGTM: Imports and network configuration are appropriate.

The default network is set to anvil for local development, which aligns with the PR objectives of deferring deployment.


14-42: LGTM: Module pipeline is correctly wired.

The four-stage pipeline (map accounts → store accounts → map plugins → database output) has proper input/output types and follows Substreams best practices.


43-52: LGTM: Sink and binary configuration is correct.

The database sink configuration properly references the schema file and the compiled WASM binary path follows Rust conventions.


53-82: LGTM: Network configurations are correct.

The initial blocks and factory addresses are properly configured for each network. Factory addresses use EIP-55 checksummed format (except line 77), which is good practice but not strictly required.

substreams/src/lib.rs (6)

1-21: LGTM: Clippy configuration and imports are appropriate.

The #![expect(clippy::not_unsafe_ptr_arg_deref)] attribute is necessary for Substreams FFI boundaries, where raw pointers are used in the handler interface.


22-25: LGTM: Factories struct is correctly defined.


27-44: LGTM: Account extraction logic is correct.

The function properly:

  • Parses factory addresses from query string parameters
  • Decodes hex addresses with error handling
  • Filters block events for ExaAccountInitialized from specified factories
  • Collects account addresses

The double .collect() pattern (lines 36 and 39) is necessary to handle error propagation and type conversion.


46-51: LGTM: Account storage logic is correct.

The function properly stores a flag (value 1) for each account using the hex-encoded address as the key. Using ordinal 0 is appropriate for existence flags.


53-71: LGTM: Plugin extraction logic is correct.

The function properly:

  • Filters PluginInstalled events from block logs
  • Handles new accounts (from current block) with deduplication to skip the first plugin (owner plugin)
  • Handles existing accounts by checking the store
  • Creates Plugin entries with address, account, and ordinal

Based on learnings, the negation in line 63 (!seen.insert(address.to_vec())) is intentional to filter out the first plugin (owner plugin) for new accounts.


73-88: Code correctly implements database operations with proper schema alignment.

The function properly:

  • Creates upsert operations for each plugin in the exa_plugins table with composite primary key (address, account) as defined in the database schema
  • Uses hex-encoded addresses for database keys
  • Creates a blocks row with timestamp when changes exist
  • Uses unwrap_or_default() as a defensive fallback for clock timestamps (though substreams should always provide this)

The schema in server/database/schema.ts confirms the table structures match the code: exa_plugins has a composite primary key on (address, account), and blocks has number and timestamp fields as expected.

@cruzdanilo cruzdanilo force-pushed the substreams branch 3 times, most recently from 28ec4bd to e13495d Compare January 8, 2026 21:11
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

🤖 Fix all issues with AI agents
In @.changeset/breezy-planes-repeat.md:
- Line 5: The changeset summary "✨ map exa accounts" is missing terminal
punctuation; update the summary to be a complete lowercase imperative
present-tense sentence by adding a period (e.g., "✨ map exa accounts.") and
ensure it remains lowercase and in imperative present tense inside
.changeset/breezy-planes-repeat.md.

In @.changeset/rare-readers-tell.md:
- Line 5: Update the changeset summary line in .changeset/rare-readers-tell.md
by removing the emoji and ensuring it is a lowercase, imperative-present tense
sentence; replace the current "✨ store exa accounts" header with a plain
sentence like "store exa accounts." to conform to the guidelines.

In @.changeset/violet-poems-heal.md:
- Line 5: The changeset summary "✨ insert exa plugins into database" is missing
terminal punctuation; edit the .changeset/violet-poems-heal.md changeset summary
line to end with a period and ensure it remains a lowercase,
imperative-present-tense sentence (e.g., "✨ insert exa plugins into database.").

In @.github/workflows/test.yaml:
- Around line 37-41: The run step that loops over repos (for repo in
firehose-ethereum substreams substreams-sink-sql) currently pipes curl output
straight into tar and does not validate the download or handle errors; change it
to (1) enable safe shell flags (e.g., set -euo pipefail) and use curl --fail so
HTTP errors cause failure, (2) first query the GitHub API and store the
resulting browser_download_url in a variable and check it is non-empty before
attempting download, (3) perform the download and verify curl's exit status
before passing data to tar, and (4) add integrity verification by fetching a
checksum/signature asset (or pin a specific release/tag) and validating the
downloaded archive with sha256sum (or signature verification), exiting with an
error if any step fails.

In @server/database/schema.ts:
- Around line 70-100: Add B-tree indexes for expected query predicates to avoid
full table scans: update the schema definitions created with substreams.table
(e.g., exaPlugins, blocks, substreamsHistory, cursors) to declare indexes on
exaPlugins.account, blocks.timestamp, substreamsHistory.blockNum (and optionally
substreamsHistory.tableName and cursors.blockNum) using the schema library’s
index/primaryKey callback form so the DB creates those indexes alongside the
tables.
- Around line 93-100: Add an explicit Drizzle relation tying exaPlugins.account
to credentials.account: create exaPluginsRelations using relations(exaPlugins,
({ one }) => ({ credential: one(credentials, { fields: [exaPlugins.account],
references: [credentials.account] }) })); then update credentialsRelations to
include the inverse (e.g., a has/many relation pointing back to exaPlugins) so
queries return typed related entities; reference the exaPlugins, credentials,
exaPluginsRelations and credentialsRelations symbols when making the changes.

In @server/script/firehose.ts:
- Line 127: The destructured variable payloadBase64 should be renamed to a
non-abbreviated, clearer name (e.g., encodedPayload or payload) to follow naming
guidelines; update the array destructuring (replace payloadBase64 with
encodedPayload) and then update all subsequent references in the same function
(decoding, parsing, logging, etc.) from payloadBase64 to the new identifier
(encodedPayload) so behavior remains identical.
- Around line 100-104: The timestamp parsing loop reads seconds and nanoseconds
without checking wire types; update the loop that uses
tsCursor.readTag()/tsCursor.readVarint() so that before calling readVarint() for
fieldNumber 1 or 2 you verify tag.wireType === 0 (the varint wire type) and only
then assign seconds/nanoseconds, otherwise call tsCursor.skip(tag.wireType) to
avoid misinterpreting unexpected types; ensure you still skip unknown
fieldNumbers as before.

In @server/test/database.ts:
- Around line 53-55: The complex template literal constructing the fireeth
command (the string containing "fireeth start
reader-node,merger,relayer,substreams-tier1 ... --reader-node-arguments=${'-c
\"...firehose.ts\"'}") is hard to read and fragile; extract that construction
into a named helper like buildFireethStartCommand (or move the inner piped
command to a standalone script e.g., scripts/firehose.sh) and have the test call
that helper or script, and if invoking from Node use spawn/exec with an args
array instead of bash -c to avoid nested quoting and ensure proper escaping.

In @substreams/build.rs:
- Around line 47-56: Replace the assert! around Command::new("substreams") with
proper Result-based error handling: run the command with
Command::new("substreams")... (use .output() to capture stdout/stderr or
.status() if you prefer), map any spawn/error via ? or map_err to return an Err,
then check the exit status and, if not successful, return an Err containing a
clear diagnostic string including the exit code and captured stderr/stdout;
update the surrounding function signature to return Result if needed so failures
propagate rather than panicking.

In @substreams/proto/exa.proto:
- Around line 1-7: The proto file declares package exa (message Accounts) but
lives at proto/exa.proto; move the file into an exa subdirectory (e.g.,
proto/exa/exa.proto) so the filesystem matches the package name, and update any
build/import paths (buf, compiler, or other proto imports) that reference
proto/exa.proto to the new path; ensure any tooling/config (e.g., buf.yaml,
go/ts import mappings) is updated to avoid breakage.

In @substreams/src/lib.rs:
- Around line 28-43: The iterator chain in map_exa_accounts is hard to read;
extract intermediate steps: parse Factories from params into a variable, map
factories to decoded Vec<u8> using Hex::decode into a Vec<Result<_,_>> and then
collect to Vec<Vec<u8>> (handling the ? on error), convert those Vecs to slices
in a separate variable (e.g., factory_slices), and pass factory_slices to
block.events::<ExaAccountInitialized>(); finally map the events to event.account
and collect into Accounts. Keep the same error propagation but split into named
variables to improve readability and debugging.
- Around line 62-66: The conditional combining new.accounts,
seen.insert(address.to_vec()), and accounts.get_last(Hex(&address).to_string())?
is dense and needs an inline explanatory comment: add a short comment above the
whole expression describing the deduplication strategy (check if address is part
of the incoming batch, dedupe via the seen HashSet returning true only for
duplicates via the negation of seen.insert, and fallback to checking persistent
store with accounts.get_last for non-batched addresses), and explicitly note why
the first plugin instance is filtered out (i.e., the negation means already-seen
addresses are excluded so the first occurrence is kept and subsequent ones are
filtered). Reference the symbols new.accounts, seen.insert, accounts.get_last,
Hex(&address), and address in the comment so readers can map explanation to the
code.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8b6cd00 and e13495d.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • substreams/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (36)
  • .changeset/breezy-planes-repeat.md
  • .changeset/rare-readers-tell.md
  • .changeset/red-toys-pump.md
  • .changeset/violet-poems-heal.md
  • .dockerignore
  • .github/matcher.json
  • .github/workflows/server-build.yaml
  • .github/workflows/substreams-build.yaml
  • .github/workflows/test.yaml
  • .vscode/extensions.json
  • common/package.json
  • contracts/package.json
  • contracts/test/mocks/BobExecute.s.sol
  • cspell.json
  • docs/package.json
  • pnpm-workspace.yaml
  • server/Dockerfile
  • server/database/schema.ts
  • server/drizzle.config.ts
  • server/package.json
  • server/script/firehose.ts
  • server/test/anvil.ts
  • server/test/database.ts
  • server/test/e2e.ts
  • substreams/.cargo/config.toml
  • substreams/.gitignore
  • substreams/Cargo.toml
  • substreams/Dockerfile
  • substreams/buf.gen.yaml
  • substreams/build.rs
  • substreams/package.json
  • substreams/proto/exa.proto
  • substreams/rust-toolchain.toml
  • substreams/rustfmt.toml
  • substreams/src/lib.rs
  • substreams/substreams.yaml
🧰 Additional context used
📓 Path-based instructions (11)
**/.changeset/*.md

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

Use a lowercase sentence in the imperative present tense for changeset summaries

Files:

  • .changeset/breezy-planes-repeat.md
  • .changeset/red-toys-pump.md
  • .changeset/violet-poems-heal.md
  • .changeset/rare-readers-tell.md
server/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/server.mdc)

server/**/*.ts: Use c.var object to pass strongly-typed data between Hono middleware and route handlers; do not use c.set
All request validation (headers, body, params) must be handled by @hono/valibot-validator middleware; do not perform manual validation inside route handlers
Use Hono's built-in error handling by throwing new HTTPException() for expected errors; unhandled errors will be caught and logged automatically
Enforce Node.js best practices using ESLint plugin:n/recommended configuration
Enforce Drizzle ORM best practices using ESLint plugin:drizzle/all configuration, including requiring where clauses for update and delete operations
Use Drizzle ORM query builder for all database interactions; do not write raw SQL queries unless absolutely unavoidable
All authentication and authorization logic must be implemented in Hono middleware
Do not access process.env directly in application code; load all configuration and secrets once at startup and pass them through dependency injection or context
Avoid long-running, synchronous operations; use async/await correctly and be mindful of CPU-intensive tasks to prevent blocking the event loop

Files:

  • server/test/anvil.ts
  • server/test/database.ts
  • server/test/e2e.ts
  • server/script/firehose.ts
  • server/drizzle.config.ts
  • server/database/schema.ts
**/*.{js,ts,tsx,jsx,sol}

📄 CodeRabbit inference engine (AGENTS.md)

Follow linter/formatter (eslint, prettier, solhint) strictly with high strictness level. No any type.

Files:

  • server/test/anvil.ts
  • server/test/database.ts
  • server/test/e2e.ts
  • server/script/firehose.ts
  • contracts/test/mocks/BobExecute.s.sol
  • server/drizzle.config.ts
  • server/database/schema.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Omit redundant type names in variable declarations - let the type system explain itself

**/*.{ts,tsx}: Use PascalCase for TypeScript types and interfaces
Use valibot for all runtime validation of API inputs, environment variables, and other data; define schemas once and reuse them
Infer TypeScript types from valibot schemas using type User = v.Input<typeof UserSchema> instead of manually defining interfaces

Files:

  • server/test/anvil.ts
  • server/test/database.ts
  • server/test/e2e.ts
  • server/script/firehose.ts
  • server/drizzle.config.ts
  • server/database/schema.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Omit contextual names - don't repeat class/module names in members
Omit meaningless words like 'data', 'state', 'manager', 'engine', 'value' from variable and function names unless they add disambiguation

**/*.{ts,tsx,js,jsx}: Prefer function declarations for all multi-line functions; use function expressions or arrow functions only for single-line implementations
Prefer const for all variable declarations by default; only use let if the variable's value will be reassigned
Declare each variable on its own line with its own const or let keyword, not multiple declarations on one line
Use camelCase for TypeScript variables and functions
Always use import type { ... } for type imports
Use relative paths for all imports within the project; avoid tsconfig path aliases
Follow eslint-plugin-import order: react, external libraries, then relative paths
Use object and array destructuring to access and use properties
Use object method shorthand syntax when a function is a property of an object
Prefer optional chaining (?.), nullish coalescing (??), object and array spreading (...), and for...of loops over traditional syntax
Do not use abbreviations or cryptic names; write out full words like error, parameters, request instead of err, params, req
Use Number.parseInt() instead of the global parseInt() function when parsing numbers
All classes called with new must use PascalCase
Use Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe() instead of the deprecated new Buffer()
Use @ts-expect-error instead of @ts-ignore; follow it immediately with a single-line lowercase comment explaining why the error is expected, without separators like - or :
Do not include the type in a variable's name; let the static type system do its job (e.g., use const user: User not const userObject: User)
Do not repeat the name of a class or module within its members; omit contextual names (e.g., use `class User { getProfil...

Files:

  • server/test/anvil.ts
  • server/test/database.ts
  • server/test/e2e.ts
  • server/script/firehose.ts
  • server/drizzle.config.ts
  • server/database/schema.ts
server/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

server/**/*.{ts,tsx}: Server API: implement schema-first approach using OpenAPI via hono with validation via valibot middleware
Server database: drizzle schema is source of truth. Migrations required. No direct database access in handlers - use c.var.db

Files:

  • server/test/anvil.ts
  • server/test/database.ts
  • server/test/e2e.ts
  • server/script/firehose.ts
  • server/drizzle.config.ts
  • server/database/schema.ts
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

For files with a single default export, name the file identically to the export; for files with multiple exports, use camelCase with a strong preference for a single word

Files:

  • server/test/anvil.ts
  • server/test/database.ts
  • server/test/e2e.ts
  • server/script/firehose.ts
  • server/drizzle.config.ts
  • server/database/schema.ts
contracts/**/*.sol

📄 CodeRabbit inference engine (.cursor/rules/contracts.mdc)

contracts/**/*.sol: Import order must be sorted alphabetically and grouped by type (openzeppelin, solady, internal, etc.), enforced by forge fmt
All functions and state variables must have explicit visibility modifiers (public, private, internal, external)
Use custom errors instead of require() statements with string messages; require() with messages is forbidden
console.log is forbidden in contract code
All external and public functions that modify state must be protected by a reentrancy guard, unless explicitly proven unnecessary
Use modifiers like onlyOwner or function-level checks for privileged functions; default to strictest access level (private > internal > external > public)
Use named parameters in mappings for clarity (e.g., mapping(address user => uint256 balance))
All code must pass pnpm --filter plugin test:slither and pnpm --filter plugin test:solhint with zero warnings; do not ignore warnings
Follow standard Solidity import order: external libraries (openzeppelin, solady) first, then project-internal contracts
All public and external functions and state variables must have complete natspec comments (@notice, @dev, @param, @return)
Use custom errors instead of require statements with string messages for better gas efficiency and error information
Internal and private variables should be prefixed with an underscore (_); function arguments should not be prefixed
Use constant keyword for true constants; use immutable for configurable values like contract addresses
Minimize writes to storage; read data into memory or calldata whenever possible for gas optimization
Use smallest integer size that is safe for the variable (e.g., uint32 for timestamps) and be aware of struct packing for gas efficiency
Prefer using the solady library over openzeppelin where possible, as it is highly optimized for gas
Correctly label functions as view or pure when they do not modify state

contracts/**/*.sol: Smart contracts: use custom errors error MyError(); instead of s...

Files:

  • contracts/test/mocks/BobExecute.s.sol
**/*.sol

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

**/*.sol: Use camelCase for Solidity variables and functions
Use PascalCase for Solidity contracts and structs

Files:

  • contracts/test/mocks/BobExecute.s.sol
**/+(app|src)/**

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

Use kebab-case for directory names and public Expo router routes

Files:

  • substreams/src/lib.rs
server/database/schema.ts

📄 CodeRabbit inference engine (.cursor/rules/server.mdc)

server/database/schema.ts: Database schema must be defined in server/database/schema.ts as the single source of truth for database structure
Define relations explicitly in the schema file to enable Drizzle to generate typed query results including related entities

Files:

  • server/database/schema.ts
🧠 Learnings (33)
📚 Learning: 2025-12-31T00:23:55.034Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 610
File: .changeset/ready-experts-fly.md:1-2
Timestamp: 2025-12-31T00:23:55.034Z
Learning: In the exactly/exa repository, allow and require empty changeset files (containing only --- separators) when changes are not user-facing and do not warrant a version bump. This is needed because CI runs changeset status --since origin/main and requires a changeset file to exist. Ensure such empty changesets are used only for non-user-facing changes and document the rationale in the commit or changelog notes.

Applied to files:

  • .changeset/breezy-planes-repeat.md
  • .changeset/red-toys-pump.md
  • .changeset/violet-poems-heal.md
  • .changeset/rare-readers-tell.md
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Avoid long-running, synchronous operations; use `async/await` correctly and be mindful of CPU-intensive tasks to prevent blocking the event loop

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Applies to server/**/*.{ts,tsx} : Server database: drizzle schema is source of truth. Migrations required. No direct database access in handlers - use `c.var.db`

Applied to files:

  • server/test/database.ts
  • server/drizzle.config.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Do not access `process.env` directly in application code; load all configuration and secrets once at startup and pass them through dependency injection or context

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Server stack: use hono, node.js, drizzle orm, postgres

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Use Drizzle ORM query builder for all database interactions; do not write raw SQL queries unless absolutely unavoidable

Applied to files:

  • server/test/database.ts
  • server/drizzle.config.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/database/schema.ts : Define relations explicitly in the schema file to enable Drizzle to generate typed query results including related entities

Applied to files:

  • server/test/database.ts
  • server/drizzle.config.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/database/migrations/** : All schema changes must be accompanied by a migration file generated by `drizzle-kit`; never edit the database schema manually

Applied to files:

  • server/test/database.ts
  • server/drizzle.config.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Server (exactly/server) uses Hono (framework), Node.js, Drizzle ORM (database), and PostgreSQL

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Enforce Drizzle ORM best practices using ESLint `plugin:drizzle/all` configuration, including requiring `where` clauses for `update` and `delete` operations

Applied to files:

  • server/test/database.ts
  • server/drizzle.config.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Zero config local dev environment: no `.env` files, mock all external services

Applied to files:

  • server/test/e2e.ts
  • contracts/test/mocks/BobExecute.s.sol
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Applies to {src/generated/**,server/generated/**,contracts/broadcast/**,contracts/cache/**} : Do not manually edit files in generated directories: `src/generated/`, `server/generated/`, `contracts/broadcast/`, and `contracts/cache/`

Applied to files:

  • .dockerignore
  • common/package.json
  • substreams/package.json
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.sol : Use custom errors instead of require statements with string messages for better gas efficiency and error information

Applied to files:

  • substreams/build.rs
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Run contract tests using `pnpm --filter plugin test` and check formatting using `pnpm --filter plugin test:fmt`

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Run all tests using `pnpm test`, which includes TypeScript compilation, ESLint, spell checking, markdown linting, and contract tests

Applied to files:

  • contracts/package.json
📚 Learning: 2026-01-08T19:46:37.829Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: contracts/test/mocks/BobExecute.s.sol:61-71
Timestamp: 2026-01-08T19:46:37.829Z
Learning: Natspec documentation requirements do not apply to test/mock/dev scripts (files in contracts/test/ or with .s.sol extension). Only production contract code in contracts/src/ requires complete natspec comments.

Applied to files:

  • contracts/package.json
  • contracts/test/mocks/BobExecute.s.sol
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.sol : All code must pass pnpm --filter plugin test:slither and pnpm --filter plugin test:solhint with zero warnings; do not ignore warnings

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Applies to contracts/**/*.test.sol : Smart contracts testing: fuzzing required for inputs and gas snapshots mandatory

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.t.sol : Use fuzz testing (testFuzz_...) extensively for functions with numerical or address inputs to cover a wide range of scenarios

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Rely on `pnpm` scripts for all tasks; all setup, testing, and generation must be automated

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Use `nx` for running tasks (build, lint, test, e2e) instead of underlying tooling directly

Applied to files:

  • contracts/package.json
  • substreams/package.json
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.t.sol : Test files must end with .t.sol and mirror the name of the contract they test (e.g., ExaPlugin.sol -> ExaPlugin.t.sol)

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-30T15:03:28.449Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Do not use abbreviations or cryptic names; write out full words like `error`, `parameters`, `request` instead of `err`, `params`, `req`

Applied to files:

  • server/script/firehose.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Omit meaningless words like 'data', 'state', 'manager', 'engine', 'value' from variable and function names unless they add disambiguation

Applied to files:

  • server/script/firehose.ts
  • cspell.json
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.sol : All public and external functions and state variables must have complete natspec comments (notice, dev, param, return)

Applied to files:

  • contracts/test/mocks/BobExecute.s.sol
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.t.sol : Use vm.prank to simulate calls from different addresses; avoid changing msg.sender through other means

Applied to files:

  • contracts/test/mocks/BobExecute.s.sol
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/database/schema.ts : Database schema must be defined in `server/database/schema.ts` as the single source of truth for database structure

Applied to files:

  • server/drizzle.config.ts
  • server/database/schema.ts
📚 Learning: 2026-01-08T19:54:10.673Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: substreams/src/lib.rs:54-71
Timestamp: 2026-01-08T19:54:10.673Z
Learning: In substreams/src/lib.rs in the map_exa_plugins function, the deduplication logic using `!seen.insert(address.to_vec())` is intentional. The first plugin installed for a new account is an owner plugin (not an exa plugin), so it must be filtered out. The negation ensures the first plugin is skipped and subsequent plugins (the actual exa plugins) are kept.

Applied to files:

  • substreams/src/lib.rs
📚 Learning: 2025-12-30T15:03:28.449Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Omit meaningless words from names like `data`, `state`, `manager`, `engine`, `object`, `entity`, and `instance`

Applied to files:

  • cspell.json
📚 Learning: 2025-12-30T15:03:28.449Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/.changeset/*.md : Use a lowercase sentence in the imperative present tense for changeset summaries

Applied to files:

  • .changeset/violet-poems-heal.md
📚 Learning: 2026-01-08T19:45:38.852Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: .github/workflows/substreams-build.yaml:2-2
Timestamp: 2026-01-08T19:45:38.852Z
Learning: In the exactly/exa repository, prefer concise inline YAML syntax (e.g., `on: { push: { tags: ["pattern"] }, workflow_dispatch: {} }`) over expanded multi-line format in GitHub workflow files, even if it triggers YAMLlint warnings about formatting.

Applied to files:

  • .github/workflows/substreams-build.yaml
📚 Learning: 2026-01-08T19:54:26.885Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: substreams/rust-toolchain.toml:1-4
Timestamp: 2026-01-08T19:54:26.885Z
Learning: Rust edition 2024 was released as stable on February 20, 2025 with Rust 1.85.0 and does not require nightly channel.

Applied to files:

  • substreams/rust-toolchain.toml
📚 Learning: 2026-01-08T20:26:33.396Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: .vscode/extensions.json:6-6
Timestamp: 2026-01-08T20:26:33.396Z
Learning: For validating VS Code extension IDs in this repository, check both the official VS Code Marketplace (marketplace.visualstudio.com) and the Open VSX registry (open-vsx.org) before flagging an extension as invalid. Open VSX is a widely-used alternative registry, so a valid ID may appear in either registry. If an ID is not present in both registries, flag it as invalid. Apply this check specifically when validating the IDs listed in .vscode/extensions.json.

Applied to files:

  • .vscode/extensions.json
🧬 Code graph analysis (1)
server/test/database.ts (1)
server/database/schema.ts (1)
  • substreams (70-70)
🪛 Buf (1.62.1)
substreams/proto/exa.proto

3-3: Files with package "exa" must be within a directory "exa" relative to root but were in directory "substreams/proto".

(PACKAGE_DIRECTORY_MATCH)

🪛 Checkov (3.2.334)
substreams/Dockerfile

[low] 1-33: Ensure that HEALTHCHECK instructions have been added to container images

(CKV_DOCKER_2)

🪛 YAMLlint (1.37.1)
.github/workflows/substreams-build.yaml

[warning] 2-2: truthy value should be one of [false, true]

(truthy)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 6-6: too many spaces inside braces

(braces)


[error] 6-6: too many spaces inside braces

(braces)


[error] 11-11: too many spaces inside braces

(braces)


[error] 11-11: too many spaces inside braces

(braces)


[error] 25-25: too many spaces inside braces

(braces)


[error] 25-25: too many spaces inside braces

(braces)

🔇 Additional comments (42)
substreams/.gitignore (1)

1-8: Standard and appropriate ignore patterns.

The gitignore entries properly cover build artifacts (target, *.log, *.spkg) and generated code directories (src/proto, src/contracts) typical for a Rust/WASM project. The file structure is well-organized.

server/drizzle.config.ts (1)

8-8: LGTM! Schema filter addition is appropriate.

The schemaFilter property correctly limits drizzle-kit operations to the public and substreams schemas, which aligns with the PR's objective of introducing substreams functionality.

cspell.json (1)

6-6: LGTM! Dictionary additions align with new tooling.

The ignore paths and dictionary words appropriately support the Rust, Protobuf, and Substreams toolchain being introduced in this PR:

  • Ignore paths exclude build artifacts (substreams/target) and generated config (.github/matcher.json)
  • Dictionary terms include legitimate Rust ecosystem tools (clippy, rustup, serde, prost), Protobuf tools (protogen, protolint, bufbuild), and Substreams-related identifiers

Also applies to: 9-9, 14-14, 26-26, 32-32, 35-35, 55-55, 72-72, 114-116, 122-123, 131-131, 134-134, 138-138

.changeset/red-toys-pump.md (1)

1-5: LGTM! Changeset follows conventions.

The changeset is properly formatted with a minor version bump for @exactly/substreams and a summary in lowercase imperative present tense as required by the coding guidelines.

server/test/e2e.ts (1)

7-7: LGTM! Sentry mock enhances test observability.

The mock correctly wraps captureException to log exceptions during e2e tests while preserving the original behavior. The implementation properly:

  • Imports and uses TypeScript types
  • Preserves all original exports
  • Maintains the function signature and return value
  • Adds debugging visibility with console.log (appropriate use of eslint-disable in test code)

Also applies to: 58-67

.vscode/extensions.json (1)

6-6: LGTM! Extension recommendations support new toolchain.

The additions of rust-lang.rust-analyzer and drblury.protobuf-vsc are appropriate for the Rust and Protobuf code being introduced in the substreams implementation. These recommendations will enhance the developer experience when working with the new codebase.

Also applies to: 16-16

.github/workflows/server-build.yaml (1)

36-37: LGTM: Explicit Dockerfile path improves clarity.

Making the Dockerfile location explicit is a good practice, especially in a monorepo with multiple build contexts.

docs/package.json (1)

37-44: LGTM: Build inputs properly configured.

The NX inputs configuration correctly tracks the OpenAPI artifact dependency for incremental builds.

server/package.json (2)

64-64: LGTM: Substreams dependency added.

The substreams package is properly declared as a dev dependency, aligning with the PR's objective of initial substreams integration.


143-146: LGTM: Build inputs extended appropriately.

The additional inputs for CHAIN_ID and contract outputs properly support the enhanced build pipeline.

server/test/database.ts (6)

6-6: LGTM: Using execa for command execution.

The $ template from execa provides a modern, promise-based interface for running external commands.


49-49: LGTM: AbortController for process lifecycle management.

Using AbortController to coordinate cancellation across multiple processes is the appropriate pattern.


91-92: LGTM: Proper teardown with graceful process termination.

Using Promise.allSettled ensures teardown completes even if individual processes fail to stop gracefully. The AbortController properly signals cancellation to all spawned processes.


60-70: Error handling may hide non-abort failures.

Similar to the firehose process, the catch block suppresses all errors when aborted but also silently swallows errors from normal failures (e.g., substreams-sink-sql not found, connection issues).

🔧 Proposed fix for error handling
  const substreams = $({
    cancelSignal: controller.signal,
    forceKillAfterDelay: 33_333,
    cwd: "node_modules/@exactly/substreams",
    env: { SUBSTREAMS_ENDPOINTS_CONFIG_ANVIL: "localhost:10016" },
  })`substreams-sink-sql run ${postgresURL}&schemaName=substreams substreams.yaml --plaintext --batch-block-flush-interval 1 --batch-row-flush-interval 0`.catch(
-   (error: unknown) => {
-     if (controller.signal.aborted) return;
+   (error) => {
+     if (controller.signal.aborted) return undefined;
      throw error;
    },
  );

Likely an incorrect or invalid review comment.


50-58: Error handling may hide non-abort failures.

The catch block swallows all errors when the controller is aborted, but if the process fails during normal operation (e.g., fireeth not found, port conflict), the error is caught and the promise resolves successfully. This masks genuine startup failures.

🔧 Proposed fix for error handling
  const firehose = $({
    cancelSignal: controller.signal,
    forceKillAfterDelay: 33_333,
  })`fireeth start reader-node,merger,relayer,substreams-tier1 --advertise-chain-name=anvil --config-file= \
      --data-dir=node_modules/@exactly/.firehose --reader-node-path=bash --reader-node-arguments=${'-c "\
-       fireeth tools poll-rpc-blocks http://localhost:8545 0 | tsx script/firehose.ts"'}`.catch((error: unknown) => {
-   if (controller.signal.aborted) return;
+       fireeth tools poll-rpc-blocks http://localhost:8545 0 | tsx script/firehose.ts"'}`.catch((error) => {
+   if (controller.signal.aborted) return undefined;
    throw error;
  });

This ensures errors are only suppressed when explicitly aborted, not when the process fails to start or crashes unexpectedly.

Likely an incorrect or invalid review comment.


65-65: No action required. The &schemaName=substreams parameter format is correct per substreams-sink-sql documentation and properly appended to the PostgreSQL URL.

contracts/test/mocks/BobExecute.s.sol (2)

39-39: LGTM!

Using getAddress instead of createAccount is appropriate here since it only computes the counterfactual address without deploying, which is correct for test setup where the account already exists from prior deployment scripts.


61-71: LGTM!

The BatchDeploy contract efficiently creates multiple accounts in its constructor. Using ++i for the loop increment is gas-efficient, and the deterministic address generation from 420 + i is suitable for test purposes.

server/test/anvil.ts (1)

159-160: LGTM!

Adding --tc BobExecuteScript is necessary because BobExecute.s.sol now contains multiple contracts (BobExecuteScript and BatchDeploy). The flag disambiguates which contract's entry point to execute.

.dockerignore (2)

6-18: LGTM!

The granular ignore patterns are well-structured:

  • Correctly preserves run-latest.json broadcast files needed for deployments while excluding other broadcast artifacts
  • Appropriately excludes substreams build artifacts (target, abi, src/contracts, src/proto)
  • Reduces Docker build context size effectively

21-25: LGTM!

Excluding documentation, logs, and build metadata files (.tsbuildinfo, lcov.info, expo-env.d.ts) from the Docker context is appropriate—these are not needed at runtime.

.github/matcher.json (1)

81-91: LGTM!

The rust-formatter matcher correctly captures rustfmt's diff output format with file and line information.

server/script/firehose.ts (1)

135-135: Verify intentional use of libNum in two positions.

The output format uses libNum for both positions 4 and 5: FIRE BLOCK ${num} ${hash} ${libNum} ${parentHash} ${libNum} ${timestamp} ${newPayload}. The input has 7 tokens but the output appears to replace position 5 with libNum again. Is this intentional to match a specific protocol version?

contracts/package.json (1)

60-100: LGTM!

The restructured NX targets create a clear dependency chain (test:sizestest:gastest:slithertest:coverage) that ensures build artifacts are available for each subsequent step. The output declarations enable proper NX caching, and test:all correctly aggregates the top-level checks.

common/package.json (1)

44-50: LGTM!

The additional inputs correctly establish dependencies between contract compilation outputs and wagmi code generation. This ensures the generated TypeScript bindings stay synchronized with contract ABIs when either the compiled outputs (contracts/out/**/*.json) or local broadcast artifacts (contracts/broadcast/*/31337/*.json) change.

substreams/.cargo/config.toml (1)

1-2: LGTM!

The wasm32-unknown-unknown target configuration is correct for building Substreams WASM modules and aligns with the toolchain setup and cdylib crate type.

pnpm-workspace.yaml (1)

7-7: LGTM!

Adding "substreams" to the workspace packages correctly integrates the new substreams package into the monorepo structure.

substreams/rustfmt.toml (1)

1-6: LGTM!

The rustfmt configuration is well-structured with reasonable settings that complement the toolchain setup and support formatting of generated Rust sources.

substreams/rust-toolchain.toml (1)

2-2: Change channel from "nightly" to "stable".

The project uses Rust edition 2024 (stable as of February 20, 2025) and contains no nightly-specific features (#![feature(...)]) or unstable attributes. The wasm32-unknown-unknown target and components (clippy, rustfmt) are all supported on the stable channel. Using stable improves reliability and build times.

⛔ Skipped due to learnings
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: substreams/rust-toolchain.toml:1-4
Timestamp: 2026-01-08T19:54:26.885Z
Learning: Rust edition 2024 was released as stable on February 20, 2025 with Rust 1.85.0 and does not require nightly channel.
substreams/buf.gen.yaml (1)

1-8: LGTM!

The Buf code generation configuration is well-structured with pinned plugin versions for reproducibility. The output path aligns with the protobuf workflow referenced in build.rs.

substreams/Cargo.toml (2)

32-35: LGTM!

The release profile settings are well-suited for WASM targets: LTO for dead code elimination, opt-level = 's' for size optimization, and stripping debug info to minimize the binary size.


1-9: Consider using stable Rust 1.85+ instead of nightly for edition 2024 compatibility.

Rust 2024 edition is stable (released February 2025 with Rust 1.85) and does not require the nightly channel. The toolchain is currently pinned to nightly, which adds an unnecessary constraint. Review whether nightly is needed for other features in this project; if not, switching to a stable toolchain would simplify CI and local setup.

⛔ Skipped due to learnings
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: substreams/rust-toolchain.toml:1-4
Timestamp: 2026-01-08T19:54:26.885Z
Learning: Rust edition 2024 was released as stable on February 20, 2025 with Rust 1.85.0 and does not require nightly channel.
.github/workflows/substreams-build.yaml (1)

1-38: LGTM!

The workflow is well-structured with comprehensive tag patterns for semver extraction, proper permissions for package publishing and attestation, and build provenance attestation for supply chain security. Based on learnings, the inline YAML syntax is the preferred style for this repository.

substreams/proto/exa.proto (1)

9-17: LGTM!

The Plugins message with nested Plugin is well-structured. Using bytes for Ethereum addresses and uint64 for ordinal tracking is appropriate for the blockchain indexing use case.

substreams/Dockerfile (2)

32-33: Default CMD is suitable for local development only.

The host.docker.internal hostname and sslmode=disable are appropriate for local development but won't work in production deployments. Ensure the CMD is overridden with proper connection strings in deployment configurations.


20-30: LGTM!

Good security practices: runs as a non-root substreams user with dumb-init for proper signal handling. File ownership is correctly set during COPY operations.

substreams/build.rs (1)

23-33: LGTM!

The ABI extraction and Rust binding generation logic is well-structured with proper error context using with_context.

substreams/substreams.yaml (2)

14-42: LGTM!

The module dependency graph is correctly structured: map_exa_accountsstore_exa_accountsmap_exa_pluginsdb_out. Input/output types align with the proto definitions in exa.proto.


53-82: Address configuration inconsistency between wagmi.config.ts and substreams.yaml for Base and Anvil networks.

The Base and Anvil factory addresses in substreams.yaml are hardcoded values, but common/wagmi.config.ts loads these addresses dynamically from deployment scripts (ExaAccountFactory.s.sol) for these networks. This creates a maintenance risk where the hardcoded addresses in substreams.yaml may become stale if deployments are re-run.

For Op-sepolia and Base-sepolia, the hardcoded addresses align with the default used elsewhere. For Optimism, verify that the four configured factories are intentional (they may represent different deployments or rollout phases).

Recommendation: Either update substreams.yaml to use dynamic deployment records for Base and Anvil, or document why the hardcoded values should be used instead of the deployment script outputs.

substreams/src/lib.rs (1)

85-85: LGTM: Safe handling of optional timestamp.

The use of unwrap_or_default() ensures that a missing timestamp is handled gracefully by providing a default value rather than panicking.

server/database/schema.ts (1)

88-91: Consider the implications of using numeric for block number and timestamp.

The blocks table uses numeric for both number and timestamp rather than bigint or PostgreSQL's native timestamp types. While numeric provides arbitrary precision, it may have performance implications for range queries and comparisons.

Verify that numeric is the appropriate type choice for your query patterns and indexing needs, especially if you'll be performing frequent range scans on block numbers.

substreams/package.json (1)

52-58: The test:clippy target has an unsatisfied dependency on the prepare task.

The target depends on "prepare" and "^prepare", but no prepare target is defined in substreams/package.json. The bare "prepare" dependency will fail when test:clippy is invoked, while "^prepare" may only succeed if substreams has dependencies with prepare tasks defined. Define a prepare target in this package to satisfy the build dependency, or remove the local prepare reference if not needed.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 24

🤖 Fix all issues with AI agents
In @.changeset/breezy-planes-repeat.md:
- Line 5: The changeset summary line contains an emoji prefix ("✨ map exa
accounts"); edit the .changeset/breezy-planes-repeat.md changeset summary to
remove the emoji and ensure the summary is a plain, lowercase sentence in
imperative present tense (e.g., "map exa accounts") so it conforms to the
project's changeset format.

In @.changeset/rare-readers-tell.md:
- Line 5: Update the changeset summary "✨ store exa accounts" to remove the
emoji and use a lowercase imperative present-tense sentence; e.g., replace it
with "store exa accounts" (or rephrase to a clear imperative like "add exa
account storage" if more precise), ensuring no emoji or capitalization is
present.

In @.changeset/violet-poems-heal.md:
- Line 5: Update the changeset message (the line currently "✨ insert exa plugins
into database") to be a complete lowercase imperative present-tense sentence by
adding the article and adjusting word order, e.g., "insert exa plugins into the
database".

In @.github/workflows/test.yaml:
- Around line 37-41: The workflow step that downloads binaries uses brittle
commands without fail-fast behavior, verification, or guaranteed PATH export;
update the shell block to enable strict error handling with set -euo pipefail,
validate that the jq/curl asset lookup returns a non-empty URL before attempting
the second curl (and fail with a clear error if no asset is found), add checksum
or signature verification for each downloaded archive before extraction (fail if
verification fails), and append the created bin directory to the runner PATH
with echo "$HOME/.local/bin" >> $GITHUB_PATH so the installed binaries are
available to later steps; refer to the existing mkdir -p $HOME/.local/bin && cd
$HOME/.local/bin loop and the nested curl | jq asset selection to locate where
to add these changes.

In @contracts/test/mocks/BobExecute.s.sol:
- Line 39: bobAccount is only assigned via factory.getAddress() (so not
deployed) but run() calls executeProposal() on it; either deploy bobAccount in
setUp() by replacing factory.getAddress(...) with factory.createAccount(...)
(assigning the returned deployed IExaAccount to bobAccount) or explicitly
deploy/create bobAccount at the start of run() before any executeProposal()
calls; update references to bobAccount accordingly so executeProposal() operates
on a deployed account and not the address-only value.

In @server/database/schema.ts:
- Around line 88-100: Add indexes for query performance by updating the table
definitions for blocks and exaPlugins: in the blocks table (symbol: blocks) add
an index on the timestamp column (symbol: timestamp) and in exaPlugins (symbol:
exaPlugins) add an index on the account column (symbol: account) by extending
the table factory callbacks to include index(...) entries; also import index
from "drizzle-orm/pg-core" alongside existing imports.
- Around line 72-77: The cursors table definition lacks NOT NULL constraints for
fields required by the sink; update the substreams.table("cursors", ...) schema
so that the column definitions for cursor, blockNum, and blockId call .notNull()
(e.g., change text("cursor") to text("cursor").notNull(), bigint("block_num", {
mode: "bigint" }) to bigint("block_num", { mode: "bigint" }).notNull(), and
text("block_id") to text("block_id").notNull()) while leaving id as the primary
key.

In @server/drizzle.config.ts:
- Line 8: Missing migration files for the "substreams" schema: run drizzle-kit
to generate migrations that create the "substreams" schema and its tables (the
schema is defined via pgSchema() in schema.ts and schemaFilter currently
includes "substreams"), commit the generated migration files, and ensure the
migration config in drizzle.config.ts (schemaFilter) aligns with the generated
migrations so the database can be migrated from a clean state using drizzle-kit.

In @server/script/firehose.ts:
- Line 127: The destructured variables use abbreviated names; replace the
variables in the array destructuring (currently declared via const [, , num,
hash, libNum, parentHash, payloadBase64] = parts) with descriptive names such as
blockNumber and lastIrreversibleBlockNumber (keep payloadBase64), and then
update all subsequent references in this file to use blockNumber and
lastIrreversibleBlockNumber instead of num and libNum; retain or remove the
eslint-disable comment as appropriate after renaming.
- Around line 130-131: The variable name "ts" is an unclear abbreviation; rename
it to a descriptive identifier such as "extractedTimestamp" where it's declared
from extractTimestamp(buffer) and update the subsequent line that computes
"timestamp" to reference extractedTimestamp instead of ts; ensure all
occurrences in the surrounding scope (e.g., in the function or block containing
extractTimestamp(buffer)) are updated to the new name to keep code consistent
with naming guidelines.
- Around line 59-64: The readBytes method can silently truncate when the decoded
length exceeds remaining buffer; before updating this.offset or calling
subarray, validate the length from readVarint() (ensure it's a non-negative
integer and <= this.buffer.length - this.offset) and throw a descriptive error
(e.g., "Unexpected end of buffer in readBytes") if it would overflow; only after
validation advance this.offset and return this.buffer.subarray(start,
this.offset). Include the check in the readBytes function to prevent silent data
truncation.
- Around line 100-104: The loop that parses timestamp fields using
tsCursor.readTag assumes fields 1 and 2 are varints but never checks
tag.wireType; update the parsing inside the for-loop to verify tag.wireType ===
0 before calling tsCursor.readVarint for fieldNumber 1 and 2, and if the wire
type is different, either call tsCursor.skip(tag.wireType) or surface an
error/log (choose consistent behavior with remaining parsing). Reference the
existing symbols tsCursor.readTag, tag.fieldNumber, tag.wireType,
tsCursor.readVarint, and tsCursor.skip when applying the change.
- Line 96: Rename the abbreviated variable tsCursor to timestampCursor where
it's declared (new ProtoCursor(timestampBuffer)) and update all subsequent
references to use timestampCursor (e.g., the usages around the existing lines
referencing tsCursor). Ensure you update any related variable names or
destructuring that depend on tsCursor to match timestampCursor and preserve the
ProtoCursor(timestampBuffer) initialization and behavior.
- Line 135: The console.log template repeats libNum twice; locate the statement
that prints FIRE BLOCK (the console.log using variables num, hash, libNum,
parentHash, libNum, timestamp, newPayload) and replace the second libNum with
the correct variable from the surrounding scope (e.g., parentNum or parentLibNum
as defined nearby) so the seven output tokens match the seven input values (num,
hash, libNum, parentHash, <correctVariable>, timestamp, newPayload); verify the
chosen variable exists and adjust the template accordingly.
- Around line 141-144: Remove the unnecessary else and make the
malformed-FIRE-BLOCK case an early return: when detecting the malformed block
(check using parts.length and trimmedLine), log the error with console.error and
immediately return from the surrounding function, instead of using an else block
followed by a standalone return; update the code around the malformed handling
in server/script/firehose.ts (the console.error call that mentions FIRE BLOCK
and the subsequent return) to consolidate into a single early-return branch.

In @substreams/buf.gen.yaml:
- Around line 1-8: Add a new buf.yaml in the substreams/ directory so buf can
find your .proto files; create substreams/buf.yaml containing version v1 and a
modules entry that points to the proto module (modules: - path: proto) so that
the existing buf.gen.yaml plugins (remote neoeinstein-prost and
neoeinstein-prost-crate) can locate files like substreams/proto/exa.proto for
compilation.

In @substreams/build.rs:
- Around line 25-36: The closure that generates contract bindings calls
Abigen::new(...).generate()?.write_to_file(format!("src/contracts/{mod_name}.rs"))
but doesn't ensure the target directory exists; add a call to
create_dir_all("src/contracts") (propagating errors with ?) before calling
write_to_file (e.g., inside the contracts.iter closure before Abigen::... or
once before the loop) so the src/contracts directory is created if missing.
- Around line 50-67: The rustfmt invocation silently ignores glob errors because
of filter_map(Result::ok); change this to propagate or surface glob errors by
collecting the iterator of Results and handling errors instead of dropping them:
gather the two glob iterators (the glob("src/contracts/**/*.rs")? and
glob("src/proto/**/*.rs")?) into a single iterator, collect into a
Result<Vec<PathBuf>, glob::GlobError> (e.g. .chain(...).collect::<Result<Vec<_>,
_>>()?), then either propagate the error with ? or call expect with a clear
message, and pass that Vec to Command::new("rustfmt").args(paths) so any glob
I/O/pattern errors are not silently ignored.

In @substreams/Dockerfile:
- Around line 32-33: The Dockerfile currently lacks a HEALTHCHECK and the
default CMD hardcodes host.docker.internal which only works on Docker Desktop;
add a HEALTHCHECK instruction that probes the service exposed by
substreams-sink-sql (e.g., TCP check or an HTTP/health endpoint if available)
using the HEALTHCHECK directive and sensible interval/retries, and change the
CMD to use an environment variable (e.g., POSTGRES_DSN or DB_HOST) with a
fallback (localhost) instead of host.docker.internal so users can override the
DSN at runtime; reference ENTRYPOINT ["dumb-init", "/app/substreams-sink-sql",
"run"] and CMD to update them accordingly.

In @substreams/proto/exa.proto:
- Line 3: Move the proto file into a directory that matches its package name:
create substreams/proto/exa/ and relocate substreams/proto/exa.proto →
substreams/proto/exa/exa.proto, keeping the package declaration `package exa;`
unchanged; then update any build/config references to the proto path (e.g.,
buf.gen.yaml, substreams.yaml, and any import statements that reference
substreams/proto/exa.proto) so they point to the new
substreams/proto/exa/exa.proto location to satisfy Buf conventions and eliminate
linter warnings.

In @substreams/rust-toolchain.toml:
- Line 2: Update the rust-toolchain.toml to use the stable toolchain by changing
the channel value from "nightly" to "stable" (modify the channel = "nightly"
line to channel = "stable"); after updating, run cargo check/build (including
any wasm32-unknown-unknown target builds) to confirm there are no nightly-only
features or attributes remaining and revert only if a nightly-gated dependency
is discovered.

In @substreams/src/lib.rs:
- Line 1: Add a brief comment immediately above the crate-level attribute
#![expect(clippy::not_unsafe_ptr_arg_deref)] explaining why the Clippy lint is
suppressed (e.g., that the suppression is required because the substreams
framework generates code via macros that intentionally uses patterns triggering
clippy::not_unsafe_ptr_arg_deref and those usages are safe/contextually
validated), and mention any specific macro or generated module (such as the
substreams codegen or the procedural macro name) and a reference to an issue/PR
or test that verifies the generated code is safe; keep the comment concise and
colocated with the attribute so future maintainers understand the reason for the
global suppression.
- Around line 27-44: The map_exa_accounts function creates unnecessary
intermediate allocations by collecting decoded hex addresses into a Vec<Vec<u8>>
and then immediately converting them to slices; instead, decode the addresses
once into a local variable (e.g., let decoded: Vec<Vec<u8>> =
serde_qs::from_str::<Factories>(&params)?.factories.iter().map(Hex::decode).collect::<Result<_,
_>>()?), then pass decoded.iter().map(Vec::as_slice).collect::<Vec<_>>() into
block.events::<ExaAccountInitialized>(), avoiding the transient two-step
collection and redundant allocations while keeping the existing error
propagation via ?.

In @substreams/substreams.yaml:
- Around line 53-77: Update the factory address in the base-sepolia network
config so it uses the EIP-55 checksummed form instead of all-lowercase: locate
the base-sepolia entry and the params.map_exa_accounts value containing the
factory address `343efd4e2073afc4cbe304b5dd50f8d6480e2dd2` and replace it with
its checksummed EIP-55 variant (generate it programmatically using
ethers.utils.getAddress or web3.utils.toChecksumAddress to avoid manual error).
Ensure only the casing changes and the hex value remains the same.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e13495d and a0ef2f4.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • substreams/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (36)
  • .changeset/breezy-planes-repeat.md
  • .changeset/rare-readers-tell.md
  • .changeset/red-toys-pump.md
  • .changeset/violet-poems-heal.md
  • .dockerignore
  • .github/matcher.json
  • .github/workflows/server-build.yaml
  • .github/workflows/substreams-build.yaml
  • .github/workflows/test.yaml
  • .vscode/extensions.json
  • common/package.json
  • contracts/package.json
  • contracts/test/mocks/BobExecute.s.sol
  • cspell.json
  • docs/package.json
  • pnpm-workspace.yaml
  • server/Dockerfile
  • server/database/schema.ts
  • server/drizzle.config.ts
  • server/package.json
  • server/script/firehose.ts
  • server/test/anvil.ts
  • server/test/database.ts
  • server/test/e2e.ts
  • substreams/.cargo/config.toml
  • substreams/.gitignore
  • substreams/Cargo.toml
  • substreams/Dockerfile
  • substreams/buf.gen.yaml
  • substreams/build.rs
  • substreams/package.json
  • substreams/proto/exa.proto
  • substreams/rust-toolchain.toml
  • substreams/rustfmt.toml
  • substreams/src/lib.rs
  • substreams/substreams.yaml
🧰 Additional context used
📓 Path-based instructions (11)
server/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/server.mdc)

server/**/*.ts: Use c.var object to pass strongly-typed data between Hono middleware and route handlers; do not use c.set
All request validation (headers, body, params) must be handled by @hono/valibot-validator middleware; do not perform manual validation inside route handlers
Use Hono's built-in error handling by throwing new HTTPException() for expected errors; unhandled errors will be caught and logged automatically
Enforce Node.js best practices using ESLint plugin:n/recommended configuration
Enforce Drizzle ORM best practices using ESLint plugin:drizzle/all configuration, including requiring where clauses for update and delete operations
Use Drizzle ORM query builder for all database interactions; do not write raw SQL queries unless absolutely unavoidable
All authentication and authorization logic must be implemented in Hono middleware
Do not access process.env directly in application code; load all configuration and secrets once at startup and pass them through dependency injection or context
Avoid long-running, synchronous operations; use async/await correctly and be mindful of CPU-intensive tasks to prevent blocking the event loop

Files:

  • server/drizzle.config.ts
  • server/test/e2e.ts
  • server/test/database.ts
  • server/database/schema.ts
  • server/test/anvil.ts
  • server/script/firehose.ts
**/*.{js,ts,tsx,jsx,sol}

📄 CodeRabbit inference engine (AGENTS.md)

Follow linter/formatter (eslint, prettier, solhint) strictly with high strictness level. No any type.

Files:

  • server/drizzle.config.ts
  • server/test/e2e.ts
  • server/test/database.ts
  • server/database/schema.ts
  • server/test/anvil.ts
  • server/script/firehose.ts
  • contracts/test/mocks/BobExecute.s.sol
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Omit redundant type names in variable declarations - let the type system explain itself

**/*.{ts,tsx}: Use PascalCase for TypeScript types and interfaces
Use valibot for all runtime validation of API inputs, environment variables, and other data; define schemas once and reuse them
Infer TypeScript types from valibot schemas using type User = v.Input<typeof UserSchema> instead of manually defining interfaces

Files:

  • server/drizzle.config.ts
  • server/test/e2e.ts
  • server/test/database.ts
  • server/database/schema.ts
  • server/test/anvil.ts
  • server/script/firehose.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Omit contextual names - don't repeat class/module names in members
Omit meaningless words like 'data', 'state', 'manager', 'engine', 'value' from variable and function names unless they add disambiguation

**/*.{ts,tsx,js,jsx}: Prefer function declarations for all multi-line functions; use function expressions or arrow functions only for single-line implementations
Prefer const for all variable declarations by default; only use let if the variable's value will be reassigned
Declare each variable on its own line with its own const or let keyword, not multiple declarations on one line
Use camelCase for TypeScript variables and functions
Always use import type { ... } for type imports
Use relative paths for all imports within the project; avoid tsconfig path aliases
Follow eslint-plugin-import order: react, external libraries, then relative paths
Use object and array destructuring to access and use properties
Use object method shorthand syntax when a function is a property of an object
Prefer optional chaining (?.), nullish coalescing (??), object and array spreading (...), and for...of loops over traditional syntax
Do not use abbreviations or cryptic names; write out full words like error, parameters, request instead of err, params, req
Use Number.parseInt() instead of the global parseInt() function when parsing numbers
All classes called with new must use PascalCase
Use Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe() instead of the deprecated new Buffer()
Use @ts-expect-error instead of @ts-ignore; follow it immediately with a single-line lowercase comment explaining why the error is expected, without separators like - or :
Do not include the type in a variable's name; let the static type system do its job (e.g., use const user: User not const userObject: User)
Do not repeat the name of a class or module within its members; omit contextual names (e.g., use `class User { getProfil...

Files:

  • server/drizzle.config.ts
  • server/test/e2e.ts
  • server/test/database.ts
  • server/database/schema.ts
  • server/test/anvil.ts
  • server/script/firehose.ts
server/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

server/**/*.{ts,tsx}: Server API: implement schema-first approach using OpenAPI via hono with validation via valibot middleware
Server database: drizzle schema is source of truth. Migrations required. No direct database access in handlers - use c.var.db

Files:

  • server/drizzle.config.ts
  • server/test/e2e.ts
  • server/test/database.ts
  • server/database/schema.ts
  • server/test/anvil.ts
  • server/script/firehose.ts
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

For files with a single default export, name the file identically to the export; for files with multiple exports, use camelCase with a strong preference for a single word

Files:

  • server/drizzle.config.ts
  • server/test/e2e.ts
  • server/test/database.ts
  • server/database/schema.ts
  • server/test/anvil.ts
  • server/script/firehose.ts
**/.changeset/*.md

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

Use a lowercase sentence in the imperative present tense for changeset summaries

Files:

  • .changeset/rare-readers-tell.md
  • .changeset/red-toys-pump.md
  • .changeset/violet-poems-heal.md
  • .changeset/breezy-planes-repeat.md
**/+(app|src)/**

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

Use kebab-case for directory names and public Expo router routes

Files:

  • substreams/src/lib.rs
server/database/schema.ts

📄 CodeRabbit inference engine (.cursor/rules/server.mdc)

server/database/schema.ts: Database schema must be defined in server/database/schema.ts as the single source of truth for database structure
Define relations explicitly in the schema file to enable Drizzle to generate typed query results including related entities

Files:

  • server/database/schema.ts
contracts/**/*.sol

📄 CodeRabbit inference engine (.cursor/rules/contracts.mdc)

contracts/**/*.sol: Import order must be sorted alphabetically and grouped by type (openzeppelin, solady, internal, etc.), enforced by forge fmt
All functions and state variables must have explicit visibility modifiers (public, private, internal, external)
Use custom errors instead of require() statements with string messages; require() with messages is forbidden
console.log is forbidden in contract code
All external and public functions that modify state must be protected by a reentrancy guard, unless explicitly proven unnecessary
Use modifiers like onlyOwner or function-level checks for privileged functions; default to strictest access level (private > internal > external > public)
Use named parameters in mappings for clarity (e.g., mapping(address user => uint256 balance))
All code must pass pnpm --filter plugin test:slither and pnpm --filter plugin test:solhint with zero warnings; do not ignore warnings
Follow standard Solidity import order: external libraries (openzeppelin, solady) first, then project-internal contracts
All public and external functions and state variables must have complete natspec comments (@notice, @dev, @param, @return)
Use custom errors instead of require statements with string messages for better gas efficiency and error information
Internal and private variables should be prefixed with an underscore (_); function arguments should not be prefixed
Use constant keyword for true constants; use immutable for configurable values like contract addresses
Minimize writes to storage; read data into memory or calldata whenever possible for gas optimization
Use smallest integer size that is safe for the variable (e.g., uint32 for timestamps) and be aware of struct packing for gas efficiency
Prefer using the solady library over openzeppelin where possible, as it is highly optimized for gas
Correctly label functions as view or pure when they do not modify state

contracts/**/*.sol: Smart contracts: use custom errors error MyError(); instead of s...

Files:

  • contracts/test/mocks/BobExecute.s.sol
**/*.sol

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

**/*.sol: Use camelCase for Solidity variables and functions
Use PascalCase for Solidity contracts and structs

Files:

  • contracts/test/mocks/BobExecute.s.sol
🧠 Learnings (33)
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/database/schema.ts : Define relations explicitly in the schema file to enable Drizzle to generate typed query results including related entities

Applied to files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Applies to server/**/*.{ts,tsx} : Server database: drizzle schema is source of truth. Migrations required. No direct database access in handlers - use `c.var.db`

Applied to files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/database/migrations/** : All schema changes must be accompanied by a migration file generated by `drizzle-kit`; never edit the database schema manually

Applied to files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/database/schema.ts : Database schema must be defined in `server/database/schema.ts` as the single source of truth for database structure

Applied to files:

  • server/drizzle.config.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Enforce Drizzle ORM best practices using ESLint `plugin:drizzle/all` configuration, including requiring `where` clauses for `update` and `delete` operations

Applied to files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Use Drizzle ORM query builder for all database interactions; do not write raw SQL queries unless absolutely unavoidable

Applied to files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • server/database/schema.ts
📚 Learning: 2025-12-31T00:23:55.034Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 610
File: .changeset/ready-experts-fly.md:1-2
Timestamp: 2025-12-31T00:23:55.034Z
Learning: In the exactly/exa repository, allow and require empty changeset files (containing only --- separators) when changes are not user-facing and do not warrant a version bump. This is needed because CI runs changeset status --since origin/main and requires a changeset file to exist. Ensure such empty changesets are used only for non-user-facing changes and document the rationale in the commit or changelog notes.

Applied to files:

  • .changeset/rare-readers-tell.md
  • .changeset/red-toys-pump.md
  • .changeset/violet-poems-heal.md
  • .changeset/breezy-planes-repeat.md
📚 Learning: 2025-12-30T15:03:28.449Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/.changeset/*.md : Use a lowercase sentence in the imperative present tense for changeset summaries

Applied to files:

  • .changeset/rare-readers-tell.md
  • .changeset/violet-poems-heal.md
  • .changeset/breezy-planes-repeat.md
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Zero config local dev environment: no `.env` files, mock all external services

Applied to files:

  • server/test/e2e.ts
  • contracts/test/mocks/BobExecute.s.sol
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Use `nx` for running tasks (build, lint, test, e2e) instead of underlying tooling directly

Applied to files:

  • substreams/package.json
  • contracts/package.json
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Applies to {src/generated/**,server/generated/**,contracts/broadcast/**,contracts/cache/**} : Do not manually edit files in generated directories: `src/generated/`, `server/generated/`, `contracts/broadcast/`, and `contracts/cache/`

Applied to files:

  • .dockerignore
  • common/package.json
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Avoid long-running, synchronous operations; use `async/await` correctly and be mindful of CPU-intensive tasks to prevent blocking the event loop

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Do not access `process.env` directly in application code; load all configuration and secrets once at startup and pass them through dependency injection or context

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Server stack: use hono, node.js, drizzle orm, postgres

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Server (exactly/server) uses Hono (framework), Node.js, Drizzle ORM (database), and PostgreSQL

Applied to files:

  • server/test/database.ts
📚 Learning: 2026-01-08T20:26:33.396Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: .vscode/extensions.json:6-6
Timestamp: 2026-01-08T20:26:33.396Z
Learning: For validating VS Code extension IDs in this repository, check both the official VS Code Marketplace (marketplace.visualstudio.com) and the Open VSX registry (open-vsx.org) before flagging an extension as invalid. Open VSX is a widely-used alternative registry, so a valid ID may appear in either registry. If an ID is not present in both registries, flag it as invalid. Apply this check specifically when validating the IDs listed in .vscode/extensions.json.

Applied to files:

  • .vscode/extensions.json
📚 Learning: 2026-01-08T19:54:10.673Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: substreams/src/lib.rs:54-71
Timestamp: 2026-01-08T19:54:10.673Z
Learning: In substreams/src/lib.rs in the map_exa_plugins function, the deduplication logic using `!seen.insert(address.to_vec())` is intentional. The first plugin installed for a new account is an owner plugin (not an exa plugin), so it must be filtered out. The negation ensures the first plugin is skipped and subsequent plugins (the actual exa plugins) are kept.

Applied to files:

  • substreams/src/lib.rs
📚 Learning: 2026-01-08T19:45:38.852Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: .github/workflows/substreams-build.yaml:2-2
Timestamp: 2026-01-08T19:45:38.852Z
Learning: In the exactly/exa repository, prefer concise inline YAML syntax (e.g., `on: { push: { tags: ["pattern"] }, workflow_dispatch: {} }`) over expanded multi-line format in GitHub workflow files, even if it triggers YAMLlint warnings about formatting.

Applied to files:

  • .github/workflows/substreams-build.yaml
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.sol : All code must pass pnpm --filter plugin test:slither and pnpm --filter plugin test:solhint with zero warnings; do not ignore warnings

Applied to files:

  • contracts/package.json
📚 Learning: 2026-01-08T19:46:37.829Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: contracts/test/mocks/BobExecute.s.sol:61-71
Timestamp: 2026-01-08T19:46:37.829Z
Learning: Natspec documentation requirements do not apply to test/mock/dev scripts (files in contracts/test/ or with .s.sol extension). Only production contract code in contracts/src/ requires complete natspec comments.

Applied to files:

  • contracts/package.json
  • contracts/test/mocks/BobExecute.s.sol
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Run contract tests using `pnpm --filter plugin test` and check formatting using `pnpm --filter plugin test:fmt`

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Applies to contracts/**/*.test.sol : Smart contracts testing: fuzzing required for inputs and gas snapshots mandatory

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.t.sol : Use fuzz testing (testFuzz_...) extensively for functions with numerical or address inputs to cover a wide range of scenarios

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Rely on `pnpm` scripts for all tasks; all setup, testing, and generation must be automated

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Run all tests using `pnpm test`, which includes TypeScript compilation, ESLint, spell checking, markdown linting, and contract tests

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.t.sol : Test files must end with .t.sol and mirror the name of the contract they test (e.g., ExaPlugin.sol -> ExaPlugin.t.sol)

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-30T15:03:28.449Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Do not use abbreviations or cryptic names; write out full words like `error`, `parameters`, `request` instead of `err`, `params`, `req`

Applied to files:

  • server/script/firehose.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Omit meaningless words like 'data', 'state', 'manager', 'engine', 'value' from variable and function names unless they add disambiguation

Applied to files:

  • server/script/firehose.ts
  • cspell.json
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.sol : All public and external functions and state variables must have complete natspec comments (notice, dev, param, return)

Applied to files:

  • contracts/test/mocks/BobExecute.s.sol
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.t.sol : Use vm.prank to simulate calls from different addresses; avoid changing msg.sender through other means

Applied to files:

  • contracts/test/mocks/BobExecute.s.sol
📚 Learning: 2025-12-30T15:03:28.449Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Omit meaningless words from names like `data`, `state`, `manager`, `engine`, `object`, `entity`, and `instance`

Applied to files:

  • cspell.json
📚 Learning: 2026-01-08T19:54:26.885Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: substreams/rust-toolchain.toml:1-4
Timestamp: 2026-01-08T19:54:26.885Z
Learning: Rust edition 2024 was released as stable on February 20, 2025 with Rust 1.85.0 and does not require nightly channel.

Applied to files:

  • substreams/rust-toolchain.toml
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.sol : Use custom errors instead of require statements with string messages for better gas efficiency and error information

Applied to files:

  • substreams/build.rs
🧬 Code graph analysis (1)
server/test/database.ts (1)
server/database/schema.ts (1)
  • substreams (70-70)
🪛 Buf (1.62.1)
substreams/proto/exa.proto

3-3: Files with package "exa" must be within a directory "exa" relative to root but were in directory "substreams/proto".

(PACKAGE_DIRECTORY_MATCH)

🪛 Checkov (3.2.334)
substreams/Dockerfile

[low] 1-33: Ensure that HEALTHCHECK instructions have been added to container images

(CKV_DOCKER_2)

🪛 YAMLlint (1.37.1)
.github/workflows/substreams-build.yaml

[warning] 2-2: truthy value should be one of [false, true]

(truthy)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 6-6: too many spaces inside braces

(braces)


[error] 6-6: too many spaces inside braces

(braces)


[error] 11-11: too many spaces inside braces

(braces)


[error] 11-11: too many spaces inside braces

(braces)


[error] 25-25: too many spaces inside braces

(braces)


[error] 25-25: too many spaces inside braces

(braces)

🔇 Additional comments (45)
substreams/.cargo/config.toml (1)

1-2: LGTM! Standard WASM build configuration.

The build target configuration is correct and aligns with the wasm32-unknown-unknown target specified in rust-toolchain.toml.

pnpm-workspace.yaml (1)

7-7: LGTM! Correctly adds substreams to workspace.

The addition of the substreams package to the workspace is properly configured and consistent with the project structure.

substreams/.gitignore (1)

1-8: LGTM!

The ignore patterns correctly exclude generated artifacts (codegen outputs, build directories, package files, and logs) from version control.

server/test/anvil.ts (1)

159-160: LGTM!

The test case selector correctly targets the specific BobExecuteScript test case, aligning with the related contract changes mentioned in the AI summary.

cspell.json (2)

6-11: LGTM!

The new ignore paths correctly exclude CI configuration, build artifacts, patches, and lockfiles from spell checking.


14-138: LGTM!

The dictionary additions cover legitimate technical terms related to Rust, Protocol Buffers, and Substreams tooling introduced in this PR.

.changeset/red-toys-pump.md (1)

5-5: Consider a more complete sentence for clarity.

While "map exa plugins" is imperative present tense, consider making it a fuller sentence for clarity (e.g., "map exa plugins to database schema" or similar, depending on what the mapping does).

Based on coding guidelines requiring lowercase sentence in imperative present tense.

⛔ Skipped due to learnings
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/.changeset/*.md : Use a lowercase sentence in the imperative present tense for changeset summaries
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/docs.mdc:0-0
Timestamp: 2025-12-23T19:57:07.685Z
Learning: Use clear, concise, and direct writing with imperative verbs (action-oriented language) and consistent terminology that matches the application's UI and codebase
.github/workflows/server-build.yaml (1)

36-37: LGTM! Explicit Dockerfile path improves clarity.

The addition of an explicit file parameter alongside the existing context is a good practice for monorepo setups, making it clear where the Dockerfile resides while maintaining the build context at the repository root.

docs/package.json (1)

38-44: LGTM! Proper nx inputs configuration for incremental builds.

The inputs configuration correctly tracks the generated OpenAPI file as a build input, ensuring nx cache invalidation when the file changes. This complements the existing dependsOn configuration for proper incremental build behavior.

substreams/Cargo.toml (4)

7-9: LGTM! Correct crate type for WASM/FFI targets.

The cdylib crate type is appropriate for building dynamic libraries that can be loaded by non-Rust code, including WASM targets used in substreams.


32-35: LGTM! Appropriate release optimizations for WASM targets.

The release profile is well-configured for WASM/substreams use cases:

  • lto = true enables link-time optimization for smaller binaries
  • opt-level = 's' optimizes for size rather than speed
  • strip = "debuginfo" removes debug symbols to reduce binary size

These settings are ideal for WASM modules where binary size is critical.


1-6: No changes needed—Rust edition "2024" is stable.

Rust edition 2024 was released as stable on February 20, 2025 with Rust 1.85.0 and does not require the nightly channel. The manifest correctly specifies edition = "2024".

Likely an incorrect or invalid review comment.


11-21: Update prost and prost-types to 0.14.1.

Most dependencies are reasonably constrained and current. However, prost and prost-types are pinned to version 0.13, while the latest stable releases are 0.14.1 (June 2025). Upgrade these for the latest improvements and stability. No critical security vulnerabilities exist in the currently specified versions, and the current major-version constraints are appropriate and standard practice.

.vscode/extensions.json (1)

6-6: Extension IDs are valid. Both rust-lang.rust-analyzer and drblury.protobuf-vsc are confirmed in the VS Code Marketplace and Open VSX registry.

server/test/e2e.ts (1)

7-7: LGTM!

The Sentry mock follows the established pattern in this file, correctly preserving the original captureException behavior while adding console logging for test observability. The type-only import is appropriate per coding guidelines.

Also applies to: 58-67

contracts/test/mocks/BobExecute.s.sol (1)

61-71: LGTM!

The BatchDeploy contract efficiently batches account creation for testing purposes. The sequential address generation starting at 420 is clear and the loop bounds are safe. Based on learnings, natspec is not required for test/mock contracts.

common/package.json (1)

44-50: LGTM!

The new dependentTasksOutputFiles entries correctly wire contract build outputs into the wagmi code generation pipeline, ensuring regeneration when contract artifacts change.

server/test/database.ts (2)

49-70: LGTM!

The process orchestration is well-structured:

  • AbortController enables coordinated shutdown across firehose and substreams processes.
  • Error suppression when aborted prevents noisy failures during teardown.
  • forceKillAfterDelay ensures processes are terminated even if they hang.
  • The substreams schema name aligns with the schema definition in server/database/schema.ts.

90-93: LGTM!

Using Promise.allSettled for teardown ensures all cleanup operations complete regardless of individual failures.

substreams/Dockerfile (1)

1-18: LGTM!

The build stage is well-structured:

  • Uses pipefail for safer bash execution.
  • Consolidates apt-get operations to reduce layers.
  • Pins specific versions for reproducibility (pnpm 9.15.9, foundry v1.3.6).
  • Uses build cache for pnpm store.
  • Runs as non-root in runtime stage for security.
.github/workflows/substreams-build.yaml (1)

1-38: LGTM!

The workflow is well-structured for Docker image publishing:

  • Proper permissions for GHCR push and attestations.
  • Multiple tag patterns for flexible version referencing.
  • Build provenance attestation enhances supply chain security.
  • Explicit Dockerfile path aligns with the new substreams/Dockerfile.

The inline YAML syntax is consistent with repository conventions (per learnings).

.dockerignore (2)

6-18: LGTM!

The granular ignore patterns correctly exclude:

  • Broadcast artifacts except run-latest.json (negation pattern works correctly).
  • Local chain (31337) and dry-run broadcasts.
  • Generated directories (contracts/out, substreams/target, substreams/src/contracts, substreams/src/proto).

This reduces Docker build context size without losing necessary artifacts.


21-25: LGTM!

Excluding *.md, *.log, *.tsbuildinfo, expo-env.d.ts, and lcov.info appropriately keeps documentation, logs, and TypeScript build metadata out of the Docker context.

.github/matcher.json (1)

64-92: LGTM! Well-structured problem matchers for Rust tooling.

The regex patterns correctly handle ANSI escape sequences and capture the relevant groups for GitHub Actions problem matching. The two-pattern approach for the "rust" matcher properly handles both the error/warning message line and the subsequent file location line, which aligns with Rust compiler output format.

contracts/package.json (1)

60-101: LGTM! Well-structured test dependency chain.

The refactored nx targets establish a clear dependency chain (test:sizes → test:gas → test:slither → test:coverage) that ensures proper build artifacts are available for each subsequent test phase. The explicit output declarations improve caching behavior over the previous implicit cache configuration.

substreams/rustfmt.toml (1)

1-6: LGTM! Reasonable Rustfmt configuration.

The configuration choices are appropriate for the project:

  • 2-space indentation aligns with the TypeScript/JavaScript formatting
  • 120 character line width provides good balance between readability and compactness
  • Crate-level import granularity and field init shorthand are idiomatic Rust practices

Ensure CI workflows run cargo fmt --check to enforce these formatting rules.

server/package.json (2)

64-64: LGTM!

The workspace dependency addition correctly integrates the new substreams package into the server's development toolchain.


142-146: LGTM!

The new NX inputs correctly model the dependencies for broadcast generation:

  • The CHAIN_ID environment variable ensures cache invalidation when targeting different chains
  • The contract outputs dependency ensures broadcasts regenerate when contracts change
substreams/proto/exa.proto (1)

1-17: LGTM!

The protobuf definitions are well-structured:

  • Appropriate use of bytes for Ethereum addresses
  • Clean nested message structure for Plugin
  • Proper field numbering and types
substreams/package.json (4)

1-11: LGTM!

The package metadata and scripts are well-configured:

  • Build uses appropriate cargo flags
  • Test suite correctly uses nx (per learnings) and includes Rust quality checks (clippy, fmt) and proto validation
  • The --deny warnings flag in clippy ensures strict code quality

12-16: LGTM!

The devDependencies correctly include tooling for protobuf generation, contract integration, and linting.


20-43: LGTM!

The NX build target is properly configured:

  • Comprehensive input tracking ensures correct cache invalidation
  • Dependencies on rustup and upstream builds are appropriate
  • WASM output path matches standard Rust target structure for wasm32-unknown-unknown

44-64: LGTM!

The NX test target configuration properly orchestrates the test suite:

  • Aggregates quality checks (clippy, fmt, protolint) through a noop executor
  • Ensures proper setup with prepare and rustup dependencies before running checks
substreams/substreams.yaml (4)

1-13: LGTM!

The manifest header and protobuf configuration are correctly structured:

  • External imports use pinned release versions for reproducibility
  • Protobuf configuration properly references the proto file and import paths
  • Default network set to anvil is appropriate for development

14-42: LGTM!

The module pipeline is well-structured:

  • Clear data flow: accounts → store → plugins → database changes
  • Appropriate module types (map for transformations, store for state)
  • Correct input/output type references to defined proto messages

43-52: LGTM!

The sink and binaries configuration correctly integrates with the server:

  • Sink references the server's generated SQL schema, ensuring consistency
  • Postgres engine matches the server's database
  • Binary path aligns with the package.json build output

78-82: LGTM!

The anvil network configuration is appropriate for local development with initialBlock: 0 and a single factory address.

substreams/build.rs (2)

1-23: LGTM!

The imports and rerun triggers are properly configured:

  • Comprehensive rerun-if-changed directives ensure correct cache invalidation
  • Contract definitions clearly map module names to contract types
  • Per-contract rerun triggers correctly track ABI changes

38-48: LGTM!

The module file generation correctly:

  • Exports all contract modules
  • Suppresses clippy warnings appropriate for generated code
  • Uses idiomatic Rust string formatting
substreams/src/lib.rs (3)

46-51: LGTM!

The store function correctly persists account flags for later lookup in map_exa_plugins.


53-71: This is an intentional and appropriate pattern, not a concern.

The ? operator inside filter_map doesn't represent problematic silent error handling—it's the standard Rust idiom for filtering. Not all logs are PluginInstalled events, and match_and_decode returning None for non-matching logs is expected behavior. Similarly, store lookups may fail for addresses not in the store, which is also expected in the filtering context. The handler correctly returns Ok(Plugins { ... }) with the successfully filtered results, making this pattern appropriate for Ethereum log filtering where heterogeneous logs and partial datasets are normal.

Likely an incorrect or invalid review comment.


73-88: Confirm timestamp handling and blocks row creation strategy.

The code creates blocks rows only when plugins are detected in a block and defaults missing timestamps to 0:

  1. Line 85: unwrap_or_default() will write timestamp 0 to the database if clock.timestamp is None. The schema requires timestamp NOT NULL, so a missing timestamp becomes a zero value—verify this doesn't impact downstream queries that assume valid timestamps.

  2. Lines 82–86: Blocks without detected plugins won't have a row created, effectively skipping empty blocks. Confirm this aligns with your indexing strategy (the CHANGELOG notes "skip empty alchemy blocks," suggesting this is intentional).

server/database/schema.ts (3)

3-17: LGTM!

Import additions support the new substreams schema definitions below.


36-36: LGTM!

Destructuring pattern improves consistency with other table definitions.


79-86: Consider whether nullable fields in substreams_history are appropriate.

All fields except id are nullable. Verify this aligns with the substreams history tracking requirements, as missing operation type (op), table name, or block number could make history rows difficult to interpret.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/test/database.ts (1)

39-47: Restore process.stdout.write via try/finally to avoid leaking global state on failures.
If pushSchema(...) / waitOn(...) rejects, Line 47 never runs and the test process can remain permanently muted.

Proposed fix
   const postgresURL = "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"; // cspell:ignore sslmode
   const stdoutWrite = process.stdout.write; // eslint-disable-line @typescript-eslint/unbound-method
   const database = drizzle(postgresURL, { schema });
-  process.stdout.write = () => true;
-  await Promise.all([
-    pushSchema(schema, database as never).then(({ apply }) => apply()),
-    waitOn({ resources: ["tcp:localhost:8545"], timeout: 33_333 }),
-    rm("node_modules/@exactly/.firehose", { recursive: true, force: true }),
-  ]);
-  process.stdout.write = stdoutWrite;
+  try {
+    process.stdout.write = () => true;
+    await Promise.all([
+      pushSchema(schema, database as never).then(({ apply }) => apply()),
+      waitOn({ resources: ["tcp:localhost:8545"], timeout: 33_333 }),
+      rm("node_modules/@exactly/.firehose", { recursive: true, force: true }),
+    ]);
+  } finally {
+    process.stdout.write = stdoutWrite;
+  }
🤖 Fix all issues with AI agents
In @.changeset/breezy-planes-repeat.md:
- Line 5: Update the changeset summary to remove the emoji prefix and ensure it
is a lowercase imperative present-tense sentence; replace the current line "✨
map exa accounts" with the plain text summary "map exa accounts" (no emoji,
punctuation, or capitalization).

In @.changeset/rare-readers-tell.md:
- Around line 1-5: Update the changeset summary string "✨ store exa accounts" to
remove the leading emoji and ensure it is a lowercase sentence (e.g., change it
to "store exa accounts") so the entry for "@exactly/substreams" complies with
changeset summary style.

In @.changeset/red-toys-pump.md:
- Around line 1-5: Remove the leading emoji from the changeset summary so it
complies with style: replace the summary line "✨ map exa plugins" with "map exa
plugins" (no emoji) in the .changeset/red-toys-pump.md file and keep the rest of
the changeset metadata unchanged.

In @.changeset/violet-poems-heal.md:
- Line 5: Edit the changeset summary line that currently reads "✨ insert exa
plugins into database" and remove the emoji prefix so the summary is plain text
and in imperative present tense: "insert exa plugins into database"; ensure no
other gitmojis or extra punctuation remain in the changeset header.

In @.dockerignore:
- Around line 6-9: The negation for "!**/contracts/broadcast/**/run-latest.json"
is ineffective because the later pattern "**/contracts/broadcast/*/31337"
excludes the parent directory containing run-latest.json; remove or adjust the
directory-ignore pattern so the negation can take effect — specifically delete
or comment out the "**/contracts/broadcast/*/31337" entry (or narrow it to not
exclude the 31337 directory that holds run-latest.json) and keep the JSON-level
ignore "**/contracts/broadcast/**/*.json" plus the explicit include
"!**/contracts/broadcast/**/run-latest.json" so run-latest.json is included in
the Docker context.

In @.github/workflows/docs.yaml:
- Line 14: Replace the single-line inline mapping "env: { NX_DAEMON: false }"
with a linter-friendly format; either remove the inner spaces to "env:
{NX_DAEMON: false}" or, preferably, expand to a multi-line mapping: "env:"
followed by an indented "NX_DAEMON: false" to satisfy YAML linting.

In @.github/workflows/test.yaml:
- Around line 38-42: The step that downloads and extracts binaries into
$HOME/.local/bin doesn't export that directory to the PATH, so later steps can't
find the executables; after the loop that extracts into $HOME/.local/bin, append
that directory to the GitHub Actions PATH by adding a command like: echo
"$HOME/.local/bin" >> $GITHUB_PATH (or export PATH="$HOME/.local/bin:$PATH"
within the same run), ensuring the change is persisted for subsequent workflow
steps that need the downloaded binaries.

In @server/script/firehose.ts:
- Around line 59-64: The readBytes() method advances offset by the decoded
length without verifying bounds, risking reading past buffer; add a bounds check
like skip() does: compute length from readVarint(), ensure this.offset + length
<= this.buffer.length and throw an error (or handle) if not, then proceed to
slice/subarray and advance this.offset; update readBytes() (and mirror the error
type/message used by skip()) to prevent out-of-range reads.
- Around line 100-104: The loop that parses the timestamp tags uses
tsCursor.readVarint() for fieldNumber 1 and 2 without verifying the protobuf
wire type; update the logic in the loop that calls tsCursor.readTag() so that
before calling tsCursor.readVarint() you check tag.wireType === 0 (varint) for
fields 1 (seconds) and 2 (nanoseconds) and only call readVarint() when true,
otherwise call tsCursor.skip(tag.wireType) to safely skip malformed or
unexpected wire types; keep the existing assignments to seconds and nanoseconds
unchanged.
- Around line 130-131: The local variable `ts` is an abbreviated name; replace
it with a descriptive name (e.g., `extractedTimestamp` or `rawTimestamp`) where
it is declared from extractTimestamp(buffer) and in the ternary that sets
`timestamp` so the code becomes: const extractedTimestamp =
extractTimestamp(buffer); const timestamp = extractedTimestamp === null ? "0" :
extractedTimestamp.toString(); update any subsequent references to `ts` to use
the new name.

In @substreams/buf.gen.yaml:
- Around line 1-8: Check the Buf plugin version pins for neoeinstein-prost and
neoeinstein-prost-crate (the remote entries specifying v0.5.0); confirm those
v0.5.0 releases actually exist in the registry and are accessible, and if they
do not, update the versions to the latest published stable releases (e.g.,
neoeinstein-prost -> v0.4.0 and neoeinstein-prost-crate -> v0.3.1) in the
plugins list so the buf.gen.yaml uses valid, available plugin versions.

In @substreams/build.rs:
- Line 55: The current build.rs line uses canonicalize("node_modules/.bin")?
which will crash if that path doesn't exist; update the PATH setup in build.rs
to check for the node_modules/.bin path first (e.g., use
Path::new("node_modules/.bin").exists() or attempt canonicalize in a match), and
if it exists use its canonicalized display value, otherwise fall back to either
the literal "node_modules/.bin" or omit prepending to PATH and log/return a
clearer error message; update the expression around .env("PATH", ...) so it does
not use the ? operator on canonicalize directly and handles the
missing-directory case gracefully.
- Line 63: The current
.args(glob("src/contracts/**/*.rs")?.chain(glob("src/proto/**/*.rs")?).filter_map(Result::ok))
silently drops IO/errors from the glob iterator; instead collect the chained
iterator of Result<Path, _> into a Result<Vec<PathBuf>, _> and propagate any
error with ? (e.g. let paths: Vec<_> =
glob("src/contracts/**/*.rs")?.chain(glob("src/proto/**/*.rs")?).collect::<Result<Vec<_>,
_>>()?; then pass paths to .args(paths) or .args(paths.iter())). Replace the
filter_map(Result::ok) usage so glob failures are returned rather than ignored.
- Around line 28-31: The code currently indexes the parsed JSON with ["abi"]
which will panic if the "abi" field is missing; update the block that calls
from_reader::<_, Value>(File::open(&path)...) and then
to_writer(File::create(...), &...["abi"]) to first parse into a Value, call
.get("abi") on that Value, and if .get("abi") returns None return a descriptive
error (via anyhow::bail! or .with_context) referencing path/mod_name; then pass
the borrowed abi Value into to_writer so missing or malformed "abi" is handled
gracefully instead of panicking.

In @substreams/Dockerfile:
- Around line 32-33: The Dockerfile currently hardcodes a development-only CMD
with a Postgres URL using host.docker.internal and may leak credentials; replace
the hardcoded CMD with one of two safe options: either remove the CMD entirely
so ENTRYPOINT ["dumb-init","/app/substreams-sink-sql","run"] requires explicit
runtime args, or change CMD to use an environment variable (e.g. CMD
["${DATABASE_URL:-}","substreams.yaml"]) and document that DATABASE_URL must be
set; ensure you remove any literal host.docker.internal/credentials from the
file and update docs to instruct running with DATABASE_URL set (and avoid
printing secrets in logs).

In @substreams/package.json:
- Around line 59-63: The test:fmt npm script currently lists test:clippy in its
dependsOn array, which creates an unnecessary serial dependency; open
package.json, locate the "test:fmt" script object and remove "test:clippy" from
its "dependsOn" array so formatting checks run independently of clippy, leaving
"test:clippy" defined separately if needed and ensuring any CI/workflow that
relied on serial ordering is updated to invoke both scripts in parallel or
separately.

In @substreams/proto/exa.proto:
- Around line 1-3: Buf is warning that proto package "exa" must live in a
matching directory; either move the file to a directory that matches the package
(e.g., put exa.proto under proto/exa/ so package exa lives in
proto/exa/exa.proto) or disable the PACKAGE_DIRECTORY_MATCH lint for this repo
by adding a substreams/buf.yaml that excludes PACKAGE_DIRECTORY_MATCH; update
the repository accordingly and commit the file move or the buf.yaml change so
the buf lint no longer reports PACKAGE_DIRECTORY_MATCH for package "exa".

In @substreams/substreams.yaml:
- Line 47: The sink references a generated schema file that is missing: update
or generate the referenced file "../server/generated/schema.sql" by running the
server-side export script; cd into the server directory and run the npm script
"generate:sql" with POSTGRES_URL pointing to your Postgres instance (e.g.,
POSTGRES_URL=postgres npm run generate:sql) to export the schema defined in
server/database/schema.ts using Drizzle Kit, then commit the produced
generated/schema.sql so the "schema: ../server/generated/schema.sql" entry is
valid before deployment.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a0ef2f4 and 07a1be3.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • substreams/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (39)
  • .changeset/breezy-planes-repeat.md
  • .changeset/rare-readers-tell.md
  • .changeset/red-toys-pump.md
  • .changeset/violet-poems-heal.md
  • .dockerignore
  • .github/matcher.json
  • .github/workflows/docs.yaml
  • .github/workflows/mobile-expo.yaml
  • .github/workflows/server-build.yaml
  • .github/workflows/substreams-build.yaml
  • .github/workflows/test.yaml
  • .github/workflows/version.yaml
  • .vscode/extensions.json
  • common/package.json
  • contracts/package.json
  • contracts/test/mocks/BobExecute.s.sol
  • cspell.json
  • docs/package.json
  • pnpm-workspace.yaml
  • server/Dockerfile
  • server/database/schema.ts
  • server/drizzle.config.ts
  • server/package.json
  • server/script/firehose.ts
  • server/test/anvil.ts
  • server/test/database.ts
  • server/test/e2e.ts
  • substreams/.cargo/config.toml
  • substreams/.gitignore
  • substreams/Cargo.toml
  • substreams/Dockerfile
  • substreams/buf.gen.yaml
  • substreams/build.rs
  • substreams/package.json
  • substreams/proto/exa.proto
  • substreams/rust-toolchain.toml
  • substreams/rustfmt.toml
  • substreams/src/lib.rs
  • substreams/substreams.yaml
🧰 Additional context used
📓 Path-based instructions (11)
**/.changeset/*.md

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

Use a lowercase sentence in the imperative present tense for changeset summaries

Files:

  • .changeset/rare-readers-tell.md
  • .changeset/violet-poems-heal.md
  • .changeset/breezy-planes-repeat.md
  • .changeset/red-toys-pump.md
server/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/server.mdc)

server/**/*.ts: Use c.var object to pass strongly-typed data between Hono middleware and route handlers; do not use c.set
All request validation (headers, body, params) must be handled by @hono/valibot-validator middleware; do not perform manual validation inside route handlers
Use Hono's built-in error handling by throwing new HTTPException() for expected errors; unhandled errors will be caught and logged automatically
Enforce Node.js best practices using ESLint plugin:n/recommended configuration
Enforce Drizzle ORM best practices using ESLint plugin:drizzle/all configuration, including requiring where clauses for update and delete operations
Use Drizzle ORM query builder for all database interactions; do not write raw SQL queries unless absolutely unavoidable
All authentication and authorization logic must be implemented in Hono middleware
Do not access process.env directly in application code; load all configuration and secrets once at startup and pass them through dependency injection or context
Avoid long-running, synchronous operations; use async/await correctly and be mindful of CPU-intensive tasks to prevent blocking the event loop

Files:

  • server/test/database.ts
  • server/test/anvil.ts
  • server/drizzle.config.ts
  • server/test/e2e.ts
  • server/database/schema.ts
  • server/script/firehose.ts
**/*.{js,ts,tsx,jsx,sol}

📄 CodeRabbit inference engine (AGENTS.md)

Follow linter/formatter (eslint, prettier, solhint) strictly with high strictness level. No any type.

Files:

  • server/test/database.ts
  • contracts/test/mocks/BobExecute.s.sol
  • server/test/anvil.ts
  • server/drizzle.config.ts
  • server/test/e2e.ts
  • server/database/schema.ts
  • server/script/firehose.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Omit redundant type names in variable declarations - let the type system explain itself

**/*.{ts,tsx}: Use PascalCase for TypeScript types and interfaces
Use valibot for all runtime validation of API inputs, environment variables, and other data; define schemas once and reuse them
Infer TypeScript types from valibot schemas using type User = v.Input<typeof UserSchema> instead of manually defining interfaces

Files:

  • server/test/database.ts
  • server/test/anvil.ts
  • server/drizzle.config.ts
  • server/test/e2e.ts
  • server/database/schema.ts
  • server/script/firehose.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Omit contextual names - don't repeat class/module names in members
Omit meaningless words like 'data', 'state', 'manager', 'engine', 'value' from variable and function names unless they add disambiguation

**/*.{ts,tsx,js,jsx}: Prefer function declarations for all multi-line functions; use function expressions or arrow functions only for single-line implementations
Prefer const for all variable declarations by default; only use let if the variable's value will be reassigned
Declare each variable on its own line with its own const or let keyword, not multiple declarations on one line
Use camelCase for TypeScript variables and functions
Always use import type { ... } for type imports
Use relative paths for all imports within the project; avoid tsconfig path aliases
Follow eslint-plugin-import order: react, external libraries, then relative paths
Use object and array destructuring to access and use properties
Use object method shorthand syntax when a function is a property of an object
Prefer optional chaining (?.), nullish coalescing (??), object and array spreading (...), and for...of loops over traditional syntax
Do not use abbreviations or cryptic names; write out full words like error, parameters, request instead of err, params, req
Use Number.parseInt() instead of the global parseInt() function when parsing numbers
All classes called with new must use PascalCase
Use Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe() instead of the deprecated new Buffer()
Use @ts-expect-error instead of @ts-ignore; follow it immediately with a single-line lowercase comment explaining why the error is expected, without separators like - or :
Do not include the type in a variable's name; let the static type system do its job (e.g., use const user: User not const userObject: User)
Do not repeat the name of a class or module within its members; omit contextual names (e.g., use `class User { getProfil...

Files:

  • server/test/database.ts
  • server/test/anvil.ts
  • server/drizzle.config.ts
  • server/test/e2e.ts
  • server/database/schema.ts
  • server/script/firehose.ts
server/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

server/**/*.{ts,tsx}: Server API: implement schema-first approach using OpenAPI via hono with validation via valibot middleware
Server database: drizzle schema is source of truth. Migrations required. No direct database access in handlers - use c.var.db

Files:

  • server/test/database.ts
  • server/test/anvil.ts
  • server/drizzle.config.ts
  • server/test/e2e.ts
  • server/database/schema.ts
  • server/script/firehose.ts
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

For files with a single default export, name the file identically to the export; for files with multiple exports, use camelCase with a strong preference for a single word

Files:

  • server/test/database.ts
  • server/test/anvil.ts
  • server/drizzle.config.ts
  • server/test/e2e.ts
  • server/database/schema.ts
  • server/script/firehose.ts
**/+(app|src)/**

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

Use kebab-case for directory names and public Expo router routes

Files:

  • substreams/src/lib.rs
contracts/**/*.sol

📄 CodeRabbit inference engine (.cursor/rules/contracts.mdc)

contracts/**/*.sol: Import order must be sorted alphabetically and grouped by type (openzeppelin, solady, internal, etc.), enforced by forge fmt
All functions and state variables must have explicit visibility modifiers (public, private, internal, external)
Use custom errors instead of require() statements with string messages; require() with messages is forbidden
console.log is forbidden in contract code
All external and public functions that modify state must be protected by a reentrancy guard, unless explicitly proven unnecessary
Use modifiers like onlyOwner or function-level checks for privileged functions; default to strictest access level (private > internal > external > public)
Use named parameters in mappings for clarity (e.g., mapping(address user => uint256 balance))
All code must pass pnpm --filter plugin test:slither and pnpm --filter plugin test:solhint with zero warnings; do not ignore warnings
Follow standard Solidity import order: external libraries (openzeppelin, solady) first, then project-internal contracts
All public and external functions and state variables must have complete natspec comments (@notice, @dev, @param, @return)
Use custom errors instead of require statements with string messages for better gas efficiency and error information
Internal and private variables should be prefixed with an underscore (_); function arguments should not be prefixed
Use constant keyword for true constants; use immutable for configurable values like contract addresses
Minimize writes to storage; read data into memory or calldata whenever possible for gas optimization
Use smallest integer size that is safe for the variable (e.g., uint32 for timestamps) and be aware of struct packing for gas efficiency
Prefer using the solady library over openzeppelin where possible, as it is highly optimized for gas
Correctly label functions as view or pure when they do not modify state

contracts/**/*.sol: Smart contracts: use custom errors error MyError(); instead of s...

Files:

  • contracts/test/mocks/BobExecute.s.sol
**/*.sol

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

**/*.sol: Use camelCase for Solidity variables and functions
Use PascalCase for Solidity contracts and structs

Files:

  • contracts/test/mocks/BobExecute.s.sol
server/database/schema.ts

📄 CodeRabbit inference engine (.cursor/rules/server.mdc)

server/database/schema.ts: Database schema must be defined in server/database/schema.ts as the single source of truth for database structure
Define relations explicitly in the schema file to enable Drizzle to generate typed query results including related entities

Files:

  • server/database/schema.ts
🧠 Learnings (35)
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Use `nx` for running tasks (build, lint, test, e2e) instead of underlying tooling directly

Applied to files:

  • .github/workflows/docs.yaml
  • substreams/package.json
  • .github/workflows/mobile-expo.yaml
  • .github/workflows/version.yaml
  • contracts/package.json
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Install dependencies using `pnpm install`

Applied to files:

  • .github/workflows/docs.yaml
  • .github/workflows/version.yaml
📚 Learning: 2025-12-31T00:23:55.034Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 610
File: .changeset/ready-experts-fly.md:1-2
Timestamp: 2025-12-31T00:23:55.034Z
Learning: In the exactly/exa repository, allow and require empty changeset files (containing only --- separators) when changes are not user-facing and do not warrant a version bump. This is needed because CI runs changeset status --since origin/main and requires a changeset file to exist. Ensure such empty changesets are used only for non-user-facing changes and document the rationale in the commit or changelog notes.

Applied to files:

  • .changeset/rare-readers-tell.md
  • .changeset/violet-poems-heal.md
  • .changeset/breezy-planes-repeat.md
  • .changeset/red-toys-pump.md
📚 Learning: 2025-12-30T15:03:28.449Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/.changeset/*.md : Use a lowercase sentence in the imperative present tense for changeset summaries

Applied to files:

  • .changeset/rare-readers-tell.md
  • .changeset/violet-poems-heal.md
  • .changeset/breezy-planes-repeat.md
📚 Learning: 2025-12-23T19:57:14.465Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/git.mdc:0-0
Timestamp: 2025-12-23T19:57:14.465Z
Learning: Select the most precise gitmoji that best communicates the intent of the change (e.g., ✨ for new feature, 🐛 for bug fix, ♻️ for refactoring, ⚡️ for performance improvement, 📝 for documentation, 🚀 for deployment)

Applied to files:

  • .changeset/rare-readers-tell.md
  • .changeset/breezy-planes-repeat.md
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Avoid long-running, synchronous operations; use `async/await` correctly and be mindful of CPU-intensive tasks to prevent blocking the event loop

Applied to files:

  • server/test/database.ts
  • server/script/firehose.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Applies to server/**/*.{ts,tsx} : Server database: drizzle schema is source of truth. Migrations required. No direct database access in handlers - use `c.var.db`

Applied to files:

  • server/test/database.ts
  • server/drizzle.config.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Do not access `process.env` directly in application code; load all configuration and secrets once at startup and pass them through dependency injection or context

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Server stack: use hono, node.js, drizzle orm, postgres

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Use Drizzle ORM query builder for all database interactions; do not write raw SQL queries unless absolutely unavoidable

Applied to files:

  • server/test/database.ts
  • server/drizzle.config.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/database/schema.ts : Define relations explicitly in the schema file to enable Drizzle to generate typed query results including related entities

Applied to files:

  • server/test/database.ts
  • server/drizzle.config.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/database/migrations/** : All schema changes must be accompanied by a migration file generated by `drizzle-kit`; never edit the database schema manually

Applied to files:

  • server/test/database.ts
  • server/drizzle.config.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Server (exactly/server) uses Hono (framework), Node.js, Drizzle ORM (database), and PostgreSQL

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Enforce Drizzle ORM best practices using ESLint `plugin:drizzle/all` configuration, including requiring `where` clauses for `update` and `delete` operations

Applied to files:

  • server/test/database.ts
  • server/drizzle.config.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Applies to {src/generated/**,server/generated/**,contracts/broadcast/**,contracts/cache/**} : Do not manually edit files in generated directories: `src/generated/`, `server/generated/`, `contracts/broadcast/`, and `contracts/cache/`

Applied to files:

  • substreams/package.json
  • substreams/.gitignore
  • substreams/build.rs
  • .dockerignore
  • common/package.json
📚 Learning: 2026-01-08T19:54:26.885Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: substreams/rust-toolchain.toml:1-4
Timestamp: 2026-01-08T19:54:26.885Z
Learning: Rust edition 2024 was released as stable on February 20, 2025 with Rust 1.85.0 and does not require nightly channel.

Applied to files:

  • substreams/rust-toolchain.toml
📚 Learning: 2026-01-08T19:54:10.673Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: substreams/src/lib.rs:54-71
Timestamp: 2026-01-08T19:54:10.673Z
Learning: In substreams/src/lib.rs in the map_exa_plugins function, the deduplication logic using `!seen.insert(address.to_vec())` is intentional. The first plugin installed for a new account is an owner plugin (not an exa plugin), so it must be filtered out. The negation ensures the first plugin is skipped and subsequent plugins (the actual exa plugins) are kept.

Applied to files:

  • substreams/src/lib.rs
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.sol : All public and external functions and state variables must have complete natspec comments (notice, dev, param, return)

Applied to files:

  • contracts/test/mocks/BobExecute.s.sol
📚 Learning: 2026-01-08T19:46:37.829Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: contracts/test/mocks/BobExecute.s.sol:61-71
Timestamp: 2026-01-08T19:46:37.829Z
Learning: Natspec documentation requirements do not apply to test/mock/dev scripts (files in contracts/test/ or with .s.sol extension). Only production contract code in contracts/src/ requires complete natspec comments.

Applied to files:

  • contracts/test/mocks/BobExecute.s.sol
  • contracts/package.json
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Zero config local dev environment: no `.env` files, mock all external services

Applied to files:

  • contracts/test/mocks/BobExecute.s.sol
  • server/test/e2e.ts
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.t.sol : Use vm.prank to simulate calls from different addresses; avoid changing msg.sender through other means

Applied to files:

  • contracts/test/mocks/BobExecute.s.sol
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/database/schema.ts : Database schema must be defined in `server/database/schema.ts` as the single source of truth for database structure

Applied to files:

  • server/drizzle.config.ts
  • server/database/schema.ts
📚 Learning: 2026-01-08T20:26:33.396Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: .vscode/extensions.json:6-6
Timestamp: 2026-01-08T20:26:33.396Z
Learning: For validating VS Code extension IDs in this repository, check both the official VS Code Marketplace (marketplace.visualstudio.com) and the Open VSX registry (open-vsx.org) before flagging an extension as invalid. Open VSX is a widely-used alternative registry, so a valid ID may appear in either registry. If an ID is not present in both registries, flag it as invalid. Apply this check specifically when validating the IDs listed in .vscode/extensions.json.

Applied to files:

  • .vscode/extensions.json
📚 Learning: 2025-12-30T15:03:28.449Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Omit meaningless words from names like `data`, `state`, `manager`, `engine`, `object`, `entity`, and `instance`

Applied to files:

  • cspell.json
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Omit meaningless words like 'data', 'state', 'manager', 'engine', 'value' from variable and function names unless they add disambiguation

Applied to files:

  • cspell.json
📚 Learning: 2026-01-08T19:45:38.852Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: .github/workflows/substreams-build.yaml:2-2
Timestamp: 2026-01-08T19:45:38.852Z
Learning: In the exactly/exa repository, prefer concise inline YAML syntax (e.g., `on: { push: { tags: ["pattern"] }, workflow_dispatch: {} }`) over expanded multi-line format in GitHub workflow files, even if it triggers YAMLlint warnings about formatting.

Applied to files:

  • .github/workflows/substreams-build.yaml
📚 Learning: 2025-12-30T15:03:28.449Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Do not use abbreviations or cryptic names; write out full words like `error`, `parameters`, `request` instead of `err`, `params`, `req`

Applied to files:

  • server/script/firehose.ts
📚 Learning: 2025-12-23T19:57:14.465Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/git.mdc:0-0
Timestamp: 2025-12-23T19:57:14.465Z
Learning: All commits must start with a single gitmoji from the official gitmoji list; this is not optional and the emoji visually communicates the intent of the change

Applied to files:

  • .changeset/breezy-planes-repeat.md
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.sol : Use custom errors instead of require statements with string messages for better gas efficiency and error information

Applied to files:

  • substreams/build.rs
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.sol : All code must pass pnpm --filter plugin test:slither and pnpm --filter plugin test:solhint with zero warnings; do not ignore warnings

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Run contract tests using `pnpm --filter plugin test` and check formatting using `pnpm --filter plugin test:fmt`

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Run all tests using `pnpm test`, which includes TypeScript compilation, ESLint, spell checking, markdown linting, and contract tests

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Applies to contracts/**/*.test.sol : Smart contracts testing: fuzzing required for inputs and gas snapshots mandatory

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.t.sol : Use fuzz testing (testFuzz_...) extensively for functions with numerical or address inputs to cover a wide range of scenarios

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Rely on `pnpm` scripts for all tasks; all setup, testing, and generation must be automated

Applied to files:

  • contracts/package.json
🪛 Buf (1.62.1)
substreams/proto/exa.proto

3-3: Files with package "exa" must be within a directory "exa" relative to root but were in directory "substreams/proto".

(PACKAGE_DIRECTORY_MATCH)

🪛 Checkov (3.2.334)
substreams/Dockerfile

[low] 1-33: Ensure that HEALTHCHECK instructions have been added to container images

(CKV_DOCKER_2)

🪛 YAMLlint (1.37.1)
.github/workflows/docs.yaml

[error] 14-14: too many spaces inside braces

(braces)


[error] 14-14: too many spaces inside braces

(braces)

.github/workflows/substreams-build.yaml

[warning] 2-2: truthy value should be one of [false, true]

(truthy)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 6-6: too many spaces inside braces

(braces)


[error] 6-6: too many spaces inside braces

(braces)


[error] 11-11: too many spaces inside braces

(braces)


[error] 11-11: too many spaces inside braces

(braces)


[error] 25-25: too many spaces inside braces

(braces)


[error] 25-25: too many spaces inside braces

(braces)

⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Seer Code Review
🔇 Additional comments (46)
contracts/test/mocks/BobExecute.s.sol (3)

39-39: LGTM: Correct use of getAddress in setUp.

Switching from createAccount to getAddress is appropriate here since the account is created by the Bob.s.sol script (executed earlier in the test setup). This correctly computes the deterministic address without attempting to redeploy.


47-58: LGTM: BatchDeploy instantiation correctly generates test accounts.

The BatchDeploy instantiation creates multiple accounts for testing purposes, likely to populate blockchain data for the new substreams functionality. While using constructor side effects is unconventional, it's acceptable in test scripts.


61-71: LGTM: BatchDeploy contract correctly implements batch account creation.

The constructor creates count accounts with sequential owner addresses. The pattern of using a constructor solely for side effects is unconventional but acceptable in test scripts. The logic correctly:

  • Creates accounts with deterministic addresses (uint160(420 + i))
  • Uses a single-owner configuration for each account
  • Leverages the factory's createAccount method
server/test/anvil.ts (1)

159-159: LGTM: Correct use of --tc flag to filter script execution.

Adding --tc BobExecuteScript ensures that only the BobExecuteScript contract is executed by forge, preventing it from attempting to run the BatchDeploy helper contract as a separate script. This is the correct approach after introducing multiple contracts in the same file.

.vscode/extensions.json (1)

6-6: Replace the invalid extension ID drblury.protobuf-vsc with a valid alternative.

The extension drblury.protobuf-vsc does not exist in either the VS Code Marketplace or Open VSX registry. Use the widely-available mike-lischke.protobuf extension instead, or verify the correct publisher/extension ID.

The rust-lang.rust-analyzer extension is valid and available in both registries—no changes needed there.

⛔ Skipped due to learnings
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: .vscode/extensions.json:6-6
Timestamp: 2026-01-08T20:26:33.396Z
Learning: When validating VS Code extension IDs, check both the official VS Code Marketplace (marketplace.visualstudio.com) and Open VSX registry (open-vsx.org) before flagging extensions as invalid, as Open VSX is a widely-used alternative extension registry.
substreams/.gitignore (1)

1-8: LGTM!

The gitignore correctly excludes generated artifacts (src/contracts, src/proto), build outputs (target), and intermediate files (*.log, *.spkg) appropriate for a Rust/WASM Substreams package.

pnpm-workspace.yaml (1)

1-8: Workspace inclusion looks correct.

Adding substreams under packages is consistent with the existing workspace entries.

server/drizzle.config.ts (1)

1-12: Verify schemaFilter is supported by your drizzle-kit version and matches the intended Postgres schema layout.

Please confirm (a) the option name/behavior is correct for your drizzle-kit version, and (b) substreams is an actual Postgres schema you want drizzle-kit to include for introspection/migration generation.

cspell.json (1)

3-12: cspell updates are scoped appropriately (generated paths + expected Rust/substreams terms).

Also applies to: 13-166

.github/workflows/server-build.yaml (1)

36-37: LGTM!

Making the Dockerfile path explicit improves clarity and maintainability. This aligns well with the pattern established in the new substreams workflow.

docs/package.json (1)

37-44: LGTM!

The addition of dependentTasksOutputFiles properly tracks the OpenAPI schema dependency, ensuring the docs rebuild when the server's API specification changes. This improves incremental build reliability.

server/package.json (2)

64-64: LGTM!

The addition of the substreams package as a devDependency aligns with the PR objective of implementing initial substreams functionality. The workspace protocol and version are appropriate for this initial integration.


143-146: LGTM!

Adding contract output tracking to the broadcasts generation inputs ensures that changes to contract artifacts correctly trigger regeneration. This improves build cache accuracy and is consistent with similar NX dependency tracking patterns elsewhere in the monorepo.

server/test/e2e.ts (1)

7-7: LGTM!

The Sentry mock enhances test observability by logging exceptions to console during e2e tests. The implementation correctly preserves the original behavior while adding debug output, follows the same pattern as other mocks in the file, and complies with coding guidelines for type imports and comments.

Also applies to: 57-67

.github/workflows/substreams-build.yaml (1)

1-38: Well-structured workflow with good security practices.

The workflow correctly implements Docker image building with appropriate permissions, tag patterns, and supply chain attestation. The inline YAML syntax aligns with repository preferences.

Likely an incorrect or invalid review comment.

contracts/package.json (2)

79-104: Nx outputs additions look consistent with forge/slither artifact locations.
Tracking {projectRoot}/out, {projectRoot}/cache, and {projectRoot}/.gas-snapshot should improve cache correctness and artifact collection.


13-16: No issue with slither . --skip-clean. This flag is the recommended practice in CI. It prevents Slither from running framework clean commands (e.g., forge clean) that would delete build artifacts. Your workflow correctly follows the safe pattern: build first (which happens via the test target's build dependency and test:gas compilation), then run Slither with --skip-clean to preserve those artifacts. Each CI run is isolated on a fresh runner, so there's no cross-run artifact pollution.

server/test/database.ts (1)

49-70: The execa $ template defaults to no shell interpretation, so the & literal is safe as written.

The review comment's concern is incorrect. By default, execa's $ tagged template does not invoke a shell (shell: false by default per execa documentation), and no shell: true option is passed in this code. This means the & character in the literal portion of the template string is treated as a regular character passed to the command, not a shell metacharacter. The interpolated ${postgresURL} is automatically escaped by execa regardless. The proposed refactoring to extract the URL to a variable does not address a real safety issue.

Regarding --config-file= (empty value): this appears intentional and cannot be confirmed as a typo without additional context about the fireeth command's expected behavior.

Likely an incorrect or invalid review comment.

common/package.json (1)

42-50: The dependentTasksOutputFiles configuration is correctly wired to actual dependencies. The ^build dependency includes @exactly/plugin's (contracts') build target, which outputs to {projectRoot}/out (mapped to contracts/out/**/*.json at workspace root). The contracts/broadcast/*/31337/*.json files are covered by the explicit @exactly/server:generate:broadcasts dependency. Cache inputs will behave as intended—no changes needed.

server/script/firehose.ts (1)

135-135: Verify the duplicated libNum in output.

The output format uses libNum twice: FIRE BLOCK ${num} ${hash} ${libNum} ${parentHash} ${libNum} ${timestamp}. The second libNum likely should be a different value. If intentional, please add a comment explaining why.

server/database/schema.ts (4)

72-77: Consider adding .notNull() constraints.

The cursor, blockNum, and blockId columns lack .notNull() constraints. If these columns should always have values, add explicit constraints to ensure data integrity.


79-86: Consider adding .notNull() constraints to history table columns.

The op, tableName, pk, prevValue, and blockNum columns are nullable. If these are required fields for history tracking, add .notNull() constraints.


88-91: Verify use of numeric type for block number and timestamp.

Using numeric (arbitrary precision) instead of bigint for number and timestamp is uncommon for block data. While this preserves precision, it returns strings in JavaScript and has performance implications. If this is intentional for handling very large values, consider adding a comment explaining the choice.


70-100: Define relations for new substreams tables.

Per coding guidelines, relations should be defined explicitly in the schema file to enable Drizzle to generate typed query results. If the substreams tables have relationships (e.g., between exaPlugins and credentials via account), consider defining them.

Based on learnings, database schema should define relations explicitly.

.github/workflows/version.yaml (1)

25-29: LGTM!

The cache path update to .nx/cache aligns with the modern Nx cache location and is consistent with other workflow changes in this PR.

substreams/.cargo/config.toml (1)

1-2: LGTM!

Setting wasm32-unknown-unknown as the default build target is correct for Substreams WASM modules.

substreams/rust-toolchain.toml (1)

1-4: Verify nightly channel requirement.

Using nightly channel introduces potential instability. If stable toolchain features suffice for the Substreams build, consider pinning to a specific stable version for reproducibility. If nightly is required for specific features (e.g., unstable rustfmt options in rustfmt.toml), consider adding a comment explaining the requirement.

.github/workflows/mobile-expo.yaml (1)

44-48: LGTM!

Cache path update to .nx/cache is consistent with the other workflow changes in this PR.

substreams/rustfmt.toml (1)

1-6: Configuration requires nightly rustfmt.

The imports_granularity option is unstable and requires nightly rustfmt, which explains the nightly toolchain choice. Consider adding a comment in rust-toolchain.toml noting this dependency.

Also, tab_spaces = 2 deviates from Rust's community standard of 4 spaces. If this is intentional to align with the project's TypeScript/JavaScript style, that's reasonable but worth documenting.

.github/workflows/docs.yaml (1)

31-35: LGTM!

Cache path update to .nx/cache aligns with modern NX conventions and is consistent with other workflow files in this PR.

.github/workflows/test.yaml (1)

9-9: LGTM!

NX_DAEMON and cache path changes are consistent with other workflow updates in this PR.

Also applies to: 34-34

substreams/package.json (1)

1-50: LGTM on overall structure!

The package manifest is well-organized with proper NX target configuration, input/output tracking for incremental builds, and appropriate dependency chains. Based on learnings, using nx for running tasks aligns with repository conventions.

substreams/proto/exa.proto (1)

5-17: LGTM on proto message definitions!

The message structures are well-designed for the substreams use case. Using bytes for addresses/accounts is appropriate for blockchain data, and the ordinal field correctly tracks event ordering.

substreams/Cargo.toml (2)

32-35: LGTM on release profile!

Good optimization choices for WASM: LTO for dead code elimination, opt-level = 's' for size optimization, and stripping debug info reduces binary size.


4-4: No action needed — Rust 2024 edition is stable (Rust 1.85.0+) and does not require special toolchain handling.

substreams/Dockerfile (1)

1-30: Well-structured multi-stage Dockerfile!

Good practices observed:

  • Multi-stage build minimizes final image size
  • dumb-init as PID 1 handles signal forwarding properly
  • Non-root user (substreams) for runtime security
  • Cache mount for pnpm speeds up rebuilds

For production readiness, consider adding a HEALTHCHECK instruction as flagged by Checkov.

substreams/substreams.yaml (3)

14-42: LGTM: Module dependency chain is well-structured.

The module definitions correctly wire the data flow: accounts are extracted, stored, then used to identify plugins, which are finally converted to database changes.


11-12: The external import URLs in the manifest are valid and accessible. Both packages are hosted at official GitHub releases from trusted streamingfast repositories and return proper HTTP responses.


58-62: Verify multiline parameter whitespace handling in Substreams runtime.

The multiline parameter format for Optimism factories uses backslash continuation within a quoted string. The indentation on continuation lines (60-62) may preserve leading whitespace in the final parameter value, resulting in factories[]=ADDR1 &factories[]=ADDR2 with unintended spaces. Confirm that the Substreams runtime or downstream parameter parser correctly handles or strips this whitespace.

All factory addresses are valid Ethereum addresses (40 hex characters each).

substreams/build.rs (2)

14-23: LGTM: Comprehensive rebuild triggers.

The rerun-if-changed directives properly cover all input sources (proto files, configs, contracts, and dependencies) to ensure the build script re-executes when necessary.


38-48: LGTM: Clean generated module structure.

The generated mod.rs file correctly exports contract modules with appropriate clippy directives for generated code.

substreams/src/lib.rs (5)

1-21: LGTM: Clean module structure and appropriate imports.

The module organization and imports are well-structured. The clippy allow for not_unsafe_ptr_arg_deref is likely necessary for Substreams FFI integration.


27-44: LGTM: Correct account extraction logic.

The function correctly parses factory addresses from params, filters ExaAccountInitialized events, and collects account addresses. Error handling via ? operator is appropriate.


46-51: LGTM: Simple and correct account storage.

The function correctly stores a flag (value 1) for each account using the hex-encoded address as the key.


53-71: LGTM: Correct plugin filtering logic.

The function properly filters PluginInstalled events:

  • For new accounts: Uses !seen.insert() to skip the first plugin (owner plugin) as intended
  • For existing accounts: Checks the stored flag

The deduplication logic is correct per the established pattern. Based on learnings, the first plugin for new accounts is an owner plugin that should be filtered.


73-88: LGTM: Correct database change generation.

The function properly generates database changes by upserting plugin rows to the exa_plugins table and creating a blocks row when changes exist. The schema defines the expected tables with matching columns (address and account as text fields), and the code correctly converts hex values to strings. The use of unwrap_or_default() for timestamp is safe.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
contracts/package.json (1)

60-74: Add the actual forge test execution to the test:parallel target.

The test:parallel target (lines 75-81) has no executor or commands — it only runs its dependencies (test:fmt, test:solhint, test:sizes), which are formatting, linting, and size checks. The actual forge test unit test suite is missing entirely from the composite test target, which calls test:parallel as its first step. The subsequent steps (test:gas, test:slither, test:coverage) run profiling, static analysis, and coverage but not the core unit tests.

Define test:parallel with an executor and command to run forge test, or add a dedicated dependency that executes it.

🤖 Fix all issues with AI agents
In @.github/workflows/docs.yaml:
- Line 14: YAMLlint flags extra spaces inside the inline mapping braces on the
env line; update the line that currently reads env: { NX_DAEMON: false } to
remove the spaces inside the braces so the inline mapping has no inner spaces
(env: {NX_DAEMON: false}), ensuring the YAML passes linting.

In @.github/workflows/substreams-build.yaml:
- Around line 26-33: The build step using docker/build-push-action@v6 (step id:
build) does not enable Docker layer caching, causing repeated reinstall of
Rust/Foundry/Node deps; update the build step to include cache-from and cache-to
inputs (e.g., cache-from: type=gha,ref=${{ github.sha }} and cache-to:
type=gha,mode=max,ref=${{ github.sha }}) and add buildx-options or cache key
usage as needed so layers are pulled and pushed between CI runs; keep existing
inputs (context, file, tags, labels) and only add these cache parameters to the
step.

In @.github/workflows/test.yaml:
- Around line 38-42: Wrap the download loop with strict shell error handling
(e.g., enable -euo pipefail) and make each curl call use fail-on-error, check
its exit status and the tar extraction result for each repo (firehose-ethereum,
substreams, substreams-sink-sql), aborting with a clear error message if any
download or tar fails; after creating ~/.local/bin ensure you export PATH by
prepending ~/.local/bin to PATH (export PATH="$HOME/.local/bin:$PATH") so
subsequent steps can find the binaries and add an existence/check step to
confirm the expected binaries are present before proceeding.

In @substreams/build.rs:
- Around line 61-67: The build script currently uses assert! around the rustfmt
invocation (Command::new("rustfmt")... .status().expect(...).success()), which
panics instead of returning a proper Result; change this to explicit
Result-based error handling like the protogen flow: run the
Command::new("rustfmt") with the same args
(glob("src/contracts/**/*.rs")?.chain(glob("src/proto/**/*.rs")?).filter_map(Result::ok)),
capture the status via .status()? and check .success(), and if it is not
successful return Err(std::io::Error::new(...) or anyhow::anyhow!(...)) (or map
the child error) so build.rs returns a Result<(), Error> instead of using
assert! or expect; ensure the function signature and callers are adjusted to
propagate the error.

In @substreams/Dockerfile:
- Around line 12-13: The Dockerfile currently fetches the "latest" substreams
CLI release in the RUN curl pipeline (the RUN line that calls GitHub API and
jq), which makes builds non-reproducible; change this to pin a specific release
by introducing a build ARG or ENV like SUBSTREAMS_VERSION and use that value in
the curl/download URL instead of querying "latest" so the RUN command downloads
a known tag (e.g., v1.10.14); update the RUN line that constructs the
browser_download_url to reference the pinned SUBSTREAMS_VERSION and consider
adding a checksum verification step after download for extra safety.

In @substreams/package.json:
- Around line 44-63: The test:protolint target lacks an explicit dependsOn entry
which makes its standalone execution unclear; add a dependsOn array to the
test:protolint target (e.g., include "prepare" or the same prerequisites as
test:clippy such as "prepare", "^prepare", "rustup") so it has consistent
prerequisites like test:clippy and test:fmt and can be run independently; update
the package.json test:protolint target to include that dependsOn configuration.

In @substreams/proto/exa.proto:
- Around line 5-17: Add clear protobuf field comments for the public API:
document message Accounts and its field accounts (e.g., whether entries are
fixed 20-byte Ethereum addresses or variable-length byte arrays and expected
encoding), document message Plugins and its repeated Plugin entries, and inside
Plugin add comments for Plugin.address (format and semantics), Plugin.account
(relationship to Accounts.accounts and whether it indexes into that list or
duplicates an address), and Plugin.ordinal (what it represents — insertion
order, sequence number, block number, etc.). Ensure each comment is concise and
placed immediately above the corresponding message/field (Accounts, accounts,
Plugins, Plugin, address, account, ordinal) so generated docs and codegen will
include them.

In @substreams/rust-toolchain.toml:
- Around line 1-4: Update the Rust toolchain configuration in
rust-toolchain.toml by changing the channel value from "nightly" to "stable"
(replace channel = "nightly" with channel = "stable"); keep the existing
components ("clippy", "rustfmt") and targets ("wasm32-unknown-unknown")
unchanged and ensure CI/workflow configs that reference the toolchain continue
to use the stable channel.

In @substreams/src/lib.rs:
- Line 1: Add a brief crate-level comment above the existing
#![expect(clippy::not_unsafe_ptr_arg_deref)] attribute that documents why the
lint is being suppressed for this crate: state that substreams exposes
FFI/extern "C" functions which accept raw pointers and rely on the caller to
guarantee validity, causing clippy false positives when it warns about
non-unsafe pointer derefs; mention that the suppression is intentional to avoid
noisy warnings while keeping safety expectations documented for maintainers
(referencing the attribute name clippy::not_unsafe_ptr_arg_deref and the
FFI/extern "C" context).
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a0ef2f4 and 6a997f5.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • substreams/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (39)
  • .changeset/breezy-planes-repeat.md
  • .changeset/rare-readers-tell.md
  • .changeset/red-toys-pump.md
  • .changeset/violet-poems-heal.md
  • .dockerignore
  • .github/matcher.json
  • .github/workflows/docs.yaml
  • .github/workflows/mobile-expo.yaml
  • .github/workflows/server-build.yaml
  • .github/workflows/substreams-build.yaml
  • .github/workflows/test.yaml
  • .github/workflows/version.yaml
  • .vscode/extensions.json
  • common/package.json
  • contracts/package.json
  • contracts/test/mocks/BobExecute.s.sol
  • cspell.json
  • docs/package.json
  • pnpm-workspace.yaml
  • server/Dockerfile
  • server/database/schema.ts
  • server/drizzle.config.ts
  • server/package.json
  • server/script/firehose.ts
  • server/test/anvil.ts
  • server/test/database.ts
  • server/test/e2e.ts
  • substreams/.cargo/config.toml
  • substreams/.gitignore
  • substreams/Cargo.toml
  • substreams/Dockerfile
  • substreams/buf.gen.yaml
  • substreams/build.rs
  • substreams/package.json
  • substreams/proto/exa.proto
  • substreams/rust-toolchain.toml
  • substreams/rustfmt.toml
  • substreams/src/lib.rs
  • substreams/substreams.yaml
🧰 Additional context used
📓 Path-based instructions (11)
server/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/server.mdc)

server/**/*.ts: Use c.var object to pass strongly-typed data between Hono middleware and route handlers; do not use c.set
All request validation (headers, body, params) must be handled by @hono/valibot-validator middleware; do not perform manual validation inside route handlers
Use Hono's built-in error handling by throwing new HTTPException() for expected errors; unhandled errors will be caught and logged automatically
Enforce Node.js best practices using ESLint plugin:n/recommended configuration
Enforce Drizzle ORM best practices using ESLint plugin:drizzle/all configuration, including requiring where clauses for update and delete operations
Use Drizzle ORM query builder for all database interactions; do not write raw SQL queries unless absolutely unavoidable
All authentication and authorization logic must be implemented in Hono middleware
Do not access process.env directly in application code; load all configuration and secrets once at startup and pass them through dependency injection or context
Avoid long-running, synchronous operations; use async/await correctly and be mindful of CPU-intensive tasks to prevent blocking the event loop

Files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • server/database/schema.ts
  • server/script/firehose.ts
  • server/test/e2e.ts
  • server/test/anvil.ts
**/*.{js,ts,tsx,jsx,sol}

📄 CodeRabbit inference engine (AGENTS.md)

Follow linter/formatter (eslint, prettier, solhint) strictly with high strictness level. No any type.

Files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • server/database/schema.ts
  • server/script/firehose.ts
  • contracts/test/mocks/BobExecute.s.sol
  • server/test/e2e.ts
  • server/test/anvil.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Omit redundant type names in variable declarations - let the type system explain itself

**/*.{ts,tsx}: Use PascalCase for TypeScript types and interfaces
Use valibot for all runtime validation of API inputs, environment variables, and other data; define schemas once and reuse them
Infer TypeScript types from valibot schemas using type User = v.Input<typeof UserSchema> instead of manually defining interfaces

Files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • server/database/schema.ts
  • server/script/firehose.ts
  • server/test/e2e.ts
  • server/test/anvil.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Omit contextual names - don't repeat class/module names in members
Omit meaningless words like 'data', 'state', 'manager', 'engine', 'value' from variable and function names unless they add disambiguation

**/*.{ts,tsx,js,jsx}: Prefer function declarations for all multi-line functions; use function expressions or arrow functions only for single-line implementations
Prefer const for all variable declarations by default; only use let if the variable's value will be reassigned
Declare each variable on its own line with its own const or let keyword, not multiple declarations on one line
Use camelCase for TypeScript variables and functions
Always use import type { ... } for type imports
Use relative paths for all imports within the project; avoid tsconfig path aliases
Follow eslint-plugin-import order: react, external libraries, then relative paths
Use object and array destructuring to access and use properties
Use object method shorthand syntax when a function is a property of an object
Prefer optional chaining (?.), nullish coalescing (??), object and array spreading (...), and for...of loops over traditional syntax
Do not use abbreviations or cryptic names; write out full words like error, parameters, request instead of err, params, req
Use Number.parseInt() instead of the global parseInt() function when parsing numbers
All classes called with new must use PascalCase
Use Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe() instead of the deprecated new Buffer()
Use @ts-expect-error instead of @ts-ignore; follow it immediately with a single-line lowercase comment explaining why the error is expected, without separators like - or :
Do not include the type in a variable's name; let the static type system do its job (e.g., use const user: User not const userObject: User)
Do not repeat the name of a class or module within its members; omit contextual names (e.g., use `class User { getProfil...

Files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • server/database/schema.ts
  • server/script/firehose.ts
  • server/test/e2e.ts
  • server/test/anvil.ts
server/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

server/**/*.{ts,tsx}: Server API: implement schema-first approach using OpenAPI via hono with validation via valibot middleware
Server database: drizzle schema is source of truth. Migrations required. No direct database access in handlers - use c.var.db

Files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • server/database/schema.ts
  • server/script/firehose.ts
  • server/test/e2e.ts
  • server/test/anvil.ts
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

For files with a single default export, name the file identically to the export; for files with multiple exports, use camelCase with a strong preference for a single word

Files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • server/database/schema.ts
  • server/script/firehose.ts
  • server/test/e2e.ts
  • server/test/anvil.ts
**/.changeset/*.md

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

Use a lowercase sentence in the imperative present tense for changeset summaries

Files:

  • .changeset/breezy-planes-repeat.md
  • .changeset/red-toys-pump.md
  • .changeset/violet-poems-heal.md
  • .changeset/rare-readers-tell.md
server/database/schema.ts

📄 CodeRabbit inference engine (.cursor/rules/server.mdc)

server/database/schema.ts: Database schema must be defined in server/database/schema.ts as the single source of truth for database structure
Define relations explicitly in the schema file to enable Drizzle to generate typed query results including related entities

Files:

  • server/database/schema.ts
contracts/**/*.sol

📄 CodeRabbit inference engine (.cursor/rules/contracts.mdc)

contracts/**/*.sol: Import order must be sorted alphabetically and grouped by type (openzeppelin, solady, internal, etc.), enforced by forge fmt
All functions and state variables must have explicit visibility modifiers (public, private, internal, external)
Use custom errors instead of require() statements with string messages; require() with messages is forbidden
console.log is forbidden in contract code
All external and public functions that modify state must be protected by a reentrancy guard, unless explicitly proven unnecessary
Use modifiers like onlyOwner or function-level checks for privileged functions; default to strictest access level (private > internal > external > public)
Use named parameters in mappings for clarity (e.g., mapping(address user => uint256 balance))
All code must pass pnpm --filter plugin test:slither and pnpm --filter plugin test:solhint with zero warnings; do not ignore warnings
Follow standard Solidity import order: external libraries (openzeppelin, solady) first, then project-internal contracts
All public and external functions and state variables must have complete natspec comments (@notice, @dev, @param, @return)
Use custom errors instead of require statements with string messages for better gas efficiency and error information
Internal and private variables should be prefixed with an underscore (_); function arguments should not be prefixed
Use constant keyword for true constants; use immutable for configurable values like contract addresses
Minimize writes to storage; read data into memory or calldata whenever possible for gas optimization
Use smallest integer size that is safe for the variable (e.g., uint32 for timestamps) and be aware of struct packing for gas efficiency
Prefer using the solady library over openzeppelin where possible, as it is highly optimized for gas
Correctly label functions as view or pure when they do not modify state

contracts/**/*.sol: Smart contracts: use custom errors error MyError(); instead of s...

Files:

  • contracts/test/mocks/BobExecute.s.sol
**/*.sol

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

**/*.sol: Use camelCase for Solidity variables and functions
Use PascalCase for Solidity contracts and structs

Files:

  • contracts/test/mocks/BobExecute.s.sol
**/+(app|src)/**

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

Use kebab-case for directory names and public Expo router routes

Files:

  • substreams/src/lib.rs
🧠 Learnings (36)
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/database/schema.ts : Define relations explicitly in the schema file to enable Drizzle to generate typed query results including related entities

Applied to files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Applies to server/**/*.{ts,tsx} : Server database: drizzle schema is source of truth. Migrations required. No direct database access in handlers - use `c.var.db`

Applied to files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/database/schema.ts : Database schema must be defined in `server/database/schema.ts` as the single source of truth for database structure

Applied to files:

  • server/drizzle.config.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/database/migrations/** : All schema changes must be accompanied by a migration file generated by `drizzle-kit`; never edit the database schema manually

Applied to files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Enforce Drizzle ORM best practices using ESLint `plugin:drizzle/all` configuration, including requiring `where` clauses for `update` and `delete` operations

Applied to files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Use Drizzle ORM query builder for all database interactions; do not write raw SQL queries unless absolutely unavoidable

Applied to files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Do not access `process.env` directly in application code; load all configuration and secrets once at startup and pass them through dependency injection or context

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Avoid long-running, synchronous operations; use `async/await` correctly and be mindful of CPU-intensive tasks to prevent blocking the event loop

Applied to files:

  • server/test/database.ts
  • server/script/firehose.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Server stack: use hono, node.js, drizzle orm, postgres

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Server (exactly/server) uses Hono (framework), Node.js, Drizzle ORM (database), and PostgreSQL

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-30T15:03:28.449Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/.changeset/*.md : Use a lowercase sentence in the imperative present tense for changeset summaries

Applied to files:

  • .changeset/breezy-planes-repeat.md
  • .changeset/violet-poems-heal.md
  • .changeset/rare-readers-tell.md
📚 Learning: 2025-12-23T19:57:14.465Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/git.mdc:0-0
Timestamp: 2025-12-23T19:57:14.465Z
Learning: Select the most precise gitmoji that best communicates the intent of the change (e.g., ✨ for new feature, 🐛 for bug fix, ♻️ for refactoring, ⚡️ for performance improvement, 📝 for documentation, 🚀 for deployment)

Applied to files:

  • .changeset/breezy-planes-repeat.md
  • .changeset/rare-readers-tell.md
📚 Learning: 2025-12-23T19:57:14.465Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/git.mdc:0-0
Timestamp: 2025-12-23T19:57:14.465Z
Learning: All commits must start with a single gitmoji from the official gitmoji list; this is not optional and the emoji visually communicates the intent of the change

Applied to files:

  • .changeset/breezy-planes-repeat.md
📚 Learning: 2025-12-23T19:57:14.465Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/git.mdc:0-0
Timestamp: 2025-12-23T19:57:14.465Z
Learning: Do not use conventional commit prefixes like `feat:`, `fix:`, or `docs(...)`; do not use title case or sentence case in the commit subject line; do not create commits without a scope

Applied to files:

  • .changeset/breezy-planes-repeat.md
📚 Learning: 2025-12-31T00:23:55.034Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 610
File: .changeset/ready-experts-fly.md:1-2
Timestamp: 2025-12-31T00:23:55.034Z
Learning: In the exactly/exa repository, allow and require empty changeset files (containing only --- separators) when changes are not user-facing and do not warrant a version bump. This is needed because CI runs changeset status --since origin/main and requires a changeset file to exist. Ensure such empty changesets are used only for non-user-facing changes and document the rationale in the commit or changelog notes.

Applied to files:

  • .changeset/breezy-planes-repeat.md
  • .changeset/red-toys-pump.md
  • .changeset/violet-poems-heal.md
  • .changeset/rare-readers-tell.md
📚 Learning: 2026-01-08T19:54:26.885Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: substreams/rust-toolchain.toml:1-4
Timestamp: 2026-01-08T19:54:26.885Z
Learning: Rust edition 2024 was released as stable on February 20, 2025 with Rust 1.85.0 and does not require nightly channel.

Applied to files:

  • substreams/rust-toolchain.toml
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Applies to {src/generated/**,server/generated/**,contracts/broadcast/**,contracts/cache/**} : Do not manually edit files in generated directories: `src/generated/`, `server/generated/`, `contracts/broadcast/`, and `contracts/cache/`

Applied to files:

  • .dockerignore
  • substreams/.gitignore
  • common/package.json
  • substreams/build.rs
📚 Learning: 2026-01-08T19:45:38.852Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: .github/workflows/substreams-build.yaml:2-2
Timestamp: 2026-01-08T19:45:38.852Z
Learning: In the exactly/exa repository, prefer concise inline YAML syntax (e.g., `on: { push: { tags: ["pattern"] }, workflow_dispatch: {} }`) over expanded multi-line format in GitHub workflow files, even if it triggers YAMLlint warnings about formatting.

Applied to files:

  • .github/workflows/substreams-build.yaml
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Use `nx` for running tasks (build, lint, test, e2e) instead of underlying tooling directly

Applied to files:

  • .github/workflows/mobile-expo.yaml
  • .github/workflows/docs.yaml
  • substreams/package.json
  • .github/workflows/version.yaml
  • contracts/package.json
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Install dependencies using `pnpm install`

Applied to files:

  • .github/workflows/docs.yaml
  • .github/workflows/version.yaml
📚 Learning: 2025-12-30T15:03:28.449Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Do not use abbreviations or cryptic names; write out full words like `error`, `parameters`, `request` instead of `err`, `params`, `req`

Applied to files:

  • server/script/firehose.ts
📚 Learning: 2026-01-08T20:26:33.396Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: .vscode/extensions.json:6-6
Timestamp: 2026-01-08T20:26:33.396Z
Learning: For validating VS Code extension IDs in this repository, check both the official VS Code Marketplace (marketplace.visualstudio.com) and the Open VSX registry (open-vsx.org) before flagging an extension as invalid. Open VSX is a widely-used alternative registry, so a valid ID may appear in either registry. If an ID is not present in both registries, flag it as invalid. Apply this check specifically when validating the IDs listed in .vscode/extensions.json.

Applied to files:

  • .vscode/extensions.json
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.sol : All public and external functions and state variables must have complete natspec comments (notice, dev, param, return)

Applied to files:

  • contracts/test/mocks/BobExecute.s.sol
📚 Learning: 2026-01-08T19:46:37.829Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: contracts/test/mocks/BobExecute.s.sol:61-71
Timestamp: 2026-01-08T19:46:37.829Z
Learning: Natspec documentation requirements do not apply to test/mock/dev scripts (files in contracts/test/ or with .s.sol extension). Only production contract code in contracts/src/ requires complete natspec comments.

Applied to files:

  • contracts/test/mocks/BobExecute.s.sol
  • contracts/package.json
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Zero config local dev environment: no `.env` files, mock all external services

Applied to files:

  • contracts/test/mocks/BobExecute.s.sol
  • server/test/e2e.ts
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.t.sol : Use vm.prank to simulate calls from different addresses; avoid changing msg.sender through other means

Applied to files:

  • contracts/test/mocks/BobExecute.s.sol
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.sol : All code must pass pnpm --filter plugin test:slither and pnpm --filter plugin test:solhint with zero warnings; do not ignore warnings

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Run contract tests using `pnpm --filter plugin test` and check formatting using `pnpm --filter plugin test:fmt`

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Run all tests using `pnpm test`, which includes TypeScript compilation, ESLint, spell checking, markdown linting, and contract tests

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Applies to contracts/**/*.test.sol : Smart contracts testing: fuzzing required for inputs and gas snapshots mandatory

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.t.sol : Use fuzz testing (testFuzz_...) extensively for functions with numerical or address inputs to cover a wide range of scenarios

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Rely on `pnpm` scripts for all tasks; all setup, testing, and generation must be automated

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.sol : Use custom errors instead of require statements with string messages for better gas efficiency and error information

Applied to files:

  • substreams/build.rs
📚 Learning: 2026-01-08T19:54:10.673Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: substreams/src/lib.rs:54-71
Timestamp: 2026-01-08T19:54:10.673Z
Learning: In substreams/src/lib.rs in the map_exa_plugins function, the deduplication logic using `!seen.insert(address.to_vec())` is intentional. The first plugin installed for a new account is an owner plugin (not an exa plugin), so it must be filtered out. The negation ensures the first plugin is skipped and subsequent plugins (the actual exa plugins) are kept.

Applied to files:

  • substreams/src/lib.rs
📚 Learning: 2025-12-30T15:03:28.449Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Omit meaningless words from names like `data`, `state`, `manager`, `engine`, `object`, `entity`, and `instance`

Applied to files:

  • cspell.json
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Omit meaningless words like 'data', 'state', 'manager', 'engine', 'value' from variable and function names unless they add disambiguation

Applied to files:

  • cspell.json
🧬 Code graph analysis (1)
server/test/database.ts (1)
server/database/schema.ts (1)
  • substreams (70-70)
🪛 Buf (1.62.1)
substreams/proto/exa.proto

3-3: Files with package "exa" must be within a directory "exa" relative to root but were in directory "substreams/proto".

(PACKAGE_DIRECTORY_MATCH)

🪛 Checkov (3.2.334)
substreams/Dockerfile

[low] 1-33: Ensure that HEALTHCHECK instructions have been added to container images

(CKV_DOCKER_2)

🪛 YAMLlint (1.37.1)
.github/workflows/substreams-build.yaml

[warning] 2-2: truthy value should be one of [false, true]

(truthy)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 6-6: too many spaces inside braces

(braces)


[error] 6-6: too many spaces inside braces

(braces)


[error] 11-11: too many spaces inside braces

(braces)


[error] 11-11: too many spaces inside braces

(braces)


[error] 25-25: too many spaces inside braces

(braces)


[error] 25-25: too many spaces inside braces

(braces)

.github/workflows/docs.yaml

[error] 14-14: too many spaces inside braces

(braces)


[error] 14-14: too many spaces inside braces

(braces)

🔇 Additional comments (57)
cspell.json (2)

6-9: LGTM!

The new ignorePaths entries are appropriate—.github/matcher.json contains technical regex patterns, and substreams/target is a Rust build output directory that should be excluded from spell checking.


14-138: LGTM!

The new dictionary words correctly cover the Rust tooling (clippy, rustup, serde, prost), Substreams/StreamingFast ecosystem (substreams, streamingfast, spkg, fireeth), and protobuf tooling (bufbuild, protogen, protolint) introduced by this PR.

.github/matcher.json (2)

64-79: LGTM!

The Rust compiler matcher correctly uses a two-pattern approach: the first captures severity, optional code, and message from warning/error lines, while the second captures file location from --> lines. The ANSI escape sequence handling (\x1b\[[\d;]+m) is comprehensive.


81-91: LGTM!

The rust-formatter matcher appropriately defaults to "severity": "warning" for formatting diffs and captures the file/line from rustfmt --check output.

.vscode/extensions.json (1)

6-6: Remove or replace "drblury.protobuf-vsc"—this extension does not exist in VS Code Marketplace or Open VSX registry.

The extension ID "drblury.protobuf-vsc" at line 6 cannot be found in either the VS Code Marketplace or the Open VSX registry. This will prevent users from installing the recommended extensions list. "rust-lang.rust-analyzer" at line 16 is valid and published in both registries.

Either remove "drblury.protobuf-vsc" or replace it with a valid protobuf extension (e.g., "panzhihao.protobuf-language-server" or "Protobuf Language Support").

⛔ Skipped due to learnings
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: .vscode/extensions.json:6-6
Timestamp: 2026-01-08T20:26:33.396Z
Learning: When validating VS Code extension IDs, check both the official VS Code Marketplace (marketplace.visualstudio.com) and Open VSX registry (open-vsx.org) before flagging extensions as invalid, as Open VSX is a widely-used alternative extension registry.
contracts/package.json (4)

9-9: LGTM: Test delegation to nx orchestration.

Correctly delegates test execution to nx, providing a clean entry point while leveraging nx's task orchestration capabilities.


13-13: LGTM: Slither optimization flag.

The --skip-clean flag is appropriate since the nx test target ensures a fresh build via dependsOn: ["build"] (line 61-63), allowing Slither to reuse build artifacts.


82-100: LGTM: Output configurations for nx caching.

The output paths are correctly specified for each test target, enabling effective nx caching. The .gas-snapshot file is appropriately included only in test:gas, and standard build artifacts (out, cache) are consistently declared across all targets.


101-107: LGTM: Coverage output configuration.

Correctly includes lcov.info along with standard build artifacts, ensuring coverage reports are cached by nx.

substreams/.cargo/config.toml (1)

1-2: LGTM!

The Cargo configuration correctly sets the build target to wasm32-unknown-unknown, which aligns with the toolchain targets specified in rust-toolchain.toml.

substreams/.gitignore (1)

1-8: LGTM!

The gitignore entries appropriately exclude build artifacts, generated code directories (src/contracts, src/proto), and output files, consistent with the repository's policy on generated directories.

.changeset/breezy-planes-repeat.md (1)

1-5: LGTM!

The changeset correctly follows the repository's conventions: lowercase sentence in imperative present tense with appropriate gitmoji.

pnpm-workspace.yaml (1)

7-7: LGTM!

The addition of the substreams package to the workspace is correct and aligns with the new substreams package being introduced in this PR.

.changeset/rare-readers-tell.md (1)

1-5: LGTM!

The changeset follows the correct format with lowercase imperative present tense and appropriate gitmoji for a new feature.

server/test/anvil.ts (1)

159-160: LGTM!

Adding the --tc BobExecuteScript test case selector ensures the correct test case is executed. This is good practice, especially when a script file contains multiple test cases.

substreams/rustfmt.toml (1)

1-6: LGTM!

The Rustfmt configuration uses standard formatting options that will ensure consistent code formatting across the substreams Rust package. The settings are reasonable and align with Rust best practices.

.changeset/red-toys-pump.md (1)

1-5: LGTM!

The changeset follows the correct format with lowercase imperative present tense and appropriate gitmoji for a new feature.

server/drizzle.config.ts (1)

8-8: schemaFilter is configuration, not a schema change—no additional migrations required.

The substreams schema is already defined in server/database/schema.ts with pgSchema("substreams") and includes all its tables (cursors, substreamsHistory, blocks, exaPlugins). Adding it to schemaFilter in the drizzle config is merely configuration telling drizzle-kit to introspect this schema alongside the public schema. This is not a schema change that requires new migrations.

Likely an incorrect or invalid review comment.

.github/workflows/server-build.yaml (1)

36-37: LGTM! Explicit Dockerfile path improves clarity.

Making the build context and Dockerfile path explicit is a best practice that enhances workflow maintainability and prevents ambiguity.

.changeset/violet-poems-heal.md (1)

1-5: LGTM! Changeset follows the guidelines.

The changeset summary uses lowercase imperative present tense ("insert exa plugins into database") and is consistent with the team's convention of using emoji prefixes.

docs/package.json (1)

37-44: LGTM! Proper NX input tracking for OpenAPI dependency.

The inputs configuration correctly tracks the OpenAPI output from the server, ensuring the docs build target properly detects changes in the API specification.

server/package.json (2)

64-64: LGTM! Workspace dependency properly added.

The new @exactly/substreams dev dependency is correctly specified as a workspace reference with the initial version.


143-146: LGTM! Enhanced NX input tracking for broadcasts generation.

The extended inputs correctly track the CHAIN_ID environment variable and contract compilation outputs, ensuring the broadcasts generation task properly detects changes in its dependencies.

substreams/Cargo.toml (4)

7-9: LGTM! Correct crate type for WASM modules.

The cdylib crate type is the appropriate choice for building WASM modules that will be loaded by the substreams runtime.


11-21: LGTM! Dependencies are appropriate for substreams development.

The dependency set includes the necessary substreams framework crates along with standard Ethereum and serialization libraries.


32-35: LGTM! Release profile optimized for WASM deployment.

The release profile settings are well-suited for WASM:

  • lto = true enables link-time optimization
  • opt-level = 's' optimizes for binary size
  • strip = "debuginfo" removes debug symbols

These settings will produce compact, production-ready WASM modules.


4-4: Rust edition "2024" is stable and properly supported. It was released as stable in February 2025 with Rust 1.85.0 and does not require the nightly channel. Using this edition in the Cargo manifest is correct.

.github/workflows/test.yaml (2)

9-9: LGTM: NX daemon disabled for CI.

Disabling the NX daemon in CI environments is a best practice to avoid state persistence issues in ephemeral runners.


34-34: LGTM: Cache path updated consistently.

The cache path change to .nx/cache aligns with similar updates across other workflows in this PR.

.github/workflows/docs.yaml (1)

33-33: LGTM: Cache path updated consistently.

The cache path change to .nx/cache aligns with similar updates across other workflows in this PR.

.github/workflows/version.yaml (1)

27-27: LGTM: Cache path updated consistently.

The cache path change to .nx/cache aligns with similar updates across other workflows in this PR.

.github/workflows/mobile-expo.yaml (1)

46-46: LGTM: Cache path updated consistently.

The cache path change to .nx/cache aligns with similar updates across other workflows in this PR.

server/test/e2e.ts (2)

7-7: LGTM: Type import follows TypeScript best practices.

Using import type for type-only imports is correct and aligns with the coding guidelines.


58-67: LGTM: Sentry mock correctly preserves original behavior while adding observability.

The mock implementation properly intercepts captureException to log arguments before delegating to the original implementation. This is a sound pattern for debugging test failures.

common/package.json (1)

44-50: LGTM!

The additional dependentTasksOutputFiles inputs correctly wire contract compilation outputs to the wagmi generation task, ensuring proper cache invalidation when contract artifacts change.

contracts/test/mocks/BobExecute.s.sol (1)

61-71: LGTM!

The BatchDeploy contract is a clean utility for batch account creation in test scenarios. The use of pre-increment (++i) is gas-efficient, and the sequential address generation starting at 420 provides deterministic test accounts.

server/test/database.ts (2)

49-70: LGTM! Well-structured process orchestration.

The use of AbortController with forceKillAfterDelay and proper error filtering for aborted signals is a clean pattern for managing external processes in tests. The sequential waitOn calls ensure proper startup order.


90-92: Good use of Promise.allSettled for cleanup.

Using allSettled ensures all cleanup operations complete regardless of individual failures, which is the correct pattern for teardown logic.

.dockerignore (1)

21-21: Verify that excluding all markdown files is intentional.

The pattern **/*.md excludes all markdown files including README.md from the Docker build context. If any markdown files are needed during the build (e.g., for documentation generation or content rendering), this pattern may cause issues.

server/script/firehose.ts (2)

4-65: LGTM! Clean minimal protobuf cursor implementation.

The ProtoCursor class correctly handles the four main wire types (0=varint, 1=64-bit, 2=length-delimited, 5=32-bit) with appropriate bounds checking and error handling for unknown wire types.


124-140: Verify the FIRE BLOCK output format.

The output on line 135 uses ${libNum} in the 5th position (where the original input has parentNum based on typical firehose protocol). The format becomes: FIRE BLOCK num hash libNum parentHash libNum timestamp payload.

If intentional (e.g., to satisfy a specific firehose version requirement), consider adding a brief comment explaining why libNum is used twice.

server/database/schema.ts (3)

36-36: LGTM!

Good use of destructuring in the index callback for cleaner syntax.


88-91: Consider using bigint for block number and timestamp.

The blocks table uses numeric for both number and timestamp. While numeric provides arbitrary precision, bigint is more typical for block numbers and provides better type safety in TypeScript. The other substreams tables (cursors, substreams_history) already use bigint for blockNum.

Was numeric chosen intentionally (e.g., for compatibility with substreams-sink-sql schema requirements)?


70-100: Schema additions look well-structured.

The substreams schema correctly separates concerns using pgSchema("substreams"). The table definitions align with typical substreams-sink-sql patterns:

  • cursors for checkpoint tracking
  • substreams_history for CDC/audit
  • blocks for indexed block data
  • exa_plugins for domain-specific data with composite primary key
.github/workflows/substreams-build.yaml (2)

20-20: Clarify the purpose of this tag pattern.

The pattern ^(?!@)(.*)$ matches any tag not starting with @. Given the workflow only triggers on @exactly/substreams@* tags, this pattern may never match or could create unexpected tags if the workflow is manually dispatched on a different branch/tag.

Consider whether this pattern is needed, or if it's a remnant from a template.


34-38: Excellent: Build provenance attestation included.

The use of attest-build-provenance enhances supply chain security by creating verifiable attestations for the built image.

substreams/proto/exa.proto (1)

3-3: Address the Buf package directory mismatch warning.

Buf's style guide expects package exa to reside in an exa/ directory. The current structure places it in substreams/proto/. Consider either:

  1. Moving to substreams/proto/exa/exa.proto (keeps proto in standard location)
  2. Changing package to exa.substreams or substreams.exa (aligns with directory)
  3. Configuring Buf to ignore this rule if the current structure is intentional for monorepo organization

Based on learnings, if this structure is intentional for the monorepo, you may want to add a Buf lint exception.

substreams/Dockerfile (4)

1-11: Good: Pinned versions for reproducible builds.

The Dockerfile pins specific versions for PNPM (9.15.9) and Foundry (v1.3.6), ensuring reproducible builds. The minimal Rust profile and deferred toolchain selection (likely via rust-toolchain.toml) are appropriate for WASM builds.


22-30: Excellent: Non-root user and proper ownership.

Running as a dedicated substreams user with appropriately owned artifacts follows security best practices.


32-33: Document that the default CMD is for local development only.

The hardcoded connection string postgres://host.docker.internal:5432/postgres is only valid for local Docker Desktop environments. Production deployments should override this CMD with environment-specific configuration.

Consider adding a comment in the Dockerfile or documentation noting this is a development default.


20-20: No action needed—the base image is already at the latest stable release.

The image ghcr.io/streamingfast/substreams-sink-sql:v4.11.3 is the current latest stable version available. No update is required.

substreams/substreams.yaml (5)

14-42: Well-structured module dependency chain.

The module pipeline is logically organized:

  1. Extract accounts from blocks with parameterized factories
  2. Store accounts with set policy for tracking
  3. Extract plugins based on stored accounts
  4. Transform to database changes

The dependency chain and module types are appropriate for the use case.


46-47: Verify the schema path resolution at runtime.

The schema path ../server/generated/schema.sql is relative and crosses package boundaries (substreams → server). When the Docker container runs, the working directory is /app (line 25 in Dockerfile), and the schema is copied to ../server/generated/schema.sql relative to /app. Verify that this path resolves correctly at runtime, as the COPY in the Dockerfile places it at /server/generated/schema.sql.

The Dockerfile copies to /server/generated/schema.sql (absolute), but the manifest references ../server/generated/schema.sql (relative). Confirm these paths align when the sink runs from /app.


58-62: Review the multi-line parameter format for correctness.

The Optimism factories parameter uses backslashes for line continuation and ampersands for query string concatenation. Verify that this multi-line YAML string correctly produces the intended query string format when parsed by the substreams runtime.

Test that the resulting parameter string is correctly interpreted as:

factories[]=8D493AF799162Ac3f273e8918B2842447f702163&factories[]=3427a595eD6E05Cc2D8115e28BAd151cB879616e&factories[]=cbeaAF42Cc39c17e84cBeFe85160995B515A9668&factories[]=961EbA47650e2198A959Ef5f337E542df5E4F61b

49-52: Binary configuration is correct.

The WASM binary path matches the standard Rust target directory structure and aligns with the Dockerfile's COPY command at line 28.


11-12: Both SPKG versions are already current. The manifest imports substreams-sink-database-changes v3.0.0 (latest as of November 2025) and substreams-sink-sql-protodefs v1.0.7 (latest in the protodefs release track). No updates are needed.

Likely an incorrect or invalid review comment.

substreams/buf.gen.yaml (1)

3-6: Verify plugin versions on buf.build directly.

The neoeinstein-prost plugins are pinned to v0.5.0. While v0.5.0 crates exist upstream, official documentation examples reference v0.4.0. Confirm that v0.5.0 is available on the Buf registry and is the intended version before merging.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/version.yaml (1)

8-8: Add NX_DAEMON: false for consistency across workflows.

Both test.yaml and docs.yaml set NX_DAEMON: false, but this workflow omits it. Since version.yaml performs substantial build operations (pnpm install and changeset publishing), disabling the daemon should be consistent across all CI workflows.

🤖 Fix all issues with AI agents
In @.github/workflows/test.yaml:
- Around line 38-42: Replace the fragile "latest" binary download loop with a
robust, pinned-and-verified installer: require a version variable per repo
(e.g., FIREHOSE_VERSION, SUBSTREAMS_VERSION, SINK_SQL_VERSION) and use those
instead of querying /releases/latest; add strict error handling (set -euo
pipefail) so the job fails on any download/parsing error; add curl timeouts and
retries (e.g., --fail --retry 3 --retry-delay 5 --max-time 60) and check exit
status; download and validate checksums/signatures (fetch the release checksum
file and verify the tarball before extracting); ensure $HOME/.local/bin is added
to PATH by echoing it to $GITHUB_PATH; simplify jq selection by matching
explicit asset names that include the pinned version and mapped arch (map uname
-m -> arm64/x86_64) rather than a fragile regex.

In @contracts/package.json:
- Line 13: The npm script "test:slither" currently includes the --skip-clean
flag which can mask stale-artifact false negatives; update the scripts and
pipeline so CI runs Slither against a fresh build (remove --skip-clean or add a
separate "test:slither:ci" without --skip-clean) and ensure a clean compile step
(rebuild artifacts, verify solc/remappings/dependencies) runs before Slither; if
--skip-clean is kept for local dev, document that explicitly and add a CI guard
that runs at least one clean Slither invocation to prevent missed warnings.

In @substreams/Dockerfile:
- Around line 20-33: The image Dockerfile lacks a HEALTHCHECK, so add a
HEALTHCHECK instruction after CMD/ENTRYPOINT to let orchestrators probe health;
implement it to run the service's native health endpoint or a lightweight check
against the running binary (substreams-sink-sql) — e.g., invoke the service's
HTTP/GRPC/CLI health probe or a simple TCP check to the port it listens on, set
sensible intervals/timeouts/retries, and ensure the check runs as the substreams
user so it reflects real runtime conditions.
- Line 13: The Dockerfile currently fetches the "latest" Substreams release via
the GitHub API in the RUN curl line which makes builds non-deterministic; update
the RUN command to pin a specific release by introducing a build ARG (e.g., ARG
SUBSTREAMS_VERSION) with a sensible default or hardcode a tag (e.g., vX.Y.Z) and
use it in the download URL instead of querying /releases/latest (replace the
current curl -s https://api.github.com/... logic with a direct URL that includes
$SUBSTREAMS_VERSION and the architecture logic (uname -m | sed
's/aarch64/arm64/') so the image pulls a fixed linux_$arch tarball).
- Line 13: The Dockerfile RUN step downloads and pipes the substreams archive
directly into tar without verifying integrity; change it to download the release
asset to a file (e.g., /tmp/substreams.tar.gz) using the same jq logic used to
get the browser_download_url, also fetch the corresponding checksum (either the
checksums.txt asset or the .sha256 file for the platform), verify the archive
with sha256sum (or sha256sum -c against checksums.txt) and fail the build on
mismatch, and only after successful verification extract the archive with tar
zxf; update the RUN that currently contains the piped curl/tar to perform:
resolve URLs with the existing jq expression, curl -L -o <file>, curl -L -o
<checksum_file>, verify with sha256sum, and then tar zxf the verified file.
- Around line 32-33: The Dockerfile currently hardcodes a development DB URL in
CMD which will fail outside Docker Desktop; remove that hardcoded CMD line and
either (a) delete CMD so the image requires the connection string at runtime
(keep ENTRYPOINT ["dumb-init", "/app/substreams-sink-sql", "run"]), or (b)
replace the hardcoded value with an environment-driven value by using an ENV/ARG
and a shell-form CMD that references DATABASE_URL (or similar) so production
must set DATABASE_URL; also update docs to state DATABASE_URL is required in
production.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e13817 and 6a40013.

📒 Files selected for processing (9)
  • .dockerignore
  • .github/workflows/docs.yaml
  • .github/workflows/mobile-expo.yaml
  • .github/workflows/server-build.yaml
  • .github/workflows/substreams-build.yaml
  • .github/workflows/test.yaml
  • .github/workflows/version.yaml
  • contracts/package.json
  • substreams/Dockerfile
🧰 Additional context used
🧠 Learnings (12)
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Use `nx` for running tasks (build, lint, test, e2e) instead of underlying tooling directly

Applied to files:

  • .github/workflows/mobile-expo.yaml
  • .github/workflows/version.yaml
  • .github/workflows/docs.yaml
  • contracts/package.json
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Install dependencies using `pnpm install`

Applied to files:

  • .github/workflows/version.yaml
  • .github/workflows/docs.yaml
📚 Learning: 2026-01-08T19:45:38.852Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: .github/workflows/substreams-build.yaml:2-2
Timestamp: 2026-01-08T19:45:38.852Z
Learning: In the exactly/exa repository, prefer concise inline YAML syntax (e.g., `on: { push: { tags: ["pattern"] }, workflow_dispatch: {} }`) over expanded multi-line format in GitHub workflow files, even if it triggers YAMLlint warnings about formatting.

Applied to files:

  • .github/workflows/substreams-build.yaml
  • .github/workflows/docs.yaml
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Applies to {src/generated/**,server/generated/**,contracts/broadcast/**,contracts/cache/**} : Do not manually edit files in generated directories: `src/generated/`, `server/generated/`, `contracts/broadcast/`, and `contracts/cache/`

Applied to files:

  • .dockerignore
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.sol : All code must pass pnpm --filter plugin test:slither and pnpm --filter plugin test:solhint with zero warnings; do not ignore warnings

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Run contract tests using `pnpm --filter plugin test` and check formatting using `pnpm --filter plugin test:fmt`

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Run all tests using `pnpm test`, which includes TypeScript compilation, ESLint, spell checking, markdown linting, and contract tests

Applied to files:

  • contracts/package.json
📚 Learning: 2026-01-08T19:46:37.829Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: contracts/test/mocks/BobExecute.s.sol:61-71
Timestamp: 2026-01-08T19:46:37.829Z
Learning: Natspec documentation requirements do not apply to test/mock/dev scripts (files in contracts/test/ or with .s.sol extension). Only production contract code in contracts/src/ requires complete natspec comments.

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Applies to contracts/**/*.test.sol : Smart contracts testing: fuzzing required for inputs and gas snapshots mandatory

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.t.sol : Use fuzz testing (testFuzz_...) extensively for functions with numerical or address inputs to cover a wide range of scenarios

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Rely on `pnpm` scripts for all tasks; all setup, testing, and generation must be automated

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.t.sol : Test files must end with .t.sol and mirror the name of the contract they test (e.g., ExaPlugin.sol -> ExaPlugin.t.sol)

Applied to files:

  • contracts/package.json
🪛 Checkov (3.2.334)
substreams/Dockerfile

[low] 1-33: Ensure that HEALTHCHECK instructions have been added to container images

(CKV_DOCKER_2)

🪛 YAMLlint (1.37.1)
.github/workflows/substreams-build.yaml

[warning] 2-2: truthy value should be one of [false, true]

(truthy)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 6-6: too many spaces inside braces

(braces)


[error] 6-6: too many spaces inside braces

(braces)


[error] 11-11: too many spaces inside braces

(braces)


[error] 11-11: too many spaces inside braces

(braces)


[error] 25-25: too many spaces inside braces

(braces)


[error] 25-25: too many spaces inside braces

(braces)

.github/workflows/docs.yaml

[error] 14-14: too many spaces inside braces

(braces)


[error] 14-14: too many spaces inside braces

(braces)

🔇 Additional comments (11)
.github/workflows/version.yaml (1)

27-27: LGTM! Cache path updated correctly.

The NX cache path change from node_modules/.cache/nx to .nx/cache aligns with the updates across other workflows in this PR.

.github/workflows/docs.yaml (2)

14-14: LGTM! Inline YAML format is acceptable.

The NX_DAEMON: false setting is appropriate for CI environments. The inline braces format triggers a YAMLlint warning, but based on learnings, the team prefers concise inline YAML syntax in workflow files.


33-33: LGTM! Cache path updated correctly.

The NX cache path change aligns with updates across other workflows in this PR.

.github/workflows/mobile-expo.yaml (1)

46-46: LGTM! Cache path updated correctly.

The NX cache path change aligns with updates across other workflows in this PR.

.github/workflows/test.yaml (2)

9-9: LGTM! NX daemon disabled for CI.

Setting NX_DAEMON: false is appropriate for CI environments to ensure deterministic builds.


34-34: LGTM! Cache path updated correctly.

The NX cache path change aligns with updates across other workflows in this PR.

contracts/package.json (2)

49-49: LGTM: Well-structured test orchestration with appropriate parallelism settings.

The addition of "parallelism": false to the build target and all test targets is correct, as Forge-based tasks share artifacts in out/ and cache/ directories. Running these tasks in parallel could cause race conditions and build inconsistencies.

The restructured NX targets provide:

  • Clear dependency orchestration through the main test target with nx:noop executor
  • Explicit outputs for each target enabling proper NX caching
  • Granular test targets (test:sizes, test:gas, test:slither, test:coverage) that can be run independently or together

This improves both CI performance (through better caching) and reliability (by preventing parallel execution conflicts).

Also applies to: 61-101


9-9: The removal of test:all target is safe. No references to this target exist elsewhere in the codebase, CI workflows, or documentation, so the restructuring from nx test:all contracts to nx test contracts will not break any existing workflows.

.github/workflows/server-build.yaml (1)

36-41: LGTM! Improved build performance with explicit targeting and caching.

The explicit file specification and GitHub Actions cache configuration will improve build reliability and performance. The mode=max exports all layers for optimal cache reuse.

.dockerignore (1)

1-29: LGTM! Well-structured ignore patterns.

The granular ignore patterns appropriately exclude build artifacts, generated files, and temporary content from the Docker context while preserving necessary files like run-latest.json through negation.

.github/workflows/substreams-build.yaml (1)

1-40: LGTM! Well-structured workflow with proper caching and attestation.

The workflow follows best practices with:

  • GitHub Actions caching for faster builds
  • Build provenance attestation for supply chain security
  • Proper permissions scoped to the job
  • Consistent structure with the server-build workflow

The inline YAML syntax aligns with the repository's preferred style.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

🤖 Fix all issues with AI agents
In @.changeset/breezy-planes-repeat.md:
- Line 5: The changeset summary currently begins with an emoji ("✨ map exa
accounts"); remove the emoji prefix so the summary is a lowercase,
imperative-present-tense sentence (e.g., change "✨ map exa accounts" to "map exa
accounts") and save the updated summary in the changeset file where that line
appears.

In @.changeset/rare-readers-tell.md:
- Around line 1-5: The changeset summary contains an emoji and doesn't follow
the required lowercase imperative style; edit the
.changeset/rare-readers-tell.md summary line (currently "✨ store exa accounts")
to remove the emoji and use a lowercase imperative sentence such as "store exa
accounts" (no emoji, no trailing punctuation) so it conforms to the repository's
.changeset formatting rules.

In @.changeset/red-toys-pump.md:
- Around line 1-5: Update the changeset summary in ".changeset/red-toys-pump.md"
to be a lowercase imperative sentence with no emoji: replace "✨ map exa plugins"
with a concise imperative like "map exa plugins" (all lowercase, no emoji or
punctuation beyond the sentence itself) so the top-level summary follows the
repository changeset guideline.

In @.changeset/violet-poems-heal.md:
- Around line 1-5: Replace the changeset summary string "✨ insert exa plugins
into database" with a lowercase, imperative sentence without emoji or extra
punctuation — e.g., change it to "insert exa plugins into database" so the
summary follows the required lowercase imperative format.

In @.github/workflows/test.yaml:
- Around line 38-42: Replace the current unpinned, unauthenticated download loop
with a robust, pinned-and-verified implementation: add shell safety flags (set
-euo pipefail), introduce environment variables for each release tag (e.g.,
FIREHOSE_VERSION, SUBSTREAMS_VERSION, SUBSTREAMS_SINK_SQL_VERSION) and use those
tags when querying the GitHub Releases API, include the Authorization header
with the GITHUB_TOKEN when calling the API, restrict asset selection to only
.tar.gz assets before downloading, verify each downloaded archive against a
pinned checksum (or .sha256 file) prior to extraction, append "$HOME/.local/bin"
to $GITHUB_PATH so extracted binaries are on PATH, and ensure failures are
detected and exit non‑zero; locate and update the loop that iterates over the
three repos and the curl/tar sequence to implement these changes.

In @cspell.json:
- Around line 3-12: The cspell configuration's ignorePaths only includes
"substreams/target" but should also ignore the other generated substreams
directories listed in .gitignore/.dockerignore; update the "ignorePaths" array
in cspell.json to add "substreams/abi", "substreams/firehose-data",
"substreams/src/contracts", and "substreams/src/proto" so cspell won't run on
generated code before builds or codegen tasks.

In @server/database/schema.ts:
- Around line 69-99: The repo is missing migration files for the new substreams
schema defined by substreams and its tables cursors, substreamsHistory, blocks,
and exaPlugins; generate and add the migration files by running the drizzle-kit
migration generator (e.g., run drizzle-kit generate against the current schema),
commit the created migrations into a migrations directory, and ensure the
generated migration contains the DDL for those tables so future
deploys/migrations reflect the changes.

In @server/script/firehose.ts:
- Around line 100-104: The timestamp parsing loop that calls tsCursor.readTag()
assumes fields 1 (seconds) and 2 (nanoseconds) are varints without validating
tag.wireType, so update the loop in the function that reads timestamps (the for
loop using tsCursor.readTag()) to only call tsCursor.readVarint() for
fieldNumber 1 or 2 when tag.wireType is the varint wire type (e.g., 0 or your
WireType.VARINT enum); otherwise call tsCursor.skip(tag.wireType) so malformed
or changed schemas won’t be mis-parsed.

In @substreams/.gitignore:
- Around line 1-8: The .gitignore entries are currently unanchored and can match
anywhere under substreams; update the patterns to anchored top-level paths
(e.g., change abi, firehose-data, target, src/contracts, src/proto to /abi/,
/firehose-data/, /target/, /src/contracts/, /src/proto/) so they only ignore
those root folders, and confirm that src/proto and src/contracts are generated
artifacts (not source-of-truth) before ignoring them; keep the global file
patterns (*.log, *.spkg) as-is.

In @substreams/build.rs:
- Around line 61-67: The current code uses assert! and expect("formatting
failed") around Command::new("rustfmt") which is inconsistent with Result-based
error handling; replace that block so you call
Command::new("rustfmt").args(...).status()? into a variable (e.g., let status =
Command::new("rustfmt").args(...).status()?;) and then check status.success()
and return an Err with a clear diagnostic (or propagate a context-wrapped error)
if it is false instead of panicking; update the enclosing function signature to
return a Result (e.g., Result<(), Box<dyn std::error::Error>> or anyhow::Result)
if needed so the failure is surfaced consistently.
- Around line 50-59: The current Command::new(...).arg("protogen")...
.status().expect(...); wrapped in assert! is inconsistent and panics; replace
this with Result-style error propagation: call
Command::new(...).arg("protogen")... .status()? to capture the ExitStatus (or
.status().map_err(|e| format!(...))? if not using ?), then check exit.success()
and if false return an Err with a clear context string (e.g.,
format!("substreams protogen failed with status: {}", exit)), avoiding assert!
and .expect(); reference the existing Command::new(...).arg("protogen")...
.env(...) invocation and the uses of .status(), .expect(), and assert! when
making the change.

In @substreams/Dockerfile:
- Line 13: The Dockerfile currently downloads the "latest" substreams CLI which
makes builds non-reproducible; change the RUN that calls the GitHub releases API
to use a fixed, pinable version (e.g., introduce an ARG like SUBSTREAMS_VERSION
and use that tag in the API/query instead of "latest" or hardcode a specific
release tag) so the curl/tar step always fetches the same release; update the
RUN line that builds the URL (the curl -s
https://api.github.com/repos/streamingfast/substreams/releases/...) to reference
that pinned version variable or explicit tag and document the ARG default value
at the top of the Dockerfile.

In @substreams/package.json:
- Around line 44-64: Add an explicit nx target named "test:protolint" to match
the existing targets (so the top-level "test" target's dependsOn entry has a
concrete target), declare any dependencies similar to "test:clippy" (e.g.,
"prepare", "^prepare", or other required setup like "rustup") and set an
appropriate executor (e.g., "nx:run-commands" or the npm script) so the target
is defined consistently alongside "test:clippy" and "test:fmt"; update the
package.json targets section to include this "test:protolint" entry referencing
the same conventions used for the other test targets.

In @substreams/proto/exa.proto:
- Around line 1-17: Buf linter complains because the proto package is declared
as "package exa" but the file lives outside an exa directory; either move this
proto so its module path matches the package (e.g., place the file under an exa
directory so package exa aligns with the filesystem) or add/update buf.yaml to
disable the PACKAGE_DIRECTORY_MATCH rule; update references to the messages
(Accounts, Plugins, Plugins.Plugin) accordingly if you move the file so imports
remain correct.

In @substreams/rust-toolchain.toml:
- Around line 1-4: The toolchain is pinned to nightly via the line channel =
"nightly"; update it to a stable channel (e.g., channel = "stable" or channel =
"1.85") to avoid unnecessary nightly usage while keeping the components =
["clippy", "rustfmt"] and targets = ["wasm32-unknown-unknown"] unchanged; locate
the [toolchain] block and replace only the channel = "nightly" entry with the
chosen stable channel string.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6a40013 and 89f6bb0.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • substreams/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (39)
  • .changeset/breezy-planes-repeat.md
  • .changeset/rare-readers-tell.md
  • .changeset/red-toys-pump.md
  • .changeset/violet-poems-heal.md
  • .dockerignore
  • .github/matcher.json
  • .github/workflows/docs.yaml
  • .github/workflows/mobile-expo.yaml
  • .github/workflows/server-build.yaml
  • .github/workflows/substreams-build.yaml
  • .github/workflows/test.yaml
  • .github/workflows/version.yaml
  • .vscode/extensions.json
  • common/package.json
  • contracts/package.json
  • contracts/test/mocks/BobExecute.s.sol
  • cspell.json
  • docs/package.json
  • pnpm-workspace.yaml
  • server/Dockerfile
  • server/database/schema.ts
  • server/drizzle.config.ts
  • server/package.json
  • server/script/firehose.ts
  • server/test/anvil.ts
  • server/test/database.ts
  • server/test/e2e.ts
  • substreams/.cargo/config.toml
  • substreams/.gitignore
  • substreams/Cargo.toml
  • substreams/Dockerfile
  • substreams/buf.gen.yaml
  • substreams/build.rs
  • substreams/package.json
  • substreams/proto/exa.proto
  • substreams/rust-toolchain.toml
  • substreams/rustfmt.toml
  • substreams/src/lib.rs
  • substreams/substreams.yaml
🧰 Additional context used
📓 Path-based instructions (11)
**/.changeset/*.md

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

Use a lowercase sentence in the imperative present tense for changeset summaries

Files:

  • .changeset/violet-poems-heal.md
  • .changeset/red-toys-pump.md
  • .changeset/rare-readers-tell.md
  • .changeset/breezy-planes-repeat.md
server/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/server.mdc)

server/**/*.ts: Use c.var object to pass strongly-typed data between Hono middleware and route handlers; do not use c.set
All request validation (headers, body, params) must be handled by @hono/valibot-validator middleware; do not perform manual validation inside route handlers
Use Hono's built-in error handling by throwing new HTTPException() for expected errors; unhandled errors will be caught and logged automatically
Enforce Node.js best practices using ESLint plugin:n/recommended configuration
Enforce Drizzle ORM best practices using ESLint plugin:drizzle/all configuration, including requiring where clauses for update and delete operations
Use Drizzle ORM query builder for all database interactions; do not write raw SQL queries unless absolutely unavoidable
All authentication and authorization logic must be implemented in Hono middleware
Do not access process.env directly in application code; load all configuration and secrets once at startup and pass them through dependency injection or context
Avoid long-running, synchronous operations; use async/await correctly and be mindful of CPU-intensive tasks to prevent blocking the event loop

Files:

  • server/drizzle.config.ts
  • server/test/e2e.ts
  • server/test/database.ts
  • server/test/anvil.ts
  • server/script/firehose.ts
  • server/database/schema.ts
**/*.{js,ts,tsx,jsx,sol}

📄 CodeRabbit inference engine (AGENTS.md)

Follow linter/formatter (eslint, prettier, solhint) strictly with high strictness level. No any type.

Files:

  • server/drizzle.config.ts
  • server/test/e2e.ts
  • server/test/database.ts
  • server/test/anvil.ts
  • server/script/firehose.ts
  • contracts/test/mocks/BobExecute.s.sol
  • server/database/schema.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Omit redundant type names in variable declarations - let the type system explain itself

**/*.{ts,tsx}: Use PascalCase for TypeScript types and interfaces
Use valibot for all runtime validation of API inputs, environment variables, and other data; define schemas once and reuse them
Infer TypeScript types from valibot schemas using type User = v.Input<typeof UserSchema> instead of manually defining interfaces

Files:

  • server/drizzle.config.ts
  • server/test/e2e.ts
  • server/test/database.ts
  • server/test/anvil.ts
  • server/script/firehose.ts
  • server/database/schema.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Omit contextual names - don't repeat class/module names in members
Omit meaningless words like 'data', 'state', 'manager', 'engine', 'value' from variable and function names unless they add disambiguation

**/*.{ts,tsx,js,jsx}: Prefer function declarations for all multi-line functions; use function expressions or arrow functions only for single-line implementations
Prefer const for all variable declarations by default; only use let if the variable's value will be reassigned
Declare each variable on its own line with its own const or let keyword, not multiple declarations on one line
Use camelCase for TypeScript variables and functions
Always use import type { ... } for type imports
Use relative paths for all imports within the project; avoid tsconfig path aliases
Follow eslint-plugin-import order: react, external libraries, then relative paths
Use object and array destructuring to access and use properties
Use object method shorthand syntax when a function is a property of an object
Prefer optional chaining (?.), nullish coalescing (??), object and array spreading (...), and for...of loops over traditional syntax
Do not use abbreviations or cryptic names; write out full words like error, parameters, request instead of err, params, req
Use Number.parseInt() instead of the global parseInt() function when parsing numbers
All classes called with new must use PascalCase
Use Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe() instead of the deprecated new Buffer()
Use @ts-expect-error instead of @ts-ignore; follow it immediately with a single-line lowercase comment explaining why the error is expected, without separators like - or :
Do not include the type in a variable's name; let the static type system do its job (e.g., use const user: User not const userObject: User)
Do not repeat the name of a class or module within its members; omit contextual names (e.g., use `class User { getProfil...

Files:

  • server/drizzle.config.ts
  • server/test/e2e.ts
  • server/test/database.ts
  • server/test/anvil.ts
  • server/script/firehose.ts
  • server/database/schema.ts
server/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

server/**/*.{ts,tsx}: Server API: implement schema-first approach using OpenAPI via hono with validation via valibot middleware
Server database: drizzle schema is source of truth. Migrations required. No direct database access in handlers - use c.var.db

Files:

  • server/drizzle.config.ts
  • server/test/e2e.ts
  • server/test/database.ts
  • server/test/anvil.ts
  • server/script/firehose.ts
  • server/database/schema.ts
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

For files with a single default export, name the file identically to the export; for files with multiple exports, use camelCase with a strong preference for a single word

Files:

  • server/drizzle.config.ts
  • server/test/e2e.ts
  • server/test/database.ts
  • server/test/anvil.ts
  • server/script/firehose.ts
  • server/database/schema.ts
**/+(app|src)/**

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

Use kebab-case for directory names and public Expo router routes

Files:

  • substreams/src/lib.rs
contracts/**/*.sol

📄 CodeRabbit inference engine (.cursor/rules/contracts.mdc)

contracts/**/*.sol: Import order must be sorted alphabetically and grouped by type (openzeppelin, solady, internal, etc.), enforced by forge fmt
All functions and state variables must have explicit visibility modifiers (public, private, internal, external)
Use custom errors instead of require() statements with string messages; require() with messages is forbidden
console.log is forbidden in contract code
All external and public functions that modify state must be protected by a reentrancy guard, unless explicitly proven unnecessary
Use modifiers like onlyOwner or function-level checks for privileged functions; default to strictest access level (private > internal > external > public)
Use named parameters in mappings for clarity (e.g., mapping(address user => uint256 balance))
All code must pass pnpm --filter plugin test:slither and pnpm --filter plugin test:solhint with zero warnings; do not ignore warnings
Follow standard Solidity import order: external libraries (openzeppelin, solady) first, then project-internal contracts
All public and external functions and state variables must have complete natspec comments (@notice, @dev, @param, @return)
Use custom errors instead of require statements with string messages for better gas efficiency and error information
Internal and private variables should be prefixed with an underscore (_); function arguments should not be prefixed
Use constant keyword for true constants; use immutable for configurable values like contract addresses
Minimize writes to storage; read data into memory or calldata whenever possible for gas optimization
Use smallest integer size that is safe for the variable (e.g., uint32 for timestamps) and be aware of struct packing for gas efficiency
Prefer using the solady library over openzeppelin where possible, as it is highly optimized for gas
Correctly label functions as view or pure when they do not modify state

contracts/**/*.sol: Smart contracts: use custom errors error MyError(); instead of s...

Files:

  • contracts/test/mocks/BobExecute.s.sol
**/*.sol

📄 CodeRabbit inference engine (.cursor/rules/style.mdc)

**/*.sol: Use camelCase for Solidity variables and functions
Use PascalCase for Solidity contracts and structs

Files:

  • contracts/test/mocks/BobExecute.s.sol
server/database/schema.ts

📄 CodeRabbit inference engine (.cursor/rules/server.mdc)

server/database/schema.ts: Database schema must be defined in server/database/schema.ts as the single source of truth for database structure
Define relations explicitly in the schema file to enable Drizzle to generate typed query results including related entities

Files:

  • server/database/schema.ts
🧠 Learnings (39)
📚 Learning: 2026-01-08T19:45:38.852Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: .github/workflows/substreams-build.yaml:2-2
Timestamp: 2026-01-08T19:45:38.852Z
Learning: In the exactly/exa repository, prefer concise inline YAML syntax (e.g., `on: { push: { tags: ["pattern"] }, workflow_dispatch: {} }`) over expanded multi-line format in GitHub workflow files, even if it triggers YAMLlint warnings about formatting.

Applied to files:

  • .github/workflows/docs.yaml
  • .github/workflows/substreams-build.yaml
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Use `nx` for running tasks (build, lint, test, e2e) instead of underlying tooling directly

Applied to files:

  • .github/workflows/docs.yaml
  • .github/workflows/mobile-expo.yaml
  • .github/workflows/version.yaml
  • contracts/package.json
  • substreams/package.json
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Install dependencies using `pnpm install`

Applied to files:

  • .github/workflows/docs.yaml
  • .github/workflows/version.yaml
📚 Learning: 2026-01-08T20:26:33.396Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: .vscode/extensions.json:6-6
Timestamp: 2026-01-08T20:26:33.396Z
Learning: For validating VS Code extension IDs in this repository, check both the official VS Code Marketplace (marketplace.visualstudio.com) and the Open VSX registry (open-vsx.org) before flagging an extension as invalid. Open VSX is a widely-used alternative registry, so a valid ID may appear in either registry. If an ID is not present in both registries, flag it as invalid. Apply this check specifically when validating the IDs listed in .vscode/extensions.json.

Applied to files:

  • .vscode/extensions.json
📚 Learning: 2025-12-31T00:23:55.034Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 610
File: .changeset/ready-experts-fly.md:1-2
Timestamp: 2025-12-31T00:23:55.034Z
Learning: In the exactly/exa repository, allow and require empty changeset files (containing only --- separators) when changes are not user-facing and do not warrant a version bump. This is needed because CI runs changeset status --since origin/main and requires a changeset file to exist. Ensure such empty changesets are used only for non-user-facing changes and document the rationale in the commit or changelog notes.

Applied to files:

  • .changeset/violet-poems-heal.md
  • .changeset/red-toys-pump.md
  • .changeset/rare-readers-tell.md
  • .changeset/breezy-planes-repeat.md
📚 Learning: 2025-12-30T15:03:28.449Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/.changeset/*.md : Use a lowercase sentence in the imperative present tense for changeset summaries

Applied to files:

  • .changeset/violet-poems-heal.md
  • .changeset/rare-readers-tell.md
  • .changeset/breezy-planes-repeat.md
📚 Learning: 2025-12-23T19:57:14.465Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/git.mdc:0-0
Timestamp: 2025-12-23T19:57:14.465Z
Learning: Select the most precise gitmoji that best communicates the intent of the change (e.g., ✨ for new feature, 🐛 for bug fix, ♻️ for refactoring, ⚡️ for performance improvement, 📝 for documentation, 🚀 for deployment)

Applied to files:

  • .changeset/violet-poems-heal.md
  • .changeset/red-toys-pump.md
  • .changeset/rare-readers-tell.md
  • .changeset/breezy-planes-repeat.md
📚 Learning: 2025-12-23T19:57:14.465Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/git.mdc:0-0
Timestamp: 2025-12-23T19:57:14.465Z
Learning: All commits must start with a single gitmoji from the official gitmoji list; this is not optional and the emoji visually communicates the intent of the change

Applied to files:

  • .changeset/violet-poems-heal.md
  • .changeset/breezy-planes-repeat.md
📚 Learning: 2025-12-23T19:57:14.465Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/git.mdc:0-0
Timestamp: 2025-12-23T19:57:14.465Z
Learning: Do not use conventional commit prefixes like `feat:`, `fix:`, or `docs(...)`; do not use title case or sentence case in the commit subject line; do not create commits without a scope

Applied to files:

  • .changeset/violet-poems-heal.md
  • .changeset/breezy-planes-repeat.md
📚 Learning: 2025-12-23T19:57:14.465Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/git.mdc:0-0
Timestamp: 2025-12-23T19:57:14.465Z
Learning: All commit messages must strictly adhere to the format: `<emoji> <scope>: <message>` where emoji is a gitmoji unicode character, scope is a mandatory short name (app, server, contracts, docs, common, dependencies, github, config, or repo), and message is a short lowercase description

Applied to files:

  • .changeset/violet-poems-heal.md
  • .changeset/breezy-planes-repeat.md
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/database/schema.ts : Define relations explicitly in the schema file to enable Drizzle to generate typed query results including related entities

Applied to files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • substreams/substreams.yaml
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Applies to server/**/*.{ts,tsx} : Server database: drizzle schema is source of truth. Migrations required. No direct database access in handlers - use `c.var.db`

Applied to files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • substreams/substreams.yaml
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/database/schema.ts : Database schema must be defined in `server/database/schema.ts` as the single source of truth for database structure

Applied to files:

  • server/drizzle.config.ts
  • substreams/substreams.yaml
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/database/migrations/** : All schema changes must be accompanied by a migration file generated by `drizzle-kit`; never edit the database schema manually

Applied to files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • substreams/substreams.yaml
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Enforce Drizzle ORM best practices using ESLint `plugin:drizzle/all` configuration, including requiring `where` clauses for `update` and `delete` operations

Applied to files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • server/database/schema.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Use Drizzle ORM query builder for all database interactions; do not write raw SQL queries unless absolutely unavoidable

Applied to files:

  • server/drizzle.config.ts
  • server/test/database.ts
  • server/database/schema.ts
📚 Learning: 2025-12-30T15:03:28.449Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Omit meaningless words from names like `data`, `state`, `manager`, `engine`, `object`, `entity`, and `instance`

Applied to files:

  • cspell.json
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Omit meaningless words like 'data', 'state', 'manager', 'engine', 'value' from variable and function names unless they add disambiguation

Applied to files:

  • cspell.json
  • server/script/firehose.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Zero config local dev environment: no `.env` files, mock all external services

Applied to files:

  • server/test/e2e.ts
  • contracts/test/mocks/BobExecute.s.sol
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.sol : All public and external functions and state variables must have complete natspec comments (notice, dev, param, return)

Applied to files:

  • substreams/proto/exa.proto
  • contracts/test/mocks/BobExecute.s.sol
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Avoid long-running, synchronous operations; use `async/await` correctly and be mindful of CPU-intensive tasks to prevent blocking the event loop

Applied to files:

  • server/test/database.ts
  • server/script/firehose.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Server stack: use hono, node.js, drizzle orm, postgres

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Server (exactly/server) uses Hono (framework), Node.js, Drizzle ORM (database), and PostgreSQL

Applied to files:

  • server/test/database.ts
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Applies to {src/generated/**,server/generated/**,contracts/broadcast/**,contracts/cache/**} : Do not manually edit files in generated directories: `src/generated/`, `server/generated/`, `contracts/broadcast/`, and `contracts/cache/`

Applied to files:

  • common/package.json
  • substreams/build.rs
  • .dockerignore
📚 Learning: 2026-01-08T19:54:26.885Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: substreams/rust-toolchain.toml:1-4
Timestamp: 2026-01-08T19:54:26.885Z
Learning: Rust edition 2024 was released as stable on February 20, 2025 with Rust 1.85.0 and does not require nightly channel.

Applied to files:

  • substreams/Cargo.toml
  • substreams/rust-toolchain.toml
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.sol : Use custom errors instead of require statements with string messages for better gas efficiency and error information

Applied to files:

  • substreams/build.rs
📚 Learning: 2026-01-08T19:54:10.673Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: substreams/src/lib.rs:54-71
Timestamp: 2026-01-08T19:54:10.673Z
Learning: In substreams/src/lib.rs in the map_exa_plugins function, the deduplication logic using `!seen.insert(address.to_vec())` is intentional. The first plugin installed for a new account is an owner plugin (not an exa plugin), so it must be filtered out. The negation ensures the first plugin is skipped and subsequent plugins (the actual exa plugins) are kept.

Applied to files:

  • substreams/src/lib.rs
📚 Learning: 2025-12-30T15:03:28.449Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Do not use abbreviations or cryptic names; write out full words like `error`, `parameters`, `request` instead of `err`, `params`, `req`

Applied to files:

  • server/script/firehose.ts
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.sol : All code must pass pnpm --filter plugin test:slither and pnpm --filter plugin test:solhint with zero warnings; do not ignore warnings

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Run contract tests using `pnpm --filter plugin test` and check formatting using `pnpm --filter plugin test:fmt`

Applied to files:

  • contracts/package.json
📚 Learning: 2026-01-08T19:46:37.829Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: contracts/test/mocks/BobExecute.s.sol:61-71
Timestamp: 2026-01-08T19:46:37.829Z
Learning: Natspec documentation requirements do not apply to test/mock/dev scripts (files in contracts/test/ or with .s.sol extension). Only production contract code in contracts/src/ requires complete natspec comments.

Applied to files:

  • contracts/package.json
  • contracts/test/mocks/BobExecute.s.sol
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Run all tests using `pnpm test`, which includes TypeScript compilation, ESLint, spell checking, markdown linting, and contract tests

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Applies to contracts/**/*.test.sol : Smart contracts testing: fuzzing required for inputs and gas snapshots mandatory

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.t.sol : Use fuzz testing (testFuzz_...) extensively for functions with numerical or address inputs to cover a wide range of scenarios

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:56.405Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/dev.mdc:0-0
Timestamp: 2025-12-23T19:56:56.405Z
Learning: Rely on `pnpm` scripts for all tasks; all setup, testing, and generation must be automated

Applied to files:

  • contracts/package.json
📚 Learning: 2026-01-09T15:14:32.535Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: contracts/package.json:13-13
Timestamp: 2026-01-09T15:14:32.535Z
Learning: In Slither with Foundry, the `--skip-clean` flag only skips an initial cleanup step but still runs `forge build --force`, ensuring fresh build artifacts. There's no risk of stale artifacts causing false negatives.

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.t.sol : Test files must end with .t.sol and mirror the name of the contract they test (e.g., ExaPlugin.sol -> ExaPlugin.t.sol)

Applied to files:

  • contracts/package.json
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.t.sol : Use vm.prank to simulate calls from different addresses; avoid changing msg.sender through other means

Applied to files:

  • contracts/test/mocks/BobExecute.s.sol
📚 Learning: 2025-12-23T19:56:43.683Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/contracts.mdc:0-0
Timestamp: 2025-12-23T19:56:43.683Z
Learning: Applies to contracts/**/*.sol : Use smallest integer size that is safe for the variable (e.g., uint32 for timestamps) and be aware of struct packing for gas efficiency

Applied to files:

  • server/database/schema.ts
🧬 Code graph analysis (2)
server/test/database.ts (1)
server/database/schema.ts (1)
  • substreams (69-69)
substreams/src/lib.rs (1)
server/database/schema.ts (1)
  • substreams (69-69)
🪛 Buf (1.62.1)
substreams/proto/exa.proto

3-3: Files with package "exa" must be within a directory "exa" relative to root but were in directory "substreams/proto".

(PACKAGE_DIRECTORY_MATCH)

🪛 Checkov (3.2.334)
substreams/Dockerfile

[low] 1-33: Ensure that HEALTHCHECK instructions have been added to container images

(CKV_DOCKER_2)

🪛 YAMLlint (1.37.1)
.github/workflows/docs.yaml

[error] 14-14: too many spaces inside braces

(braces)


[error] 14-14: too many spaces inside braces

(braces)

.github/workflows/substreams-build.yaml

[warning] 2-2: truthy value should be one of [false, true]

(truthy)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 2-2: too many spaces inside braces

(braces)


[error] 6-6: too many spaces inside braces

(braces)


[error] 6-6: too many spaces inside braces

(braces)


[error] 11-11: too many spaces inside braces

(braces)


[error] 11-11: too many spaces inside braces

(braces)


[error] 25-25: too many spaces inside braces

(braces)


[error] 25-25: too many spaces inside braces

(braces)

🔇 Additional comments (48)
.vscode/extensions.json (2)

6-6: Verify drblury.protobuf-vsc publisher casing + Open VSX availability (if you support VSCodium). (marketplace.visualstudio.com)

Marketplace lists this as DrBlury.protobuf-vsc; if any tooling treats IDs as case-sensitive, the current lower-case form could fail to resolve. Based on learnings, also check Open VSX before concluding it’s invalid/unavailable.

Proposed tweak (only if casing is required)
-    "drblury.protobuf-vsc",
+    "DrBlury.protobuf-vsc",

16-16: rust-lang.rust-analyzer is a solid recommendation for the new Rust surface area. (marketplace.visualstudio.com)

No concerns with the identifier.

substreams/.cargo/config.toml (1)

1-2: LGTM!

Standard Cargo configuration for WASM builds. The wasm32-unknown-unknown target is appropriate for substreams WASM modules.

pnpm-workspace.yaml (1)

7-7: LGTM!

Correctly adds the substreams package to the pnpm workspace, aligning with the PR's objective to introduce initial substreams implementation.

.github/workflows/docs.yaml (2)

14-14: LGTM!

The inline YAML format aligns with the team's preference for concise syntax in workflow files. The YAMLlint warning can be safely ignored.

Based on learnings, concise inline YAML syntax is preferred in this repository even if it triggers formatting warnings.


33-33: LGTM!

Cache path update from node_modules/.cache/nx to .nx/cache correctly aligns with the broader NX cache location changes across workflows in this PR.

.github/workflows/version.yaml (1)

27-27: LGTM!

Cache path update correctly aligns with the NX cache location changes applied consistently across multiple workflows in this PR.

.github/workflows/mobile-expo.yaml (1)

44-48: LGTM: Nx cache path updated to .nx/cache.

This aligns with the newer Nx default cache directory and should improve cache portability across installs.

server/drizzle.config.ts (1)

8-9: schemaFilter is fully supported in drizzle-kit 0.31.8 and the configuration is correctly implemented. The schemaFilter: ["public", "substreams"] explicitly includes only those schemas during introspection and migrations—it does not hide required tables; it intentionally filters to specified schemas. No changes needed.

substreams/rustfmt.toml (1)

1-6: All rustfmt options in this configuration are valid and supported by the nightly toolchain. The tab_spaces = 2 setting is intentional (the default is 4 spaces), representing a deliberate project choice for 2-space indentation. No action required.

.github/workflows/server-build.yaml (2)

40-41: GHA cache enablement looks good; confirm it is scoped as intended across environments.


32-41: Build context configuration is already properly safeguarded.

The root .dockerignore is present and well-maintained, aggressively excluding build artifacts (dist, cache, build, generated), dependencies (node_modules), secrets (.env), and development files (.log, .md, .tsbuildinfo). The multi-stage Dockerfile further minimizes risk by keeping only production dependencies in the final image. The context: . setup is safe as-is.

server/test/e2e.ts (1)

7-7: Type-only import is fine for importOriginal<typeof sentry>() typing.

.github/workflows/test.yaml (1)

8-36: NX daemon disable + cache relocation look good for CI stability.

docs/package.json (1)

31-45: No changes needed. The dependentTasksOutputFiles: "server/generated/openapi.json" path is correct and properly reflects the workspace-relative output location of the server package's generate:openapi target.

common/package.json (1)

44-50: LGTM!

The addition of dependentTasksOutputFiles entries correctly establishes dependencies on contract build outputs (contracts/out/**/*.json) and broadcast artifacts (contracts/broadcast/*/31337/*.json). This ensures the wagmi generation target properly invalidates its cache when upstream contract artifacts change.

server/test/database.ts (2)

49-70: Well-structured process management with proper cancellation.

The use of AbortController with cancelSignal and forceKillAfterDelay is appropriate for managing long-running external processes. Error handling correctly ignores abort-related errors while propagating unexpected failures.


90-93: Clean teardown with graceful process termination.

Using Promise.allSettled ensures all cleanup operations complete regardless of individual failures, which is the correct pattern for teardown functions.

contracts/test/mocks/BobExecute.s.sol (2)

39-39: LGTM!

Using getAddress instead of createAccount is appropriate here since setUp only needs to derive the account address for labeling and testing, not actually deploy it.


61-71: LGTM!

The BatchDeploy helper contract efficiently creates multiple test accounts in a single transaction. The sequential address generation (420 + i) provides deterministic test addresses. Based on learnings, natspec documentation is not required for test/mock contracts.

server/test/anvil.ts (1)

159-160: LGTM!

Adding --tc BobExecuteScript correctly targets only the intended test contract, which is necessary now that the file contains the additional BatchDeploy contract.

server/script/firehose.ts (2)

135-135: Verify the output format uses libNum twice intentionally.

The output duplicates libNum in positions 5 and 6:

FIRE BLOCK ${num} ${hash} ${libNum} ${parentHash} ${libNum} ${timestamp} ${newPayload}

The input has 7 tokens with presumably different values at positions 5 and 6. Please confirm this duplication is the intended protocol transformation.


4-65: Well-implemented minimal protobuf cursor.

The ProtoCursor class correctly handles varint decoding with proper overflow protection and supports the common wire types (0, 1, 2, 5). The implementation avoids heavy protobuf library dependencies for this lightweight processing task.

contracts/package.json (3)

61-71: LGTM!

The composite test target with executor: "nx:noop" properly orchestrates all test subtasks through dependsOn. This provides granular caching while maintaining a single entry point for running all tests.


72-92: LGTM!

Setting parallelism: false on forge-based targets prevents concurrent executions that could cause file system conflicts with shared out and cache directories. The explicit outputs declarations enable proper NX caching.


13-13: LGTM!

The --skip-clean flag is appropriate here. Based on learnings, Slither with Foundry still runs forge build --force internally, ensuring fresh artifacts without risk of stale data.

server/database/schema.ts (2)

35-35: LGTM!

The updated destructured syntax ({ account }) => [...] is cleaner and aligns with the pattern used in the new tables. This is a minor stylistic improvement with no functional change.


87-90: Consider using appropriate integer types for block timestamps.

Per coding guidelines, the smallest safe integer size should be used. Unix timestamps in seconds fit in integer (32-bit), but if timestamp stores nanoseconds (as suggested by the firehose script's seconds * 1_000_000_000n + nanoseconds), then bigint is correct. Please confirm the timestamp unit.

substreams/buf.gen.yaml (1)

1-8: LGTM!

The Buf code-generation configuration is well-structured with pinned plugin versions for reproducibility. The output directory src/proto aligns with the Rust code that consumes these generated types.

server/package.json (2)

64-64: LGTM!

Adding @exactly/substreams as a workspace dev dependency appropriately integrates the new substreams package for server-side tooling and build processes.


143-146: LGTM!

Adding dependentTasksOutputFiles for contract build artifacts ensures proper NX cache invalidation when contract outputs change, establishing the correct dependency chain for the generate:broadcasts target.

substreams/Dockerfile (1)

20-33: LGTM!

The runtime stage follows security best practices: non-root user (substreams), proper signal handling with dumb-init, and minimal runtime image. The static analysis hint about missing HEALTHCHECK is acceptable for a sink container that connects to external Postgres—health is better determined by the orchestration layer.

substreams/Cargo.toml (1)

1-35: LGTM!

The Cargo manifest is well-configured for a substreams WASM library:

  • Edition 2024 is appropriately used (stable since Rust 1.85.0)
  • cdylib crate type is correct for WASM output
  • Release profile optimizes for size (opt-level = 's', LTO, strip debuginfo), which is ideal for WASM deployment
.github/workflows/substreams-build.yaml (1)

1-40: LGTM!

The workflow is well-structured with:

  • Appropriate permissions scoped for packages, attestations, and id-token
  • Comprehensive Docker tag patterns for semantic versioning
  • GHA caching for build performance
  • Build provenance attestation for supply chain security

The YAMLlint warnings about inline brace syntax are intentional per repository conventions that prefer concise inline YAML syntax. Based on learnings, this style is preferred in the exactly/exa repository.

substreams/package.json (1)

1-66: LGTM! Well-structured nx configuration.

The package manifest properly integrates with the nx workspace. The build target correctly specifies inputs (including CHAIN_ID env and contract artifacts) and outputs, and follows the learning to use nx for running tasks via "test": "nx test substreams".

substreams/build.rs (1)

13-48: LGTM! Well-structured codegen workflow.

The ABI extraction and Rust binding generation flow is well-implemented with proper error context via with_context. The generated module file correctly applies #[expect(clippy::style, clippy::complexity)] to suppress expected lints on generated code.

substreams/substreams.yaml (2)

54-82: Verify hardcoded factory addresses match deployed contracts.

The factory addresses are hardcoded per network. Please ensure these addresses are correct and consider adding a comment or documentation reference for each address to aid future maintenance.


1-52: LGTM! Well-structured Substreams manifest.

The module pipeline (map_exa_accountsstore_exa_accountsmap_exa_pluginsdb_out) correctly chains data flow. The sink configuration properly references the server schema and the external SPKG imports are version-pinned.

substreams/src/lib.rs (5)

53-71: LGTM! Deduplication logic is correct and intentional.

The !seen.insert(address.to_vec()) logic correctly skips the first plugin (the owner plugin) for new accounts and keeps subsequent exa plugins. The ? operator on get_last() correctly short-circuits in the filter_map context when the account isn't in the store. Based on learnings, this pattern is intentional.


27-44: LGTM! Clean event filtering implementation.

The factory address decoding and event filtering chain is well-structured. The use of try_for_each semantics via collect::<Result<Vec<_>, _>>() ensures proper error propagation from hex decoding.


46-51: LGTM!

The store handler correctly persists a flag for each account using the hex-encoded address as the key.


73-88: LGTM! Database output handler.

The db_out handler correctly upserts plugin rows and conditionally creates a blocks entry when there are changes. The use of Tables API aligns with the substreams-sink-database-changes pattern.


77-85: Update table names to include schema qualification for substreams sink compatibility.

The code references "exa_plugins" (line 78) and "blocks" (line 84) without schema qualification. However, the server schema (server/database/schema.ts) defines both tables under the substreams pgSchema, and drizzle-kit export generates schema-qualified SQL (e.g., CREATE TABLE "substreams"."exa_plugins"). The substreams-sink-sql sink uses the table name string directly, so plain table names will not match the schema-qualified tables in the database.

Update the table references to:

  • "substreams.exa_plugins" (line 78)
  • "substreams.blocks" (line 84)
⛔ Skipped due to learnings
Learnt from: cruzdanilo
Repo: exactly/exa PR: 627
File: substreams/src/lib.rs:54-71
Timestamp: 2026-01-08T19:54:10.673Z
Learning: In substreams/src/lib.rs in the map_exa_plugins function, the deduplication logic using `!seen.insert(address.to_vec())` is intentional. The first plugin installed for a new account is an owner plugin (not an exa plugin), so it must be filtered out. The negation ensures the first plugin is skipped and subsequent plugins (the actual exa plugins) are kept.
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Applies to server/**/*.{ts,tsx} : Server database: drizzle schema is source of truth. Migrations required. No direct database access in handlers - use `c.var.db`
cspell.json (1)

14-165: New Rust/Substreams dictionary entries look good.
The added terms match the introduced toolchain and should reduce false positives.

.dockerignore (2)

6-10: Negation for run-latest.json is correctly ordered.
!**/contracts/broadcast/**/run-latest.json comes after the **/*.json ignore, so it should be included as intended.


6-25: No action needed — the excluded server artifacts are all safely generated during the Docker build.

The entries **/server/app, **/server/drizzle, and **/server/openapi.json are safe to ignore. server/app is generated from dist during the build (line 23 of Dockerfile), server/drizzle is a cache directory, and server/openapi.json does not exist as a tracked file (the actual generated file is server/generated/openapi.json). None are pre-existing requirements for the Docker build.

.github/matcher.json (2)

81-92: rust-formatter matcher seems reasonable; please confirm output format matches your formatter invocation.
If the tool prints Diff in <file> at line <n>: (as your regex expects), this will annotate PRs nicely.


64-92: The severity concern is not a real issue; the Windows path limitation doesn't apply to this project.

The rustc compiler outputs warning: and error: in lowercase in human-readable format, so the regex captures will always be lowercase—GitHub Actions receives the correct normalized values it expects. The regex defensively allows capitalized variants, but they never occur in actual rustc output.

The Windows path issue is technically valid (: in filenames like C:\path), but this project has no Windows CI workflows and is Unix-focused. The proposed .+? fix doesn't fundamentally solve Windows path ambiguity anyway; the pattern is inherently brittle for paths containing colons.

The rust-formatter matcher is correct as written.

@cruzdanilo cruzdanilo merged commit 89f6bb0 into main Jan 9, 2026
4 of 6 checks passed
@cruzdanilo cruzdanilo deleted the substreams branch January 9, 2026 16:40
@coderabbitai coderabbitai bot mentioned this pull request Jan 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant