Skip to content

draft: suggestion to add Virtual Domain#2269

Closed
sevenzing wants to merge 1 commit into
mainfrom
ll/virtual-domain-suggestion
Closed

draft: suggestion to add Virtual Domain#2269
sevenzing wants to merge 1 commit into
mainfrom
ll/virtual-domain-suggestion

Conversation

@sevenzing
Copy link
Copy Markdown
Member

Lite PR

Tip: Review docs on the ENSNode PR process

Summary

  • What changed (1-3 bullets, no essays).

Why

  • Why this change exists. Link to related GitHub issues where relevant.

Testing

  • How this was tested.
  • If you didn't test it, say why.

Notes for Reviewer (Optional)

  • Anything non-obvious or worth a heads-up.

Pre-Review Checklist (Blocking)

  • This PR does not introduce significant changes and is low-risk to review quickly.
  • Relevant changesets are included (or are not required)

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jun 6, 2026

⚠️ No Changeset found

Latest commit: 14b03f8

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Jun 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
admin.ensnode.io Ready Ready Preview, Comment Jun 6, 2026 10:45am
enskit-react-example.ensnode.io Building Building Preview Jun 6, 2026 10:45am
ensnode.io Ready Ready Preview, Comment Jun 6, 2026 10:45am
ensrainbow.io Ready Ready Preview, Comment Jun 6, 2026 10:45am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 6, 2026

Need the big picture first? Review this PR in Change Stack to see what changed before going file by file.

Review Change Stack

📝 Walkthrough

Walkthrough

This PR extends ENS domain resolution to support names that have on-chain resolvers but remain unindexed by introducing virtual domain representations, on-chain resolver verification, and query orchestration to serve these unindexed-but-valid domains alongside indexed ones.

Changes

Virtual Domains for Unindexed ENS Names

Layer / File(s) Summary
On-chain resolver existence check
apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts
New nameHasResolver async utility queries the ENS Root UniversalResolver contract via rootChainPublicClient.readContract to verify whether a name has a resolver on-chain, returning true when the resolver address is not zeroAddress. Imports updated to include address comparison and byte-encoding helpers.
Virtual domain model and GraphQL representation
apps/ensapi/src/omnigraph-api/schema/domain.ts
New VirtualDomain type and makeVirtualDomain factory synthesize GraphQL representations for unindexed but resolver-backed names, deriving labelhash/namehash identifiers and setting indexed-backed fields to null. VirtualDomainRef is registered as a DomainInterfaceRef with isTypeOf discriminator. Expanded enssdk imports to include name interpretation and hash utilities.
Reverse-resolution account stub synthesis
apps/ensapi/src/omnigraph-api/schema/account.ts
AccountRef.load is converted to async and extended to create placeholder { id } stub entries for any requested addresses not found in the database, enabling reverse resolution for all input IDs.
Query domain resolver orchestration
apps/ensapi/src/omnigraph-api/schema/query.ts
Query.domain resolver is made async and now returns indexed domains by ID, calls nameHasResolver to verify on-chain resolver existence for unindexed names, returns null when no resolver exists, and returns a VirtualDomain when a resolver exists but the name is unindexed. Imports updated to include nameHasResolver and makeVirtualDomain.

Sequence Diagram

sequenceDiagram
  participant Client
  participant QueryResolver as Query.domain Resolver
  participant DomainIndex as Domain Index
  participant UniversalResolver
  participant VirtualDomain as Virtual Domain Factory

  Client->>QueryResolver: domain(by: { name })
  QueryResolver->>DomainIndex: getDomainIdByInterpretedName(name)
  alt Domain indexed
    DomainIndex-->>QueryResolver: domainId
    QueryResolver-->>Client: indexed Domain
  else Domain not indexed
    DomainIndex-->>QueryResolver: null
    QueryResolver->>UniversalResolver: findResolver(packetToBytes(name))
    alt Resolver exists on-chain
      UniversalResolver-->>QueryResolver: resolverAddress
      QueryResolver->>VirtualDomain: makeVirtualDomain(name)
      VirtualDomain-->>QueryResolver: VirtualDomain
      QueryResolver-->>Client: VirtualDomain
    else No resolver on-chain
      UniversalResolver-->>QueryResolver: zeroAddress
      QueryResolver-->>Client: null
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Virtual domains bloom where resolvers dwell,
Unindexed names that whisper their tale—
On-chain we verify, in stubs we trust,
A query that sparkles through data's dust! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is a blank template with all sections unfilled, lacking any substantive information about changes, rationale, testing, or reviewer notes required by the repository guidelines. Complete all required sections: provide 1-3 bullets summarizing changes, explain the rationale and link related issues, describe testing approach, and check the blocking checklist items.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'draft: suggestion to add Virtual Domain' is vague and generic, using non-descriptive terms like 'suggestion' that don't convey meaningful information about the actual implementation changes. Revise the title to be more specific and descriptive, e.g., 'Add Virtual Domain support for unindexed ENS names with resolver checks' to clearly reflect the actual implementation.
✅ Passed checks (2 passed)
Check name Status Explanation
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ll/virtual-domain-suggestion

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.

Comment on lines +404 to +405
// TODO: implement this
return true;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
// TODO: implement this
return true;
// VirtualDomains are unindexed names without canonical metadata, so they are never canonical.
// All canonical metadata fields (canonicalDepth, canonicalPath, canonicalNode, etc.) are set
// to null for VirtualDomains, indicating they are not in the canonical nametree.
return false;

The isCanonicalName function incorrectly returns true for all VirtualDomains, marking unindexed names as canonical when they should never be canonical.

Fix on Vercel

Comment on lines +162 to +165
// small hack for testing since I cannot connect to production database :(
if (name === "offchaindemo.eth") {
return true;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
// small hack for testing since I cannot connect to production database :(
if (name === "offchaindemo.eth") {
return true;
}

Hardcoded test data in production code causes incorrect resolver check for "offchaindemo.eth"

Fix on Vercel

export function makeVirtualDomain(name: InterpretedName): Domain {
const firstLabel = interpretedNameToInterpretedLabels(name)[0];
const labelHash = labelhashInterpretedLabel(firstLabel);
const virtualDomain: VirtualDomain = {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Unsafe type casting in makeVirtualDomain bypasses TypeScript type safety for ENSv1DomainId construction

Fix on Vercel

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: 4

🤖 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 `@apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts`:
- Around line 161-165: The function nameHasResolver contains a hardcoded test
bypass returning true for "offchaindemo.eth"; remove that conditional so the
function always performs the real resolver check (i.e., eliminate the
special-case branch in nameHasResolver and rely on the existing chain lookup
logic), or if needed for local tests move the bypass behind a proper test
seam/flag checked only in test code (e.g., use a test-only environment variable
or dependency-injected mock) so the production code path for nameHasResolver and
Query.domain remains accurate.

In `@apps/ensapi/src/omnigraph-api/schema/domain.ts`:
- Around line 384-405: The virtual domain code is incorrectly marking every
virtual domain as canonical because isCanonicalName(name) always returns true;
change isCanonicalName to return false until you can correctly derive
canonicality (or implement the real logic), so that DomainInterfaceRef.canonical
(the canonical field set when creating virtualDomain) remains false/null when
canonical path/node metadata (canonicalPath, canonicalNode,
canonicalLabelHashPath, __canonicalNamePrefix) are not populated; update the
isCanonicalName function to return false by default or explicitly check for the
presence of canonical metadata before returning true.

In `@apps/ensapi/src/omnigraph-api/schema/query.ts`:
- Around line 131-139: The resolver currently returns
makeVirtualDomain(args.by.name) for names without indexed IDs but still treats
args.by.id as a normal DomainId—causing virtual IDs to not reload. Fix by
detecting virtual domain IDs and materializing them instead of passing them to
the standard loader: either (A) in the resolve function add a check for a
virtual-id pattern (or use a small helper isVirtualDomainId) and if true return
makeVirtualDomain(extractNameFromVirtualId(args.by.id)), otherwise continue to
use args.by.id; or (B) update DomainInterfaceRef.load to recognize the
virtual-id format and return makeVirtualDomain(...) when encountering it.
Reference functions/types: resolve, makeVirtualDomain,
getDomainIdByInterpretedName, nameHasResolver, DomainInterfaceRef.load, and
ensDb.query.domain so the change targets the correct code paths.
🪄 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: 5937e228-10f5-4e4f-86a8-3fa3a41380ad

📥 Commits

Reviewing files that changed from the base of the PR and between ff75f79 and 14b03f8.

⛔ Files ignored due to path filters (1)
  • packages/enssdk/src/omnigraph/generated/schema.graphql is excluded by !**/generated/**
📒 Files selected for processing (4)
  • apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts
  • apps/ensapi/src/omnigraph-api/schema/account.ts
  • apps/ensapi/src/omnigraph-api/schema/domain.ts
  • apps/ensapi/src/omnigraph-api/schema/query.ts

Comment on lines +161 to +165
export async function nameHasResolver(name: InterpretedName): Promise<boolean> {
// small hack for testing since I cannot connect to production database :(
if (name === "offchaindemo.eth") {
return true;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove the hardcoded resolver bypass.

Line 163 makes nameHasResolver() return true without consulting the chain, so Query.domain(by: { name: "offchaindemo.eth" }) can surface a VirtualDomain even when the real resolver lookup would fail in the active namespace/environment. If this is only needed for local testing, it should live behind a test seam, not in the shipped code.

Suggested change
 export async function nameHasResolver(name: InterpretedName): Promise<boolean> {
-  // small hack for testing since I cannot connect to production database :(
-  if (name === "offchaindemo.eth") {
-    return true;
-  }
   const {
     contracts: {
       UniversalResolver: { address, abi },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function nameHasResolver(name: InterpretedName): Promise<boolean> {
// small hack for testing since I cannot connect to production database :(
if (name === "offchaindemo.eth") {
return true;
}
export async function nameHasResolver(name: InterpretedName): Promise<boolean> {
const {
contracts: {
UniversalResolver: { address, abi },
🤖 Prompt for 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.

In `@apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts` around
lines 161 - 165, The function nameHasResolver contains a hardcoded test bypass
returning true for "offchaindemo.eth"; remove that conditional so the function
always performs the real resolver check (i.e., eliminate the special-case branch
in nameHasResolver and rely on the existing chain lookup logic), or if needed
for local tests move the bypass behind a proper test seam/flag checked only in
test code (e.g., use a test-only environment variable or dependency-injected
mock) so the production code path for nameHasResolver and Query.domain remains
accurate.

Comment on lines +375 to +386
export function makeVirtualDomain(name: InterpretedName): Domain {
const firstLabel = interpretedNameToInterpretedLabels(name)[0];
const labelHash = labelhashInterpretedLabel(firstLabel);
const virtualDomain: VirtualDomain = {
type: "VirtualDomain",
// probably not correct
id: namehashInterpretedName(name) as unknown as ENSv1DomainId,
label: { labelHash, interpreted: firstLabel },
labelHash,
canonical: isCanonicalName(name),
registryId: getRootRegistryId(di.context.namespace),
subregistryId: null,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

The synthetic registry is wrong for nested virtual names.

Line 385 always assigns getRootRegistryId(...), but this file defines Domain.registry as the registry under which the domain exists. For foo.bar.eth, the leaf label is foo (Line 376), so its registry is bar.eth’s subregistry, not the ENS root. As written, registry, parent, and any registry-scoped follow-up queries are only correct for depth-1 names.

Comment on lines +384 to +405
canonical: isCanonicalName(name),
registryId: getRootRegistryId(di.context.namespace),
subregistryId: null,
tokenId: null,
node: null,
ownerId: null,
rootRegistryOwnerId: null,
canonicalName: name,
canonicalDepth: null,
canonicalPath: null,
canonicalNode: null,
canonicalLabelHashPath: null,
__canonicalNamePrefix: null,
__latestRegistrationExpiry: 2n ** 256n - 1n,
__latestRegistrationStart: 2n ** 256n - 1n,
};
return virtualDomain as unknown as Domain;
}

function isCanonicalName(name: InterpretedName): boolean {
// TODO: implement this
return true;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not mark every virtual domain as canonical.

DomainInterfaceRef.canonical at Lines 132-137 becomes non-null whenever domain.canonical is truthy. Here isCanonicalName() always returns true, but Lines 392-395 leave the canonical path/node metadata null, so virtual domains claim canonicality without any canonical data to support it. Until canonicality can be derived, this should stay false/null.

Suggested change
   const virtualDomain: VirtualDomain = {
     type: "VirtualDomain",
@@
-    canonical: isCanonicalName(name),
+    canonical: false,
@@
-}
-
-function isCanonicalName(name: InterpretedName): boolean {
-  // TODO: implement this
-  return true;
 }
🤖 Prompt for 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.

In `@apps/ensapi/src/omnigraph-api/schema/domain.ts` around lines 384 - 405, The
virtual domain code is incorrectly marking every virtual domain as canonical
because isCanonicalName(name) always returns true; change isCanonicalName to
return false until you can correctly derive canonicality (or implement the real
logic), so that DomainInterfaceRef.canonical (the canonical field set when
creating virtualDomain) remains false/null when canonical path/node metadata
(canonicalPath, canonicalNode, canonicalLabelHashPath, __canonicalNamePrefix)
are not populated; update the isCanonicalName function to return false by
default or explicitly check for the presence of canonical metadata before
returning true.

Comment on lines +131 to +139
resolve: async (parent, args, ctx, info) => {
if (args.by.id !== undefined) return args.by.id;
return getDomainIdByInterpretedName(args.by.name);
const domainId = await getDomainIdByInterpretedName(args.by.name);
if (domainId !== null) return domainId;
// Name is not indexed. Verify it actually has a resolver on-chain (via UniversalResolver)
// before returning a VirtualDomain — prevents surfacing a VirtualDomain for names that
// don't exist (e.g. random-string.eth with no resolver).
if (!(await nameHasResolver(args.by.name))) return null;
return makeVirtualDomain(args.by.name);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Virtual domain IDs are not reloadable through this API.

This resolver can now return makeVirtualDomain(args.by.name), but Line 132 still treats by.id as an indexed DomainId and hands it to the normal DomainInterfaceRef.load path. That loader only hits ensDb.query.domain in apps/ensapi/src/omnigraph-api/schema/domain.ts Lines 73-80, so echoing back the id from a virtual domain will not reconstruct the same object. Right now the new VirtualDomain.id is not a stable API reference in practice.

🤖 Prompt for 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.

In `@apps/ensapi/src/omnigraph-api/schema/query.ts` around lines 131 - 139, The
resolver currently returns makeVirtualDomain(args.by.name) for names without
indexed IDs but still treats args.by.id as a normal DomainId—causing virtual IDs
to not reload. Fix by detecting virtual domain IDs and materializing them
instead of passing them to the standard loader: either (A) in the resolve
function add a check for a virtual-id pattern (or use a small helper
isVirtualDomainId) and if true return
makeVirtualDomain(extractNameFromVirtualId(args.by.id)), otherwise continue to
use args.by.id; or (B) update DomainInterfaceRef.load to recognize the
virtual-id format and return makeVirtualDomain(...) when encountering it.
Reference functions/types: resolve, makeVirtualDomain,
getDomainIdByInterpretedName, nameHasResolver, DomainInterfaceRef.load, and
ensDb.query.domain so the change targets the correct code paths.

@shrugs
Copy link
Copy Markdown
Member

shrugs commented Jun 8, 2026

closing in favor of #2271

@shrugs shrugs closed this Jun 8, 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.

2 participants