feat(ensapi): resolvable-but-unindexed Domains & Accounts (UnindexedDomain)#2271
Conversation
🦋 Changeset detectedLatest commit: 944ba53 The changes in this PR will be included in the next version bump. This PR includes changesets to release 24 packages
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 |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (8)
📝 WalkthroughWalkthroughThe PR adds support for resolvable-but-unindexed ENS domains and accounts by introducing ResolvableName validation, returning structured namegraph walk results with resolver extended metadata, synthesizing UnindexedDomain/Account objects from walk rows, and persisting resolver ENSIP-10 capability at index time. ChangesUnindexed Domain and Account Resolution
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Pull request overview
This PR extends the ENSApi Omnigraph “entity-shaped” resolution experience so Query.domain(by:{name}) and Query.account(by:{address}) can return resolvable entities even when no indexed Domain/Account row exists (via a new UnindexedDomain concrete type and Account virtualization), while also enriching the index with resolver.extended (ENSIP-10 wildcard support).
Changes:
- Add
UnindexedDomain(implementsDomain) and updateQuery.domainto return it when a name is resolvable-but-unindexed. - Virtualize
Accountto be address-backed (reverse resolution works regardless of indexing) and constrain resolution inputs via newResolvableName. - Capture and persist
resolver.extendedin the indexer and expose it for query-time predicates without RPC.
Reviewed changes
Copilot reviewed 26 out of 28 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/ensskills/skills/omnigraph/SKILL.md | Update Omnigraph skill docs to include UnindexedDomain and clarify resolver semantics. |
| packages/enssdk/src/omnigraph/generated/schema.graphql | Add UnindexedDomain GraphQL type to the generated schema. |
| packages/enssdk/src/omnigraph/generated/introspection.ts | Regenerate introspection to include UnindexedDomain. |
| packages/enssdk/src/lib/types/ensv2.ts | Add UnindexedDomainId and include it in DomainId. |
| packages/enssdk/src/lib/resolvable-name.ts | Introduce ResolvableName (+ validators) to constrain resolvable inputs. |
| packages/enssdk/src/lib/resolvable-name.test.ts | Unit tests for ResolvableName constraints. |
| packages/enssdk/src/lib/index.ts | Export resolvable-name from the SDK public surface. |
| packages/enssdk/src/lib/ids.ts | Add makeUnindexedDomainId helper. |
| packages/enskit/src/react/omnigraph/_lib/by-id-lookup-resolvers.ts | Teach client cache lookup to resolve UnindexedDomain by id. |
| packages/ensdb-sdk/src/ensindexer-abstract/protocol-acceleration.schema.ts | Add resolver.extended column to protocol acceleration schema. |
| docs/ensnode.io/src/content/docs/docs/services/ensdb/concepts/database-schemas.mdx | Document resolver observation semantics and new extended column. |
| apps/ensindexer/src/lib/protocol-acceleration/resolver-db-helpers.ts | Add upsertResolver to capture extended via cached supportsInterface. |
| apps/ensindexer/src/lib/protocol-acceleration/domain-resolver-relationship-db-helpers.ts | Ensure resolver entities exist when writing domain-resolver relations. |
| apps/ensapi/src/omnigraph-api/schema/query.ts | Update Query.domain to return indexed leaf or UnindexedDomain based on namegraph walk. |
| apps/ensapi/src/omnigraph-api/schema/query.integration.test.ts | Add integration tests covering UnindexedDomain behaviors and edge cases. |
| apps/ensapi/src/omnigraph-api/schema/primary-name-record.ts | Gate forward resolution of primary name record on isResolvableName. |
| apps/ensapi/src/omnigraph-api/schema/domain.ts | Extend Domain interface shape to include UnindexedDomain and add UnindexedDomain concrete GraphQL type. |
| apps/ensapi/src/omnigraph-api/schema/domain-canonical.ts | Build canonical path for UnindexedDomain via resolved values rather than id-loading. |
| apps/ensapi/src/omnigraph-api/schema/account.ts | Virtualize Account as NormalizedAddress (no DB load) to support unindexed accounts. |
| apps/ensapi/src/omnigraph-api/schema/account.integration.test.ts | Add integration test ensuring Query.account returns a virtual account for unindexed addresses. |
| apps/ensapi/src/omnigraph-api/lib/unindexed-domain.ts | Implement UnindexedDomain construction + lazy canonical path computation. |
| apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts | Refactor namegraph walking to return { rows, exact } for indexed vs unindexed decisions. |
| apps/ensapi/src/lib/resolution/reverse-resolution.ts | Constrain reverse resolution internal names/records to ResolvableName. |
| apps/ensapi/src/lib/resolution/forward-resolution.ts | Require ResolvableName for forward resolution and tighten input validation. |
| apps/ensapi/src/lib/protocol-acceleration/forward-walk-disjoint-namegraph.ts | Extend walk rows to include registryId, extended, and canonicalPath. |
| apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts | Update resolver-finding logic to use the new walkResultRowHasResolver helper. |
| .changeset/unindexed-domain-resolution.md | Add changeset describing new resolvable-but-unindexed Domain/Account behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Greptile SummaryThis PR corrects a protocol-correctness gap in the Omnigraph API:
Confidence Score: 5/5Safe to merge — the core namegraph walk logic is correct, the wildcard-resolver predicate faithfully mirrors UniversalResolver's _checkResolver, and all bridged/ENSv1/ENSv2 hop cases preserve existing forward-resolution semantics. The walk correctness was carefully verified: canonicalPath.length (not the walk-frame depth) determines the indexed prefix for bridged names, upsertResolver with onConflictDoNothing handles concurrent indexing races cleanly, and the Account virtualization is a straightforward collapse of a no-op dataloader. Integration tests cover the canonical UnindexedDomain path, multi-level virtual nodes, null for resolver-less names, null for encoded-labelhash leaves under wildcard resolvers, and unindexed account virtualization. Only two documentation/style observations were found. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["Query.domain(by: { name })"] --> B{forwardWalkNamegraph}
B --> C{rows.length === 0?}
C -- yes --> D["return null"]
C -- no --> E{exact match?}
E -- yes --> F["return rows[0].domainId\n(IndexedDomain via loader)"]
E -- no --> G["makeUnindexedDomain(name, rows)"]
G --> H{isResolvableName?}
H -- no --> I["return null"]
H -- yes --> J{deepest ancestor has\nextended Resolver?}
J -- no --> K["return null"]
J -- yes --> L["return UnindexedDomain"]
L --> M["DomainCanonical.path requested?"]
M --> N["computeUnindexedDomainCanonicalPath"]
N --> O["indexedPrefix = rows[0].canonicalPath"]
O --> P["slice suffix hierarchy past indexed prefix"]
P --> Q["return [...indexedPrefix, ...virtualNodes]"]
Reviews (11): Last reviewed commit: "fix: generate" | Re-trigger Greptile |
Thread the namegraph walk rows onto UnindexedDomain so canonical.path no longer re-walks; reuse getNameHierarchy/labelhashInterpretedLabel; flatten reverse-resolution name narrowing.
Slice the virtual-node suffix by the deepest indexed ancestor's canonical path length rather than the walk-frame depth, which is relative to the post-bridge sub-path and over-mints/duplicates ancestors for bridged names. Also: document UnindexedDomain registry/parent semantics on the Domain interface fields, drop a redundant asResolvableName call in reverse resolution, and fix the resolver-walk extended-NULL docstring.
…lver.extended Rename the resolvers table column extended -> is_extended (Drizzle property isExtended) and surface it through the Omnigraph API as a new non-nullable Resolver.extended Boolean field. Regenerated SDL/introspection and updated the ENSDb schema docs.
|
@greptile review |
Describe the resolvers.is_extended column as a net addition (it is new vs the last release, not a rename), and add an enssdk changeset for the new ResolvableName/UnindexedDomainId public API.
|
@greptile review |
| // the deepest indexed ancestor (rows are ordered depth DESC) anchors the indexed canonical prefix. | ||
| // Its materialized canonicalPath is root→leaf inclusive, so its length is that ancestor's canonical | ||
| // depth — the count of leading labels already covered by indexed Domains. We slice by this length | ||
| // (NOT the walk-frame `depth`, which is relative to the post-bridge sub-path and undercounts for | ||
| // bridged names, over-minting and duplicating the indexed ancestors). | ||
| const indexedPrefix = rows[0]?.canonicalPath ?? []; | ||
|
|
Summary
Resolvable-but-unindexed ENS names and addresses are now resolvable through the Omnigraph API, instead of returning
null. Todayresolve(forward) andprimaryNames/resolve(reverse) are protocol operations keyed on a name/address — they work for any valid name/address and the index only accelerates them — but modeling them as fields on indexed entities makes them unreachable whenever noDomain/Accountrow exists (off-chain / CCIP-Read names, unindexed 3DNS/.box, wildcard subnames, unobserved addresses). This is a correctness gap, not just DX.Query.domain(by: { name })andQuery.account(by: { address })now return a resolvable entity for these inputs.Design (summary of the spec for discussion)
Full spec/discussion doc: Resolving Unindexed Names & Addresses in the Omnigraph API. Key decisions:
Query.resolve/Query.reversedecoupled from entities) was rejected — resolution must stay reachable through the existingdomain(by:{name})/account(by:{address})DX so theaddress → primary name → forward resolvecomposition keeps working in one query. The backend does more work to preserve correctness + DX.AccountRefis now a plainobjectRef<NormalizedAddress>— there's nothing to load), so reverse resolution works regardless of indexing; indexed relations (domains/events/permissions) return empty for an unobserved address.UnindexedDomainconcrete type implementing theDomaininterface (Domain = IndexedDomain | UnindexedDomain), returned bydomain(by:{name})when there is no indexed leaf but the name is resolvable.IExtendedResolver) Resolver. A static ancestor Resolver cannot resolve a descendant, so e.g.doesnotexist.ethand ordinarysub.vitalik.eth(PublicResolver, non-wildcard) correctly staynull— only genuinely wildcard-capable ancestors (cb.id, uni.eth, …) yield a result. The walk already follows Bridged/ENSv1/ENSv2 hops, so the ENSv2.eth→ENSv1 bridge false-positive is handled for free.extendedis index-resident (Optimize Resolution API by upserting resolvers in SetResolver/ResolverUpdated #1991):resolver.extendedis captured via a single cachedsupportsInterfaceRPC the first time a Resolver is observed, so the predicate runs fully from the index with no query-time RPC.upsertResolvernow runs wherever a Resolver is referenced (every Domain-Resolver Relation), not only where it self-emits.UnindexedDomainis Canonical (it's named, via the queried name).DomainCanonical.pathis built lazily, label-by-label: the deepest indexed ancestor's materialized canonical path + a virtualUnindexedDomainper label below it (passed through as resolved values, since virtual nodes can't be loaded by id).registryvirtualizes to the registry that manages the wildcard-Resolver's Domain.idisunindexed-{registryId}-{node}(prefixed to disambiguate fromENSv1DomainId).ResolvableName: a name is only virtualized as anUnindexedDomainif it is aResolvableName— normalized, no Encoded-LabelHash segment, every label < 256 bytes (DNS-encodable). New enssdk type withisResolvableName/asResolvableName.Notable deviations from the original spec
findResolver(index path) is not gated onextended. The ENSUniversalResolver.findResolverreturns the deepest resolver + offset without anIExtendedResolvercheck (that gate lives in_checkResolver/resolve), so to stay 1:1 with the UR the index path also leaves the ENSIP-10 check to the consumer.extendedis consumed by theUnindexedDomainpredicate (which mirrors_checkResolver).MaybeUnindexedDomain→UnindexedDomain.What changed
resolver.extendedcolumn;upsertResolver(cachedsupportsInterface) wired intoensureDomainResolverRelation(covers ENSv1/ENSv2/3DNS) andensureResolverAndRecords.forwardWalkDisjointNamegraphreturnsregistryId+extended+ the node'scanonicalPath;forwardWalkNamegraphreturns{ rows, exact }.UnindexedDomainId(aDomainId) +makeUnindexedDomainId;ResolvableName+isResolvableName/asResolvableName.UnindexedDomaintype;omnigraph-api/lib/unindexed-domain.ts(makeUnindexedDomain(name, rows)+ lazycomputeUnindexedDomainCanonicalPath);Query.domain/Query.accountupdated;DomainCanonical.pathhandles virtual nodes.UnindexedDomain.resolverstable documentsextended.Closes / supersedes
resolver.extended).Testing
pnpm typecheck,pnpm lint,pnpm generateclean.pnpm test— 1847 passing.pnpm test:integration:ci— 360 passing, incl. new coverage: canonicalUnindexedDomain(name/depth/path), label-by-label path with intermediate virtual nodes (a.b.parent.eth), null for a resolver-less subname, null for an Encoded-LabelHash leaf under a wildcard resolver, and Account virtualization for an unindexed address; plusResolvableNameunit tests.