Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions apps/ensadmin/src/app/api/omnigraph/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
import { useSearchParams } from "next/navigation";
import { useMemo } from "react";

import { getNamespaceSpecificValue } from "@ensnode/ensnode-sdk";
import { GRAPHQL_API_EXAMPLE_QUERIES } from "@ensnode/ensnode-sdk/internal";
import { GRAPHQL_API_EXAMPLE_QUERIES, getNamespaceSpecificValue } from "@ensnode/ensnode-sdk";
Comment thread
vercel[bot] marked this conversation as resolved.
Outdated
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.

P1 GRAPHQL_API_EXAMPLE_QUERIES not exported from main entry

GRAPHQL_API_EXAMPLE_QUERIES is imported from @ensnode/ensnode-sdk (the "." entry), but packages/ensnode-sdk/src/index.ts does not re-export it — it exports from ./omnigraph-api/prerequisites, not from ./omnigraph-api/example-queries. The new dedicated export path added in package.json is ./omnigraph-api/example-queries, so the correct import would be @ensnode/ensnode-sdk/omnigraph-api/example-queries. As written, this will fail to resolve at type-check and build time.


import { GraphiQLEditor } from "@/components/graphiql-editor";
import { RequireENSAdminFeature } from "@/components/require-ensadmin-feature";
Expand Down
11 changes: 11 additions & 0 deletions packages/ensnode-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
],
"exports": {
".": "./src/index.ts",
"./omnigraph-api/example-queries": "./src/omnigraph-api/example-queries.ts",
"./internal": "./src/internal.ts"
},
Comment on lines 19 to 23
"sideEffects": false,
Expand All @@ -34,6 +35,16 @@
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./omnigraph-api/example-queries": {
"import": {
"types": "./dist/omnigraph-api/example-queries.d.ts",
"default": "./dist/omnigraph-api/example-queries.js"
},
"require": {
"types": "./dist/omnigraph-api/example-queries.d.cts",
"default": "./dist/omnigraph-api/example-queries.cjs"
}
}
},
Comment on lines 20 to 48
"main": "./dist/index.cjs",
Expand Down
95 changes: 66 additions & 29 deletions packages/ensnode-sdk/src/omnigraph-api/example-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,38 @@ const ENS_TEST_ENV_V2_ETH_REGISTRAR = maybeGetDatasourceContract(
const VITALIK_ADDRESS = toNormalizedAddress("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");

// owns sfmonicdeb*.eth (mix of v1 + v2) on sepolia-v2 and holds v2 ETHRegistry permissions
const SEPOLIA_V2_USER_ADDRESS = toNormalizedAddress("0x2f8e8b1126e75fde0b7f731e7cb5847eba2d2574");
const _SEPOLIA_V2_USER_ADDRESS = toNormalizedAddress("0x2f8e8b1126e75fde0b7f731e7cb5847eba2d2574");

Comment on lines 35 to +37
Copy link
Copy Markdown
Contributor

@vercel vercel Bot May 18, 2026

Choose a reason for hiding this comment

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

Unused variable _SEPOLIA_V2_USER_ADDRESS declared but never referenced anywhere in the codebase

Fix on Vercel

const SEPOLIA_V2_ADDRESS_WITH_LOT_OF_NAMES = toNormalizedAddress(
"0x205d2686da3bf33f64c17f21462c51b5ead462cf",
);
Comment on lines +36 to +40
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

Use the Sepolia V2 fixtures that actually demonstrate permissions.

Line 35 introduces _SEPOLIA_V2_USER_ADDRESS as an address with v2 ETHRegistry permissions, but Lines 303-304 and 328-329 still point the exported Sepolia V2 examples at fixtures that are explicitly documented as returning empty results. That means the published permissions-by-contract and permissions-by-user examples stay blank in the namespace this PR is trying to make consumable.

Suggested patch
     variables: {
       // TODO: same as above
       default: { contract: ENS_TEST_ENV_V2_ETH_REGISTRAR },
-      // TODO: example response is empty for this address on Sepolia V2
-      [ENSNamespaceIds.SepoliaV2]: { contract: SEPOLIA_V2_V2_ETH_REGISTRAR },
+      [ENSNamespaceIds.SepoliaV2]: { contract: SEPOLIA_V2_V2_ETH_REGISTRY },
     },
   },
@@
     variables: {
       default: { address: accounts.deployer.address },
-      // TODO: example response is empty for this address on Sepolia V2
-      [ENSNamespaceIds.SepoliaV2]: { address: SEPOLIA_V2_ADDRESS_WITH_LOT_OF_NAMES },
+      [ENSNamespaceIds.SepoliaV2]: { address: _SEPOLIA_V2_USER_ADDRESS },
     },
   },

Also applies to: 300-304, 326-329

🤖 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 `@packages/ensnode-sdk/src/omnigraph-api/example-queries.ts` around lines 36 -
40, The Sepolia V2 example exports are pointing at fixtures that return empty
results; update the exported examples for "permissions-by-contract" and
"permissions-by-user" to use the actual Sepolia V2 fixture addresses that
demonstrate permissions: replace the empty-result addresses with the constants
_SEPOLIA_V2_USER_ADDRESS and SEPOLIA_V2_ADDRESS_WITH_LOT_OF_NAMES (the variables
defined at top of the file) so the examples show non-empty permission data;
locate the export objects/entries named "permissions-by-contract" and
"permissions-by-user" and swap their address values to these constants.


const DEVNET_NAME_WITH_OWNED_RESOLVER = asInterpretedName("example.eth");

const SEPOLIA_V2_NAME_WITH_OWNED_RESOLVER = asInterpretedName("sfmonicdebmig.eth");

export const GRAPHQL_API_EXAMPLE_QUERIES: Array<{
const SEPOLIA_V2_TEST_NAME = asInterpretedName("test-name.eth");

export type GraphqlApiExampleQuery = {
id: string;
query: string;
variables: NamespaceSpecificValue<Record<string, unknown>>;
}> = [
};
Comment on lines +48 to +52
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.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Enforce the query-ID invariant explicitly and fail fast on duplicates.

Line 422 currently allows duplicate id values to overwrite silently in the lookup map. Since id is now a public lookup contract, encode it as a dedicated alias and throw on duplicates during map construction.

Suggested patch
+export type GraphqlApiExampleQueryId = string;
+
 export type GraphqlApiExampleQuery = {
-  id: string;
+  id: GraphqlApiExampleQueryId;
   query: string;
   variables: NamespaceSpecificValue<Record<string, unknown>>;
 };
 
-export function getGraphqlApiExampleQueryById(id: string): GraphqlApiExampleQuery {
+export function getGraphqlApiExampleQueryById(id: GraphqlApiExampleQueryId): GraphqlApiExampleQuery {
   const found = graphqlApiExampleQueryById.get(id);
   if (!found) {
     throw new Error(`Unknown GraphQL API example query id: ${id}`);
   }
   return found;
 }
@@
-const graphqlApiExampleQueryById = new Map(
-  GRAPHQL_API_EXAMPLE_QUERIES.map((entry) => [entry.id, entry]),
-);
+const graphqlApiExampleQueryById = (() => {
+  const byId = new Map<GraphqlApiExampleQueryId, GraphqlApiExampleQuery>();
+  for (const entry of GRAPHQL_API_EXAMPLE_QUERIES) {
+    if (byId.has(entry.id)) {
+      throw new Error(`Duplicate GraphQL API example query id: ${entry.id}`);
+    }
+    byId.set(entry.id, entry);
+  }
+  return byId;
+})();
As per coding guidelines, "Use type aliases to document invariants; each invariant MUST be documented exactly once on its type alias".

Also applies to: 421-423

🤖 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 `@packages/ensnode-sdk/src/omnigraph-api/example-queries.ts` around lines 48 -
52, Introduce a dedicated type alias (e.g., QueryId) that documents the
invariant that GraphqlApiExampleQuery.id is a unique public lookup key, replace
usages of raw string for ids with this alias on GraphqlApiExampleQuery, and
change the example-queries map construction code (the routine that builds the
lookup by id) to check for existing keys and throw an error immediately on
duplicate ids instead of silently overwriting; update any call sites that assume
string ids to use QueryId where appropriate.


export function getGraphqlApiExampleQueryById(id: string): GraphqlApiExampleQuery {
const found = graphqlApiExampleQueryById.get(id);
if (!found) {
throw new Error(`Unknown GraphQL API example query id: ${id}`);
}
return found;
}

export const GRAPHQL_API_EXAMPLE_QUERIES: GraphqlApiExampleQuery[] = [
////////////////
// Hello World
////////////////
{
id: "hello-world",
query: `#
# Welcome to this interactive playground for
# ENSNode's GraphQL API!
Expand All @@ -56,7 +74,7 @@ export const GRAPHQL_API_EXAMPLE_QUERIES: Array<{
#
# There are also example queries in the tabs above ☝️
query HelloWorld {
domain(by: { name: "eth" }) { name owner { address } }
domain(by: { name: "eth" }) { canonical { name } owner { address } }
}`,
Comment on lines 74 to 78
variables: { default: {} },
},
Expand All @@ -65,9 +83,10 @@ query HelloWorld {
// Find Domains
/////////////////
{
id: "find-domains",
query: `
query FindDomains(
$name: String!
$name: DomainsNameFilter!
$order: DomainsOrderInput
) {
domains(
Expand All @@ -80,62 +99,65 @@ query FindDomains(
__typename
id
label { interpreted hash }
name
canonical { name }

registration { expiry event { timestamp } }
}
}
}
}`,
variables: {
default: { name: "vitalik", order: { by: "NAME", dir: "DESC" } },
[ENSNamespaceIds.EnsTestEnv]: { name: "c", order: { by: "NAME", dir: "DESC" } },
[ENSNamespaceIds.SepoliaV2]: { name: "sfmonic", order: { by: "NAME", dir: "DESC" } },
default: { name: { starts_with: "vitalik" }, order: { by: "NAME", dir: "DESC" } },
[ENSNamespaceIds.EnsTestEnv]: {
name: { starts_with: "c" },
order: { by: "NAME", dir: "DESC" },
},
[ENSNamespaceIds.SepoliaV2]: {
name: { starts_with: "test-na" },
order: { by: "NAME", dir: "DESC" },
},
},
},

///////////////////
// Domain By Name
///////////////////
{
id: "domain-by-name",
query: `
query DomainByName($name: InterpretedName!) {
domain(by: {name: $name}) {
__typename
id
label { interpreted hash }
name
canonical { name node path { id } }
owner { address }
subregistry { contract { chainId address } }

... on ENSv1Domain {
rootRegistryOwner { address }
}

... on ENSv2Domain {
subregistry {
contract { chainId address }
}
}
}
}`,
variables: {
default: { name: "eth" },
[ENSNamespaceIds.SepoliaV2]: { name: "sfmonicdebmig.eth" },
[ENSNamespaceIds.SepoliaV2]: { name: SEPOLIA_V2_TEST_NAME },
},
},

//////////////////////
// Domain Subdomains
//////////////////////
{
id: "domain-subdomains",
query: `
query DomainSubdomains($name: InterpretedName!) {
domain(by: {name: $name}) {
name
canonical { name }
subdomains(first: 10) {
edges {
node {
name
canonical { name }
}
}
}
Expand All @@ -148,6 +170,7 @@ query DomainSubdomains($name: InterpretedName!) {
// Domain Events
/////////////////
{
id: "domain-events",
query: `
query DomainEvents($name: InterpretedName!) {
domain(by: {name: $name}) {
Expand Down Expand Up @@ -176,6 +199,7 @@ query DomainEvents($name: InterpretedName!) {
// Account Domains
////////////////////
{
id: "domains-by-address",
query: `
query AccountDomains(
$address: Address!
Expand All @@ -185,7 +209,7 @@ query AccountDomains(
edges {
node {
label { interpreted }
name
canonical { name }
}
}
}
Expand All @@ -194,14 +218,15 @@ query AccountDomains(
variables: {
default: { address: VITALIK_ADDRESS },
[ENSNamespaceIds.EnsTestEnv]: { address: accounts.owner.address },
[ENSNamespaceIds.SepoliaV2]: { address: SEPOLIA_V2_USER_ADDRESS },
[ENSNamespaceIds.SepoliaV2]: { address: SEPOLIA_V2_ADDRESS_WITH_LOT_OF_NAMES },
},
},

////////////////////
// Account Events
////////////////////
{
id: "account-events",
query: `
query AccountEvents(
$address: Address!
Expand All @@ -213,14 +238,15 @@ query AccountEvents(
variables: {
default: { address: VITALIK_ADDRESS },
[ENSNamespaceIds.EnsTestEnv]: { address: accounts.deployer.address },
[ENSNamespaceIds.SepoliaV2]: { address: SEPOLIA_V2_USER_ADDRESS },
[ENSNamespaceIds.SepoliaV2]: { address: SEPOLIA_V2_ADDRESS_WITH_LOT_OF_NAMES },
},
},

/////////////////////
// Registry Domains
/////////////////////
{
id: "registry-domains",
query: `
query RegistryDomains(
$registry: AccountIdInput!
Expand All @@ -230,7 +256,7 @@ query RegistryDomains(
edges {
node {
label { interpreted }
name
canonical { name }
}
}
}
Expand All @@ -247,6 +273,7 @@ query RegistryDomains(
// Permissions By Contract
////////////////////////////
{
id: "permissions-by-contract",
query: `
query PermissionsByContract(
$contract: AccountIdInput!
Expand Down Expand Up @@ -274,6 +301,7 @@ query PermissionsByContract(
variables: {
// TODO: same as above
default: { contract: ENS_TEST_ENV_V2_ETH_REGISTRAR },
// TODO: example response is empty for this address on Sepolia V2
[ENSNamespaceIds.SepoliaV2]: { contract: SEPOLIA_V2_V2_ETH_REGISTRAR },
},
},
Expand All @@ -282,6 +310,7 @@ query PermissionsByContract(
// Permissions By User
////////////////////////
{
id: "permissions-by-user",
query: `
query PermissionsByUser($address: Address!) {
account(by: { address: $address }) {
Expand All @@ -297,14 +326,16 @@ query PermissionsByUser($address: Address!) {
}`,
variables: {
default: { address: accounts.deployer.address },
[ENSNamespaceIds.SepoliaV2]: { address: SEPOLIA_V2_USER_ADDRESS },
// TODO: example response is empty for this address on Sepolia V2
[ENSNamespaceIds.SepoliaV2]: { address: SEPOLIA_V2_ADDRESS_WITH_LOT_OF_NAMES },
},
},

//////////////////////////////////
// Account Resolver Permissions
//////////////////////////////////
{
id: "account-resolver-permissions",
query: `
query AccountResolverPermissions($address: Address!) {
account(by: { address: $address }) {
Expand All @@ -323,18 +354,19 @@ query AccountResolverPermissions($address: Address!) {
}`,
variables: {
default: { address: accounts.deployer.address },
[ENSNamespaceIds.SepoliaV2]: { address: SEPOLIA_V2_USER_ADDRESS },
[ENSNamespaceIds.SepoliaV2]: { address: SEPOLIA_V2_ADDRESS_WITH_LOT_OF_NAMES },
},
},

//////////////////////////////
// Domain's Assigned Resolver
//////////////////////////////
{
id: "domain-resolver",
query: `
query DomainResolver($name: InterpretedName!) {
domain(by: { name: $name }) {
resolver {
assignedResolver {
records { edges { node { node keys coinTypes } } }
permissions { resources { edges { node { resource users { edges { node { user { address } roles } } } } } } }
events { totalCount edges { node { topics data timestamp } } }
Expand All @@ -352,24 +384,25 @@ query DomainResolver($name: InterpretedName!) {
// Namegraph
//////////////
{
id: "namegraph",
query: `
query Namegraph {
root {
id
domains {
edges {
node {
name
canonical { name }

subdomains {
edges {
node {
name
canonical { name }

subdomains {
edges {
node {
name
canonical { name }
}
}
}
Expand All @@ -384,3 +417,7 @@ query Namegraph {
variables: { default: {} },
},
];

const graphqlApiExampleQueryById = new Map(
GRAPHQL_API_EXAMPLE_QUERIES.map((entry) => [entry.id, entry]),
);
3 changes: 2 additions & 1 deletion packages/ensnode-sdk/src/shared/datasource-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
type ENSNamespaceId,
maybeGetDatasource,
} from "@ensnode/datasources";
import { accountIdEqual } from "@ensnode/ensnode-sdk";

import { accountIdEqual } from "./account-id";

/**
* Gets the AccountId for the contract in the specified namespace, datasource, and
Expand Down
2 changes: 1 addition & 1 deletion packages/ensnode-sdk/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineConfig } from "tsup";

export default defineConfig({
entry: ["src/index.ts"],
entry: ["src/index.ts", "src/omnigraph-api/example-queries.ts"],
platform: "neutral",
format: ["esm", "cjs"],
target: "es2022",
Expand Down
Loading