Skip to content

feat: index the Ethereum Follow Protocol (EFP) and expose it via the Omnigraph API#2224

Open
Quantumlyy wants to merge 46 commits into
namehash:mainfrom
Quantumlyy:Quantumlyy/efp-plugin
Open

feat: index the Ethereum Follow Protocol (EFP) and expose it via the Omnigraph API#2224
Quantumlyy wants to merge 46 commits into
namehash:mainfrom
Quantumlyy:Quantumlyy/efp-plugin

Conversation

@Quantumlyy
Copy link
Copy Markdown

@Quantumlyy Quantumlyy commented May 29, 2026

Status: draft. Delivers the root efp namespace plus account-rooted access: account(by:) { efp { ... } } (an account's validated primaryList and its lists) and an EfpListRecord.account edge linking a record to the Account it points at.

Open question to discuss (please weigh in; blocks marking ready): EfpListRecord.account and account(by:) resolve to null for an address with no row in the accounts table, and that table only gets rows for addresses the ENS indexer touches (owners, registrants, and so on). Most EFP followees are arbitrary addresses with no ENS presence, so today the cross-user walk record.account.efp.primaryList is null for them. (The EFP data for any address is still reachable via the root efp.primaryList(recordData).)

To make the account-rooted walk universal, AccountRef.load could synthesize a bare { id } Account for a missing key: the accounts row carries no data beyond the address, so every Account field already derives from the id. That is a roughly 3-line change, but it flips core Account semantics, since account(by: { address }) would become non-null for every unseen address, affecting all consumers and not just EFP. Because Account is a shared core type, this is raised here rather than decided inside this PR. Do we want Account to be address-centric (always resolvable), or keep null-for-unknown?

Reviewer Focus (Read This First)

Spend the most time on three areas:

  1. Data model and handlers (packages/ensdb-sdk/.../efp.schema.ts, apps/ensindexer/src/plugins/efp/handlers/). The PK-only access pattern is the load-bearing design: a (chainId, contract, slot) to tokenId reverse index (efp_list_storage_locations) lets list-metadata events resolve the owning NFT by primary key, and a record's tags are an embedded text[] rather than a join table, so removing a record is a single primary-key delete with no cascade.
  2. Byte decoders (apps/ensindexer/src/plugins/efp/lib/parse-*.ts, list-metadata.ts). These are the trust boundary for arbitrary on-chain input: version, length, record-type, and canonical-key validation.
  3. Omnigraph surface (apps/ensapi/src/omnigraph-api/schema/efp*.ts), especially primaryList and its two-step validation.

Least confident: handler runtime behavior. The parsers and types are verified, but no integration test exercises the handlers against live EFP data yet (see Testing Evidence).

Problem & Motivation

The Omnigraph exists to be "the graph of all things": one API unifying indexed ENS data, dynamic ENS resolutions, ENSv1 and ENSv2, and highly-ENS-adjacent protocols in a single query. EFP is the canonical adjacent protocol that vision points to. It is the on-chain social graph keyed by Ethereum address, with ENS as the identity layer.

This PR delivers the first such integration. EFP is indexed as its own efp_* model and surfaced through the Omnigraph under a single efp root, so one query can resolve a name's address, its validated primary EFP list, and who follows it. It is fully opt-in and independent of the ENS surfaces: the efp namespace appears when the plugin is enabled, and is a no-op otherwise.

What Changed (Concrete)

  1. New opt-in efp plugin (apps/ensindexer/src/plugins/efp/): Ponder config, event handlers, pure byte decoders, constants, README. Enabled via PLUGINS, activatable only on the mainnet ENS namespace (enforced by datasource presence).
  2. Datasource catalog entries EFPBase, EFPOptimism, EFPEthereum with addresses, ABIs, and start blocks for ListRegistry, AccountMetadata, and ListRecords.
  3. ENSDb abstract schema (efp.schema.ts): efp_lists, efp_list_storage_locations, efp_list_records (tags embedded as text[]), efp_account_metadata, efp_pending_list_metadata.
  4. Indexing: list NFTs (ListRegistry, Base), list records and tags (ListRecords, Base, Optimism, Ethereum mainnet), account metadata (AccountMetadata, Base, including primary-list).
  5. Omnigraph API: a single efp root namespace (list / lists, listRecords with each record's tags, owning list, and target account, accountMetadata / accountMetadatas, validated primaryList(address)), plus account-rooted access: Account.efp (an account's validated primaryList and the lists it is the user of) and an EfpListRecord.account edge resolving a record's target address to its Account. Together these let one query walk from an account to whom it follows and on into ENS names and EFP lists. Cursor-paginated connections.
  6. Regenerated Omnigraph SDL and introspection in enssdk.
  7. Changesets for the affected packages.

Example queries

A forward join: an account, its validated primary list, and the accounts it follows.

# "Show me an account's primary list and everyone it follows."
query AccountGraph($account: Address!) {
  efp {
    primaryList(address: $account) {
      tokenId
      user
      records(first: 100) {
        totalCount
        edges {
          node {
            recordData   # each account this list follows
            tags         # e.g. "top8", "block", "mute"
          }
        }
      }
    }
  }
}

A reverse join, correlating one account to other users: who follows a target, and which user owns each following list.

# "Who follows this account, and which users own those lists?"
query Followers($target: Address!) {
  efp {
    listRecords(where: { recordData: $target }, first: 100) {
      totalCount
      edges {
        node {
          tags
          list {
            tokenId
            user    # the follower account behind this list
            owner
          }
        }
      }
    }
  }
}

The first traverses primaryList to its records; the second traverses listRecords to each record.list and its user. Both resolve in one request through object edges, with no client-side stitching.

Account-rooted, the same graph walks across users and into ENS names in a single query:

# An account's ENS name, its EFP lists, and (per followed account) their ENS name and own primary list.
query AccountEfp($address: Address!) {
  account(by: { address: $address }) {
    domains(first: 1) { edges { node { canonical { name { interpreted } } } } }
    efp {
      lists { totalCount }
      primaryList {
        tokenId
        records(first: 50) {
          edges {
            node {
              recordData
              tags
              account {
                domains(first: 1) { edges { node { canonical { name { interpreted } } } } }
                efp { primaryList { tokenId } }
              }
            }
          }
        }
      }
    }
  }
}

Today account is null for a followee with no ENS presence (see the open question at the top); efp.primaryList(recordData) resolves any address regardless.

Design & Planning

  • Structure mirrors the tokenscope plugin, the closest in-repo analog (a standalone plugin that indexes its own contracts and needs no ENS Registry/Resolver data). The implementation started from the efpnode proof-of-concept and was reworked to be idiomatic: contracts come from the datasource catalog, handlers write directly to context.ensDb by primary key, and the PoC's EFPStore abstraction, hard-coded addresses, and multi-column WHERE lookups were removed.
  • PK-only access (the repo hot-path rule) drove two model choices: the slot-to-tokenId reverse index, and embedding tags on the record. The embedded-array read-modify-write follows an existing production pattern (the subgraph Resolver texts column).
  • Alternatives considered and rejected:
    • A separate efp_list_record_tags join table with a cascade delete on REMOVE_RECORD: rejected because the cascade is a non-PK write in the indexing hot path, and re-adding a removed record would resurrect stale tags.
    • Rejecting junk-suffixed records outright: rejected because normalize-and-key keeps parity with the canonical api-v2 indexer at no cost, and avoids orphaning a clean add paired with a junk-suffixed remove.
  • Excluded as non-spec: the off-chain / locationType = 2 list storage, and the eth.efp.list ENS text-record convention. Neither is part of the EFP spec.
  • Planning artifacts: an implementation plan plus the EFP spec pages (List Ops, List Records, List Metadata, List Storage Location, Account Metadata). No separate shared design doc.
  • Reviewed / approved by: no formal human pre-review; see Self-Review.

Self-Review

Bugs caught while reviewing the diff end-to-end:

  1. Record id and record column used the untruncated payload while tag and remove ops referenced the canonical 22-byte prefix, so tags would not join and a junk-suffixed record would orphan on removal. Fixed by keying every record by the canonical prefix.
  2. Reserved (non-type-1) records were exposed through a non-null Address scalar. Now only type-1 address records are indexed.
  3. Non-version-1 ListOps, records, and storage locations were decoded as v1. Now rejected before dispatch.
  4. Non-20-byte role metadata stored a truncated or empty address. Now clears the role instead.
  5. ENSApi resolvers used the indexer's find() API where the client is plain Drizzle. Fixed to .select().limit(1).

Logic simplified: REMOVE_RECORD went from a record delete plus a non-PK tag cascade to a single primary-key delete (tags travel with the row).

Naming: aligned to "Unigraph" (data model) versus "Omnigraph" (API); consolidated all EFP queries under one efp root so they do not clutter the Query root.

Dead code removed: the eth.efp.list Resolver subscription, its parser, and the efp_ens_list_pointers table, when that feature was cut as non-spec.

Cross-Codebase Alignment

  • Search terms used: efpListRecordTags, metadataValueToAddress, .array( (onchainTable array columns), attach_Resolver, texts (array writes in handlers).
  • Reviewed but unchanged: the tokenscope plugin (structural model); the subgraph Resolver handler (confirmed the embedded-array read-modify-write pattern at Resolver.ts:202 and :414); the Omnigraph builder and connection helpers.
  • Confirmed EFP is independent: not in the alpha preset, no compatibility coupling, handlers and contracts gated on the plugin being enabled. Root typecheck confirms no other workspace consumes the efp_* tables.

Downstream & Consumer Impact

  • Public APIs: adds the efp root field to the Omnigraph GraphQL API. Additive and behind the plugin; no change to existing ENS queries. SDL and introspection regenerated.
  • Operators: enabling the plugin needs Base, Optimism, and Ethereum RPCs and the mainnet namespace. A re-index is implied by the new schema and handlers.
  • Readers and maintainers: new efp_* tables and the efp namespace. One term to call out: "primary list" is validated (the primary-list metadata plus a user-role match), distinct from the raw primary-list claim that accountMetadatas returns.
  • Docs updated: plugin README and changesets.

Testing Evidence

  • Testing performed: unit tests for all byte decoders (parse-list-op, parse-list-storage-location, list-metadata). Full repo verification passes: root typecheck across all workspaces, Biome, pnpm generate (no SDL or introspection diff), and the full test suite with no regressions.
  • Known gaps: handler runtime behavior is not integration-tested yet. No EFP devnet exists; one is planned, similar to the ENS devnet. The embedded-array read-modify-write is de-risked by the existing subgraph Resolver precedent, so the residual gap is EFP-specific handler wiring, not the text[] round-trip.
  • What breaks first if wrong: the tag set on a record (add, dedup, remove, re-add starts empty), canonical keying on a junk-suffixed remove, primaryList decode and validation, and the clear-on-malformed-role behavior. These are the queued devnet assertions.
  • Manual reasoning for reviewers: that the handlers persist what the parsers produce, since no test currently exercises the live or pending-drain paths.

Scope Reductions

  • Deferred follow-ups: the EFP devnet and integration tests (assertions above); an optional doc note on efp.accountMetadata steering consumers to the validated primaryList; a possible tag filter on listRecords with a supporting index, if demand appears.
  • Why the rest is deferred: keep this PR reviewable and shippable. The devnet is separate infrastructure.

Risk Analysis

  • Assumptions: user / manager metadata values are exactly 20-byte addresses; EFP structures are version 1; type-1 records carry a 20-byte address; ops for a given list are totally ordered within its chain, so a record is added before it is tagged.
  • Failure modes: a malformed input the parsers wrongly accept or reject; the array round-trip failing at runtime (mitigated by the production precedent).
  • Blast radius: contained. Opt-in, a no-op when disabled, isolated tables, no change to existing ENS data or queries. Worst case is incorrect EFP rows, recoverable by a re-index after a fix.
  • Mitigations and rollback: disable the efp plugin (removes the surface), then re-index.

Pre-Review Checklist (Blocking)

  • I reviewed every line of this diff and understand it end-to-end
  • I'm prepared to defend this PR line-by-line in review
  • I'm comfortable being the on-call owner for this change
  • Relevant changesets are included (or are not required)

Quantumlyy added 16 commits May 22, 2026 11:23
Add the EFP ListRegistry, AccountMetadata, and ListRecords contracts to the
mainnet ENS namespace as three per-chain datasources (EFPBase, EFPOptimism,
EFPEthereum), plus an address-less Resolver subscription on Ethereum mainnet
used to index the eth.efp.list text record.

ABIs are event-only subsets scoped to the events the EFP plugin indexes.
Add the efp_* tables to the abstract ENSDb schema: efp_lists, a
efp_list_storage_locations reverse index (slot -> token id, so list-metadata
events resolve their list NFT by primary key), efp_list_records,
efp_list_record_tags, efp_account_metadata, efp_pending_list_metadata, and
efp_ens_list_pointers. Register PluginName.EFP in the SDK enum.
Pure decoders for EFP ListOp payloads, the onchain ListStorageLocation
(locationType 1 only, per the EFP spec), and the eth.efp.list text record,
plus composite-id helpers, the list-metadata value decoder, and EFP constants.
Colocated unit tests cover the decoders.
Event handlers for ListRegistry (Transfer, UpdateListStorageLocation),
ListRecords (ListOp, UpdateListMetadata), AccountMetadata, and the eth.efp.list
Resolver TextChanged, writing directly to context.ensDb by primary key. The
slot->tokenId mapping keeps the user/manager path PK-only; the lone non-PK op
(cascading tag deletes on record removal) uses the drizzle escape hatch. Adds
the Ponder plugin config and registers it in ALL_PLUGINS.
Conditionally attach the EFP event handlers when `efp` is in PLUGINS, and add a
changeset for the new plugin.
Add a single `efp` root field (an EfpQuery namespace) to the Omnigraph GraphQL API,
grouping EFP queries so they do not clutter the Query root. Exposes EfpList (with a
nested records connection), EfpListRecord (with tags), EfpAccountMetadata, and
EfpListPointer (the eth.efp.list correlation), with cursor-paginated connections and
where-filters (owner/user/manager, recordData, address, node/listTokenId). Resolvers
read ENSDb directly via di.context.ensDb.
Output of `pnpm generate` after adding the EFP Omnigraph types.
Communicates the new `efp` Omnigraph root field for release notes.
Add the per-field // Entity.field banner comments used across the Omnigraph entity files
(account/renewal/domain), the // Inputs banner in the dedicated inputs file, and leading
docstrings on the plugin handler files (matching tokenscope). Comments only — no SDL change.
The eth.efp.list text record is not part of the EFP spec (docs.efp.app); the canonical
account-to-list association is the `primary-list` account metadata. Remove the address-less
Resolver subscription, the eth.efp.list text-record parser, and the efp_ens_list_pointers table.
EFP now indexes only the spec contracts: ListRegistry, AccountMetadata, and ListRecords.
Replace the removed eth.efp.list `listPointers` query with `efp.primaryList(address)`, which
reads the account `primary-list` metadata and returns the list only when its `user` role matches
the account (the EFP two-step Primary List validation). Add `EfpListRecord.list` so a record
navigates to its list, and consolidate the cross-service id mirrors in `efp-ids.ts`.
…bytes

EFP defines only record type 1 (a 20-byte address); types 0 and 2-255 are reserved with no
defined data layout. `parseRecord` now returns null for them, so the indexer never stores a record
whose `recordData` is not an address (which the Omnigraph API exposes through a non-null `Address`
scalar).

For type-1 records it also exposes the canonical `version | type | address` 22-byte prefix,
truncating any trailing junk after the 20-byte address. Tag and remove ops carry that same prefix,
so keying records by it (next commit) makes them resolve to the same row.
…oval

Records are now keyed by the canonical 22-byte `version | type | address` prefix in both
ADD_RECORD and REMOVE_RECORD, so a clean remove op deletes a junk-suffixed record (and vice versa)
and tag ops resolve to the same row (completes the record-identity fix).

Tags move from the `efp_list_record_tags` join table onto an embedded `tags` array on
`efp_list_records`. REMOVE_RECORD is now a single primary-key delete — the tags travel with the row
— instead of a non-PK cascade in the indexing hot path, and a re-added record starts with no stale
tags. ADD_TAG/REMOVE_TAG read-modify-write the record tag set by primary key, and the Omnigraph
`EfpListRecord.tags` resolver reads the column directly (removing a per-record query). Tag ops for a
record not in the list are ignored, since ops may arrive in any order.
…rted version or length

The leading version byte defines each structure's decoding schema, so an unsupported version
(or an out-of-spec length) must not be decoded as v1. `parseListOp` and `parseRecord` now reject any
version != 1, and `parseListStorageLocation` requires version 1, locationType 1, and the exact
86-byte payload (it previously accepted >= 86 bytes of any version). A shared `EFP_VERSION` constant
documents the single protocol version EFP defines today.
…roles

The generic metadata setter can emit arbitrary bytes for the `user`/`manager` keys.
`metadataValueToAddress` now returns null for any value that is not exactly 20 bytes, clearing the
role rather than storing a truncated or empty `0x` address that would later surface through a
GraphQL `Address`. Both call sites write the nullable `user`/`manager` columns, so a malformed value
clears the role consistently on the live and pending-drain paths.
Copilot AI review requested due to automatic review settings May 29, 2026 16:33
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 29, 2026

🦋 Changeset detected

Latest commit: d1954e9

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

This PR includes changesets to release 24 packages
Name Type
ensapi Minor
enssdk Minor
@ensnode/datasources Minor
@ensnode/ensdb-sdk Minor
@ensnode/ensnode-sdk Minor
ensindexer Minor
ensadmin Minor
ensrainbow Minor
@docs/ensnode Minor
@ensnode/enskit-react-example Patch
@ensnode/enssdk-example Patch
@namehash/ens-referrals Minor
enskit Minor
@ensnode/ensrainbow-sdk Minor
@namehash/namehash-ui Minor
fallback-ensapi Minor
@ensnode/integration-test-env Minor
@docs/ensrainbow Minor
enscli Minor
ensskills Minor
@ensnode/ponder-sdk Minor
@ensnode/ponder-subgraph Minor
@ensnode/shared-configs Minor
@ensnode/ensindexer-perf-testing 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

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 29, 2026

@Quantumlyy is attempting to deploy a commit to the NameHash Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 29, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds EFP support across the stack: datasources/ABIs, ENSDb tables, EFP indexer plugin and handlers, parsing/ID helpers, Omnigraph GraphQL efp namespace with Account.efp, devnet seeding, docker/devnet orchestration, and tests.

Changes

EFP Indexer and API Integration

Layer / File(s) Summary
Changesets, plugin enum, datasources & ABIs
.changeset/*, packages/datasources/src/abis/efp/*, packages/datasources/src/lib/types.ts, packages/datasources/src/mainnet.ts, packages/datasources/src/ens-test-env.ts, packages/datasources/src/devnet/constants.ts, packages/ensnode-sdk/src/ensindexer/config/types.ts
Adds release notes/changesets for EFP features, PluginName.EFP, new EFP datasources (EFPBase/EFPOptimism/EFPEthereum), and event-only ABIs for ListRegistry/ListRecords/AccountMetadata plus devnet constants.
ENSDb EFP schema & re-export
packages/ensdb-sdk/src/ensindexer-abstract/efp.schema.ts, packages/ensdb-sdk/src/ensindexer-abstract/index.ts
Defines efpLists, efpListStorageLocations, efpListRecords, efpAccountMetadata, and efpListMetadata on-chain table models and re-exports them.
Indexer plugin & constants
apps/ensindexer/src/plugins/efp/plugin.ts, apps/ensindexer/src/plugins/efp/constants.ts, apps/ensindexer/src/plugins/index.ts, apps/ensindexer/ponder/src/register-handlers.ts
Adds EFP Ponder plugin, schema/version constants, opcode/metadata key maps, registers plugin in ALL_PLUGINS, and conditionally attaches EFP handlers when enabled.
Event handlers: ListRegistry / ListRecords / AccountMetadata
apps/ensindexer/src/plugins/efp/handlers/ListRegistry.ts, apps/ensindexer/src/plugins/efp/handlers/ListRecords.ts, apps/ensindexer/src/plugins/efp/handlers/AccountMetadata.ts, apps/ensindexer/src/plugins/efp/event-handlers.ts
Handlers for ERC-721 Transfer, UpdateListStorageLocation, ListOp, UpdateListMetadata, and UpdateAccountMetadata that upsert/delete efp_* tables, maintain reverse mappings, manage tags, and persist durable metadata.
Parsing utilities & tests
apps/ensindexer/src/plugins/efp/lib/parse-list-op.ts, apps/ensindexer/src/plugins/efp/lib/parse-list-storage-location.ts, apps/ensindexer/src/plugins/efp/lib/list-metadata.ts, *test.ts
Pure decoders for list ops, records, tags, storage-location payloads, slot padding, metadata-to-address normalization, and comprehensive unit tests covering valid and malformed payloads.
ID generation helpers
apps/ensindexer/src/plugins/efp/lib/ids.ts, apps/ensapi/src/omnigraph-api/schema/efp-ids.ts
Deterministic composite-key helpers (storageLocationId, listRecordId, accountMetadataId, listMetadataId) normalizing hex parts and stripping NULs for consistent IDs.
Omnigraph schema imports & Account.efp
apps/ensapi/src/omnigraph-api/schema.ts, apps/ensapi/src/omnigraph-api/schema/account.ts, apps/ensapi/src/omnigraph-api/schema/account-efp.ts
Imports EFP schema modules and adds Account.efp: AccountEfpRef! exposing primaryList and lists connection for an account.
GraphQL EFP types, inputs, and ID utilities
apps/ensapi/src/omnigraph-api/schema/efp-inputs.ts, apps/ensapi/src/omnigraph-api/schema/efp-ids.ts, apps/ensapi/src/omnigraph-api/schema/efp-account-metadata.ts, apps/ensapi/src/omnigraph-api/schema/efp-list-record.ts
Adds where-inputs for lists/records/accountMetadatas, EfpAccountMetadata loadable, EfpListRecord loadable with relational fields, and composite ID utilities.
GraphQL EfpList and records connection
apps/ensapi/src/omnigraph-api/schema/efp-list.ts
EfpList loadable with token metadata, decoded storage-location fields, timestamps, and a cursor-paginated records connection scoped by decoded storage location.
Primary-list decoder & validation
apps/ensapi/src/omnigraph-api/schema/efp-primary-list.ts, apps/ensapi/src/omnigraph-api/schema/efp-primary-list.test.ts
Strict 32-byte uint256-to-decimal decoder for primary-list metadata and resolver that validates referenced list exists and user role matches address; tests for valid/invalid encodings.
EfpQuery root namespace
apps/ensapi/src/omnigraph-api/schema/efp.ts
Registers top-level efp query exposing list, lists (tokenId-paginated), listRecords, accountMetadata, accountMetadatas, and primaryList using cursor pagination and where-filters.
Devnet orchestration, seeding & integration tests
docker/*, packages/integration-test-env/*, apps/ensapi/src/omnigraph-api/schema/efp.integration.test.ts
Adds efp-devnet service and volume, updates devnet PLUGINS/env, seedEfpDevnet script to populate demoGraph scenarios, lifecycle changes, and integration tests that exercise primaryList, Account.efp, deep-walks, and handler edge cases.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • namehash/ensnode#2128: Related changes to Omnigraph API config gating and plugin support semantics.
  • namehash/ensnode#1778: Prior schema/barrel refactor exposing ENSIndexer schema used by new EFP tables.
  • namehash/ensnode#2091: Changes to integration-test orchestrator/lifecycle similar to devnet startup adjustments.

Poem

🐰 I chew on hex and hop through logs,

lists and tags and careful progs,
efp now joins the ENS parade,
seeds and queries neatly laid,
a rabbit cheers — the index made!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: adding EFP indexing and Omnigraph API exposure, which is the primary focus of this comprehensive PR.
Description check ✅ Passed The PR description comprehensively covers objectives, changes, testing, design rationale, and risk analysis, closely matching the required template structure with all key sections present and well-detailed.
Docstring Coverage ✅ Passed Docstring coverage is 94.74% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds an Ethereum Follow Protocol (EFP) indexing plugin to ENSIndexer and exposes the indexed data via a new efp namespace in the ENSApi Omnigraph. The plugin indexes EFP list NFTs (Base), records/tags (Base/Optimism/Ethereum), and account metadata (Base) into new efp_* ENSDb tables, with a corresponding GraphQL surface for lists, records, account metadata, and validated primary lists.

Changes:

  • Add EFP datasources, ABIs, schema tables, plugin handlers (with byte decoders and tests) and a new PluginName.EFP.
  • Add Omnigraph types/resolvers (EfpQuery, EfpList, EfpListRecord, EfpAccountMetadata, and where-input filters) plus regenerated SDK schema/introspection.
  • Add changesets and a plugin README documenting activation and the new tables.

Reviewed changes

Copilot reviewed 33 out of 35 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/enssdk/src/omnigraph/generated/schema.graphql Regenerated SDK GraphQL schema for the EFP types and root efp field.
packages/enssdk/src/omnigraph/generated/introspection.ts Regenerated introspection metadata matching the new EFP schema.
packages/ensnode-sdk/src/ensindexer/config/types.ts Adds PluginName.EFP.
packages/ensdb-sdk/src/ensindexer-abstract/index.ts Re-exports the new EFP abstract schema.
packages/ensdb-sdk/src/ensindexer-abstract/efp.schema.ts Defines the efp_* Ponder tables and indexes.
packages/datasources/src/mainnet.ts Registers the three EFP datasources on Base/Optimism/Ethereum.
packages/datasources/src/lib/types.ts Adds EFP datasource name keys.
packages/datasources/src/abis/efp/{ListRegistry,ListRecords,AccountMetadata}.ts Event-only ABIs for the EFP contracts.
apps/ensindexer/src/plugins/index.ts Registers the EFP plugin in ALL_PLUGINS.
apps/ensindexer/src/plugins/efp/plugin.ts Builds the Ponder config for the EFP plugin across three chains.
apps/ensindexer/src/plugins/efp/constants.ts EFP version, opcodes, and well-known metadata keys.
apps/ensindexer/src/plugins/efp/lib/parse-list-storage-location.{ts,test.ts} Decoder + tests for UpdateListStorageLocation payloads.
apps/ensindexer/src/plugins/efp/lib/parse-list-op.{ts,test.ts} Decoders + tests for ListOp payloads, records, and tags.
apps/ensindexer/src/plugins/efp/lib/list-metadata.{ts,test.ts} Helper for interpreting list-metadata value as an address.
apps/ensindexer/src/plugins/efp/lib/ids.ts Composite primary-key builders for the indexer side.
apps/ensindexer/src/plugins/efp/handlers/{ListRegistry,ListRecords,AccountMetadata}.ts Event handlers that write into the new efp_* tables (with a pending-metadata staging mechanism for cross-contract ordering).
apps/ensindexer/src/plugins/efp/event-handlers.ts Attaches all EFP handlers.
apps/ensindexer/src/plugins/efp/README.md EFP plugin overview.
apps/ensindexer/ponder/src/register-handlers.ts Wires up EFP handlers when the plugin is active.
apps/ensapi/src/omnigraph-api/schema.ts Imports the new EFP schema modules.
apps/ensapi/src/omnigraph-api/schema/efp{,-list,-list-record,-account-metadata,-inputs,-ids}.ts Pothos types/resolvers for the efp namespace.
.changeset/efp-{plugin,omnigraph}.md Changesets describing the new feature for each package surface.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/ensindexer/src/plugins/efp/constants.ts Outdated
Comment thread apps/ensindexer/src/plugins/efp/handlers/ListRegistry.ts
Comment thread apps/ensapi/src/omnigraph-api/schema/efp.ts Outdated
Comment thread apps/ensindexer/src/plugins/efp/lib/parse-list-storage-location.ts Outdated
Comment thread apps/ensindexer/src/plugins/efp/handlers/ListRecords.ts Outdated
Comment thread apps/ensindexer/src/plugins/efp/lib/parse-list-storage-location.ts Outdated
Comment thread apps/ensapi/src/omnigraph-api/schema/efp.ts Outdated
Comment thread apps/ensindexer/src/plugins/efp/constants.ts Outdated
Comment thread apps/ensindexer/src/plugins/efp/handlers/ListRegistry.ts Outdated
Comment thread apps/ensindexer/src/plugins/efp/handlers/ListRecords.ts Outdated
…plicitly

The list-op, list-record, and List Storage Location payloads each carry an independent leading
version byte that EFP can bump separately, so the single EFP_VERSION constant is replaced by
EFP_LIST_OP_VERSION, EFP_RECORD_VERSION, and EFP_LSL_VERSION (all 1 today) and each decoder enforces
its own. Also rename the LSL decoder's HEX_BYTES to HEX_CHARS_PER_BYTE and give each field an
explicit named hex-char offset so the fixed 86-byte layout is auditable by inspection.
…s owner

ERC-721 emits Transfer(to=0) on a burn; the handler upserted that as owner = 0x00..00, which
then surfaced through EfpList.owner (non-null Address) and lists(where: { owner }). Detect a burn
(to == zeroAddress) and delete the list row plus its storage-location reverse mapping instead.
Within a (chain, contract, slot) EFP ops are indexed in on-chain order, so an ADD_TAG/REMOVE_TAG
for a missing record means the record was removed earlier or, anomalously, never added. Log a
warning rather than dropping it silently, and correct the comment that wrongly cited out-of-order
arrival as the rationale.
primaryList lower-cased only the stored user before comparing it to the requested address. The
Address scalar already normalizes the argument, but lower-casing both sides removes the asymmetry
and keeps validation independent of input casing.
…ed helper

Move the `primary-list` decode and two-step user-role validation out of the root
`efp.primaryList` resolver into `resolveValidatedPrimaryListTokenId`, so `Account.efp.primaryList`
(next commit) reuses the same logic. No behavior change.
…stRecord.account

Add `Account.efp` (an account's validated `primaryList` and the `lists` it is the `user` of)
and an `EfpListRecord.account` edge that resolves a record's target address to its `Account`.
Together they let a single Omnigraph query walk from an account to whom it follows and on into their
ENS names and own EFP lists, while the root `efp` namespace stays the protocol-rooted entry point.
The Omnigraph endpoint was gated on `unigraph`/`ensv2`, so `PLUGINS=efp` (or `subgraph,efp`)
returned 503 and the indexed EFP data was unqueryable, since the `efp` namespace is its only API
surface. The shared `hasOmnigraphApiConfigSupport` prerequisite (in ensnode-sdk, also consumed by
ensadmin) now also accepts `efp`, keeping EFP independent of Unigraph as intended. With EFP alone the
ENS query fields are present but return no data. Adds a unit test for the gate.
Copilot AI review requested due to automatic review settings May 29, 2026 20:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 39 out of 41 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (2)

packages/datasources/src/mainnet.ts:1

  • The EFP AccountMetadata contract on Base is configured at 0x5289fe5dabc021d02fddf23d4a4df96f4e0f17ef, which is the exact same address used below for the ListRecords contract on Ethereum mainnet (line 558). These are two different contract types, so having identical addresses across the two configurations is suspicious and at least one is likely incorrect. Please cross-check the canonical EFP deployments — Base AccountMetadata should be 0x5289…17EF only if that matches the docs; otherwise either the Base address or the Ethereum ListRecords address needs to be updated. An incorrect address will silently produce zero events and an empty efp_* dataset on that chain.
    packages/ensdb-sdk/src/ensindexer-abstract/efp.schema.ts:1
  • efp_list_metadata is mutable (the ListRecords:UpdateListMetadata handler does onConflictDoUpdate({ value })) but only stores createdAt. Every other mutable EFP table (efp_lists, efp_list_storage_locations, efp_account_metadata) also tracks updatedAt, which is useful both for debugging and for operators who want to see when a role was last changed. Consider adding an updatedAt: t.bigint().notNull() column here and writing it in the update branch for consistency.

Comment thread apps/ensindexer/src/plugins/efp/handlers/ListRecords.ts
Comment thread apps/ensindexer/src/plugins/efp/lib/parse-list-op.ts
Comment thread apps/ensindexer/src/plugins/efp/handlers/ListRegistry.ts
Comment thread apps/ensapi/src/omnigraph-api/schema/efp.ts
Define EFPBase, EFPOptimism, and EFPEthereum datasources for the ens-test-env
(devnet) namespace, all pointing at the single Anvil chain (id 31337) where the
EFP contracts are deployed. This unlocks the `efp` plugin on ens-test-env via the
existing required-datasource check; the plugin's per-chain-id Ponder config then
collapses the three datasources onto chain 31337 (their shared ListRecords address
makes this a no-op merge rather than triple-indexing).

Addresses are captured from the EFP devnet image deployed in attach mode on top of
the pinned contracts-v2 ENS deployment, and verified stable across clean redeploys.
Add a config test asserting `efp` activates on ens-test-env and indexes one chain.
Add an efp-devnet service that deploys the EFP contracts onto the ENS devnet's
anvil node in attach mode (image pinned to a master build, mirroring the
contracts-v2 pin), gate ensindexer on its health, and add `efp` to the devnet
PLUGINS. The result of `docker compose -f docker/docker-compose.devnet.yml up` is
one chain (id 31337) carrying both ENS v2 and EFP contracts, indexed together.
Bring the efp-devnet service up alongside the ENS devnet in the integration
orchestrator, gate it on devnet health, and add `efp` to the indexer's plugins so
the integration stack indexes ENS and EFP together on chain 31337.

Add EFP Omnigraph integration assertions against the demoGraph scenario: the
two-step-validated `efp.primaryList` (and its null case), the account-rooted
`Account.efp` view, and the full `account -> primaryList.records ->
node.account.efp.primaryList` deep walk. These double as the silent-failure guard
for the hardcoded devnet addresses: wrong addresses index zero EFP rows and fail
these loudly rather than passing vacuously.
Add a TypeScript EFP seeder (run during the integration seed phase) that mints
dedicated lists and drives the op sequences the demoGraph scenario does not:

  - tag de-duplication (a repeated ADD_TAG is a no-op),
  - the embedded-tags cascade on REMOVE_RECORD plus a fresh re-ADD,
  - a junk-suffixed REMOVE_RECORD that still deletes the canonically-keyed record,
  - a malformed `user` metadata value that clears the role (live path), and
  - role durability across a storage-location re-point: a list moved away from its
    slot and back recovers its role from the durable per-slot metadata.

Each record is anchored by a synthetic target address (shared via
`@ensnode/datasources`) so the assertions look records up by `recordData` without
depending on a token id; the targets double as the `EfpListRecord.account` null
path. Also exposes the EFPListMinter devnet address (not indexed) for the seeder.
Copy link
Copy Markdown
Contributor

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docker/services/efp-devnet.yml`:
- Line 24: Update the YAML array formatting for the healthcheck under the "test"
key: remove the extra spaces inside the bracketed array so it uses compact form
(e.g., change the current test: [ "CMD", "curl", "--fail", "-s",
"http://localhost:8000/health" ] to a compact bracket style) to satisfy YAMLlint
and maintain style consistency for the bracketed array containing "CMD", "curl",
"--fail", "-s", "http://localhost:8000/health".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d04c6be7-e458-473c-8762-f6fa52a4da34

📥 Commits

Reviewing files that changed from the base of the PR and between 49dee93 and 25677dc.

📒 Files selected for processing (13)
  • .changeset/efp-plugin.md
  • apps/ensapi/src/omnigraph-api/schema/efp.integration.test.ts
  • apps/ensindexer/src/config/config.test.ts
  • docker/docker-compose.devnet.yml
  • docker/docker-compose.orchestrator.yml
  • docker/envs/.env.docker.devnet
  • docker/services/efp-devnet.yml
  • packages/datasources/src/devnet/constants.ts
  • packages/datasources/src/ens-test-env.ts
  • packages/ensnode-sdk/src/omnigraph-api/prerequisites.test.ts
  • packages/ensnode-sdk/src/omnigraph-api/prerequisites.ts
  • packages/integration-test-env/src/lifecycle.ts
  • packages/integration-test-env/src/seed/efp.ts

Comment thread docker/services/efp-devnet.yml
# Conflicts:
#	apps/ensapi/src/omnigraph-api/schema/account.ts
Comment thread apps/ensindexer/src/plugins/efp/handlers/AccountMetadata.ts Outdated
Comment thread apps/ensindexer/src/plugins/efp/lib/parse-list-op.ts
- AccountMetadata: strip NUL bytes from the free-form on-chain `key` before using
  it as a primary-key component (Postgres text columns reject NUL; matches the tag
  path's api-v2 parity normalization).
- Add a parse-list-storage-location test for the exact safe-integer chain-id
  boundary (2^53-1 accepted, 2^53 rejected).
- Add an EFP integration assertion for the primaryList two-step validation's
  mismatch branch (metadata present but the list's `user` does not match -> null),
  with a seed actor address exposed for it.
Copilot AI review requested due to automatic review settings June 1, 2026 08:16
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 49 out of 51 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

packages/datasources/src/mainnet.ts:1

  • The EFPEthereum datasource config sets ListRecords.address to 0x5289..., which is also used above as the Base AccountMetadata address. That strongly suggests a copy/paste or wiring mistake and would cause the EFP plugin to index the wrong contract (or no relevant events) on Ethereum mainnet. Please verify the correct Ethereum mainnet ListRecords deployment address (and its start block), and update this datasource accordingly.

Comment thread apps/ensapi/src/omnigraph-api/schema/efp-ids.ts
Quantumlyy and others added 2 commits June 1, 2026 10:23
…sync

The indexer now strips NUL bytes from the AccountMetadata `key` before writing, so
the ENSApi-side `efpAccountMetadataId` must do the same (and lowercase the address)
to build a matching lookup id. Apply the identical normalization in both helpers.
Copilot AI review requested due to automatic review settings June 1, 2026 09:31
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 49 out of 51 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

packages/datasources/src/mainnet.ts:1

  • The EFPEthereum datasource is configured with a ListRecords address that matches the Base AccountMetadata address used earlier in this same file (0x5289...17ef). This is very likely a copy/paste error and would cause the EFP plugin to index the wrong contract on Ethereum mainnet (silently producing wrong/empty EFP data for that chain). Update DatasourceNames.EFPEthereum.contracts.ListRecords.address to the actual Ethereum mainnet ListRecords contract address from EFP’s published deployment coordinates (and keep the ABI as ListRecords).

Comment thread apps/ensapi/src/omnigraph-api/schema/efp-list-record.ts Outdated
`EfpListRecord.list` did one storage-location -> tokenId lookup per node, an N+1
on `efp.listRecords { node { list } }`. Convert it to a Pothos loadable field
(the dataloader plugin is already enabled) keyed by storage-location id, so a page
resolves all `list` back-refs in two batched queries instead of one per record.
@Quantumlyy Quantumlyy marked this pull request as ready for review June 1, 2026 12:16
@Quantumlyy Quantumlyy requested a review from a team as a code owner June 1, 2026 12:16
Copilot AI review requested due to automatic review settings June 1, 2026 12:16
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 49 out of 51 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (2)

packages/datasources/src/mainnet.ts:1

  • The EFPEthereum.ListRecords.address is set to the same value used for the Base AccountMetadata contract (0x5289...). That’s an internal inconsistency in this file (same address assigned to two different contract roles), and would cause the Ethereum ListRecords datasource to index the wrong contract/events. Please verify the Ethereum-mainnet ListRecords address and correct it (and consider adding a comment or link to the provenance for each chain’s address to avoid future copy/paste slips).
    packages/datasources/src/mainnet.ts:1
  • The EFPEthereum.ListRecords.address is set to the same value used for the Base AccountMetadata contract (0x5289...). That’s an internal inconsistency in this file (same address assigned to two different contract roles), and would cause the Ethereum ListRecords datasource to index the wrong contract/events. Please verify the Ethereum-mainnet ListRecords address and correct it (and consider adding a comment or link to the provenance for each chain’s address to avoid future copy/paste slips).

Comment thread apps/ensapi/src/omnigraph-api/schema/efp-ids.ts
Comment thread apps/ensindexer/src/plugins/efp/handlers/ListRecords.ts
Comment thread apps/ensindexer/src/plugins/efp/handlers/ListRecords.ts
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 1, 2026

Greptile Summary

This PR introduces an opt-in EFP (Ethereum Follow Protocol) plugin that indexes three on-chain contracts (ListRegistry, ListRecords, AccountMetadata) and exposes the data through a new efp root namespace on the Omnigraph API. The implementation is self-contained: isolated efp_* tables, dedicated event handlers, pure byte decoders with version/type guards, and Account.efp / EfpListRecord.account edges to link ENS accounts into the social graph.

  • Indexing: Ponder handlers on Base, Optimism, and Ethereum mainnet process list mints/burns/transfers, record add/remove/tag ops, list metadata (user/manager), and account primary-list metadata. A efp_list_storage_locations reverse index lets metadata events find their owning list NFT by primary key, and tags are embedded in the record row so a single PK delete removes record and tags together.
  • Omnigraph surface: A new efp query root exposes paginated lists, listRecords, accountMetadatas, and a two-step validated primaryList(address). Lexicographic-ordering bugs on tokenId (a sequential text column) are addressed via new paginateByNumericText / orderByNumericText helpers that cast to ::numeric in SQL.
  • Byte decoders: parseListOp, parseRecord, parseTagOp, and parseListStorageLocation all reject unknown versions, wrong types, and short payloads before any DB write; unit tests cover the key paths.

Confidence Score: 5/5

Safe to merge. The change is fully opt-in, isolated to new efp_* tables, and additive to the Omnigraph API with no modifications to existing ENS queries or data.

The byte decoders are well-guarded (version checks, type checks, length checks, no coercion of malformed input). The tokenId lexicographic-ordering issue that would have broken pagination is fixed via the new numeric-text helpers. Handler logic handles all four ListOp opcodes, burn/transfer/re-point cases, and the event-ordering ambiguity between UpdateListMetadata and UpdateListStorageLocation. The two-step primaryList validation is correct. The integration test seed covers the trickiest behavioral paths (tag dedup, cascade delete, junk-suffixed remove, malformed-role clear). The two findings are quality/performance notes, not correctness blockers.

efp.schema.ts (missing compound index for recordData+id cursor pagination) and ListRegistry.ts (orphaned records on storage-location move worth a decision comment).

Important Files Changed

Filename Overview
apps/ensindexer/src/plugins/efp/handlers/ListRecords.ts Handles all ListOp opcodes and UpdateListMetadata events. Read-modify-write tag logic is safe because Ponder processes events for a given contract sequentially. Dual-write pattern (store metadata durably, drain into list when storage location links) handles both event orderings correctly.
apps/ensindexer/src/plugins/efp/handlers/ListRegistry.ts Handles Transfer (mint/burn/transfer) and UpdateListStorageLocation. Burn correctly removes the storage-location reverse mapping and list row while leaving list records in place (mirrors on-chain state). Re-pointing logic clears stale roles and re-applies pending metadata from efp_list_metadata.
packages/ensdb-sdk/src/ensindexer-abstract/efp.schema.ts Schema is clean. tokenId stored as text (correct for uint256). Tags embedded as text[] on efp_list_records for single-PK-delete semantics. Reverse index efp_list_storage_locations correctly avoids full scans. idx_recordData index on efp_list_records exists but is not compound with id, which affects cursor-paginated filtered queries.
apps/ensindexer/src/plugins/efp/lib/parse-list-op.ts Well-guarded decoders: version byte checked before dispatch, record type must be 1, tag payload must be byte-aligned, junk after the 20-byte address is truncated to the canonical 22-byte prefix. NUL stripping mirrors api-v2. Unit tests cover all branches.
apps/ensindexer/src/plugins/efp/lib/parse-list-storage-location.ts Strict 86-byte fixed-length check, version and locationType guards, JS-safe chain-id range validation. Returns null for anything unrepresentable rather than truncating or coercing.
apps/ensapi/src/omnigraph-api/schema/efp-primary-list.ts Two-step primary-list validation (metadata lookup → user-role match) is correct. Rejects malformed values before tokenId conversion. Case-insensitive user comparison is defensive and correct. Shared by both efp.primaryList and Account.efp.primaryList.
apps/ensapi/src/omnigraph-api/lib/connection-helpers.ts New paginateByNumericText / orderByNumericText helpers correctly use ::numeric cast to fix lexicographic-ordering bugs on tokenId text columns. Drizzle sql template parameterises the cursor value, so no SQL injection risk.
apps/ensapi/src/omnigraph-api/schema/efp-list-record.ts EfpListRecord.list uses t.loadable with storage-location IDs as batch keys; the two-step load (location → tokenId → list) is a correct N+1 prevention pattern. EfpListRecord.account correctly returns null for unknown addresses.
apps/ensapi/src/omnigraph-api/schema/efp.ts EfpQuery root correctly uses paginateByNumericText for lists (tokenId) and paginateBy for listRecords/accountMetadatas (composite text id). listRecords with a recordData filter uses only a single-column idx_recordData index; cursor-paginated scans over popular addresses will scan all matching rows per page.
apps/ensapi/src/omnigraph-api/schema/account-efp.ts AccountEfp.lists correctly uses paginateByNumericText + orderByNumericText for the user-scoped list connection. primaryList delegates to the shared resolveValidatedPrimaryListTokenId.
packages/integration-test-env/src/seed/efp.ts Seed exercises tag dedup, embedded-tags cascade on REMOVE_RECORD, junk-suffixed remove, and malformed-role clear. Uses a dedicated Anvil account (index 6) isolated from demoGraph, and synthetic target addresses that double as null-account test coverage.

Reviews (3): Last reviewed commit: "fix(efp): order and paginate EFP lists n..." | Re-trigger Greptile

Comment thread packages/datasources/src/mainnet.ts
Comment thread apps/ensindexer/src/plugins/efp/lib/parse-list-op.ts
Comment thread packages/ensdb-sdk/src/ensindexer-abstract/efp.schema.ts
Annotate each EFP contract address in the mainnet namespace with its chain and a
reference to https://docs.efp.app/production/deployments/. In particular, call out
that EFPAccountMetadata (Base) and EFPListRecords (Ethereum mainnet) intentionally
share 0x5289…0F17EF: EFP deploys via CREATE2, so one address maps to a different
contract per chain. Confirmed against the EFP deployments doc and api-v2.
Lowercase the address in the ENSApi-side id helper so it builds the exact same key
the indexer persists (which lowercases via `accountMetadataId`), regardless of the
caller's casing. No behavior change for normalized input; makes the helper canonical.
Comment thread apps/ensapi/src/omnigraph-api/schema/efp-list.ts
`efpLists.tokenId` is a `text` column (a uint256 that does not fit a Postgres
integer type), so `efp.lists` / `Account.efp.lists` ordered and cursor-compared it
lexicographically: "10" sorted before "2", and the before/after cursors skipped or
repeated rows once there were more than 9 lists. Add `paginateByNumericText` /
`orderByNumericText` helpers that cast to `::numeric`, and use them for the
tokenId-keyed list connections (the id-keyed record/metadata connections are
unchanged). Verified by a new integration test that seeds >9 lists and paginates
across the single->double-digit boundary in small pages.
Copilot AI review requested due to automatic review settings June 1, 2026 13:43
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 50 out of 52 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (2)

docker/services/efp-devnet.yml:1

  • healthcheck.start_interval is not supported uniformly across older Docker Engine / docker compose versions and can cause compose parsing failures in CI or developer environments. If you need quicker initial probes, prefer adjusting interval/start_period (portable), or document/pin the minimum required Docker/Compose versions for running this stack.
    apps/ensindexer/src/plugins/efp/plugin.ts:1
  • When multiple datasources share the same chain.id (as in the devnet “collapse”), these object spreads will silently overwrite earlier per-chain entries if the keys collide, which can mask misconfiguration (e.g., if one datasource’s ListRecords address diverges, the last spread “wins” without an error). Consider deduplicating by chainId first (or explicitly validating that colliding chainIds have identical contract coordinates) and throwing on mismatch so this failure mode becomes loud instead of silent.

Comment on lines +51 to +59
export const paginateByNumericText = (
column: Column,
before: string | undefined,
after: string | undefined,
) =>
and(
before ? sql`${column}::numeric < ${cursors.decode<string>(before)}::numeric` : undefined,
after ? sql`${column}::numeric > ${cursors.decode<string>(after)}::numeric` : undefined,
);
Comment on lines +65 to +66
export const orderByNumericText = (column: Column, inverted: boolean) =>
inverted ? desc(sql`${column}::numeric`) : asc(sql`${column}::numeric`);
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.

2 participants