-
Notifications
You must be signed in to change notification settings - Fork 17
draft: suggestion to add Virtual Domain #2269
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,8 +8,10 @@ import { | |||||||||||||||||||
| type LabelHashPath, | ||||||||||||||||||||
| type RegistryId, | ||||||||||||||||||||
| } from "enssdk"; | ||||||||||||||||||||
| import { isAddressEqual, toHex, zeroAddress } from "viem"; | ||||||||||||||||||||
| import { packetToBytes } from "viem/ens"; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import { DatasourceNames } from "@ensnode/datasources"; | ||||||||||||||||||||
| import { DatasourceNames, getDatasource } from "@ensnode/datasources"; | ||||||||||||||||||||
| import { | ||||||||||||||||||||
| getENSv1RootRegistryId, | ||||||||||||||||||||
| getENSv2RootRegistryId, | ||||||||||||||||||||
|
|
@@ -147,3 +149,32 @@ async function forwardWalkNamegraph( | |||||||||||||||||||
| // finally, return the exact match if it was the leaf | ||||||||||||||||||||
| return exact ? deepest.domainId : null; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** | ||||||||||||||||||||
| * Returns true if the UniversalResolver can find an active resolver for `name` — meaning the name | ||||||||||||||||||||
| * exists and is resolvable (on-chain or off-chain via CCIP-Read). Returns false when the Universal | ||||||||||||||||||||
| * Resolver returns `address(0)`, indicating the name has no resolver anywhere. | ||||||||||||||||||||
| * | ||||||||||||||||||||
| * Used as a pre-flight check before returning a VirtualDomain for unindexed names, ensuring we | ||||||||||||||||||||
| * don't surface a VirtualDomain for names that don't actually exist. | ||||||||||||||||||||
| */ | ||||||||||||||||||||
| 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; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
+161
to
+165
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove the hardcoded resolver bypass. Line 163 makes 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| const { | ||||||||||||||||||||
| contracts: { | ||||||||||||||||||||
| UniversalResolver: { address, abi }, | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| } = getDatasource(di.context.namespace, DatasourceNames.ENSRoot); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const [resolverAddress] = await di.context.rootChainPublicClient.readContract({ | ||||||||||||||||||||
| address, | ||||||||||||||||||||
| abi, | ||||||||||||||||||||
| functionName: "findResolver", | ||||||||||||||||||||
| args: [toHex(packetToBytes(name))], | ||||||||||||||||||||
| }); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return !isAddressEqual(resolverAddress, zeroAddress); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,9 +1,18 @@ | ||||||||||||||
| import { trace } from "@opentelemetry/api"; | ||||||||||||||
| import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay"; | ||||||||||||||
| import { and, count, eq, getTableColumns, inArray, sql } from "drizzle-orm"; | ||||||||||||||
| import { type DomainId, isNormalizedName } from "enssdk"; | ||||||||||||||
| import { | ||||||||||||||
| type DomainId, | ||||||||||||||
| type ENSv1DomainId, | ||||||||||||||
| type InterpretedName, | ||||||||||||||
| interpretedNameToInterpretedLabels, | ||||||||||||||
| isNormalizedName, | ||||||||||||||
| labelhashInterpretedLabel, | ||||||||||||||
| namehashInterpretedName, | ||||||||||||||
| } from "enssdk"; | ||||||||||||||
|
|
||||||||||||||
| import type { RequiredAndNotNull, RequiredAndNull } from "@ensnode/ensnode-sdk"; | ||||||||||||||
| import { getRootRegistryId } from "@ensnode/ensnode-sdk"; | ||||||||||||||
|
|
||||||||||||||
| import di from "@/di"; | ||||||||||||||
| import { withSpanAsync } from "@/lib/instrumentation/auto-span"; | ||||||||||||||
|
|
@@ -350,6 +359,62 @@ ENSv1DomainRef.implement({ | |||||||||||||
| }), | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| /////////////////////////////////////////////////////////////// | ||||||||||||||
| // VirtualDomain — synthetic domain for valid unindexed names | ||||||||||||||
| /////////////////////////////////////////////////////////////// | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * A synthetic domain object for ENS names that are valid but not present in the index (e.g. | ||||||||||||||
| * off-chain names resolved via CCIP-Read). Returned by `Query.domain(by: { name })` when the | ||||||||||||||
| * namegraph walk finds no matching indexed domain. All indexed-data fields (label, registry, etc.) | ||||||||||||||
| * are derived or null; `canonicalName` carries the input name so that `Domain.resolve` can | ||||||||||||||
| * forward-resolve records via the UniversalResolver. | ||||||||||||||
| */ | ||||||||||||||
| export type VirtualDomain = Omit<Domain, "type"> & { type: "VirtualDomain" }; | ||||||||||||||
|
|
||||||||||||||
| export function makeVirtualDomain(name: InterpretedName): Domain { | ||||||||||||||
| const firstLabel = interpretedNameToInterpretedLabels(name)[0]; | ||||||||||||||
| const labelHash = labelhashInterpretedLabel(firstLabel); | ||||||||||||||
| const virtualDomain: VirtualDomain = { | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||
| 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, | ||||||||||||||
|
Comment on lines
+375
to
+386
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The synthetic registry is wrong for nested virtual names. Line 385 always assigns |
||||||||||||||
| 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; | ||||||||||||||
|
Comment on lines
+404
to
+405
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
The isCanonicalName function incorrectly returns true for all VirtualDomains, marking unindexed names as canonical when they should never be canonical.
Comment on lines
+384
to
+405
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not mark every virtual domain as canonical.
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 |
||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| export const VirtualDomainRef = builder.objectRef<Domain>("VirtualDomain"); | ||||||||||||||
|
|
||||||||||||||
| VirtualDomainRef.implement({ | ||||||||||||||
| description: | ||||||||||||||
| "A valid ENS name that is not currently indexed (e.g. an off-chain or CCIP-Read name). Indexed-data fields are absent; `resolve` performs full forward resolution via the UniversalResolver.", | ||||||||||||||
| interfaces: [DomainInterfaceRef], | ||||||||||||||
| isTypeOf: (value) => (value as VirtualDomain).type === "VirtualDomain", | ||||||||||||||
| fields: () => ({}), | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| ////////////////////////////// | ||||||||||||||
| // ENSv2Domain Implementation | ||||||||||||||
| ////////////////////////////// | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,12 +7,15 @@ import di from "@/di"; | |
| import { builder } from "@/omnigraph-api/builder"; | ||
| import { orderPaginationBy, paginateBy } from "@/omnigraph-api/lib/connection-helpers"; | ||
| import { resolveFindDomains } from "@/omnigraph-api/lib/find-domains/find-domains-resolver"; | ||
| import { getDomainIdByInterpretedName } from "@/omnigraph-api/lib/get-domain-by-interpreted-name"; | ||
| import { | ||
| getDomainIdByInterpretedName, | ||
| nameHasResolver, | ||
| } from "@/omnigraph-api/lib/get-domain-by-interpreted-name"; | ||
| import { INCLUDE_DEV_METHODS } from "@/omnigraph-api/lib/include-dev-methods"; | ||
| import { lazyConnection } from "@/omnigraph-api/lib/lazy-connection"; | ||
| import { AccountByInput, AccountRef } from "@/omnigraph-api/schema/account"; | ||
| import { ID_PAGINATED_CONNECTION_ARGS } from "@/omnigraph-api/schema/constants"; | ||
| import { DomainInterfaceRef } from "@/omnigraph-api/schema/domain"; | ||
| import { DomainInterfaceRef, makeVirtualDomain } from "@/omnigraph-api/schema/domain"; | ||
| import { | ||
| DOMAINS_ORDERING_DESCRIPTION, | ||
| DomainIdInput, | ||
|
|
@@ -125,9 +128,15 @@ builder.queryType({ | |
| type: DomainInterfaceRef, | ||
| args: { by: t.arg({ type: DomainIdInput, required: true }) }, | ||
| nullable: true, | ||
| resolve: (parent, args, ctx, info) => { | ||
| 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); | ||
|
Comment on lines
+131
to
+139
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Virtual domain IDs are not reloadable through this API. This resolver can now return 🤖 Prompt for AI Agents |
||
| }, | ||
| }), | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hardcoded test data in production code causes incorrect resolver check for "offchaindemo.eth"