From 3d6232b923e0898a0d4aa1ab6a2157684359be7e Mon Sep 17 00:00:00 2001 From: shrugs Date: Thu, 6 Nov 2025 13:16:53 -0600 Subject: [PATCH 001/102] checkpoint --- apps/ensindexer/package.json | 2 +- .../src/plugins/ensv2/event-handlers.ts | 9 ++ apps/ensindexer/src/plugins/ensv2/plugin.ts | 38 ++++++ apps/ensindexer/src/plugins/index.ts | 3 + packages/ensnode-schema/src/ponder.schema.ts | 1 + .../src/schemas/ensv2.schema.ts | 125 ++++++++++++++++++ .../src/ensindexer/config/types.ts | 1 + pnpm-lock.yaml | 49 ++++--- pnpm-workspace.yaml | 16 +-- 9 files changed, 213 insertions(+), 31 deletions(-) create mode 100644 apps/ensindexer/src/plugins/ensv2/event-handlers.ts create mode 100644 apps/ensindexer/src/plugins/ensv2/plugin.ts create mode 100644 packages/ensnode-schema/src/schemas/ensv2.schema.ts diff --git a/apps/ensindexer/package.json b/apps/ensindexer/package.json index d7fed2c27..8b27ddb12 100644 --- a/apps/ensindexer/package.json +++ b/apps/ensindexer/package.json @@ -12,7 +12,7 @@ }, "homepage": "https://github.com/namehash/ensnode/tree/main/apps/ensindexer", "scripts": { - "dev": "DATABASE_SCHEMA=public ponder --root ./ponder dev", + "dev": "DATABASE_SCHEMA=public ponder --root ./ponder dev --disable-ui", "start": "ponder --root ./ponder start", "serve": "ponder --root ./ponder serve", "db": "ponder --root ./ponder db", diff --git a/apps/ensindexer/src/plugins/ensv2/event-handlers.ts b/apps/ensindexer/src/plugins/ensv2/event-handlers.ts new file mode 100644 index 000000000..0edf0027c --- /dev/null +++ b/apps/ensindexer/src/plugins/ensv2/event-handlers.ts @@ -0,0 +1,9 @@ +import { ponder } from "ponder:registry"; + +import { PluginName } from "@ensnode/ensnode-sdk"; + +import { namespaceContract } from "@/lib/plugin-helpers"; + +ponder.on(namespaceContract(PluginName.ENSv2, "RegistryOld:setup"), async ({ context }) => { + console.log("hello world"); +}); diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts new file mode 100644 index 000000000..ec1d74a7e --- /dev/null +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -0,0 +1,38 @@ +import { createConfig } from "ponder"; + +import { DatasourceNames, getDatasource } from "@ensnode/datasources"; +import { PluginName } from "@ensnode/ensnode-sdk"; + +import { createPlugin, namespaceContract } from "@/lib/plugin-helpers"; +import { chainConfigForContract, chainsConnectionConfig } from "@/lib/ponder-helpers"; + +/** + + */ +export const pluginName = PluginName.ENSv2; + +export default createPlugin({ + name: pluginName, + requiredDatasourceNames: [DatasourceNames.ENSRoot], + createPonderConfig(config) { + const ensroot = getDatasource(config.namespace, DatasourceNames.ENSRoot); + + return createConfig({ + chains: chainsConnectionConfig(config.rpcConfigs, ensroot.chain.id), + + contracts: { + // index the RegistryOld on ENS Root Chain + [namespaceContract(pluginName, "RegistryOld")]: { + abi: ensroot.contracts.RegistryOld.abi, + chain: { + ...chainConfigForContract( + config.globalBlockrange, + ensroot.chain.id, + ensroot.contracts.RegistryOld, + ), + }, + }, + }, + }); + }, +}); diff --git a/apps/ensindexer/src/plugins/index.ts b/apps/ensindexer/src/plugins/index.ts index 66dd86cdb..04db9ba90 100644 --- a/apps/ensindexer/src/plugins/index.ts +++ b/apps/ensindexer/src/plugins/index.ts @@ -2,6 +2,8 @@ import type { PluginName } from "@ensnode/ensnode-sdk"; import type { MergedTypes } from "@/lib/lib-helpers"; +// ENSV2 Core Plugin +import ensv2Plugin from "./ensv2/plugin"; // Core-Schema-Indepdendent Plugins import protocolAccelerationPlugin from "./protocol-acceleration/plugin"; import referralsPlugin from "./referrals/plugin"; @@ -20,6 +22,7 @@ export const ALL_PLUGINS = [ tokenScopePlugin, protocolAccelerationPlugin, referralsPlugin, + ensv2Plugin, ] as const; /** diff --git a/packages/ensnode-schema/src/ponder.schema.ts b/packages/ensnode-schema/src/ponder.schema.ts index 11ec675c7..19122e903 100644 --- a/packages/ensnode-schema/src/ponder.schema.ts +++ b/packages/ensnode-schema/src/ponder.schema.ts @@ -2,6 +2,7 @@ * Merge the various sub-schemas into a single ponder (drizzle) schema. */ +export * from "./schemas/ensv2.schema"; export * from "./schemas/protocol-acceleration.schema"; export * from "./schemas/referrals.schema"; export * from "./schemas/subgraph.schema"; diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/schemas/ensv2.schema.ts new file mode 100644 index 000000000..eec268ed2 --- /dev/null +++ b/packages/ensnode-schema/src/schemas/ensv2.schema.ts @@ -0,0 +1,125 @@ +import { onchainEnum, onchainTable, primaryKey, relations } from "ponder"; + +// Registry<->Domain is 1:1 +// Registry->Doimains is 1:many + +/** + * Polymorphism in this Drizzle v1 schema is acomplished with _Type and _Id columns. In the future, + * when ponder supports it, we can/should move to Drizzle v2 conditional relations to support + * polymorphism. + * + * In the future, when Ponder supports `check` constraints, we can include them for additional + * guarantees. + */ + +//////////// +// Registry +//////////// + +export const registryType = onchainEnum("RegistryType", ["RegistryContract", "ImplicitRegistry"]); + +export const registry = onchainTable( + "registries", + (t) => ({ + // If RegistryContract: CAIP-10 Account ID + // If ImplicitRegistry: parentDomainNode + id: t.text().primaryKey(), + type: registryType().notNull(), + + chainId: t.integer(), + address: t.hex(), + parentDomainNode: t.hex(), + }), + (t) => ({ + // TODO: enforce that it is RegistryContract | ImplicitRegistry with associated property constraints + // registryType: check( + // "registry_type_check", + // sql` + // (type = 'RegistryContract' AND + // ${t.chainId} IS NOT NULL AND + // ${t.address} IS NOT NULL AND + // ${t.parentDomainNode} IS NULL) OR + // (type = 'ImplicitRegistry' AND + // ${t.chainId} ISNULL AND + // ${t.address} ISNULL AND + // ${t.parentDomainNode} IS NOT NULL) + // `, + // ), + }), +); + +export const relations_registry = relations(registry, ({ one, many }) => ({ + domain: one(domain, { + relationName: "subregistry", + fields: [registry.id], + references: [domain.registryId], + }), + domains: many(domain, { relationName: "registry" }), + permissions: one(permissions, { + relationName: "permissions", + fields: [registry.chainId, registry.address], + references: [permissions.chainId, permissions.address], + }), +})); + +////////// +// Domain +////////// + +export const domain = onchainTable( + "domains", + (t) => ({ + // belongs to registry + registryId: t.text().notNull(), + tokenId: t.bigint().notNull(), + + labelHash: t.hex().notNull(), + label: t.text().notNull(), + + // may have one subregistry + subregistryId: t.text(), + + _hasAttemptedLabelHeal: t.boolean().notNull().default(false), + }), + (t) => ({ + pk: primaryKey({ columns: [t.registryId, t.tokenId] }), + }), +); + +export const relations_domain = relations(domain, ({ one }) => ({ + registry: one(registry, { + relationName: "registry", + fields: [domain.registryId], + references: [registry.id], + }), + subregistry: one(registry, { + relationName: "subregistry", + fields: [domain.subregistryId], + references: [registry.id], + }), +})); + +/////////////// +// Permissions +/////////////// + +export const permissions = onchainTable( + "permissions", + (t) => ({ + chainId: t.integer().notNull(), + address: t.hex().notNull(), + }), + (t) => ({ + pk: primaryKey({ columns: [t.chainId, t.address] }), + }), +); + +export const relations_permissions = relations(permissions, ({ one, many }) => ({ + registry: one(registry), +})); + +// export const namespaceEntry = onchainTable( +// "namespace_entries", +// (t) => ({}), +// (t) => ({}), +// ); diff --git a/packages/ensnode-sdk/src/ensindexer/config/types.ts b/packages/ensnode-sdk/src/ensindexer/config/types.ts index 9f4f68038..80e17c541 100644 --- a/packages/ensnode-sdk/src/ensindexer/config/types.ts +++ b/packages/ensnode-sdk/src/ensindexer/config/types.ts @@ -15,6 +15,7 @@ export enum PluginName { ProtocolAcceleration = "protocol-acceleration", Referrals = "referrals", TokenScope = "tokenscope", + ENSv2 = "ensv2", } /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9297e793..b974b2766 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,7 +37,7 @@ catalogs: specifier: 4.1.0 version: 4.1.0 drizzle-orm: - specifier: '=0.41.0' + specifier: 0.41.0 version: 0.41.0 hono: specifier: ^4.10.2 @@ -49,8 +49,8 @@ catalogs: specifier: 10.1.0 version: 10.1.0 ponder: - specifier: 0.13.14 - version: 0.13.14 + specifier: 0.14.13 + version: 0.14.13 tsup: specifier: ^8.3.6 version: 8.5.0 @@ -346,7 +346,7 @@ importers: version: 10.1.0 ponder: specifier: 'catalog:' - version: 0.13.14(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/node@22.18.13)(hono@4.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@3.25.76))(yaml@2.8.1)(zod@3.25.76) + version: 0.14.13(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/node@22.18.13)(hono@4.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@3.25.76))(yaml@2.8.1)(zod@3.25.76) ponder-enrich-gql-docs-middleware: specifier: ^0.1.3 version: 0.1.3(graphql@16.11.0)(hono@4.10.3) @@ -416,7 +416,7 @@ importers: version: 2.9.1 ponder: specifier: 'catalog:' - version: 0.13.14(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/node@22.18.13)(hono@4.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@3.25.76))(yaml@2.8.1)(zod@3.25.76) + version: 0.14.13(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/node@22.18.13)(hono@4.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@3.25.76))(yaml@2.8.1)(zod@3.25.76) viem: specifier: 'catalog:' version: 2.38.5(typescript@5.9.3)(zod@3.25.76) @@ -731,7 +731,7 @@ importers: dependencies: ponder: specifier: 'catalog:' - version: 0.13.14(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/node@22.18.13)(hono@4.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@3.25.76))(yaml@2.8.1)(zod@3.25.76) + version: 0.14.13(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/node@22.18.13)(hono@4.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@3.25.76))(yaml@2.8.1)(zod@3.25.76) viem: specifier: 'catalog:' version: 2.38.5(typescript@5.9.3)(zod@3.25.76) @@ -831,7 +831,7 @@ importers: version: 4.10.3 ponder: specifier: 'catalog:' - version: 0.13.14(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/node@22.18.13)(hono@4.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@3.25.76))(yaml@2.8.1)(zod@3.25.76) + version: 0.14.13(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/node@22.18.13)(hono@4.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@3.25.76))(yaml@2.8.1)(zod@3.25.76) tsup: specifier: 'catalog:' version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) @@ -1671,12 +1671,6 @@ packages: peerDependencies: react: '>= 16 || ^19.0.0-rc' - '@hono/node-server@1.13.3': - resolution: {integrity: sha512-tEo3hcyQ6chvSnJ3tKzfX4z2sd7Q+ZkBwwBdW1Ya8Mz29dukxC2xcWiB/lAMwGJrYMW8QTgknIsLu1AsnMBe7A==} - engines: {node: '>=18.14.1'} - peerDependencies: - hono: ^4 - '@hono/node-server@1.19.5': resolution: {integrity: sha512-iBuhh+uaaggeAuf+TftcjZyWh2GEgZcVGXkNtskLVoWaXhnJtC5HLHrU8W1KHDoucqO1MswwglmkWLFyiDn4WQ==} engines: {node: '>=18.14.1'} @@ -2403,6 +2397,15 @@ packages: typescript: optional: true + '@ponder/utils@0.2.15': + resolution: {integrity: sha512-3qb7FvCIJuabBvLUfh89pL1w/kcKmvQr6bdlg3LbCZGipfhphqmBwN+eZcgy8aiceHStc4ebLFe79bZitREopA==} + peerDependencies: + typescript: '>=5.0.4' + viem: '>=2' + peerDependenciesMeta: + typescript: + optional: true + '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -6214,8 +6217,8 @@ packages: graphql: ^16.10.0 hono: ^4.6.19 - ponder@0.13.14: - resolution: {integrity: sha512-TGQ7vH75iHasWUyCSNEgtKOzs0YbllxC+rA838jCjCarbkBvFSOD1eXU9QtzF91P0R+0MIecZkmwlYJwz9ZmEw==} + ponder@0.14.13: + resolution: {integrity: sha512-NrOm76TB7ppHyZpfWdrfDX7A4B0DYFn4Ycgs5ipm4QT9yE0PUNvT0aZNSiZx2cniQCZAbh0kDczZmeFNRUZoCg==} engines: {node: '>=18.14'} hasBin: true peerDependencies: @@ -8881,10 +8884,6 @@ snapshots: dependencies: react: 18.3.1 - '@hono/node-server@1.13.3(hono@4.10.3)': - dependencies: - hono: 4.10.3 - '@hono/node-server@1.19.5(hono@4.10.3)': dependencies: hono: 4.10.3 @@ -9669,6 +9668,12 @@ snapshots: optionalDependencies: typescript: 5.9.3 + '@ponder/utils@0.2.15(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@3.25.76))': + dependencies: + viem: 2.38.5(typescript@5.9.3)(zod@3.25.76) + optionalDependencies: + typescript: 5.9.3 + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -13997,7 +14002,7 @@ snapshots: graphql: 16.11.0 hono: 4.10.3 - ponder@0.13.14(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/node@22.18.13)(hono@4.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@3.25.76))(yaml@2.8.1)(zod@3.25.76): + ponder@0.14.13(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/node@22.18.13)(hono@4.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@3.25.76))(yaml@2.8.1)(zod@3.25.76): dependencies: '@babel/code-frame': 7.27.1 '@commander-js/extra-typings': 12.1.0(commander@12.1.0) @@ -14005,8 +14010,8 @@ snapshots: '@escape.tech/graphql-armor-max-aliases': 2.6.2 '@escape.tech/graphql-armor-max-depth': 2.4.2 '@escape.tech/graphql-armor-max-tokens': 2.5.1 - '@hono/node-server': 1.13.3(hono@4.10.3) - '@ponder/utils': 0.2.14(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@3.25.76)) + '@hono/node-server': 1.19.5(hono@4.10.3) + '@ponder/utils': 0.2.15(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@3.25.76)) abitype: 0.10.3(typescript@5.9.3)(zod@3.25.76) ansi-escapes: 7.1.1 commander: 12.1.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6e603a251..2c9b8e55e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,22 +4,22 @@ packages: - packages/* catalog: - "@adraffy/ens-normalize": 1.11.1 - "@astrojs/react": ^4.4.1 - "@astrojs/tailwind": ^6.0.2 - "@namehash/namekit-react": 0.12.0 - "@ponder/utils": 0.2.14 - "@types/node": ^22.14.0 + '@adraffy/ens-normalize': 1.11.1 + '@astrojs/react': ^4.4.1 + '@astrojs/tailwind': ^6.0.2 + '@namehash/namekit-react': 0.12.0 + '@ponder/utils': 0.2.14 + '@types/node': ^22.14.0 astro: ^5.15.2 astro-font: ^1.1.0 astro-seo: ^0.8.4 caip: 1.1.1 date-fns: 4.1.0 - drizzle-orm: "=0.41.0" + drizzle-orm: 0.41.0 hono: ^4.10.2 pg-connection-string: ^2.9.1 pino: 10.1.0 - ponder: 0.13.14 + ponder: 0.14.13 tsup: ^8.3.6 typescript: ^5.7.3 viem: ^2.22.13 From fc91a0ff7e3b4cdbcaafb9f405d3c37e94ab442e Mon Sep 17 00:00:00 2001 From: shrugs Date: Thu, 6 Nov 2025 17:19:25 -0600 Subject: [PATCH 002/102] checkpoint --- .../ens-root-registry.ts | 2 +- .../ponder/src/register-handlers.ts | 6 + apps/ensindexer/src/lib/make-account-id.ts | 6 + apps/ensindexer/src/lib/plugin-helpers.ts | 1 + apps/ensindexer/src/lib/ponder-helpers.ts | 7 +- .../resolver-records-db-helpers.ts | 11 +- .../src/plugins/ensv2/event-handlers.ts | 14 +- .../ensv2/handlers/EnhancedAccessControl.ts | 124 +++++ .../src/plugins/ensv2/handlers/Registry.ts | 145 ++++++ apps/ensindexer/src/plugins/ensv2/plugin.ts | 75 ++- .../handlers/Registry.ts | 10 +- .../handlers/Resolver.ts | 18 +- .../plugins/protocol-acceleration/plugin.ts | 14 +- .../plugins/subgraph/handlers/Registry.ts | 65 ++- .../subgraph/plugins/subgraph/plugin.ts | 20 +- .../abis/namechain/EnhancedAccessControl.ts | 469 +++++++++++++++++ .../src/abis/namechain/Registry.ts | 484 ++++++++++++++++++ packages/datasources/src/ens-test-env.ts | 44 +- packages/datasources/src/holesky.ts | 9 +- packages/datasources/src/index.ts | 3 +- packages/datasources/src/lib/resolver.ts | 28 - packages/datasources/src/lib/types.ts | 34 +- packages/datasources/src/mainnet.ts | 43 +- packages/datasources/src/sepolia.ts | 11 +- .../src/schemas/ensv2.schema.ts | 86 +++- .../schemas/protocol-acceleration.schema.ts | 26 +- pnpm-workspace.yaml | 12 +- 27 files changed, 1578 insertions(+), 189 deletions(-) create mode 100644 apps/ensindexer/src/lib/make-account-id.ts create mode 100644 apps/ensindexer/src/plugins/ensv2/handlers/EnhancedAccessControl.ts create mode 100644 apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts create mode 100644 packages/datasources/src/abis/namechain/EnhancedAccessControl.ts create mode 100644 packages/datasources/src/abis/namechain/Registry.ts diff --git a/apps/ensapi/src/lib/protocol-acceleration/ens-root-registry.ts b/apps/ensapi/src/lib/protocol-acceleration/ens-root-registry.ts index 02113f4b4..c03bdb9d4 100644 --- a/apps/ensapi/src/lib/protocol-acceleration/ens-root-registry.ts +++ b/apps/ensapi/src/lib/protocol-acceleration/ens-root-registry.ts @@ -10,7 +10,7 @@ const ensRoot = getDatasource(config.namespace, DatasourceNames.ENSRoot); */ export const ENS_ROOT_REGISTRY: AccountId = { chainId: ensRoot.chain.id, - address: ensRoot.contracts.Registry.address, + address: ensRoot.contracts.ENSv1Registry.address, }; export function isENSRootRegistry(accountId: AccountId) { diff --git a/apps/ensindexer/ponder/src/register-handlers.ts b/apps/ensindexer/ponder/src/register-handlers.ts index 05e95d3f7..874fedb02 100644 --- a/apps/ensindexer/ponder/src/register-handlers.ts +++ b/apps/ensindexer/ponder/src/register-handlers.ts @@ -7,6 +7,7 @@ import config from "@/config"; import { PluginName } from "@ensnode/ensnode-sdk"; +import attach_ENSv2Handlers from "@/plugins/ensv2/event-handlers"; import attach_protocolAccelerationHandlers from "@/plugins/protocol-acceleration/event-handlers"; import attach_RegistrarsHandlers from "@/plugins/registrars/event-handlers"; import attach_BasenamesHandlers from "@/plugins/subgraph/plugins/basenames/event-handlers"; @@ -49,3 +50,8 @@ if (config.plugins.includes(PluginName.Registrars)) { if (config.plugins.includes(PluginName.TokenScope)) { attach_TokenscopeHandlers(); } + +// ENSv2 Plugin +if (config.plugins.includes(PluginName.ENSv2)) { + attach_ENSv2Handlers(); +} diff --git a/apps/ensindexer/src/lib/make-account-id.ts b/apps/ensindexer/src/lib/make-account-id.ts new file mode 100644 index 000000000..90c9d1485 --- /dev/null +++ b/apps/ensindexer/src/lib/make-account-id.ts @@ -0,0 +1,6 @@ +import type { Context, Event } from "ponder:registry"; + +import type { AccountId } from "@ensnode/ensnode-sdk"; + +export const makeAccountId = (context: Context, event: Event) => + ({ chainId: context.chain.id, address: event.log.address }) satisfies AccountId; diff --git a/apps/ensindexer/src/lib/plugin-helpers.ts b/apps/ensindexer/src/lib/plugin-helpers.ts index f48c6f451..d0bf8cf19 100644 --- a/apps/ensindexer/src/lib/plugin-helpers.ts +++ b/apps/ensindexer/src/lib/plugin-helpers.ts @@ -4,6 +4,7 @@ import { type DatasourceName, type ENSNamespaceId, getENSNamespace } from "@ensn import { PluginName, uniq } from "@ensnode/ensnode-sdk"; import type { ENSIndexerConfig } from "@/config/types"; +import type { MergedTypes } from "@/lib/lib-helpers"; /** * Creates a namespaced contract name for Ponder handlers. diff --git a/apps/ensindexer/src/lib/ponder-helpers.ts b/apps/ensindexer/src/lib/ponder-helpers.ts index 21d1dd493..ae6842b64 100644 --- a/apps/ensindexer/src/lib/ponder-helpers.ts +++ b/apps/ensindexer/src/lib/ponder-helpers.ts @@ -9,7 +9,7 @@ import type { ChainConfig } from "ponder"; import type { Address, PublicClient } from "viem"; import * as z from "zod/v4"; -import type { ContractConfig } from "@ensnode/datasources"; +import { type ContractConfig, ensTestEnvL1Chain, ensTestEnvL2Chain } from "@ensnode/datasources"; import type { Blockrange, ChainId } from "@ensnode/ensnode-sdk"; import type { BlockInfo, PonderStatus } from "@ensnode/ponder-metadata"; @@ -280,7 +280,10 @@ export function chainsConnectionConfig( rpc: rpcConfig.httpRPCs.map((httpRPC) => httpRPC.toString()), ws: rpcConfig.websocketRPC?.toString(), // NOTE: disable cache on local chains (e.g. Anvil, Ganache) - ...((chainId === 31337 || chainId === 1337) && { disableCache: true }), + ...((chainId === 31337 || + chainId === 1337 || + chainId === ensTestEnvL1Chain.id || + chainId === ensTestEnvL2Chain.id) && { disableCache: true }), } satisfies ChainConfig, }; } diff --git a/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts b/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts index 982e523b4..d5e68a5e7 100644 --- a/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts +++ b/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts @@ -37,9 +37,16 @@ export function makeResolverRecordsId( } /** - * Ensures that the ResolverRecords entity described by `id` exists. + * Ensures that the Resolver and ResolverRecords entities described by `id` exists. */ -export async function ensureResolverRecords(context: Context, id: ResolverRecordsId) { +export async function ensureResolverAndResolverRecords(context: Context, id: ResolverRecordsId) { + // ensure Resolver + await context.db + .insert(schema.resolver) + .values({ chainId: id.chainId, address: id.resolver }) + .onConflictDoNothing(); + + // ensure ResolverRecords await context.db.insert(schema.resolverRecords).values(id).onConflictDoNothing(); } diff --git a/apps/ensindexer/src/plugins/ensv2/event-handlers.ts b/apps/ensindexer/src/plugins/ensv2/event-handlers.ts index 0edf0027c..ef682c228 100644 --- a/apps/ensindexer/src/plugins/ensv2/event-handlers.ts +++ b/apps/ensindexer/src/plugins/ensv2/event-handlers.ts @@ -1,9 +1,7 @@ -import { ponder } from "ponder:registry"; +import attach_EnhancedAccessControlHandlers from "./handlers/EnhancedAccessControl"; +import attach_RegistryHandlers from "./handlers/Registry"; -import { PluginName } from "@ensnode/ensnode-sdk"; - -import { namespaceContract } from "@/lib/plugin-helpers"; - -ponder.on(namespaceContract(PluginName.ENSv2, "RegistryOld:setup"), async ({ context }) => { - console.log("hello world"); -}); +export default function () { + attach_RegistryHandlers(); + attach_EnhancedAccessControlHandlers(); +} diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/EnhancedAccessControl.ts b/apps/ensindexer/src/plugins/ensv2/handlers/EnhancedAccessControl.ts new file mode 100644 index 000000000..af93d9dfd --- /dev/null +++ b/apps/ensindexer/src/plugins/ensv2/handlers/EnhancedAccessControl.ts @@ -0,0 +1,124 @@ +import { type Context, ponder } from "ponder:registry"; +import schema from "ponder:schema"; +import type { Address } from "viem"; + +import { PluginName } from "@ensnode/ensnode-sdk"; + +import { makeAccountId } from "@/lib/make-account-id"; +import { namespaceContract } from "@/lib/plugin-helpers"; +import type { EventWithArgs } from "@/lib/ponder-helpers"; + +/** + * Infer the type of the Permission entity's composite primary key. + */ +type PermissionsId = Pick; + +/** + * Infer the type of the PermissionsUsers entity's composite primary key. + */ +type PermissionsUsersId = Pick< + typeof schema.permissionsUser.$inferInsert, + "chainId" | "address" | "resource" | "user" +>; + +const ensurePermissionsResource = async (context: Context, id: PermissionsId, resource: bigint) => { + await context.db.insert(schema.permissions).values(id).onConflictDoNothing(); + await context.db + .insert(schema.permissionsResource) + .values({ ...id, resource }) + .onConflictDoNothing(); +}; + +const isZeroRoles = (roles: bigint) => roles === 0n; + +async function upsertNewRoles(context: Context, id: PermissionsUsersId, roles: bigint) { + if (isZeroRoles(roles)) { + // ensure deleted + await context.db.delete(schema.permissionsUser, id); + } else { + // ensure upserted + await context.db + .insert(schema.permissionsUser) + .values({ ...id, roles }) + .onConflictDoUpdate({ roles }); + } +} + +export default function () { + ponder.on( + namespaceContract(PluginName.ENSv2, "EnhancedAccessControl:EACRolesGranted"), + async ({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ + resource: bigint; + roleBitmap: bigint; + account: Address; + }>; + }) => { + const { resource, roleBitmap: roles, account: user } = event.args; + + const accountId = makeAccountId(context, event); + await ensurePermissionsResource(context, accountId, resource); + + const permissionsUserId: PermissionsUsersId = { ...accountId, resource, user }; + const existing = await context.db.find(schema.permissionsUser, permissionsUserId); + + // https://github.com/ensdomains/namechain/blob/main/contracts/src/common/access-control/EnhancedAccessControl.sol#L292 + const newRoles = (existing?.roles ?? 0n) | roles; + await upsertNewRoles(context, permissionsUserId, newRoles); + }, + ); + + ponder.on( + namespaceContract(PluginName.ENSv2, "EnhancedAccessControl:EACRolesRevoked"), + async ({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ + resource: bigint; + roleBitmap: bigint; + account: Address; + }>; + }) => { + const { resource, roleBitmap: roles, account: user } = event.args; + + const accountId = makeAccountId(context, event); + await ensurePermissionsResource(context, accountId, resource); + + const permissionsUserId: PermissionsUsersId = { ...accountId, resource, user }; + const existing = await context.db.find(schema.permissionsUser, permissionsUserId); + + // https://github.com/ensdomains/namechain/blob/main/contracts/src/common/access-control/EnhancedAccessControl.sol#L325 + const newRoles = (existing?.roles ?? 0n) & ~roles; + await upsertNewRoles(context, permissionsUserId, newRoles); + }, + ); + + ponder.on( + namespaceContract(PluginName.ENSv2, "EnhancedAccessControl:EACAllRolesRevoked"), + async ({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ + resource: bigint; + account: Address; + }>; + }) => { + const { resource, account: user } = event.args; + + const accountId = makeAccountId(context, event); + await ensurePermissionsResource(context, accountId, resource); + + const permissionsUserId: PermissionsUsersId = { ...accountId, resource, user }; + + await upsertNewRoles(context, permissionsUserId, 0n); + }, + ); +} diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts new file mode 100644 index 000000000..482c75836 --- /dev/null +++ b/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts @@ -0,0 +1,145 @@ +import { type Context, ponder } from "ponder:registry"; +import schema from "ponder:schema"; +import { replaceBigInts } from "ponder"; +import { type Address, hexToBigInt, isAddressEqual, labelhash, zeroAddress } from "viem"; + +import { type AccountId, PluginName, serializeAccountId } from "@ensnode/ensnode-sdk"; + +import { makeAccountId } from "@/lib/make-account-id"; +import { namespaceContract } from "@/lib/plugin-helpers"; +import type { EventWithArgs } from "@/lib/ponder-helpers"; + +// will need to filter all ERC1155 events by whether a Registry entity exists already or not, to +// avoid indexing literally all ERC1155 contracts on every chain. we still filter for those, which +// is awful performance.. hmm... + +/** + * A Domain's canonical ID is uint256(labelHash) with right-most 32 bits zero'd. + */ +const getCanonicalId = (tokenId: bigint) => tokenId ^ (tokenId & 0xffffffffn); + +export default function () { + ponder.on( + namespaceContract(PluginName.ENSv2, "Registry:NameRegistered"), + async ({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ + tokenId: bigint; + label: string; + expiration: bigint; + registeredBy: Address; + }>; + }) => { + const { tokenId, label, expiration, registeredBy: registrant } = event.args; + + const registryAccountId = makeAccountId(context, event); + const registryId = serializeAccountId(registryAccountId); + await context.db + .insert(schema.registry) + .values({ + id: registryId, + type: "RegistryContract", + ...registryAccountId, + }) + .onConflictDoNothing(); + + const canonicalId = getCanonicalId(tokenId); + const labelHash = labelhash(label); + + // Sanity Check: Canonical Id must match emitted label + if (canonicalId !== getCanonicalId(hexToBigInt(labelhash(label)))) { + throw new Error( + `Sanity Check: Domain's Canonical Id !== getCanonicalId(uint256(labelhash(label)))\n${JSON.stringify( + replaceBigInts( + { + tokenId, + canonicalId, + label, + labelHash, + hexToBigInt: hexToBigInt(labelhash(label)), + }, + String, + ), + )}`, + ); + } + + await context.db.insert(schema.domain).values({ + registryId, + canonicalId, + labelHash, + label, + }); + + // TODO: insert Registration entity for this domain as well: expiration, registrant + }, + ); + + ponder.on( + namespaceContract(PluginName.ENSv2, "Registry:SubregistryUpdate"), + async ({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ + id: bigint; + subregistry: Address; + }>; + }) => { + const { id: tokenId, subregistry } = event.args; + + const canonicalId = getCanonicalId(tokenId); + const registryAccountId = makeAccountId(context, event); + const registryId = serializeAccountId(registryAccountId); + + // update domain's subregistry + const isDeletion = isAddressEqual(subregistry, zeroAddress); + if (isDeletion) { + await context.db + .update(schema.domain, { registryId, canonicalId }) + .set({ subregistryId: null }); + } else { + const subregistryAccountId: AccountId = { chainId: context.chain.id, address: subregistry }; + const subregistryId = serializeAccountId(subregistryAccountId); + + await context.db.update(schema.domain, { registryId, canonicalId }).set({ subregistryId }); + } + }, + ); + + ponder.on( + namespaceContract(PluginName.ENSv2, "Registry:ResolverUpdate"), + async ({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ + id: bigint; + resolver: Address; + }>; + }) => { + const { id: tokenId, resolver } = event.args; + + const canonicalId = getCanonicalId(tokenId); + const registryAccountId = makeAccountId(context, event); + const registryId = serializeAccountId(registryAccountId); + + // update domain's resolver + const isDeletion = isAddressEqual(resolver, zeroAddress); + if (isDeletion) { + await context.db + .update(schema.domain, { registryId, canonicalId }) + .set({ resolverChainId: null, resolverAddress: null }); + } else { + await context.db + .update(schema.domain, { registryId, canonicalId }) + .set({ resolverChainId: context.chain.id, resolverAddress: resolver }); + } + }, + ); +} diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index ec1d74a7e..30a696397 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -1,6 +1,14 @@ -import { createConfig } from "ponder"; +import { type ChainConfig, createConfig } from "ponder"; -import { DatasourceNames, getDatasource } from "@ensnode/datasources"; +import { + type DatasourceName, + DatasourceNames, + ENSNamespaceIds, + EnhancedAccessControlABI, + getDatasource, + maybeGetDatasource, + RegistryABI, +} from "@ensnode/datasources"; import { PluginName } from "@ensnode/ensnode-sdk"; import { createPlugin, namespaceContract } from "@/lib/plugin-helpers"; @@ -11,26 +19,65 @@ import { chainConfigForContract, chainsConnectionConfig } from "@/lib/ponder-hel */ export const pluginName = PluginName.ENSv2; +const ALL_DATASOURCE_NAMES = [ + DatasourceNames.ENSRoot, + DatasourceNames.Namechain, +] as const satisfies DatasourceName[]; + export default createPlugin({ name: pluginName, - requiredDatasourceNames: [DatasourceNames.ENSRoot], + requiredDatasourceNames: ALL_DATASOURCE_NAMES, createPonderConfig(config) { + // TODO: remove this, helps with types while only targeting ens-test-env + if (config.namespace !== ENSNamespaceIds.EnsTestEnv) process.exit(1); + const ensroot = getDatasource(config.namespace, DatasourceNames.ENSRoot); + // biome-ignore lint/style/noNonNullAssertion: allowed for now + const namechain = maybeGetDatasource(config.namespace, DatasourceNames.Namechain)!; + + const allDatasources = ALL_DATASOURCE_NAMES.map((datasourceName) => + maybeGetDatasource(config.namespace, datasourceName), + ).filter((datasource) => !!datasource); return createConfig({ - chains: chainsConnectionConfig(config.rpcConfigs, ensroot.chain.id), + chains: allDatasources + .map((datasource) => datasource.chain) + .reduce>( + (memo, chain) => ({ + ...memo, + ...chainsConnectionConfig(config.rpcConfigs, chain.id), + }), + {}, + ), contracts: { - // index the RegistryOld on ENS Root Chain - [namespaceContract(pluginName, "RegistryOld")]: { - abi: ensroot.contracts.RegistryOld.abi, - chain: { - ...chainConfigForContract( - config.globalBlockrange, - ensroot.chain.id, - ensroot.contracts.RegistryOld, - ), - }, + [namespaceContract(pluginName, "Registry")]: { + abi: RegistryABI, + chain: [ensroot, namechain].reduce( + (memo, datasource) => ({ + ...memo, + ...chainConfigForContract( + config.globalBlockrange, + datasource.chain.id, + datasource.contracts.Registry, + ), + }), + {}, + ), + }, + [namespaceContract(pluginName, "EnhancedAccessControl")]: { + abi: EnhancedAccessControlABI, + chain: [ensroot, namechain].reduce( + (memo, datasource) => ({ + ...memo, + ...chainConfigForContract( + config.globalBlockrange, + datasource.chain.id, + datasource.contracts.EnhancedAccessControl, + ), + }), + {}, + ), }, }, }); diff --git a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Registry.ts b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Registry.ts index 7416934c2..c31a4c1a3 100644 --- a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Registry.ts +++ b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Registry.ts @@ -47,7 +47,7 @@ export default function () { * - ENS Root Chain's (new) Registry */ ponder.on( - namespaceContract(PluginName.ProtocolAcceleration, "Registry:NewOwner"), + namespaceContract(PluginName.ProtocolAcceleration, "ENSv1Registry:NewOwner"), async ({ context, event, @@ -72,10 +72,10 @@ export default function () { /** * Handles Registry#NewResolver for: - * - ENS Root Chain's RegistryOld + * - ENS Root Chain's ENSv1RegistryOld */ ponder.on( - namespaceContract(PluginName.ProtocolAcceleration, "RegistryOld:NewResolver"), + namespaceContract(PluginName.ProtocolAcceleration, "ENSv1RegistryOld:NewResolver"), async ({ context, event, @@ -83,7 +83,7 @@ export default function () { context: Context; event: EventWithArgs<{ node: Node; resolver: Address }>; }) => { - // ignore the event on RegistryOld if node is migrated to new Registry + // ignore the event on ENSv1RegistryOld if node is migrated to new Registry const shouldIgnoreEvent = await nodeIsMigrated(context, event.args.node); if (shouldIgnoreEvent) return; @@ -98,7 +98,7 @@ export default function () { * - Lineanames's (shadow) Registry */ ponder.on( - namespaceContract(PluginName.ProtocolAcceleration, "Registry:NewResolver"), + namespaceContract(PluginName.ProtocolAcceleration, "ENSv1Registry:NewResolver"), async ({ context, event, diff --git a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Resolver.ts b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Resolver.ts index 9d0bc2f3f..cd113ca02 100644 --- a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Resolver.ts +++ b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Resolver.ts @@ -5,7 +5,7 @@ import { ETH_COIN_TYPE, PluginName } from "@ensnode/ensnode-sdk"; import { parseDnsTxtRecordArgs } from "@/lib/dns-helpers"; import { namespaceContract } from "@/lib/plugin-helpers"; import { - ensureResolverRecords, + ensureResolverAndResolverRecords, handleResolverAddressRecordUpdate, handleResolverNameUpdate, handleResolverTextRecordUpdate, @@ -23,7 +23,7 @@ export default function () { const { a: address } = event.args; const id = makeResolverRecordsId(context, event); - await ensureResolverRecords(context, id); + await ensureResolverAndResolverRecords(context, id); // the Resolver#AddrChanged event is just Resolver#AddressChanged with implicit coinType of ETH await handleResolverAddressRecordUpdate(context, id, BigInt(ETH_COIN_TYPE), address); @@ -36,7 +36,7 @@ export default function () { const { coinType, newAddress } = event.args; const id = makeResolverRecordsId(context, event); - await ensureResolverRecords(context, id); + await ensureResolverAndResolverRecords(context, id); await handleResolverAddressRecordUpdate(context, id, coinType, newAddress); }, ); @@ -47,7 +47,7 @@ export default function () { const { name } = event.args; const id = makeResolverRecordsId(context, event); - await ensureResolverRecords(context, id); + await ensureResolverAndResolverRecords(context, id); await handleResolverNameUpdate(context, id, name); }, ); @@ -75,7 +75,7 @@ export default function () { } catch {} // no-op if readContract throws for whatever reason const id = makeResolverRecordsId(context, event); - await ensureResolverRecords(context, id); + await ensureResolverAndResolverRecords(context, id); await handleResolverTextRecordUpdate(context, id, key, value); }, ); @@ -89,7 +89,7 @@ export default function () { const { key, value } = event.args; const id = makeResolverRecordsId(context, event); - await ensureResolverRecords(context, id); + await ensureResolverAndResolverRecords(context, id); await handleResolverTextRecordUpdate(context, id, key, value); }, ); @@ -106,7 +106,7 @@ export default function () { if (key === null) return; // no key to operate over? args were malformed, ignore event const id = makeResolverRecordsId(context, event); - await ensureResolverRecords(context, id); + await ensureResolverAndResolverRecords(context, id); await handleResolverTextRecordUpdate(context, id, key, value); }, ); @@ -122,7 +122,7 @@ export default function () { if (key === null) return; // no key to operate over? args were malformed, ignore event const id = makeResolverRecordsId(context, event); - await ensureResolverRecords(context, id); + await ensureResolverAndResolverRecords(context, id); await handleResolverTextRecordUpdate(context, id, key, value); }, ); @@ -134,7 +134,7 @@ export default function () { if (key === null) return; // no key to operate over? args were malformed, ignore event const id = makeResolverRecordsId(context, event); - await ensureResolverRecords(context, id); + await ensureResolverAndResolverRecords(context, id); await handleResolverTextRecordUpdate(context, id, key, null); }, ); diff --git a/apps/ensindexer/src/plugins/protocol-acceleration/plugin.ts b/apps/ensindexer/src/plugins/protocol-acceleration/plugin.ts index 97e17312f..8b2bf4ccb 100644 --- a/apps/ensindexer/src/plugins/protocol-acceleration/plugin.ts +++ b/apps/ensindexer/src/plugins/protocol-acceleration/plugin.ts @@ -131,27 +131,27 @@ export default createPlugin({ ), }, - // index the RegistryOld on ENS Root Chain - [namespaceContract(pluginName, "RegistryOld")]: { - abi: ensroot.contracts.RegistryOld.abi, + // index the ENSv1RegistryOld on ENS Root Chain + [namespaceContract(pluginName, "ENSv1RegistryOld")]: { + abi: ensroot.contracts.ENSv1RegistryOld.abi, chain: { ...chainConfigForContract( config.globalBlockrange, ensroot.chain.id, - ensroot.contracts.RegistryOld, + ensroot.contracts.ENSv1RegistryOld, ), }, }, // a multi-chain Registry ContractConfig - [namespaceContract(pluginName, "Registry")]: { - abi: ensroot.contracts.Registry.abi, + [namespaceContract(pluginName, "ENSv1Registry")]: { + abi: ensroot.contracts.ENSv1Registry.abi, chain: { // ENS Root Chain Registry ...chainConfigForContract( config.globalBlockrange, ensroot.chain.id, - ensroot.contracts.Registry, + ensroot.contracts.ENSv1Registry, ), // Basenames (shadow)Registry ...(basenames && diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/handlers/Registry.ts b/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/handlers/Registry.ts index dad6df0aa..a3efc376d 100644 --- a/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/handlers/Registry.ts +++ b/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/handlers/Registry.ts @@ -31,22 +31,25 @@ async function shouldIgnoreRegistryOldEvents(context: Context, node: Node) { export default function () { const pluginName = PluginName.Subgraph; - ponder.on(namespaceContract(pluginName, "RegistryOld:setup"), setupRootNode); + ponder.on(namespaceContract(pluginName, "ENSv1RegistryOld:setup"), setupRootNode); // old registry functions are proxied to the current handlers // iff the domain has not yet been migrated - ponder.on(namespaceContract(pluginName, "RegistryOld:NewOwner"), async ({ context, event }) => { - const { label: labelHash, node: parentNode } = event.args; + ponder.on( + namespaceContract(pluginName, "ENSv1RegistryOld:NewOwner"), + async ({ context, event }) => { + const { label: labelHash, node: parentNode } = event.args; - const node = makeSubdomainNode(labelHash, parentNode); - const shouldIgnoreEvent = await shouldIgnoreRegistryOldEvents(context, node); - if (shouldIgnoreEvent) return; + const node = makeSubdomainNode(labelHash, parentNode); + const shouldIgnoreEvent = await shouldIgnoreRegistryOldEvents(context, node); + if (shouldIgnoreEvent) return; - return handleNewOwner(false)({ context, event }); - }); + return handleNewOwner(false)({ context, event }); + }, + ); ponder.on( - namespaceContract(pluginName, "RegistryOld:NewResolver"), + namespaceContract(pluginName, "ENSv1RegistryOld:NewResolver"), async ({ context, event }) => { const shouldIgnoreEvent = await shouldIgnoreRegistryOldEvents(context, event.args.node); const isRootNode = event.args.node === ROOT_NODE; @@ -60,28 +63,34 @@ export default function () { }, ); - ponder.on(namespaceContract(pluginName, "RegistryOld:NewTTL"), async ({ context, event }) => { - const shouldIgnoreEvent = await shouldIgnoreRegistryOldEvents(context, event.args.node); - if (shouldIgnoreEvent) return; + ponder.on( + namespaceContract(pluginName, "ENSv1RegistryOld:NewTTL"), + async ({ context, event }) => { + const shouldIgnoreEvent = await shouldIgnoreRegistryOldEvents(context, event.args.node); + if (shouldIgnoreEvent) return; - return handleNewTTL({ context, event }); - }); + return handleNewTTL({ context, event }); + }, + ); - ponder.on(namespaceContract(pluginName, "RegistryOld:Transfer"), async ({ context, event }) => { - // NOTE: this logic derived from the subgraph introduces a bug for queries with a blockheight - // below 9380380, when the new Registry was deployed, as it implicitly ignores Transfer events - // of the ROOT_NODE. as a result, the root node's owner is always zeroAddress until the new - // Registry events are picked up. for backwards compatibility this beahvior is re-implemented - // here. + ponder.on( + namespaceContract(pluginName, "ENSv1RegistryOld:Transfer"), + async ({ context, event }) => { + // NOTE: this logic derived from the subgraph introduces a bug for queries with a blockheight + // below 9380380, when the new Registry was deployed, as it implicitly ignores Transfer events + // of the ROOT_NODE. as a result, the root node's owner is always zeroAddress until the new + // Registry events are picked up. for backwards compatibility this beahvior is re-implemented + // here. - const shouldIgnoreEvent = await shouldIgnoreRegistryOldEvents(context, event.args.node); - if (shouldIgnoreEvent) return; + const shouldIgnoreEvent = await shouldIgnoreRegistryOldEvents(context, event.args.node); + if (shouldIgnoreEvent) return; - return handleTransfer({ context, event }); - }); + return handleTransfer({ context, event }); + }, + ); - ponder.on(namespaceContract(pluginName, "Registry:NewOwner"), handleNewOwner(true)); - ponder.on(namespaceContract(pluginName, "Registry:NewResolver"), handleNewResolver); - ponder.on(namespaceContract(pluginName, "Registry:NewTTL"), handleNewTTL); - ponder.on(namespaceContract(pluginName, "Registry:Transfer"), handleTransfer); + ponder.on(namespaceContract(pluginName, "ENSv1Registry:NewOwner"), handleNewOwner(true)); + ponder.on(namespaceContract(pluginName, "ENSv1Registry:NewResolver"), handleNewResolver); + ponder.on(namespaceContract(pluginName, "ENSv1Registry:NewTTL"), handleNewTTL); + ponder.on(namespaceContract(pluginName, "ENSv1Registry:Transfer"), handleTransfer); } diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/plugin.ts b/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/plugin.ts index a4e050380..7562c100c 100644 --- a/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/plugin.ts +++ b/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/plugin.ts @@ -29,13 +29,21 @@ export default createPlugin({ return ponder.createConfig({ chains: chainsConnectionConfig(config.rpcConfigs, chain.id), contracts: { - [namespaceContract(pluginName, "RegistryOld")]: { - chain: chainConfigForContract(config.globalBlockrange, chain.id, contracts.RegistryOld), - abi: contracts.Registry.abi, + [namespaceContract(pluginName, "ENSv1RegistryOld")]: { + chain: chainConfigForContract( + config.globalBlockrange, + chain.id, + contracts.ENSv1RegistryOld, + ), + abi: contracts.ENSv1RegistryOld.abi, }, - [namespaceContract(pluginName, "Registry")]: { - chain: chainConfigForContract(config.globalBlockrange, chain.id, contracts.Registry), - abi: contracts.Registry.abi, + [namespaceContract(pluginName, "ENSv1Registry")]: { + chain: chainConfigForContract( + config.globalBlockrange, + chain.id, + contracts.ENSv1RegistryOld, + ), + abi: contracts.ENSv1Registry.abi, }, [namespaceContract(pluginName, "BaseRegistrar")]: { chain: chainConfigForContract(config.globalBlockrange, chain.id, contracts.BaseRegistrar), diff --git a/packages/datasources/src/abis/namechain/EnhancedAccessControl.ts b/packages/datasources/src/abis/namechain/EnhancedAccessControl.ts new file mode 100644 index 000000000..15fd8ad5d --- /dev/null +++ b/packages/datasources/src/abis/namechain/EnhancedAccessControl.ts @@ -0,0 +1,469 @@ +export const EnhancedAccessControl = [ + { + type: "function", + name: "ROOT_RESOURCE", + inputs: [], + outputs: [ + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "getAssigneeCount", + inputs: [ + { + name: "resource", + type: "uint256", + internalType: "uint256", + }, + { + name: "roleBitmap", + type: "uint256", + internalType: "uint256", + }, + ], + outputs: [ + { + name: "counts", + type: "uint256", + internalType: "uint256", + }, + { + name: "mask", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "grantRoles", + inputs: [ + { + name: "resource", + type: "uint256", + internalType: "uint256", + }, + { + name: "roleBitmap", + type: "uint256", + internalType: "uint256", + }, + { + name: "account", + type: "address", + internalType: "address", + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "grantRootRoles", + inputs: [ + { + name: "roleBitmap", + type: "uint256", + internalType: "uint256", + }, + { + name: "account", + type: "address", + internalType: "address", + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "hasAssignees", + inputs: [ + { + name: "resource", + type: "uint256", + internalType: "uint256", + }, + { + name: "roleBitmap", + type: "uint256", + internalType: "uint256", + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "hasRoles", + inputs: [ + { + name: "resource", + type: "uint256", + internalType: "uint256", + }, + { + name: "rolesBitmap", + type: "uint256", + internalType: "uint256", + }, + { + name: "account", + type: "address", + internalType: "address", + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "hasRootRoles", + inputs: [ + { + name: "rolesBitmap", + type: "uint256", + internalType: "uint256", + }, + { + name: "account", + type: "address", + internalType: "address", + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "revokeRoles", + inputs: [ + { + name: "resource", + type: "uint256", + internalType: "uint256", + }, + { + name: "roleBitmap", + type: "uint256", + internalType: "uint256", + }, + { + name: "account", + type: "address", + internalType: "address", + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "revokeRootRoles", + inputs: [ + { + name: "roleBitmap", + type: "uint256", + internalType: "uint256", + }, + { + name: "account", + type: "address", + internalType: "address", + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "roleCount", + inputs: [ + { + name: "resource", + type: "uint256", + internalType: "uint256", + }, + ], + outputs: [ + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "roles", + inputs: [ + { + name: "resource", + type: "uint256", + internalType: "uint256", + }, + { + name: "account", + type: "address", + internalType: "address", + }, + ], + outputs: [ + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "supportsInterface", + inputs: [ + { + name: "interfaceId", + type: "bytes4", + internalType: "bytes4", + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "view", + }, + { + type: "event", + name: "EACAllRolesRevoked", + inputs: [ + { + name: "resource", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + { + name: "account", + type: "address", + indexed: false, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "EACRolesGranted", + inputs: [ + { + name: "resource", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + { + name: "roleBitmap", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + { + name: "account", + type: "address", + indexed: false, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "EACRolesRevoked", + inputs: [ + { + name: "resource", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + { + name: "roleBitmap", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + { + name: "account", + type: "address", + indexed: false, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "error", + name: "EACCannotGrantRoles", + inputs: [ + { + name: "resource", + type: "uint256", + internalType: "uint256", + }, + { + name: "roleBitmap", + type: "uint256", + internalType: "uint256", + }, + { + name: "account", + type: "address", + internalType: "address", + }, + ], + }, + { + type: "error", + name: "EACCannotRevokeRoles", + inputs: [ + { + name: "resource", + type: "uint256", + internalType: "uint256", + }, + { + name: "roleBitmap", + type: "uint256", + internalType: "uint256", + }, + { + name: "account", + type: "address", + internalType: "address", + }, + ], + }, + { + type: "error", + name: "EACInvalidRoleBitmap", + inputs: [ + { + name: "roleBitmap", + type: "uint256", + internalType: "uint256", + }, + ], + }, + { + type: "error", + name: "EACMaxAssignees", + inputs: [ + { + name: "resource", + type: "uint256", + internalType: "uint256", + }, + { + name: "role", + type: "uint256", + internalType: "uint256", + }, + ], + }, + { + type: "error", + name: "EACMinAssignees", + inputs: [ + { + name: "resource", + type: "uint256", + internalType: "uint256", + }, + { + name: "role", + type: "uint256", + internalType: "uint256", + }, + ], + }, + { + type: "error", + name: "EACRootResourceNotAllowed", + inputs: [], + }, + { + type: "error", + name: "EACUnauthorizedAccountRoles", + inputs: [ + { + name: "resource", + type: "uint256", + internalType: "uint256", + }, + { + name: "roleBitmap", + type: "uint256", + internalType: "uint256", + }, + { + name: "account", + type: "address", + internalType: "address", + }, + ], + }, +] as const; diff --git a/packages/datasources/src/abis/namechain/Registry.ts b/packages/datasources/src/abis/namechain/Registry.ts new file mode 100644 index 000000000..68df46609 --- /dev/null +++ b/packages/datasources/src/abis/namechain/Registry.ts @@ -0,0 +1,484 @@ +export const Registry = [ + { + type: "function", + name: "balanceOf", + inputs: [ + { + name: "account", + type: "address", + internalType: "address", + }, + { + name: "id", + type: "uint256", + internalType: "uint256", + }, + ], + outputs: [ + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "balanceOfBatch", + inputs: [ + { + name: "accounts", + type: "address[]", + internalType: "address[]", + }, + { + name: "ids", + type: "uint256[]", + internalType: "uint256[]", + }, + ], + outputs: [ + { + name: "", + type: "uint256[]", + internalType: "uint256[]", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "getResolver", + inputs: [ + { + name: "label", + type: "string", + internalType: "string", + }, + ], + outputs: [ + { + name: "", + type: "address", + internalType: "address", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "getSubregistry", + inputs: [ + { + name: "label", + type: "string", + internalType: "string", + }, + ], + outputs: [ + { + name: "", + type: "address", + internalType: "contract IRegistry", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "isApprovedForAll", + inputs: [ + { + name: "account", + type: "address", + internalType: "address", + }, + { + name: "operator", + type: "address", + internalType: "address", + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "ownerOf", + inputs: [ + { + name: "id", + type: "uint256", + internalType: "uint256", + }, + ], + outputs: [ + { + name: "owner", + type: "address", + internalType: "address", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "safeBatchTransferFrom", + inputs: [ + { + name: "from", + type: "address", + internalType: "address", + }, + { + name: "to", + type: "address", + internalType: "address", + }, + { + name: "ids", + type: "uint256[]", + internalType: "uint256[]", + }, + { + name: "values", + type: "uint256[]", + internalType: "uint256[]", + }, + { + name: "data", + type: "bytes", + internalType: "bytes", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "safeTransferFrom", + inputs: [ + { + name: "from", + type: "address", + internalType: "address", + }, + { + name: "to", + type: "address", + internalType: "address", + }, + { + name: "id", + type: "uint256", + internalType: "uint256", + }, + { + name: "value", + type: "uint256", + internalType: "uint256", + }, + { + name: "data", + type: "bytes", + internalType: "bytes", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "setApprovalForAll", + inputs: [ + { + name: "operator", + type: "address", + internalType: "address", + }, + { + name: "approved", + type: "bool", + internalType: "bool", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "supportsInterface", + inputs: [ + { + name: "interfaceId", + type: "bytes4", + internalType: "bytes4", + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "view", + }, + { + type: "event", + name: "ApprovalForAll", + inputs: [ + { + name: "account", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "operator", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "approved", + type: "bool", + indexed: false, + internalType: "bool", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "NameBurned", + inputs: [ + { + name: "tokenId", + type: "uint256", + indexed: true, + internalType: "uint256", + }, + { + name: "burnedBy", + type: "address", + indexed: false, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "NameRegistered", + inputs: [ + { + name: "tokenId", + type: "uint256", + indexed: true, + internalType: "uint256", + }, + { + name: "label", + type: "string", + indexed: false, + internalType: "string", + }, + { + name: "expiration", + type: "uint64", + indexed: false, + internalType: "uint64", + }, + { + name: "registeredBy", + type: "address", + indexed: false, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "NameRenewed", + inputs: [ + { + name: "tokenId", + type: "uint256", + indexed: true, + internalType: "uint256", + }, + { + name: "newExpiration", + type: "uint64", + indexed: false, + internalType: "uint64", + }, + { + name: "renewedBy", + type: "address", + indexed: false, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "ResolverUpdate", + inputs: [ + { + name: "id", + type: "uint256", + indexed: true, + internalType: "uint256", + }, + { + name: "resolver", + type: "address", + indexed: false, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "SubregistryUpdate", + inputs: [ + { + name: "id", + type: "uint256", + indexed: true, + internalType: "uint256", + }, + { + name: "subregistry", + type: "address", + indexed: false, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "TokenRegenerated", + inputs: [ + { + name: "oldTokenId", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + { + name: "newTokenId", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "TransferBatch", + inputs: [ + { + name: "operator", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "from", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "to", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "ids", + type: "uint256[]", + indexed: false, + internalType: "uint256[]", + }, + { + name: "values", + type: "uint256[]", + indexed: false, + internalType: "uint256[]", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "TransferSingle", + inputs: [ + { + name: "operator", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "from", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "to", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "id", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + { + name: "value", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "URI", + inputs: [ + { + name: "value", + type: "string", + indexed: false, + internalType: "string", + }, + { + name: "id", + type: "uint256", + indexed: true, + internalType: "uint256", + }, + ], + anonymous: false, + }, +] as const; diff --git a/packages/datasources/src/ens-test-env.ts b/packages/datasources/src/ens-test-env.ts index f90a0fd41..631e12877 100644 --- a/packages/datasources/src/ens-test-env.ts +++ b/packages/datasources/src/ens-test-env.ts @@ -1,3 +1,5 @@ +import { EnhancedAccessControl } from "./abis/namechain/EnhancedAccessControl"; +import { Registry } from "./abis/namechain/Registry"; // ABIs for ENSRoot Datasource import { BaseRegistrar as root_BaseRegistrar } from "./abis/root/BaseRegistrar"; import { LegacyEthRegistrarController as root_LegacyEthRegistrarController } from "./abis/root/LegacyEthRegistrarController"; @@ -6,9 +8,9 @@ import { Registry as root_Registry } from "./abis/root/Registry"; import { UniversalResolver as root_UniversalResolver } from "./abis/root/UniversalResolver"; import { UnwrappedEthRegistrarController as root_UnwrappedEthRegistrarController } from "./abis/root/UnwrappedEthRegistrarController"; import { WrappedEthRegistrarController as root_WrappedEthRegistrarController } from "./abis/root/WrappedEthRegistrarController"; -import { ensTestEnvL1Chain } from "./lib/chains"; +import { ensTestEnvL1Chain, ensTestEnvL2Chain } from "./lib/chains"; // Shared ABIs -import { ResolverABI, ResolverFilter } from "./lib/resolver"; +import { ResolverABI } from "./lib/resolver"; // Types import { DatasourceNames, type ENSNamespace } from "./lib/types"; @@ -34,19 +36,18 @@ export default { [DatasourceNames.ENSRoot]: { chain: ensTestEnvL1Chain, contracts: { - RegistryOld: { + ENSv1RegistryOld: { abi: root_Registry, // Registry was redeployed, same abi address: "0x610178da211fef7d417bc0e6fed39f05609ad788", startBlock: 0, }, - Registry: { + ENSv1Registry: { abi: root_Registry, // Registry was redeployed, same abi address: "0xb7f8bc63bbcad18155201308c8f3540b07f84f5e", startBlock: 0, }, Resolver: { abi: ResolverABI, - filter: ResolverFilter, startBlock: 0, }, BaseRegistrar: { @@ -71,12 +72,41 @@ export default { }, NameWrapper: { abi: root_NameWrapper, - address: "0x2e2ed0cfd3ad2f1d34481277b3204d807ca2f8c2", + address: "0x162a433068f51e18b7d13932f27e66a3f99e6890", startBlock: 0, }, UniversalResolver: { abi: root_UniversalResolver, - address: "0xd84379ceae14aa33c123af12424a37803f885889", + address: "0x7a9ec1d04904907de0ed7b6839ccdd59c3716ac9", + startBlock: 0, + }, + + // + + RootRegistry: { + abi: Registry, + address: "0x8a791620dd6260079bf849dc5567adc3f2fdc318", + startBlock: 0, + }, + Registry: { + abi: Registry, + startBlock: 0, + }, + EnhancedAccessControl: { + abi: EnhancedAccessControl, + startBlock: 0, + }, + }, + }, + [DatasourceNames.Namechain]: { + chain: ensTestEnvL2Chain, + contracts: { + Registry: { + abi: Registry, + startBlock: 0, + }, + EnhancedAccessControl: { + abi: EnhancedAccessControl, startBlock: 0, }, }, diff --git a/packages/datasources/src/holesky.ts b/packages/datasources/src/holesky.ts index c121fae8e..7999dc32c 100644 --- a/packages/datasources/src/holesky.ts +++ b/packages/datasources/src/holesky.ts @@ -9,7 +9,7 @@ import { UniversalResolver as root_UniversalResolver } from "./abis/root/Univers import { UnwrappedEthRegistrarController as root_UnwrappedEthRegistrarController } from "./abis/root/UnwrappedEthRegistrarController"; import { WrappedEthRegistrarController as root_WrappedEthRegistrarController } from "./abis/root/WrappedEthRegistrarController"; // Shared ABIs -import { ResolverABI, ResolverFilter } from "./lib/resolver"; +import { ResolverABI } from "./lib/resolver"; // Types import { DatasourceNames, type ENSNamespace } from "./lib/types"; @@ -29,20 +29,19 @@ export default { [DatasourceNames.ENSRoot]: { chain: holesky, contracts: { - RegistryOld: { + ENSv1RegistryOld: { abi: root_Registry, // Registry was redeployed, same abi address: "0x94f523b8261b815b87effcf4d18e6abef18d6e4b", startBlock: 801536, }, - Registry: { + ENSv1Registry: { abi: root_Registry, // Registry was redeployed, same abi address: "0x00000000000c2e074ec69a0dfb2997ba6c7d2e1e", startBlock: 801613, }, Resolver: { abi: ResolverABI, - filter: ResolverFilter, - startBlock: 801536, // ignores any Resolver events prior to `startBlock` of RegistryOld on Holeksy + startBlock: 801536, // ignores any Resolver events prior to `startBlock` of ENSv1RegistryOld on Holeksy }, BaseRegistrar: { abi: root_BaseRegistrar, diff --git a/packages/datasources/src/index.ts b/packages/datasources/src/index.ts index ef0ad331f..5b0346428 100644 --- a/packages/datasources/src/index.ts +++ b/packages/datasources/src/index.ts @@ -1,7 +1,8 @@ +export { EnhancedAccessControl as EnhancedAccessControlABI } from "./abis/namechain/EnhancedAccessControl"; +export { Registry as RegistryABI } from "./abis/namechain/Registry"; export { StandaloneReverseRegistrar as StandaloneReverseRegistrarABI } from "./abis/shared/StandaloneReverseRegistrar"; export { ThreeDNSToken as ThreeDNSTokenABI } from "./abis/threedns/ThreeDNSToken"; export * from "./lib/chains"; -// export shared ABIs for consumer convenience export { ResolverABI } from "./lib/resolver"; export * from "./lib/types"; export * from "./namespaces"; diff --git a/packages/datasources/src/lib/resolver.ts b/packages/datasources/src/lib/resolver.ts index 394566ea0..ae88ebd94 100644 --- a/packages/datasources/src/lib/resolver.ts +++ b/packages/datasources/src/lib/resolver.ts @@ -2,7 +2,6 @@ import { mergeAbis } from "@ponder/utils"; import { LegacyPublicResolver } from "../abis/shared/LegacyPublicResolver"; import { Resolver } from "../abis/shared/Resolver"; -import type { ContractConfig } from "./types"; /** * This Resolver ABI represents the set of all well-known Resolver events/methods, including the @@ -11,30 +10,3 @@ import type { ContractConfig } from "./types"; * methods available in this ABI. */ export const ResolverABI = mergeAbis([LegacyPublicResolver, Resolver]); - -/** - * This is the ContractConfig['filter'] describing the set of events that Resolver contracts emit. - * It is not technically necessary for Ponder to function, but we explicitly document it here. - */ -export const ResolverFilter = [ - { event: "AddrChanged", args: {} }, - { event: "AddressChanged", args: {} }, - { event: "NameChanged", args: {} }, - { event: "ABIChanged", args: {} }, - { event: "PubkeyChanged", args: {} }, - { - event: "TextChanged(bytes32 indexed node, string indexed indexedKey, string key)", - args: {}, - }, - { - event: "TextChanged(bytes32 indexed node, string indexed indexedKey, string key, string value)", - args: {}, - }, - { event: "ContenthashChanged", args: {} }, - { event: "InterfaceChanged", args: {} }, - { event: "AuthorisationChanged", args: {} }, - { event: "VersionChanged", args: {} }, - { event: "DNSRecordChanged", args: {} }, - { event: "DNSRecordDeleted", args: {} }, - { event: "DNSZonehashChanged", args: {} }, -] as const satisfies ContractConfig["filter"]; diff --git a/packages/datasources/src/lib/types.ts b/packages/datasources/src/lib/types.ts index a18382782..a98ed844e 100644 --- a/packages/datasources/src/lib/types.ts +++ b/packages/datasources/src/lib/types.ts @@ -64,6 +64,7 @@ export const DatasourceNames = { ReverseResolverOptimism: "reverse-resolver-optimism", ReverseResolverArbitrum: "reverse-resolver-arbitrum", ReverseResolverScroll: "reverse-resolver-scroll", + Namechain: "namechain", } as const; export type DatasourceName = (typeof DatasourceNames)[keyof typeof DatasourceNames]; @@ -78,42 +79,25 @@ export interface EventFilter { } /** - * Defines the abi, address, filter, and startBlock of a contract relevant to a Datasource. + * Defines the abi, address, and startBlock of a contract relevant to a Datasource. * * A contract is located onchain either by * 1. a single Address in `address`, * 2. a set of Address[] in `address`, - * 3. or a set of event signatures in `filter`. + * 3. or any contract that emits events as defined in `abi`. * * This type is intentionally a subset of Ponder's ContractConfig. * * @param abi - the ABI of the contract * @param address - (optional) Address of the contract or Address[] of each contract to be indexed - * @param filter - (optional) array of event signatures to filter the log by * @param startBlock - block number the contract was deployed in */ -export type ContractConfig = - | { - readonly abi: Abi; - readonly address: Address; - readonly filter?: never; - readonly startBlock: number; - readonly endBlock?: number; - } - | { - readonly abi: Abi; - readonly address: Address[]; - readonly filter?: never; - readonly startBlock: number; - readonly endBlock?: number; - } - | { - readonly abi: Abi; - readonly address?: never; - readonly filter: EventFilter[]; - readonly startBlock: number; - readonly endBlock?: number; - }; +export type ContractConfig = { + readonly abi: Abi; + readonly address?: Address | Address[]; + readonly startBlock: number; + readonly endBlock?: number; +}; /** * ENSNamespace encodes a set of known Datasources associated with the same ENS namespace. diff --git a/packages/datasources/src/mainnet.ts b/packages/datasources/src/mainnet.ts index 1e0dacb8d..5dbd1db3c 100644 --- a/packages/datasources/src/mainnet.ts +++ b/packages/datasources/src/mainnet.ts @@ -11,6 +11,9 @@ import { BaseRegistrar as linea_BaseRegistrar } from "./abis/lineanames/BaseRegi import { EthRegistrarController as linea_EthRegistrarController } from "./abis/lineanames/EthRegistrarController"; import { NameWrapper as linea_NameWrapper } from "./abis/lineanames/NameWrapper"; import { Registry as linea_Registry } from "./abis/lineanames/Registry"; +// ABIs for Namechain +import { EnhancedAccessControl } from "./abis/namechain/EnhancedAccessControl"; +import { Registry } from "./abis/namechain/Registry"; // ABIs for ENSRoot Datasource import { BaseRegistrar as root_BaseRegistrar } from "./abis/root/BaseRegistrar"; import { LegacyEthRegistrarController as root_LegacyEthRegistrarController } from "./abis/root/LegacyEthRegistrarController"; @@ -23,7 +26,7 @@ import { Seaport as Seaport1_5 } from "./abis/seaport/Seaport1.5"; // Shared ABIs import { StandaloneReverseRegistrar } from "./abis/shared/StandaloneReverseRegistrar"; import { ThreeDNSToken } from "./abis/threedns/ThreeDNSToken"; -import { ResolverABI, ResolverFilter } from "./lib/resolver"; +import { ResolverABI } from "./lib/resolver"; // Types import { DatasourceNames, type ENSNamespace } from "./lib/types"; @@ -40,20 +43,19 @@ export default { [DatasourceNames.ENSRoot]: { chain: mainnet, contracts: { - RegistryOld: { + ENSv1RegistryOld: { abi: root_Registry, // Registry was redeployed, same abi address: "0x314159265dd8dbb310642f98f50c066173c1259b", startBlock: 3327417, }, - Registry: { + ENSv1Registry: { abi: root_Registry, // Registry was redeployed, same abi address: "0x00000000000c2e074ec69a0dfb2997ba6c7d2e1e", startBlock: 9380380, }, Resolver: { abi: ResolverABI, - filter: ResolverFilter, - startBlock: 3327417, // ignores any Resolver events prior to `startBlock` of RegistryOld on Mainnet + startBlock: 3327417, // ignores any Resolver events prior to `startBlock` of ENSv1RegistryOld on Mainnet }, BaseRegistrar: { abi: root_BaseRegistrar, @@ -95,6 +97,35 @@ export default { address: "0xde16ee87b0c019499cebdde29c9f7686560f679a", startBlock: 20410692, }, + + // + RootRegistry: { + abi: Registry, + address: "0x8a791620dd6260079bf849dc5567adc3f2fdc318", + startBlock: 0, + }, + Registry: { + abi: Registry, + startBlock: 0, + }, + EnhancedAccessControl: { + abi: EnhancedAccessControl, + startBlock: 0, + }, + }, + }, + + [DatasourceNames.Namechain]: { + chain: mainnet, + contracts: { + Registry: { + abi: Registry, + startBlock: 0, + }, + EnhancedAccessControl: { + abi: EnhancedAccessControl, + startBlock: 0, + }, }, }, @@ -129,7 +160,6 @@ export default { }, Resolver: { abi: ResolverABI, - filter: ResolverFilter, startBlock: 17571480, // based on startBlock of Registry on Base }, BaseRegistrar: { @@ -209,7 +239,6 @@ export default { }, Resolver: { abi: ResolverABI, - filter: ResolverFilter, startBlock: 6682888, // based on startBlock of Registry on Linea }, BaseRegistrar: { diff --git a/packages/datasources/src/sepolia.ts b/packages/datasources/src/sepolia.ts index 181e4d099..d06918fb8 100644 --- a/packages/datasources/src/sepolia.ts +++ b/packages/datasources/src/sepolia.ts @@ -29,7 +29,7 @@ import { WrappedEthRegistrarController as root_WrappedEthRegistrarController } f import { Seaport as Seaport1_5 } from "./abis/seaport/Seaport1.5"; // Shared ABIs import { StandaloneReverseRegistrar } from "./abis/shared/StandaloneReverseRegistrar"; -import { ResolverABI, ResolverFilter } from "./lib/resolver"; +import { ResolverABI } from "./lib/resolver"; // Types import { DatasourceNames, type ENSNamespace } from "./lib/types"; @@ -48,20 +48,19 @@ export default { [DatasourceNames.ENSRoot]: { chain: sepolia, contracts: { - RegistryOld: { + ENSv1RegistryOld: { abi: root_Registry, // Registry was redeployed, same abi address: "0x94f523b8261b815b87effcf4d18e6abef18d6e4b", startBlock: 3702721, }, - Registry: { + ENSv1Registry: { abi: root_Registry, // Registry was redeployed, same abi address: "0x00000000000c2e074ec69a0dfb2997ba6c7d2e1e", startBlock: 3702728, }, Resolver: { abi: ResolverABI, - filter: ResolverFilter, - startBlock: 3702721, // ignores any Resolver events prior to `startBlock` of RegistryOld on Sepolia + startBlock: 3702721, // ignores any Resolver events prior to `startBlock` of ENSv1RegistryOld on Sepolia }, BaseRegistrar: { abi: root_BaseRegistrar, @@ -127,7 +126,6 @@ export default { }, Resolver: { abi: ResolverABI, - filter: ResolverFilter, startBlock: 13012458, }, BaseRegistrar: { @@ -189,7 +187,6 @@ export default { }, Resolver: { abi: ResolverABI, - filter: ResolverFilter, startBlock: 2395094, // based on startBlock of Registry on Linea Sepolia }, BaseRegistrar: { diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/schemas/ensv2.schema.ts index eec268ed2..a9dda66dd 100644 --- a/packages/ensnode-schema/src/schemas/ensv2.schema.ts +++ b/packages/ensnode-schema/src/schemas/ensv2.schema.ts @@ -31,20 +31,7 @@ export const registry = onchainTable( parentDomainNode: t.hex(), }), (t) => ({ - // TODO: enforce that it is RegistryContract | ImplicitRegistry with associated property constraints - // registryType: check( - // "registry_type_check", - // sql` - // (type = 'RegistryContract' AND - // ${t.chainId} IS NOT NULL AND - // ${t.address} IS NOT NULL AND - // ${t.parentDomainNode} IS NULL) OR - // (type = 'ImplicitRegistry' AND - // ${t.chainId} ISNULL AND - // ${t.address} ISNULL AND - // ${t.parentDomainNode} IS NOT NULL) - // `, - // ), + // }), ); @@ -69,20 +56,26 @@ export const relations_registry = relations(registry, ({ one, many }) => ({ export const domain = onchainTable( "domains", (t) => ({ - // belongs to registry + // belongs to registry by (registryId) registryId: t.text().notNull(), - tokenId: t.bigint().notNull(), + canonicalId: t.bigint().notNull(), labelHash: t.hex().notNull(), label: t.text().notNull(), - // may have one subregistry + // may have one subregistry by (id) subregistryId: t.text(), + // may have one resolver by (chainId, address) + resolverChainId: t.integer(), + resolverAddress: t.hex(), + + // internals _hasAttemptedLabelHeal: t.boolean().notNull().default(false), }), (t) => ({ - pk: primaryKey({ columns: [t.registryId, t.tokenId] }), + // unique by (registryId, canonicalId) + pk: primaryKey({ columns: [t.registryId, t.canonicalId] }), }), ); @@ -115,9 +108,64 @@ export const permissions = onchainTable( ); export const relations_permissions = relations(permissions, ({ one, many }) => ({ - registry: one(registry), + resources: many(permissionsResource), + users: many(permissionsUser), +})); + +export const permissionsResource = onchainTable( + "permissions_resources", + (t) => ({ + chainId: t.integer().notNull(), + address: t.hex().notNull(), + resource: t.bigint().notNull(), + }), + (t) => ({ + pk: primaryKey({ columns: [t.chainId, t.address, t.resource] }), + }), +); + +export const relations_permissionsResource = relations(permissionsResource, ({ one, many }) => ({ + permissions: one(permissions, { + fields: [permissionsResource.chainId, permissionsResource.address], + references: [permissions.chainId, permissions.address], + }), +})); + +export const permissionsUser = onchainTable( + "permissions_users", + (t) => ({ + chainId: t.integer().notNull(), + address: t.hex().notNull(), + resource: t.bigint().notNull(), + user: t.hex().notNull(), + + // has one roles bitmap + // TODO: can materialize into more semantic (polymorphic) interpretation of roles based on source + // contract, but not now + roles: t.bigint().notNull(), + }), + (t) => ({ + pk: primaryKey({ columns: [t.chainId, t.address, t.resource, t.user] }), + }), +); + +export const relations_permissionsUser = relations(permissionsUser, ({ one, many }) => ({ + permissions: one(permissions, { + fields: [permissionsUser.chainId, permissionsUser.address], + references: [permissions.chainId, permissions.address], + }), + resource: one(permissionsResource, { + fields: [permissionsUser.chainId, permissionsUser.address, permissionsUser.resource], + references: [ + permissionsResource.chainId, + permissionsResource.address, + permissionsResource.resource, + ], + }), })); +// TODO: Permissions USer — should it just be Account? + // export const namespaceEntry = onchainTable( // "namespace_entries", // (t) => ({}), diff --git a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts index 91c29cd60..6f8e85cc8 100644 --- a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts +++ b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts @@ -71,6 +71,22 @@ export const nodeResolverRelation = onchainTable( }), ); +export const resolver = onchainTable( + "resolvers", + (t) => ({ + // keyed by (chainId, address) + chainId: t.integer().notNull(), + address: t.hex().notNull(), + }), + (t) => ({ + pk: primaryKey({ columns: [t.chainId, t.address] }), + }), +); + +export const resolver_relations = relations(resolver, ({ one, many }) => ({ + records: many(resolverRecords), +})); + /** * Tracks a set of records for a specified `node` within a `resolver` contract on `chainId`. * @@ -110,7 +126,13 @@ export const resolverRecords = onchainTable( }), ); -export const resolverRecords_relations = relations(resolverRecords, ({ many }) => ({ +export const resolverRecords_relations = relations(resolverRecords, ({ one, many }) => ({ + // belongs to resolver + resolver: one(resolver, { + fields: [resolverRecords.chainId, resolverRecords.resolver], + references: [resolver.chainId, resolver.address], + }), + // resolverRecord has many address records addressRecords: many(resolverAddressRecord), @@ -167,7 +189,7 @@ export const resolverAddressRecordRelations = relations(resolverAddressRecord, ( * then additionally keyed by (key). */ export const resolverTextRecord = onchainTable( - "resolver_trecords", + "resolver_text_records", (t) => ({ // keyed by ((chainId, resolver, node), key) chainId: t.integer().notNull(), diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2c9b8e55e..85b578a93 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,12 +4,12 @@ packages: - packages/* catalog: - '@adraffy/ens-normalize': 1.11.1 - '@astrojs/react': ^4.4.1 - '@astrojs/tailwind': ^6.0.2 - '@namehash/namekit-react': 0.12.0 - '@ponder/utils': 0.2.14 - '@types/node': ^22.14.0 + "@adraffy/ens-normalize": 1.11.1 + "@astrojs/react": ^4.4.1 + "@astrojs/tailwind": ^6.0.2 + "@namehash/namekit-react": 0.12.0 + "@ponder/utils": 0.2.14 + "@types/node": ^22.14.0 astro: ^5.15.2 astro-font: ^1.1.0 astro-seo: ^0.8.4 From d109dc406b175a0e649707df1483e1ef79d674a0 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 7 Nov 2025 15:21:16 -0600 Subject: [PATCH 003/102] checkpoint, gql api --- apps/ensapi/package.json | 4 + apps/ensapi/src/graphql-api/builder.ts | 14 +++ apps/ensapi/src/graphql-api/schema.ts | 11 ++ .../src/graphql-api/schema/account-id.ts | 20 ++++ apps/ensapi/src/graphql-api/schema/account.ts | 35 ++++++ apps/ensapi/src/graphql-api/schema/domain.ts | 93 ++++++++++++++++ .../graphql-api/schema/name-in-namespace.ts | 62 +++++++++++ .../src/graphql-api/schema/permissions.ts | 19 ++++ apps/ensapi/src/graphql-api/schema/query.ts | 99 +++++++++++++++++ .../ensapi/src/graphql-api/schema/registry.ts | 101 ++++++++++++++++++ apps/ensapi/src/graphql-api/schema/scalars.ts | 54 ++++++++++ apps/ensapi/src/handlers/ensnode-api.ts | 4 + .../src/handlers/ensnode-graphql-api.ts | 55 ++++++++++ apps/ensapi/src/handlers/subgraph-api.ts | 91 +++++++--------- apps/ensapi/src/lib/handlers/drizzle.ts | 16 +-- .../require-core-plugin.middleware.ts | 7 +- apps/ensindexer/src/lib/ensv2/db-helpers.ts | 7 ++ .../src/lib/ensv2/labelspace-db-helpers.ts | 22 ++++ .../src/lib/ensv2/reconciliation.ts | 43 ++++++++ .../src/plugins/ensv2/handlers/Registry.ts | 101 +++++++++++++----- biome.jsonc | 6 +- .../src/schemas/ensv2.schema.ts | 85 ++++++++++++--- packages/ponder-subgraph/package.json | 3 +- packages/ponder-subgraph/src/drizzle.ts | 22 ++++ packages/ponder-subgraph/src/graphql.ts | 2 +- packages/ponder-subgraph/src/index.ts | 1 - packages/ponder-subgraph/src/middleware.ts | 25 +++-- pnpm-lock.yaml | 95 +++++++++++++++- 28 files changed, 977 insertions(+), 120 deletions(-) create mode 100644 apps/ensapi/src/graphql-api/builder.ts create mode 100644 apps/ensapi/src/graphql-api/schema.ts create mode 100644 apps/ensapi/src/graphql-api/schema/account-id.ts create mode 100644 apps/ensapi/src/graphql-api/schema/account.ts create mode 100644 apps/ensapi/src/graphql-api/schema/domain.ts create mode 100644 apps/ensapi/src/graphql-api/schema/name-in-namespace.ts create mode 100644 apps/ensapi/src/graphql-api/schema/permissions.ts create mode 100644 apps/ensapi/src/graphql-api/schema/query.ts create mode 100644 apps/ensapi/src/graphql-api/schema/registry.ts create mode 100644 apps/ensapi/src/graphql-api/schema/scalars.ts create mode 100644 apps/ensapi/src/handlers/ensnode-graphql-api.ts create mode 100644 apps/ensindexer/src/lib/ensv2/db-helpers.ts create mode 100644 apps/ensindexer/src/lib/ensv2/labelspace-db-helpers.ts create mode 100644 apps/ensindexer/src/lib/ensv2/reconciliation.ts create mode 100644 packages/ponder-subgraph/src/drizzle.ts diff --git a/apps/ensapi/package.json b/apps/ensapi/package.json index 8a355b7f6..be3cc1f78 100644 --- a/apps/ensapi/package.json +++ b/apps/ensapi/package.json @@ -38,8 +38,12 @@ "@opentelemetry/sdk-trace-base": "^2.0.1", "@opentelemetry/sdk-trace-node": "^2.0.1", "@opentelemetry/semantic-conventions": "^1.34.0", + "@ponder/client": "^0.14.13", + "@pothos/core": "^4.10.0", "date-fns": "catalog:", "drizzle-orm": "catalog:", + "graphql": "^16.11.0", + "graphql-yoga": "^5.16.0", "hono": "catalog:", "p-memoize": "^8.0.0", "p-reflect": "^3.1.0", diff --git a/apps/ensapi/src/graphql-api/builder.ts b/apps/ensapi/src/graphql-api/builder.ts new file mode 100644 index 000000000..f045fd832 --- /dev/null +++ b/apps/ensapi/src/graphql-api/builder.ts @@ -0,0 +1,14 @@ +import SchemaBuilder from "@pothos/core"; +import type { Address } from "viem"; + +import type { ChainId, Name, Node } from "@ensnode/ensnode-sdk"; + +export const builder = new SchemaBuilder<{ + Scalars: { + BigInt: { Input: bigint; Output: bigint }; + Address: { Input: Address; Output: Address }; + ChainId: { Input: ChainId; Output: ChainId }; + Node: { Input: Node; Output: Node }; + Name: { Input: Name; Output: Name }; + }; +}>({}); diff --git a/apps/ensapi/src/graphql-api/schema.ts b/apps/ensapi/src/graphql-api/schema.ts new file mode 100644 index 000000000..cf78df953 --- /dev/null +++ b/apps/ensapi/src/graphql-api/schema.ts @@ -0,0 +1,11 @@ +import { builder } from "@/graphql-api/builder"; + +import "./schema/account-id"; +import "./schema/domain"; +import "./schema/name-in-namespace"; +import "./schema/permissions"; +import "./schema/query"; +import "./schema/registry"; +import "./schema/scalars"; + +export const schema = builder.toSchema(); diff --git a/apps/ensapi/src/graphql-api/schema/account-id.ts b/apps/ensapi/src/graphql-api/schema/account-id.ts new file mode 100644 index 000000000..c248beb4c --- /dev/null +++ b/apps/ensapi/src/graphql-api/schema/account-id.ts @@ -0,0 +1,20 @@ +import type { AccountId } from "@ensnode/ensnode-sdk"; + +import { builder } from "@/graphql-api/builder"; + +export const AccountIdRef = builder.objectRef("AccountId"); +AccountIdRef.implement({ + description: "A CAIP-10 Account ID.", + fields: (t) => ({ + chainId: t.expose("chainId", { type: "ChainId" }), + address: t.expose("address", { type: "Address" }), + }), +}); + +export const AccountIdInput = builder.inputType("AccountIdInput", { + description: "A CAIP-10 Account ID.", + fields: (t) => ({ + chainId: t.field({ type: "ChainId", required: true }), + address: t.field({ type: "Address", required: true }), + }), +}); diff --git a/apps/ensapi/src/graphql-api/schema/account.ts b/apps/ensapi/src/graphql-api/schema/account.ts new file mode 100644 index 000000000..1e657c823 --- /dev/null +++ b/apps/ensapi/src/graphql-api/schema/account.ts @@ -0,0 +1,35 @@ +import type * as schema from "@ensnode/ensnode-schema"; + +import { builder } from "@/graphql-api/builder"; +import { DomainRef } from "@/graphql-api/schema/domain"; +import { db } from "@/lib/db"; + +type Account = typeof schema.account.$inferSelect; + +export const AccountRef = builder.objectRef("Account"); + +AccountRef.implement({ + description: "TODO", + fields: (t) => ({ + ////////////////////// + // Account.address + ////////////////////// + address: t.expose("address", { + type: "Address", + description: "TODO", + nullable: false, + }), + + ////////////////////// + // Account.domains + ////////////////////// + domains: t.field({ + type: [DomainRef], + description: "TODO", + resolve: ({ address }) => + db.query.domain.findMany({ + where: (t, { eq }) => eq(t.ownerId, address), + }), + }), + }), +}); diff --git a/apps/ensapi/src/graphql-api/schema/domain.ts b/apps/ensapi/src/graphql-api/schema/domain.ts new file mode 100644 index 000000000..fb09d05d4 --- /dev/null +++ b/apps/ensapi/src/graphql-api/schema/domain.ts @@ -0,0 +1,93 @@ +import type * as schema from "@ensnode/ensnode-schema"; + +import { builder } from "@/graphql-api/builder"; +import { AccountRef } from "@/graphql-api/schema/account"; +import { RegistryInterface } from "@/graphql-api/schema/registry"; +import { db } from "@/lib/db"; + +type Domain = typeof schema.domain.$inferSelect; + +export const DomainRef = builder.objectRef("Domain"); + +DomainRef.implement({ + description: "a Domain", + fields: (t) => ({ + ////////////////////// + // Domain.canonicalId + ////////////////////// + canonicalId: t.expose("canonicalId", { + type: "BigInt", + description: "TODO", + nullable: false, + }), + + ////////////////////// + // Domain.label + ////////////////////// + label: t.field({ + type: "String", + description: "TODO", + nullable: false, + resolve: async ({ labelHash }) => { + const label = await db.query.labelInNamespace.findFirst({ + where: (t, { eq }) => eq(t.labelHash, labelHash), + }); + + if (!label) throw new Error(`Invariant: label expected`); + return label.value; + }, + }), + + ////////////////////// + // Domain.owner + ////////////////////// + owner: t.field({ + type: AccountRef, + description: "TODO", + nullable: false, + resolve: async ({ ownerId }) => { + // TODO(dataloader): just id + if (ownerId === null) throw new Error(`Invariant: ownerId null`); + const owner = await db.query.account.findFirst({ + where: (t, { eq }) => eq(t.address, ownerId), + }); + + if (!owner) throw new Error(`Invariant: owner expected`); + return owner; + }, + }), + + ////////////////////// + // Domain.registry + ////////////////////// + registry: t.field({ + type: RegistryInterface, + description: "TODO", + nullable: false, + resolve: async ({ registryId }, args, ctx, info) => { + const registry = await db.query.registry.findFirst({ + where: (t, { eq }) => eq(t.id, registryId), + }); + if (!registry) throw new Error(`Invariant: Domain does not have parent Registry (???)`); + return registry; + }, + }), + + ////////////////////// + // Domain.subregistry + ////////////////////// + subregistry: t.field({ + type: RegistryInterface, + description: "TODO", + resolve: async ({ subregistryId }, args, ctx, info) => { + if (subregistryId === null) return null; + + const subregistry = await db.query.registry.findFirst({ + where: (t, { eq }) => eq(t.id, subregistryId), + }); + + return subregistry ?? null; + }, + }), + }), +}); diff --git a/apps/ensapi/src/graphql-api/schema/name-in-namespace.ts b/apps/ensapi/src/graphql-api/schema/name-in-namespace.ts new file mode 100644 index 000000000..6b7ef60c7 --- /dev/null +++ b/apps/ensapi/src/graphql-api/schema/name-in-namespace.ts @@ -0,0 +1,62 @@ +import type * as schema from "@ensnode/ensnode-schema"; + +import { builder } from "@/graphql-api/builder"; +import { DomainRef } from "@/graphql-api/schema/domain"; +import { db } from "@/lib/db"; + +type NameInNamespace = typeof schema.nameInNamespace.$inferSelect; + +export const NameInNamespaceRef = builder.objectRef("NameInNamespace"); + +NameInNamespaceRef.implement({ + description: "An ENS Name in the indexed namespace.", + fields: (t) => ({ + ////////////////////// + // NameInNamespace.node + ////////////////////// + node: t.expose("node", { + type: "Node", + description: "TODO", + nullable: false, + }), + + ////////////////////// + // NameInNamespace.fqdn + ////////////////////// + fqdn: t.expose("fqdn", { + type: "Name", + description: "TODO", + nullable: false, + }), + + ////////////////////// + // NameInNamespace.domain + ////////////////////// + domain: t.field({ + type: DomainRef, + description: "TODO", + nullable: false, + resolve: async ({ domainRegistryId, domainCanonicalId }) => { + // TODO(dataloader): just return id + const domain = await db.query.domain.findFirst({ + where: (t, { eq, and }) => + and( + eq(t.registryId, domainRegistryId), // + eq(t.canonicalId, domainCanonicalId), + ), + }); + if (!domain) throw new Error(`Invariant: domain expected`); + return domain; + }, + }), + }), +}); + +export const NameOrNodeInput = builder.inputType("NameOrNodeInput", { + description: "TODO", + isOneOf: true, + fields: (t) => ({ + name: t.field({ type: "Name", required: false }), + node: t.field({ type: "Node", required: false }), + }), +}); diff --git a/apps/ensapi/src/graphql-api/schema/permissions.ts b/apps/ensapi/src/graphql-api/schema/permissions.ts new file mode 100644 index 000000000..b1fc1f4b4 --- /dev/null +++ b/apps/ensapi/src/graphql-api/schema/permissions.ts @@ -0,0 +1,19 @@ +import type * as schema from "@ensnode/ensnode-schema"; + +import { builder } from "@/graphql-api/builder"; +import { AccountIdRef } from "@/graphql-api/schema/account-id"; + +type Permissions = typeof schema.permissions.$inferSelect; + +export const PermissionsRef = builder.objectRef("Permissions"); +PermissionsRef.implement({ + description: "Permissions", + fields: (t) => ({ + contract: t.field({ + type: AccountIdRef, + description: "TODO", + nullable: false, + resolve: ({ chainId, address }) => ({ chainId, address }), + }), + }), +}); diff --git a/apps/ensapi/src/graphql-api/schema/query.ts b/apps/ensapi/src/graphql-api/schema/query.ts new file mode 100644 index 000000000..110a1f1fb --- /dev/null +++ b/apps/ensapi/src/graphql-api/schema/query.ts @@ -0,0 +1,99 @@ +/** biome-ignore-all lint/correctness/noUnusedFunctionParameters: ignore unused resolve arguments */ + +import config from "@/config"; + +import { namehash } from "viem"; + +import { DatasourceNames, getDatasource } from "@ensnode/datasources"; +import { serializeAccountId } from "@ensnode/ensnode-sdk"; + +import { builder } from "@/graphql-api/builder"; +import { NameInNamespaceRef, NameOrNodeInput } from "@/graphql-api/schema/name-in-namespace"; +import { + type RegistryContract, + RegistryContractRef, + RegistryIdInput, + RegistryInterface, +} from "@/graphql-api/schema/registry"; +import { db } from "@/lib/db"; + +builder.queryType({ + fields: (t) => ({ + ///////////////////////////////////////// + // Get Name in Namespace by Node or FQDN + ///////////////////////////////////////// + name: t.field({ + description: "TODO", + type: NameInNamespaceRef, + args: { + id: t.arg({ type: NameOrNodeInput, required: true }), + }, + resolve: async (parent, args, ctx, info) => { + // TODO(dataloader): just return node + // TODO: make sure `namehash` is encoded-label-hash-aware + const node = args.id.node ? args.id.node : namehash(args.id.name); + + const name = await db.query.nameInNamespace.findFirst({ + where: (t, { eq }) => eq(t.node, node), + }); + + return name; + }, + }), + + ////////////////////// + // Get Registry by Id + ////////////////////// + registry: t.field({ + description: "TODO", + type: RegistryInterface, + args: { + id: t.arg({ type: RegistryIdInput, required: true }), + }, + resolve: async (parent, args, ctx, info) => { + // TODO(dataloader): just return registryId + const registryId = args.id.contract + ? serializeAccountId(args.id.contract) + : args.id.implicit.parent; + + const registry = await db.query.registry.findFirst({ + where: (t, { eq }) => eq(t.id, registryId), + }); + + return registry; + }, + }), + + ///////////////// + // Get Root Registry + ///////////////// + root: t.field({ + type: RegistryContractRef, + description: "TODO", + nullable: false, + resolve: async () => { + // TODO: remove, helps types while implementing + if (config.ensIndexerPublicConfig.namespace !== "ens-test-env") throw new Error("nope"); + + // TODO(dataloader): just return rootRegistry id + const datasource = getDatasource( + config.ensIndexerPublicConfig.namespace, + DatasourceNames.ENSRoot, + ); + + const registryId = serializeAccountId({ + chainId: datasource.chain.id, + address: datasource.contracts.RootRegistry.address, + }); + + const rootRegistry = await db.query.registry.findFirst({ + where: (t, { eq }) => eq(t.id, registryId), + }); + + if (!rootRegistry) throw new Error(`Invariant: Root Registry expected.`); + + return rootRegistry as RegistryContract; + }, + }), + }), +}); diff --git a/apps/ensapi/src/graphql-api/schema/registry.ts b/apps/ensapi/src/graphql-api/schema/registry.ts new file mode 100644 index 000000000..09877efe3 --- /dev/null +++ b/apps/ensapi/src/graphql-api/schema/registry.ts @@ -0,0 +1,101 @@ +import type * as schema from "@ensnode/ensnode-schema"; + +import { builder } from "@/graphql-api/builder"; +import { AccountIdInput, AccountIdRef } from "@/graphql-api/schema/account-id"; +import { DomainRef } from "@/graphql-api/schema/domain"; +import { PermissionsRef } from "@/graphql-api/schema/permissions"; +import { db } from "@/lib/db"; + +type RequiredAndNotNull = T & { + [P in K]-?: NonNullable; +}; + +type _Registry = typeof schema.registry.$inferSelect; + +type Registry = Pick<_Registry, "type" | "id">; + +export type RegistryContract = Registry & RequiredAndNotNull<_Registry, "chainId" | "address">; +export type ImplicitRegistry = Registry & RequiredAndNotNull<_Registry, "parentDomainNode">; + +export const RegistryInterface = builder.interfaceRef("Registry"); +RegistryInterface.implement({ + description: "TODO", + fields: (t) => ({ + ////////////////////// + // Registry.domain + ////////////////////// + domain: t.field({ + type: DomainRef, + description: "TODO", + nullable: true, + resolve: (parent) => null, + }), + + ////////////////////// + // Registry.domains + ////////////////////// + domains: t.field({ + type: [DomainRef], + description: "TODO", + resolve: ({ id }) => + db.query.domain.findMany({ + where: (t, { eq }) => eq(t.registryId, id), + }), + }), + }), +}); + +export const RegistryContractRef = builder.objectRef("RegistryContract"); +RegistryContractRef.implement({ + description: "A Registry Contract", + interfaces: [RegistryInterface], + isTypeOf: (value) => (value as _Registry).type === "RegistryContract", + fields: (t) => ({ + //////////////////////////////// + // RegistryContract.permissions + //////////////////////////////// + permissions: t.field({ + type: PermissionsRef, + description: "TODO", + resolve: ({ chainId, address }) => null, + }), + + ///////////////////////////// + // RegistryContract.contract + ///////////////////////////// + contract: t.field({ + type: AccountIdRef, + description: "TODO", + nullable: false, + resolve: ({ chainId, address }) => ({ chainId, address }), + }), + }), +}); + +export const ImplicitRegistryRef = builder.objectRef("ImplicitRegistry"); +ImplicitRegistryRef.implement({ + description: "An Implicit Registry", + interfaces: [RegistryInterface], + isTypeOf: (value) => (value as _Registry).type === "ImplicitRegistry", + fields: (t) => ({}), +}); + +export const ImplicitRegistryIdInput = builder.inputType("ImplicitRegistryIdInput", { + description: "TODO", + fields: (t) => ({ + parent: t.field({ + type: "Node", + description: "TODO", + required: true, + }), + }), +}); + +export const RegistryIdInput = builder.inputType("RegistryIdInput", { + description: "TODO", + isOneOf: true, + fields: (t) => ({ + contract: t.field({ type: AccountIdInput, required: false }), + implicit: t.field({ type: ImplicitRegistryIdInput, required: false }), + }), +}); diff --git a/apps/ensapi/src/graphql-api/schema/scalars.ts b/apps/ensapi/src/graphql-api/schema/scalars.ts new file mode 100644 index 000000000..005af95b2 --- /dev/null +++ b/apps/ensapi/src/graphql-api/schema/scalars.ts @@ -0,0 +1,54 @@ +import { type Address, isHex, size } from "viem"; +import { z } from "zod/v4"; + +import type { ChainId, Name, Node } from "@ensnode/ensnode-sdk"; +import { makeChainIdSchema, makeLowercaseAddressSchema } from "@ensnode/ensnode-sdk/internal"; + +import { builder } from "@/graphql-api/builder"; + +builder.scalarType("BigInt", { + description: "BigInt represents non-fractional signed whole numeric values.", + serialize: (value: bigint) => value.toString(), + parseValue: (value) => z.coerce.bigint().parse(value), +}); + +builder.scalarType("Address", { + description: "Address represents a lowercase (unchecksummed) viem#Address.", + serialize: (value: Address) => value.toString(), + parseValue: (value) => makeLowercaseAddressSchema("Address").parse(value), +}); + +builder.scalarType("ChainId", { + description: "ChainId represents a @ensnode/ensnode-sdk#ChainId.", + serialize: (value: ChainId) => value, + parseValue: (value) => makeChainIdSchema("ChainId").parse(value), +}); + +builder.scalarType("Node", { + description: "Node represents a @ensnode/ensnode-sdk#Node.", + serialize: (value: Node) => value, + parseValue: (value) => + z.coerce + .string() + .check((ctx) => { + if (isHex(ctx.value) && size(ctx.value) === 32) return; + + ctx.issues.push({ + code: "custom", + message: `Node must be a valid Node`, + input: ctx.value, + }); + }) + .transform((val) => val as Node) + .parse(value), +}); + +builder.scalarType("Name", { + description: "Name represents a @ensnode/ensnode-sdk#Name.", + serialize: (value: Name) => value, + parseValue: (value) => + z + .string() // TODO: additional validation, check with `isInterpretedName` + .transform((val) => val as Name) + .parse(value), +}); diff --git a/apps/ensapi/src/handlers/ensnode-api.ts b/apps/ensapi/src/handlers/ensnode-api.ts index 2fd9ad89c..1ef512ae9 100644 --- a/apps/ensapi/src/handlers/ensnode-api.ts +++ b/apps/ensapi/src/handlers/ensnode-api.ts @@ -10,6 +10,7 @@ import { import { buildEnsApiPublicConfig } from "@/config/config.schema"; import { factory } from "@/lib/hono-factory"; +import ensnodeGraphQLApi from "./ensnode-graphql-api"; import resolutionApi from "./resolution-api"; const app = factory.createApp(); @@ -38,4 +39,7 @@ app.get("/indexing-status", async (c) => { // Resolution API app.route("/resolve", resolutionApi); +// ENSNode GraphQL API +app.route("/graphql", ensnodeGraphQLApi); + export default app; diff --git a/apps/ensapi/src/handlers/ensnode-graphql-api.ts b/apps/ensapi/src/handlers/ensnode-graphql-api.ts new file mode 100644 index 000000000..24f4feb88 --- /dev/null +++ b/apps/ensapi/src/handlers/ensnode-graphql-api.ts @@ -0,0 +1,55 @@ +// import { maxAliasesPlugin } from "@escape.tech/graphql-armor-max-aliases"; +// import { maxDepthPlugin } from "@escape.tech/graphql-armor-max-depth"; +// import { maxTokensPlugin } from "@escape.tech/graphql-armor-max-tokens"; + +import { createYoga } from "graphql-yoga"; + +import { schema } from "@/graphql-api/schema"; +import { factory } from "@/lib/hono-factory"; +import { makeLogger } from "@/lib/logger"; + +const logger = makeLogger("ensnode-graphql"); + +const yoga = createYoga({ + graphqlEndpoint: "*", + schema, + graphiql: { + defaultQuery: `query GetCanonicalNametree { + root { + domain { label } + domains { + label + subregistry { + domains { + label + subregistry { + domains { + label + } + } + } + } + } + } +}`, + }, + + // integrate logging with pino + logging: logger, + + // TODO: plugins + // plugins: [ + // maxTokensPlugin({ n: maxOperationTokens }), + // maxDepthPlugin({ n: maxOperationDepth, ignoreIntrospection: false }), + // maxAliasesPlugin({ n: maxOperationAliases, allowList: [] }), + // ], +}); + +const app = factory.createApp(); + +app.use(async (c) => { + const response = await yoga.fetch(c.req.raw, c.var); + return response; +}); + +export default app; diff --git a/apps/ensapi/src/handlers/subgraph-api.ts b/apps/ensapi/src/handlers/subgraph-api.ts index 491d4e6fd..d426d5963 100644 --- a/apps/ensapi/src/handlers/subgraph-api.ts +++ b/apps/ensapi/src/handlers/subgraph-api.ts @@ -4,9 +4,8 @@ import { createDocumentationMiddleware } from "ponder-enrich-gql-docs-middleware import * as schema from "@ensnode/ensnode-schema"; import type { Duration } from "@ensnode/ensnode-sdk"; -import { buildGraphQLSchema, subgraphGraphQLMiddleware } from "@ensnode/ponder-subgraph"; +import { subgraphGraphQLMiddleware } from "@ensnode/ponder-subgraph"; -import { makeDrizzle } from "@/lib/handlers/drizzle"; import { factory } from "@/lib/hono-factory"; import { makeSubgraphApiDocumentation } from "@/lib/subgraph/api-documentation"; import { filterSchemaByPrefix } from "@/lib/subgraph/filter-schema-by-prefix"; @@ -21,13 +20,6 @@ const MAX_REALTIME_DISTANCE_TO_RESOLVE: Duration = 10 * 60; // 10 minutes in sec // generate a subgraph-specific subset of the schema const subgraphSchema = filterSchemaByPrefix("subgraph_", schema); -// make subgraph-specific drizzle db -const drizzle = makeDrizzle({ - schema: subgraphSchema, - databaseUrl: config.databaseUrl, - databaseSchema: config.databaseSchemaName, -}); - const app = factory.createApp(); // 404 if subgraph core plugin not enabled @@ -51,48 +43,47 @@ app.use(subgraphMetaMiddleware); // use subgraph middleware app.use( subgraphGraphQLMiddleware({ - drizzle, - graphqlSchema: buildGraphQLSchema({ - schema: subgraphSchema, - // describes the polymorphic (interface) relationships in the schema - polymorphicConfig: { - types: { - DomainEvent: [ - subgraphSchema.transfer, - subgraphSchema.newOwner, - subgraphSchema.newResolver, - subgraphSchema.newTTL, - subgraphSchema.wrappedTransfer, - subgraphSchema.nameWrapped, - subgraphSchema.nameUnwrapped, - subgraphSchema.fusesSet, - subgraphSchema.expiryExtended, - ], - RegistrationEvent: [ - subgraphSchema.nameRegistered, - subgraphSchema.nameRenewed, - subgraphSchema.nameTransferred, - ], - ResolverEvent: [ - subgraphSchema.addrChanged, - subgraphSchema.multicoinAddrChanged, - subgraphSchema.nameChanged, - subgraphSchema.abiChanged, - subgraphSchema.pubkeyChanged, - subgraphSchema.textChanged, - subgraphSchema.contenthashChanged, - subgraphSchema.interfaceChanged, - subgraphSchema.authorisationChanged, - subgraphSchema.versionChanged, - ], - }, - fields: { - "Domain.events": "DomainEvent", - "Registration.events": "RegistrationEvent", - "Resolver.events": "ResolverEvent", - }, + databaseUrl: config.databaseUrl, + databaseSchema: config.databaseSchemaName, + schema: subgraphSchema, + // describes the polymorphic (interface) relationships in the schema + polymorphicConfig: { + types: { + DomainEvent: [ + subgraphSchema.transfer, + subgraphSchema.newOwner, + subgraphSchema.newResolver, + subgraphSchema.newTTL, + subgraphSchema.wrappedTransfer, + subgraphSchema.nameWrapped, + subgraphSchema.nameUnwrapped, + subgraphSchema.fusesSet, + subgraphSchema.expiryExtended, + ], + RegistrationEvent: [ + subgraphSchema.nameRegistered, + subgraphSchema.nameRenewed, + subgraphSchema.nameTransferred, + ], + ResolverEvent: [ + subgraphSchema.addrChanged, + subgraphSchema.multicoinAddrChanged, + subgraphSchema.nameChanged, + subgraphSchema.abiChanged, + subgraphSchema.pubkeyChanged, + subgraphSchema.textChanged, + subgraphSchema.contenthashChanged, + subgraphSchema.interfaceChanged, + subgraphSchema.authorisationChanged, + subgraphSchema.versionChanged, + ], + }, + fields: { + "Domain.events": "DomainEvent", + "Registration.events": "RegistrationEvent", + "Resolver.events": "ResolverEvent", }, - }), + }, }), ); diff --git a/apps/ensapi/src/lib/handlers/drizzle.ts b/apps/ensapi/src/lib/handlers/drizzle.ts index e397173ef..c0a3e3016 100644 --- a/apps/ensapi/src/lib/handlers/drizzle.ts +++ b/apps/ensapi/src/lib/handlers/drizzle.ts @@ -1,22 +1,8 @@ -import { isTable, Table } from "drizzle-orm"; +import { setDatabaseSchema } from "@ponder/client"; import { drizzle } from "drizzle-orm/node-postgres"; -import { isPgEnum } from "drizzle-orm/pg-core"; type Schema = { [name: string]: unknown }; -// https://github.com/ponder-sh/ponder/blob/f7f6444ab8d1a870fe6492023941091df7b7cddf/packages/client/src/index.ts#L226C1-L239C3 -const setDatabaseSchema = (schema: T, schemaName: string) => { - for (const table of Object.values(schema)) { - if (isTable(table)) { - // @ts-expect-error - table[Table.Symbol.Schema] = schemaName; - } else if (isPgEnum(table)) { - // @ts-expect-error - table.schema = schemaName; - } - } -}; - /** * Makes a Drizzle DB object. */ diff --git a/apps/ensapi/src/middleware/require-core-plugin.middleware.ts b/apps/ensapi/src/middleware/require-core-plugin.middleware.ts index 7b951719a..a07bd5457 100644 --- a/apps/ensapi/src/middleware/require-core-plugin.middleware.ts +++ b/apps/ensapi/src/middleware/require-core-plugin.middleware.ts @@ -16,14 +16,17 @@ import { factory } from "@/lib/hono-factory"; export const requireCorePluginMiddleware = (core: "subgraph" | "ensv2") => factory.createMiddleware(async (c, next) => { if ( - core === "subgraph" && + core === "subgraph" && // !config.ensIndexerPublicConfig.plugins.includes(PluginName.Subgraph) ) { return c.notFound(); } // TODO: enable ensv2 checking - if (core === "ensv2") { + if ( + core === "ensv2" && // + !config.ensIndexerPublicConfig.plugins.includes(PluginName.ENSv2) + ) { return c.notFound(); } diff --git a/apps/ensindexer/src/lib/ensv2/db-helpers.ts b/apps/ensindexer/src/lib/ensv2/db-helpers.ts new file mode 100644 index 000000000..83421e261 --- /dev/null +++ b/apps/ensindexer/src/lib/ensv2/db-helpers.ts @@ -0,0 +1,7 @@ +import type { Context } from "ponder:registry"; +import schema from "ponder:schema"; +import type { Address } from "viem"; + +export async function ensureAccount(context: Context, address: Address) { + await context.db.insert(schema.account).values({ address }).onConflictDoNothing(); +} diff --git a/apps/ensindexer/src/lib/ensv2/labelspace-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/labelspace-db-helpers.ts new file mode 100644 index 000000000..f96a41dff --- /dev/null +++ b/apps/ensindexer/src/lib/ensv2/labelspace-db-helpers.ts @@ -0,0 +1,22 @@ +import type { Context } from "ponder:registry"; +import schema from "ponder:schema"; +import { labelhash } from "viem"; + +import { type LiteralLabel, literalLabelToInterpretedLabel } from "@ensnode/ensnode-sdk"; + +import { reconcileLabelChange } from "@/lib/ensv2/reconciliation"; + +export async function ensureLabel(context: Context, label: LiteralLabel) { + const labelHash = labelhash(label); + const interpretedLabel = literalLabelToInterpretedLabel(label); + + const existing = await context.db.find(schema.labelInNamespace, { labelHash }); + + await context.db + .insert(schema.labelInNamespace) + .values({ labelHash, value: interpretedLabel }) + .onConflictDoUpdate({ value: interpretedLabel }); + + // if the label's interpreted value changed, reconcile in namespace + if (existing?.value !== interpretedLabel) await reconcileLabelChange(context, labelHash); +} diff --git a/apps/ensindexer/src/lib/ensv2/reconciliation.ts b/apps/ensindexer/src/lib/ensv2/reconciliation.ts new file mode 100644 index 000000000..29d61c734 --- /dev/null +++ b/apps/ensindexer/src/lib/ensv2/reconciliation.ts @@ -0,0 +1,43 @@ +import type { Context } from "ponder:registry"; +import type schema from "ponder:schema"; + +import type { LabelHash } from "@ensnode/ensnode-sdk"; + +export type DomainId = Pick; + +// for claude: context.db.sql is the Drizzle object +// https://ponder.sh/docs/indexing/write#query-builder +// https://orm.drizzle.team/docs/rqb + +export async function reconcileRegistryAddition(context: Context, registryId: string) { + // 0. if this registry does not terminate at root, no-op + // 1. for each registry.domains, reconcileDomainAddition in transaction + // TODO: perhaps reconcileDomainAddition can be implemented as batch +} + +export async function reconcileRegistryRemoval(context: Context, registryId: string) { + // 0. if this registry does not terminate at root, no-op + // 1. for each registry.domains, reconcileDomainRemoval in transaction + // TODO: perhaps reconcileDomainRemoval can be implemented as batch +} + +export async function reconcileDomainAddition(context: Context, id: DomainId) { + // 0. if this domain does not terminate at root, no-op + // 1. fetch set of all Domains that have this domain as a parent (include this domain) + // - include: recursive path to root + // 2. for each of these Domains, compute node/fqdn using path + // 3. bulk insert into namespace +} + +export async function reconcileDomainRemoval(context: Context, id: DomainId) { + // 0. if this domain is not in namespace, no-op + // 1. identify this domain + all recursive children (via strict suffix match) + // 2. bulk remove from namespace +} + +export async function reconcileLabelChange(context: Context, labelHash: LabelHash) { + // 1. identify all Domains in the namespace that use this label + all recursive children (via strict suffix match) + // - include: recursive path to root + // 2. for each, compute new fqdn using path + // 3. bulk update +} diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts index 482c75836..e4640cb69 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts @@ -3,8 +3,22 @@ import schema from "ponder:schema"; import { replaceBigInts } from "ponder"; import { type Address, hexToBigInt, isAddressEqual, labelhash, zeroAddress } from "viem"; -import { type AccountId, PluginName, serializeAccountId } from "@ensnode/ensnode-sdk"; - +import { + type AccountId, + type LiteralLabel, + PluginName, + serializeAccountId, +} from "@ensnode/ensnode-sdk"; + +import { ensureAccount } from "@/lib/ensv2/db-helpers"; +import { ensureLabel } from "@/lib/ensv2/labelspace-db-helpers"; +import { + type DomainId, + reconcileDomainAddition, + reconcileDomainRemoval, + reconcileRegistryAddition, + reconcileRegistryRemoval, +} from "@/lib/ensv2/reconciliation"; import { makeAccountId } from "@/lib/make-account-id"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; @@ -33,21 +47,14 @@ export default function () { registeredBy: Address; }>; }) => { - const { tokenId, label, expiration, registeredBy: registrant } = event.args; + const { tokenId, label: _label, expiration, registeredBy: registrant } = event.args; + const label = _label as LiteralLabel; const registryAccountId = makeAccountId(context, event); const registryId = serializeAccountId(registryAccountId); - await context.db - .insert(schema.registry) - .values({ - id: registryId, - type: "RegistryContract", - ...registryAccountId, - }) - .onConflictDoNothing(); - const canonicalId = getCanonicalId(tokenId); const labelHash = labelhash(label); + const domainId: DomainId = { registryId, canonicalId }; // Sanity Check: Canonical Id must match emitted label if (canonicalId !== getCanonicalId(hexToBigInt(labelhash(label)))) { @@ -67,14 +74,21 @@ export default function () { ); } - await context.db.insert(schema.domain).values({ - registryId, - canonicalId, - labelHash, - label, - }); + await context.db + .insert(schema.registry) + .values({ + id: registryId, + type: "RegistryContract", + ...registryAccountId, + }) + .onConflictDoNothing(); + + await ensureLabel(context, label); + await context.db.insert(schema.domain).values({ ...domainId, labelHash }); + await reconcileDomainAddition(context, domainId); // TODO: insert Registration entity for this domain as well: expiration, registrant + await ensureAccount(context, registrant); }, ); @@ -95,18 +109,29 @@ export default function () { const canonicalId = getCanonicalId(tokenId); const registryAccountId = makeAccountId(context, event); const registryId = serializeAccountId(registryAccountId); + const domainId: DomainId = { registryId, canonicalId }; + + const existing = await context.db.find(schema.domain, domainId); // update domain's subregistry const isDeletion = isAddressEqual(subregistry, zeroAddress); if (isDeletion) { - await context.db - .update(schema.domain, { registryId, canonicalId }) - .set({ subregistryId: null }); + await context.db.update(schema.domain, domainId).set({ subregistryId: null }); + + // reconcile the removal of this registry from the canonical nametree + if (existing && existing.subregistryId !== null) { + await reconcileRegistryRemoval(context, existing.subregistryId); + } } else { const subregistryAccountId: AccountId = { chainId: context.chain.id, address: subregistry }; const subregistryId = serializeAccountId(subregistryAccountId); - await context.db.update(schema.domain, { registryId, canonicalId }).set({ subregistryId }); + await context.db.update(schema.domain, domainId).set({ subregistryId }); + + // reconcile the addition of this registry to the canonical nametree + if (existing?.subregistryId !== subregistryId) { + await reconcileRegistryAddition(context, subregistryId); + } } }, ); @@ -128,18 +153,46 @@ export default function () { const canonicalId = getCanonicalId(tokenId); const registryAccountId = makeAccountId(context, event); const registryId = serializeAccountId(registryAccountId); + const domainId: DomainId = { registryId, canonicalId }; // update domain's resolver const isDeletion = isAddressEqual(resolver, zeroAddress); if (isDeletion) { await context.db - .update(schema.domain, { registryId, canonicalId }) + .update(schema.domain, domainId) .set({ resolverChainId: null, resolverAddress: null }); } else { await context.db - .update(schema.domain, { registryId, canonicalId }) + .update(schema.domain, domainId) .set({ resolverChainId: context.chain.id, resolverAddress: resolver }); } }, ); + + ponder.on( + namespaceContract(PluginName.ENSv2, "Registry:NameBurned"), + async ({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ + tokenId: bigint; + burnedBy: Address; + }>; + }) => { + const { tokenId } = event.args; + + const canonicalId = getCanonicalId(tokenId); + const registryAccountId = makeAccountId(context, event); + const registryId = serializeAccountId(registryAccountId); + const domainId: DomainId = { registryId, canonicalId }; + + await context.db.delete(schema.domain, domainId); + // TODO: delete registration (?) + await reconcileDomainRemoval(context, domainId); + }, + ); + + // TODO: ERC1155 transfer handlers for ownership } diff --git a/biome.jsonc b/biome.jsonc index 32b9d5e8d..2c8e5e688 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -56,7 +56,8 @@ "noSvgWithoutTitle": "off" }, "suspicious": { - "noExplicitAny": "off" + "noExplicitAny": "off", + "useIterableCallbackReturn": "off" } } }, @@ -76,8 +77,7 @@ "includes": ["**/*.test.ts"], "linter": { "rules": { - "style": { "noNonNullAssertion": "off" }, - "suspicious": { "useIterableCallbackReturn": "off" } + "style": { "noNonNullAssertion": "off" } } } } diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/schemas/ensv2.schema.ts index a9dda66dd..b373a12dd 100644 --- a/packages/ensnode-schema/src/schemas/ensv2.schema.ts +++ b/packages/ensnode-schema/src/schemas/ensv2.schema.ts @@ -1,4 +1,4 @@ -import { onchainEnum, onchainTable, primaryKey, relations } from "ponder"; +import { onchainEnum, onchainTable, primaryKey, relations, uniqueIndex } from "ponder"; // Registry<->Domain is 1:1 // Registry->Doimains is 1:many @@ -12,6 +12,21 @@ import { onchainEnum, onchainTable, primaryKey, relations } from "ponder"; * guarantees. */ +/////////// +// Account +/////////// + +export const account = onchainTable("accounts", (t) => ({ + address: t.hex().primaryKey(), +})); + +export const account_relations = relations(account, ({ many }) => ({ + // registrations, + // dedicatedResolvers, + domains: many(domain), + permissions: many(permissionsUser), +})); + //////////// // Registry //////////// @@ -59,9 +74,9 @@ export const domain = onchainTable( // belongs to registry by (registryId) registryId: t.text().notNull(), canonicalId: t.bigint().notNull(), - labelHash: t.hex().notNull(), - label: t.text().notNull(), + + ownerId: t.hex(), // may have one subregistry by (id) subregistryId: t.text(), @@ -69,9 +84,6 @@ export const domain = onchainTable( // may have one resolver by (chainId, address) resolverChainId: t.integer(), resolverAddress: t.hex(), - - // internals - _hasAttemptedLabelHeal: t.boolean().notNull().default(false), }), (t) => ({ // unique by (registryId, canonicalId) @@ -80,6 +92,11 @@ export const domain = onchainTable( ); export const relations_domain = relations(domain, ({ one }) => ({ + owner: one(account, { + relationName: "owner", + fields: [domain.ownerId], + references: [account.address], + }), registry: one(registry, { relationName: "registry", fields: [domain.registryId], @@ -90,6 +107,12 @@ export const relations_domain = relations(domain, ({ one }) => ({ fields: [domain.subregistryId], references: [registry.id], }), + label: one(labelInNamespace, { + relationName: "label", + fields: [domain.labelHash], + references: [labelInNamespace.labelHash], + }), + name: one(nameInNamespace), })); /////////////// @@ -150,6 +173,10 @@ export const permissionsUser = onchainTable( ); export const relations_permissionsUser = relations(permissionsUser, ({ one, many }) => ({ + account: one(account, { + fields: [permissionsUser.user], + references: [account.address], + }), permissions: one(permissions, { fields: [permissionsUser.chainId, permissionsUser.address], references: [permissions.chainId, permissions.address], @@ -164,10 +191,44 @@ export const relations_permissionsUser = relations(permissionsUser, ({ one, many }), })); -// TODO: Permissions USer — should it just be Account? +////////// +// Labels +////////// + +export const labelInNamespace = onchainTable("labels_in_namespace", (t) => ({ + labelHash: t.hex().primaryKey(), + value: t.text().notNull(), -// export const namespaceEntry = onchainTable( -// "namespace_entries", -// (t) => ({}), -// (t) => ({}), -// ); + // internals + hasAttemptedHeal: t.boolean().notNull().default(false), +})); + +export const labelInNamespace_relations = relations(labelInNamespace, ({ many }) => ({ + domains: many(domain), +})); + +///////// +// Names +///////// + +export const nameInNamespace = onchainTable( + "names_in_namespace", + (t) => ({ + node: t.hex().primaryKey(), + fqdn: t.text().notNull(), + + domainRegistryId: t.text().notNull(), + domainCanonicalId: t.bigint().notNull(), + }), + (t) => ({ + byFqdn: uniqueIndex().on(t.fqdn), + }), +); + +export const nameInNamespace_relations = relations(nameInNamespace, ({ one }) => ({ + domain: one(domain, { + relationName: "name", + fields: [nameInNamespace.domainRegistryId, nameInNamespace.domainCanonicalId], + references: [domain.registryId, domain.canonicalId], + }), +})); diff --git a/packages/ponder-subgraph/package.json b/packages/ponder-subgraph/package.json index 320833f21..9eb351740 100644 --- a/packages/ponder-subgraph/package.json +++ b/packages/ponder-subgraph/package.json @@ -42,8 +42,9 @@ "@escape.tech/graphql-armor-max-aliases": "^2.6.2", "@escape.tech/graphql-armor-max-depth": "^2.4.2", "@escape.tech/graphql-armor-max-tokens": "^2.5.1", + "@ponder/client": "^0.14.13", "dataloader": "^2.2.3", - "drizzle-orm": "catalog:", + "drizzle-orm": "0.41.0", "graphql": "^16.10.0", "graphql-scalars": "^1.24.0", "graphql-yoga": "^5.10.9" diff --git a/packages/ponder-subgraph/src/drizzle.ts b/packages/ponder-subgraph/src/drizzle.ts new file mode 100644 index 000000000..60146b73e --- /dev/null +++ b/packages/ponder-subgraph/src/drizzle.ts @@ -0,0 +1,22 @@ +import { setDatabaseSchema } from "@ponder/client"; +import { drizzle } from "drizzle-orm/node-postgres"; + +import type { Schema } from "./types"; + +/** + * Makes a Drizzle DB object. + */ +export const makeDrizzle = ({ + schema, + databaseUrl, + databaseSchema, +}: { + schema: SCHEMA; + databaseUrl: string; + databaseSchema: string; +}) => { + // monkeypatch schema onto tables + setDatabaseSchema(schema, databaseSchema); + + return drizzle(databaseUrl, { schema, casing: "snake_case" }); +}; diff --git a/packages/ponder-subgraph/src/graphql.ts b/packages/ponder-subgraph/src/graphql.ts index 539956a68..6d3ed1c77 100644 --- a/packages/ponder-subgraph/src/graphql.ts +++ b/packages/ponder-subgraph/src/graphql.ts @@ -153,7 +153,7 @@ export interface PolymorphicConfig { fields: Record; } -interface BuildGraphQLSchemaOptions { +export interface BuildGraphQLSchemaOptions { schema: Schema; polymorphicConfig?: PolymorphicConfig; } diff --git a/packages/ponder-subgraph/src/index.ts b/packages/ponder-subgraph/src/index.ts index 75fd0f39d..4d253c99c 100644 --- a/packages/ponder-subgraph/src/index.ts +++ b/packages/ponder-subgraph/src/index.ts @@ -1,3 +1,2 @@ -export { buildGraphQLSchema } from "./graphql"; export * from "./middleware"; export * from "./types"; diff --git a/packages/ponder-subgraph/src/middleware.ts b/packages/ponder-subgraph/src/middleware.ts index 66d14928c..6e1122abe 100644 --- a/packages/ponder-subgraph/src/middleware.ts +++ b/packages/ponder-subgraph/src/middleware.ts @@ -10,21 +10,23 @@ import { maxAliasesPlugin } from "@escape.tech/graphql-armor-max-aliases"; import { maxDepthPlugin } from "@escape.tech/graphql-armor-max-depth"; import { maxTokensPlugin } from "@escape.tech/graphql-armor-max-tokens"; -import type { GraphQLSchema } from "graphql"; import { createYoga } from "graphql-yoga"; import { createMiddleware } from "hono/factory"; -import { buildDataLoaderCache } from "./graphql"; -import type { Drizzle } from "./types"; +import { makeDrizzle } from "./drizzle"; +import { + type BuildGraphQLSchemaOptions, + buildDataLoaderCache, + buildGraphQLSchema, +} from "./graphql"; export function subgraphGraphQLMiddleware( { - drizzle, - graphqlSchema, - }: { - drizzle: Drizzle; - graphqlSchema: GraphQLSchema; - }, + databaseUrl, + databaseSchema, + schema, + polymorphicConfig, + }: BuildGraphQLSchemaOptions & { databaseUrl: string; databaseSchema: string }, { maxOperationTokens = 1000, maxOperationDepth = 100, @@ -41,6 +43,11 @@ export function subgraphGraphQLMiddleware( maxOperationAliases: 30, }, ) { + // make subgraph-specific drizzle db + const drizzle = makeDrizzle({ schema, databaseUrl, databaseSchema }); + + const graphqlSchema = buildGraphQLSchema({ schema, polymorphicConfig }); + const yoga = createYoga({ graphqlEndpoint: "*", // Disable built-in route validation, use Hono routing instead schema: graphqlSchema, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 36ef910b9..7f8e76435 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -320,12 +320,24 @@ importers: '@opentelemetry/semantic-conventions': specifier: ^1.34.0 version: 1.37.0 + '@ponder/client': + specifier: ^0.14.13 + version: 0.14.13(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(kysely@0.26.3)(pg@8.16.3)(typescript@5.9.3) + '@pothos/core': + specifier: ^4.10.0 + version: 4.10.0(graphql@16.11.0) date-fns: specifier: 'catalog:' version: 4.1.0 drizzle-orm: specifier: 'catalog:' version: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(kysely@0.26.3)(pg@8.16.3) + graphql: + specifier: ^16.11.0 + version: 16.11.0 + graphql-yoga: + specifier: ^5.16.0 + version: 5.16.0(graphql@16.11.0) hono: specifier: 'catalog:' version: 4.10.3 @@ -856,11 +868,14 @@ importers: '@escape.tech/graphql-armor-max-tokens': specifier: ^2.5.1 version: 2.5.1 + '@ponder/client': + specifier: ^0.14.13 + version: 0.14.13(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(kysely@0.26.3)(pg@8.16.3)(typescript@5.9.3) dataloader: specifier: ^2.2.3 version: 2.2.3 drizzle-orm: - specifier: 'catalog:' + specifier: 0.41.0 version: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(kysely@0.26.3)(pg@8.16.3) graphql: specifier: ^16.10.0 @@ -2391,6 +2406,14 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@ponder/client@0.14.13': + resolution: {integrity: sha512-MNmuppswNiL6TIdMuwmz6fhz/PPK4OSTXiBq62UFMLXIK55g8j8bECOPxOEZ2JKeSmhaOZ/s0fNMJGh/TQf1Nw==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + '@ponder/utils@0.2.14': resolution: {integrity: sha512-O4t14Hb6/tVcD0WoS13ghFnDntP6x33/DDvA+sd0tRjemzS+Cne4YTkXl9TKW3AawBIEwMjGrGbAn82C8gXQWQ==} peerDependencies: @@ -2409,6 +2432,11 @@ packages: typescript: optional: true + '@pothos/core@4.10.0': + resolution: {integrity: sha512-spC7v6N80GfDKqt6ZSGELLu7EFDZsuQBb/oCqAtDwAe+HIL5T0STjv220IumLeC8lIAMgTaZMa/WnMNKkawbFg==} + peerDependencies: + graphql: ^16.10.0 + '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -4696,6 +4724,14 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -9665,6 +9701,43 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@ponder/client@0.14.13(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(kysely@0.26.3)(pg@8.16.3)(typescript@5.9.3)': + dependencies: + drizzle-orm: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(kysely@0.26.3)(pg@8.16.3) + eventsource: 3.0.7 + superjson: 2.2.5 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@aws-sdk/client-rds-data' + - '@cloudflare/workers-types' + - '@electric-sql/pglite' + - '@libsql/client' + - '@libsql/client-wasm' + - '@neondatabase/serverless' + - '@op-engineering/op-sqlite' + - '@opentelemetry/api' + - '@planetscale/database' + - '@prisma/client' + - '@tidbcloud/serverless' + - '@types/better-sqlite3' + - '@types/pg' + - '@types/sql.js' + - '@vercel/postgres' + - '@xata.io/client' + - better-sqlite3 + - bun-types + - expo-sqlite + - gel + - knex + - kysely + - mysql2 + - pg + - postgres + - prisma + - sql.js + - sqlite3 + '@ponder/utils@0.2.14(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@3.25.76))': dependencies: viem: 2.38.5(typescript@5.9.3)(zod@3.25.76) @@ -9677,6 +9750,10 @@ snapshots: optionalDependencies: typescript: 5.9.3 + '@pothos/core@4.10.0(graphql@16.11.0)': + dependencies: + graphql: 16.11.0 + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -10773,6 +10850,14 @@ snapshots: chai: 6.2.0 tinyrainbow: 3.0.3 + '@vitest/mocker@4.0.5(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 4.0.5 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) + '@vitest/mocker@4.0.5(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@vitest/spy': 4.0.5 @@ -12088,6 +12173,12 @@ snapshots: events@3.3.0: {} + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -15425,7 +15516,7 @@ snapshots: vitest@4.0.5(@types/debug@4.1.12)(@types/node@20.19.24)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.6))(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1): dependencies: '@vitest/expect': 4.0.5 - '@vitest/mocker': 4.0.5(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/mocker': 4.0.5(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/pretty-format': 4.0.5 '@vitest/runner': 4.0.5 '@vitest/snapshot': 4.0.5 From a7aab18feab4f92c0fdcb8a00f260a4d006d7b09 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 7 Nov 2025 15:40:08 -0600 Subject: [PATCH 004/102] feat: account.names --- apps/ensapi/src/graphql-api/schema/account.ts | 28 ++++++++++++++++- apps/ensapi/src/graphql-api/schema/query.ts | 21 +++++++++++++ apps/ensindexer/src/lib/make-account-id.ts | 2 +- .../src/plugins/ensv2/handlers/Registry.ts | 31 ++++++++++++++++++- 4 files changed, 79 insertions(+), 3 deletions(-) diff --git a/apps/ensapi/src/graphql-api/schema/account.ts b/apps/ensapi/src/graphql-api/schema/account.ts index 1e657c823..377666302 100644 --- a/apps/ensapi/src/graphql-api/schema/account.ts +++ b/apps/ensapi/src/graphql-api/schema/account.ts @@ -1,7 +1,10 @@ -import type * as schema from "@ensnode/ensnode-schema"; +import { and, eq } from "drizzle-orm"; + +import * as schema from "@ensnode/ensnode-schema"; import { builder } from "@/graphql-api/builder"; import { DomainRef } from "@/graphql-api/schema/domain"; +import { NameInNamespaceRef } from "@/graphql-api/schema/name-in-namespace"; import { db } from "@/lib/db"; type Account = typeof schema.account.$inferSelect; @@ -31,5 +34,28 @@ AccountRef.implement({ where: (t, { eq }) => eq(t.ownerId, address), }), }), + + ////////////////////// + // Account.names + ////////////////////// + names: t.field({ + type: [NameInNamespaceRef], + description: "TODO", + resolve: async ({ address }) => { + const result = await db + .select() + .from(schema.nameInNamespace) + .innerJoin( + schema.domain, + and( + eq(schema.nameInNamespace.domainRegistryId, schema.domain.registryId), + eq(schema.nameInNamespace.domainCanonicalId, schema.domain.canonicalId), + ), + ) + .where(eq(schema.domain.ownerId, address)); + + return result.map((row) => row.names_in_namespace); + }, + }), }), }); diff --git a/apps/ensapi/src/graphql-api/schema/query.ts b/apps/ensapi/src/graphql-api/schema/query.ts index 110a1f1fb..1061d907d 100644 --- a/apps/ensapi/src/graphql-api/schema/query.ts +++ b/apps/ensapi/src/graphql-api/schema/query.ts @@ -8,6 +8,7 @@ import { DatasourceNames, getDatasource } from "@ensnode/datasources"; import { serializeAccountId } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; +import { AccountRef } from "@/graphql-api/schema/account"; import { NameInNamespaceRef, NameOrNodeInput } from "@/graphql-api/schema/name-in-namespace"; import { type RegistryContract, @@ -41,6 +42,26 @@ builder.queryType({ }, }), + ///////////////////////////////////////// + // Get Account by address + ///////////////////////////////////////// + account: t.field({ + description: "TODO", + type: AccountRef, + args: { + address: t.arg({ type: "Address", required: true }), + }, + resolve: async (parent, args, ctx, info) => { + // TODO(dataloader): just return address + + const account = await db.query.account.findFirst({ + where: (t, { eq }) => eq(t.address, args.address), + }); + + return account; + }, + }), + ////////////////////// // Get Registry by Id ////////////////////// diff --git a/apps/ensindexer/src/lib/make-account-id.ts b/apps/ensindexer/src/lib/make-account-id.ts index 90c9d1485..525379673 100644 --- a/apps/ensindexer/src/lib/make-account-id.ts +++ b/apps/ensindexer/src/lib/make-account-id.ts @@ -2,5 +2,5 @@ import type { Context, Event } from "ponder:registry"; import type { AccountId } from "@ensnode/ensnode-sdk"; -export const makeAccountId = (context: Context, event: Event) => +export const makeAccountId = (context: Context, event: Pick) => ({ chainId: context.chain.id, address: event.log.address }) satisfies AccountId; diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts index e4640cb69..9af429c2a 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts @@ -194,5 +194,34 @@ export default function () { }, ); - // TODO: ERC1155 transfer handlers for ownership + async function handleTransferSingle({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ id: bigint; to: Address }>; + }) { + const { id: tokenId, to: owner } = event.args; + + const canonicalId = getCanonicalId(tokenId); + const registryAccountId = makeAccountId(context, event); + const registryId = serializeAccountId(registryAccountId); + const domainId: DomainId = { registryId, canonicalId }; + + // just update the owner, NameBurned handles existence + await context.db.update(schema.domain, domainId).set({ ownerId: owner }); + } + + ponder.on(namespaceContract(PluginName.ENSv2, "Registry:TransferSingle"), handleTransferSingle); + ponder.on( + namespaceContract(PluginName.ENSv2, "Registry:TransferBatch"), + async ({ context, event }) => { + for (const id of event.args.ids) { + await handleTransferSingle({ + context, + event: { ...event, args: { ...event.args, id } }, + }); + } + }, + ); } From e3321767ecd17e47e1b50dd9942bd7616bd7d9f4 Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 10 Nov 2025 17:05:48 -0600 Subject: [PATCH 005/102] checkpoint: initial graph traversal --- apps/ensapi/src/graphql-api/builder.ts | 4 +- apps/ensapi/src/graphql-api/lib/db-types.ts | 19 ++++ .../src/graphql-api/lib/get-canonical-path.ts | 62 ++++++++++ .../src/graphql-api/lib/get-domain-by-fqdn.ts | 71 ++++++++++++ .../graphql-api/lib/sort-by-array-order.ts | 5 + apps/ensapi/src/graphql-api/schema.ts | 1 - apps/ensapi/src/graphql-api/schema/account.ts | 28 +---- apps/ensapi/src/graphql-api/schema/domain.ts | 87 ++++++++++++-- .../graphql-api/schema/name-in-namespace.ts | 62 ---------- apps/ensapi/src/graphql-api/schema/query.ts | 61 ++++------ .../ensapi/src/graphql-api/schema/registry.ts | 44 +++---- apps/ensapi/src/graphql-api/schema/scalars.ts | 25 +++- .../ens-root-registry.ts | 18 --- .../protocol-acceleration/find-resolver.ts | 9 +- .../src/lib/resolution/forward-resolution.ts | 4 +- apps/ensapi/src/lib/root-registry.ts | 18 +++ .../{db-helpers.ts => account-db-helpers.ts} | 0 .../src/lib/ensv2/labelspace-db-helpers.ts | 4 +- .../src/lib/ensv2/reconciliation.ts | 5 +- ...e-account-id.ts => get-this-account-id.ts} | 2 +- .../ensv2/handlers/EnhancedAccessControl.ts | 8 +- .../src/plugins/ensv2/handlers/Registry.ts | 86 ++++++++------ packages/ensnode-schema/package.json | 1 + .../src/schemas/ensv2.schema.ts | 107 ++++++++---------- packages/ensnode-sdk/src/ens/types.ts | 20 ++++ packages/ensnode-sdk/src/ensv2/ids-lib.ts | 38 +++++++ packages/ensnode-sdk/src/ensv2/ids.ts | 38 +++++++ packages/ensnode-sdk/src/ensv2/index.ts | 2 + packages/ensnode-sdk/src/index.ts | 1 + .../ensnode-sdk/src/shared/interpretation.ts | 60 ++++++++++ packages/ensnode-sdk/src/shared/serialize.ts | 20 +++- .../src/shared/serialized-types.ts | 9 ++ packages/ensnode-sdk/src/shared/types.ts | 18 +++ pnpm-lock.yaml | 13 +-- 34 files changed, 649 insertions(+), 301 deletions(-) create mode 100644 apps/ensapi/src/graphql-api/lib/db-types.ts create mode 100644 apps/ensapi/src/graphql-api/lib/get-canonical-path.ts create mode 100644 apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts create mode 100644 apps/ensapi/src/graphql-api/lib/sort-by-array-order.ts delete mode 100644 apps/ensapi/src/graphql-api/schema/name-in-namespace.ts delete mode 100644 apps/ensapi/src/lib/protocol-acceleration/ens-root-registry.ts create mode 100644 apps/ensapi/src/lib/root-registry.ts rename apps/ensindexer/src/lib/ensv2/{db-helpers.ts => account-db-helpers.ts} (100%) rename apps/ensindexer/src/lib/{make-account-id.ts => get-this-account-id.ts} (70%) create mode 100644 packages/ensnode-sdk/src/ensv2/ids-lib.ts create mode 100644 packages/ensnode-sdk/src/ensv2/ids.ts create mode 100644 packages/ensnode-sdk/src/ensv2/index.ts diff --git a/apps/ensapi/src/graphql-api/builder.ts b/apps/ensapi/src/graphql-api/builder.ts index f045fd832..32743bb8a 100644 --- a/apps/ensapi/src/graphql-api/builder.ts +++ b/apps/ensapi/src/graphql-api/builder.ts @@ -1,7 +1,7 @@ import SchemaBuilder from "@pothos/core"; import type { Address } from "viem"; -import type { ChainId, Name, Node } from "@ensnode/ensnode-sdk"; +import type { ChainId, InterpretedName, Node } from "@ensnode/ensnode-sdk"; export const builder = new SchemaBuilder<{ Scalars: { @@ -9,6 +9,6 @@ export const builder = new SchemaBuilder<{ Address: { Input: Address; Output: Address }; ChainId: { Input: ChainId; Output: ChainId }; Node: { Input: Node; Output: Node }; - Name: { Input: Name; Output: Name }; + Name: { Input: InterpretedName; Output: InterpretedName }; }; }>({}); diff --git a/apps/ensapi/src/graphql-api/lib/db-types.ts b/apps/ensapi/src/graphql-api/lib/db-types.ts new file mode 100644 index 000000000..417d84fba --- /dev/null +++ b/apps/ensapi/src/graphql-api/lib/db-types.ts @@ -0,0 +1,19 @@ +import type * as schema from "@ensnode/ensnode-schema"; +import type { RequiredAndNotNull } from "@ensnode/ensnode-sdk"; + +////////// +// Domain +////////// + +export type Domain = typeof schema.domain.$inferSelect; + +//////////// +// Registry +//////////// + +export type Registry = typeof schema.registry.$inferSelect; + +export type RegistryInterface = Pick; +export type RegistryContract = RegistryInterface & + RequiredAndNotNull; +export type ImplicitRegistry = RegistryInterface & RequiredAndNotNull; diff --git a/apps/ensapi/src/graphql-api/lib/get-canonical-path.ts b/apps/ensapi/src/graphql-api/lib/get-canonical-path.ts new file mode 100644 index 000000000..e381a5b4a --- /dev/null +++ b/apps/ensapi/src/graphql-api/lib/get-canonical-path.ts @@ -0,0 +1,62 @@ +import { sql } from "drizzle-orm"; + +import * as schema from "@ensnode/ensnode-schema"; +import type { CanonicalPath, DomainId, RegistryId } from "@ensnode/ensnode-sdk"; + +import { db } from "@/lib/db"; +import { ROOT_REGISTRY_ID } from "@/lib/root-registry"; + +const MAX_DEPTH = 16; + +/** + * Provide the canonical parents from the Root Registry to `domainId`. + * i.e. reverse traversal of the namegraph + * + * TODO: this implementation is more or less first-write-wins, need to updated based on proposed reverse mapping + */ +export async function getCanonicalPath(domainId: DomainId): Promise { + const result = await db.execute(sql` + WITH RECURSIVE upward AS ( + -- Base case: start from the target domain + SELECT + d.id AS domain_id, + d.registry_id, + d.label_hash, + 1 AS depth + FROM ${schema.domain} d + WHERE d.id = ${domainId} + + UNION ALL + + -- Step upward: domain -> registry -> parent domain + SELECT + pd.id AS domain_id, + pd.registry_id, + pd.label_hash, + upward.depth + 1 + FROM upward + JOIN ${schema.registry} r + ON r.id = upward.registry_id + JOIN ${schema.domain} pd + ON pd.subregistry_id = r.id + WHERE r.id != ${ROOT_REGISTRY_ID} + AND upward.depth < ${MAX_DEPTH} + ) + SELECT * + FROM upward + ORDER BY depth; + `); + + const rows = result.rows as { domain_id: DomainId; registry_id: RegistryId }[]; + + if (rows.length === 0) { + throw new Error(`Invariant(getCanonicalPath): DomainId '${domainId}' did not exist.`); + } + + const tld = rows[rows.length - 1]; + const isCanonical = tld.registry_id === ROOT_REGISTRY_ID; + + if (!isCanonical) return null; + + return rows.map((row) => row.domain_id); +} diff --git a/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts new file mode 100644 index 000000000..bb021bca5 --- /dev/null +++ b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts @@ -0,0 +1,71 @@ +import { Param, sql } from "drizzle-orm"; + +import * as schema from "@ensnode/ensnode-schema"; +import { + type DomainId, + type InterpretedName, + interpretedNameToLabelPath, + type LabelHash, + type RegistryId, +} from "@ensnode/ensnode-sdk"; + +import { db } from "@/lib/db"; +import { ROOT_REGISTRY_ID } from "@/lib/root-registry"; + +/** + * Gets the Domain addressed by `name`. + * i.e. forward traversal of the namegraph + */ +export async function getDomainIdByInterpretedName( + name: InterpretedName, +): Promise { + const labelPath = interpretedNameToLabelPath(name); + + // https://github.com/drizzle-team/drizzle-orm/issues/1289#issuecomment-2688581070 + const rawLabelPathArray = sql`${new Param(labelPath)}::text[]`; + + const result = await db.execute(sql` + WITH RECURSIVE path AS ( + SELECT + r.id AS registry_id, + NULL::text AS domain_id, + NULL::text AS label_hash, + 0 AS depth + FROM ${schema.registry} r + WHERE r.id = ${ROOT_REGISTRY_ID} + + UNION ALL + + SELECT + d.subregistry_id AS registry_id, + d.id AS domain_id, + d.label_hash, + path.depth + 1 + FROM path + JOIN ${schema.domain} d + ON d.registry_id = path.registry_id + WHERE d.label_hash = (${rawLabelPathArray})[path.depth + 1] + AND path.depth + 1 <= array_length(${rawLabelPathArray}, 1) + ) + SELECT * + FROM path + WHERE domain_id IS NOT NULL + ORDER BY depth; + `); + + // couldn't for the life of me figure out how to drizzle this correctly... + const rows = result.rows as { + registry_id: RegistryId; + domain_id: DomainId; + label_hash: LabelHash; + depth: number; + }[]; + + const exists = rows.length > 0 && rows.length === labelPath.length; + if (!exists) return null; + + // biome-ignore lint/style/noNonNullAssertion: length check above + const leaf = rows[rows.length - 1]!; + + return leaf.domain_id; +} diff --git a/apps/ensapi/src/graphql-api/lib/sort-by-array-order.ts b/apps/ensapi/src/graphql-api/lib/sort-by-array-order.ts new file mode 100644 index 000000000..d09baa404 --- /dev/null +++ b/apps/ensapi/src/graphql-api/lib/sort-by-array-order.ts @@ -0,0 +1,5 @@ +export function sortByArrayOrder(arr: T[], acc: (o: O) => T) { + return function comparator(a: O, b: O) { + return arr.indexOf(acc(a)) > arr.indexOf(acc(b)) ? 1 : -1; + }; +} diff --git a/apps/ensapi/src/graphql-api/schema.ts b/apps/ensapi/src/graphql-api/schema.ts index cf78df953..50b689269 100644 --- a/apps/ensapi/src/graphql-api/schema.ts +++ b/apps/ensapi/src/graphql-api/schema.ts @@ -2,7 +2,6 @@ import { builder } from "@/graphql-api/builder"; import "./schema/account-id"; import "./schema/domain"; -import "./schema/name-in-namespace"; import "./schema/permissions"; import "./schema/query"; import "./schema/registry"; diff --git a/apps/ensapi/src/graphql-api/schema/account.ts b/apps/ensapi/src/graphql-api/schema/account.ts index 377666302..1e657c823 100644 --- a/apps/ensapi/src/graphql-api/schema/account.ts +++ b/apps/ensapi/src/graphql-api/schema/account.ts @@ -1,10 +1,7 @@ -import { and, eq } from "drizzle-orm"; - -import * as schema from "@ensnode/ensnode-schema"; +import type * as schema from "@ensnode/ensnode-schema"; import { builder } from "@/graphql-api/builder"; import { DomainRef } from "@/graphql-api/schema/domain"; -import { NameInNamespaceRef } from "@/graphql-api/schema/name-in-namespace"; import { db } from "@/lib/db"; type Account = typeof schema.account.$inferSelect; @@ -34,28 +31,5 @@ AccountRef.implement({ where: (t, { eq }) => eq(t.ownerId, address), }), }), - - ////////////////////// - // Account.names - ////////////////////// - names: t.field({ - type: [NameInNamespaceRef], - description: "TODO", - resolve: async ({ address }) => { - const result = await db - .select() - .from(schema.nameInNamespace) - .innerJoin( - schema.domain, - and( - eq(schema.nameInNamespace.domainRegistryId, schema.domain.registryId), - eq(schema.nameInNamespace.domainCanonicalId, schema.domain.canonicalId), - ), - ) - .where(eq(schema.domain.ownerId, address)); - - return result.map((row) => row.names_in_namespace); - }, - }), }), }); diff --git a/apps/ensapi/src/graphql-api/schema/domain.ts b/apps/ensapi/src/graphql-api/schema/domain.ts index fb09d05d4..01ebf0b5a 100644 --- a/apps/ensapi/src/graphql-api/schema/domain.ts +++ b/apps/ensapi/src/graphql-api/schema/domain.ts @@ -1,17 +1,27 @@ -import type * as schema from "@ensnode/ensnode-schema"; +import { interpretedLabelsToInterpretedName } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; +import type { Domain } from "@/graphql-api/lib/db-types"; +import { getCanonicalPath } from "@/graphql-api/lib/get-canonical-path"; +import { sortByArrayOrder } from "@/graphql-api/lib/sort-by-array-order"; import { AccountRef } from "@/graphql-api/schema/account"; -import { RegistryInterface } from "@/graphql-api/schema/registry"; +import { RegistryInterfaceRef } from "@/graphql-api/schema/registry"; import { db } from "@/lib/db"; -type Domain = typeof schema.domain.$inferSelect; - export const DomainRef = builder.objectRef("Domain"); DomainRef.implement({ description: "a Domain", fields: (t) => ({ + ////////////////////// + // Domain.id + ////////////////////// + id: t.expose("id", { + type: "ID", + description: "TODO", + nullable: false, + }), + ////////////////////// // Domain.canonicalId ////////////////////// @@ -29,7 +39,7 @@ DomainRef.implement({ description: "TODO", nullable: false, resolve: async ({ labelHash }) => { - const label = await db.query.labelInNamespace.findFirst({ + const label = await db.query.label.findFirst({ where: (t, { eq }) => eq(t.labelHash, labelHash), }); @@ -38,6 +48,69 @@ DomainRef.implement({ }, }), + //////////////////// + // Domain.canonical + //////////////////// + canonical: t.field({ + description: "TODO", + type: "Name", + nullable: true, + resolve: async ({ id }) => { + // TODO: dataloader the getCanonicalPath(domainId) function + const canonicalPath = await getCanonicalPath(id); + if (!canonicalPath) return null; + + // TODO: use the dataloaded version of this findMany w/ labels + const domainsAndLabels = await db.query.domain.findMany({ + where: (t, { inArray }) => inArray(t.id, canonicalPath), + with: { label: true }, + }); + + return interpretedLabelsToInterpretedName( + canonicalPath.map((domainId) => { + const found = domainsAndLabels.find((d) => d.id === domainId); + if (!found) throw new Error(`Invariant`); + return found.label.value; + }), + ); + }, + }), + + ////////////////// + // Domain.parents + ////////////////// + parents: t.field({ + description: "TODO", + type: [DomainRef], + nullable: true, + resolve: async ({ id }) => { + // TODO: dataloader the getCanonicalPath(domainId) function + const canonicalPath = await getCanonicalPath(id); + if (!canonicalPath) return null; + + const domains = await db.query.domain.findMany({ + where: (t, { inArray }) => inArray(t.id, canonicalPath), + }); + + return domains.sort(sortByArrayOrder(canonicalPath, (domain) => domain.id)).slice(1); + }, + }), + + ////////////////// + // Domain.aliases + ////////////////// + aliases: t.field({ + description: "TODO", + type: ["Name"], + nullable: false, + resolve: async ({ registryId, canonicalId }) => { + // a domain's aliases are all of the paths from root to this domain for which it can be + // resolved. naively reverse-traverse the namegaph until the root is reached... yikes. + // if materializing namespace: simply lookup namesInNamespace by domainId + return []; + }, + }), + ////////////////////// // Domain.owner ////////////////////// @@ -61,7 +134,7 @@ DomainRef.implement({ // Domain.registry ////////////////////// registry: t.field({ - type: RegistryInterface, + type: RegistryInterfaceRef, description: "TODO", nullable: false, resolve: async ({ registryId }, args, ctx, info) => { @@ -77,7 +150,7 @@ DomainRef.implement({ // Domain.subregistry ////////////////////// subregistry: t.field({ - type: RegistryInterface, + type: RegistryInterfaceRef, description: "TODO", resolve: async ({ subregistryId }, args, ctx, info) => { if (subregistryId === null) return null; diff --git a/apps/ensapi/src/graphql-api/schema/name-in-namespace.ts b/apps/ensapi/src/graphql-api/schema/name-in-namespace.ts deleted file mode 100644 index 6b7ef60c7..000000000 --- a/apps/ensapi/src/graphql-api/schema/name-in-namespace.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type * as schema from "@ensnode/ensnode-schema"; - -import { builder } from "@/graphql-api/builder"; -import { DomainRef } from "@/graphql-api/schema/domain"; -import { db } from "@/lib/db"; - -type NameInNamespace = typeof schema.nameInNamespace.$inferSelect; - -export const NameInNamespaceRef = builder.objectRef("NameInNamespace"); - -NameInNamespaceRef.implement({ - description: "An ENS Name in the indexed namespace.", - fields: (t) => ({ - ////////////////////// - // NameInNamespace.node - ////////////////////// - node: t.expose("node", { - type: "Node", - description: "TODO", - nullable: false, - }), - - ////////////////////// - // NameInNamespace.fqdn - ////////////////////// - fqdn: t.expose("fqdn", { - type: "Name", - description: "TODO", - nullable: false, - }), - - ////////////////////// - // NameInNamespace.domain - ////////////////////// - domain: t.field({ - type: DomainRef, - description: "TODO", - nullable: false, - resolve: async ({ domainRegistryId, domainCanonicalId }) => { - // TODO(dataloader): just return id - const domain = await db.query.domain.findFirst({ - where: (t, { eq, and }) => - and( - eq(t.registryId, domainRegistryId), // - eq(t.canonicalId, domainCanonicalId), - ), - }); - if (!domain) throw new Error(`Invariant: domain expected`); - return domain; - }, - }), - }), -}); - -export const NameOrNodeInput = builder.inputType("NameOrNodeInput", { - description: "TODO", - isOneOf: true, - fields: (t) => ({ - name: t.field({ type: "Name", required: false }), - node: t.field({ type: "Node", required: false }), - }), -}); diff --git a/apps/ensapi/src/graphql-api/schema/query.ts b/apps/ensapi/src/graphql-api/schema/query.ts index 1061d907d..675e223c0 100644 --- a/apps/ensapi/src/graphql-api/schema/query.ts +++ b/apps/ensapi/src/graphql-api/schema/query.ts @@ -1,44 +1,42 @@ /** biome-ignore-all lint/correctness/noUnusedFunctionParameters: ignore unused resolve arguments */ -import config from "@/config"; - -import { namehash } from "viem"; - -import { DatasourceNames, getDatasource } from "@ensnode/datasources"; -import { serializeAccountId } from "@ensnode/ensnode-sdk"; +import { type ImplicitRegistryId, makeRegistryContractId } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; +import type { RegistryContract } from "@/graphql-api/lib/db-types"; +import { getDomainIdByInterpretedName } from "@/graphql-api/lib/get-domain-by-fqdn"; import { AccountRef } from "@/graphql-api/schema/account"; -import { NameInNamespaceRef, NameOrNodeInput } from "@/graphql-api/schema/name-in-namespace"; +import { DomainRef } from "@/graphql-api/schema/domain"; import { - type RegistryContract, RegistryContractRef, RegistryIdInput, - RegistryInterface, + RegistryInterfaceRef, } from "@/graphql-api/schema/registry"; import { db } from "@/lib/db"; +import { ROOT_REGISTRY_ID } from "@/lib/root-registry"; builder.queryType({ fields: (t) => ({ - ///////////////////////////////////////// - // Get Name in Namespace by Node or FQDN - ///////////////////////////////////////// + ////////////////////// + // Get Domain by FQDN + ////////////////////// name: t.field({ description: "TODO", - type: NameInNamespaceRef, + type: DomainRef, args: { - id: t.arg({ type: NameOrNodeInput, required: true }), + fqdn: t.arg({ type: "Name", required: true }), }, + nullable: true, resolve: async (parent, args, ctx, info) => { - // TODO(dataloader): just return node - // TODO: make sure `namehash` is encoded-label-hash-aware - const node = args.id.node ? args.id.node : namehash(args.id.name); + const domainId = await getDomainIdByInterpretedName(args.fqdn); + // TODO: traverse the namegraph to identify the addressed Domain + // TODO(dataloader): just return domainId - const name = await db.query.nameInNamespace.findFirst({ - where: (t, { eq }) => eq(t.node, node), - }); + if (!domainId) return null; - return name; + return await db.query.domain.findFirst({ + where: (t, { eq }) => eq(t.id, domainId), + }); }, }), @@ -67,15 +65,15 @@ builder.queryType({ ////////////////////// registry: t.field({ description: "TODO", - type: RegistryInterface, + type: RegistryInterfaceRef, args: { id: t.arg({ type: RegistryIdInput, required: true }), }, resolve: async (parent, args, ctx, info) => { // TODO(dataloader): just return registryId const registryId = args.id.contract - ? serializeAccountId(args.id.contract) - : args.id.implicit.parent; + ? makeRegistryContractId(args.id.contract) + : (args.id.implicit.parent as ImplicitRegistryId); // TODO: move this case into scalar const registry = await db.query.registry.findFirst({ where: (t, { eq }) => eq(t.id, registryId), @@ -93,22 +91,9 @@ builder.queryType({ description: "TODO", nullable: false, resolve: async () => { - // TODO: remove, helps types while implementing - if (config.ensIndexerPublicConfig.namespace !== "ens-test-env") throw new Error("nope"); - // TODO(dataloader): just return rootRegistry id - const datasource = getDatasource( - config.ensIndexerPublicConfig.namespace, - DatasourceNames.ENSRoot, - ); - - const registryId = serializeAccountId({ - chainId: datasource.chain.id, - address: datasource.contracts.RootRegistry.address, - }); - const rootRegistry = await db.query.registry.findFirst({ - where: (t, { eq }) => eq(t.id, registryId), + where: (t, { eq }) => eq(t.id, ROOT_REGISTRY_ID), }); if (!rootRegistry) throw new Error(`Invariant: Root Registry expected.`); diff --git a/apps/ensapi/src/graphql-api/schema/registry.ts b/apps/ensapi/src/graphql-api/schema/registry.ts index 09877efe3..0d4a4c157 100644 --- a/apps/ensapi/src/graphql-api/schema/registry.ts +++ b/apps/ensapi/src/graphql-api/schema/registry.ts @@ -1,31 +1,34 @@ -import type * as schema from "@ensnode/ensnode-schema"; - import { builder } from "@/graphql-api/builder"; +import type { + ImplicitRegistry, + Registry, + RegistryContract, + RegistryInterface, +} from "@/graphql-api/lib/db-types"; import { AccountIdInput, AccountIdRef } from "@/graphql-api/schema/account-id"; import { DomainRef } from "@/graphql-api/schema/domain"; import { PermissionsRef } from "@/graphql-api/schema/permissions"; import { db } from "@/lib/db"; -type RequiredAndNotNull = T & { - [P in K]-?: NonNullable; -}; - -type _Registry = typeof schema.registry.$inferSelect; - -type Registry = Pick<_Registry, "type" | "id">; - -export type RegistryContract = Registry & RequiredAndNotNull<_Registry, "chainId" | "address">; -export type ImplicitRegistry = Registry & RequiredAndNotNull<_Registry, "parentDomainNode">; - -export const RegistryInterface = builder.interfaceRef("Registry"); -RegistryInterface.implement({ +export const RegistryInterfaceRef = builder.interfaceRef("Registry"); +RegistryInterfaceRef.implement({ description: "TODO", fields: (t) => ({ + ////////////////////// + // Registry.id + ////////////////////// + id: t.field({ + type: "ID", + description: "TODO", + nullable: false, + resolve: (parent) => parent.id, + }), + ////////////////////// // Registry.domain ////////////////////// domain: t.field({ - type: DomainRef, + type: [DomainRef], description: "TODO", nullable: true, resolve: (parent) => null, @@ -48,8 +51,8 @@ RegistryInterface.implement({ export const RegistryContractRef = builder.objectRef("RegistryContract"); RegistryContractRef.implement({ description: "A Registry Contract", - interfaces: [RegistryInterface], - isTypeOf: (value) => (value as _Registry).type === "RegistryContract", + interfaces: [RegistryInterfaceRef], + isTypeOf: (value) => (value as RegistryInterface).type === "RegistryContract", fields: (t) => ({ //////////////////////////////// // RegistryContract.permissions @@ -57,6 +60,7 @@ RegistryContractRef.implement({ permissions: t.field({ type: PermissionsRef, description: "TODO", + // TODO: render a RegistryPermissions model that parses the backing permissions into registry-semantic roles resolve: ({ chainId, address }) => null, }), @@ -75,8 +79,8 @@ RegistryContractRef.implement({ export const ImplicitRegistryRef = builder.objectRef("ImplicitRegistry"); ImplicitRegistryRef.implement({ description: "An Implicit Registry", - interfaces: [RegistryInterface], - isTypeOf: (value) => (value as _Registry).type === "ImplicitRegistry", + interfaces: [RegistryInterfaceRef], + isTypeOf: (value) => (value as RegistryInterface).type === "ImplicitRegistry", fields: (t) => ({}), }); diff --git a/apps/ensapi/src/graphql-api/schema/scalars.ts b/apps/ensapi/src/graphql-api/schema/scalars.ts index 005af95b2..893c988d5 100644 --- a/apps/ensapi/src/graphql-api/schema/scalars.ts +++ b/apps/ensapi/src/graphql-api/schema/scalars.ts @@ -1,7 +1,13 @@ import { type Address, isHex, size } from "viem"; import { z } from "zod/v4"; -import type { ChainId, Name, Node } from "@ensnode/ensnode-sdk"; +import { + type ChainId, + type InterpretedName, + isInterpretedName, + type Name, + type Node, +} from "@ensnode/ensnode-sdk"; import { makeChainIdSchema, makeLowercaseAddressSchema } from "@ensnode/ensnode-sdk/internal"; import { builder } from "@/graphql-api/builder"; @@ -44,11 +50,20 @@ builder.scalarType("Node", { }); builder.scalarType("Name", { - description: "Name represents a @ensnode/ensnode-sdk#Name.", + description: "Name represents a @ensnode/ensnode-sdk#InterpretedName.", serialize: (value: Name) => value, parseValue: (value) => - z - .string() // TODO: additional validation, check with `isInterpretedName` - .transform((val) => val as Name) + z.coerce + .string() + .check((ctx) => { + if (!isInterpretedName(ctx.value)) { + ctx.issues.push({ + code: "custom", + message: "Name must consist exclusively of Encoded LabelHashes or normalized labels.", + input: ctx.value, + }); + } + }) + .transform((val) => val as InterpretedName) .parse(value), }); diff --git a/apps/ensapi/src/lib/protocol-acceleration/ens-root-registry.ts b/apps/ensapi/src/lib/protocol-acceleration/ens-root-registry.ts deleted file mode 100644 index c03bdb9d4..000000000 --- a/apps/ensapi/src/lib/protocol-acceleration/ens-root-registry.ts +++ /dev/null @@ -1,18 +0,0 @@ -import config from "@/config"; - -import { DatasourceNames, getDatasource } from "@ensnode/datasources"; -import { type AccountId, accountIdEqual } from "@ensnode/ensnode-sdk"; - -const ensRoot = getDatasource(config.namespace, DatasourceNames.ENSRoot); - -/** - * The AccountId of the ENS Registry on the Root Chain. - */ -export const ENS_ROOT_REGISTRY: AccountId = { - chainId: ensRoot.chain.id, - address: ensRoot.contracts.ENSv1Registry.address, -}; - -export function isENSRootRegistry(accountId: AccountId) { - return accountIdEqual(accountId, ENS_ROOT_REGISTRY); -} diff --git a/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts b/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts index 16d85fa55..38c3187b3 100644 --- a/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts +++ b/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts @@ -21,8 +21,9 @@ import { type NormalizedName, } from "@ensnode/ensnode-sdk"; +import { sortByArrayOrder } from "@/graphql-api/lib/sort-by-array-order"; import { db } from "@/lib/db"; -import { isENSRootRegistry } from "@/lib/protocol-acceleration/ens-root-registry"; +import { isRootRegistry } from "@/lib/root-registry"; import { withActiveSpanAsync, withSpanAsync } from "@/lib/tracing/auto-span"; type FindResolverResult = @@ -72,7 +73,7 @@ export async function findResolver({ } // Invariant: UniversalResolver#findResolver only works for ENS Root Registry - if (!isENSRootRegistry(registry)) { + if (!isRootRegistry(registry)) { throw new Error( `Invariant(findResolver): UniversalResolver#findResolver only identifies active resolvers agains the ENs Root Registry, but a different Registry contract was passed: ${JSON.stringify(registry)}.`, ); @@ -208,9 +209,7 @@ async function findResolverWithIndex( ); // 3.1 sort into the same order as `nodes`, db results are not guaranteed to match `inArray` order - nodeResolverRelations.sort((a, b) => - nodes.indexOf(a.node) > nodes.indexOf(b.node) ? 1 : -1, - ); + nodeResolverRelations.sort(sortByArrayOrder(nodes, (nrr) => nrr.node)); // 4. iterate up the hierarchy and return the first valid resolver for (const { node, resolver } of nodeResolverRelations) { diff --git a/apps/ensapi/src/lib/resolution/forward-resolution.ts b/apps/ensapi/src/lib/resolution/forward-resolution.ts index 251d5c1e8..335aa167f 100644 --- a/apps/ensapi/src/lib/resolution/forward-resolution.ts +++ b/apps/ensapi/src/lib/resolution/forward-resolution.ts @@ -20,7 +20,6 @@ import { } from "@ensnode/ensnode-sdk"; import { makeLogger } from "@/lib/logger"; -import { ENS_ROOT_REGISTRY } from "@/lib/protocol-acceleration/ens-root-registry"; import { findResolver } from "@/lib/protocol-acceleration/find-resolver"; import { getENSIP19ReverseNameRecordFromIndex } from "@/lib/protocol-acceleration/get-primary-name-from-index"; import { getRecordsFromIndex } from "@/lib/protocol-acceleration/get-records-from-index"; @@ -38,6 +37,7 @@ import { interpretRawCallsAndResults, makeResolveCalls, } from "@/lib/resolution/resolve-calls-and-results"; +import { ROOT_REGISTRY } from "@/lib/root-registry"; import { supportsENSIP10Interface } from "@/lib/rpc/ensip-10"; import { getPublicClient } from "@/lib/rpc/public-client"; import { withActiveSpanAsync, withSpanAsync } from "@/lib/tracing/auto-span"; @@ -87,7 +87,7 @@ export async function resolveForward ): Promise> { // NOTE: `resolveForward` is just `_resolveForward` with the enforcement that `registry` must // initially be ENS Root Chain's Registry: see `_resolveForward` for additional context. - return _resolveForward(name, selection, { ...options, registry: ENS_ROOT_REGISTRY }); + return _resolveForward(name, selection, { ...options, registry: ROOT_REGISTRY }); } /** diff --git a/apps/ensapi/src/lib/root-registry.ts b/apps/ensapi/src/lib/root-registry.ts new file mode 100644 index 000000000..8bfddc3a0 --- /dev/null +++ b/apps/ensapi/src/lib/root-registry.ts @@ -0,0 +1,18 @@ +import config from "@/config"; + +import { DatasourceNames, getDatasource } from "@ensnode/datasources"; +import { type AccountId, accountIdEqual, makeRegistryContractId } from "@ensnode/ensnode-sdk"; + +// TODO: remove, helps types while implementing +if (config.namespace !== "ens-test-env") throw new Error("nope"); + +const ensroot = getDatasource(config.namespace, DatasourceNames.ENSRoot); + +export const ROOT_REGISTRY = { + chainId: ensroot.chain.id, + address: ensroot.contracts.RootRegistry.address, +} satisfies AccountId; + +export const ROOT_REGISTRY_ID = makeRegistryContractId(ROOT_REGISTRY); + +export const isRootRegistry = (accountId: AccountId) => accountIdEqual(accountId, ROOT_REGISTRY); diff --git a/apps/ensindexer/src/lib/ensv2/db-helpers.ts b/apps/ensindexer/src/lib/ensv2/account-db-helpers.ts similarity index 100% rename from apps/ensindexer/src/lib/ensv2/db-helpers.ts rename to apps/ensindexer/src/lib/ensv2/account-db-helpers.ts diff --git a/apps/ensindexer/src/lib/ensv2/labelspace-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/labelspace-db-helpers.ts index f96a41dff..34c032cce 100644 --- a/apps/ensindexer/src/lib/ensv2/labelspace-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/labelspace-db-helpers.ts @@ -10,10 +10,10 @@ export async function ensureLabel(context: Context, label: LiteralLabel) { const labelHash = labelhash(label); const interpretedLabel = literalLabelToInterpretedLabel(label); - const existing = await context.db.find(schema.labelInNamespace, { labelHash }); + const existing = await context.db.find(schema.label, { labelHash }); await context.db - .insert(schema.labelInNamespace) + .insert(schema.label) .values({ labelHash, value: interpretedLabel }) .onConflictDoUpdate({ value: interpretedLabel }); diff --git a/apps/ensindexer/src/lib/ensv2/reconciliation.ts b/apps/ensindexer/src/lib/ensv2/reconciliation.ts index 29d61c734..bf22bb704 100644 --- a/apps/ensindexer/src/lib/ensv2/reconciliation.ts +++ b/apps/ensindexer/src/lib/ensv2/reconciliation.ts @@ -1,9 +1,6 @@ import type { Context } from "ponder:registry"; -import type schema from "ponder:schema"; -import type { LabelHash } from "@ensnode/ensnode-sdk"; - -export type DomainId = Pick; +import type { DomainId, LabelHash } from "@ensnode/ensnode-sdk"; // for claude: context.db.sql is the Drizzle object // https://ponder.sh/docs/indexing/write#query-builder diff --git a/apps/ensindexer/src/lib/make-account-id.ts b/apps/ensindexer/src/lib/get-this-account-id.ts similarity index 70% rename from apps/ensindexer/src/lib/make-account-id.ts rename to apps/ensindexer/src/lib/get-this-account-id.ts index 525379673..9310c2ae5 100644 --- a/apps/ensindexer/src/lib/make-account-id.ts +++ b/apps/ensindexer/src/lib/get-this-account-id.ts @@ -2,5 +2,5 @@ import type { Context, Event } from "ponder:registry"; import type { AccountId } from "@ensnode/ensnode-sdk"; -export const makeAccountId = (context: Context, event: Pick) => +export const getThisAccountId = (context: Context, event: Pick) => ({ chainId: context.chain.id, address: event.log.address }) satisfies AccountId; diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/EnhancedAccessControl.ts b/apps/ensindexer/src/plugins/ensv2/handlers/EnhancedAccessControl.ts index af93d9dfd..0f10657cf 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/EnhancedAccessControl.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/EnhancedAccessControl.ts @@ -4,7 +4,7 @@ import type { Address } from "viem"; import { PluginName } from "@ensnode/ensnode-sdk"; -import { makeAccountId } from "@/lib/make-account-id"; +import { getThisAccountId } from "@/lib/get-this-account-id"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; @@ -60,7 +60,7 @@ export default function () { }) => { const { resource, roleBitmap: roles, account: user } = event.args; - const accountId = makeAccountId(context, event); + const accountId = getThisAccountId(context, event); await ensurePermissionsResource(context, accountId, resource); const permissionsUserId: PermissionsUsersId = { ...accountId, resource, user }; @@ -87,7 +87,7 @@ export default function () { }) => { const { resource, roleBitmap: roles, account: user } = event.args; - const accountId = makeAccountId(context, event); + const accountId = getThisAccountId(context, event); await ensurePermissionsResource(context, accountId, resource); const permissionsUserId: PermissionsUsersId = { ...accountId, resource, user }; @@ -113,7 +113,7 @@ export default function () { }) => { const { resource, account: user } = event.args; - const accountId = makeAccountId(context, event); + const accountId = getThisAccountId(context, event); await ensurePermissionsResource(context, accountId, resource); const permissionsUserId: PermissionsUsersId = { ...accountId, resource, user }; diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts index 9af429c2a..72ae91bd2 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts @@ -5,33 +5,25 @@ import { type Address, hexToBigInt, isAddressEqual, labelhash, zeroAddress } fro import { type AccountId, + getCanonicalId, type LiteralLabel, + makeENSv2DomainId, PluginName, serializeAccountId, } from "@ensnode/ensnode-sdk"; -import { ensureAccount } from "@/lib/ensv2/db-helpers"; +import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; import { ensureLabel } from "@/lib/ensv2/labelspace-db-helpers"; import { - type DomainId, reconcileDomainAddition, reconcileDomainRemoval, reconcileRegistryAddition, reconcileRegistryRemoval, } from "@/lib/ensv2/reconciliation"; -import { makeAccountId } from "@/lib/make-account-id"; +import { getThisAccountId } from "@/lib/get-this-account-id"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; -// will need to filter all ERC1155 events by whether a Registry entity exists already or not, to -// avoid indexing literally all ERC1155 contracts on every chain. we still filter for those, which -// is awful performance.. hmm... - -/** - * A Domain's canonical ID is uint256(labelHash) with right-most 32 bits zero'd. - */ -const getCanonicalId = (tokenId: bigint) => tokenId ^ (tokenId & 0xffffffffn); - export default function () { ponder.on( namespaceContract(PluginName.ENSv2, "Registry:NameRegistered"), @@ -50,11 +42,11 @@ export default function () { const { tokenId, label: _label, expiration, registeredBy: registrant } = event.args; const label = _label as LiteralLabel; - const registryAccountId = makeAccountId(context, event); + const registryAccountId = getThisAccountId(context, event); const registryId = serializeAccountId(registryAccountId); const canonicalId = getCanonicalId(tokenId); const labelHash = labelhash(label); - const domainId: DomainId = { registryId, canonicalId }; + const domainId = makeENSv2DomainId(registryAccountId, canonicalId); // Sanity Check: Canonical Id must match emitted label if (canonicalId !== getCanonicalId(hexToBigInt(labelhash(label)))) { @@ -74,6 +66,7 @@ export default function () { ); } + // upsert Registry await context.db .insert(schema.registry) .values({ @@ -83,11 +76,19 @@ export default function () { }) .onConflictDoNothing(); + // ensure discovered Label await ensureLabel(context, label); - await context.db.insert(schema.domain).values({ ...domainId, labelHash }); + + // insert Domain + await context.db + .insert(schema.domain) + .values({ id: domainId, registryId, labelHash, canonicalId }); + + // reconcile Domain addition await reconcileDomainAddition(context, domainId); // TODO: insert Registration entity for this domain as well: expiration, registrant + // ensure Registrant await ensureAccount(context, registrant); }, ); @@ -106,17 +107,16 @@ export default function () { }) => { const { id: tokenId, subregistry } = event.args; + const registryAccountId = getThisAccountId(context, event); const canonicalId = getCanonicalId(tokenId); - const registryAccountId = makeAccountId(context, event); - const registryId = serializeAccountId(registryAccountId); - const domainId: DomainId = { registryId, canonicalId }; + const domainId = makeENSv2DomainId(registryAccountId, canonicalId); - const existing = await context.db.find(schema.domain, domainId); + const existing = await context.db.find(schema.domain, { id: domainId }); // update domain's subregistry const isDeletion = isAddressEqual(subregistry, zeroAddress); if (isDeletion) { - await context.db.update(schema.domain, domainId).set({ subregistryId: null }); + await context.db.update(schema.domain, { id: domainId }).set({ subregistryId: null }); // reconcile the removal of this registry from the canonical nametree if (existing && existing.subregistryId !== null) { @@ -126,7 +126,7 @@ export default function () { const subregistryAccountId: AccountId = { chainId: context.chain.id, address: subregistry }; const subregistryId = serializeAccountId(subregistryAccountId); - await context.db.update(schema.domain, domainId).set({ subregistryId }); + await context.db.update(schema.domain, { id: domainId }).set({ subregistryId }); // reconcile the addition of this registry to the canonical nametree if (existing?.subregistryId !== subregistryId) { @@ -151,19 +151,18 @@ export default function () { const { id: tokenId, resolver } = event.args; const canonicalId = getCanonicalId(tokenId); - const registryAccountId = makeAccountId(context, event); - const registryId = serializeAccountId(registryAccountId); - const domainId: DomainId = { registryId, canonicalId }; + const registryAccountId = getThisAccountId(context, event); + const domainId = makeENSv2DomainId(registryAccountId, canonicalId); // update domain's resolver const isDeletion = isAddressEqual(resolver, zeroAddress); if (isDeletion) { await context.db - .update(schema.domain, domainId) + .update(schema.domain, { id: domainId }) .set({ resolverChainId: null, resolverAddress: null }); } else { await context.db - .update(schema.domain, domainId) + .update(schema.domain, { id: domainId }) .set({ resolverChainId: context.chain.id, resolverAddress: resolver }); } }, @@ -184,11 +183,10 @@ export default function () { const { tokenId } = event.args; const canonicalId = getCanonicalId(tokenId); - const registryAccountId = makeAccountId(context, event); - const registryId = serializeAccountId(registryAccountId); - const domainId: DomainId = { registryId, canonicalId }; + const registryAccountId = getThisAccountId(context, event); + const domainId = makeENSv2DomainId(registryAccountId, canonicalId); - await context.db.delete(schema.domain, domainId); + await context.db.delete(schema.domain, { id: domainId }); // TODO: delete registration (?) await reconcileDomainRemoval(context, domainId); }, @@ -204,18 +202,36 @@ export default function () { const { id: tokenId, to: owner } = event.args; const canonicalId = getCanonicalId(tokenId); - const registryAccountId = makeAccountId(context, event); - const registryId = serializeAccountId(registryAccountId); - const domainId: DomainId = { registryId, canonicalId }; + const registryAccountId = getThisAccountId(context, event); + const domainId = makeENSv2DomainId(registryAccountId, canonicalId); // just update the owner, NameBurned handles existence - await context.db.update(schema.domain, domainId).set({ ownerId: owner }); + await context.db.update(schema.domain, { id: domainId }).set({ ownerId: owner }); } - ponder.on(namespaceContract(PluginName.ENSv2, "Registry:TransferSingle"), handleTransferSingle); + ponder.on( + namespaceContract(PluginName.ENSv2, "Registry:TransferSingle"), + async ({ context, event }) => { + const registryAccountId = getThisAccountId(context, event); + const registryId = serializeAccountId(registryAccountId); + + // TODO(registry-announcement): ideally remove this + const registry = await context.db.find(schema.registry, { id: registryId }); + if (registry === null) return; // no-op non-Registry ERC1155 Transfers + + await handleTransferSingle({ context, event }); + }, + ); ponder.on( namespaceContract(PluginName.ENSv2, "Registry:TransferBatch"), async ({ context, event }) => { + const registryAccountId = getThisAccountId(context, event); + const registryId = serializeAccountId(registryAccountId); + + // TODO(registry-announcement): ideally remove this + const registry = await context.db.find(schema.registry, { id: registryId }); + if (registry === null) return; // no-op non-Registry ERC1155 Transfers + for (const id of event.args.ids) { await handleTransferSingle({ context, diff --git a/packages/ensnode-schema/package.json b/packages/ensnode-schema/package.json index 15375c841..46c21700b 100644 --- a/packages/ensnode-schema/package.json +++ b/packages/ensnode-schema/package.json @@ -41,6 +41,7 @@ "viem": "catalog:" }, "devDependencies": { + "@ensnode/ensnode-sdk": "workspace:", "@ensnode/shared-configs": "workspace:*", "tsup": "catalog:", "typescript": "catalog:" diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/schemas/ensv2.schema.ts index b373a12dd..6907d725b 100644 --- a/packages/ensnode-schema/src/schemas/ensv2.schema.ts +++ b/packages/ensnode-schema/src/schemas/ensv2.schema.ts @@ -1,4 +1,15 @@ -import { onchainEnum, onchainTable, primaryKey, relations, uniqueIndex } from "ponder"; +import { onchainEnum, onchainTable, primaryKey, relations } from "ponder"; +import type { Address } from "viem"; + +import type { + CanonicalId, + ChainId, + DomainId, + InterpretedLabel, + LabelHash, + Node, + RegistryId, +} from "@ensnode/ensnode-sdk"; // Registry<->Domain is 1:1 // Registry->Doimains is 1:many @@ -17,7 +28,7 @@ import { onchainEnum, onchainTable, primaryKey, relations, uniqueIndex } from "p /////////// export const account = onchainTable("accounts", (t) => ({ - address: t.hex().primaryKey(), + address: t.hex().primaryKey().$type
(), })); export const account_relations = relations(account, ({ many }) => ({ @@ -36,14 +47,13 @@ export const registryType = onchainEnum("RegistryType", ["RegistryContract", "Im export const registry = onchainTable( "registries", (t) => ({ - // If RegistryContract: CAIP-10 Account ID - // If ImplicitRegistry: parentDomainNode - id: t.text().primaryKey(), + // see RegistryId for guarantees + id: t.text().primaryKey().$type(), type: registryType().notNull(), - chainId: t.integer(), - address: t.hex(), - parentDomainNode: t.hex(), + chainId: t.integer().$type(), + address: t.hex().$type
(), + parentDomainNode: t.hex().$type(), }), (t) => ({ // @@ -71,23 +81,27 @@ export const relations_registry = relations(registry, ({ one, many }) => ({ export const domain = onchainTable( "domains", (t) => ({ + // see DomainId for guarantees + id: t.text().primaryKey().$type(), + // belongs to registry by (registryId) - registryId: t.text().notNull(), - canonicalId: t.bigint().notNull(), - labelHash: t.hex().notNull(), + registryId: t.text().notNull().$type(), + + // TODO: we could probably avoid storing this at all and compute it on-demand + canonicalId: t.bigint().notNull().$type(), + labelHash: t.hex().notNull().$type(), - ownerId: t.hex(), + ownerId: t.hex().$type
(), // may have one subregistry by (id) - subregistryId: t.text(), + subregistryId: t.text().$type(), // may have one resolver by (chainId, address) - resolverChainId: t.integer(), - resolverAddress: t.hex(), + resolverChainId: t.integer().$type(), + resolverAddress: t.hex().$type
(), }), (t) => ({ - // unique by (registryId, canonicalId) - pk: primaryKey({ columns: [t.registryId, t.canonicalId] }), + // }), ); @@ -107,14 +121,19 @@ export const relations_domain = relations(domain, ({ one }) => ({ fields: [domain.subregistryId], references: [registry.id], }), - label: one(labelInNamespace, { + label: one(label, { relationName: "label", fields: [domain.labelHash], - references: [labelInNamespace.labelHash], + references: [label.labelHash], }), - name: one(nameInNamespace), })); +///////////////// +// Registrations +///////////////// + +// TODO: derive from registries plugin + /////////////// // Permissions /////////////// @@ -122,8 +141,8 @@ export const relations_domain = relations(domain, ({ one }) => ({ export const permissions = onchainTable( "permissions", (t) => ({ - chainId: t.integer().notNull(), - address: t.hex().notNull(), + chainId: t.integer().notNull().$type(), + address: t.hex().notNull().$type
(), }), (t) => ({ pk: primaryKey({ columns: [t.chainId, t.address] }), @@ -138,8 +157,8 @@ export const relations_permissions = relations(permissions, ({ one, many }) => ( export const permissionsResource = onchainTable( "permissions_resources", (t) => ({ - chainId: t.integer().notNull(), - address: t.hex().notNull(), + chainId: t.integer().notNull().$type(), + address: t.hex().notNull().$type
(), resource: t.bigint().notNull(), }), (t) => ({ @@ -157,10 +176,10 @@ export const relations_permissionsResource = relations(permissionsResource, ({ o export const permissionsUser = onchainTable( "permissions_users", (t) => ({ - chainId: t.integer().notNull(), - address: t.hex().notNull(), + chainId: t.integer().notNull().$type(), + address: t.hex().notNull().$type
(), resource: t.bigint().notNull(), - user: t.hex().notNull(), + user: t.hex().notNull().$type
(), // has one roles bitmap // TODO: can materialize into more semantic (polymorphic) interpretation of roles based on source @@ -195,40 +214,14 @@ export const relations_permissionsUser = relations(permissionsUser, ({ one, many // Labels ////////// -export const labelInNamespace = onchainTable("labels_in_namespace", (t) => ({ - labelHash: t.hex().primaryKey(), - value: t.text().notNull(), +export const label = onchainTable("labels", (t) => ({ + labelHash: t.hex().primaryKey().$type(), + value: t.text().notNull().$type(), // internals hasAttemptedHeal: t.boolean().notNull().default(false), })); -export const labelInNamespace_relations = relations(labelInNamespace, ({ many }) => ({ +export const label_relations = relations(label, ({ many }) => ({ domains: many(domain), })); - -///////// -// Names -///////// - -export const nameInNamespace = onchainTable( - "names_in_namespace", - (t) => ({ - node: t.hex().primaryKey(), - fqdn: t.text().notNull(), - - domainRegistryId: t.text().notNull(), - domainCanonicalId: t.bigint().notNull(), - }), - (t) => ({ - byFqdn: uniqueIndex().on(t.fqdn), - }), -); - -export const nameInNamespace_relations = relations(nameInNamespace, ({ one }) => ({ - domain: one(domain, { - relationName: "name", - fields: [nameInNamespace.domainRegistryId, nameInNamespace.domainCanonicalId], - references: [domain.registryId, domain.canonicalId], - }), -})); diff --git a/packages/ensnode-sdk/src/ens/types.ts b/packages/ensnode-sdk/src/ens/types.ts index 6321f2bc5..8309c2bea 100644 --- a/packages/ensnode-sdk/src/ens/types.ts +++ b/packages/ensnode-sdk/src/ens/types.ts @@ -1,5 +1,7 @@ import type { Hex } from "viem"; +import type { DomainId } from "../ensv2"; + export type { ENSNamespaceId } from "@ensnode/datasources"; // re-export ENSNamespaceIds and ENSNamespaceId from @ensnode/datasources // so consumers don't need it as a dependency @@ -50,6 +52,24 @@ export type NormalizedName = Name & { __brand: "NormalizedName" }; */ export type LabelHash = Hex; +/** + * A LabelPath is an ordered list of LabelHashes that uniquely identify an ENS Name. + * It is ordered in namegraph TRAVERSAL order (i.e. the opposite order of an ENS Name's labels). + * + * ex: example.eth's LabelPath is + * [ + * '0x4f5b812789fc606be1b3b16908db13fc7a9adf7ca72641f84d75b47069d3d7f0', // 'eth' + * '0x6fd43e7cffc31bb581d7421c8698e29aa2bd8e7186a394b85299908b4eb9b175', // 'example' + * ] + */ +export type LabelPath = LabelHash[]; + +/** + * CanonicalPath is an ordered list of DomainIds describing the canonical path to a Domain. + * It is ordered in namegraph TRAVERSAL order (i.e. the opposite order of an ENS Name's labels). + */ +export type CanonicalPath = DomainId[]; + /** * A Label is a single part of an ENS Name. * diff --git a/packages/ensnode-sdk/src/ensv2/ids-lib.ts b/packages/ensnode-sdk/src/ensv2/ids-lib.ts new file mode 100644 index 000000000..cacece6e9 --- /dev/null +++ b/packages/ensnode-sdk/src/ensv2/ids-lib.ts @@ -0,0 +1,38 @@ +import { hexToBigInt } from "viem"; + +import { + type AccountId, + type LabelHash, + serializeAccountId, + serializeAssetId, +} from "@ensnode/ensnode-sdk"; + +import type { CanonicalId, ENSv2DomainId, RegistryContractId } from "./ids"; + +/** + * Serializes and brands an AccountId as a RegistryId. + */ +export const makeRegistryContractId = (accountId: AccountId) => + serializeAccountId(accountId) as RegistryContractId; + +/** + * Makes an ENSv2 Domain Id given the parent `registry` and the domain's `canonicalId`. + */ +export const makeENSv2DomainId = (registry: AccountId, canonicalId: CanonicalId) => + serializeAssetId({ + ...registry, + tokenId: canonicalId, + }) as ENSv2DomainId; + +/** + * Masks the lower 32 bits of `num`. + */ +const maskLower32Bits = (num: bigint) => num ^ (num & 0xffffffffn); + +/** + * Computes a Domain's {@link CanonicalId} given its tokenId or LabelHash as `input`. + */ +export const getCanonicalId = (input: bigint | LabelHash): CanonicalId => { + if (typeof input === "bigint") return maskLower32Bits(input); + return getCanonicalId(hexToBigInt(input)); +}; diff --git a/packages/ensnode-sdk/src/ensv2/ids.ts b/packages/ensnode-sdk/src/ensv2/ids.ts new file mode 100644 index 000000000..e99141975 --- /dev/null +++ b/packages/ensnode-sdk/src/ensv2/ids.ts @@ -0,0 +1,38 @@ +import type { Hex } from "viem"; + +import type { Node } from "@ensnode/ensnode-sdk"; + +/** + * Serialized CAIP-10 Asset ID that uniquely identifies a Registry contract. + */ +export type RegistryContractId = string & { __brand: "RegistryContractId" }; + +/** + * Parent Node that uniquely identifies an Implicit Registry. + */ +export type ImplicitRegistryId = Hex & { __brand: "ImplicitRegistryId" }; + +/** + * A RegistryId is one of RegistryContractId or ImplicitRegistryId. + */ +export type RegistryId = RegistryContractId | ImplicitRegistryId; + +/** + * A Domain's Canonical Id is uint256(labelHash) with lower (right-most) 32 bits zero'd. + */ +export type CanonicalId = bigint; + +/** + * The node that uniquely identifies an ENSv1 name. + */ +export type ENSv1DomainId = Node & { __brand: "ENSv1DomainId" }; + +/** + * The Serialized CAIP-19 Asset ID that uniquely identifies an ENSv2 name. + */ +export type ENSv2DomainId = string & { __brand: "ENSv2DomainId" }; + +/** + * A DomainId is one of ENSv1DomainId or ENSv2DomainId. + */ +export type DomainId = ENSv1DomainId | ENSv2DomainId; diff --git a/packages/ensnode-sdk/src/ensv2/index.ts b/packages/ensnode-sdk/src/ensv2/index.ts new file mode 100644 index 000000000..2e08fd50b --- /dev/null +++ b/packages/ensnode-sdk/src/ensv2/index.ts @@ -0,0 +1,2 @@ +export * from "./ids"; +export * from "./ids-lib"; diff --git a/packages/ensnode-sdk/src/index.ts b/packages/ensnode-sdk/src/index.ts index aba4fcb95..efc8b6165 100644 --- a/packages/ensnode-sdk/src/index.ts +++ b/packages/ensnode-sdk/src/index.ts @@ -5,6 +5,7 @@ export * from "./ens"; export * from "./ensapi"; export * from "./ensindexer"; export * from "./ensrainbow"; +export * from "./ensv2"; export * from "./identity"; export * from "./registrars"; export * from "./resolution"; diff --git a/packages/ensnode-sdk/src/shared/interpretation.ts b/packages/ensnode-sdk/src/shared/interpretation.ts index 1f3d95269..bc77615dd 100644 --- a/packages/ensnode-sdk/src/shared/interpretation.ts +++ b/packages/ensnode-sdk/src/shared/interpretation.ts @@ -1,11 +1,17 @@ +import { isHex } from "viem"; +import { labelhash } from "viem/ens"; + import { encodeLabelHash, type InterpretedLabel, type InterpretedName, isNormalizedLabel, type Label, + type LabelHash, + type LabelPath, type LiteralLabel, type LiteralName, + type Name, } from "../ens"; import { labelhashLiteralLabel } from "./labelhash"; @@ -66,3 +72,57 @@ export function interpretedLabelsToInterpretedName(labels: InterpretedLabel[]): export function literalLabelsToLiteralName(labels: LiteralLabel[]): LiteralName { return labels.join(".") as LiteralName; } + +/** + * Converts an Interpreted Name into a list of Interpreted Labels. + */ +export function interpretedNameToInterpretedLabels(name: InterpretedName): InterpretedLabel[] { + return name.split(".") as InterpretedLabel[]; +} + +// https://github.com/wevm/viem/blob/main/src/utils/ens/encodedLabelToLabelhash.ts +export function encodedLabelToLabelhash(label: string): LabelHash | null { + if (label.length !== 66) return null; + if (label.indexOf("[") !== 0) return null; + if (label.indexOf("]") !== 65) return null; + const hash = `0x${label.slice(1, 65)}`; + if (!isHex(hash)) return null; + return hash; +} + +export function isInterpetedLabel(label: Label): label is InterpretedLabel { + // if it looks like an encoded labelhash, it must be one + if (label.startsWith("[")) { + const labelHash = encodedLabelToLabelhash(label); + if (labelHash === null) return false; + } + + // otherwise label must be normalized + return isNormalizedLabel(label); +} + +export function isInterpretedName(name: Name): name is InterpretedName { + return name.split(".").every(isInterpetedLabel); +} + +/** + * Converts an InterpretedName into a LabelPath. + */ +export function interpretedNameToLabelPath(name: InterpretedName): LabelPath { + return interpretedNameToInterpretedLabels(name) + .map((label) => { + if (!isInterpetedLabel) { + throw new Error( + `Invariant(interpretedNameToLabelPath): Expected InterpretedLabel, received '${label}'.`, + ); + } + + // if it looks like an encoded labelhash, return it + const maybeLabelHash = encodedLabelToLabelhash(label); + if (maybeLabelHash !== null) return maybeLabelHash; + + // otherwise, labelhash it + return labelhash(label); + }) + .toReversed(); +} diff --git a/packages/ensnode-sdk/src/shared/serialize.ts b/packages/ensnode-sdk/src/shared/serialize.ts index aeda234dc..09ed7d723 100644 --- a/packages/ensnode-sdk/src/shared/serialize.ts +++ b/packages/ensnode-sdk/src/shared/serialize.ts @@ -1,13 +1,14 @@ -import { AccountId as CaipAccountId } from "caip"; +import { AccountId as CaipAccountId, AssetId as CaipAssetId } from "caip"; import type { Price, SerializedPrice } from "./currencies"; import type { ChainIdString, DatetimeISO8601, SerializedAccountId, + SerializedAssetId, UrlString, } from "./serialized-types"; -import type { AccountId, ChainId, Datetime } from "./types"; +import type { AccountId, AssetId, ChainId, Datetime } from "./types"; /** * Serializes a {@link ChainId} value into its string representation. @@ -53,3 +54,18 @@ export function serializeAccountId(accountId: AccountId): SerializedAccountId { address: accountId.address, }).toLowerCase(); } + +/** + * Serializes {@link AssetId} object. + * + * Formatted as a fully lowercase CAIP-19 AssetId. + * + * @see https://chainagnostic.org/CAIPs/caip-19 + */ +export function serializeAssetId({ chainId, address, tokenId }: AssetId): SerializedAssetId { + return CaipAssetId.format({ + chainId: { namespace: "eip155", reference: chainId.toString() }, + assetName: { namespace: "erc1155", reference: address }, + tokenId: tokenId.toString(), + }).toLowerCase(); +} diff --git a/packages/ensnode-sdk/src/shared/serialized-types.ts b/packages/ensnode-sdk/src/shared/serialized-types.ts index d5186f68a..921064c12 100644 --- a/packages/ensnode-sdk/src/shared/serialized-types.ts +++ b/packages/ensnode-sdk/src/shared/serialized-types.ts @@ -25,3 +25,12 @@ export type UrlString = string; * @see https://chainagnostic.org/CAIPs/caip-10 */ export type SerializedAccountId = string; + +/** + * Serialized representation of {@link AssetId}. + * + * Formatted as a fully lowercase CAIP-19 AssetId. + * + * @see https://chainagnostic.org/CAIPs/caip-19 + */ +export type SerializedAssetId = string; diff --git a/packages/ensnode-sdk/src/shared/types.ts b/packages/ensnode-sdk/src/shared/types.ts index d3b41c7a3..8c9e02155 100644 --- a/packages/ensnode-sdk/src/shared/types.ts +++ b/packages/ensnode-sdk/src/shared/types.ts @@ -32,6 +32,17 @@ export interface AccountId { address: Address; } +/** + * Represents an ERC1155 asset by `tokenId` at `address` on chain `chainId`. + * + * @see https://chainagnostic.org/CAIPs/caip-19 + */ +export interface AssetId { + chainId: ChainId; + address: Address; + tokenId: bigint; +} + /** * Block Number * @@ -135,3 +146,10 @@ export type DeepPartial = { ? DeepPartial : T[P]; }; + +/** + * Marks keys in K as required (not undefined) and not null. + */ +export type RequiredAndNotNull = T & { + [P in K]-?: NonNullable; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f8e76435..4f4680c7b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -748,6 +748,9 @@ importers: specifier: 'catalog:' version: 2.38.5(typescript@5.9.3)(zod@3.25.76) devDependencies: + '@ensnode/ensnode-sdk': + specifier: 'workspace:' + version: link:../ensnode-sdk '@ensnode/shared-configs': specifier: workspace:* version: link:../shared-configs @@ -10850,14 +10853,6 @@ snapshots: chai: 6.2.0 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.5(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1))': - dependencies: - '@vitest/spy': 4.0.5 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) - '@vitest/mocker@4.0.5(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@vitest/spy': 4.0.5 @@ -15516,7 +15511,7 @@ snapshots: vitest@4.0.5(@types/debug@4.1.12)(@types/node@20.19.24)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.6))(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1): dependencies: '@vitest/expect': 4.0.5 - '@vitest/mocker': 4.0.5(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/mocker': 4.0.5(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/pretty-format': 4.0.5 '@vitest/runner': 4.0.5 '@vitest/snapshot': 4.0.5 From ccdcf9ddd5c34869d138f93fcb6ff24cfbd04cdc Mon Sep 17 00:00:00 2001 From: shrugs Date: Tue, 11 Nov 2025 11:10:33 -0600 Subject: [PATCH 006/102] checkpoint --- .../ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts | 12 ++++++------ .../src/plugins/ensv2/handlers/Registry.ts | 10 +++++----- packages/ensnode-schema/src/schemas/ensv2.schema.ts | 1 + packages/ensnode-sdk/src/ens/types.ts | 6 +++--- packages/ensnode-sdk/src/shared/interpretation.ts | 8 ++++---- packages/shared-configs/tsconfig.ponder.json | 4 ++-- 6 files changed, 21 insertions(+), 20 deletions(-) diff --git a/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts index bb021bca5..41be9e776 100644 --- a/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts +++ b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts @@ -4,7 +4,7 @@ import * as schema from "@ensnode/ensnode-schema"; import { type DomainId, type InterpretedName, - interpretedNameToLabelPath, + interpretedNameToLabelHashPath, type LabelHash, type RegistryId, } from "@ensnode/ensnode-sdk"; @@ -19,10 +19,10 @@ import { ROOT_REGISTRY_ID } from "@/lib/root-registry"; export async function getDomainIdByInterpretedName( name: InterpretedName, ): Promise { - const labelPath = interpretedNameToLabelPath(name); + const labelHashPath = interpretedNameToLabelHashPath(name); // https://github.com/drizzle-team/drizzle-orm/issues/1289#issuecomment-2688581070 - const rawLabelPathArray = sql`${new Param(labelPath)}::text[]`; + const rawLabelHashPathArray = sql`${new Param(labelHashPath)}::text[]`; const result = await db.execute(sql` WITH RECURSIVE path AS ( @@ -44,8 +44,8 @@ export async function getDomainIdByInterpretedName( FROM path JOIN ${schema.domain} d ON d.registry_id = path.registry_id - WHERE d.label_hash = (${rawLabelPathArray})[path.depth + 1] - AND path.depth + 1 <= array_length(${rawLabelPathArray}, 1) + WHERE d.label_hash = (${rawLabelHashPathArray})[path.depth + 1] + AND path.depth + 1 <= array_length(${rawLabelHashPathArray}, 1) ) SELECT * FROM path @@ -61,7 +61,7 @@ export async function getDomainIdByInterpretedName( depth: number; }[]; - const exists = rows.length > 0 && rows.length === labelPath.length; + const exists = rows.length > 0 && rows.length === labelHashPath.length; if (!exists) return null; // biome-ignore lint/style/noNonNullAssertion: length check above diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts index 72ae91bd2..b85e5d69a 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts @@ -8,8 +8,8 @@ import { getCanonicalId, type LiteralLabel, makeENSv2DomainId, + makeRegistryContractId, PluginName, - serializeAccountId, } from "@ensnode/ensnode-sdk"; import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; @@ -43,7 +43,7 @@ export default function () { const label = _label as LiteralLabel; const registryAccountId = getThisAccountId(context, event); - const registryId = serializeAccountId(registryAccountId); + const registryId = makeRegistryContractId(registryAccountId); const canonicalId = getCanonicalId(tokenId); const labelHash = labelhash(label); const domainId = makeENSv2DomainId(registryAccountId, canonicalId); @@ -124,7 +124,7 @@ export default function () { } } else { const subregistryAccountId: AccountId = { chainId: context.chain.id, address: subregistry }; - const subregistryId = serializeAccountId(subregistryAccountId); + const subregistryId = makeRegistryContractId(subregistryAccountId); await context.db.update(schema.domain, { id: domainId }).set({ subregistryId }); @@ -213,7 +213,7 @@ export default function () { namespaceContract(PluginName.ENSv2, "Registry:TransferSingle"), async ({ context, event }) => { const registryAccountId = getThisAccountId(context, event); - const registryId = serializeAccountId(registryAccountId); + const registryId = makeRegistryContractId(registryAccountId); // TODO(registry-announcement): ideally remove this const registry = await context.db.find(schema.registry, { id: registryId }); @@ -226,7 +226,7 @@ export default function () { namespaceContract(PluginName.ENSv2, "Registry:TransferBatch"), async ({ context, event }) => { const registryAccountId = getThisAccountId(context, event); - const registryId = serializeAccountId(registryAccountId); + const registryId = makeRegistryContractId(registryAccountId); // TODO(registry-announcement): ideally remove this const registry = await context.db.find(schema.registry, { id: registryId }); diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/schemas/ensv2.schema.ts index 6907d725b..ffec8fe5e 100644 --- a/packages/ensnode-schema/src/schemas/ensv2.schema.ts +++ b/packages/ensnode-schema/src/schemas/ensv2.schema.ts @@ -216,6 +216,7 @@ export const relations_permissionsUser = relations(permissionsUser, ({ one, many export const label = onchainTable("labels", (t) => ({ labelHash: t.hex().primaryKey().$type(), + // TODO: store literal/interpeted values as well or only interpreted? value: t.text().notNull().$type(), // internals diff --git a/packages/ensnode-sdk/src/ens/types.ts b/packages/ensnode-sdk/src/ens/types.ts index 8309c2bea..32350a30b 100644 --- a/packages/ensnode-sdk/src/ens/types.ts +++ b/packages/ensnode-sdk/src/ens/types.ts @@ -53,16 +53,16 @@ export type NormalizedName = Name & { __brand: "NormalizedName" }; export type LabelHash = Hex; /** - * A LabelPath is an ordered list of LabelHashes that uniquely identify an ENS Name. + * A LabelHashPath is an ordered list of LabelHashes that uniquely identify an ENS Name. * It is ordered in namegraph TRAVERSAL order (i.e. the opposite order of an ENS Name's labels). * - * ex: example.eth's LabelPath is + * ex: example.eth's LabelHashPath is * [ * '0x4f5b812789fc606be1b3b16908db13fc7a9adf7ca72641f84d75b47069d3d7f0', // 'eth' * '0x6fd43e7cffc31bb581d7421c8698e29aa2bd8e7186a394b85299908b4eb9b175', // 'example' * ] */ -export type LabelPath = LabelHash[]; +export type LabelHashPath = LabelHash[]; /** * CanonicalPath is an ordered list of DomainIds describing the canonical path to a Domain. diff --git a/packages/ensnode-sdk/src/shared/interpretation.ts b/packages/ensnode-sdk/src/shared/interpretation.ts index bc77615dd..1a5bac1c8 100644 --- a/packages/ensnode-sdk/src/shared/interpretation.ts +++ b/packages/ensnode-sdk/src/shared/interpretation.ts @@ -8,7 +8,7 @@ import { isNormalizedLabel, type Label, type LabelHash, - type LabelPath, + type LabelHashPath, type LiteralLabel, type LiteralName, type Name, @@ -106,14 +106,14 @@ export function isInterpretedName(name: Name): name is InterpretedName { } /** - * Converts an InterpretedName into a LabelPath. + * Converts an InterpretedName into a LabelHashPath. */ -export function interpretedNameToLabelPath(name: InterpretedName): LabelPath { +export function interpretedNameToLabelHashPath(name: InterpretedName): LabelHashPath { return interpretedNameToInterpretedLabels(name) .map((label) => { if (!isInterpetedLabel) { throw new Error( - `Invariant(interpretedNameToLabelPath): Expected InterpretedLabel, received '${label}'.`, + `Invariant(interpretedNameToLabelHashPath): Expected InterpretedLabel, received '${label}'.`, ); } diff --git a/packages/shared-configs/tsconfig.ponder.json b/packages/shared-configs/tsconfig.ponder.json index af74c0f2a..5a554f22e 100644 --- a/packages/shared-configs/tsconfig.ponder.json +++ b/packages/shared-configs/tsconfig.ponder.json @@ -10,8 +10,8 @@ "moduleResolution": "bundler", "module": "ESNext", "noEmit": true, - "lib": ["ES2022"], - "target": "ES2022", + "lib": ["ESNext"], + "target": "ESNext", "skipLibCheck": true } } From 9f0e80d3c79a85fc7c19e573940d1ff794ba3466 Mon Sep 17 00:00:00 2001 From: shrugs Date: Tue, 11 Nov 2025 11:11:42 -0600 Subject: [PATCH 007/102] fix: remove reconciliation for now --- .../src/lib/ensv2/labelspace-db-helpers.ts | 7 ---- .../src/lib/ensv2/reconciliation.ts | 40 ------------------- .../src/plugins/ensv2/handlers/Registry.ts | 22 ---------- 3 files changed, 69 deletions(-) delete mode 100644 apps/ensindexer/src/lib/ensv2/reconciliation.ts diff --git a/apps/ensindexer/src/lib/ensv2/labelspace-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/labelspace-db-helpers.ts index 34c032cce..c97fbd90c 100644 --- a/apps/ensindexer/src/lib/ensv2/labelspace-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/labelspace-db-helpers.ts @@ -4,19 +4,12 @@ import { labelhash } from "viem"; import { type LiteralLabel, literalLabelToInterpretedLabel } from "@ensnode/ensnode-sdk"; -import { reconcileLabelChange } from "@/lib/ensv2/reconciliation"; - export async function ensureLabel(context: Context, label: LiteralLabel) { const labelHash = labelhash(label); const interpretedLabel = literalLabelToInterpretedLabel(label); - const existing = await context.db.find(schema.label, { labelHash }); - await context.db .insert(schema.label) .values({ labelHash, value: interpretedLabel }) .onConflictDoUpdate({ value: interpretedLabel }); - - // if the label's interpreted value changed, reconcile in namespace - if (existing?.value !== interpretedLabel) await reconcileLabelChange(context, labelHash); } diff --git a/apps/ensindexer/src/lib/ensv2/reconciliation.ts b/apps/ensindexer/src/lib/ensv2/reconciliation.ts deleted file mode 100644 index bf22bb704..000000000 --- a/apps/ensindexer/src/lib/ensv2/reconciliation.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { Context } from "ponder:registry"; - -import type { DomainId, LabelHash } from "@ensnode/ensnode-sdk"; - -// for claude: context.db.sql is the Drizzle object -// https://ponder.sh/docs/indexing/write#query-builder -// https://orm.drizzle.team/docs/rqb - -export async function reconcileRegistryAddition(context: Context, registryId: string) { - // 0. if this registry does not terminate at root, no-op - // 1. for each registry.domains, reconcileDomainAddition in transaction - // TODO: perhaps reconcileDomainAddition can be implemented as batch -} - -export async function reconcileRegistryRemoval(context: Context, registryId: string) { - // 0. if this registry does not terminate at root, no-op - // 1. for each registry.domains, reconcileDomainRemoval in transaction - // TODO: perhaps reconcileDomainRemoval can be implemented as batch -} - -export async function reconcileDomainAddition(context: Context, id: DomainId) { - // 0. if this domain does not terminate at root, no-op - // 1. fetch set of all Domains that have this domain as a parent (include this domain) - // - include: recursive path to root - // 2. for each of these Domains, compute node/fqdn using path - // 3. bulk insert into namespace -} - -export async function reconcileDomainRemoval(context: Context, id: DomainId) { - // 0. if this domain is not in namespace, no-op - // 1. identify this domain + all recursive children (via strict suffix match) - // 2. bulk remove from namespace -} - -export async function reconcileLabelChange(context: Context, labelHash: LabelHash) { - // 1. identify all Domains in the namespace that use this label + all recursive children (via strict suffix match) - // - include: recursive path to root - // 2. for each, compute new fqdn using path - // 3. bulk update -} diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts index b85e5d69a..8e631fd3e 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts @@ -14,12 +14,6 @@ import { import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; import { ensureLabel } from "@/lib/ensv2/labelspace-db-helpers"; -import { - reconcileDomainAddition, - reconcileDomainRemoval, - reconcileRegistryAddition, - reconcileRegistryRemoval, -} from "@/lib/ensv2/reconciliation"; import { getThisAccountId } from "@/lib/get-this-account-id"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; @@ -84,9 +78,6 @@ export default function () { .insert(schema.domain) .values({ id: domainId, registryId, labelHash, canonicalId }); - // reconcile Domain addition - await reconcileDomainAddition(context, domainId); - // TODO: insert Registration entity for this domain as well: expiration, registrant // ensure Registrant await ensureAccount(context, registrant); @@ -111,27 +102,15 @@ export default function () { const canonicalId = getCanonicalId(tokenId); const domainId = makeENSv2DomainId(registryAccountId, canonicalId); - const existing = await context.db.find(schema.domain, { id: domainId }); - // update domain's subregistry const isDeletion = isAddressEqual(subregistry, zeroAddress); if (isDeletion) { await context.db.update(schema.domain, { id: domainId }).set({ subregistryId: null }); - - // reconcile the removal of this registry from the canonical nametree - if (existing && existing.subregistryId !== null) { - await reconcileRegistryRemoval(context, existing.subregistryId); - } } else { const subregistryAccountId: AccountId = { chainId: context.chain.id, address: subregistry }; const subregistryId = makeRegistryContractId(subregistryAccountId); await context.db.update(schema.domain, { id: domainId }).set({ subregistryId }); - - // reconcile the addition of this registry to the canonical nametree - if (existing?.subregistryId !== subregistryId) { - await reconcileRegistryAddition(context, subregistryId); - } } }, ); @@ -188,7 +167,6 @@ export default function () { await context.db.delete(schema.domain, { id: domainId }); // TODO: delete registration (?) - await reconcileDomainRemoval(context, domainId); }, ); From 157a48e9c8a7d6b601e22c3cfefd296b60308324 Mon Sep 17 00:00:00 2001 From: shrugs Date: Tue, 11 Nov 2025 11:23:56 -0600 Subject: [PATCH 008/102] checkpoint --- apps/ensapi/src/graphql-api/builder.ts | 3 +- apps/ensapi/src/graphql-api/schema/domain.ts | 9 +++++ apps/ensapi/src/graphql-api/schema/query.ts | 39 +++++++++---------- apps/ensapi/src/graphql-api/schema/scalars.ts | 11 ++++++ 4 files changed, 41 insertions(+), 21 deletions(-) diff --git a/apps/ensapi/src/graphql-api/builder.ts b/apps/ensapi/src/graphql-api/builder.ts index 32743bb8a..bf2c3d45c 100644 --- a/apps/ensapi/src/graphql-api/builder.ts +++ b/apps/ensapi/src/graphql-api/builder.ts @@ -1,7 +1,7 @@ import SchemaBuilder from "@pothos/core"; import type { Address } from "viem"; -import type { ChainId, InterpretedName, Node } from "@ensnode/ensnode-sdk"; +import type { ChainId, DomainId, InterpretedName, Node } from "@ensnode/ensnode-sdk"; export const builder = new SchemaBuilder<{ Scalars: { @@ -10,5 +10,6 @@ export const builder = new SchemaBuilder<{ ChainId: { Input: ChainId; Output: ChainId }; Node: { Input: Node; Output: Node }; Name: { Input: InterpretedName; Output: InterpretedName }; + DomainId: { Input: DomainId; Output: DomainId }; }; }>({}); diff --git a/apps/ensapi/src/graphql-api/schema/domain.ts b/apps/ensapi/src/graphql-api/schema/domain.ts index 01ebf0b5a..a77f68316 100644 --- a/apps/ensapi/src/graphql-api/schema/domain.ts +++ b/apps/ensapi/src/graphql-api/schema/domain.ts @@ -164,3 +164,12 @@ DomainRef.implement({ }), }), }); + +export const DomainIdInput = builder.inputType("DomainIdInput", { + description: "TODO", + isOneOf: true, + fields: (t) => ({ + name: t.field({ type: "Name" }), + id: t.field({ type: "DomainId" }), + }), +}); diff --git a/apps/ensapi/src/graphql-api/schema/query.ts b/apps/ensapi/src/graphql-api/schema/query.ts index 675e223c0..12c7c7b8d 100644 --- a/apps/ensapi/src/graphql-api/schema/query.ts +++ b/apps/ensapi/src/graphql-api/schema/query.ts @@ -6,7 +6,7 @@ import { builder } from "@/graphql-api/builder"; import type { RegistryContract } from "@/graphql-api/lib/db-types"; import { getDomainIdByInterpretedName } from "@/graphql-api/lib/get-domain-by-fqdn"; import { AccountRef } from "@/graphql-api/schema/account"; -import { DomainRef } from "@/graphql-api/schema/domain"; +import { DomainIdInput, DomainRef } from "@/graphql-api/schema/domain"; import { RegistryContractRef, RegistryIdInput, @@ -15,21 +15,24 @@ import { import { db } from "@/lib/db"; import { ROOT_REGISTRY_ID } from "@/lib/root-registry"; +// TODO: maybe should still implement query/return by id, exposing the db's primary key? +// maybe necessary for connections pattern... +// if leaning into opaque ids, then probably prefer that, and avoid exposing semantic searches? unclear + builder.queryType({ fields: (t) => ({ - ////////////////////// - // Get Domain by FQDN - ////////////////////// - name: t.field({ + ////////////////////////////////// + // Get Domain by Name or DomainId + ////////////////////////////////// + domain: t.field({ description: "TODO", type: DomainRef, - args: { - fqdn: t.arg({ type: "Name", required: true }), - }, + args: { by: t.arg({ type: DomainIdInput, required: true }) }, nullable: true, resolve: async (parent, args, ctx, info) => { - const domainId = await getDomainIdByInterpretedName(args.fqdn); - // TODO: traverse the namegraph to identify the addressed Domain + const domainId = args.by.name + ? await getDomainIdByInterpretedName(args.by.name) + : args.by.id; // TODO(dataloader): just return domainId if (!domainId) return null; @@ -40,15 +43,13 @@ builder.queryType({ }, }), - ///////////////////////////////////////// + ////////////////////////// // Get Account by address - ///////////////////////////////////////// + ////////////////////////// account: t.field({ description: "TODO", type: AccountRef, - args: { - address: t.arg({ type: "Address", required: true }), - }, + args: { address: t.arg({ type: "Address", required: true }) }, resolve: async (parent, args, ctx, info) => { // TODO(dataloader): just return address @@ -66,9 +67,7 @@ builder.queryType({ registry: t.field({ description: "TODO", type: RegistryInterfaceRef, - args: { - id: t.arg({ type: RegistryIdInput, required: true }), - }, + args: { id: t.arg({ type: RegistryIdInput, required: true }) }, resolve: async (parent, args, ctx, info) => { // TODO(dataloader): just return registryId const registryId = args.id.contract @@ -83,9 +82,9 @@ builder.queryType({ }, }), - ///////////////// + ///////////////////// // Get Root Registry - ///////////////// + ///////////////////// root: t.field({ type: RegistryContractRef, description: "TODO", diff --git a/apps/ensapi/src/graphql-api/schema/scalars.ts b/apps/ensapi/src/graphql-api/schema/scalars.ts index 893c988d5..5198d5c77 100644 --- a/apps/ensapi/src/graphql-api/schema/scalars.ts +++ b/apps/ensapi/src/graphql-api/schema/scalars.ts @@ -3,6 +3,7 @@ import { z } from "zod/v4"; import { type ChainId, + type DomainId, type InterpretedName, isInterpretedName, type Name, @@ -67,3 +68,13 @@ builder.scalarType("Name", { .transform((val) => val as InterpretedName) .parse(value), }); + +builder.scalarType("DomainId", { + description: "DomainId represents a @ensnode/ensnode-sdk#DomainId.", + serialize: (value: Name) => value, + parseValue: (value) => + z.coerce + .string() + .transform((val) => val as DomainId) + .parse(value), +}); From d4d464d6af22e1de83ec225ba903d44253b67d10 Mon Sep 17 00:00:00 2001 From: shrugs Date: Tue, 11 Nov 2025 12:28:42 -0600 Subject: [PATCH 009/102] checkpoint --- apps/ensapi/package.json | 2 + apps/ensapi/src/graphql-api/builder.ts | 16 +++- apps/ensapi/src/graphql-api/lib/db-types.ts | 19 ----- .../src/graphql-api/lib/reject-any-errors.ts | 18 +++++ apps/ensapi/src/graphql-api/schema/account.ts | 1 + apps/ensapi/src/graphql-api/schema/domain.ts | 55 ++++++++------ apps/ensapi/src/graphql-api/schema/query.ts | 49 +++--------- .../ensapi/src/graphql-api/schema/registry.ts | 74 +++++++++++-------- apps/ensapi/src/graphql-api/schema/scalars.ts | 24 +++++- pnpm-lock.yaml | 29 +++++++- 10 files changed, 172 insertions(+), 115 deletions(-) delete mode 100644 apps/ensapi/src/graphql-api/lib/db-types.ts create mode 100644 apps/ensapi/src/graphql-api/lib/reject-any-errors.ts diff --git a/apps/ensapi/package.json b/apps/ensapi/package.json index be3cc1f78..27ea0b5e5 100644 --- a/apps/ensapi/package.json +++ b/apps/ensapi/package.json @@ -40,6 +40,8 @@ "@opentelemetry/semantic-conventions": "^1.34.0", "@ponder/client": "^0.14.13", "@pothos/core": "^4.10.0", + "@pothos/plugin-dataloader": "^4.4.3", + "dataloader": "^2.2.3", "date-fns": "catalog:", "drizzle-orm": "catalog:", "graphql": "^16.11.0", diff --git a/apps/ensapi/src/graphql-api/builder.ts b/apps/ensapi/src/graphql-api/builder.ts index bf2c3d45c..e47de92a1 100644 --- a/apps/ensapi/src/graphql-api/builder.ts +++ b/apps/ensapi/src/graphql-api/builder.ts @@ -1,7 +1,15 @@ import SchemaBuilder from "@pothos/core"; +import DataloaderPlugin from "@pothos/plugin-dataloader"; import type { Address } from "viem"; -import type { ChainId, DomainId, InterpretedName, Node } from "@ensnode/ensnode-sdk"; +import type { + ChainId, + DomainId, + ImplicitRegistryId, + InterpretedName, + Node, + RegistryId, +} from "@ensnode/ensnode-sdk"; export const builder = new SchemaBuilder<{ Scalars: { @@ -11,5 +19,9 @@ export const builder = new SchemaBuilder<{ Node: { Input: Node; Output: Node }; Name: { Input: InterpretedName; Output: InterpretedName }; DomainId: { Input: DomainId; Output: DomainId }; + RegistryId: { Input: RegistryId; Output: RegistryId }; + ImplicitRegistryId: { Input: ImplicitRegistryId; Output: ImplicitRegistryId }; }; -}>({}); +}>({ + plugins: [DataloaderPlugin], +}); diff --git a/apps/ensapi/src/graphql-api/lib/db-types.ts b/apps/ensapi/src/graphql-api/lib/db-types.ts deleted file mode 100644 index 417d84fba..000000000 --- a/apps/ensapi/src/graphql-api/lib/db-types.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type * as schema from "@ensnode/ensnode-schema"; -import type { RequiredAndNotNull } from "@ensnode/ensnode-sdk"; - -////////// -// Domain -////////// - -export type Domain = typeof schema.domain.$inferSelect; - -//////////// -// Registry -//////////// - -export type Registry = typeof schema.registry.$inferSelect; - -export type RegistryInterface = Pick; -export type RegistryContract = RegistryInterface & - RequiredAndNotNull; -export type ImplicitRegistry = RegistryInterface & RequiredAndNotNull; diff --git a/apps/ensapi/src/graphql-api/lib/reject-any-errors.ts b/apps/ensapi/src/graphql-api/lib/reject-any-errors.ts new file mode 100644 index 000000000..2264ff29f --- /dev/null +++ b/apps/ensapi/src/graphql-api/lib/reject-any-errors.ts @@ -0,0 +1,18 @@ +/** + * Given a Promise<(Error | T)[]>, throws with the first Error, if any. + * + * @throws The first Error encountered in `promise`, if any. + */ +export async function rejectAnyErrors( + promise: Promise, +): Promise { + const values = await promise; + + for (const element of values) { + if (element instanceof Error) { + throw element; + } + } + + return values as readonly T[]; +} diff --git a/apps/ensapi/src/graphql-api/schema/account.ts b/apps/ensapi/src/graphql-api/schema/account.ts index 1e657c823..b784de44f 100644 --- a/apps/ensapi/src/graphql-api/schema/account.ts +++ b/apps/ensapi/src/graphql-api/schema/account.ts @@ -29,6 +29,7 @@ AccountRef.implement({ resolve: ({ address }) => db.query.domain.findMany({ where: (t, { eq }) => eq(t.ownerId, address), + with: { label: true }, }), }), }), diff --git a/apps/ensapi/src/graphql-api/schema/domain.ts b/apps/ensapi/src/graphql-api/schema/domain.ts index a77f68316..17d54603e 100644 --- a/apps/ensapi/src/graphql-api/schema/domain.ts +++ b/apps/ensapi/src/graphql-api/schema/domain.ts @@ -1,14 +1,30 @@ -import { interpretedLabelsToInterpretedName } from "@ensnode/ensnode-sdk"; +/** biome-ignore-all lint/correctness/noUnusedFunctionParameters: graphql resolve pattern */ +import { rejectErrors } from "@pothos/plugin-dataloader"; + +import { type DomainId, interpretedLabelsToInterpretedName } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; -import type { Domain } from "@/graphql-api/lib/db-types"; import { getCanonicalPath } from "@/graphql-api/lib/get-canonical-path"; -import { sortByArrayOrder } from "@/graphql-api/lib/sort-by-array-order"; +import { rejectAnyErrors } from "@/graphql-api/lib/reject-any-errors"; import { AccountRef } from "@/graphql-api/schema/account"; import { RegistryInterfaceRef } from "@/graphql-api/schema/registry"; import { db } from "@/lib/db"; -export const DomainRef = builder.objectRef("Domain"); +export const DomainRef = builder.loadableObjectRef("Domain", { + load: (ids: DomainId[]) => + db.query.domain.findMany({ + where: (t, { inArray }) => inArray(t.id, ids), + with: { label: true }, + }), + toKey: (domain) => domain.id, + cacheResolved: true, + sort: true, +}); + +export type Domain = Exclude; + +// we want to dataloader labels by labelhash +// we want to dataloader a domain's canonical path, but without exposing it DomainRef.implement({ description: "a Domain", @@ -38,14 +54,7 @@ DomainRef.implement({ type: "String", description: "TODO", nullable: false, - resolve: async ({ labelHash }) => { - const label = await db.query.label.findFirst({ - where: (t, { eq }) => eq(t.labelHash, labelHash), - }); - - if (!label) throw new Error(`Invariant: label expected`); - return label.value; - }, + resolve: async ({ label }) => label.value, }), //////////////////// @@ -55,20 +64,18 @@ DomainRef.implement({ description: "TODO", type: "Name", nullable: true, - resolve: async ({ id }) => { + resolve: async ({ id }, args, context) => { // TODO: dataloader the getCanonicalPath(domainId) function const canonicalPath = await getCanonicalPath(id); if (!canonicalPath) return null; - // TODO: use the dataloaded version of this findMany w/ labels - const domainsAndLabels = await db.query.domain.findMany({ - where: (t, { inArray }) => inArray(t.id, canonicalPath), - with: { label: true }, - }); + const domains = await rejectAnyErrors( + DomainRef.getDataloader(context).loadMany(canonicalPath), + ); return interpretedLabelsToInterpretedName( canonicalPath.map((domainId) => { - const found = domainsAndLabels.find((d) => d.id === domainId); + const found = domains.find((d) => d.id === domainId); if (!found) throw new Error(`Invariant`); return found.label.value; }), @@ -83,16 +90,16 @@ DomainRef.implement({ description: "TODO", type: [DomainRef], nullable: true, - resolve: async ({ id }) => { + resolve: async ({ id }, args, context) => { // TODO: dataloader the getCanonicalPath(domainId) function const canonicalPath = await getCanonicalPath(id); if (!canonicalPath) return null; - const domains = await db.query.domain.findMany({ - where: (t, { inArray }) => inArray(t.id, canonicalPath), - }); + const domains = await rejectErrors( + DomainRef.getDataloader(context).loadMany(canonicalPath), + ); - return domains.sort(sortByArrayOrder(canonicalPath, (domain) => domain.id)).slice(1); + return domains.slice(1); }, }), diff --git a/apps/ensapi/src/graphql-api/schema/query.ts b/apps/ensapi/src/graphql-api/schema/query.ts index 12c7c7b8d..06ad71b10 100644 --- a/apps/ensapi/src/graphql-api/schema/query.ts +++ b/apps/ensapi/src/graphql-api/schema/query.ts @@ -1,17 +1,12 @@ /** biome-ignore-all lint/correctness/noUnusedFunctionParameters: ignore unused resolve arguments */ -import { type ImplicitRegistryId, makeRegistryContractId } from "@ensnode/ensnode-sdk"; +import { makeRegistryContractId } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; -import type { RegistryContract } from "@/graphql-api/lib/db-types"; import { getDomainIdByInterpretedName } from "@/graphql-api/lib/get-domain-by-fqdn"; import { AccountRef } from "@/graphql-api/schema/account"; import { DomainIdInput, DomainRef } from "@/graphql-api/schema/domain"; -import { - RegistryContractRef, - RegistryIdInput, - RegistryInterfaceRef, -} from "@/graphql-api/schema/registry"; +import { RegistryIdInput, RegistryInterfaceRef } from "@/graphql-api/schema/registry"; import { db } from "@/lib/db"; import { ROOT_REGISTRY_ID } from "@/lib/root-registry"; @@ -30,16 +25,8 @@ builder.queryType({ args: { by: t.arg({ type: DomainIdInput, required: true }) }, nullable: true, resolve: async (parent, args, ctx, info) => { - const domainId = args.by.name - ? await getDomainIdByInterpretedName(args.by.name) - : args.by.id; - // TODO(dataloader): just return domainId - - if (!domainId) return null; - - return await db.query.domain.findFirst({ - where: (t, { eq }) => eq(t.id, domainId), - }); + if (args.by.id !== undefined) return args.by.id; + return getDomainIdByInterpretedName(args.by.name); }, }), @@ -67,18 +54,11 @@ builder.queryType({ registry: t.field({ description: "TODO", type: RegistryInterfaceRef, - args: { id: t.arg({ type: RegistryIdInput, required: true }) }, + args: { by: t.arg({ type: RegistryIdInput, required: true }) }, resolve: async (parent, args, ctx, info) => { - // TODO(dataloader): just return registryId - const registryId = args.id.contract - ? makeRegistryContractId(args.id.contract) - : (args.id.implicit.parent as ImplicitRegistryId); // TODO: move this case into scalar - - const registry = await db.query.registry.findFirst({ - where: (t, { eq }) => eq(t.id, registryId), - }); - - return registry; + if (args.by.id !== undefined) return args.by.id; + if (args.by.implicit !== undefined) return args.by.implicit.parent; + return makeRegistryContractId(args.by.contract); }, }), @@ -86,19 +66,10 @@ builder.queryType({ // Get Root Registry ///////////////////// root: t.field({ - type: RegistryContractRef, description: "TODO", + type: RegistryInterfaceRef, nullable: false, - resolve: async () => { - // TODO(dataloader): just return rootRegistry id - const rootRegistry = await db.query.registry.findFirst({ - where: (t, { eq }) => eq(t.id, ROOT_REGISTRY_ID), - }); - - if (!rootRegistry) throw new Error(`Invariant: Root Registry expected.`); - - return rootRegistry as RegistryContract; - }, + resolve: () => ROOT_REGISTRY_ID, }), }), }); diff --git a/apps/ensapi/src/graphql-api/schema/registry.ts b/apps/ensapi/src/graphql-api/schema/registry.ts index 0d4a4c157..9dd2a7aff 100644 --- a/apps/ensapi/src/graphql-api/schema/registry.ts +++ b/apps/ensapi/src/graphql-api/schema/registry.ts @@ -1,16 +1,26 @@ +import type { RegistryId, RequiredAndNotNull } from "@ensnode/ensnode-sdk"; + import { builder } from "@/graphql-api/builder"; -import type { - ImplicitRegistry, - Registry, - RegistryContract, - RegistryInterface, -} from "@/graphql-api/lib/db-types"; import { AccountIdInput, AccountIdRef } from "@/graphql-api/schema/account-id"; -import { DomainRef } from "@/graphql-api/schema/domain"; +import { type Domain, DomainRef } from "@/graphql-api/schema/domain"; import { PermissionsRef } from "@/graphql-api/schema/permissions"; import { db } from "@/lib/db"; -export const RegistryInterfaceRef = builder.interfaceRef("Registry"); +export const RegistryInterfaceRef = builder.loadableInterfaceRef("Registry", { + load: (ids: RegistryId[]) => + db.query.registry.findMany({ + where: (t, { inArray }) => inArray(t.id, ids), + }), + toKey: (registry) => registry.id, + cacheResolved: true, + sort: true, +}); + +export type Registry = Exclude; +export type RegistryInterface = Pick; +export type RegistryContract = RequiredAndNotNull; +export type ImplicitRegistry = RequiredAndNotNull; + RegistryInterfaceRef.implement({ description: "TODO", fields: (t) => ({ @@ -18,32 +28,41 @@ RegistryInterfaceRef.implement({ // Registry.id ////////////////////// id: t.field({ - type: "ID", description: "TODO", + type: "ID", nullable: false, resolve: (parent) => parent.id, }), - ////////////////////// - // Registry.domain - ////////////////////// - domain: t.field({ - type: [DomainRef], + //////////////////// + // Registry.parents + //////////////////// + parents: t.loadableGroup({ description: "TODO", - nullable: true, - resolve: (parent) => null, + type: DomainRef, + load: (ids: RegistryId[]) => + db.query.domain.findMany({ + where: (t, { inArray }) => inArray(t.subregistryId, ids), + with: { label: true }, + }), + // biome-ignore lint/style/noNonNullAssertion: subregistryId guaranteed to exist via inArray + group: (domain) => (domain as Domain).subregistryId!, + resolve: (registry) => registry.id, }), ////////////////////// // Registry.domains ////////////////////// - domains: t.field({ - type: [DomainRef], + domains: t.loadableGroup({ description: "TODO", - resolve: ({ id }) => + type: DomainRef, + load: (ids: RegistryId[]) => db.query.domain.findMany({ - where: (t, { eq }) => eq(t.registryId, id), + where: (t, { inArray }) => inArray(t.registryId, ids), + with: { label: true }, }), + group: (domain) => (domain as Domain).registryId, + resolve: (registry) => registry.id, }), }), }); @@ -58,8 +77,8 @@ RegistryContractRef.implement({ // RegistryContract.permissions //////////////////////////////// permissions: t.field({ - type: PermissionsRef, description: "TODO", + type: PermissionsRef, // TODO: render a RegistryPermissions model that parses the backing permissions into registry-semantic roles resolve: ({ chainId, address }) => null, }), @@ -68,8 +87,8 @@ RegistryContractRef.implement({ // RegistryContract.contract ///////////////////////////// contract: t.field({ - type: AccountIdRef, description: "TODO", + type: AccountIdRef, nullable: false, resolve: ({ chainId, address }) => ({ chainId, address }), }), @@ -87,11 +106,7 @@ ImplicitRegistryRef.implement({ export const ImplicitRegistryIdInput = builder.inputType("ImplicitRegistryIdInput", { description: "TODO", fields: (t) => ({ - parent: t.field({ - type: "Node", - description: "TODO", - required: true, - }), + parent: t.field({ type: "ImplicitRegistryId", required: true }), }), }); @@ -99,7 +114,8 @@ export const RegistryIdInput = builder.inputType("RegistryIdInput", { description: "TODO", isOneOf: true, fields: (t) => ({ - contract: t.field({ type: AccountIdInput, required: false }), - implicit: t.field({ type: ImplicitRegistryIdInput, required: false }), + id: t.field({ type: "RegistryId" }), + contract: t.field({ type: AccountIdInput }), + implicit: t.field({ type: ImplicitRegistryIdInput }), }), }); diff --git a/apps/ensapi/src/graphql-api/schema/scalars.ts b/apps/ensapi/src/graphql-api/schema/scalars.ts index 5198d5c77..12f36a892 100644 --- a/apps/ensapi/src/graphql-api/schema/scalars.ts +++ b/apps/ensapi/src/graphql-api/schema/scalars.ts @@ -4,10 +4,12 @@ import { z } from "zod/v4"; import { type ChainId, type DomainId, + type ImplicitRegistryId, type InterpretedName, isInterpretedName, type Name, type Node, + type RegistryId, } from "@ensnode/ensnode-sdk"; import { makeChainIdSchema, makeLowercaseAddressSchema } from "@ensnode/ensnode-sdk/internal"; @@ -71,10 +73,30 @@ builder.scalarType("Name", { builder.scalarType("DomainId", { description: "DomainId represents a @ensnode/ensnode-sdk#DomainId.", - serialize: (value: Name) => value, + serialize: (value: DomainId) => value, parseValue: (value) => z.coerce .string() .transform((val) => val as DomainId) .parse(value), }); + +builder.scalarType("RegistryId", { + description: "RegistryId represents a @ensnode/ensnode-sdk#RegistryId.", + serialize: (value: RegistryId) => value, + parseValue: (value) => + z.coerce + .string() + .transform((val) => val as RegistryId) + .parse(value), +}); + +builder.scalarType("ImplicitRegistryId", { + description: "ImplicitRegistryId represents a @ensnode/ensnode-sdk#ImplicitRegistryId.", + serialize: (value: ImplicitRegistryId) => value, + parseValue: (value) => + z.coerce + .string() + .transform((val) => val as ImplicitRegistryId) + .parse(value), +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f4680c7b..5d3676c38 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -326,6 +326,12 @@ importers: '@pothos/core': specifier: ^4.10.0 version: 4.10.0(graphql@16.11.0) + '@pothos/plugin-dataloader': + specifier: ^4.4.3 + version: 4.4.3(@pothos/core@4.10.0(graphql@16.11.0))(dataloader@2.2.3)(graphql@16.11.0) + dataloader: + specifier: ^2.2.3 + version: 2.2.3 date-fns: specifier: 'catalog:' version: 4.1.0 @@ -2440,6 +2446,13 @@ packages: peerDependencies: graphql: ^16.10.0 + '@pothos/plugin-dataloader@4.4.3': + resolution: {integrity: sha512-W80Cne0+IkoO/splip7ndrg9FRdHfaDoTPfwoZjJT7rGXb57kTGuQWDnm66O8363Z58jqdcVVfwwO2+EmkdEFw==} + peerDependencies: + '@pothos/core': '*' + dataloader: '2' + graphql: ^16.10.0 + '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -9757,6 +9770,12 @@ snapshots: dependencies: graphql: 16.11.0 + '@pothos/plugin-dataloader@4.4.3(@pothos/core@4.10.0(graphql@16.11.0))(dataloader@2.2.3)(graphql@16.11.0)': + dependencies: + '@pothos/core': 4.10.0(graphql@16.11.0) + dataloader: 2.2.3 + graphql: 16.11.0 + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -10853,6 +10872,14 @@ snapshots: chai: 6.2.0 tinyrainbow: 3.0.3 + '@vitest/mocker@4.0.5(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 4.0.5 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) + '@vitest/mocker@4.0.5(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@vitest/spy': 4.0.5 @@ -15511,7 +15538,7 @@ snapshots: vitest@4.0.5(@types/debug@4.1.12)(@types/node@20.19.24)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.6))(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1): dependencies: '@vitest/expect': 4.0.5 - '@vitest/mocker': 4.0.5(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/mocker': 4.0.5(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/pretty-format': 4.0.5 '@vitest/runner': 4.0.5 '@vitest/snapshot': 4.0.5 From 4f26dbef01fc90a7d326cb5cb5d8eda683e8f0cf Mon Sep 17 00:00:00 2001 From: shrugs Date: Tue, 11 Nov 2025 13:10:01 -0600 Subject: [PATCH 010/102] checkpoint --- apps/ensapi/src/graphql-api/builder.ts | 1 + apps/ensapi/src/graphql-api/lib/get-id.ts | 1 + apps/ensapi/src/graphql-api/schema/account.ts | 50 ++++++++++---- apps/ensapi/src/graphql-api/schema/domain.ts | 37 +++-------- .../src/graphql-api/schema/permissions.ts | 19 +++++- apps/ensapi/src/graphql-api/schema/query.ts | 11 +--- .../ensapi/src/graphql-api/schema/registry.ts | 10 +-- .../src/lib/ensv2/account-db-helpers.ts | 4 +- .../ensv2/handlers/EnhancedAccessControl.ts | 66 ++++++++++++------- .../src/plugins/ensv2/handlers/Registry.ts | 10 ++- .../src/schemas/ensv2.schema.ts | 36 ++++++---- packages/ensnode-sdk/src/ensv2/ids-lib.ts | 35 +++++++++- packages/ensnode-sdk/src/ensv2/ids.ts | 24 ++++++- 13 files changed, 195 insertions(+), 109 deletions(-) create mode 100644 apps/ensapi/src/graphql-api/lib/get-id.ts diff --git a/apps/ensapi/src/graphql-api/builder.ts b/apps/ensapi/src/graphql-api/builder.ts index e47de92a1..6f1667bab 100644 --- a/apps/ensapi/src/graphql-api/builder.ts +++ b/apps/ensapi/src/graphql-api/builder.ts @@ -21,6 +21,7 @@ export const builder = new SchemaBuilder<{ DomainId: { Input: DomainId; Output: DomainId }; RegistryId: { Input: RegistryId; Output: RegistryId }; ImplicitRegistryId: { Input: ImplicitRegistryId; Output: ImplicitRegistryId }; + // PermissionsId: { Input: PermissionsId; Output: PermissionsId }; }; }>({ plugins: [DataloaderPlugin], diff --git a/apps/ensapi/src/graphql-api/lib/get-id.ts b/apps/ensapi/src/graphql-api/lib/get-id.ts new file mode 100644 index 000000000..b983320ba --- /dev/null +++ b/apps/ensapi/src/graphql-api/lib/get-id.ts @@ -0,0 +1 @@ +export const getModelId = (model: T): ID => model.id; diff --git a/apps/ensapi/src/graphql-api/schema/account.ts b/apps/ensapi/src/graphql-api/schema/account.ts index b784de44f..800e295ce 100644 --- a/apps/ensapi/src/graphql-api/schema/account.ts +++ b/apps/ensapi/src/graphql-api/schema/account.ts @@ -1,36 +1,58 @@ -import type * as schema from "@ensnode/ensnode-schema"; +import type { Address } from "viem"; import { builder } from "@/graphql-api/builder"; -import { DomainRef } from "@/graphql-api/schema/domain"; +import { getModelId } from "@/graphql-api/lib/get-id"; +import { type Domain, DomainRef } from "@/graphql-api/schema/domain"; import { db } from "@/lib/db"; -type Account = typeof schema.account.$inferSelect; +export const AccountRef = builder.loadableObjectRef("Account", { + load: (ids: Address[]) => + db.query.account.findMany({ + where: (t, { inArray }) => inArray(t.id, ids), + }), + toKey: getModelId, + cacheResolved: true, + sort: true, +}); -export const AccountRef = builder.objectRef("Account"); +export type Account = Exclude; AccountRef.implement({ description: "TODO", fields: (t) => ({ - ////////////////////// - // Account.address - ////////////////////// - address: t.expose("address", { + /////////////////// + // Account.id + /////////////////// + id: t.expose("id", { + description: "TODO", type: "Address", + nullable: false, + }), + + /////////////////// + // Account.address + /////////////////// + address: t.field({ description: "TODO", + type: "Address", nullable: false, + resolve: (parent) => parent.id, }), - ////////////////////// + /////////////////// // Account.domains - ////////////////////// - domains: t.field({ - type: [DomainRef], + /////////////////// + domains: t.loadableGroup({ description: "TODO", - resolve: ({ address }) => + type: DomainRef, + load: (ids: Address[]) => db.query.domain.findMany({ - where: (t, { eq }) => eq(t.ownerId, address), + where: (t, { inArray }) => inArray(t.ownerId, ids), with: { label: true }, }), + // biome-ignore lint/style/noNonNullAssertion: guaranteed due to inArray + group: (domain) => (domain as Domain).ownerId!, + resolve: getModelId, }), }), }); diff --git a/apps/ensapi/src/graphql-api/schema/domain.ts b/apps/ensapi/src/graphql-api/schema/domain.ts index 17d54603e..780f92e6b 100644 --- a/apps/ensapi/src/graphql-api/schema/domain.ts +++ b/apps/ensapi/src/graphql-api/schema/domain.ts @@ -5,6 +5,7 @@ import { type DomainId, interpretedLabelsToInterpretedName } from "@ensnode/ensn import { builder } from "@/graphql-api/builder"; import { getCanonicalPath } from "@/graphql-api/lib/get-canonical-path"; +import { getModelId } from "@/graphql-api/lib/get-id"; import { rejectAnyErrors } from "@/graphql-api/lib/reject-any-errors"; import { AccountRef } from "@/graphql-api/schema/account"; import { RegistryInterfaceRef } from "@/graphql-api/schema/registry"; @@ -16,7 +17,7 @@ export const DomainRef = builder.loadableObjectRef("Domain", { where: (t, { inArray }) => inArray(t.id, ids), with: { label: true }, }), - toKey: (domain) => domain.id, + toKey: getModelId, cacheResolved: true, sort: true, }); @@ -124,33 +125,18 @@ DomainRef.implement({ owner: t.field({ type: AccountRef, description: "TODO", - nullable: false, - resolve: async ({ ownerId }) => { - // TODO(dataloader): just id - if (ownerId === null) throw new Error(`Invariant: ownerId null`); - const owner = await db.query.account.findFirst({ - where: (t, { eq }) => eq(t.address, ownerId), - }); - - if (!owner) throw new Error(`Invariant: owner expected`); - return owner; - }, + nullable: true, + resolve: (parent) => parent.ownerId, }), ////////////////////// // Domain.registry ////////////////////// registry: t.field({ - type: RegistryInterfaceRef, description: "TODO", + type: RegistryInterfaceRef, nullable: false, - resolve: async ({ registryId }, args, ctx, info) => { - const registry = await db.query.registry.findFirst({ - where: (t, { eq }) => eq(t.id, registryId), - }); - if (!registry) throw new Error(`Invariant: Domain does not have parent Registry (???)`); - return registry; - }, + resolve: (parent) => parent.registryId, }), ////////////////////// @@ -159,15 +145,8 @@ DomainRef.implement({ subregistry: t.field({ type: RegistryInterfaceRef, description: "TODO", - resolve: async ({ subregistryId }, args, ctx, info) => { - if (subregistryId === null) return null; - - const subregistry = await db.query.registry.findFirst({ - where: (t, { eq }) => eq(t.id, subregistryId), - }); - - return subregistry ?? null; - }, + nullable: true, + resolve: (parent) => parent.subregistryId, }), }), }); diff --git a/apps/ensapi/src/graphql-api/schema/permissions.ts b/apps/ensapi/src/graphql-api/schema/permissions.ts index b1fc1f4b4..2aa5298e7 100644 --- a/apps/ensapi/src/graphql-api/schema/permissions.ts +++ b/apps/ensapi/src/graphql-api/schema/permissions.ts @@ -1,11 +1,22 @@ -import type * as schema from "@ensnode/ensnode-schema"; +import type { PermissionsId } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; +import { getModelId } from "@/graphql-api/lib/get-id"; import { AccountIdRef } from "@/graphql-api/schema/account-id"; +import { db } from "@/lib/db"; -type Permissions = typeof schema.permissions.$inferSelect; +export const PermissionsRef = builder.loadableObjectRef("Permissions", { + load: (ids: PermissionsId[]) => + db.query.permissions.findMany({ + where: (t, { inArray }) => inArray(t.id, ids), + }), + toKey: getModelId, + cacheResolved: true, + sort: true, +}); + +export type Permissions = Exclude; -export const PermissionsRef = builder.objectRef("Permissions"); PermissionsRef.implement({ description: "Permissions", fields: (t) => ({ @@ -15,5 +26,7 @@ PermissionsRef.implement({ nullable: false, resolve: ({ chainId, address }) => ({ chainId, address }), }), + + // resources... }), }); diff --git a/apps/ensapi/src/graphql-api/schema/query.ts b/apps/ensapi/src/graphql-api/schema/query.ts index 06ad71b10..1516c29cb 100644 --- a/apps/ensapi/src/graphql-api/schema/query.ts +++ b/apps/ensapi/src/graphql-api/schema/query.ts @@ -7,7 +7,6 @@ import { getDomainIdByInterpretedName } from "@/graphql-api/lib/get-domain-by-fq import { AccountRef } from "@/graphql-api/schema/account"; import { DomainIdInput, DomainRef } from "@/graphql-api/schema/domain"; import { RegistryIdInput, RegistryInterfaceRef } from "@/graphql-api/schema/registry"; -import { db } from "@/lib/db"; import { ROOT_REGISTRY_ID } from "@/lib/root-registry"; // TODO: maybe should still implement query/return by id, exposing the db's primary key? @@ -37,15 +36,7 @@ builder.queryType({ description: "TODO", type: AccountRef, args: { address: t.arg({ type: "Address", required: true }) }, - resolve: async (parent, args, ctx, info) => { - // TODO(dataloader): just return address - - const account = await db.query.account.findFirst({ - where: (t, { eq }) => eq(t.address, args.address), - }); - - return account; - }, + resolve: async (parent, args, ctx, info) => args.address, }), ////////////////////// diff --git a/apps/ensapi/src/graphql-api/schema/registry.ts b/apps/ensapi/src/graphql-api/schema/registry.ts index 9dd2a7aff..afe686eef 100644 --- a/apps/ensapi/src/graphql-api/schema/registry.ts +++ b/apps/ensapi/src/graphql-api/schema/registry.ts @@ -1,6 +1,7 @@ import type { RegistryId, RequiredAndNotNull } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; +import { getModelId } from "@/graphql-api/lib/get-id"; import { AccountIdInput, AccountIdRef } from "@/graphql-api/schema/account-id"; import { type Domain, DomainRef } from "@/graphql-api/schema/domain"; import { PermissionsRef } from "@/graphql-api/schema/permissions"; @@ -11,7 +12,7 @@ export const RegistryInterfaceRef = builder.loadableInterfaceRef("Registry", { db.query.registry.findMany({ where: (t, { inArray }) => inArray(t.id, ids), }), - toKey: (registry) => registry.id, + toKey: getModelId, cacheResolved: true, sort: true, }); @@ -27,11 +28,10 @@ RegistryInterfaceRef.implement({ ////////////////////// // Registry.id ////////////////////// - id: t.field({ + id: t.expose("id", { description: "TODO", type: "ID", nullable: false, - resolve: (parent) => parent.id, }), //////////////////// @@ -47,7 +47,7 @@ RegistryInterfaceRef.implement({ }), // biome-ignore lint/style/noNonNullAssertion: subregistryId guaranteed to exist via inArray group: (domain) => (domain as Domain).subregistryId!, - resolve: (registry) => registry.id, + resolve: getModelId, }), ////////////////////// @@ -62,7 +62,7 @@ RegistryInterfaceRef.implement({ with: { label: true }, }), group: (domain) => (domain as Domain).registryId, - resolve: (registry) => registry.id, + resolve: getModelId, }), }), }); diff --git a/apps/ensindexer/src/lib/ensv2/account-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/account-db-helpers.ts index 83421e261..887b1a5e2 100644 --- a/apps/ensindexer/src/lib/ensv2/account-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/account-db-helpers.ts @@ -2,6 +2,6 @@ import type { Context } from "ponder:registry"; import schema from "ponder:schema"; import type { Address } from "viem"; -export async function ensureAccount(context: Context, address: Address) { - await context.db.insert(schema.account).values({ address }).onConflictDoNothing(); +export async function ensureAccount(context: Context, id: Address) { + await context.db.insert(schema.account).values({ id }).onConflictDoNothing(); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/EnhancedAccessControl.ts b/apps/ensindexer/src/plugins/ensv2/handlers/EnhancedAccessControl.ts index 0f10657cf..8a05be4ce 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/EnhancedAccessControl.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/EnhancedAccessControl.ts @@ -2,44 +2,68 @@ import { type Context, ponder } from "ponder:registry"; import schema from "ponder:schema"; import type { Address } from "viem"; -import { PluginName } from "@ensnode/ensnode-sdk"; +import { + makePermissionsId, + makePermissionsResourceId, + makePermissionsUserId, + PluginName, +} from "@ensnode/ensnode-sdk"; import { getThisAccountId } from "@/lib/get-this-account-id"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; /** - * Infer the type of the Permission entity's composite primary key. + * Infer the type of the Permission entity's composite key. */ -type PermissionsId = Pick; +type PermissionsCompositeKey = Pick; /** - * Infer the type of the PermissionsUsers entity's composite primary key. + * Infer the type of the PermissionsUsers entity's composite key. */ -type PermissionsUsersId = Pick< +type PermissionsUsersCompositeKey = Pick< typeof schema.permissionsUser.$inferInsert, "chainId" | "address" | "resource" | "user" >; -const ensurePermissionsResource = async (context: Context, id: PermissionsId, resource: bigint) => { - await context.db.insert(schema.permissions).values(id).onConflictDoNothing(); +const ensurePermissionsResource = async ( + context: Context, + contract: PermissionsCompositeKey, + resource: bigint, +) => { + const permissionsId = makePermissionsId(contract); + const permissionsResourceId = makePermissionsResourceId(contract, resource); + + // ensure permissions + await context.db + .insert(schema.permissions) + .values({ id: permissionsId, ...contract }) + .onConflictDoNothing(); + + // ensure permissions resource await context.db .insert(schema.permissionsResource) - .values({ ...id, resource }) + .values({ id: permissionsResourceId, ...contract, resource }) .onConflictDoNothing(); }; const isZeroRoles = (roles: bigint) => roles === 0n; -async function upsertNewRoles(context: Context, id: PermissionsUsersId, roles: bigint) { +async function upsertNewRoles(context: Context, key: PermissionsUsersCompositeKey, roles: bigint) { + const permissionsUserId = makePermissionsUserId( + { chainId: key.chainId, address: key.address }, + key.resource, + key.user, + ); + if (isZeroRoles(roles)) { // ensure deleted - await context.db.delete(schema.permissionsUser, id); + await context.db.delete(schema.permissionsUser, { id: permissionsUserId }); } else { // ensure upserted await context.db .insert(schema.permissionsUser) - .values({ ...id, roles }) + .values({ id: permissionsUserId, ...key, roles }) .onConflictDoUpdate({ roles }); } } @@ -61,14 +85,14 @@ export default function () { const { resource, roleBitmap: roles, account: user } = event.args; const accountId = getThisAccountId(context, event); - await ensurePermissionsResource(context, accountId, resource); + const permissionsUserId = makePermissionsUserId(accountId, resource, user); - const permissionsUserId: PermissionsUsersId = { ...accountId, resource, user }; - const existing = await context.db.find(schema.permissionsUser, permissionsUserId); + await ensurePermissionsResource(context, accountId, resource); + const existing = await context.db.find(schema.permissionsUser, { id: permissionsUserId }); // https://github.com/ensdomains/namechain/blob/main/contracts/src/common/access-control/EnhancedAccessControl.sol#L292 const newRoles = (existing?.roles ?? 0n) | roles; - await upsertNewRoles(context, permissionsUserId, newRoles); + await upsertNewRoles(context, { ...accountId, resource, user }, newRoles); }, ); @@ -88,14 +112,14 @@ export default function () { const { resource, roleBitmap: roles, account: user } = event.args; const accountId = getThisAccountId(context, event); - await ensurePermissionsResource(context, accountId, resource); + const permissionsUserId = makePermissionsUserId(accountId, resource, user); - const permissionsUserId: PermissionsUsersId = { ...accountId, resource, user }; - const existing = await context.db.find(schema.permissionsUser, permissionsUserId); + await ensurePermissionsResource(context, accountId, resource); + const existing = await context.db.find(schema.permissionsUser, { id: permissionsUserId }); // https://github.com/ensdomains/namechain/blob/main/contracts/src/common/access-control/EnhancedAccessControl.sol#L325 const newRoles = (existing?.roles ?? 0n) & ~roles; - await upsertNewRoles(context, permissionsUserId, newRoles); + await upsertNewRoles(context, { ...accountId, resource, user }, newRoles); }, ); @@ -116,9 +140,7 @@ export default function () { const accountId = getThisAccountId(context, event); await ensurePermissionsResource(context, accountId, resource); - const permissionsUserId: PermissionsUsersId = { ...accountId, resource, user }; - - await upsertNewRoles(context, permissionsUserId, 0n); + await upsertNewRoles(context, { ...accountId, resource, user }, 0n); }, ); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts index 8e631fd3e..8b8b65da1 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts @@ -9,6 +9,7 @@ import { type LiteralLabel, makeENSv2DomainId, makeRegistryContractId, + makeResolverId, PluginName, } from "@ensnode/ensnode-sdk"; @@ -136,13 +137,10 @@ export default function () { // update domain's resolver const isDeletion = isAddressEqual(resolver, zeroAddress); if (isDeletion) { - await context.db - .update(schema.domain, { id: domainId }) - .set({ resolverChainId: null, resolverAddress: null }); + await context.db.update(schema.domain, { id: domainId }).set({ resolverId: null }); } else { - await context.db - .update(schema.domain, { id: domainId }) - .set({ resolverChainId: context.chain.id, resolverAddress: resolver }); + const resolverId = makeResolverId({ chainId: context.chain.id, address: resolver }); + await context.db.update(schema.domain, { id: domainId }).set({ resolverId }); } }, ); diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/schemas/ensv2.schema.ts index ffec8fe5e..06405df5d 100644 --- a/packages/ensnode-schema/src/schemas/ensv2.schema.ts +++ b/packages/ensnode-schema/src/schemas/ensv2.schema.ts @@ -1,4 +1,4 @@ -import { onchainEnum, onchainTable, primaryKey, relations } from "ponder"; +import { onchainEnum, onchainTable, relations, uniqueIndex } from "ponder"; import type { Address } from "viem"; import type { @@ -8,7 +8,11 @@ import type { InterpretedLabel, LabelHash, Node, + PermissionsId, + PermissionsResourceId, + PermissionsUserId, RegistryId, + ResolverId, } from "@ensnode/ensnode-sdk"; // Registry<->Domain is 1:1 @@ -28,7 +32,7 @@ import type { /////////// export const account = onchainTable("accounts", (t) => ({ - address: t.hex().primaryKey().$type
(), + id: t.hex().primaryKey().$type
(), })); export const account_relations = relations(account, ({ many }) => ({ @@ -84,21 +88,21 @@ export const domain = onchainTable( // see DomainId for guarantees id: t.text().primaryKey().$type(), - // belongs to registry by (registryId) + // belongs to registry registryId: t.text().notNull().$type(), // TODO: we could probably avoid storing this at all and compute it on-demand canonicalId: t.bigint().notNull().$type(), labelHash: t.hex().notNull().$type(), + // may have an owner ownerId: t.hex().$type
(), - // may have one subregistry by (id) + // may have one subregistry subregistryId: t.text().$type(), - // may have one resolver by (chainId, address) - resolverChainId: t.integer().$type(), - resolverAddress: t.hex().$type
(), + // may have one resolver + resolverId: t.text().$type(), }), (t) => ({ // @@ -109,7 +113,7 @@ export const relations_domain = relations(domain, ({ one }) => ({ owner: one(account, { relationName: "owner", fields: [domain.ownerId], - references: [account.address], + references: [account.id], }), registry: one(registry, { relationName: "registry", @@ -141,11 +145,13 @@ export const relations_domain = relations(domain, ({ one }) => ({ export const permissions = onchainTable( "permissions", (t) => ({ + id: t.text().primaryKey().$type(), + chainId: t.integer().notNull().$type(), address: t.hex().notNull().$type
(), }), (t) => ({ - pk: primaryKey({ columns: [t.chainId, t.address] }), + byId: uniqueIndex().on(t.chainId, t.address), }), ); @@ -157,12 +163,14 @@ export const relations_permissions = relations(permissions, ({ one, many }) => ( export const permissionsResource = onchainTable( "permissions_resources", (t) => ({ + id: t.text().primaryKey().$type(), + chainId: t.integer().notNull().$type(), address: t.hex().notNull().$type
(), resource: t.bigint().notNull(), }), (t) => ({ - pk: primaryKey({ columns: [t.chainId, t.address, t.resource] }), + byId: uniqueIndex().on(t.chainId, t.address, t.resource), }), ); @@ -176,25 +184,25 @@ export const relations_permissionsResource = relations(permissionsResource, ({ o export const permissionsUser = onchainTable( "permissions_users", (t) => ({ + id: t.text().primaryKey().$type(), + chainId: t.integer().notNull().$type(), address: t.hex().notNull().$type
(), resource: t.bigint().notNull(), user: t.hex().notNull().$type
(), // has one roles bitmap - // TODO: can materialize into more semantic (polymorphic) interpretation of roles based on source - // contract, but not now roles: t.bigint().notNull(), }), (t) => ({ - pk: primaryKey({ columns: [t.chainId, t.address, t.resource, t.user] }), + byId: uniqueIndex().on(t.chainId, t.address, t.resource, t.user), }), ); export const relations_permissionsUser = relations(permissionsUser, ({ one, many }) => ({ account: one(account, { fields: [permissionsUser.user], - references: [account.address], + references: [account.id], }), permissions: one(permissions, { fields: [permissionsUser.chainId, permissionsUser.address], diff --git a/packages/ensnode-sdk/src/ensv2/ids-lib.ts b/packages/ensnode-sdk/src/ensv2/ids-lib.ts index cacece6e9..0a4ad4b53 100644 --- a/packages/ensnode-sdk/src/ensv2/ids-lib.ts +++ b/packages/ensnode-sdk/src/ensv2/ids-lib.ts @@ -1,4 +1,4 @@ -import { hexToBigInt } from "viem"; +import { type Address, hexToBigInt } from "viem"; import { type AccountId, @@ -7,7 +7,15 @@ import { serializeAssetId, } from "@ensnode/ensnode-sdk"; -import type { CanonicalId, ENSv2DomainId, RegistryContractId } from "./ids"; +import type { + CanonicalId, + ENSv2DomainId, + PermissionsId, + PermissionsResourceId, + PermissionsUserId, + RegistryContractId, + ResolverId, +} from "./ids"; /** * Serializes and brands an AccountId as a RegistryId. @@ -36,3 +44,26 @@ export const getCanonicalId = (input: bigint | LabelHash): CanonicalId => { if (typeof input === "bigint") return maskLower32Bits(input); return getCanonicalId(hexToBigInt(input)); }; + +/** + * Serializes and brands an AccountId as a PermissionsId. + */ +export const makePermissionsId = (contract: AccountId) => + serializeAccountId(contract) as PermissionsId; + +/** + * + */ +export const makePermissionsResourceId = (contract: AccountId, resource: bigint) => + `${serializeAccountId(contract)}/${resource}` as PermissionsResourceId; + +/** + * + */ +export const makePermissionsUserId = (contract: AccountId, resource: bigint, user: Address) => + `${serializeAccountId(contract)}/${resource}/${user}` as PermissionsUserId; + +/** + * Serializes and brands an AccountId as a ResolverId. + */ +export const makeResolverId = (contract: AccountId) => serializeAccountId(contract) as ResolverId; diff --git a/packages/ensnode-sdk/src/ensv2/ids.ts b/packages/ensnode-sdk/src/ensv2/ids.ts index e99141975..a40820345 100644 --- a/packages/ensnode-sdk/src/ensv2/ids.ts +++ b/packages/ensnode-sdk/src/ensv2/ids.ts @@ -1,6 +1,6 @@ -import type { Hex } from "viem"; +import type { Address, Hex } from "viem"; -import type { Node } from "@ensnode/ensnode-sdk"; +import type { Node, SerializedAccountId } from "@ensnode/ensnode-sdk"; /** * Serialized CAIP-10 Asset ID that uniquely identifies a Registry contract. @@ -36,3 +36,23 @@ export type ENSv2DomainId = string & { __brand: "ENSv2DomainId" }; * A DomainId is one of ENSv1DomainId or ENSv2DomainId. */ export type DomainId = ENSv1DomainId | ENSv2DomainId; + +/** + * + */ +export type PermissionsId = SerializedAccountId & { __brand: "PermissionsId" }; + +/** + * + */ +export type PermissionsResourceId = string & { __brand: "PermissionsResourceId" }; + +/** + * + */ +export type PermissionsUserId = string & { __brand: "PermissionsUserId" }; + +/** + * + */ +export type ResolverId = SerializedAccountId & { __brand: "ResolverId" }; From 8a0aa48e76d25e9af9d78e0a88618ba4cab973d5 Mon Sep 17 00:00:00 2001 From: shrugs Date: Tue, 11 Nov 2025 14:01:32 -0600 Subject: [PATCH 011/102] checkpoint --- apps/ensapi/src/graphql-api/builder.ts | 4 + apps/ensapi/src/graphql-api/schema/domain.ts | 11 ++ .../ensapi/src/graphql-api/schema/resolver.ts | 126 ++++++++++++++++++ apps/ensapi/src/graphql-api/schema/scalars.ts | 24 +++- .../get-primary-name-from-index.ts | 14 +- .../get-records-from-index.ts | 4 +- .../resolution/make-records-response.test.ts | 4 +- .../lib/resolution/make-records-response.ts | 4 +- .../resolver-records-db-helpers.ts | 73 +++++++--- .../handlers/Resolver.ts | 62 +++++---- .../handlers/StandaloneReverseRegistrar.ts | 2 +- .../schemas/protocol-acceleration.schema.ts | 67 ++++++---- packages/ensnode-sdk/src/ensv2/ids-lib.ts | 12 +- packages/ensnode-sdk/src/ensv2/ids.ts | 5 + 14 files changed, 316 insertions(+), 96 deletions(-) create mode 100644 apps/ensapi/src/graphql-api/schema/resolver.ts diff --git a/apps/ensapi/src/graphql-api/builder.ts b/apps/ensapi/src/graphql-api/builder.ts index 6f1667bab..62637226d 100644 --- a/apps/ensapi/src/graphql-api/builder.ts +++ b/apps/ensapi/src/graphql-api/builder.ts @@ -4,11 +4,13 @@ import type { Address } from "viem"; import type { ChainId, + CoinType, DomainId, ImplicitRegistryId, InterpretedName, Node, RegistryId, + ResolverId, } from "@ensnode/ensnode-sdk"; export const builder = new SchemaBuilder<{ @@ -16,11 +18,13 @@ export const builder = new SchemaBuilder<{ BigInt: { Input: bigint; Output: bigint }; Address: { Input: Address; Output: Address }; ChainId: { Input: ChainId; Output: ChainId }; + CoinType: { Input: CoinType; Output: CoinType }; Node: { Input: Node; Output: Node }; Name: { Input: InterpretedName; Output: InterpretedName }; DomainId: { Input: DomainId; Output: DomainId }; RegistryId: { Input: RegistryId; Output: RegistryId }; ImplicitRegistryId: { Input: ImplicitRegistryId; Output: ImplicitRegistryId }; + ResolverId: { Input: ResolverId; Output: ResolverId }; // PermissionsId: { Input: PermissionsId; Output: PermissionsId }; }; }>({ diff --git a/apps/ensapi/src/graphql-api/schema/domain.ts b/apps/ensapi/src/graphql-api/schema/domain.ts index 780f92e6b..71c28f0e9 100644 --- a/apps/ensapi/src/graphql-api/schema/domain.ts +++ b/apps/ensapi/src/graphql-api/schema/domain.ts @@ -9,6 +9,7 @@ import { getModelId } from "@/graphql-api/lib/get-id"; import { rejectAnyErrors } from "@/graphql-api/lib/reject-any-errors"; import { AccountRef } from "@/graphql-api/schema/account"; import { RegistryInterfaceRef } from "@/graphql-api/schema/registry"; +import { ResolverRef } from "@/graphql-api/schema/resolver"; import { db } from "@/lib/db"; export const DomainRef = builder.loadableObjectRef("Domain", { @@ -148,6 +149,16 @@ DomainRef.implement({ nullable: true, resolve: (parent) => parent.subregistryId, }), + + ////////////////////// + // Domain.resolver + ////////////////////// + resolver: t.field({ + description: "TODO", + type: ResolverRef, + nullable: true, + resolve: (parent) => parent.resolverId, + }), }), }); diff --git a/apps/ensapi/src/graphql-api/schema/resolver.ts b/apps/ensapi/src/graphql-api/schema/resolver.ts new file mode 100644 index 000000000..719aa6df2 --- /dev/null +++ b/apps/ensapi/src/graphql-api/schema/resolver.ts @@ -0,0 +1,126 @@ +/** biome-ignore-all lint/correctness/noUnusedFunctionParameters: graphql resolve pattern */ + +import { + makeResolverRecordsId, + type ResolverId, + type ResolverRecordsId, +} from "@ensnode/ensnode-sdk"; + +import { builder } from "@/graphql-api/builder"; +import { getModelId } from "@/graphql-api/lib/get-id"; +import { AccountIdInput, AccountIdRef } from "@/graphql-api/schema/account-id"; +import { db } from "@/lib/db"; + +export const ResolverRef = builder.loadableObjectRef("Resolver", { + load: (ids: ResolverId[]) => + db.query.resolver.findMany({ + where: (t, { inArray }) => inArray(t.id, ids), + }), + toKey: getModelId, + cacheResolved: true, + sort: true, +}); + +export type Resolver = Exclude; + +export const ResolverRecordsRef = builder.loadableObjectRef("ResolverRecords", { + load: (ids: ResolverRecordsId[]) => + db.query.resolverRecords.findMany({ + where: (t, { inArray }) => inArray(t.id, ids), + with: { textRecords: true, addressRecords: true }, + }), + toKey: getModelId, + cacheResolved: true, + sort: true, +}); + +ResolverRef.implement({ + description: "A Resolver Contract", + fields: (t) => ({ + /////////////// + // Resolver.id + /////////////// + id: t.expose("id", { + type: "ID", + description: "TODO", + nullable: false, + }), + + ///////////////////// + // Resolver.contract + ///////////////////// + contract: t.field({ + description: "TODO", + type: AccountIdRef, + nullable: false, + resolve: ({ chainId, address }) => ({ chainId, address }), + }), + + //////////////////////////// + // Resolver.records by Node + //////////////////////////// + // TODO: make node optional and allow connection to all records + records: t.field({ + description: "TODO", + type: ResolverRecordsRef, + args: { node: t.arg({ type: "Node", required: true }) }, + nullable: false, + resolve: async ({ chainId, address }, { node }) => + makeResolverRecordsId({ chainId, address }, node), + }), + }), +}); + +export type ResolverRecords = Exclude; + +ResolverRecordsRef.implement({ + description: "TODO", + fields: (t) => ({ + ////////////////////// + // ResolverRecords.id + ////////////////////// + id: t.expose("id", { + description: "TODO", + type: "ID", + nullable: false, + }), + + //////////////////////// + // ResolverRecords.name + //////////////////////// + name: t.expose("name", { + description: "TODO", + type: "String", + nullable: true, + }), + + //////////////////////// + // ResolverRecords.keys + //////////////////////// + keys: t.field({ + description: "TODO", + type: ["String"], + nullable: false, + resolve: (parent) => parent.textRecords.map((r) => r.key).toSorted(), + }), + + ///////////////////////////// + // ResolverRecords.coinTypes + ///////////////////////////// + coinTypes: t.field({ + description: "TODO", + type: ["CoinType"], + nullable: false, + resolve: (parent) => parent.addressRecords.map((r) => r.coinType).toSorted(), + }), + }), +}); + +export const ResolverIdInput = builder.inputType("ResolverIdInput", { + description: "TODO", + isOneOf: true, + fields: (t) => ({ + id: t.field({ type: "ResolverId" }), + contract: t.field({ type: AccountIdInput }), + }), +}); diff --git a/apps/ensapi/src/graphql-api/schema/scalars.ts b/apps/ensapi/src/graphql-api/schema/scalars.ts index 12f36a892..3fab4e65c 100644 --- a/apps/ensapi/src/graphql-api/schema/scalars.ts +++ b/apps/ensapi/src/graphql-api/schema/scalars.ts @@ -3,6 +3,7 @@ import { z } from "zod/v4"; import { type ChainId, + type CoinType, type DomainId, type ImplicitRegistryId, type InterpretedName, @@ -10,8 +11,13 @@ import { type Name, type Node, type RegistryId, + type ResolverId, } from "@ensnode/ensnode-sdk"; -import { makeChainIdSchema, makeLowercaseAddressSchema } from "@ensnode/ensnode-sdk/internal"; +import { + makeChainIdSchema, + makeCoinTypeSchema, + makeLowercaseAddressSchema, +} from "@ensnode/ensnode-sdk/internal"; import { builder } from "@/graphql-api/builder"; @@ -33,6 +39,12 @@ builder.scalarType("ChainId", { parseValue: (value) => makeChainIdSchema("ChainId").parse(value), }); +builder.scalarType("CoinType", { + description: "CoinType represents a @ensnode/ensnode-sdk#CoinType.", + serialize: (value: CoinType) => value, + parseValue: (value) => makeCoinTypeSchema("CoinType").parse(value), +}); + builder.scalarType("Node", { description: "Node represents a @ensnode/ensnode-sdk#Node.", serialize: (value: Node) => value, @@ -100,3 +112,13 @@ builder.scalarType("ImplicitRegistryId", { .transform((val) => val as ImplicitRegistryId) .parse(value), }); + +builder.scalarType("ResolverId", { + description: "ResolverId represents a @ensnode/ensnode-sdk#ResolverId.", + serialize: (value: ResolverId) => value, + parseValue: (value) => + z.coerce + .string() + .transform((val) => val as ResolverId) + .parse(value), +}); diff --git a/apps/ensapi/src/lib/protocol-acceleration/get-primary-name-from-index.ts b/apps/ensapi/src/lib/protocol-acceleration/get-primary-name-from-index.ts index cce3abc10..e38002e68 100644 --- a/apps/ensapi/src/lib/protocol-acceleration/get-primary-name-from-index.ts +++ b/apps/ensapi/src/lib/protocol-acceleration/get-primary-name-from-index.ts @@ -13,14 +13,10 @@ import { withSpanAsync } from "@/lib/tracing/auto-span"; const tracer = trace.getTracer("get-primary-name"); -const DEFAULT_EVM_COIN_TYPE_BIGINT = BigInt(DEFAULT_EVM_COIN_TYPE); - export async function getENSIP19ReverseNameRecordFromIndex( address: Address, coinType: CoinType, ): Promise { - const _coinType = BigInt(coinType); - // retrieve from index const records = await withSpanAsync( tracer, @@ -32,17 +28,15 @@ export async function getENSIP19ReverseNameRecordFromIndex( and( // address = address eq(t.address, address), - // AND coinType IN [_coinType, DEFAULT_EVM_COIN_TYPE] - inArray(t.coinType, [_coinType, DEFAULT_EVM_COIN_TYPE_BIGINT]), + // AND coinType IN [coinType, DEFAULT_EVM_COIN_TYPE] + inArray(t.coinType, [coinType, DEFAULT_EVM_COIN_TYPE]), ), columns: { coinType: true, value: true }, }), ); - const coinTypeName = records.find((pn) => pn.coinType === _coinType)?.value ?? null; - - const defaultName = - records.find((pn) => pn.coinType === DEFAULT_EVM_COIN_TYPE_BIGINT)?.value ?? null; + const coinTypeName = records.find((pn) => pn.coinType === coinType)?.value ?? null; + const defaultName = records.find((pn) => pn.coinType === DEFAULT_EVM_COIN_TYPE)?.value ?? null; return coinTypeName ?? defaultName; } diff --git a/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts b/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts index eecd2b8ab..15ef30cb6 100644 --- a/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts +++ b/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts @@ -34,7 +34,7 @@ export async function getRecordsFromIndex and( eq(resolver.chainId, chainId), - eq(resolver.resolver, resolverAddress), + eq(resolver.address, resolverAddress), eq(resolver.node, node), ), columns: { name: true }, @@ -61,7 +61,7 @@ export async function getRecordsFromIndex { const mockRecords: IndexedResolverRecords = { name: "test.eth", addressRecords: [ - { coinType: 60n, address: "0x123" }, - { coinType: 1001n, address: "0x456" }, + { coinType: 60n, value: "0x123" }, + { coinType: 1001n, value: "0x456" }, ], textRecords: [ { key: "com.twitter", value: "@test" }, diff --git a/apps/ensapi/src/lib/resolution/make-records-response.ts b/apps/ensapi/src/lib/resolution/make-records-response.ts index 1f8d62cf4..ff77bf387 100644 --- a/apps/ensapi/src/lib/resolution/make-records-response.ts +++ b/apps/ensapi/src/lib/resolution/make-records-response.ts @@ -9,7 +9,7 @@ import type { ResolveCallsAndResults } from "./resolve-calls-and-results"; export interface IndexedResolverRecords { name: string | null; - addressRecords: { coinType: bigint; address: string }[]; + addressRecords: { coinType: bigint; value: string }[]; textRecords: { key: string; value: string }[]; } @@ -34,7 +34,7 @@ export function makeRecordsResponseFromIndexedRecords { memo[coinType] = - records.addressRecords.find((r) => bigintToCoinType(r.coinType) === coinType)?.address || + records.addressRecords.find((r) => bigintToCoinType(r.coinType) === coinType)?.value || null; return memo; }, diff --git a/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts b/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts index d5e68a5e7..cbac6f0da 100644 --- a/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts +++ b/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts @@ -2,7 +2,12 @@ import type { Context } from "ponder:registry"; import schema from "ponder:schema"; import type { Address } from "viem"; -import type { Node } from "@ensnode/ensnode-sdk"; +import { + type CoinType, + makeResolverId, + makeResolverRecordsId, + type Node, +} from "@ensnode/ensnode-sdk"; import { interpretAddressRecordValue, interpretNameRecordValue, @@ -13,25 +18,30 @@ import { import type { EventWithArgs } from "@/lib/ponder-helpers"; /** - * Infer the type of the ResolverRecord entity's composite primary key. + * Infer the type of the Resolver entity's composite key. + */ +type ResolverCompositeKey = Pick; + +/** + * Infer the type of the ResolverRecord entity's composite key. */ -type ResolverRecordsId = Pick< +type ResolverRecordsCompositeKey = Pick< typeof schema.resolverRecords.$inferInsert, - "chainId" | "resolver" | "node" + "chainId" | "address" | "node" >; /** - * Constructs a ResolverRecordsId from a provided Resolver event. + * Constructs a ResolverRecordsCompositeKey from a provided Resolver event. * - * @returns ResolverRecordsId + * @returns ResolverRecordsCompositeKey */ -export function makeResolverRecordsId( +export function makeResolverRecordsCompositeKey( context: Context, event: EventWithArgs<{ node: Node }>, -): ResolverRecordsId { +): ResolverRecordsCompositeKey { return { chainId: context.chain.id, - resolver: event.log.address, + address: event.log.address, node: event.args.node, }; } @@ -39,15 +49,31 @@ export function makeResolverRecordsId( /** * Ensures that the Resolver and ResolverRecords entities described by `id` exists. */ -export async function ensureResolverAndResolverRecords(context: Context, id: ResolverRecordsId) { +export async function ensureResolverAndResolverRecords( + context: Context, + resolverRecordsKey: ResolverRecordsCompositeKey, +) { + const resolverKey: ResolverCompositeKey = { + chainId: resolverRecordsKey.chainId, + address: resolverRecordsKey.address, + }; + const resolverId = makeResolverId(resolverKey); + const resolverRecordsId = makeResolverRecordsId(resolverKey, resolverRecordsKey.node); + // ensure Resolver await context.db .insert(schema.resolver) - .values({ chainId: id.chainId, address: id.resolver }) + .values({ id: resolverId, ...resolverKey }) .onConflictDoNothing(); // ensure ResolverRecords - await context.db.insert(schema.resolverRecords).values(id).onConflictDoNothing(); + await context.db + .insert(schema.resolverRecords) + .values({ + id: resolverRecordsId, + ...resolverRecordsKey, + }) + .onConflictDoNothing(); } /** @@ -55,10 +81,17 @@ export async function ensureResolverAndResolverRecords(context: Context, id: Res */ export async function handleResolverNameUpdate( context: Context, - id: ResolverRecordsId, + resolverRecordsKey: ResolverRecordsCompositeKey, name: string, ) { - await context.db.update(schema.resolverRecords, id).set({ name: interpretNameRecordValue(name) }); + const resolverRecordsId = makeResolverRecordsId( + { chainId: resolverRecordsKey.chainId, address: resolverRecordsKey.address }, + resolverRecordsKey.node, + ); + + await context.db + .update(schema.resolverRecords, { id: resolverRecordsId }) + .set({ name: interpretNameRecordValue(name) }); } /** @@ -66,12 +99,12 @@ export async function handleResolverNameUpdate( */ export async function handleResolverAddressRecordUpdate( context: Context, - resolverRecordsId: ResolverRecordsId, - coinType: bigint, + resolverRecordsKey: ResolverRecordsCompositeKey, + coinType: CoinType, address: Address, ) { // construct the ResolverAddressRecord's Composite Key - const id = { ...resolverRecordsId, coinType }; + const id = { ...resolverRecordsKey, coinType }; // interpret the incoming address record value const interpretedValue = interpretAddressRecordValue(address); @@ -85,8 +118,8 @@ export async function handleResolverAddressRecordUpdate( // upsert await context.db .insert(schema.resolverAddressRecord) - .values({ ...id, address: interpretedValue }) - .onConflictDoUpdate({ address: interpretedValue }); + .values({ ...id, value: interpretedValue }) + .onConflictDoUpdate({ value: interpretedValue }); } } @@ -97,7 +130,7 @@ export async function handleResolverAddressRecordUpdate( */ export async function handleResolverTextRecordUpdate( context: Context, - resolverRecordsId: ResolverRecordsId, + resolverRecordsId: ResolverRecordsCompositeKey, key: string, value: string | null, ) { diff --git a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Resolver.ts b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Resolver.ts index cd113ca02..6eb6280b7 100644 --- a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Resolver.ts +++ b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Resolver.ts @@ -1,6 +1,6 @@ import { ponder } from "ponder:registry"; -import { ETH_COIN_TYPE, PluginName } from "@ensnode/ensnode-sdk"; +import { bigintToCoinType, type CoinType, ETH_COIN_TYPE, PluginName } from "@ensnode/ensnode-sdk"; import { parseDnsTxtRecordArgs } from "@/lib/dns-helpers"; import { namespaceContract } from "@/lib/plugin-helpers"; @@ -9,7 +9,7 @@ import { handleResolverAddressRecordUpdate, handleResolverNameUpdate, handleResolverTextRecordUpdate, - makeResolverRecordsId, + makeResolverRecordsCompositeKey, } from "@/lib/protocol-acceleration/resolver-records-db-helpers"; /** @@ -22,22 +22,30 @@ export default function () { async ({ context, event }) => { const { a: address } = event.args; - const id = makeResolverRecordsId(context, event); - await ensureResolverAndResolverRecords(context, id); + const resolverRecordsKey = makeResolverRecordsCompositeKey(context, event); + await ensureResolverAndResolverRecords(context, resolverRecordsKey); // the Resolver#AddrChanged event is just Resolver#AddressChanged with implicit coinType of ETH - await handleResolverAddressRecordUpdate(context, id, BigInt(ETH_COIN_TYPE), address); + await handleResolverAddressRecordUpdate(context, resolverRecordsKey, ETH_COIN_TYPE, address); }, ); ponder.on( namespaceContract(PluginName.ProtocolAcceleration, "Resolver:AddressChanged"), async ({ context, event }) => { - const { coinType, newAddress } = event.args; + const { coinType: _coinType, newAddress } = event.args; - const id = makeResolverRecordsId(context, event); - await ensureResolverAndResolverRecords(context, id); - await handleResolverAddressRecordUpdate(context, id, coinType, newAddress); + // all well-known CoinTypes fit into number, so we coerce here + let coinType: CoinType; + try { + coinType = bigintToCoinType(_coinType); + } catch { + return; // ignore if bigint can't be coerced to known CoinType + } + + const resolverRecordsKey = makeResolverRecordsCompositeKey(context, event); + await ensureResolverAndResolverRecords(context, resolverRecordsKey); + await handleResolverAddressRecordUpdate(context, resolverRecordsKey, coinType, newAddress); }, ); @@ -46,9 +54,9 @@ export default function () { async ({ context, event }) => { const { name } = event.args; - const id = makeResolverRecordsId(context, event); - await ensureResolverAndResolverRecords(context, id); - await handleResolverNameUpdate(context, id, name); + const resolverRecordsKey = makeResolverRecordsCompositeKey(context, event); + await ensureResolverAndResolverRecords(context, resolverRecordsKey); + await handleResolverNameUpdate(context, resolverRecordsKey, name); }, ); @@ -74,9 +82,9 @@ export default function () { }); } catch {} // no-op if readContract throws for whatever reason - const id = makeResolverRecordsId(context, event); - await ensureResolverAndResolverRecords(context, id); - await handleResolverTextRecordUpdate(context, id, key, value); + const resolverRecordsKey = makeResolverRecordsCompositeKey(context, event); + await ensureResolverAndResolverRecords(context, resolverRecordsKey); + await handleResolverTextRecordUpdate(context, resolverRecordsKey, key, value); }, ); @@ -88,9 +96,9 @@ export default function () { async ({ context, event }) => { const { key, value } = event.args; - const id = makeResolverRecordsId(context, event); - await ensureResolverAndResolverRecords(context, id); - await handleResolverTextRecordUpdate(context, id, key, value); + const resolverRecordsKey = makeResolverRecordsCompositeKey(context, event); + await ensureResolverAndResolverRecords(context, resolverRecordsKey); + await handleResolverTextRecordUpdate(context, resolverRecordsKey, key, value); }, ); @@ -105,9 +113,9 @@ export default function () { const { key, value } = parseDnsTxtRecordArgs(event.args); if (key === null) return; // no key to operate over? args were malformed, ignore event - const id = makeResolverRecordsId(context, event); - await ensureResolverAndResolverRecords(context, id); - await handleResolverTextRecordUpdate(context, id, key, value); + const resolverRecordsKey = makeResolverRecordsCompositeKey(context, event); + await ensureResolverAndResolverRecords(context, resolverRecordsKey); + await handleResolverTextRecordUpdate(context, resolverRecordsKey, key, value); }, ); @@ -121,9 +129,9 @@ export default function () { const { key, value } = parseDnsTxtRecordArgs(event.args); if (key === null) return; // no key to operate over? args were malformed, ignore event - const id = makeResolverRecordsId(context, event); - await ensureResolverAndResolverRecords(context, id); - await handleResolverTextRecordUpdate(context, id, key, value); + const resolverRecordsKey = makeResolverRecordsCompositeKey(context, event); + await ensureResolverAndResolverRecords(context, resolverRecordsKey); + await handleResolverTextRecordUpdate(context, resolverRecordsKey, key, value); }, ); @@ -133,9 +141,9 @@ export default function () { const { key } = parseDnsTxtRecordArgs(event.args); if (key === null) return; // no key to operate over? args were malformed, ignore event - const id = makeResolverRecordsId(context, event); - await ensureResolverAndResolverRecords(context, id); - await handleResolverTextRecordUpdate(context, id, key, null); + const resolverRecordsKey = makeResolverRecordsCompositeKey(context, event); + await ensureResolverAndResolverRecords(context, resolverRecordsKey); + await handleResolverTextRecordUpdate(context, resolverRecordsKey, key, null); }, ); } diff --git a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/StandaloneReverseRegistrar.ts b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/StandaloneReverseRegistrar.ts index 19ad6b5d6..50ce9cbee 100644 --- a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/StandaloneReverseRegistrar.ts +++ b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/StandaloneReverseRegistrar.ts @@ -31,7 +31,7 @@ export default function () { : evmChainIdToCoinType(context.chain.id); // construct the ReverseNameRecord entity's Composite Primary Key - const id = { address, coinType: BigInt(coinType) }; + const id = { address, coinType }; // interpret the emitted name record value (see `interpretNameRecordValue` for guarantees) const interpretedValue = interpretNameRecordValue(name); diff --git a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts index 6f8e85cc8..4ed82406b 100644 --- a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts +++ b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts @@ -2,7 +2,12 @@ * Schema Definitions that power Protocol Acceleration in the Resolution API. */ -import { onchainTable, primaryKey, relations } from "ponder"; +import { onchainTable, primaryKey, relations, uniqueIndex } from "ponder"; +import type { Address } from "viem"; + +import type { ChainId, CoinType, Node, ResolverId, ResolverRecordsId } from "@ensnode/ensnode-sdk"; + +// TODO: implement resolverType & polymorphic field availability /** * Tracks an Account's ENSIP-19 Reverse Name Records by CoinType. @@ -21,8 +26,8 @@ export const reverseNameRecord = onchainTable( "reverse_name_records", (t) => ({ // keyed by (address, coinType) - address: t.hex().notNull(), - coinType: t.bigint().notNull(), + address: t.hex().notNull().$type
(), + coinType: t.integer().notNull().$type(), /** * Represents the ENSIP-19 Reverse Name Record for a given (address, coinType). @@ -56,15 +61,15 @@ export const nodeResolverRelation = onchainTable( "node_resolver_relations", (t) => ({ // keyed by (chainId, registry, node) - chainId: t.integer().notNull(), - registry: t.hex().notNull(), - node: t.hex().notNull(), + chainId: t.integer().notNull().$type(), + registry: t.hex().notNull().$type
(), + node: t.hex().notNull().$type(), /** * The Address of the Resolver contract this `node` has set (via Registry#NewResolver) within * the Registry on `chainId`. */ - resolver: t.hex().notNull(), + resolver: t.hex().notNull().$type
(), }), (t) => ({ pk: primaryKey({ columns: [t.chainId, t.registry, t.node] }), @@ -75,11 +80,13 @@ export const resolver = onchainTable( "resolvers", (t) => ({ // keyed by (chainId, address) - chainId: t.integer().notNull(), - address: t.hex().notNull(), + id: t.text().primaryKey().$type(), + + chainId: t.integer().notNull().$type(), + address: t.hex().notNull().$type
(), }), (t) => ({ - pk: primaryKey({ columns: [t.chainId, t.address] }), + byId: uniqueIndex().on(t.chainId, t.address), }), ); @@ -105,9 +112,11 @@ export const resolverRecords = onchainTable( "resolver_records", (t) => ({ // keyed by (chainId, resolver, node) - chainId: t.integer().notNull(), - resolver: t.hex().notNull(), - node: t.hex().notNull(), + id: t.text().primaryKey().$type(), + + chainId: t.integer().notNull().$type(), + address: t.hex().notNull().$type
(), + node: t.hex().notNull().$type(), /** * Represents the value of the reverse-resolution (ENSIP-3) name() record, used for Reverse Resolution. @@ -122,14 +131,14 @@ export const resolverRecords = onchainTable( name: t.text(), }), (t) => ({ - pk: primaryKey({ columns: [t.chainId, t.resolver, t.node] }), + byId: uniqueIndex().on(t.chainId, t.address, t.node), }), ); export const resolverRecords_relations = relations(resolverRecords, ({ one, many }) => ({ // belongs to resolver resolver: one(resolver, { - fields: [resolverRecords.chainId, resolverRecords.resolver], + fields: [resolverRecords.chainId, resolverRecords.address], references: [resolver.chainId, resolver.address], }), @@ -151,10 +160,10 @@ export const resolverAddressRecord = onchainTable( "resolver_address_records", (t) => ({ // keyed by ((chainId, resolver, node), coinType) - chainId: t.integer().notNull(), - resolver: t.hex().notNull(), - node: t.hex().notNull(), - coinType: t.bigint().notNull(), + chainId: t.integer().notNull().$type(), + address: t.hex().notNull().$type
(), + node: t.hex().notNull().$type(), + coinType: t.integer().notNull().$type(), /** * Represents the value of the Addresss Record specified by ((chainId, resolver, node), coinType). @@ -162,10 +171,10 @@ export const resolverAddressRecord = onchainTable( * The value of this field is interpreted by `interpretAddressRecordValue` — see its implementation * for additional context and specific guarantees. */ - address: t.text().notNull(), + value: t.text().notNull(), }), (t) => ({ - pk: primaryKey({ columns: [t.chainId, t.resolver, t.node, t.coinType] }), + pk: primaryKey({ columns: [t.chainId, t.address, t.node, t.coinType] }), }), ); @@ -174,10 +183,10 @@ export const resolverAddressRecordRelations = relations(resolverAddressRecord, ( resolver: one(resolverRecords, { fields: [ resolverAddressRecord.chainId, - resolverAddressRecord.resolver, + resolverAddressRecord.address, resolverAddressRecord.node, ], - references: [resolverRecords.chainId, resolverRecords.resolver, resolverRecords.node], + references: [resolverRecords.chainId, resolverRecords.address, resolverRecords.node], }), })); @@ -192,9 +201,9 @@ export const resolverTextRecord = onchainTable( "resolver_text_records", (t) => ({ // keyed by ((chainId, resolver, node), key) - chainId: t.integer().notNull(), - resolver: t.hex().notNull(), - node: t.hex().notNull(), + chainId: t.integer().notNull().$type(), + address: t.hex().notNull().$type
(), + node: t.hex().notNull().$type(), key: t.text().notNull(), /** @@ -206,15 +215,15 @@ export const resolverTextRecord = onchainTable( value: t.text().notNull(), }), (t) => ({ - pk: primaryKey({ columns: [t.chainId, t.resolver, t.node, t.key] }), + pk: primaryKey({ columns: [t.chainId, t.address, t.node, t.key] }), }), ); export const resolverTextRecordRelations = relations(resolverTextRecord, ({ one }) => ({ // belongs to resolverRecord resolver: one(resolverRecords, { - fields: [resolverTextRecord.chainId, resolverTextRecord.resolver, resolverTextRecord.node], - references: [resolverRecords.chainId, resolverRecords.resolver, resolverRecords.node], + fields: [resolverTextRecord.chainId, resolverTextRecord.address, resolverTextRecord.node], + references: [resolverRecords.chainId, resolverRecords.address, resolverRecords.node], }), })); diff --git a/packages/ensnode-sdk/src/ensv2/ids-lib.ts b/packages/ensnode-sdk/src/ensv2/ids-lib.ts index 0a4ad4b53..6e51f546f 100644 --- a/packages/ensnode-sdk/src/ensv2/ids-lib.ts +++ b/packages/ensnode-sdk/src/ensv2/ids-lib.ts @@ -3,6 +3,7 @@ import { type Address, hexToBigInt } from "viem"; import { type AccountId, type LabelHash, + type Node, serializeAccountId, serializeAssetId, } from "@ensnode/ensnode-sdk"; @@ -15,6 +16,7 @@ import type { PermissionsUserId, RegistryContractId, ResolverId, + ResolverRecordsId, } from "./ids"; /** @@ -55,15 +57,21 @@ export const makePermissionsId = (contract: AccountId) => * */ export const makePermissionsResourceId = (contract: AccountId, resource: bigint) => - `${serializeAccountId(contract)}/${resource}` as PermissionsResourceId; + `${makePermissionsId(contract)}/${resource}` as PermissionsResourceId; /** * */ export const makePermissionsUserId = (contract: AccountId, resource: bigint, user: Address) => - `${serializeAccountId(contract)}/${resource}/${user}` as PermissionsUserId; + `${makePermissionsId(contract)}/${resource}/${user}` as PermissionsUserId; /** * Serializes and brands an AccountId as a ResolverId. */ export const makeResolverId = (contract: AccountId) => serializeAccountId(contract) as ResolverId; + +/** + * + */ +export const makeResolverRecordsId = (contract: AccountId, node: Node) => + `${makeResolverId(contract)}/${node}` as ResolverRecordsId; diff --git a/packages/ensnode-sdk/src/ensv2/ids.ts b/packages/ensnode-sdk/src/ensv2/ids.ts index a40820345..aed7d42eb 100644 --- a/packages/ensnode-sdk/src/ensv2/ids.ts +++ b/packages/ensnode-sdk/src/ensv2/ids.ts @@ -56,3 +56,8 @@ export type PermissionsUserId = string & { __brand: "PermissionsUserId" }; * */ export type ResolverId = SerializedAccountId & { __brand: "ResolverId" }; + +/** + * + */ +export type ResolverRecordsId = string & { __brand: "ResolverRecordsId" }; From ae47be65cabc94689b4744cc626c06d239b01d28 Mon Sep 17 00:00:00 2001 From: shrugs Date: Tue, 11 Nov 2025 19:07:59 -0600 Subject: [PATCH 012/102] checkpoint --- .../src/graphql-api/lib/get-canonical-path.ts | 11 +- .../src/graphql-api/lib/get-domain-by-fqdn.ts | 6 +- apps/ensapi/src/graphql-api/schema/query.ts | 7 +- .../ensapi/src/graphql-api/schema/resolver.ts | 10 +- .../protocol-acceleration/find-resolver.ts | 4 +- .../src/lib/resolution/forward-resolution.ts | 7 +- apps/ensapi/src/lib/root-registry.ts | 18 --- apps/ensindexer/src/config/validations.ts | 19 ++- .../src/lib/ensv2/label-db-helpers.ts | 30 ++++ .../src/lib/ensv2/labelspace-db-helpers.ts | 15 -- .../src/lib/ensv2/resolver-db-helpers.ts | 16 ++ .../src/plugins/ensv2/event-handlers.ts | 2 + .../plugins/ensv2/handlers/ENSv1Registry.ts | 146 ++++++++++++++++++ .../src/plugins/ensv2/handlers/Registry.ts | 14 +- apps/ensindexer/src/plugins/ensv2/plugin.ts | 71 ++++++++- .../src/schemas/ensv2.schema.ts | 4 - packages/ensnode-sdk/src/shared/index.ts | 1 + .../ensnode-sdk/src/shared/root-registry.ts | 29 ++++ 18 files changed, 348 insertions(+), 62 deletions(-) delete mode 100644 apps/ensapi/src/lib/root-registry.ts create mode 100644 apps/ensindexer/src/lib/ensv2/label-db-helpers.ts delete mode 100644 apps/ensindexer/src/lib/ensv2/labelspace-db-helpers.ts create mode 100644 apps/ensindexer/src/lib/ensv2/resolver-db-helpers.ts create mode 100644 apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts create mode 100644 packages/ensnode-sdk/src/shared/root-registry.ts diff --git a/apps/ensapi/src/graphql-api/lib/get-canonical-path.ts b/apps/ensapi/src/graphql-api/lib/get-canonical-path.ts index e381a5b4a..bdb621388 100644 --- a/apps/ensapi/src/graphql-api/lib/get-canonical-path.ts +++ b/apps/ensapi/src/graphql-api/lib/get-canonical-path.ts @@ -1,12 +1,19 @@ +import config from "@/config"; + import { sql } from "drizzle-orm"; import * as schema from "@ensnode/ensnode-schema"; -import type { CanonicalPath, DomainId, RegistryId } from "@ensnode/ensnode-sdk"; +import { + type CanonicalPath, + type DomainId, + getRootRegistryId, + type RegistryId, +} from "@ensnode/ensnode-sdk"; import { db } from "@/lib/db"; -import { ROOT_REGISTRY_ID } from "@/lib/root-registry"; const MAX_DEPTH = 16; +const ROOT_REGISTRY_ID = getRootRegistryId(config.namespace); /** * Provide the canonical parents from the Root Registry to `domainId`. diff --git a/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts index 41be9e776..9192d5b2c 100644 --- a/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts +++ b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts @@ -1,8 +1,11 @@ +import config from "@/config"; + import { Param, sql } from "drizzle-orm"; import * as schema from "@ensnode/ensnode-schema"; import { type DomainId, + getRootRegistryId, type InterpretedName, interpretedNameToLabelHashPath, type LabelHash, @@ -10,7 +13,8 @@ import { } from "@ensnode/ensnode-sdk"; import { db } from "@/lib/db"; -import { ROOT_REGISTRY_ID } from "@/lib/root-registry"; + +const ROOT_REGISTRY_ID = getRootRegistryId(config.namespace); /** * Gets the Domain addressed by `name`. diff --git a/apps/ensapi/src/graphql-api/schema/query.ts b/apps/ensapi/src/graphql-api/schema/query.ts index 1516c29cb..ac35bd504 100644 --- a/apps/ensapi/src/graphql-api/schema/query.ts +++ b/apps/ensapi/src/graphql-api/schema/query.ts @@ -1,13 +1,14 @@ /** biome-ignore-all lint/correctness/noUnusedFunctionParameters: ignore unused resolve arguments */ -import { makeRegistryContractId } from "@ensnode/ensnode-sdk"; +import config from "@/config"; + +import { getRootRegistryId, makeRegistryContractId } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import { getDomainIdByInterpretedName } from "@/graphql-api/lib/get-domain-by-fqdn"; import { AccountRef } from "@/graphql-api/schema/account"; import { DomainIdInput, DomainRef } from "@/graphql-api/schema/domain"; import { RegistryIdInput, RegistryInterfaceRef } from "@/graphql-api/schema/registry"; -import { ROOT_REGISTRY_ID } from "@/lib/root-registry"; // TODO: maybe should still implement query/return by id, exposing the db's primary key? // maybe necessary for connections pattern... @@ -60,7 +61,7 @@ builder.queryType({ description: "TODO", type: RegistryInterfaceRef, nullable: false, - resolve: () => ROOT_REGISTRY_ID, + resolve: () => getRootRegistryId(config.namespace), }), }), }); diff --git a/apps/ensapi/src/graphql-api/schema/resolver.ts b/apps/ensapi/src/graphql-api/schema/resolver.ts index 719aa6df2..a3b2175f4 100644 --- a/apps/ensapi/src/graphql-api/schema/resolver.ts +++ b/apps/ensapi/src/graphql-api/schema/resolver.ts @@ -56,11 +56,15 @@ ResolverRef.implement({ resolve: ({ chainId, address }) => ({ chainId, address }), }), + //////////////////// + // Resolver.records + //////////////////// + // TODO: connection to all ResolverRecords by (address, chainId) + //////////////////////////// - // Resolver.records by Node + // Resolver.recordsFor node //////////////////////////// - // TODO: make node optional and allow connection to all records - records: t.field({ + recordsFor: t.field({ description: "TODO", type: ResolverRecordsRef, args: { node: t.arg({ type: "Node", required: true }) }, diff --git a/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts b/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts index 38c3187b3..d5a740c9a 100644 --- a/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts +++ b/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts @@ -16,6 +16,7 @@ import { DatasourceNames, getDatasource } from "@ensnode/datasources"; import { type AccountId, getNameHierarchy, + isRootRegistry, type Name, type Node, type NormalizedName, @@ -23,7 +24,6 @@ import { import { sortByArrayOrder } from "@/graphql-api/lib/sort-by-array-order"; import { db } from "@/lib/db"; -import { isRootRegistry } from "@/lib/root-registry"; import { withActiveSpanAsync, withSpanAsync } from "@/lib/tracing/auto-span"; type FindResolverResult = @@ -73,7 +73,7 @@ export async function findResolver({ } // Invariant: UniversalResolver#findResolver only works for ENS Root Registry - if (!isRootRegistry(registry)) { + if (!isRootRegistry(config.namespace, registry)) { throw new Error( `Invariant(findResolver): UniversalResolver#findResolver only identifies active resolvers agains the ENs Root Registry, but a different Registry contract was passed: ${JSON.stringify(registry)}.`, ); diff --git a/apps/ensapi/src/lib/resolution/forward-resolution.ts b/apps/ensapi/src/lib/resolution/forward-resolution.ts index 335aa167f..5bc9f3ed4 100644 --- a/apps/ensapi/src/lib/resolution/forward-resolution.ts +++ b/apps/ensapi/src/lib/resolution/forward-resolution.ts @@ -10,6 +10,7 @@ import { type ForwardResolutionArgs, ForwardResolutionProtocolStep, type ForwardResolutionResult, + getRootRegistry, isNormalizedName, isSelectionEmpty, type Node, @@ -37,7 +38,6 @@ import { interpretRawCallsAndResults, makeResolveCalls, } from "@/lib/resolution/resolve-calls-and-results"; -import { ROOT_REGISTRY } from "@/lib/root-registry"; import { supportsENSIP10Interface } from "@/lib/rpc/ensip-10"; import { getPublicClient } from "@/lib/rpc/public-client"; import { withActiveSpanAsync, withSpanAsync } from "@/lib/tracing/auto-span"; @@ -87,7 +87,10 @@ export async function resolveForward ): Promise> { // NOTE: `resolveForward` is just `_resolveForward` with the enforcement that `registry` must // initially be ENS Root Chain's Registry: see `_resolveForward` for additional context. - return _resolveForward(name, selection, { ...options, registry: ROOT_REGISTRY }); + return _resolveForward(name, selection, { + ...options, + registry: getRootRegistry(config.namespace), + }); } /** diff --git a/apps/ensapi/src/lib/root-registry.ts b/apps/ensapi/src/lib/root-registry.ts deleted file mode 100644 index 8bfddc3a0..000000000 --- a/apps/ensapi/src/lib/root-registry.ts +++ /dev/null @@ -1,18 +0,0 @@ -import config from "@/config"; - -import { DatasourceNames, getDatasource } from "@ensnode/datasources"; -import { type AccountId, accountIdEqual, makeRegistryContractId } from "@ensnode/ensnode-sdk"; - -// TODO: remove, helps types while implementing -if (config.namespace !== "ens-test-env") throw new Error("nope"); - -const ensroot = getDatasource(config.namespace, DatasourceNames.ENSRoot); - -export const ROOT_REGISTRY = { - chainId: ensroot.chain.id, - address: ensroot.contracts.RootRegistry.address, -} satisfies AccountId; - -export const ROOT_REGISTRY_ID = makeRegistryContractId(ROOT_REGISTRY); - -export const isRootRegistry = (accountId: AccountId) => accountIdEqual(accountId, ROOT_REGISTRY); diff --git a/apps/ensindexer/src/config/validations.ts b/apps/ensindexer/src/config/validations.ts index 2544782f9..6d5e36b5c 100644 --- a/apps/ensindexer/src/config/validations.ts +++ b/apps/ensindexer/src/config/validations.ts @@ -2,7 +2,7 @@ import { type Address, isAddress } from "viem"; import type { z } from "zod/v4"; import type { DatasourceName } from "@ensnode/datasources"; -import { asLowerCaseAddress, uniq } from "@ensnode/ensnode-sdk"; +import { asLowerCaseAddress, PluginName, uniq } from "@ensnode/ensnode-sdk"; import { getENSNamespaceAsFullyDefinedAtCompileTime } from "@/lib/plugin-helpers"; import { getPlugin } from "@/plugins"; @@ -133,3 +133,20 @@ export function invariant_validContractConfigs( } } } + +// Invariant: ensv2 core plugin requires protocol acceleration +export function invariant_ensv2RequiresProtocolAcceleration( + ctx: ZodCheckFnInput>, +) { + const { value: config } = ctx; + + // TODO: getCorePlugin(config.plugins) + if ( + config.plugins.includes(PluginName.ENSv2) && + !config.plugins.includes(PluginName.ProtocolAcceleration) + ) { + throw new Error( + `Core Plugin '${PluginName.ENSv2}' requires inclusion of '${PluginName.ProtocolAcceleration}' plugin.`, + ); + } +} diff --git a/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts new file mode 100644 index 000000000..4938a4fcf --- /dev/null +++ b/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts @@ -0,0 +1,30 @@ +import type { Context } from "ponder:registry"; +import schema from "ponder:schema"; +import { labelhash } from "viem"; + +import { + encodeLabelHash, + type InterpretedLabel, + type LabelHash, + type LiteralLabel, + literalLabelToInterpretedLabel, +} from "@ensnode/ensnode-sdk"; + +export async function ensureLabel(context: Context, label: LiteralLabel) { + const labelHash = labelhash(label); + const interpretedLabel = literalLabelToInterpretedLabel(label); + + await context.db + .insert(schema.label) + .values({ labelHash, value: interpretedLabel }) + .onConflictDoUpdate({ value: interpretedLabel }); +} + +export async function ensureUnknownLabel(context: Context, labelHash: LabelHash) { + const interpretedLabel = encodeLabelHash(labelHash) as InterpretedLabel; + + await context.db + .insert(schema.label) + .values({ labelHash, value: interpretedLabel }) + .onConflictDoUpdate({ value: interpretedLabel }); +} diff --git a/apps/ensindexer/src/lib/ensv2/labelspace-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/labelspace-db-helpers.ts deleted file mode 100644 index c97fbd90c..000000000 --- a/apps/ensindexer/src/lib/ensv2/labelspace-db-helpers.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Context } from "ponder:registry"; -import schema from "ponder:schema"; -import { labelhash } from "viem"; - -import { type LiteralLabel, literalLabelToInterpretedLabel } from "@ensnode/ensnode-sdk"; - -export async function ensureLabel(context: Context, label: LiteralLabel) { - const labelHash = labelhash(label); - const interpretedLabel = literalLabelToInterpretedLabel(label); - - await context.db - .insert(schema.label) - .values({ labelHash, value: interpretedLabel }) - .onConflictDoUpdate({ value: interpretedLabel }); -} diff --git a/apps/ensindexer/src/lib/ensv2/resolver-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/resolver-db-helpers.ts new file mode 100644 index 000000000..3322af077 --- /dev/null +++ b/apps/ensindexer/src/lib/ensv2/resolver-db-helpers.ts @@ -0,0 +1,16 @@ +import type { Context } from "ponder:registry"; +import schema from "ponder:schema"; + +import { type AccountId, makeResolverId, type ResolverId } from "@ensnode/ensnode-sdk"; + +export async function ensureResolver(context: Context, resolver: AccountId): Promise { + const id = makeResolverId(resolver); + await context.db + .insert(schema.resolver) + .values({ + id, + ...resolver, + }) + .onConflictDoNothing(); + return id; +} diff --git a/apps/ensindexer/src/plugins/ensv2/event-handlers.ts b/apps/ensindexer/src/plugins/ensv2/event-handlers.ts index ef682c228..4e0751b08 100644 --- a/apps/ensindexer/src/plugins/ensv2/event-handlers.ts +++ b/apps/ensindexer/src/plugins/ensv2/event-handlers.ts @@ -1,7 +1,9 @@ +import attach_ENSv1RegistryHandlers from "./handlers/ENSv1Registry"; import attach_EnhancedAccessControlHandlers from "./handlers/EnhancedAccessControl"; import attach_RegistryHandlers from "./handlers/Registry"; export default function () { attach_RegistryHandlers(); attach_EnhancedAccessControlHandlers(); + attach_ENSv1RegistryHandlers(); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts new file mode 100644 index 000000000..76605baeb --- /dev/null +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts @@ -0,0 +1,146 @@ +import config from "@/config"; + +import { type Context, ponder } from "ponder:registry"; +import schema from "ponder:schema"; +import { type Address, isAddressEqual, zeroAddress } from "viem"; + +import { + getRootRegistry, + getRootRegistryId, + type LabelHash, + makeSubdomainNode, + type Node, + PluginName, +} from "@ensnode/ensnode-sdk"; + +import { namespaceContract } from "@/lib/plugin-helpers"; +import type { EventWithArgs } from "@/lib/ponder-helpers"; +import { + removeNodeResolverRelation, + upsertNodeResolverRelation, +} from "@/lib/protocol-acceleration/node-resolver-relationship-db-helpers"; +import { nodeIsMigrated } from "@/lib/protocol-acceleration/registry-migration-status"; + +async function handleNewOwner({ + context, + event, +}: { + context: Context; + event: EventWithArgs<{ + // NOTE: `node` event arg represents a `Node` that is the _parent_ of the node the NewOwner event is about + node: Node; + // NOTE: `label` event arg represents a `LabelHash` for the sub-node under `node` + label: LabelHash; + owner: Address; + }>; +}) { + // +} + +async function handleNewResolver({ + context, + event, +}: { + context: Context; + event: EventWithArgs<{ node: Node; resolver: Address }>; +}) { + const { node, resolver: resolverAddress } = event.args; + const registry = event.log.address; + const isZeroResolver = isAddressEqual(zeroAddress, resolverAddress); + + if (isZeroResolver) { + await removeNodeResolverRelation(context, registry, node); + } else { + await upsertNodeResolverRelation(context, registry, node, resolverAddress); + } +} + +/** + * Handler functions for ENSv1 Regsitry contracts. + * - piggybacks Protocol Resolution plugin's Node Migration status + */ +export default function () { + /** + * Sets up the ENSv2 Root Registry + */ + ponder.on(namespaceContract(PluginName.ENSv2, "ENSv1RegistryOld:setup"), async ({ context }) => { + // ensures that the Root Registry (which is eventually backed by the ENSv2 Root Registry) is + // populated in the database + await context.db + .insert(schema.registry) + .values({ + id: getRootRegistryId(config.namespace), + type: "RegistryContract", + ...getRootRegistry(config.namespace), + }) + .onConflictDoNothing(); + }); + + /** + * Handles Registry#NewOwner for: + * - ENS Root Chain's ENSv1RegistryOld + */ + ponder.on( + namespaceContract(PluginName.ENSv2, "ENSv1RegistryOld:NewOwner"), + async ({ context, event }) => { + const { label: labelHash, node: parentNode } = event.args; + + // ignore the event on ENSv1RegistryOld if node is migrated to new Registry + const node = makeSubdomainNode(labelHash, parentNode); + const shouldIgnoreEvent = await nodeIsMigrated(context, node); + if (shouldIgnoreEvent) return; + + return handleNewOwner({ context, event }); + }, + ); + + /** + * Handles Registry#NewResolver for: + * - ENS Root Chain's ENSv1RegistryOld + */ + ponder.on( + namespaceContract(PluginName.ENSv2, "ENSv1RegistryOld:NewResolver"), + async ({ context, event }) => { + // ignore the event on ENSv1RegistryOld if node is migrated to new Registry + const shouldIgnoreEvent = await nodeIsMigrated(context, event.args.node); + if (shouldIgnoreEvent) return; + + return handleNewResolver({ context, event }); + }, + ); + + /** + * Handles Registry#NewResolver for: + * - ENS Root Chain's ENSv1RegistryOld + */ + ponder.on( + namespaceContract(PluginName.ENSv2, "ENSv1RegistryOld:NewTTL"), + async ({ context, event }) => { + const shouldIgnoreEvent = await nodeIsMigrated(context, event.args.node); + if (shouldIgnoreEvent) return; + + return handleNewTTL({ context, event }); + }, + ); + + ponder.on( + namespaceContract(PluginName.ENSv2, "ENSv1RegistryOld:Transfer"), + async ({ context, event }) => { + const shouldIgnoreEvent = await nodeIsMigrated(context, event.args.node); + if (shouldIgnoreEvent) return; + + return handleTransfer({ context, event }); + }, + ); + + /** + * Handles Registry events for: + * - ENS Root Chain's (new) Registry + * - Basenames Registry + * - Lineanames Registry + */ + ponder.on(namespaceContract(pluginName, "ENSv1Registry:NewOwner"), handleNewOwner); + ponder.on(namespaceContract(pluginName, "ENSv1Registry:NewResolver"), handleNewResolver); + ponder.on(namespaceContract(pluginName, "ENSv1Registry:NewTTL"), handleNewTTL); + ponder.on(namespaceContract(pluginName, "ENSv1Registry:Transfer"), handleTransfer); +} diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts index 8b8b65da1..4538a9834 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts @@ -9,12 +9,12 @@ import { type LiteralLabel, makeENSv2DomainId, makeRegistryContractId, - makeResolverId, PluginName, } from "@ensnode/ensnode-sdk"; import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; -import { ensureLabel } from "@/lib/ensv2/labelspace-db-helpers"; +import { ensureLabel } from "@/lib/ensv2/label-db-helpers"; +import { ensureResolver } from "@/lib/ensv2/resolver-db-helpers"; import { getThisAccountId } from "@/lib/get-this-account-id"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; @@ -128,18 +128,22 @@ export default function () { resolver: Address; }>; }) => { - const { id: tokenId, resolver } = event.args; + const { id: tokenId, resolver: address } = event.args; const canonicalId = getCanonicalId(tokenId); const registryAccountId = getThisAccountId(context, event); const domainId = makeENSv2DomainId(registryAccountId, canonicalId); // update domain's resolver - const isDeletion = isAddressEqual(resolver, zeroAddress); + const isDeletion = isAddressEqual(address, zeroAddress); if (isDeletion) { await context.db.update(schema.domain, { id: domainId }).set({ resolverId: null }); } else { - const resolverId = makeResolverId({ chainId: context.chain.id, address: resolver }); + const resolverId = await ensureResolver(context, { + chainId: context.chain.id, + address: address, + }); + await context.db.update(schema.domain, { id: domainId }).set({ resolverId }); } }, diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index 30a696397..7e9600f98 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -11,7 +11,11 @@ import { } from "@ensnode/datasources"; import { PluginName } from "@ensnode/ensnode-sdk"; -import { createPlugin, namespaceContract } from "@/lib/plugin-helpers"; +import { + createPlugin, + getDatasourceAsFullyDefinedAtCompileTime, + namespaceContract, +} from "@/lib/plugin-helpers"; import { chainConfigForContract, chainsConnectionConfig } from "@/lib/ponder-helpers"; /** @@ -19,21 +23,37 @@ import { chainConfigForContract, chainsConnectionConfig } from "@/lib/ponder-hel */ export const pluginName = PluginName.ENSv2; -const ALL_DATASOURCE_NAMES = [ +const REQUIRED_DATASOURCE_NAMES = [ DatasourceNames.ENSRoot, DatasourceNames.Namechain, ] as const satisfies DatasourceName[]; +const ALL_DATASOURCE_NAMES = [ + ...REQUIRED_DATASOURCE_NAMES, + DatasourceNames.Basenames, + DatasourceNames.Lineanames, +] as const satisfies DatasourceName[]; + export default createPlugin({ name: pluginName, - requiredDatasourceNames: ALL_DATASOURCE_NAMES, + requiredDatasourceNames: REQUIRED_DATASOURCE_NAMES, createPonderConfig(config) { // TODO: remove this, helps with types while only targeting ens-test-env - if (config.namespace !== ENSNamespaceIds.EnsTestEnv) process.exit(1); + if (config.namespace !== ENSNamespaceIds.EnsTestEnv) throw new Error("only ens-test-env"); const ensroot = getDatasource(config.namespace, DatasourceNames.ENSRoot); - // biome-ignore lint/style/noNonNullAssertion: allowed for now - const namechain = maybeGetDatasource(config.namespace, DatasourceNames.Namechain)!; + const namechain = getDatasourceAsFullyDefinedAtCompileTime( + config.namespace, + DatasourceNames.Namechain, + ); + const basenames = getDatasourceAsFullyDefinedAtCompileTime( + config.namespace, + DatasourceNames.Basenames, + ); + const lineanames = getDatasourceAsFullyDefinedAtCompileTime( + config.namespace, + DatasourceNames.Lineanames, + ); const allDatasources = ALL_DATASOURCE_NAMES.map((datasourceName) => maybeGetDatasource(config.namespace, datasourceName), @@ -79,6 +99,45 @@ export default createPlugin({ {}, ), }, + + // index the ENSv1RegistryOld on ENS Root Chain + [namespaceContract(pluginName, "ENSv1RegistryOld")]: { + abi: ensroot.contracts.ENSv1RegistryOld.abi, + chain: { + ...chainConfigForContract( + config.globalBlockrange, + ensroot.chain.id, + ensroot.contracts.ENSv1RegistryOld, + ), + }, + }, + + // index ENSv1Registry on ENS Root Chain, Basenames, Lineanames + [namespaceContract(pluginName, "ENSv1Registry")]: { + abi: ensroot.contracts.ENSv1Registry.abi, + chain: { + // ENS Root Chain Registry + ...chainConfigForContract( + config.globalBlockrange, + ensroot.chain.id, + ensroot.contracts.ENSv1Registry, + ), + // Basenames (shadow)Registry if defined + ...(basenames && + chainConfigForContract( + config.globalBlockrange, + basenames.chain.id, + basenames.contracts.Registry, + )), + // Lineanames (shadow)Registry if defined + ...(lineanames && + chainConfigForContract( + config.globalBlockrange, + lineanames.chain.id, + lineanames.contracts.Registry, + )), + }, + }, }, }); }, diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/schemas/ensv2.schema.ts index 06405df5d..8de97fa63 100644 --- a/packages/ensnode-schema/src/schemas/ensv2.schema.ts +++ b/packages/ensnode-schema/src/schemas/ensv2.schema.ts @@ -224,11 +224,7 @@ export const relations_permissionsUser = relations(permissionsUser, ({ one, many export const label = onchainTable("labels", (t) => ({ labelHash: t.hex().primaryKey().$type(), - // TODO: store literal/interpeted values as well or only interpreted? value: t.text().notNull().$type(), - - // internals - hasAttemptedHeal: t.boolean().notNull().default(false), })); export const label_relations = relations(label, ({ many }) => ({ diff --git a/packages/ensnode-sdk/src/shared/index.ts b/packages/ensnode-sdk/src/shared/index.ts index 6dde8f001..8024e71ca 100644 --- a/packages/ensnode-sdk/src/shared/index.ts +++ b/packages/ensnode-sdk/src/shared/index.ts @@ -19,6 +19,7 @@ export * from "./interpretation"; export * from "./labelhash"; export * from "./null-bytes"; export * from "./numbers"; +export * from "./root-registry"; export * from "./serialize"; export * from "./serialized-types"; export * from "./types"; diff --git a/packages/ensnode-sdk/src/shared/root-registry.ts b/packages/ensnode-sdk/src/shared/root-registry.ts new file mode 100644 index 000000000..e7d14f290 --- /dev/null +++ b/packages/ensnode-sdk/src/shared/root-registry.ts @@ -0,0 +1,29 @@ +import { DatasourceNames, type ENSNamespaceId, getDatasource } from "@ensnode/datasources"; +import { type AccountId, accountIdEqual, makeRegistryContractId } from "@ensnode/ensnode-sdk"; + +/** + * TODO + */ +export const getRootRegistry = (namespace: ENSNamespaceId) => { + // TODO: remove, helps types while implementing + if (namespace !== "ens-test-env") throw new Error("nope"); + + const ensroot = getDatasource(namespace, DatasourceNames.ENSRoot); + + return { + chainId: ensroot.chain.id, + address: ensroot.contracts.RootRegistry.address, + } satisfies AccountId; +}; + +/** + * TODO + */ +export const getRootRegistryId = (namespace: ENSNamespaceId) => + makeRegistryContractId(getRootRegistry(namespace)); + +/** + * TODO + */ +export const isRootRegistry = (namespace: ENSNamespaceId, accountId: AccountId) => + accountIdEqual(getRootRegistry(namespace), accountId); From edfc6e20f150a3a3548868051de00e1f8be7c826 Mon Sep 17 00:00:00 2001 From: shrugs Date: Wed, 12 Nov 2025 12:46:16 -0600 Subject: [PATCH 013/102] checkpoint --- apps/ensapi/biome.jsonc | 17 +++ apps/ensapi/src/graphql-api/schema/domain.ts | 12 +- apps/ensapi/src/graphql-api/schema/query.ts | 2 - .../ensapi/src/graphql-api/schema/resolver.ts | 2 - .../known-ensip-19-reverse-resolvers.ts | 24 ++-- .../known-onchain-static-resolver.ts | 58 ++++----- .../ponder/src/register-handlers.ts | 4 + apps/ensindexer/src/lib/datasource-helpers.ts | 21 +++- .../src/lib/ensv2/is-name-wrapper.ts | 29 +++++ .../src/lib/ensv2/resolver-db-helpers.ts | 16 --- .../plugins/ensv2/handlers/ENSv1Registry.ts | 113 +++++++++++++----- .../src/plugins/ensv2/handlers/NameWrapper.ts | 1 + .../src/plugins/ensv2/handlers/Registry.ts | 12 +- apps/ensindexer/src/plugins/ensv2/plugin.ts | 107 +++++++++-------- .../subgraph/shared-handlers/NameWrapper.ts | 2 +- packages/datasources/src/lib/types.ts | 28 +++++ packages/datasources/src/namespaces.ts | 19 +-- packages/datasources/src/sepolia.ts | 31 +++++ .../src/schemas/ensv2.schema.ts | 4 - packages/ensnode-sdk/src/ensv2/ids-lib.ts | 12 ++ .../src/shared/datasources-with-resolvers.ts | 2 +- .../ensnode-sdk/src/shared/root-registry.ts | 7 +- 22 files changed, 344 insertions(+), 179 deletions(-) create mode 100644 apps/ensapi/biome.jsonc create mode 100644 apps/ensindexer/src/lib/ensv2/is-name-wrapper.ts delete mode 100644 apps/ensindexer/src/lib/ensv2/resolver-db-helpers.ts create mode 100644 apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts diff --git a/apps/ensapi/biome.jsonc b/apps/ensapi/biome.jsonc new file mode 100644 index 000000000..a9e9629ae --- /dev/null +++ b/apps/ensapi/biome.jsonc @@ -0,0 +1,17 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.2/schema.json", + "extends": "//", + "overrides": [ + // allow unused function parameters in pothos schema files due to resolve() pattern + { + "includes": ["./src/graphql-api/**/*.ts"], + "linter": { + "rules": { + "correctness": { + "noUnusedFunctionParameters": "off" + } + } + } + } + ] +} diff --git a/apps/ensapi/src/graphql-api/schema/domain.ts b/apps/ensapi/src/graphql-api/schema/domain.ts index 71c28f0e9..12c798fe3 100644 --- a/apps/ensapi/src/graphql-api/schema/domain.ts +++ b/apps/ensapi/src/graphql-api/schema/domain.ts @@ -1,7 +1,10 @@ -/** biome-ignore-all lint/correctness/noUnusedFunctionParameters: graphql resolve pattern */ import { rejectErrors } from "@pothos/plugin-dataloader"; -import { type DomainId, interpretedLabelsToInterpretedName } from "@ensnode/ensnode-sdk"; +import { + type DomainId, + getCanonicalId, + interpretedLabelsToInterpretedName, +} from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import { getCanonicalPath } from "@/graphql-api/lib/get-canonical-path"; @@ -43,10 +46,11 @@ DomainRef.implement({ ////////////////////// // Domain.canonicalId ////////////////////// - canonicalId: t.expose("canonicalId", { + canonicalId: t.field({ type: "BigInt", description: "TODO", nullable: false, + resolve: (parent) => getCanonicalId(parent.labelHash), }), ////////////////////// @@ -112,7 +116,7 @@ DomainRef.implement({ description: "TODO", type: ["Name"], nullable: false, - resolve: async ({ registryId, canonicalId }) => { + resolve: async (parent) => { // a domain's aliases are all of the paths from root to this domain for which it can be // resolved. naively reverse-traverse the namegaph until the root is reached... yikes. // if materializing namespace: simply lookup namesInNamespace by domainId diff --git a/apps/ensapi/src/graphql-api/schema/query.ts b/apps/ensapi/src/graphql-api/schema/query.ts index ac35bd504..ba993060a 100644 --- a/apps/ensapi/src/graphql-api/schema/query.ts +++ b/apps/ensapi/src/graphql-api/schema/query.ts @@ -1,5 +1,3 @@ -/** biome-ignore-all lint/correctness/noUnusedFunctionParameters: ignore unused resolve arguments */ - import config from "@/config"; import { getRootRegistryId, makeRegistryContractId } from "@ensnode/ensnode-sdk"; diff --git a/apps/ensapi/src/graphql-api/schema/resolver.ts b/apps/ensapi/src/graphql-api/schema/resolver.ts index a3b2175f4..ef87e70a1 100644 --- a/apps/ensapi/src/graphql-api/schema/resolver.ts +++ b/apps/ensapi/src/graphql-api/schema/resolver.ts @@ -1,5 +1,3 @@ -/** biome-ignore-all lint/correctness/noUnusedFunctionParameters: graphql resolve pattern */ - import { makeResolverRecordsId, type ResolverId, diff --git a/apps/ensapi/src/lib/protocol-acceleration/known-ensip-19-reverse-resolvers.ts b/apps/ensapi/src/lib/protocol-acceleration/known-ensip-19-reverse-resolvers.ts index 8f423e9b3..44aaa469c 100644 --- a/apps/ensapi/src/lib/protocol-acceleration/known-ensip-19-reverse-resolvers.ts +++ b/apps/ensapi/src/lib/protocol-acceleration/known-ensip-19-reverse-resolvers.ts @@ -18,16 +18,16 @@ export function isKnownENSIP19ReverseResolver(chainId: ChainId, resolverAddress: // NOTE: ENSIP-19 Reverse Resolvers are only valid in the context of the ENS Root chain if (chainId !== rrRoot?.chain.id) return false; - return [ - // DefaultReverseResolver (default.reverse) - rrRoot?.contracts.DefaultReverseResolver3?.address, - // the following are each ChainReverseResolver ([coinType].reverse) - rrRoot?.contracts.BaseReverseResolver?.address, - rrRoot?.contracts.LineaReverseResolver?.address, - rrRoot?.contracts.OptimismReverseResolver?.address, - rrRoot?.contracts.ArbitrumReverseResolver?.address, - rrRoot?.contracts.ScrollReverseResolver?.address, - ] - .filter((address): address is Address => !!address) - .includes(resolverAddress); + return ( + [ + // DefaultReverseResolver (default.reverse) + rrRoot.contracts.DefaultReverseResolver3.address, + // the following are each ChainReverseResolver ([coinType].reverse) + rrRoot.contracts.BaseReverseResolver.address, + rrRoot.contracts.LineaReverseResolver.address, + rrRoot.contracts.OptimismReverseResolver.address, + rrRoot.contracts.ArbitrumReverseResolver.address, + rrRoot.contracts.ScrollReverseResolver.address, + ] as Address[] + ).includes(resolverAddress); } diff --git a/apps/ensapi/src/lib/protocol-acceleration/known-onchain-static-resolver.ts b/apps/ensapi/src/lib/protocol-acceleration/known-onchain-static-resolver.ts index 6acbaa561..73dbc5737 100644 --- a/apps/ensapi/src/lib/protocol-acceleration/known-onchain-static-resolver.ts +++ b/apps/ensapi/src/lib/protocol-acceleration/known-onchain-static-resolver.ts @@ -26,29 +26,29 @@ const basenames = maybeGetDatasource(config.namespace, DatasourceNames.Basenames export function isKnownOnchainStaticResolver(chainId: ChainId, resolverAddress: Address): boolean { // on the ENS Deployment Chain if (chainId === rrRoot?.chain.id) { - return [ - // the Root LegacyDefaultResolver is an Onchain Static Resolver - rrRoot.contracts.DefaultPublicResolver1?.address, + return ( + [ + // the Root LegacyDefaultResolver is an Onchain Static Resolver + rrRoot.contracts.DefaultPublicResolver1.address, - // NOTE: this is _also_ the ENSIP-11 ReverseResolver (aka DefaultReverseResolver2) - rrRoot.contracts.DefaultPublicResolver2?.address, + // NOTE: this is _also_ the ENSIP-11 ReverseResolver (aka DefaultReverseResolver2) + rrRoot.contracts.DefaultPublicResolver2.address, - // the ENSIP-19 default PublicResolver is an Onchain Static Resolver - rrRoot.contracts.DefaultPublicResolver3?.address, - ] - .filter((address): address is Address => !!address) - .includes(resolverAddress); + // the ENSIP-19 default PublicResolver is an Onchain Static Resolver + rrRoot.contracts.DefaultPublicResolver3.address, + ] as Address[] + ).includes(resolverAddress); } // on Base Chain if (chainId === basenames?.chain.id) { - return [ - // the Basenames Default Resolvers are Onchain Static Resolvers - basenames.contracts.L2Resolver1?.address, - basenames.contracts.L2Resolver2?.address, - ] - .filter((address): address is Address => !!address) - .includes(resolverAddress); + return ( + [ + // the Basenames Default Resolvers are Onchain Static Resolvers + "L2Resolver1" in basenames.contracts && basenames.contracts.L2Resolver1.address, + "L2Resolver2" in basenames.contracts && basenames.contracts.L2Resolver2.address, + ] as Address[] + ).includes(resolverAddress); } return false; @@ -65,22 +65,22 @@ export function onchainStaticResolverImplementsDefaultAddress( ): boolean { // on ENS Root Chain if (chainId === rrRoot?.chain.id) { - return [ - // the DefaultPublicResolver3 (ENSIP-19 default PublicResolver) implements address defaulting - rrRoot.contracts.DefaultPublicResolver3?.address, - ] - .filter((address): address is Address => !!address) - .includes(resolverAddress); + return ( + [ + // the DefaultPublicResolver3 (ENSIP-19 default PublicResolver) implements address defaulting + rrRoot.contracts.DefaultPublicResolver3.address, + ] as Address[] + ).includes(resolverAddress); } // on Base Chain if (chainId === basenames?.chain.id) { - return [ - // the Basenames L2Resolver2 implements address defaulting - basenames.contracts.L2Resolver2?.address, - ] - .filter((address): address is Address => !!address) - .includes(resolverAddress); + return ( + [ + // the Basenames L2Resolver2 implements address defaulting + "L2Resolver2" in basenames.contracts && basenames.contracts.L2Resolver2.address, + ] as Address[] + ).includes(resolverAddress); } return false; diff --git a/apps/ensindexer/ponder/src/register-handlers.ts b/apps/ensindexer/ponder/src/register-handlers.ts index 874fedb02..20a398fdc 100644 --- a/apps/ensindexer/ponder/src/register-handlers.ts +++ b/apps/ensindexer/ponder/src/register-handlers.ts @@ -52,6 +52,10 @@ if (config.plugins.includes(PluginName.TokenScope)) { } // ENSv2 Plugin +// NOTE: Because the ENSv2 plugin depends on node migration logic in the ProtocolAcceleration plugin, +// it's important that ENSv2 handlers are registered _after_ Protocol Acceleration handlers. This +// ensures that the Protocol Acceleration handlers are executed first and the results of their node +// migration indexing is available for the identical handlers in the ENSv2 plugin. if (config.plugins.includes(PluginName.ENSv2)) { attach_ENSv2Handlers(); } diff --git a/apps/ensindexer/src/lib/datasource-helpers.ts b/apps/ensindexer/src/lib/datasource-helpers.ts index 1902ebeda..014b56c1c 100644 --- a/apps/ensindexer/src/lib/datasource-helpers.ts +++ b/apps/ensindexer/src/lib/datasource-helpers.ts @@ -1,4 +1,9 @@ -import { type DatasourceName, type ENSNamespaceId, maybeGetDatasource } from "@ensnode/datasources"; +import { + type Datasource, + type DatasourceName, + type ENSNamespaceId, + maybeGetDatasource, +} from "@ensnode/datasources"; import type { AccountId } from "@ensnode/ensnode-sdk"; /** @@ -15,12 +20,16 @@ import type { AccountId } from "@ensnode/ensnode-sdk"; * @returns The AccountId of the contract with the given namespace, datasource, * and contract name, or undefined if it is not found or is not a single AccountId */ -export const maybeGetDatasourceContract = ( - namespaceId: ENSNamespaceId, - datasourceName: DatasourceName, - contractName: string, +export const maybeGetDatasourceContract = < + N extends ENSNamespaceId, + D extends DatasourceName, + C extends string, +>( + namespaceId: N, + datasourceName: D, + contractName: C, ): AccountId | undefined => { - const datasource = maybeGetDatasource(namespaceId, datasourceName); + const datasource = maybeGetDatasource(namespaceId, datasourceName) as Datasource | undefined; if (!datasource) return undefined; const address = datasource.contracts[contractName]?.address; diff --git a/apps/ensindexer/src/lib/ensv2/is-name-wrapper.ts b/apps/ensindexer/src/lib/ensv2/is-name-wrapper.ts new file mode 100644 index 000000000..da849d48f --- /dev/null +++ b/apps/ensindexer/src/lib/ensv2/is-name-wrapper.ts @@ -0,0 +1,29 @@ +import { + DatasourceNames, + type ENSNamespaceId, + getDatasource, + maybeGetDatasource, +} from "@ensnode/datasources"; +import { type AccountId, accountIdEqual } from "@ensnode/ensnode-sdk"; + +/** + * + */ +export function isNameWrapper(namespace: ENSNamespaceId, contract: AccountId) { + const ensroot = getDatasource(namespace, DatasourceNames.ENSRoot); + const lineanames = maybeGetDatasource(namespace, DatasourceNames.Lineanames); + + const isRootNameWrapper = accountIdEqual(contract, { + chainId: ensroot.chain.id, + address: ensroot.contracts.NameWrapper.address, + }); + + const isLineanamesNameWrapper = + lineanames !== undefined && + accountIdEqual(contract, { + chainId: lineanames.chain.id, + address: lineanames.contracts.NameWrapper.address, + }); + + return isRootNameWrapper || isLineanamesNameWrapper; +} diff --git a/apps/ensindexer/src/lib/ensv2/resolver-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/resolver-db-helpers.ts deleted file mode 100644 index 3322af077..000000000 --- a/apps/ensindexer/src/lib/ensv2/resolver-db-helpers.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Context } from "ponder:registry"; -import schema from "ponder:schema"; - -import { type AccountId, makeResolverId, type ResolverId } from "@ensnode/ensnode-sdk"; - -export async function ensureResolver(context: Context, resolver: AccountId): Promise { - const id = makeResolverId(resolver); - await context.db - .insert(schema.resolver) - .values({ - id, - ...resolver, - }) - .onConflictDoNothing(); - return id; -} diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts index 76605baeb..ba4a17980 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts @@ -2,25 +2,28 @@ import config from "@/config"; import { type Context, ponder } from "ponder:registry"; import schema from "ponder:schema"; -import { type Address, isAddressEqual, zeroAddress } from "viem"; +import { type Address, isAddressEqual, zeroAddress, zeroHash } from "viem"; import { getRootRegistry, getRootRegistryId, type LabelHash, + makeENSv1DomainId, + makeImplicitRegistryId, + makeResolverId, makeSubdomainNode, type Node, PluginName, } from "@ensnode/ensnode-sdk"; +import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; +import { ensureUnknownLabel } from "@/lib/ensv2/label-db-helpers"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; -import { - removeNodeResolverRelation, - upsertNodeResolverRelation, -} from "@/lib/protocol-acceleration/node-resolver-relationship-db-helpers"; import { nodeIsMigrated } from "@/lib/protocol-acceleration/registry-migration-status"; +const pluginName = PluginName.ENSv2; + async function handleNewOwner({ context, event, @@ -34,7 +37,38 @@ async function handleNewOwner({ owner: Address; }>; }) { - // + const { label: labelHash, node: parentNode, owner } = event.args; + + // if someone mints a node to the zero address, nothing happens in the Registry, so no-op + if (isAddressEqual(zeroAddress, owner)) return; + + const node = makeSubdomainNode(labelHash, parentNode); + const registryId = + parentNode === zeroHash + ? getRootRegistryId(config.namespace) + : makeImplicitRegistryId(parentNode); + const domainId = makeENSv1DomainId(node); + + // this is either a NEW Domain OR the owner of the parent changing the owner of the child + + // TODO: import label healing logic from subgraph plugin + await ensureUnknownLabel(context, labelHash); + await context.db + .insert(schema.domain) + .values({ + id: domainId, + labelHash, + registryId, + }) + .onConflictDoNothing(); + + // TODO: if owner is special registrar, ignore + + // ensure owner account + await ensureAccount(context, owner); + + // update owner + await context.db.update(schema.domain, { id: domainId }).set({ ownerId: owner }); } async function handleNewResolver({ @@ -44,17 +78,49 @@ async function handleNewResolver({ context: Context; event: EventWithArgs<{ node: Node; resolver: Address }>; }) { - const { node, resolver: resolverAddress } = event.args; - const registry = event.log.address; - const isZeroResolver = isAddressEqual(zeroAddress, resolverAddress); + const { node, resolver: address } = event.args; - if (isZeroResolver) { - await removeNodeResolverRelation(context, registry, node); + const domainId = makeENSv1DomainId(node); + + // update domain's resolver + const isDeletion = isAddressEqual(address, zeroAddress); + if (isDeletion) { + await context.db.update(schema.domain, { id: domainId }).set({ resolverId: null }); } else { - await upsertNodeResolverRelation(context, registry, node, resolverAddress); + const resolverId = makeResolverId({ chainId: context.chain.id, address: address }); + await context.db.update(schema.domain, { id: domainId }).set({ resolverId }); } } +export async function handleTransfer({ + context, + event, +}: { + context: Context; + event: EventWithArgs<{ node: Node; owner: Address }>; +}) { + const { node, owner } = event.args; + + // ENSv2 model does not include root node, no-op + if (node === zeroHash) return; + + const domainId = makeENSv1DomainId(node); + + const isDeletion = isAddressEqual(zeroAddress, owner); + if (isDeletion) { + await context.db.delete(schema.domain, { id: domainId }); + return; + } + + // TODO: if owner is special registrar, ignore + + // ensure owner account + await ensureAccount(context, owner); + + // update owner + await context.db.update(schema.domain, { id: domainId }).set({ ownerId: owner }); +} + /** * Handler functions for ENSv1 Regsitry contracts. * - piggybacks Protocol Resolution plugin's Node Migration status @@ -63,7 +129,7 @@ export default function () { /** * Sets up the ENSv2 Root Registry */ - ponder.on(namespaceContract(PluginName.ENSv2, "ENSv1RegistryOld:setup"), async ({ context }) => { + ponder.on(namespaceContract(pluginName, "ENSv1RegistryOld:setup"), async ({ context }) => { // ensures that the Root Registry (which is eventually backed by the ENSv2 Root Registry) is // populated in the database await context.db @@ -81,7 +147,7 @@ export default function () { * - ENS Root Chain's ENSv1RegistryOld */ ponder.on( - namespaceContract(PluginName.ENSv2, "ENSv1RegistryOld:NewOwner"), + namespaceContract(pluginName, "ENSv1RegistryOld:NewOwner"), async ({ context, event }) => { const { label: labelHash, node: parentNode } = event.args; @@ -99,7 +165,7 @@ export default function () { * - ENS Root Chain's ENSv1RegistryOld */ ponder.on( - namespaceContract(PluginName.ENSv2, "ENSv1RegistryOld:NewResolver"), + namespaceContract(pluginName, "ENSv1RegistryOld:NewResolver"), async ({ context, event }) => { // ignore the event on ENSv1RegistryOld if node is migrated to new Registry const shouldIgnoreEvent = await nodeIsMigrated(context, event.args.node); @@ -109,22 +175,8 @@ export default function () { }, ); - /** - * Handles Registry#NewResolver for: - * - ENS Root Chain's ENSv1RegistryOld - */ - ponder.on( - namespaceContract(PluginName.ENSv2, "ENSv1RegistryOld:NewTTL"), - async ({ context, event }) => { - const shouldIgnoreEvent = await nodeIsMigrated(context, event.args.node); - if (shouldIgnoreEvent) return; - - return handleNewTTL({ context, event }); - }, - ); - ponder.on( - namespaceContract(PluginName.ENSv2, "ENSv1RegistryOld:Transfer"), + namespaceContract(pluginName, "ENSv1RegistryOld:Transfer"), async ({ context, event }) => { const shouldIgnoreEvent = await nodeIsMigrated(context, event.args.node); if (shouldIgnoreEvent) return; @@ -141,6 +193,5 @@ export default function () { */ ponder.on(namespaceContract(pluginName, "ENSv1Registry:NewOwner"), handleNewOwner); ponder.on(namespaceContract(pluginName, "ENSv1Registry:NewResolver"), handleNewResolver); - ponder.on(namespaceContract(pluginName, "ENSv1Registry:NewTTL"), handleNewTTL); ponder.on(namespaceContract(pluginName, "ENSv1Registry:Transfer"), handleTransfer); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts b/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts new file mode 100644 index 000000000..8337712ea --- /dev/null +++ b/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts @@ -0,0 +1 @@ +// diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts index 4538a9834..bf4a3ba4d 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts @@ -9,12 +9,12 @@ import { type LiteralLabel, makeENSv2DomainId, makeRegistryContractId, + makeResolverId, PluginName, } from "@ensnode/ensnode-sdk"; import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; import { ensureLabel } from "@/lib/ensv2/label-db-helpers"; -import { ensureResolver } from "@/lib/ensv2/resolver-db-helpers"; import { getThisAccountId } from "@/lib/get-this-account-id"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; @@ -75,9 +75,7 @@ export default function () { await ensureLabel(context, label); // insert Domain - await context.db - .insert(schema.domain) - .values({ id: domainId, registryId, labelHash, canonicalId }); + await context.db.insert(schema.domain).values({ id: domainId, registryId, labelHash }); // TODO: insert Registration entity for this domain as well: expiration, registrant // ensure Registrant @@ -139,11 +137,7 @@ export default function () { if (isDeletion) { await context.db.update(schema.domain, { id: domainId }).set({ resolverId: null }); } else { - const resolverId = await ensureResolver(context, { - chainId: context.chain.id, - address: address, - }); - + const resolverId = makeResolverId({ chainId: context.chain.id, address: address }); await context.db.update(schema.domain, { id: domainId }).set({ resolverId }); } }, diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index 7e9600f98..d8d22643d 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -3,7 +3,6 @@ import { type ChainConfig, createConfig } from "ponder"; import { type DatasourceName, DatasourceNames, - ENSNamespaceIds, EnhancedAccessControlABI, getDatasource, maybeGetDatasource, @@ -11,11 +10,7 @@ import { } from "@ensnode/datasources"; import { PluginName } from "@ensnode/ensnode-sdk"; -import { - createPlugin, - getDatasourceAsFullyDefinedAtCompileTime, - namespaceContract, -} from "@/lib/plugin-helpers"; +import { createPlugin, namespaceContract } from "@/lib/plugin-helpers"; import { chainConfigForContract, chainsConnectionConfig } from "@/lib/ponder-helpers"; /** @@ -28,39 +23,25 @@ const REQUIRED_DATASOURCE_NAMES = [ DatasourceNames.Namechain, ] as const satisfies DatasourceName[]; -const ALL_DATASOURCE_NAMES = [ - ...REQUIRED_DATASOURCE_NAMES, - DatasourceNames.Basenames, - DatasourceNames.Lineanames, -] as const satisfies DatasourceName[]; - export default createPlugin({ name: pluginName, requiredDatasourceNames: REQUIRED_DATASOURCE_NAMES, createPonderConfig(config) { // TODO: remove this, helps with types while only targeting ens-test-env - if (config.namespace !== ENSNamespaceIds.EnsTestEnv) throw new Error("only ens-test-env"); + if (config.namespace !== "ens-test-env" && config.namespace !== "mainnet") { + throw new Error("only ens-test-env and mainnet"); + } const ensroot = getDatasource(config.namespace, DatasourceNames.ENSRoot); - const namechain = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.Namechain, - ); - const basenames = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.Basenames, - ); - const lineanames = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.Lineanames, - ); + const namechain = maybeGetDatasource(config.namespace, DatasourceNames.Namechain); + const basenames = maybeGetDatasource(config.namespace, DatasourceNames.Basenames); + const lineanames = maybeGetDatasource(config.namespace, DatasourceNames.Lineanames); - const allDatasources = ALL_DATASOURCE_NAMES.map((datasourceName) => - maybeGetDatasource(config.namespace, datasourceName), - ).filter((datasource) => !!datasource); + if (!("Registry" in ensroot.contracts)) throw new Error(""); return createConfig({ - chains: allDatasources + chains: [ensroot, namechain, basenames, lineanames] + .filter((ds) => !!ds) .map((datasource) => datasource.chain) .reduce>( (memo, chain) => ({ @@ -73,31 +54,35 @@ export default createPlugin({ contracts: { [namespaceContract(pluginName, "Registry")]: { abi: RegistryABI, - chain: [ensroot, namechain].reduce( - (memo, datasource) => ({ - ...memo, - ...chainConfigForContract( - config.globalBlockrange, - datasource.chain.id, - datasource.contracts.Registry, - ), - }), - {}, - ), + chain: [ensroot, namechain] + .filter((ds) => !!ds) + .reduce( + (memo, datasource) => ({ + ...memo, + ...chainConfigForContract( + config.globalBlockrange, + datasource.chain.id, + datasource.contracts.Registry, + ), + }), + {}, + ), }, [namespaceContract(pluginName, "EnhancedAccessControl")]: { abi: EnhancedAccessControlABI, - chain: [ensroot, namechain].reduce( - (memo, datasource) => ({ - ...memo, - ...chainConfigForContract( - config.globalBlockrange, - datasource.chain.id, - datasource.contracts.EnhancedAccessControl, - ), - }), - {}, - ), + chain: [ensroot, namechain] + .filter((ds) => !!ds) + .reduce( + (memo, datasource) => ({ + ...memo, + ...chainConfigForContract( + config.globalBlockrange, + datasource.chain.id, + datasource.contracts.EnhancedAccessControl, + ), + }), + {}, + ), }, // index the ENSv1RegistryOld on ENS Root Chain @@ -138,6 +123,26 @@ export default createPlugin({ )), }, }, + + // index NameWrapper on ENS Root Chain, Lineanames + [namespaceContract(pluginName, "NameWrapper")]: { + abi: ensroot.contracts.NameWrapper.abi, + chain: { + // ENS Root Chain NameWrapper + ...chainConfigForContract( + config.globalBlockrange, + ensroot.chain.id, + ensroot.contracts.NameWrapper, + ), + // Lineanames NameWrapper if defined + ...(lineanames && + chainConfigForContract( + config.globalBlockrange, + lineanames.chain.id, + lineanames.contracts.NameWrapper, + )), + }, + }, }, }); }, diff --git a/apps/ensindexer/src/plugins/subgraph/shared-handlers/NameWrapper.ts b/apps/ensindexer/src/plugins/subgraph/shared-handlers/NameWrapper.ts index 6303d242a..5d1e22caf 100644 --- a/apps/ensindexer/src/plugins/subgraph/shared-handlers/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/subgraph/shared-handlers/NameWrapper.ts @@ -122,7 +122,7 @@ async function materializeDomainExpiryDate(context: Context, node: Node) { /** * makes a set of shared handlers for the NameWrapper contract * - * @param registrarManagedName the name that the Registrar that NameWrapper interacts with registers subnames of + * @param registrarManagedName the name of the Registrar that NameWrapper interacts with registers subnames of */ export const makeNameWrapperHandlers = ({ registrarManagedName, diff --git a/packages/datasources/src/lib/types.ts b/packages/datasources/src/lib/types.ts index a98ed844e..767951e98 100644 --- a/packages/datasources/src/lib/types.ts +++ b/packages/datasources/src/lib/types.ts @@ -108,3 +108,31 @@ export type ContractConfig = { export type ENSNamespace = { [DatasourceNames.ENSRoot]: Datasource; } & Partial, Datasource>>; + +/** + * Helper type to merge multiple types into one. + */ +type MergedTypes = (T extends any ? (x: T) => void : never) extends (x: infer R) => void + ? R + : never; + +/** + * Preserves the chain union while merging contracts from multiple objects + */ +export type MergeNamespaces = T extends ENSNamespace + ? { + chain: T extends { chain: infer C } ? C : never; + contracts: T extends { [DatasourceNames.ENSRoot]: { contracts: infer C } } + ? MergedTypes + : never; + } + : never; + +/** + * Helper type to extract the datasource type for a specific datasource name across all namespaces. + * Returns the union of all possible datasource types for that datasource name, or never if not found. + */ +export type ExtractDatasourceType< + Namespaces extends ENSNamespace, + D extends DatasourceName, +> = Namespaces extends any ? (D extends keyof Namespaces ? Namespaces[D] : never) : never; diff --git a/packages/datasources/src/namespaces.ts b/packages/datasources/src/namespaces.ts index 636daf7d4..ecf99bfb8 100644 --- a/packages/datasources/src/namespaces.ts +++ b/packages/datasources/src/namespaces.ts @@ -1,11 +1,10 @@ import ensTestEnv from "./ens-test-env"; import holesky from "./holesky"; import { - type Datasource, type DatasourceName, DatasourceNames, - type ENSNamespace, type ENSNamespaceId, + type ExtractDatasourceType, } from "./lib/types"; import mainnet from "./mainnet"; import sepolia from "./sepolia"; @@ -49,7 +48,7 @@ export const getDatasource = < >( namespaceId: N, datasourceName: D, -): ReturnType>[D] => getENSNamespace(namespaceId)[datasourceName]; +) => getENSNamespace(namespaceId)[datasourceName]; /** * Returns the `datasourceName` Datasource within the specified `namespaceId` namespace, or undefined @@ -59,16 +58,20 @@ export const getDatasource = < * or may not actually be defined. For example, if using {@link getDatasource}, with a * `namespaceId: ENSNamespaceId`, the typechecker will enforce that the only valid `datasourceName` * is ENSRoot (the only Datasource present in all namespaces). This method allows you to receive - * `Datasource | undefined` for a specified `datasourceName`. + * the const Datasource or undefined for a specified `datasourceName`. * * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', 'ens-test-env') * @param datasourceName - The name of the Datasource to retrieve * @returns The Datasource object for the given name within the specified namespace, or undefined if it does not exist */ -export const maybeGetDatasource = ( - namespaceId: ENSNamespaceId, - datasourceName: DatasourceName, -): Datasource | undefined => (getENSNamespace(namespaceId) as ENSNamespace)[datasourceName]; +export const maybeGetDatasource = < + N extends ENSNamespaceId, + D extends DatasourceName = DatasourceName, +>( + namespaceId: N, + datasourceName: D, +): ExtractDatasourceType>, D> | undefined => + (getENSNamespace(namespaceId) as any)[datasourceName]; /** * Returns the chain for the ENS Root Datasource within the selected namespace. diff --git a/packages/datasources/src/sepolia.ts b/packages/datasources/src/sepolia.ts index d06918fb8..d701f0faf 100644 --- a/packages/datasources/src/sepolia.ts +++ b/packages/datasources/src/sepolia.ts @@ -18,6 +18,9 @@ import { BaseRegistrar as linea_BaseRegistrar } from "./abis/lineanames/BaseRegi import { EthRegistrarController as linea_EthRegistrarController } from "./abis/lineanames/EthRegistrarController"; import { NameWrapper as linea_NameWrapper } from "./abis/lineanames/NameWrapper"; import { Registry as linea_Registry } from "./abis/lineanames/Registry"; +// ABIs for Namechain +import { EnhancedAccessControl } from "./abis/namechain/EnhancedAccessControl"; +import { Registry } from "./abis/namechain/Registry"; // ABIs for ENSRoot Datasource import { BaseRegistrar as root_BaseRegistrar } from "./abis/root/BaseRegistrar"; import { LegacyEthRegistrarController as root_LegacyEthRegistrarController } from "./abis/root/LegacyEthRegistrarController"; @@ -92,6 +95,34 @@ export default { address: "0xb7b7dadf4d42a08b3ec1d3a1079959dfbc8cffcc", startBlock: 8515717, }, + + RootRegistry: { + abi: Registry, + address: "0x8a791620dd6260079bf849dc5567adc3f2fdc318", + startBlock: 0, + }, + Registry: { + abi: Registry, + startBlock: 0, + }, + EnhancedAccessControl: { + abi: EnhancedAccessControl, + startBlock: 0, + }, + }, + }, + + [DatasourceNames.Namechain]: { + chain: sepolia, + contracts: { + Registry: { + abi: Registry, + startBlock: 0, + }, + EnhancedAccessControl: { + abi: EnhancedAccessControl, + startBlock: 0, + }, }, }, diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/schemas/ensv2.schema.ts index 8de97fa63..d59382745 100644 --- a/packages/ensnode-schema/src/schemas/ensv2.schema.ts +++ b/packages/ensnode-schema/src/schemas/ensv2.schema.ts @@ -2,7 +2,6 @@ import { onchainEnum, onchainTable, relations, uniqueIndex } from "ponder"; import type { Address } from "viem"; import type { - CanonicalId, ChainId, DomainId, InterpretedLabel, @@ -90,9 +89,6 @@ export const domain = onchainTable( // belongs to registry registryId: t.text().notNull().$type(), - - // TODO: we could probably avoid storing this at all and compute it on-demand - canonicalId: t.bigint().notNull().$type(), labelHash: t.hex().notNull().$type(), // may have an owner diff --git a/packages/ensnode-sdk/src/ensv2/ids-lib.ts b/packages/ensnode-sdk/src/ensv2/ids-lib.ts index 6e51f546f..16dfa81f1 100644 --- a/packages/ensnode-sdk/src/ensv2/ids-lib.ts +++ b/packages/ensnode-sdk/src/ensv2/ids-lib.ts @@ -10,7 +10,9 @@ import { import type { CanonicalId, + ENSv1DomainId, ENSv2DomainId, + ImplicitRegistryId, PermissionsId, PermissionsResourceId, PermissionsUserId, @@ -25,6 +27,16 @@ import type { export const makeRegistryContractId = (accountId: AccountId) => serializeAccountId(accountId) as RegistryContractId; +/** + * Brands a node as an ImplicitRegistryId. + */ +export const makeImplicitRegistryId = (node: Node) => node as ImplicitRegistryId; + +/** + * Makes an ENSv1 Domain Id given the ENSv1 Domain's `node` + */ +export const makeENSv1DomainId = (node: Node) => node as ENSv1DomainId; + /** * Makes an ENSv2 Domain Id given the parent `registry` and the domain's `canonicalId`. */ diff --git a/packages/ensnode-sdk/src/shared/datasources-with-resolvers.ts b/packages/ensnode-sdk/src/shared/datasources-with-resolvers.ts index d152964bb..3e534e6db 100644 --- a/packages/ensnode-sdk/src/shared/datasources-with-resolvers.ts +++ b/packages/ensnode-sdk/src/shared/datasources-with-resolvers.ts @@ -28,7 +28,7 @@ export const getDatasourcesWithResolvers = ( maybeGetDatasource(namespace, datasourceName), ) .filter((datasource) => !!datasource) - .filter((datasource): datasource is DatasourceWithResolverContract => { + .filter((datasource) => { // all of the relevant datasources provide a Resolver ContractConfig with a `startBlock` if (!datasource.contracts.Resolver) { console.warn( diff --git a/packages/ensnode-sdk/src/shared/root-registry.ts b/packages/ensnode-sdk/src/shared/root-registry.ts index e7d14f290..17c175fd9 100644 --- a/packages/ensnode-sdk/src/shared/root-registry.ts +++ b/packages/ensnode-sdk/src/shared/root-registry.ts @@ -5,11 +5,12 @@ import { type AccountId, accountIdEqual, makeRegistryContractId } from "@ensnode * TODO */ export const getRootRegistry = (namespace: ENSNamespaceId) => { - // TODO: remove, helps types while implementing - if (namespace !== "ens-test-env") throw new Error("nope"); - const ensroot = getDatasource(namespace, DatasourceNames.ENSRoot); + // TODO: remove when all defined + if (!("RootRegistry" in ensroot.contracts)) + throw new Error(`Namespace ${namespace} does not define ENSv2 Root Registry.`); + return { chainId: ensroot.chain.id, address: ensroot.contracts.RootRegistry.address, From a9ebd3e4f40fce4238586c1151e9bce86ea17c35 Mon Sep 17 00:00:00 2001 From: shrugs Date: Wed, 12 Nov 2025 13:32:43 -0600 Subject: [PATCH 014/102] checkpoint: fixed typings for datasource package --- apps/ensindexer/src/config/derived-params.ts | 9 +- apps/ensindexer/src/config/validations.ts | 32 ++- apps/ensindexer/src/lib/dns-helpers.test.ts | 2 +- apps/ensindexer/src/lib/plugin-helpers.ts | 47 +--- apps/ensindexer/src/lib/ponder-helpers.ts | 79 +++++- apps/ensindexer/src/plugins/ensv2/plugin.ts | 63 +++-- .../plugins/protocol-acceleration/plugin.ts | 108 +++----- .../src/plugins/registrars/plugin.ts | 257 +++++++++--------- .../subgraph/plugins/basenames/plugin.ts | 27 +- .../subgraph/plugins/lineanames/plugin.ts | 27 +- .../subgraph/plugins/subgraph/plugin.ts | 27 +- .../subgraph/plugins/threedns/plugin.ts | 53 ++-- .../src/plugins/tokenscope/plugin.ts | 81 +++--- packages/datasources/src/lib/types.ts | 16 +- 14 files changed, 405 insertions(+), 423 deletions(-) diff --git a/apps/ensindexer/src/config/derived-params.ts b/apps/ensindexer/src/config/derived-params.ts index 78d014ae8..c6713e3e6 100644 --- a/apps/ensindexer/src/config/derived-params.ts +++ b/apps/ensindexer/src/config/derived-params.ts @@ -1,7 +1,7 @@ +import { type ENSNamespace, getENSNamespace } from "@ensnode/datasources"; import type { ChainId } from "@ensnode/ensnode-sdk"; import type { ENSIndexerConfig } from "@/config/types"; -import { getENSNamespaceAsFullyDefinedAtCompileTime } from "@/lib/plugin-helpers"; import { getPlugin } from "@/plugins"; /** @@ -18,15 +18,14 @@ export const derive_indexedChainIds = < ): CONFIG & { indexedChainIds: ENSIndexerConfig["indexedChainIds"] } => { const indexedChainIds = new Set(); - const datasources = getENSNamespaceAsFullyDefinedAtCompileTime(config.namespace); + const datasources = getENSNamespace(config.namespace) as ENSNamespace; for (const pluginName of config.plugins) { const datasourceNames = getPlugin(pluginName).requiredDatasourceNames; for (const datasourceName of datasourceNames) { - const { chain } = datasources[datasourceName]; - - indexedChainIds.add(chain.id); + const datasource = datasources[datasourceName]; + if (datasource) indexedChainIds.add(datasource.chain.id); } } diff --git a/apps/ensindexer/src/config/validations.ts b/apps/ensindexer/src/config/validations.ts index 6d5e36b5c..c224039a0 100644 --- a/apps/ensindexer/src/config/validations.ts +++ b/apps/ensindexer/src/config/validations.ts @@ -1,10 +1,14 @@ import { type Address, isAddress } from "viem"; import type { z } from "zod/v4"; -import type { DatasourceName } from "@ensnode/datasources"; +import { + type DatasourceName, + type ENSNamespace, + getENSNamespace, + maybeGetDatasource, +} from "@ensnode/datasources"; import { asLowerCaseAddress, PluginName, uniq } from "@ensnode/ensnode-sdk"; -import { getENSNamespaceAsFullyDefinedAtCompileTime } from "@/lib/plugin-helpers"; import { getPlugin } from "@/plugins"; import type { ENSIndexerConfig } from "./types"; @@ -18,7 +22,7 @@ export function invariant_requiredDatasources( ) { const { value: config } = ctx; - const datasources = getENSNamespaceAsFullyDefinedAtCompileTime(config.namespace); + const datasources = getENSNamespace(config.namespace); const availableDatasourceNames = Object.keys(datasources) as DatasourceName[]; // validate that each active plugin's requiredDatasources are available in availableDatasourceNames @@ -50,19 +54,18 @@ export function invariant_rpcConfigsSpecifiedForIndexedChains( ) { const { value: config } = ctx; - const datasources = getENSNamespaceAsFullyDefinedAtCompileTime(config.namespace); - for (const pluginName of config.plugins) { const datasourceNames = getPlugin(pluginName).requiredDatasourceNames; for (const datasourceName of datasourceNames) { - const { chain } = datasources[datasourceName]; + const datasource = maybeGetDatasource(config.namespace, datasourceName); + if (!datasource) continue; // ignore undefined datasources, caught by requiredDatasources invariant - if (!config.rpcConfigs.has(chain.id)) { + if (!config.rpcConfigs.has(datasource.chain.id)) { ctx.issues.push({ code: "custom", input: config, - message: `Plugin '${pluginName}' indexes chain with id ${chain.id} but RPC_URL_${chain.id} is not specified.`, + message: `Plugin '${pluginName}' indexes chain with id ${datasource.chain.id} but RPC_URL_${datasource.chain.id} is not specified.`, }); } } @@ -77,11 +80,12 @@ export function invariant_globalBlockrange( const { globalBlockrange } = config; if (globalBlockrange.startBlock !== undefined || globalBlockrange.endBlock !== undefined) { - const datasources = getENSNamespaceAsFullyDefinedAtCompileTime(config.namespace); + const datasources = getENSNamespace(config.namespace) as ENSNamespace; const indexedChainIds = uniq( config.plugins .flatMap((pluginName) => getPlugin(pluginName).requiredDatasourceNames) .map((datasourceName) => datasources[datasourceName]) + .filter((ds) => !!ds) // ignore undefined datasources, caught by requiredDatasources invariant .map((datasource) => datasource.chain.id), ); @@ -112,12 +116,14 @@ export function invariant_validContractConfigs( ) { const { value: config } = ctx; - const datasources = getENSNamespaceAsFullyDefinedAtCompileTime(config.namespace); - for (const datasourceName of Object.keys(datasources) as DatasourceName[]) { - const { contracts } = datasources[datasourceName]; + const datasources = getENSNamespace(config.namespace) as ENSNamespace; + const datasourceNames = Object.keys(datasources) as DatasourceName[]; + for (const datasourceName of datasourceNames) { + const datasource = datasources[datasourceName]; + if (!datasource) continue; // ignore undefined datasources, caught by requiredDatasources invariant // Invariant: `contracts` must provide valid addresses if a filter is not provided - for (const [contractName, contractConfig] of Object.entries(contracts)) { + for (const [contractName, contractConfig] of Object.entries(datasource.contracts)) { if ("address" in contractConfig && typeof contractConfig.address === "string") { // only ContractConfigs with `address` defined const isValidAddress = diff --git a/apps/ensindexer/src/lib/dns-helpers.test.ts b/apps/ensindexer/src/lib/dns-helpers.test.ts index 6ac10f464..38eaac0a1 100644 --- a/apps/ensindexer/src/lib/dns-helpers.test.ts +++ b/apps/ensindexer/src/lib/dns-helpers.test.ts @@ -16,7 +16,7 @@ import { // Example TXT `record` representing key: 'com.twitter', value: '0xTko' // via: https://optimistic.etherscan.io/tx/0xf32db67e7bf2118ea2c3dd8f40fc48d18e83a4a2317fbbddce8f741e30a1e8d7#eventlog const { args } = decodeEventLog({ - abi: getDatasource("mainnet", "threedns-base").contracts.Resolver.abi, + abi: getDatasource("mainnet", "threednsBase").contracts.Resolver.abi, topics: [ "0xaaac3b4b3e6807b5b4585562beabaa2de9bd07db514a1eba2c11d1af5b9d9dc7", "0x6470e2677db6a5bb6c69e51fce7271aeeb5f2808ea7dfdf34b703749555b3e10", diff --git a/apps/ensindexer/src/lib/plugin-helpers.ts b/apps/ensindexer/src/lib/plugin-helpers.ts index d0bf8cf19..f1f8638a6 100644 --- a/apps/ensindexer/src/lib/plugin-helpers.ts +++ b/apps/ensindexer/src/lib/plugin-helpers.ts @@ -1,10 +1,9 @@ import type { createConfig as createPonderConfig } from "ponder"; -import { type DatasourceName, type ENSNamespaceId, getENSNamespace } from "@ensnode/datasources"; +import type { DatasourceName } from "@ensnode/datasources"; import { PluginName, uniq } from "@ensnode/ensnode-sdk"; import type { ENSIndexerConfig } from "@/config/types"; -import type { MergedTypes } from "@/lib/lib-helpers"; /** * Creates a namespaced contract name for Ponder handlers. @@ -84,50 +83,6 @@ type PonderConfigResult< BLOCKS extends object = {}, > = ReturnType>; -/** - * ENSNamespaceFullyDefinedAtCompileTime is a helper type necessary to support runtime-conditional - * Ponder plugins. - * - * 1. ENSNode can be configured to index in the context of different ENS namespaces, - * (currently: mainnet, sepolia, holesky, ens-test-env), using a user-specified set of plugins. - * 2. Ponder's inferred type-checking requires const-typed values, and so those plugins must be able - * to define their Ponder config statically so the types can be inferred at compile-time, regardless - * of whether the plugin's config and handler logic is loaded/executed at runtime. - * 3. To make this work, we provide a ENSNamespaceFullyDefinedAtCompileTime, set to the typeof mainnet's - * ENSNamespace, which fully defines all known Datasources (if this is ever not the case, a merged - * type can be used to ensure that this type has the full set of possible Datasources). Plugins - * can use the runtime value returned from {@link getENSNamespaceAsFullyDefinedAtCompileTime} and - * by casting it to ENSNamespaceFullyDefinedAtCompileTime we ensure that the values expected by - * those plugins pass the typechecker. ENSNode ensures that non-active plugins are not executed, - * so the issue of type/value mismatch does not occur during execution. - */ -type ENSNamespaceFullyDefinedAtCompileTime = ReturnType>; - -/** - * Returns the ENSNamespace for the provided `namespaceId`, cast to ENSNamespaceFullyDefinedAtCompileTime. - * - * See {@link ENSNamespaceFullyDefinedAtCompileTime} for more info. - * - * @param namespaceId - The ENS namespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', 'ens-test-env') - * @returns the ENSNamespace - */ -export const getENSNamespaceAsFullyDefinedAtCompileTime = (namespaceId: ENSNamespaceId) => - getENSNamespace(namespaceId) as ENSNamespaceFullyDefinedAtCompileTime; - -/** - * Returns the `datasourceName` Datasource within the `namespaceId` namespace, cast as ENSNamespaceFullyDefinedAtCompileTime. - * - * NOTE: the typescript typechecker will _not_ enforce validity. i.e. using an invalid `datasourceName` - * within the specified `namespaceId` will have a valid return type but be undefined at runtime. - */ -export const getDatasourceAsFullyDefinedAtCompileTime = < - N extends ENSNamespaceId, - D extends keyof ENSNamespaceFullyDefinedAtCompileTime, ->( - namespaceId: N, - datasourceName: D, -) => getENSNamespaceAsFullyDefinedAtCompileTime(namespaceId)[datasourceName]; - /** * Options type for `buildPlugin` function input. * diff --git a/apps/ensindexer/src/lib/ponder-helpers.ts b/apps/ensindexer/src/lib/ponder-helpers.ts index ae6842b64..c03ea0368 100644 --- a/apps/ensindexer/src/lib/ponder-helpers.ts +++ b/apps/ensindexer/src/lib/ponder-helpers.ts @@ -9,8 +9,14 @@ import type { ChainConfig } from "ponder"; import type { Address, PublicClient } from "viem"; import * as z from "zod/v4"; -import { type ContractConfig, ensTestEnvL1Chain, ensTestEnvL2Chain } from "@ensnode/datasources"; -import type { Blockrange, ChainId } from "@ensnode/ensnode-sdk"; +import { + type ContractConfig, + type DatasourceName, + ensTestEnvL1Chain, + ensTestEnvL2Chain, + maybeGetDatasource, +} from "@ensnode/datasources"; +import type { Blockrange, ChainId, ENSNamespaceId } from "@ensnode/ensnode-sdk"; import type { BlockInfo, PonderStatus } from "@ensnode/ponder-metadata"; import type { ENSIndexerConfig } from "@/config/types"; @@ -320,6 +326,75 @@ export function chainConfigForContract( }; } +/** + * TODO + */ +export function chainsConnectionConfigForDatasources( + namespace: ENSNamespaceId, + rpcConfigs: ENSIndexerConfig["rpcConfigs"], + datasourceNames: DatasourceName[], +) { + return datasourceNames + .map((datasourceName) => maybeGetDatasource(namespace, datasourceName)) + .filter((ds) => !!ds) + .map((datasource) => datasource.chain) + .reduce>( + (memo, chain) => ({ + ...memo, + ...chainsConnectionConfig(rpcConfigs, chain.id), + }), + {}, + ); +} + +type MapOfRequiredDatasources< + N extends ENSNamespaceId, + DATASOURCE_NAMES extends readonly DatasourceName[], +> = { + [K in DATASOURCE_NAMES[number]]: Exclude>, undefined>; +}; + +type MapOfMaybeDatasources< + N extends ENSNamespaceId, + DATASOURCE_NAMES extends readonly DatasourceName[], +> = { + [K in DATASOURCE_NAMES[number]]: ReturnType>; +}; + +/** + * TODO + */ +export function getRequiredDatasources< + N extends ENSNamespaceId, + DATASOURCE_NAMES extends DatasourceName[], +>(namespace: N, datasourceNames: DATASOURCE_NAMES) { + return Object.fromEntries( + datasourceNames.map((datasourceName) => { + const datasource = maybeGetDatasource(namespace, datasourceName); + if (!datasource) { + throw new Error( + `Required datasource "${datasourceName}" not found for namespace "${namespace}"`, + ); + } + return [datasourceName, datasource] as const; + }), + ) as MapOfRequiredDatasources; +} + +/** + * TODO + */ +export function maybeGetDatasources< + N extends ENSNamespaceId, + DATASOURCE_NAMES extends DatasourceName[], +>(namespace: N, datasourceNames: DATASOURCE_NAMES) { + return Object.fromEntries( + datasourceNames.map( + (datasourceName) => [datasourceName, maybeGetDatasource(namespace, datasourceName)] as const, + ), + ) as MapOfMaybeDatasources; +} + /** * Merges a set of ContractConfigs representing contracts at specific addresses on the same chain. * Uses the lowest startBlock to ensure all events are indexed. diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index d8d22643d..d18bc5893 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -1,27 +1,32 @@ -import { type ChainConfig, createConfig } from "ponder"; +/** + * TODO + */ -import { - type DatasourceName, - DatasourceNames, - EnhancedAccessControlABI, - getDatasource, - maybeGetDatasource, - RegistryABI, -} from "@ensnode/datasources"; +import { createConfig } from "ponder"; + +import { DatasourceNames, EnhancedAccessControlABI, RegistryABI } from "@ensnode/datasources"; import { PluginName } from "@ensnode/ensnode-sdk"; import { createPlugin, namespaceContract } from "@/lib/plugin-helpers"; -import { chainConfigForContract, chainsConnectionConfig } from "@/lib/ponder-helpers"; - -/** +import { + chainConfigForContract, + chainsConnectionConfigForDatasources, + getRequiredDatasources, + maybeGetDatasources, +} from "@/lib/ponder-helpers"; - */ export const pluginName = PluginName.ENSv2; const REQUIRED_DATASOURCE_NAMES = [ - DatasourceNames.ENSRoot, + DatasourceNames.ENSRoot, // DatasourceNames.Namechain, -] as const satisfies DatasourceName[]; +]; + +const ALL_DATASOURCE_NAMES = [ + ...REQUIRED_DATASOURCE_NAMES, + DatasourceNames.Basenames, + DatasourceNames.Lineanames, +]; export default createPlugin({ name: pluginName, @@ -32,24 +37,22 @@ export default createPlugin({ throw new Error("only ens-test-env and mainnet"); } - const ensroot = getDatasource(config.namespace, DatasourceNames.ENSRoot); - const namechain = maybeGetDatasource(config.namespace, DatasourceNames.Namechain); - const basenames = maybeGetDatasource(config.namespace, DatasourceNames.Basenames); - const lineanames = maybeGetDatasource(config.namespace, DatasourceNames.Lineanames); + const { + ensroot, // + namechain, + } = getRequiredDatasources(config.namespace, REQUIRED_DATASOURCE_NAMES); - if (!("Registry" in ensroot.contracts)) throw new Error(""); + const { + basenames, // + lineanames, + } = maybeGetDatasources(config.namespace, ALL_DATASOURCE_NAMES); return createConfig({ - chains: [ensroot, namechain, basenames, lineanames] - .filter((ds) => !!ds) - .map((datasource) => datasource.chain) - .reduce>( - (memo, chain) => ({ - ...memo, - ...chainsConnectionConfig(config.rpcConfigs, chain.id), - }), - {}, - ), + chains: chainsConnectionConfigForDatasources( + config.namespace, + config.rpcConfigs, + ALL_DATASOURCE_NAMES, + ), contracts: { [namespaceContract(pluginName, "Registry")]: { diff --git a/apps/ensindexer/src/plugins/protocol-acceleration/plugin.ts b/apps/ensindexer/src/plugins/protocol-acceleration/plugin.ts index 8b2bf4ccb..7636a6adc 100644 --- a/apps/ensindexer/src/plugins/protocol-acceleration/plugin.ts +++ b/apps/ensindexer/src/plugins/protocol-acceleration/plugin.ts @@ -1,9 +1,7 @@ -import { type ChainConfig, createConfig } from "ponder"; +import { createConfig } from "ponder"; import { - type DatasourceName, DatasourceNames, - getDatasource, ResolverABI, StandaloneReverseRegistrarABI, ThreeDNSTokenABI, @@ -14,15 +12,13 @@ import { getDatasourcesWithResolvers, } from "@ensnode/ensnode-sdk/internal"; -import { - createPlugin, - getDatasourceAsFullyDefinedAtCompileTime, - namespaceContract, -} from "@/lib/plugin-helpers"; +import { createPlugin, namespaceContract } from "@/lib/plugin-helpers"; import { chainConfigForContract, - chainsConnectionConfig, + chainsConnectionConfigForDatasources, constrainBlockrange, + getRequiredDatasources, + maybeGetDatasources, } from "@/lib/ponder-helpers"; /** @@ -46,75 +42,39 @@ const DATASOURCE_NAMES_WITH_REVERSE_RESOLVERS = [ DatasourceNames.ReverseResolverOptimism, DatasourceNames.ReverseResolverArbitrum, DatasourceNames.ReverseResolverScroll, -] as const satisfies DatasourceName[]; +]; const ALL_DATASOURCE_NAMES = [ ...DATASOURCE_NAMES_WITH_RESOLVERS, ...DATASOURCE_NAMES_WITH_REVERSE_RESOLVERS, -] as const satisfies DatasourceName[]; +]; + +const REQUIRED_DATASOURCE_NAMES = [DatasourceNames.ENSRoot]; export default createPlugin({ name: pluginName, - requiredDatasourceNames: [DatasourceNames.ENSRoot], + requiredDatasourceNames: REQUIRED_DATASOURCE_NAMES, createPonderConfig(config) { - const allDatasources = ALL_DATASOURCE_NAMES.map((datasourceName) => - getDatasourceAsFullyDefinedAtCompileTime(config.namespace, datasourceName), - ).filter((datasource) => !!datasource); - - const ensroot = getDatasource(config.namespace, DatasourceNames.ENSRoot); - const basenames = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.Basenames, - ); - const lineanames = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.Lineanames, - ); - const threeDNSOptimism = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.ThreeDNSOptimism, - ); - const threeDNSBase = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.ThreeDNSBase, - ); - - const rrRoot = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.ReverseResolverRoot, - ); - const rrBase = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.ReverseResolverBase, - ); - const rrLinea = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.ReverseResolverLinea, - ); - const rrOptimism = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.ReverseResolverOptimism, - ); - const rrArbitrum = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.ReverseResolverArbitrum, - ); - const rrScroll = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.ReverseResolverScroll, - ); + const { ensroot } = getRequiredDatasources(config.namespace, REQUIRED_DATASOURCE_NAMES); + const { + basenames, + lineanames, + threednsOptimism, + threednsBase, + rrRoot, + rrBase, + rrLinea, + rrOptimism, + rrArbitrum, + rrScroll, + } = maybeGetDatasources(config.namespace, ALL_DATASOURCE_NAMES); return createConfig({ - chains: allDatasources - .map((datasource) => datasource.chain) - .reduce>( - (memo, chain) => ({ - ...memo, - ...chainsConnectionConfig(config.rpcConfigs, chain.id), - }), - {}, - ), - + chains: chainsConnectionConfigForDatasources( + config.namespace, + config.rpcConfigs, + ALL_DATASOURCE_NAMES, + ), contracts: { // a multi-chain Resolver ContractConfig [namespaceContract(pluginName, "Resolver")]: { @@ -174,17 +134,17 @@ export default createPlugin({ [namespaceContract(pluginName, "ThreeDNSToken")]: { abi: ThreeDNSTokenABI, chain: { - ...(threeDNSOptimism && + ...(threednsOptimism && chainConfigForContract( config.globalBlockrange, - threeDNSOptimism.chain.id, - threeDNSOptimism.contracts.ThreeDNSToken, + threednsOptimism.chain.id, + threednsOptimism.contracts.ThreeDNSToken, )), - ...(threeDNSBase && + ...(threednsBase && chainConfigForContract( config.globalBlockrange, - threeDNSBase.chain.id, - threeDNSBase.contracts.ThreeDNSToken, + threednsBase.chain.id, + threednsBase.contracts.ThreeDNSToken, )), }, }, diff --git a/apps/ensindexer/src/plugins/registrars/plugin.ts b/apps/ensindexer/src/plugins/registrars/plugin.ts index 164b4fe44..6150ee4b0 100644 --- a/apps/ensindexer/src/plugins/registrars/plugin.ts +++ b/apps/ensindexer/src/plugins/registrars/plugin.ts @@ -7,157 +7,146 @@ * - Lineanames */ -import * as ponder from "ponder"; +import { createConfig } from "ponder"; import { DatasourceNames } from "@ensnode/datasources"; import { PluginName } from "@ensnode/ensnode-sdk"; +import { createPlugin, namespaceContract } from "@/lib/plugin-helpers"; import { - createPlugin, - getDatasourceAsFullyDefinedAtCompileTime, - namespaceContract, -} from "@/lib/plugin-helpers"; -import { chainConfigForContract, chainsConnectionConfig } from "@/lib/ponder-helpers"; + chainConfigForContract, + chainsConnectionConfigForDatasources, + getRequiredDatasources, +} from "@/lib/ponder-helpers"; const pluginName = PluginName.Registrars; +const REQUIRED_DATASOURCE_NAMES = [ + DatasourceNames.ENSRoot, + DatasourceNames.Basenames, + DatasourceNames.Lineanames, +]; + export default createPlugin({ name: pluginName, - requiredDatasourceNames: [ - DatasourceNames.ENSRoot, - DatasourceNames.Basenames, - DatasourceNames.Lineanames, - ], + requiredDatasourceNames: REQUIRED_DATASOURCE_NAMES, createPonderConfig(config) { - // configure Ethnames dependencies - const ethnamesDatasource = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.ENSRoot, - ); - - const ethnamesRegistrarContracts = { - [namespaceContract(pluginName, "Ethnames_BaseRegistrar")]: { - chain: chainConfigForContract( - config.globalBlockrange, - ethnamesDatasource.chain.id, - ethnamesDatasource.contracts.BaseRegistrar, - ), - abi: ethnamesDatasource.contracts.BaseRegistrar.abi, - }, - }; + const { + ensroot: ethnames, + basenames, + lineanames, + } = getRequiredDatasources(config.namespace, REQUIRED_DATASOURCE_NAMES); - const ethnamesRegistrarControllerContracts = { - [namespaceContract(pluginName, "Ethnames_LegacyEthRegistrarController")]: { - chain: chainConfigForContract( - config.globalBlockrange, - ethnamesDatasource.chain.id, - ethnamesDatasource.contracts.LegacyEthRegistrarController, - ), - abi: ethnamesDatasource.contracts.LegacyEthRegistrarController.abi, - }, - [namespaceContract(pluginName, "Ethnames_WrappedEthRegistrarController")]: { - chain: chainConfigForContract( - config.globalBlockrange, - ethnamesDatasource.chain.id, - ethnamesDatasource.contracts.WrappedEthRegistrarController, - ), - abi: ethnamesDatasource.contracts.WrappedEthRegistrarController.abi, - }, - [namespaceContract(pluginName, "Ethnames_UnwrappedEthRegistrarController")]: { - chain: chainConfigForContract( - config.globalBlockrange, - ethnamesDatasource.chain.id, - ethnamesDatasource.contracts.UnwrappedEthRegistrarController, - ), - abi: ethnamesDatasource.contracts.UnwrappedEthRegistrarController.abi, - }, - }; - - // configure Basenames dependencies - const basenamesDatasource = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.Basenames, - ); - - const basenamesRegistrarContracts = { - [namespaceContract(pluginName, "Basenames_BaseRegistrar")]: { - chain: chainConfigForContract( - config.globalBlockrange, - basenamesDatasource.chain.id, - basenamesDatasource.contracts.BaseRegistrar, - ), - abi: basenamesDatasource.contracts.BaseRegistrar.abi, - }, - }; + return createConfig({ + chains: chainsConnectionConfigForDatasources( + config.namespace, + config.rpcConfigs, + REQUIRED_DATASOURCE_NAMES, + ), + contracts: { + ////////////////////// + // Ethnames Registrar + ////////////////////// + [namespaceContract(pluginName, "Ethnames_BaseRegistrar")]: { + chain: chainConfigForContract( + config.globalBlockrange, + ethnames.chain.id, + ethnames.contracts.BaseRegistrar, + ), + abi: ethnames.contracts.BaseRegistrar.abi, + }, - const basenamesRegistrarControllerContracts = { - [namespaceContract(pluginName, "Basenames_EARegistrarController")]: { - chain: chainConfigForContract( - config.globalBlockrange, - basenamesDatasource.chain.id, - basenamesDatasource.contracts.EARegistrarController, - ), - abi: basenamesDatasource.contracts.EARegistrarController.abi, - }, - [namespaceContract(pluginName, "Basenames_RegistrarController")]: { - chain: chainConfigForContract( - config.globalBlockrange, - basenamesDatasource.chain.id, - basenamesDatasource.contracts.RegistrarController, - ), - abi: basenamesDatasource.contracts.RegistrarController.abi, - }, - [namespaceContract(pluginName, "Basenames_UpgradeableRegistrarController")]: { - chain: chainConfigForContract( - config.globalBlockrange, - basenamesDatasource.chain.id, - basenamesDatasource.contracts.UpgradeableRegistrarController, - ), - abi: basenamesDatasource.contracts.UpgradeableRegistrarController.abi, - }, - }; + ////////////////////////////////// + // Ethnames Registrar Controllers + ////////////////////////////////// + [namespaceContract(pluginName, "Ethnames_LegacyEthRegistrarController")]: { + chain: chainConfigForContract( + config.globalBlockrange, + ethnames.chain.id, + ethnames.contracts.LegacyEthRegistrarController, + ), + abi: ethnames.contracts.LegacyEthRegistrarController.abi, + }, + [namespaceContract(pluginName, "Ethnames_WrappedEthRegistrarController")]: { + chain: chainConfigForContract( + config.globalBlockrange, + ethnames.chain.id, + ethnames.contracts.WrappedEthRegistrarController, + ), + abi: ethnames.contracts.WrappedEthRegistrarController.abi, + }, + [namespaceContract(pluginName, "Ethnames_UnwrappedEthRegistrarController")]: { + chain: chainConfigForContract( + config.globalBlockrange, + ethnames.chain.id, + ethnames.contracts.UnwrappedEthRegistrarController, + ), + abi: ethnames.contracts.UnwrappedEthRegistrarController.abi, + }, - // configure Lineanames dependencies - const linenamesDatasource = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.Lineanames, - ); + /////////////////////// + // Basenames Registrar + /////////////////////// + [namespaceContract(pluginName, "Basenames_BaseRegistrar")]: { + chain: chainConfigForContract( + config.globalBlockrange, + basenames.chain.id, + basenames.contracts.BaseRegistrar, + ), + abi: basenames.contracts.BaseRegistrar.abi, + }, - const lineanamesRegistrarContracts = { - [namespaceContract(pluginName, "Lineanames_BaseRegistrar")]: { - chain: chainConfigForContract( - config.globalBlockrange, - linenamesDatasource.chain.id, - linenamesDatasource.contracts.BaseRegistrar, - ), - abi: linenamesDatasource.contracts.BaseRegistrar.abi, - }, - }; + /////////////////////////////////// + // Basenames Registrar Controllers + /////////////////////////////////// + [namespaceContract(pluginName, "Basenames_EARegistrarController")]: { + chain: chainConfigForContract( + config.globalBlockrange, + basenames.chain.id, + basenames.contracts.EARegistrarController, + ), + abi: basenames.contracts.EARegistrarController.abi, + }, + [namespaceContract(pluginName, "Basenames_RegistrarController")]: { + chain: chainConfigForContract( + config.globalBlockrange, + basenames.chain.id, + basenames.contracts.RegistrarController, + ), + abi: basenames.contracts.RegistrarController.abi, + }, + [namespaceContract(pluginName, "Basenames_UpgradeableRegistrarController")]: { + chain: chainConfigForContract( + config.globalBlockrange, + basenames.chain.id, + basenames.contracts.UpgradeableRegistrarController, + ), + abi: basenames.contracts.UpgradeableRegistrarController.abi, + }, - const lineanamesRegistrarControllerContracts = { - [namespaceContract(pluginName, "Lineanames_EthRegistrarController")]: { - chain: chainConfigForContract( - config.globalBlockrange, - linenamesDatasource.chain.id, - linenamesDatasource.contracts.EthRegistrarController, - ), - abi: linenamesDatasource.contracts.EthRegistrarController.abi, - }, - }; + //////////////////////// + // Lineanames Registrar + //////////////////////// + [namespaceContract(pluginName, "Lineanames_BaseRegistrar")]: { + chain: chainConfigForContract( + config.globalBlockrange, + lineanames.chain.id, + lineanames.contracts.BaseRegistrar, + ), + abi: lineanames.contracts.BaseRegistrar.abi, + }, - return ponder.createConfig({ - chains: { - ...chainsConnectionConfig(config.rpcConfigs, ethnamesDatasource.chain.id), - ...chainsConnectionConfig(config.rpcConfigs, basenamesDatasource.chain.id), - ...chainsConnectionConfig(config.rpcConfigs, linenamesDatasource.chain.id), - }, - contracts: { - ...ethnamesRegistrarContracts, - ...ethnamesRegistrarControllerContracts, - ...basenamesRegistrarContracts, - ...basenamesRegistrarControllerContracts, - ...lineanamesRegistrarContracts, - ...lineanamesRegistrarControllerContracts, + //////////////////////////////////// + // Lineanames Registrar Controllers + //////////////////////////////////// + [namespaceContract(pluginName, "Lineanames_EthRegistrarController")]: { + chain: chainConfigForContract( + config.globalBlockrange, + lineanames.chain.id, + lineanames.contracts.EthRegistrarController, + ), + abi: lineanames.contracts.EthRegistrarController.abi, + }, }, }); }, diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/basenames/plugin.ts b/apps/ensindexer/src/plugins/subgraph/plugins/basenames/plugin.ts index da909a22a..6242058b2 100644 --- a/apps/ensindexer/src/plugins/subgraph/plugins/basenames/plugin.ts +++ b/apps/ensindexer/src/plugins/subgraph/plugins/basenames/plugin.ts @@ -7,26 +7,31 @@ import * as ponder from "ponder"; import { DatasourceNames } from "@ensnode/datasources"; import { PluginName } from "@ensnode/ensnode-sdk"; +import { createPlugin, namespaceContract } from "@/lib/plugin-helpers"; import { - createPlugin, - getDatasourceAsFullyDefinedAtCompileTime, - namespaceContract, -} from "@/lib/plugin-helpers"; -import { chainConfigForContract, chainsConnectionConfig } from "@/lib/ponder-helpers"; + chainConfigForContract, + chainsConnectionConfigForDatasources, + getRequiredDatasources, +} from "@/lib/ponder-helpers"; const pluginName = PluginName.Basenames; +const REQUIRED_DATASOURCE_NAMES = [DatasourceNames.Basenames]; + export default createPlugin({ name: pluginName, - requiredDatasourceNames: [DatasourceNames.Basenames], + requiredDatasourceNames: REQUIRED_DATASOURCE_NAMES, createPonderConfig(config) { - const { chain, contracts } = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.Basenames, - ); + const { + basenames: { chain, contracts }, + } = getRequiredDatasources(config.namespace, REQUIRED_DATASOURCE_NAMES); return ponder.createConfig({ - chains: chainsConnectionConfig(config.rpcConfigs, chain.id), + chains: chainsConnectionConfigForDatasources( + config.namespace, + config.rpcConfigs, + REQUIRED_DATASOURCE_NAMES, + ), contracts: { [namespaceContract(pluginName, "Registry")]: { chain: chainConfigForContract(config.globalBlockrange, chain.id, contracts.Registry), diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/plugin.ts b/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/plugin.ts index bbc26139c..4458567e6 100644 --- a/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/plugin.ts +++ b/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/plugin.ts @@ -8,26 +8,31 @@ import * as ponder from "ponder"; import { DatasourceNames } from "@ensnode/datasources"; import { PluginName } from "@ensnode/ensnode-sdk"; +import { createPlugin, namespaceContract } from "@/lib/plugin-helpers"; import { - createPlugin, - getDatasourceAsFullyDefinedAtCompileTime, - namespaceContract, -} from "@/lib/plugin-helpers"; -import { chainConfigForContract, chainsConnectionConfig } from "@/lib/ponder-helpers"; + chainConfigForContract, + chainsConnectionConfigForDatasources, + getRequiredDatasources, +} from "@/lib/ponder-helpers"; const pluginName = PluginName.Lineanames; +const REQUIRED_DATASOURCE_NAMES = [DatasourceNames.Lineanames]; + export default createPlugin({ name: pluginName, - requiredDatasourceNames: [DatasourceNames.Lineanames], + requiredDatasourceNames: REQUIRED_DATASOURCE_NAMES, createPonderConfig(config) { - const { chain, contracts } = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.Lineanames, - ); + const { + lineanames: { chain, contracts }, + } = getRequiredDatasources(config.namespace, REQUIRED_DATASOURCE_NAMES); return ponder.createConfig({ - chains: chainsConnectionConfig(config.rpcConfigs, chain.id), + chains: chainsConnectionConfigForDatasources( + config.namespace, + config.rpcConfigs, + REQUIRED_DATASOURCE_NAMES, + ), contracts: { [namespaceContract(pluginName, "Registry")]: { chain: chainConfigForContract(config.globalBlockrange, chain.id, contracts.Registry), diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/plugin.ts b/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/plugin.ts index 7562c100c..74c226e0c 100644 --- a/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/plugin.ts +++ b/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/plugin.ts @@ -8,26 +8,31 @@ import * as ponder from "ponder"; import { DatasourceNames } from "@ensnode/datasources"; import { PluginName } from "@ensnode/ensnode-sdk"; +import { createPlugin, namespaceContract } from "@/lib/plugin-helpers"; import { - createPlugin, - getDatasourceAsFullyDefinedAtCompileTime, - namespaceContract, -} from "@/lib/plugin-helpers"; -import { chainConfigForContract, chainsConnectionConfig } from "@/lib/ponder-helpers"; + chainConfigForContract, + chainsConnectionConfigForDatasources, + getRequiredDatasources, +} from "@/lib/ponder-helpers"; const pluginName = PluginName.Subgraph; +const REQUIRED_DATASOURCE_NAMES = [DatasourceNames.ENSRoot]; + export default createPlugin({ name: pluginName, - requiredDatasourceNames: [DatasourceNames.ENSRoot], + requiredDatasourceNames: REQUIRED_DATASOURCE_NAMES, createPonderConfig(config) { - const { chain, contracts } = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.ENSRoot, - ); + const { + ensroot: { chain, contracts }, + } = getRequiredDatasources(config.namespace, REQUIRED_DATASOURCE_NAMES); return ponder.createConfig({ - chains: chainsConnectionConfig(config.rpcConfigs, chain.id), + chains: chainsConnectionConfigForDatasources( + config.namespace, + config.rpcConfigs, + REQUIRED_DATASOURCE_NAMES, + ), contracts: { [namespaceContract(pluginName, "ENSv1RegistryOld")]: { chain: chainConfigForContract( diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/threedns/plugin.ts b/apps/ensindexer/src/plugins/subgraph/plugins/threedns/plugin.ts index b16e136ea..c531556af 100644 --- a/apps/ensindexer/src/plugins/subgraph/plugins/threedns/plugin.ts +++ b/apps/ensindexer/src/plugins/subgraph/plugins/threedns/plugin.ts @@ -7,50 +7,49 @@ import * as ponder from "ponder"; import { DatasourceNames, ResolverABI } from "@ensnode/datasources"; import { PluginName } from "@ensnode/ensnode-sdk"; +import { createPlugin, namespaceContract } from "@/lib/plugin-helpers"; import { - createPlugin, - getDatasourceAsFullyDefinedAtCompileTime, - namespaceContract, -} from "@/lib/plugin-helpers"; -import { chainConfigForContract, chainsConnectionConfig } from "@/lib/ponder-helpers"; + chainConfigForContract, + chainsConnectionConfigForDatasources, + getRequiredDatasources, +} from "@/lib/ponder-helpers"; const pluginName = PluginName.ThreeDNS; +const REQUIRED_DATASOURCE_NAMES = [DatasourceNames.ThreeDNSOptimism, DatasourceNames.ThreeDNSBase]; + export default createPlugin({ name: pluginName, - requiredDatasourceNames: [DatasourceNames.ThreeDNSOptimism, DatasourceNames.ThreeDNSBase], + requiredDatasourceNames: REQUIRED_DATASOURCE_NAMES, createPonderConfig(config) { - const threeDNSOptimism = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.ThreeDNSOptimism, - ); - const threeDNSBase = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.ThreeDNSBase, - ); + const { + threednsOptimism, // + threednsBase, + } = getRequiredDatasources(config.namespace, REQUIRED_DATASOURCE_NAMES); return ponder.createConfig({ - chains: { - ...chainsConnectionConfig(config.rpcConfigs, threeDNSOptimism.chain.id), - ...chainsConnectionConfig(config.rpcConfigs, threeDNSBase.chain.id), - }, + chains: chainsConnectionConfigForDatasources( + config.namespace, + config.rpcConfigs, + REQUIRED_DATASOURCE_NAMES, + ), contracts: { // multi-chain ThreeDNSToken indexing config [namespaceContract(pluginName, "ThreeDNSToken")]: { chain: { ...chainConfigForContract( config.globalBlockrange, - threeDNSOptimism.chain.id, - threeDNSOptimism.contracts.ThreeDNSToken, + threednsOptimism.chain.id, + threednsOptimism.contracts.ThreeDNSToken, ), ...chainConfigForContract( config.globalBlockrange, - threeDNSBase.chain.id, - threeDNSBase.contracts.ThreeDNSToken, + threednsBase.chain.id, + threednsBase.contracts.ThreeDNSToken, ), }, // NOTE: abi is identical in a multi-chain ponder config, just use Optimism's here - abi: threeDNSOptimism.contracts.ThreeDNSToken.abi, + abi: threednsOptimism.contracts.ThreeDNSToken.abi, }, // multi-chain ThreeDNS-specific Resolver indexing config [namespaceContract(pluginName, "Resolver")]: { @@ -58,13 +57,13 @@ export default createPlugin({ chain: { ...chainConfigForContract( config.globalBlockrange, - threeDNSOptimism.chain.id, - threeDNSOptimism.contracts.Resolver, + threednsOptimism.chain.id, + threednsOptimism.contracts.Resolver, ), ...chainConfigForContract( config.globalBlockrange, - threeDNSBase.chain.id, - threeDNSBase.contracts.Resolver, + threednsBase.chain.id, + threednsBase.contracts.Resolver, ), }, }, diff --git a/apps/ensindexer/src/plugins/tokenscope/plugin.ts b/apps/ensindexer/src/plugins/tokenscope/plugin.ts index b444de86b..53aed98f6 100644 --- a/apps/ensindexer/src/plugins/tokenscope/plugin.ts +++ b/apps/ensindexer/src/plugins/tokenscope/plugin.ts @@ -9,55 +9,36 @@ import * as ponder from "ponder"; import { DatasourceNames } from "@ensnode/datasources"; import { PluginName } from "@ensnode/ensnode-sdk"; +import { createPlugin, namespaceContract } from "@/lib/plugin-helpers"; import { - createPlugin, - getDatasourceAsFullyDefinedAtCompileTime, - namespaceContract, -} from "@/lib/plugin-helpers"; -import { chainConfigForContract, chainsConnectionConfig } from "@/lib/ponder-helpers"; + chainConfigForContract, + chainsConnectionConfig, + getRequiredDatasources, +} from "@/lib/ponder-helpers"; const pluginName = PluginName.TokenScope; +const REQUIRED_DATASOURCE_NAMES = [ + DatasourceNames.Seaport, + DatasourceNames.ENSRoot, + DatasourceNames.Basenames, + DatasourceNames.Lineanames, + DatasourceNames.ThreeDNSOptimism, + DatasourceNames.ThreeDNSBase, +]; + export default createPlugin({ name: pluginName, - requiredDatasourceNames: [ - DatasourceNames.Seaport, - DatasourceNames.ENSRoot, - DatasourceNames.Basenames, - DatasourceNames.Lineanames, - DatasourceNames.ThreeDNSOptimism, - DatasourceNames.ThreeDNSBase, - ], + requiredDatasourceNames: REQUIRED_DATASOURCE_NAMES, createPonderConfig(config) { - const seaport = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.Seaport, - ); - - const ensroot = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.ENSRoot, - ); - - const basenames = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.Basenames, - ); - - const lineanames = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.Lineanames, - ); - - const threeDNSOptimism = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.ThreeDNSOptimism, - ); - - const threeDNSBase = getDatasourceAsFullyDefinedAtCompileTime( - config.namespace, - DatasourceNames.ThreeDNSBase, - ); + const { + seaport, // + ensroot, + basenames, + lineanames, + threednsOptimism, + threednsBase, + } = getRequiredDatasources(config.namespace, REQUIRED_DATASOURCE_NAMES); // Sanity Check: Seaport and ENSRoot are on the same chain if (seaport.chain.id !== ensroot.chain.id) { @@ -65,7 +46,7 @@ export default createPlugin({ } // Sanity Check: ThreeDNSBase and Basenames are on the same chain - if (threeDNSBase.chain.id !== basenames.chain.id) { + if (threednsBase.chain.id !== basenames.chain.id) { throw new Error( "ThreeDNSBase and Basenames datasources are expected to be on the same chain", ); @@ -77,8 +58,8 @@ export default createPlugin({ ...chainsConnectionConfig(config.rpcConfigs, ensroot.chain.id), ...chainsConnectionConfig(config.rpcConfigs, basenames.chain.id), ...chainsConnectionConfig(config.rpcConfigs, lineanames.chain.id), - ...chainsConnectionConfig(config.rpcConfigs, threeDNSOptimism.chain.id), - ...chainsConnectionConfig(config.rpcConfigs, threeDNSBase.chain.id), + ...chainsConnectionConfig(config.rpcConfigs, threednsOptimism.chain.id), + ...chainsConnectionConfig(config.rpcConfigs, threednsBase.chain.id), }, contracts: { [namespaceContract(pluginName, "Seaport")]: { @@ -148,17 +129,17 @@ export default createPlugin({ chain: { ...chainConfigForContract( config.globalBlockrange, - threeDNSOptimism.chain.id, - threeDNSOptimism.contracts.ThreeDNSToken, + threednsOptimism.chain.id, + threednsOptimism.contracts.ThreeDNSToken, ), ...chainConfigForContract( config.globalBlockrange, - threeDNSBase.chain.id, - threeDNSBase.contracts.ThreeDNSToken, + threednsBase.chain.id, + threednsBase.contracts.ThreeDNSToken, ), }, // NOTE: abi is identical in a multi-chain ponder config, just use Optimism's here - abi: threeDNSOptimism.contracts.ThreeDNSToken.abi, + abi: threednsOptimism.contracts.ThreeDNSToken.abi, }, }, }); diff --git a/packages/datasources/src/lib/types.ts b/packages/datasources/src/lib/types.ts index 767951e98..161b3d109 100644 --- a/packages/datasources/src/lib/types.ts +++ b/packages/datasources/src/lib/types.ts @@ -56,14 +56,14 @@ export const DatasourceNames = { Basenames: "basenames", Lineanames: "lineanames", Seaport: "seaport", - ThreeDNSOptimism: "threedns-optimism", - ThreeDNSBase: "threedns-base", - ReverseResolverRoot: "reverse-resolver-root", - ReverseResolverBase: "reverse-resolver-base", - ReverseResolverLinea: "reverse-resolver-linea", - ReverseResolverOptimism: "reverse-resolver-optimism", - ReverseResolverArbitrum: "reverse-resolver-arbitrum", - ReverseResolverScroll: "reverse-resolver-scroll", + ThreeDNSOptimism: "threednsOptimism", + ThreeDNSBase: "threednsBase", + ReverseResolverRoot: "rrRoot", + ReverseResolverBase: "rrBase", + ReverseResolverLinea: "rrLinea", + ReverseResolverOptimism: "rrOptimism", + ReverseResolverArbitrum: "rrArbitrum", + ReverseResolverScroll: "rrScroll", Namechain: "namechain", } as const; From b8204e85bffa509a2973e08c754e11cd58a52ff8 Mon Sep 17 00:00:00 2001 From: shrugs Date: Wed, 12 Nov 2025 17:50:17 -0600 Subject: [PATCH 015/102] checkpoint --- apps/ensapi/src/graphql-api/builder.ts | 3 +- .../src/graphql-api/lib/get-domain-by-fqdn.ts | 5 + apps/ensapi/src/graphql-api/schema/query.ts | 9 + .../src/graphql-api/schema/registration.ts | 129 ++++++++++ apps/ensapi/src/graphql-api/schema/scalars.ts | 21 +- .../src/lib/ensv2/label-db-helpers.ts | 2 +- .../src/plugins/ensv2/event-handlers.ts | 6 +- .../plugins/ensv2/handlers/BaseRegistrar.ts | 8 + .../src/plugins/ensv2/handlers/NameWrapper.ts | 228 +++++++++++++++++- .../src/plugins/ensv2/handlers/Registry.ts | 45 ++-- packages/datasources/src/ens-test-env.ts | 10 +- .../src/schemas/ensv2.schema.ts | 58 ++++- packages/ensnode-sdk/src/ensv2/ids-lib.ts | 8 + packages/ensnode-sdk/src/ensv2/ids.ts | 5 + 14 files changed, 501 insertions(+), 36 deletions(-) create mode 100644 apps/ensapi/src/graphql-api/schema/registration.ts create mode 100644 apps/ensindexer/src/plugins/ensv2/handlers/BaseRegistrar.ts diff --git a/apps/ensapi/src/graphql-api/builder.ts b/apps/ensapi/src/graphql-api/builder.ts index 62637226d..102396047 100644 --- a/apps/ensapi/src/graphql-api/builder.ts +++ b/apps/ensapi/src/graphql-api/builder.ts @@ -1,6 +1,6 @@ import SchemaBuilder from "@pothos/core"; import DataloaderPlugin from "@pothos/plugin-dataloader"; -import type { Address } from "viem"; +import type { Address, Hex } from "viem"; import type { ChainId, @@ -17,6 +17,7 @@ export const builder = new SchemaBuilder<{ Scalars: { BigInt: { Input: bigint; Output: bigint }; Address: { Input: Address; Output: Address }; + Hex: { Input: Hex; Output: Hex }; ChainId: { Input: ChainId; Output: ChainId }; CoinType: { Input: CoinType; Output: CoinType }; Node: { Input: Node; Output: Node }; diff --git a/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts index 9192d5b2c..39b48b028 100644 --- a/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts +++ b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts @@ -65,6 +65,11 @@ export async function getDomainIdByInterpretedName( depth: number; }[]; + console.log({ + labelHashPath, + rows, + }); + const exists = rows.length > 0 && rows.length === labelHashPath.length; if (!exists) return null; diff --git a/apps/ensapi/src/graphql-api/schema/query.ts b/apps/ensapi/src/graphql-api/schema/query.ts index ba993060a..a5c2a9ae2 100644 --- a/apps/ensapi/src/graphql-api/schema/query.ts +++ b/apps/ensapi/src/graphql-api/schema/query.ts @@ -7,6 +7,7 @@ import { getDomainIdByInterpretedName } from "@/graphql-api/lib/get-domain-by-fq import { AccountRef } from "@/graphql-api/schema/account"; import { DomainIdInput, DomainRef } from "@/graphql-api/schema/domain"; import { RegistryIdInput, RegistryInterfaceRef } from "@/graphql-api/schema/registry"; +import { db } from "@/lib/db"; // TODO: maybe should still implement query/return by id, exposing the db's primary key? // maybe necessary for connections pattern... @@ -14,6 +15,13 @@ import { RegistryIdInput, RegistryInterfaceRef } from "@/graphql-api/schema/regi builder.queryType({ fields: (t) => ({ + domains: t.field({ + description: "DELETE ME", + type: [DomainRef], + nullable: false, + resolve: () => db.query.domain.findMany({ with: { label: true } }), + }), + ////////////////////////////////// // Get Domain by Name or DomainId ////////////////////////////////// @@ -24,6 +32,7 @@ builder.queryType({ nullable: true, resolve: async (parent, args, ctx, info) => { if (args.by.id !== undefined) return args.by.id; + console.log(await getDomainIdByInterpretedName(args.by.name)); return getDomainIdByInterpretedName(args.by.name); }, }), diff --git a/apps/ensapi/src/graphql-api/schema/registration.ts b/apps/ensapi/src/graphql-api/schema/registration.ts new file mode 100644 index 000000000..c20d719af --- /dev/null +++ b/apps/ensapi/src/graphql-api/schema/registration.ts @@ -0,0 +1,129 @@ +import type { RegistrationId, RequiredAndNotNull } from "@ensnode/ensnode-sdk"; + +import { builder } from "@/graphql-api/builder"; +import { getModelId } from "@/graphql-api/lib/get-id"; +import { AccountIdRef } from "@/graphql-api/schema/account-id"; +import { DomainRef } from "@/graphql-api/schema/domain"; +import { db } from "@/lib/db"; + +export const RegistrationInterfaceRef = builder.loadableInterfaceRef("Registration", { + load: (ids: RegistrationId[]) => + db.query.registration.findMany({ + where: (t, { inArray }) => inArray(t.id, ids), + }), + toKey: getModelId, + cacheResolved: true, + sort: true, +}); + +export type Registration = Exclude; +export type RegistrationInterface = Pick< + Registration, + | "id" + | "type" + | "index" + | "domainId" + | "start" + | "expiration" + | "registrarChainId" + | "registrarAddress" + | "referrer" +>; +export type NameWrapperRegistration = RequiredAndNotNull; +export type BaseRegistrarRegistration = RequiredAndNotNull; + +RegistrationInterfaceRef.implement({ + description: "TODO", + fields: (t) => ({ + ////////////////////// + // Registration.id + ////////////////////// + id: t.expose("id", { + description: "TODO", + type: "ID", + nullable: false, + }), + + /////////////////////// + // Registration.domain + /////////////////////// + domain: t.field({ + description: "TODO", + type: DomainRef, + nullable: false, + resolve: (parent) => parent.domainId, + }), + + ////////////////////////// + // Registration.registrar + ////////////////////////// + registrar: t.field({ + description: "TODO", + type: AccountIdRef, + nullable: false, + resolve: (parent) => ({ chainId: parent.registrarChainId, address: parent.registrarAddress }), + }), + + ////////////////////// + // Registration.start + ////////////////////// + start: t.field({ + description: "TODO", + type: "BigInt", + nullable: false, + resolve: (parent) => parent.start, + }), + + /////////////////////////// + // Registration.expiration + /////////////////////////// + expiration: t.field({ + description: "TODO", + type: "BigInt", + nullable: true, + resolve: (parent) => parent.expiration, + }), + + ///////////////////////// + // Registration.referrer + ///////////////////////// + referrer: t.field({ + description: "TODO", + type: "Hex", + nullable: true, + resolve: (parent) => parent.referrer, + }), + }), +}); + +export const NameWrapperRegistrationRef = + builder.objectRef("NameWrapperRegistration"); +NameWrapperRegistrationRef.implement({ + description: "TODO", + interfaces: [RegistrationInterfaceRef], + isTypeOf: (value) => (value as RegistrationInterface).type === "NameWrapper", + fields: (t) => ({ + ///////////////////////////////// + // NameWrapperRegistration.fuses + ///////////////////////////////// + fuses: t.field({ + description: "TODO", + type: "Int", + nullable: false, + // TODO: decode/render Fuses enum + // biome-ignore lint/style/noNonNullAssertion: guaranteed in NameWrapperRegistration + resolve: (parent) => parent.fuses!, + }), + }), +}); + +export const BaseRegistrarRegistrationRef = builder.objectRef( + "BaseRegistrarRegistration", +); + +BaseRegistrarRegistrationRef.implement({ + description: "TODO", + interfaces: [RegistrationInterfaceRef], + isTypeOf: (value) => (value as RegistrationInterface).type === "BaseRegistrar", + fields: (t) => ({}), +}); diff --git a/apps/ensapi/src/graphql-api/schema/scalars.ts b/apps/ensapi/src/graphql-api/schema/scalars.ts index 3fab4e65c..f24d4f0c8 100644 --- a/apps/ensapi/src/graphql-api/schema/scalars.ts +++ b/apps/ensapi/src/graphql-api/schema/scalars.ts @@ -1,4 +1,4 @@ -import { type Address, isHex, size } from "viem"; +import { type Address, type Hex, isHex, size } from "viem"; import { z } from "zod/v4"; import { @@ -33,6 +33,25 @@ builder.scalarType("Address", { parseValue: (value) => makeLowercaseAddressSchema("Address").parse(value), }); +builder.scalarType("Hex", { + description: "Hex represents viem#Hex.", + serialize: (value: Hex) => value.toString(), + parseValue: (value) => + z.coerce + .string() + .check((ctx) => { + if (!isHex(value)) { + ctx.issues.push({ + code: "custom", + message: "Must be a valid Hex", + input: ctx.value, + }); + } + }) + .transform((val) => val as Hex) + .parse(value), +}); + builder.scalarType("ChainId", { description: "ChainId represents a @ensnode/ensnode-sdk#ChainId.", serialize: (value: ChainId) => value, diff --git a/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts index 4938a4fcf..ef9fbf97e 100644 --- a/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts @@ -26,5 +26,5 @@ export async function ensureUnknownLabel(context: Context, labelHash: LabelHash) await context.db .insert(schema.label) .values({ labelHash, value: interpretedLabel }) - .onConflictDoUpdate({ value: interpretedLabel }); + .onConflictDoNothing(); } diff --git a/apps/ensindexer/src/plugins/ensv2/event-handlers.ts b/apps/ensindexer/src/plugins/ensv2/event-handlers.ts index 4e0751b08..ccf1a6fe6 100644 --- a/apps/ensindexer/src/plugins/ensv2/event-handlers.ts +++ b/apps/ensindexer/src/plugins/ensv2/event-handlers.ts @@ -1,9 +1,13 @@ +import attach_BaseRegistrarHandlers from "./handlers/BaseRegistrar"; import attach_ENSv1RegistryHandlers from "./handlers/ENSv1Registry"; import attach_EnhancedAccessControlHandlers from "./handlers/EnhancedAccessControl"; +import attach_NameWrapperHandlers from "./handlers/NameWrapper"; import attach_RegistryHandlers from "./handlers/Registry"; export default function () { - attach_RegistryHandlers(); + attach_BaseRegistrarHandlers(); attach_EnhancedAccessControlHandlers(); attach_ENSv1RegistryHandlers(); + attach_NameWrapperHandlers(); + attach_RegistryHandlers(); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/BaseRegistrar.ts b/apps/ensindexer/src/plugins/ensv2/handlers/BaseRegistrar.ts new file mode 100644 index 000000000..cdb375ced --- /dev/null +++ b/apps/ensindexer/src/plugins/ensv2/handlers/BaseRegistrar.ts @@ -0,0 +1,8 @@ +import { PluginName } from "@ensnode/ensnode-sdk"; + +const pluginName = PluginName.ENSv2; + +export default function () { + // BaseRegistrar for all three + // all relevant RegistrarControllers +} diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts b/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts index 8337712ea..1d6414a18 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts @@ -1 +1,227 @@ -// +import { type Context, ponder } from "ponder:registry"; +import schema from "ponder:schema"; +import { type Address, isAddressEqual, zeroAddress } from "viem"; + +import { + type DNSEncodedLiteralName, + type DNSEncodedName, + type DomainId, + decodeDNSEncodedLiteralName, + makeENSv1DomainId, + makeRegistrationId, + type Node, + PluginName, + RegistrationId, + uint256ToHex32, +} from "@ensnode/ensnode-sdk"; + +import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; +import { ensureLabel } from "@/lib/ensv2/label-db-helpers"; +import { namespaceContract } from "@/lib/plugin-helpers"; +import type { EventWithArgs } from "@/lib/ponder-helpers"; + +const pluginName = PluginName.ENSv2; + +/** + * When a name is wrapped in the NameWrapper contract, an ERC1155 token is minted that tokenizes + * ownership of the name. The minted token will be assigned a unique tokenId represented as + * uint256(namehash(name)) where name is the fqdn of the name being wrapped. + * https://github.com/ensdomains/ens-contracts/blob/db613bc/contracts/wrapper/ERC1155Fuse.sol#L262 + */ +const tokenIdToNode = (tokenId: bigint): Node => uint256ToHex32(tokenId); + +// registrar is source of truth for expiry if eth 2LD +// otherwise namewrapper is registrar and source of truth for expiry + +// namewrapper registration does not have a registrant +// probably need an isSubnameOfRegistrarManagedName helper to identify .eth 2lds (and linea 2lds) +// in the namewrapper and then affect the registration managed by those contracts instead of the +// one managed by the namewrapper. +// so if it's wrapped in the namewrapper, much like the chain, changes are materialized back to the +// source + +async function getLatestRegistration(context: Context, domainId: DomainId) { + const registrationId = makeRegistrationId(domainId, 0); + return await context.db.find(schema.registration, { id: registrationId }); +} + +export default function () { + async function handleTransfer({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ + operator: Address; + from: Address; + to: Address; + id: bigint; + }>; + }) { + const { id: tokenId, to } = event.args; + + const isBurn = isAddressEqual(zeroAddress, to); + // burning is always followed by NameWrapper#NameUnwrapped + if (isBurn) return; + + const domainId = makeENSv1DomainId(tokenIdToNode(tokenId)); + const registration = await getLatestRegistration(context, domainId); + + // ignore NameWrapper#Transfer* events if there's no Registration for the Domain in question + // this allows us to ignore the first Transfer event that occurs when wrapping a token + // TODO: if !registration || !isActive(registration) + if (!registration) return; + + // 1. the domain derived from token id definitely exists + // 2. its definitely in the namewrapper + // 3. therefore materialize Domain.ownerId + await ensureAccount(context, to); + await context.db.update(schema.domain, { id: domainId }).set({ ownerId: to }); + } + + ponder.on( + namespaceContract(pluginName, "NameWrapper:NameWrapped"), + async ({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ + node: Node; + name: DNSEncodedName; + owner: Address; + fuses: number; + expiry: bigint; + }>; + }) => { + const { node, name, owner, fuses, expiry: expiration } = event.args; + + const domainId = makeENSv1DomainId(node); + + const latest = await getLatestRegistration(context, domainId); + + // TODO: latest && isActive(latest) + if (latest) { + throw new Error( + `Invariant(NameWrapper:NameWrapped): NameWrapped emitted but an active registration already exists.`, + ); + } + + const registrationId = makeRegistrationId(domainId, 0); // TODO: (latest?.index + 1) ?? 0 + + await ensureAccount(context, owner); + await context.db.insert(schema.registration).values({ + id: registrationId, + type: "NameWrapper", + registrarChainId: context.chain.id, + registrarAddress: event.log.address, + domainId, + start: event.block.timestamp, + fuses, + expiration, + }); + + // materialize domain owner + await ensureAccount(context, owner); + await context.db.update(schema.domain, { id: domainId }).set({ ownerId: owner }); + + // decode name and discover labels + try { + const labels = decodeDNSEncodedLiteralName(name as DNSEncodedLiteralName); + for (const label of labels) { + await ensureLabel(context, label); + } + } catch { + // NameWrapper name decoding failed, no-op + console.warn(`NameWrapper emitted malformed DNSEncoded Name: '${name}'`); + } + }, + ); + + ponder.on( + namespaceContract(pluginName, "NameWrapper:NameUnwrapped"), + async ({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ node: Node; owner: Address }>; + }) => { + const { node, owner } = event.args; + + const domainId = makeENSv1DomainId(node); + const latest = await getLatestRegistration(context, domainId); + + if (!latest) { + throw new Error(`Invariant(NameWrapper:NameUnwrapped): Registration expected`); + } + + // TODO: instead of deleting, mark it as inactive perhaps by setting its expiry to block.timestamp + await context.db.delete(schema.registration, { id: latest.id }); + + // NOTE: we don't need to adjust Domain.ownerId because NameWrapper calls ens.setOwner + }, + ); + + ponder.on( + namespaceContract(pluginName, "NameWrapper:FusesSet"), + async ({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ node: Node; fuses: number }>; + }) => { + const { node, fuses } = event.args; + + const domainId = makeENSv1DomainId(node); + const registration = await getLatestRegistration(context, domainId); + + // TODO: || !isActive(registration) + if (!registration) { + throw new Error(`Invariant(NameWrapper:FusesSet): Registration expected.`); + } + + // upsert fuses + await context.db.update(schema.registration, { id: registration.id }).set({ fuses }); + + // TODO: expiration-related logic? + }, + ); + + ponder.on( + namespaceContract(pluginName, "NameWrapper:ExpiryExtended"), + async ({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ node: Node; expiry: bigint }>; + }) => { + const { node, expiry: expiration } = event.args; + + const domainId = makeENSv1DomainId(node); + const registration = await getLatestRegistration(context, domainId); + + // TODO: || !isActive(registration) + if (!registration) { + throw new Error(`Invariant(NameWrapper:FusesSet): Registration expected.`); + } + + await context.db.update(schema.registration, { id: registration.id }).set({ expiration }); + }, + ); + + ponder.on(namespaceContract(pluginName, "NameWrapper:TransferSingle"), handleTransfer); + ponder.on( + namespaceContract(pluginName, "NameWrapper:TransferBatch"), + async ({ context, event }) => { + for (const id of event.args.ids) { + await handleTransfer({ + context, + event: { ...event, args: { ...event.args, id } }, + }); + } + }, + ); +} diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts index bf4a3ba4d..acd6dcfe4 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts @@ -19,9 +19,11 @@ import { getThisAccountId } from "@/lib/get-this-account-id"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; +const pluginName = PluginName.ENSv2; + export default function () { ponder.on( - namespaceContract(PluginName.ENSv2, "Registry:NameRegistered"), + namespaceContract(pluginName, "Registry:NameRegistered"), async ({ context, event, @@ -84,7 +86,7 @@ export default function () { ); ponder.on( - namespaceContract(PluginName.ENSv2, "Registry:SubregistryUpdate"), + namespaceContract(pluginName, "Registry:SubregistryUpdate"), async ({ context, event, @@ -115,7 +117,7 @@ export default function () { ); ponder.on( - namespaceContract(PluginName.ENSv2, "Registry:ResolverUpdate"), + namespaceContract(pluginName, "Registry:ResolverUpdate"), async ({ context, event, @@ -144,7 +146,7 @@ export default function () { ); ponder.on( - namespaceContract(PluginName.ENSv2, "Registry:NameBurned"), + namespaceContract(pluginName, "Registry:NameBurned"), async ({ context, event, @@ -184,7 +186,7 @@ export default function () { } ponder.on( - namespaceContract(PluginName.ENSv2, "Registry:TransferSingle"), + namespaceContract(pluginName, "Registry:TransferSingle"), async ({ context, event }) => { const registryAccountId = getThisAccountId(context, event); const registryId = makeRegistryContractId(registryAccountId); @@ -196,22 +198,19 @@ export default function () { await handleTransferSingle({ context, event }); }, ); - ponder.on( - namespaceContract(PluginName.ENSv2, "Registry:TransferBatch"), - async ({ context, event }) => { - const registryAccountId = getThisAccountId(context, event); - const registryId = makeRegistryContractId(registryAccountId); - - // TODO(registry-announcement): ideally remove this - const registry = await context.db.find(schema.registry, { id: registryId }); - if (registry === null) return; // no-op non-Registry ERC1155 Transfers - - for (const id of event.args.ids) { - await handleTransferSingle({ - context, - event: { ...event, args: { ...event.args, id } }, - }); - } - }, - ); + ponder.on(namespaceContract(pluginName, "Registry:TransferBatch"), async ({ context, event }) => { + const registryAccountId = getThisAccountId(context, event); + const registryId = makeRegistryContractId(registryAccountId); + + // TODO(registry-announcement): ideally remove this + const registry = await context.db.find(schema.registry, { id: registryId }); + if (registry === null) return; // no-op non-Registry ERC1155 Transfers + + for (const id of event.args.ids) { + await handleTransferSingle({ + context, + event: { ...event, args: { ...event.args, id } }, + }); + } + }); } diff --git a/packages/datasources/src/ens-test-env.ts b/packages/datasources/src/ens-test-env.ts index 631e12877..d937aa8aa 100644 --- a/packages/datasources/src/ens-test-env.ts +++ b/packages/datasources/src/ens-test-env.ts @@ -38,12 +38,12 @@ export default { contracts: { ENSv1RegistryOld: { abi: root_Registry, // Registry was redeployed, same abi - address: "0x610178da211fef7d417bc0e6fed39f05609ad788", + address: "0xa51c1fc2f0d1a1b8494ed1fe312d7c3a78ed91c0", startBlock: 0, }, ENSv1Registry: { abi: root_Registry, // Registry was redeployed, same abi - address: "0xb7f8bc63bbcad18155201308c8f3540b07f84f5e", + address: "0x0dcd1bf9a1b36ce34237eeafef220932846bcd82", startBlock: 0, }, Resolver: { @@ -52,12 +52,12 @@ export default { }, BaseRegistrar: { abi: root_BaseRegistrar, - address: "0xa82ff9afd8f496c3d6ac40e2a0f282e47488cfc9", + address: "0x851356ae760d987e095750cceb3bc6014560891c", startBlock: 0, }, LegacyEthRegistrarController: { abi: root_LegacyEthRegistrarController, - address: "0x5081a39b8a5f0e35a8d959395a630b68b74dd30f", + address: "0x172076e0166d1f9cc711c77adf8488051744980c", startBlock: 0, }, WrappedEthRegistrarController: { @@ -67,7 +67,7 @@ export default { }, UnwrappedEthRegistrarController: { abi: root_UnwrappedEthRegistrarController, - address: "0x36b58f5c1969b7b6591d752ea6f5486d069010ab", + address: "0xd84379ceae14aa33c123af12424a37803f885889", startBlock: 0, }, NameWrapper: { diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/schemas/ensv2.schema.ts index d59382745..76c5ec6ef 100644 --- a/packages/ensnode-schema/src/schemas/ensv2.schema.ts +++ b/packages/ensnode-schema/src/schemas/ensv2.schema.ts @@ -1,17 +1,20 @@ import { onchainEnum, onchainTable, relations, uniqueIndex } from "ponder"; -import type { Address } from "viem"; +import type { Address, Hex } from "viem"; import type { ChainId, DomainId, + EncodedReferrer, InterpretedLabel, LabelHash, Node, PermissionsId, PermissionsResourceId, PermissionsUserId, + RegistrationId, RegistryId, ResolverId, + UnixTimestamp, } from "@ensnode/ensnode-sdk"; // Registry<->Domain is 1:1 @@ -105,7 +108,7 @@ export const domain = onchainTable( }), ); -export const relations_domain = relations(domain, ({ one }) => ({ +export const relations_domain = relations(domain, ({ one, many }) => ({ owner: one(account, { relationName: "owner", fields: [domain.ownerId], @@ -126,13 +129,62 @@ export const relations_domain = relations(domain, ({ one }) => ({ fields: [domain.labelHash], references: [label.labelHash], }), + registrations: many(registration), })); ///////////////// // Registrations ///////////////// -// TODO: derive from registries plugin +export const registrationType = onchainEnum("RegistrationType", [ + "NameWrapper", + "BaseRegistrar", + "ThreeDNS", +]); + +export const registration = onchainTable( + "registrations", + (t) => ({ + // keyed by (domainId, index) + id: t.text().primaryKey().$type(), + type: registrationType().notNull(), + + domainId: t.text().notNull().$type(), + index: t.integer().notNull().default(0), + + // must have a start timestamp + start: t.bigint().notNull(), + // may have an expiration + expiration: t.bigint(), + + // registrar AccountId + registrarChainId: t.integer().notNull().$type(), + registrarAddress: t.hex().notNull().$type
(), + + // references registrant + registrantId: t.hex().$type
(), + + // references referrer + referrer: t.hex().$type(), + + // may have fuses + fuses: t.integer(), + }), + (t) => ({ + byId: uniqueIndex().on(t.domainId, t.index), + }), +); + +export const registration_relations = relations(registration, ({ one, many }) => ({ + domain: one(domain, { + fields: [registration.domainId], + references: [domain.id], + }), + registrant: one(account, { + fields: [registration.registrantId], + references: [account.id], + }), +})); /////////////// // Permissions diff --git a/packages/ensnode-sdk/src/ensv2/ids-lib.ts b/packages/ensnode-sdk/src/ensv2/ids-lib.ts index 16dfa81f1..beb33b8c5 100644 --- a/packages/ensnode-sdk/src/ensv2/ids-lib.ts +++ b/packages/ensnode-sdk/src/ensv2/ids-lib.ts @@ -10,12 +10,14 @@ import { import type { CanonicalId, + DomainId, ENSv1DomainId, ENSv2DomainId, ImplicitRegistryId, PermissionsId, PermissionsResourceId, PermissionsUserId, + RegistrationId, RegistryContractId, ResolverId, ResolverRecordsId, @@ -87,3 +89,9 @@ export const makeResolverId = (contract: AccountId) => serializeAccountId(contra */ export const makeResolverRecordsId = (contract: AccountId, node: Node) => `${makeResolverId(contract)}/${node}` as ResolverRecordsId; + +/** + * + */ +export const makeRegistrationId = (domainId: DomainId, index: number = 0) => + `${domainId}/${index}` as RegistrationId; diff --git a/packages/ensnode-sdk/src/ensv2/ids.ts b/packages/ensnode-sdk/src/ensv2/ids.ts index aed7d42eb..3493643cf 100644 --- a/packages/ensnode-sdk/src/ensv2/ids.ts +++ b/packages/ensnode-sdk/src/ensv2/ids.ts @@ -61,3 +61,8 @@ export type ResolverId = SerializedAccountId & { __brand: "ResolverId" }; * */ export type ResolverRecordsId = string & { __brand: "ResolverRecordsId" }; + +/** + * + */ +export type RegistrationId = string & { __brand: "RegistrationId" }; From 8c1a7bb1b4abd1de89add696da6633191ed25970 Mon Sep 17 00:00:00 2001 From: shrugs Date: Thu, 13 Nov 2025 12:21:29 -0600 Subject: [PATCH 016/102] fix: revert cointype storage to bigint --- apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts | 5 ----- apps/ensapi/src/graphql-api/schema/resolver.ts | 7 ++++++- .../get-primary-name-from-index.ts | 11 ++++++++--- .../resolver-records-db-helpers.ts | 2 +- .../handlers/StandaloneReverseRegistrar.ts | 2 +- .../src/schemas/protocol-acceleration.schema.ts | 6 +++--- 6 files changed, 19 insertions(+), 14 deletions(-) diff --git a/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts index 39b48b028..9192d5b2c 100644 --- a/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts +++ b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts @@ -65,11 +65,6 @@ export async function getDomainIdByInterpretedName( depth: number; }[]; - console.log({ - labelHashPath, - rows, - }); - const exists = rows.length > 0 && rows.length === labelHashPath.length; if (!exists) return null; diff --git a/apps/ensapi/src/graphql-api/schema/resolver.ts b/apps/ensapi/src/graphql-api/schema/resolver.ts index ef87e70a1..f14f30bf0 100644 --- a/apps/ensapi/src/graphql-api/schema/resolver.ts +++ b/apps/ensapi/src/graphql-api/schema/resolver.ts @@ -1,4 +1,5 @@ import { + bigintToCoinType, makeResolverRecordsId, type ResolverId, type ResolverRecordsId, @@ -113,7 +114,11 @@ ResolverRecordsRef.implement({ description: "TODO", type: ["CoinType"], nullable: false, - resolve: (parent) => parent.addressRecords.map((r) => r.coinType).toSorted(), + resolve: (parent) => + parent.addressRecords + .map((r) => r.coinType) + .map(bigintToCoinType) + .toSorted(), }), }), }); diff --git a/apps/ensapi/src/lib/protocol-acceleration/get-primary-name-from-index.ts b/apps/ensapi/src/lib/protocol-acceleration/get-primary-name-from-index.ts index e38002e68..45bc02ccb 100644 --- a/apps/ensapi/src/lib/protocol-acceleration/get-primary-name-from-index.ts +++ b/apps/ensapi/src/lib/protocol-acceleration/get-primary-name-from-index.ts @@ -13,10 +13,14 @@ import { withSpanAsync } from "@/lib/tracing/auto-span"; const tracer = trace.getTracer("get-primary-name"); +const DEFAULT_EVM_COIN_TYPE_BIGINT = BigInt(DEFAULT_EVM_COIN_TYPE); + export async function getENSIP19ReverseNameRecordFromIndex( address: Address, coinType: CoinType, ): Promise { + const _coinType = BigInt(coinType); + // retrieve from index const records = await withSpanAsync( tracer, @@ -29,14 +33,15 @@ export async function getENSIP19ReverseNameRecordFromIndex( // address = address eq(t.address, address), // AND coinType IN [coinType, DEFAULT_EVM_COIN_TYPE] - inArray(t.coinType, [coinType, DEFAULT_EVM_COIN_TYPE]), + inArray(t.coinType, [_coinType, DEFAULT_EVM_COIN_TYPE_BIGINT]), ), columns: { coinType: true, value: true }, }), ); - const coinTypeName = records.find((pn) => pn.coinType === coinType)?.value ?? null; - const defaultName = records.find((pn) => pn.coinType === DEFAULT_EVM_COIN_TYPE)?.value ?? null; + const coinTypeName = records.find((pn) => pn.coinType === _coinType)?.value ?? null; + const defaultName = + records.find((pn) => pn.coinType === DEFAULT_EVM_COIN_TYPE_BIGINT)?.value ?? null; return coinTypeName ?? defaultName; } diff --git a/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts b/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts index cbac6f0da..4400d6a88 100644 --- a/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts +++ b/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts @@ -104,7 +104,7 @@ export async function handleResolverAddressRecordUpdate( address: Address, ) { // construct the ResolverAddressRecord's Composite Key - const id = { ...resolverRecordsKey, coinType }; + const id = { ...resolverRecordsKey, coinType: BigInt(coinType) }; // interpret the incoming address record value const interpretedValue = interpretAddressRecordValue(address); diff --git a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/StandaloneReverseRegistrar.ts b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/StandaloneReverseRegistrar.ts index 50ce9cbee..19ad6b5d6 100644 --- a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/StandaloneReverseRegistrar.ts +++ b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/StandaloneReverseRegistrar.ts @@ -31,7 +31,7 @@ export default function () { : evmChainIdToCoinType(context.chain.id); // construct the ReverseNameRecord entity's Composite Primary Key - const id = { address, coinType }; + const id = { address, coinType: BigInt(coinType) }; // interpret the emitted name record value (see `interpretNameRecordValue` for guarantees) const interpretedValue = interpretNameRecordValue(name); diff --git a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts index 4ed82406b..a0b924ff6 100644 --- a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts +++ b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts @@ -5,7 +5,7 @@ import { onchainTable, primaryKey, relations, uniqueIndex } from "ponder"; import type { Address } from "viem"; -import type { ChainId, CoinType, Node, ResolverId, ResolverRecordsId } from "@ensnode/ensnode-sdk"; +import type { ChainId, Node, ResolverId, ResolverRecordsId } from "@ensnode/ensnode-sdk"; // TODO: implement resolverType & polymorphic field availability @@ -27,7 +27,7 @@ export const reverseNameRecord = onchainTable( (t) => ({ // keyed by (address, coinType) address: t.hex().notNull().$type
(), - coinType: t.integer().notNull().$type(), + coinType: t.bigint().notNull(), /** * Represents the ENSIP-19 Reverse Name Record for a given (address, coinType). @@ -163,7 +163,7 @@ export const resolverAddressRecord = onchainTable( chainId: t.integer().notNull().$type(), address: t.hex().notNull().$type
(), node: t.hex().notNull().$type(), - coinType: t.integer().notNull().$type(), + coinType: t.bigint().notNull(), /** * Represents the value of the Addresss Record specified by ((chainId, resolver, node), coinType). From 763ae9d9e765f252ff4be14f0f1697786b4a57d3 Mon Sep 17 00:00:00 2001 From: shrugs Date: Thu, 13 Nov 2025 12:56:53 -0600 Subject: [PATCH 017/102] checkpoint --- .../src/plugins/ensv2/event-handlers.ts | 4 +- .../plugins/ensv2/handlers/BaseRegistrar.ts | 8 -- .../src/plugins/ensv2/handlers/Registrar.ts | 97 +++++++++++++++++++ apps/ensindexer/src/plugins/ensv2/plugin.ts | 95 +++++++++++++++++- .../src/abis/shared/AnyRegistrar.ts | 11 +++ .../src/abis/shared/AnyRegistrarController.ts | 22 +++++ packages/datasources/src/index.ts | 2 + 7 files changed, 228 insertions(+), 11 deletions(-) delete mode 100644 apps/ensindexer/src/plugins/ensv2/handlers/BaseRegistrar.ts create mode 100644 apps/ensindexer/src/plugins/ensv2/handlers/Registrar.ts create mode 100644 packages/datasources/src/abis/shared/AnyRegistrar.ts create mode 100644 packages/datasources/src/abis/shared/AnyRegistrarController.ts diff --git a/apps/ensindexer/src/plugins/ensv2/event-handlers.ts b/apps/ensindexer/src/plugins/ensv2/event-handlers.ts index ccf1a6fe6..fb9311d73 100644 --- a/apps/ensindexer/src/plugins/ensv2/event-handlers.ts +++ b/apps/ensindexer/src/plugins/ensv2/event-handlers.ts @@ -1,11 +1,11 @@ -import attach_BaseRegistrarHandlers from "./handlers/BaseRegistrar"; import attach_ENSv1RegistryHandlers from "./handlers/ENSv1Registry"; import attach_EnhancedAccessControlHandlers from "./handlers/EnhancedAccessControl"; import attach_NameWrapperHandlers from "./handlers/NameWrapper"; +import attach_RegistrarHandlers from "./handlers/Registrar"; import attach_RegistryHandlers from "./handlers/Registry"; export default function () { - attach_BaseRegistrarHandlers(); + attach_RegistrarHandlers(); attach_EnhancedAccessControlHandlers(); attach_ENSv1RegistryHandlers(); attach_NameWrapperHandlers(); diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/BaseRegistrar.ts b/apps/ensindexer/src/plugins/ensv2/handlers/BaseRegistrar.ts deleted file mode 100644 index cdb375ced..000000000 --- a/apps/ensindexer/src/plugins/ensv2/handlers/BaseRegistrar.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { PluginName } from "@ensnode/ensnode-sdk"; - -const pluginName = PluginName.ENSv2; - -export default function () { - // BaseRegistrar for all three - // all relevant RegistrarControllers -} diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/Registrar.ts b/apps/ensindexer/src/plugins/ensv2/handlers/Registrar.ts new file mode 100644 index 000000000..7e52ab568 --- /dev/null +++ b/apps/ensindexer/src/plugins/ensv2/handlers/Registrar.ts @@ -0,0 +1,97 @@ +import config from "@/config"; + +import { type Context, ponder } from "ponder:registry"; +import type { Address } from "viem"; + +import { DatasourceNames } from "@ensnode/datasources"; +import { type AccountId, accountIdEqual, PluginName } from "@ensnode/ensnode-sdk"; + +import { getDatasourceContract, maybeGetDatasourceContract } from "@/lib/datasource-helpers"; +import { namespaceContract } from "@/lib/plugin-helpers"; +import type { EventWithArgs } from "@/lib/ponder-helpers"; + +const pluginName = PluginName.ENSv2; + +const ethnamesRegistrar = getDatasourceContract( + config.namespace, + DatasourceNames.ENSRoot, + "BaseRegistrar", +); +const basenamesRegistar = maybeGetDatasourceContract( + config.namespace, + DatasourceNames.Basenames, + "BaseRegistrar", +); +const lineanamesRegistar = maybeGetDatasourceContract( + config.namespace, + DatasourceNames.Lineanames, + "BaseRegistrar", +); + +const getRegistrarManagedName = (registrar: AccountId) => { + if (accountIdEqual(ethnamesRegistrar, registrar)) return "eth"; + if (basenamesRegistar && accountIdEqual(basenamesRegistar, registrar)) return "base.eth"; + if (lineanamesRegistar && accountIdEqual(lineanamesRegistar, registrar)) return "linea.eth"; + throw new Error("never"); +}; + +export default function () { + ////////////// + // Registrars + ////////////// + ponder.on( + namespaceContract(pluginName, "Registrar:NameRegistered"), + async ({ context, event }) => { + // upsert relevant registration for domain + }, + ); + + ponder.on( + namespaceContract(pluginName, "Registrar:NameRegisteredWithRecord"), + async ({ context, event }) => { + // upsert relevant registration for domain + }, + ); + + ponder.on(namespaceContract(pluginName, "Registrar:NameRenewed"), async ({ context, event }) => { + // update registration expiration, add renewal log + }); + + ponder.on(namespaceContract(pluginName, "Registrar:NameMigrated"), async ({ context, event }) => { + // TODO: what does this mean and from where? + }); + + async function handleTransfer({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ + from: Address; + to: Address; + tokenId: bigint; + }>; + }) { + // + } + + ponder.on( + namespaceContract( + pluginName, + "Registrar:Transfer(address indexed from, address indexed to, uint256 indexed id)", + ), + ({ context, event }) => + handleTransfer({ + context, + event: { ...event, args: { ...event.args, tokenId: event.args.id } }, + }), + ); + + ponder.on( + namespaceContract( + pluginName, + "Registrar:Transfer(address indexed from, address indexed to, uint256 indexed tokenId)", + ), + handleTransfer, + ); +} diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index d18bc5893..8052a6e5b 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -4,7 +4,13 @@ import { createConfig } from "ponder"; -import { DatasourceNames, EnhancedAccessControlABI, RegistryABI } from "@ensnode/datasources"; +import { + AnyRegistrarABI, + AnyRegistrarControllerABI, + DatasourceNames, + EnhancedAccessControlABI, + RegistryABI, +} from "@ensnode/datasources"; import { PluginName } from "@ensnode/ensnode-sdk"; import { createPlugin, namespaceContract } from "@/lib/plugin-helpers"; @@ -146,6 +152,93 @@ export default createPlugin({ )), }, }, + + ////////////// + // Registrars + ////////////// + [namespaceContract(pluginName, "Registrar")]: { + abi: AnyRegistrarABI, + chain: { + // Ethnames BaseRegistrar + ...chainConfigForContract( + config.globalBlockrange, + ensroot.chain.id, + ensroot.contracts.BaseRegistrar, + ), + // Basenames BaseRegistrar, if exists + ...(basenames && + chainConfigForContract( + config.globalBlockrange, + basenames.chain.id, + basenames.contracts.BaseRegistrar, + )), + // Lineanames BaseRegistrar, if exists + ...(lineanames && + chainConfigForContract( + config.globalBlockrange, + lineanames.chain.id, + lineanames.contracts.BaseRegistrar, + )), + }, + }, + + ///////////////////////// + // Registrar Controllers + ///////////////////////// + [namespaceContract(pluginName, "RegistrarController")]: { + abi: AnyRegistrarControllerABI, + chain: { + /////////////////////////////////// + // Ethnames Registrar Controllers + /////////////////////////////////// + ...chainConfigForContract( + config.globalBlockrange, + ensroot.chain.id, + ensroot.contracts.LegacyEthRegistrarController, + ), + ...chainConfigForContract( + config.globalBlockrange, + ensroot.chain.id, + ensroot.contracts.WrappedEthRegistrarController, + ), + ...chainConfigForContract( + config.globalBlockrange, + ensroot.chain.id, + ensroot.contracts.UnwrappedEthRegistrarController, + ), + + /////////////////////////////////// + // Basenames Registrar Controllers + /////////////////////////////////// + ...(basenames && { + ...chainConfigForContract( + config.globalBlockrange, + basenames.chain.id, + basenames.contracts.EARegistrarController, + ), + ...chainConfigForContract( + config.globalBlockrange, + basenames.chain.id, + basenames.contracts.RegistrarController, + ), + ...chainConfigForContract( + config.globalBlockrange, + basenames.chain.id, + basenames.contracts.UpgradeableRegistrarController, + ), + }), + + //////////////////////////////////// + // Lineanames Registrar Controllers + //////////////////////////////////// + ...(lineanames && + chainConfigForContract( + config.globalBlockrange, + lineanames.chain.id, + lineanames.contracts.EthRegistrarController, + )), + }, + }, }, }); }, diff --git a/packages/datasources/src/abis/shared/AnyRegistrar.ts b/packages/datasources/src/abis/shared/AnyRegistrar.ts new file mode 100644 index 000000000..ddc2fc383 --- /dev/null +++ b/packages/datasources/src/abis/shared/AnyRegistrar.ts @@ -0,0 +1,11 @@ +import { mergeAbis } from "@ponder/utils"; + +import { BaseRegistrar as basenames_BaseRegistrar } from "../basenames/BaseRegistrar"; +import { BaseRegistrar as lineanames_BaseRegistrar } from "../lineanames/BaseRegistrar"; +import { BaseRegistrar as ethnames_BaseRegistrar } from "../root/BaseRegistrar"; + +export const AnyRegistrarABI = mergeAbis([ + ethnames_BaseRegistrar, + basenames_BaseRegistrar, + lineanames_BaseRegistrar, +]); diff --git a/packages/datasources/src/abis/shared/AnyRegistrarController.ts b/packages/datasources/src/abis/shared/AnyRegistrarController.ts new file mode 100644 index 000000000..35c5f32cb --- /dev/null +++ b/packages/datasources/src/abis/shared/AnyRegistrarController.ts @@ -0,0 +1,22 @@ +import { mergeAbis } from "@ponder/utils"; + +import { EarlyAccessRegistrarController } from "../basenames/EARegistrarController"; +import { RegistrarController } from "../basenames/RegistrarController"; +import { UpgradeableRegistrarController } from "../basenames/UpgradeableRegistrarController"; +import { EthRegistrarController } from "../lineanames/EthRegistrarController"; +import { LegacyEthRegistrarController } from "../root/LegacyEthRegistrarController"; +import { UnwrappedEthRegistrarController } from "../root/UnwrappedEthRegistrarController"; +import { WrappedEthRegistrarController } from "../root/WrappedEthRegistrarController"; + +export const AnyRegistrarControllerABI = mergeAbis([ + // ethnames + LegacyEthRegistrarController, + WrappedEthRegistrarController, + UnwrappedEthRegistrarController, + // basenames + EarlyAccessRegistrarController, + RegistrarController, + UpgradeableRegistrarController, + // lineanames + EthRegistrarController, +]); diff --git a/packages/datasources/src/index.ts b/packages/datasources/src/index.ts index 5b0346428..41b7d20e7 100644 --- a/packages/datasources/src/index.ts +++ b/packages/datasources/src/index.ts @@ -1,5 +1,7 @@ export { EnhancedAccessControl as EnhancedAccessControlABI } from "./abis/namechain/EnhancedAccessControl"; export { Registry as RegistryABI } from "./abis/namechain/Registry"; +export { AnyRegistrarABI } from "./abis/shared/AnyRegistrar"; +export { AnyRegistrarControllerABI } from "./abis/shared/AnyRegistrarController"; export { StandaloneReverseRegistrar as StandaloneReverseRegistrarABI } from "./abis/shared/StandaloneReverseRegistrar"; export { ThreeDNSToken as ThreeDNSTokenABI } from "./abis/threedns/ThreeDNSToken"; export * from "./lib/chains"; From d267671ed2063fab107bc1bda5fe4990515d9721 Mon Sep 17 00:00:00 2001 From: shrugs Date: Thu, 13 Nov 2025 17:52:58 -0600 Subject: [PATCH 018/102] checkpoint --- .../src/lib/ensv2/domain-db-helpers.ts | 18 ++ .../src/lib/ensv2/is-name-wrapper.ts | 29 --- .../ensindexer/src/lib/ensv2/registrar-lib.ts | 102 +++++++++ .../src/lib/ensv2/registration-db-helpers.ts | 12 + .../src/plugins/ensv2/event-handlers.ts | 6 +- .../plugins/ensv2/handlers/ENSv1Registry.ts | 22 +- .../src/plugins/ensv2/handlers/NameWrapper.ts | 58 ++--- .../src/plugins/ensv2/handlers/Registrar.ts | 211 ++++++++++++------ .../ensv2/handlers/RegistrarController.ts | 160 +++++++++++++ .../plugins/basenames/handlers/Registrar.ts | 5 +- .../tokenscope/handlers/BaseRegistrars.ts | 2 +- .../src/abis/basenames/BaseRegistrar.ts | 2 +- .../src/schemas/ensv2.schema.ts | 9 +- 13 files changed, 497 insertions(+), 139 deletions(-) create mode 100644 apps/ensindexer/src/lib/ensv2/domain-db-helpers.ts delete mode 100644 apps/ensindexer/src/lib/ensv2/is-name-wrapper.ts create mode 100644 apps/ensindexer/src/lib/ensv2/registrar-lib.ts create mode 100644 apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts create mode 100644 apps/ensindexer/src/plugins/ensv2/handlers/RegistrarController.ts diff --git a/apps/ensindexer/src/lib/ensv2/domain-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/domain-db-helpers.ts new file mode 100644 index 000000000..23a71d86f --- /dev/null +++ b/apps/ensindexer/src/lib/ensv2/domain-db-helpers.ts @@ -0,0 +1,18 @@ +import type { Context } from "ponder:registry"; +import schema from "ponder:schema"; +import type { Address } from "viem"; + +import type { DomainId } from "@ensnode/ensnode-sdk"; + +import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; + +/** + * Sets `domainId`'s owner to `owner` if exists. + */ +export async function materializeDomainOwner(context: Context, domainId: DomainId, owner: Address) { + const domain = await context.db.find(schema.domain, { id: domainId }); + if (domain) { + await ensureAccount(context, owner); + await context.db.update(schema.domain, { id: domainId }).set({ ownerId: owner }); + } +} diff --git a/apps/ensindexer/src/lib/ensv2/is-name-wrapper.ts b/apps/ensindexer/src/lib/ensv2/is-name-wrapper.ts deleted file mode 100644 index da849d48f..000000000 --- a/apps/ensindexer/src/lib/ensv2/is-name-wrapper.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { - DatasourceNames, - type ENSNamespaceId, - getDatasource, - maybeGetDatasource, -} from "@ensnode/datasources"; -import { type AccountId, accountIdEqual } from "@ensnode/ensnode-sdk"; - -/** - * - */ -export function isNameWrapper(namespace: ENSNamespaceId, contract: AccountId) { - const ensroot = getDatasource(namespace, DatasourceNames.ENSRoot); - const lineanames = maybeGetDatasource(namespace, DatasourceNames.Lineanames); - - const isRootNameWrapper = accountIdEqual(contract, { - chainId: ensroot.chain.id, - address: ensroot.contracts.NameWrapper.address, - }); - - const isLineanamesNameWrapper = - lineanames !== undefined && - accountIdEqual(contract, { - chainId: lineanames.chain.id, - address: lineanames.contracts.NameWrapper.address, - }); - - return isRootNameWrapper || isLineanamesNameWrapper; -} diff --git a/apps/ensindexer/src/lib/ensv2/registrar-lib.ts b/apps/ensindexer/src/lib/ensv2/registrar-lib.ts new file mode 100644 index 000000000..28a5f0d75 --- /dev/null +++ b/apps/ensindexer/src/lib/ensv2/registrar-lib.ts @@ -0,0 +1,102 @@ +import config from "@/config"; + +import { DatasourceNames } from "@ensnode/datasources"; +import { + type AccountId, + accountIdEqual, + type LabelHash, + type Name, + uint256ToHex32, +} from "@ensnode/ensnode-sdk"; + +import { getDatasourceContract, maybeGetDatasourceContract } from "@/lib/datasource-helpers"; + +const ethnamesNameWrapper = getDatasourceContract( + config.namespace, + DatasourceNames.ENSRoot, + "NameWrapper", +); + +const lineanamesNameWrapper = maybeGetDatasourceContract( + config.namespace, + DatasourceNames.Lineanames, + "NameWrapper", +); + +const REGISTRAR_CONTRACTS_BY_MANAGED_NAME: Record = { + eth: [ + getDatasourceContract( + config.namespace, // + DatasourceNames.ENSRoot, + "BaseRegistrar", + ), + getDatasourceContract( + config.namespace, + DatasourceNames.ENSRoot, + "LegacyEthRegistrarController", + ), + getDatasourceContract( + config.namespace, + DatasourceNames.ENSRoot, + "WrappedEthRegistrarController", + ), + getDatasourceContract( + config.namespace, + DatasourceNames.ENSRoot, + "UnwrappedEthRegistrarController", + ), + ], + "base.eth": [ + maybeGetDatasourceContract( + config.namespace, // + DatasourceNames.Basenames, + "BaseRegistrar", + ), + maybeGetDatasourceContract( + config.namespace, + DatasourceNames.Basenames, + "EARegistrarController", + ), + maybeGetDatasourceContract( + config.namespace, // + DatasourceNames.Basenames, + "RegistrarController", + ), + maybeGetDatasourceContract( + config.namespace, + DatasourceNames.Basenames, + "UpgradeableRegistrarController", + ), + ].filter((c) => !!c), + "linea.eth": [ + maybeGetDatasourceContract(config.namespace, DatasourceNames.Lineanames, "BaseRegistrar"), + maybeGetDatasourceContract( + config.namespace, + DatasourceNames.Lineanames, + "EthRegistrarController", + ), + ].filter((c) => !!c), +}; + +export const getRegistrarManagedName = (contract: AccountId) => { + for (const [managedName, contracts] of Object.entries(REGISTRAR_CONTRACTS_BY_MANAGED_NAME)) { + const isAnyOfTheContracts = contracts.some((_contract) => accountIdEqual(_contract, contract)); + if (isAnyOfTheContracts) return managedName; + } + + throw new Error("never"); +}; + +export function isNameWrapper(contract: AccountId) { + if (accountIdEqual(ethnamesNameWrapper, contract)) return true; + if (lineanamesNameWrapper && accountIdEqual(lineanamesNameWrapper, contract)) return true; + return false; +} + +/** + * BaseRegistrar-derived Registrars register direct subnames of a RegistrarManagedName. As such, the + * tokens issued by them are keyed by the direct subname's label's labelHash. + * + * https://github.com/ensdomains/ens-contracts/blob/db613bc/contracts/ethregistrar/ETHRegistrarController.sol#L215 + */ +export const registrarTokenIdToLabelHash = (tokenId: bigint): LabelHash => uint256ToHex32(tokenId); diff --git a/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts new file mode 100644 index 000000000..952bac634 --- /dev/null +++ b/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts @@ -0,0 +1,12 @@ +import type { Context } from "ponder:registry"; +import schema from "ponder:schema"; + +import { type DomainId, makeRegistrationId } from "@ensnode/ensnode-sdk"; + +/** + * TODO: find the most recent registration, active or otherwise + */ +export async function getLatestRegistration(context: Context, domainId: DomainId) { + const registrationId = makeRegistrationId(domainId, 0); + return await context.db.find(schema.registration, { id: registrationId }); +} diff --git a/apps/ensindexer/src/plugins/ensv2/event-handlers.ts b/apps/ensindexer/src/plugins/ensv2/event-handlers.ts index fb9311d73..c4befa12a 100644 --- a/apps/ensindexer/src/plugins/ensv2/event-handlers.ts +++ b/apps/ensindexer/src/plugins/ensv2/event-handlers.ts @@ -2,12 +2,14 @@ import attach_ENSv1RegistryHandlers from "./handlers/ENSv1Registry"; import attach_EnhancedAccessControlHandlers from "./handlers/EnhancedAccessControl"; import attach_NameWrapperHandlers from "./handlers/NameWrapper"; import attach_RegistrarHandlers from "./handlers/Registrar"; +import attach_RegistrarControllerHandlers from "./handlers/RegistrarController"; import attach_RegistryHandlers from "./handlers/Registry"; export default function () { - attach_RegistrarHandlers(); - attach_EnhancedAccessControlHandlers(); attach_ENSv1RegistryHandlers(); + attach_EnhancedAccessControlHandlers(); attach_NameWrapperHandlers(); + attach_RegistrarHandlers(); + attach_RegistrarControllerHandlers(); attach_RegistryHandlers(); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts index ba4a17980..c25fa5855 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts @@ -17,6 +17,7 @@ import { } from "@ensnode/ensnode-sdk"; import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; +import { materializeDomainOwner } from "@/lib/ensv2/domain-db-helpers"; import { ensureUnknownLabel } from "@/lib/ensv2/label-db-helpers"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; @@ -42,16 +43,17 @@ async function handleNewOwner({ // if someone mints a node to the zero address, nothing happens in the Registry, so no-op if (isAddressEqual(zeroAddress, owner)) return; + // this is either a NEW Domain OR the owner of the parent changing the owner of the child + const node = makeSubdomainNode(labelHash, parentNode); + const domainId = makeENSv1DomainId(node); const registryId = parentNode === zeroHash ? getRootRegistryId(config.namespace) : makeImplicitRegistryId(parentNode); - const domainId = makeENSv1DomainId(node); - - // this is either a NEW Domain OR the owner of the parent changing the owner of the child // TODO: import label healing logic from subgraph plugin + await ensureUnknownLabel(context, labelHash); await context.db .insert(schema.domain) @@ -62,13 +64,13 @@ async function handleNewOwner({ }) .onConflictDoNothing(); - // TODO: if owner is special registrar, ignore - - // ensure owner account - await ensureAccount(context, owner); - - // update owner - await context.db.update(schema.domain, { id: domainId }).set({ ownerId: owner }); + // materialize domain owner + // NOTE: despite Domain.ownerId being materialized from other sources of truth (i.e. Registrars + // like BaseRegistrars & NameWrapper) it's ok to always set it here because the Registrar-emitted + // events occur _after_ the Registry events. So when a name is wrapped, for example, the Registry's + // owner changes to that of the NameWrapper but then the NameWrapper emits NameWrapped, and this + // indexing code re-materializes the Domain.ownerId to the NameWraper-emitted value. + await materializeDomainOwner(context, domainId, owner); } async function handleNewResolver({ diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts b/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts index 1d6414a18..5e45bfd76 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts @@ -5,18 +5,19 @@ import { type Address, isAddressEqual, zeroAddress } from "viem"; import { type DNSEncodedLiteralName, type DNSEncodedName, - type DomainId, decodeDNSEncodedLiteralName, makeENSv1DomainId, makeRegistrationId, type Node, PluginName, - RegistrationId, uint256ToHex32, } from "@ensnode/ensnode-sdk"; import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; +import { materializeDomainOwner } from "@/lib/ensv2/domain-db-helpers"; import { ensureLabel } from "@/lib/ensv2/label-db-helpers"; +import { getLatestRegistration } from "@/lib/ensv2/registration-db-helpers"; +import { getThisAccountId } from "@/lib/get-this-account-id"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; @@ -32,6 +33,13 @@ const tokenIdToNode = (tokenId: bigint): Node => uint256ToHex32(tokenId); // registrar is source of truth for expiry if eth 2LD // otherwise namewrapper is registrar and source of truth for expiry +// maybe should more or less ignore .eth 2LD (and other registrar-managed names) in the namewrapper? + +// for non-.eth-2ld names is infinite expiration represented as 0 or max int? probably max int. if so +// need to interpret that into null to indicate that it doesn't expire +// for .eth 2lds we need any namewrapper events to be, like, ignored, basically. maybe a BaseRegistrar +// Registration can include a `wrappedTokenId` field to indicate that it exists in the namewrapper +// but NameWrapper Registrations are exclusively for non-.eth-2lds // namewrapper registration does not have a registrant // probably need an isSubnameOfRegistrarManagedName helper to identify .eth 2lds (and linea 2lds) @@ -40,11 +48,6 @@ const tokenIdToNode = (tokenId: bigint): Node => uint256ToHex32(tokenId); // so if it's wrapped in the namewrapper, much like the chain, changes are materialized back to the // source -async function getLatestRegistration(context: Context, domainId: DomainId) { - const registrationId = makeRegistrationId(domainId, 0); - return await context.db.find(schema.registration, { id: registrationId }); -} - export default function () { async function handleTransfer({ context, @@ -58,25 +61,28 @@ export default function () { id: bigint; }>; }) { - const { id: tokenId, to } = event.args; + const { from, to, id: tokenId } = event.args; + const isMint = isAddressEqual(zeroAddress, from); const isBurn = isAddressEqual(zeroAddress, to); - // burning is always followed by NameWrapper#NameUnwrapped + + // minting is always followed by NameWrapper#NameWrapped, safe to ignore + if (isMint) return; + + // burning is always followed by NameWrapper#NameUnwrapped, safe to ignore if (isBurn) return; const domainId = makeENSv1DomainId(tokenIdToNode(tokenId)); const registration = await getLatestRegistration(context, domainId); - // ignore NameWrapper#Transfer* events if there's no Registration for the Domain in question - // this allows us to ignore the first Transfer event that occurs when wrapping a token - // TODO: if !registration || !isActive(registration) - if (!registration) return; + // TODO: || !isActive(registration) ? + // TODO: specifically check that there are no NameWrapper Registrations? + if (!registration) { + throw new Error(`Invariant(NameWrapper:Transfer): Expected registration.`); + } - // 1. the domain derived from token id definitely exists - // 2. its definitely in the namewrapper - // 3. therefore materialize Domain.ownerId - await ensureAccount(context, to); - await context.db.update(schema.domain, { id: domainId }).set({ ownerId: to }); + // materialize domain owner + await materializeDomainOwner(context, domainId, to); } ponder.on( @@ -96,12 +102,13 @@ export default function () { }) => { const { node, name, owner, fuses, expiry: expiration } = event.args; + const registrar = getThisAccountId(context, event); const domainId = makeENSv1DomainId(node); - const latest = await getLatestRegistration(context, domainId); + const registration = await getLatestRegistration(context, domainId); // TODO: latest && isActive(latest) - if (latest) { + if (registration) { throw new Error( `Invariant(NameWrapper:NameWrapped): NameWrapped emitted but an active registration already exists.`, ); @@ -113,8 +120,8 @@ export default function () { await context.db.insert(schema.registration).values({ id: registrationId, type: "NameWrapper", - registrarChainId: context.chain.id, - registrarAddress: event.log.address, + registrarChainId: registrar.chainId, + registrarAddress: registrar.address, domainId, start: event.block.timestamp, fuses, @@ -122,8 +129,7 @@ export default function () { }); // materialize domain owner - await ensureAccount(context, owner); - await context.db.update(schema.domain, { id: domainId }).set({ ownerId: owner }); + await materializeDomainOwner(context, domainId, owner); // decode name and discover labels try { @@ -147,7 +153,7 @@ export default function () { context: Context; event: EventWithArgs<{ node: Node; owner: Address }>; }) => { - const { node, owner } = event.args; + const { node } = event.args; const domainId = makeENSv1DomainId(node); const latest = await getLatestRegistration(context, domainId); @@ -159,7 +165,7 @@ export default function () { // TODO: instead of deleting, mark it as inactive perhaps by setting its expiry to block.timestamp await context.db.delete(schema.registration, { id: latest.id }); - // NOTE: we don't need to adjust Domain.ownerId because NameWrapper calls ens.setOwner + // NOTE: we don't need to adjust Domain.ownerId because NameWrapper always calls ens.setOwner }, ); diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/Registrar.ts b/apps/ensindexer/src/plugins/ensv2/handlers/Registrar.ts index 7e52ab568..56c26fc39 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/Registrar.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/Registrar.ts @@ -1,97 +1,174 @@ -import config from "@/config"; - import { type Context, ponder } from "ponder:registry"; -import type { Address } from "viem"; +import schema from "ponder:schema"; +import { GRACE_PERIOD_SECONDS } from "@ensdomains/ensjs/utils"; +import { type Address, isAddressEqual, namehash, zeroAddress } from "viem"; -import { DatasourceNames } from "@ensnode/datasources"; -import { type AccountId, accountIdEqual, PluginName } from "@ensnode/ensnode-sdk"; +import { + makeENSv1DomainId, + makeRegistrationId, + makeSubdomainNode, + PluginName, +} from "@ensnode/ensnode-sdk"; -import { getDatasourceContract, maybeGetDatasourceContract } from "@/lib/datasource-helpers"; +import { materializeDomainOwner } from "@/lib/ensv2/domain-db-helpers"; +import { getRegistrarManagedName, registrarTokenIdToLabelHash } from "@/lib/ensv2/registrar-lib"; +import { getLatestRegistration } from "@/lib/ensv2/registration-db-helpers"; +import { getThisAccountId } from "@/lib/get-this-account-id"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; const pluginName = PluginName.ENSv2; -const ethnamesRegistrar = getDatasourceContract( - config.namespace, - DatasourceNames.ENSRoot, - "BaseRegistrar", -); -const basenamesRegistar = maybeGetDatasourceContract( - config.namespace, - DatasourceNames.Basenames, - "BaseRegistrar", -); -const lineanamesRegistar = maybeGetDatasourceContract( - config.namespace, - DatasourceNames.Lineanames, - "BaseRegistrar", -); - -const getRegistrarManagedName = (registrar: AccountId) => { - if (accountIdEqual(ethnamesRegistrar, registrar)) return "eth"; - if (basenamesRegistar && accountIdEqual(basenamesRegistar, registrar)) return "base.eth"; - if (lineanamesRegistar && accountIdEqual(lineanamesRegistar, registrar)) return "linea.eth"; - throw new Error("never"); -}; +// legacy always updates registry so domain is guaranteed to exist +// wrapped always updates registry so domain is guaranteed to exist +// unwrapped always updates registry so domain is guaranteed to exist + +// technically all BaseRegistry-derived contracts have the ability to `registerOnly` and therefore +// don't represent a valid ENS name. then they can be re-registered +// renewals work on registered names because that's obvious +// and in this model it's valid for a registration to reference a domain that does not exist +// and we _don't_ update owner +// that's why registrars manage their own set of owners (registrants), which can be desynced from a domain's owner +// so in the registrar handlers we should never touch schema.domain, only reference them. + +// ok so in Registrar handlers we can reference domains that may or may not exist + +// ok yeah owner of the registration can desync from the registry, because they don't publish changes +// in ownership when transferring tokens. so the owner of a domain probably should be materialized +// to the domain in question (if exists) and export default function () { - ////////////// - // Registrars - ////////////// ponder.on( - namespaceContract(pluginName, "Registrar:NameRegistered"), - async ({ context, event }) => { - // upsert relevant registration for domain - }, - ); + namespaceContract(pluginName, "Registrar:Transfer"), + async ({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ + from: Address; + to: Address; + tokenId: bigint; + }>; + }) => { + const { from, to, tokenId } = event.args; - ponder.on( - namespaceContract(pluginName, "Registrar:NameRegisteredWithRecord"), - async ({ context, event }) => { - // upsert relevant registration for domain - }, - ); + const labelHash = registrarTokenIdToLabelHash(tokenId); + const registrar = getThisAccountId(context, event); + const managedNode = namehash(getRegistrarManagedName(registrar)); + const node = makeSubdomainNode(labelHash, managedNode); + + const domainId = makeENSv1DomainId(node); + const registration = await getLatestRegistration(context, domainId); + + const isMint = isAddressEqual(zeroAddress, from); + const isBurn = isAddressEqual(zeroAddress, to); + + // minting is always followed by Registrar#NameRegistered, safe to ignore + if (isMint) return; + + if (isBurn) { + // requires an existing registration + if (!registration) { + throw new Error(`Invariant(Registrar:Transfer): _burn expected existing Registration`); + } - ponder.on(namespaceContract(pluginName, "Registrar:NameRenewed"), async ({ context, event }) => { - // update registration expiration, add renewal log - }); + // for now, just delete the registration + // TODO: mark Registration as inactive or something instead of burning it + await context.db.delete(schema.registration, { id: registration.id }); + } else { + if (!registration) { + throw new Error(`Invariant(Registrar:Transfer): expected existing Registration`); + } - ponder.on(namespaceContract(pluginName, "Registrar:NameMigrated"), async ({ context, event }) => { - // TODO: what does this mean and from where? - }); + // materialize Domain owner + await materializeDomainOwner(context, domainId, to); + } + }, + ); - async function handleTransfer({ + async function handleNameRegistered({ context, event, }: { context: Context; event: EventWithArgs<{ - from: Address; - to: Address; - tokenId: bigint; + id: bigint; + owner: Address; + expires: bigint; }>; }) { - // + const { id: tokenId, owner, expires: expiration } = event.args; + + const labelHash = registrarTokenIdToLabelHash(tokenId); + const registrar = getThisAccountId(context, event); + const managedNode = namehash(getRegistrarManagedName(registrar)); + const node = makeSubdomainNode(labelHash, managedNode); + + const domainId = makeENSv1DomainId(node); + const registration = await getLatestRegistration(context, domainId); + + // TODO: && isActive(registration) + if (registration) { + throw new Error( + `Invariant(Registrar:NameRegistered): Existing registration found in NameRegistered, expected none.`, + ); + } + + const registrationId = makeRegistrationId(domainId, 0); + + // upsert relevant registration for domain + await context.db.insert(schema.registration).values({ + id: registrationId, + type: "BaseRegistrar", + registrarChainId: registrar.chainId, + registrarAddress: registrar.address, + registrantId: owner, + domainId, + start: event.block.timestamp, + expiration, + // all BaseRegistrar-derived Registrars use the same GRACE_PERIOD + gracePeriod: BigInt(GRACE_PERIOD_SECONDS), + }); + + // materialize Domain owner + await materializeDomainOwner(context, domainId, owner); } + ponder.on(namespaceContract(pluginName, "Registrar:NameRegistered"), handleNameRegistered); ponder.on( - namespaceContract( - pluginName, - "Registrar:Transfer(address indexed from, address indexed to, uint256 indexed id)", - ), - ({ context, event }) => - handleTransfer({ - context, - event: { ...event, args: { ...event.args, tokenId: event.args.id } }, - }), + namespaceContract(pluginName, "Registrar:NameRegisteredWithRecord"), + handleNameRegistered, ); ponder.on( - namespaceContract( - pluginName, - "Registrar:Transfer(address indexed from, address indexed to, uint256 indexed tokenId)", - ), - handleTransfer, + namespaceContract(pluginName, "Registrar:NameRenewed"), + async ({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ id: bigint; expires: bigint }>; + }) => { + const { id: tokenId, expires: expiration } = event.args; + + const labelHash = registrarTokenIdToLabelHash(tokenId); + const registrar = getThisAccountId(context, event); + const managedNode = namehash(getRegistrarManagedName(registrar)); + const node = makeSubdomainNode(labelHash, managedNode); + const domainId = makeENSv1DomainId(node); + const registration = await getLatestRegistration(context, domainId); + + // TODO: || !isActive(registration) + if (!registration) { + throw new Error( + `Invariant(Registrar:NameRenewed): NameRenewed emitted but no active registration.`, + ); + } + + await context.db.update(schema.registration, { id: registration.id }).set({ expiration }); + + // TODO: insert renewal & reference registration + }, ); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/RegistrarController.ts b/apps/ensindexer/src/plugins/ensv2/handlers/RegistrarController.ts new file mode 100644 index 000000000..09e9a5b07 --- /dev/null +++ b/apps/ensindexer/src/plugins/ensv2/handlers/RegistrarController.ts @@ -0,0 +1,160 @@ +import { type Context, ponder } from "ponder:registry"; +import schema from "ponder:schema"; +import { labelhash, namehash } from "viem"; + +import { + type EncodedReferrer, + type LiteralLabel, + labelhashLiteralLabel, + makeENSv1DomainId, + makeSubdomainNode, + PluginName, +} from "@ensnode/ensnode-sdk"; + +import { ensureLabel } from "@/lib/ensv2/label-db-helpers"; +import { getRegistrarManagedName } from "@/lib/ensv2/registrar-lib"; +import { getLatestRegistration } from "@/lib/ensv2/registration-db-helpers"; +import { getThisAccountId } from "@/lib/get-this-account-id"; +import { namespaceContract } from "@/lib/plugin-helpers"; +import type { EventWithArgs } from "@/lib/ponder-helpers"; + +const pluginName = PluginName.ENSv2; + +export default function () { + async function handleNameRegisteredByController({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ + label: string; + baseCost?: bigint; + premium?: bigint; + referrer?: EncodedReferrer; + }>; + }) { + const { label: _label, baseCost, premium, referrer } = event.args; + const label = _label as LiteralLabel; + + const controller = getThisAccountId(context, event); + const managedNode = namehash(getRegistrarManagedName(controller)); + const labelHash = labelhashLiteralLabel(label); + + const node = makeSubdomainNode(labelHash, managedNode); + const domainId = makeENSv1DomainId(node); + const registration = await getLatestRegistration(context, domainId); + + if (!registration) { + throw new Error( + `Invariant(RegistrarController:NameRegistered): NameRegistered but no Registration.`, + ); + } + + // ensure label + await ensureLabel(context, label); + + // update registration's baseCost/premium + await context.db + .update(schema.registration, { id: registration.id }) + .set({ baseCost, premium, referrer }); + } + + async function handleNameRenewedByController({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ + label: string; + baseCost?: bigint; + premium?: bigint; + referrer?: EncodedReferrer; + }>; + }) { + const { label: _label, baseCost, premium, referrer } = event.args; + const label = _label as LiteralLabel; + + const controller = getThisAccountId(context, event); + const managedNode = namehash(getRegistrarManagedName(controller)); + const labelHash = labelhash(label); + const node = makeSubdomainNode(labelHash, managedNode); + const domainId = makeENSv1DomainId(node); + const registration = await getLatestRegistration(context, domainId); + + if (!registration) { + throw new Error( + `Invariant(RegistrarController:NameRenewed): NameRegistered but no Registration.`, + ); + } + + // TODO: update renewal with base/premium + // const renewal = await getLatestRenewal(context, registration.id); + // if (!renewal) invariant + // await context.db.update(schema.renewal, { id: renewal.id }).set({ baseCost, premium, referrer }) + } + + ////////////////////////////////////// + // RegistrarController:NameRegistered + ////////////////////////////////////// + ponder.on( + namespaceContract( + pluginName, + "RegistrarController:NameRegistered(string label, bytes32 indexed labelhash, address indexed owner, uint256 baseCost, uint256 premium, uint256 expires, bytes32 referrer)", + ), + handleNameRegisteredByController, + ); + ponder.on( + namespaceContract( + pluginName, + "RegistrarController:NameRegistered(string name, bytes32 indexed label, address indexed owner, uint256 baseCost, uint256 premium, uint256 expires)", + ), + handleNameRegisteredByController, + ); + ponder.on( + namespaceContract( + pluginName, + "RegistrarController:NameRegistered(string name, bytes32 indexed label, address indexed owner, uint256 cost, uint256 expires)", + ), + handleNameRegisteredByController, + ); + ponder.on( + namespaceContract( + pluginName, + "RegistrarController:NameRegistered(string name, bytes32 indexed label, address indexed owner, uint256 expires)", + ), + handleNameRegisteredByController, + ); + + /////////////////////////////////// + // RegistrarController:NameRenewed + /////////////////////////////////// + ponder.on( + namespaceContract( + pluginName, + "RegistrarController:NameRenewed(string label, bytes32 indexed labelhash, uint256 cost, uint256 expires, bytes32 referrer)", + ), + ({ context, event }) => + handleNameRenewedByController({ + context, + event: { ...event, args: { ...event.args, baseCost: event.args.cost } }, + }), + ); + ponder.on( + namespaceContract( + pluginName, + "RegistrarController:NameRenewed(string name, bytes32 indexed label, uint256 cost, uint256 expires)", + ), + ({ context, event }) => + handleNameRenewedByController({ + context, + event: { ...event, args: { ...event.args, baseCost: event.args.cost } }, + }), + ); + ponder.on( + namespaceContract( + pluginName, + "RegistrarController:NameRenewed(string name, bytes32 indexed label, uint256 expires)", + ), + handleNameRenewedByController, + ); +} diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/basenames/handlers/Registrar.ts b/apps/ensindexer/src/plugins/subgraph/plugins/basenames/handlers/Registrar.ts index 933e6f917..37f647197 100644 --- a/apps/ensindexer/src/plugins/subgraph/plugins/basenames/handlers/Registrar.ts +++ b/apps/ensindexer/src/plugins/subgraph/plugins/basenames/handlers/Registrar.ts @@ -71,7 +71,10 @@ export default function () { ponder.on(namespaceContract(pluginName, "BaseRegistrar:Transfer"), async ({ context, event }) => { await handleNameTransferred({ context, - event: { ...event, args: { ...event.args, labelHash: tokenIdToLabelHash(event.args.id) } }, + event: { + ...event, + args: { ...event.args, labelHash: tokenIdToLabelHash(event.args.tokenId) }, + }, }); }); diff --git a/apps/ensindexer/src/plugins/tokenscope/handlers/BaseRegistrars.ts b/apps/ensindexer/src/plugins/tokenscope/handlers/BaseRegistrars.ts index 267ecef87..35735915c 100644 --- a/apps/ensindexer/src/plugins/tokenscope/handlers/BaseRegistrars.ts +++ b/apps/ensindexer/src/plugins/tokenscope/handlers/BaseRegistrars.ts @@ -46,7 +46,7 @@ export default function () { config.namespace, DatasourceNames.Basenames, "BaseRegistrar", - event.args.id, + event.args.tokenId, ); const metadata: NFTTransferEventMetadata = { diff --git a/packages/datasources/src/abis/basenames/BaseRegistrar.ts b/packages/datasources/src/abis/basenames/BaseRegistrar.ts index 0cf2d6aba..fa18354bb 100644 --- a/packages/datasources/src/abis/basenames/BaseRegistrar.ts +++ b/packages/datasources/src/abis/basenames/BaseRegistrar.ts @@ -254,7 +254,7 @@ export const BaseRegistrar = [ inputs: [ { indexed: true, internalType: "address", name: "from", type: "address" }, { indexed: true, internalType: "address", name: "to", type: "address" }, - { indexed: true, internalType: "uint256", name: "id", type: "uint256" }, + { indexed: true, internalType: "uint256", name: "tokenId", type: "uint256" }, ], name: "Transfer", type: "event", diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/schemas/ensv2.schema.ts index 76c5ec6ef..64aeaa2ec 100644 --- a/packages/ensnode-schema/src/schemas/ensv2.schema.ts +++ b/packages/ensnode-schema/src/schemas/ensv2.schema.ts @@ -1,5 +1,5 @@ import { onchainEnum, onchainTable, relations, uniqueIndex } from "ponder"; -import type { Address, Hex } from "viem"; +import type { Address } from "viem"; import type { ChainId, @@ -14,7 +14,6 @@ import type { RegistrationId, RegistryId, ResolverId, - UnixTimestamp, } from "@ensnode/ensnode-sdk"; // Registry<->Domain is 1:1 @@ -156,6 +155,8 @@ export const registration = onchainTable( start: t.bigint().notNull(), // may have an expiration expiration: t.bigint(), + // maybe have a grace period (BaseRegistrar) + gracePeriod: t.bigint(), // registrar AccountId registrarChainId: t.integer().notNull().$type(), @@ -169,6 +170,10 @@ export const registration = onchainTable( // may have fuses fuses: t.integer(), + + // may have baseCost/premium + baseCost: t.bigint(), + premium: t.bigint(), }), (t) => ({ byId: uniqueIndex().on(t.domainId, t.index), From f95d3561bf0c769bdedb86dc561a04e9a9c42eab Mon Sep 17 00:00:00 2001 From: shrugs Date: Thu, 13 Nov 2025 18:30:28 -0600 Subject: [PATCH 019/102] checkpoint: batch healing --- .../src/lib/ensraibow-api-client.ts | 4 +- .../src/lib/ensv2/label-db-helpers.ts | 2 +- .../ensindexer/src/lib/get-this-account-id.ts | 6 +- apps/ensindexer/src/lib/ponder-helpers.ts | 31 ++++++++- .../ensindexer/src/lib/subgraph/db-helpers.ts | 5 +- .../src/plugins/ensv2/event-handlers.ts | 2 + .../src/plugins/ensv2/handlers/Block.ts | 67 +++++++++++++++++++ apps/ensindexer/src/plugins/ensv2/plugin.ts | 9 +++ .../src/schemas/ensv2.schema.ts | 2 + 9 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 apps/ensindexer/src/plugins/ensv2/handlers/Block.ts diff --git a/apps/ensindexer/src/lib/ensraibow-api-client.ts b/apps/ensindexer/src/lib/ensraibow-api-client.ts index edac19015..d41ed44a1 100644 --- a/apps/ensindexer/src/lib/ensraibow-api-client.ts +++ b/apps/ensindexer/src/lib/ensraibow-api-client.ts @@ -16,9 +16,7 @@ export function getENSRainbowApiClient() { EnsRainbowApiClient.defaultOptions().endpointUrl ) { console.warn( - `Using default public ENSRainbow server which may cause increased network latency. -For production, use your own ENSRainbow server that runs on the same network -as the ENSIndexer server.`, + `Using default public ENSRainbow server which may cause increased network latency. For production, use your own ENSRainbow server that runs on the same network as the ENSIndexer server.`, ); } diff --git a/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts index ef9fbf97e..4f28f6008 100644 --- a/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts @@ -25,6 +25,6 @@ export async function ensureUnknownLabel(context: Context, labelHash: LabelHash) await context.db .insert(schema.label) - .values({ labelHash, value: interpretedLabel }) + .values({ labelHash, value: interpretedLabel, needsHeal: true }) .onConflictDoNothing(); } diff --git a/apps/ensindexer/src/lib/get-this-account-id.ts b/apps/ensindexer/src/lib/get-this-account-id.ts index 9310c2ae5..701879f9c 100644 --- a/apps/ensindexer/src/lib/get-this-account-id.ts +++ b/apps/ensindexer/src/lib/get-this-account-id.ts @@ -1,6 +1,8 @@ -import type { Context, Event } from "ponder:registry"; +import type { Context } from "ponder:registry"; import type { AccountId } from "@ensnode/ensnode-sdk"; -export const getThisAccountId = (context: Context, event: Pick) => +import type { LogEvent } from "@/lib/ponder-helpers"; + +export const getThisAccountId = (context: Context, event: Pick) => ({ chainId: context.chain.id, address: event.log.address }) satisfies AccountId; diff --git a/apps/ensindexer/src/lib/ponder-helpers.ts b/apps/ensindexer/src/lib/ponder-helpers.ts index c03ea0368..d8e48527b 100644 --- a/apps/ensindexer/src/lib/ponder-helpers.ts +++ b/apps/ensindexer/src/lib/ponder-helpers.ts @@ -4,7 +4,7 @@ * as the config object will not be ready yet. */ -import type { Event } from "ponder:registry"; +import type { Event, EventNames } from "ponder:registry"; import type { ChainConfig } from "ponder"; import type { Address, PublicClient } from "viem"; import * as z from "zod/v4"; @@ -21,10 +21,35 @@ import type { BlockInfo, PonderStatus } from "@ensnode/ponder-metadata"; import type { ENSIndexerConfig } from "@/config/types"; -export type EventWithArgs = {}> = Omit & { +/** + * A type that represents only log events (case 6 in the Event conditional type). + * This filters out block events, transaction events, transfer events, call trace events, and setup events. + * + * Valid event names have the pattern: `${ContractName}:${EventName}` where EventName is not "setup". + * Invalid patterns that are excluded: + * - `${string}:block` (block events) + * - `${string}:transaction:${"from" | "to"}` (transaction events) + * - `${string}:transfer:${"from" | "to"}` (transfer events) + * - `${string}.${string}` (call trace events) + * - `${ContractName}:setup` (setup events) + */ +export type LogEvent = T extends `${string}:block` + ? never + : T extends `${string}:transaction:${"from" | "to"}` + ? never + : T extends `${string}:transfer:${"from" | "to"}` + ? never + : T extends `${string}.${string}` + ? never + : T extends `${string}:setup` + ? never + : T extends `${string}:${string}` + ? Event + : never; + +export type EventWithArgs = {}> = Omit & { args: ARGS; }; - /** * Given a contract's block range, returns a block range describing a start and end block * that maintains validity within the global blockrange. The returned start block will always be diff --git a/apps/ensindexer/src/lib/subgraph/db-helpers.ts b/apps/ensindexer/src/lib/subgraph/db-helpers.ts index e55be291a..56a0e48bf 100644 --- a/apps/ensindexer/src/lib/subgraph/db-helpers.ts +++ b/apps/ensindexer/src/lib/subgraph/db-helpers.ts @@ -1,7 +1,8 @@ -import type { Context, Event } from "ponder:registry"; +import type { Context } from "ponder:registry"; import schema from "ponder:schema"; import type { Address } from "viem"; +import type { LogEvent } from "@/lib/ponder-helpers"; import { makeEventId } from "@/lib/subgraph/ids"; export async function upsertAccount(context: Context, address: Address) { @@ -42,7 +43,7 @@ export async function upsertRegistration( } // simplifies generating the shared event column values from the ponder Event object -export function sharedEventValues(chainId: number, event: Omit) { +export function sharedEventValues(chainId: number, event: Omit) { return { id: makeEventId(chainId, event.block.number, event.log.logIndex), blockNumber: event.block.number, diff --git a/apps/ensindexer/src/plugins/ensv2/event-handlers.ts b/apps/ensindexer/src/plugins/ensv2/event-handlers.ts index c4befa12a..e9f154472 100644 --- a/apps/ensindexer/src/plugins/ensv2/event-handlers.ts +++ b/apps/ensindexer/src/plugins/ensv2/event-handlers.ts @@ -1,3 +1,4 @@ +import attach_BlockHandlers from "./handlers/Block"; import attach_ENSv1RegistryHandlers from "./handlers/ENSv1Registry"; import attach_EnhancedAccessControlHandlers from "./handlers/EnhancedAccessControl"; import attach_NameWrapperHandlers from "./handlers/NameWrapper"; @@ -6,6 +7,7 @@ import attach_RegistrarControllerHandlers from "./handlers/RegistrarController"; import attach_RegistryHandlers from "./handlers/Registry"; export default function () { + attach_BlockHandlers(); attach_ENSv1RegistryHandlers(); attach_EnhancedAccessControlHandlers(); attach_NameWrapperHandlers(); diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/Block.ts b/apps/ensindexer/src/plugins/ensv2/handlers/Block.ts new file mode 100644 index 000000000..02084225a --- /dev/null +++ b/apps/ensindexer/src/plugins/ensv2/handlers/Block.ts @@ -0,0 +1,67 @@ +import { ponder } from "ponder:registry"; +import schema from "ponder:schema"; +import { eq } from "ponder"; + +import { + type LiteralLabel, + literalLabelToInterpretedLabel, + PluginName, +} from "@ensnode/ensnode-sdk"; +import { ErrorCode, StatusCode } from "@ensnode/ensrainbow-sdk"; + +import { getENSRainbowApiClient } from "@/lib/ensraibow-api-client"; +import { namespaceContract } from "@/lib/plugin-helpers"; + +const pluginName = PluginName.ENSv2; + +const BATCH_SIZE = 100; +const ensrainbow = getENSRainbowApiClient(); + +export default function () { + ponder.on(namespaceContract(pluginName, "ENSRainbowBatchHeal:block"), async ({ context }) => { + while (true) { + const unhealed = await context.db.sql + .select() + .from(schema.label) + .where(eq(schema.label.needsHeal, true)) + .limit(BATCH_SIZE); + + if (unhealed.length === 0) break; + + // TODO: ENSRainbow batchHeal(unhealed) + const results = await Promise.all( + unhealed.map(async ({ labelHash }) => ({ + labelHash, + response: await ensrainbow.heal(labelHash), + })), + ); + + // NOTE: transactions/batch not supported by Ponder's Postgres Proxy Driver + for (const { labelHash, response } of results) { + switch (response.status) { + case StatusCode.Success: { + const interpretedLabel = literalLabelToInterpretedLabel(response.label as LiteralLabel); + return context.db.sql + .update(schema.label) + .set({ value: interpretedLabel, needsHeal: false }) + .where(eq(schema.label.labelHash, labelHash)); + } + case "error": { + switch (response.errorCode) { + case ErrorCode.BadRequest: + case ErrorCode.NotFound: { + return context.db.sql + .update(schema.label) + .set({ needsHeal: false }) + .where(eq(schema.label.labelHash, labelHash)); + } + case ErrorCode.ServerError: { + // simply no-op ServerErrors so we try again in the next block + } + } + } + } + } + } + }); +} diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index 8052a6e5b..d29f5d6c8 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -60,6 +60,15 @@ export default createPlugin({ ALL_DATASOURCE_NAMES, ), + blocks: { + // trigger ENSRainbowBatchHeal every ENS Root Chain block + [namespaceContract(pluginName, "ENSRainbowBatchHeal")]: { + chain: ensroot.chain.id.toString(), + interval: 1, + startBlock: ensroot.contracts.ENSv1RegistryOld.startBlock, + }, + }, + contracts: { [namespaceContract(pluginName, "Registry")]: { abi: RegistryABI, diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/schemas/ensv2.schema.ts index 64aeaa2ec..f13fdb46d 100644 --- a/packages/ensnode-schema/src/schemas/ensv2.schema.ts +++ b/packages/ensnode-schema/src/schemas/ensv2.schema.ts @@ -278,6 +278,8 @@ export const relations_permissionsUser = relations(permissionsUser, ({ one, many export const label = onchainTable("labels", (t) => ({ labelHash: t.hex().primaryKey().$type(), value: t.text().notNull().$type(), + + needsHeal: t.boolean().default(false), })); export const label_relations = relations(label, ({ many }) => ({ From a35b628fba1dd32a753be4073f30178493f0f9b8 Mon Sep 17 00:00:00 2001 From: shrugs Date: Thu, 13 Nov 2025 18:40:49 -0600 Subject: [PATCH 020/102] checkpoint --- .../plugins/ensv2/handlers/ENSv1Registry.ts | 221 ++++++++++-------- 1 file changed, 122 insertions(+), 99 deletions(-) diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts index c25fa5855..4989a21e3 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts @@ -5,6 +5,8 @@ import schema from "ponder:schema"; import { type Address, isAddressEqual, zeroAddress, zeroHash } from "viem"; import { + ADDR_REVERSE_NODE, + getENSRootChainId, getRootRegistry, getRootRegistryId, type LabelHash, @@ -16,118 +18,135 @@ import { PluginName, } from "@ensnode/ensnode-sdk"; -import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; import { materializeDomainOwner } from "@/lib/ensv2/domain-db-helpers"; -import { ensureUnknownLabel } from "@/lib/ensv2/label-db-helpers"; +import { ensureLabel, ensureUnknownLabel } from "@/lib/ensv2/label-db-helpers"; +import { healAddrReverseSubnameLabel } from "@/lib/heal-addr-reverse-subname-label"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; import { nodeIsMigrated } from "@/lib/protocol-acceleration/registry-migration-status"; const pluginName = PluginName.ENSv2; -async function handleNewOwner({ - context, - event, -}: { - context: Context; - event: EventWithArgs<{ - // NOTE: `node` event arg represents a `Node` that is the _parent_ of the node the NewOwner event is about - node: Node; - // NOTE: `label` event arg represents a `LabelHash` for the sub-node under `node` - label: LabelHash; - owner: Address; - }>; -}) { - const { label: labelHash, node: parentNode, owner } = event.args; - - // if someone mints a node to the zero address, nothing happens in the Registry, so no-op - if (isAddressEqual(zeroAddress, owner)) return; - - // this is either a NEW Domain OR the owner of the parent changing the owner of the child - - const node = makeSubdomainNode(labelHash, parentNode); - const domainId = makeENSv1DomainId(node); - const registryId = - parentNode === zeroHash - ? getRootRegistryId(config.namespace) - : makeImplicitRegistryId(parentNode); - - // TODO: import label healing logic from subgraph plugin - - await ensureUnknownLabel(context, labelHash); - await context.db - .insert(schema.domain) - .values({ - id: domainId, - labelHash, - registryId, - }) - .onConflictDoNothing(); - - // materialize domain owner - // NOTE: despite Domain.ownerId being materialized from other sources of truth (i.e. Registrars - // like BaseRegistrars & NameWrapper) it's ok to always set it here because the Registrar-emitted - // events occur _after_ the Registry events. So when a name is wrapped, for example, the Registry's - // owner changes to that of the NameWrapper but then the NameWrapper emits NameWrapped, and this - // indexing code re-materializes the Domain.ownerId to the NameWraper-emitted value. - await materializeDomainOwner(context, domainId, owner); -} +/** + * Handler functions for ENSv1 Regsitry contracts. + * - piggybacks Protocol Resolution plugin's Node Migration status + */ +export default function () { + /** + * Registry#NewOwner is either a new Domain OR the owner of the parent changing the owner of the child. + */ + async function handleNewOwner({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ + // NOTE: `node` event arg represents a `Node` that is the _parent_ of the node the NewOwner event is about + node: Node; + // NOTE: `label` event arg represents a `LabelHash` for the sub-node under `node` + label: LabelHash; + owner: Address; + }>; + }) { + const { label: labelHash, node: parentNode, owner } = event.args; + + // if someone mints a node to the zero address, nothing happens in the Registry, so no-op + if (isAddressEqual(zeroAddress, owner)) return; + + const node = makeSubdomainNode(labelHash, parentNode); + const domainId = makeENSv1DomainId(node); + const registryId = + parentNode === zeroHash + ? getRootRegistryId(config.namespace) + : makeImplicitRegistryId(parentNode); + + // If this is a direct subname of addr.reverse, we have 100% on-chain label discovery. + // + // Note: Per ENSIP-19, only the ENS Root chain may record primary names under the `addr.reverse` + // subname. Also per ENSIP-19 no Reverse Names need exist in (shadow)Registries on non-root + // chains, so we explicitly only support Root chain addr.reverse-based Reverse Names: ENSIP-19 + // CoinType-specific Reverse Names (ex: [address].[coinType].reverse) don't actually exist in + // the ENS Registry: wildcard resolution is used, so this NewOwner event will never be emitted + // with a domain created as a child of a Coin-Type specific Reverse Node (ex: [coinType].reverse). + if ( + parentNode === ADDR_REVERSE_NODE && + context.chain.id === getENSRootChainId(config.namespace) + ) { + const label = await healAddrReverseSubnameLabel(context, event, labelHash); + await ensureLabel(context, label); + } else { + await ensureUnknownLabel(context, labelHash); + } + + // upsert domain + await context.db + .insert(schema.domain) + .values({ + id: domainId, + labelHash, + registryId, + }) + .onConflictDoNothing(); -async function handleNewResolver({ - context, - event, -}: { - context: Context; - event: EventWithArgs<{ node: Node; resolver: Address }>; -}) { - const { node, resolver: address } = event.args; - - const domainId = makeENSv1DomainId(node); - - // update domain's resolver - const isDeletion = isAddressEqual(address, zeroAddress); - if (isDeletion) { - await context.db.update(schema.domain, { id: domainId }).set({ resolverId: null }); - } else { - const resolverId = makeResolverId({ chainId: context.chain.id, address: address }); - await context.db.update(schema.domain, { id: domainId }).set({ resolverId }); + // materialize domain owner + // NOTE: despite Domain.ownerId being materialized from other sources of truth (i.e. Registrars + // like BaseRegistrars & NameWrapper) it's ok to always set it here because the Registrar-emitted + // events occur _after_ the Registry events. So when a name is registered, for example, the Registry's + // owner changes to that of the NameWrapper but then the NameWrapper emits NameWrapped, and this + // indexing code re-materializes the Domain.ownerId to the NameWraper-emitted value. + await materializeDomainOwner(context, domainId, owner); } -} - -export async function handleTransfer({ - context, - event, -}: { - context: Context; - event: EventWithArgs<{ node: Node; owner: Address }>; -}) { - const { node, owner } = event.args; - // ENSv2 model does not include root node, no-op - if (node === zeroHash) return; - - const domainId = makeENSv1DomainId(node); - - const isDeletion = isAddressEqual(zeroAddress, owner); - if (isDeletion) { - await context.db.delete(schema.domain, { id: domainId }); - return; + async function handleNewResolver({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ node: Node; resolver: Address }>; + }) { + const { node, resolver: address } = event.args; + + const domainId = makeENSv1DomainId(node); + + // update domain's resolver + const isDeletion = isAddressEqual(address, zeroAddress); + if (isDeletion) { + await context.db.update(schema.domain, { id: domainId }).set({ resolverId: null }); + } else { + const resolverId = makeResolverId({ chainId: context.chain.id, address: address }); + await context.db.update(schema.domain, { id: domainId }).set({ resolverId }); + } } - // TODO: if owner is special registrar, ignore - - // ensure owner account - await ensureAccount(context, owner); - - // update owner - await context.db.update(schema.domain, { id: domainId }).set({ ownerId: owner }); -} + async function handleTransfer({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ node: Node; owner: Address }>; + }) { + const { node, owner } = event.args; + + // ENSv2 model does not include root node, no-op + if (node === zeroHash) return; + + const domainId = makeENSv1DomainId(node); + + const isDeletion = isAddressEqual(zeroAddress, owner); + if (isDeletion) { + await context.db.delete(schema.domain, { id: domainId }); + return; + } + + // materialize domain owner + // NOTE: despite Domain.ownerId being materialized from other sources of truth (i.e. Registrars + // like BaseRegistrars & NameWrapper) it's ok to always set it here because the Registrar-emitted + // events occur _after_ the Registry events. So when a name is wrapped, for example, the Registry's + // owner changes to that of the NameWrapper but then the NameWrapper emits NameWrapped, and this + // indexing code re-materializes the Domain.ownerId to the NameWraper-emitted value. + await materializeDomainOwner(context, domainId, owner); + } -/** - * Handler functions for ENSv1 Regsitry contracts. - * - piggybacks Protocol Resolution plugin's Node Migration status - */ -export default function () { /** * Sets up the ENSv2 Root Registry */ @@ -177,6 +196,10 @@ export default function () { }, ); + /** + * Handles Registry#Transfer for: + * - ENS Root Chain's ENSv1RegistryOld + */ ponder.on( namespaceContract(pluginName, "ENSv1RegistryOld:Transfer"), async ({ context, event }) => { From 87f8b001b4be14866ebe226454d5d5873c9de3f7 Mon Sep 17 00:00:00 2001 From: shrugs Date: Thu, 13 Nov 2025 19:02:27 -0600 Subject: [PATCH 021/102] checkpoint --- .../lib/get-latest-registration.ts | 10 ++++ apps/ensapi/src/graphql-api/schema/domain.ts | 12 +++++ .../src/graphql-api/schema/name-or-node.ts | 10 ++++ apps/ensapi/src/graphql-api/schema/query.ts | 1 - .../src/graphql-api/schema/registration.ts | 50 +++++++++++++++++-- .../ensapi/src/graphql-api/schema/resolver.ts | 23 +++++---- apps/ensindexer/src/plugins/ensv2/plugin.ts | 2 + 7 files changed, 95 insertions(+), 13 deletions(-) create mode 100644 apps/ensapi/src/graphql-api/lib/get-latest-registration.ts create mode 100644 apps/ensapi/src/graphql-api/schema/name-or-node.ts diff --git a/apps/ensapi/src/graphql-api/lib/get-latest-registration.ts b/apps/ensapi/src/graphql-api/lib/get-latest-registration.ts new file mode 100644 index 000000000..88b418a16 --- /dev/null +++ b/apps/ensapi/src/graphql-api/lib/get-latest-registration.ts @@ -0,0 +1,10 @@ +import type { DomainId } from "@ensnode/ensnode-sdk"; + +import { db } from "@/lib/db"; + +export async function getLatestRegistration(domainId: DomainId) { + return await db.query.registration.findFirst({ + where: (t, { eq }) => eq(t.domainId, domainId), + orderBy: (t, { asc }) => asc(t.index), + }); +} diff --git a/apps/ensapi/src/graphql-api/schema/domain.ts b/apps/ensapi/src/graphql-api/schema/domain.ts index 12c798fe3..b4d0fb30a 100644 --- a/apps/ensapi/src/graphql-api/schema/domain.ts +++ b/apps/ensapi/src/graphql-api/schema/domain.ts @@ -9,8 +9,10 @@ import { import { builder } from "@/graphql-api/builder"; import { getCanonicalPath } from "@/graphql-api/lib/get-canonical-path"; import { getModelId } from "@/graphql-api/lib/get-id"; +import { getLatestRegistration } from "@/graphql-api/lib/get-latest-registration"; import { rejectAnyErrors } from "@/graphql-api/lib/reject-any-errors"; import { AccountRef } from "@/graphql-api/schema/account"; +import { RegistrationInterfaceRef } from "@/graphql-api/schema/registration"; import { RegistryInterfaceRef } from "@/graphql-api/schema/registry"; import { ResolverRef } from "@/graphql-api/schema/resolver"; import { db } from "@/lib/db"; @@ -163,6 +165,16 @@ DomainRef.implement({ nullable: true, resolve: (parent) => parent.resolverId, }), + + /////////////////////// + // Domain.registration + /////////////////////// + registration: t.field({ + description: "TODO", + type: RegistrationInterfaceRef, + nullable: true, + resolve: (parent) => getLatestRegistration(parent.id), + }), }), }); diff --git a/apps/ensapi/src/graphql-api/schema/name-or-node.ts b/apps/ensapi/src/graphql-api/schema/name-or-node.ts new file mode 100644 index 000000000..764b6673a --- /dev/null +++ b/apps/ensapi/src/graphql-api/schema/name-or-node.ts @@ -0,0 +1,10 @@ +import { builder } from "@/graphql-api/builder"; + +export const NameOrNodeInput = builder.inputType("NameOrNodeInput", { + description: "TODO", + isOneOf: true, + fields: (t) => ({ + name: t.field({ type: "Name" }), + node: t.field({ type: "Node" }), + }), +}); diff --git a/apps/ensapi/src/graphql-api/schema/query.ts b/apps/ensapi/src/graphql-api/schema/query.ts index a5c2a9ae2..f3b607ee5 100644 --- a/apps/ensapi/src/graphql-api/schema/query.ts +++ b/apps/ensapi/src/graphql-api/schema/query.ts @@ -32,7 +32,6 @@ builder.queryType({ nullable: true, resolve: async (parent, args, ctx, info) => { if (args.by.id !== undefined) return args.by.id; - console.log(await getDomainIdByInterpretedName(args.by.name)); return getDomainIdByInterpretedName(args.by.name); }, }), diff --git a/apps/ensapi/src/graphql-api/schema/registration.ts b/apps/ensapi/src/graphql-api/schema/registration.ts index c20d719af..ca278d236 100644 --- a/apps/ensapi/src/graphql-api/schema/registration.ts +++ b/apps/ensapi/src/graphql-api/schema/registration.ts @@ -27,10 +27,15 @@ export type RegistrationInterface = Pick< | "expiration" | "registrarChainId" | "registrarAddress" + | "registrantId" | "referrer" >; export type NameWrapperRegistration = RequiredAndNotNull; -export type BaseRegistrarRegistration = RequiredAndNotNull; +export type BaseRegistrarRegistration = RequiredAndNotNull & { + baseCost: bigint | null; + premium: bigint | null; +}; +export type ThreeDNSRegistration = Registration; RegistrationInterfaceRef.implement({ description: "TODO", @@ -96,6 +101,9 @@ RegistrationInterfaceRef.implement({ }), }); +/////////////////////////// +// NameWrapperRegistration +/////////////////////////// export const NameWrapperRegistrationRef = builder.objectRef("NameWrapperRegistration"); NameWrapperRegistrationRef.implement({ @@ -117,13 +125,49 @@ NameWrapperRegistrationRef.implement({ }), }); +///////////////////////////// +// BaseRegistrarRegistration +///////////////////////////// export const BaseRegistrarRegistrationRef = builder.objectRef( "BaseRegistrarRegistration", ); - BaseRegistrarRegistrationRef.implement({ description: "TODO", interfaces: [RegistrationInterfaceRef], isTypeOf: (value) => (value as RegistrationInterface).type === "BaseRegistrar", - fields: (t) => ({}), + fields: (t) => ({ + ////////////////////////////////////// + // BaseRegistrarRegistration.baseCost + ////////////////////////////////////// + baseCost: t.field({ + description: "TODO", + type: "BigInt", + nullable: true, + resolve: (parent) => parent.baseCost, + }), + + ///////////////////////////////////// + // BaseRegistrarRegistration.premium + ///////////////////////////////////// + premium: t.field({ + description: "TODO", + type: "BigInt", + nullable: true, + resolve: (parent) => parent.premium, + }), + }), +}); + +//////////////////////// +// ThreeDNSRegistration +//////////////////////// +export const ThreeDNSRegistrationRef = + builder.objectRef("ThreeDNSRegistration"); +ThreeDNSRegistrationRef.implement({ + description: "TODO", + interfaces: [RegistrationInterfaceRef], + isTypeOf: (value) => (value as RegistrationInterface).type === "ThreeDNS", + fields: (t) => ({ + // + }), }); diff --git a/apps/ensapi/src/graphql-api/schema/resolver.ts b/apps/ensapi/src/graphql-api/schema/resolver.ts index f14f30bf0..55893b20e 100644 --- a/apps/ensapi/src/graphql-api/schema/resolver.ts +++ b/apps/ensapi/src/graphql-api/schema/resolver.ts @@ -1,3 +1,5 @@ +import { namehash } from "viem"; + import { bigintToCoinType, makeResolverRecordsId, @@ -8,6 +10,7 @@ import { import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-id"; import { AccountIdInput, AccountIdRef } from "@/graphql-api/schema/account-id"; +import { NameOrNodeInput } from "@/graphql-api/schema/name-or-node"; import { db } from "@/lib/db"; export const ResolverRef = builder.loadableObjectRef("Resolver", { @@ -58,18 +61,20 @@ ResolverRef.implement({ //////////////////// // Resolver.records //////////////////// - // TODO: connection to all ResolverRecords by (address, chainId) + // TODO: connection to all ResolverRecords by (address, chainId)? - //////////////////////////// - // Resolver.recordsFor node - //////////////////////////// - recordsFor: t.field({ + ///////////////////////////////// + // Resolver.records by Name or Node + ///////////////////////////////// + records: t.field({ description: "TODO", type: ResolverRecordsRef, - args: { node: t.arg({ type: "Node", required: true }) }, - nullable: false, - resolve: async ({ chainId, address }, { node }) => - makeResolverRecordsId({ chainId, address }, node), + args: { for: t.arg({ type: NameOrNodeInput, required: true }) }, + nullable: true, + resolve: async ({ chainId, address }, args) => { + const node = args.for.node ?? namehash(args.for.name); + return makeResolverRecordsId({ chainId, address }, node); + }, }), }), }); diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index d29f5d6c8..dd5cf1a2a 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -1,5 +1,7 @@ /** * TODO + * - ThreeDNS + * - Registration Renewals */ import { createConfig } from "ponder"; From a592fd777bebfc2bd703435105877d9b8272aee8 Mon Sep 17 00:00:00 2001 From: shrugs Date: Thu, 13 Nov 2025 19:49:53 -0600 Subject: [PATCH 022/102] fix: remove deferred healing --- .../src/lib/ensv2/label-db-helpers.ts | 14 ++- .../src/plugins/ensv2/handlers/Block.ts | 111 ++++++++++-------- apps/ensindexer/src/plugins/ensv2/plugin.ts | 9 -- packages/datasources/src/mainnet.ts | 10 +- .../src/schemas/ensv2.schema.ts | 2 - 5 files changed, 81 insertions(+), 65 deletions(-) diff --git a/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts index 4f28f6008..d2f1470b5 100644 --- a/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts @@ -10,6 +10,8 @@ import { literalLabelToInterpretedLabel, } from "@ensnode/ensnode-sdk"; +import { labelByLabelHash } from "@/lib/graphnode-helpers"; + export async function ensureLabel(context: Context, label: LiteralLabel) { const labelHash = labelhash(label); const interpretedLabel = literalLabelToInterpretedLabel(label); @@ -21,10 +23,18 @@ export async function ensureLabel(context: Context, label: LiteralLabel) { } export async function ensureUnknownLabel(context: Context, labelHash: LabelHash) { - const interpretedLabel = encodeLabelHash(labelHash) as InterpretedLabel; + // do nothing for existing labels, they're either healed or we don't know them + const exists = await context.db.find(schema.label, { labelHash }); + if (exists) return; + + // attempt ENSRainbow heal + const healedLabel = await labelByLabelHash(labelHash); + if (healedLabel) return await ensureLabel(context, healedLabel); + // otherwise + const interpretedLabel = encodeLabelHash(labelHash) as InterpretedLabel; await context.db .insert(schema.label) - .values({ labelHash, value: interpretedLabel, needsHeal: true }) + .values({ labelHash, value: interpretedLabel }) .onConflictDoNothing(); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/Block.ts b/apps/ensindexer/src/plugins/ensv2/handlers/Block.ts index 02084225a..1ea1c115d 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/Block.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/Block.ts @@ -14,54 +14,71 @@ import { namespaceContract } from "@/lib/plugin-helpers"; const pluginName = PluginName.ENSv2; -const BATCH_SIZE = 100; +const BATCH_SIZE = 1000; const ensrainbow = getENSRainbowApiClient(); export default function () { - ponder.on(namespaceContract(pluginName, "ENSRainbowBatchHeal:block"), async ({ context }) => { - while (true) { - const unhealed = await context.db.sql - .select() - .from(schema.label) - .where(eq(schema.label.needsHeal, true)) - .limit(BATCH_SIZE); - - if (unhealed.length === 0) break; - - // TODO: ENSRainbow batchHeal(unhealed) - const results = await Promise.all( - unhealed.map(async ({ labelHash }) => ({ - labelHash, - response: await ensrainbow.heal(labelHash), - })), - ); - - // NOTE: transactions/batch not supported by Ponder's Postgres Proxy Driver - for (const { labelHash, response } of results) { - switch (response.status) { - case StatusCode.Success: { - const interpretedLabel = literalLabelToInterpretedLabel(response.label as LiteralLabel); - return context.db.sql - .update(schema.label) - .set({ value: interpretedLabel, needsHeal: false }) - .where(eq(schema.label.labelHash, labelHash)); - } - case "error": { - switch (response.errorCode) { - case ErrorCode.BadRequest: - case ErrorCode.NotFound: { - return context.db.sql - .update(schema.label) - .set({ needsHeal: false }) - .where(eq(schema.label.labelHash, labelHash)); - } - case ErrorCode.ServerError: { - // simply no-op ServerErrors so we try again in the next block - } - } - } - } - } - } - }); + // this ended up being slower than otherwise, because it runs for every block even in backfill + // and the overhead of the "get unhealed labels that need healing" query is killing any performance + // gain from batching ensrainbow heals. keeping code here for now just in case + // + // ponder.on( + // namespaceContract(pluginName, "ENSRainbowBatchHeal:block"), + // async ({ context, event }) => { + // while (true) { + // const unhealed = await context.db.sql + // .select() + // .from(schema.label) + // .where(eq(schema.label.needsHeal, true)) + // .limit(BATCH_SIZE); + // if (unhealed.length === 0) return; + // console.log(`Healing ${unhealed.length} labels in block ${event.block.number}`); + // // TODO: ENSRainbow batchHeal + // let now = performance.now(); + // const results = await Promise.all( + // unhealed.map(async ({ labelHash }) => ({ + // labelHash, + // response: await ensrainbow.heal(labelHash), + // })), + // ); + // console.log(`ensrainbow duration: ${performance.now() - now}ms`); + // // NOTE: transactions/batch not supported by Ponder's Postgres Proxy Driver + // now = performance.now(); + // for (const { labelHash, response } of results) { + // switch (response.status) { + // case StatusCode.Success: { + // const interpretedLabel = literalLabelToInterpretedLabel( + // response.label as LiteralLabel, + // ); + // await context.db.sql + // .update(schema.label) + // .set({ value: interpretedLabel, needsHeal: false }) + // .where(eq(schema.label.labelHash, labelHash)); + // break; + // } + // case "error": { + // switch (response.errorCode) { + // case ErrorCode.BadRequest: + // case ErrorCode.NotFound: { + // await context.db.sql + // .update(schema.label) + // .set({ needsHeal: false }) + // .where(eq(schema.label.labelHash, labelHash)); + // break; + // } + // case ErrorCode.ServerError: { + // // this requires ENSRainbow to be available, does not tolerate downtime + // // ideally instead we can do some sort of queue for failed tasks... + // throw new Error( + // `Error healing labelHash: "${labelHash}". Error (${response.errorCode}): ${response.error}.`, + // ); + // } + // } + // } + // } + // } + // console.log(`update duration: ${performance.now() - now}ms`); + // } + // }, + // ); } diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index dd5cf1a2a..7d398ec66 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -62,15 +62,6 @@ export default createPlugin({ ALL_DATASOURCE_NAMES, ), - blocks: { - // trigger ENSRainbowBatchHeal every ENS Root Chain block - [namespaceContract(pluginName, "ENSRainbowBatchHeal")]: { - chain: ensroot.chain.id.toString(), - interval: 1, - startBlock: ensroot.contracts.ENSv1RegistryOld.startBlock, - }, - }, - contracts: { [namespaceContract(pluginName, "Registry")]: { abi: RegistryABI, diff --git a/packages/datasources/src/mainnet.ts b/packages/datasources/src/mainnet.ts index 5dbd1db3c..20344719a 100644 --- a/packages/datasources/src/mainnet.ts +++ b/packages/datasources/src/mainnet.ts @@ -102,15 +102,15 @@ export default { RootRegistry: { abi: Registry, address: "0x8a791620dd6260079bf849dc5567adc3f2fdc318", - startBlock: 0, + startBlock: 23794084, }, Registry: { abi: Registry, - startBlock: 0, + startBlock: 23794084, }, EnhancedAccessControl: { abi: EnhancedAccessControl, - startBlock: 0, + startBlock: 23794084, }, }, }, @@ -120,11 +120,11 @@ export default { contracts: { Registry: { abi: Registry, - startBlock: 0, + startBlock: 23794084, }, EnhancedAccessControl: { abi: EnhancedAccessControl, - startBlock: 0, + startBlock: 23794084, }, }, }, diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/schemas/ensv2.schema.ts index f13fdb46d..64aeaa2ec 100644 --- a/packages/ensnode-schema/src/schemas/ensv2.schema.ts +++ b/packages/ensnode-schema/src/schemas/ensv2.schema.ts @@ -278,8 +278,6 @@ export const relations_permissionsUser = relations(permissionsUser, ({ one, many export const label = onchainTable("labels", (t) => ({ labelHash: t.hex().primaryKey().$type(), value: t.text().notNull().$type(), - - needsHeal: t.boolean().default(false), })); export const label_relations = relations(label, ({ many }) => ({ From c08504fcfbf6ad889435e2063edab2919a4210b4 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 14 Nov 2025 11:02:51 -0600 Subject: [PATCH 023/102] checkpoint --- .../src/graphql-api/schema/registration.ts | 16 ++++- .../wrapped-baseregistrar-registration.ts | 25 +++++++ .../ensindexer/src/lib/ensv2/registrar-lib.ts | 2 + .../src/lib/ensv2/registration-db-helpers.ts | 14 ++++ .../src/plugins/ensv2/handlers/NameWrapper.ts | 67 +++++++++++-------- .../src/schemas/ensv2.schema.ts | 5 ++ 6 files changed, 100 insertions(+), 29 deletions(-) create mode 100644 apps/ensapi/src/graphql-api/schema/wrapped-baseregistrar-registration.ts diff --git a/apps/ensapi/src/graphql-api/schema/registration.ts b/apps/ensapi/src/graphql-api/schema/registration.ts index ca278d236..e477fea9d 100644 --- a/apps/ensapi/src/graphql-api/schema/registration.ts +++ b/apps/ensapi/src/graphql-api/schema/registration.ts @@ -4,6 +4,7 @@ import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-id"; import { AccountIdRef } from "@/graphql-api/schema/account-id"; import { DomainRef } from "@/graphql-api/schema/domain"; +import { WrappedBaseRegistrarRegistrationRef } from "@/graphql-api/schema/wrapped-baseregistrar-registration"; import { db } from "@/lib/db"; export const RegistrationInterfaceRef = builder.loadableInterfaceRef("Registration", { @@ -31,7 +32,10 @@ export type RegistrationInterface = Pick< | "referrer" >; export type NameWrapperRegistration = RequiredAndNotNull; -export type BaseRegistrarRegistration = RequiredAndNotNull & { +export type BaseRegistrarRegistration = RequiredAndNotNull< + Registration, + "gracePeriod" | "wrapped" | "wrappedExpiration" | "wrappedFuses" +> & { baseCost: bigint | null; premium: bigint | null; }; @@ -155,6 +159,16 @@ BaseRegistrarRegistrationRef.implement({ nullable: true, resolve: (parent) => parent.premium, }), + + ///////////////////////////////////// + // BaseRegistrarRegistration.wrapped + ///////////////////////////////////// + wrapped: t.field({ + description: "TODO", + type: WrappedBaseRegistrarRegistrationRef, + nullable: true, + resolve: (parent) => (parent.wrapped ? parent : null), + }), }), }); diff --git a/apps/ensapi/src/graphql-api/schema/wrapped-baseregistrar-registration.ts b/apps/ensapi/src/graphql-api/schema/wrapped-baseregistrar-registration.ts new file mode 100644 index 000000000..e15baacd4 --- /dev/null +++ b/apps/ensapi/src/graphql-api/schema/wrapped-baseregistrar-registration.ts @@ -0,0 +1,25 @@ +import { hexToBigInt } from "viem"; + +import type { ENSv1DomainId } from "@ensnode/ensnode-sdk"; + +import { builder } from "@/graphql-api/builder"; +import type { BaseRegistrarRegistration } from "@/graphql-api/schema/registration"; + +export const WrappedBaseRegistrarRegistrationRef = builder.objectRef< + Pick +>("WrappedBaseRegistrarRegistration"); + +WrappedBaseRegistrarRegistrationRef.implement({ + description: "TODO", + fields: (t) => ({ + /////////////////// + // Wrapped.tokenId + /////////////////// + tokenId: t.field({ + description: "TODO", + type: "BigInt", + nullable: false, + resolve: (parent) => hexToBigInt(parent.domainId as ENSv1DomainId), + }), + }), +}); diff --git a/apps/ensindexer/src/lib/ensv2/registrar-lib.ts b/apps/ensindexer/src/lib/ensv2/registrar-lib.ts index 28a5f0d75..2963f6a30 100644 --- a/apps/ensindexer/src/lib/ensv2/registrar-lib.ts +++ b/apps/ensindexer/src/lib/ensv2/registrar-lib.ts @@ -45,6 +45,7 @@ const REGISTRAR_CONTRACTS_BY_MANAGED_NAME: Record = { DatasourceNames.ENSRoot, "UnwrappedEthRegistrarController", ), + ethnamesNameWrapper, ], "base.eth": [ maybeGetDatasourceContract( @@ -75,6 +76,7 @@ const REGISTRAR_CONTRACTS_BY_MANAGED_NAME: Record = { DatasourceNames.Lineanames, "EthRegistrarController", ), + lineanamesNameWrapper, ].filter((c) => !!c), }; diff --git a/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts index 952bac634..9180c0d0c 100644 --- a/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts @@ -10,3 +10,17 @@ export async function getLatestRegistration(context: Context, domainId: DomainId const registrationId = makeRegistrationId(domainId, 0); return await context.db.find(schema.registration, { id: registrationId }); } + +export function isRegistrationActive( + registration: typeof schema.registration.$inferSelect | null, + now: bigint, +) { + // no registration, not active + if (registration === null) return false; + + // no expiration, always active + if (registration.expiration === null) return true; + + // otherwise check against now + return registration.expiration + (registration.gracePeriod ?? 0n) < now; +} diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts b/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts index 5e45bfd76..4d6211e56 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts @@ -3,9 +3,11 @@ import schema from "ponder:schema"; import { type Address, isAddressEqual, zeroAddress } from "viem"; import { + type AccountId, type DNSEncodedLiteralName, type DNSEncodedName, decodeDNSEncodedLiteralName, + LiteralLabel, makeENSv1DomainId, makeRegistrationId, type Node, @@ -16,7 +18,8 @@ import { import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; import { materializeDomainOwner } from "@/lib/ensv2/domain-db-helpers"; import { ensureLabel } from "@/lib/ensv2/label-db-helpers"; -import { getLatestRegistration } from "@/lib/ensv2/registration-db-helpers"; +import { getRegistrarManagedName } from "@/lib/ensv2/registrar-lib"; +import { getLatestRegistration, isRegistrationActive } from "@/lib/ensv2/registration-db-helpers"; import { getThisAccountId } from "@/lib/get-this-account-id"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; @@ -48,6 +51,10 @@ const tokenIdToNode = (tokenId: bigint): Node => uint256ToHex32(tokenId); // so if it's wrapped in the namewrapper, much like the chain, changes are materialized back to the // source +const isSubnameOfRegistrarManagedName = (contract: AccountId, name: DNSEncodedLiteralName) => { + const managedName = getRegistrarManagedName(contract); +}; + export default function () { async function handleTransfer({ context, @@ -105,32 +112,6 @@ export default function () { const registrar = getThisAccountId(context, event); const domainId = makeENSv1DomainId(node); - const registration = await getLatestRegistration(context, domainId); - - // TODO: latest && isActive(latest) - if (registration) { - throw new Error( - `Invariant(NameWrapper:NameWrapped): NameWrapped emitted but an active registration already exists.`, - ); - } - - const registrationId = makeRegistrationId(domainId, 0); // TODO: (latest?.index + 1) ?? 0 - - await ensureAccount(context, owner); - await context.db.insert(schema.registration).values({ - id: registrationId, - type: "NameWrapper", - registrarChainId: registrar.chainId, - registrarAddress: registrar.address, - domainId, - start: event.block.timestamp, - fuses, - expiration, - }); - - // materialize domain owner - await materializeDomainOwner(context, domainId, owner); - // decode name and discover labels try { const labels = decodeDNSEncodedLiteralName(name as DNSEncodedLiteralName); @@ -138,9 +119,39 @@ export default function () { await ensureLabel(context, label); } } catch { - // NameWrapper name decoding failed, no-op + // NameWrapper emitted malformed name? just warn console.warn(`NameWrapper emitted malformed DNSEncoded Name: '${name}'`); } + + const registration = await getLatestRegistration(context, domainId); + const isActive = isRegistrationActive(registration, event.block.timestamp); + + if (registration && isActive && !isSubnameOfRegistrarManagedName()) { + throw new Error(`Invariant()`); + } + + // materialize domain owner + await materializeDomainOwner(context, domainId, owner); + + if (registration && registration.type === "BaseRegistrar") { + // if there's an existing active registration, this this must be the wrap of a + // direct-subname-of-registrar-managed-name + // const managed + } else if (!registration) { + const registrationId = makeRegistrationId(domainId, 0); // TODO: (latest?.index + 1) ?? 0 + await context.db.insert(schema.registration).values({ + id: registrationId, + type: "NameWrapper", + registrarChainId: registrar.chainId, + registrarAddress: registrar.address, + domainId, + start: event.block.timestamp, + fuses, + expiration, + }); + } else { + throw new Error(`NameWrapped but `); + } }, ); diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/schemas/ensv2.schema.ts index 64aeaa2ec..ab0abf697 100644 --- a/packages/ensnode-schema/src/schemas/ensv2.schema.ts +++ b/packages/ensnode-schema/src/schemas/ensv2.schema.ts @@ -174,6 +174,11 @@ export const registration = onchainTable( // may have baseCost/premium baseCost: t.bigint(), premium: t.bigint(), + + // may be Wrapped (BaseRegistrar) + wrapped: t.boolean().default(false), + wrappedFuses: t.integer(), + wrappedExpiration: t.bigint(), }), (t) => ({ byId: uniqueIndex().on(t.domainId, t.index), From d8f301c413e08a7ff39c418113ccc3ed11356911 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 14 Nov 2025 12:07:04 -0600 Subject: [PATCH 024/102] checkpoint --- apps/ensapi/src/graphql-api/schema/domain.ts | 2 + .../src/graphql-api/schema/registration.ts | 5 +- .../ensapi/src/graphql-api/schema/registry.ts | 2 +- .../ensapi/src/graphql-api/schema/resolver.ts | 4 +- .../wrapped-baseregistrar-registration.ts | 18 ++- .../ensindexer/src/lib/ensv2/registrar-lib.ts | 3 +- .../ponder-metadata/zod-schemas.ts | 4 +- .../plugins/ensv2/handlers/ENSv1Registry.ts | 8 + .../src/plugins/ensv2/handlers/NameWrapper.ts | 147 ++++++++++++------ .../src/schemas/ensv2.schema.ts | 2 +- 10 files changed, 137 insertions(+), 58 deletions(-) diff --git a/apps/ensapi/src/graphql-api/schema/domain.ts b/apps/ensapi/src/graphql-api/schema/domain.ts index b4d0fb30a..5b0cedbd8 100644 --- a/apps/ensapi/src/graphql-api/schema/domain.ts +++ b/apps/ensapi/src/graphql-api/schema/domain.ts @@ -32,6 +32,8 @@ export type Domain = Exclude; // we want to dataloader labels by labelhash // we want to dataloader a domain's canonical path, but without exposing it +// TODO: consider interface with ... on ENSv2Domain { canonicalId } etc +// ... on ENSv1Domain { node } etc DomainRef.implement({ description: "a Domain", diff --git a/apps/ensapi/src/graphql-api/schema/registration.ts b/apps/ensapi/src/graphql-api/schema/registration.ts index e477fea9d..c37a9aaac 100644 --- a/apps/ensapi/src/graphql-api/schema/registration.ts +++ b/apps/ensapi/src/graphql-api/schema/registration.ts @@ -34,7 +34,7 @@ export type RegistrationInterface = Pick< export type NameWrapperRegistration = RequiredAndNotNull; export type BaseRegistrarRegistration = RequiredAndNotNull< Registration, - "gracePeriod" | "wrapped" | "wrappedExpiration" | "wrappedFuses" + "gracePeriod" | "wrapped" | "wrappedExpiration" | "wrappedFuses" | "wrappedOwnerId" > & { baseCost: bigint | null; premium: bigint | null; @@ -123,8 +123,7 @@ NameWrapperRegistrationRef.implement({ type: "Int", nullable: false, // TODO: decode/render Fuses enum - // biome-ignore lint/style/noNonNullAssertion: guaranteed in NameWrapperRegistration - resolve: (parent) => parent.fuses!, + resolve: (parent) => parent.fuses, }), }), }); diff --git a/apps/ensapi/src/graphql-api/schema/registry.ts b/apps/ensapi/src/graphql-api/schema/registry.ts index afe686eef..7158976b7 100644 --- a/apps/ensapi/src/graphql-api/schema/registry.ts +++ b/apps/ensapi/src/graphql-api/schema/registry.ts @@ -20,7 +20,7 @@ export const RegistryInterfaceRef = builder.loadableInterfaceRef("Registry", { export type Registry = Exclude; export type RegistryInterface = Pick; export type RegistryContract = RequiredAndNotNull; -export type ImplicitRegistry = RequiredAndNotNull; +export type ImplicitRegistry = Registry; RegistryInterfaceRef.implement({ description: "TODO", diff --git a/apps/ensapi/src/graphql-api/schema/resolver.ts b/apps/ensapi/src/graphql-api/schema/resolver.ts index 55893b20e..403c051bf 100644 --- a/apps/ensapi/src/graphql-api/schema/resolver.ts +++ b/apps/ensapi/src/graphql-api/schema/resolver.ts @@ -63,9 +63,9 @@ ResolverRef.implement({ //////////////////// // TODO: connection to all ResolverRecords by (address, chainId)? - ///////////////////////////////// + //////////////////////////////////// // Resolver.records by Name or Node - ///////////////////////////////// + //////////////////////////////////// records: t.field({ description: "TODO", type: ResolverRecordsRef, diff --git a/apps/ensapi/src/graphql-api/schema/wrapped-baseregistrar-registration.ts b/apps/ensapi/src/graphql-api/schema/wrapped-baseregistrar-registration.ts index e15baacd4..2d996d1af 100644 --- a/apps/ensapi/src/graphql-api/schema/wrapped-baseregistrar-registration.ts +++ b/apps/ensapi/src/graphql-api/schema/wrapped-baseregistrar-registration.ts @@ -5,9 +5,9 @@ import type { ENSv1DomainId } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import type { BaseRegistrarRegistration } from "@/graphql-api/schema/registration"; -export const WrappedBaseRegistrarRegistrationRef = builder.objectRef< - Pick ->("WrappedBaseRegistrarRegistration"); +export const WrappedBaseRegistrarRegistrationRef = builder.objectRef( + "WrappedBaseRegistrarRegistration", +); WrappedBaseRegistrarRegistrationRef.implement({ description: "TODO", @@ -19,7 +19,19 @@ WrappedBaseRegistrarRegistrationRef.implement({ description: "TODO", type: "BigInt", nullable: false, + // NOTE: only ENSv1 Domains can be wrapped, id is guaranteed to be ENSv1DomainId === Node resolve: (parent) => hexToBigInt(parent.domainId as ENSv1DomainId), }), + + ///////////////// + // Wrapped.fuses + ///////////////// + fuses: t.field({ + description: "TODO", + type: "Int", + nullable: false, + // TODO: decode/render Fuses enum + resolve: (parent) => parent.wrappedFuses, + }), }), }); diff --git a/apps/ensindexer/src/lib/ensv2/registrar-lib.ts b/apps/ensindexer/src/lib/ensv2/registrar-lib.ts index 2963f6a30..8962f4057 100644 --- a/apps/ensindexer/src/lib/ensv2/registrar-lib.ts +++ b/apps/ensindexer/src/lib/ensv2/registrar-lib.ts @@ -4,6 +4,7 @@ import { DatasourceNames } from "@ensnode/datasources"; import { type AccountId, accountIdEqual, + type InterpretedName, type LabelHash, type Name, uint256ToHex32, @@ -83,7 +84,7 @@ const REGISTRAR_CONTRACTS_BY_MANAGED_NAME: Record = { export const getRegistrarManagedName = (contract: AccountId) => { for (const [managedName, contracts] of Object.entries(REGISTRAR_CONTRACTS_BY_MANAGED_NAME)) { const isAnyOfTheContracts = contracts.some((_contract) => accountIdEqual(_contract, contract)); - if (isAnyOfTheContracts) return managedName; + if (isAnyOfTheContracts) return managedName as InterpretedName; } throw new Error("never"); diff --git a/apps/ensindexer/src/lib/indexing-status/ponder-metadata/zod-schemas.ts b/apps/ensindexer/src/lib/indexing-status/ponder-metadata/zod-schemas.ts index 2e0f0d777..21914cb55 100644 --- a/apps/ensindexer/src/lib/indexing-status/ponder-metadata/zod-schemas.ts +++ b/apps/ensindexer/src/lib/indexing-status/ponder-metadata/zod-schemas.ts @@ -27,9 +27,9 @@ const makeChainNameSchema = (indexedChainNames: string[]) => z.enum(indexedChain const PonderBlockRefSchema = makeBlockRefSchema(); -const PonderCommandSchema = z.enum(["dev", "start"]); +const PonderCommandSchema = z.enum(["dev", "start", "serve"]); -const PonderOrderingSchema = z.literal("omnichain"); +const PonderOrderingSchema = z.literal("omnichain").prefault("omnichain"); export const PonderAppSettingsSchema = z.strictObject({ command: PonderCommandSchema, diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts index 4989a21e3..e828628f0 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts @@ -59,6 +59,7 @@ export default function () { parentNode === zeroHash ? getRootRegistryId(config.namespace) : makeImplicitRegistryId(parentNode); + const subregistryId = makeImplicitRegistryId(node); // If this is a direct subname of addr.reverse, we have 100% on-chain label discovery. // @@ -85,9 +86,16 @@ export default function () { id: domainId, labelHash, registryId, + subregistryId, }) .onConflictDoNothing(); + // upsert the domain's own ImplicitRegistry + await context.db + .insert(schema.registry) + .values({ id: subregistryId, type: "ImplicitRegistry" }) + .onConflictDoNothing(); + // materialize domain owner // NOTE: despite Domain.ownerId being materialized from other sources of truth (i.e. Registrars // like BaseRegistrars & NameWrapper) it's ok to always set it here because the Registrar-emitted diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts b/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts index 4d6211e56..ce113ea7f 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts @@ -1,21 +1,20 @@ import { type Context, ponder } from "ponder:registry"; import schema from "ponder:schema"; -import { type Address, isAddressEqual, zeroAddress } from "viem"; +import { type Address, isAddressEqual, labelhash, namehash, zeroAddress } from "viem"; import { - type AccountId, type DNSEncodedLiteralName, type DNSEncodedName, decodeDNSEncodedLiteralName, - LiteralLabel, + type LiteralLabel, makeENSv1DomainId, makeRegistrationId, + makeSubdomainNode, type Node, PluginName, uint256ToHex32, } from "@ensnode/ensnode-sdk"; -import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; import { materializeDomainOwner } from "@/lib/ensv2/domain-db-helpers"; import { ensureLabel } from "@/lib/ensv2/label-db-helpers"; import { getRegistrarManagedName } from "@/lib/ensv2/registrar-lib"; @@ -51,8 +50,31 @@ const tokenIdToNode = (tokenId: bigint): Node => uint256ToHex32(tokenId); // so if it's wrapped in the namewrapper, much like the chain, changes are materialized back to the // source -const isSubnameOfRegistrarManagedName = (contract: AccountId, name: DNSEncodedLiteralName) => { - const managedName = getRegistrarManagedName(contract); +const isDirectSubnameOfRegistrarManagedName = ( + managedNode: Node, + name: DNSEncodedLiteralName, + node: Node, +) => { + let labels: LiteralLabel[]; + try { + labels = decodeDNSEncodedLiteralName(name); + + // extra runtime assertion of valid decode + if (labels.length === 0) throw new Error("never"); + } catch { + // must be decodable + throw new Error( + `Invariant(isSubnameOfRegistrarManagedName): NameWrapper emitted DNSEncodedNames for direct-subnames-of-registrar-managed-names MUST be decodable`, + ); + } + + // construct the expected node using emitted name's leaf label and the registrarManagedNode + // biome-ignore lint/style/noNonNullAssertion: length check above + const leaf = labelhash(labels[0]!); + const expectedNode = makeSubdomainNode(leaf, managedNode); + + // Nodes must exactly match + return node === expectedNode; }; export default function () { @@ -81,11 +103,11 @@ export default function () { const domainId = makeENSv1DomainId(tokenIdToNode(tokenId)); const registration = await getLatestRegistration(context, domainId); + const isActive = isRegistrationActive(registration, event.block.timestamp); - // TODO: || !isActive(registration) ? - // TODO: specifically check that there are no NameWrapper Registrations? - if (!registration) { - throw new Error(`Invariant(NameWrapper:Transfer): Expected registration.`); + // Invariant: must have active Registration + if (!registration || !isActive) { + throw new Error(`Invariant(NameWrapper:Transfer): Active Registration expected.`); } // materialize domain owner @@ -107,14 +129,15 @@ export default function () { expiry: bigint; }>; }) => { - const { node, name, owner, fuses, expiry: expiration } = event.args; + const { node, name: _name, owner, fuses, expiry: expiration } = event.args; + const name = _name as DNSEncodedLiteralName; const registrar = getThisAccountId(context, event); const domainId = makeENSv1DomainId(node); // decode name and discover labels try { - const labels = decodeDNSEncodedLiteralName(name as DNSEncodedLiteralName); + const labels = decodeDNSEncodedLiteralName(name); for (const label of labels) { await ensureLabel(context, label); } @@ -126,32 +149,49 @@ export default function () { const registration = await getLatestRegistration(context, domainId); const isActive = isRegistrationActive(registration, event.block.timestamp); - if (registration && isActive && !isSubnameOfRegistrarManagedName()) { - throw new Error(`Invariant()`); - } - // materialize domain owner await materializeDomainOwner(context, domainId, owner); - if (registration && registration.type === "BaseRegistrar") { - // if there's an existing active registration, this this must be the wrap of a + if (registration && isActive && registration.type === "BaseRegistrar") { + // if there's an existing active BaseRegistrar registration, this this must be the wrap of a // direct-subname-of-registrar-managed-name - // const managed - } else if (!registration) { - const registrationId = makeRegistrationId(domainId, 0); // TODO: (latest?.index + 1) ?? 0 - await context.db.insert(schema.registration).values({ - id: registrationId, - type: "NameWrapper", - registrarChainId: registrar.chainId, - registrarAddress: registrar.address, - domainId, - start: event.block.timestamp, - fuses, - expiration, + + const managedNode = namehash(getRegistrarManagedName(getThisAccountId(context, event))); + + // Invariant: emitted Name is decodable and is a direct subname of the RegistrarManagedName + if (!isDirectSubnameOfRegistrarManagedName(managedNode, name, node)) { + throw new Error( + `Invariant(NameWrapper:NameWrapped): An active BaseRegistrar Registration was found, but the name in question is NOT a direct subname of this NameWrapper's BaseRegistrar's RegistrarManagedName — wtf?`, + ); + } + + await context.db.update(schema.registration, { id: registration.id }).set({ + wrapped: true, + wrappedFuses: fuses, + // expiration, // TODO: NameWrapper expiration logic }); - } else { - throw new Error(`NameWrapped but `); } + + // Invariant: there shouldn't be an active registration + if (registration && isActive) { + throw new Error( + `Invariant(NameWrapper:NameWrapped): NameWrapped but there's an existing active non-BaseRegistrar Registration.`, + ); + } + + // create a new NameWrapper Registration + const nextIndex = registration ? registration.index + 1 : 0; + const registrationId = makeRegistrationId(domainId, nextIndex); + await context.db.insert(schema.registration).values({ + id: registrationId, + type: "NameWrapper", + registrarChainId: registrar.chainId, + registrarAddress: registrar.address, + domainId, + start: event.block.timestamp, + fuses, + expiration, + }); }, ); @@ -167,14 +207,24 @@ export default function () { const { node } = event.args; const domainId = makeENSv1DomainId(node); - const latest = await getLatestRegistration(context, domainId); + const registration = await getLatestRegistration(context, domainId); - if (!latest) { + if (!registration) { throw new Error(`Invariant(NameWrapper:NameUnwrapped): Registration expected`); } - // TODO: instead of deleting, mark it as inactive perhaps by setting its expiry to block.timestamp - await context.db.delete(schema.registration, { id: latest.id }); + if (registration.type === "BaseRegistrar") { + // if this is a wrapped BaseRegisrar Registration, unwrap it + await context.db.update(schema.registration, { id: registration.id }).set({ + wrapped: false, + wrappedFuses: null, + // expiration: null // TODO: NameWrapper expiration logic + }); + } else { + // otherwise, deactivate the NameWrapper Registrations + // TODO: instead of deleting, mark it as inactive perhaps by setting its expiry to block.timestamp + await context.db.delete(schema.registration, { id: registration.id }); + } // NOTE: we don't need to adjust Domain.ownerId because NameWrapper always calls ens.setOwner }, @@ -193,16 +243,22 @@ export default function () { const domainId = makeENSv1DomainId(node); const registration = await getLatestRegistration(context, domainId); + const isActive = isRegistrationActive(registration, event.block.timestamp); - // TODO: || !isActive(registration) - if (!registration) { - throw new Error(`Invariant(NameWrapper:FusesSet): Registration expected.`); + // Invariant: must have active Registration + if (!registration || !isActive) { + throw new Error(`Invariant(NameWrapper:FusesSet): Active Registration expected.`); } // upsert fuses - await context.db.update(schema.registration, { id: registration.id }).set({ fuses }); - - // TODO: expiration-related logic? + if (registration.type === "BaseRegistrar") { + await context.db.update(schema.registration, { id: registration.id }).set({ + wrappedFuses: fuses, + // expiration: // TODO: NameWrapper expiration logic + }); + } else { + await context.db.update(schema.registration, { id: registration.id }).set({ fuses }); + } }, ); @@ -219,10 +275,11 @@ export default function () { const domainId = makeENSv1DomainId(node); const registration = await getLatestRegistration(context, domainId); + const isActive = isRegistrationActive(registration, event.block.timestamp); - // TODO: || !isActive(registration) - if (!registration) { - throw new Error(`Invariant(NameWrapper:FusesSet): Registration expected.`); + // Invariant: must have active Registration + if (!registration || !isActive) { + throw new Error(`Invariant(NameWrapper:ExpiryExtended): Active Registration expected.`); } await context.db.update(schema.registration, { id: registration.id }).set({ expiration }); diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/schemas/ensv2.schema.ts index ab0abf697..d83e65489 100644 --- a/packages/ensnode-schema/src/schemas/ensv2.schema.ts +++ b/packages/ensnode-schema/src/schemas/ensv2.schema.ts @@ -56,9 +56,9 @@ export const registry = onchainTable( id: t.text().primaryKey().$type(), type: registryType().notNull(), + // has contract AccountId (RegistryContract) chainId: t.integer().$type(), address: t.hex().$type
(), - parentDomainNode: t.hex().$type(), }), (t) => ({ // From ceeeb38517c65e4c33d31e7b4efb12bf46cd3f9c Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 14 Nov 2025 12:15:28 -0600 Subject: [PATCH 025/102] remove holesky --- .github/workflows/deploy_ensnode.yml | 13 ---- .../deploy_switch_ensnode_environment.yml | 4 - apps/ensadmin/.env.local.example | 4 +- .../src/app/mock/config-info/data.json | 31 +------- .../src/components/chains/ChainIcon.tsx | 4 - .../src/lib/default-records-selection.ts | 4 - apps/ensadmin/src/lib/env.ts | 1 - apps/ensadmin/src/lib/namespace-utils.ts | 8 -- apps/ensapi/.env.local.example | 3 +- apps/ensapi/src/lib/thegraph.ts | 2 - apps/ensindexer/.env.local.example | 10 +-- apps/ensindexer/src/config/validations.ts | 2 +- apps/ensindexer/src/lib/currencies.ts | 18 +---- apps/ensindexer/src/lib/datasource-helpers.ts | 6 +- .../ensindexer/src/lib/ensv2/registrar-lib.ts | 1 + .../src/lib/tokenscope/nft-issuers.ts | 5 +- apps/ensindexer/src/lib/tokenscope/seaport.ts | 3 +- .../basenames/lib/registrar-helpers.ts | 1 - .../ethnames/lib/registrar-helpers.ts | 1 - .../lineanames/lib/registrar-helpers.ts | 1 - .../basenames/lib/registrar-helpers.ts | 1 - .../lineanames/lib/registrar-helpers.ts | 1 - .../docs/docs/concepts/what-is-ensnode.mdx | 2 +- .../concepts/what-is-the-ens-subgraph.mdx | 8 +- .../content/docs/docs/deploying/terraform.mdx | 5 +- .../docs/usage/hosted-ensnode-instances.mdx | 10 --- packages/datasources/README.md | 7 +- packages/datasources/src/holesky.ts | 78 ------------------- packages/datasources/src/lib/types.ts | 5 +- packages/datasources/src/namespaces.ts | 9 +-- .../src/shared/config/build-rpc-urls.ts | 5 -- terraform/.env.sample | 8 +- terraform/main.tf | 9 --- 33 files changed, 29 insertions(+), 241 deletions(-) delete mode 100644 packages/datasources/src/holesky.ts diff --git a/.github/workflows/deploy_ensnode.yml b/.github/workflows/deploy_ensnode.yml index 6c89e8337..e42740cbc 100644 --- a/.github/workflows/deploy_ensnode.yml +++ b/.github/workflows/deploy_ensnode.yml @@ -87,9 +87,6 @@ jobs: #SEPOLIA echo "SEPOLIA_API_SVC_ID="${{ secrets.GREEN_SEPOLIA_API_SVC_ID }} >> "$GITHUB_ENV" echo "SEPOLIA_INDEXER_SVC_ID="${{ secrets.GREEN_SEPOLIA_INDEXER_SVC_ID }} >> "$GITHUB_ENV" - #HOLESKY - echo "HOLESKY_API_SVC_ID="${{ secrets.GREEN_HOLESKY_API_SVC_ID }} >> "$GITHUB_ENV" - echo "HOLESKY_INDEXER_SVC_ID="${{ secrets.GREEN_HOLESKY_INDEXER_SVC_ID }} >> "$GITHUB_ENV" #ENSRAINBOW echo "ENSRAINBOW_SVC_ID="${{ secrets.GREEN_ENSRAINBOW_SVC_ID }} >> "$GITHUB_ENV" #ENSADMIN @@ -111,9 +108,6 @@ jobs: #SEPOLIA echo "SEPOLIA_API_SVC_ID="${{ secrets.BLUE_SEPOLIA_API_SVC_ID }} >> "$GITHUB_ENV" echo "SEPOLIA_INDEXER_SVC_ID="${{ secrets.BLUE_SEPOLIA_INDEXER_SVC_ID }} >> "$GITHUB_ENV" - #HOLESKY - echo "HOLESKY_API_SVC_ID="${{ secrets.BLUE_HOLESKY_API_SVC_ID }} >> "$GITHUB_ENV" - echo "HOLESKY_INDEXER_SVC_ID="${{ secrets.BLUE_HOLESKY_INDEXER_SVC_ID }} >> "$GITHUB_ENV" #ENSRAINBOW echo "ENSRAINBOW_SVC_ID="${{ secrets.BLUE_ENSRAINBOW_SVC_ID }} >> "$GITHUB_ENV" #ENSADMIN @@ -162,9 +156,6 @@ jobs: #SEPOLIA update_service_image ${RAILWAY_ENVIRONMENT_ID} ${SEPOLIA_API_SVC_ID} ${{ env.ENSAPI_DOCKER_IMAGE }} update_service_image ${RAILWAY_ENVIRONMENT_ID} ${SEPOLIA_INDEXER_SVC_ID} ${{ env.ENSINDEXER_DOCKER_IMAGE }} - #HOLESKY - update_service_image ${RAILWAY_ENVIRONMENT_ID} ${HOLESKY_API_SVC_ID} ${{ env.ENSAPI_DOCKER_IMAGE }} - update_service_image ${RAILWAY_ENVIRONMENT_ID} ${HOLESKY_INDEXER_SVC_ID} ${{ env.ENSINDEXER_DOCKER_IMAGE }} #ENSRAINBOW update_service_image ${RAILWAY_ENVIRONMENT_ID} ${ENSRAINBOW_SVC_ID} ${{ env.ENSRAINBOW_DOCKER_IMAGE }} #ENSADMIN @@ -192,7 +183,6 @@ jobs: set_shared_variable ${RAILWAY_ENVIRONMENT_ID} "MAINNET_DATABASE_SCHEMA" "mainnetSchema${TAG}" set_shared_variable ${RAILWAY_ENVIRONMENT_ID} "ALPHA-SEPOLIA_DATABASE_SCHEMA" "alphaSepoliaSchema${TAG}" set_shared_variable ${RAILWAY_ENVIRONMENT_ID} "SEPOLIA_DATABASE_SCHEMA" "sepoliaSchema${TAG}" - set_shared_variable ${RAILWAY_ENVIRONMENT_ID} "HOLESKY_DATABASE_SCHEMA" "holeskySchema${TAG}" - name: Redeploy ENSNode instances run: | @@ -221,9 +211,6 @@ jobs: #SEPOLIA redeploy_service ${RAILWAY_ENVIRONMENT_ID} ${SEPOLIA_API_SVC_ID} redeploy_service ${RAILWAY_ENVIRONMENT_ID} ${SEPOLIA_INDEXER_SVC_ID} - #HOLESKY - redeploy_service ${RAILWAY_ENVIRONMENT_ID} ${HOLESKY_API_SVC_ID} - redeploy_service ${RAILWAY_ENVIRONMENT_ID} ${HOLESKY_INDEXER_SVC_ID} #ENSRAINBOW redeploy_service ${RAILWAY_ENVIRONMENT_ID} ${ENSRAINBOW_SVC_ID} #ENSADMIN diff --git a/.github/workflows/deploy_switch_ensnode_environment.yml b/.github/workflows/deploy_switch_ensnode_environment.yml index 13bc55ec1..4f048c444 100644 --- a/.github/workflows/deploy_switch_ensnode_environment.yml +++ b/.github/workflows/deploy_switch_ensnode_environment.yml @@ -64,10 +64,6 @@ jobs: redis-cli -u $REDIS_URL SET traefik/http/routers/sepolia-api-router/service "${TARGET_ENVIRONMENT}-sepolia-api" redis-cli -u $REDIS_URL SET traefik/http/routers/sepolia-indexer-router/service "${TARGET_ENVIRONMENT}-sepolia-indexer" - # HOLESKY - redis-cli -u $REDIS_URL SET traefik/http/routers/holesky-api-router/service "${TARGET_ENVIRONMENT}-holesky-api" - redis-cli -u $REDIS_URL SET traefik/http/routers/holesky-indexer-router/service "${TARGET_ENVIRONMENT}-holesky-indexer" - # ENSRAINBOW redis-cli -u $REDIS_URL SET traefik/http/routers/ensrainbow-api-router/service "${TARGET_ENVIRONMENT}-ensrainbow-api" diff --git a/apps/ensadmin/.env.local.example b/apps/ensadmin/.env.local.example index 04584a75a..60ee0b83c 100644 --- a/apps/ensadmin/.env.local.example +++ b/apps/ensadmin/.env.local.example @@ -7,8 +7,8 @@ ENSADMIN_PUBLIC_URL=http://localhost:4173 # Server's library of ENSNode URLs offered as connection options in the connection picker. -# Optional. If not set, defaults to `DEFAULT_SERVER_CONNECTION_LIBRARY` (https://api.alpha.ensnode.io,https://api.alpha-sepolia.ensnode.io,https://api.mainnet.ensnode.io,https://api.sepolia.ensnode.io,https://api.holesky.ensnode.io). +# Optional. If not set, defaults to `DEFAULT_SERVER_CONNECTION_LIBRARY` (https://api.alpha.ensnode.io,https://api.alpha-sepolia.ensnode.io,https://api.mainnet.ensnode.io,https://api.sepolia.ensnode.io). # Note: it must be a comma-separated list of URLs that are accessible from a web browser # (i.e. it cannot be a hostname in a docker network) # Note: if a user doesn't explicitly select an ENSNode connection then, by default, ENSAdmin will automatically select the first URL in this list as the ENSNode instance to connect the user to. -NEXT_PUBLIC_SERVER_CONNECTION_LIBRARY=https://api.alpha.ensnode.io,https://api.alpha-sepolia.ensnode.io,https://api.mainnet.ensnode.io,https://api.sepolia.ensnode.io,https://api.holesky.ensnode.io +NEXT_PUBLIC_SERVER_CONNECTION_LIBRARY=https://api.alpha.ensnode.io,https://api.alpha-sepolia.ensnode.io,https://api.mainnet.ensnode.io,https://api.sepolia.ensnode.io diff --git a/apps/ensadmin/src/app/mock/config-info/data.json b/apps/ensadmin/src/app/mock/config-info/data.json index 94e906485..6ef858f9d 100644 --- a/apps/ensadmin/src/app/mock/config-info/data.json +++ b/apps/ensadmin/src/app/mock/config-info/data.json @@ -122,33 +122,6 @@ "isSubgraphCompatible": true } }, - "Subgraph Holesky": { - "version": "0.35.0", - "theGraphFallback": { - "canFallback": false, - "reason": "no-api-key" - }, - "ensIndexerPublicConfig": { - "labelSet": { - "labelSetId": "subgraph", - "labelSetVersion": 0 - }, - "versionInfo": { - "nodejs": "22.18.0", - "ponder": "0.11.43", - "ensDb": "0.35.0", - "ensIndexer": "0.35.0", - "ensNormalize": "1.11.1", - "ensRainbow": "0.34.0", - "ensRainbowSchema": 3 - }, - "indexedChainIds": [17000], - "namespace": "holesky", - "plugins": ["subgraph"], - "databaseSchemaName": "holeskySchema0.34.0", - "isSubgraphCompatible": true - } - }, "Serialization Error": { "version": "0.35.0", "theGraphFallback": { @@ -169,8 +142,8 @@ "ensRainbow": "", "ensRainbowSchema": -1 }, - "indexedChainIds": [17000], - "namespace": "holesky", + "indexedChainIds": [11155111], + "namespace": "sepolia", "plugins": ["subgraph"], "databaseSchemaName": "DeserializationSchema0.34.0", "isSubgraphCompatible": true diff --git a/apps/ensadmin/src/components/chains/ChainIcon.tsx b/apps/ensadmin/src/components/chains/ChainIcon.tsx index c1c05938d..831138203 100644 --- a/apps/ensadmin/src/components/chains/ChainIcon.tsx +++ b/apps/ensadmin/src/components/chains/ChainIcon.tsx @@ -3,7 +3,6 @@ import { arbitrumSepolia, base, baseSepolia, - holesky, linea, lineaSepolia, mainnet, @@ -58,9 +57,6 @@ const chainIcons = new Map([ [ensTestEnvL1Chain.id, "Ethereum Local (ens-test-env)"], [mainnet.id, "Ethereum"], [sepolia.id, "Ethereum Sepolia"], - [holesky.id, "Ethereum Holesky"], [base.id, "Base"], [baseSepolia.id, "Base Sepolia"], [linea.id, "Linea"], @@ -84,8 +81,6 @@ export function getEnsManagerAppUrl(namespaceId: ENSNamespaceId): URL | null { return new URL(`https://app.ens.domains/`); case ENSNamespaceIds.Sepolia: return new URL(`https://sepolia.app.ens.domains/`); - case ENSNamespaceIds.Holesky: - return new URL(`https://holesky.app.ens.domains/`); case ENSNamespaceIds.EnsTestEnv: // ens-test-env runs on a local chain and is not supported by app.ens.domains return null; @@ -115,9 +110,6 @@ export function buildEnsMetadataServiceAvatarUrl( return new URL(name, `https://metadata.ens.domains/mainnet/avatar/`); case ENSNamespaceIds.Sepolia: return new URL(name, `https://metadata.ens.domains/sepolia/avatar/`); - case ENSNamespaceIds.Holesky: - // metadata.ens.domains doesn't currently support holesky - return null; case ENSNamespaceIds.EnsTestEnv: // ens-test-env runs on a local chain and is not supported by metadata.ens.domains // TODO: Above comment is not true. Details at https://github.com/namehash/ensnode/issues/1078 diff --git a/apps/ensapi/.env.local.example b/apps/ensapi/.env.local.example index 07a875e2f..101795ae3 100644 --- a/apps/ensapi/.env.local.example +++ b/apps/ensapi/.env.local.example @@ -17,7 +17,7 @@ DATABASE_URL=postgresql://dbuser:abcd1234@localhost:5432/my_database # ENSApi: RPC Configuration # Required. ENSApi requires an HTTP RPC to the connected ENSIndexer's ENS Root Chain, which depends -# on ENSIndexer's NAMESPACE (ex: mainnet, sepolia, holesky, ens-test-env). This ENS Root Chain RPC +# on ENSIndexer's NAMESPACE (ex: mainnet, sepolia, ens-test-env). This ENS Root Chain RPC # is used to power the Resolution API, in situations where Protocol Acceleration is not possible. # # When ENSApi starts up it connects to the indicated ENSINDEXER_URL verifies that the ENS Root Chain @@ -51,7 +51,6 @@ DATABASE_URL=postgresql://dbuser:abcd1234@localhost:5432/my_database # Chain-Specific: # RPC_URL_1=https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY # RPC_URL_11155111=https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY -# RPC_URL_17000=https://eth-holesky.g.alchemy.com/v2/YOUR_API_KEY # RPC_URL_1337=http://localhost:8545 # Log Level diff --git a/apps/ensapi/src/lib/thegraph.ts b/apps/ensapi/src/lib/thegraph.ts index e30e5b5ec..fd8749448 100644 --- a/apps/ensapi/src/lib/thegraph.ts +++ b/apps/ensapi/src/lib/thegraph.ts @@ -31,8 +31,6 @@ export const makeTheGraphSubgraphUrl = (namespace: ENSNamespaceId, apiKey: strin return `https://gateway.thegraph.com/api/${apiKey}/subgraphs/id/5XqPmWe6gjyrJtFn9cLy237i4cWw2j9HcUJEXsP5qGtH`; case "sepolia": return `https://gateway.thegraph.com/api/${apiKey}/subgraphs/id/G1SxZs317YUb9nQX3CC98hDyvxfMJNZH5pPRGpNrtvwN`; - case "holesky": - return `https://gateway.thegraph.com/api/${apiKey}/subgraphs/id/i5EXyL9MzTXWKCmpJ2LG6sbzBfXneUPVuTXaSjYhDDF`; default: return null; } diff --git a/apps/ensindexer/.env.local.example b/apps/ensindexer/.env.local.example index 3546bb5a7..b1467ad37 100644 --- a/apps/ensindexer/.env.local.example +++ b/apps/ensindexer/.env.local.example @@ -104,12 +104,6 @@ RPC_URL_421614= # - required by plugins: protocol-acceleration RPC_URL_534351= -# === ENS Namespace: Holesky === -# Ethereum Holesky (public testnet) -# - required if the configured namespace is holesky -# - required by plugins: subgraph, protocol-acceleration, referrals, tokenscope -RPC_URL_17000= - # === ENS Namespace: ens-test-env === # ens-test-env (local testnet) # - required if the configured namespace is ens-test-env @@ -138,8 +132,8 @@ DATABASE_SCHEMA=production DATABASE_URL=postgresql://dbuser:abcd1234@localhost:5432/my_database # ENS Namespace Configuration -# Required. Must be an ENS namespace's Identifier such as mainnet, sepolia, holesky, -# or ens-test-env. (see `@ensnode/datasources` for available options). +# Required. Must be an ENS namespace's Identifier such as mainnet, sepolia, or ens-test-env. +# (see `@ensnode/datasources` for available options). NAMESPACE=mainnet # Plugin Configuration diff --git a/apps/ensindexer/src/config/validations.ts b/apps/ensindexer/src/config/validations.ts index c224039a0..e09af83a5 100644 --- a/apps/ensindexer/src/config/validations.ts +++ b/apps/ensindexer/src/config/validations.ts @@ -101,7 +101,7 @@ export function invariant_globalBlockrange( END_BLOCK=${globalBlockrange.endBlock || "n/a"} The usage you're most likely interested in is: - NAMESPACE=(mainnet|sepolia|holesky) PLUGINS=subgraph END_BLOCK=x pnpm run start + NAMESPACE=(mainnet|sepolia) PLUGINS=subgraph END_BLOCK=x pnpm run start which runs just the 'subgraph' plugin with a specific end block, suitable for snapshotting ENSNode and comparing to Subgraph snapshots. In the future, indexing multiple chains with chain-specific blockrange constraints may be possible.`, diff --git a/apps/ensindexer/src/lib/currencies.ts b/apps/ensindexer/src/lib/currencies.ts index 7a879344e..6b6f3c570 100644 --- a/apps/ensindexer/src/lib/currencies.ts +++ b/apps/ensindexer/src/lib/currencies.ts @@ -1,14 +1,5 @@ import { type Address, zeroAddress } from "viem"; -import { - base, - baseSepolia, - holesky, - linea, - lineaSepolia, - mainnet, - optimism, - sepolia, -} from "viem/chains"; +import { base, baseSepolia, linea, lineaSepolia, mainnet, optimism, sepolia } from "viem/chains"; import { type AccountId, type ChainId, type CurrencyId, CurrencyIds } from "@ensnode/ensnode-sdk"; @@ -53,13 +44,6 @@ const SUPPORTED_CURRENCY_CONTRACTS: Record> "0x176211869ca2b568f2a7d4ee941e073a821ee1ff": CurrencyIds.USDC, "0x4af15ec2a0bd43db75dd04e62faa3b8ef36b00d5": CurrencyIds.DAI, }, - - /** holesky namespace */ - [holesky.id]: { - [zeroAddress]: CurrencyIds.ETH, - "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238": CurrencyIds.USDC, - "0x3e622317f8c93f7328350cf0b56d9ed4c620c5d6": CurrencyIds.DAI, - }, }; /** diff --git a/apps/ensindexer/src/lib/datasource-helpers.ts b/apps/ensindexer/src/lib/datasource-helpers.ts index 014b56c1c..4f2445b81 100644 --- a/apps/ensindexer/src/lib/datasource-helpers.ts +++ b/apps/ensindexer/src/lib/datasource-helpers.ts @@ -13,8 +13,7 @@ import type { AccountId } from "@ensnode/ensnode-sdk"; * This is useful when you want to retrieve the AccountId for a contract by its name * where it may or may not actually be defined for the given namespace and datasource. * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', - * 'ens-test-env') + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'ens-test-env') * @param datasourceName - The name of the Datasource to search for contractName in * @param contractName - The name of the contract to retrieve * @returns The AccountId of the contract with the given namespace, datasource, @@ -45,8 +44,7 @@ export const maybeGetDatasourceContract = < * Gets the AccountId for the contract in the specified namespace, datasource, and * contract name, or throws an error if it is not defined or is not a single AccountId. * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', - * 'ens-test-env') + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'ens-test-env') * @param datasourceName - The name of the Datasource to search for contractName in * @param contractName - The name of the contract to retrieve * @returns The AccountId of the contract with the given namespace, datasource, diff --git a/apps/ensindexer/src/lib/ensv2/registrar-lib.ts b/apps/ensindexer/src/lib/ensv2/registrar-lib.ts index 8962f4057..bab4c6097 100644 --- a/apps/ensindexer/src/lib/ensv2/registrar-lib.ts +++ b/apps/ensindexer/src/lib/ensv2/registrar-lib.ts @@ -24,6 +24,7 @@ const lineanamesNameWrapper = maybeGetDatasourceContract( "NameWrapper", ); +// TODO: need to handle namespace-specific remap const REGISTRAR_CONTRACTS_BY_MANAGED_NAME: Record = { eth: [ getDatasourceContract( diff --git a/apps/ensindexer/src/lib/tokenscope/nft-issuers.ts b/apps/ensindexer/src/lib/tokenscope/nft-issuers.ts index d55093dca..def8844cd 100644 --- a/apps/ensindexer/src/lib/tokenscope/nft-issuers.ts +++ b/apps/ensindexer/src/lib/tokenscope/nft-issuers.ts @@ -73,7 +73,7 @@ const labelHashGeneratedTokenIdToNode = (tokenId: TokenId, parentNode: Node): No /** * Gets all the SupportedNFTIssuer for the specified namespace. * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', 'ens-test-env') + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'ens-test-env') * @returns an array of 0 or more SupportedNFTIssuer for the specified namespace */ const getSupportedNFTIssuers = (namespaceId: ENSNamespaceId): SupportedNFTIssuer[] => { @@ -176,8 +176,7 @@ const getSupportedNFTIssuers = (namespaceId: ENSNamespaceId): SupportedNFTIssuer /** * Gets the SupportedNFTIssuer for the given contract in the specified namespace. * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', - * 'ens-test-env') + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'ens-test-env') * @param contract - The AccountId of the contract to get the SupportedNFTIssuer for * @returns the SupportedNFTIssuer for the given contract, or null * if the contract is not a SupportedNFTIssuer in the specified namespace diff --git a/apps/ensindexer/src/lib/tokenscope/seaport.ts b/apps/ensindexer/src/lib/tokenscope/seaport.ts index 10db16242..a49447f8a 100644 --- a/apps/ensindexer/src/lib/tokenscope/seaport.ts +++ b/apps/ensindexer/src/lib/tokenscope/seaport.ts @@ -35,8 +35,7 @@ const getAssetNamespace = (itemType: ItemType): AssetNamespace | null => { /** * Gets the supported NFT from a given Seaport item. * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', - * 'ens-test-env') + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'ens-test-env') * @param chainId - The chain ID of the Seaport item * @param item - The Seaport item to get the supported NFT from * @returns the supported NFT from the given Seaport item, or `null` if the Seaport item is diff --git a/apps/ensindexer/src/plugins/registrars/basenames/lib/registrar-helpers.ts b/apps/ensindexer/src/plugins/registrars/basenames/lib/registrar-helpers.ts index 2345b4ff7..bf74faf4d 100644 --- a/apps/ensindexer/src/plugins/registrars/basenames/lib/registrar-helpers.ts +++ b/apps/ensindexer/src/plugins/registrars/basenames/lib/registrar-helpers.ts @@ -29,7 +29,6 @@ export function getRegistrarManagedName(namespaceId: ENSNamespaceId): RegistrarM return "base.eth"; case "sepolia": return "basetest.eth"; - case "holesky": case "ens-test-env": throw new Error( `No registrar managed name is known for the 'basenames' subregistry within the "${namespaceId}" namespace.`, diff --git a/apps/ensindexer/src/plugins/registrars/ethnames/lib/registrar-helpers.ts b/apps/ensindexer/src/plugins/registrars/ethnames/lib/registrar-helpers.ts index 0ca10b56c..f749dd000 100644 --- a/apps/ensindexer/src/plugins/registrars/ethnames/lib/registrar-helpers.ts +++ b/apps/ensindexer/src/plugins/registrars/ethnames/lib/registrar-helpers.ts @@ -26,7 +26,6 @@ export function getRegistrarManagedName(namespaceId: ENSNamespaceId): RegistrarM switch (namespaceId) { case "mainnet": case "sepolia": - case "holesky": case "ens-test-env": return "eth"; } diff --git a/apps/ensindexer/src/plugins/registrars/lineanames/lib/registrar-helpers.ts b/apps/ensindexer/src/plugins/registrars/lineanames/lib/registrar-helpers.ts index 13bba68f3..ec6d1ec49 100644 --- a/apps/ensindexer/src/plugins/registrars/lineanames/lib/registrar-helpers.ts +++ b/apps/ensindexer/src/plugins/registrars/lineanames/lib/registrar-helpers.ts @@ -29,7 +29,6 @@ export function getRegistrarManagedName(namespaceId: ENSNamespaceId): RegistrarM return "linea.eth"; case "sepolia": return "linea-sepolia.eth"; - case "holesky": case "ens-test-env": throw new Error( `No registrar managed name is known for the 'lineanames' subregistry within the "${namespaceId}" namespace.`, diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/basenames/lib/registrar-helpers.ts b/apps/ensindexer/src/plugins/subgraph/plugins/basenames/lib/registrar-helpers.ts index a00a69ab0..74d74988c 100644 --- a/apps/ensindexer/src/plugins/subgraph/plugins/basenames/lib/registrar-helpers.ts +++ b/apps/ensindexer/src/plugins/subgraph/plugins/basenames/lib/registrar-helpers.ts @@ -16,7 +16,6 @@ export function getRegistrarManagedName(namespaceId: ENSNamespaceId): RegistrarM return "base.eth"; case "sepolia": return "basetest.eth"; - case "holesky": case "ens-test-env": throw new Error( `No registrar managed name is known for the Basenames plugin within the "${namespaceId}" namespace.`, diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/lib/registrar-helpers.ts b/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/lib/registrar-helpers.ts index 49cb6243f..6a99aed2f 100644 --- a/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/lib/registrar-helpers.ts +++ b/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/lib/registrar-helpers.ts @@ -16,7 +16,6 @@ export function getRegistrarManagedName(namespaceId: ENSNamespaceId): RegistrarM return "linea.eth"; case "sepolia": return "linea-sepolia.eth"; - case "holesky": case "ens-test-env": throw new Error( `No registrar managed name is known for the Linea Names plugin within the "${namespaceId}" namespace.`, diff --git a/docs/ensnode.io/src/content/docs/docs/concepts/what-is-ensnode.mdx b/docs/ensnode.io/src/content/docs/docs/concepts/what-is-ensnode.mdx index 1a0b0cfd2..1b7d3512e 100644 --- a/docs/ensnode.io/src/content/docs/docs/concepts/what-is-ensnode.mdx +++ b/docs/ensnode.io/src/content/docs/docs/concepts/what-is-ensnode.mdx @@ -90,7 +90,7 @@ When ENSIndexer is run, the configs for all of the active plugins (those selecte This package provides configurations for each known ENS namespace. An ENS namespace represents a single, unified set of ENS names with a distinct onchain root Registry and the capability to span across multiple chains, subregistries, and offchain resources. -Each namespace is logically independent - for instance, the Sepolia and Holesky testnet namespaces are entirely separate from the canonical mainnet namespace. This package centralizes the contract addresses, start blocks, and other configuration needed to interact with each namespace. +Each namespace is logically independent - for instance, the Sepolia namespace is entirely separate from the canonical mainnet namespace. This package centralizes the contract addresses, start blocks, and other configuration needed to interact with each namespace. ENSIndexer uses `@ensnode/datasources` to configure its plugins and determine which are available for a given target namespace. diff --git a/docs/ensnode.io/src/content/docs/docs/concepts/what-is-the-ens-subgraph.mdx b/docs/ensnode.io/src/content/docs/docs/concepts/what-is-the-ens-subgraph.mdx index 9b293c99f..1f5f90b0d 100644 --- a/docs/ensnode.io/src/content/docs/docs/concepts/what-is-the-ens-subgraph.mdx +++ b/docs/ensnode.io/src/content/docs/docs/concepts/what-is-the-ens-subgraph.mdx @@ -45,7 +45,7 @@ This server is running [Graph Node version 0.39.1](https://github.com/graphproto ## NameHash Labs Hosted ENS Subgraphs -Our Graph Node instance indexes the ENS Subgraph on mainnet, sepolia, and holesky. The live indexing status of each can be monitored with [the indexing status API endpoint](https://graphnode.namehashlabs.org:8030/graphql/playground?query=%7B%0A%20%20indexingStatuses%20%7B%0A%20%20%20%20subgraph%0A%20%20%20%20health%0A%20%20%20%20historyBlocks%0A%20%20%20%20paused%0A%20%20%20%20synced%0A%20%20%20%20chains%20%7B%0A%20%20%20%20%20%20network%0A%20%20%20%20%20%20chainHeadBlock%20%7B%0A%20%20%20%20%20%20%20%20number%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20latestBlock%20%7B%0A%20%20%20%20%20%20%20%20number%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D). +Our Graph Node instance indexes the ENS Subgraph on mainnet and sepolia. The live indexing status of each can be monitored with [the indexing status API endpoint](https://graphnode.namehashlabs.org:8030/graphql/playground?query=%7B%0A%20%20indexingStatuses%20%7B%0A%20%20%20%20subgraph%0A%20%20%20%20health%0A%20%20%20%20historyBlocks%0A%20%20%20%20paused%0A%20%20%20%20synced%0A%20%20%20%20chains%20%7B%0A%20%20%20%20%20%20network%0A%20%20%20%20%20%20chainHeadBlock%20%7B%0A%20%20%20%20%20%20%20%20number%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20latestBlock%20%7B%0A%20%20%20%20%20%20%20%20number%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D). As of the latest update to this page on July 15, 2025: @@ -63,9 +63,3 @@ As of the latest update to this page on July 15, 2025: - Subgraph ID: `QmZkCMqRDzq8tWfJy12KudmRjwzHLSA5F6KnSnHRoC6kQe` - [GraphQL Endpoint](https://graphnode.namehashlabs.org:8000/subgraphs/name/namehash/ens-subgraph-3) - [Interactive GraphiQL Explorer](https://graphnode.namehashlabs.org:8000/subgraphs/name/namehash/ens-subgraph-3/graphql) - -**Holesky** - -- Subgraph ID: `QmQCDNHEuV359KwtyYZBWv4godb8t6kzErcYSFs7YgdqAM` -- [GraphQL Endpoint](https://graphnode.namehashlabs.org:8000/subgraphs/name/namehash/ens-subgraph-holesky) -- [Interactive GraphiQL Explorer](https://graphnode.namehashlabs.org:8000/subgraphs/name/namehash/ens-subgraph-holesky/graphql) diff --git a/docs/ensnode.io/src/content/docs/docs/deploying/terraform.mdx b/docs/ensnode.io/src/content/docs/docs/deploying/terraform.mdx index dff3b9c3c..c2182d596 100644 --- a/docs/ensnode.io/src/content/docs/docs/deploying/terraform.mdx +++ b/docs/ensnode.io/src/content/docs/docs/deploying/terraform.mdx @@ -24,7 +24,7 @@ These Terraform scripts are currently specific to ENSNode instances hosted by Na - [Terraform](https://www.terraform.io/downloads.html) installed - [Render](https://https://render.com/) account - Render API token (generate from https://render.com/docs/api#1-create-an-api-key) -- RPC URLs for the chains you want to support (Mainnet, Sepolia, Holesky, Base, Linea) +- RPC URLs for the chains you want to support (Mainnet, Sepolia, Base, Linea) - AWS account (for DNS management) - AWS S3 bucket defined inside AWS account - `ensnode-terraform` (for Terraform state) @@ -60,9 +60,6 @@ linea_sepolia_rpc_url = "your_linea_sepolia_rpc_url" optimism_sepolia_rpc_url = "your_optimism_sepolia_rpc_url" arbitrum_sepolia_rpc_url = "your_arbitrum_sepolia_rpc_url" scroll_sepolia_rpc_url = "your_scroll_sepolia_rpc_url" - -# Holesky RPC URLs -ethereum_holesky_rpc_url = "your_ethereum_holesky_rpc_url" ``` ## Infrastructure Components diff --git a/docs/ensnode.io/src/content/docs/docs/usage/hosted-ensnode-instances.mdx b/docs/ensnode.io/src/content/docs/docs/usage/hosted-ensnode-instances.mdx index b4d38e59e..e0dee7423 100644 --- a/docs/ensnode.io/src/content/docs/docs/usage/hosted-ensnode-instances.mdx +++ b/docs/ensnode.io/src/content/docs/docs/usage/hosted-ensnode-instances.mdx @@ -70,16 +70,6 @@ These ENSNode instances focus on maximizing backwards compatibility with the ENS purpose="Demonstration of ENSNode's backwards compatibility with the ENS Subgraph. Provides 1:1 Subgraph compatible data on Sepolia." /> -#### ENSNode 'Holesky' - - - ## Endpoints For more details on how to use these instances, refer to the [ENSNode Quickstart](/docs/). diff --git a/packages/datasources/README.md b/packages/datasources/README.md index 6cd02775c..f37567a20 100644 --- a/packages/datasources/README.md +++ b/packages/datasources/README.md @@ -10,7 +10,7 @@ For example, the canonical ENS Namespace on mainnet includes: - The `threedns-optimism` and `threedns-base` Datasources documenting the 3DNS contracts on Optimism and Base, respectively - 🚧 Various offchain Datasources (e.g. `.cb.id`, `.uni.eth`) -Each ENS namespace is logically independent and isolated from the others: for instance, the `sepolia` and `holesky` testnet namespaces manage a set of names that is entirely separate from the canonical `mainnet` namespace, and have distinct `basenames` and `lineanames` **Datasource**s defined. +Each ENS namespace is logically independent and isolated from the others: for instance, the `sepolia` testnet manages a set of names that is entirely separate from the canonical `mainnet` namespace, and have distinct `basenames` and `lineanames` **Datasource**s defined. The `ens-test-env` namespace describes the contracts deployed to an _Anvil_ chain for development and testing with the [ens-test-env](https://github.com/ensdomains/ens-test-env) tool. @@ -48,8 +48,8 @@ import { getDatasource } from '@ensnode/datasources'; // get ensroot datasource relative to mainnet ENS namespace const { chain, contracts } = getDatasource('mainnet', 'ensroot'); -// get ensroot datasource relative to holesky ENS namespace -const { chain, contracts } = getDatasource('holesky', 'ensroot'); +// get ensroot datasource relative to sepolia ENS namespace +const { chain, contracts } = getDatasource('sepolia', 'ensroot'); // get threedns-base datasource relative to mainnet ENS namespace const { chain, contracts } = getDatasource('mainnet', 'threedns-base'); @@ -58,7 +58,6 @@ const { chain, contracts } = getDatasource('mainnet', 'threedns-base'); The available `ENSNamespaceId`s are: - `mainnet` - `sepolia` -- `holesky` - `ens-test-env` — Represents a local testing namespace running on an Anvil chain (chain id 1337) with deterministic configurations that deliberately start at block zero for rapid testing and development. See [ens-test-env](https://github.com/ensdomains/ens-test-env) for additional context. ### DatasourceName diff --git a/packages/datasources/src/holesky.ts b/packages/datasources/src/holesky.ts deleted file mode 100644 index 7999dc32c..000000000 --- a/packages/datasources/src/holesky.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { holesky } from "viem/chains"; - -// ABIs for ENSRoot Datasource -import { BaseRegistrar as root_BaseRegistrar } from "./abis/root/BaseRegistrar"; -import { LegacyEthRegistrarController as root_LegacyEthRegistrarController } from "./abis/root/LegacyEthRegistrarController"; -import { NameWrapper as root_NameWrapper } from "./abis/root/NameWrapper"; -import { Registry as root_Registry } from "./abis/root/Registry"; -import { UniversalResolver as root_UniversalResolver } from "./abis/root/UniversalResolver"; -import { UnwrappedEthRegistrarController as root_UnwrappedEthRegistrarController } from "./abis/root/UnwrappedEthRegistrarController"; -import { WrappedEthRegistrarController as root_WrappedEthRegistrarController } from "./abis/root/WrappedEthRegistrarController"; -// Shared ABIs -import { ResolverABI } from "./lib/resolver"; -// Types -import { DatasourceNames, type ENSNamespace } from "./lib/types"; - -/** - * The Holesky ENSNamespace - * - * NOTE: The Holesky ENS namespace has no known Datasource for Basenames, Lineanames, or 3DNS. - * NOTE: The Holesky ENS namespace does not support ENSIP-19. - */ -export default { - /** - * ENSRoot Datasource - * - * Addresses and Start Blocks from ENS Holesky Subgraph Manifest - * https://ipfs.io/ipfs/Qmd94vseLpkUrSFvJ3GuPubJSyHz8ornhNrwEAt6pjcbex - */ - [DatasourceNames.ENSRoot]: { - chain: holesky, - contracts: { - ENSv1RegistryOld: { - abi: root_Registry, // Registry was redeployed, same abi - address: "0x94f523b8261b815b87effcf4d18e6abef18d6e4b", - startBlock: 801536, - }, - ENSv1Registry: { - abi: root_Registry, // Registry was redeployed, same abi - address: "0x00000000000c2e074ec69a0dfb2997ba6c7d2e1e", - startBlock: 801613, - }, - Resolver: { - abi: ResolverABI, - startBlock: 801536, // ignores any Resolver events prior to `startBlock` of ENSv1RegistryOld on Holeksy - }, - BaseRegistrar: { - abi: root_BaseRegistrar, - address: "0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85", - startBlock: 801686, - }, - LegacyEthRegistrarController: { - abi: root_LegacyEthRegistrarController, - address: "0xf13fc748601fdc5afa255e9d9166eb43f603a903", - startBlock: 815355, - }, - WrappedEthRegistrarController: { - abi: root_WrappedEthRegistrarController, - address: "0x179be112b24ad4cfc392ef8924dfa08c20ad8583", - startBlock: 815359, - }, - UnwrappedEthRegistrarController: { - abi: root_UnwrappedEthRegistrarController, - address: "0xfce6ce4373cb6e7e470eaa55329638acd9dbd202", - startBlock: 4027261, - }, - NameWrapper: { - abi: root_NameWrapper, - address: "0xab50971078225d365994dc1edcb9b7fd72bb4862", - startBlock: 815127, - }, - UniversalResolver: { - abi: root_UniversalResolver, - address: "0xe3f3174fc2f2b17644cd2dbac3e47bc82ae0cf81", - startBlock: 8515717, - }, - }, - }, -} satisfies ENSNamespace; diff --git a/packages/datasources/src/lib/types.ts b/packages/datasources/src/lib/types.ts index 161b3d109..b87a5f59a 100644 --- a/packages/datasources/src/lib/types.ts +++ b/packages/datasources/src/lib/types.ts @@ -17,8 +17,8 @@ import type { Abi, Address, Chain } from "viem"; * - Etc.. * * Each ENS namespace is logically independent of & isolated from the others, and not exclusively - * correlated with a specific L1 chain. For example, the Sepolia and Holesky testnet ENS namepaces - * are independent of the canonical ENS namespace on mainnet, and there could be an additional + * correlated with a specific L1 chain. For example, the Sepolia testnet ENS namepace + * is independent of the canonical ENS namespace on mainnet, and there could be an additional * deployment of the ENS protocol to mainnet, configured with different Datasources, resulting in a * logically isolated set of ENS names. * @@ -29,7 +29,6 @@ import type { Abi, Address, Chain } from "viem"; export const ENSNamespaceIds = { Mainnet: "mainnet", Sepolia: "sepolia", - Holesky: "holesky", EnsTestEnv: "ens-test-env", } as const; diff --git a/packages/datasources/src/namespaces.ts b/packages/datasources/src/namespaces.ts index ecf99bfb8..19a999ba7 100644 --- a/packages/datasources/src/namespaces.ts +++ b/packages/datasources/src/namespaces.ts @@ -1,5 +1,4 @@ import ensTestEnv from "./ens-test-env"; -import holesky from "./holesky"; import { type DatasourceName, DatasourceNames, @@ -13,19 +12,17 @@ import sepolia from "./sepolia"; const ENSNamespacesById: { readonly mainnet: typeof mainnet; readonly sepolia: typeof sepolia; - readonly holesky: typeof holesky; readonly "ens-test-env": typeof ensTestEnv; } = { mainnet, sepolia, - holesky, "ens-test-env": ensTestEnv, } as const; /** * Returns the ENSNamespace for a specified `namespaceId`. * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', 'ens-test-env') + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'ens-test-env') * @returns the ENSNamespace */ export const getENSNamespace = ( @@ -38,7 +35,7 @@ export const getENSNamespace = ( * NOTE: the typescript typechecker _will_ enforce validity. i.e. using an invalid `datasourceName` * within the specified `namespaceId` will be a type error. * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', 'ens-test-env') + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'ens-test-env') * @param datasourceName - The name of the Datasource to retrieve * @returns The Datasource object for the given name within the specified namespace */ @@ -60,7 +57,7 @@ export const getDatasource = < * is ENSRoot (the only Datasource present in all namespaces). This method allows you to receive * the const Datasource or undefined for a specified `datasourceName`. * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', 'ens-test-env') + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'ens-test-env') * @param datasourceName - The name of the Datasource to retrieve * @returns The Datasource object for the given name within the specified namespace, or undefined if it does not exist */ diff --git a/packages/ensnode-sdk/src/shared/config/build-rpc-urls.ts b/packages/ensnode-sdk/src/shared/config/build-rpc-urls.ts index 5f3e59c2c..7677eac7c 100644 --- a/packages/ensnode-sdk/src/shared/config/build-rpc-urls.ts +++ b/packages/ensnode-sdk/src/shared/config/build-rpc-urls.ts @@ -3,7 +3,6 @@ import { arbitrumSepolia, base, baseSepolia, - holesky, linea, lineaSepolia, mainnet, @@ -35,8 +34,6 @@ export function buildAlchemyBaseUrl(chainId: ChainId, key: string): string | und return `eth-mainnet.g.alchemy.com/v2/${key}`; case sepolia.id: return `eth-sepolia.g.alchemy.com/v2/${key}`; - case holesky.id: - return `eth-holesky.g.alchemy.com/v2/${key}`; case arbitrum.id: return `arb-mainnet.g.alchemy.com/v2/${key}`; case arbitrumSepolia.id: @@ -81,8 +78,6 @@ export function buildDRPCUrl(chainId: ChainId, key: string): string | undefined return `https://lb.drpc.live/ethereum/${key}`; case sepolia.id: return `https://lb.drpc.live/ethereum-sepolia/${key}`; - case holesky.id: - return `https://lb.drpc.live/holesky/${key}`; case arbitrum.id: return `https://lb.drpc.live/arbitrum/${key}`; case arbitrumSepolia.id: diff --git a/terraform/.env.sample b/terraform/.env.sample index ce8c1c7cf..dfbc808db 100644 --- a/terraform/.env.sample +++ b/terraform/.env.sample @@ -13,14 +13,12 @@ TF_VAR_ethereum_mainnet_rpc_url= TF_VAR_base_mainnet_rpc_url= TF_VAR_linea_mainnet_rpc_url= TF_VAR_optimism_mainnet_rpc_url= -TF_VAR_arbitrum_mainnet_rpc_url= -TF_VAR_scroll_mainnet_rpc_url= +TF_VAR_arbitrum_mainnet_rpc_url= +TF_VAR_scroll_mainnet_rpc_url= # Sepolia TF_VAR_ethereum_sepolia_rpc_url= TF_VAR_base_sepolia_rpc_url= TF_VAR_linea_sepolia_rpc_url= TF_VAR_optimism_sepolia_rpc_url= -TF_VAR_arbitrum_sepolia_rpc_url= +TF_VAR_arbitrum_sepolia_rpc_url= TF_VAR_scroll_sepolia_rpc_url= -# Holesky -TF_VAR_ethereum_holesky_rpc_url= diff --git a/terraform/main.tf b/terraform/main.tf index 875a14a74..aaef76126 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -19,15 +19,6 @@ locals { # See https://render.com/docs/blueprint-spec#region render_region = "ohio" ensindexer_instances = { - holesky = { - ensnode_indexer_type = "holesky" - ensnode_environment_name = var.render_environment - database_schema = "holeskySchema-${var.ensnode_version}" - plugins = "subgraph" - namespace = "holesky" - render_instance_plan = "starter" - subgraph_compat = true - } sepolia = { ensnode_indexer_type = "sepolia" ensnode_environment_name = var.render_environment From e5576fccfe5e10617961884b33010e16ab5e7613 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 14 Nov 2025 12:16:40 -0600 Subject: [PATCH 026/102] checkpoint --- apps/ensapi/src/graphql-api/schema/registration.ts | 2 +- apps/ensindexer/src/plugins/ensv2/plugin.ts | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/ensapi/src/graphql-api/schema/registration.ts b/apps/ensapi/src/graphql-api/schema/registration.ts index c37a9aaac..0476190fc 100644 --- a/apps/ensapi/src/graphql-api/schema/registration.ts +++ b/apps/ensapi/src/graphql-api/schema/registration.ts @@ -34,7 +34,7 @@ export type RegistrationInterface = Pick< export type NameWrapperRegistration = RequiredAndNotNull; export type BaseRegistrarRegistration = RequiredAndNotNull< Registration, - "gracePeriod" | "wrapped" | "wrappedExpiration" | "wrappedFuses" | "wrappedOwnerId" + "gracePeriod" | "wrapped" | "wrappedFuses" > & { baseCost: bigint | null; premium: bigint | null; diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index 7d398ec66..214d6c11b 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -40,11 +40,6 @@ export default createPlugin({ name: pluginName, requiredDatasourceNames: REQUIRED_DATASOURCE_NAMES, createPonderConfig(config) { - // TODO: remove this, helps with types while only targeting ens-test-env - if (config.namespace !== "ens-test-env" && config.namespace !== "mainnet") { - throw new Error("only ens-test-env and mainnet"); - } - const { ensroot, // namechain, From 07362d891891813221992316baa9efe550ed7765 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 14 Nov 2025 12:25:19 -0600 Subject: [PATCH 027/102] add back deferrred batch healing --- .../src/lib/ensv2/label-db-helpers.ts | 17 ++- .../src/plugins/ensv2/handlers/Block.ts | 126 +++++++++--------- apps/ensindexer/src/plugins/ensv2/plugin.ts | 8 ++ .../src/schemas/ensv2.schema.ts | 1 + 4 files changed, 80 insertions(+), 72 deletions(-) diff --git a/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts index d2f1470b5..e91921ae6 100644 --- a/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts @@ -10,8 +10,6 @@ import { literalLabelToInterpretedLabel, } from "@ensnode/ensnode-sdk"; -import { labelByLabelHash } from "@/lib/graphnode-helpers"; - export async function ensureLabel(context: Context, label: LiteralLabel) { const labelHash = labelhash(label); const interpretedLabel = literalLabelToInterpretedLabel(label); @@ -23,18 +21,19 @@ export async function ensureLabel(context: Context, label: LiteralLabel) { } export async function ensureUnknownLabel(context: Context, labelHash: LabelHash) { - // do nothing for existing labels, they're either healed or we don't know them - const exists = await context.db.find(schema.label, { labelHash }); - if (exists) return; + // NOTE: keep this code around just in case batch label healing is less efficient for some reason + // // do nothing for existing labels, they're either healed or we don't know them + // const exists = await context.db.find(schema.label, { labelHash }); + // if (exists) return; - // attempt ENSRainbow heal - const healedLabel = await labelByLabelHash(labelHash); - if (healedLabel) return await ensureLabel(context, healedLabel); + // // attempt ENSRainbow heal + // const healedLabel = await labelByLabelHash(labelHash); + // if (healedLabel) return await ensureLabel(context, healedLabel); // otherwise const interpretedLabel = encodeLabelHash(labelHash) as InterpretedLabel; await context.db .insert(schema.label) - .values({ labelHash, value: interpretedLabel }) + .values({ labelHash, value: interpretedLabel, needsHeal: true }) .onConflictDoNothing(); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/Block.ts b/apps/ensindexer/src/plugins/ensv2/handlers/Block.ts index 1ea1c115d..e02324487 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/Block.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/Block.ts @@ -18,67 +18,67 @@ const BATCH_SIZE = 1000; const ensrainbow = getENSRainbowApiClient(); export default function () { - // this ended up being slower than otherwise, because it runs for every block even in backfill - // and the overhead of the "get unhealed labels that need healing" query is killing any performance - // gain from batching ensrainbow heals. keeping code here for now just in case - // - // ponder.on( - // namespaceContract(pluginName, "ENSRainbowBatchHeal:block"), - // async ({ context, event }) => { - // while (true) { - // const unhealed = await context.db.sql - // .select() - // .from(schema.label) - // .where(eq(schema.label.needsHeal, true)) - // .limit(BATCH_SIZE); - // if (unhealed.length === 0) return; - // console.log(`Healing ${unhealed.length} labels in block ${event.block.number}`); - // // TODO: ENSRainbow batchHeal - // let now = performance.now(); - // const results = await Promise.all( - // unhealed.map(async ({ labelHash }) => ({ - // labelHash, - // response: await ensrainbow.heal(labelHash), - // })), - // ); - // console.log(`ensrainbow duration: ${performance.now() - now}ms`); - // // NOTE: transactions/batch not supported by Ponder's Postgres Proxy Driver - // now = performance.now(); - // for (const { labelHash, response } of results) { - // switch (response.status) { - // case StatusCode.Success: { - // const interpretedLabel = literalLabelToInterpretedLabel( - // response.label as LiteralLabel, - // ); - // await context.db.sql - // .update(schema.label) - // .set({ value: interpretedLabel, needsHeal: false }) - // .where(eq(schema.label.labelHash, labelHash)); - // break; - // } - // case "error": { - // switch (response.errorCode) { - // case ErrorCode.BadRequest: - // case ErrorCode.NotFound: { - // await context.db.sql - // .update(schema.label) - // .set({ needsHeal: false }) - // .where(eq(schema.label.labelHash, labelHash)); - // break; - // } - // case ErrorCode.ServerError: { - // // this requires ENSRainbow to be available, does not tolerate downtime - // // ideally instead we can do some sort of queue for failed tasks... - // throw new Error( - // `Error healing labelHash: "${labelHash}". Error (${response.errorCode}): ${response.error}.`, - // ); - // } - // } - // } - // } - // } - // console.log(`update duration: ${performance.now() - now}ms`); - // } - // }, - // ); + ponder.on( + namespaceContract(pluginName, "ENSRainbowBatchHeal:block"), + async ({ context, event }) => { + while (true) { + const unhealed = await context.db.sql + .select() + .from(schema.label) + .where(eq(schema.label.needsHeal, true)) + .limit(BATCH_SIZE); + + if (unhealed.length === 0) return; + + console.log(`Healing ${unhealed.length} labels in block ${event.block.number}`); + + // TODO: ENSRainbow batchHeal + let now = performance.now(); + const results = await Promise.all( + unhealed.map(async ({ labelHash }) => ({ + labelHash, + response: await ensrainbow.heal(labelHash), + })), + ); + console.log(`ensrainbow duration: ${performance.now() - now}ms`); + + // NOTE: transactions/batch not supported by Ponder's Postgres Proxy Driver + now = performance.now(); + for (const { labelHash, response } of results) { + switch (response.status) { + case StatusCode.Success: { + const interpretedLabel = literalLabelToInterpretedLabel( + response.label as LiteralLabel, + ); + await context.db.sql + .update(schema.label) + .set({ value: interpretedLabel, needsHeal: false }) + .where(eq(schema.label.labelHash, labelHash)); + break; + } + case "error": { + switch (response.errorCode) { + case ErrorCode.BadRequest: + case ErrorCode.NotFound: { + await context.db.sql + .update(schema.label) + .set({ needsHeal: false }) + .where(eq(schema.label.labelHash, labelHash)); + break; + } + case ErrorCode.ServerError: { + // this requires ENSRainbow to be available, does not tolerate downtime + // ideally instead we can do some sort of queue for failed tasks with backoff... + throw new Error( + `Error healing labelHash: "${labelHash}". Error (${response.errorCode}): ${response.error}.`, + ); + } + } + } + } + } + console.log(`update duration: ${performance.now() - now}ms`); + } + }, + ); } diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index 214d6c11b..58c2d3887 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -57,6 +57,14 @@ export default createPlugin({ ALL_DATASOURCE_NAMES, ), + blocks: { + [namespaceContract(pluginName, "ENSRainbowBatchHeal")]: { + chain: ensroot.chain.id.toString(), + interval: 1, + startBlock: ensroot.contracts.Registry.startBlock, + }, + }, + contracts: { [namespaceContract(pluginName, "Registry")]: { abi: RegistryABI, diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/schemas/ensv2.schema.ts index d83e65489..244d1e333 100644 --- a/packages/ensnode-schema/src/schemas/ensv2.schema.ts +++ b/packages/ensnode-schema/src/schemas/ensv2.schema.ts @@ -283,6 +283,7 @@ export const relations_permissionsUser = relations(permissionsUser, ({ one, many export const label = onchainTable("labels", (t) => ({ labelHash: t.hex().primaryKey().$type(), value: t.text().notNull().$type(), + needsHeal: t.boolean().default(false), })); export const label_relations = relations(label, ({ many }) => ({ From e2bb29534d1a2363c3eb61c6b9038a2760449e6b Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 14 Nov 2025 16:14:16 -0600 Subject: [PATCH 028/102] checkpoint --- .../graphql-api/lib/get-domain-resolver.ts | 19 ++ .../lib/get-latest-registration.ts | 2 +- apps/ensapi/src/graphql-api/schema/domain.ts | 20 +- .../src/graphql-api/schema/registration.ts | 2 +- .../wrapped-baseregistrar-registration.ts | 2 +- .../src/lib/ensv2/label-db-helpers.ts | 17 +- .../src/lib/ensv2/registration-db-helpers.ts | 55 ++++- .../src/lib/json-stringify-with-bigints.ts | 4 + .../src/plugins/ensv2/event-handlers.ts | 2 - .../src/plugins/ensv2/handlers/Block.ts | 84 ------- .../plugins/ensv2/handlers/ENSv1Registry.ts | 75 ++----- .../src/plugins/ensv2/handlers/NameWrapper.ts | 205 ++++++++++++------ .../src/plugins/ensv2/handlers/Registry.ts | 58 ++--- apps/ensindexer/src/plugins/ensv2/plugin.ts | 8 - packages/datasources/src/sepolia.ts | 10 +- .../src/schemas/ensv2.schema.ts | 12 +- packages/ensnode-sdk/src/ens/fuses.ts | 4 + packages/ensnode-sdk/src/ens/index.ts | 1 + 18 files changed, 294 insertions(+), 286 deletions(-) create mode 100644 apps/ensapi/src/graphql-api/lib/get-domain-resolver.ts create mode 100644 apps/ensindexer/src/lib/json-stringify-with-bigints.ts delete mode 100644 apps/ensindexer/src/plugins/ensv2/handlers/Block.ts create mode 100644 packages/ensnode-sdk/src/ens/fuses.ts diff --git a/apps/ensapi/src/graphql-api/lib/get-domain-resolver.ts b/apps/ensapi/src/graphql-api/lib/get-domain-resolver.ts new file mode 100644 index 000000000..c9195ab6d --- /dev/null +++ b/apps/ensapi/src/graphql-api/lib/get-domain-resolver.ts @@ -0,0 +1,19 @@ +import { + type DomainId, + type ENSv1DomainId, + makeResolverId, + type ResolverId, +} from "@ensnode/ensnode-sdk"; + +import { db } from "@/lib/db"; + +export async function getDomainResolver(domainId: DomainId): Promise { + // TODO: refactor nodeResolverRelation to be domainResolverRelation using DomainId + const nrr = await db.query.nodeResolverRelation.findFirst({ + where: (t, { eq }) => eq(t.node, domainId as ENSv1DomainId), + }); + + if (!nrr) return undefined; + + return makeResolverId({ chainId: nrr.chainId, address: nrr.resolver }); +} diff --git a/apps/ensapi/src/graphql-api/lib/get-latest-registration.ts b/apps/ensapi/src/graphql-api/lib/get-latest-registration.ts index 88b418a16..fef11702a 100644 --- a/apps/ensapi/src/graphql-api/lib/get-latest-registration.ts +++ b/apps/ensapi/src/graphql-api/lib/get-latest-registration.ts @@ -5,6 +5,6 @@ import { db } from "@/lib/db"; export async function getLatestRegistration(domainId: DomainId) { return await db.query.registration.findFirst({ where: (t, { eq }) => eq(t.domainId, domainId), - orderBy: (t, { asc }) => asc(t.index), + orderBy: (t, { desc }) => desc(t.index), }); } diff --git a/apps/ensapi/src/graphql-api/schema/domain.ts b/apps/ensapi/src/graphql-api/schema/domain.ts index 5b0cedbd8..43c65dc9e 100644 --- a/apps/ensapi/src/graphql-api/schema/domain.ts +++ b/apps/ensapi/src/graphql-api/schema/domain.ts @@ -8,11 +8,12 @@ import { import { builder } from "@/graphql-api/builder"; import { getCanonicalPath } from "@/graphql-api/lib/get-canonical-path"; +import { getDomainResolver } from "@/graphql-api/lib/get-domain-resolver"; import { getModelId } from "@/graphql-api/lib/get-id"; import { getLatestRegistration } from "@/graphql-api/lib/get-latest-registration"; import { rejectAnyErrors } from "@/graphql-api/lib/reject-any-errors"; import { AccountRef } from "@/graphql-api/schema/account"; -import { RegistrationInterfaceRef } from "@/graphql-api/schema/registration"; +import { type Registration, RegistrationInterfaceRef } from "@/graphql-api/schema/registration"; import { RegistryInterfaceRef } from "@/graphql-api/schema/registry"; import { ResolverRef } from "@/graphql-api/schema/resolver"; import { db } from "@/lib/db"; @@ -165,7 +166,7 @@ DomainRef.implement({ description: "TODO", type: ResolverRef, nullable: true, - resolve: (parent) => parent.resolverId, + resolve: (parent) => getDomainResolver(parent.id), }), /////////////////////// @@ -177,6 +178,21 @@ DomainRef.implement({ nullable: true, resolve: (parent) => getLatestRegistration(parent.id), }), + + //////////////////////// + // Domain.registrations + //////////////////////// + registrations: t.loadableGroup({ + description: "TODO", + type: RegistrationInterfaceRef, + load: (ids: DomainId[]) => + db.query.registration.findMany({ + where: (t, { inArray }) => inArray(t.domainId, ids), + orderBy: (t, { desc }) => desc(t.index), + }), + group: (registration) => (registration as Registration).domainId, + resolve: getModelId, + }), }), }); diff --git a/apps/ensapi/src/graphql-api/schema/registration.ts b/apps/ensapi/src/graphql-api/schema/registration.ts index 0476190fc..ce27772b5 100644 --- a/apps/ensapi/src/graphql-api/schema/registration.ts +++ b/apps/ensapi/src/graphql-api/schema/registration.ts @@ -34,7 +34,7 @@ export type RegistrationInterface = Pick< export type NameWrapperRegistration = RequiredAndNotNull; export type BaseRegistrarRegistration = RequiredAndNotNull< Registration, - "gracePeriod" | "wrapped" | "wrappedFuses" + "gracePeriod" | "wrapped" | "fuses" > & { baseCost: bigint | null; premium: bigint | null; diff --git a/apps/ensapi/src/graphql-api/schema/wrapped-baseregistrar-registration.ts b/apps/ensapi/src/graphql-api/schema/wrapped-baseregistrar-registration.ts index 2d996d1af..eae2a60a5 100644 --- a/apps/ensapi/src/graphql-api/schema/wrapped-baseregistrar-registration.ts +++ b/apps/ensapi/src/graphql-api/schema/wrapped-baseregistrar-registration.ts @@ -31,7 +31,7 @@ WrappedBaseRegistrarRegistrationRef.implement({ type: "Int", nullable: false, // TODO: decode/render Fuses enum - resolve: (parent) => parent.wrappedFuses, + resolve: (parent) => parent.fuses, }), }), }); diff --git a/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts index e91921ae6..d2f1470b5 100644 --- a/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts @@ -10,6 +10,8 @@ import { literalLabelToInterpretedLabel, } from "@ensnode/ensnode-sdk"; +import { labelByLabelHash } from "@/lib/graphnode-helpers"; + export async function ensureLabel(context: Context, label: LiteralLabel) { const labelHash = labelhash(label); const interpretedLabel = literalLabelToInterpretedLabel(label); @@ -21,19 +23,18 @@ export async function ensureLabel(context: Context, label: LiteralLabel) { } export async function ensureUnknownLabel(context: Context, labelHash: LabelHash) { - // NOTE: keep this code around just in case batch label healing is less efficient for some reason - // // do nothing for existing labels, they're either healed or we don't know them - // const exists = await context.db.find(schema.label, { labelHash }); - // if (exists) return; + // do nothing for existing labels, they're either healed or we don't know them + const exists = await context.db.find(schema.label, { labelHash }); + if (exists) return; - // // attempt ENSRainbow heal - // const healedLabel = await labelByLabelHash(labelHash); - // if (healedLabel) return await ensureLabel(context, healedLabel); + // attempt ENSRainbow heal + const healedLabel = await labelByLabelHash(labelHash); + if (healedLabel) return await ensureLabel(context, healedLabel); // otherwise const interpretedLabel = encodeLabelHash(labelHash) as InterpretedLabel; await context.db .insert(schema.label) - .values({ labelHash, value: interpretedLabel, needsHeal: true }) + .values({ labelHash, value: interpretedLabel }) .onConflictDoNothing(); } diff --git a/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts index 9180c0d0c..518ecd535 100644 --- a/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts @@ -1,26 +1,59 @@ import type { Context } from "ponder:registry"; -import schema from "ponder:schema"; +import type schema from "ponder:schema"; -import { type DomainId, makeRegistrationId } from "@ensnode/ensnode-sdk"; +import type { DomainId } from "@ensnode/ensnode-sdk"; /** * TODO: find the most recent registration, active or otherwise */ export async function getLatestRegistration(context: Context, domainId: DomainId) { - const registrationId = makeRegistrationId(domainId, 0); - return await context.db.find(schema.registration, { id: registrationId }); + return await context.db.sql.query.registration.findFirst({ + where: (t, { eq }) => eq(t.domainId, domainId), + orderBy: (t, { desc }) => desc(t.index), + }); } -export function isRegistrationActive( - registration: typeof schema.registration.$inferSelect | null, +/** + * Returns whether Registration is expired. + * + * @dev Grace Period is considered 'expired'. + */ +export function isRegistrationExpired( + registration: typeof schema.registration.$inferSelect, now: bigint, ) { - // no registration, not active - if (registration === null) return false; + // no expiration, never expired + if (registration.expiration === null) return false; + + // otherwise check against now + return registration.expiration <= now; +} - // no expiration, always active - if (registration.expiration === null) return true; +/** + * Returns whether Registration is fully expired. + * @dev Grace Period is considered 'unexpired'. + */ +export function isRegistrationFullyExpired( + registration: typeof schema.registration.$inferSelect, + now: bigint, +) { + // no expiration, never expired + if (registration.expiration === null) return false; // otherwise check against now - return registration.expiration + (registration.gracePeriod ?? 0n) < now; + return registration.expiration + (registration.gracePeriod ?? 0n) <= now; +} + +/** + * Returns whether Registration is in grace period. + */ +export function isRegistrationInGracePeriod( + registration: typeof schema.registration.$inferSelect, + now: bigint, +) { + if (registration.expiration === null) return false; + if (registration.gracePeriod === null) return false; + + // + return registration.expiration <= now && registration.expiration + registration.gracePeriod > now; } diff --git a/apps/ensindexer/src/lib/json-stringify-with-bigints.ts b/apps/ensindexer/src/lib/json-stringify-with-bigints.ts new file mode 100644 index 000000000..059d147c4 --- /dev/null +++ b/apps/ensindexer/src/lib/json-stringify-with-bigints.ts @@ -0,0 +1,4 @@ +import { replaceBigInts } from "ponder"; + +export const toJson = (value: unknown, pretty = true) => + JSON.stringify(replaceBigInts(value, String), null, pretty ? 2 : undefined); diff --git a/apps/ensindexer/src/plugins/ensv2/event-handlers.ts b/apps/ensindexer/src/plugins/ensv2/event-handlers.ts index e9f154472..c4befa12a 100644 --- a/apps/ensindexer/src/plugins/ensv2/event-handlers.ts +++ b/apps/ensindexer/src/plugins/ensv2/event-handlers.ts @@ -1,4 +1,3 @@ -import attach_BlockHandlers from "./handlers/Block"; import attach_ENSv1RegistryHandlers from "./handlers/ENSv1Registry"; import attach_EnhancedAccessControlHandlers from "./handlers/EnhancedAccessControl"; import attach_NameWrapperHandlers from "./handlers/NameWrapper"; @@ -7,7 +6,6 @@ import attach_RegistrarControllerHandlers from "./handlers/RegistrarController"; import attach_RegistryHandlers from "./handlers/Registry"; export default function () { - attach_BlockHandlers(); attach_ENSv1RegistryHandlers(); attach_EnhancedAccessControlHandlers(); attach_NameWrapperHandlers(); diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/Block.ts b/apps/ensindexer/src/plugins/ensv2/handlers/Block.ts deleted file mode 100644 index e02324487..000000000 --- a/apps/ensindexer/src/plugins/ensv2/handlers/Block.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { ponder } from "ponder:registry"; -import schema from "ponder:schema"; -import { eq } from "ponder"; - -import { - type LiteralLabel, - literalLabelToInterpretedLabel, - PluginName, -} from "@ensnode/ensnode-sdk"; -import { ErrorCode, StatusCode } from "@ensnode/ensrainbow-sdk"; - -import { getENSRainbowApiClient } from "@/lib/ensraibow-api-client"; -import { namespaceContract } from "@/lib/plugin-helpers"; - -const pluginName = PluginName.ENSv2; - -const BATCH_SIZE = 1000; -const ensrainbow = getENSRainbowApiClient(); - -export default function () { - ponder.on( - namespaceContract(pluginName, "ENSRainbowBatchHeal:block"), - async ({ context, event }) => { - while (true) { - const unhealed = await context.db.sql - .select() - .from(schema.label) - .where(eq(schema.label.needsHeal, true)) - .limit(BATCH_SIZE); - - if (unhealed.length === 0) return; - - console.log(`Healing ${unhealed.length} labels in block ${event.block.number}`); - - // TODO: ENSRainbow batchHeal - let now = performance.now(); - const results = await Promise.all( - unhealed.map(async ({ labelHash }) => ({ - labelHash, - response: await ensrainbow.heal(labelHash), - })), - ); - console.log(`ensrainbow duration: ${performance.now() - now}ms`); - - // NOTE: transactions/batch not supported by Ponder's Postgres Proxy Driver - now = performance.now(); - for (const { labelHash, response } of results) { - switch (response.status) { - case StatusCode.Success: { - const interpretedLabel = literalLabelToInterpretedLabel( - response.label as LiteralLabel, - ); - await context.db.sql - .update(schema.label) - .set({ value: interpretedLabel, needsHeal: false }) - .where(eq(schema.label.labelHash, labelHash)); - break; - } - case "error": { - switch (response.errorCode) { - case ErrorCode.BadRequest: - case ErrorCode.NotFound: { - await context.db.sql - .update(schema.label) - .set({ needsHeal: false }) - .where(eq(schema.label.labelHash, labelHash)); - break; - } - case ErrorCode.ServerError: { - // this requires ENSRainbow to be available, does not tolerate downtime - // ideally instead we can do some sort of queue for failed tasks with backoff... - throw new Error( - `Error healing labelHash: "${labelHash}". Error (${response.errorCode}): ${response.error}.`, - ); - } - } - } - } - } - console.log(`update duration: ${performance.now() - now}ms`); - } - }, - ); -} diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts index e828628f0..9df918d90 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts @@ -12,10 +12,10 @@ import { type LabelHash, makeENSv1DomainId, makeImplicitRegistryId, - makeResolverId, makeSubdomainNode, type Node, PluginName, + ROOT_NODE, } from "@ensnode/ensnode-sdk"; import { materializeDomainOwner } from "@/lib/ensv2/domain-db-helpers"; @@ -32,6 +32,22 @@ const pluginName = PluginName.ENSv2; * - piggybacks Protocol Resolution plugin's Node Migration status */ export default function () { + /** + * Sets up the ENSv2 Root Registry + */ + ponder.on(namespaceContract(pluginName, "ENSv1RegistryOld:setup"), async ({ context }) => { + // ensures that the Root Registry (which is eventually backed by the ENSv2 Root Registry) is + // populated in the database + await context.db + .insert(schema.registry) + .values({ + id: getRootRegistryId(config.namespace), + type: "RegistryContract", + ...getRootRegistry(config.namespace), + }) + .onConflictDoNothing(); + }); + /** * Registry#NewOwner is either a new Domain OR the owner of the parent changing the owner of the child. */ @@ -90,7 +106,7 @@ export default function () { }) .onConflictDoNothing(); - // upsert the domain's own ImplicitRegistry + // and its ImplicitRegistry await context.db .insert(schema.registry) .values({ id: subregistryId, type: "ImplicitRegistry" }) @@ -105,27 +121,6 @@ export default function () { await materializeDomainOwner(context, domainId, owner); } - async function handleNewResolver({ - context, - event, - }: { - context: Context; - event: EventWithArgs<{ node: Node; resolver: Address }>; - }) { - const { node, resolver: address } = event.args; - - const domainId = makeENSv1DomainId(node); - - // update domain's resolver - const isDeletion = isAddressEqual(address, zeroAddress); - if (isDeletion) { - await context.db.update(schema.domain, { id: domainId }).set({ resolverId: null }); - } else { - const resolverId = makeResolverId({ chainId: context.chain.id, address: address }); - await context.db.update(schema.domain, { id: domainId }).set({ resolverId }); - } - } - async function handleTransfer({ context, event, @@ -136,7 +131,7 @@ export default function () { const { node, owner } = event.args; // ENSv2 model does not include root node, no-op - if (node === zeroHash) return; + if (node === ROOT_NODE) return; const domainId = makeENSv1DomainId(node); @@ -155,22 +150,6 @@ export default function () { await materializeDomainOwner(context, domainId, owner); } - /** - * Sets up the ENSv2 Root Registry - */ - ponder.on(namespaceContract(pluginName, "ENSv1RegistryOld:setup"), async ({ context }) => { - // ensures that the Root Registry (which is eventually backed by the ENSv2 Root Registry) is - // populated in the database - await context.db - .insert(schema.registry) - .values({ - id: getRootRegistryId(config.namespace), - type: "RegistryContract", - ...getRootRegistry(config.namespace), - }) - .onConflictDoNothing(); - }); - /** * Handles Registry#NewOwner for: * - ENS Root Chain's ENSv1RegistryOld @@ -189,21 +168,6 @@ export default function () { }, ); - /** - * Handles Registry#NewResolver for: - * - ENS Root Chain's ENSv1RegistryOld - */ - ponder.on( - namespaceContract(pluginName, "ENSv1RegistryOld:NewResolver"), - async ({ context, event }) => { - // ignore the event on ENSv1RegistryOld if node is migrated to new Registry - const shouldIgnoreEvent = await nodeIsMigrated(context, event.args.node); - if (shouldIgnoreEvent) return; - - return handleNewResolver({ context, event }); - }, - ); - /** * Handles Registry#Transfer for: * - ENS Root Chain's ENSv1RegistryOld @@ -225,6 +189,5 @@ export default function () { * - Lineanames Registry */ ponder.on(namespaceContract(pluginName, "ENSv1Registry:NewOwner"), handleNewOwner); - ponder.on(namespaceContract(pluginName, "ENSv1Registry:NewResolver"), handleNewResolver); ponder.on(namespaceContract(pluginName, "ENSv1Registry:Transfer"), handleTransfer); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts b/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts index ce113ea7f..682a4de9f 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts @@ -6,6 +6,7 @@ import { type DNSEncodedLiteralName, type DNSEncodedName, decodeDNSEncodedLiteralName, + isPccFuseSet, type LiteralLabel, makeENSv1DomainId, makeRegistrationId, @@ -18,8 +19,14 @@ import { import { materializeDomainOwner } from "@/lib/ensv2/domain-db-helpers"; import { ensureLabel } from "@/lib/ensv2/label-db-helpers"; import { getRegistrarManagedName } from "@/lib/ensv2/registrar-lib"; -import { getLatestRegistration, isRegistrationActive } from "@/lib/ensv2/registration-db-helpers"; +import { + getLatestRegistration, + isRegistrationExpired, + isRegistrationFullyExpired, + isRegistrationInGracePeriod, +} from "@/lib/ensv2/registration-db-helpers"; import { getThisAccountId } from "@/lib/get-this-account-id"; +import { toJson } from "@/lib/json-stringify-with-bigints"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; @@ -33,22 +40,23 @@ const pluginName = PluginName.ENSv2; */ const tokenIdToNode = (tokenId: bigint): Node => uint256ToHex32(tokenId); +/** + * NameWrapper emits expiration but 0 means 'doesn't expire', we represent as null. + */ +const interpretExpiration = (expiration: bigint): bigint | null => + expiration === 0n ? null : expiration; + // registrar is source of truth for expiry if eth 2LD // otherwise namewrapper is registrar and source of truth for expiry -// maybe should more or less ignore .eth 2LD (and other registrar-managed names) in the namewrapper? -// for non-.eth-2ld names is infinite expiration represented as 0 or max int? probably max int. if so -// need to interpret that into null to indicate that it doesn't expire -// for .eth 2lds we need any namewrapper events to be, like, ignored, basically. maybe a BaseRegistrar -// Registration can include a `wrappedTokenId` field to indicate that it exists in the namewrapper -// but NameWrapper Registrations are exclusively for non-.eth-2lds +// +// The FusesSet event indicates that fuses were written to storage, but: +// Does not guarantee the name is not expired +// Does not guarantee the fuses are actually active (they could be cleared by _clearOwnerAndFuses on read) +// Simply records the fuse value that was stored, regardless of expiration status +// For indexers, this means you need to track both the FusesSet event AND the expiry to determine the actual active fuses at any point in time. -// namewrapper registration does not have a registrant -// probably need an isSubnameOfRegistrarManagedName helper to identify .eth 2lds (and linea 2lds) -// in the namewrapper and then affect the registration managed by those contracts instead of the -// one managed by the namewrapper. -// so if it's wrapped in the namewrapper, much like the chain, changes are materialized back to the -// source +// .eth 2LDs always have PARENT_CANNOT_CONTROL set ('burned'), they cannot be transferred during grace period const isDirectSubnameOfRegistrarManagedName = ( managedNode: Node, @@ -78,6 +86,9 @@ const isDirectSubnameOfRegistrarManagedName = ( }; export default function () { + /** + * Transfer* events can occur for both expired and unexpired names. + */ async function handleTransfer({ context, event, @@ -101,16 +112,29 @@ export default function () { // burning is always followed by NameWrapper#NameUnwrapped, safe to ignore if (isBurn) return; + // otherwise is transfer of existing registration + const domainId = makeENSv1DomainId(tokenIdToNode(tokenId)); const registration = await getLatestRegistration(context, domainId); - const isActive = isRegistrationActive(registration, event.block.timestamp); + const isExpired = registration && isRegistrationExpired(registration, event.block.timestamp); + + // Invariant: must have Registration + if (!registration) { + throw new Error( + `Invariant(NameWrapper:Transfer): Registration expected:\n${toJson(registration)}`, + ); + } - // Invariant: must have active Registration - if (!registration || !isActive) { - throw new Error(`Invariant(NameWrapper:Transfer): Active Registration expected.`); + // Expired Registrations are non-transferrable if PCC is set + const cannotTransferWhileExpired = registration.fuses && isPccFuseSet(registration.fuses); + if (isExpired && cannotTransferWhileExpired) { + throw new Error( + `Invariant(NameWrapper:Transfer): Transfer of expired Registration with PARENT_CANNOT_CONTROL set:\n${toJson(registration)} ${JSON.stringify({ isPccFuseSet: isPccFuseSet(registration.fuses ?? 0) })}`, + ); } - // materialize domain owner + // now guaranteed to be an unexpired transferrable Registration + // so materialize domain owner await materializeDomainOwner(context, domainId, to); } @@ -129,7 +153,8 @@ export default function () { expiry: bigint; }>; }) => { - const { node, name: _name, owner, fuses, expiry: expiration } = event.args; + const { node, name: _name, owner, fuses, expiry: _expiration } = event.args; + const expiration = interpretExpiration(_expiration); const name = _name as DNSEncodedLiteralName; const registrar = getThisAccountId(context, event); @@ -147,51 +172,87 @@ export default function () { } const registration = await getLatestRegistration(context, domainId); - const isActive = isRegistrationActive(registration, event.block.timestamp); + const isFullyExpired = + registration && isRegistrationFullyExpired(registration, event.block.timestamp); // materialize domain owner await materializeDomainOwner(context, domainId, owner); - if (registration && isActive && registration.type === "BaseRegistrar") { - // if there's an existing active BaseRegistrar registration, this this must be the wrap of a - // direct-subname-of-registrar-managed-name - + // handle wraps of direct-subname-of-registrar-managed-names + if (registration && !isFullyExpired && registration.type === "BaseRegistrar") { const managedNode = namehash(getRegistrarManagedName(getThisAccountId(context, event))); - // Invariant: emitted Name is decodable and is a direct subname of the RegistrarManagedName + // Invariant: Emitted name is a direct subname of the RegistrarManagedName if (!isDirectSubnameOfRegistrarManagedName(managedNode, name, node)) { throw new Error( - `Invariant(NameWrapper:NameWrapped): An active BaseRegistrar Registration was found, but the name in question is NOT a direct subname of this NameWrapper's BaseRegistrar's RegistrarManagedName — wtf?`, + `Invariant(NameWrapper:NameWrapped): An unexpired BaseRegistrar Registration was found, but the name in question is NOT a direct subname of this NameWrapper's BaseRegistrar's RegistrarManagedName — wtf?`, + ); + } + + // Invariant: Cannot wrap grace period names + if (isRegistrationInGracePeriod(registration, event.block.timestamp)) { + throw new Error( + `Invariant(NameWrapper:NameWrapped): Cannot wrap direct-subname-of-registrar-managed-names in GRACE_PERIOD \n${toJson(registration)}`, ); } + // Invariant: cannot re-wrap, right? NameWrapped -> NameUnwrapped -> NameWrapped + if (registration.wrapped) { + throw new Error( + `Invariant(NameWrapper:NameWrapped): Re-wrapping already wrapped BaseRegistrar registration\n${toJson(registration)}`, + ); + } + + // Invariant: BaseRegistrar always provides Expiration + if (expiration === null) { + throw new Error( + `Invariant(NameWrapper:NameWrapped): Wrap of BaseRegistrar Registration does not include expiration!\n${toJson(registration)}`, + ); + } + + // Invariant: Expiration Alignment + if ( + // If BaseRegistrar Registration has an expiration, + registration.expiration && + // The NameWrapper epiration must be greater than that (+ grace period). + expiration > registration.expiration + (registration.gracePeriod ?? 0n) + ) { + throw new Error("Wrapper expiry exceeds registrar expiry + grace period"); + } + await context.db.update(schema.registration, { id: registration.id }).set({ wrapped: true, - wrappedFuses: fuses, + fuses, // expiration, // TODO: NameWrapper expiration logic }); - } + } else { + // Invariant: If there's an existing Registration, it should be expired + if (registration && !isFullyExpired) { + throw new Error( + `Invariant(NameWrapper:NameWrapped): NameWrapped but there's an existing unexpired non-BaseRegistrar Registration:\n${toJson(registration)}`, + ); + } - // Invariant: there shouldn't be an active registration - if (registration && isActive) { - throw new Error( - `Invariant(NameWrapper:NameWrapped): NameWrapped but there's an existing active non-BaseRegistrar Registration.`, - ); - } + const isAlreadyExpired = expiration && expiration <= event.block.timestamp; + if (isAlreadyExpired) { + console.warn(`Creating NameWrapper registration for already-expired name: ${node}`); + } - // create a new NameWrapper Registration - const nextIndex = registration ? registration.index + 1 : 0; - const registrationId = makeRegistrationId(domainId, nextIndex); - await context.db.insert(schema.registration).values({ - id: registrationId, - type: "NameWrapper", - registrarChainId: registrar.chainId, - registrarAddress: registrar.address, - domainId, - start: event.block.timestamp, - fuses, - expiration, - }); + // create a new NameWrapper Registration + const nextIndex = registration ? registration.index + 1 : 0; + const registrationId = makeRegistrationId(domainId, nextIndex); + await context.db.insert(schema.registration).values({ + id: registrationId, + type: "NameWrapper", + index: nextIndex, + registrarChainId: registrar.chainId, + registrarAddress: registrar.address, + domainId, + start: event.block.timestamp, + fuses, + expiration, + }); + } }, ); @@ -217,19 +278,23 @@ export default function () { // if this is a wrapped BaseRegisrar Registration, unwrap it await context.db.update(schema.registration, { id: registration.id }).set({ wrapped: false, - wrappedFuses: null, - // expiration: null // TODO: NameWrapper expiration logic + fuses: null, + // expiration: null // TODO: NameWrapper expiration logic? maybe nothing to do here }); } else { - // otherwise, deactivate the NameWrapper Registrations - // TODO: instead of deleting, mark it as inactive perhaps by setting its expiry to block.timestamp - await context.db.delete(schema.registration, { id: registration.id }); + // otherwise, deactivate the current registration by setting its expiry to this block + await context.db.update(schema.registration, { id: registration.id }).set({ + expiration: event.block.timestamp, + }); } // NOTE: we don't need to adjust Domain.ownerId because NameWrapper always calls ens.setOwner }, ); + /** + * FusesSet can occur for expired or unexpired Registrations. + */ ponder.on( namespaceContract(pluginName, "NameWrapper:FusesSet"), async ({ @@ -243,25 +308,25 @@ export default function () { const domainId = makeENSv1DomainId(node); const registration = await getLatestRegistration(context, domainId); - const isActive = isRegistrationActive(registration, event.block.timestamp); - // Invariant: must have active Registration - if (!registration || !isActive) { - throw new Error(`Invariant(NameWrapper:FusesSet): Active Registration expected.`); + // Invariant: must have a Registration + if (!registration) { + throw new Error( + `Invariant(NameWrapper:FusesSet): Registration expected:\n${toJson(registration)}`, + ); } // upsert fuses - if (registration.type === "BaseRegistrar") { - await context.db.update(schema.registration, { id: registration.id }).set({ - wrappedFuses: fuses, - // expiration: // TODO: NameWrapper expiration logic - }); - } else { - await context.db.update(schema.registration, { id: registration.id }).set({ fuses }); - } + await context.db.update(schema.registration, { id: registration.id }).set({ + fuses, + // expiration: // TODO: NameWrapper expiration logic ? + }); }, ); + /** + * ExpiryExtended can occur for expired or unexpired Registrations. + */ ponder.on( namespaceContract(pluginName, "NameWrapper:ExpiryExtended"), async ({ @@ -271,15 +336,17 @@ export default function () { context: Context; event: EventWithArgs<{ node: Node; expiry: bigint }>; }) => { - const { node, expiry: expiration } = event.args; + const { node, expiry: _expiration } = event.args; + const expiration = interpretExpiration(_expiration); const domainId = makeENSv1DomainId(node); const registration = await getLatestRegistration(context, domainId); - const isActive = isRegistrationActive(registration, event.block.timestamp); - // Invariant: must have active Registration - if (!registration || !isActive) { - throw new Error(`Invariant(NameWrapper:ExpiryExtended): Active Registration expected.`); + // Invariant: must have Registration + if (!registration) { + throw new Error( + `Invariant(NameWrapper:ExpiryExtended): Registration expected\n${toJson(registration)}`, + ); } await context.db.update(schema.registration, { id: registration.id }).set({ expiration }); diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts index acd6dcfe4..24fbd102a 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts @@ -9,7 +9,6 @@ import { type LiteralLabel, makeENSv2DomainId, makeRegistryContractId, - makeResolverId, PluginName, } from "@ensnode/ensnode-sdk"; @@ -116,34 +115,35 @@ export default function () { }, ); - ponder.on( - namespaceContract(pluginName, "Registry:ResolverUpdate"), - async ({ - context, - event, - }: { - context: Context; - event: EventWithArgs<{ - id: bigint; - resolver: Address; - }>; - }) => { - const { id: tokenId, resolver: address } = event.args; - - const canonicalId = getCanonicalId(tokenId); - const registryAccountId = getThisAccountId(context, event); - const domainId = makeENSv2DomainId(registryAccountId, canonicalId); - - // update domain's resolver - const isDeletion = isAddressEqual(address, zeroAddress); - if (isDeletion) { - await context.db.update(schema.domain, { id: domainId }).set({ resolverId: null }); - } else { - const resolverId = makeResolverId({ chainId: context.chain.id, address: address }); - await context.db.update(schema.domain, { id: domainId }).set({ resolverId }); - } - }, - ); + // TODO: add this logic to Protocol Acceleration plugin + // ponder.on( + // namespaceContract(pluginName, "Registry:ResolverUpdate"), + // async ({ + // context, + // event, + // }: { + // context: Context; + // event: EventWithArgs<{ + // id: bigint; + // resolver: Address; + // }>; + // }) => { + // const { id: tokenId, resolver: address } = event.args; + + // const canonicalId = getCanonicalId(tokenId); + // const registryAccountId = getThisAccountId(context, event); + // const domainId = makeENSv2DomainId(registryAccountId, canonicalId); + + // // update domain's resolver + // const isDeletion = isAddressEqual(address, zeroAddress); + // if (isDeletion) { + // await context.db.update(schema.domain, { id: domainId }).set({ resolverId: null }); + // } else { + // const resolverId = makeResolverId({ chainId: context.chain.id, address: address }); + // await context.db.update(schema.domain, { id: domainId }).set({ resolverId }); + // } + // }, + // ); ponder.on( namespaceContract(pluginName, "Registry:NameBurned"), diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index 58c2d3887..214d6c11b 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -57,14 +57,6 @@ export default createPlugin({ ALL_DATASOURCE_NAMES, ), - blocks: { - [namespaceContract(pluginName, "ENSRainbowBatchHeal")]: { - chain: ensroot.chain.id.toString(), - interval: 1, - startBlock: ensroot.contracts.Registry.startBlock, - }, - }, - contracts: { [namespaceContract(pluginName, "Registry")]: { abi: RegistryABI, diff --git a/packages/datasources/src/sepolia.ts b/packages/datasources/src/sepolia.ts index d701f0faf..70bd7f3d5 100644 --- a/packages/datasources/src/sepolia.ts +++ b/packages/datasources/src/sepolia.ts @@ -99,15 +99,15 @@ export default { RootRegistry: { abi: Registry, address: "0x8a791620dd6260079bf849dc5567adc3f2fdc318", - startBlock: 0, + startBlock: 9629999, }, Registry: { abi: Registry, - startBlock: 0, + startBlock: 9629999, }, EnhancedAccessControl: { abi: EnhancedAccessControl, - startBlock: 0, + startBlock: 9629999, }, }, }, @@ -117,11 +117,11 @@ export default { contracts: { Registry: { abi: Registry, - startBlock: 0, + startBlock: 9629999, }, EnhancedAccessControl: { abi: EnhancedAccessControl, - startBlock: 0, + startBlock: 9629999, }, }, }, diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/schemas/ensv2.schema.ts index 244d1e333..857939549 100644 --- a/packages/ensnode-schema/src/schemas/ensv2.schema.ts +++ b/packages/ensnode-schema/src/schemas/ensv2.schema.ts @@ -7,13 +7,11 @@ import type { EncodedReferrer, InterpretedLabel, LabelHash, - Node, PermissionsId, PermissionsResourceId, PermissionsUserId, RegistrationId, RegistryId, - ResolverId, } from "@ensnode/ensnode-sdk"; // Registry<->Domain is 1:1 @@ -99,8 +97,7 @@ export const domain = onchainTable( // may have one subregistry subregistryId: t.text().$type(), - // may have one resolver - resolverId: t.text().$type(), + // NOTE: Domain-Resolver Relations tracked via Protocol Acceleration plugin }), (t) => ({ // @@ -168,17 +165,15 @@ export const registration = onchainTable( // references referrer referrer: t.hex().$type(), - // may have fuses + // may have fuses (NameWrapper, Wrapped BaseRegistrar) fuses: t.integer(), - // may have baseCost/premium + // may have baseCost/premium (BaseRegistrar) baseCost: t.bigint(), premium: t.bigint(), // may be Wrapped (BaseRegistrar) wrapped: t.boolean().default(false), - wrappedFuses: t.integer(), - wrappedExpiration: t.bigint(), }), (t) => ({ byId: uniqueIndex().on(t.domainId, t.index), @@ -283,7 +278,6 @@ export const relations_permissionsUser = relations(permissionsUser, ({ one, many export const label = onchainTable("labels", (t) => ({ labelHash: t.hex().primaryKey().$type(), value: t.text().notNull().$type(), - needsHeal: t.boolean().default(false), })); export const label_relations = relations(label, ({ many }) => ({ diff --git a/packages/ensnode-sdk/src/ens/fuses.ts b/packages/ensnode-sdk/src/ens/fuses.ts new file mode 100644 index 000000000..013b0b220 --- /dev/null +++ b/packages/ensnode-sdk/src/ens/fuses.ts @@ -0,0 +1,4 @@ +const PARENT_CANNOT_CONTROL = 0x10000; + +export const isPccFuseSet = (fuses: number) => + (fuses & PARENT_CANNOT_CONTROL) === PARENT_CANNOT_CONTROL; diff --git a/packages/ensnode-sdk/src/ens/index.ts b/packages/ensnode-sdk/src/ens/index.ts index 502250384..eccef2036 100644 --- a/packages/ensnode-sdk/src/ens/index.ts +++ b/packages/ensnode-sdk/src/ens/index.ts @@ -4,6 +4,7 @@ export * from "./coin-type"; export * from "./constants"; export * from "./dns-encoded-name"; export * from "./encode-labelhash"; +export * from "./fuses"; export * from "./is-normalized"; export * from "./names"; export * from "./parse-reverse-name"; From a050ea267740ae9a67ef5baa943a4c6b5ccfd8e1 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 14 Nov 2025 18:10:34 -0600 Subject: [PATCH 029/102] checkpoint --- .../graphql-api/lib/get-domain-resolver.ts | 19 ++---- apps/ensapi/src/graphql-api/schema/domain.ts | 1 + .../protocol-acceleration/find-resolver.ts | 36 ++++++----- .../ensindexer/src/lib/ensv2/registrar-lib.ts | 26 ++++---- .../node-resolver-relationship-db-helpers.ts | 31 +++++---- apps/ensindexer/src/plugins/ensv2/plugin.ts | 2 + .../handlers/Registry.ts | 64 ++++++++++--------- .../handlers/ThreeDNSToken.ts | 12 +++- .../schemas/protocol-acceleration.schema.ts | 40 ++++++------ 9 files changed, 123 insertions(+), 108 deletions(-) diff --git a/apps/ensapi/src/graphql-api/lib/get-domain-resolver.ts b/apps/ensapi/src/graphql-api/lib/get-domain-resolver.ts index c9195ab6d..2913d22a7 100644 --- a/apps/ensapi/src/graphql-api/lib/get-domain-resolver.ts +++ b/apps/ensapi/src/graphql-api/lib/get-domain-resolver.ts @@ -1,19 +1,12 @@ -import { - type DomainId, - type ENSv1DomainId, - makeResolverId, - type ResolverId, -} from "@ensnode/ensnode-sdk"; +import type { DomainId } from "@ensnode/ensnode-sdk"; import { db } from "@/lib/db"; -export async function getDomainResolver(domainId: DomainId): Promise { - // TODO: refactor nodeResolverRelation to be domainResolverRelation using DomainId - const nrr = await db.query.nodeResolverRelation.findFirst({ - where: (t, { eq }) => eq(t.node, domainId as ENSv1DomainId), +export async function getDomainResolver(domainId: DomainId) { + const drr = await db.query.domainResolverRelation.findFirst({ + where: (t, { eq }) => eq(t.domainId, domainId), + with: { resolver: true }, }); - if (!nrr) return undefined; - - return makeResolverId({ chainId: nrr.chainId, address: nrr.resolver }); + return drr?.resolver; } diff --git a/apps/ensapi/src/graphql-api/schema/domain.ts b/apps/ensapi/src/graphql-api/schema/domain.ts index 43c65dc9e..5d30e53bd 100644 --- a/apps/ensapi/src/graphql-api/schema/domain.ts +++ b/apps/ensapi/src/graphql-api/schema/domain.ts @@ -166,6 +166,7 @@ DomainRef.implement({ description: "TODO", type: ResolverRef, nullable: true, + // TODO: dataloader this resolve: (parent) => getDomainResolver(parent.id), }), diff --git a/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts b/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts index d5a740c9a..415a827ed 100644 --- a/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts +++ b/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts @@ -15,6 +15,7 @@ import { packetToBytes } from "viem/ens"; import { DatasourceNames, getDatasource } from "@ensnode/datasources"; import { type AccountId, + type ENSv1DomainId, getNameHierarchy, isRootRegistry, type Name, @@ -176,6 +177,8 @@ async function findResolverWithIndex( "findResolverWithIndex", { chainId: registry.chainId, registry: registry.address, name }, async () => { + // TODO: all of this logic needs to be updated for ENSv2 Datamodel, need to reference new UR + // 1. construct a hierarchy of names. i.e. sub.example.eth -> [sub.example.eth, example.eth, eth] const names = getNameHierarchy(name); @@ -186,57 +189,58 @@ async function findResolverWithIndex( // 2. compute node of each via namehash const nodes = names.map((name) => namehash(name) as Node); + const domainIds = nodes as ENSv1DomainId[]; // 3. for each node, find its associated resolver (only in the specified registry) - const nodeResolverRelations = await withSpanAsync( + const domainResolverRelations = await withSpanAsync( tracer, - "nodeResolverRelation.findMany", + "domainResolverRelation.findMany", {}, async () => { - const records = await db.query.nodeResolverRelation.findMany({ - where: (nrr, { inArray, and, eq }) => + const records = await db.query.domainResolverRelation.findMany({ + where: (t, { inArray, and, eq }) => and( - eq(nrr.chainId, registry.chainId), // exclusively for the requested registry - eq(nrr.registry, registry.address), // exclusively for the requested registry - inArray(nrr.node, nodes), // find Relations for the following Nodes + eq(t.chainId, registry.chainId), // exclusively for the requested registry + eq(t.address, registry.address), // exclusively for the requested registry + inArray(t.domainId, domainIds), // find Relations for the following Domains ), - columns: { node: true, resolver: true }, + columns: { domainId: true, address: true }, }); // cast into our semantic types - return records as { node: Node; resolver: Address }[]; + return records as { domainId: ENSv1DomainId; address: Address }[]; }, ); // 3.1 sort into the same order as `nodes`, db results are not guaranteed to match `inArray` order - nodeResolverRelations.sort(sortByArrayOrder(nodes, (nrr) => nrr.node)); + domainResolverRelations.sort(sortByArrayOrder(domainIds, (nrr) => nrr.domainId)); // 4. iterate up the hierarchy and return the first valid resolver - for (const { node, resolver } of nodeResolverRelations) { + for (const { domainId, address } of domainResolverRelations) { // NOTE: this zeroAddress check is not strictly necessary, as the ProtocolAcceleration plugin // encodes a zeroAddress resolver as the _absence_ of a Node-Resolver relation, so there is // no case where a Node-Resolver relation exists and the resolverAddress is zeroAddress, but // we include this invariant here to encode that expectation explicitly. - if (isAddressEqual(resolver, zeroAddress)) { + if (isAddressEqual(zeroAddress, address)) { throw new Error( - `Invariant(findResolverWithIndex): Encountered a zeroAddress resolverAddress for node ${node}, which should be impossible: check ProtocolAcceleration Node-Resolver Relation indexing logic.`, + `Invariant(findResolverWithIndex): Encountered a zeroAddress resolverAddress for Domain ${domainId}, which should be impossible: check ProtocolAcceleration Node-Resolver Relation indexing logic.`, ); } // map the relation's `node` back to its name in `names` - const indexInHierarchy = nodes.indexOf(node); + const indexInHierarchy = domainIds.indexOf(domainId); const activeName = names[indexInHierarchy]; // will never occur, exlusively for typechecking if (!activeName) { throw new Error( - `Invariant(findResolverWithIndex): activeName could not be determined. names = ${JSON.stringify(names)} nodes = ${JSON.stringify(nodes)} active resolver's node: ${node}.`, + `Invariant(findResolverWithIndex): activeName could not be determined. names = ${JSON.stringify(names)} nodes = ${JSON.stringify(nodes)} active resolver's domainId: ${domainId}.`, ); } return { activeName, - activeResolver: resolver, + activeResolver: address, // this resolver must have wildcard support if it was not for the first node in our hierarchy requiresWildcardSupport: indexInHierarchy > 0, }; diff --git a/apps/ensindexer/src/lib/ensv2/registrar-lib.ts b/apps/ensindexer/src/lib/ensv2/registrar-lib.ts index bab4c6097..80d964910 100644 --- a/apps/ensindexer/src/lib/ensv2/registrar-lib.ts +++ b/apps/ensindexer/src/lib/ensv2/registrar-lib.ts @@ -1,13 +1,11 @@ import config from "@/config"; -import { DatasourceNames } from "@ensnode/datasources"; +import { DatasourceNames, type ENSNamespaceId } from "@ensnode/datasources"; import { type AccountId, accountIdEqual, type InterpretedName, - type LabelHash, type Name, - uint256ToHex32, } from "@ensnode/ensnode-sdk"; import { getDatasourceContract, maybeGetDatasourceContract } from "@/lib/datasource-helpers"; @@ -82,10 +80,22 @@ const REGISTRAR_CONTRACTS_BY_MANAGED_NAME: Record = { ].filter((c) => !!c), }; +const RMN_NAMESPACE_OVERRIDE: Partial>> = { + sepolia: { + "base.eth": "basetest.eth", + "linea.eth": "linea-sepolia.eth", + }, +}; + export const getRegistrarManagedName = (contract: AccountId) => { for (const [managedName, contracts] of Object.entries(REGISTRAR_CONTRACTS_BY_MANAGED_NAME)) { const isAnyOfTheContracts = contracts.some((_contract) => accountIdEqual(_contract, contract)); - if (isAnyOfTheContracts) return managedName as InterpretedName; + if (isAnyOfTheContracts) { + const namespaceSpecificManagedName = + RMN_NAMESPACE_OVERRIDE[config.namespace]?.[managedName] ?? managedName; + // override the rmn with namespace-specific version if available + return namespaceSpecificManagedName as InterpretedName; + } } throw new Error("never"); @@ -96,11 +106,3 @@ export function isNameWrapper(contract: AccountId) { if (lineanamesNameWrapper && accountIdEqual(lineanamesNameWrapper, contract)) return true; return false; } - -/** - * BaseRegistrar-derived Registrars register direct subnames of a RegistrarManagedName. As such, the - * tokens issued by them are keyed by the direct subname's label's labelHash. - * - * https://github.com/ensdomains/ens-contracts/blob/db613bc/contracts/ethregistrar/ETHRegistrarController.sol#L215 - */ -export const registrarTokenIdToLabelHash = (tokenId: bigint): LabelHash => uint256ToHex32(tokenId); diff --git a/apps/ensindexer/src/lib/protocol-acceleration/node-resolver-relationship-db-helpers.ts b/apps/ensindexer/src/lib/protocol-acceleration/node-resolver-relationship-db-helpers.ts index b36f8f155..0c9f5a380 100644 --- a/apps/ensindexer/src/lib/protocol-acceleration/node-resolver-relationship-db-helpers.ts +++ b/apps/ensindexer/src/lib/protocol-acceleration/node-resolver-relationship-db-helpers.ts @@ -1,25 +1,24 @@ import type { Context } from "ponder:registry"; import schema from "ponder:schema"; -import type { Address } from "viem"; -import type { Node } from "@ensnode/ensnode-sdk"; +import type { AccountId, DomainId, ResolverId } from "@ensnode/ensnode-sdk"; -export async function removeNodeResolverRelation(context: Context, registry: Address, node: Node) { - const chainId = context.chain.id; - - await context.db.delete(schema.nodeResolverRelation, { chainId, registry, node }); +export async function removedomainResolverRelation( + context: Context, + registry: AccountId, + domainId: DomainId, +) { + await context.db.delete(schema.domainResolverRelation, { ...registry, domainId }); } -export async function upsertNodeResolverRelation( +export async function upsertdomainResolverRelation( context: Context, - registry: Address, - node: Node, - resolver: Address, + registry: AccountId, + domainId: DomainId, + resolverId: ResolverId, ) { - const chainId = context.chain.id; - - return context.db - .insert(schema.nodeResolverRelation) - .values({ chainId, registry, node, resolver }) - .onConflictDoUpdate({ resolver }); + await context.db + .insert(schema.domainResolverRelation) + .values({ ...registry, domainId, resolverId }) + .onConflictDoUpdate({ resolverId }); } diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index 214d6c11b..2722b90dd 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -2,6 +2,8 @@ * TODO * - ThreeDNS * - Registration Renewals + * + * move all entity ids to opaque base58 encoded IDs? kinda nice since they're just supposed to be opaque, useful for relay purposes, allows the scalar types to all be ID and then casted. but nice to use CAIP identifiers for resolvers and permissions etc. so many just for domains and registries? */ import { createConfig } from "ponder"; diff --git a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Registry.ts b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Registry.ts index c31a4c1a3..841660e91 100644 --- a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Registry.ts +++ b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Registry.ts @@ -4,36 +4,27 @@ import { type Context, ponder } from "ponder:registry"; import { type Address, isAddressEqual, zeroAddress } from "viem"; import { getENSRootChainId } from "@ensnode/datasources"; -import { type LabelHash, makeSubdomainNode, type Node, PluginName } from "@ensnode/ensnode-sdk"; +import { + type ENSv1DomainId, + type LabelHash, + makeENSv1DomainId, + makeResolverId, + makeSubdomainNode, + type Node, + PluginName, +} from "@ensnode/ensnode-sdk"; +import { getThisAccountId } from "@/lib/get-this-account-id"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; import { - removeNodeResolverRelation, - upsertNodeResolverRelation, + removedomainResolverRelation, + upsertdomainResolverRelation, } from "@/lib/protocol-acceleration/node-resolver-relationship-db-helpers"; import { migrateNode, nodeIsMigrated } from "@/lib/protocol-acceleration/registry-migration-status"; const ensRootChainId = getENSRootChainId(config.namespace); -async function handleNewResolver({ - context, - event, -}: { - context: Context; - event: EventWithArgs<{ node: Node; resolver: Address }>; -}) { - const { node, resolver: resolverAddress } = event.args; - const registry = event.log.address; - const isZeroResolver = isAddressEqual(zeroAddress, resolverAddress); - - if (isZeroResolver) { - await removeNodeResolverRelation(context, registry, node); - } else { - await upsertNodeResolverRelation(context, registry, node, resolverAddress); - } -} - /** * Handler functions for Regsitry contracts in the Protocol Acceleration plugin. * - indexes ENS Root Chain Registry migration status @@ -42,6 +33,27 @@ async function handleNewResolver({ * Note that this registry migration status tracking is isolated to the protocol */ export default function () { + async function handleNewResolver({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ node: Node; resolver: Address }>; + }) { + const { node, resolver } = event.args; + + const registry = getThisAccountId(context, event); + const domainId = makeENSv1DomainId(node); + + const isZeroResolver = isAddressEqual(zeroAddress, resolver); + if (isZeroResolver) { + await removedomainResolverRelation(context, registry, domainId); + } else { + const resolverId = makeResolverId({ chainId: registry.chainId, address: resolver }); + await upsertdomainResolverRelation(context, registry, domainId, resolverId); + } + } + /** * Handles Registry#NewOwner for: * - ENS Root Chain's (new) Registry @@ -99,14 +111,6 @@ export default function () { */ ponder.on( namespaceContract(PluginName.ProtocolAcceleration, "ENSv1Registry:NewResolver"), - async ({ - context, - event, - }: { - context: Context; - event: EventWithArgs<{ node: Node; resolver: Address }>; - }) => { - await handleNewResolver({ context, event }); - }, + handleNewResolver, ); } diff --git a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ThreeDNSToken.ts b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ThreeDNSToken.ts index 326ad8215..26653537c 100644 --- a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ThreeDNSToken.ts +++ b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ThreeDNSToken.ts @@ -7,14 +7,17 @@ import { DatasourceNames, maybeGetDatasource } from "@ensnode/datasources"; import { type ChainId, type LabelHash, + makeENSv1DomainId, + makeResolverId, makeSubdomainNode, type Node, PluginName, } from "@ensnode/ensnode-sdk"; +import { getThisAccountId } from "@/lib/get-this-account-id"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; -import { upsertNodeResolverRelation } from "@/lib/protocol-acceleration/node-resolver-relationship-db-helpers"; +import { upsertdomainResolverRelation } from "@/lib/protocol-acceleration/node-resolver-relationship-db-helpers"; const ThreeDNSResolverByChainId: Record = [ DatasourceNames.ThreeDNSBase, @@ -52,8 +55,9 @@ export default function () { }>; }) => { const { label: labelHash, node: parentNode } = event.args; - const registry = event.log.address; + const registry = getThisAccountId(context, event); const node = makeSubdomainNode(labelHash, parentNode); + const domainId = makeENSv1DomainId(node); const resolverAddress = ThreeDNSResolverByChainId[context.chain.id]; if (!resolverAddress) { @@ -62,8 +66,10 @@ export default function () { ); } + const resolverId = makeResolverId({ chainId: registry.chainId, address: resolverAddress }); + // all ThreeDNSToken nodes have a hardcoded resolver at that address - await upsertNodeResolverRelation(context, registry, node, resolverAddress); + await upsertdomainResolverRelation(context, registry, domainId, resolverId); }, ); } diff --git a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts index a0b924ff6..d7d8a8189 100644 --- a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts +++ b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts @@ -5,7 +5,7 @@ import { onchainTable, primaryKey, relations, uniqueIndex } from "ponder"; import type { Address } from "viem"; -import type { ChainId, Node, ResolverId, ResolverRecordsId } from "@ensnode/ensnode-sdk"; +import type { ChainId, DomainId, Node, ResolverId, ResolverRecordsId } from "@ensnode/ensnode-sdk"; // TODO: implement resolverType & polymorphic field availability @@ -46,36 +46,40 @@ export const reverseNameRecord = onchainTable( ); /** - * Tracks Node-Resolver relationships to accelerate the identification of a node's active resolver - * in a specific (shadow)Registry. + * Tracks Domain-Resolver Relationships. This powers: + * 1. Domain-Resolver Realtionships within the GraphQL API, and + * 2. Accelerated lookups of a Domain's Resolver within the Resolution API. * - * Note that this model supports the indexing of Node-Resolver relationships across any Registry on - * on any chain, in particular to support the acceleration of ForwardResolution#findResolver for the - * ENS Root Chain's Registry which can have any number of (shadow)Registries (like Basenames' and - * Lineanames') on any chain. - * - * It is keyed by (chainId, registry, node) to match the on-chain datamodel of Registry/(shadow)Registry - * Node-Resolver relationships. + * It is keyed by (chainId, address, domainId) to match the on-chain datamodel of + * Registry/(shadow)Registry Domain-Resolver relationships. */ -export const nodeResolverRelation = onchainTable( - "node_resolver_relations", +export const domainResolverRelation = onchainTable( + "domain_resolver_relations", (t) => ({ // keyed by (chainId, registry, node) chainId: t.integer().notNull().$type(), - registry: t.hex().notNull().$type
(), - node: t.hex().notNull().$type(), + + // The Registry (ENSv1Registry or ENSv2Registry)'s AccountId. + address: t.hex().notNull().$type
(), + domainId: t.hex().notNull().$type(), /** - * The Address of the Resolver contract this `node` has set (via Registry#NewResolver) within - * the Registry on `chainId`. + * The Domain's assigned Resolver address within the Registry identified by (chainId, address). */ - resolver: t.hex().notNull().$type
(), + resolverId: t.hex().notNull().$type(), }), (t) => ({ - pk: primaryKey({ columns: [t.chainId, t.registry, t.node] }), + pk: primaryKey({ columns: [t.chainId, t.address, t.domainId] }), }), ); +export const domainResolverRelation_relations = relations(domainResolverRelation, ({ one }) => ({ + resolver: one(resolver, { + fields: [domainResolverRelation.resolverId], + references: [resolver.id], + }), +})); + export const resolver = onchainTable( "resolvers", (t) => ({ From 4cacc6f3ad56d14652ab4da6997db69e979e79bb Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 14 Nov 2025 18:13:37 -0600 Subject: [PATCH 030/102] oops --- apps/ensindexer/src/lib/ensv2/registrar-lib.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/ensindexer/src/lib/ensv2/registrar-lib.ts b/apps/ensindexer/src/lib/ensv2/registrar-lib.ts index 80d964910..f43eb70b1 100644 --- a/apps/ensindexer/src/lib/ensv2/registrar-lib.ts +++ b/apps/ensindexer/src/lib/ensv2/registrar-lib.ts @@ -5,7 +5,9 @@ import { type AccountId, accountIdEqual, type InterpretedName, + type LabelHash, type Name, + uint256ToHex32, } from "@ensnode/ensnode-sdk"; import { getDatasourceContract, maybeGetDatasourceContract } from "@/lib/datasource-helpers"; @@ -106,3 +108,11 @@ export function isNameWrapper(contract: AccountId) { if (lineanamesNameWrapper && accountIdEqual(lineanamesNameWrapper, contract)) return true; return false; } + +/** + * BaseRegistrar-derived Registrars register direct subnames of a RegistrarManagedName. As such, the + * tokens issued by them are keyed by the direct subname's label's labelHash. + * + * https://github.com/ensdomains/ens-contracts/blob/db613bc/contracts/ethregistrar/ETHRegistrarController.sol#L215 + */ +export const registrarTokenIdToLabelHash = (tokenId: bigint): LabelHash => uint256ToHex32(tokenId); From bdd20d443307a23d0b78f6e503eeed8829784c76 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 14 Nov 2025 19:55:42 -0600 Subject: [PATCH 031/102] fix: adjust invariants for registrar controllers --- .../src/plugins/ensv2/handlers/Registrar.ts | 24 +++++++++++++------ apps/ensindexer/src/plugins/ensv2/plugin.ts | 4 ++-- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/Registrar.ts b/apps/ensindexer/src/plugins/ensv2/handlers/Registrar.ts index 56c26fc39..e910f26d4 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/Registrar.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/Registrar.ts @@ -12,8 +12,12 @@ import { import { materializeDomainOwner } from "@/lib/ensv2/domain-db-helpers"; import { getRegistrarManagedName, registrarTokenIdToLabelHash } from "@/lib/ensv2/registrar-lib"; -import { getLatestRegistration } from "@/lib/ensv2/registration-db-helpers"; +import { + getLatestRegistration, + isRegistrationFullyExpired, +} from "@/lib/ensv2/registration-db-helpers"; import { getThisAccountId } from "@/lib/get-this-account-id"; +import { toJson } from "@/lib/json-stringify-with-bigints"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; @@ -107,19 +111,23 @@ export default function () { const domainId = makeENSv1DomainId(node); const registration = await getLatestRegistration(context, domainId); + const isFullyExpired = + registration && isRegistrationFullyExpired(registration, event.block.timestamp); - // TODO: && isActive(registration) - if (registration) { + // Invariant: If there is an existing Registration, it must be fully expired. + if (registration && !isFullyExpired) { throw new Error( `Invariant(Registrar:NameRegistered): Existing registration found in NameRegistered, expected none.`, ); } - const registrationId = makeRegistrationId(domainId, 0); + const nextIndex = registration ? registration.index + 1 : 0; + const registrationId = makeRegistrationId(domainId, nextIndex); // upsert relevant registration for domain await context.db.insert(schema.registration).values({ id: registrationId, + index: nextIndex, type: "BaseRegistrar", registrarChainId: registrar.chainId, registrarAddress: registrar.address, @@ -158,11 +166,13 @@ export default function () { const node = makeSubdomainNode(labelHash, managedNode); const domainId = makeENSv1DomainId(node); const registration = await getLatestRegistration(context, domainId); + const isFullyExpired = + registration && isRegistrationFullyExpired(registration, event.block.timestamp); - // TODO: || !isActive(registration) - if (!registration) { + // Invariant: There must be an unexired Registration to renew. + if (!registration || !isFullyExpired) { throw new Error( - `Invariant(Registrar:NameRenewed): NameRenewed emitted but no active registration.`, + `Invariant(Registrar:NameRenewed): NameRenewed emitted but no unexpired registration\n${toJson(registration)}`, ); } diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index 2722b90dd..63d3b42da 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -164,14 +164,14 @@ export default createPlugin({ ensroot.chain.id, ensroot.contracts.BaseRegistrar, ), - // Basenames BaseRegistrar, if exists + // Basenames BaseRegistrar, if defined ...(basenames && chainConfigForContract( config.globalBlockrange, basenames.chain.id, basenames.contracts.BaseRegistrar, )), - // Lineanames BaseRegistrar, if exists + // Lineanames BaseRegistrar, if defined ...(lineanames && chainConfigForContract( config.globalBlockrange, From f9f535d8b5386d1e73996ff4f5f1567c8f97a6ac Mon Sep 17 00:00:00 2001 From: shrugs Date: Sat, 15 Nov 2025 09:39:13 -0600 Subject: [PATCH 032/102] checkpoint --- .../src/lib/ensv2/registration-db-helpers.ts | 4 +- .../src/plugins/ensv2/event-handlers.ts | 4 +- .../{Registrar.ts => BaseRegistrar.ts} | 33 ++++++---- .../src/plugins/ensv2/handlers/NameWrapper.ts | 2 +- .../ensv2/handlers/RegistrarController.ts | 63 +++++++++++++++---- apps/ensindexer/src/plugins/ensv2/plugin.ts | 14 +++-- 6 files changed, 87 insertions(+), 33 deletions(-) rename apps/ensindexer/src/plugins/ensv2/handlers/{Registrar.ts => BaseRegistrar.ts} (81%) diff --git a/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts index 518ecd535..d9903efc2 100644 --- a/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts @@ -40,8 +40,8 @@ export function isRegistrationFullyExpired( // no expiration, never expired if (registration.expiration === null) return false; - // otherwise check against now - return registration.expiration + (registration.gracePeriod ?? 0n) <= now; + // otherwise it is expired if now >= expiration + grace + return now >= registration.expiration + (registration.gracePeriod ?? 0n); } /** diff --git a/apps/ensindexer/src/plugins/ensv2/event-handlers.ts b/apps/ensindexer/src/plugins/ensv2/event-handlers.ts index c4befa12a..27e8c3b59 100644 --- a/apps/ensindexer/src/plugins/ensv2/event-handlers.ts +++ b/apps/ensindexer/src/plugins/ensv2/event-handlers.ts @@ -1,7 +1,7 @@ +import attach_BaseRegistrarHandlers from "./handlers/BaseRegistrar"; import attach_ENSv1RegistryHandlers from "./handlers/ENSv1Registry"; import attach_EnhancedAccessControlHandlers from "./handlers/EnhancedAccessControl"; import attach_NameWrapperHandlers from "./handlers/NameWrapper"; -import attach_RegistrarHandlers from "./handlers/Registrar"; import attach_RegistrarControllerHandlers from "./handlers/RegistrarController"; import attach_RegistryHandlers from "./handlers/Registry"; @@ -9,7 +9,7 @@ export default function () { attach_ENSv1RegistryHandlers(); attach_EnhancedAccessControlHandlers(); attach_NameWrapperHandlers(); - attach_RegistrarHandlers(); + attach_BaseRegistrarHandlers(); attach_RegistrarControllerHandlers(); attach_RegistryHandlers(); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/Registrar.ts b/apps/ensindexer/src/plugins/ensv2/handlers/BaseRegistrar.ts similarity index 81% rename from apps/ensindexer/src/plugins/ensv2/handlers/Registrar.ts rename to apps/ensindexer/src/plugins/ensv2/handlers/BaseRegistrar.ts index e910f26d4..c9f188cd2 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/Registrar.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/BaseRegistrar.ts @@ -43,7 +43,7 @@ const pluginName = PluginName.ENSv2; export default function () { ponder.on( - namespaceContract(pluginName, "Registrar:Transfer"), + namespaceContract(pluginName, "BaseRegistrar:Transfer"), async ({ context, event, @@ -74,7 +74,9 @@ export default function () { if (isBurn) { // requires an existing registration if (!registration) { - throw new Error(`Invariant(Registrar:Transfer): _burn expected existing Registration`); + throw new Error( + `Invariant(BaseRegistrar:Transfer): _burn expected existing Registration`, + ); } // for now, just delete the registration @@ -82,7 +84,7 @@ export default function () { await context.db.delete(schema.registration, { id: registration.id }); } else { if (!registration) { - throw new Error(`Invariant(Registrar:Transfer): expected existing Registration`); + throw new Error(`Invariant(BaseRegistrar:Transfer): expected existing Registration`); } // materialize Domain owner @@ -117,7 +119,7 @@ export default function () { // Invariant: If there is an existing Registration, it must be fully expired. if (registration && !isFullyExpired) { throw new Error( - `Invariant(Registrar:NameRegistered): Existing registration found in NameRegistered, expected none.`, + `Invariant(BaseRegistrar:NameRegistered): Existing registration found in NameRegistered, expected none.`, ); } @@ -143,14 +145,14 @@ export default function () { await materializeDomainOwner(context, domainId, owner); } - ponder.on(namespaceContract(pluginName, "Registrar:NameRegistered"), handleNameRegistered); + ponder.on(namespaceContract(pluginName, "BaseRegistrar:NameRegistered"), handleNameRegistered); ponder.on( - namespaceContract(pluginName, "Registrar:NameRegisteredWithRecord"), + namespaceContract(pluginName, "BaseRegistrar:NameRegisteredWithRecord"), handleNameRegistered, ); ponder.on( - namespaceContract(pluginName, "Registrar:NameRenewed"), + namespaceContract(pluginName, "BaseRegistrar:NameRenewed"), async ({ context, event, @@ -166,16 +168,23 @@ export default function () { const node = makeSubdomainNode(labelHash, managedNode); const domainId = makeENSv1DomainId(node); const registration = await getLatestRegistration(context, domainId); - const isFullyExpired = - registration && isRegistrationFullyExpired(registration, event.block.timestamp); - // Invariant: There must be an unexired Registration to renew. - if (!registration || !isFullyExpired) { + // Invariant: There must be a Registration to renew. + if (!registration) { throw new Error( - `Invariant(Registrar:NameRenewed): NameRenewed emitted but no unexpired registration\n${toJson(registration)}`, + `Invariant(BaseRegistrar:NameRenewed): NameRenewed emitted but no Registration to renew.`, ); } + // Invariant: The Registation must not be fully expired. + // https://github.com/ensdomains/ens-contracts/blob/b6cb0e26/contracts/ethregistrar/BaseRegistrarImplementation.sol#L161 + if (isRegistrationFullyExpired(registration, event.block.timestamp)) { + throw new Error( + `Invariant(BaseRegistrar:NameRenewed): NameRenewed emitted but no unexpired registration\n${toJson({ registration, timestamp: event.block.timestamp })}`, + ); + } + + // update the registration await context.db.update(schema.registration, { id: registration.id }).set({ expiration }); // TODO: insert renewal & reference registration diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts b/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts index 682a4de9f..a19d839f1 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts @@ -229,7 +229,7 @@ export default function () { // Invariant: If there's an existing Registration, it should be expired if (registration && !isFullyExpired) { throw new Error( - `Invariant(NameWrapper:NameWrapped): NameWrapped but there's an existing unexpired non-BaseRegistrar Registration:\n${toJson(registration)}`, + `Invariant(NameWrapper:NameWrapped): NameWrapped but there's an existing unexpired non-BaseRegistrar Registration:\n${toJson({ registration, timestamp: event.block.timestamp })}`, ); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/RegistrarController.ts b/apps/ensindexer/src/plugins/ensv2/handlers/RegistrarController.ts index 09e9a5b07..faa975b4b 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/RegistrarController.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/RegistrarController.ts @@ -4,6 +4,8 @@ import { labelhash, namehash } from "viem"; import { type EncodedReferrer, + type Label, + type LabelHash, type LiteralLabel, labelhashLiteralLabel, makeENSv1DomainId, @@ -11,7 +13,7 @@ import { PluginName, } from "@ensnode/ensnode-sdk"; -import { ensureLabel } from "@/lib/ensv2/label-db-helpers"; +import { ensureLabel, ensureUnknownLabel } from "@/lib/ensv2/label-db-helpers"; import { getRegistrarManagedName } from "@/lib/ensv2/registrar-lib"; import { getLatestRegistration } from "@/lib/ensv2/registration-db-helpers"; import { getThisAccountId } from "@/lib/get-this-account-id"; @@ -27,18 +29,25 @@ export default function () { }: { context: Context; event: EventWithArgs<{ - label: string; + label?: Label; + labelHash: LabelHash; baseCost?: bigint; premium?: bigint; referrer?: EncodedReferrer; }>; }) { - const { label: _label, baseCost, premium, referrer } = event.args; - const label = _label as LiteralLabel; + const { label: _label, labelHash, baseCost, premium, referrer } = event.args; + const label = _label as LiteralLabel | undefined; + + // Invariant: If emitted, label must align with labelHash + if (label !== undefined && labelHash !== labelhashLiteralLabel(label)) { + throw new Error( + `Invariant(RegistrarController:NameRegistered): Emitted label '${label}' does not labelhash to emitted labelHash '${labelHash}'.`, + ); + } const controller = getThisAccountId(context, event); const managedNode = namehash(getRegistrarManagedName(controller)); - const labelHash = labelhashLiteralLabel(label); const node = makeSubdomainNode(labelHash, managedNode); const domainId = makeENSv1DomainId(node); @@ -51,7 +60,11 @@ export default function () { } // ensure label - await ensureLabel(context, label); + if (label !== undefined) { + await ensureLabel(context, label); + } else { + await ensureUnknownLabel(context, labelHash); + } // update registration's baseCost/premium await context.db @@ -83,7 +96,7 @@ export default function () { if (!registration) { throw new Error( - `Invariant(RegistrarController:NameRenewed): NameRegistered but no Registration.`, + `Invariant(RegistrarController:NameRenewed): NameRenewed but no Registration.`, ); } @@ -101,28 +114,56 @@ export default function () { pluginName, "RegistrarController:NameRegistered(string label, bytes32 indexed labelhash, address indexed owner, uint256 baseCost, uint256 premium, uint256 expires, bytes32 referrer)", ), - handleNameRegisteredByController, + ({ context, event }) => + handleNameRegisteredByController({ + context, + event: { + ...event, + args: { ...event.args, labelHash: event.args.labelhash }, + }, + }), ); ponder.on( namespaceContract( pluginName, "RegistrarController:NameRegistered(string name, bytes32 indexed label, address indexed owner, uint256 baseCost, uint256 premium, uint256 expires)", ), - handleNameRegisteredByController, + ({ context, event }) => + handleNameRegisteredByController({ + context, + event: { + ...event, + args: { ...event.args, label: event.args.name, labelHash: event.args.label }, + }, + }), ); ponder.on( namespaceContract( pluginName, "RegistrarController:NameRegistered(string name, bytes32 indexed label, address indexed owner, uint256 cost, uint256 expires)", ), - handleNameRegisteredByController, + ({ context, event }) => + handleNameRegisteredByController({ + context, + event: { + ...event, + args: { ...event.args, label: event.args.name, labelHash: event.args.label }, + }, + }), ); ponder.on( namespaceContract( pluginName, "RegistrarController:NameRegistered(string name, bytes32 indexed label, address indexed owner, uint256 expires)", ), - handleNameRegisteredByController, + ({ context, event }) => + handleNameRegisteredByController({ + context, + event: { + ...event, + args: { ...event.args, label: event.args.name, labelHash: event.args.label }, + }, + }), ); /////////////////////////////////// diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index 63d3b42da..1bc2a40b1 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -1,9 +1,13 @@ /** * TODO * - ThreeDNS - * - Registration Renewals + * - Renewals + * - indexes + * - https://pothos-graphql.dev/docs/plugins/tracing + * - connections w/ limits & cursors * * move all entity ids to opaque base58 encoded IDs? kinda nice since they're just supposed to be opaque, useful for relay purposes, allows the scalar types to all be ID and then casted. but nice to use CAIP identifiers for resolvers and permissions etc. so many just for domains and registries? + * test alpha sepolia + protocol accelerateion index time vs ensv2 */ import { createConfig } from "ponder"; @@ -152,10 +156,10 @@ export default createPlugin({ }, }, - ////////////// - // Registrars - ////////////// - [namespaceContract(pluginName, "Registrar")]: { + /////////////////// + // Base Registrars + /////////////////// + [namespaceContract(pluginName, "BaseRegistrar")]: { abi: AnyRegistrarABI, chain: { // Ethnames BaseRegistrar From cbde7e5c6d7cbcb790b6c2853c496cce5d33d6cf Mon Sep 17 00:00:00 2001 From: shrugs Date: Sat, 15 Nov 2025 16:05:05 -0600 Subject: [PATCH 033/102] fix: addressable latest registrations with superceding --- .../src/lib/ensv2/registration-db-helpers.ts | 33 ++++++++++++++++--- .../plugins/ensv2/handlers/BaseRegistrar.ts | 11 +++++-- .../src/plugins/ensv2/handlers/NameWrapper.ts | 23 ++++++++----- apps/ensindexer/src/plugins/ensv2/plugin.ts | 12 +++++++ packages/ensnode-sdk/src/ensv2/ids-lib.ts | 6 ++++ 5 files changed, 70 insertions(+), 15 deletions(-) diff --git a/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts index d9903efc2..2d76194f2 100644 --- a/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts @@ -1,15 +1,38 @@ import type { Context } from "ponder:registry"; -import type schema from "ponder:schema"; +import schema from "ponder:schema"; -import type { DomainId } from "@ensnode/ensnode-sdk"; +import { type DomainId, makeLatestRegistrationId, makeRegistrationId } from "@ensnode/ensnode-sdk"; + +import { toJson } from "@/lib/json-stringify-with-bigints"; /** * TODO: find the most recent registration, active or otherwise */ export async function getLatestRegistration(context: Context, domainId: DomainId) { - return await context.db.sql.query.registration.findFirst({ - where: (t, { eq }) => eq(t.domainId, domainId), - orderBy: (t, { desc }) => desc(t.index), + return context.db.find(schema.registration, { id: makeLatestRegistrationId(domainId) }); +} + +/** + * TODO + */ +export async function supercedeLatestRegistration( + context: Context, + registration: typeof schema.registration.$inferSelect, +) { + // Invariant: Must be the latest Registration + if (registration.id !== makeLatestRegistrationId(registration.domainId)) { + throw new Error( + `Invariant(supercedeRegistration): Attempted to supercede non-latest Registration:\n${toJson(registration)}`, + ); + } + + // delete latest + await context.db.delete(schema.registration, { id: registration.id }); + + // insert existing data into new Registration w/ indexed id + await context.db.insert(schema.registration).values({ + ...registration, + id: makeRegistrationId(registration.domainId, registration.index), }); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/BaseRegistrar.ts b/apps/ensindexer/src/plugins/ensv2/handlers/BaseRegistrar.ts index c9f188cd2..5e5081e17 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/BaseRegistrar.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/BaseRegistrar.ts @@ -5,6 +5,7 @@ import { type Address, isAddressEqual, namehash, zeroAddress } from "viem"; import { makeENSv1DomainId, + makeLatestRegistrationId, makeRegistrationId, makeSubdomainNode, PluginName, @@ -15,6 +16,7 @@ import { getRegistrarManagedName, registrarTokenIdToLabelHash } from "@/lib/ensv import { getLatestRegistration, isRegistrationFullyExpired, + supercedeLatestRegistration, } from "@/lib/ensv2/registration-db-helpers"; import { getThisAccountId } from "@/lib/get-this-account-id"; import { toJson } from "@/lib/json-stringify-with-bigints"; @@ -123,10 +125,15 @@ export default function () { ); } + // supercede the latest Registration if exists + if (registration) { + await supercedeLatestRegistration(context, registration); + } + const nextIndex = registration ? registration.index + 1 : 0; - const registrationId = makeRegistrationId(domainId, nextIndex); + const registrationId = makeLatestRegistrationId(domainId); - // upsert relevant registration for domain + // insert BaseRegistrar Registration await context.db.insert(schema.registration).values({ id: registrationId, index: nextIndex, diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts b/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts index a19d839f1..aee0066fe 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts @@ -9,7 +9,7 @@ import { isPccFuseSet, type LiteralLabel, makeENSv1DomainId, - makeRegistrationId, + makeLatestRegistrationId, makeSubdomainNode, type Node, PluginName, @@ -24,6 +24,7 @@ import { isRegistrationExpired, isRegistrationFullyExpired, isRegistrationInGracePeriod, + supercedeLatestRegistration, } from "@/lib/ensv2/registration-db-helpers"; import { getThisAccountId } from "@/lib/get-this-account-id"; import { toJson } from "@/lib/json-stringify-with-bigints"; @@ -125,7 +126,7 @@ export default function () { ); } - // Expired Registrations are non-transferrable if PCC is set + // Invariant: Expired Registrations are non-transferrable if PCC is set const cannotTransferWhileExpired = registration.fuses && isPccFuseSet(registration.fuses); if (isExpired && cannotTransferWhileExpired) { throw new Error( @@ -167,8 +168,8 @@ export default function () { await ensureLabel(context, label); } } catch { - // NameWrapper emitted malformed name? just warn - console.warn(`NameWrapper emitted malformed DNSEncoded Name: '${name}'`); + // NameWrapper emitted malformed name? just warn and move on + console.warn(`NameWrapper emitted malformed DNSEncodedName: '${name}'`); } const registration = await getLatestRegistration(context, domainId); @@ -238,13 +239,19 @@ export default function () { console.warn(`Creating NameWrapper registration for already-expired name: ${node}`); } - // create a new NameWrapper Registration + // supercede the latest Registration if exists + if (registration) { + await supercedeLatestRegistration(context, registration); + } + const nextIndex = registration ? registration.index + 1 : 0; - const registrationId = makeRegistrationId(domainId, nextIndex); + const registrationId = makeLatestRegistrationId(domainId); + + // insert NameWrapper Registration await context.db.insert(schema.registration).values({ id: registrationId, - type: "NameWrapper", index: nextIndex, + type: "NameWrapper", registrarChainId: registrar.chainId, registrarAddress: registrar.address, domainId, @@ -282,7 +289,7 @@ export default function () { // expiration: null // TODO: NameWrapper expiration logic? maybe nothing to do here }); } else { - // otherwise, deactivate the current registration by setting its expiry to this block + // otherwise, deactivate the latest registration by setting its expiry to this block await context.db.update(schema.registration, { id: registration.id }).set({ expiration: event.block.timestamp, }); diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index 1bc2a40b1..0795e6631 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -1,10 +1,22 @@ /** * TODO + * - mainnet sync is really really slow probably because of the getLatestRegistration + * - can we have two parallel tables, one of which always holds the `latestRegistration` which can be looked up exactly by domainId? + * - alternatively maybe the latest registration's id can always be domainId/latest and when it gets superceded by another one we can + * clone the data in /latest to /latest.index and then delete the latest and then insert a new latest with the new registration data + * - simpler than having to maintain parallel tables and sort by index always works for api layer + * * - ThreeDNS * - Renewals * - indexes * - https://pothos-graphql.dev/docs/plugins/tracing * - connections w/ limits & cursors + * - Resolver polymorphism & Bridged Resolver materialization + * - Account.dedicatedResolvers + * - Registry.canonicalName + * - then update canonical traversal to use canonicalName + * - Account.permissions -> PermissionsUser[] + * * * move all entity ids to opaque base58 encoded IDs? kinda nice since they're just supposed to be opaque, useful for relay purposes, allows the scalar types to all be ID and then casted. but nice to use CAIP identifiers for resolvers and permissions etc. so many just for domains and registries? * test alpha sepolia + protocol accelerateion index time vs ensv2 diff --git a/packages/ensnode-sdk/src/ensv2/ids-lib.ts b/packages/ensnode-sdk/src/ensv2/ids-lib.ts index beb33b8c5..5f98e9dce 100644 --- a/packages/ensnode-sdk/src/ensv2/ids-lib.ts +++ b/packages/ensnode-sdk/src/ensv2/ids-lib.ts @@ -95,3 +95,9 @@ export const makeResolverRecordsId = (contract: AccountId, node: Node) => */ export const makeRegistrationId = (domainId: DomainId, index: number = 0) => `${domainId}/${index}` as RegistrationId; + +/** + * + */ +export const makeLatestRegistrationId = (domainId: DomainId) => + `${domainId}/latest` as RegistrationId; From 521b6660addb7ae2fd82877351d9c1311b33bbbe Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 17 Nov 2025 11:40:51 -0600 Subject: [PATCH 034/102] fix: namewrapper name decoding for encoded-labelhash-looking names oops --- apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts b/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts index aee0066fe..c72a06ba8 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts @@ -1,6 +1,6 @@ import { type Context, ponder } from "ponder:registry"; import schema from "ponder:schema"; -import { type Address, isAddressEqual, labelhash, namehash, zeroAddress } from "viem"; +import { type Address, isAddressEqual, namehash, zeroAddress } from "viem"; import { type DNSEncodedLiteralName, @@ -8,6 +8,7 @@ import { decodeDNSEncodedLiteralName, isPccFuseSet, type LiteralLabel, + labelhashLiteralLabel, makeENSv1DomainId, makeLatestRegistrationId, makeSubdomainNode, @@ -79,7 +80,7 @@ const isDirectSubnameOfRegistrarManagedName = ( // construct the expected node using emitted name's leaf label and the registrarManagedNode // biome-ignore lint/style/noNonNullAssertion: length check above - const leaf = labelhash(labels[0]!); + const leaf = labelhashLiteralLabel(labels[0]!); const expectedNode = makeSubdomainNode(leaf, managedNode); // Nodes must exactly match From 4db5dd377b8951057618a861f0b59faa7f7a2f19 Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 17 Nov 2025 11:53:22 -0600 Subject: [PATCH 035/102] add ensrainbow error message --- apps/ensindexer/src/lib/graphnode-helpers.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/ensindexer/src/lib/graphnode-helpers.ts b/apps/ensindexer/src/lib/graphnode-helpers.ts index 5c7252524..b3d2e0ce0 100644 --- a/apps/ensindexer/src/lib/graphnode-helpers.ts +++ b/apps/ensindexer/src/lib/graphnode-helpers.ts @@ -14,7 +14,16 @@ const ensRainbowApiClient = getENSRainbowApiClient(); * @throws if the labelHash is not correctly formatted, or server error occurs, or connection error occurs. **/ export async function labelByLabelHash(labelHash: LabelHash): Promise { - const response = await ensRainbowApiClient.heal(labelHash); + let response: Awaited>; + try { + response = await ensRainbowApiClient.heal(labelHash); + } catch (error) { + if (error instanceof Error) { + error.message = `ENSRainbow Heal Request Failed: ENSRainbow unavailable at '${ensRainbowApiClient.getOptions().endpointUrl}'.`; + } + + throw error; + } if (isHealError(response)) { // no original label found for the labelHash From 2faaa5ff25f74a5852a9fd93bd6e522e0a263d32 Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 17 Nov 2025 12:03:00 -0600 Subject: [PATCH 036/102] ens-test-env default urls --- packages/datasources/src/lib/chains.ts | 2 ++ .../src/shared/config/rpc-configs-from-env.ts | 24 +++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/datasources/src/lib/chains.ts b/packages/datasources/src/lib/chains.ts index 4f2e36763..0363c1be4 100644 --- a/packages/datasources/src/lib/chains.ts +++ b/packages/datasources/src/lib/chains.ts @@ -16,10 +16,12 @@ export const ensTestEnvL1Chain = { ...localhost, id: l1ChainId, name: "ens-test-env L1", + rpcUrls: { default: { http: ["http://localhost:8545"] } }, } satisfies Chain; export const ensTestEnvL2Chain = { ...localhost, id: l2ChainId, name: "ens-test-env L2", + rpcUrls: { default: { http: ["http://localhost:8546"] } }, } satisfies Chain; diff --git a/packages/ensnode-sdk/src/shared/config/rpc-configs-from-env.ts b/packages/ensnode-sdk/src/shared/config/rpc-configs-from-env.ts index a27c04534..69641aa24 100644 --- a/packages/ensnode-sdk/src/shared/config/rpc-configs-from-env.ts +++ b/packages/ensnode-sdk/src/shared/config/rpc-configs-from-env.ts @@ -1,4 +1,10 @@ -import { type Datasource, type ENSNamespaceId, getENSNamespace } from "@ensnode/datasources"; +import { + type Datasource, + type ENSNamespaceId, + ensTestEnvL1Chain, + ensTestEnvL2Chain, + getENSNamespace, +} from "@ensnode/datasources"; import { serializeChainId } from "../serialize"; import type { ChainIdString } from "../serialized-types"; @@ -19,7 +25,7 @@ import type { ChainIdSpecificRpcEnvironmentVariable, RpcEnvironment } from "./en * 2. Alchemy, if ALCHEMY_API_KEY is available in the env * 3. DRPC, if DRPC_API_KEY is available in the env * - * TODO: also inject wss:// urls for alchemy, drpc keys + * It also provides a single Alchemy wss:// url if ALCHEMY_API_KEY is available in the env. * * NOTE: This function returns raw RpcConfigEnvironment values which are not yet parsed or validated. */ @@ -44,6 +50,20 @@ export function buildRpcConfigsFromEnv( continue; } + // ens-test-env L1 Chain + if (chain.id === ensTestEnvL1Chain.id) { + rpcConfigs[serializeChainId(ensTestEnvL1Chain.id)] = + ensTestEnvL1Chain.rpcUrls.default.http[0]; + continue; + } + + // ens-test-env L2 Chain + if (chain.id === ensTestEnvL2Chain.id) { + rpcConfigs[serializeChainId(ensTestEnvL2Chain.id)] = + ensTestEnvL2Chain.rpcUrls.default.http[0]; + continue; + } + const httpUrls = [ // alchemy, if specified and available alchemyApiKey && From caf47ed505b0df6d0da0d838ae5f2c7d25c5da9f Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 17 Nov 2025 18:22:00 -0600 Subject: [PATCH 037/102] checkpoint --- apps/ensapi/src/graphql-api/schema/account.ts | 21 ++ apps/ensapi/src/graphql-api/schema/query.ts | 16 +- .../get-records-from-index.ts | 48 ++-- ...nown-ccip-read-shadow-registry-resolver.ts | 86 ------ .../known-onchain-static-resolver.ts | 87 ------ .../ensapi/src/lib/{rpc => }/public-client.ts | 2 +- .../src/lib/resolution/forward-resolution.ts | 272 ++++++++---------- apps/ensapi/src/lib/rpc/eip-165.ts | 52 ---- apps/ensapi/src/lib/rpc/ensip-10.ts | 26 -- .../is-bridged-resolver.ts | 57 ++++ .../is-ensip-19-reverse-resolver.ts} | 0 .../is-static-resolver.ts | 56 ++++ .../resolver-records-db-helpers.ts | 108 +++++-- apps/ensindexer/src/plugins/ensv2/plugin.ts | 21 +- .../handlers/Resolver.ts | 120 +++++--- .../src/abis/shared/AnyRegistrar.ts | 11 - .../src/abis/shared/AnyRegistrarController.ts | 22 -- .../datasources/src/abis/shared/Ownable.ts | 34 +++ packages/datasources/src/index.ts | 4 +- packages/datasources/src/lib/AnyRegistrar.ts | 11 + .../src/lib/AnyRegistrarController.ts | 22 ++ packages/datasources/src/lib/resolver.ts | 17 +- .../schemas/protocol-acceleration.schema.ts | 50 +++- packages/ensnode-sdk/src/ens/constants.ts | 9 +- packages/ensnode-sdk/src/internal.ts | 1 + packages/ensnode-sdk/src/rpc/eip-165.ts | 76 +++++ packages/ensnode-sdk/src/rpc/index.ts | 3 + .../src/rpc/is-dedicated-resolver.ts | 12 + .../src/rpc/is-extended-resolver.ts | 12 + 29 files changed, 729 insertions(+), 527 deletions(-) delete mode 100644 apps/ensapi/src/lib/protocol-acceleration/known-ccip-read-shadow-registry-resolver.ts delete mode 100644 apps/ensapi/src/lib/protocol-acceleration/known-onchain-static-resolver.ts rename apps/ensapi/src/lib/{rpc => }/public-client.ts (88%) delete mode 100644 apps/ensapi/src/lib/rpc/eip-165.ts delete mode 100644 apps/ensapi/src/lib/rpc/ensip-10.ts create mode 100644 apps/ensindexer/src/lib/protocol-acceleration/is-bridged-resolver.ts rename apps/{ensapi/src/lib/protocol-acceleration/known-ensip-19-reverse-resolvers.ts => ensindexer/src/lib/protocol-acceleration/is-ensip-19-reverse-resolver.ts} (100%) create mode 100644 apps/ensindexer/src/lib/protocol-acceleration/is-static-resolver.ts delete mode 100644 packages/datasources/src/abis/shared/AnyRegistrar.ts delete mode 100644 packages/datasources/src/abis/shared/AnyRegistrarController.ts create mode 100644 packages/datasources/src/abis/shared/Ownable.ts create mode 100644 packages/datasources/src/lib/AnyRegistrar.ts create mode 100644 packages/datasources/src/lib/AnyRegistrarController.ts create mode 100644 packages/ensnode-sdk/src/rpc/eip-165.ts create mode 100644 packages/ensnode-sdk/src/rpc/index.ts create mode 100644 packages/ensnode-sdk/src/rpc/is-dedicated-resolver.ts create mode 100644 packages/ensnode-sdk/src/rpc/is-extended-resolver.ts diff --git a/apps/ensapi/src/graphql-api/schema/account.ts b/apps/ensapi/src/graphql-api/schema/account.ts index 800e295ce..6675bba8b 100644 --- a/apps/ensapi/src/graphql-api/schema/account.ts +++ b/apps/ensapi/src/graphql-api/schema/account.ts @@ -3,6 +3,7 @@ import type { Address } from "viem"; import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-id"; import { type Domain, DomainRef } from "@/graphql-api/schema/domain"; +import { type Resolver, ResolverRef } from "@/graphql-api/schema/resolver"; import { db } from "@/lib/db"; export const AccountRef = builder.loadableObjectRef("Account", { @@ -54,5 +55,25 @@ AccountRef.implement({ group: (domain) => (domain as Domain).ownerId!, resolve: getModelId, }), + + ////////////////////////////// + // Account.dedicatedResolvers + ////////////////////////////// + dedicatedResolvers: t.loadableGroup({ + description: "TODO", + // TODO: resolver polymorphism, return DedicatedResolverRef + type: ResolverRef, + load: (ids: Address[]) => + db.query.resolver.findMany({ + where: (t, { inArray, and, eq }) => + and( + inArray(t.ownerId, ids), // owned by id + eq(t.isDedicated, true), // must be dedicated resolver + ), + }), + // biome-ignore lint/style/noNonNullAssertion: guaranteed due to inArray + group: (resolver) => (resolver as Resolver).ownerId!, + resolve: getModelId, + }), }), }); diff --git a/apps/ensapi/src/graphql-api/schema/query.ts b/apps/ensapi/src/graphql-api/schema/query.ts index f3b607ee5..7e4146b17 100644 --- a/apps/ensapi/src/graphql-api/schema/query.ts +++ b/apps/ensapi/src/graphql-api/schema/query.ts @@ -1,12 +1,13 @@ import config from "@/config"; -import { getRootRegistryId, makeRegistryContractId } from "@ensnode/ensnode-sdk"; +import { getRootRegistryId, makeRegistryContractId, makeResolverId } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import { getDomainIdByInterpretedName } from "@/graphql-api/lib/get-domain-by-fqdn"; import { AccountRef } from "@/graphql-api/schema/account"; import { DomainIdInput, DomainRef } from "@/graphql-api/schema/domain"; import { RegistryIdInput, RegistryInterfaceRef } from "@/graphql-api/schema/registry"; +import { ResolverIdInput, ResolverRef } from "@/graphql-api/schema/resolver"; import { db } from "@/lib/db"; // TODO: maybe should still implement query/return by id, exposing the db's primary key? @@ -60,6 +61,19 @@ builder.queryType({ }, }), + ////////////////////// + // Get Resolver by Id + ////////////////////// + resolver: t.field({ + description: "TODO", + type: ResolverRef, + args: { by: t.arg({ type: ResolverIdInput, required: true }) }, + resolve: async (parent, args, ctx, info) => { + if (args.by.id !== undefined) return args.by.id; + return makeResolverId(args.by.contract); + }, + }), + ///////////////////// // Get Root Registry ///////////////////// diff --git a/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts b/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts index 15ef30cb6..8eb1d2ff4 100644 --- a/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts +++ b/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts @@ -1,54 +1,50 @@ -import { trace } from "@opentelemetry/api"; -import type { Address } from "viem"; - import { - type ChainId, + type AccountId, DEFAULT_EVM_COIN_TYPE, + makeResolverId, type Node, type ResolverRecordsSelection, } from "@ensnode/ensnode-sdk"; import { db } from "@/lib/db"; -import { onchainStaticResolverImplementsDefaultAddress } from "@/lib/protocol-acceleration/known-onchain-static-resolver"; import type { IndexedResolverRecords } from "@/lib/resolution/make-records-response"; -import { withSpanAsync } from "@/lib/tracing/auto-span"; - -const tracer = trace.getTracer("get-records"); const DEFAULT_EVM_COIN_TYPE_BIGINT = BigInt(DEFAULT_EVM_COIN_TYPE); export async function getRecordsFromIndex({ - chainId, - resolverAddress, + resolver: _resolver, node, selection, }: { - chainId: ChainId; - resolverAddress: Address; + resolver: AccountId; node: Node; selection: SELECTION; }): Promise { - // fetch the Resolver Records from index - const resolverRecords = await withSpanAsync(tracer, "resolverRecords.findFirst", {}, async () => { - const records = await db.query.resolverRecords.findFirst({ - where: (resolver, { and, eq }) => - and( - eq(resolver.chainId, chainId), - eq(resolver.address, resolverAddress), - eq(resolver.node, node), - ), - columns: { name: true }, - with: { addressRecords: true, textRecords: true }, - }); + const resolverId = makeResolverId(_resolver); + const resolver = await db.query.resolver.findFirst({ + where: (t, { eq }) => eq(t.id, resolverId), + }); - return records as IndexedResolverRecords | undefined; + if (!resolver) return null; + + const records = await db.query.resolverRecords.findFirst({ + where: (resolver, { and, eq }) => + and( + eq(resolver.chainId, resolver.chainId), + eq(resolver.address, resolver.address), + eq(resolver.node, node), + ), + columns: { name: true }, + with: { addressRecords: true, textRecords: true }, }); + const resolverRecords = records as IndexedResolverRecords | undefined; + if (!resolverRecords) return null; // if the resolver implements address record defaulting, materialize all selected address records // that do not yet exist - if (onchainStaticResolverImplementsDefaultAddress(chainId, resolverAddress)) { + if (resolver?.implementsAddressRecordDefaulting) { if (selection.addresses) { const defaultRecord = resolverRecords.addressRecords.find( (record) => record.coinType === DEFAULT_EVM_COIN_TYPE_BIGINT, diff --git a/apps/ensapi/src/lib/protocol-acceleration/known-ccip-read-shadow-registry-resolver.ts b/apps/ensapi/src/lib/protocol-acceleration/known-ccip-read-shadow-registry-resolver.ts deleted file mode 100644 index d375953ce..000000000 --- a/apps/ensapi/src/lib/protocol-acceleration/known-ccip-read-shadow-registry-resolver.ts +++ /dev/null @@ -1,86 +0,0 @@ -import config from "@/config"; - -import type { Address } from "viem"; - -import { type ContractConfig, type ENSNamespace, getENSNamespace } from "@ensnode/datasources"; -import { type AccountId, accountIdEqual } from "@ensnode/ensnode-sdk"; - -const namespace = getENSNamespace(config.namespace) as ENSNamespace; - -type ContractConfigWithSingleAddress = ContractConfig & { address: Address }; -const hasSingleAddress = ( - contractConfig: ContractConfig | undefined, -): contractConfig is ContractConfigWithSingleAddress => - !!contractConfig?.address && typeof contractConfig.address === "string"; - -/** - * For a given `resolver`, if it is a known CCIP-Read Shadow Registry Resolver, return the - * AccountId describing the (shadow)Registry it defers resolution to. - * - * These CCIP-Read Shadow Registry Resolvers must abide the following pattern: - * 1. They _always_ emit OffchainLookup for any resolve() call to a well-known CCIP-Read Gateway, - * 2. That CCIP-Read Gateway exclusively consults a specific (shadow)Registry in order to identify - * a name's active resolver and resolve records, and - * 3. Its behavior is unlikely to change (i.e. the contract is not upgradable or is unlikely to be - * upgraded in a way that violates principles 1. or 2.). - * - * The goal is to encode the pattern followed by projects like Basenames and Lineanames where a - * wildcard resolver is used for subnames of base.eth and that L1Resolver always returns OffchainLookup - * instructing the caller to consult a well-known CCIP-Read Gateway. This CCIP-Read Gateway then - * exclusively behaves in the following way: it identifies the name's active resolver via a well-known - * (shadow)Registry (likely on an L2), and resolves records on that active resolver. - * - * In these cases, if the Node-Resolver relationships for the (shadow)Registry in question are indexed, - * then the CCIP-Read can be short-circuited, in favor of performing an _accelerated_ Forward Resolution - * against the (shadow)Registry in question. - * - * TODO: these relationships could/should be encoded in an ENSIP, likely as a mapping from - * resolverAddress to (shadow)Registry on a specified chain. - */ -export function possibleKnownCCIPReadShadowRegistryResolverDefersTo( - resolver: AccountId, -): AccountId | null { - const { ensroot, basenames, lineanames } = namespace; - - if ( - basenames && - hasSingleAddress(basenames.contracts.Registry) && - hasSingleAddress(ensroot.contracts.BasenamesL1Resolver) - ) { - // the ENSRoot's BasenamesL1Resolver defers to the Basenames (shadow)Registry - const isBasenamesL1Resolver = accountIdEqual(resolver, { - chainId: ensroot.chain.id, - address: ensroot.contracts.BasenamesL1Resolver.address, - }); - - if (isBasenamesL1Resolver) { - return { - chainId: basenames.chain.id, - address: basenames.contracts.Registry.address, - }; - } - } - - if ( - lineanames && - hasSingleAddress(lineanames.contracts.Registry) && - hasSingleAddress(ensroot.contracts.LineanamesL1Resolver) - ) { - // the ENSRoot's LineanamesL1Resolver defers to the Lineanames (shadow)Registry - const isLineanamesL1Resolver = accountIdEqual(resolver, { - chainId: ensroot.chain.id, - address: ensroot.contracts.LineanamesL1Resolver.address, - }); - - if (isLineanamesL1Resolver) { - return { - chainId: lineanames.chain.id, - address: lineanames.contracts.Registry.address, - }; - } - } - - // TODO: ThreeDNS - - return null; -} diff --git a/apps/ensapi/src/lib/protocol-acceleration/known-onchain-static-resolver.ts b/apps/ensapi/src/lib/protocol-acceleration/known-onchain-static-resolver.ts deleted file mode 100644 index 73dbc5737..000000000 --- a/apps/ensapi/src/lib/protocol-acceleration/known-onchain-static-resolver.ts +++ /dev/null @@ -1,87 +0,0 @@ -import config from "@/config"; - -import type { Address } from "viem"; - -import { DatasourceNames, maybeGetDatasource } from "@ensnode/datasources"; -import type { ChainId } from "@ensnode/ensnode-sdk"; - -const rrRoot = maybeGetDatasource(config.namespace, DatasourceNames.ReverseResolverRoot); -const basenames = maybeGetDatasource(config.namespace, DatasourceNames.Basenames); - -/** - * Returns whether `resolverAddress` on `chainId` is a Known Onchain Static Resolver. - * - * Onchain Static Resolvers must abide the following pattern: - * 1. Onchain: all information necessary for resolution is stored on-chain, and - * 2. Static: All resolve() calls resolve to the exact value previously emitted by the Resolver in - * its events (i.e. no post-processing or other logic, a simple return of the on-chain data). - * 2.a the Resolver MAY implement address record defaulting and still be considered Static (see below). - * 3. Its behavior is unlikely to change (i.e. the contract is not upgradable or is unlikely to be - * upgraded in a way that violates principles 1. or 2.). - * - * NOTE: ContractConfig['address'] can be Address | Address[] but we know all of these are just Address - * - * TODO: these relationships could be encoded in an ENSIP - */ -export function isKnownOnchainStaticResolver(chainId: ChainId, resolverAddress: Address): boolean { - // on the ENS Deployment Chain - if (chainId === rrRoot?.chain.id) { - return ( - [ - // the Root LegacyDefaultResolver is an Onchain Static Resolver - rrRoot.contracts.DefaultPublicResolver1.address, - - // NOTE: this is _also_ the ENSIP-11 ReverseResolver (aka DefaultReverseResolver2) - rrRoot.contracts.DefaultPublicResolver2.address, - - // the ENSIP-19 default PublicResolver is an Onchain Static Resolver - rrRoot.contracts.DefaultPublicResolver3.address, - ] as Address[] - ).includes(resolverAddress); - } - - // on Base Chain - if (chainId === basenames?.chain.id) { - return ( - [ - // the Basenames Default Resolvers are Onchain Static Resolvers - "L2Resolver1" in basenames.contracts && basenames.contracts.L2Resolver1.address, - "L2Resolver2" in basenames.contracts && basenames.contracts.L2Resolver2.address, - ] as Address[] - ).includes(resolverAddress); - } - - return false; -} - -/** - * Returns whether `resolverAddress` on `chainId` implements address record defaulting. - * - * @see https://docs.ens.domains/ensip/19/#default-address - */ -export function onchainStaticResolverImplementsDefaultAddress( - chainId: ChainId, - resolverAddress: Address, -): boolean { - // on ENS Root Chain - if (chainId === rrRoot?.chain.id) { - return ( - [ - // the DefaultPublicResolver3 (ENSIP-19 default PublicResolver) implements address defaulting - rrRoot.contracts.DefaultPublicResolver3.address, - ] as Address[] - ).includes(resolverAddress); - } - - // on Base Chain - if (chainId === basenames?.chain.id) { - return ( - [ - // the Basenames L2Resolver2 implements address defaulting - "L2Resolver2" in basenames.contracts && basenames.contracts.L2Resolver2.address, - ] as Address[] - ).includes(resolverAddress); - } - - return false; -} diff --git a/apps/ensapi/src/lib/rpc/public-client.ts b/apps/ensapi/src/lib/public-client.ts similarity index 88% rename from apps/ensapi/src/lib/rpc/public-client.ts rename to apps/ensapi/src/lib/public-client.ts index bbf74618c..4fdf73d5a 100644 --- a/apps/ensapi/src/lib/rpc/public-client.ts +++ b/apps/ensapi/src/lib/public-client.ts @@ -5,7 +5,7 @@ import { createPublicClient, fallback, http, type PublicClient } from "viem"; import type { ChainId } from "@ensnode/ensnode-sdk"; export function getPublicClient(chainId: ChainId): PublicClient { - // Invariant: ENSIndexer must have an rpcConfig for the requested `chainId` + // Invariant: Must have an rpcConfig for the requested `chainId` const rpcConfig = config.rpcConfigs.get(chainId); if (!rpcConfig) { throw new Error(`Invariant: ENSIndexer does not have an RPC to chain id '${chainId}'.`); diff --git a/apps/ensapi/src/lib/resolution/forward-resolution.ts b/apps/ensapi/src/lib/resolution/forward-resolution.ts index 5bc9f3ed4..2cc8b4f8b 100644 --- a/apps/ensapi/src/lib/resolution/forward-resolution.ts +++ b/apps/ensapi/src/lib/resolution/forward-resolution.ts @@ -13,21 +13,22 @@ import { getRootRegistry, isNormalizedName, isSelectionEmpty, + makeResolverId, type Node, parseReverseName, type ResolverRecordsResponse, type ResolverRecordsSelection, TraceableENSProtocol, } from "@ensnode/ensnode-sdk"; +import { isExtendedResolver } from "@ensnode/ensnode-sdk/internal"; +import { db } from "@/lib/db"; import { makeLogger } from "@/lib/logger"; import { findResolver } from "@/lib/protocol-acceleration/find-resolver"; import { getENSIP19ReverseNameRecordFromIndex } from "@/lib/protocol-acceleration/get-primary-name-from-index"; import { getRecordsFromIndex } from "@/lib/protocol-acceleration/get-records-from-index"; -import { possibleKnownCCIPReadShadowRegistryResolverDefersTo } from "@/lib/protocol-acceleration/known-ccip-read-shadow-registry-resolver"; -import { isKnownENSIP19ReverseResolver } from "@/lib/protocol-acceleration/known-ensip-19-reverse-resolvers"; -import { isKnownOnchainStaticResolver } from "@/lib/protocol-acceleration/known-onchain-static-resolver"; import { areResolverRecordsIndexedByProtocolAccelerationPluginOnChainId } from "@/lib/protocol-acceleration/resolver-records-indexed-on-chain"; +import { getPublicClient } from "@/lib/public-client"; import { makeEmptyResolverRecordsResponse, makeRecordsResponseFromIndexedRecords, @@ -38,8 +39,6 @@ import { interpretRawCallsAndResults, makeResolveCalls, } from "@/lib/resolution/resolve-calls-and-results"; -import { supportsENSIP10Interface } from "@/lib/rpc/ensip-10"; -import { getPublicClient } from "@/lib/rpc/public-client"; import { withActiveSpanAsync, withSpanAsync } from "@/lib/tracing/auto-span"; import { addProtocolStepEvent, withProtocolStepAsync } from "@/lib/tracing/protocol-tracing"; @@ -206,150 +205,133 @@ async function _resolveForward( ////////////////////////////////////////////////// ////////////////////////////////////////////////// - // Protocol Acceleration: ENSIP-19 Reverse Resolvers - // If: - // 1) the caller requested acceleration, and - // 2) the ProtocolAcceleration plugin is active, and - // 3) the activeResolver is a Known ENSIP-19 Reverse Resolver, - // then we can just read the name record value directly from the index. + // Protocol Acceleration ////////////////////////////////////////////////// - if (accelerate) { - const activeResolverIsKnownENSIP19ReverseResolver = isKnownENSIP19ReverseResolver( - chainId, - activeResolver, - ); - - if (canAccelerate && activeResolverIsKnownENSIP19ReverseResolver) { - return withProtocolStepAsync( - TraceableENSProtocol.ForwardResolution, - ForwardResolutionProtocolStep.AccelerateENSIP19ReverseResolver, - {}, - async () => { - // Invariant: consumer must be selecting the `name` record at this point - if (selection.name !== true) { - throw new Error( - `Invariant(ENSIP-19 Reverse Resolvers Protocol Acceleration): expected 'name' record in selection but instead received: ${JSON.stringify(selection)}.`, - ); - } - - // Sanity Check: This should only happen in the context of Reverse Resolution, and - // the selection should just be `{ name: true }`, but technically not prohibited to - // select more records than just 'name', so just warn if that happens. - if (selection.addresses !== undefined || selection.texts !== undefined) { - logger.warn( - `Sanity Check(ENSIP-19 Reverse Resolvers Protocol Acceleration): expected a selection of exactly '{ name: true }' but received ${JSON.stringify(selection)}.`, - ); - } + if (accelerate && canAccelerate) { + const resolverId = makeResolverId({ chainId, address: activeResolver }); + const resolver = await db.query.resolver.findFirst({ + where: (t, { eq }) => eq(t.id, resolverId), + }); - // Invariant: the name in question should be an ENSIP-19 Reverse Name that we're able to parse - const parsed = parseReverseName(name); - if (!parsed) { - throw new Error( - `Invariant(ENSIP-19 Reverse Resolvers Protocol Acceleration): expected a valid ENSIP-19 Reverse Name but recieved '${name}'.`, + // Must have an indexed Resolver in order to accelerate further — otherwise fall back to + // Forward Resolution + if (resolver) { + ////////////////////////////////////////////////// + // Protocol Acceleration: ENSIP-19 Reverse Resolvers + // If the activeResolver is a Known ENSIP-19 Reverse Resolver, + // then we can just read the name record value directly from the index. + ////////////////////////////////////////////////// + if (resolver.isENSIP19ReverseResolver) { + return withProtocolStepAsync( + TraceableENSProtocol.ForwardResolution, + ForwardResolutionProtocolStep.AccelerateENSIP19ReverseResolver, + {}, + async () => { + // Invariant: consumer must be selecting the `name` record at this point + if (selection.name !== true) { + throw new Error( + `Invariant(ENSIP-19 Reverse Resolvers Protocol Acceleration): expected 'name' record in selection but instead received: ${JSON.stringify(selection)}.`, + ); + } + + // Sanity Check: This should only happen in the context of Reverse Resolution, and + // the selection should just be `{ name: true }`, but technically not prohibited to + // select more records than just 'name', so just warn if that happens. + if (selection.addresses !== undefined || selection.texts !== undefined) { + logger.warn( + `Sanity Check(ENSIP-19 Reverse Resolvers Protocol Acceleration): expected a selection of exactly '{ name: true }' but received ${JSON.stringify(selection)}.`, + ); + } + + // Invariant: the name in question should be an ENSIP-19 Reverse Name that we're able to parse + const parsed = parseReverseName(name); + if (!parsed) { + throw new Error( + `Invariant(ENSIP-19 Reverse Resolvers Protocol Acceleration): expected a valid ENSIP-19 Reverse Name but recieved '${name}'.`, + ); + } + + // retrieve the name record from the index + const nameRecordValue = await getENSIP19ReverseNameRecordFromIndex( + parsed.address, + parsed.coinType, ); - } - - // retrieve the name record from the index - const nameRecordValue = await getENSIP19ReverseNameRecordFromIndex( - parsed.address, - parsed.coinType, - ); - - // NOTE: typecast is ok because of sanity checks above - return { name: nameRecordValue } as ResolverRecordsResponse; - }, - ); - } - } - - ////////////////////////////////////////////////// - // Protocol Acceleration: CCIP-Read Shadow Registry Resolvers - // If: - // 1) the caller requested acceleration, and - // 2) the ProtocolAcceleration Plugin is active, and - // 3) the activeResolver is a CCIP-Read Shadow Registry Resolver, - // then we can short-circuit the CCIP-Read and defer resolution to the indicated - // (shadow)Registry. - ////////////////////////////////////////////////// - if (accelerate) { - const defersToRegistry = possibleKnownCCIPReadShadowRegistryResolverDefersTo({ - chainId, - address: activeResolver, - }); - if (canAccelerate && defersToRegistry !== null) { - return withProtocolStepAsync( + // NOTE: typecast is ok because of sanity checks above + return { name: nameRecordValue } as ResolverRecordsResponse; + }, + ); + } + + ////////////////////////////////////////////////// + // Protocol Acceleration: CCIP-Read Shadow Registry Resolvers + // If the activeResolver is a CCIP-Read Shadow Registry Resolver, + // then we can short-circuit the CCIP-Read and defer resolution to the indicated (shadow)Registry. + ////////////////////////////////////////////////// + // if (resolver.bridgesToRegistryId) { + // return withProtocolStepAsync( + // TraceableENSProtocol.ForwardResolution, + // ForwardResolutionProtocolStep.AccelerateKnownOffchainLookupResolver, + // {}, + // () => + // _resolveForward(name, selection, { + // ...options, + // registry: resolver.bridgesToRegistryId, // TODO: refactor to pass id? or fetch registry and pass (address, chainId) + // }), + // ); + // } + + addProtocolStepEvent( + protocolTracingSpan, TraceableENSProtocol.ForwardResolution, ForwardResolutionProtocolStep.AccelerateKnownOffchainLookupResolver, - {}, - () => _resolveForward(name, selection, { ...options, registry: defersToRegistry }), + false, ); - } - - addProtocolStepEvent( - protocolTracingSpan, - TraceableENSProtocol.ForwardResolution, - ForwardResolutionProtocolStep.AccelerateKnownOffchainLookupResolver, - false, - ); - } - - ////////////////////////////////////////////////// - // Protocol Acceleration: Known On-Chain Static Resolvers - // If: - // 1) the caller requested acceleration, and - // 2) the ProtocolAcceleration Plugin is active, and - // 3) the ProtocolAcceleration Plugin indexes records for all Resolver contracts on - // this chain, and - // 4) the activeResolver is a Known Onchain Static Resolver on this chain, - // then we can retrieve records directly from the database. - ////////////////////////////////////////////////// - if (accelerate) { - const resolverRecordsAreIndexed = - areResolverRecordsIndexedByProtocolAccelerationPluginOnChainId( - config.namespace, - chainId, - ); - - const activeResolverIsKnownOnchainStaticResolver = isKnownOnchainStaticResolver( - chainId, - activeResolver, - ); - if ( - canAccelerate && - resolverRecordsAreIndexed && - activeResolverIsKnownOnchainStaticResolver - ) { - return withProtocolStepAsync( + ////////////////////////////////////////////////// + // Protocol Acceleration: Known On-Chain Static Resolvers + // If: + // 1) the ProtocolAcceleration Plugin indexes records for all Resolver contracts on + // this chain, and + // 2) the activeResolver is a Static Resolver, + // then we can retrieve records directly from the database. + ////////////////////////////////////////////////// + const resolverRecordsAreIndexed = + areResolverRecordsIndexedByProtocolAccelerationPluginOnChainId( + config.namespace, + chainId, + ); + + if (resolverRecordsAreIndexed && resolver.isStatic) { + return withProtocolStepAsync( + TraceableENSProtocol.ForwardResolution, + ForwardResolutionProtocolStep.AccelerateKnownOnchainStaticResolver, + {}, + async () => { + const resolver = await getRecordsFromIndex({ + resolver: { chainId, address: activeResolver }, + node, + selection, + }); + + // if resolver doesn't exist here, there are no records in the index + if (!resolver) { + return makeEmptyResolverRecordsResponse(selection); + } + + // format into RecordsResponse and return + return makeRecordsResponseFromIndexedRecords(selection, resolver); + }, + ); + } + + addProtocolStepEvent( + protocolTracingSpan, TraceableENSProtocol.ForwardResolution, ForwardResolutionProtocolStep.AccelerateKnownOnchainStaticResolver, - {}, - async () => { - const resolver = await getRecordsFromIndex({ - chainId, - resolverAddress: activeResolver, - node, - selection, - }); - - // if resolver doesn't exist here, there are no records in the index - if (!resolver) { - return makeEmptyResolverRecordsResponse(selection); - } - - // format into RecordsResponse and return - return makeRecordsResponseFromIndexedRecords(selection, resolver); - }, + false, ); } - - addProtocolStepEvent( - protocolTracingSpan, - TraceableENSProtocol.ForwardResolution, - ForwardResolutionProtocolStep.AccelerateKnownOnchainStaticResolver, - false, - ); } ////////////////////////////////////////////////// @@ -359,28 +341,28 @@ async function _resolveForward( ////////////////////////////////////////////////// // 3.1 requireResolver() — verifies that the resolver supports ENSIP-10 if necessary - const isExtendedResolver = await withProtocolStepAsync( + const extended = await withProtocolStepAsync( TraceableENSProtocol.ForwardResolution, ForwardResolutionProtocolStep.RequireResolver, { chainId, activeResolver, requiresWildcardSupport }, async (span) => { - const isExtendedResolver = await withSpanAsync( + const extended = await withSpanAsync( tracer, - "supportsENSIP10Interface", + "isExtendedResolver", { chainId, address: activeResolver }, - () => supportsENSIP10Interface({ address: activeResolver, publicClient }), + () => isExtendedResolver({ address: activeResolver, publicClient }), ); - span.setAttribute("isExtendedResolver", isExtendedResolver); + span.setAttribute("isExtendedResolver", extended); - return isExtendedResolver; + return extended; }, ); // if we require wildcard support and this is NOT an extended resolver, the resolver is not // valid, i.e. there is no active resolver for the name // https://docs.ens.domains/ensip/10/#specification - if (requiresWildcardSupport && !isExtendedResolver) { + if (requiresWildcardSupport && !extended) { return makeEmptyResolverRecordsResponse(selection); } @@ -395,7 +377,7 @@ async function _resolveForward( resolverAddress: activeResolver, // NOTE: ENSIP-10 specifies that if a resolver supports IExtendedResolver, // the client MUST use the ENSIP-10 resolve() method over the legacy methods. - useENSIP10Resolve: isExtendedResolver, + useENSIP10Resolve: extended, calls, publicClient, }), diff --git a/apps/ensapi/src/lib/rpc/eip-165.ts b/apps/ensapi/src/lib/rpc/eip-165.ts deleted file mode 100644 index 7d4a01482..000000000 --- a/apps/ensapi/src/lib/rpc/eip-165.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { Address, Hex, PublicClient } from "viem"; - -/** - * EIP-165 ABI - * @see https://eips.ethereum.org/EIPS/eip-165 - */ -const EIP_165_ABI = [ - { - inputs: [ - { - internalType: "bytes4", - name: "interfaceID", - type: "bytes4", - }, - ], - name: "supportsInterface", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool", - }, - ], - stateMutability: "view", - type: "function", - }, -] as const; - -/** - * Determines whether a Contract at `address` supports a specific EIP-165 `interfaceId`. - */ -export async function supportsInterface({ - publicClient, - interfaceId: selector, - address, -}: { - address: Address; - interfaceId: Hex; - publicClient: PublicClient; -}) { - try { - return await publicClient.readContract({ - abi: EIP_165_ABI, - functionName: "supportsInterface", - address, - args: [selector], - }); - } catch { - // this call reverted for whatever reason — this contract does not support the interface - return false; - } -} diff --git a/apps/ensapi/src/lib/rpc/ensip-10.ts b/apps/ensapi/src/lib/rpc/ensip-10.ts deleted file mode 100644 index 79d8b6a18..000000000 --- a/apps/ensapi/src/lib/rpc/ensip-10.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { Address, PublicClient } from "viem"; - -import { supportsInterface } from "./eip-165"; - -/** - * ENSIP-10 Wildcard Resolution Interface Id - * @see https://docs.ens.domains/ensip/10 - */ -const ENSIP10_INTERFACE_ID = "0x9061b923"; - -/** - * Determines whether a Resolver contract supports ENSIP-10. - */ -export async function supportsENSIP10Interface({ - address, - publicClient, -}: { - address: Address; - publicClient: PublicClient; -}) { - return await supportsInterface({ - address, - interfaceId: ENSIP10_INTERFACE_ID, - publicClient, - }); -} diff --git a/apps/ensindexer/src/lib/protocol-acceleration/is-bridged-resolver.ts b/apps/ensindexer/src/lib/protocol-acceleration/is-bridged-resolver.ts new file mode 100644 index 000000000..ef6d4b6c8 --- /dev/null +++ b/apps/ensindexer/src/lib/protocol-acceleration/is-bridged-resolver.ts @@ -0,0 +1,57 @@ +import config from "@/config"; + +import { DatasourceNames } from "@ensnode/datasources"; +import { type AccountId, accountIdEqual } from "@ensnode/ensnode-sdk"; + +import { getDatasourceContract, maybeGetDatasourceContract } from "@/lib/datasource-helpers"; + +/** + * For a given `resolver`, if it is a known Bridged Resolver, return the + * AccountId describing the (shadow)Registry it defers resolution to. + * + * These Bridged Resolvers must abide the following pattern: + * 1. They _always_ emit OffchainLookup for any resolve() call to a well-known CCIP-Read Gateway, + * 2. That CCIP-Read Gateway exclusively consults a specific (shadow)Registry in order to identify + * a name's active resolver and resolve records, and + * 3. Its behavior is unlikely to change (i.e. the contract is not upgradable or is unlikely to be + * upgraded in a way that violates principles 1. or 2.). + * + * The goal is to encode the pattern followed by projects like Basenames and Lineanames where a + * wildcard resolver is used for subnames of base.eth and that L1Resolver always returns OffchainLookup + * instructing the caller to consult a well-known CCIP-Read Gateway. This CCIP-Read Gateway then + * exclusively behaves in the following way: it identifies the name's active resolver via a well-known + * (shadow)Registry (likely on an L2), and resolves records on that active resolver. + * + * In these cases, if the Node-Resolver relationships for the (shadow)Registry in question are indexed, + * then the CCIP-Read can be short-circuited, in favor of performing an _accelerated_ Forward Resolution + * against the (shadow)Registry in question. + * + * TODO: these relationships could/should be encoded in an ENSIP + */ +export function isBridgedResolver(resolver: AccountId): AccountId | null { + const basenamesL1Resolver = maybeGetDatasourceContract( + config.namespace, + DatasourceNames.ENSRoot, + "BasenamesL1Resolver", + ); + + // the ENSRoot's BasenamesL1Resolver bridges to the Basenames (shadow)Registry + if (basenamesL1Resolver && accountIdEqual(basenamesL1Resolver, resolver)) { + return getDatasourceContract(config.namespace, DatasourceNames.Basenames, "Registry"); + } + + const lineanamesL1Resolver = maybeGetDatasourceContract( + config.namespace, + DatasourceNames.ENSRoot, + "LineanamesL1Resolver", + ); + + // the ENSRoot's LineanamesL1Resolver bridges to the Lineanames (shadow)Registry + if (lineanamesL1Resolver && accountIdEqual(lineanamesL1Resolver, resolver)) { + return getDatasourceContract(config.namespace, DatasourceNames.Lineanames, "Registry"); + } + + // TODO: ThreeDNS + + return null; +} diff --git a/apps/ensapi/src/lib/protocol-acceleration/known-ensip-19-reverse-resolvers.ts b/apps/ensindexer/src/lib/protocol-acceleration/is-ensip-19-reverse-resolver.ts similarity index 100% rename from apps/ensapi/src/lib/protocol-acceleration/known-ensip-19-reverse-resolvers.ts rename to apps/ensindexer/src/lib/protocol-acceleration/is-ensip-19-reverse-resolver.ts diff --git a/apps/ensindexer/src/lib/protocol-acceleration/is-static-resolver.ts b/apps/ensindexer/src/lib/protocol-acceleration/is-static-resolver.ts new file mode 100644 index 000000000..ec15d59db --- /dev/null +++ b/apps/ensindexer/src/lib/protocol-acceleration/is-static-resolver.ts @@ -0,0 +1,56 @@ +import config from "@/config"; + +import { type DatasourceName, DatasourceNames } from "@ensnode/datasources"; +import { type AccountId, accountIdEqual } from "@ensnode/ensnode-sdk"; + +import { maybeGetDatasourceContract } from "@/lib/datasource-helpers"; + +const makeEq = (b: AccountId) => (datasourceName: DatasourceName, contractName: string) => { + const a = maybeGetDatasourceContract(config.namespace, datasourceName, contractName); + return a && accountIdEqual(a, b); +}; + +/** + * Returns whether `resolver` is an Static Resolver. + * + * Static Resolvers must abide the following pattern: + * 1. All information necessary for resolution is stored on-chain, and + * 2. All resolve() calls resolve to the exact value previously emitted by the Resolver in + * its events (i.e. no post-processing or other logic, a simple return of the on-chain data). + * 2.a the Resolver MAY implement address record defaulting and still be considered Static (see below). + * 3. Its behavior is unlikely to change (i.e. the contract is not upgradable or is unlikely to be + * upgraded in a way that violates principles 1. or 2.). + * + * TODO: these relationships could be encoded in an ENSIP + */ +export function isStaticResolver(resolver: AccountId): boolean { + const isResolver = makeEq(resolver); + + return [ + // ENS Root Chain + isResolver(DatasourceNames.ReverseResolverRoot, "DefaultPublicResolver1"), + isResolver(DatasourceNames.ReverseResolverRoot, "DefaultPublicResolver2"), + isResolver(DatasourceNames.ReverseResolverRoot, "DefaultPublicResolver3"), + + // Base Chain + isResolver(DatasourceNames.Basenames, "L2Resolver1"), + isResolver(DatasourceNames.Basenames, "L2Resolver2"), + ].some(Boolean); +} + +/** + * Returns whether `resolver` implements address record defaulting. + * + * @see https://docs.ens.domains/ensip/19/#default-address + */ +export function staticResolverImplementsAddressRecordDefaulting(resolver: AccountId): boolean { + const isResolver = makeEq(resolver); + + return [ + // ENS Root Chain + isResolver(DatasourceNames.ReverseResolverRoot, "DefaultPublicResolver3"), + + // Base Chain + isResolver(DatasourceNames.Basenames, "L2Resolver2"), + ].some(Boolean); +} diff --git a/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts b/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts index 4400d6a88..31d9f12e8 100644 --- a/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts +++ b/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts @@ -1,9 +1,12 @@ import type { Context } from "ponder:registry"; import schema from "ponder:schema"; -import type { Address } from "viem"; +import { type Address, isAddressEqual, zeroAddress } from "viem"; +import { ResolverABI } from "@ensnode/datasources"; import { + type AccountId, type CoinType, + makeRegistryContractId, makeResolverId, makeResolverRecordsId, type Node, @@ -13,14 +16,17 @@ import { interpretNameRecordValue, interpretTextRecordKey, interpretTextRecordValue, + isDedicatedResolver, + isExtendedResolver, } from "@ensnode/ensnode-sdk/internal"; import type { EventWithArgs } from "@/lib/ponder-helpers"; - -/** - * Infer the type of the Resolver entity's composite key. - */ -type ResolverCompositeKey = Pick; +import { isBridgedResolver } from "@/lib/protocol-acceleration/is-bridged-resolver"; +import { isKnownENSIP19ReverseResolver } from "@/lib/protocol-acceleration/is-ensip-19-reverse-resolver"; +import { + isStaticResolver, + staticResolverImplementsAddressRecordDefaulting, +} from "@/lib/protocol-acceleration/is-static-resolver"; /** * Infer the type of the ResolverRecord entity's composite key. @@ -30,41 +36,89 @@ type ResolverRecordsCompositeKey = Pick< "chainId" | "address" | "node" >; +const interpretOwner = (owner: Address) => (isAddressEqual(zeroAddress, owner) ? null : owner); + /** * Constructs a ResolverRecordsCompositeKey from a provided Resolver event. * * @returns ResolverRecordsCompositeKey */ export function makeResolverRecordsCompositeKey( - context: Context, + resolver: AccountId, event: EventWithArgs<{ node: Node }>, ): ResolverRecordsCompositeKey { return { - chainId: context.chain.id, - address: event.log.address, + ...resolver, node: event.args.node, }; } /** - * Ensures that the Resolver and ResolverRecords entities described by `id` exists. + * Ensures that the Resolver contract described by `resolver` exists, including behavioral metadata + * on initial insert. + */ +export async function ensureResolver(context: Context, resolver: AccountId) { + const resolverId = makeResolverId(resolver); + const existing = await context.db.find(schema.resolver, { id: resolverId }); + if (existing) return; + + const isExtended = await isExtendedResolver({ + address: resolver.address, + publicClient: context.client, + }); + + const isDedicated = await isDedicatedResolver({ + address: resolver.address, + publicClient: context.client, + }); + + const isENSIP19ReverseResolver = isKnownENSIP19ReverseResolver( + resolver.chainId, // TODO: pass AccountId + resolver.address, + ); + const bridgesToRegistry = isBridgedResolver(resolver); + const isStatic = isStaticResolver(resolver); + + const implementsAddressRecordDefaulting = isStatic + ? staticResolverImplementsAddressRecordDefaulting(resolver) + : null; + + let ownerId: Address | null = null; + try { + const rawOwner = await context.client.readContract({ + address: resolver.address, + abi: ResolverABI, + functionName: "owner", + }); + ownerId = interpretOwner(rawOwner); + } catch {} + + // ensure Resolver + await context.db.insert(schema.resolver).values({ + id: resolverId, + ...resolver, + ownerId, + isExtended, + isDedicated, + isStatic, + isENSIP19ReverseResolver, + implementsAddressRecordDefaulting, + bridgesToRegistryId: bridgesToRegistry ? makeRegistryContractId(bridgesToRegistry) : null, + }); +} + +/** + * Ensures that the ResolverRecords entity described by `resolverRecordsKey` exists. */ -export async function ensureResolverAndResolverRecords( +export async function ensureResolverRecords( context: Context, resolverRecordsKey: ResolverRecordsCompositeKey, ) { - const resolverKey: ResolverCompositeKey = { + const resolver: AccountId = { chainId: resolverRecordsKey.chainId, address: resolverRecordsKey.address, }; - const resolverId = makeResolverId(resolverKey); - const resolverRecordsId = makeResolverRecordsId(resolverKey, resolverRecordsKey.node); - - // ensure Resolver - await context.db - .insert(schema.resolver) - .values({ id: resolverId, ...resolverKey }) - .onConflictDoNothing(); + const resolverRecordsId = makeResolverRecordsId(resolver, resolverRecordsKey.node); // ensure ResolverRecords await context.db @@ -158,3 +212,17 @@ export async function handleResolverTextRecordUpdate( .onConflictDoUpdate({ value: interpretedValue }); } } + +/** + * Updates the resolver's `owner`, interpreting zeroAddress as null. + */ +export async function handleResolverOwnerUpdate( + context: Context, + resolver: AccountId, + owner: Address, +) { + // upsert owner, interpreting zeroAddress as null + await context.db + .update(schema.resolver, { id: makeResolverId(resolver) }) + .set({ ownerId: interpretOwner(owner) }); +} diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index 0795e6631..2f5b2f4a1 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -1,10 +1,21 @@ /** * TODO - * - mainnet sync is really really slow probably because of the getLatestRegistration - * - can we have two parallel tables, one of which always holds the `latestRegistration` which can be looked up exactly by domainId? - * - alternatively maybe the latest registration's id can always be domainId/latest and when it gets superceded by another one we can - * clone the data in /latest to /latest.index and then delete the latest and then insert a new latest with the new registration data - * - simpler than having to maintain parallel tables and sort by index always works for api layer + * - polymorphic resolver metadata + * - polymorphic resolver in graphql, add owner to DedicatedResolver schema + * + * Migration + * - individual names are migrated to v2 and can choose to move to an ENSv2 Registry on L1 or L2 + * - locked names (wrapped and not unwrappable) are 'frozen' by having their fuses burned + * - will need to observe the correct event and then override the existing domain/registratioon info + * - need to know migration status of every domain in order to to construct canonical namegraph at index-time. + * - maybe instead of constructing canonical namegraph we keep it all separate? when addressing domains by name we'd have to more or less perform traversals, including bridgedresolvers. but that's fine? we're going to have to mix forward resolution logic into the api anyway, either at the canonical namegraph construction or while traversing the namegraph + * v2 .eth registry will have a special fallback resolver that resolvers via namechain state + * - fuck me, there can be multiple registrations in v2 world. sub.example.xyz, if not emancipated, cannot be migrated, but sub.example.xyz can still be created in v2 registry in the example.xyz registry. + * - if a v2 name is registered but there's an active namewrapper registration for that same name, we should perhaps ignore all future namewrapper events, as the v2 name overrides it in resolution and the namewrapper is never more consulted for that name (and i guess any subnames under it?) + * - shadow-registering an existing name in v2 also shadows every name under it + * + * + * - for MigratedWrappedNameRegistries, need to check name expiry during resolution and avoid resolving expired names * * - ThreeDNS * - Renewals diff --git a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Resolver.ts b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Resolver.ts index 6eb6280b7..248a2c72f 100644 --- a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Resolver.ts +++ b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Resolver.ts @@ -1,37 +1,48 @@ import { ponder } from "ponder:registry"; +import schema from "ponder:schema"; -import { bigintToCoinType, type CoinType, ETH_COIN_TYPE, PluginName } from "@ensnode/ensnode-sdk"; +import { + bigintToCoinType, + type CoinType, + ETH_COIN_TYPE, + makeResolverId, + PluginName, +} from "@ensnode/ensnode-sdk"; import { parseDnsTxtRecordArgs } from "@/lib/dns-helpers"; +import { getThisAccountId } from "@/lib/get-this-account-id"; import { namespaceContract } from "@/lib/plugin-helpers"; import { - ensureResolverAndResolverRecords, + ensureResolver, + ensureResolverRecords, handleResolverAddressRecordUpdate, handleResolverNameUpdate, + handleResolverOwnerUpdate, handleResolverTextRecordUpdate, makeResolverRecordsCompositeKey, } from "@/lib/protocol-acceleration/resolver-records-db-helpers"; +const pluginName = PluginName.ProtocolAcceleration; + /** * Handlers for Resolver contracts in the Protocol Acceleration plugin. * - indexes all Resolver Records described by protocol-acceleration.schema.ts */ export default function () { - ponder.on( - namespaceContract(PluginName.ProtocolAcceleration, "Resolver:AddrChanged"), - async ({ context, event }) => { - const { a: address } = event.args; + ponder.on(namespaceContract(pluginName, "Resolver:AddrChanged"), async ({ context, event }) => { + const { a: address } = event.args; + const resolver = getThisAccountId(context, event); + await ensureResolver(context, resolver); - const resolverRecordsKey = makeResolverRecordsCompositeKey(context, event); - await ensureResolverAndResolverRecords(context, resolverRecordsKey); + const resolverRecordsKey = makeResolverRecordsCompositeKey(resolver, event); + await ensureResolverRecords(context, resolverRecordsKey); - // the Resolver#AddrChanged event is just Resolver#AddressChanged with implicit coinType of ETH - await handleResolverAddressRecordUpdate(context, resolverRecordsKey, ETH_COIN_TYPE, address); - }, - ); + // the Resolver#AddrChanged event is just Resolver#AddressChanged with implicit coinType of ETH + await handleResolverAddressRecordUpdate(context, resolverRecordsKey, ETH_COIN_TYPE, address); + }); ponder.on( - namespaceContract(PluginName.ProtocolAcceleration, "Resolver:AddressChanged"), + namespaceContract(pluginName, "Resolver:AddressChanged"), async ({ context, event }) => { const { coinType: _coinType, newAddress } = event.args; @@ -43,22 +54,27 @@ export default function () { return; // ignore if bigint can't be coerced to known CoinType } - const resolverRecordsKey = makeResolverRecordsCompositeKey(context, event); - await ensureResolverAndResolverRecords(context, resolverRecordsKey); + const resolver = getThisAccountId(context, event); + await ensureResolver(context, resolver); + + const resolverRecordsKey = makeResolverRecordsCompositeKey(resolver, event); + await ensureResolverRecords(context, resolverRecordsKey); + await handleResolverAddressRecordUpdate(context, resolverRecordsKey, coinType, newAddress); }, ); - ponder.on( - namespaceContract(PluginName.ProtocolAcceleration, "Resolver:NameChanged"), - async ({ context, event }) => { - const { name } = event.args; + ponder.on(namespaceContract(pluginName, "Resolver:NameChanged"), async ({ context, event }) => { + const { name } = event.args; - const resolverRecordsKey = makeResolverRecordsCompositeKey(context, event); - await ensureResolverAndResolverRecords(context, resolverRecordsKey); - await handleResolverNameUpdate(context, resolverRecordsKey, name); - }, - ); + const resolver = getThisAccountId(context, event); + await ensureResolver(context, resolver); + + const resolverRecordsKey = makeResolverRecordsCompositeKey(resolver, event); + await ensureResolverRecords(context, resolverRecordsKey); + + await handleResolverNameUpdate(context, resolverRecordsKey, name); + }); ponder.on( namespaceContract( @@ -82,8 +98,12 @@ export default function () { }); } catch {} // no-op if readContract throws for whatever reason - const resolverRecordsKey = makeResolverRecordsCompositeKey(context, event); - await ensureResolverAndResolverRecords(context, resolverRecordsKey); + const resolver = getThisAccountId(context, event); + await ensureResolver(context, resolver); + + const resolverRecordsKey = makeResolverRecordsCompositeKey(resolver, event); + await ensureResolverRecords(context, resolverRecordsKey); + await handleResolverTextRecordUpdate(context, resolverRecordsKey, key, value); }, ); @@ -96,8 +116,12 @@ export default function () { async ({ context, event }) => { const { key, value } = event.args; - const resolverRecordsKey = makeResolverRecordsCompositeKey(context, event); - await ensureResolverAndResolverRecords(context, resolverRecordsKey); + const resolver = getThisAccountId(context, event); + await ensureResolver(context, resolver); + + const resolverRecordsKey = makeResolverRecordsCompositeKey(resolver, event); + await ensureResolverRecords(context, resolverRecordsKey); + await handleResolverTextRecordUpdate(context, resolverRecordsKey, key, value); }, ); @@ -113,8 +137,12 @@ export default function () { const { key, value } = parseDnsTxtRecordArgs(event.args); if (key === null) return; // no key to operate over? args were malformed, ignore event - const resolverRecordsKey = makeResolverRecordsCompositeKey(context, event); - await ensureResolverAndResolverRecords(context, resolverRecordsKey); + const resolver = getThisAccountId(context, event); + await ensureResolver(context, resolver); + + const resolverRecordsKey = makeResolverRecordsCompositeKey(resolver, event); + await ensureResolverRecords(context, resolverRecordsKey); + await handleResolverTextRecordUpdate(context, resolverRecordsKey, key, value); }, ); @@ -129,21 +157,43 @@ export default function () { const { key, value } = parseDnsTxtRecordArgs(event.args); if (key === null) return; // no key to operate over? args were malformed, ignore event - const resolverRecordsKey = makeResolverRecordsCompositeKey(context, event); - await ensureResolverAndResolverRecords(context, resolverRecordsKey); + const resolver = getThisAccountId(context, event); + await ensureResolver(context, resolver); + + const resolverRecordsKey = makeResolverRecordsCompositeKey(resolver, event); + await ensureResolverRecords(context, resolverRecordsKey); + await handleResolverTextRecordUpdate(context, resolverRecordsKey, key, value); }, ); ponder.on( - namespaceContract(PluginName.ProtocolAcceleration, "Resolver:DNSRecordDeleted"), + namespaceContract(pluginName, "Resolver:DNSRecordDeleted"), async ({ context, event }) => { const { key } = parseDnsTxtRecordArgs(event.args); if (key === null) return; // no key to operate over? args were malformed, ignore event - const resolverRecordsKey = makeResolverRecordsCompositeKey(context, event); - await ensureResolverAndResolverRecords(context, resolverRecordsKey); + const resolver = getThisAccountId(context, event); + await ensureResolver(context, resolver); + + const resolverRecordsKey = makeResolverRecordsCompositeKey(resolver, event); + await ensureResolverRecords(context, resolverRecordsKey); + await handleResolverTextRecordUpdate(context, resolverRecordsKey, key, null); }, ); + + ponder.on( + namespaceContract(pluginName, "Resolver:OwnershipTransferred"), + async ({ context, event }) => { + // ignore OwnershipTransferred events that are not about Resolvers we're aware of + const resolver = getThisAccountId(context, event); + const resolverId = makeResolverId(resolver); + const existing = await context.db.find(schema.resolver, { id: resolverId }); + if (!existing) return; + + const { newOwner } = event.args; + await handleResolverOwnerUpdate(context, resolver, newOwner); + }, + ); } diff --git a/packages/datasources/src/abis/shared/AnyRegistrar.ts b/packages/datasources/src/abis/shared/AnyRegistrar.ts deleted file mode 100644 index ddc2fc383..000000000 --- a/packages/datasources/src/abis/shared/AnyRegistrar.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { mergeAbis } from "@ponder/utils"; - -import { BaseRegistrar as basenames_BaseRegistrar } from "../basenames/BaseRegistrar"; -import { BaseRegistrar as lineanames_BaseRegistrar } from "../lineanames/BaseRegistrar"; -import { BaseRegistrar as ethnames_BaseRegistrar } from "../root/BaseRegistrar"; - -export const AnyRegistrarABI = mergeAbis([ - ethnames_BaseRegistrar, - basenames_BaseRegistrar, - lineanames_BaseRegistrar, -]); diff --git a/packages/datasources/src/abis/shared/AnyRegistrarController.ts b/packages/datasources/src/abis/shared/AnyRegistrarController.ts deleted file mode 100644 index 35c5f32cb..000000000 --- a/packages/datasources/src/abis/shared/AnyRegistrarController.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { mergeAbis } from "@ponder/utils"; - -import { EarlyAccessRegistrarController } from "../basenames/EARegistrarController"; -import { RegistrarController } from "../basenames/RegistrarController"; -import { UpgradeableRegistrarController } from "../basenames/UpgradeableRegistrarController"; -import { EthRegistrarController } from "../lineanames/EthRegistrarController"; -import { LegacyEthRegistrarController } from "../root/LegacyEthRegistrarController"; -import { UnwrappedEthRegistrarController } from "../root/UnwrappedEthRegistrarController"; -import { WrappedEthRegistrarController } from "../root/WrappedEthRegistrarController"; - -export const AnyRegistrarControllerABI = mergeAbis([ - // ethnames - LegacyEthRegistrarController, - WrappedEthRegistrarController, - UnwrappedEthRegistrarController, - // basenames - EarlyAccessRegistrarController, - RegistrarController, - UpgradeableRegistrarController, - // lineanames - EthRegistrarController, -]); diff --git a/packages/datasources/src/abis/shared/Ownable.ts b/packages/datasources/src/abis/shared/Ownable.ts new file mode 100644 index 000000000..74d613369 --- /dev/null +++ b/packages/datasources/src/abis/shared/Ownable.ts @@ -0,0 +1,34 @@ +export const Ownable = [ + { + type: "event", + name: "OwnershipTransferred", + inputs: [ + { + name: "previousOwner", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "newOwner", + type: "address", + indexed: true, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "function", + name: "owner", + inputs: [], + outputs: [ + { + name: "", + type: "address", + internalType: "address", + }, + ], + stateMutability: "view", + }, +] as const; diff --git a/packages/datasources/src/index.ts b/packages/datasources/src/index.ts index 41b7d20e7..3c64db80f 100644 --- a/packages/datasources/src/index.ts +++ b/packages/datasources/src/index.ts @@ -1,9 +1,9 @@ export { EnhancedAccessControl as EnhancedAccessControlABI } from "./abis/namechain/EnhancedAccessControl"; export { Registry as RegistryABI } from "./abis/namechain/Registry"; -export { AnyRegistrarABI } from "./abis/shared/AnyRegistrar"; -export { AnyRegistrarControllerABI } from "./abis/shared/AnyRegistrarController"; export { StandaloneReverseRegistrar as StandaloneReverseRegistrarABI } from "./abis/shared/StandaloneReverseRegistrar"; export { ThreeDNSToken as ThreeDNSTokenABI } from "./abis/threedns/ThreeDNSToken"; +export { AnyRegistrarABI } from "./lib/AnyRegistrar"; +export { AnyRegistrarControllerABI } from "./lib/AnyRegistrarController"; export * from "./lib/chains"; export { ResolverABI } from "./lib/resolver"; export * from "./lib/types"; diff --git a/packages/datasources/src/lib/AnyRegistrar.ts b/packages/datasources/src/lib/AnyRegistrar.ts new file mode 100644 index 000000000..642ed3410 --- /dev/null +++ b/packages/datasources/src/lib/AnyRegistrar.ts @@ -0,0 +1,11 @@ +import { mergeAbis } from "@ponder/utils"; + +import { BaseRegistrar as basenames_BaseRegistrar } from "../abis/basenames/BaseRegistrar"; +import { BaseRegistrar as lineanames_BaseRegistrar } from "../abis/lineanames/BaseRegistrar"; +import { BaseRegistrar as ethnames_BaseRegistrar } from "../abis/root/BaseRegistrar"; + +export const AnyRegistrarABI = mergeAbis([ + ethnames_BaseRegistrar, + basenames_BaseRegistrar, + lineanames_BaseRegistrar, +]); diff --git a/packages/datasources/src/lib/AnyRegistrarController.ts b/packages/datasources/src/lib/AnyRegistrarController.ts new file mode 100644 index 000000000..3978508e0 --- /dev/null +++ b/packages/datasources/src/lib/AnyRegistrarController.ts @@ -0,0 +1,22 @@ +import { mergeAbis } from "@ponder/utils"; + +import { EarlyAccessRegistrarController } from "../abis/basenames/EARegistrarController"; +import { RegistrarController } from "../abis/basenames/RegistrarController"; +import { UpgradeableRegistrarController } from "../abis/basenames/UpgradeableRegistrarController"; +import { EthRegistrarController } from "../abis/lineanames/EthRegistrarController"; +import { LegacyEthRegistrarController } from "../abis/root/LegacyEthRegistrarController"; +import { UnwrappedEthRegistrarController } from "../abis/root/UnwrappedEthRegistrarController"; +import { WrappedEthRegistrarController } from "../abis/root/WrappedEthRegistrarController"; + +export const AnyRegistrarControllerABI = mergeAbis([ + // ethnames + LegacyEthRegistrarController, + WrappedEthRegistrarController, + UnwrappedEthRegistrarController, + // basenames + EarlyAccessRegistrarController, + RegistrarController, + UpgradeableRegistrarController, + // lineanames + EthRegistrarController, +]); diff --git a/packages/datasources/src/lib/resolver.ts b/packages/datasources/src/lib/resolver.ts index ae88ebd94..2986a925b 100644 --- a/packages/datasources/src/lib/resolver.ts +++ b/packages/datasources/src/lib/resolver.ts @@ -1,12 +1,19 @@ import { mergeAbis } from "@ponder/utils"; import { LegacyPublicResolver } from "../abis/shared/LegacyPublicResolver"; +import { Ownable } from "../abis/shared/Ownable"; import { Resolver } from "../abis/shared/Resolver"; /** - * This Resolver ABI represents the set of all well-known Resolver events/methods, including the - * the LegacyPublicResolver's TextChanged event. A Resolver contract is a contract that emits - * _any_ (not _all_) of the events specified here and may or may not support any number of the - * methods available in this ABI. + * This Resolver ABI represents the set of all well-known Resolver events/methods, including: + * - LegacyPublicResolver + * - TextChanged event without value + * - IResolver + * - modern Resolver ABI + * - Dedicated Resolver + * - Ownable + * + * A Resolver contract is a contract that emits _any_ (not _all_) of the events specified here and + * may or may not support any number of the methods available in this ABI. */ -export const ResolverABI = mergeAbis([LegacyPublicResolver, Resolver]); +export const ResolverABI = mergeAbis([LegacyPublicResolver, Resolver, Ownable]); diff --git a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts index d7d8a8189..ee54b2671 100644 --- a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts +++ b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts @@ -2,10 +2,17 @@ * Schema Definitions that power Protocol Acceleration in the Resolution API. */ -import { onchainTable, primaryKey, relations, uniqueIndex } from "ponder"; +import { onchainEnum, onchainTable, primaryKey, relations, uniqueIndex } from "ponder"; import type { Address } from "viem"; -import type { ChainId, DomainId, Node, ResolverId, ResolverRecordsId } from "@ensnode/ensnode-sdk"; +import type { + ChainId, + DomainId, + Node, + RegistryId, + ResolverId, + ResolverRecordsId, +} from "@ensnode/ensnode-sdk"; // TODO: implement resolverType & polymorphic field availability @@ -88,6 +95,43 @@ export const resolver = onchainTable( chainId: t.integer().notNull().$type(), address: t.hex().notNull().$type
(), + + /** + * A Resolver may have an `owner` via Ownable. + * + * Mainly relevant for DedicatedResolvers. + */ + ownerId: t.hex().$type
(), + + /** + * Whether the Resolver implements IExtendedResolver. + */ + isExtended: t.boolean().default(false), + + /** + * Whether the Resolver implements IDedicatedResolver. + */ + isDedicated: t.boolean().default(false), + + /** + * Whether the Resolver is an Onchain Static Resolver. + */ + isStatic: t.boolean().default(false), + + /** + * Whether the Resolver is an ENSIP19ReverseResolver. + */ + isENSIP19ReverseResolver: t.boolean().default(false), + + /** + * If dedicated or static, whether the Resolver implements Address Record Defaulting. + */ + implementsAddressRecordDefaulting: t.boolean(), + + /** + * If set, the Resolver is a Bridged Resolver that bridges to the RegistryId indicated. + */ + bridgesToRegistryId: t.text().$type(), }), (t) => ({ byId: uniqueIndex().on(t.chainId, t.address), @@ -167,6 +211,8 @@ export const resolverAddressRecord = onchainTable( chainId: t.integer().notNull().$type(), address: t.hex().notNull().$type
(), node: t.hex().notNull().$type(), + // NOTE: all well-known CoinTypes fit into javascript number but NOT postgres .integer, must be + // stored as BigInt coinType: t.bigint().notNull(), /** diff --git a/packages/ensnode-sdk/src/ens/constants.ts b/packages/ensnode-sdk/src/ens/constants.ts index 4a23d0b27..682aac5b8 100644 --- a/packages/ensnode-sdk/src/ens/constants.ts +++ b/packages/ensnode-sdk/src/ens/constants.ts @@ -1,4 +1,4 @@ -import { namehash } from "viem"; +import { namehash, zeroHash } from "viem"; import type { Node } from "./types"; @@ -7,3 +7,10 @@ export const ETH_NODE: Node = namehash("eth"); export const BASENAMES_NODE: Node = namehash("base.eth"); export const LINEANAMES_NODE: Node = namehash("linea.eth"); export const ADDR_REVERSE_NODE: Node = namehash("addr.reverse"); + +/** + * NODE_ANY is a placeholder Node used in the context of DedicatedResolvers — IResolver events are + * emitted with NODE_ANY as the `node` for which the records are issued, but the DedicatedResolver + * returns those records regardless of the name used for record resolution. + */ +export const NODE_ANY: Node = zeroHash; diff --git a/packages/ensnode-sdk/src/internal.ts b/packages/ensnode-sdk/src/internal.ts index 63e0c985b..404ae1f2d 100644 --- a/packages/ensnode-sdk/src/internal.ts +++ b/packages/ensnode-sdk/src/internal.ts @@ -14,6 +14,7 @@ export * from "./api/zod-schemas"; export * from "./ensindexer/config/zod-schemas"; +export * from "./rpc"; export * from "./shared/config/build-rpc-urls"; export * from "./shared/config/environments"; export * from "./shared/config/pretty-printing"; diff --git a/packages/ensnode-sdk/src/rpc/eip-165.ts b/packages/ensnode-sdk/src/rpc/eip-165.ts new file mode 100644 index 000000000..32b66e7ef --- /dev/null +++ b/packages/ensnode-sdk/src/rpc/eip-165.ts @@ -0,0 +1,76 @@ +import type { + Abi, + Address, + ContractFunctionArgs, + ContractFunctionName, + Hex, + ReadContractParameters, + ReadContractReturnType, +} from "viem"; + +/** + * EIP-165 ABI + * @see https://eips.ethereum.org/EIPS/eip-165 + */ +const EIP_165_ABI = [ + { + inputs: [ + { + internalType: "bytes4", + name: "interfaceID", + type: "bytes4", + }, + ], + name: "supportsInterface", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, +] as const; + +// construct a restricted publicClient type that matches both viem#PublicClient and Ponder's Context['client'] +type ReadContract = < + const abi extends Abi | readonly unknown[], + functionName extends ContractFunctionName, + const args extends ContractFunctionArgs, +>( + args: Omit, "blockNumber" | "blockTag">, +) => Promise>; + +/** + * Determines whether a Contract at `address` supports a specific EIP-165 `interfaceId`. + */ +async function supportsInterface({ + publicClient, + interfaceId: selector, + address, +}: { + address: Address; + interfaceId: Hex; + publicClient: { readContract: ReadContract }; +}) { + try { + return await publicClient.readContract({ + abi: EIP_165_ABI, + functionName: "supportsInterface", + address, + args: [selector], + }); + } catch { + // this call reverted for whatever reason — this contract does not support the interface + return false; + } +} + +export const makeSupportsInterfaceReader = + (interfaceId: Hex) => (args: Omit[0], "interfaceId">) => + supportsInterface({ + ...args, + interfaceId, + }); diff --git a/packages/ensnode-sdk/src/rpc/index.ts b/packages/ensnode-sdk/src/rpc/index.ts new file mode 100644 index 000000000..157a5f395 --- /dev/null +++ b/packages/ensnode-sdk/src/rpc/index.ts @@ -0,0 +1,3 @@ +export * from "./eip-165"; +export * from "./is-dedicated-resolver"; +export * from "./is-extended-resolver"; diff --git a/packages/ensnode-sdk/src/rpc/is-dedicated-resolver.ts b/packages/ensnode-sdk/src/rpc/is-dedicated-resolver.ts new file mode 100644 index 000000000..fe52179c5 --- /dev/null +++ b/packages/ensnode-sdk/src/rpc/is-dedicated-resolver.ts @@ -0,0 +1,12 @@ +import { makeSupportsInterfaceReader } from "./eip-165"; + +/** + * DedicatedResolver InterfaceId + * @see https://github.com/ensdomains/namechain/blob/main/contracts/src/common/resolver/interfaces/IDedicatedResolverSetters.sol#L9 + */ +const IDedicatedResolverInterfaceId = "0x92349baa"; + +/** + * Determines whether a Resolver contract supports ENSIP-10. + */ +export const isDedicatedResolver = makeSupportsInterfaceReader(IDedicatedResolverInterfaceId); diff --git a/packages/ensnode-sdk/src/rpc/is-extended-resolver.ts b/packages/ensnode-sdk/src/rpc/is-extended-resolver.ts new file mode 100644 index 000000000..40e54f54a --- /dev/null +++ b/packages/ensnode-sdk/src/rpc/is-extended-resolver.ts @@ -0,0 +1,12 @@ +import { makeSupportsInterfaceReader } from "./eip-165"; + +/** + * ENSIP-10 Wildcard Resolution Interface Id + * @see https://docs.ens.domains/ensip/10 + */ +const IExtendedResolverInterfaceId = "0x9061b923"; + +/** + * Determines whether a Resolver contract supports ENSIP-10. + */ +export const isExtendedResolver = makeSupportsInterfaceReader(IExtendedResolverInterfaceId); From e57c69c4b71ab5e3cefe5d16e9a7f6653e5379e5 Mon Sep 17 00:00:00 2001 From: shrugs Date: Tue, 18 Nov 2025 11:16:31 -0600 Subject: [PATCH 038/102] checkpoint --- apps/ensindexer/src/lib/datasource-helpers.ts | 12 ++++++- .../is-bridged-resolver.ts | 20 +++-------- .../is-ensip-19-reverse-resolver.ts | 36 +++++++++---------- .../is-static-resolver.ts | 29 +++++++-------- .../resolver-records-db-helpers.ts | 6 ++-- apps/ensindexer/src/plugins/ensv2/plugin.ts | 2 +- packages/datasources/src/ens-test-env.ts | 2 +- packages/datasources/src/index.ts | 6 ++-- .../{AnyRegistrar.ts => AnyRegistrarABI.ts} | 0 ...roller.ts => AnyRegistrarControllerABI.ts} | 0 .../src/lib/{resolver.ts => ResolverABI.ts} | 14 +++++--- packages/datasources/src/lib/chains.ts | 4 +-- packages/datasources/src/mainnet.ts | 2 +- packages/datasources/src/sepolia.ts | 2 +- .../schemas/protocol-acceleration.schema.ts | 2 ++ .../ensnode-sdk/src/api/zod-schemas.test.ts | 2 +- packages/ensnode-sdk/src/api/zod-schemas.ts | 2 +- .../ensnode-sdk/src/registrars/zod-schemas.ts | 2 +- packages/ensnode-sdk/src/rpc/eip-165.ts | 34 ++++++++---------- 19 files changed, 84 insertions(+), 93 deletions(-) rename packages/datasources/src/lib/{AnyRegistrar.ts => AnyRegistrarABI.ts} (100%) rename packages/datasources/src/lib/{AnyRegistrarController.ts => AnyRegistrarControllerABI.ts} (100%) rename packages/datasources/src/lib/{resolver.ts => ResolverABI.ts} (63%) diff --git a/apps/ensindexer/src/lib/datasource-helpers.ts b/apps/ensindexer/src/lib/datasource-helpers.ts index 4f2445b81..9f2355db1 100644 --- a/apps/ensindexer/src/lib/datasource-helpers.ts +++ b/apps/ensindexer/src/lib/datasource-helpers.ts @@ -4,7 +4,7 @@ import { type ENSNamespaceId, maybeGetDatasource, } from "@ensnode/datasources"; -import type { AccountId } from "@ensnode/ensnode-sdk"; +import { type AccountId, accountIdEqual } from "@ensnode/ensnode-sdk"; /** * Gets the AccountId for the contract in the specified namespace, datasource, and @@ -64,3 +64,13 @@ export const getDatasourceContract = ( } return contract; }; + +/** + * Makes a comparator fn for `b` against the contract described by `namespace`, `datasourceName`, and `contractName`. + */ +export const makeContractMatcher = + (namespace: ENSNamespaceId, b: AccountId) => + (datasourceName: DatasourceName, contractName: string) => { + const a = maybeGetDatasourceContract(namespace, datasourceName, contractName); + return a && accountIdEqual(a, b); + }; diff --git a/apps/ensindexer/src/lib/protocol-acceleration/is-bridged-resolver.ts b/apps/ensindexer/src/lib/protocol-acceleration/is-bridged-resolver.ts index ef6d4b6c8..167acb0be 100644 --- a/apps/ensindexer/src/lib/protocol-acceleration/is-bridged-resolver.ts +++ b/apps/ensindexer/src/lib/protocol-acceleration/is-bridged-resolver.ts @@ -1,9 +1,9 @@ import config from "@/config"; import { DatasourceNames } from "@ensnode/datasources"; -import { type AccountId, accountIdEqual } from "@ensnode/ensnode-sdk"; +import type { AccountId } from "@ensnode/ensnode-sdk"; -import { getDatasourceContract, maybeGetDatasourceContract } from "@/lib/datasource-helpers"; +import { getDatasourceContract, makeContractMatcher } from "@/lib/datasource-helpers"; /** * For a given `resolver`, if it is a known Bridged Resolver, return the @@ -29,25 +29,15 @@ import { getDatasourceContract, maybeGetDatasourceContract } from "@/lib/datasou * TODO: these relationships could/should be encoded in an ENSIP */ export function isBridgedResolver(resolver: AccountId): AccountId | null { - const basenamesL1Resolver = maybeGetDatasourceContract( - config.namespace, - DatasourceNames.ENSRoot, - "BasenamesL1Resolver", - ); + const resolverEq = makeContractMatcher(config.namespace, resolver); // the ENSRoot's BasenamesL1Resolver bridges to the Basenames (shadow)Registry - if (basenamesL1Resolver && accountIdEqual(basenamesL1Resolver, resolver)) { + if (resolverEq(DatasourceNames.ENSRoot, "BasenamesL1Resolver")) { return getDatasourceContract(config.namespace, DatasourceNames.Basenames, "Registry"); } - const lineanamesL1Resolver = maybeGetDatasourceContract( - config.namespace, - DatasourceNames.ENSRoot, - "LineanamesL1Resolver", - ); - // the ENSRoot's LineanamesL1Resolver bridges to the Lineanames (shadow)Registry - if (lineanamesL1Resolver && accountIdEqual(lineanamesL1Resolver, resolver)) { + if (resolverEq(DatasourceNames.ENSRoot, "LineanamesL1Resolver")) { return getDatasourceContract(config.namespace, DatasourceNames.Lineanames, "Registry"); } diff --git a/apps/ensindexer/src/lib/protocol-acceleration/is-ensip-19-reverse-resolver.ts b/apps/ensindexer/src/lib/protocol-acceleration/is-ensip-19-reverse-resolver.ts index 44aaa469c..d4709a756 100644 --- a/apps/ensindexer/src/lib/protocol-acceleration/is-ensip-19-reverse-resolver.ts +++ b/apps/ensindexer/src/lib/protocol-acceleration/is-ensip-19-reverse-resolver.ts @@ -1,11 +1,9 @@ import config from "@/config"; -import type { Address } from "viem"; +import { DatasourceNames } from "@ensnode/datasources"; +import type { AccountId } from "@ensnode/ensnode-sdk"; -import { DatasourceNames, maybeGetDatasource } from "@ensnode/datasources"; -import type { ChainId } from "@ensnode/ensnode-sdk"; - -const rrRoot = maybeGetDatasource(config.namespace, DatasourceNames.ReverseResolverRoot); +import { makeContractMatcher } from "@/lib/datasource-helpers"; /** * ENSIP-19 Reverse Resolvers (i.e. DefaultReverseResolver or ChainReverseResolver) simply: @@ -14,20 +12,18 @@ const rrRoot = maybeGetDatasource(config.namespace, DatasourceNames.ReverseResol * * We encode this behavior here, for the purposes of Protocol Acceleration. */ -export function isKnownENSIP19ReverseResolver(chainId: ChainId, resolverAddress: Address): boolean { - // NOTE: ENSIP-19 Reverse Resolvers are only valid in the context of the ENS Root chain - if (chainId !== rrRoot?.chain.id) return false; +export function isKnownENSIP19ReverseResolver(resolver: AccountId): boolean { + const resolverEq = makeContractMatcher(config.namespace, resolver); + + return [ + // DefaultReverseResolver (default.reverse) + resolverEq(DatasourceNames.ReverseResolverRoot, "DefaultReverseResolver3"), - return ( - [ - // DefaultReverseResolver (default.reverse) - rrRoot.contracts.DefaultReverseResolver3.address, - // the following are each ChainReverseResolver ([coinType].reverse) - rrRoot.contracts.BaseReverseResolver.address, - rrRoot.contracts.LineaReverseResolver.address, - rrRoot.contracts.OptimismReverseResolver.address, - rrRoot.contracts.ArbitrumReverseResolver.address, - rrRoot.contracts.ScrollReverseResolver.address, - ] as Address[] - ).includes(resolverAddress); + // the following are each ChainReverseResolver ([coinType].reverse) + resolverEq(DatasourceNames.ReverseResolverRoot, "BaseReverseResolver"), + resolverEq(DatasourceNames.ReverseResolverRoot, "LineaReverseResolver"), + resolverEq(DatasourceNames.ReverseResolverRoot, "OptimismReverseResolver"), + resolverEq(DatasourceNames.ReverseResolverRoot, "ArbitrumReverseResolver"), + resolverEq(DatasourceNames.ReverseResolverRoot, "ScrollReverseResolver"), + ].some(Boolean); } diff --git a/apps/ensindexer/src/lib/protocol-acceleration/is-static-resolver.ts b/apps/ensindexer/src/lib/protocol-acceleration/is-static-resolver.ts index ec15d59db..ddf2293a8 100644 --- a/apps/ensindexer/src/lib/protocol-acceleration/is-static-resolver.ts +++ b/apps/ensindexer/src/lib/protocol-acceleration/is-static-resolver.ts @@ -1,14 +1,9 @@ import config from "@/config"; -import { type DatasourceName, DatasourceNames } from "@ensnode/datasources"; -import { type AccountId, accountIdEqual } from "@ensnode/ensnode-sdk"; +import { DatasourceNames } from "@ensnode/datasources"; +import type { AccountId } from "@ensnode/ensnode-sdk"; -import { maybeGetDatasourceContract } from "@/lib/datasource-helpers"; - -const makeEq = (b: AccountId) => (datasourceName: DatasourceName, contractName: string) => { - const a = maybeGetDatasourceContract(config.namespace, datasourceName, contractName); - return a && accountIdEqual(a, b); -}; +import { makeContractMatcher } from "@/lib/datasource-helpers"; /** * Returns whether `resolver` is an Static Resolver. @@ -24,17 +19,17 @@ const makeEq = (b: AccountId) => (datasourceName: DatasourceName, contractName: * TODO: these relationships could be encoded in an ENSIP */ export function isStaticResolver(resolver: AccountId): boolean { - const isResolver = makeEq(resolver); + const resolverEq = makeContractMatcher(config.namespace, resolver); return [ // ENS Root Chain - isResolver(DatasourceNames.ReverseResolverRoot, "DefaultPublicResolver1"), - isResolver(DatasourceNames.ReverseResolverRoot, "DefaultPublicResolver2"), - isResolver(DatasourceNames.ReverseResolverRoot, "DefaultPublicResolver3"), + resolverEq(DatasourceNames.ReverseResolverRoot, "DefaultPublicResolver1"), + resolverEq(DatasourceNames.ReverseResolverRoot, "DefaultPublicResolver2"), + resolverEq(DatasourceNames.ReverseResolverRoot, "DefaultPublicResolver3"), // Base Chain - isResolver(DatasourceNames.Basenames, "L2Resolver1"), - isResolver(DatasourceNames.Basenames, "L2Resolver2"), + resolverEq(DatasourceNames.Basenames, "L2Resolver1"), + resolverEq(DatasourceNames.Basenames, "L2Resolver2"), ].some(Boolean); } @@ -44,13 +39,13 @@ export function isStaticResolver(resolver: AccountId): boolean { * @see https://docs.ens.domains/ensip/19/#default-address */ export function staticResolverImplementsAddressRecordDefaulting(resolver: AccountId): boolean { - const isResolver = makeEq(resolver); + const resolverEq = makeContractMatcher(config.namespace, resolver); return [ // ENS Root Chain - isResolver(DatasourceNames.ReverseResolverRoot, "DefaultPublicResolver3"), + resolverEq(DatasourceNames.ReverseResolverRoot, "DefaultPublicResolver3"), // Base Chain - isResolver(DatasourceNames.Basenames, "L2Resolver2"), + resolverEq(DatasourceNames.Basenames, "L2Resolver2"), ].some(Boolean); } diff --git a/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts b/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts index 31d9f12e8..8c261a072 100644 --- a/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts +++ b/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts @@ -72,10 +72,7 @@ export async function ensureResolver(context: Context, resolver: AccountId) { publicClient: context.client, }); - const isENSIP19ReverseResolver = isKnownENSIP19ReverseResolver( - resolver.chainId, // TODO: pass AccountId - resolver.address, - ); + const isENSIP19ReverseResolver = isKnownENSIP19ReverseResolver(resolver); const bridgesToRegistry = isBridgedResolver(resolver); const isStatic = isStaticResolver(resolver); @@ -83,6 +80,7 @@ export async function ensureResolver(context: Context, resolver: AccountId) { ? staticResolverImplementsAddressRecordDefaulting(resolver) : null; + // TODO: remove this in favor of EAC let ownerId: Address | null = null; try { const rawOwner = await context.client.readContract({ diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index 2f5b2f4a1..970cd6666 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -12,7 +12,7 @@ * v2 .eth registry will have a special fallback resolver that resolvers via namechain state * - fuck me, there can be multiple registrations in v2 world. sub.example.xyz, if not emancipated, cannot be migrated, but sub.example.xyz can still be created in v2 registry in the example.xyz registry. * - if a v2 name is registered but there's an active namewrapper registration for that same name, we should perhaps ignore all future namewrapper events, as the v2 name overrides it in resolution and the namewrapper is never more consulted for that name (and i guess any subnames under it?) - * - shadow-registering an existing name in v2 also shadows every name under it + * - shadow-registering an existing name in v2 also shadows every name under it so we kind of need to do a recursive deletion of all of a shadowed name's subnames, right? cause resolution terminates at the first v2-registered name. * * * - for MigratedWrappedNameRegistries, need to check name expiry during resolution and avoid resolving expired names diff --git a/packages/datasources/src/ens-test-env.ts b/packages/datasources/src/ens-test-env.ts index d937aa8aa..2753400bd 100644 --- a/packages/datasources/src/ens-test-env.ts +++ b/packages/datasources/src/ens-test-env.ts @@ -10,7 +10,7 @@ import { UnwrappedEthRegistrarController as root_UnwrappedEthRegistrarController import { WrappedEthRegistrarController as root_WrappedEthRegistrarController } from "./abis/root/WrappedEthRegistrarController"; import { ensTestEnvL1Chain, ensTestEnvL2Chain } from "./lib/chains"; // Shared ABIs -import { ResolverABI } from "./lib/resolver"; +import { ResolverABI } from "./lib/ResolverABI"; // Types import { DatasourceNames, type ENSNamespace } from "./lib/types"; diff --git a/packages/datasources/src/index.ts b/packages/datasources/src/index.ts index 3c64db80f..237a4a431 100644 --- a/packages/datasources/src/index.ts +++ b/packages/datasources/src/index.ts @@ -2,9 +2,9 @@ export { EnhancedAccessControl as EnhancedAccessControlABI } from "./abis/namech export { Registry as RegistryABI } from "./abis/namechain/Registry"; export { StandaloneReverseRegistrar as StandaloneReverseRegistrarABI } from "./abis/shared/StandaloneReverseRegistrar"; export { ThreeDNSToken as ThreeDNSTokenABI } from "./abis/threedns/ThreeDNSToken"; -export { AnyRegistrarABI } from "./lib/AnyRegistrar"; -export { AnyRegistrarControllerABI } from "./lib/AnyRegistrarController"; +export { AnyRegistrarABI } from "./lib/AnyRegistrarABI"; +export { AnyRegistrarControllerABI } from "./lib/AnyRegistrarControllerABI"; export * from "./lib/chains"; -export { ResolverABI } from "./lib/resolver"; +export { ResolverABI } from "./lib/ResolverABI"; export * from "./lib/types"; export * from "./namespaces"; diff --git a/packages/datasources/src/lib/AnyRegistrar.ts b/packages/datasources/src/lib/AnyRegistrarABI.ts similarity index 100% rename from packages/datasources/src/lib/AnyRegistrar.ts rename to packages/datasources/src/lib/AnyRegistrarABI.ts diff --git a/packages/datasources/src/lib/AnyRegistrarController.ts b/packages/datasources/src/lib/AnyRegistrarControllerABI.ts similarity index 100% rename from packages/datasources/src/lib/AnyRegistrarController.ts rename to packages/datasources/src/lib/AnyRegistrarControllerABI.ts diff --git a/packages/datasources/src/lib/resolver.ts b/packages/datasources/src/lib/ResolverABI.ts similarity index 63% rename from packages/datasources/src/lib/resolver.ts rename to packages/datasources/src/lib/ResolverABI.ts index 2986a925b..c26ffe71e 100644 --- a/packages/datasources/src/lib/resolver.ts +++ b/packages/datasources/src/lib/ResolverABI.ts @@ -1,5 +1,6 @@ import { mergeAbis } from "@ponder/utils"; +// import { EnhancedAccessControl } from "../abis/namechain/EnhancedAccessControl"; import { LegacyPublicResolver } from "../abis/shared/LegacyPublicResolver"; import { Ownable } from "../abis/shared/Ownable"; import { Resolver } from "../abis/shared/Resolver"; @@ -9,11 +10,16 @@ import { Resolver } from "../abis/shared/Resolver"; * - LegacyPublicResolver * - TextChanged event without value * - IResolver - * - modern Resolver ABI - * - Dedicated Resolver - * - Ownable + * - modern Resolver ABI, TextChanged with value + * - DedicatedResolver + * - EnhancedAccessControl * * A Resolver contract is a contract that emits _any_ (not _all_) of the events specified here and * may or may not support any number of the methods available in this ABI. */ -export const ResolverABI = mergeAbis([LegacyPublicResolver, Resolver, Ownable]); +export const ResolverABI = mergeAbis([ + LegacyPublicResolver, + Resolver, + Ownable, + // EnhancedAccessControl, // TODO: mmove this to EAC +]); diff --git a/packages/datasources/src/lib/chains.ts b/packages/datasources/src/lib/chains.ts index 0363c1be4..6199a59d0 100644 --- a/packages/datasources/src/lib/chains.ts +++ b/packages/datasources/src/lib/chains.ts @@ -17,11 +17,11 @@ export const ensTestEnvL1Chain = { id: l1ChainId, name: "ens-test-env L1", rpcUrls: { default: { http: ["http://localhost:8545"] } }, -} satisfies Chain; +} as const satisfies Chain; export const ensTestEnvL2Chain = { ...localhost, id: l2ChainId, name: "ens-test-env L2", rpcUrls: { default: { http: ["http://localhost:8546"] } }, -} satisfies Chain; +} as const satisfies Chain; diff --git a/packages/datasources/src/mainnet.ts b/packages/datasources/src/mainnet.ts index 20344719a..b724d7f42 100644 --- a/packages/datasources/src/mainnet.ts +++ b/packages/datasources/src/mainnet.ts @@ -26,7 +26,7 @@ import { Seaport as Seaport1_5 } from "./abis/seaport/Seaport1.5"; // Shared ABIs import { StandaloneReverseRegistrar } from "./abis/shared/StandaloneReverseRegistrar"; import { ThreeDNSToken } from "./abis/threedns/ThreeDNSToken"; -import { ResolverABI } from "./lib/resolver"; +import { ResolverABI } from "./lib/ResolverABI"; // Types import { DatasourceNames, type ENSNamespace } from "./lib/types"; diff --git a/packages/datasources/src/sepolia.ts b/packages/datasources/src/sepolia.ts index 70bd7f3d5..7e35188b2 100644 --- a/packages/datasources/src/sepolia.ts +++ b/packages/datasources/src/sepolia.ts @@ -32,7 +32,7 @@ import { WrappedEthRegistrarController as root_WrappedEthRegistrarController } f import { Seaport as Seaport1_5 } from "./abis/seaport/Seaport1.5"; // Shared ABIs import { StandaloneReverseRegistrar } from "./abis/shared/StandaloneReverseRegistrar"; -import { ResolverABI } from "./lib/resolver"; +import { ResolverABI } from "./lib/ResolverABI"; // Types import { DatasourceNames, type ENSNamespace } from "./lib/types"; diff --git a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts index ee54b2671..a499fc897 100644 --- a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts +++ b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts @@ -99,6 +99,8 @@ export const resolver = onchainTable( /** * A Resolver may have an `owner` via Ownable. * + * TODO: move this to EAC + * * Mainly relevant for DedicatedResolvers. */ ownerId: t.hex().$type
(), diff --git a/packages/ensnode-sdk/src/api/zod-schemas.test.ts b/packages/ensnode-sdk/src/api/zod-schemas.test.ts index 38410e4f7..5fafc3734 100644 --- a/packages/ensnode-sdk/src/api/zod-schemas.test.ts +++ b/packages/ensnode-sdk/src/api/zod-schemas.test.ts @@ -1,13 +1,13 @@ import { describe, expect, it } from "vitest"; import type { InterpretedName } from "../ens"; -import { makeRegistrarActionsResponseSchema } from "./zod-schemas"; import type { SerializedNamedRegistrarAction, SerializedRegistrarActionsResponseError, SerializedRegistrarActionsResponseOk, } from "./serialized-types"; import { RegistrarActionsResponseCodes } from "./types"; +import { makeRegistrarActionsResponseSchema } from "./zod-schemas"; describe("ENSNode API Schema", () => { describe("Registrar Actions API", () => { diff --git a/packages/ensnode-sdk/src/api/zod-schemas.ts b/packages/ensnode-sdk/src/api/zod-schemas.ts index f3e38b213..fdabc220a 100644 --- a/packages/ensnode-sdk/src/api/zod-schemas.ts +++ b/packages/ensnode-sdk/src/api/zod-schemas.ts @@ -3,8 +3,8 @@ import { z } from "zod/v4"; import type { ParsePayload } from "zod/v4/core"; import { makeRealtimeIndexingStatusProjectionSchema } from "../ensindexer/indexing-status/zod-schemas"; -import { makeReinterpretedNameSchema } from "../shared/zod-schemas"; import { makeRegistrarActionSchema } from "../registrars/zod-schemas"; +import { makeReinterpretedNameSchema } from "../shared/zod-schemas"; import { type IndexingStatusResponse, IndexingStatusResponseCodes, diff --git a/packages/ensnode-sdk/src/registrars/zod-schemas.ts b/packages/ensnode-sdk/src/registrars/zod-schemas.ts index c370208b5..5fa988d7e 100644 --- a/packages/ensnode-sdk/src/registrars/zod-schemas.ts +++ b/packages/ensnode-sdk/src/registrars/zod-schemas.ts @@ -3,6 +3,7 @@ import type { Address } from "viem"; import { z } from "zod/v4"; import type { ParsePayload } from "zod/v4/core"; +import { addPrices, isPriceEqual } from "../shared"; import { makeBlockRefSchema, makeDurationSchema, @@ -14,7 +15,6 @@ import { makeTransactionHashSchema, makeUnixTimestampSchema, } from "../shared/zod-schemas"; -import { addPrices, isPriceEqual } from "../shared"; import { type RegistrarAction, type RegistrarActionEventId, diff --git a/packages/ensnode-sdk/src/rpc/eip-165.ts b/packages/ensnode-sdk/src/rpc/eip-165.ts index 32b66e7ef..1b9142c87 100644 --- a/packages/ensnode-sdk/src/rpc/eip-165.ts +++ b/packages/ensnode-sdk/src/rpc/eip-165.ts @@ -1,12 +1,4 @@ -import type { - Abi, - Address, - ContractFunctionArgs, - ContractFunctionName, - Hex, - ReadContractParameters, - ReadContractReturnType, -} from "viem"; +import type { Address, Hex } from "viem"; /** * EIP-165 ABI @@ -34,26 +26,28 @@ const EIP_165_ABI = [ }, ] as const; -// construct a restricted publicClient type that matches both viem#PublicClient and Ponder's Context['client'] -type ReadContract = < - const abi extends Abi | readonly unknown[], - functionName extends ContractFunctionName, - const args extends ContractFunctionArgs, ->( - args: Omit, "blockNumber" | "blockTag">, -) => Promise>; - /** * Determines whether a Contract at `address` supports a specific EIP-165 `interfaceId`. + * + * Accepts both viem PublicClient and Ponder Context client types. */ -async function supportsInterface({ +async function supportsInterface< + TClient extends { + readContract: (params: { + abi: typeof EIP_165_ABI; + functionName: "supportsInterface"; + address: Address; + args: readonly [Hex]; + }) => Promise; + }, +>({ publicClient, interfaceId: selector, address, }: { address: Address; interfaceId: Hex; - publicClient: { readContract: ReadContract }; + publicClient: TClient; }) { try { return await publicClient.readContract({ From a1ff54f65e2fcc472cb99f9d76be8fc87f3542e9 Mon Sep 17 00:00:00 2001 From: shrugs Date: Tue, 18 Nov 2025 11:50:54 -0600 Subject: [PATCH 039/102] dedicated and bridged resolver extensions --- .../graphql-api/schema/resolver-records.ts | 65 ++++++++++ .../ensapi/src/graphql-api/schema/resolver.ts | 114 ++++++++++-------- .../src/lib/resolution/forward-resolution.ts | 32 +++-- .../resolver-records-db-helpers.ts | 3 +- apps/ensindexer/src/plugins/ensv2/plugin.ts | 2 +- .../schemas/protocol-acceleration.schema.ts | 5 +- 6 files changed, 155 insertions(+), 66 deletions(-) create mode 100644 apps/ensapi/src/graphql-api/schema/resolver-records.ts diff --git a/apps/ensapi/src/graphql-api/schema/resolver-records.ts b/apps/ensapi/src/graphql-api/schema/resolver-records.ts new file mode 100644 index 000000000..cbebd0aad --- /dev/null +++ b/apps/ensapi/src/graphql-api/schema/resolver-records.ts @@ -0,0 +1,65 @@ +import { bigintToCoinType, type ResolverRecordsId } from "@ensnode/ensnode-sdk"; + +import { builder } from "@/graphql-api/builder"; +import { getModelId } from "@/graphql-api/lib/get-id"; +import { db } from "@/lib/db"; + +export const ResolverRecordsRef = builder.loadableObjectRef("ResolverRecords", { + load: (ids: ResolverRecordsId[]) => + db.query.resolverRecords.findMany({ + where: (t, { inArray }) => inArray(t.id, ids), + with: { textRecords: true, addressRecords: true }, + }), + toKey: getModelId, + cacheResolved: true, + sort: true, +}); + +export type ResolverRecords = Exclude; + +ResolverRecordsRef.implement({ + description: "TODO", + fields: (t) => ({ + ////////////////////// + // ResolverRecords.id + ////////////////////// + id: t.expose("id", { + description: "TODO", + type: "ID", + nullable: false, + }), + + //////////////////////// + // ResolverRecords.name + //////////////////////// + name: t.expose("name", { + description: "TODO", + type: "String", + nullable: true, + }), + + //////////////////////// + // ResolverRecords.keys + //////////////////////// + keys: t.field({ + description: "TODO", + type: ["String"], + nullable: false, + resolve: (parent) => parent.textRecords.map((r) => r.key).toSorted(), + }), + + ///////////////////////////// + // ResolverRecords.coinTypes + ///////////////////////////// + coinTypes: t.field({ + description: "TODO", + type: ["CoinType"], + nullable: false, + resolve: (parent) => + parent.addressRecords + .map((r) => r.coinType) + .map(bigintToCoinType) + .toSorted(), + }), + }), +}); diff --git a/apps/ensapi/src/graphql-api/schema/resolver.ts b/apps/ensapi/src/graphql-api/schema/resolver.ts index 403c051bf..be52b2e46 100644 --- a/apps/ensapi/src/graphql-api/schema/resolver.ts +++ b/apps/ensapi/src/graphql-api/schema/resolver.ts @@ -1,18 +1,26 @@ import { namehash } from "viem"; import { - bigintToCoinType, makeResolverRecordsId, + NODE_ANY, + type RequiredAndNotNull, type ResolverId, - type ResolverRecordsId, } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-id"; +import { AccountRef } from "@/graphql-api/schema/account"; import { AccountIdInput, AccountIdRef } from "@/graphql-api/schema/account-id"; import { NameOrNodeInput } from "@/graphql-api/schema/name-or-node"; +import { ResolverRecordsRef } from "@/graphql-api/schema/resolver-records"; import { db } from "@/lib/db"; +const isDedicatedResolver = (resolver: Resolver): resolver is DedicatedResolver => + resolver.isDedicated === true; + +const isBridgedResolver = (resolver: Resolver): resolver is BridgedResolver => + resolver.bridgesToRegistryChainId !== null && resolver.bridgesToRegistryAddress !== null; + export const ResolverRef = builder.loadableObjectRef("Resolver", { load: (ids: ResolverId[]) => db.query.resolver.findMany({ @@ -24,18 +32,15 @@ export const ResolverRef = builder.loadableObjectRef("Resolver", { }); export type Resolver = Exclude; +export type DedicatedResolver = Omit & { isDedicated: true }; +export type BridgedResolver = RequiredAndNotNull< + Resolver, + "bridgesToRegistryChainId" | "bridgesToRegistryAddress" +>; -export const ResolverRecordsRef = builder.loadableObjectRef("ResolverRecords", { - load: (ids: ResolverRecordsId[]) => - db.query.resolverRecords.findMany({ - where: (t, { inArray }) => inArray(t.id, ids), - with: { textRecords: true, addressRecords: true }, - }), - toKey: getModelId, - cacheResolved: true, - sort: true, -}); - +//////////// +// Resolver +//////////// ResolverRef.implement({ description: "A Resolver Contract", fields: (t) => ({ @@ -58,11 +63,6 @@ ResolverRef.implement({ resolve: ({ chainId, address }) => ({ chainId, address }), }), - //////////////////// - // Resolver.records - //////////////////// - // TODO: connection to all ResolverRecords by (address, chainId)? - //////////////////////////////////// // Resolver.records by Name or Node //////////////////////////////////// @@ -76,58 +76,72 @@ ResolverRef.implement({ return makeResolverRecordsId({ chainId, address }, node); }, }), - }), -}); -export type ResolverRecords = Exclude; - -ResolverRecordsRef.implement({ - description: "TODO", - fields: (t) => ({ ////////////////////// - // ResolverRecords.id + // Resolver.dedicated ////////////////////// - id: t.expose("id", { + dedicated: t.field({ description: "TODO", - type: "ID", - nullable: false, + type: DedicatedResolverMetadataRef, + nullable: true, + resolve: (parent) => (isDedicatedResolver(parent) ? parent : null), }), - //////////////////////// - // ResolverRecords.name - //////////////////////// - name: t.expose("name", { + //////////////////// + // Resolver.bridged + //////////////////// + bridged: t.field({ description: "TODO", - type: "String", + type: AccountIdRef, nullable: true, + resolve: (parent) => { + if (!isBridgedResolver(parent)) return null; + return { + chainId: parent.bridgesToRegistryChainId, + address: parent.bridgesToRegistryAddress, + }; + }, }), + }), +}); - //////////////////////// - // ResolverRecords.keys - //////////////////////// - keys: t.field({ +///////////////////////////// +// DedicatedResolverMetadata +///////////////////////////// +export const DedicatedResolverMetadataRef = builder.objectRef( + "DedicatedResolverMetadataRef", +); +DedicatedResolverMetadataRef.implement({ + description: "TODO", + fields: (t) => ({ + /////////////////////////// + // DedicatedResolver.owner + /////////////////////////// + owner: t.field({ description: "TODO", - type: ["String"], - nullable: false, - resolve: (parent) => parent.textRecords.map((r) => r.key).toSorted(), + type: AccountRef, + nullable: true, + // TODO: resolve via EAC + resolve: (parent) => parent.ownerId, }), ///////////////////////////// - // ResolverRecords.coinTypes + // Resolver.dedicatedRecords ///////////////////////////// - coinTypes: t.field({ + records: t.field({ description: "TODO", - type: ["CoinType"], - nullable: false, - resolve: (parent) => - parent.addressRecords - .map((r) => r.coinType) - .map(bigintToCoinType) - .toSorted(), + type: ResolverRecordsRef, + nullable: true, + resolve: ({ chainId, address }, args) => + makeResolverRecordsId({ chainId, address }, NODE_ANY), }), }), }); +///////////////////// +// Inputs +///////////////////// + export const ResolverIdInput = builder.inputType("ResolverIdInput", { description: "TODO", isOneOf: true, diff --git a/apps/ensapi/src/lib/resolution/forward-resolution.ts b/apps/ensapi/src/lib/resolution/forward-resolution.ts index 2cc8b4f8b..fb8421efa 100644 --- a/apps/ensapi/src/lib/resolution/forward-resolution.ts +++ b/apps/ensapi/src/lib/resolution/forward-resolution.ts @@ -268,18 +268,26 @@ async function _resolveForward( // If the activeResolver is a CCIP-Read Shadow Registry Resolver, // then we can short-circuit the CCIP-Read and defer resolution to the indicated (shadow)Registry. ////////////////////////////////////////////////// - // if (resolver.bridgesToRegistryId) { - // return withProtocolStepAsync( - // TraceableENSProtocol.ForwardResolution, - // ForwardResolutionProtocolStep.AccelerateKnownOffchainLookupResolver, - // {}, - // () => - // _resolveForward(name, selection, { - // ...options, - // registry: resolver.bridgesToRegistryId, // TODO: refactor to pass id? or fetch registry and pass (address, chainId) - // }), - // ); - // } + if ( + resolver.bridgesToRegistryChainId !== null && + resolver.bridgesToRegistryAddress !== null + ) { + return withProtocolStepAsync( + TraceableENSProtocol.ForwardResolution, + ForwardResolutionProtocolStep.AccelerateKnownOffchainLookupResolver, + {}, + () => + _resolveForward(name, selection, { + ...options, + registry: { + // biome-ignore lint/style/noNonNullAssertion: null check above + chainId: resolver.bridgesToRegistryChainId!, + // biome-ignore lint/style/noNonNullAssertion: null check above + address: resolver.bridgesToRegistryAddress!, + }, + }), + ); + } addProtocolStepEvent( protocolTracingSpan, diff --git a/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts b/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts index 8c261a072..0334c41b3 100644 --- a/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts +++ b/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts @@ -101,7 +101,8 @@ export async function ensureResolver(context: Context, resolver: AccountId) { isStatic, isENSIP19ReverseResolver, implementsAddressRecordDefaulting, - bridgesToRegistryId: bridgesToRegistry ? makeRegistryContractId(bridgesToRegistry) : null, + bridgesToRegistryChainId: bridgesToRegistry?.chainId ?? null, + bridgesToRegistryAddress: bridgesToRegistry?.address ?? null, }); } diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index 970cd6666..cfaf4c9c3 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -1,6 +1,6 @@ /** * TODO - * - polymorphic resolver metadata + * - polymorphic resolver metadata in drizzle schema * - polymorphic resolver in graphql, add owner to DedicatedResolver schema * * Migration diff --git a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts index a499fc897..a93bdf529 100644 --- a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts +++ b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts @@ -131,9 +131,10 @@ export const resolver = onchainTable( implementsAddressRecordDefaulting: t.boolean(), /** - * If set, the Resolver is a Bridged Resolver that bridges to the RegistryId indicated. + * If set, the Resolver is a Bridged Resolver that bridges to the AccountId indicated. */ - bridgesToRegistryId: t.text().$type(), + bridgesToRegistryChainId: t.text().$type(), + bridgesToRegistryAddress: t.hex().$type
(), }), (t) => ({ byId: uniqueIndex().on(t.chainId, t.address), From fc13bb72681ef152712e6250fd4afdd43cfa7f2c Mon Sep 17 00:00:00 2001 From: shrugs Date: Tue, 18 Nov 2025 19:12:59 -0600 Subject: [PATCH 040/102] checkpoint: cursors and connections --- apps/ensapi/package.json | 1 + apps/ensapi/src/graphql-api/builder.ts | 8 +- apps/ensapi/src/graphql-api/schema/account.ts | 77 +++++++--- .../src/graphql-api/schema/constants.ts | 7 + apps/ensapi/src/graphql-api/schema/cursors.ts | 4 + apps/ensapi/src/graphql-api/schema/domain.ts | 7 + .../src/graphql-api/schema/permissions.ts | 140 +++++++++++++++++- apps/ensapi/src/graphql-api/schema/query.ts | 58 ++++++-- .../ensapi/src/graphql-api/schema/registry.ts | 33 +++-- apps/ensapi/src/lib/handlers/drizzle.ts | 20 ++- .../plugins/ensv2/handlers/ENSv1Registry.ts | 2 +- apps/ensindexer/src/plugins/ensv2/plugin.ts | 34 ++--- pnpm-lock.yaml | 40 +++++ 13 files changed, 362 insertions(+), 69 deletions(-) create mode 100644 apps/ensapi/src/graphql-api/schema/constants.ts create mode 100644 apps/ensapi/src/graphql-api/schema/cursors.ts diff --git a/apps/ensapi/package.json b/apps/ensapi/package.json index 27ea0b5e5..18e5386b6 100644 --- a/apps/ensapi/package.json +++ b/apps/ensapi/package.json @@ -41,6 +41,7 @@ "@ponder/client": "^0.14.13", "@pothos/core": "^4.10.0", "@pothos/plugin-dataloader": "^4.4.3", + "@pothos/plugin-relay": "^4.6.2", "dataloader": "^2.2.3", "date-fns": "catalog:", "drizzle-orm": "catalog:", diff --git a/apps/ensapi/src/graphql-api/builder.ts b/apps/ensapi/src/graphql-api/builder.ts index 102396047..f66906c1e 100644 --- a/apps/ensapi/src/graphql-api/builder.ts +++ b/apps/ensapi/src/graphql-api/builder.ts @@ -1,5 +1,6 @@ import SchemaBuilder from "@pothos/core"; import DataloaderPlugin from "@pothos/plugin-dataloader"; +import RelayPlugin from "@pothos/plugin-relay"; import type { Address, Hex } from "viem"; import type { @@ -29,5 +30,10 @@ export const builder = new SchemaBuilder<{ // PermissionsId: { Input: PermissionsId; Output: PermissionsId }; }; }>({ - plugins: [DataloaderPlugin], + plugins: [DataloaderPlugin, RelayPlugin], + relay: { + // disable the Query.node & Query.nodes methods + nodeQueryOptions: false, + nodesQueryOptions: false, + }, }); diff --git a/apps/ensapi/src/graphql-api/schema/account.ts b/apps/ensapi/src/graphql-api/schema/account.ts index 6675bba8b..600e18467 100644 --- a/apps/ensapi/src/graphql-api/schema/account.ts +++ b/apps/ensapi/src/graphql-api/schema/account.ts @@ -1,7 +1,12 @@ +import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay"; import type { Address } from "viem"; +import type { DomainId, ResolverId } from "@ensnode/ensnode-sdk"; + import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-id"; +import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; +import { cursors } from "@/graphql-api/schema/cursors"; import { type Domain, DomainRef } from "@/graphql-api/schema/domain"; import { type Resolver, ResolverRef } from "@/graphql-api/schema/resolver"; import { db } from "@/lib/db"; @@ -21,9 +26,9 @@ export type Account = Exclude; AccountRef.implement({ description: "TODO", fields: (t) => ({ - /////////////////// + ////////////// // Account.id - /////////////////// + ////////////// id: t.expose("id", { description: "TODO", type: "Address", @@ -43,37 +48,61 @@ AccountRef.implement({ /////////////////// // Account.domains /////////////////// - domains: t.loadableGroup({ + domains: t.connection({ description: "TODO", type: DomainRef, - load: (ids: Address[]) => - db.query.domain.findMany({ - where: (t, { inArray }) => inArray(t.ownerId, ids), - with: { label: true }, - }), - // biome-ignore lint/style/noNonNullAssertion: guaranteed due to inArray - group: (domain) => (domain as Domain).ownerId!, - resolve: getModelId, + resolve: (parent, args) => + // TODO(dataloader) — confirm this is dataloaded? + resolveCursorConnection( + { ...DEFAULT_CONNECTION_ARGS, args }, + ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => + db.query.domain.findMany({ + where: (t, { lt, gt, and, eq }) => + and( + ...[ + eq(t.ownerId, parent.id), + before !== undefined && lt(t.id, cursors.decode(before)), + after !== undefined && gt(t.id, cursors.decode(after)), + ].filter((c) => !!c), + ), + orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), + limit, + with: { label: true }, + }), + ), }), + ////////////////////// + // Account.registries + ////////////////////// + // TODO: account's registries via EAC + // similar logic for dedicatedResolvers + ////////////////////////////// // Account.dedicatedResolvers ////////////////////////////// - dedicatedResolvers: t.loadableGroup({ + dedicatedResolvers: t.connection({ description: "TODO", - // TODO: resolver polymorphism, return DedicatedResolverRef type: ResolverRef, - load: (ids: Address[]) => - db.query.resolver.findMany({ - where: (t, { inArray, and, eq }) => - and( - inArray(t.ownerId, ids), // owned by id - eq(t.isDedicated, true), // must be dedicated resolver - ), - }), - // biome-ignore lint/style/noNonNullAssertion: guaranteed due to inArray - group: (resolver) => (resolver as Resolver).ownerId!, - resolve: getModelId, + resolve: (parent, args) => + // TODO(dataloader) — confirm this is dataloaded? + // TODO(EAC) — migrate to Permissions lookup + resolveCursorConnection( + { ...DEFAULT_CONNECTION_ARGS, args }, + ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => + db.query.resolver.findMany({ + where: (t, { lt, gt, and, eq }) => + and( + ...[ + eq(t.ownerId, parent.id), + before !== undefined && lt(t.id, cursors.decode(before)), + after !== undefined && gt(t.id, cursors.decode(after)), + ].filter((c) => !!c), + ), + orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), + limit, + }), + ), }), }), }); diff --git a/apps/ensapi/src/graphql-api/schema/constants.ts b/apps/ensapi/src/graphql-api/schema/constants.ts new file mode 100644 index 000000000..31682b793 --- /dev/null +++ b/apps/ensapi/src/graphql-api/schema/constants.ts @@ -0,0 +1,7 @@ +import { cursors } from "@/graphql-api/schema/cursors"; + +export const DEFAULT_CONNECTION_ARGS = { + toCursor: (model: T) => cursors.encode(model.id), + defaultSize: 100, + maxSize: 1000, +} as const; diff --git a/apps/ensapi/src/graphql-api/schema/cursors.ts b/apps/ensapi/src/graphql-api/schema/cursors.ts new file mode 100644 index 000000000..2e38379b5 --- /dev/null +++ b/apps/ensapi/src/graphql-api/schema/cursors.ts @@ -0,0 +1,4 @@ +export const cursors = { + encode: (id: string) => Buffer.from(id, "utf8").toString("base64"), + decode: (cursor: string) => Buffer.from(cursor, "base64").toString("utf8") as T, +}; diff --git a/apps/ensapi/src/graphql-api/schema/domain.ts b/apps/ensapi/src/graphql-api/schema/domain.ts index 5d30e53bd..a24940118 100644 --- a/apps/ensapi/src/graphql-api/schema/domain.ts +++ b/apps/ensapi/src/graphql-api/schema/domain.ts @@ -71,6 +71,13 @@ DomainRef.implement({ //////////////////// // Domain.canonical //////////////////// + // canonical: t.loadable({ + // description: "TODO", + // type: "Name", + // nullable: true, + // load: (ids: DomainId[], context) => context.loadPosts(ids), + // resolve: (user, args) => user.lastPostID, + // }), canonical: t.field({ description: "TODO", type: "Name", diff --git a/apps/ensapi/src/graphql-api/schema/permissions.ts b/apps/ensapi/src/graphql-api/schema/permissions.ts index 2aa5298e7..bddf16144 100644 --- a/apps/ensapi/src/graphql-api/schema/permissions.ts +++ b/apps/ensapi/src/graphql-api/schema/permissions.ts @@ -1,25 +1,58 @@ -import type { PermissionsId } from "@ensnode/ensnode-sdk"; +import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay"; + +import type { PermissionsId, PermissionsResourceId, PermissionsUserId } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-id"; +import { AccountRef } from "@/graphql-api/schema/account"; import { AccountIdRef } from "@/graphql-api/schema/account-id"; +import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; +import { cursors } from "@/graphql-api/schema/cursors"; import { db } from "@/lib/db"; export const PermissionsRef = builder.loadableObjectRef("Permissions", { load: (ids: PermissionsId[]) => - db.query.permissions.findMany({ - where: (t, { inArray }) => inArray(t.id, ids), - }), + db.query.permissions.findMany({ where: (t, { inArray }) => inArray(t.id, ids) }), + toKey: getModelId, + cacheResolved: true, + sort: true, +}); + +export const PermissionsResourceRef = builder.loadableObjectRef("PermissionsResource", { + load: (ids: PermissionsResourceId[]) => + db.query.permissionsResource.findMany({ where: (t, { inArray }) => inArray(t.id, ids) }), + toKey: getModelId, + cacheResolved: true, + sort: true, +}); + +export const PermissionsUserRef = builder.loadableObjectRef("PermissionsUser", { + load: (ids: PermissionsUserId[]) => + db.query.permissionsUser.findMany({ where: (t, { inArray }) => inArray(t.id, ids) }), toKey: getModelId, cacheResolved: true, sort: true, }); export type Permissions = Exclude; +export type PermissionsResource = Exclude< + typeof PermissionsResourceRef.$inferType, + PermissionsResourceId +>; +export type PermissionsUserResource = Exclude< + typeof PermissionsUserRef.$inferType, + PermissionsUserId +>; +/////////////// +// Permissions +/////////////// PermissionsRef.implement({ description: "Permissions", fields: (t) => ({ + //////////////////////// + // Permissions.contract + //////////////////////// contract: t.field({ type: AccountIdRef, description: "TODO", @@ -27,6 +60,103 @@ PermissionsRef.implement({ resolve: ({ chainId, address }) => ({ chainId, address }), }), - // resources... + ///////////////////////// + // Permissions.resources + ///////////////////////// + resources: t.connection({ + description: "TODO", + type: PermissionsResourceRef, + resolve: (parent, args, context) => + resolveCursorConnection( + { ...DEFAULT_CONNECTION_ARGS, args }, + ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => + db.query.permissionsResource.findMany({ + where: (t, { lt, gt, eq, and }) => + and( + ...[ + eq(t.chainId, parent.chainId), + eq(t.address, parent.address), + before !== undefined && lt(t.id, cursors.decode(before)), + after !== undefined && gt(t.id, cursors.decode(after)), + ].filter((c) => !!c), + ), + orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), + limit, + }), + ), + }), + }), +}); + +/////////////////////// +// PermissionsResource +/////////////////////// +PermissionsResourceRef.implement({ + description: "PermissionsResource", + fields: (t) => ({ + //////////////////////////////// + // PermissionsResource.resource + //////////////////////////////// + resource: t.field({ + description: "TODO", + type: "BigInt", + nullable: false, + resolve: (parent) => parent.resource, + }), + + ///////////////////////////// + // PermissionsResource.users + ///////////////////////////// + users: t.connection({ + description: "TODO", + type: PermissionsUserRef, + resolve: (parent, args, context) => + resolveCursorConnection( + { ...DEFAULT_CONNECTION_ARGS, args }, + ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => + db.query.permissionsUser.findMany({ + where: (t, { lt, gt, eq, and }) => + and( + ...[ + eq(t.chainId, parent.chainId), + eq(t.address, parent.address), + eq(t.resource, parent.resource), + before !== undefined && lt(t.id, cursors.decode(before)), + after !== undefined && gt(t.id, cursors.decode(after)), + ].filter((c) => !!c), + ), + orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), + limit, + }), + ), + }), + }), +}); + +/////////////////// +// PermissionsUser +/////////////////// +PermissionsUserRef.implement({ + description: "PermissionsUser", + fields: (t) => ({ + //////////////////////// + // PermissionsUser.user + //////////////////////// + user: t.field({ + description: "TODO", + type: AccountRef, + nullable: false, + resolve: (parent) => parent.user, + }), + + ///////////////////////// + // PermissionsUser.roles + ///////////////////////// + roles: t.field({ + description: "TODO", + type: "BigInt", + nullable: false, + resolve: (parent) => parent.roles, + }), }), }); diff --git a/apps/ensapi/src/graphql-api/schema/query.ts b/apps/ensapi/src/graphql-api/schema/query.ts index 7e4146b17..6f4264011 100644 --- a/apps/ensapi/src/graphql-api/schema/query.ts +++ b/apps/ensapi/src/graphql-api/schema/query.ts @@ -1,26 +1,54 @@ import config from "@/config"; -import { getRootRegistryId, makeRegistryContractId, makeResolverId } from "@ensnode/ensnode-sdk"; +import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay"; + +import { + type DomainId, + getRootRegistryId, + makePermissionsId, + makeRegistryContractId, + makeResolverId, +} from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import { getDomainIdByInterpretedName } from "@/graphql-api/lib/get-domain-by-fqdn"; import { AccountRef } from "@/graphql-api/schema/account"; +import { AccountIdInput } from "@/graphql-api/schema/account-id"; +import { cursors } from "@/graphql-api/schema/cursors"; import { DomainIdInput, DomainRef } from "@/graphql-api/schema/domain"; +import { PermissionsRef } from "@/graphql-api/schema/permissions"; import { RegistryIdInput, RegistryInterfaceRef } from "@/graphql-api/schema/registry"; import { ResolverIdInput, ResolverRef } from "@/graphql-api/schema/resolver"; import { db } from "@/lib/db"; -// TODO: maybe should still implement query/return by id, exposing the db's primary key? -// maybe necessary for connections pattern... -// if leaning into opaque ids, then probably prefer that, and avoid exposing semantic searches? unclear - builder.queryType({ fields: (t) => ({ - domains: t.field({ - description: "DELETE ME", - type: [DomainRef], - nullable: false, - resolve: () => db.query.domain.findMany({ with: { label: true } }), + // testing, delete this + domains: t.connection({ + description: "TODO", + type: DomainRef, + resolve: (parent, args, context) => + resolveCursorConnection( + { + args, + toCursor: (domain) => cursors.encode(domain.id), + defaultSize: 100, + maxSize: 1000, + }, + ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => + db.query.domain.findMany({ + where: (t, { lt, gt, and }) => + and( + ...[ + before !== undefined && lt(t.id, cursors.decode(before)), + after !== undefined && gt(t.id, cursors.decode(after)), + ].filter((c) => !!c), + ), + orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), + limit, + with: { label: true }, + }), + ), }), ////////////////////////////////// @@ -74,6 +102,16 @@ builder.queryType({ }, }), + /////////////////////////////// + // Get Permissions by Contract + /////////////////////////////// + permissions: t.field({ + description: "TODO", + type: PermissionsRef, + args: { for: t.arg({ type: AccountIdInput, required: true }) }, + resolve: (parent, args, ctx, info) => makePermissionsId(args.for), + }), + ///////////////////// // Get Root Registry ///////////////////// diff --git a/apps/ensapi/src/graphql-api/schema/registry.ts b/apps/ensapi/src/graphql-api/schema/registry.ts index 7158976b7..462e04140 100644 --- a/apps/ensapi/src/graphql-api/schema/registry.ts +++ b/apps/ensapi/src/graphql-api/schema/registry.ts @@ -1,8 +1,12 @@ -import type { RegistryId, RequiredAndNotNull } from "@ensnode/ensnode-sdk"; +import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay"; + +import type { DomainId, RegistryId, RequiredAndNotNull } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-id"; import { AccountIdInput, AccountIdRef } from "@/graphql-api/schema/account-id"; +import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; +import { cursors } from "@/graphql-api/schema/cursors"; import { type Domain, DomainRef } from "@/graphql-api/schema/domain"; import { PermissionsRef } from "@/graphql-api/schema/permissions"; import { db } from "@/lib/db"; @@ -53,16 +57,27 @@ RegistryInterfaceRef.implement({ ////////////////////// // Registry.domains ////////////////////// - domains: t.loadableGroup({ + domains: t.connection({ description: "TODO", type: DomainRef, - load: (ids: RegistryId[]) => - db.query.domain.findMany({ - where: (t, { inArray }) => inArray(t.registryId, ids), - with: { label: true }, - }), - group: (domain) => (domain as Domain).registryId, - resolve: getModelId, + resolve: (parent, args, context) => + resolveCursorConnection( + { ...DEFAULT_CONNECTION_ARGS, args }, + ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => + db.query.domain.findMany({ + where: (t, { lt, gt, eq, and }) => + and( + ...[ + eq(t.registryId, parent.id), + before !== undefined && lt(t.id, cursors.decode(before)), + after !== undefined && gt(t.id, cursors.decode(after)), + ].filter((c) => !!c), + ), + orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), + limit, + with: { label: true }, + }), + ), }), }), }); diff --git a/apps/ensapi/src/lib/handlers/drizzle.ts b/apps/ensapi/src/lib/handlers/drizzle.ts index c0a3e3016..8ff71b44a 100644 --- a/apps/ensapi/src/lib/handlers/drizzle.ts +++ b/apps/ensapi/src/lib/handlers/drizzle.ts @@ -1,8 +1,22 @@ import { setDatabaseSchema } from "@ponder/client"; +import type { Logger } from "drizzle-orm/logger"; import { drizzle } from "drizzle-orm/node-postgres"; +import type pino from "pino"; + +import { makeLogger } from "@/lib/logger"; type Schema = { [name: string]: unknown }; +const logger = makeLogger("drizzle"); + +class PinoDrizzleLogger implements Logger { + constructor(private readonly logger: pino.Logger) {} + + logQuery(query: string, params: unknown[]): void { + this.logger.debug({ params }, query); + } +} + /** * Makes a Drizzle DB object. */ @@ -18,5 +32,9 @@ export const makeDrizzle = ({ // monkeypatch schema onto tables setDatabaseSchema(schema, databaseSchema); - return drizzle(databaseUrl, { schema, casing: "snake_case" }); + return drizzle(databaseUrl, { + schema, + casing: "snake_case", + logger: new PinoDrizzleLogger(logger), + }); }; diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts index 9df918d90..6f94802e8 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts @@ -2,7 +2,7 @@ import config from "@/config"; import { type Context, ponder } from "ponder:registry"; import schema from "ponder:schema"; -import { type Address, isAddressEqual, zeroAddress, zeroHash } from "viem"; +import { type Address, isAddressEqual, namehash, zeroAddress, zeroHash } from "viem"; import { ADDR_REVERSE_NODE, diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index cfaf4c9c3..6712b4cea 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -1,7 +1,21 @@ /** * TODO - * - polymorphic resolver metadata in drizzle schema - * - polymorphic resolver in graphql, add owner to DedicatedResolver schema + * - connections w/ limits & cursors + * - Renewals + * - indexes + * - ? https://pothos-graphql.dev/docs/plugins/tracing + * - ThreeDNS + * - ? polymorphic resolver metadata in drizzle schema for better typechecking + * - ? move all entity ids to opaque base58 encoded IDs? kinda nice since they're just supposed to be opaque, useful for relay purposes, allows the scalar types to all be ID and then casted. but nice to use CAIP identifiers for resolvers and permissions etc. so many just for domains and registries? + * + * - *.addr.reverse subnames in ENSv1Registry aren't correctly connected to the ENSv2 Domain that represents addr.reverse + * - the scripts never mint reverse and addr.reverse into the ENSv1 Registry so that's annoying + * - the new .addr.reverse resolver does some fallback bullshit + * - https://github.com/ensdomains/ens-contracts/blob/staging/contracts/reverseResolver/ETHReverseResolver.sol + * + * Pending + * - DedicatedResolver moving to EAC + * - Registry.canonicalName indexing + adjust Domain.canonical reverse traversal logic * * Migration * - individual names are migrated to v2 and can choose to move to an ENSv2 Registry on L1 or L2 @@ -13,24 +27,8 @@ * - fuck me, there can be multiple registrations in v2 world. sub.example.xyz, if not emancipated, cannot be migrated, but sub.example.xyz can still be created in v2 registry in the example.xyz registry. * - if a v2 name is registered but there's an active namewrapper registration for that same name, we should perhaps ignore all future namewrapper events, as the v2 name overrides it in resolution and the namewrapper is never more consulted for that name (and i guess any subnames under it?) * - shadow-registering an existing name in v2 also shadows every name under it so we kind of need to do a recursive deletion of all of a shadowed name's subnames, right? cause resolution terminates at the first v2-registered name. - * - * * - for MigratedWrappedNameRegistries, need to check name expiry during resolution and avoid resolving expired names * - * - ThreeDNS - * - Renewals - * - indexes - * - https://pothos-graphql.dev/docs/plugins/tracing - * - connections w/ limits & cursors - * - Resolver polymorphism & Bridged Resolver materialization - * - Account.dedicatedResolvers - * - Registry.canonicalName - * - then update canonical traversal to use canonicalName - * - Account.permissions -> PermissionsUser[] - * - * - * move all entity ids to opaque base58 encoded IDs? kinda nice since they're just supposed to be opaque, useful for relay purposes, allows the scalar types to all be ID and then casted. but nice to use CAIP identifiers for resolvers and permissions etc. so many just for domains and registries? - * test alpha sepolia + protocol accelerateion index time vs ensv2 */ import { createConfig } from "ponder"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9e316b156..835c989b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -329,6 +329,12 @@ importers: '@pothos/plugin-dataloader': specifier: ^4.4.3 version: 4.4.3(@pothos/core@4.10.0(graphql@16.11.0))(dataloader@2.2.3)(graphql@16.11.0) + '@pothos/plugin-relay': + specifier: ^4.6.2 + version: 4.6.2(@pothos/core@4.10.0(graphql@16.11.0))(graphql@16.11.0) + bs58: + specifier: ^6.0.0 + version: 6.0.0 dataloader: specifier: ^2.2.3 version: 2.2.3 @@ -378,6 +384,9 @@ importers: '@ensnode/shared-configs': specifier: workspace:* version: link:../../packages/shared-configs + '@types/bs58': + specifier: ^5.0.0 + version: 5.0.0 '@types/node': specifier: 'catalog:' version: 22.18.13 @@ -2453,6 +2462,12 @@ packages: dataloader: '2' graphql: ^16.10.0 + '@pothos/plugin-relay@4.6.2': + resolution: {integrity: sha512-9aweCv9T53z4+CmE+JF8QoXeAEd+wT/rZflZNzrvH6ln2Lj6qy/EVEcL5BMr6en3/IYHH+ROyHAAsy12t4uIUQ==} + peerDependencies: + '@pothos/core': '*' + graphql: ^16.10.0 + '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -3299,6 +3314,10 @@ packages: '@types/braces@3.0.5': resolution: {integrity: sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==} + '@types/bs58@5.0.0': + resolution: {integrity: sha512-cAw/jKBzo98m6Xz1X5ETqymWfIMbXbu6nK15W4LQYjeHJkVqSmM5PO8Bd9KVHQJ/F4rHcSso9LcjtgCW6TGu2w==} + deprecated: This is a stub types definition. bs58 provides its own type definitions, so you do not need this installed. + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -3822,6 +3841,9 @@ packages: base-64@1.0.0: resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} + base-x@5.0.1: + resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -3884,6 +3906,9 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} @@ -9801,6 +9826,11 @@ snapshots: dataloader: 2.2.3 graphql: 16.11.0 + '@pothos/plugin-relay@4.6.2(@pothos/core@4.10.0(graphql@16.11.0))(graphql@16.11.0)': + dependencies: + '@pothos/core': 4.10.0(graphql@16.11.0) + graphql: 16.11.0 + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -10677,6 +10707,10 @@ snapshots: '@types/braces@3.0.5': {} + '@types/bs58@5.0.0': + dependencies: + bs58: 6.0.0 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -11365,6 +11399,8 @@ snapshots: base-64@1.0.0: {} + base-x@5.0.1: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.8.21: {} @@ -11442,6 +11478,10 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.1.4(browserslist@4.27.0) + bs58@6.0.0: + dependencies: + base-x: 5.0.1 + buffer-crc32@0.2.13: {} buffer@6.0.3: From 82dca36ddc9f0e589b24dc2410118aa0b6b4d19c Mon Sep 17 00:00:00 2001 From: shrugs Date: Wed, 19 Nov 2025 09:46:57 -0600 Subject: [PATCH 041/102] fix tests --- packages/ensnode-sdk/src/ensapi/config/conversions.test.ts | 2 +- packages/ensnode-sdk/src/shared/cache.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ensnode-sdk/src/ensapi/config/conversions.test.ts b/packages/ensnode-sdk/src/ensapi/config/conversions.test.ts index c394b339e..affd46cd7 100644 --- a/packages/ensnode-sdk/src/ensapi/config/conversions.test.ts +++ b/packages/ensnode-sdk/src/ensapi/config/conversions.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { ENSNamespaceIds } from "@ensnode/datasources"; -import { PluginName } from "../../ensindexer"; +import { PluginName } from "../../ensindexer/config/types"; import { deserializeENSApiPublicConfig, serializeENSApiPublicConfig } from "."; import type { ENSApiPublicConfig } from "./types"; diff --git a/packages/ensnode-sdk/src/shared/cache.test.ts b/packages/ensnode-sdk/src/shared/cache.test.ts index 17d721dc7..92c8927d4 100644 --- a/packages/ensnode-sdk/src/shared/cache.test.ts +++ b/packages/ensnode-sdk/src/shared/cache.test.ts @@ -80,7 +80,7 @@ describe("LruCache", () => { describe("TtlCache", () => { beforeEach(() => { - vi.useFakeTimers(); + vi.useFakeTimers({ shouldAdvanceTime: true, now: new Date(2024, 0, 1) }); }); afterEach(() => { @@ -211,7 +211,7 @@ describe("TtlCache", () => { describe("staleWhileRevalidate", () => { beforeEach(() => { - vi.useFakeTimers(); + vi.useFakeTimers({ shouldAdvanceTime: true, now: new Date(2024, 0, 1) }); }); afterEach(() => { From 29c4647824148622f6e3ecbf410dcd7676c4f814 Mon Sep 17 00:00:00 2001 From: shrugs Date: Wed, 19 Nov 2025 10:04:04 -0600 Subject: [PATCH 042/102] fix: switch to actual helthcechck for integration script --- .github/scripts/run_ensindexer_healthcheck.sh | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/.github/scripts/run_ensindexer_healthcheck.sh b/.github/scripts/run_ensindexer_healthcheck.sh index 42c462b8e..ca7a35860 100755 --- a/.github/scripts/run_ensindexer_healthcheck.sh +++ b/.github/scripts/run_ensindexer_healthcheck.sh @@ -5,7 +5,7 @@ # Set default timeout if not provided by environment # Use env var if set, otherwise default to 60 seconds -: "${HEALTH_CHECK_TIMEOUT:=60}" +: "${HEALTH_CHECK_TIMEOUT:=60}" # Detect if running from CI or local if [ -n "$GITHUB_WORKSPACE" ]; then @@ -38,19 +38,27 @@ PID=$! echo "ENSIndexer started with PID: $PID" +# Require ENSINDEXER_URL to be set +if [ -z "$ENSINDEXER_URL" ]; then + echo "Error: ENSINDEXER_URL environment variable must be set" + kill -9 $PID 2>/dev/null || true + wait $PID 2>/dev/null || true + rm -f "$LOG_FILE" + [ -f "$ENV_FILE" ] && rm -f "$ENV_FILE" + exit 1 +fi + # Wait for health check to pass -echo "Waiting for health check to pass (up to $HEALTH_CHECK_TIMEOUT seconds)..." +echo "Waiting for health check to pass at ${ENSINDEXER_URL}/health (up to $HEALTH_CHECK_TIMEOUT seconds)..." health_check_start=$(date +%s) last_log_check=0 while true; do current_time=$(date +%s) - # Periodically show log progress (every 15 seconds) to prevent CI timeout + # Periodically show progress (every 15 seconds) to prevent CI timeout if [ $((current_time - last_log_check)) -ge 15 ]; then echo "Still waiting for health check at $(date) (elapsed: $((current_time - health_check_start)) seconds)..." - echo "Recent log entries:" - tail -n 10 "$LOG_FILE" last_log_check=$current_time fi @@ -63,30 +71,26 @@ while true; do echo "Last 30 lines of log:" tail -n 30 "$LOG_FILE" rm -f "$LOG_FILE" - # Clean up env file [ -f "$ENV_FILE" ] && rm -f "$ENV_FILE" exit 1 fi - # Check for health ready message - if grep -q "Started returning 200 responses from /health endpoint" "$LOG_FILE"; then + # Check health endpoint + if curl -sf "${ENSINDEXER_URL}/health" >/dev/null 2>&1; then echo "Health check passed! ENSIndexer is up and running." echo "Test successful - terminating ENSIndexer" # Force kill the ENSIndexer process kill -9 $PID 2>/dev/null || true - # Make sure we don't wait for the process to exit since we've force killed it wait $PID 2>/dev/null || true # Clean up the log file and env file rm -f "$LOG_FILE" [ -f "$ENV_FILE" ] && rm -f "$ENV_FILE" - # Explicitly exit with success code echo "Exiting with success code 0" exit 0 fi # Check if we've reached the health check timeout elapsed=$((current_time - health_check_start)) - if [ $elapsed -ge $HEALTH_CHECK_TIMEOUT ]; then echo "Health check timeout reached. ENSIndexer did not become healthy." kill -9 $PID 2>/dev/null || true @@ -94,7 +98,6 @@ while true; do echo "Last 30 lines of log:" tail -n 30 "$LOG_FILE" rm -f "$LOG_FILE" - # Clean up env file [ -f "$ENV_FILE" ] && rm -f "$ENV_FILE" exit 1 fi From 147d769e1a0ca4dff5f8bec3f85b584de027af33 Mon Sep 17 00:00:00 2001 From: shrugs Date: Wed, 19 Nov 2025 12:17:10 -0600 Subject: [PATCH 043/102] checkpoint --- apps/ensindexer/src/plugins/ensv2/plugin.ts | 10 +++++- packages/datasources/src/ens-test-env.ts | 37 +++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index 6712b4cea..92ea4ec68 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -1,6 +1,5 @@ /** * TODO - * - connections w/ limits & cursors * - Renewals * - indexes * - ? https://pothos-graphql.dev/docs/plugins/tracing @@ -13,6 +12,15 @@ * - the new .addr.reverse resolver does some fallback bullshit * - https://github.com/ensdomains/ens-contracts/blob/staging/contracts/reverseResolver/ETHReverseResolver.sol * + * - we either need to keep the indexed model 1:1 with the on-chain model and stitch things together at the api layer + * OR go hard with materialization, and we need to reparent Domains based on individual v2 registry's fallback mechanisms + * which seems really flimsy and annoying and reparenting is not good for cache behavior. + * - maybe better to have ENSv1Domain and ENSv2Domain models. then all polymorphism is applied at the api layer + * - forward traversal (accessing Domain by name) would follow forward resolution, including the fallback logics + * - yeah we'd have to literally do forward resolution + * - anything that returns Domains would need to join against v1 and v2 domains + * - the v1 model need not include implicit registries + * * Pending * - DedicatedResolver moving to EAC * - Registry.canonicalName indexing + adjust Domain.canonical reverse traversal logic diff --git a/packages/datasources/src/ens-test-env.ts b/packages/datasources/src/ens-test-env.ts index f223a663a..973f45dc5 100644 --- a/packages/datasources/src/ens-test-env.ts +++ b/packages/datasources/src/ens-test-env.ts @@ -11,6 +11,7 @@ import { UniversalRegistrarRenewalWithReferrer as root_UniversalRegistrarRenewal import { UniversalResolver as root_UniversalResolver } from "./abis/root/UniversalResolver"; import { UnwrappedEthRegistrarController as root_UnwrappedEthRegistrarController } from "./abis/root/UnwrappedEthRegistrarController"; import { WrappedEthRegistrarController as root_WrappedEthRegistrarController } from "./abis/root/WrappedEthRegistrarController"; +import { StandaloneReverseRegistrar } from "./abis/shared/StandaloneReverseRegistrar"; import { ensTestEnvL1Chain, ensTestEnvL2Chain } from "./lib/chains"; // Shared ABIs import { ResolverABI } from "./lib/ResolverABI"; @@ -91,6 +92,12 @@ export default { // + ETHTLDResolver: { + abi: ResolverABI, + address: "0x99bba657f2bbc93c02d617f8ba121cb8fc104acf", + startBlock: 0, + }, + RootRegistry: { abi: Registry, address: "0x8a791620dd6260079bf849dc5567adc3f2fdc318", @@ -106,6 +113,7 @@ export default { }, }, }, + [DatasourceNames.Namechain]: { chain: ensTestEnvL2Chain, contracts: { @@ -119,4 +127,33 @@ export default { }, }, }, + + [DatasourceNames.ReverseResolverRoot]: { + chain: ensTestEnvL1Chain, + contracts: { + DefaultReverseRegistrar: { + abi: StandaloneReverseRegistrar, + address: "0x8f86403a4de0bb5791fa46b8e795c547942fe4cf", + startBlock: 0, + }, + + DefaultReverseResolver3: { + abi: ResolverABI, + address: "0x5eb3bc0a489c5a8288765d2336659ebca68fcd00", + startBlock: 0, + }, + + DefaultPublicResolver2: { + abi: ResolverABI, + address: "0x367761085bf3c12e5da2df99ac6e1a824612b8fb", + startBlock: 0, + }, + + DefaultPublicResolver3: { + abi: ResolverABI, + address: "0x4c2f7092c2ae51d986befee378e50bd4db99c901", + startBlock: 0, + }, + }, + }, } satisfies ENSNamespace; From 31c8941a777e01268b47ccfb357b3d5a52109d1d Mon Sep 17 00:00:00 2001 From: shrugs Date: Wed, 19 Nov 2025 13:18:18 -0600 Subject: [PATCH 044/102] checkpoint --- apps/ensapi/src/graphql-api/builder.ts | 2 - .../src/graphql-api/lib/get-canonical-path.ts | 7 +- .../src/graphql-api/lib/get-domain-by-fqdn.ts | 4 +- apps/ensapi/src/graphql-api/schema/account.ts | 6 +- apps/ensapi/src/graphql-api/schema/domain.ts | 226 +++++++++++------- apps/ensapi/src/graphql-api/schema/query.ts | 10 +- .../src/graphql-api/schema/registration.ts | 4 +- .../ensapi/src/graphql-api/schema/registry.ts | 75 ++---- apps/ensapi/src/graphql-api/schema/scalars.ts | 11 - .../src/lib/ensv2/domain-db-helpers.ts | 15 +- .../resolver-records-db-helpers.ts | 2 +- .../src/plugins/ensv2/event-handlers.ts | 16 +- .../handlers/{ => ensv1}/BaseRegistrar.ts | 7 +- .../handlers/{ => ensv1}/ENSv1Registry.ts | 41 +--- .../ensv2/handlers/{ => ensv1}/NameWrapper.ts | 6 +- .../{ => ensv1}/RegistrarController.ts | 0 .../{Registry.ts => ensv2/ENSv2Registry.ts} | 60 +++-- .../{ => ensv2}/EnhancedAccessControl.ts | 0 apps/ensindexer/src/plugins/ensv2/plugin.ts | 26 +- .../src/schemas/ensv2.schema.ts | 140 +++++++---- packages/ensnode-sdk/src/ensv2/ids-lib.ts | 11 +- packages/ensnode-sdk/src/ensv2/ids.ts | 14 +- .../ensnode-sdk/src/shared/root-registry.ts | 4 +- 23 files changed, 362 insertions(+), 325 deletions(-) rename apps/ensindexer/src/plugins/ensv2/handlers/{ => ensv1}/BaseRegistrar.ts (97%) rename apps/ensindexer/src/plugins/ensv2/handlers/{ => ensv1}/ENSv1Registry.ts (84%) rename apps/ensindexer/src/plugins/ensv2/handlers/{ => ensv1}/NameWrapper.ts (98%) rename apps/ensindexer/src/plugins/ensv2/handlers/{ => ensv1}/RegistrarController.ts (100%) rename apps/ensindexer/src/plugins/ensv2/handlers/{Registry.ts => ensv2/ENSv2Registry.ts} (74%) rename apps/ensindexer/src/plugins/ensv2/handlers/{ => ensv2}/EnhancedAccessControl.ts (100%) diff --git a/apps/ensapi/src/graphql-api/builder.ts b/apps/ensapi/src/graphql-api/builder.ts index f66906c1e..a6ecb0388 100644 --- a/apps/ensapi/src/graphql-api/builder.ts +++ b/apps/ensapi/src/graphql-api/builder.ts @@ -7,7 +7,6 @@ import type { ChainId, CoinType, DomainId, - ImplicitRegistryId, InterpretedName, Node, RegistryId, @@ -25,7 +24,6 @@ export const builder = new SchemaBuilder<{ Name: { Input: InterpretedName; Output: InterpretedName }; DomainId: { Input: DomainId; Output: DomainId }; RegistryId: { Input: RegistryId; Output: RegistryId }; - ImplicitRegistryId: { Input: ImplicitRegistryId; Output: ImplicitRegistryId }; ResolverId: { Input: ResolverId; Output: ResolverId }; // PermissionsId: { Input: PermissionsId; Output: PermissionsId }; }; diff --git a/apps/ensapi/src/graphql-api/lib/get-canonical-path.ts b/apps/ensapi/src/graphql-api/lib/get-canonical-path.ts index bdb621388..8323f35d3 100644 --- a/apps/ensapi/src/graphql-api/lib/get-canonical-path.ts +++ b/apps/ensapi/src/graphql-api/lib/get-canonical-path.ts @@ -19,7 +19,8 @@ const ROOT_REGISTRY_ID = getRootRegistryId(config.namespace); * Provide the canonical parents from the Root Registry to `domainId`. * i.e. reverse traversal of the namegraph * - * TODO: this implementation is more or less first-write-wins, need to updated based on proposed reverse mapping + * TODO: this implementation has undefined canonical name behavior, need to updated based on proposed + * reverse mapping */ export async function getCanonicalPath(domainId: DomainId): Promise { const result = await db.execute(sql` @@ -30,7 +31,7 @@ export async function getCanonicalPath(domainId: DomainId): Promise // TODO(dataloader) — confirm this is dataloaded? resolveCursorConnection( diff --git a/apps/ensapi/src/graphql-api/schema/domain.ts b/apps/ensapi/src/graphql-api/schema/domain.ts index a24940118..d8308f60e 100644 --- a/apps/ensapi/src/graphql-api/schema/domain.ts +++ b/apps/ensapi/src/graphql-api/schema/domain.ts @@ -1,9 +1,8 @@ -import { rejectErrors } from "@pothos/plugin-dataloader"; - import { type DomainId, + type ENSv1DomainId, + type ENSv2DomainId, getCanonicalId, - interpretedLabelsToInterpretedName, } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; @@ -14,13 +13,19 @@ import { getLatestRegistration } from "@/graphql-api/lib/get-latest-registration import { rejectAnyErrors } from "@/graphql-api/lib/reject-any-errors"; import { AccountRef } from "@/graphql-api/schema/account"; import { type Registration, RegistrationInterfaceRef } from "@/graphql-api/schema/registration"; -import { RegistryInterfaceRef } from "@/graphql-api/schema/registry"; +import { RegistryRef } from "@/graphql-api/schema/registry"; import { ResolverRef } from "@/graphql-api/schema/resolver"; import { db } from "@/lib/db"; -export const DomainRef = builder.loadableObjectRef("Domain", { - load: (ids: DomainId[]) => - db.query.domain.findMany({ +const isENSv1Domain = (domain: Domain): domain is ENSv1Domain => "parentId" in domain; + +////////////////////// +// Refs +////////////////////// + +export const ENSv1DomainRef = builder.loadableObjectRef("v1Domain", { + load: (ids: ENSv1DomainId[]) => + db.query.v1Domain.findMany({ where: (t, { inArray }) => inArray(t.id, ids), with: { label: true }, }), @@ -28,15 +33,27 @@ export const DomainRef = builder.loadableObjectRef("Domain", { cacheResolved: true, sort: true, }); +export const ENSv2DomainRef = builder.loadableObjectRef("v2Domain", { + load: (ids: ENSv2DomainId[]) => + db.query.v2Domain.findMany({ + where: (t, { inArray }) => inArray(t.id, ids), + with: { label: true }, + }), + toKey: getModelId, + cacheResolved: true, + sort: true, +}); +export const DomainInterfaceRef = builder.interfaceRef("Domain"); -export type Domain = Exclude; +export type ENSv1Domain = Exclude; +export type ENSv2Domain = Exclude; +export type Domain = Exclude; -// we want to dataloader labels by labelhash -// we want to dataloader a domain's canonical path, but without exposing it -// TODO: consider interface with ... on ENSv2Domain { canonicalId } etc -// ... on ENSv1Domain { node } etc +////////////////////////////// +// DomainInterface Implementation +////////////////////////////// -DomainRef.implement({ +DomainInterfaceRef.implement({ description: "a Domain", fields: (t) => ({ ////////////////////// @@ -48,16 +65,6 @@ DomainRef.implement({ nullable: false, }), - ////////////////////// - // Domain.canonicalId - ////////////////////// - canonicalId: t.field({ - type: "BigInt", - description: "TODO", - nullable: false, - resolve: (parent) => getCanonicalId(parent.labelHash), - }), - ////////////////////// // Domain.label ////////////////////// @@ -78,63 +85,63 @@ DomainRef.implement({ // load: (ids: DomainId[], context) => context.loadPosts(ids), // resolve: (user, args) => user.lastPostID, // }), - canonical: t.field({ - description: "TODO", - type: "Name", - nullable: true, - resolve: async ({ id }, args, context) => { - // TODO: dataloader the getCanonicalPath(domainId) function - const canonicalPath = await getCanonicalPath(id); - if (!canonicalPath) return null; - - const domains = await rejectAnyErrors( - DomainRef.getDataloader(context).loadMany(canonicalPath), - ); - - return interpretedLabelsToInterpretedName( - canonicalPath.map((domainId) => { - const found = domains.find((d) => d.id === domainId); - if (!found) throw new Error(`Invariant`); - return found.label.value; - }), - ); - }, - }), + // canonical: t.field({ + // description: "TODO", + // type: "Name", + // nullable: true, + // resolve: async ({ id }, args, context) => { + // // TODO: dataloader the getCanonicalPath(domainId) function + // const canonicalPath = await getCanonicalPath(id); + // if (!canonicalPath) return null; + + // const domains = await rejectAnyErrors( + // DomainInterfaceRef.getDataloader(context).loadMany(canonicalPath), + // ); + + // return interpretedLabelsToInterpretedName( + // canonicalPath.map((domainId) => { + // const found = domains.find((d) => d.id === domainId); + // if (!found) throw new Error(`Invariant`); + // return found.label.value; + // }), + // ); + // }, + // }), ////////////////// // Domain.parents ////////////////// - parents: t.field({ - description: "TODO", - type: [DomainRef], - nullable: true, - resolve: async ({ id }, args, context) => { - // TODO: dataloader the getCanonicalPath(domainId) function - const canonicalPath = await getCanonicalPath(id); - if (!canonicalPath) return null; + // parents: t.field({ + // description: "TODO", + // type: [DomainInterfaceRef], + // nullable: true, + // resolve: async ({ id }, args, context) => { + // // TODO: dataloader the getCanonicalPath(domainId) function + // const canonicalPath = await getCanonicalPath(id); + // if (!canonicalPath) return null; - const domains = await rejectErrors( - DomainRef.getDataloader(context).loadMany(canonicalPath), - ); + // const domains = await rejectErrors( + // DomainInterfaceRef.getDataloader(context).loadMany(canonicalPath), + // ); - return domains.slice(1); - }, - }), + // return domains.slice(1); + // }, + // }), ////////////////// // Domain.aliases ////////////////// - aliases: t.field({ - description: "TODO", - type: ["Name"], - nullable: false, - resolve: async (parent) => { - // a domain's aliases are all of the paths from root to this domain for which it can be - // resolved. naively reverse-traverse the namegaph until the root is reached... yikes. - // if materializing namespace: simply lookup namesInNamespace by domainId - return []; - }, - }), + // aliases: t.field({ + // description: "TODO", + // type: ["Name"], + // nullable: false, + // resolve: async (parent) => { + // // a domain's aliases are all of the paths from root to this domain for which it can be + // // resolved. naively reverse-traverse the namegaph until the root is reached... yikes. + // // if materializing namespace: simply lookup namesInNamespace by domainId + // return []; + // }, + // }), ////////////////////// // Domain.owner @@ -146,26 +153,6 @@ DomainRef.implement({ resolve: (parent) => parent.ownerId, }), - ////////////////////// - // Domain.registry - ////////////////////// - registry: t.field({ - description: "TODO", - type: RegistryInterfaceRef, - nullable: false, - resolve: (parent) => parent.registryId, - }), - - ////////////////////// - // Domain.subregistry - ////////////////////// - subregistry: t.field({ - type: RegistryInterfaceRef, - description: "TODO", - nullable: true, - resolve: (parent) => parent.subregistryId, - }), - ////////////////////// // Domain.resolver ////////////////////// @@ -173,7 +160,6 @@ DomainRef.implement({ description: "TODO", type: ResolverRef, nullable: true, - // TODO: dataloader this resolve: (parent) => getDomainResolver(parent.id), }), @@ -204,6 +190,64 @@ DomainRef.implement({ }), }); +////////////////////////////// +// ENSv1Domain Implementation +////////////////////////////// + +ENSv1DomainRef.implement({ + description: "TODO", + interfaces: [DomainInterfaceRef], + isTypeOf: (domain) => isENSv1Domain(domain as Domain), + fields: (t) => ({ + // + }), +}); + +////////////////////////////// +// ENSv2Domain Implementation +////////////////////////////// + +ENSv2DomainRef.implement({ + description: "TODO", + interfaces: [DomainInterfaceRef], + isTypeOf: (domain) => !isENSv1Domain(domain as Domain), + fields: (t) => ({ + ////////////////////// + // Domain.canonicalId + ////////////////////// + canonicalId: t.field({ + type: "BigInt", + description: "TODO", + nullable: false, + resolve: (parent) => getCanonicalId(parent.labelHash), + }), + + ////////////////////// + // Domain.registry + ////////////////////// + registry: t.field({ + description: "TODO", + type: RegistryRef, + nullable: false, + resolve: (parent) => parent.registryId, + }), + + ////////////////////// + // Domain.subregistry + ////////////////////// + subregistry: t.field({ + type: RegistryRef, + description: "TODO", + nullable: true, + resolve: (parent) => parent.subregistryId, + }), + }), +}); + +////////////////////// +// Inputs +////////////////////// + export const DomainIdInput = builder.inputType("DomainIdInput", { description: "TODO", isOneOf: true, diff --git a/apps/ensapi/src/graphql-api/schema/query.ts b/apps/ensapi/src/graphql-api/schema/query.ts index 6f4264011..cd94bfd6e 100644 --- a/apps/ensapi/src/graphql-api/schema/query.ts +++ b/apps/ensapi/src/graphql-api/schema/query.ts @@ -6,7 +6,7 @@ import { type DomainId, getRootRegistryId, makePermissionsId, - makeRegistryContractId, + makeRegistryId, makeResolverId, } from "@ensnode/ensnode-sdk"; @@ -15,7 +15,7 @@ import { getDomainIdByInterpretedName } from "@/graphql-api/lib/get-domain-by-fq import { AccountRef } from "@/graphql-api/schema/account"; import { AccountIdInput } from "@/graphql-api/schema/account-id"; import { cursors } from "@/graphql-api/schema/cursors"; -import { DomainIdInput, DomainRef } from "@/graphql-api/schema/domain"; +import { DomainIdInput, DomainInterfaceRef } from "@/graphql-api/schema/domain"; import { PermissionsRef } from "@/graphql-api/schema/permissions"; import { RegistryIdInput, RegistryInterfaceRef } from "@/graphql-api/schema/registry"; import { ResolverIdInput, ResolverRef } from "@/graphql-api/schema/resolver"; @@ -26,7 +26,7 @@ builder.queryType({ // testing, delete this domains: t.connection({ description: "TODO", - type: DomainRef, + type: DomainInterfaceRef, resolve: (parent, args, context) => resolveCursorConnection( { @@ -56,7 +56,7 @@ builder.queryType({ ////////////////////////////////// domain: t.field({ description: "TODO", - type: DomainRef, + type: DomainInterfaceRef, args: { by: t.arg({ type: DomainIdInput, required: true }) }, nullable: true, resolve: async (parent, args, ctx, info) => { @@ -85,7 +85,7 @@ builder.queryType({ resolve: async (parent, args, ctx, info) => { if (args.by.id !== undefined) return args.by.id; if (args.by.implicit !== undefined) return args.by.implicit.parent; - return makeRegistryContractId(args.by.contract); + return makeRegistryId(args.by.contract); }, }), diff --git a/apps/ensapi/src/graphql-api/schema/registration.ts b/apps/ensapi/src/graphql-api/schema/registration.ts index ce27772b5..e9a2e020d 100644 --- a/apps/ensapi/src/graphql-api/schema/registration.ts +++ b/apps/ensapi/src/graphql-api/schema/registration.ts @@ -3,7 +3,7 @@ import type { RegistrationId, RequiredAndNotNull } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-id"; import { AccountIdRef } from "@/graphql-api/schema/account-id"; -import { DomainRef } from "@/graphql-api/schema/domain"; +import { DomainInterfaceRef } from "@/graphql-api/schema/domain"; import { WrappedBaseRegistrarRegistrationRef } from "@/graphql-api/schema/wrapped-baseregistrar-registration"; import { db } from "@/lib/db"; @@ -58,7 +58,7 @@ RegistrationInterfaceRef.implement({ /////////////////////// domain: t.field({ description: "TODO", - type: DomainRef, + type: DomainInterfaceRef, nullable: false, resolve: (parent) => parent.domainId, }), diff --git a/apps/ensapi/src/graphql-api/schema/registry.ts b/apps/ensapi/src/graphql-api/schema/registry.ts index 462e04140..8a0061172 100644 --- a/apps/ensapi/src/graphql-api/schema/registry.ts +++ b/apps/ensapi/src/graphql-api/schema/registry.ts @@ -1,32 +1,27 @@ import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay"; -import type { DomainId, RegistryId, RequiredAndNotNull } from "@ensnode/ensnode-sdk"; +import type { ENSv2DomainId, RegistryId } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-id"; import { AccountIdInput, AccountIdRef } from "@/graphql-api/schema/account-id"; import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; import { cursors } from "@/graphql-api/schema/cursors"; -import { type Domain, DomainRef } from "@/graphql-api/schema/domain"; +import { type ENSv2Domain, ENSv2DomainRef } from "@/graphql-api/schema/domain"; import { PermissionsRef } from "@/graphql-api/schema/permissions"; import { db } from "@/lib/db"; -export const RegistryInterfaceRef = builder.loadableInterfaceRef("Registry", { +export const RegistryRef = builder.loadableObjectRef("Registry", { load: (ids: RegistryId[]) => - db.query.registry.findMany({ - where: (t, { inArray }) => inArray(t.id, ids), - }), + db.query.registry.findMany({ where: (t, { inArray }) => inArray(t.id, ids) }), toKey: getModelId, cacheResolved: true, sort: true, }); -export type Registry = Exclude; -export type RegistryInterface = Pick; -export type RegistryContract = RequiredAndNotNull; -export type ImplicitRegistry = Registry; +export type Registry = Exclude; -RegistryInterfaceRef.implement({ +RegistryRef.implement({ description: "TODO", fields: (t) => ({ ////////////////////// @@ -38,19 +33,19 @@ RegistryInterfaceRef.implement({ nullable: false, }), - //////////////////// - // Registry.parents - //////////////////// + // //////////////////// + // // Registry.parents + // //////////////////// parents: t.loadableGroup({ description: "TODO", - type: DomainRef, + type: ENSv2DomainRef, load: (ids: RegistryId[]) => - db.query.domain.findMany({ + db.query.v2Domain.findMany({ where: (t, { inArray }) => inArray(t.subregistryId, ids), with: { label: true }, }), // biome-ignore lint/style/noNonNullAssertion: subregistryId guaranteed to exist via inArray - group: (domain) => (domain as Domain).subregistryId!, + group: (domain) => (domain as ENSv2Domain).subregistryId!, resolve: getModelId, }), @@ -59,18 +54,18 @@ RegistryInterfaceRef.implement({ ////////////////////// domains: t.connection({ description: "TODO", - type: DomainRef, + type: ENSv2DomainRef, resolve: (parent, args, context) => resolveCursorConnection( { ...DEFAULT_CONNECTION_ARGS, args }, ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => - db.query.domain.findMany({ + db.query.v2Domain.findMany({ where: (t, { lt, gt, eq, and }) => and( ...[ eq(t.registryId, parent.id), - before !== undefined && lt(t.id, cursors.decode(before)), - after !== undefined && gt(t.id, cursors.decode(after)), + before !== undefined && lt(t.id, cursors.decode(before)), + after !== undefined && gt(t.id, cursors.decode(after)), ].filter((c) => !!c), ), orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), @@ -79,18 +74,10 @@ RegistryInterfaceRef.implement({ }), ), }), - }), -}); -export const RegistryContractRef = builder.objectRef("RegistryContract"); -RegistryContractRef.implement({ - description: "A Registry Contract", - interfaces: [RegistryInterfaceRef], - isTypeOf: (value) => (value as RegistryInterface).type === "RegistryContract", - fields: (t) => ({ - //////////////////////////////// - // RegistryContract.permissions - //////////////////////////////// + //////////////////////// + // Registry.permissions + //////////////////////// permissions: t.field({ description: "TODO", type: PermissionsRef, @@ -98,9 +85,9 @@ RegistryContractRef.implement({ resolve: ({ chainId, address }) => null, }), - ///////////////////////////// - // RegistryContract.contract - ///////////////////////////// + ///////////////////// + // Registry.contract + ///////////////////// contract: t.field({ description: "TODO", type: AccountIdRef, @@ -110,20 +97,9 @@ RegistryContractRef.implement({ }), }); -export const ImplicitRegistryRef = builder.objectRef("ImplicitRegistry"); -ImplicitRegistryRef.implement({ - description: "An Implicit Registry", - interfaces: [RegistryInterfaceRef], - isTypeOf: (value) => (value as RegistryInterface).type === "ImplicitRegistry", - fields: (t) => ({}), -}); - -export const ImplicitRegistryIdInput = builder.inputType("ImplicitRegistryIdInput", { - description: "TODO", - fields: (t) => ({ - parent: t.field({ type: "ImplicitRegistryId", required: true }), - }), -}); +////////// +// Inputs +////////// export const RegistryIdInput = builder.inputType("RegistryIdInput", { description: "TODO", @@ -131,6 +107,5 @@ export const RegistryIdInput = builder.inputType("RegistryIdInput", { fields: (t) => ({ id: t.field({ type: "RegistryId" }), contract: t.field({ type: AccountIdInput }), - implicit: t.field({ type: ImplicitRegistryIdInput }), }), }); diff --git a/apps/ensapi/src/graphql-api/schema/scalars.ts b/apps/ensapi/src/graphql-api/schema/scalars.ts index f24d4f0c8..c5091ef96 100644 --- a/apps/ensapi/src/graphql-api/schema/scalars.ts +++ b/apps/ensapi/src/graphql-api/schema/scalars.ts @@ -5,7 +5,6 @@ import { type ChainId, type CoinType, type DomainId, - type ImplicitRegistryId, type InterpretedName, isInterpretedName, type Name, @@ -122,16 +121,6 @@ builder.scalarType("RegistryId", { .parse(value), }); -builder.scalarType("ImplicitRegistryId", { - description: "ImplicitRegistryId represents a @ensnode/ensnode-sdk#ImplicitRegistryId.", - serialize: (value: ImplicitRegistryId) => value, - parseValue: (value) => - z.coerce - .string() - .transform((val) => val as ImplicitRegistryId) - .parse(value), -}); - builder.scalarType("ResolverId", { description: "ResolverId represents a @ensnode/ensnode-sdk#ResolverId.", serialize: (value: ResolverId) => value, diff --git a/apps/ensindexer/src/lib/ensv2/domain-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/domain-db-helpers.ts index 23a71d86f..ead0178cc 100644 --- a/apps/ensindexer/src/lib/ensv2/domain-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/domain-db-helpers.ts @@ -2,17 +2,22 @@ import type { Context } from "ponder:registry"; import schema from "ponder:schema"; import type { Address } from "viem"; -import type { DomainId } from "@ensnode/ensnode-sdk"; +import type { ENSv1DomainId } from "@ensnode/ensnode-sdk"; import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; /** - * Sets `domainId`'s owner to `owner` if exists. + * Sets an ENSv1 Domain's owner to `owner` if exists. */ -export async function materializeDomainOwner(context: Context, domainId: DomainId, owner: Address) { - const domain = await context.db.find(schema.domain, { id: domainId }); +export async function materializeENSv1DomainOwner( + context: Context, + id: ENSv1DomainId, + owner: Address, +) { + const domain = await context.db.find(schema.v1Domain, { id }); + // TODO: why did i want to put this in a conditional again? why doesn't v1 domain always exist? if (domain) { await ensureAccount(context, owner); - await context.db.update(schema.domain, { id: domainId }).set({ ownerId: owner }); + await context.db.update(schema.v1Domain, { id }).set({ ownerId: owner }); } } diff --git a/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts b/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts index 0334c41b3..9de5d4803 100644 --- a/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts +++ b/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts @@ -6,7 +6,7 @@ import { ResolverABI } from "@ensnode/datasources"; import { type AccountId, type CoinType, - makeRegistryContractId, + makeRegistryId, makeResolverId, makeResolverRecordsId, type Node, diff --git a/apps/ensindexer/src/plugins/ensv2/event-handlers.ts b/apps/ensindexer/src/plugins/ensv2/event-handlers.ts index 27e8c3b59..7e0207eb2 100644 --- a/apps/ensindexer/src/plugins/ensv2/event-handlers.ts +++ b/apps/ensindexer/src/plugins/ensv2/event-handlers.ts @@ -1,15 +1,15 @@ -import attach_BaseRegistrarHandlers from "./handlers/BaseRegistrar"; -import attach_ENSv1RegistryHandlers from "./handlers/ENSv1Registry"; -import attach_EnhancedAccessControlHandlers from "./handlers/EnhancedAccessControl"; -import attach_NameWrapperHandlers from "./handlers/NameWrapper"; -import attach_RegistrarControllerHandlers from "./handlers/RegistrarController"; -import attach_RegistryHandlers from "./handlers/Registry"; +import attach_BaseRegistrarHandlers from "./handlers/ensv1/BaseRegistrar"; +import attach_ENSv1RegistryHandlers from "./handlers/ensv1/ENSv1Registry"; +import attach_NameWrapperHandlers from "./handlers/ensv1/NameWrapper"; +import attach_RegistrarControllerHandlers from "./handlers/ensv1/RegistrarController"; +import attach_RegistryHandlers from "./handlers/ensv2/ENSv2Registry"; +import attach_EnhancedAccessControlHandlers from "./handlers/ensv2/EnhancedAccessControl"; export default function () { + attach_BaseRegistrarHandlers(); attach_ENSv1RegistryHandlers(); - attach_EnhancedAccessControlHandlers(); attach_NameWrapperHandlers(); - attach_BaseRegistrarHandlers(); attach_RegistrarControllerHandlers(); + attach_EnhancedAccessControlHandlers(); attach_RegistryHandlers(); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/BaseRegistrar.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts similarity index 97% rename from apps/ensindexer/src/plugins/ensv2/handlers/BaseRegistrar.ts rename to apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts index 5e5081e17..47a3615e7 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/BaseRegistrar.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts @@ -6,12 +6,11 @@ import { type Address, isAddressEqual, namehash, zeroAddress } from "viem"; import { makeENSv1DomainId, makeLatestRegistrationId, - makeRegistrationId, makeSubdomainNode, PluginName, } from "@ensnode/ensnode-sdk"; -import { materializeDomainOwner } from "@/lib/ensv2/domain-db-helpers"; +import { materializeENSv1DomainOwner } from "@/lib/ensv2/domain-db-helpers"; import { getRegistrarManagedName, registrarTokenIdToLabelHash } from "@/lib/ensv2/registrar-lib"; import { getLatestRegistration, @@ -90,7 +89,7 @@ export default function () { } // materialize Domain owner - await materializeDomainOwner(context, domainId, to); + await materializeENSv1DomainOwner(context, domainId, to); } }, ); @@ -149,7 +148,7 @@ export default function () { }); // materialize Domain owner - await materializeDomainOwner(context, domainId, owner); + await materializeENSv1DomainOwner(context, domainId, owner); } ponder.on(namespaceContract(pluginName, "BaseRegistrar:NameRegistered"), handleNameRegistered); diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts similarity index 84% rename from apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts rename to apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts index 6f94802e8..99657bb6e 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ENSv1Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts @@ -2,7 +2,7 @@ import config from "@/config"; import { type Context, ponder } from "ponder:registry"; import schema from "ponder:schema"; -import { type Address, isAddressEqual, namehash, zeroAddress, zeroHash } from "viem"; +import { type Address, isAddressEqual, zeroAddress } from "viem"; import { ADDR_REVERSE_NODE, @@ -11,14 +11,13 @@ import { getRootRegistryId, type LabelHash, makeENSv1DomainId, - makeImplicitRegistryId, makeSubdomainNode, type Node, PluginName, ROOT_NODE, } from "@ensnode/ensnode-sdk"; -import { materializeDomainOwner } from "@/lib/ensv2/domain-db-helpers"; +import { materializeENSv1DomainOwner } from "@/lib/ensv2/domain-db-helpers"; import { ensureLabel, ensureUnknownLabel } from "@/lib/ensv2/label-db-helpers"; import { healAddrReverseSubnameLabel } from "@/lib/heal-addr-reverse-subname-label"; import { namespaceContract } from "@/lib/plugin-helpers"; @@ -38,14 +37,11 @@ export default function () { ponder.on(namespaceContract(pluginName, "ENSv1RegistryOld:setup"), async ({ context }) => { // ensures that the Root Registry (which is eventually backed by the ENSv2 Root Registry) is // populated in the database - await context.db - .insert(schema.registry) - .values({ - id: getRootRegistryId(config.namespace), - type: "RegistryContract", - ...getRootRegistry(config.namespace), - }) - .onConflictDoNothing(); + await context.db.insert(schema.registry).values({ + id: getRootRegistryId(config.namespace), + type: "RegistryContract", + ...getRootRegistry(config.namespace), + }); }); /** @@ -71,11 +67,7 @@ export default function () { const node = makeSubdomainNode(labelHash, parentNode); const domainId = makeENSv1DomainId(node); - const registryId = - parentNode === zeroHash - ? getRootRegistryId(config.namespace) - : makeImplicitRegistryId(parentNode); - const subregistryId = makeImplicitRegistryId(node); + const parentId = makeENSv1DomainId(parentNode); // If this is a direct subname of addr.reverse, we have 100% on-chain label discovery. // @@ -97,28 +89,21 @@ export default function () { // upsert domain await context.db - .insert(schema.domain) + .insert(schema.v1Domain) .values({ id: domainId, + parentId, labelHash, - registryId, - subregistryId, }) .onConflictDoNothing(); - // and its ImplicitRegistry - await context.db - .insert(schema.registry) - .values({ id: subregistryId, type: "ImplicitRegistry" }) - .onConflictDoNothing(); - // materialize domain owner // NOTE: despite Domain.ownerId being materialized from other sources of truth (i.e. Registrars // like BaseRegistrars & NameWrapper) it's ok to always set it here because the Registrar-emitted // events occur _after_ the Registry events. So when a name is registered, for example, the Registry's // owner changes to that of the NameWrapper but then the NameWrapper emits NameWrapped, and this // indexing code re-materializes the Domain.ownerId to the NameWraper-emitted value. - await materializeDomainOwner(context, domainId, owner); + await materializeENSv1DomainOwner(context, domainId, owner); } async function handleTransfer({ @@ -137,7 +122,7 @@ export default function () { const isDeletion = isAddressEqual(zeroAddress, owner); if (isDeletion) { - await context.db.delete(schema.domain, { id: domainId }); + await context.db.delete(schema.v1Domain, { id: domainId }); return; } @@ -147,7 +132,7 @@ export default function () { // events occur _after_ the Registry events. So when a name is wrapped, for example, the Registry's // owner changes to that of the NameWrapper but then the NameWrapper emits NameWrapped, and this // indexing code re-materializes the Domain.ownerId to the NameWraper-emitted value. - await materializeDomainOwner(context, domainId, owner); + await materializeENSv1DomainOwner(context, domainId, owner); } /** diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts similarity index 98% rename from apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts rename to apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts index c72a06ba8..cb097d0dd 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts @@ -17,7 +17,7 @@ import { uint256ToHex32, } from "@ensnode/ensnode-sdk"; -import { materializeDomainOwner } from "@/lib/ensv2/domain-db-helpers"; +import { materializeENSv1DomainOwner } from "@/lib/ensv2/domain-db-helpers"; import { ensureLabel } from "@/lib/ensv2/label-db-helpers"; import { getRegistrarManagedName } from "@/lib/ensv2/registrar-lib"; import { @@ -137,7 +137,7 @@ export default function () { // now guaranteed to be an unexpired transferrable Registration // so materialize domain owner - await materializeDomainOwner(context, domainId, to); + await materializeENSv1DomainOwner(context, domainId, to); } ponder.on( @@ -178,7 +178,7 @@ export default function () { registration && isRegistrationFullyExpired(registration, event.block.timestamp); // materialize domain owner - await materializeDomainOwner(context, domainId, owner); + await materializeENSv1DomainOwner(context, domainId, owner); // handle wraps of direct-subname-of-registrar-managed-names if (registration && !isFullyExpired && registration.type === "BaseRegistrar") { diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/RegistrarController.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts similarity index 100% rename from apps/ensindexer/src/plugins/ensv2/handlers/RegistrarController.ts rename to apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts similarity index 74% rename from apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts rename to apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts index 24fbd102a..25fe9a2b8 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts @@ -8,7 +8,7 @@ import { getCanonicalId, type LiteralLabel, makeENSv2DomainId, - makeRegistryContractId, + makeRegistryId, PluginName, } from "@ensnode/ensnode-sdk"; @@ -22,7 +22,7 @@ const pluginName = PluginName.ENSv2; export default function () { ponder.on( - namespaceContract(pluginName, "Registry:NameRegistered"), + namespaceContract(pluginName, "ENSv2Registry:NameRegistered"), async ({ context, event, @@ -39,7 +39,7 @@ export default function () { const label = _label as LiteralLabel; const registryAccountId = getThisAccountId(context, event); - const registryId = makeRegistryContractId(registryAccountId); + const registryId = makeRegistryId(registryAccountId); const canonicalId = getCanonicalId(tokenId); const labelHash = labelhash(label); const domainId = makeENSv2DomainId(registryAccountId, canonicalId); @@ -76,7 +76,7 @@ export default function () { await ensureLabel(context, label); // insert Domain - await context.db.insert(schema.domain).values({ id: domainId, registryId, labelHash }); + await context.db.insert(schema.v2Domain).values({ id: domainId, registryId, labelHash }); // TODO: insert Registration entity for this domain as well: expiration, registrant // ensure Registrant @@ -85,7 +85,7 @@ export default function () { ); ponder.on( - namespaceContract(pluginName, "Registry:SubregistryUpdate"), + namespaceContract(pluginName, "ENSv2Registry:SubregistryUpdate"), async ({ context, event, @@ -105,19 +105,19 @@ export default function () { // update domain's subregistry const isDeletion = isAddressEqual(subregistry, zeroAddress); if (isDeletion) { - await context.db.update(schema.domain, { id: domainId }).set({ subregistryId: null }); + await context.db.update(schema.v2Domain, { id: domainId }).set({ subregistryId: null }); } else { const subregistryAccountId: AccountId = { chainId: context.chain.id, address: subregistry }; - const subregistryId = makeRegistryContractId(subregistryAccountId); + const subregistryId = makeRegistryId(subregistryAccountId); - await context.db.update(schema.domain, { id: domainId }).set({ subregistryId }); + await context.db.update(schema.v2Domain, { id: domainId }).set({ subregistryId }); } }, ); // TODO: add this logic to Protocol Acceleration plugin // ponder.on( - // namespaceContract(pluginName, "Registry:ResolverUpdate"), + // namespaceContract(pluginName, "ENSv2Registry:ResolverUpdate"), // async ({ // context, // event, @@ -137,16 +137,16 @@ export default function () { // // update domain's resolver // const isDeletion = isAddressEqual(address, zeroAddress); // if (isDeletion) { - // await context.db.update(schema.domain, { id: domainId }).set({ resolverId: null }); + // await context.db.update(schema.v2Domain, { id: domainId }).set({ resolverId: null }); // } else { // const resolverId = makeResolverId({ chainId: context.chain.id, address: address }); - // await context.db.update(schema.domain, { id: domainId }).set({ resolverId }); + // await context.db.update(schema.v2Domain, { id: domainId }).set({ resolverId }); // } // }, // ); ponder.on( - namespaceContract(pluginName, "Registry:NameBurned"), + namespaceContract(pluginName, "ENSv2Registry:NameBurned"), async ({ context, event, @@ -163,8 +163,8 @@ export default function () { const registryAccountId = getThisAccountId(context, event); const domainId = makeENSv2DomainId(registryAccountId, canonicalId); - await context.db.delete(schema.domain, { id: domainId }); - // TODO: delete registration (?) + await context.db.delete(schema.v2Domain, { id: domainId }); + // TODO: delete registrations (?) }, ); @@ -182,14 +182,14 @@ export default function () { const domainId = makeENSv2DomainId(registryAccountId, canonicalId); // just update the owner, NameBurned handles existence - await context.db.update(schema.domain, { id: domainId }).set({ ownerId: owner }); + await context.db.update(schema.v2Domain, { id: domainId }).set({ ownerId: owner }); } ponder.on( - namespaceContract(pluginName, "Registry:TransferSingle"), + namespaceContract(pluginName, "ENSv2Registry:TransferSingle"), async ({ context, event }) => { const registryAccountId = getThisAccountId(context, event); - const registryId = makeRegistryContractId(registryAccountId); + const registryId = makeRegistryId(registryAccountId); // TODO(registry-announcement): ideally remove this const registry = await context.db.find(schema.registry, { id: registryId }); @@ -198,19 +198,15 @@ export default function () { await handleTransferSingle({ context, event }); }, ); - ponder.on(namespaceContract(pluginName, "Registry:TransferBatch"), async ({ context, event }) => { - const registryAccountId = getThisAccountId(context, event); - const registryId = makeRegistryContractId(registryAccountId); - - // TODO(registry-announcement): ideally remove this - const registry = await context.db.find(schema.registry, { id: registryId }); - if (registry === null) return; // no-op non-Registry ERC1155 Transfers - - for (const id of event.args.ids) { - await handleTransferSingle({ - context, - event: { ...event, args: { ...event.args, id } }, - }); - } - }); + ponder.on( + namespaceContract(pluginName, "ENSv2Registry:TransferBatch"), + async ({ context, event }) => { + for (const id of event.args.ids) { + await handleTransferSingle({ + context, + event: { ...event, args: { ...event.args, id } }, + }); + } + }, + ); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/EnhancedAccessControl.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/EnhancedAccessControl.ts similarity index 100% rename from apps/ensindexer/src/plugins/ensv2/handlers/EnhancedAccessControl.ts rename to apps/ensindexer/src/plugins/ensv2/handlers/ensv2/EnhancedAccessControl.ts diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index 92ea4ec68..e56403ee9 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -93,7 +93,10 @@ export default createPlugin({ ), contracts: { - [namespaceContract(pluginName, "Registry")]: { + //////////////////////////// + // ENSv2 Registry Contracts + //////////////////////////// + [namespaceContract(pluginName, "ENSv2Registry")]: { abi: RegistryABI, chain: [ensroot, namechain] .filter((ds) => !!ds) @@ -109,6 +112,10 @@ export default createPlugin({ {}, ), }, + + /////////////////////////////////// + // EnhancedAccessControl Contracts + /////////////////////////////////// [namespaceContract(pluginName, "EnhancedAccessControl")]: { abi: EnhancedAccessControlABI, chain: [ensroot, namechain] @@ -126,7 +133,9 @@ export default createPlugin({ ), }, - // index the ENSv1RegistryOld on ENS Root Chain + ////////////////////////////////////// + // ENSv1RegistryOld on ENS Root Chain + ////////////////////////////////////// [namespaceContract(pluginName, "ENSv1RegistryOld")]: { abi: ensroot.contracts.ENSv1RegistryOld.abi, chain: { @@ -138,7 +147,12 @@ export default createPlugin({ }, }, - // index ENSv1Registry on ENS Root Chain, Basenames, Lineanames + ////////////////////////////////////// + // ENSv1Registry on + // - ENS Root Chain + // - Basenames + // - Lineanames + ////////////////////////////////////// [namespaceContract(pluginName, "ENSv1Registry")]: { abi: ensroot.contracts.ENSv1Registry.abi, chain: { @@ -165,7 +179,11 @@ export default createPlugin({ }, }, - // index NameWrapper on ENS Root Chain, Lineanames + ////////////////////////////////////// + // NameWrapper on + // - ENS Root Chain + // - Lineanames + ////////////////////////////////////// [namespaceContract(pluginName, "NameWrapper")]: { abi: ensroot.contracts.NameWrapper.abi, chain: { diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/schemas/ensv2.schema.ts index 857939549..6f4a170fc 100644 --- a/packages/ensnode-schema/src/schemas/ensv2.schema.ts +++ b/packages/ensnode-schema/src/schemas/ensv2.schema.ts @@ -1,9 +1,11 @@ -import { onchainEnum, onchainTable, relations, uniqueIndex } from "ponder"; +import { index, onchainEnum, onchainTable, relations, uniqueIndex } from "ponder"; import type { Address } from "viem"; import type { ChainId, DomainId, + ENSv1DomainId, + ENSv2DomainId, EncodedReferrer, InterpretedLabel, LabelHash, @@ -14,18 +16,6 @@ import type { RegistryId, } from "@ensnode/ensnode-sdk"; -// Registry<->Domain is 1:1 -// Registry->Doimains is 1:many - -/** - * Polymorphism in this Drizzle v1 schema is acomplished with _Type and _Id columns. In the future, - * when ponder supports it, we can/should move to Drizzle v2 conditional relations to support - * polymorphism. - * - * In the future, when Ponder supports `check` constraints, we can include them for additional - * guarantees. - */ - /////////// // Account /////////// @@ -35,9 +25,8 @@ export const account = onchainTable("accounts", (t) => ({ })); export const account_relations = relations(account, ({ many }) => ({ - // registrations, - // dedicatedResolvers, - domains: many(domain), + registrations: many(registration, { relationName: "registrant" }), + domains: many(v2Domain), permissions: many(permissionsUser), })); @@ -45,31 +34,27 @@ export const account_relations = relations(account, ({ many }) => ({ // Registry //////////// -export const registryType = onchainEnum("RegistryType", ["RegistryContract", "ImplicitRegistry"]); - export const registry = onchainTable( "registries", (t) => ({ // see RegistryId for guarantees id: t.text().primaryKey().$type(), - type: registryType().notNull(), - // has contract AccountId (RegistryContract) - chainId: t.integer().$type(), - address: t.hex().$type
(), + chainId: t.integer().notNull().$type(), + address: t.hex().notNull().$type
(), }), (t) => ({ - // + byId: uniqueIndex().on(t.chainId, t.address), }), ); export const relations_registry = relations(registry, ({ one, many }) => ({ - domain: one(domain, { + domain: one(v2Domain, { relationName: "subregistry", fields: [registry.id], - references: [domain.registryId], + references: [v2Domain.registryId], }), - domains: many(domain, { relationName: "registry" }), + domains: many(v2Domain, { relationName: "registry" }), permissions: one(permissions, { relationName: "permissions", fields: [registry.chainId, registry.address], @@ -77,52 +62,103 @@ export const relations_registry = relations(registry, ({ one, many }) => ({ }), })); -////////// -// Domain -////////// +/////////// +// Domains +/////////// -export const domain = onchainTable( - "domains", +export const v1Domain = onchainTable( + "v1_domains", (t) => ({ - // see DomainId for guarantees - id: t.text().primaryKey().$type(), + // keyed by node, see ENSv1DomainId for guarantees. + id: t.text().primaryKey().$type(), - // belongs to registry - registryId: t.text().notNull().$type(), - labelHash: t.hex().notNull().$type(), + // must have a parent v1Domain (note: root node does not exist in index) + parentId: t.text().notNull().$type(), // may have an owner ownerId: t.hex().$type
(), - // may have one subregistry - subregistryId: t.text().$type(), + // represents a labelHash + labelHash: t.hex().notNull().$type(), // NOTE: Domain-Resolver Relations tracked via Protocol Acceleration plugin }), (t) => ({ - // + byParent: index().on(t.parentId), + byOwner: index().on(t.ownerId), }), ); -export const relations_domain = relations(domain, ({ one, many }) => ({ +export const relations_v1Domain = relations(v1Domain, ({ one, many }) => ({ + // v1Domain + parent: one(v1Domain, { + fields: [v1Domain.parentId], + references: [v1Domain.id], + }), + children: many(v1Domain, { relationName: "parent" }), + + // shared owner: one(account, { relationName: "owner", - fields: [domain.ownerId], + fields: [v1Domain.ownerId], references: [account.id], }), + label: one(label, { + relationName: "label", + fields: [v1Domain.labelHash], + references: [label.labelHash], + }), + registrations: many(registration), +})); + +export const v2Domain = onchainTable( + "v2_domains", + (t) => ({ + // see ENSv2DomainId for guarantees + id: t.text().primaryKey().$type(), + + // belongs to registry + registryId: t.text().notNull().$type(), + + // may have one subregistry + subregistryId: t.text().$type(), + + // may have an owner + ownerId: t.hex().$type
(), + + // represents a labelHash + labelHash: t.hex().notNull().$type(), + + // NOTE: Domain-Resolver Relations tracked via Protocol Acceleration plugin + }), + (t) => ({ + byRegistry: index().on(t.registryId), + byOwner: index().on(t.ownerId), + }), +); + +export const relations_v2Domain = relations(v2Domain, ({ one, many }) => ({ + // v2Domain registry: one(registry, { relationName: "registry", - fields: [domain.registryId], + fields: [v2Domain.registryId], references: [registry.id], }), subregistry: one(registry, { relationName: "subregistry", - fields: [domain.subregistryId], + fields: [v2Domain.subregistryId], references: [registry.id], }), + + // shared + owner: one(account, { + relationName: "owner", + fields: [v2Domain.ownerId], + references: [account.id], + }), label: one(label, { relationName: "label", - fields: [domain.labelHash], + fields: [v2Domain.labelHash], references: [label.labelHash], }), registrations: many(registration), @@ -162,7 +198,7 @@ export const registration = onchainTable( // references registrant registrantId: t.hex().$type
(), - // references referrer + // may have a referrer referrer: t.hex().$type(), // may have fuses (NameWrapper, Wrapped BaseRegistrar) @@ -181,13 +217,21 @@ export const registration = onchainTable( ); export const registration_relations = relations(registration, ({ one, many }) => ({ - domain: one(domain, { + // belongs to either v1Domain or v2Domain + v1Domain: one(v1Domain, { fields: [registration.domainId], - references: [domain.id], + references: [v1Domain.id], }), + v2Domain: one(v2Domain, { + fields: [registration.domainId], + references: [v2Domain.id], + }), + + // has one registrant registrant: one(account, { fields: [registration.registrantId], references: [account.id], + relationName: "registrant", }), })); @@ -281,5 +325,5 @@ export const label = onchainTable("labels", (t) => ({ })); export const label_relations = relations(label, ({ many }) => ({ - domains: many(domain), + domains: many(v2Domain), })); diff --git a/packages/ensnode-sdk/src/ensv2/ids-lib.ts b/packages/ensnode-sdk/src/ensv2/ids-lib.ts index 5f98e9dce..051a0e3d0 100644 --- a/packages/ensnode-sdk/src/ensv2/ids-lib.ts +++ b/packages/ensnode-sdk/src/ensv2/ids-lib.ts @@ -13,12 +13,11 @@ import type { DomainId, ENSv1DomainId, ENSv2DomainId, - ImplicitRegistryId, PermissionsId, PermissionsResourceId, PermissionsUserId, RegistrationId, - RegistryContractId, + RegistryId, ResolverId, ResolverRecordsId, } from "./ids"; @@ -26,13 +25,7 @@ import type { /** * Serializes and brands an AccountId as a RegistryId. */ -export const makeRegistryContractId = (accountId: AccountId) => - serializeAccountId(accountId) as RegistryContractId; - -/** - * Brands a node as an ImplicitRegistryId. - */ -export const makeImplicitRegistryId = (node: Node) => node as ImplicitRegistryId; +export const makeRegistryId = (accountId: AccountId) => serializeAccountId(accountId) as RegistryId; /** * Makes an ENSv1 Domain Id given the ENSv1 Domain's `node` diff --git a/packages/ensnode-sdk/src/ensv2/ids.ts b/packages/ensnode-sdk/src/ensv2/ids.ts index 3493643cf..b12483e32 100644 --- a/packages/ensnode-sdk/src/ensv2/ids.ts +++ b/packages/ensnode-sdk/src/ensv2/ids.ts @@ -1,21 +1,9 @@ -import type { Address, Hex } from "viem"; - import type { Node, SerializedAccountId } from "@ensnode/ensnode-sdk"; /** * Serialized CAIP-10 Asset ID that uniquely identifies a Registry contract. */ -export type RegistryContractId = string & { __brand: "RegistryContractId" }; - -/** - * Parent Node that uniquely identifies an Implicit Registry. - */ -export type ImplicitRegistryId = Hex & { __brand: "ImplicitRegistryId" }; - -/** - * A RegistryId is one of RegistryContractId or ImplicitRegistryId. - */ -export type RegistryId = RegistryContractId | ImplicitRegistryId; +export type RegistryId = string & { __brand: "RegistryContractId" }; /** * A Domain's Canonical Id is uint256(labelHash) with lower (right-most) 32 bits zero'd. diff --git a/packages/ensnode-sdk/src/shared/root-registry.ts b/packages/ensnode-sdk/src/shared/root-registry.ts index 17c175fd9..b820cf3c7 100644 --- a/packages/ensnode-sdk/src/shared/root-registry.ts +++ b/packages/ensnode-sdk/src/shared/root-registry.ts @@ -1,5 +1,5 @@ import { DatasourceNames, type ENSNamespaceId, getDatasource } from "@ensnode/datasources"; -import { type AccountId, accountIdEqual, makeRegistryContractId } from "@ensnode/ensnode-sdk"; +import { type AccountId, accountIdEqual, makeRegistryId } from "@ensnode/ensnode-sdk"; /** * TODO @@ -21,7 +21,7 @@ export const getRootRegistry = (namespace: ENSNamespaceId) => { * TODO */ export const getRootRegistryId = (namespace: ENSNamespaceId) => - makeRegistryContractId(getRootRegistry(namespace)); + makeRegistryId(getRootRegistry(namespace)); /** * TODO From 25f615c0c608e52a25c4d427c67ed97c8ee0644f Mon Sep 17 00:00:00 2001 From: shrugs Date: Wed, 19 Nov 2025 14:29:11 -0600 Subject: [PATCH 045/102] feat: separate v1 and v2 domains in datamodel --- .../src/graphql-api/lib/get-domain-by-id.ts | 0 apps/ensapi/src/graphql-api/schema/account.ts | 94 +++++++++++++++++-- apps/ensapi/src/graphql-api/schema/domain.ts | 64 ++++++++++++- apps/ensapi/src/graphql-api/schema/query.ts | 76 ++++++++------- 4 files changed, 186 insertions(+), 48 deletions(-) create mode 100644 apps/ensapi/src/graphql-api/lib/get-domain-by-id.ts diff --git a/apps/ensapi/src/graphql-api/lib/get-domain-by-id.ts b/apps/ensapi/src/graphql-api/lib/get-domain-by-id.ts new file mode 100644 index 000000000..e69de29bb diff --git a/apps/ensapi/src/graphql-api/schema/account.ts b/apps/ensapi/src/graphql-api/schema/account.ts index 380e9fa4e..babe5d73c 100644 --- a/apps/ensapi/src/graphql-api/schema/account.ts +++ b/apps/ensapi/src/graphql-api/schema/account.ts @@ -1,13 +1,13 @@ import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay"; import type { Address } from "viem"; -import type { DomainId, ResolverId } from "@ensnode/ensnode-sdk"; +import type { ENSv1DomainId, ENSv2DomainId, ResolverId } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-id"; import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; import { cursors } from "@/graphql-api/schema/cursors"; -import { DomainInterfaceRef } from "@/graphql-api/schema/domain"; +import { ENSv1DomainRef, ENSv2DomainRef } from "@/graphql-api/schema/domain"; import { ResolverRef } from "@/graphql-api/schema/resolver"; import { db } from "@/lib/db"; @@ -48,21 +48,97 @@ AccountRef.implement({ /////////////////// // Account.domains /////////////////// - domains: t.connection({ + // domains: t.connection({ + // description: "TODO", + // type: DomainInterfaceRef, + // resolve: (parent, args, context) => + // // TODO(dataloader) — confirm this is dataloaded? + // resolveCursorConnection( + // { ...DEFAULT_CONNECTION_ARGS, args }, + // async ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => { + // const v1Domains = db + // .select({ id: schema.v1Domain.id }) + // .from(schema.v1Domain) + // .where(eq(schema.v1Domain.ownerId, parent.id)) + // .leftJoin(schema.label, eq(schema.v1Domain.labelHash, schema.label.labelHash)); + + // const v2Domains = db + // .select({ id: schema.v2Domain.id }) + // .from(schema.v2Domain) + // .where(eq(schema.v2Domain.ownerId, parent.id)) + // .leftJoin(schema.label, eq(schema.v2Domain.labelHash, schema.label.labelHash)); + + // // @ts-expect-error ignore that id column types differ + // const domains = db.$with("domains").as(unionAll(v1Domains, v2Domains)); + + // const results = await db + // .with(domains) + // .select() + // .from(domains) + // .where( + // and( + // ...[ + // // TODO: using any because drizzle infers id as ENSv1DomainId + // before && lt(domains.id, cursors.decode(before)), + // after && gt(domains.id, cursors.decode(after)), + // ].filter((c) => !!c), + // ), + // ) + // .orderBy(inverted ? desc(domains.id) : asc(domains.id)) + // .limit(limit); + + // return rejectAnyErrors( + // DomainInterfaceRef.getDataloader(context).loadMany( + // results.map((result) => result.id), + // ), + // ); + // }, + // ), + // }), + + ///////////////////// + // Account.v1Domains + ///////////////////// + v1Domains: t.connection({ description: "TODO", - type: DomainInterfaceRef, - resolve: (parent, args) => - // TODO(dataloader) — confirm this is dataloaded? + type: ENSv1DomainRef, + resolve: (parent, args, context) => + resolveCursorConnection( + { ...DEFAULT_CONNECTION_ARGS, args }, + ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => + db.query.v1Domain.findMany({ + where: (t, { lt, gt, and, eq }) => + and( + ...[ + eq(t.ownerId, parent.id), + before !== undefined && lt(t.id, cursors.decode(before)), + after !== undefined && gt(t.id, cursors.decode(after)), + ].filter((c) => !!c), + ), + orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), + limit, + with: { label: true }, + }), + ), + }), + + ///////////////////// + // Account.v2Domains + ///////////////////// + v2Domains: t.connection({ + description: "TODO", + type: ENSv2DomainRef, + resolve: (parent, args, context) => resolveCursorConnection( { ...DEFAULT_CONNECTION_ARGS, args }, ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => - db.query.domain.findMany({ + db.query.v2Domain.findMany({ where: (t, { lt, gt, and, eq }) => and( ...[ eq(t.ownerId, parent.id), - before !== undefined && lt(t.id, cursors.decode(before)), - after !== undefined && gt(t.id, cursors.decode(after)), + before !== undefined && lt(t.id, cursors.decode(before)), + after !== undefined && gt(t.id, cursors.decode(after)), ].filter((c) => !!c), ), orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), diff --git a/apps/ensapi/src/graphql-api/schema/domain.ts b/apps/ensapi/src/graphql-api/schema/domain.ts index d8308f60e..8e9dc35d4 100644 --- a/apps/ensapi/src/graphql-api/schema/domain.ts +++ b/apps/ensapi/src/graphql-api/schema/domain.ts @@ -1,3 +1,5 @@ +import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay"; + import { type DomainId, type ENSv1DomainId, @@ -12,6 +14,8 @@ import { getModelId } from "@/graphql-api/lib/get-id"; import { getLatestRegistration } from "@/graphql-api/lib/get-latest-registration"; import { rejectAnyErrors } from "@/graphql-api/lib/reject-any-errors"; import { AccountRef } from "@/graphql-api/schema/account"; +import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; +import { cursors } from "@/graphql-api/schema/cursors"; import { type Registration, RegistrationInterfaceRef } from "@/graphql-api/schema/registration"; import { RegistryRef } from "@/graphql-api/schema/registry"; import { ResolverRef } from "@/graphql-api/schema/resolver"; @@ -33,6 +37,7 @@ export const ENSv1DomainRef = builder.loadableObjectRef("v1Domain", { cacheResolved: true, sort: true, }); + export const ENSv2DomainRef = builder.loadableObjectRef("v2Domain", { load: (ids: ENSv2DomainId[]) => db.query.v2Domain.findMany({ @@ -43,7 +48,26 @@ export const ENSv2DomainRef = builder.loadableObjectRef("v2Domain", { cacheResolved: true, sort: true, }); -export const DomainInterfaceRef = builder.interfaceRef("Domain"); + +export const DomainInterfaceRef = builder.loadableInterfaceRef("Domain", { + load: async (ids: DomainId[]) => { + const [v1Domains, v2Domains] = await Promise.all([ + db.query.v1Domain.findMany({ + where: (t, { inArray }) => inArray(t.id, ids as any), // ignore downcast to ENSv1DomainId + with: { label: true }, + }), + db.query.v2Domain.findMany({ + where: (t, { inArray }) => inArray(t.id, ids as any), // ignore downcast to ENSv2DomainId + with: { label: true }, + }), + ]); + + return [...v1Domains, ...v2Domains]; + }, + toKey: getModelId, + cacheResolved: true, + sort: true, +}); export type ENSv1Domain = Exclude; export type ENSv2Domain = Exclude; @@ -193,20 +217,52 @@ DomainInterfaceRef.implement({ ////////////////////////////// // ENSv1Domain Implementation ////////////////////////////// - ENSv1DomainRef.implement({ description: "TODO", interfaces: [DomainInterfaceRef], isTypeOf: (domain) => isENSv1Domain(domain as Domain), fields: (t) => ({ - // + ////////////////////// + // ENSv1Domain.parent + ////////////////////// + parent: t.field({ + description: "TODO", + type: ENSv1DomainRef, + nullable: true, + resolve: (parent) => parent.parentId, + }), + + //////////////////////// + // ENSv1Domain.children + //////////////////////// + children: t.connection({ + description: "TODO", + type: ENSv1DomainRef, + resolve: (parent, args, context) => + resolveCursorConnection( + { ...DEFAULT_CONNECTION_ARGS, args }, + ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => + db.query.v1Domain.findMany({ + where: (t, { lt, gt, and, eq }) => + and( + ...[ + eq(t.parentId, parent.id), + before !== undefined && lt(t.id, cursors.decode(before)), + after !== undefined && gt(t.id, cursors.decode(after)), + ].filter((c) => !!c), + ), + orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), + limit, + with: { label: true }, + }), + ), + }), }), }); ////////////////////////////// // ENSv2Domain Implementation ////////////////////////////// - ENSv2DomainRef.implement({ description: "TODO", interfaces: [DomainInterfaceRef], diff --git a/apps/ensapi/src/graphql-api/schema/query.ts b/apps/ensapi/src/graphql-api/schema/query.ts index cd94bfd6e..cf0687cc0 100644 --- a/apps/ensapi/src/graphql-api/schema/query.ts +++ b/apps/ensapi/src/graphql-api/schema/query.ts @@ -4,6 +4,8 @@ import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@poth import { type DomainId, + type ENSv1DomainId, + type ENSv2DomainId, getRootRegistryId, makePermissionsId, makeRegistryId, @@ -15,41 +17,46 @@ import { getDomainIdByInterpretedName } from "@/graphql-api/lib/get-domain-by-fq import { AccountRef } from "@/graphql-api/schema/account"; import { AccountIdInput } from "@/graphql-api/schema/account-id"; import { cursors } from "@/graphql-api/schema/cursors"; -import { DomainIdInput, DomainInterfaceRef } from "@/graphql-api/schema/domain"; +import { + DomainIdInput, + DomainInterfaceRef, + ENSv1DomainRef, + ENSv2DomainRef, +} from "@/graphql-api/schema/domain"; import { PermissionsRef } from "@/graphql-api/schema/permissions"; -import { RegistryIdInput, RegistryInterfaceRef } from "@/graphql-api/schema/registry"; +import { RegistryIdInput, RegistryRef } from "@/graphql-api/schema/registry"; import { ResolverIdInput, ResolverRef } from "@/graphql-api/schema/resolver"; import { db } from "@/lib/db"; builder.queryType({ fields: (t) => ({ // testing, delete this - domains: t.connection({ - description: "TODO", - type: DomainInterfaceRef, - resolve: (parent, args, context) => - resolveCursorConnection( - { - args, - toCursor: (domain) => cursors.encode(domain.id), - defaultSize: 100, - maxSize: 1000, - }, - ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => - db.query.domain.findMany({ - where: (t, { lt, gt, and }) => - and( - ...[ - before !== undefined && lt(t.id, cursors.decode(before)), - after !== undefined && gt(t.id, cursors.decode(after)), - ].filter((c) => !!c), - ), - orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), - limit, - with: { label: true }, - }), - ), - }), + // domains: t.connection({ + // description: "TODO", + // type: DomainInterfaceRef, + // resolve: (parent, args, context) => + // resolveCursorConnection( + // { + // args, + // toCursor: (domain) => cursors.encode(domain.id), + // defaultSize: 100, + // maxSize: 1000, + // }, + // ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => + // db.query.domain.findMany({ + // where: (t, { lt, gt, and }) => + // and( + // ...[ + // before !== undefined && lt(t.id, cursors.decode(before)), + // after !== undefined && gt(t.id, cursors.decode(after)), + // ].filter((c) => !!c), + // ), + // orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), + // limit, + // with: { label: true }, + // }), + // ), + // }), ////////////////////////////////// // Get Domain by Name or DomainId @@ -72,7 +79,7 @@ builder.queryType({ description: "TODO", type: AccountRef, args: { address: t.arg({ type: "Address", required: true }) }, - resolve: async (parent, args, ctx, info) => args.address, + resolve: async (parent, args, context, info) => args.address, }), ////////////////////// @@ -80,11 +87,10 @@ builder.queryType({ ////////////////////// registry: t.field({ description: "TODO", - type: RegistryInterfaceRef, + type: RegistryRef, args: { by: t.arg({ type: RegistryIdInput, required: true }) }, - resolve: async (parent, args, ctx, info) => { + resolve: async (parent, args, context, info) => { if (args.by.id !== undefined) return args.by.id; - if (args.by.implicit !== undefined) return args.by.implicit.parent; return makeRegistryId(args.by.contract); }, }), @@ -96,7 +102,7 @@ builder.queryType({ description: "TODO", type: ResolverRef, args: { by: t.arg({ type: ResolverIdInput, required: true }) }, - resolve: async (parent, args, ctx, info) => { + resolve: async (parent, args, context, info) => { if (args.by.id !== undefined) return args.by.id; return makeResolverId(args.by.contract); }, @@ -109,7 +115,7 @@ builder.queryType({ description: "TODO", type: PermissionsRef, args: { for: t.arg({ type: AccountIdInput, required: true }) }, - resolve: (parent, args, ctx, info) => makePermissionsId(args.for), + resolve: (parent, args, context, info) => makePermissionsId(args.for), }), ///////////////////// @@ -117,7 +123,7 @@ builder.queryType({ ///////////////////// root: t.field({ description: "TODO", - type: RegistryInterfaceRef, + type: RegistryRef, nullable: false, resolve: () => getRootRegistryId(config.namespace), }), From 0cbec9a0db8d72870d087bbbc3094cce8934b0f8 Mon Sep 17 00:00:00 2001 From: shrugs Date: Wed, 19 Nov 2025 17:03:35 -0600 Subject: [PATCH 046/102] feat: polymorphic cursor for account.doamins --- apps/ensapi/src/graphql-api/schema/account.ts | 131 ++++++------------ apps/ensapi/src/graphql-api/schema/domain.ts | 7 +- apps/ensapi/src/graphql-api/schema/query.ts | 78 +++++++---- 3 files changed, 94 insertions(+), 122 deletions(-) diff --git a/apps/ensapi/src/graphql-api/schema/account.ts b/apps/ensapi/src/graphql-api/schema/account.ts index babe5d73c..196d95cda 100644 --- a/apps/ensapi/src/graphql-api/schema/account.ts +++ b/apps/ensapi/src/graphql-api/schema/account.ts @@ -1,13 +1,17 @@ import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay"; +import { and, asc, desc, eq, gt, lt } from "drizzle-orm"; +import { unionAll } from "drizzle-orm/pg-core"; import type { Address } from "viem"; -import type { ENSv1DomainId, ENSv2DomainId, ResolverId } from "@ensnode/ensnode-sdk"; +import * as schema from "@ensnode/ensnode-schema"; +import type { ResolverId } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-id"; +import { rejectAnyErrors } from "@/graphql-api/lib/reject-any-errors"; import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; import { cursors } from "@/graphql-api/schema/cursors"; -import { ENSv1DomainRef, ENSv2DomainRef } from "@/graphql-api/schema/domain"; +import { DomainInterfaceRef } from "@/graphql-api/schema/domain"; import { ResolverRef } from "@/graphql-api/schema/resolver"; import { db } from "@/lib/db"; @@ -48,103 +52,50 @@ AccountRef.implement({ /////////////////// // Account.domains /////////////////// - // domains: t.connection({ - // description: "TODO", - // type: DomainInterfaceRef, - // resolve: (parent, args, context) => - // // TODO(dataloader) — confirm this is dataloaded? - // resolveCursorConnection( - // { ...DEFAULT_CONNECTION_ARGS, args }, - // async ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => { - // const v1Domains = db - // .select({ id: schema.v1Domain.id }) - // .from(schema.v1Domain) - // .where(eq(schema.v1Domain.ownerId, parent.id)) - // .leftJoin(schema.label, eq(schema.v1Domain.labelHash, schema.label.labelHash)); - - // const v2Domains = db - // .select({ id: schema.v2Domain.id }) - // .from(schema.v2Domain) - // .where(eq(schema.v2Domain.ownerId, parent.id)) - // .leftJoin(schema.label, eq(schema.v2Domain.labelHash, schema.label.labelHash)); - - // // @ts-expect-error ignore that id column types differ - // const domains = db.$with("domains").as(unionAll(v1Domains, v2Domains)); - - // const results = await db - // .with(domains) - // .select() - // .from(domains) - // .where( - // and( - // ...[ - // // TODO: using any because drizzle infers id as ENSv1DomainId - // before && lt(domains.id, cursors.decode(before)), - // after && gt(domains.id, cursors.decode(after)), - // ].filter((c) => !!c), - // ), - // ) - // .orderBy(inverted ? desc(domains.id) : asc(domains.id)) - // .limit(limit); - - // return rejectAnyErrors( - // DomainInterfaceRef.getDataloader(context).loadMany( - // results.map((result) => result.id), - // ), - // ); - // }, - // ), - // }), - - ///////////////////// - // Account.v1Domains - ///////////////////// - v1Domains: t.connection({ + domains: t.connection({ description: "TODO", - type: ENSv1DomainRef, + type: DomainInterfaceRef, resolve: (parent, args, context) => resolveCursorConnection( { ...DEFAULT_CONNECTION_ARGS, args }, - ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => - db.query.v1Domain.findMany({ - where: (t, { lt, gt, and, eq }) => - and( - ...[ - eq(t.ownerId, parent.id), - before !== undefined && lt(t.id, cursors.decode(before)), - after !== undefined && gt(t.id, cursors.decode(after)), - ].filter((c) => !!c), - ), - orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), - limit, - with: { label: true }, - }), - ), - }), + async ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => { + const v1Domains = db + .select({ id: schema.v1Domain.id }) + .from(schema.v1Domain) + .where(eq(schema.v1Domain.ownerId, parent.id)) + .leftJoin(schema.label, eq(schema.v1Domain.labelHash, schema.label.labelHash)); - ///////////////////// - // Account.v2Domains - ///////////////////// - v2Domains: t.connection({ - description: "TODO", - type: ENSv2DomainRef, - resolve: (parent, args, context) => - resolveCursorConnection( - { ...DEFAULT_CONNECTION_ARGS, args }, - ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => - db.query.v2Domain.findMany({ - where: (t, { lt, gt, and, eq }) => + const v2Domains = db + .select({ id: schema.v2Domain.id }) + .from(schema.v2Domain) + .where(eq(schema.v2Domain.ownerId, parent.id)) + .leftJoin(schema.label, eq(schema.v2Domain.labelHash, schema.label.labelHash)); + + // use any to ignore id column type mismatch (ENSv1DomainId & ENSv2DomainId) + const domains = db.$with("domains").as(unionAll(v1Domains, v2Domains as any)); + + const results = await db + .with(domains) + .select() + .from(domains) + .where( and( ...[ - eq(t.ownerId, parent.id), - before !== undefined && lt(t.id, cursors.decode(before)), - after !== undefined && gt(t.id, cursors.decode(after)), + // TODO: using any because drizzle infers id as ENSv1DomainId + before && lt(domains.id, cursors.decode(before)), + after && gt(domains.id, cursors.decode(after)), ].filter((c) => !!c), ), - orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), - limit, - with: { label: true }, - }), + ) + .orderBy(inverted ? desc(domains.id) : asc(domains.id)) + .limit(limit); + + return rejectAnyErrors( + DomainInterfaceRef.getDataloader(context).loadMany( + results.map((result) => result.id), + ), + ); + }, ), }), diff --git a/apps/ensapi/src/graphql-api/schema/domain.ts b/apps/ensapi/src/graphql-api/schema/domain.ts index 8e9dc35d4..6160645b6 100644 --- a/apps/ensapi/src/graphql-api/schema/domain.ts +++ b/apps/ensapi/src/graphql-api/schema/domain.ts @@ -27,7 +27,7 @@ const isENSv1Domain = (domain: Domain): domain is ENSv1Domain => "parentId" in d // Refs ////////////////////// -export const ENSv1DomainRef = builder.loadableObjectRef("v1Domain", { +export const ENSv1DomainRef = builder.loadableObjectRef("ENSv1Domain", { load: (ids: ENSv1DomainId[]) => db.query.v1Domain.findMany({ where: (t, { inArray }) => inArray(t.id, ids), @@ -38,7 +38,7 @@ export const ENSv1DomainRef = builder.loadableObjectRef("v1Domain", { sort: true, }); -export const ENSv2DomainRef = builder.loadableObjectRef("v2Domain", { +export const ENSv2DomainRef = builder.loadableObjectRef("ENSv2Domain", { load: (ids: ENSv2DomainId[]) => db.query.v2Domain.findMany({ where: (t, { inArray }) => inArray(t.id, ids), @@ -50,7 +50,7 @@ export const ENSv2DomainRef = builder.loadableObjectRef("v2Domain", { }); export const DomainInterfaceRef = builder.loadableInterfaceRef("Domain", { - load: async (ids: DomainId[]) => { + load: async (ids: DomainId[]): Promise<(ENSv1Domain | ENSv2Domain)[]> => { const [v1Domains, v2Domains] = await Promise.all([ db.query.v1Domain.findMany({ where: (t, { inArray }) => inArray(t.id, ids as any), // ignore downcast to ENSv1DomainId @@ -76,7 +76,6 @@ export type Domain = Exclude; ////////////////////////////// // DomainInterface Implementation ////////////////////////////// - DomainInterfaceRef.implement({ description: "a Domain", fields: (t) => ({ diff --git a/apps/ensapi/src/graphql-api/schema/query.ts b/apps/ensapi/src/graphql-api/schema/query.ts index cf0687cc0..5f78712a7 100644 --- a/apps/ensapi/src/graphql-api/schema/query.ts +++ b/apps/ensapi/src/graphql-api/schema/query.ts @@ -3,7 +3,6 @@ import config from "@/config"; import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay"; import { - type DomainId, type ENSv1DomainId, type ENSv2DomainId, getRootRegistryId, @@ -16,6 +15,7 @@ import { builder } from "@/graphql-api/builder"; import { getDomainIdByInterpretedName } from "@/graphql-api/lib/get-domain-by-fqdn"; import { AccountRef } from "@/graphql-api/schema/account"; import { AccountIdInput } from "@/graphql-api/schema/account-id"; +import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; import { cursors } from "@/graphql-api/schema/cursors"; import { DomainIdInput, @@ -30,33 +30,55 @@ import { db } from "@/lib/db"; builder.queryType({ fields: (t) => ({ - // testing, delete this - // domains: t.connection({ - // description: "TODO", - // type: DomainInterfaceRef, - // resolve: (parent, args, context) => - // resolveCursorConnection( - // { - // args, - // toCursor: (domain) => cursors.encode(domain.id), - // defaultSize: 100, - // maxSize: 1000, - // }, - // ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => - // db.query.domain.findMany({ - // where: (t, { lt, gt, and }) => - // and( - // ...[ - // before !== undefined && lt(t.id, cursors.decode(before)), - // after !== undefined && gt(t.id, cursors.decode(after)), - // ].filter((c) => !!c), - // ), - // orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), - // limit, - // with: { label: true }, - // }), - // ), - // }), + ///////////////////// + // Query.v1Domains (Testing) + ///////////////////// + v1Domains: t.connection({ + description: "TODO", + type: ENSv1DomainRef, + resolve: (parent, args, context) => + resolveCursorConnection( + { ...DEFAULT_CONNECTION_ARGS, args }, + ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => + db.query.v1Domain.findMany({ + where: (t, { lt, gt, and }) => + and( + ...[ + before !== undefined && lt(t.id, cursors.decode(before)), + after !== undefined && gt(t.id, cursors.decode(after)), + ].filter((c) => !!c), + ), + orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), + limit, + with: { label: true }, + }), + ), + }), + + ///////////////////// + // Query.v2Domains (Testing) + ///////////////////// + v2Domains: t.connection({ + description: "TODO", + type: ENSv2DomainRef, + resolve: (parent, args, context) => + resolveCursorConnection( + { ...DEFAULT_CONNECTION_ARGS, args }, + ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => + db.query.v2Domain.findMany({ + where: (t, { lt, gt, and }) => + and( + ...[ + before !== undefined && lt(t.id, cursors.decode(before)), + after !== undefined && gt(t.id, cursors.decode(after)), + ].filter((c) => !!c), + ), + orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), + limit, + with: { label: true }, + }), + ), + }), ////////////////////////////////// // Get Domain by Name or DomainId From 3229ade7cc6bb99a2ac4a25bd5ef9ade145a165b Mon Sep 17 00:00:00 2001 From: shrugs Date: Thu, 20 Nov 2025 14:10:50 -0600 Subject: [PATCH 047/102] checkpoint --- .../src/graphql-api/lib/get-domain-by-fqdn.ts | 1 + apps/ensapi/src/graphql-api/schema/account.ts | 33 ++++- .../src/graphql-api/schema/permissions.ts | 73 +++++++++- apps/ensapi/src/graphql-api/schema/query.ts | 136 +++++++++++------- .../ensapi/src/graphql-api/schema/registry.ts | 4 +- .../graphql-api/schema/resolver-records.ts | 10 ++ .../ensapi/src/graphql-api/schema/resolver.ts | 47 +++++- .../handlers/ensv2/EnhancedAccessControl.ts | 7 + packages/datasources/src/ens-test-env.ts | 4 + packages/datasources/src/mainnet.ts | 4 + packages/datasources/src/sepolia.ts | 5 + packages/ensnode-sdk/src/ens/constants.ts | 5 + .../src/shared/datasources-with-resolvers.ts | 1 + .../ensnode-sdk/src/shared/root-registry.ts | 4 - 14 files changed, 271 insertions(+), 63 deletions(-) diff --git a/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts index 45c2bbd95..87972920b 100644 --- a/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts +++ b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts @@ -15,6 +15,7 @@ import { import { db } from "@/lib/db"; const ROOT_REGISTRY_ID = getRootRegistryId(config.namespace); +// const ENSV1_REGISTRY = /** * Gets the Domain addressed by `name`. diff --git a/apps/ensapi/src/graphql-api/schema/account.ts b/apps/ensapi/src/graphql-api/schema/account.ts index 196d95cda..590e3e6d4 100644 --- a/apps/ensapi/src/graphql-api/schema/account.ts +++ b/apps/ensapi/src/graphql-api/schema/account.ts @@ -4,7 +4,7 @@ import { unionAll } from "drizzle-orm/pg-core"; import type { Address } from "viem"; import * as schema from "@ensnode/ensnode-schema"; -import type { ResolverId } from "@ensnode/ensnode-sdk"; +import type { PermissionsUserId, ResolverId } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-id"; @@ -12,6 +12,7 @@ import { rejectAnyErrors } from "@/graphql-api/lib/reject-any-errors"; import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; import { cursors } from "@/graphql-api/schema/cursors"; import { DomainInterfaceRef } from "@/graphql-api/schema/domain"; +import { PermissionsUserRef } from "@/graphql-api/schema/permissions"; import { ResolverRef } from "@/graphql-api/schema/resolver"; import { db } from "@/lib/db"; @@ -99,6 +100,36 @@ AccountRef.implement({ ), }), + /////////////////////// + // Account.permissions + /////////////////////// + permissions: t.connection({ + description: "TODO", + type: PermissionsUserRef, + // TODO: allow permissions(in: { contract: { chainId, address } }) + // or permissions(type: 'Registry' | 'Resolver') + // and then join (chainId, address) against Registry/Resolver index to see what it refers to + // and then filter on that — pretty expensive-sounding + // args: {}, + resolve: (parent, args, context) => + resolveCursorConnection( + { ...DEFAULT_CONNECTION_ARGS, args }, + async ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => + db.query.permissionsUser.findMany({ + where: (t, { lt, gt, and, eq }) => + and( + ...[ + eq(t.user, parent.id), + before !== undefined && lt(t.id, cursors.decode(before)), + after !== undefined && gt(t.id, cursors.decode(after)), + ].filter((c) => !!c), + ), + orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), + limit, + }), + ), + }), + ////////////////////// // Account.registries ////////////////////// diff --git a/apps/ensapi/src/graphql-api/schema/permissions.ts b/apps/ensapi/src/graphql-api/schema/permissions.ts index bddf16144..dd7507d0c 100644 --- a/apps/ensapi/src/graphql-api/schema/permissions.ts +++ b/apps/ensapi/src/graphql-api/schema/permissions.ts @@ -1,6 +1,13 @@ import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay"; -import type { PermissionsId, PermissionsResourceId, PermissionsUserId } from "@ensnode/ensnode-sdk"; +import { + makePermissionsId, + makePermissionsResourceId, + type PermissionsId, + type PermissionsResourceId, + type PermissionsUserId, + ROOT_RESOURCE, +} from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-id"; @@ -50,16 +57,37 @@ export type PermissionsUserResource = Exclude< PermissionsRef.implement({ description: "Permissions", fields: (t) => ({ + //////////////////////////// + // Permissions.id + //////////////////////////// + id: t.field({ + description: "TODO", + type: "ID", + nullable: false, + resolve: (parent) => parent.id, + }), + //////////////////////// // Permissions.contract //////////////////////// contract: t.field({ - type: AccountIdRef, description: "TODO", + type: AccountIdRef, nullable: false, resolve: ({ chainId, address }) => ({ chainId, address }), }), + //////////////////// + // Permissions.root + //////////////////// + root: t.field({ + description: "TODO", + type: PermissionsResourceRef, + nullable: false, + resolve: ({ chainId, address }) => + makePermissionsResourceId({ chainId, address }, ROOT_RESOURCE), + }), + ///////////////////////// // Permissions.resources ///////////////////////// @@ -94,6 +122,26 @@ PermissionsRef.implement({ PermissionsResourceRef.implement({ description: "PermissionsResource", fields: (t) => ({ + //////////////////////////// + // PermissionsResource.id + //////////////////////////// + id: t.field({ + description: "TODO", + type: "ID", + nullable: false, + resolve: (parent) => parent.id, + }), + + /////////////////////////////////// + // PermissionsResource.permissions + /////////////////////////////////// + permissions: t.field({ + description: "TODO", + type: PermissionsRef, + nullable: false, + resolve: ({ chainId, address }) => makePermissionsId({ chainId, address }), + }), + //////////////////////////////// // PermissionsResource.resource //////////////////////////////// @@ -139,6 +187,27 @@ PermissionsResourceRef.implement({ PermissionsUserRef.implement({ description: "PermissionsUser", fields: (t) => ({ + //////////////////////////// + // PermissionsUser.id + //////////////////////////// + id: t.field({ + description: "TODO", + type: "ID", + nullable: false, + resolve: (parent) => parent.id, + }), + + //////////////////////////// + // PermissionsUser.resource + //////////////////////////// + resource: t.field({ + description: "TODO", + type: PermissionsResourceRef, + nullable: false, + resolve: ({ chainId, address, resource }) => + makePermissionsResourceId({ chainId, address }, resource), + }), + //////////////////////// // PermissionsUser.user //////////////////////// diff --git a/apps/ensapi/src/graphql-api/schema/query.ts b/apps/ensapi/src/graphql-api/schema/query.ts index 5f78712a7..04de7493a 100644 --- a/apps/ensapi/src/graphql-api/schema/query.ts +++ b/apps/ensapi/src/graphql-api/schema/query.ts @@ -9,6 +9,7 @@ import { makePermissionsId, makeRegistryId, makeResolverId, + type ResolverId, } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; @@ -28,56 +29,85 @@ import { RegistryIdInput, RegistryRef } from "@/graphql-api/schema/registry"; import { ResolverIdInput, ResolverRef } from "@/graphql-api/schema/resolver"; import { db } from "@/lib/db"; +// don't want them to get familiar/accustom to these methods until their necessity is certain +const INCLUDE_DEV_METHODS = process.env.NODE_ENV === "development"; + builder.queryType({ fields: (t) => ({ - ///////////////////// - // Query.v1Domains (Testing) - ///////////////////// - v1Domains: t.connection({ - description: "TODO", - type: ENSv1DomainRef, - resolve: (parent, args, context) => - resolveCursorConnection( - { ...DEFAULT_CONNECTION_ARGS, args }, - ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => - db.query.v1Domain.findMany({ - where: (t, { lt, gt, and }) => - and( - ...[ - before !== undefined && lt(t.id, cursors.decode(before)), - after !== undefined && gt(t.id, cursors.decode(after)), - ].filter((c) => !!c), - ), - orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), - limit, - with: { label: true }, - }), - ), - }), + ...(INCLUDE_DEV_METHODS && { + ///////////////////////////// + // Query.v1Domains (Testing) + ///////////////////////////// + v1Domains: t.connection({ + description: "TODO", + type: ENSv1DomainRef, + resolve: (parent, args, context) => + resolveCursorConnection( + { ...DEFAULT_CONNECTION_ARGS, args }, + ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => + db.query.v1Domain.findMany({ + where: (t, { lt, gt, and }) => + and( + ...[ + before !== undefined && lt(t.id, cursors.decode(before)), + after !== undefined && gt(t.id, cursors.decode(after)), + ].filter((c) => !!c), + ), + orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), + limit, + with: { label: true }, + }), + ), + }), - ///////////////////// - // Query.v2Domains (Testing) - ///////////////////// - v2Domains: t.connection({ - description: "TODO", - type: ENSv2DomainRef, - resolve: (parent, args, context) => - resolveCursorConnection( - { ...DEFAULT_CONNECTION_ARGS, args }, - ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => - db.query.v2Domain.findMany({ - where: (t, { lt, gt, and }) => - and( - ...[ - before !== undefined && lt(t.id, cursors.decode(before)), - after !== undefined && gt(t.id, cursors.decode(after)), - ].filter((c) => !!c), - ), - orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), - limit, - with: { label: true }, - }), - ), + ///////////////////////////// + // Query.v2Domains (Testing) + ///////////////////////////// + v2Domains: t.connection({ + description: "TODO", + type: ENSv2DomainRef, + resolve: (parent, args, context) => + resolveCursorConnection( + { ...DEFAULT_CONNECTION_ARGS, args }, + ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => + db.query.v2Domain.findMany({ + where: (t, { lt, gt, and }) => + and( + ...[ + before !== undefined && lt(t.id, cursors.decode(before)), + after !== undefined && gt(t.id, cursors.decode(after)), + ].filter((c) => !!c), + ), + orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), + limit, + with: { label: true }, + }), + ), + }), + + ///////////////////////////// + // Query.resolvers (Testing) + ///////////////////////////// + resolvers: t.connection({ + description: "TODO", + type: ResolverRef, + resolve: (parent, args, context) => + resolveCursorConnection( + { ...DEFAULT_CONNECTION_ARGS, args }, + ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => + db.query.resolver.findMany({ + where: (t, { lt, gt, and }) => + and( + ...[ + before !== undefined && lt(t.id, cursors.decode(before)), + after !== undefined && gt(t.id, cursors.decode(after)), + ].filter((c) => !!c), + ), + orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), + limit, + }), + ), + }), }), ////////////////////////////////// @@ -104,9 +134,9 @@ builder.queryType({ resolve: async (parent, args, context, info) => args.address, }), - ////////////////////// - // Get Registry by Id - ////////////////////// + /////////////////////////////////// + // Get Registry by Id or AccountId + /////////////////////////////////// registry: t.field({ description: "TODO", type: RegistryRef, @@ -117,9 +147,9 @@ builder.queryType({ }, }), - ////////////////////// - // Get Resolver by Id - ////////////////////// + /////////////////////////////////// + // Get Resolver by Id or AccountId + /////////////////////////////////// resolver: t.field({ description: "TODO", type: ResolverRef, diff --git a/apps/ensapi/src/graphql-api/schema/registry.ts b/apps/ensapi/src/graphql-api/schema/registry.ts index 8a0061172..c029cb5a5 100644 --- a/apps/ensapi/src/graphql-api/schema/registry.ts +++ b/apps/ensapi/src/graphql-api/schema/registry.ts @@ -1,6 +1,6 @@ import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay"; -import type { ENSv2DomainId, RegistryId } from "@ensnode/ensnode-sdk"; +import { type ENSv2DomainId, makePermissionsId, type RegistryId } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-id"; @@ -82,7 +82,7 @@ RegistryRef.implement({ description: "TODO", type: PermissionsRef, // TODO: render a RegistryPermissions model that parses the backing permissions into registry-semantic roles - resolve: ({ chainId, address }) => null, + resolve: ({ chainId, address }) => makePermissionsId({ chainId, address }), }), ///////////////////// diff --git a/apps/ensapi/src/graphql-api/schema/resolver-records.ts b/apps/ensapi/src/graphql-api/schema/resolver-records.ts index cbebd0aad..440a9f1b0 100644 --- a/apps/ensapi/src/graphql-api/schema/resolver-records.ts +++ b/apps/ensapi/src/graphql-api/schema/resolver-records.ts @@ -29,6 +29,16 @@ ResolverRecordsRef.implement({ nullable: false, }), + //////////////////////// + // ResolverRecords.node + //////////////////////// + node: t.field({ + description: "TODO", + type: "Node", + nullable: false, + resolve: (parent) => parent.node, + }), + //////////////////////// // ResolverRecords.name //////////////////////// diff --git a/apps/ensapi/src/graphql-api/schema/resolver.ts b/apps/ensapi/src/graphql-api/schema/resolver.ts index be52b2e46..396e59948 100644 --- a/apps/ensapi/src/graphql-api/schema/resolver.ts +++ b/apps/ensapi/src/graphql-api/schema/resolver.ts @@ -1,17 +1,23 @@ +import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay"; import { namehash } from "viem"; import { + makePermissionsId, makeResolverRecordsId, NODE_ANY, type RequiredAndNotNull, type ResolverId, + type ResolverRecordsId, } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-id"; import { AccountRef } from "@/graphql-api/schema/account"; import { AccountIdInput, AccountIdRef } from "@/graphql-api/schema/account-id"; +import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; +import { cursors } from "@/graphql-api/schema/cursors"; import { NameOrNodeInput } from "@/graphql-api/schema/name-or-node"; +import { PermissionsRef } from "@/graphql-api/schema/permissions"; import { ResolverRecordsRef } from "@/graphql-api/schema/resolver-records"; import { db } from "@/lib/db"; @@ -63,10 +69,37 @@ ResolverRef.implement({ resolve: ({ chainId, address }) => ({ chainId, address }), }), + //////////////////// + // Resolver.records + //////////////////// + records: t.connection({ + description: "TODO", + type: ResolverRecordsRef, + resolve: (parent, args, context) => + resolveCursorConnection( + { ...DEFAULT_CONNECTION_ARGS, args }, + ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => + db.query.resolverRecords.findMany({ + where: (t, { lt, gt, and, eq }) => + and( + ...[ + eq(t.chainId, parent.chainId), + eq(t.address, parent.address), + before !== undefined && lt(t.id, cursors.decode(before)), + after !== undefined && gt(t.id, cursors.decode(after)), + ].filter((c) => !!c), + ), + orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), + limit, + with: { textRecords: true, addressRecords: true }, + }), + ), + }), + //////////////////////////////////// // Resolver.records by Name or Node //////////////////////////////////// - records: t.field({ + records_: t.field({ description: "TODO", type: ResolverRecordsRef, args: { for: t.arg({ type: NameOrNodeInput, required: true }) }, @@ -125,6 +158,18 @@ DedicatedResolverMetadataRef.implement({ resolve: (parent) => parent.ownerId, }), + ///////////////////////////////// + // DedicatedResolver.permissions + ///////////////////////////////// + // TODO(EAC) — support DedicatedResolver.permissions after EAC change + // permissions: t.field({ + // description: "TODO", + // type: PermissionsRef, + // nullable: false, + // // TODO: render a DedicatedResolverPermissions model that parses the backing permissions into dedicated-resolver-semantic roles? + // resolve: ({ chainId, address }) => makePermissionsId({ chainId, address }), + // }), + ///////////////////////////// // Resolver.dedicatedRecords ///////////////////////////// diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/EnhancedAccessControl.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/EnhancedAccessControl.ts index 8a05be4ce..a3bfaa244 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/EnhancedAccessControl.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/EnhancedAccessControl.ts @@ -9,6 +9,7 @@ import { PluginName, } from "@ensnode/ensnode-sdk"; +import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; import { getThisAccountId } from "@/lib/get-this-account-id"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; @@ -84,6 +85,8 @@ export default function () { }) => { const { resource, roleBitmap: roles, account: user } = event.args; + await ensureAccount(context, user); + const accountId = getThisAccountId(context, event); const permissionsUserId = makePermissionsUserId(accountId, resource, user); @@ -111,6 +114,8 @@ export default function () { }) => { const { resource, roleBitmap: roles, account: user } = event.args; + await ensureAccount(context, user); + const accountId = getThisAccountId(context, event); const permissionsUserId = makePermissionsUserId(accountId, resource, user); @@ -137,6 +142,8 @@ export default function () { }) => { const { resource, account: user } = event.args; + await ensureAccount(context, user); + const accountId = getThisAccountId(context, event); await ensurePermissionsResource(context, accountId, resource); diff --git a/packages/datasources/src/ens-test-env.ts b/packages/datasources/src/ens-test-env.ts index 973f45dc5..42cf80df8 100644 --- a/packages/datasources/src/ens-test-env.ts +++ b/packages/datasources/src/ens-test-env.ts @@ -117,6 +117,10 @@ export default { [DatasourceNames.Namechain]: { chain: ensTestEnvL2Chain, contracts: { + Resolver: { + abi: ResolverABI, + startBlock: 0, + }, Registry: { abi: Registry, startBlock: 0, diff --git a/packages/datasources/src/mainnet.ts b/packages/datasources/src/mainnet.ts index 0ddd6da5f..0b4c04d8a 100644 --- a/packages/datasources/src/mainnet.ts +++ b/packages/datasources/src/mainnet.ts @@ -124,6 +124,10 @@ export default { [DatasourceNames.Namechain]: { chain: mainnet, contracts: { + Resolver: { + abi: ResolverABI, + startBlock: 23794084, + }, Registry: { abi: Registry, startBlock: 23794084, diff --git a/packages/datasources/src/sepolia.ts b/packages/datasources/src/sepolia.ts index 7e90ce401..e2bbc79cd 100644 --- a/packages/datasources/src/sepolia.ts +++ b/packages/datasources/src/sepolia.ts @@ -121,6 +121,11 @@ export default { [DatasourceNames.Namechain]: { chain: sepolia, contracts: { + Resolver: { + abi: ResolverABI, + startBlock: 9629999, + }, + Registry: { abi: Registry, startBlock: 9629999, diff --git a/packages/ensnode-sdk/src/ens/constants.ts b/packages/ensnode-sdk/src/ens/constants.ts index 682aac5b8..491fd2ea7 100644 --- a/packages/ensnode-sdk/src/ens/constants.ts +++ b/packages/ensnode-sdk/src/ens/constants.ts @@ -14,3 +14,8 @@ export const ADDR_REVERSE_NODE: Node = namehash("addr.reverse"); * returns those records regardless of the name used for record resolution. */ export const NODE_ANY: Node = zeroHash; + +/** + * ROOT_RESOURCE represents the 'root' resource in an EnhancedAccessControl contract. + */ +export const ROOT_RESOURCE = 0n; diff --git a/packages/ensnode-sdk/src/shared/datasources-with-resolvers.ts b/packages/ensnode-sdk/src/shared/datasources-with-resolvers.ts index 3e534e6db..1b316289a 100644 --- a/packages/ensnode-sdk/src/shared/datasources-with-resolvers.ts +++ b/packages/ensnode-sdk/src/shared/datasources-with-resolvers.ts @@ -15,6 +15,7 @@ export const DATASOURCE_NAMES_WITH_RESOLVERS = [ DatasourceNames.Lineanames, DatasourceNames.ThreeDNSOptimism, DatasourceNames.ThreeDNSBase, + DatasourceNames.Namechain, ] as const satisfies DatasourceName[]; /** diff --git a/packages/ensnode-sdk/src/shared/root-registry.ts b/packages/ensnode-sdk/src/shared/root-registry.ts index b820cf3c7..fb71cbf08 100644 --- a/packages/ensnode-sdk/src/shared/root-registry.ts +++ b/packages/ensnode-sdk/src/shared/root-registry.ts @@ -7,10 +7,6 @@ import { type AccountId, accountIdEqual, makeRegistryId } from "@ensnode/ensnode export const getRootRegistry = (namespace: ENSNamespaceId) => { const ensroot = getDatasource(namespace, DatasourceNames.ENSRoot); - // TODO: remove when all defined - if (!("RootRegistry" in ensroot.contracts)) - throw new Error(`Namespace ${namespace} does not define ENSv2 Root Registry.`); - return { chainId: ensroot.chain.id, address: ensroot.contracts.RootRegistry.address, From f223a49122d5daa792fa501540d418400baf247a Mon Sep 17 00:00:00 2001 From: shrugs Date: Thu, 20 Nov 2025 14:46:01 -0600 Subject: [PATCH 048/102] checkpoint --- .../src/graphql-api/lib/get-domain-by-fqdn.ts | 41 ++++++++++++-- .../src/lib/ensv2/domain-db-helpers.ts | 17 +++--- .../resolver-records-db-helpers.ts | 10 ++-- .../ensv2/handlers/ensv1/BaseRegistrar.ts | 56 ++++++++----------- .../handlers/ensv2/EnhancedAccessControl.ts | 11 +++- packages/ensnode-sdk/src/internal.ts | 2 +- packages/ensnode-sdk/src/shared/index.ts | 1 - .../src/shared/interpretation/index.ts | 3 + .../interpretation/interpret-address.ts | 7 +++ .../interpret-record-values.test.ts | 0 .../interpret-record-values.ts | 0 .../interpreted-names-and-labels.test.ts} | 6 +- .../interpreted-names-and-labels.ts} | 0 .../reinterpretation.test.ts | 0 .../{ => interpretation}/reinterpretation.ts | 2 +- .../ensnode-sdk/src/shared/zod-schemas.ts | 2 +- 16 files changed, 96 insertions(+), 62 deletions(-) create mode 100644 packages/ensnode-sdk/src/shared/interpretation/index.ts create mode 100644 packages/ensnode-sdk/src/shared/interpretation/interpret-address.ts rename packages/ensnode-sdk/src/shared/{protocol-acceleration => interpretation}/interpret-record-values.test.ts (100%) rename packages/ensnode-sdk/src/shared/{protocol-acceleration => interpretation}/interpret-record-values.ts (100%) rename packages/ensnode-sdk/src/shared/{interpretation.test.ts => interpretation/interpreted-names-and-labels.test.ts} (96%) rename packages/ensnode-sdk/src/shared/{interpretation.ts => interpretation/interpreted-names-and-labels.ts} (100%) rename packages/ensnode-sdk/src/shared/{ => interpretation}/reinterpretation.test.ts (100%) rename packages/ensnode-sdk/src/shared/{ => interpretation}/reinterpretation.ts (99%) diff --git a/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts index 87972920b..c4e9f2a86 100644 --- a/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts +++ b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts @@ -1,36 +1,51 @@ import config from "@/config"; import { Param, sql } from "drizzle-orm"; +import { namehash } from "viem"; import * as schema from "@ensnode/ensnode-schema"; import { type DomainId, + type ENSv1DomainId, + type ENSv2DomainId, getRootRegistryId, type InterpretedName, interpretedNameToLabelHashPath, type LabelHash, + makeENSv1DomainId, type RegistryId, } from "@ensnode/ensnode-sdk"; import { db } from "@/lib/db"; const ROOT_REGISTRY_ID = getRootRegistryId(config.namespace); -// const ENSV1_REGISTRY = /** - * Gets the Domain addressed by `name`. - * i.e. forward traversal of the namegraph - * - * TODO + * Gets the DomainId of the Domain addressed by `name`. */ export async function getDomainIdByInterpretedName( name: InterpretedName, ): Promise { + const [v1DomainId, v2DomainId] = await Promise.all([ + getENSv1DomainIdByFqdn(name), + getENSv2DomainIdByFqdn(name), + ]); + + // prefer v2DomainId + return v2DomainId || v1DomainId || null; +} + +/** + * Forward-traverses the ENSv2 namegraph in order to identify the Domain addressed by `name`. + */ +async function getENSv2DomainIdByFqdn(name: InterpretedName): Promise { const labelHashPath = interpretedNameToLabelHashPath(name); // https://github.com/drizzle-team/drizzle-orm/issues/1289#issuecomment-2688581070 const rawLabelHashPathArray = sql`${new Param(labelHashPath)}::text[]`; + // TODO: need to join latest registration and confirm that it's not expired, otherwise should treat the domain as not existing + const result = await db.execute(sql` WITH RECURSIVE path AS ( SELECT @@ -63,7 +78,7 @@ export async function getDomainIdByInterpretedName( // couldn't for the life of me figure out how to drizzle this correctly... const rows = result.rows as { registry_id: RegistryId; - domain_id: DomainId; + domain_id: ENSv2DomainId; label_hash: LabelHash; depth: number; }[]; @@ -76,3 +91,17 @@ export async function getDomainIdByInterpretedName( return leaf.domain_id; } + +/** + * Retrieves the ENSv1DomainId for the provided `name`, if exists. + */ +async function getENSv1DomainIdByFqdn(name: InterpretedName): Promise { + const node = namehash(name); + const domainId = makeENSv1DomainId(node); + + const domain = await db.query.v1Domain.findFirst({ + where: (t, { eq }) => eq(t.id, domainId), + }); + + return domain?.id ?? null; +} diff --git a/apps/ensindexer/src/lib/ensv2/domain-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/domain-db-helpers.ts index ead0178cc..70127f650 100644 --- a/apps/ensindexer/src/lib/ensv2/domain-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/domain-db-helpers.ts @@ -2,22 +2,23 @@ import type { Context } from "ponder:registry"; import schema from "ponder:schema"; import type { Address } from "viem"; -import type { ENSv1DomainId } from "@ensnode/ensnode-sdk"; +import { type ENSv1DomainId, interpretAddress } from "@ensnode/ensnode-sdk"; import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; /** - * Sets an ENSv1 Domain's owner to `owner` if exists. + * Sets an ENSv1 Domain's owner to `owner`, ensuring that the `owner` account also exists. */ export async function materializeENSv1DomainOwner( context: Context, id: ENSv1DomainId, owner: Address, ) { - const domain = await context.db.find(schema.v1Domain, { id }); - // TODO: why did i want to put this in a conditional again? why doesn't v1 domain always exist? - if (domain) { - await ensureAccount(context, owner); - await context.db.update(schema.v1Domain, { id }).set({ ownerId: owner }); - } + const ownerId = interpretAddress(owner); + + // ensure owner Account if non-zeroAddress + if (ownerId !== null) await ensureAccount(context, ownerId); + + // update v1Domain's effective owner + await context.db.update(schema.v1Domain, { id }).set({ ownerId }); } diff --git a/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts b/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts index 9de5d4803..d723eaeda 100644 --- a/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts +++ b/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts @@ -1,12 +1,12 @@ import type { Context } from "ponder:registry"; import schema from "ponder:schema"; -import { type Address, isAddressEqual, zeroAddress } from "viem"; +import type { Address } from "viem"; import { ResolverABI } from "@ensnode/datasources"; import { type AccountId, type CoinType, - makeRegistryId, + interpretAddress, makeResolverId, makeResolverRecordsId, type Node, @@ -36,8 +36,6 @@ type ResolverRecordsCompositeKey = Pick< "chainId" | "address" | "node" >; -const interpretOwner = (owner: Address) => (isAddressEqual(zeroAddress, owner) ? null : owner); - /** * Constructs a ResolverRecordsCompositeKey from a provided Resolver event. * @@ -88,7 +86,7 @@ export async function ensureResolver(context: Context, resolver: AccountId) { abi: ResolverABI, functionName: "owner", }); - ownerId = interpretOwner(rawOwner); + ownerId = interpretAddress(rawOwner); } catch {} // ensure Resolver @@ -223,5 +221,5 @@ export async function handleResolverOwnerUpdate( // upsert owner, interpreting zeroAddress as null await context.db .update(schema.resolver, { id: makeResolverId(resolver) }) - .set({ ownerId: interpretOwner(owner) }); + .set({ ownerId: interpretAddress(owner) }); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts index 47a3615e7..c586391c7 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts @@ -36,12 +36,6 @@ const pluginName = PluginName.ENSv2; // that's why registrars manage their own set of owners (registrants), which can be desynced from a domain's owner // so in the registrar handlers we should never touch schema.domain, only reference them. -// ok so in Registrar handlers we can reference domains that may or may not exist - -// ok yeah owner of the registration can desync from the registry, because they don't publish changes -// in ownership when transferring tokens. so the owner of a domain probably should be materialized -// to the domain in question (if exists) and - export default function () { ponder.on( namespaceContract(pluginName, "BaseRegistrar:Transfer"), @@ -58,39 +52,32 @@ export default function () { }) => { const { from, to, tokenId } = event.args; + const isMint = isAddressEqual(zeroAddress, from); + + // minting is always followed by Registrar#NameRegistered, safe to ignore + if (isMint) return; + + // this is either: + // a) a user transfering their registration token, or + // b) re-registering a name that has expired, and it will emit NameRegistered directly afterwards, or + // c) user intentionally burning their registration token by transferring to zeroAddress. + // + // in all such cases, a Registration is expected and we can + const labelHash = registrarTokenIdToLabelHash(tokenId); const registrar = getThisAccountId(context, event); const managedNode = namehash(getRegistrarManagedName(registrar)); const node = makeSubdomainNode(labelHash, managedNode); - const domainId = makeENSv1DomainId(node); - const registration = await getLatestRegistration(context, domainId); - - const isMint = isAddressEqual(zeroAddress, from); - const isBurn = isAddressEqual(zeroAddress, to); - // minting is always followed by Registrar#NameRegistered, safe to ignore - if (isMint) return; - - if (isBurn) { - // requires an existing registration - if (!registration) { - throw new Error( - `Invariant(BaseRegistrar:Transfer): _burn expected existing Registration`, - ); - } - - // for now, just delete the registration - // TODO: mark Registration as inactive or something instead of burning it - await context.db.delete(schema.registration, { id: registration.id }); - } else { - if (!registration) { - throw new Error(`Invariant(BaseRegistrar:Transfer): expected existing Registration`); - } - - // materialize Domain owner - await materializeENSv1DomainOwner(context, domainId, to); + const registration = await getLatestRegistration(context, domainId); + if (!registration) { + throw new Error(`Invariant(BaseRegistrar:Transfer): expected existing Registration`); } + + // materialize Domain owner if exists + const domain = await context.db.find(schema.v1Domain, { id: domainId }); + if (domain) await materializeENSv1DomainOwner(context, domainId, to); }, ); @@ -147,8 +134,9 @@ export default function () { gracePeriod: BigInt(GRACE_PERIOD_SECONDS), }); - // materialize Domain owner - await materializeENSv1DomainOwner(context, domainId, owner); + // materialize Domain owner if exists + const domain = await context.db.find(schema.v1Domain, { id: domainId }); + if (domain) await materializeENSv1DomainOwner(context, domainId, owner); } ponder.on(namespaceContract(pluginName, "BaseRegistrar:NameRegistered"), handleNameRegistered); diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/EnhancedAccessControl.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/EnhancedAccessControl.ts index a3bfaa244..074f947eb 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/EnhancedAccessControl.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/EnhancedAccessControl.ts @@ -1,6 +1,6 @@ import { type Context, ponder } from "ponder:registry"; import schema from "ponder:schema"; -import type { Address } from "viem"; +import { type Address, isAddressEqual, zeroAddress } from "viem"; import { makePermissionsId, @@ -85,6 +85,9 @@ export default function () { }) => { const { resource, roleBitmap: roles, account: user } = event.args; + // ignore roles for zeroAddress + if (isAddressEqual(zeroAddress, user)) return; + await ensureAccount(context, user); const accountId = getThisAccountId(context, event); @@ -114,6 +117,9 @@ export default function () { }) => { const { resource, roleBitmap: roles, account: user } = event.args; + // ignore roles for zeroAddress + if (isAddressEqual(zeroAddress, user)) return; + await ensureAccount(context, user); const accountId = getThisAccountId(context, event); @@ -142,6 +148,9 @@ export default function () { }) => { const { resource, account: user } = event.args; + // ignore roles for zeroAddress + if (isAddressEqual(zeroAddress, user)) return; + await ensureAccount(context, user); const accountId = getThisAccountId(context, event); diff --git a/packages/ensnode-sdk/src/internal.ts b/packages/ensnode-sdk/src/internal.ts index 404ae1f2d..43cfb47a3 100644 --- a/packages/ensnode-sdk/src/internal.ts +++ b/packages/ensnode-sdk/src/internal.ts @@ -25,5 +25,5 @@ export * from "./shared/config/validatons"; export * from "./shared/config/zod-schemas"; export * from "./shared/datasources-with-resolvers"; export * from "./shared/log-level"; -export * from "./shared/protocol-acceleration/interpret-record-values"; +export * from "./shared/interpretation/interpret-record-values"; export * from "./shared/zod-schemas"; diff --git a/packages/ensnode-sdk/src/shared/index.ts b/packages/ensnode-sdk/src/shared/index.ts index d4cd4f337..debb02817 100644 --- a/packages/ensnode-sdk/src/shared/index.ts +++ b/packages/ensnode-sdk/src/shared/index.ts @@ -1,4 +1,3 @@ -export * from "../ens/is-normalized"; export * from "./account-id"; export * from "./address"; export * from "./cache"; diff --git a/packages/ensnode-sdk/src/shared/interpretation/index.ts b/packages/ensnode-sdk/src/shared/interpretation/index.ts new file mode 100644 index 000000000..1741768c1 --- /dev/null +++ b/packages/ensnode-sdk/src/shared/interpretation/index.ts @@ -0,0 +1,3 @@ +export * from "./interpret-address"; +export * from "./interpret-record-values"; +export * from "./interpreted-names-and-labels"; diff --git a/packages/ensnode-sdk/src/shared/interpretation/interpret-address.ts b/packages/ensnode-sdk/src/shared/interpretation/interpret-address.ts new file mode 100644 index 000000000..f9d8a17bf --- /dev/null +++ b/packages/ensnode-sdk/src/shared/interpretation/interpret-address.ts @@ -0,0 +1,7 @@ +import { type Address, isAddressEqual, zeroAddress } from "viem"; + +/** + * Interprets a viem#Address. zeroAddress is interpreted as null, otherwise Address. + */ +export const interpretAddress = (owner: Address) => + isAddressEqual(zeroAddress, owner) ? null : owner; diff --git a/packages/ensnode-sdk/src/shared/protocol-acceleration/interpret-record-values.test.ts b/packages/ensnode-sdk/src/shared/interpretation/interpret-record-values.test.ts similarity index 100% rename from packages/ensnode-sdk/src/shared/protocol-acceleration/interpret-record-values.test.ts rename to packages/ensnode-sdk/src/shared/interpretation/interpret-record-values.test.ts diff --git a/packages/ensnode-sdk/src/shared/protocol-acceleration/interpret-record-values.ts b/packages/ensnode-sdk/src/shared/interpretation/interpret-record-values.ts similarity index 100% rename from packages/ensnode-sdk/src/shared/protocol-acceleration/interpret-record-values.ts rename to packages/ensnode-sdk/src/shared/interpretation/interpret-record-values.ts diff --git a/packages/ensnode-sdk/src/shared/interpretation.test.ts b/packages/ensnode-sdk/src/shared/interpretation/interpreted-names-and-labels.test.ts similarity index 96% rename from packages/ensnode-sdk/src/shared/interpretation.test.ts rename to packages/ensnode-sdk/src/shared/interpretation/interpreted-names-and-labels.test.ts index 429335041..3c4e2bfda 100644 --- a/packages/ensnode-sdk/src/shared/interpretation.test.ts +++ b/packages/ensnode-sdk/src/shared/interpretation/interpreted-names-and-labels.test.ts @@ -1,12 +1,12 @@ import { describe, expect, it } from "vitest"; -import { encodeLabelHash, type InterpretedLabel, type LiteralLabel } from "../ens"; +import { encodeLabelHash, type InterpretedLabel, type LiteralLabel } from "../../ens"; +import { labelhashLiteralLabel } from "../labelhash"; import { interpretedLabelsToInterpretedName, literalLabelsToInterpretedName, literalLabelToInterpretedLabel, -} from "./interpretation"; -import { labelhashLiteralLabel } from "./labelhash"; +} from "./interpreted-names-and-labels"; const ENCODED_LABELHASH_LABEL = /^\[[\da-f]{64}\]$/; diff --git a/packages/ensnode-sdk/src/shared/interpretation.ts b/packages/ensnode-sdk/src/shared/interpretation/interpreted-names-and-labels.ts similarity index 100% rename from packages/ensnode-sdk/src/shared/interpretation.ts rename to packages/ensnode-sdk/src/shared/interpretation/interpreted-names-and-labels.ts diff --git a/packages/ensnode-sdk/src/shared/reinterpretation.test.ts b/packages/ensnode-sdk/src/shared/interpretation/reinterpretation.test.ts similarity index 100% rename from packages/ensnode-sdk/src/shared/reinterpretation.test.ts rename to packages/ensnode-sdk/src/shared/interpretation/reinterpretation.test.ts diff --git a/packages/ensnode-sdk/src/shared/reinterpretation.ts b/packages/ensnode-sdk/src/shared/interpretation/reinterpretation.ts similarity index 99% rename from packages/ensnode-sdk/src/shared/reinterpretation.ts rename to packages/ensnode-sdk/src/shared/interpretation/reinterpretation.ts index cc7011b28..7da091f84 100644 --- a/packages/ensnode-sdk/src/shared/reinterpretation.ts +++ b/packages/ensnode-sdk/src/shared/interpretation/reinterpretation.ts @@ -6,7 +6,7 @@ import { type InterpretedName, isEncodedLabelHash, isNormalizedLabel, -} from "../ens"; +} from "../../ens"; /** * Reinterpret Label diff --git a/packages/ensnode-sdk/src/shared/zod-schemas.ts b/packages/ensnode-sdk/src/shared/zod-schemas.ts index de1bf4e82..c822952f9 100644 --- a/packages/ensnode-sdk/src/shared/zod-schemas.ts +++ b/packages/ensnode-sdk/src/shared/zod-schemas.ts @@ -14,7 +14,7 @@ import { z } from "zod/v4"; import { ENSNamespaceIds, type InterpretedName, Node } from "../ens"; import { asLowerCaseAddress } from "./address"; import { type CurrencyId, CurrencyIds, Price, type PriceEth } from "./currencies"; -import { reinterpretName } from "./reinterpretation"; +import { reinterpretName } from "./interpretation/reinterpretation"; import type { SerializedAccountId } from "./serialized-types"; import type { AccountId, From 7c7be2225db3b58b5ef96d9f6b71c95e5ba05906 Mon Sep 17 00:00:00 2001 From: shrugs Date: Thu, 20 Nov 2025 14:47:55 -0600 Subject: [PATCH 049/102] fix --- .../src/shared/interpretation/interpreted-names-and-labels.ts | 4 ++-- .../src/shared/interpretation/reinterpretation.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ensnode-sdk/src/shared/interpretation/interpreted-names-and-labels.ts b/packages/ensnode-sdk/src/shared/interpretation/interpreted-names-and-labels.ts index 1a5bac1c8..da38157e6 100644 --- a/packages/ensnode-sdk/src/shared/interpretation/interpreted-names-and-labels.ts +++ b/packages/ensnode-sdk/src/shared/interpretation/interpreted-names-and-labels.ts @@ -12,8 +12,8 @@ import { type LiteralLabel, type LiteralName, type Name, -} from "../ens"; -import { labelhashLiteralLabel } from "./labelhash"; +} from "../../ens"; +import { labelhashLiteralLabel } from "../labelhash"; /** * Interprets a Literal Label, producing an Interpreted Label. diff --git a/packages/ensnode-sdk/src/shared/interpretation/reinterpretation.test.ts b/packages/ensnode-sdk/src/shared/interpretation/reinterpretation.test.ts index 93121a079..f2964a577 100644 --- a/packages/ensnode-sdk/src/shared/interpretation/reinterpretation.test.ts +++ b/packages/ensnode-sdk/src/shared/interpretation/reinterpretation.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import type { InterpretedLabel } from "../ens"; +import type { InterpretedLabel } from "../../ens"; import { reinterpretLabel } from "./reinterpretation"; describe("Reinterpretation", () => { From d246d604caa630de6a968eb371cf73e61eba935d Mon Sep 17 00:00:00 2001 From: shrugs Date: Thu, 20 Nov 2025 15:14:32 -0600 Subject: [PATCH 050/102] v2 registrations --- .../src/graphql-api/schema/registration.ts | 14 +++ .../src/lib/ensv2/account-db-helpers.ts | 12 ++- .../src/lib/ensv2/domain-db-helpers.ts | 12 +-- .../ensv2/handlers/ensv1/BaseRegistrar.ts | 18 ++-- .../ensv2/handlers/ensv1/ENSv1Registry.ts | 6 +- .../ensv2/handlers/ensv1/NameWrapper.ts | 15 +-- .../ensv2/handlers/ensv2/ENSv2Registry.ts | 101 ++++++++++++++++-- .../src/schemas/ensv2.schema.ts | 5 +- 8 files changed, 147 insertions(+), 36 deletions(-) diff --git a/apps/ensapi/src/graphql-api/schema/registration.ts b/apps/ensapi/src/graphql-api/schema/registration.ts index e9a2e020d..e126ac423 100644 --- a/apps/ensapi/src/graphql-api/schema/registration.ts +++ b/apps/ensapi/src/graphql-api/schema/registration.ts @@ -40,6 +40,7 @@ export type BaseRegistrarRegistration = RequiredAndNotNull< premium: bigint | null; }; export type ThreeDNSRegistration = Registration; +export type ENSv2RegistryRegistration = Registration; RegistrationInterfaceRef.implement({ description: "TODO", @@ -184,3 +185,16 @@ ThreeDNSRegistrationRef.implement({ // }), }); + +/////////////////////////// +// ENSv2RegistryRegistration +/////////////////////////// +export const ENSv2RegistryRegistrationRef = builder.objectRef( + "ENSv2RegistryRegistration", +); +ENSv2RegistryRegistrationRef.implement({ + description: "TODO", + interfaces: [RegistrationInterfaceRef], + isTypeOf: (value) => (value as RegistrationInterface).type === "ENSv2Registry", + fields: (t) => ({}), +}); diff --git a/apps/ensindexer/src/lib/ensv2/account-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/account-db-helpers.ts index 887b1a5e2..214017dc5 100644 --- a/apps/ensindexer/src/lib/ensv2/account-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/account-db-helpers.ts @@ -2,6 +2,14 @@ import type { Context } from "ponder:registry"; import schema from "ponder:schema"; import type { Address } from "viem"; -export async function ensureAccount(context: Context, id: Address) { - await context.db.insert(schema.account).values({ id }).onConflictDoNothing(); +import { interpretAddress } from "@ensnode/ensnode-sdk"; + +/** + * TODO + */ +export async function ensureAccount(context: Context, address: Address) { + const interpreted = interpretAddress(address); + if (interpreted === null) return; + + await context.db.insert(schema.account).values({ id: interpreted }).onConflictDoNothing(); } diff --git a/apps/ensindexer/src/lib/ensv2/domain-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/domain-db-helpers.ts index 70127f650..90097edbe 100644 --- a/apps/ensindexer/src/lib/ensv2/domain-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/domain-db-helpers.ts @@ -7,18 +7,16 @@ import { type ENSv1DomainId, interpretAddress } from "@ensnode/ensnode-sdk"; import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; /** - * Sets an ENSv1 Domain's owner to `owner`, ensuring that the `owner` account also exists. + * Sets an ENSv1 Domain's effective owner to `owner`. */ -export async function materializeENSv1DomainOwner( +export async function materializeENSv1DomainEffectiveOwner( context: Context, id: ENSv1DomainId, owner: Address, ) { - const ownerId = interpretAddress(owner); - - // ensure owner Account if non-zeroAddress - if (ownerId !== null) await ensureAccount(context, ownerId); + // ensure owner + await ensureAccount(context, owner); // update v1Domain's effective owner - await context.db.update(schema.v1Domain, { id }).set({ ownerId }); + await context.db.update(schema.v1Domain, { id }).set({ ownerId: interpretAddress(owner) }); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts index c586391c7..40b85cc9d 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts @@ -4,13 +4,15 @@ import { GRACE_PERIOD_SECONDS } from "@ensdomains/ensjs/utils"; import { type Address, isAddressEqual, namehash, zeroAddress } from "viem"; import { + interpretAddress, makeENSv1DomainId, makeLatestRegistrationId, makeSubdomainNode, PluginName, } from "@ensnode/ensnode-sdk"; -import { materializeENSv1DomainOwner } from "@/lib/ensv2/domain-db-helpers"; +import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; +import { materializeENSv1DomainEffectiveOwner } from "@/lib/ensv2/domain-db-helpers"; import { getRegistrarManagedName, registrarTokenIdToLabelHash } from "@/lib/ensv2/registrar-lib"; import { getLatestRegistration, @@ -77,7 +79,7 @@ export default function () { // materialize Domain owner if exists const domain = await context.db.find(schema.v1Domain, { id: domainId }); - if (domain) await materializeENSv1DomainOwner(context, domainId, to); + if (domain) await materializeENSv1DomainEffectiveOwner(context, domainId, to); }, ); @@ -107,26 +109,26 @@ export default function () { // Invariant: If there is an existing Registration, it must be fully expired. if (registration && !isFullyExpired) { throw new Error( - `Invariant(BaseRegistrar:NameRegistered): Existing registration found in NameRegistered, expected none.`, + `Invariant(BaseRegistrar:NameRegistered): Existing unexpired registration found in NameRegistered, expected none or expired.\n${toJson(registration)}`, ); } // supercede the latest Registration if exists - if (registration) { - await supercedeLatestRegistration(context, registration); - } + if (registration) await supercedeLatestRegistration(context, registration); const nextIndex = registration ? registration.index + 1 : 0; const registrationId = makeLatestRegistrationId(domainId); + const registrant = owner; // insert BaseRegistrar Registration + await ensureAccount(context, registrant); await context.db.insert(schema.registration).values({ id: registrationId, index: nextIndex, type: "BaseRegistrar", registrarChainId: registrar.chainId, registrarAddress: registrar.address, - registrantId: owner, + registrantId: interpretAddress(registrant), domainId, start: event.block.timestamp, expiration, @@ -136,7 +138,7 @@ export default function () { // materialize Domain owner if exists const domain = await context.db.find(schema.v1Domain, { id: domainId }); - if (domain) await materializeENSv1DomainOwner(context, domainId, owner); + if (domain) await materializeENSv1DomainEffectiveOwner(context, domainId, owner); } ponder.on(namespaceContract(pluginName, "BaseRegistrar:NameRegistered"), handleNameRegistered); diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts index 99657bb6e..3a5f333f3 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts @@ -17,7 +17,7 @@ import { ROOT_NODE, } from "@ensnode/ensnode-sdk"; -import { materializeENSv1DomainOwner } from "@/lib/ensv2/domain-db-helpers"; +import { materializeENSv1DomainEffectiveOwner } from "@/lib/ensv2/domain-db-helpers"; import { ensureLabel, ensureUnknownLabel } from "@/lib/ensv2/label-db-helpers"; import { healAddrReverseSubnameLabel } from "@/lib/heal-addr-reverse-subname-label"; import { namespaceContract } from "@/lib/plugin-helpers"; @@ -103,7 +103,7 @@ export default function () { // events occur _after_ the Registry events. So when a name is registered, for example, the Registry's // owner changes to that of the NameWrapper but then the NameWrapper emits NameWrapped, and this // indexing code re-materializes the Domain.ownerId to the NameWraper-emitted value. - await materializeENSv1DomainOwner(context, domainId, owner); + await materializeENSv1DomainEffectiveOwner(context, domainId, owner); } async function handleTransfer({ @@ -132,7 +132,7 @@ export default function () { // events occur _after_ the Registry events. So when a name is wrapped, for example, the Registry's // owner changes to that of the NameWrapper but then the NameWrapper emits NameWrapped, and this // indexing code re-materializes the Domain.ownerId to the NameWraper-emitted value. - await materializeENSv1DomainOwner(context, domainId, owner); + await materializeENSv1DomainEffectiveOwner(context, domainId, owner); } /** diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts index cb097d0dd..5092d9f31 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts @@ -6,6 +6,7 @@ import { type DNSEncodedLiteralName, type DNSEncodedName, decodeDNSEncodedLiteralName, + interpretAddress, isPccFuseSet, type LiteralLabel, labelhashLiteralLabel, @@ -17,7 +18,8 @@ import { uint256ToHex32, } from "@ensnode/ensnode-sdk"; -import { materializeENSv1DomainOwner } from "@/lib/ensv2/domain-db-helpers"; +import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; +import { materializeENSv1DomainEffectiveOwner } from "@/lib/ensv2/domain-db-helpers"; import { ensureLabel } from "@/lib/ensv2/label-db-helpers"; import { getRegistrarManagedName } from "@/lib/ensv2/registrar-lib"; import { @@ -137,7 +139,7 @@ export default function () { // now guaranteed to be an unexpired transferrable Registration // so materialize domain owner - await materializeENSv1DomainOwner(context, domainId, to); + await materializeENSv1DomainEffectiveOwner(context, domainId, to); } ponder.on( @@ -178,7 +180,7 @@ export default function () { registration && isRegistrationFullyExpired(registration, event.block.timestamp); // materialize domain owner - await materializeENSv1DomainOwner(context, domainId, owner); + await materializeENSv1DomainEffectiveOwner(context, domainId, owner); // handle wraps of direct-subname-of-registrar-managed-names if (registration && !isFullyExpired && registration.type === "BaseRegistrar") { @@ -241,20 +243,21 @@ export default function () { } // supercede the latest Registration if exists - if (registration) { - await supercedeLatestRegistration(context, registration); - } + if (registration) await supercedeLatestRegistration(context, registration); const nextIndex = registration ? registration.index + 1 : 0; const registrationId = makeLatestRegistrationId(domainId); + const registrant = owner; // insert NameWrapper Registration + await ensureAccount(context, registrant); await context.db.insert(schema.registration).values({ id: registrationId, index: nextIndex, type: "NameWrapper", registrarChainId: registrar.chainId, registrarAddress: registrar.address, + registrantId: interpretAddress(registrant), domainId, start: event.block.timestamp, fuses, diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts index 25fe9a2b8..3d3b5da9e 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts @@ -6,15 +6,23 @@ import { type Address, hexToBigInt, isAddressEqual, labelhash, zeroAddress } fro import { type AccountId, getCanonicalId, + interpretAddress, type LiteralLabel, makeENSv2DomainId, + makeLatestRegistrationId, makeRegistryId, PluginName, } from "@ensnode/ensnode-sdk"; import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; import { ensureLabel } from "@/lib/ensv2/label-db-helpers"; +import { + getLatestRegistration, + isRegistrationExpired, + supercedeLatestRegistration, +} from "@/lib/ensv2/registration-db-helpers"; import { getThisAccountId } from "@/lib/get-this-account-id"; +import { toJson } from "@/lib/json-stringify-with-bigints"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; @@ -38,11 +46,11 @@ export default function () { const { tokenId, label: _label, expiration, registeredBy: registrant } = event.args; const label = _label as LiteralLabel; - const registryAccountId = getThisAccountId(context, event); - const registryId = makeRegistryId(registryAccountId); + const registry = getThisAccountId(context, event); + const registryId = makeRegistryId(registry); const canonicalId = getCanonicalId(tokenId); const labelHash = labelhash(label); - const domainId = makeENSv2DomainId(registryAccountId, canonicalId); + const domainId = makeENSv2DomainId(registry, canonicalId); // Sanity Check: Canonical Id must match emitted label if (canonicalId !== getCanonicalId(hexToBigInt(labelhash(label)))) { @@ -63,24 +71,98 @@ export default function () { } // upsert Registry + // TODO(signals) — move to NewRegistry and add invariant here await context.db .insert(schema.registry) .values({ id: registryId, type: "RegistryContract", - ...registryAccountId, + ...registry, }) .onConflictDoNothing(); // ensure discovered Label await ensureLabel(context, label); - // insert Domain - await context.db.insert(schema.v2Domain).values({ id: domainId, registryId, labelHash }); + // insert v2Domain + await context.db.insert(schema.v2Domain).values({ + id: domainId, + registryId, + labelHash, + // NOTE: ownerId omitted, Tranfer* events are sole source of ownership + }); + + const registration = await getLatestRegistration(context, domainId); + const isExpired = registration && isRegistrationExpired(registration, event.block.timestamp); + + // Invariant: If there is an existing Registration, it must be expired. + if (registration && !isExpired) { + throw new Error( + `Invariant(ENSv2Registry:NameRegistered): Existing unexpired registration found in NameRegistered, expected none or expired.\n${toJson(registration)}`, + ); + } + + // supercede the latest Registration if exists + if (registration) await supercedeLatestRegistration(context, registration); - // TODO: insert Registration entity for this domain as well: expiration, registrant - // ensure Registrant + const nextIndex = registration ? registration.index + 1 : 0; + const registrationId = makeLatestRegistrationId(domainId); + + // insert ENSv2Registry Registration await ensureAccount(context, registrant); + await context.db.insert(schema.registration).values({ + id: registrationId, + index: nextIndex, + type: "ENSv2Registry", + registrarChainId: registry.chainId, + registrarAddress: registry.address, + registrantId: interpretAddress(registrant), + domainId, + start: event.block.timestamp, + expiration, + }); + }, + ); + + ponder.on( + namespaceContract(pluginName, "ENSv2Registry:NameRenewed"), + async ({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ + tokenId: bigint; + newExpiration: bigint; + renewedBy: Address; + }>; + }) => { + const { tokenId, newExpiration: expiration, renewedBy: renewer } = event.args; + + const registry = getThisAccountId(context, event); + const canonicalId = getCanonicalId(tokenId); + const domainId = makeENSv2DomainId(registry, canonicalId); + + const registration = await getLatestRegistration(context, domainId); + + // Invariant: Registration must exist + if (!registration) { + throw new Error(`Invariant(ENSv2Registry:NameRenewed): Registration expected none found.`); + } + + // Invariant: Registration must not be expired + if (isRegistrationExpired(registration, event.block.timestamp)) { + throw new Error( + `Invariant(ENSv2Registry:NameRenewed): Registration found but it is expired:\n${toJson(registration)}`, + ); + } + + // TODO: optional invariant, v2Domain must also exist? implied by expiry check + + // update Registration + await context.db.update(schema.registration, { id: registration.id }).set({ expiration }); + + // TODO: insert Renewal }, ); @@ -164,7 +246,8 @@ export default function () { const domainId = makeENSv2DomainId(registryAccountId, canonicalId); await context.db.delete(schema.v2Domain, { id: domainId }); - // TODO: delete registrations (?) + + // NOTE: we explicitly keep the Registration/Renewal entities around }, ); diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/schemas/ensv2.schema.ts index 6f4a170fc..52f28094b 100644 --- a/packages/ensnode-schema/src/schemas/ensv2.schema.ts +++ b/packages/ensnode-schema/src/schemas/ensv2.schema.ts @@ -172,6 +172,7 @@ export const registrationType = onchainEnum("RegistrationType", [ "NameWrapper", "BaseRegistrar", "ThreeDNS", + "ENSv2Registry", ]); export const registration = onchainTable( @@ -179,11 +180,13 @@ export const registration = onchainTable( (t) => ({ // keyed by (domainId, index) id: t.text().primaryKey().$type(), - type: registrationType().notNull(), domainId: t.text().notNull().$type(), index: t.integer().notNull().default(0), + // has a type + type: registrationType().notNull(), + // must have a start timestamp start: t.bigint().notNull(), // may have an expiration From bcc3fe6a04f56e1f57f55af34eba935d846d2015 Mon Sep 17 00:00:00 2001 From: shrugs Date: Thu, 20 Nov 2025 16:50:30 -0600 Subject: [PATCH 051/102] forward traversal --- .../src/graphql-api/lib/get-domain-by-fqdn.ts | 129 +- .../src/plugins/ensv2/event-handlers.ts | 2 + .../ensv2/handlers/ensv2/ENSv2Registry.ts | 13 +- .../ensv2/handlers/ensv2/ETHRegistrar.ts | 23 + apps/ensindexer/src/plugins/ensv2/plugin.ts | 12 + .../src/abis/namechain/ETHRegistrar.ts | 1152 +++++++++++++++++ packages/datasources/src/ens-test-env.ts | 18 +- packages/datasources/src/mainnet.ts | 17 + packages/datasources/src/sepolia.ts | 19 +- 9 files changed, 1354 insertions(+), 31 deletions(-) create mode 100644 apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ETHRegistrar.ts create mode 100644 packages/datasources/src/abis/namechain/ETHRegistrar.ts diff --git a/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts index c4e9f2a86..d559569b8 100644 --- a/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts +++ b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts @@ -1,25 +1,48 @@ import config from "@/config"; import { Param, sql } from "drizzle-orm"; -import { namehash } from "viem"; +import { labelhash, namehash } from "viem"; +import { DatasourceNames, getDatasource } from "@ensnode/datasources"; import * as schema from "@ensnode/ensnode-schema"; import { type DomainId, - type ENSv1DomainId, type ENSv2DomainId, + ETH_NODE, getRootRegistryId, type InterpretedName, + interpretedLabelsToInterpretedName, + interpretedNameToInterpretedLabels, interpretedNameToLabelHashPath, type LabelHash, + type LiteralLabel, + labelhashLiteralLabel, makeENSv1DomainId, + makeRegistryId, + makeSubdomainNode, type RegistryId, } from "@ensnode/ensnode-sdk"; +import { getLatestRegistration } from "@/graphql-api/lib/get-latest-registration"; import { db } from "@/lib/db"; +const ensroot = getDatasource(config.namespace, DatasourceNames.ENSRoot); +const namechain = getDatasource(config.namespace, DatasourceNames.Namechain); + +const ETH_LABELHASH = labelhashLiteralLabel("eth" as LiteralLabel); + const ROOT_REGISTRY_ID = getRootRegistryId(config.namespace); +const ENS_ROOT_V2_ETH_REGISTRY_ID = makeRegistryId({ + chainId: ensroot.chain.id, + address: ensroot.contracts.ETHRegistry.address, +}); + +const NAMECHAIN_V2_ETH_REGISTRY_ID = makeRegistryId({ + chainId: namechain.chain.id, + address: namechain.contracts.ETHRegistry.address, +}); + /** * Gets the DomainId of the Domain addressed by `name`. */ @@ -27,18 +50,34 @@ export async function getDomainIdByInterpretedName( name: InterpretedName, ): Promise { const [v1DomainId, v2DomainId] = await Promise.all([ - getENSv1DomainIdByFqdn(name), - getENSv2DomainIdByFqdn(name), + v1_getDomainIdByFqdn(name), + v2_getDomainIdByFqdn(ROOT_REGISTRY_ID, name), ]); // prefer v2DomainId return v2DomainId || v1DomainId || null; } +/** + * Retrieves the ENSv1DomainId for the provided `name`, if exists. + */ +async function v1_getDomainIdByFqdn(name: InterpretedName): Promise { + const node = namehash(name); + const domainId = makeENSv1DomainId(node); + + const domain = await db.query.v1Domain.findFirst({ where: (t, { eq }) => eq(t.id, domainId) }); + return domain?.id ?? null; +} + /** * Forward-traverses the ENSv2 namegraph in order to identify the Domain addressed by `name`. + * + * If the exact Domain was not found, and the path terminates at a bridging resolver, */ -async function getENSv2DomainIdByFqdn(name: InterpretedName): Promise { +async function v2_getDomainIdByFqdn( + registryId: RegistryId, + name: InterpretedName, +): Promise { const labelHashPath = interpretedNameToLabelHashPath(name); // https://github.com/drizzle-team/drizzle-orm/issues/1289#issuecomment-2688581070 @@ -54,7 +93,7 @@ async function getENSv2DomainIdByFqdn(name: InterpretedName): Promise 0 && rows.length === labelHashPath.length; - if (!exists) return null; + // this was a query for a TLD and it does not exist in ENS Root Chain ENSv2 + if (rows.length === 0) return null; // biome-ignore lint/style/noNonNullAssertion: length check above const leaf = rows[rows.length - 1]!; - return leaf.domain_id; -} - -/** - * Retrieves the ENSv1DomainId for the provided `name`, if exists. - */ -async function getENSv1DomainIdByFqdn(name: InterpretedName): Promise { - const node = namehash(name); - const domainId = makeENSv1DomainId(node); - - const domain = await db.query.v1Domain.findFirst({ - where: (t, { eq }) => eq(t.id, domainId), - }); - - return domain?.id ?? null; + // we have an exact match within ENSv2 on the ENS Root Chain + const exact = rows.length === labelHashPath.length; + if (exact) { + console.log(`Found ${name} in ENSv2 from Registry ${registryId}`); + return leaf.domain_id; + } + + console.log(name); + console.log(JSON.stringify(rows, null, 2)); + + // we did not find an exact match for the Domain within ENSv2 on the ENS Root Chain + // if the path terminates at the .eth Registry, we must implement the logic in ETHTLDResolver + // TODO: we could ad an additional invariant that the .eth v2 Registry does indeed have the ETHTLDResolver + // set as its resolver, but that is unnecessary at the moment and incurs additional db requests or a join against + // domain_resolver_relationships + // TODO: generalize this into other future bridging resolvers depending on how basenames etc do it + if (leaf.registry_id === ENS_ROOT_V2_ETH_REGISTRY_ID) { + // Invariant: must be >= 2LD + if (labelHashPath.length < 2) { + throw new Error(`Invariant: Not >= 2LD??`); + } + + // Invariant: must be a .eth subname + if (labelHashPath[0] !== ETH_LABELHASH) { + throw new Error(`Invariant: Not .eth subname????`); + } + + // Invariant: must be a .eth subname + if (leaf.label_hash !== labelhash("eth")) { + throw new Error(`Invariant: Not .eth subname??`); + } + + // construct the node of the 2ld + const dotEth2LDNode = makeSubdomainNode(labelHashPath[1], ETH_NODE); + + // 1. if there's an active registration in ENSv1 for the .eth 2LD, then resolve from ENSv1 + const ensv1DomainId = makeENSv1DomainId(dotEth2LDNode); + const registration = await getLatestRegistration(ensv1DomainId); + + // TODO: && isRegistrationFullyExpired(registration,) + if (registration) { + console.log( + `ETHTLDResolver deferring to actively registered name ${dotEth2LDNode} in ENSv1...`, + ); + return await v1_getDomainIdByFqdn(name); + } + + // 2. otherwise, direct to Namechain ENSv2 .eth Registry + const nameWithoutTld = interpretedLabelsToInterpretedName( + interpretedNameToInterpretedLabels(name).slice(0, -1), + ); + console.log( + `ETHTLDResolver deferring ${nameWithoutTld} to ENSv2 .eth Registry on Namechain...`, + ); + return v2_getDomainIdByFqdn(NAMECHAIN_V2_ETH_REGISTRY_ID, nameWithoutTld); + } + + // finally, not found + return null; } diff --git a/apps/ensindexer/src/plugins/ensv2/event-handlers.ts b/apps/ensindexer/src/plugins/ensv2/event-handlers.ts index 7e0207eb2..4c2f256a3 100644 --- a/apps/ensindexer/src/plugins/ensv2/event-handlers.ts +++ b/apps/ensindexer/src/plugins/ensv2/event-handlers.ts @@ -4,6 +4,7 @@ import attach_NameWrapperHandlers from "./handlers/ensv1/NameWrapper"; import attach_RegistrarControllerHandlers from "./handlers/ensv1/RegistrarController"; import attach_RegistryHandlers from "./handlers/ensv2/ENSv2Registry"; import attach_EnhancedAccessControlHandlers from "./handlers/ensv2/EnhancedAccessControl"; +import attach_ETHRegistrarHandlers from "./handlers/ensv2/ETHRegistrar"; export default function () { attach_BaseRegistrarHandlers(); @@ -12,4 +13,5 @@ export default function () { attach_RegistrarControllerHandlers(); attach_EnhancedAccessControlHandlers(); attach_RegistryHandlers(); + attach_ETHRegistrarHandlers(); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts index 3d3b5da9e..f5ebef792 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts @@ -46,10 +46,10 @@ export default function () { const { tokenId, label: _label, expiration, registeredBy: registrant } = event.args; const label = _label as LiteralLabel; + const labelHash = labelhash(label); const registry = getThisAccountId(context, event); const registryId = makeRegistryId(registry); const canonicalId = getCanonicalId(tokenId); - const labelHash = labelhash(label); const domainId = makeENSv2DomainId(registry, canonicalId); // Sanity Check: Canonical Id must match emitted label @@ -184,8 +184,15 @@ export default function () { const canonicalId = getCanonicalId(tokenId); const domainId = makeENSv2DomainId(registryAccountId, canonicalId); + if ( + domainId === + "eip155:15658734/erc1155:0x5fc8d32690cc91d4c39d9d3abcbd16989f875707/115467421361047454831865845388833304924993579272809100258442734802450801229824" + ) { + console.log("parent.eth subregistry update!", event.args); + } + // update domain's subregistry - const isDeletion = isAddressEqual(subregistry, zeroAddress); + const isDeletion = isAddressEqual(zeroAddress, subregistry); if (isDeletion) { await context.db.update(schema.v2Domain, { id: domainId }).set({ subregistryId: null }); } else { @@ -217,7 +224,7 @@ export default function () { // const domainId = makeENSv2DomainId(registryAccountId, canonicalId); // // update domain's resolver - // const isDeletion = isAddressEqual(address, zeroAddress); + // const isDeletion = isAddressEqual(zeroAddress, address); // if (isDeletion) { // await context.db.update(schema.v2Domain, { id: domainId }).set({ resolverId: null }); // } else { diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ETHRegistrar.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ETHRegistrar.ts new file mode 100644 index 000000000..7df1b4ff2 --- /dev/null +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ETHRegistrar.ts @@ -0,0 +1,23 @@ +import { ponder } from "ponder:registry"; + +import { PluginName } from "@ensnode/ensnode-sdk"; + +import { namespaceContract } from "@/lib/plugin-helpers"; + +const pluginName = PluginName.ENSv2; + +export default function () { + ponder.on( + namespaceContract(pluginName, "ETHRegistrar:NameRegistered"), + async ({ context, event }) => { + // TODO add to existing Registration, override registrant (BaseRegistrar uses msg.sender) + }, + ); + + ponder.on( + namespaceContract(pluginName, "ETHRegistrar:NameRenewed"), + async ({ context, event }) => { + // TODO add to existing Renewal ditto above + }, + ); +} diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index e56403ee9..a3d56a4c7 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -133,6 +133,18 @@ export default createPlugin({ ), }, + ////////////////////////// + // Namechain ETHRegistrar + ////////////////////////// + [namespaceContract(pluginName, "ETHRegistrar")]: { + abi: namechain.contracts.ETHRegistrar.abi, + chain: chainConfigForContract( + config.globalBlockrange, + namechain.chain.id, + namechain.contracts.ETHRegistrar, + ), + }, + ////////////////////////////////////// // ENSv1RegistryOld on ENS Root Chain ////////////////////////////////////// diff --git a/packages/datasources/src/abis/namechain/ETHRegistrar.ts b/packages/datasources/src/abis/namechain/ETHRegistrar.ts new file mode 100644 index 000000000..0d52f82bb --- /dev/null +++ b/packages/datasources/src/abis/namechain/ETHRegistrar.ts @@ -0,0 +1,1152 @@ +export const ETHRegistrar = [ + { + inputs: [ + { + internalType: "contract IPermissionedRegistry", + name: "registry", + type: "address", + }, + { + internalType: "address", + name: "beneficiary", + type: "address", + }, + { + internalType: "uint64", + name: "minCommitmentAge", + type: "uint64", + }, + { + internalType: "uint64", + name: "maxCommitmentAge", + type: "uint64", + }, + { + internalType: "uint64", + name: "minRegisterDuration", + type: "uint64", + }, + { + internalType: "contract IRentPriceOracle", + name: "rentPriceOracle_", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "commitment", + type: "bytes32", + }, + { + internalType: "uint64", + name: "validFrom", + type: "uint64", + }, + { + internalType: "uint64", + name: "blockTimestamp", + type: "uint64", + }, + ], + name: "CommitmentTooNew", + type: "error", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "commitment", + type: "bytes32", + }, + { + internalType: "uint64", + name: "validTo", + type: "uint64", + }, + { + internalType: "uint64", + name: "blockTimestamp", + type: "uint64", + }, + ], + name: "CommitmentTooOld", + type: "error", + }, + { + inputs: [ + { + internalType: "uint64", + name: "duration", + type: "uint64", + }, + { + internalType: "uint64", + name: "minDuration", + type: "uint64", + }, + ], + name: "DurationTooShort", + type: "error", + }, + { + inputs: [ + { + internalType: "uint256", + name: "resource", + type: "uint256", + }, + { + internalType: "uint256", + name: "roleBitmap", + type: "uint256", + }, + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "EACCannotGrantRoles", + type: "error", + }, + { + inputs: [ + { + internalType: "uint256", + name: "resource", + type: "uint256", + }, + { + internalType: "uint256", + name: "roleBitmap", + type: "uint256", + }, + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "EACCannotRevokeRoles", + type: "error", + }, + { + inputs: [ + { + internalType: "uint256", + name: "roleBitmap", + type: "uint256", + }, + ], + name: "EACInvalidRoleBitmap", + type: "error", + }, + { + inputs: [ + { + internalType: "uint256", + name: "resource", + type: "uint256", + }, + { + internalType: "uint256", + name: "role", + type: "uint256", + }, + ], + name: "EACMaxAssignees", + type: "error", + }, + { + inputs: [ + { + internalType: "uint256", + name: "resource", + type: "uint256", + }, + { + internalType: "uint256", + name: "role", + type: "uint256", + }, + ], + name: "EACMinAssignees", + type: "error", + }, + { + inputs: [], + name: "EACRootResourceNotAllowed", + type: "error", + }, + { + inputs: [ + { + internalType: "uint256", + name: "resource", + type: "uint256", + }, + { + internalType: "uint256", + name: "roleBitmap", + type: "uint256", + }, + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "EACUnauthorizedAccountRoles", + type: "error", + }, + { + inputs: [], + name: "MaxCommitmentAgeTooLow", + type: "error", + }, + { + inputs: [ + { + internalType: "string", + name: "label", + type: "string", + }, + ], + name: "NameAlreadyRegistered", + type: "error", + }, + { + inputs: [ + { + internalType: "string", + name: "label", + type: "string", + }, + ], + name: "NameNotRegistered", + type: "error", + }, + { + inputs: [ + { + internalType: "string", + name: "label", + type: "string", + }, + ], + name: "NotValid", + type: "error", + }, + { + inputs: [ + { + internalType: "contract IERC20", + name: "paymentToken", + type: "address", + }, + ], + name: "PaymentTokenNotSupported", + type: "error", + }, + { + inputs: [ + { + internalType: "address", + name: "token", + type: "address", + }, + ], + name: "SafeERC20FailedOperation", + type: "error", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "commitment", + type: "bytes32", + }, + ], + name: "UnexpiredCommitmentExists", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "bytes32", + name: "commitment", + type: "bytes32", + }, + ], + name: "CommitmentMade", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "resource", + type: "uint256", + }, + { + indexed: false, + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "EACAllRolesRevoked", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "resource", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "roleBitmap", + type: "uint256", + }, + { + indexed: false, + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "EACRolesGranted", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "resource", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "roleBitmap", + type: "uint256", + }, + { + indexed: false, + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "EACRolesRevoked", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + { + indexed: false, + internalType: "string", + name: "label", + type: "string", + }, + { + indexed: false, + internalType: "address", + name: "owner", + type: "address", + }, + { + indexed: false, + internalType: "contract IRegistry", + name: "subregistry", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "resolver", + type: "address", + }, + { + indexed: false, + internalType: "uint64", + name: "duration", + type: "uint64", + }, + { + indexed: false, + internalType: "contract IERC20", + name: "paymentToken", + type: "address", + }, + { + indexed: false, + internalType: "bytes32", + name: "referrer", + type: "bytes32", + }, + { + indexed: false, + internalType: "uint256", + name: "base", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "premium", + type: "uint256", + }, + ], + name: "NameRegistered", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + { + indexed: false, + internalType: "string", + name: "label", + type: "string", + }, + { + indexed: false, + internalType: "uint64", + name: "duration", + type: "uint64", + }, + { + indexed: false, + internalType: "uint64", + name: "newExpiry", + type: "uint64", + }, + { + indexed: false, + internalType: "contract IERC20", + name: "paymentToken", + type: "address", + }, + { + indexed: false, + internalType: "bytes32", + name: "referrer", + type: "bytes32", + }, + { + indexed: false, + internalType: "uint256", + name: "base", + type: "uint256", + }, + ], + name: "NameRenewed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "contract IERC20", + name: "paymentToken", + type: "address", + }, + ], + name: "PaymentTokenAdded", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "contract IERC20", + name: "paymentToken", + type: "address", + }, + ], + name: "PaymentTokenRemoved", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "contract IRentPriceOracle", + name: "oracle", + type: "address", + }, + ], + name: "RentPriceOracleChanged", + type: "event", + }, + { + inputs: [], + name: "BENEFICIARY", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "MAX_COMMITMENT_AGE", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "MIN_COMMITMENT_AGE", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "MIN_REGISTER_DURATION", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "REGISTRY", + outputs: [ + { + internalType: "contract IPermissionedRegistry", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "ROOT_RESOURCE", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "commitment", + type: "bytes32", + }, + ], + name: "commit", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "commitment", + type: "bytes32", + }, + ], + name: "commitmentAt", + outputs: [ + { + internalType: "uint64", + name: "commitTime", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "resource", + type: "uint256", + }, + { + internalType: "uint256", + name: "roleBitmap", + type: "uint256", + }, + ], + name: "getAssigneeCount", + outputs: [ + { + internalType: "uint256", + name: "counts", + type: "uint256", + }, + { + internalType: "uint256", + name: "mask", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "resource", + type: "uint256", + }, + { + internalType: "uint256", + name: "roleBitmap", + type: "uint256", + }, + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "grantRoles", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "roleBitmap", + type: "uint256", + }, + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "grantRootRoles", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "resource", + type: "uint256", + }, + { + internalType: "uint256", + name: "roleBitmap", + type: "uint256", + }, + ], + name: "hasAssignees", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "resource", + type: "uint256", + }, + { + internalType: "uint256", + name: "rolesBitmap", + type: "uint256", + }, + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "hasRoles", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "rolesBitmap", + type: "uint256", + }, + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "hasRootRoles", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "string", + name: "label", + type: "string", + }, + ], + name: "isAvailable", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "contract IERC20", + name: "paymentToken", + type: "address", + }, + ], + name: "isPaymentToken", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "string", + name: "label", + type: "string", + }, + ], + name: "isValid", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "string", + name: "label", + type: "string", + }, + { + internalType: "address", + name: "owner", + type: "address", + }, + { + internalType: "bytes32", + name: "secret", + type: "bytes32", + }, + { + internalType: "contract IRegistry", + name: "subregistry", + type: "address", + }, + { + internalType: "address", + name: "resolver", + type: "address", + }, + { + internalType: "uint64", + name: "duration", + type: "uint64", + }, + { + internalType: "bytes32", + name: "referrer", + type: "bytes32", + }, + ], + name: "makeCommitment", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "pure", + type: "function", + }, + { + inputs: [ + { + internalType: "string", + name: "label", + type: "string", + }, + { + internalType: "address", + name: "owner", + type: "address", + }, + { + internalType: "bytes32", + name: "secret", + type: "bytes32", + }, + { + internalType: "contract IRegistry", + name: "subregistry", + type: "address", + }, + { + internalType: "address", + name: "resolver", + type: "address", + }, + { + internalType: "uint64", + name: "duration", + type: "uint64", + }, + { + internalType: "contract IERC20", + name: "paymentToken", + type: "address", + }, + { + internalType: "bytes32", + name: "referrer", + type: "bytes32", + }, + ], + name: "register", + outputs: [ + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "string", + name: "label", + type: "string", + }, + { + internalType: "uint64", + name: "duration", + type: "uint64", + }, + { + internalType: "contract IERC20", + name: "paymentToken", + type: "address", + }, + { + internalType: "bytes32", + name: "referrer", + type: "bytes32", + }, + ], + name: "renew", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "string", + name: "label", + type: "string", + }, + { + internalType: "address", + name: "owner", + type: "address", + }, + { + internalType: "uint64", + name: "duration", + type: "uint64", + }, + { + internalType: "contract IERC20", + name: "paymentToken", + type: "address", + }, + ], + name: "rentPrice", + outputs: [ + { + internalType: "uint256", + name: "base", + type: "uint256", + }, + { + internalType: "uint256", + name: "premium", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "rentPriceOracle", + outputs: [ + { + internalType: "contract IRentPriceOracle", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "resource", + type: "uint256", + }, + { + internalType: "uint256", + name: "roleBitmap", + type: "uint256", + }, + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "revokeRoles", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "roleBitmap", + type: "uint256", + }, + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "revokeRootRoles", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "resource", + type: "uint256", + }, + ], + name: "roleCount", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "resource", + type: "uint256", + }, + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "roles", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "contract IRentPriceOracle", + name: "oracle", + type: "address", + }, + ], + name: "setRentPriceOracle", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes4", + name: "interfaceId", + type: "bytes4", + }, + ], + name: "supportsInterface", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, +] as const; diff --git a/packages/datasources/src/ens-test-env.ts b/packages/datasources/src/ens-test-env.ts index 42cf80df8..5417acf7a 100644 --- a/packages/datasources/src/ens-test-env.ts +++ b/packages/datasources/src/ens-test-env.ts @@ -1,6 +1,7 @@ import { zeroAddress } from "viem"; import { EnhancedAccessControl } from "./abis/namechain/EnhancedAccessControl"; +import { ETHRegistrar } from "./abis/namechain/ETHRegistrar"; import { Registry } from "./abis/namechain/Registry"; // ABIs for ENSRoot Datasource import { BaseRegistrar as root_BaseRegistrar } from "./abis/root/BaseRegistrar"; @@ -92,12 +93,11 @@ export default { // - ETHTLDResolver: { - abi: ResolverABI, - address: "0x99bba657f2bbc93c02d617f8ba121cb8fc104acf", + ETHRegistry: { + abi: Registry, + address: "0x1291be112d480055dafd8a610b7d1e203891c274", startBlock: 0, }, - RootRegistry: { abi: Registry, address: "0x8a791620dd6260079bf849dc5567adc3f2fdc318", @@ -129,6 +129,16 @@ export default { abi: EnhancedAccessControl, startBlock: 0, }, + ETHRegistry: { + abi: Registry, + address: "0x5fc8d32690cc91d4c39d9d3abcbd16989f875707", + startBlock: 0, + }, + ETHRegistrar: { + abi: ETHRegistrar, + address: "0xa513e6e4b8f2a923d98304ec87f64353c4d5c853", + startBlock: 0, + }, }, }, diff --git a/packages/datasources/src/mainnet.ts b/packages/datasources/src/mainnet.ts index 0b4c04d8a..b296d323e 100644 --- a/packages/datasources/src/mainnet.ts +++ b/packages/datasources/src/mainnet.ts @@ -13,6 +13,7 @@ import { NameWrapper as linea_NameWrapper } from "./abis/lineanames/NameWrapper" import { Registry as linea_Registry } from "./abis/lineanames/Registry"; // ABIs for Namechain import { EnhancedAccessControl } from "./abis/namechain/EnhancedAccessControl"; +import { ETHRegistrar } from "./abis/namechain/ETHRegistrar"; import { Registry } from "./abis/namechain/Registry"; // ABIs for ENSRoot Datasource import { BaseRegistrar as root_BaseRegistrar } from "./abis/root/BaseRegistrar"; @@ -105,6 +106,12 @@ export default { }, // + + ETHRegistry: { + abi: Registry, + address: "0x1291be112d480055dafd8a610b7d1e203891c274", + startBlock: 23794084, + }, RootRegistry: { abi: Registry, address: "0x8a791620dd6260079bf849dc5567adc3f2fdc318", @@ -136,6 +143,16 @@ export default { abi: EnhancedAccessControl, startBlock: 23794084, }, + ETHRegistry: { + abi: Registry, + address: "0x5fc8d32690cc91d4c39d9d3abcbd16989f875707", + startBlock: 23794084, + }, + ETHRegistrar: { + abi: ETHRegistrar, + address: "0xa513e6e4b8f2a923d98304ec87f64353c4d5c853", + startBlock: 23794084, + }, }, }, diff --git a/packages/datasources/src/sepolia.ts b/packages/datasources/src/sepolia.ts index e2bbc79cd..40644b63c 100644 --- a/packages/datasources/src/sepolia.ts +++ b/packages/datasources/src/sepolia.ts @@ -20,6 +20,7 @@ import { NameWrapper as linea_NameWrapper } from "./abis/lineanames/NameWrapper" import { Registry as linea_Registry } from "./abis/lineanames/Registry"; // ABIs for Namechain import { EnhancedAccessControl } from "./abis/namechain/EnhancedAccessControl"; +import { ETHRegistrar } from "./abis/namechain/ETHRegistrar"; import { Registry } from "./abis/namechain/Registry"; // ABIs for ENSRoot Datasource import { BaseRegistrar as root_BaseRegistrar } from "./abis/root/BaseRegistrar"; @@ -102,6 +103,13 @@ export default { startBlock: 8515717, }, + // + + ETHRegistry: { + abi: Registry, + address: "0x1291be112d480055dafd8a610b7d1e203891c274", + startBlock: 9629999, + }, RootRegistry: { abi: Registry, address: "0x8a791620dd6260079bf849dc5567adc3f2fdc318", @@ -125,7 +133,6 @@ export default { abi: ResolverABI, startBlock: 9629999, }, - Registry: { abi: Registry, startBlock: 9629999, @@ -134,6 +141,16 @@ export default { abi: EnhancedAccessControl, startBlock: 9629999, }, + ETHRegistry: { + abi: Registry, + address: "0x5fc8d32690cc91d4c39d9d3abcbd16989f875707", + startBlock: 9629999, + }, + ETHRegistrar: { + abi: ETHRegistrar, + address: "0xa513e6e4b8f2a923d98304ec87f64353c4d5c853", + startBlock: 9629999, + }, }, }, From 96ce01a17a38b1854b63df2782a87005811f5c70 Mon Sep 17 00:00:00 2001 From: shrugs Date: Thu, 20 Nov 2025 16:56:13 -0600 Subject: [PATCH 052/102] fix: lint --- apps/ensapi/src/graphql-api/schema/domain.ts | 2 -- apps/ensapi/src/graphql-api/schema/resolver.ts | 2 -- .../ensv2/handlers/ensv1/RegistrarController.ts | 1 + .../plugins/ensv2/handlers/ensv2/ENSv2Registry.ts | 8 +------- .../plugins/ensv2/handlers/ensv2/ETHRegistrar.ts | 1 + .../protocol-acceleration/handlers/Registry.ts | 1 - packages/ensnode-schema/src/schemas/ensv2.schema.ts | 8 ++++---- .../src/schemas/protocol-acceleration.schema.ts | 13 +++---------- packages/ensnode-sdk/src/internal.ts | 2 +- 9 files changed, 11 insertions(+), 27 deletions(-) diff --git a/apps/ensapi/src/graphql-api/schema/domain.ts b/apps/ensapi/src/graphql-api/schema/domain.ts index 6160645b6..1e60a9f11 100644 --- a/apps/ensapi/src/graphql-api/schema/domain.ts +++ b/apps/ensapi/src/graphql-api/schema/domain.ts @@ -8,11 +8,9 @@ import { } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; -import { getCanonicalPath } from "@/graphql-api/lib/get-canonical-path"; import { getDomainResolver } from "@/graphql-api/lib/get-domain-resolver"; import { getModelId } from "@/graphql-api/lib/get-id"; import { getLatestRegistration } from "@/graphql-api/lib/get-latest-registration"; -import { rejectAnyErrors } from "@/graphql-api/lib/reject-any-errors"; import { AccountRef } from "@/graphql-api/schema/account"; import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; import { cursors } from "@/graphql-api/schema/cursors"; diff --git a/apps/ensapi/src/graphql-api/schema/resolver.ts b/apps/ensapi/src/graphql-api/schema/resolver.ts index 396e59948..b2a2f1ac3 100644 --- a/apps/ensapi/src/graphql-api/schema/resolver.ts +++ b/apps/ensapi/src/graphql-api/schema/resolver.ts @@ -2,7 +2,6 @@ import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@poth import { namehash } from "viem"; import { - makePermissionsId, makeResolverRecordsId, NODE_ANY, type RequiredAndNotNull, @@ -17,7 +16,6 @@ import { AccountIdInput, AccountIdRef } from "@/graphql-api/schema/account-id"; import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; import { cursors } from "@/graphql-api/schema/cursors"; import { NameOrNodeInput } from "@/graphql-api/schema/name-or-node"; -import { PermissionsRef } from "@/graphql-api/schema/permissions"; import { ResolverRecordsRef } from "@/graphql-api/schema/resolver-records"; import { db } from "@/lib/db"; diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts index faa975b4b..abf06e7e0 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts @@ -1,3 +1,4 @@ +/** biome-ignore-all lint/correctness/noUnusedVariables: ignore for now */ import { type Context, ponder } from "ponder:registry"; import schema from "ponder:schema"; import { labelhash, namehash } from "viem"; diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts index f5ebef792..318096e5e 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts @@ -1,3 +1,4 @@ +/** biome-ignore-all lint/correctness/noUnusedVariables: ignore for now */ import { type Context, ponder } from "ponder:registry"; import schema from "ponder:schema"; import { replaceBigInts } from "ponder"; @@ -184,13 +185,6 @@ export default function () { const canonicalId = getCanonicalId(tokenId); const domainId = makeENSv2DomainId(registryAccountId, canonicalId); - if ( - domainId === - "eip155:15658734/erc1155:0x5fc8d32690cc91d4c39d9d3abcbd16989f875707/115467421361047454831865845388833304924993579272809100258442734802450801229824" - ) { - console.log("parent.eth subregistry update!", event.args); - } - // update domain's subregistry const isDeletion = isAddressEqual(zeroAddress, subregistry); if (isDeletion) { diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ETHRegistrar.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ETHRegistrar.ts index 7df1b4ff2..86e070439 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ETHRegistrar.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ETHRegistrar.ts @@ -1,3 +1,4 @@ +/** biome-ignore-all lint/correctness/noUnusedFunctionParameters: ignore for now */ import { ponder } from "ponder:registry"; import { PluginName } from "@ensnode/ensnode-sdk"; diff --git a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Registry.ts b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Registry.ts index 841660e91..4660a1c5f 100644 --- a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Registry.ts +++ b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Registry.ts @@ -5,7 +5,6 @@ import { type Address, isAddressEqual, zeroAddress } from "viem"; import { getENSRootChainId } from "@ensnode/datasources"; import { - type ENSv1DomainId, type LabelHash, makeENSv1DomainId, makeResolverId, diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/schemas/ensv2.schema.ts index 52f28094b..e88e5f29a 100644 --- a/packages/ensnode-schema/src/schemas/ensv2.schema.ts +++ b/packages/ensnode-schema/src/schemas/ensv2.schema.ts @@ -219,7 +219,7 @@ export const registration = onchainTable( }), ); -export const registration_relations = relations(registration, ({ one, many }) => ({ +export const registration_relations = relations(registration, ({ one }) => ({ // belongs to either v1Domain or v2Domain v1Domain: one(v1Domain, { fields: [registration.domainId], @@ -255,7 +255,7 @@ export const permissions = onchainTable( }), ); -export const relations_permissions = relations(permissions, ({ one, many }) => ({ +export const relations_permissions = relations(permissions, ({ many }) => ({ resources: many(permissionsResource), users: many(permissionsUser), })); @@ -274,7 +274,7 @@ export const permissionsResource = onchainTable( }), ); -export const relations_permissionsResource = relations(permissionsResource, ({ one, many }) => ({ +export const relations_permissionsResource = relations(permissionsResource, ({ one }) => ({ permissions: one(permissions, { fields: [permissionsResource.chainId, permissionsResource.address], references: [permissions.chainId, permissions.address], @@ -299,7 +299,7 @@ export const permissionsUser = onchainTable( }), ); -export const relations_permissionsUser = relations(permissionsUser, ({ one, many }) => ({ +export const relations_permissionsUser = relations(permissionsUser, ({ one }) => ({ account: one(account, { fields: [permissionsUser.user], references: [account.id], diff --git a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts index a93bdf529..e7b7f6987 100644 --- a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts +++ b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts @@ -2,17 +2,10 @@ * Schema Definitions that power Protocol Acceleration in the Resolution API. */ -import { onchainEnum, onchainTable, primaryKey, relations, uniqueIndex } from "ponder"; +import { onchainTable, primaryKey, relations, uniqueIndex } from "ponder"; import type { Address } from "viem"; -import type { - ChainId, - DomainId, - Node, - RegistryId, - ResolverId, - ResolverRecordsId, -} from "@ensnode/ensnode-sdk"; +import type { ChainId, DomainId, Node, ResolverId, ResolverRecordsId } from "@ensnode/ensnode-sdk"; // TODO: implement resolverType & polymorphic field availability @@ -141,7 +134,7 @@ export const resolver = onchainTable( }), ); -export const resolver_relations = relations(resolver, ({ one, many }) => ({ +export const resolver_relations = relations(resolver, ({ many }) => ({ records: many(resolverRecords), })); diff --git a/packages/ensnode-sdk/src/internal.ts b/packages/ensnode-sdk/src/internal.ts index 43cfb47a3..74cd6c38b 100644 --- a/packages/ensnode-sdk/src/internal.ts +++ b/packages/ensnode-sdk/src/internal.ts @@ -24,6 +24,6 @@ export * from "./shared/config/types"; export * from "./shared/config/validatons"; export * from "./shared/config/zod-schemas"; export * from "./shared/datasources-with-resolvers"; -export * from "./shared/log-level"; export * from "./shared/interpretation/interpret-record-values"; +export * from "./shared/log-level"; export * from "./shared/zod-schemas"; From 6a46c9fa598516162542d0cbdf0bc5233256b67f Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 21 Nov 2025 11:39:15 -0600 Subject: [PATCH 053/102] fix weird namerenewed error and move lists to connections --- apps/ensapi/src/graphql-api/schema/domain.ts | 27 ++++--- .../ensapi/src/graphql-api/schema/registry.ts | 36 ++++++---- .../src/lib/ensv2/registration-db-helpers.ts | 9 ++- .../ensv2/handlers/ensv1/BaseRegistrar.ts | 9 +-- .../ensv2/handlers/ensv1/NameWrapper.ts | 9 +-- .../ensv2/handlers/ensv2/ENSv2Registry.ts | 28 ++++---- apps/ensindexer/src/plugins/ensv2/plugin.ts | 70 ++++++++++--------- 7 files changed, 104 insertions(+), 84 deletions(-) diff --git a/apps/ensapi/src/graphql-api/schema/domain.ts b/apps/ensapi/src/graphql-api/schema/domain.ts index 1e60a9f11..fa60ab46e 100644 --- a/apps/ensapi/src/graphql-api/schema/domain.ts +++ b/apps/ensapi/src/graphql-api/schema/domain.ts @@ -5,6 +5,7 @@ import { type ENSv1DomainId, type ENSv2DomainId, getCanonicalId, + type RegistrationId, } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; @@ -197,16 +198,26 @@ DomainInterfaceRef.implement({ //////////////////////// // Domain.registrations //////////////////////// - registrations: t.loadableGroup({ + registrations: t.connection({ description: "TODO", type: RegistrationInterfaceRef, - load: (ids: DomainId[]) => - db.query.registration.findMany({ - where: (t, { inArray }) => inArray(t.domainId, ids), - orderBy: (t, { desc }) => desc(t.index), - }), - group: (registration) => (registration as Registration).domainId, - resolve: getModelId, + resolve: (parent, args, context) => + resolveCursorConnection( + { ...DEFAULT_CONNECTION_ARGS, args }, + async ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => + db.query.registration.findMany({ + where: (t, { lt, gt, and, eq }) => + and( + ...[ + eq(t.domainId, parent.id), + before !== undefined && lt(t.id, cursors.decode(before)), + after !== undefined && gt(t.id, cursors.decode(after)), + ].filter((c) => !!c), + ), + orderBy: (t, { asc, desc }) => (inverted ? asc(t.index) : desc(t.index)), + limit, + }), + ), }), }), }); diff --git a/apps/ensapi/src/graphql-api/schema/registry.ts b/apps/ensapi/src/graphql-api/schema/registry.ts index c029cb5a5..7af8b6a1b 100644 --- a/apps/ensapi/src/graphql-api/schema/registry.ts +++ b/apps/ensapi/src/graphql-api/schema/registry.ts @@ -7,7 +7,7 @@ import { getModelId } from "@/graphql-api/lib/get-id"; import { AccountIdInput, AccountIdRef } from "@/graphql-api/schema/account-id"; import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; import { cursors } from "@/graphql-api/schema/cursors"; -import { type ENSv2Domain, ENSv2DomainRef } from "@/graphql-api/schema/domain"; +import { ENSv2DomainRef } from "@/graphql-api/schema/domain"; import { PermissionsRef } from "@/graphql-api/schema/permissions"; import { db } from "@/lib/db"; @@ -33,20 +33,30 @@ RegistryRef.implement({ nullable: false, }), - // //////////////////// - // // Registry.parents - // //////////////////// - parents: t.loadableGroup({ + //////////////////// + // Registry.parents + //////////////////// + parents: t.connection({ description: "TODO", type: ENSv2DomainRef, - load: (ids: RegistryId[]) => - db.query.v2Domain.findMany({ - where: (t, { inArray }) => inArray(t.subregistryId, ids), - with: { label: true }, - }), - // biome-ignore lint/style/noNonNullAssertion: subregistryId guaranteed to exist via inArray - group: (domain) => (domain as ENSv2Domain).subregistryId!, - resolve: getModelId, + resolve: (parent, args, context) => + resolveCursorConnection( + { ...DEFAULT_CONNECTION_ARGS, args }, + async ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => + db.query.v2Domain.findMany({ + where: (t, { lt, gt, and, eq }) => + and( + ...[ + eq(t.subregistryId, parent.id), + before !== undefined && lt(t.id, cursors.decode(before)), + after !== undefined && gt(t.id, cursors.decode(after)), + ].filter((c) => !!c), + ), + orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), + limit, + with: { label: true }, + }), + ), }), ////////////////////// diff --git a/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts index 2d76194f2..29f4c8836 100644 --- a/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts @@ -37,9 +37,8 @@ export async function supercedeLatestRegistration( } /** - * Returns whether Registration is expired. - * - * @dev Grace Period is considered 'expired'. + * Returns whether Registration is expired. If the Registration includes a Grace Period, the + * Grace Period window is considered expired. */ export function isRegistrationExpired( registration: typeof schema.registration.$inferSelect, @@ -53,8 +52,8 @@ export function isRegistrationExpired( } /** - * Returns whether Registration is fully expired. - * @dev Grace Period is considered 'unexpired'. + * Returns whether Registration is fully expired. If the Registration includes a Grace Period, the + * Grace Period window is considered NOT fully-expired. */ export function isRegistrationFullyExpired( registration: typeof schema.registration.$inferSelect, diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts index 40b85cc9d..8b4b22e26 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts @@ -95,6 +95,7 @@ export default function () { }>; }) { const { id: tokenId, owner, expires: expiration } = event.args; + const registrant = owner; const labelHash = registrarTokenIdToLabelHash(tokenId); const registrar = getThisAccountId(context, event); @@ -116,15 +117,11 @@ export default function () { // supercede the latest Registration if exists if (registration) await supercedeLatestRegistration(context, registration); - const nextIndex = registration ? registration.index + 1 : 0; - const registrationId = makeLatestRegistrationId(domainId); - const registrant = owner; - // insert BaseRegistrar Registration await ensureAccount(context, registrant); await context.db.insert(schema.registration).values({ - id: registrationId, - index: nextIndex, + id: makeLatestRegistrationId(domainId), + index: registration ? registration.index + 1 : 0, type: "BaseRegistrar", registrarChainId: registrar.chainId, registrarAddress: registrar.address, diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts index 5092d9f31..399fc6430 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts @@ -160,6 +160,7 @@ export default function () { const { node, name: _name, owner, fuses, expiry: _expiration } = event.args; const expiration = interpretExpiration(_expiration); const name = _name as DNSEncodedLiteralName; + const registrant = owner; const registrar = getThisAccountId(context, event); const domainId = makeENSv1DomainId(node); @@ -245,15 +246,11 @@ export default function () { // supercede the latest Registration if exists if (registration) await supercedeLatestRegistration(context, registration); - const nextIndex = registration ? registration.index + 1 : 0; - const registrationId = makeLatestRegistrationId(domainId); - const registrant = owner; - // insert NameWrapper Registration await ensureAccount(context, registrant); await context.db.insert(schema.registration).values({ - id: registrationId, - index: nextIndex, + id: makeLatestRegistrationId(domainId), + index: registration ? registration.index + 1 : 0, type: "NameWrapper", registrarChainId: registrar.chainId, registrarAddress: registrar.address, diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts index 318096e5e..3edc8c201 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts @@ -19,7 +19,7 @@ import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; import { ensureLabel } from "@/lib/ensv2/label-db-helpers"; import { getLatestRegistration, - isRegistrationExpired, + isRegistrationFullyExpired, supercedeLatestRegistration, } from "@/lib/ensv2/registration-db-helpers"; import { getThisAccountId } from "@/lib/get-this-account-id"; @@ -94,10 +94,11 @@ export default function () { }); const registration = await getLatestRegistration(context, domainId); - const isExpired = registration && isRegistrationExpired(registration, event.block.timestamp); + const isFullyExpired = + registration && isRegistrationFullyExpired(registration, event.block.timestamp); // Invariant: If there is an existing Registration, it must be expired. - if (registration && !isExpired) { + if (registration && !isFullyExpired) { throw new Error( `Invariant(ENSv2Registry:NameRegistered): Existing unexpired registration found in NameRegistered, expected none or expired.\n${toJson(registration)}`, ); @@ -106,14 +107,11 @@ export default function () { // supercede the latest Registration if exists if (registration) await supercedeLatestRegistration(context, registration); - const nextIndex = registration ? registration.index + 1 : 0; - const registrationId = makeLatestRegistrationId(domainId); - // insert ENSv2Registry Registration await ensureAccount(context, registrant); await context.db.insert(schema.registration).values({ - id: registrationId, - index: nextIndex, + id: makeLatestRegistrationId(domainId), + index: registration ? registration.index + 1 : 0, type: "ENSv2Registry", registrarChainId: registry.chainId, registrarAddress: registry.address, @@ -148,15 +146,17 @@ export default function () { // Invariant: Registration must exist if (!registration) { - throw new Error(`Invariant(ENSv2Registry:NameRenewed): Registration expected none found.`); + throw new Error(`Invariant(ENSv2Registry:NameRenewed): Registration expected, none found.`); } + // TODO: based on my read, NameRenewed cannot be emitted for an expired registration... so why + // is this hitting? // Invariant: Registration must not be expired - if (isRegistrationExpired(registration, event.block.timestamp)) { - throw new Error( - `Invariant(ENSv2Registry:NameRenewed): Registration found but it is expired:\n${toJson(registration)}`, - ); - } + // if (isRegistrationFullyExpired(registration, event.block.timestamp)) { + // throw new Error( + // `Invariant(ENSv2Registry:NameRenewed): Registration found but it is expired:\n${toJson(registration)}`, + // ); + // } // TODO: optional invariant, v2Domain must also exist? implied by expiry check diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index a3d56a4c7..d61507c7d 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -1,41 +1,47 @@ /** - * TODO - * - Renewals - * - indexes - * - ? https://pothos-graphql.dev/docs/plugins/tracing - * - ThreeDNS - * - ? polymorphic resolver metadata in drizzle schema for better typechecking - * - ? move all entity ids to opaque base58 encoded IDs? kinda nice since they're just supposed to be opaque, useful for relay purposes, allows the scalar types to all be ID and then casted. but nice to use CAIP identifiers for resolvers and permissions etc. so many just for domains and registries? + * TODO TODAY + * - document schema approach, only materialization is v1 effective token owner into v1Domain.owner + * - all polymorphism is done at graphql layer + * - materialization of the canonical namegraph within ponder becomes really complex really quickly + * - this absoutely nukes performance + * - some state (i.e. registration expirations) are only knowable at Forward-Resolution-time and both the on-chain and indexed state need to check that at resolve-time to be compliant. we _could_ implement garbage collection to mark registrations as expired, but again that defeats all of ponder's cache heuristics and isn't even that helpful. + * - the absolutely least-likely-to-cause-horrible-logic approach is to mirror on-chain state 1:1 and perform at query time all of the resolution-time logic that ens applies — this forces the implementations to match as closely as possible. obvious exception for needing to materialize certain aspects of the state (like v1Domain.owner) because actually performing that filter at runtime is abominable. i.e. we have to realize that the performance tradeoffs of evm code and typescript code against postgres are different. ex: trivial to batch-load the full labelhashpath in v2, but more extensive to recursively loop the query (like evm code does) because our individual loads from the db are relatively more expensive. + * - self-review and document where needed * - * - *.addr.reverse subnames in ENSv1Registry aren't correctly connected to the ENSv2 Domain that represents addr.reverse - * - the scripts never mint reverse and addr.reverse into the ENSv1 Registry so that's annoying - * - the new .addr.reverse resolver does some fallback bullshit - * - https://github.com/ensdomains/ens-contracts/blob/staging/contracts/reverseResolver/ETHReverseResolver.sol + * - indexes + * - move all list methods to collections + * - Registry.parents + * - update protocol acceleration plugin to track v2 drr * - * - we either need to keep the indexed model 1:1 with the on-chain model and stitch things together at the api layer - * OR go hard with materialization, and we need to reparent Domains based on individual v2 registry's fallback mechanisms - * which seems really flimsy and annoying and reparenting is not good for cache behavior. - * - maybe better to have ENSv1Domain and ENSv2Domain models. then all polymorphism is applied at the api layer - * - forward traversal (accessing Domain by name) would follow forward resolution, including the fallback logics - * - yeah we'd have to literally do forward resolution - * - anything that returns Domains would need to join against v1 and v2 domains - * - the v1 model need not include implicit registries + * TODO LATER + * - modify Registration schema to more closely match ENSv2, map v1 into it + * - Renewals (v1, v2) + * - include similar /latest / superceding logic, need to be able to reference latest renewal to upsert referrers + * - ThreeDNS + * - Migration + * - need to understand migration pattern better + * - individual names are migrated to v2 and can choose to move to an ENSv2 Registry on L1 or L2 + * - locked names (wrapped and not unwrappable) are 'frozen' by having their fuses burned + * - will need to observe the correct event and then override the existing domain/registratioon info + * - for MigratedWrappedNameRegistries, need to check name expiry during resolution and avoid resolving expired names * - * Pending + * PENDING ENS TEAM * - DedicatedResolver moving to EAC - * - Registry.canonicalName indexing + adjust Domain.canonical reverse traversal logic + * - depends on: namechain --testNames script not crashing in commit >= 803a940 + * - Domain.canonical/Domain.canonicalPath/Domain.fqdn depends on: + * - depends on: Registry.canonicalName implementation + indexing + * - Signal Pattern for Registry contracts + * - depends on: ens team implementing in namechain contracts + * + * MAYBE DO LATER? + * - ? better typechecking for polymorphic entities in drizzle schema + * - could do polymorphic resolver/registration metadata + * - would map well to resolver extensions in graphql + * - ? move all entity ids to opaque base58 encoded IDs? kinda nice since they're just supposed to be opaque, useful for relay purposes, allows the scalar types to all be ID and then casted. but nice to use CAIP identifiers for resolvers and permissions etc. so just for domains and registries? * - * Migration - * - individual names are migrated to v2 and can choose to move to an ENSv2 Registry on L1 or L2 - * - locked names (wrapped and not unwrappable) are 'frozen' by having their fuses burned - * - will need to observe the correct event and then override the existing domain/registratioon info - * - need to know migration status of every domain in order to to construct canonical namegraph at index-time. - * - maybe instead of constructing canonical namegraph we keep it all separate? when addressing domains by name we'd have to more or less perform traversals, including bridgedresolvers. but that's fine? we're going to have to mix forward resolution logic into the api anyway, either at the canonical namegraph construction or while traversing the namegraph - * v2 .eth registry will have a special fallback resolver that resolvers via namechain state - * - fuck me, there can be multiple registrations in v2 world. sub.example.xyz, if not emancipated, cannot be migrated, but sub.example.xyz can still be created in v2 registry in the example.xyz registry. - * - if a v2 name is registered but there's an active namewrapper registration for that same name, we should perhaps ignore all future namewrapper events, as the v2 name overrides it in resolution and the namewrapper is never more consulted for that name (and i guess any subnames under it?) - * - shadow-registering an existing name in v2 also shadows every name under it so we kind of need to do a recursive deletion of all of a shadowed name's subnames, right? cause resolution terminates at the first v2-registered name. - * - for MigratedWrappedNameRegistries, need to check name expiry during resolution and avoid resolving expired names + * TODO MUCH LATER + * - after moving protocol-tracing away from otel we can use otel for ourselves + * https://pothos-graphql.dev/docs/plugins/tracing * */ From c910e71a47adc3ed4b429b5525ca15b550c7c826 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 21 Nov 2025 11:49:56 -0600 Subject: [PATCH 054/102] v2 drr --- ...domain-resolver-relationship-db-helpers.ts | 23 ++++++++++ .../node-resolver-relationship-db-helpers.ts | 24 ----------- .../ensv2/handlers/ensv2/ENSv2Registry.ts | 30 ------------- apps/ensindexer/src/plugins/ensv2/plugin.ts | 2 - .../protocol-acceleration/event-handlers.ts | 6 ++- .../{Registry.ts => ENSv1Registry.ts} | 16 ++----- .../handlers/ENSv2Registry.ts | 35 ++++++++++++++++ .../handlers/ThreeDNSToken.ts | 2 +- .../plugins/protocol-acceleration/plugin.ts | 42 ++++++++++++++++--- 9 files changed, 103 insertions(+), 77 deletions(-) create mode 100644 apps/ensindexer/src/lib/protocol-acceleration/domain-resolver-relationship-db-helpers.ts delete mode 100644 apps/ensindexer/src/lib/protocol-acceleration/node-resolver-relationship-db-helpers.ts rename apps/ensindexer/src/plugins/protocol-acceleration/handlers/{Registry.ts => ENSv1Registry.ts} (83%) create mode 100644 apps/ensindexer/src/plugins/protocol-acceleration/handlers/ENSv2Registry.ts diff --git a/apps/ensindexer/src/lib/protocol-acceleration/domain-resolver-relationship-db-helpers.ts b/apps/ensindexer/src/lib/protocol-acceleration/domain-resolver-relationship-db-helpers.ts new file mode 100644 index 000000000..6156008cb --- /dev/null +++ b/apps/ensindexer/src/lib/protocol-acceleration/domain-resolver-relationship-db-helpers.ts @@ -0,0 +1,23 @@ +import type { Context } from "ponder:registry"; +import schema from "ponder:schema"; +import { type Address, isAddressEqual, zeroAddress } from "viem"; + +import { type AccountId, type DomainId, makeResolverId } from "@ensnode/ensnode-sdk"; + +export async function handleResolverForDomain( + context: Context, + registry: AccountId, + domainId: DomainId, + resolver: Address, +) { + const isZeroResolver = isAddressEqual(zeroAddress, resolver); + if (isZeroResolver) { + await context.db.delete(schema.domainResolverRelation, { ...registry, domainId }); + } else { + const resolverId = makeResolverId({ chainId: registry.chainId, address: resolver }); + await context.db + .insert(schema.domainResolverRelation) + .values({ ...registry, domainId, resolverId }) + .onConflictDoUpdate({ resolverId }); + } +} diff --git a/apps/ensindexer/src/lib/protocol-acceleration/node-resolver-relationship-db-helpers.ts b/apps/ensindexer/src/lib/protocol-acceleration/node-resolver-relationship-db-helpers.ts deleted file mode 100644 index 0c9f5a380..000000000 --- a/apps/ensindexer/src/lib/protocol-acceleration/node-resolver-relationship-db-helpers.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Context } from "ponder:registry"; -import schema from "ponder:schema"; - -import type { AccountId, DomainId, ResolverId } from "@ensnode/ensnode-sdk"; - -export async function removedomainResolverRelation( - context: Context, - registry: AccountId, - domainId: DomainId, -) { - await context.db.delete(schema.domainResolverRelation, { ...registry, domainId }); -} - -export async function upsertdomainResolverRelation( - context: Context, - registry: AccountId, - domainId: DomainId, - resolverId: ResolverId, -) { - await context.db - .insert(schema.domainResolverRelation) - .values({ ...registry, domainId, resolverId }) - .onConflictDoUpdate({ resolverId }); -} diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts index 3edc8c201..7f607d367 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts @@ -198,36 +198,6 @@ export default function () { }, ); - // TODO: add this logic to Protocol Acceleration plugin - // ponder.on( - // namespaceContract(pluginName, "ENSv2Registry:ResolverUpdate"), - // async ({ - // context, - // event, - // }: { - // context: Context; - // event: EventWithArgs<{ - // id: bigint; - // resolver: Address; - // }>; - // }) => { - // const { id: tokenId, resolver: address } = event.args; - - // const canonicalId = getCanonicalId(tokenId); - // const registryAccountId = getThisAccountId(context, event); - // const domainId = makeENSv2DomainId(registryAccountId, canonicalId); - - // // update domain's resolver - // const isDeletion = isAddressEqual(zeroAddress, address); - // if (isDeletion) { - // await context.db.update(schema.v2Domain, { id: domainId }).set({ resolverId: null }); - // } else { - // const resolverId = makeResolverId({ chainId: context.chain.id, address: address }); - // await context.db.update(schema.v2Domain, { id: domainId }).set({ resolverId }); - // } - // }, - // ); - ponder.on( namespaceContract(pluginName, "ENSv2Registry:NameBurned"), async ({ diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index d61507c7d..7da3979e2 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -9,8 +9,6 @@ * - self-review and document where needed * * - indexes - * - move all list methods to collections - * - Registry.parents * - update protocol acceleration plugin to track v2 drr * * TODO LATER diff --git a/apps/ensindexer/src/plugins/protocol-acceleration/event-handlers.ts b/apps/ensindexer/src/plugins/protocol-acceleration/event-handlers.ts index 911c61ce9..8c1b91ffd 100644 --- a/apps/ensindexer/src/plugins/protocol-acceleration/event-handlers.ts +++ b/apps/ensindexer/src/plugins/protocol-acceleration/event-handlers.ts @@ -1,10 +1,12 @@ -import attach_RegistryHandlers from "./handlers/Registry"; +import attach_ENSv1RegistryHandlers from "./handlers/ENSv1Registry"; +import attach_ENSv2RegistryHandlers from "./handlers/ENSv2Registry"; import attach_ResolverHandlers from "./handlers/Resolver"; import attach_StandaloneReverseRegistrarHandlers from "./handlers/StandaloneReverseRegistrar"; import attach_ThreeDNSTokenHandlers from "./handlers/ThreeDNSToken"; export default function () { - attach_RegistryHandlers(); + attach_ENSv1RegistryHandlers(); + attach_ENSv2RegistryHandlers(); attach_ResolverHandlers(); attach_StandaloneReverseRegistrarHandlers(); attach_ThreeDNSTokenHandlers(); diff --git a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Registry.ts b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ENSv1Registry.ts similarity index 83% rename from apps/ensindexer/src/plugins/protocol-acceleration/handlers/Registry.ts rename to apps/ensindexer/src/plugins/protocol-acceleration/handlers/ENSv1Registry.ts index 4660a1c5f..3da1f1073 100644 --- a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Registry.ts +++ b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ENSv1Registry.ts @@ -1,13 +1,12 @@ import config from "@/config"; import { type Context, ponder } from "ponder:registry"; -import { type Address, isAddressEqual, zeroAddress } from "viem"; +import type { Address } from "viem"; import { getENSRootChainId } from "@ensnode/datasources"; import { type LabelHash, makeENSv1DomainId, - makeResolverId, makeSubdomainNode, type Node, PluginName, @@ -16,10 +15,7 @@ import { import { getThisAccountId } from "@/lib/get-this-account-id"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; -import { - removedomainResolverRelation, - upsertdomainResolverRelation, -} from "@/lib/protocol-acceleration/node-resolver-relationship-db-helpers"; +import { handleResolverForDomain } from "@/lib/protocol-acceleration/domain-resolver-relationship-db-helpers"; import { migrateNode, nodeIsMigrated } from "@/lib/protocol-acceleration/registry-migration-status"; const ensRootChainId = getENSRootChainId(config.namespace); @@ -44,13 +40,7 @@ export default function () { const registry = getThisAccountId(context, event); const domainId = makeENSv1DomainId(node); - const isZeroResolver = isAddressEqual(zeroAddress, resolver); - if (isZeroResolver) { - await removedomainResolverRelation(context, registry, domainId); - } else { - const resolverId = makeResolverId({ chainId: registry.chainId, address: resolver }); - await upsertdomainResolverRelation(context, registry, domainId, resolverId); - } + await handleResolverForDomain(context, registry, domainId, resolver); } /** diff --git a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ENSv2Registry.ts b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ENSv2Registry.ts new file mode 100644 index 000000000..db835c8c4 --- /dev/null +++ b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ENSv2Registry.ts @@ -0,0 +1,35 @@ +import { type Context, ponder } from "ponder:registry"; +import type { Address } from "viem"; + +import { getCanonicalId, makeENSv2DomainId, PluginName } from "@ensnode/ensnode-sdk"; + +import { getThisAccountId } from "@/lib/get-this-account-id"; +import { namespaceContract } from "@/lib/plugin-helpers"; +import type { EventWithArgs } from "@/lib/ponder-helpers"; +import { handleResolverForDomain } from "@/lib/protocol-acceleration/domain-resolver-relationship-db-helpers"; + +const pluginName = PluginName.ProtocolAcceleration; + +export default function () { + ponder.on( + namespaceContract(pluginName, "ENSv2Registry:ResolverUpdate"), + async ({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ + id: bigint; + resolver: Address; + }>; + }) => { + const { id: tokenId, resolver } = event.args; + + const registry = getThisAccountId(context, event); + const canonicalId = getCanonicalId(tokenId); + const domainId = makeENSv2DomainId(registry, canonicalId); + + await handleResolverForDomain(context, registry, domainId, resolver); + }, + ); +} diff --git a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ThreeDNSToken.ts b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ThreeDNSToken.ts index 26653537c..83eea02ce 100644 --- a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ThreeDNSToken.ts +++ b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ThreeDNSToken.ts @@ -17,7 +17,7 @@ import { import { getThisAccountId } from "@/lib/get-this-account-id"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; -import { upsertdomainResolverRelation } from "@/lib/protocol-acceleration/node-resolver-relationship-db-helpers"; +import { upsertdomainResolverRelation } from "@/lib/protocol-acceleration/domain-resolver-relationship-db-helpers"; const ThreeDNSResolverByChainId: Record = [ DatasourceNames.ThreeDNSBase, diff --git a/apps/ensindexer/src/plugins/protocol-acceleration/plugin.ts b/apps/ensindexer/src/plugins/protocol-acceleration/plugin.ts index 7636a6adc..6ef2c8b29 100644 --- a/apps/ensindexer/src/plugins/protocol-acceleration/plugin.ts +++ b/apps/ensindexer/src/plugins/protocol-acceleration/plugin.ts @@ -2,6 +2,7 @@ import { createConfig } from "ponder"; import { DatasourceNames, + RegistryABI, ResolverABI, StandaloneReverseRegistrarABI, ThreeDNSTokenABI, @@ -57,6 +58,7 @@ export default createPlugin({ createPonderConfig(config) { const { ensroot } = getRequiredDatasources(config.namespace, REQUIRED_DATASOURCE_NAMES); const { + namechain, basenames, lineanames, threednsOptimism, @@ -76,7 +78,9 @@ export default createPlugin({ ALL_DATASOURCE_NAMES, ), contracts: { - // a multi-chain Resolver ContractConfig + ////////////////////// + // Resolver Contracts + ////////////////////// [namespaceContract(pluginName, "Resolver")]: { abi: ResolverABI, chain: getDatasourcesWithResolvers(config.namespace).reduce( @@ -91,7 +95,9 @@ export default createPlugin({ ), }, - // index the ENSv1RegistryOld on ENS Root Chain + ///////////////////// + // ENSv1 RegistryOld + ///////////////////// [namespaceContract(pluginName, "ENSv1RegistryOld")]: { abi: ensroot.contracts.ENSv1RegistryOld.abi, chain: { @@ -103,7 +109,9 @@ export default createPlugin({ }, }, - // a multi-chain Registry ContractConfig + //////////////////////////// + // ENSv1 Registry Contracts + //////////////////////////// [namespaceContract(pluginName, "ENSv1Registry")]: { abi: ensroot.contracts.ENSv1Registry.abi, chain: { @@ -130,7 +138,29 @@ export default createPlugin({ }, }, - // a multi-chain ThreeDNSToken ContractConfig + //////////////////////////// + // ENSv2 Registry Contracts + //////////////////////////// + [namespaceContract(pluginName, "ENSv2Registry")]: { + abi: RegistryABI, + chain: { + ...chainConfigForContract( + config.globalBlockrange, + ensroot.chain.id, + ensroot.contracts.Registry, + ), + ...(namechain && + chainConfigForContract( + config.globalBlockrange, + namechain.chain.id, + namechain.contracts.Registry, + )), + }, + }, + + ///////////////// + // ThreeDNSToken + ///////////////// [namespaceContract(pluginName, "ThreeDNSToken")]: { abi: ThreeDNSTokenABI, chain: { @@ -149,7 +179,9 @@ export default createPlugin({ }, }, - // a multi-chain StandaloneReverseRegistrar ContractConfig + /////////////////////////////// + // StandaloneReverseRegistrars + /////////////////////////////// [namespaceContract(pluginName, "StandaloneReverseRegistrar")]: { abi: StandaloneReverseRegistrarABI, chain: { From 7c25a1a08b3b7e4f7be26a8a6ff6862e916f312b Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 21 Nov 2025 11:52:08 -0600 Subject: [PATCH 055/102] fix: threedns --- .../domain-resolver-relationship-db-helpers.ts | 2 +- .../protocol-acceleration/handlers/ENSv1Registry.ts | 4 ++-- .../protocol-acceleration/handlers/ENSv2Registry.ts | 4 ++-- .../protocol-acceleration/handlers/ThreeDNSToken.ts | 12 +++++------- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/apps/ensindexer/src/lib/protocol-acceleration/domain-resolver-relationship-db-helpers.ts b/apps/ensindexer/src/lib/protocol-acceleration/domain-resolver-relationship-db-helpers.ts index 6156008cb..52b78fc37 100644 --- a/apps/ensindexer/src/lib/protocol-acceleration/domain-resolver-relationship-db-helpers.ts +++ b/apps/ensindexer/src/lib/protocol-acceleration/domain-resolver-relationship-db-helpers.ts @@ -4,7 +4,7 @@ import { type Address, isAddressEqual, zeroAddress } from "viem"; import { type AccountId, type DomainId, makeResolverId } from "@ensnode/ensnode-sdk"; -export async function handleResolverForDomain( +export async function ensureDomainResolverRelation( context: Context, registry: AccountId, domainId: DomainId, diff --git a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ENSv1Registry.ts b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ENSv1Registry.ts index 3da1f1073..91b2dd996 100644 --- a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ENSv1Registry.ts +++ b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ENSv1Registry.ts @@ -15,7 +15,7 @@ import { import { getThisAccountId } from "@/lib/get-this-account-id"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; -import { handleResolverForDomain } from "@/lib/protocol-acceleration/domain-resolver-relationship-db-helpers"; +import { ensureDomainResolverRelation } from "@/lib/protocol-acceleration/domain-resolver-relationship-db-helpers"; import { migrateNode, nodeIsMigrated } from "@/lib/protocol-acceleration/registry-migration-status"; const ensRootChainId = getENSRootChainId(config.namespace); @@ -40,7 +40,7 @@ export default function () { const registry = getThisAccountId(context, event); const domainId = makeENSv1DomainId(node); - await handleResolverForDomain(context, registry, domainId, resolver); + await ensureDomainResolverRelation(context, registry, domainId, resolver); } /** diff --git a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ENSv2Registry.ts b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ENSv2Registry.ts index db835c8c4..a2e5f8412 100644 --- a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ENSv2Registry.ts +++ b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ENSv2Registry.ts @@ -6,7 +6,7 @@ import { getCanonicalId, makeENSv2DomainId, PluginName } from "@ensnode/ensnode- import { getThisAccountId } from "@/lib/get-this-account-id"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; -import { handleResolverForDomain } from "@/lib/protocol-acceleration/domain-resolver-relationship-db-helpers"; +import { ensureDomainResolverRelation } from "@/lib/protocol-acceleration/domain-resolver-relationship-db-helpers"; const pluginName = PluginName.ProtocolAcceleration; @@ -29,7 +29,7 @@ export default function () { const canonicalId = getCanonicalId(tokenId); const domainId = makeENSv2DomainId(registry, canonicalId); - await handleResolverForDomain(context, registry, domainId, resolver); + await ensureDomainResolverRelation(context, registry, domainId, resolver); }, ); } diff --git a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ThreeDNSToken.ts b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ThreeDNSToken.ts index 83eea02ce..2f705890c 100644 --- a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ThreeDNSToken.ts +++ b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ThreeDNSToken.ts @@ -17,7 +17,7 @@ import { import { getThisAccountId } from "@/lib/get-this-account-id"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; -import { upsertdomainResolverRelation } from "@/lib/protocol-acceleration/domain-resolver-relationship-db-helpers"; +import { ensureDomainResolverRelation } from "@/lib/protocol-acceleration/domain-resolver-relationship-db-helpers"; const ThreeDNSResolverByChainId: Record = [ DatasourceNames.ThreeDNSBase, @@ -59,17 +59,15 @@ export default function () { const node = makeSubdomainNode(labelHash, parentNode); const domainId = makeENSv1DomainId(node); - const resolverAddress = ThreeDNSResolverByChainId[context.chain.id]; - if (!resolverAddress) { + // all ThreeDNSToken nodes have a hardcoded resolver + const resolver = ThreeDNSResolverByChainId[context.chain.id]; + if (!resolver) { throw new Error( `Invariant: ThreeDNSToken ${event.log.address} on chain ${context.chain.id} doesn't have an associated Resolver?`, ); } - const resolverId = makeResolverId({ chainId: registry.chainId, address: resolverAddress }); - - // all ThreeDNSToken nodes have a hardcoded resolver at that address - await upsertdomainResolverRelation(context, registry, domainId, resolverId); + await ensureDomainResolverRelation(context, registry, domainId, resolver); }, ); } From 8ddcebf97b75ea6ad168310566257d9368ffa8c7 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 21 Nov 2025 12:27:20 -0600 Subject: [PATCH 056/102] fix: update to latest namechain --- apps/ensapi/src/graphql-api/schema/domain.ts | 12 +- .../ensv2/handlers/ensv2/ENSv2Registry.ts | 63 +- apps/ensindexer/src/plugins/ensv2/plugin.ts | 1 - .../handlers/ENSv2Registry.ts | 6 +- .../src/abis/ensv2/ETHRegistrar.ts | 534 ++++++++ .../EnhancedAccessControl.ts | 410 +++--- .../src/abis/{namechain => ensv2}/Registry.ts | 503 ++++--- .../src/abis/namechain/ETHRegistrar.ts | 1152 ----------------- packages/datasources/src/ens-test-env.ts | 6 +- packages/datasources/src/index.ts | 4 +- packages/datasources/src/lib/ResolverABI.ts | 2 +- packages/datasources/src/mainnet.ts | 8 +- packages/datasources/src/sepolia.ts | 8 +- .../src/schemas/ensv2.schema.ts | 3 + 14 files changed, 1050 insertions(+), 1662 deletions(-) create mode 100644 packages/datasources/src/abis/ensv2/ETHRegistrar.ts rename packages/datasources/src/abis/{namechain => ensv2}/EnhancedAccessControl.ts (100%) rename packages/datasources/src/abis/{namechain => ensv2}/Registry.ts (94%) delete mode 100644 packages/datasources/src/abis/namechain/ETHRegistrar.ts diff --git a/apps/ensapi/src/graphql-api/schema/domain.ts b/apps/ensapi/src/graphql-api/schema/domain.ts index fa60ab46e..66c855fc0 100644 --- a/apps/ensapi/src/graphql-api/schema/domain.ts +++ b/apps/ensapi/src/graphql-api/schema/domain.ts @@ -283,7 +283,17 @@ ENSv2DomainRef.implement({ type: "BigInt", description: "TODO", nullable: false, - resolve: (parent) => getCanonicalId(parent.labelHash), + resolve: (parent) => getCanonicalId(parent.tokenId), + }), + + ////////////////////// + // Domain.tokenId + ////////////////////// + tokenId: t.field({ + type: "BigInt", + description: "TODO", + nullable: false, + resolve: (parent) => parent.tokenId, }), ////////////////////// diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts index 7f607d367..377d756b3 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts @@ -40,11 +40,11 @@ export default function () { event: EventWithArgs<{ tokenId: bigint; label: string; - expiration: bigint; + expiry: bigint; registeredBy: Address; }>; }) => { - const { tokenId, label: _label, expiration, registeredBy: registrant } = event.args; + const { tokenId, label: _label, expiry: expiration, registeredBy: registrant } = event.args; const label = _label as LiteralLabel; const labelHash = labelhash(label); @@ -88,9 +88,10 @@ export default function () { // insert v2Domain await context.db.insert(schema.v2Domain).values({ id: domainId, + tokenId, registryId, labelHash, - // NOTE: ownerId omitted, Tranfer* events are sole source of ownership + // NOTE: ownerId omitted, Transfer* events are sole source of ownership }); const registration = await getLatestRegistration(context, domainId); @@ -124,7 +125,7 @@ export default function () { ); ponder.on( - namespaceContract(pluginName, "ENSv2Registry:NameRenewed"), + namespaceContract(pluginName, "ENSv2Registry:ExpiryUpdated"), async ({ context, event, @@ -132,11 +133,11 @@ export default function () { context: Context; event: EventWithArgs<{ tokenId: bigint; - newExpiration: bigint; - renewedBy: Address; + newExpiry: bigint; + changedBy: Address; }>; }) => { - const { tokenId, newExpiration: expiration, renewedBy: renewer } = event.args; + const { tokenId, newExpiry: expiration, changedBy: renewer } = event.args; const registry = getThisAccountId(context, event); const canonicalId = getCanonicalId(tokenId); @@ -149,16 +150,12 @@ export default function () { throw new Error(`Invariant(ENSv2Registry:NameRenewed): Registration expected, none found.`); } - // TODO: based on my read, NameRenewed cannot be emitted for an expired registration... so why - // is this hitting? // Invariant: Registration must not be expired - // if (isRegistrationFullyExpired(registration, event.block.timestamp)) { - // throw new Error( - // `Invariant(ENSv2Registry:NameRenewed): Registration found but it is expired:\n${toJson(registration)}`, - // ); - // } - - // TODO: optional invariant, v2Domain must also exist? implied by expiry check + if (isRegistrationFullyExpired(registration, event.block.timestamp)) { + throw new Error( + `Invariant(ENSv2Registry:NameRenewed): Registration found but it is expired:\n${toJson(registration)}`, + ); + } // update Registration await context.db.update(schema.registration, { id: registration.id }).set({ expiration }); @@ -168,18 +165,18 @@ export default function () { ); ponder.on( - namespaceContract(pluginName, "ENSv2Registry:SubregistryUpdate"), + namespaceContract(pluginName, "ENSv2Registry:SubregistryUpdated"), async ({ context, event, }: { context: Context; event: EventWithArgs<{ - id: bigint; + tokenId: bigint; subregistry: Address; }>; }) => { - const { id: tokenId, subregistry } = event.args; + const { tokenId, subregistry } = event.args; const registryAccountId = getThisAccountId(context, event); const canonicalId = getCanonicalId(tokenId); @@ -199,26 +196,33 @@ export default function () { ); ponder.on( - namespaceContract(pluginName, "ENSv2Registry:NameBurned"), + namespaceContract(pluginName, "ENSv2Registry:TokenRegenerated"), async ({ context, event, }: { context: Context; event: EventWithArgs<{ - tokenId: bigint; - burnedBy: Address; + oldTokenId: bigint; + newTokenId: bigint; + resource: bigint; }>; }) => { - const { tokenId } = event.args; + const { oldTokenId, newTokenId, resource } = event.args; - const canonicalId = getCanonicalId(tokenId); + // Invariant: CanonicalIds match + if (getCanonicalId(oldTokenId) !== getCanonicalId(newTokenId)) { + throw new Error(`Invariant(ENSv2Registry:TokenRegenerated): Canonical ID Malformed.`); + } + + const canonicalId = getCanonicalId(oldTokenId); const registryAccountId = getThisAccountId(context, event); const domainId = makeENSv2DomainId(registryAccountId, canonicalId); - await context.db.delete(schema.v2Domain, { id: domainId }); + // TODO: likely need to track resource as well, since it depends on eacVersion + // then we can likely provide a Domain.resource -> PermissionsResource resolver in the api - // NOTE: we explicitly keep the Registration/Renewal entities around + await context.db.update(schema.v2Domain, { id: domainId }).set({ tokenId: newTokenId }); }, ); @@ -235,8 +239,11 @@ export default function () { const registryAccountId = getThisAccountId(context, event); const domainId = makeENSv2DomainId(registryAccountId, canonicalId); - // just update the owner, NameBurned handles existence - await context.db.update(schema.v2Domain, { id: domainId }).set({ ownerId: owner }); + // just update the owner + // any _burns are always followed by a _mint, which would set the owner correctly + await context.db + .update(schema.v2Domain, { id: domainId }) + .set({ ownerId: interpretAddress(owner) }); } ponder.on( diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index 7da3979e2..653d94030 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -9,7 +9,6 @@ * - self-review and document where needed * * - indexes - * - update protocol acceleration plugin to track v2 drr * * TODO LATER * - modify Registration schema to more closely match ENSv2, map v1 into it diff --git a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ENSv2Registry.ts b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ENSv2Registry.ts index a2e5f8412..e918bc447 100644 --- a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ENSv2Registry.ts +++ b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ENSv2Registry.ts @@ -12,18 +12,18 @@ const pluginName = PluginName.ProtocolAcceleration; export default function () { ponder.on( - namespaceContract(pluginName, "ENSv2Registry:ResolverUpdate"), + namespaceContract(pluginName, "ENSv2Registry:ResolverUpdated"), async ({ context, event, }: { context: Context; event: EventWithArgs<{ - id: bigint; + tokenId: bigint; resolver: Address; }>; }) => { - const { id: tokenId, resolver } = event.args; + const { tokenId, resolver } = event.args; const registry = getThisAccountId(context, event); const canonicalId = getCanonicalId(tokenId); diff --git a/packages/datasources/src/abis/ensv2/ETHRegistrar.ts b/packages/datasources/src/abis/ensv2/ETHRegistrar.ts new file mode 100644 index 000000000..f9415f916 --- /dev/null +++ b/packages/datasources/src/abis/ensv2/ETHRegistrar.ts @@ -0,0 +1,534 @@ +export const ETHRegistrar = [ + { + "inputs": [ + { + "internalType": "bytes32", + "name": "commitment", + "type": "bytes32" + }, + { + "internalType": "uint64", + "name": "validFrom", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "blockTimestamp", + "type": "uint64" + } + ], + "name": "CommitmentTooNew", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "commitment", + "type": "bytes32" + }, + { + "internalType": "uint64", + "name": "validTo", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "blockTimestamp", + "type": "uint64" + } + ], + "name": "CommitmentTooOld", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "duration", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "minDuration", + "type": "uint64" + } + ], + "name": "DurationTooShort", + "type": "error" + }, + { + "inputs": [], + "name": "MaxCommitmentAgeTooLow", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "label", + "type": "string" + } + ], + "name": "NameAlreadyRegistered", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "label", + "type": "string" + } + ], + "name": "NameNotRegistered", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "label", + "type": "string" + } + ], + "name": "NotValid", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "paymentToken", + "type": "address" + } + ], + "name": "PaymentTokenNotSupported", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "commitment", + "type": "bytes32" + } + ], + "name": "UnexpiredCommitmentExists", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "commitment", + "type": "bytes32" + } + ], + "name": "CommitmentMade", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "label", + "type": "string" + }, + { + "indexed": false, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract IRegistry", + "name": "subregistry", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "resolver", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "duration", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "contract IERC20", + "name": "paymentToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "referrer", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "base", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "premium", + "type": "uint256" + } + ], + "name": "NameRegistered", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "label", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "duration", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "newExpiry", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "contract IERC20", + "name": "paymentToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "referrer", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "base", + "type": "uint256" + } + ], + "name": "NameRenewed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IERC20", + "name": "paymentToken", + "type": "address" + } + ], + "name": "PaymentTokenAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IERC20", + "name": "paymentToken", + "type": "address" + } + ], + "name": "PaymentTokenRemoved", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "commitment", + "type": "bytes32" + } + ], + "name": "commit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "commitment", + "type": "bytes32" + } + ], + "name": "commitmentAt", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "label", + "type": "string" + } + ], + "name": "isAvailable", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "paymentToken", + "type": "address" + } + ], + "name": "isPaymentToken", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "label", + "type": "string" + } + ], + "name": "isValid", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "label", + "type": "string" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "secret", + "type": "bytes32" + }, + { + "internalType": "contract IRegistry", + "name": "subregistry", + "type": "address" + }, + { + "internalType": "address", + "name": "resolver", + "type": "address" + }, + { + "internalType": "uint64", + "name": "duration", + "type": "uint64" + }, + { + "internalType": "bytes32", + "name": "referrer", + "type": "bytes32" + } + ], + "name": "makeCommitment", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "label", + "type": "string" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "secret", + "type": "bytes32" + }, + { + "internalType": "contract IRegistry", + "name": "subregistry", + "type": "address" + }, + { + "internalType": "address", + "name": "resolver", + "type": "address" + }, + { + "internalType": "uint64", + "name": "duration", + "type": "uint64" + }, + { + "internalType": "contract IERC20", + "name": "paymentToken", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "referrer", + "type": "bytes32" + } + ], + "name": "register", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "label", + "type": "string" + }, + { + "internalType": "uint64", + "name": "duration", + "type": "uint64" + }, + { + "internalType": "contract IERC20", + "name": "paymentToken", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "referrer", + "type": "bytes32" + } + ], + "name": "renew", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "label", + "type": "string" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint64", + "name": "duration", + "type": "uint64" + }, + { + "internalType": "contract IERC20", + "name": "paymentToken", + "type": "address" + } + ], + "name": "rentPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "base", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "premium", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] as const; diff --git a/packages/datasources/src/abis/namechain/EnhancedAccessControl.ts b/packages/datasources/src/abis/ensv2/EnhancedAccessControl.ts similarity index 100% rename from packages/datasources/src/abis/namechain/EnhancedAccessControl.ts rename to packages/datasources/src/abis/ensv2/EnhancedAccessControl.ts index 15fd8ad5d..db90a209a 100644 --- a/packages/datasources/src/abis/namechain/EnhancedAccessControl.ts +++ b/packages/datasources/src/abis/ensv2/EnhancedAccessControl.ts @@ -1,469 +1,469 @@ export const EnhancedAccessControl = [ { - type: "function", - name: "ROOT_RESOURCE", - inputs: [], - outputs: [ + inputs: [ { - name: "", + internalType: "uint256", + name: "resource", type: "uint256", + }, + { internalType: "uint256", + name: "roleBitmap", + type: "uint256", + }, + { + internalType: "address", + name: "account", + type: "address", }, ], - stateMutability: "view", + name: "EACCannotGrantRoles", + type: "error", }, { - type: "function", - name: "getAssigneeCount", inputs: [ { + internalType: "uint256", name: "resource", type: "uint256", - internalType: "uint256", }, { + internalType: "uint256", name: "roleBitmap", type: "uint256", + }, + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "EACCannotRevokeRoles", + type: "error", + }, + { + inputs: [ + { internalType: "uint256", + name: "roleBitmap", + type: "uint256", }, ], - outputs: [ + name: "EACInvalidRoleBitmap", + type: "error", + }, + { + inputs: [ { - name: "counts", + internalType: "uint256", + name: "resource", type: "uint256", + }, + { internalType: "uint256", + name: "role", + type: "uint256", }, + ], + name: "EACMaxAssignees", + type: "error", + }, + { + inputs: [ { - name: "mask", + internalType: "uint256", + name: "resource", type: "uint256", + }, + { internalType: "uint256", + name: "role", + type: "uint256", }, ], - stateMutability: "view", + name: "EACMinAssignees", + type: "error", + }, + { + inputs: [], + name: "EACRootResourceNotAllowed", + type: "error", }, { - type: "function", - name: "grantRoles", inputs: [ { + internalType: "uint256", name: "resource", type: "uint256", - internalType: "uint256", }, { + internalType: "uint256", name: "roleBitmap", type: "uint256", - internalType: "uint256", }, { + internalType: "address", name: "account", type: "address", - internalType: "address", }, ], - outputs: [ - { - name: "", - type: "bool", - internalType: "bool", - }, - ], - stateMutability: "nonpayable", + name: "EACUnauthorizedAccountRoles", + type: "error", }, { - type: "function", - name: "grantRootRoles", + anonymous: false, inputs: [ { - name: "roleBitmap", - type: "uint256", + indexed: false, internalType: "uint256", + name: "resource", + type: "uint256", }, { + indexed: false, + internalType: "address", name: "account", type: "address", - internalType: "address", }, ], - outputs: [ - { - name: "", - type: "bool", - internalType: "bool", - }, - ], - stateMutability: "nonpayable", + name: "EACAllRolesRevoked", + type: "event", }, { - type: "function", - name: "hasAssignees", + anonymous: false, inputs: [ { + indexed: false, + internalType: "uint256", name: "resource", type: "uint256", - internalType: "uint256", }, { + indexed: false, + internalType: "uint256", name: "roleBitmap", type: "uint256", - internalType: "uint256", }, - ], - outputs: [ { - name: "", - type: "bool", - internalType: "bool", + indexed: false, + internalType: "address", + name: "account", + type: "address", }, ], - stateMutability: "view", + name: "EACRolesGranted", + type: "event", }, { - type: "function", - name: "hasRoles", + anonymous: false, inputs: [ { + indexed: false, + internalType: "uint256", name: "resource", type: "uint256", - internalType: "uint256", }, { - name: "rolesBitmap", - type: "uint256", + indexed: false, internalType: "uint256", + name: "roleBitmap", + type: "uint256", }, { + indexed: false, + internalType: "address", name: "account", type: "address", - internalType: "address", }, ], + name: "EACRolesRevoked", + type: "event", + }, + { + inputs: [], + name: "ROOT_RESOURCE", outputs: [ { + internalType: "uint256", name: "", - type: "bool", - internalType: "bool", + type: "uint256", }, ], stateMutability: "view", + type: "function", }, { - type: "function", - name: "hasRootRoles", inputs: [ { - name: "rolesBitmap", - type: "uint256", internalType: "uint256", + name: "resource", + type: "uint256", }, { - name: "account", - type: "address", - internalType: "address", + internalType: "uint256", + name: "roleBitmap", + type: "uint256", }, ], + name: "getAssigneeCount", outputs: [ { - name: "", - type: "bool", - internalType: "bool", + internalType: "uint256", + name: "counts", + type: "uint256", + }, + { + internalType: "uint256", + name: "mask", + type: "uint256", }, ], stateMutability: "view", + type: "function", }, { - type: "function", - name: "revokeRoles", inputs: [ { + internalType: "uint256", name: "resource", type: "uint256", - internalType: "uint256", }, { + internalType: "uint256", name: "roleBitmap", type: "uint256", - internalType: "uint256", }, { + internalType: "address", name: "account", type: "address", - internalType: "address", }, ], + name: "grantRoles", outputs: [ { + internalType: "bool", name: "", type: "bool", - internalType: "bool", }, ], stateMutability: "nonpayable", + type: "function", }, { - type: "function", - name: "revokeRootRoles", inputs: [ { + internalType: "uint256", name: "roleBitmap", type: "uint256", - internalType: "uint256", }, { + internalType: "address", name: "account", type: "address", - internalType: "address", }, ], + name: "grantRootRoles", outputs: [ { + internalType: "bool", name: "", type: "bool", - internalType: "bool", }, ], stateMutability: "nonpayable", + type: "function", }, { - type: "function", - name: "roleCount", inputs: [ { + internalType: "uint256", name: "resource", type: "uint256", + }, + { internalType: "uint256", + name: "roleBitmap", + type: "uint256", }, ], + name: "hasAssignees", outputs: [ { + internalType: "bool", name: "", - type: "uint256", - internalType: "uint256", + type: "bool", }, ], stateMutability: "view", + type: "function", }, { - type: "function", - name: "roles", inputs: [ { + internalType: "uint256", name: "resource", type: "uint256", + }, + { internalType: "uint256", + name: "rolesBitmap", + type: "uint256", }, { + internalType: "address", name: "account", type: "address", - internalType: "address", }, ], + name: "hasRoles", outputs: [ { + internalType: "bool", name: "", - type: "uint256", - internalType: "uint256", + type: "bool", }, ], stateMutability: "view", + type: "function", }, { - type: "function", - name: "supportsInterface", inputs: [ { - name: "interfaceId", - type: "bytes4", - internalType: "bytes4", + internalType: "uint256", + name: "rolesBitmap", + type: "uint256", + }, + { + internalType: "address", + name: "account", + type: "address", }, ], + name: "hasRootRoles", outputs: [ { + internalType: "bool", name: "", type: "bool", - internalType: "bool", }, ], stateMutability: "view", + type: "function", }, { - type: "event", - name: "EACAllRolesRevoked", inputs: [ { - name: "resource", - type: "uint256", - indexed: false, internalType: "uint256", - }, - { - name: "account", - type: "address", - indexed: false, - internalType: "address", - }, - ], - anonymous: false, - }, - { - type: "event", - name: "EACRolesGranted", - inputs: [ - { name: "resource", type: "uint256", - indexed: false, - internalType: "uint256", }, { + internalType: "uint256", name: "roleBitmap", type: "uint256", - indexed: false, - internalType: "uint256", }, { + internalType: "address", name: "account", type: "address", - indexed: false, - internalType: "address", }, ], - anonymous: false, - }, - { - type: "event", - name: "EACRolesRevoked", - inputs: [ - { - name: "resource", - type: "uint256", - indexed: false, - internalType: "uint256", - }, - { - name: "roleBitmap", - type: "uint256", - indexed: false, - internalType: "uint256", - }, + name: "revokeRoles", + outputs: [ { - name: "account", - type: "address", - indexed: false, - internalType: "address", + internalType: "bool", + name: "", + type: "bool", }, ], - anonymous: false, + stateMutability: "nonpayable", + type: "function", }, { - type: "error", - name: "EACCannotGrantRoles", inputs: [ { - name: "resource", - type: "uint256", internalType: "uint256", - }, - { name: "roleBitmap", type: "uint256", - internalType: "uint256", }, { + internalType: "address", name: "account", type: "address", - internalType: "address", }, ], + name: "revokeRootRoles", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "nonpayable", + type: "function", }, { - type: "error", - name: "EACCannotRevokeRoles", inputs: [ { - name: "resource", - type: "uint256", internalType: "uint256", - }, - { - name: "roleBitmap", + name: "resource", type: "uint256", - internalType: "uint256", - }, - { - name: "account", - type: "address", - internalType: "address", }, ], - }, - { - type: "error", - name: "EACInvalidRoleBitmap", - inputs: [ + name: "roleCount", + outputs: [ { - name: "roleBitmap", - type: "uint256", internalType: "uint256", + name: "", + type: "uint256", }, ], + stateMutability: "view", + type: "function", }, { - type: "error", - name: "EACMaxAssignees", inputs: [ { + internalType: "uint256", name: "resource", type: "uint256", - internalType: "uint256", }, { - name: "role", - type: "uint256", - internalType: "uint256", + internalType: "address", + name: "account", + type: "address", }, ], - }, - { - type: "error", - name: "EACMinAssignees", - inputs: [ + name: "roles", + outputs: [ { - name: "resource", - type: "uint256", internalType: "uint256", - }, - { - name: "role", + name: "", type: "uint256", - internalType: "uint256", }, ], + stateMutability: "view", + type: "function", }, { - type: "error", - name: "EACRootResourceNotAllowed", - inputs: [], - }, - { - type: "error", - name: "EACUnauthorizedAccountRoles", inputs: [ { - name: "resource", - type: "uint256", - internalType: "uint256", - }, - { - name: "roleBitmap", - type: "uint256", - internalType: "uint256", + internalType: "bytes4", + name: "interfaceId", + type: "bytes4", }, + ], + name: "supportsInterface", + outputs: [ { - name: "account", - type: "address", - internalType: "address", + internalType: "bool", + name: "", + type: "bool", }, ], + stateMutability: "view", + type: "function", }, ] as const; diff --git a/packages/datasources/src/abis/namechain/Registry.ts b/packages/datasources/src/abis/ensv2/Registry.ts similarity index 94% rename from packages/datasources/src/abis/namechain/Registry.ts rename to packages/datasources/src/abis/ensv2/Registry.ts index 68df46609..517e43e15 100644 --- a/packages/datasources/src/abis/namechain/Registry.ts +++ b/packages/datasources/src/abis/ensv2/Registry.ts @@ -1,484 +1,471 @@ export const Registry = [ { - type: "function", - name: "balanceOf", + anonymous: false, inputs: [ { + indexed: true, + internalType: "address", name: "account", type: "address", - internalType: "address", }, { - name: "id", - type: "uint256", - internalType: "uint256", + indexed: true, + internalType: "address", + name: "operator", + type: "address", }, - ], - outputs: [ { - name: "", - type: "uint256", - internalType: "uint256", + indexed: false, + internalType: "bool", + name: "approved", + type: "bool", }, ], - stateMutability: "view", + name: "ApprovalForAll", + type: "event", }, { - type: "function", - name: "balanceOfBatch", + anonymous: false, inputs: [ { - name: "accounts", - type: "address[]", - internalType: "address[]", + indexed: true, + internalType: "uint256", + name: "tokenId", + type: "uint256", }, { - name: "ids", - type: "uint256[]", - internalType: "uint256[]", + indexed: false, + internalType: "uint64", + name: "newExpiry", + type: "uint64", }, - ], - outputs: [ { - name: "", - type: "uint256[]", - internalType: "uint256[]", + indexed: false, + internalType: "address", + name: "changedBy", + type: "address", }, ], - stateMutability: "view", + name: "ExpiryUpdated", + type: "event", }, { - type: "function", - name: "getResolver", + anonymous: false, inputs: [ { + indexed: true, + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + { + indexed: false, + internalType: "string", name: "label", type: "string", - internalType: "string", }, - ], - outputs: [ { - name: "", - type: "address", + indexed: false, + internalType: "uint64", + name: "expiry", + type: "uint64", + }, + { + indexed: false, internalType: "address", + name: "registeredBy", + type: "address", }, ], - stateMutability: "view", + name: "NameRegistered", + type: "event", }, { - type: "function", - name: "getSubregistry", + anonymous: false, inputs: [ { - name: "label", - type: "string", - internalType: "string", + indexed: true, + internalType: "uint256", + name: "tokenId", + type: "uint256", }, - ], - outputs: [ { - name: "", + indexed: false, + internalType: "address", + name: "resolver", type: "address", - internalType: "contract IRegistry", }, ], - stateMutability: "view", + name: "ResolverUpdated", + type: "event", }, { - type: "function", - name: "isApprovedForAll", + anonymous: false, inputs: [ { - name: "account", - type: "address", - internalType: "address", + indexed: true, + internalType: "uint256", + name: "tokenId", + type: "uint256", }, { - name: "operator", + indexed: false, + internalType: "contract IRegistry", + name: "subregistry", type: "address", - internalType: "address", }, ], - outputs: [ - { - name: "", - type: "bool", - internalType: "bool", - }, - ], - stateMutability: "view", + name: "SubregistryUpdated", + type: "event", }, { - type: "function", - name: "ownerOf", + anonymous: false, inputs: [ { - name: "id", + indexed: true, + internalType: "uint256", + name: "oldTokenId", type: "uint256", + }, + { + indexed: true, internalType: "uint256", + name: "newTokenId", + type: "uint256", }, - ], - outputs: [ { - name: "owner", - type: "address", - internalType: "address", + indexed: false, + internalType: "uint256", + name: "resource", + type: "uint256", }, ], - stateMutability: "view", + name: "TokenRegenerated", + type: "event", }, { - type: "function", - name: "safeBatchTransferFrom", + anonymous: false, inputs: [ { - name: "from", + indexed: true, + internalType: "address", + name: "operator", type: "address", + }, + { + indexed: true, internalType: "address", + name: "from", + type: "address", }, { + indexed: true, + internalType: "address", name: "to", type: "address", - internalType: "address", }, { + indexed: false, + internalType: "uint256[]", name: "ids", type: "uint256[]", - internalType: "uint256[]", }, { + indexed: false, + internalType: "uint256[]", name: "values", type: "uint256[]", - internalType: "uint256[]", - }, - { - name: "data", - type: "bytes", - internalType: "bytes", }, ], - outputs: [], - stateMutability: "nonpayable", + name: "TransferBatch", + type: "event", }, { - type: "function", - name: "safeTransferFrom", + anonymous: false, inputs: [ { - name: "from", + indexed: true, + internalType: "address", + name: "operator", type: "address", + }, + { + indexed: true, internalType: "address", + name: "from", + type: "address", }, { + indexed: true, + internalType: "address", name: "to", type: "address", - internalType: "address", }, { + indexed: false, + internalType: "uint256", name: "id", type: "uint256", - internalType: "uint256", }, { + indexed: false, + internalType: "uint256", name: "value", type: "uint256", - internalType: "uint256", - }, - { - name: "data", - type: "bytes", - internalType: "bytes", }, ], - outputs: [], - stateMutability: "nonpayable", + name: "TransferSingle", + type: "event", }, { - type: "function", - name: "setApprovalForAll", + anonymous: false, inputs: [ { - name: "operator", - type: "address", - internalType: "address", + indexed: false, + internalType: "string", + name: "value", + type: "string", }, { - name: "approved", - type: "bool", - internalType: "bool", + indexed: true, + internalType: "uint256", + name: "id", + type: "uint256", }, ], - outputs: [], - stateMutability: "nonpayable", + name: "URI", + type: "event", }, { - type: "function", - name: "supportsInterface", inputs: [ { - name: "interfaceId", - type: "bytes4", - internalType: "bytes4", + internalType: "address", + name: "account", + type: "address", + }, + { + internalType: "uint256", + name: "id", + type: "uint256", }, ], + name: "balanceOf", outputs: [ { + internalType: "uint256", name: "", - type: "bool", - internalType: "bool", + type: "uint256", }, ], stateMutability: "view", + type: "function", }, { - type: "event", - name: "ApprovalForAll", inputs: [ { - name: "account", - type: "address", - indexed: true, - internalType: "address", + internalType: "address[]", + name: "accounts", + type: "address[]", }, { - name: "operator", - type: "address", - indexed: true, - internalType: "address", + internalType: "uint256[]", + name: "ids", + type: "uint256[]", }, + ], + name: "balanceOfBatch", + outputs: [ { - name: "approved", - type: "bool", - indexed: false, - internalType: "bool", + internalType: "uint256[]", + name: "", + type: "uint256[]", }, ], - anonymous: false, + stateMutability: "view", + type: "function", }, { - type: "event", - name: "NameBurned", inputs: [ { - name: "tokenId", - type: "uint256", - indexed: true, - internalType: "uint256", + internalType: "string", + name: "label", + type: "string", }, + ], + name: "getResolver", + outputs: [ { - name: "burnedBy", - type: "address", - indexed: false, internalType: "address", + name: "", + type: "address", }, ], - anonymous: false, + stateMutability: "view", + type: "function", }, { - type: "event", - name: "NameRegistered", inputs: [ { - name: "tokenId", - type: "uint256", - indexed: true, - internalType: "uint256", - }, - { + internalType: "string", name: "label", type: "string", - indexed: false, - internalType: "string", - }, - { - name: "expiration", - type: "uint64", - indexed: false, - internalType: "uint64", }, + ], + name: "getSubregistry", + outputs: [ { - name: "registeredBy", + internalType: "contract IRegistry", + name: "", type: "address", - indexed: false, - internalType: "address", }, ], - anonymous: false, + stateMutability: "view", + type: "function", }, { - type: "event", - name: "NameRenewed", inputs: [ { - name: "tokenId", - type: "uint256", - indexed: true, - internalType: "uint256", - }, - { - name: "newExpiration", - type: "uint64", - indexed: false, - internalType: "uint64", + internalType: "address", + name: "account", + type: "address", }, { - name: "renewedBy", - type: "address", - indexed: false, internalType: "address", + name: "operator", + type: "address", }, ], - anonymous: false, - }, - { - type: "event", - name: "ResolverUpdate", - inputs: [ - { - name: "id", - type: "uint256", - indexed: true, - internalType: "uint256", - }, + name: "isApprovedForAll", + outputs: [ { - name: "resolver", - type: "address", - indexed: false, - internalType: "address", + internalType: "bool", + name: "", + type: "bool", }, ], - anonymous: false, + stateMutability: "view", + type: "function", }, { - type: "event", - name: "SubregistryUpdate", inputs: [ { + internalType: "uint256", name: "id", type: "uint256", - indexed: true, - internalType: "uint256", - }, - { - name: "subregistry", - type: "address", - indexed: false, - internalType: "address", }, ], - anonymous: false, - }, - { - type: "event", - name: "TokenRegenerated", - inputs: [ - { - name: "oldTokenId", - type: "uint256", - indexed: false, - internalType: "uint256", - }, + name: "ownerOf", + outputs: [ { - name: "newTokenId", - type: "uint256", - indexed: false, - internalType: "uint256", + internalType: "address", + name: "owner", + type: "address", }, ], - anonymous: false, + stateMutability: "view", + type: "function", }, { - type: "event", - name: "TransferBatch", inputs: [ { - name: "operator", - type: "address", - indexed: true, internalType: "address", - }, - { name: "from", type: "address", - indexed: true, - internalType: "address", }, { + internalType: "address", name: "to", type: "address", - indexed: true, - internalType: "address", }, { + internalType: "uint256[]", name: "ids", type: "uint256[]", - indexed: false, - internalType: "uint256[]", }, { + internalType: "uint256[]", name: "values", type: "uint256[]", - indexed: false, - internalType: "uint256[]", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", }, ], - anonymous: false, + name: "safeBatchTransferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", }, { - type: "event", - name: "TransferSingle", inputs: [ { - name: "operator", - type: "address", - indexed: true, internalType: "address", - }, - { name: "from", type: "address", - indexed: true, - internalType: "address", }, { + internalType: "address", name: "to", type: "address", - indexed: true, - internalType: "address", }, { + internalType: "uint256", name: "id", type: "uint256", - indexed: false, - internalType: "uint256", }, { + internalType: "uint256", name: "value", type: "uint256", - indexed: false, - internalType: "uint256", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", }, ], - anonymous: false, + name: "safeTransferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", }, { - type: "event", - name: "URI", inputs: [ { - name: "value", - type: "string", - indexed: false, - internalType: "string", + internalType: "address", + name: "operator", + type: "address", }, { - name: "id", - type: "uint256", - indexed: true, - internalType: "uint256", + internalType: "bool", + name: "approved", + type: "bool", }, ], - anonymous: false, + name: "setApprovalForAll", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes4", + name: "interfaceId", + type: "bytes4", + }, + ], + name: "supportsInterface", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", }, ] as const; diff --git a/packages/datasources/src/abis/namechain/ETHRegistrar.ts b/packages/datasources/src/abis/namechain/ETHRegistrar.ts deleted file mode 100644 index 0d52f82bb..000000000 --- a/packages/datasources/src/abis/namechain/ETHRegistrar.ts +++ /dev/null @@ -1,1152 +0,0 @@ -export const ETHRegistrar = [ - { - inputs: [ - { - internalType: "contract IPermissionedRegistry", - name: "registry", - type: "address", - }, - { - internalType: "address", - name: "beneficiary", - type: "address", - }, - { - internalType: "uint64", - name: "minCommitmentAge", - type: "uint64", - }, - { - internalType: "uint64", - name: "maxCommitmentAge", - type: "uint64", - }, - { - internalType: "uint64", - name: "minRegisterDuration", - type: "uint64", - }, - { - internalType: "contract IRentPriceOracle", - name: "rentPriceOracle_", - type: "address", - }, - ], - stateMutability: "nonpayable", - type: "constructor", - }, - { - inputs: [ - { - internalType: "bytes32", - name: "commitment", - type: "bytes32", - }, - { - internalType: "uint64", - name: "validFrom", - type: "uint64", - }, - { - internalType: "uint64", - name: "blockTimestamp", - type: "uint64", - }, - ], - name: "CommitmentTooNew", - type: "error", - }, - { - inputs: [ - { - internalType: "bytes32", - name: "commitment", - type: "bytes32", - }, - { - internalType: "uint64", - name: "validTo", - type: "uint64", - }, - { - internalType: "uint64", - name: "blockTimestamp", - type: "uint64", - }, - ], - name: "CommitmentTooOld", - type: "error", - }, - { - inputs: [ - { - internalType: "uint64", - name: "duration", - type: "uint64", - }, - { - internalType: "uint64", - name: "minDuration", - type: "uint64", - }, - ], - name: "DurationTooShort", - type: "error", - }, - { - inputs: [ - { - internalType: "uint256", - name: "resource", - type: "uint256", - }, - { - internalType: "uint256", - name: "roleBitmap", - type: "uint256", - }, - { - internalType: "address", - name: "account", - type: "address", - }, - ], - name: "EACCannotGrantRoles", - type: "error", - }, - { - inputs: [ - { - internalType: "uint256", - name: "resource", - type: "uint256", - }, - { - internalType: "uint256", - name: "roleBitmap", - type: "uint256", - }, - { - internalType: "address", - name: "account", - type: "address", - }, - ], - name: "EACCannotRevokeRoles", - type: "error", - }, - { - inputs: [ - { - internalType: "uint256", - name: "roleBitmap", - type: "uint256", - }, - ], - name: "EACInvalidRoleBitmap", - type: "error", - }, - { - inputs: [ - { - internalType: "uint256", - name: "resource", - type: "uint256", - }, - { - internalType: "uint256", - name: "role", - type: "uint256", - }, - ], - name: "EACMaxAssignees", - type: "error", - }, - { - inputs: [ - { - internalType: "uint256", - name: "resource", - type: "uint256", - }, - { - internalType: "uint256", - name: "role", - type: "uint256", - }, - ], - name: "EACMinAssignees", - type: "error", - }, - { - inputs: [], - name: "EACRootResourceNotAllowed", - type: "error", - }, - { - inputs: [ - { - internalType: "uint256", - name: "resource", - type: "uint256", - }, - { - internalType: "uint256", - name: "roleBitmap", - type: "uint256", - }, - { - internalType: "address", - name: "account", - type: "address", - }, - ], - name: "EACUnauthorizedAccountRoles", - type: "error", - }, - { - inputs: [], - name: "MaxCommitmentAgeTooLow", - type: "error", - }, - { - inputs: [ - { - internalType: "string", - name: "label", - type: "string", - }, - ], - name: "NameAlreadyRegistered", - type: "error", - }, - { - inputs: [ - { - internalType: "string", - name: "label", - type: "string", - }, - ], - name: "NameNotRegistered", - type: "error", - }, - { - inputs: [ - { - internalType: "string", - name: "label", - type: "string", - }, - ], - name: "NotValid", - type: "error", - }, - { - inputs: [ - { - internalType: "contract IERC20", - name: "paymentToken", - type: "address", - }, - ], - name: "PaymentTokenNotSupported", - type: "error", - }, - { - inputs: [ - { - internalType: "address", - name: "token", - type: "address", - }, - ], - name: "SafeERC20FailedOperation", - type: "error", - }, - { - inputs: [ - { - internalType: "bytes32", - name: "commitment", - type: "bytes32", - }, - ], - name: "UnexpiredCommitmentExists", - type: "error", - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: "bytes32", - name: "commitment", - type: "bytes32", - }, - ], - name: "CommitmentMade", - type: "event", - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: "uint256", - name: "resource", - type: "uint256", - }, - { - indexed: false, - internalType: "address", - name: "account", - type: "address", - }, - ], - name: "EACAllRolesRevoked", - type: "event", - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: "uint256", - name: "resource", - type: "uint256", - }, - { - indexed: false, - internalType: "uint256", - name: "roleBitmap", - type: "uint256", - }, - { - indexed: false, - internalType: "address", - name: "account", - type: "address", - }, - ], - name: "EACRolesGranted", - type: "event", - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: "uint256", - name: "resource", - type: "uint256", - }, - { - indexed: false, - internalType: "uint256", - name: "roleBitmap", - type: "uint256", - }, - { - indexed: false, - internalType: "address", - name: "account", - type: "address", - }, - ], - name: "EACRolesRevoked", - type: "event", - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: "uint256", - name: "tokenId", - type: "uint256", - }, - { - indexed: false, - internalType: "string", - name: "label", - type: "string", - }, - { - indexed: false, - internalType: "address", - name: "owner", - type: "address", - }, - { - indexed: false, - internalType: "contract IRegistry", - name: "subregistry", - type: "address", - }, - { - indexed: false, - internalType: "address", - name: "resolver", - type: "address", - }, - { - indexed: false, - internalType: "uint64", - name: "duration", - type: "uint64", - }, - { - indexed: false, - internalType: "contract IERC20", - name: "paymentToken", - type: "address", - }, - { - indexed: false, - internalType: "bytes32", - name: "referrer", - type: "bytes32", - }, - { - indexed: false, - internalType: "uint256", - name: "base", - type: "uint256", - }, - { - indexed: false, - internalType: "uint256", - name: "premium", - type: "uint256", - }, - ], - name: "NameRegistered", - type: "event", - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: "uint256", - name: "tokenId", - type: "uint256", - }, - { - indexed: false, - internalType: "string", - name: "label", - type: "string", - }, - { - indexed: false, - internalType: "uint64", - name: "duration", - type: "uint64", - }, - { - indexed: false, - internalType: "uint64", - name: "newExpiry", - type: "uint64", - }, - { - indexed: false, - internalType: "contract IERC20", - name: "paymentToken", - type: "address", - }, - { - indexed: false, - internalType: "bytes32", - name: "referrer", - type: "bytes32", - }, - { - indexed: false, - internalType: "uint256", - name: "base", - type: "uint256", - }, - ], - name: "NameRenewed", - type: "event", - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: "contract IERC20", - name: "paymentToken", - type: "address", - }, - ], - name: "PaymentTokenAdded", - type: "event", - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: "contract IERC20", - name: "paymentToken", - type: "address", - }, - ], - name: "PaymentTokenRemoved", - type: "event", - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: "contract IRentPriceOracle", - name: "oracle", - type: "address", - }, - ], - name: "RentPriceOracleChanged", - type: "event", - }, - { - inputs: [], - name: "BENEFICIARY", - outputs: [ - { - internalType: "address", - name: "", - type: "address", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "MAX_COMMITMENT_AGE", - outputs: [ - { - internalType: "uint64", - name: "", - type: "uint64", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "MIN_COMMITMENT_AGE", - outputs: [ - { - internalType: "uint64", - name: "", - type: "uint64", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "MIN_REGISTER_DURATION", - outputs: [ - { - internalType: "uint64", - name: "", - type: "uint64", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "REGISTRY", - outputs: [ - { - internalType: "contract IPermissionedRegistry", - name: "", - type: "address", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "ROOT_RESOURCE", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "bytes32", - name: "commitment", - type: "bytes32", - }, - ], - name: "commit", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "bytes32", - name: "commitment", - type: "bytes32", - }, - ], - name: "commitmentAt", - outputs: [ - { - internalType: "uint64", - name: "commitTime", - type: "uint64", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "resource", - type: "uint256", - }, - { - internalType: "uint256", - name: "roleBitmap", - type: "uint256", - }, - ], - name: "getAssigneeCount", - outputs: [ - { - internalType: "uint256", - name: "counts", - type: "uint256", - }, - { - internalType: "uint256", - name: "mask", - type: "uint256", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "resource", - type: "uint256", - }, - { - internalType: "uint256", - name: "roleBitmap", - type: "uint256", - }, - { - internalType: "address", - name: "account", - type: "address", - }, - ], - name: "grantRoles", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool", - }, - ], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "roleBitmap", - type: "uint256", - }, - { - internalType: "address", - name: "account", - type: "address", - }, - ], - name: "grantRootRoles", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool", - }, - ], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "resource", - type: "uint256", - }, - { - internalType: "uint256", - name: "roleBitmap", - type: "uint256", - }, - ], - name: "hasAssignees", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "resource", - type: "uint256", - }, - { - internalType: "uint256", - name: "rolesBitmap", - type: "uint256", - }, - { - internalType: "address", - name: "account", - type: "address", - }, - ], - name: "hasRoles", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "rolesBitmap", - type: "uint256", - }, - { - internalType: "address", - name: "account", - type: "address", - }, - ], - name: "hasRootRoles", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "string", - name: "label", - type: "string", - }, - ], - name: "isAvailable", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "contract IERC20", - name: "paymentToken", - type: "address", - }, - ], - name: "isPaymentToken", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "string", - name: "label", - type: "string", - }, - ], - name: "isValid", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "string", - name: "label", - type: "string", - }, - { - internalType: "address", - name: "owner", - type: "address", - }, - { - internalType: "bytes32", - name: "secret", - type: "bytes32", - }, - { - internalType: "contract IRegistry", - name: "subregistry", - type: "address", - }, - { - internalType: "address", - name: "resolver", - type: "address", - }, - { - internalType: "uint64", - name: "duration", - type: "uint64", - }, - { - internalType: "bytes32", - name: "referrer", - type: "bytes32", - }, - ], - name: "makeCommitment", - outputs: [ - { - internalType: "bytes32", - name: "", - type: "bytes32", - }, - ], - stateMutability: "pure", - type: "function", - }, - { - inputs: [ - { - internalType: "string", - name: "label", - type: "string", - }, - { - internalType: "address", - name: "owner", - type: "address", - }, - { - internalType: "bytes32", - name: "secret", - type: "bytes32", - }, - { - internalType: "contract IRegistry", - name: "subregistry", - type: "address", - }, - { - internalType: "address", - name: "resolver", - type: "address", - }, - { - internalType: "uint64", - name: "duration", - type: "uint64", - }, - { - internalType: "contract IERC20", - name: "paymentToken", - type: "address", - }, - { - internalType: "bytes32", - name: "referrer", - type: "bytes32", - }, - ], - name: "register", - outputs: [ - { - internalType: "uint256", - name: "tokenId", - type: "uint256", - }, - ], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "string", - name: "label", - type: "string", - }, - { - internalType: "uint64", - name: "duration", - type: "uint64", - }, - { - internalType: "contract IERC20", - name: "paymentToken", - type: "address", - }, - { - internalType: "bytes32", - name: "referrer", - type: "bytes32", - }, - ], - name: "renew", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "string", - name: "label", - type: "string", - }, - { - internalType: "address", - name: "owner", - type: "address", - }, - { - internalType: "uint64", - name: "duration", - type: "uint64", - }, - { - internalType: "contract IERC20", - name: "paymentToken", - type: "address", - }, - ], - name: "rentPrice", - outputs: [ - { - internalType: "uint256", - name: "base", - type: "uint256", - }, - { - internalType: "uint256", - name: "premium", - type: "uint256", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "rentPriceOracle", - outputs: [ - { - internalType: "contract IRentPriceOracle", - name: "", - type: "address", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "resource", - type: "uint256", - }, - { - internalType: "uint256", - name: "roleBitmap", - type: "uint256", - }, - { - internalType: "address", - name: "account", - type: "address", - }, - ], - name: "revokeRoles", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool", - }, - ], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "roleBitmap", - type: "uint256", - }, - { - internalType: "address", - name: "account", - type: "address", - }, - ], - name: "revokeRootRoles", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool", - }, - ], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "resource", - type: "uint256", - }, - ], - name: "roleCount", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "resource", - type: "uint256", - }, - { - internalType: "address", - name: "account", - type: "address", - }, - ], - name: "roles", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "contract IRentPriceOracle", - name: "oracle", - type: "address", - }, - ], - name: "setRentPriceOracle", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "bytes4", - name: "interfaceId", - type: "bytes4", - }, - ], - name: "supportsInterface", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool", - }, - ], - stateMutability: "view", - type: "function", - }, -] as const; diff --git a/packages/datasources/src/ens-test-env.ts b/packages/datasources/src/ens-test-env.ts index 5417acf7a..ed2e0e67f 100644 --- a/packages/datasources/src/ens-test-env.ts +++ b/packages/datasources/src/ens-test-env.ts @@ -1,8 +1,8 @@ import { zeroAddress } from "viem"; -import { EnhancedAccessControl } from "./abis/namechain/EnhancedAccessControl"; -import { ETHRegistrar } from "./abis/namechain/ETHRegistrar"; -import { Registry } from "./abis/namechain/Registry"; +import { EnhancedAccessControl } from "./abis/ensv2/EnhancedAccessControl"; +import { ETHRegistrar } from "./abis/ensv2/ETHRegistrar"; +import { Registry } from "./abis/ensv2/Registry"; // ABIs for ENSRoot Datasource import { BaseRegistrar as root_BaseRegistrar } from "./abis/root/BaseRegistrar"; import { LegacyEthRegistrarController as root_LegacyEthRegistrarController } from "./abis/root/LegacyEthRegistrarController"; diff --git a/packages/datasources/src/index.ts b/packages/datasources/src/index.ts index 237a4a431..9faeee5f6 100644 --- a/packages/datasources/src/index.ts +++ b/packages/datasources/src/index.ts @@ -1,5 +1,5 @@ -export { EnhancedAccessControl as EnhancedAccessControlABI } from "./abis/namechain/EnhancedAccessControl"; -export { Registry as RegistryABI } from "./abis/namechain/Registry"; +export { EnhancedAccessControl as EnhancedAccessControlABI } from "./abis/ensv2/EnhancedAccessControl"; +export { Registry as RegistryABI } from "./abis/ensv2/Registry"; export { StandaloneReverseRegistrar as StandaloneReverseRegistrarABI } from "./abis/shared/StandaloneReverseRegistrar"; export { ThreeDNSToken as ThreeDNSTokenABI } from "./abis/threedns/ThreeDNSToken"; export { AnyRegistrarABI } from "./lib/AnyRegistrarABI"; diff --git a/packages/datasources/src/lib/ResolverABI.ts b/packages/datasources/src/lib/ResolverABI.ts index c26ffe71e..b21613648 100644 --- a/packages/datasources/src/lib/ResolverABI.ts +++ b/packages/datasources/src/lib/ResolverABI.ts @@ -1,6 +1,6 @@ import { mergeAbis } from "@ponder/utils"; -// import { EnhancedAccessControl } from "../abis/namechain/EnhancedAccessControl"; +// import { EnhancedAccessControl } from "../abis/ensv2/EnhancedAccessControl"; import { LegacyPublicResolver } from "../abis/shared/LegacyPublicResolver"; import { Ownable } from "../abis/shared/Ownable"; import { Resolver } from "../abis/shared/Resolver"; diff --git a/packages/datasources/src/mainnet.ts b/packages/datasources/src/mainnet.ts index b296d323e..b7ee197eb 100644 --- a/packages/datasources/src/mainnet.ts +++ b/packages/datasources/src/mainnet.ts @@ -6,15 +6,15 @@ import { EarlyAccessRegistrarController as base_EARegistrarController } from "./ import { RegistrarController as base_RegistrarController } from "./abis/basenames/RegistrarController"; import { Registry as base_Registry } from "./abis/basenames/Registry"; import { UpgradeableRegistrarController as base_UpgradeableRegistrarController } from "./abis/basenames/UpgradeableRegistrarController"; +// ABIs for Namechain +import { EnhancedAccessControl } from "./abis/ensv2/EnhancedAccessControl"; +import { ETHRegistrar } from "./abis/ensv2/ETHRegistrar"; +import { Registry } from "./abis/ensv2/Registry"; // ABIs for Lineanames Datasource import { BaseRegistrar as linea_BaseRegistrar } from "./abis/lineanames/BaseRegistrar"; import { EthRegistrarController as linea_EthRegistrarController } from "./abis/lineanames/EthRegistrarController"; import { NameWrapper as linea_NameWrapper } from "./abis/lineanames/NameWrapper"; import { Registry as linea_Registry } from "./abis/lineanames/Registry"; -// ABIs for Namechain -import { EnhancedAccessControl } from "./abis/namechain/EnhancedAccessControl"; -import { ETHRegistrar } from "./abis/namechain/ETHRegistrar"; -import { Registry } from "./abis/namechain/Registry"; // ABIs for ENSRoot Datasource import { BaseRegistrar as root_BaseRegistrar } from "./abis/root/BaseRegistrar"; import { LegacyEthRegistrarController as root_LegacyEthRegistrarController } from "./abis/root/LegacyEthRegistrarController"; diff --git a/packages/datasources/src/sepolia.ts b/packages/datasources/src/sepolia.ts index 40644b63c..1397894ec 100644 --- a/packages/datasources/src/sepolia.ts +++ b/packages/datasources/src/sepolia.ts @@ -13,15 +13,15 @@ import { EarlyAccessRegistrarController as base_EARegistrarController } from "./ import { RegistrarController as base_RegistrarController } from "./abis/basenames/RegistrarController"; import { Registry as base_Registry } from "./abis/basenames/Registry"; import { UpgradeableRegistrarController as base_UpgradeableRegistrarController } from "./abis/basenames/UpgradeableRegistrarController"; +// ABIs for Namechain +import { EnhancedAccessControl } from "./abis/ensv2/EnhancedAccessControl"; +import { ETHRegistrar } from "./abis/ensv2/ETHRegistrar"; +import { Registry } from "./abis/ensv2/Registry"; // ABIs for Lineanames Datasource import { BaseRegistrar as linea_BaseRegistrar } from "./abis/lineanames/BaseRegistrar"; import { EthRegistrarController as linea_EthRegistrarController } from "./abis/lineanames/EthRegistrarController"; import { NameWrapper as linea_NameWrapper } from "./abis/lineanames/NameWrapper"; import { Registry as linea_Registry } from "./abis/lineanames/Registry"; -// ABIs for Namechain -import { EnhancedAccessControl } from "./abis/namechain/EnhancedAccessControl"; -import { ETHRegistrar } from "./abis/namechain/ETHRegistrar"; -import { Registry } from "./abis/namechain/Registry"; // ABIs for ENSRoot Datasource import { BaseRegistrar as root_BaseRegistrar } from "./abis/root/BaseRegistrar"; import { LegacyEthRegistrarController as root_LegacyEthRegistrarController } from "./abis/root/LegacyEthRegistrarController"; diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/schemas/ensv2.schema.ts index e88e5f29a..e3a314e8b 100644 --- a/packages/ensnode-schema/src/schemas/ensv2.schema.ts +++ b/packages/ensnode-schema/src/schemas/ensv2.schema.ts @@ -117,6 +117,9 @@ export const v2Domain = onchainTable( // see ENSv2DomainId for guarantees id: t.text().primaryKey().$type(), + // has a tokenId + tokenId: t.bigint().notNull(), + // belongs to registry registryId: t.text().notNull().$type(), From 540a04557f8f44bcecaaaf8e815895f4028d5319 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 21 Nov 2025 12:33:57 -0600 Subject: [PATCH 057/102] fix: dev methods --- apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts | 2 ++ apps/ensapi/src/graphql-api/schema/query.ts | 2 +- apps/ensindexer/src/plugins/ensv2/plugin.ts | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts index d559569b8..467fa8abe 100644 --- a/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts +++ b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts @@ -49,6 +49,8 @@ const NAMECHAIN_V2_ETH_REGISTRY_ID = makeRegistryId({ export async function getDomainIdByInterpretedName( name: InterpretedName, ): Promise { + // Domains addressable in v2 are preferred, but v1 lookups are cheap, so just do them both ahead of time + // TODO: when v2 names are the majority, we can unroll this into a v2 then v1 lookup. const [v1DomainId, v2DomainId] = await Promise.all([ v1_getDomainIdByFqdn(name), v2_getDomainIdByFqdn(ROOT_REGISTRY_ID, name), diff --git a/apps/ensapi/src/graphql-api/schema/query.ts b/apps/ensapi/src/graphql-api/schema/query.ts index 04de7493a..c406c8417 100644 --- a/apps/ensapi/src/graphql-api/schema/query.ts +++ b/apps/ensapi/src/graphql-api/schema/query.ts @@ -30,7 +30,7 @@ import { ResolverIdInput, ResolverRef } from "@/graphql-api/schema/resolver"; import { db } from "@/lib/db"; // don't want them to get familiar/accustom to these methods until their necessity is certain -const INCLUDE_DEV_METHODS = process.env.NODE_ENV === "development"; +const INCLUDE_DEV_METHODS = process.env.NODE_ENV !== "production"; builder.queryType({ fields: (t) => ({ diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index 653d94030..f286d12e9 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -6,8 +6,8 @@ * - this absoutely nukes performance * - some state (i.e. registration expirations) are only knowable at Forward-Resolution-time and both the on-chain and indexed state need to check that at resolve-time to be compliant. we _could_ implement garbage collection to mark registrations as expired, but again that defeats all of ponder's cache heuristics and isn't even that helpful. * - the absolutely least-likely-to-cause-horrible-logic approach is to mirror on-chain state 1:1 and perform at query time all of the resolution-time logic that ens applies — this forces the implementations to match as closely as possible. obvious exception for needing to materialize certain aspects of the state (like v1Domain.owner) because actually performing that filter at runtime is abominable. i.e. we have to realize that the performance tradeoffs of evm code and typescript code against postgres are different. ex: trivial to batch-load the full labelhashpath in v2, but more extensive to recursively loop the query (like evm code does) because our individual loads from the db are relatively more expensive. - * - self-review and document where needed * + * - self-review and document where needed * - indexes * * TODO LATER From 7debf8ba77a710ef5e0c767632007f0f778a74c0 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 21 Nov 2025 13:17:28 -0600 Subject: [PATCH 058/102] expiration to expiry --- .../src/graphql-api/schema/registration.ts | 8 +-- .../src/lib/ensv2/registration-db-helpers.ts | 18 ++--- .../ensv2/handlers/ensv1/BaseRegistrar.ts | 8 +-- .../ensv2/handlers/ensv1/NameWrapper.ts | 44 ++++++------ .../ensv2/handlers/ensv2/ENSv2Registry.ts | 8 +-- apps/ensindexer/src/plugins/ensv2/plugin.ts | 10 +-- .../src/schemas/ensv2.schema.ts | 71 ++++++++++++++++++- 7 files changed, 114 insertions(+), 53 deletions(-) diff --git a/apps/ensapi/src/graphql-api/schema/registration.ts b/apps/ensapi/src/graphql-api/schema/registration.ts index e126ac423..1a68801a8 100644 --- a/apps/ensapi/src/graphql-api/schema/registration.ts +++ b/apps/ensapi/src/graphql-api/schema/registration.ts @@ -25,7 +25,7 @@ export type RegistrationInterface = Pick< | "index" | "domainId" | "start" - | "expiration" + | "expiry" | "registrarChainId" | "registrarAddress" | "registrantId" @@ -85,13 +85,13 @@ RegistrationInterfaceRef.implement({ }), /////////////////////////// - // Registration.expiration + // Registration.expiry /////////////////////////// - expiration: t.field({ + expiry: t.field({ description: "TODO", type: "BigInt", nullable: true, - resolve: (parent) => parent.expiration, + resolve: (parent) => parent.expiry, }), ///////////////////////// diff --git a/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts index 29f4c8836..3c3a4137a 100644 --- a/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts @@ -44,11 +44,11 @@ export function isRegistrationExpired( registration: typeof schema.registration.$inferSelect, now: bigint, ) { - // no expiration, never expired - if (registration.expiration === null) return false; + // no expiry, never expired + if (registration.expiry === null) return false; // otherwise check against now - return registration.expiration <= now; + return registration.expiry <= now; } /** @@ -59,11 +59,11 @@ export function isRegistrationFullyExpired( registration: typeof schema.registration.$inferSelect, now: bigint, ) { - // no expiration, never expired - if (registration.expiration === null) return false; + // no expiry, never expired + if (registration.expiry === null) return false; - // otherwise it is expired if now >= expiration + grace - return now >= registration.expiration + (registration.gracePeriod ?? 0n); + // otherwise it is expired if now >= expiry + grace + return now >= registration.expiry + (registration.gracePeriod ?? 0n); } /** @@ -73,9 +73,9 @@ export function isRegistrationInGracePeriod( registration: typeof schema.registration.$inferSelect, now: bigint, ) { - if (registration.expiration === null) return false; + if (registration.expiry === null) return false; if (registration.gracePeriod === null) return false; // - return registration.expiration <= now && registration.expiration + registration.gracePeriod > now; + return registration.expiry <= now && registration.expiry + registration.gracePeriod > now; } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts index 8b4b22e26..f5e122afc 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts @@ -94,7 +94,7 @@ export default function () { expires: bigint; }>; }) { - const { id: tokenId, owner, expires: expiration } = event.args; + const { id: tokenId, owner, expires: expiry } = event.args; const registrant = owner; const labelHash = registrarTokenIdToLabelHash(tokenId); @@ -128,7 +128,7 @@ export default function () { registrantId: interpretAddress(registrant), domainId, start: event.block.timestamp, - expiration, + expiry, // all BaseRegistrar-derived Registrars use the same GRACE_PERIOD gracePeriod: BigInt(GRACE_PERIOD_SECONDS), }); @@ -153,7 +153,7 @@ export default function () { context: Context; event: EventWithArgs<{ id: bigint; expires: bigint }>; }) => { - const { id: tokenId, expires: expiration } = event.args; + const { id: tokenId, expires: expiry } = event.args; const labelHash = registrarTokenIdToLabelHash(tokenId); const registrar = getThisAccountId(context, event); @@ -178,7 +178,7 @@ export default function () { } // update the registration - await context.db.update(schema.registration, { id: registration.id }).set({ expiration }); + await context.db.update(schema.registration, { id: registration.id }).set({ expiry }); // TODO: insert renewal & reference registration }, diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts index 399fc6430..e9f5618df 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts @@ -45,10 +45,9 @@ const pluginName = PluginName.ENSv2; const tokenIdToNode = (tokenId: bigint): Node => uint256ToHex32(tokenId); /** - * NameWrapper emits expiration but 0 means 'doesn't expire', we represent as null. + * NameWrapper emits expiry but 0 means 'doesn't expire', we represent as null. */ -const interpretExpiration = (expiration: bigint): bigint | null => - expiration === 0n ? null : expiration; +const interpretExpiry = (expiry: bigint): bigint | null => (expiry === 0n ? null : expiry); // registrar is source of truth for expiry if eth 2LD // otherwise namewrapper is registrar and source of truth for expiry @@ -57,7 +56,7 @@ const interpretExpiration = (expiration: bigint): bigint | null => // The FusesSet event indicates that fuses were written to storage, but: // Does not guarantee the name is not expired // Does not guarantee the fuses are actually active (they could be cleared by _clearOwnerAndFuses on read) -// Simply records the fuse value that was stored, regardless of expiration status +// Simply records the fuse value that was stored, regardless of expiry status // For indexers, this means you need to track both the FusesSet event AND the expiry to determine the actual active fuses at any point in time. // .eth 2LDs always have PARENT_CANNOT_CONTROL set ('burned'), they cannot be transferred during grace period @@ -157,8 +156,8 @@ export default function () { expiry: bigint; }>; }) => { - const { node, name: _name, owner, fuses, expiry: _expiration } = event.args; - const expiration = interpretExpiration(_expiration); + const { node, name: _name, owner, fuses, expiry: _expiry } = event.args; + const expiry = interpretExpiry(_expiry); const name = _name as DNSEncodedLiteralName; const registrant = owner; @@ -208,19 +207,19 @@ export default function () { ); } - // Invariant: BaseRegistrar always provides Expiration - if (expiration === null) { + // Invariant: BaseRegistrar always provides expiry + if (expiry === null) { throw new Error( - `Invariant(NameWrapper:NameWrapped): Wrap of BaseRegistrar Registration does not include expiration!\n${toJson(registration)}`, + `Invariant(NameWrapper:NameWrapped): Wrap of BaseRegistrar Registration does not include expiry!\n${toJson(registration)}`, ); } - // Invariant: Expiration Alignment + // Invariant: Expiry Alignment if ( - // If BaseRegistrar Registration has an expiration, - registration.expiration && + // If BaseRegistrar Registration has an expiry, + registration.expiry && // The NameWrapper epiration must be greater than that (+ grace period). - expiration > registration.expiration + (registration.gracePeriod ?? 0n) + expiry > registration.expiry + (registration.gracePeriod ?? 0n) ) { throw new Error("Wrapper expiry exceeds registrar expiry + grace period"); } @@ -228,7 +227,7 @@ export default function () { await context.db.update(schema.registration, { id: registration.id }).set({ wrapped: true, fuses, - // expiration, // TODO: NameWrapper expiration logic + // expiry, // TODO: NameWrapper expiry logic }); } else { // Invariant: If there's an existing Registration, it should be expired @@ -238,8 +237,9 @@ export default function () { ); } - const isAlreadyExpired = expiration && expiration <= event.block.timestamp; + const isAlreadyExpired = expiry && expiry <= event.block.timestamp; if (isAlreadyExpired) { + // technically this is allowed... may as well just remove the warning console.warn(`Creating NameWrapper registration for already-expired name: ${node}`); } @@ -258,7 +258,7 @@ export default function () { domainId, start: event.block.timestamp, fuses, - expiration, + expiry, }); } }, @@ -287,12 +287,12 @@ export default function () { await context.db.update(schema.registration, { id: registration.id }).set({ wrapped: false, fuses: null, - // expiration: null // TODO: NameWrapper expiration logic? maybe nothing to do here + // expiry: null // TODO: NameWrapper expiry logic? maybe nothing to do here }); } else { // otherwise, deactivate the latest registration by setting its expiry to this block await context.db.update(schema.registration, { id: registration.id }).set({ - expiration: event.block.timestamp, + expiry: event.block.timestamp, }); } @@ -327,7 +327,7 @@ export default function () { // upsert fuses await context.db.update(schema.registration, { id: registration.id }).set({ fuses, - // expiration: // TODO: NameWrapper expiration logic ? + // expiry: // TODO: NameWrapper expiry logic ? }); }, ); @@ -344,8 +344,8 @@ export default function () { context: Context; event: EventWithArgs<{ node: Node; expiry: bigint }>; }) => { - const { node, expiry: _expiration } = event.args; - const expiration = interpretExpiration(_expiration); + const { node, expiry: _expiry } = event.args; + const expiry = interpretExpiry(_expiry); const domainId = makeENSv1DomainId(node); const registration = await getLatestRegistration(context, domainId); @@ -357,7 +357,7 @@ export default function () { ); } - await context.db.update(schema.registration, { id: registration.id }).set({ expiration }); + await context.db.update(schema.registration, { id: registration.id }).set({ expiry }); }, ); diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts index 377d756b3..5f0355cb8 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts @@ -44,7 +44,7 @@ export default function () { registeredBy: Address; }>; }) => { - const { tokenId, label: _label, expiry: expiration, registeredBy: registrant } = event.args; + const { tokenId, label: _label, expiry, registeredBy: registrant } = event.args; const label = _label as LiteralLabel; const labelHash = labelhash(label); @@ -119,7 +119,7 @@ export default function () { registrantId: interpretAddress(registrant), domainId, start: event.block.timestamp, - expiration, + expiry, }); }, ); @@ -137,7 +137,7 @@ export default function () { changedBy: Address; }>; }) => { - const { tokenId, newExpiry: expiration, changedBy: renewer } = event.args; + const { tokenId, newExpiry: expiry, changedBy: renewer } = event.args; const registry = getThisAccountId(context, event); const canonicalId = getCanonicalId(tokenId); @@ -158,7 +158,7 @@ export default function () { } // update Registration - await context.db.update(schema.registration, { id: registration.id }).set({ expiration }); + await context.db.update(schema.registration, { id: registration.id }).set({ expiry }); // TODO: insert Renewal }, diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index f286d12e9..6c30db59a 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -1,16 +1,9 @@ /** * TODO TODAY - * - document schema approach, only materialization is v1 effective token owner into v1Domain.owner - * - all polymorphism is done at graphql layer - * - materialization of the canonical namegraph within ponder becomes really complex really quickly - * - this absoutely nukes performance - * - some state (i.e. registration expirations) are only knowable at Forward-Resolution-time and both the on-chain and indexed state need to check that at resolve-time to be compliant. we _could_ implement garbage collection to mark registrations as expired, but again that defeats all of ponder's cache heuristics and isn't even that helpful. - * - the absolutely least-likely-to-cause-horrible-logic approach is to mirror on-chain state 1:1 and perform at query time all of the resolution-time logic that ens applies — this forces the implementations to match as closely as possible. obvious exception for needing to materialize certain aspects of the state (like v1Domain.owner) because actually performing that filter at runtime is abominable. i.e. we have to realize that the performance tradeoffs of evm code and typescript code against postgres are different. ex: trivial to batch-load the full labelhashpath in v2, but more extensive to recursively loop the query (like evm code does) because our individual loads from the db are relatively more expensive. - * * - self-review and document where needed - * - indexes * * TODO LATER + * - indexes based on graphql queries, ask claude to compile recommendations * - modify Registration schema to more closely match ENSv2, map v1 into it * - Renewals (v1, v2) * - include similar /latest / superceding logic, need to be able to reference latest renewal to upsert referrers @@ -21,6 +14,7 @@ * - locked names (wrapped and not unwrappable) are 'frozen' by having their fuses burned * - will need to observe the correct event and then override the existing domain/registratioon info * - for MigratedWrappedNameRegistries, need to check name expiry during resolution and avoid resolving expired names + * - autocomplete api * * PENDING ENS TEAM * - DedicatedResolver moving to EAC diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/schemas/ensv2.schema.ts index e3a314e8b..1a3f6d6f9 100644 --- a/packages/ensnode-schema/src/schemas/ensv2.schema.ts +++ b/packages/ensnode-schema/src/schemas/ensv2.schema.ts @@ -16,6 +16,73 @@ import type { RegistryId, } from "@ensnode/ensnode-sdk"; +/** + * The ENSv2 Schema + * + * While the initial approach was a highly materialized view of the ENS protocol, abstracting away + * as many on-chain details as possible, in practice—due to the sheer complexity of the protocol at + * resolution-time—it becomes more or less impossible to appropriately materialize the canonical + * namegraph. + * + * As a result, this schema takes a balanced approach. It mimics on-chain state as closely as possible, + * with the obvious exception of materializing specific state that must trivially filterable. Then, + * resolution-time logic is applied on _top_ of this index, at query-time, mimicking ENS's own resolution-time + * behavior. This forces our implementation to match the protocol as closely as possible, with the + * obvious note that the performance tradeoffs of evm code and our app are different. For example, + * it's more expensive for us to recursively traverse the namegraph (like evm code does) because our + * individual roundtrips from the db are relatively more expensive. + * + * For the datamodel, this means that instead of a polymorphic Domain entity, representing both v1 + * and v2 Domains, this schema employs separate (but overlapping) v1Domains and v2Domains entities. + * This avoids resolution-time complications and more accurately represents the on-chain state. + * Domain polymorphism is applied at the API later, via GraphQL Interfaces, to simplify queries. + * + * In general: the indexed schema should match on-chain state as closely as possible, and + * resolution-time behavior within the ENS protocol should _also_ be implemented at resolution time + * in ENSApi. The current obvious exception to this is that v1Domain.owner is the _materialized_ + * _effective_ owner of the v1Domain. ENSv1 includes a mind-boggling number of ways to 'own' a v1Domain, + * including the ENSv1 Registry, various Registrars, and the NameWrapper. The ENSv1 indexing logic + * within this ENSv2 plugin materialize the v1Domain's effective owner to simplify this aspect of ENS, + * and enable efficient queries against v1Domain.owner. + * + * Many datamodels are sharable between ENSv1 and ENSv2, including Registrations, Renewals, and Resolvers. + * + * Resolvers implement 'extensions' more so than polymorphism — a Resovler can abide by many + * permutations of behavior (IExtendedResolver, IDedicatedResolver, BridgedResolver, ...etc), that are + * not technically mutually exclusive, as they'd be with a truly polymorphic entity. + * + * Registrations are polymorphic between the defined RegistrationTypes, depending on the associated + * guarantees (for example, ENSv1 BaseRegistrar Registrations may have a gracePeriod, but ENSv2 + * Registry Registrations do not). + * + * Instead of materializing a Domain's name at any point, we maintain an internal rainbow table of + * labelHash -> InterpretedLabel (the Label entity). This ensures that regardless of how or when a + * new label is encountered onchain, all Domains that use that label are automatically healed at + * resolution-time. + * + * v1Domains exist in a flat namespace and are absolutely addressed by `node`. As such, they inhabit + * a simple tree datamodel of: + * v1Domain -> v1Domain(s) -> v1Domain(s) -> ...etc + * + * v2Domains exist in a set of namegraphs. Each namegraph is a possibly cicular directed graph of + * (Root)Registry -> v2Domain(s) -> (sub)Regsitry -> v2Domain(s) -> ...etc + * with exactly one RootRegistry on the ENS Root Chain establishing the beginning of the _canonical_ + * namegraph. As discussed above, the canonical namegraph is never materialized, only _navigated_ + * at resolution-time, in order to correctly implement the complexities of the ENS protocol. + * + * Note also that the Protocol Acceleration plugin is a hard requirement for the ENSv2 plugin. This + * allows us to rely on the shared logic for indexing: + * a) ENSv1RegistryOld -> ENSv1Registry migration status + * b) Domain-Resolver Relations for both v1Domains and v2Domains + * As such, none of that information is present in this ensv2.schema.ts file. + * + * In general, entities are keyed by a nominally-typed `id` that uniquely references them. This + * allows us to trivially implement cursor-based pagination and allow consumers to reference these + * deeply nested entities by a straightforward string ID. In cases where an entity's `id` is composed + * of multiple pieces of information (for example, a Registry is identified by (chainId, address)), + * then that information is, as well, included in the entity. + */ + /////////// // Account /////////// @@ -192,8 +259,8 @@ export const registration = onchainTable( // must have a start timestamp start: t.bigint().notNull(), - // may have an expiration - expiration: t.bigint(), + // may have an expiry + expiry: t.bigint(), // maybe have a grace period (BaseRegistrar) gracePeriod: t.bigint(), From 996656959e8e67c8ccb218a41619612d04e30191 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 21 Nov 2025 13:18:56 -0600 Subject: [PATCH 059/102] docs(changeset): BREAKING: Removed holesky ENSNamespace. --- .changeset/legal-mammals-try.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/legal-mammals-try.md diff --git a/.changeset/legal-mammals-try.md b/.changeset/legal-mammals-try.md new file mode 100644 index 000000000..b11a04db4 --- /dev/null +++ b/.changeset/legal-mammals-try.md @@ -0,0 +1,5 @@ +--- +"ensindexer": minor +--- + +BREAKING: Removed holesky ENSNamespace. From a8d5b510f2b594bdd4c351bf62e64e7a349c2c37 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 21 Nov 2025 13:19:31 -0600 Subject: [PATCH 060/102] changeset --- .changeset/legal-mammals-try.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.changeset/legal-mammals-try.md b/.changeset/legal-mammals-try.md index b11a04db4..8e63c997b 100644 --- a/.changeset/legal-mammals-try.md +++ b/.changeset/legal-mammals-try.md @@ -1,5 +1,6 @@ --- "ensindexer": minor +"@ensnode/datasources": minor --- BREAKING: Removed holesky ENSNamespace. From da99ed87c402315ba6aa616d14664500b8f8f803 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 21 Nov 2025 13:20:17 -0600 Subject: [PATCH 061/102] docs(changeset): Introduces the ENSv2 Plugin ('ensv2') for indexing both ENSv1 and the future ENSv2 protocol. --- .changeset/frank-beds-taste.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/frank-beds-taste.md diff --git a/.changeset/frank-beds-taste.md b/.changeset/frank-beds-taste.md new file mode 100644 index 000000000..4245c39e1 --- /dev/null +++ b/.changeset/frank-beds-taste.md @@ -0,0 +1,8 @@ +--- +"@ensnode/ensnode-schema": minor +"@ensnode/datasources": minor +"ensindexer": minor +"ensapi": minor +--- + +Introduces the ENSv2 Plugin ('ensv2') for indexing both ENSv1 and the future ENSv2 protocol. From 790176a112f587c8e6bc5c18d2c640696453403b Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 21 Nov 2025 13:53:46 -0600 Subject: [PATCH 062/102] docs and stuff --- .../src/graphql-api/lib/get-domain-by-id.ts | 0 .../lib/get-latest-registration.ts | 3 +++ .../lib/{get-id.ts => get-model-id.ts} | 3 +++ .../src/graphql-api/lib/reject-any-errors.ts | 2 ++ .../graphql-api/lib/sort-by-array-order.ts | 6 +++++ .../src/graphql-api/schema/account-id.ts | 4 +-- apps/ensapi/src/graphql-api/schema/account.ts | 6 +++-- .../src/graphql-api/schema/constants.ts | 6 ++++- apps/ensapi/src/graphql-api/schema/cursors.ts | 4 +++ apps/ensapi/src/graphql-api/schema/domain.ts | 9 +++---- .../src/graphql-api/schema/name-or-node.ts | 3 +++ .../src/graphql-api/schema/permissions.ts | 2 +- .../src/graphql-api/schema/registration.ts | 2 +- .../ensapi/src/graphql-api/schema/registry.ts | 2 +- .../graphql-api/schema/resolver-records.ts | 2 +- .../ensapi/src/graphql-api/schema/resolver.ts | 2 +- .../src/handlers/ensnode-graphql-api.ts | 25 +++++++++++-------- .../require-core-plugin.middleware.ts | 1 - .../src/lib/ensv2/account-db-helpers.ts | 3 ++- .../src/lib/ensv2/label-db-helpers.ts | 11 +++++++- .../ensindexer/src/lib/ensv2/registrar-lib.ts | 14 ++++++++++- .../src/lib/ensv2/registration-db-helpers.ts | 2 +- apps/ensindexer/src/plugins/ensv2/plugin.ts | 4 +++ 23 files changed, 86 insertions(+), 30 deletions(-) delete mode 100644 apps/ensapi/src/graphql-api/lib/get-domain-by-id.ts rename apps/ensapi/src/graphql-api/lib/{get-id.ts => get-model-id.ts} (52%) diff --git a/apps/ensapi/src/graphql-api/lib/get-domain-by-id.ts b/apps/ensapi/src/graphql-api/lib/get-domain-by-id.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/ensapi/src/graphql-api/lib/get-latest-registration.ts b/apps/ensapi/src/graphql-api/lib/get-latest-registration.ts index fef11702a..6706ee575 100644 --- a/apps/ensapi/src/graphql-api/lib/get-latest-registration.ts +++ b/apps/ensapi/src/graphql-api/lib/get-latest-registration.ts @@ -2,6 +2,9 @@ import type { DomainId } from "@ensnode/ensnode-sdk"; import { db } from "@/lib/db"; +/** + * Gets the latest Registration entity for Domain `domainId`. + */ export async function getLatestRegistration(domainId: DomainId) { return await db.query.registration.findFirst({ where: (t, { eq }) => eq(t.domainId, domainId), diff --git a/apps/ensapi/src/graphql-api/lib/get-id.ts b/apps/ensapi/src/graphql-api/lib/get-model-id.ts similarity index 52% rename from apps/ensapi/src/graphql-api/lib/get-id.ts rename to apps/ensapi/src/graphql-api/lib/get-model-id.ts index b983320ba..e03df509f 100644 --- a/apps/ensapi/src/graphql-api/lib/get-id.ts +++ b/apps/ensapi/src/graphql-api/lib/get-model-id.ts @@ -1 +1,4 @@ +/** + * Simple type-safe accessor for *.id for use with Dataloader. + */ export const getModelId = (model: T): ID => model.id; diff --git a/apps/ensapi/src/graphql-api/lib/reject-any-errors.ts b/apps/ensapi/src/graphql-api/lib/reject-any-errors.ts index 2264ff29f..ccf310841 100644 --- a/apps/ensapi/src/graphql-api/lib/reject-any-errors.ts +++ b/apps/ensapi/src/graphql-api/lib/reject-any-errors.ts @@ -2,6 +2,8 @@ * Given a Promise<(Error | T)[]>, throws with the first Error, if any. * * @throws The first Error encountered in `promise`, if any. + * @dev This is useful for making manual Dataloaded arrays conform to T[]. + * @example return rejectAnyErrors(SomeLoadableRef.getDataloader(context).load(ids)) */ export async function rejectAnyErrors( promise: Promise, diff --git a/apps/ensapi/src/graphql-api/lib/sort-by-array-order.ts b/apps/ensapi/src/graphql-api/lib/sort-by-array-order.ts index d09baa404..4553b3944 100644 --- a/apps/ensapi/src/graphql-api/lib/sort-by-array-order.ts +++ b/apps/ensapi/src/graphql-api/lib/sort-by-array-order.ts @@ -1,3 +1,9 @@ +/** + * Produces a comparator capable of sorting a possibly-unordered result array by a set of ordered + * keys in `arr`. + * + * @example results.sort(sortByArrayOrder(ids, (result) => result.id)) + */ export function sortByArrayOrder(arr: T[], acc: (o: O) => T) { return function comparator(a: O, b: O) { return arr.indexOf(acc(a)) > arr.indexOf(acc(b)) ? 1 : -1; diff --git a/apps/ensapi/src/graphql-api/schema/account-id.ts b/apps/ensapi/src/graphql-api/schema/account-id.ts index c248beb4c..1376dd489 100644 --- a/apps/ensapi/src/graphql-api/schema/account-id.ts +++ b/apps/ensapi/src/graphql-api/schema/account-id.ts @@ -4,7 +4,7 @@ import { builder } from "@/graphql-api/builder"; export const AccountIdRef = builder.objectRef("AccountId"); AccountIdRef.implement({ - description: "A CAIP-10 Account ID.", + description: "A CAIP-10 Account ID including chainId and address.", fields: (t) => ({ chainId: t.expose("chainId", { type: "ChainId" }), address: t.expose("address", { type: "Address" }), @@ -12,7 +12,7 @@ AccountIdRef.implement({ }); export const AccountIdInput = builder.inputType("AccountIdInput", { - description: "A CAIP-10 Account ID.", + description: "A CAIP-10 Account ID including chainId and address.", fields: (t) => ({ chainId: t.field({ type: "ChainId", required: true }), address: t.field({ type: "Address", required: true }), diff --git a/apps/ensapi/src/graphql-api/schema/account.ts b/apps/ensapi/src/graphql-api/schema/account.ts index 590e3e6d4..a578976ee 100644 --- a/apps/ensapi/src/graphql-api/schema/account.ts +++ b/apps/ensapi/src/graphql-api/schema/account.ts @@ -7,7 +7,7 @@ import * as schema from "@ensnode/ensnode-schema"; import type { PermissionsUserId, ResolverId } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; -import { getModelId } from "@/graphql-api/lib/get-id"; +import { getModelId } from "@/graphql-api/lib/get-model-id"; import { rejectAnyErrors } from "@/graphql-api/lib/reject-any-errors"; import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; import { cursors } from "@/graphql-api/schema/cursors"; @@ -28,6 +28,9 @@ export const AccountRef = builder.loadableObjectRef("Account", { export type Account = Exclude; +/////////// +// Account +/////////// AccountRef.implement({ description: "TODO", fields: (t) => ({ @@ -143,7 +146,6 @@ AccountRef.implement({ description: "TODO", type: ResolverRef, resolve: (parent, args) => - // TODO(dataloader) — confirm this is dataloaded? // TODO(EAC) — migrate to Permissions lookup resolveCursorConnection( { ...DEFAULT_CONNECTION_ARGS, args }, diff --git a/apps/ensapi/src/graphql-api/schema/constants.ts b/apps/ensapi/src/graphql-api/schema/constants.ts index 31682b793..9af5c8cab 100644 --- a/apps/ensapi/src/graphql-api/schema/constants.ts +++ b/apps/ensapi/src/graphql-api/schema/constants.ts @@ -1,7 +1,11 @@ +import { getModelId } from "@/graphql-api/lib/get-model-id"; import { cursors } from "@/graphql-api/schema/cursors"; +/** + * Default Connection field arguments for use with the Relay plugin. + */ export const DEFAULT_CONNECTION_ARGS = { - toCursor: (model: T) => cursors.encode(model.id), + toCursor: (model: T) => cursors.encode(getModelId(model)), defaultSize: 100, maxSize: 1000, } as const; diff --git a/apps/ensapi/src/graphql-api/schema/cursors.ts b/apps/ensapi/src/graphql-api/schema/cursors.ts index 2e38379b5..6545bf53c 100644 --- a/apps/ensapi/src/graphql-api/schema/cursors.ts +++ b/apps/ensapi/src/graphql-api/schema/cursors.ts @@ -1,3 +1,7 @@ +/** + * It's considered good practice to provide cursors as opaque strings exclusively useful for + * paginating sets, so we encode/decode entity ids using base64. + */ export const cursors = { encode: (id: string) => Buffer.from(id, "utf8").toString("base64"), decode: (cursor: string) => Buffer.from(cursor, "base64").toString("utf8") as T, diff --git a/apps/ensapi/src/graphql-api/schema/domain.ts b/apps/ensapi/src/graphql-api/schema/domain.ts index 66c855fc0..05135c9ff 100644 --- a/apps/ensapi/src/graphql-api/schema/domain.ts +++ b/apps/ensapi/src/graphql-api/schema/domain.ts @@ -10,12 +10,12 @@ import { import { builder } from "@/graphql-api/builder"; import { getDomainResolver } from "@/graphql-api/lib/get-domain-resolver"; -import { getModelId } from "@/graphql-api/lib/get-id"; import { getLatestRegistration } from "@/graphql-api/lib/get-latest-registration"; +import { getModelId } from "@/graphql-api/lib/get-model-id"; import { AccountRef } from "@/graphql-api/schema/account"; import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; import { cursors } from "@/graphql-api/schema/cursors"; -import { type Registration, RegistrationInterfaceRef } from "@/graphql-api/schema/registration"; +import { RegistrationInterfaceRef } from "@/graphql-api/schema/registration"; import { RegistryRef } from "@/graphql-api/schema/registry"; import { ResolverRef } from "@/graphql-api/schema/resolver"; import { db } from "@/lib/db"; @@ -72,9 +72,9 @@ export type ENSv1Domain = Exclude; export type Domain = Exclude; -////////////////////////////// +////////////////////////////////// // DomainInterface Implementation -////////////////////////////// +////////////////////////////////// DomainInterfaceRef.implement({ description: "a Domain", fields: (t) => ({ @@ -160,7 +160,6 @@ DomainInterfaceRef.implement({ // resolve: async (parent) => { // // a domain's aliases are all of the paths from root to this domain for which it can be // // resolved. naively reverse-traverse the namegaph until the root is reached... yikes. - // // if materializing namespace: simply lookup namesInNamespace by domainId // return []; // }, // }), diff --git a/apps/ensapi/src/graphql-api/schema/name-or-node.ts b/apps/ensapi/src/graphql-api/schema/name-or-node.ts index 764b6673a..a52714e16 100644 --- a/apps/ensapi/src/graphql-api/schema/name-or-node.ts +++ b/apps/ensapi/src/graphql-api/schema/name-or-node.ts @@ -1,5 +1,8 @@ import { builder } from "@/graphql-api/builder"; +/** + * Input that requires one of `name` or `node`. + */ export const NameOrNodeInput = builder.inputType("NameOrNodeInput", { description: "TODO", isOneOf: true, diff --git a/apps/ensapi/src/graphql-api/schema/permissions.ts b/apps/ensapi/src/graphql-api/schema/permissions.ts index dd7507d0c..57e6520e4 100644 --- a/apps/ensapi/src/graphql-api/schema/permissions.ts +++ b/apps/ensapi/src/graphql-api/schema/permissions.ts @@ -10,7 +10,7 @@ import { } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; -import { getModelId } from "@/graphql-api/lib/get-id"; +import { getModelId } from "@/graphql-api/lib/get-model-id"; import { AccountRef } from "@/graphql-api/schema/account"; import { AccountIdRef } from "@/graphql-api/schema/account-id"; import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; diff --git a/apps/ensapi/src/graphql-api/schema/registration.ts b/apps/ensapi/src/graphql-api/schema/registration.ts index 1a68801a8..2f64a6957 100644 --- a/apps/ensapi/src/graphql-api/schema/registration.ts +++ b/apps/ensapi/src/graphql-api/schema/registration.ts @@ -1,7 +1,7 @@ import type { RegistrationId, RequiredAndNotNull } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; -import { getModelId } from "@/graphql-api/lib/get-id"; +import { getModelId } from "@/graphql-api/lib/get-model-id"; import { AccountIdRef } from "@/graphql-api/schema/account-id"; import { DomainInterfaceRef } from "@/graphql-api/schema/domain"; import { WrappedBaseRegistrarRegistrationRef } from "@/graphql-api/schema/wrapped-baseregistrar-registration"; diff --git a/apps/ensapi/src/graphql-api/schema/registry.ts b/apps/ensapi/src/graphql-api/schema/registry.ts index 7af8b6a1b..4d6fc49f1 100644 --- a/apps/ensapi/src/graphql-api/schema/registry.ts +++ b/apps/ensapi/src/graphql-api/schema/registry.ts @@ -3,7 +3,7 @@ import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@poth import { type ENSv2DomainId, makePermissionsId, type RegistryId } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; -import { getModelId } from "@/graphql-api/lib/get-id"; +import { getModelId } from "@/graphql-api/lib/get-model-id"; import { AccountIdInput, AccountIdRef } from "@/graphql-api/schema/account-id"; import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; import { cursors } from "@/graphql-api/schema/cursors"; diff --git a/apps/ensapi/src/graphql-api/schema/resolver-records.ts b/apps/ensapi/src/graphql-api/schema/resolver-records.ts index 440a9f1b0..c2c7daf64 100644 --- a/apps/ensapi/src/graphql-api/schema/resolver-records.ts +++ b/apps/ensapi/src/graphql-api/schema/resolver-records.ts @@ -1,7 +1,7 @@ import { bigintToCoinType, type ResolverRecordsId } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; -import { getModelId } from "@/graphql-api/lib/get-id"; +import { getModelId } from "@/graphql-api/lib/get-model-id"; import { db } from "@/lib/db"; export const ResolverRecordsRef = builder.loadableObjectRef("ResolverRecords", { diff --git a/apps/ensapi/src/graphql-api/schema/resolver.ts b/apps/ensapi/src/graphql-api/schema/resolver.ts index b2a2f1ac3..5e4b81d09 100644 --- a/apps/ensapi/src/graphql-api/schema/resolver.ts +++ b/apps/ensapi/src/graphql-api/schema/resolver.ts @@ -10,7 +10,7 @@ import { } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; -import { getModelId } from "@/graphql-api/lib/get-id"; +import { getModelId } from "@/graphql-api/lib/get-model-id"; import { AccountRef } from "@/graphql-api/schema/account"; import { AccountIdInput, AccountIdRef } from "@/graphql-api/schema/account-id"; import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; diff --git a/apps/ensapi/src/handlers/ensnode-graphql-api.ts b/apps/ensapi/src/handlers/ensnode-graphql-api.ts index 24f4feb88..0896385f2 100644 --- a/apps/ensapi/src/handlers/ensnode-graphql-api.ts +++ b/apps/ensapi/src/handlers/ensnode-graphql-api.ts @@ -7,6 +7,7 @@ import { createYoga } from "graphql-yoga"; import { schema } from "@/graphql-api/schema"; import { factory } from "@/lib/hono-factory"; import { makeLogger } from "@/lib/logger"; +import { requireCorePluginMiddleware } from "@/middleware/require-core-plugin.middleware"; const logger = makeLogger("ensnode-graphql"); @@ -14,18 +15,21 @@ const yoga = createYoga({ graphqlEndpoint: "*", schema, graphiql: { - defaultQuery: `query GetCanonicalNametree { - root { - domain { label } + defaultQuery: `query DomainsByOwner { + account(address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266") { domains { - label - subregistry { - domains { + edges { + node { + id label - subregistry { - domains { - label - } + owner { address } + registration { expiry } + ... on ENSv1Domain { + parent { label } + } + ... on ENSv2Domain { + canonicalId + registry { contract {chainId address}} } } } @@ -47,6 +51,7 @@ const yoga = createYoga({ const app = factory.createApp(); +app.use(requireCorePluginMiddleware("ensv2")); app.use(async (c) => { const response = await yoga.fetch(c.req.raw, c.var); return response; diff --git a/apps/ensapi/src/middleware/require-core-plugin.middleware.ts b/apps/ensapi/src/middleware/require-core-plugin.middleware.ts index a07bd5457..fe38e147a 100644 --- a/apps/ensapi/src/middleware/require-core-plugin.middleware.ts +++ b/apps/ensapi/src/middleware/require-core-plugin.middleware.ts @@ -22,7 +22,6 @@ export const requireCorePluginMiddleware = (core: "subgraph" | "ensv2") => return c.notFound(); } - // TODO: enable ensv2 checking if ( core === "ensv2" && // !config.ensIndexerPublicConfig.plugins.includes(PluginName.ENSv2) diff --git a/apps/ensindexer/src/lib/ensv2/account-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/account-db-helpers.ts index 214017dc5..a7c2e40be 100644 --- a/apps/ensindexer/src/lib/ensv2/account-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/account-db-helpers.ts @@ -5,7 +5,8 @@ import type { Address } from "viem"; import { interpretAddress } from "@ensnode/ensnode-sdk"; /** - * TODO + * Ensures that the account identified by `address` exists. + * If `address` is the zeroAddress, no-op. */ export async function ensureAccount(context: Context, address: Address) { const interpreted = interpretAddress(address); diff --git a/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts index d2f1470b5..87c257627 100644 --- a/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/label-db-helpers.ts @@ -12,6 +12,9 @@ import { import { labelByLabelHash } from "@/lib/graphnode-helpers"; +/** + * Ensures that the LiteralLabel `label` is interpreted and upserted into the Label rainbow table. + */ export async function ensureLabel(context: Context, label: LiteralLabel) { const labelHash = labelhash(label); const interpretedLabel = literalLabelToInterpretedLabel(label); @@ -22,6 +25,10 @@ export async function ensureLabel(context: Context, label: LiteralLabel) { .onConflictDoUpdate({ value: interpretedLabel }); } +/** + * Ensures that the LabelHash `labelHash` is available in the Label rainbow table, attempting an + * ENSRainbow heal if this is the first time it has been encountered. + */ export async function ensureUnknownLabel(context: Context, labelHash: LabelHash) { // do nothing for existing labels, they're either healed or we don't know them const exists = await context.db.find(schema.label, { labelHash }); @@ -29,9 +36,11 @@ export async function ensureUnknownLabel(context: Context, labelHash: LabelHash) // attempt ENSRainbow heal const healedLabel = await labelByLabelHash(labelHash); + + // if healed, ensure (known) label if (healedLabel) return await ensureLabel(context, healedLabel); - // otherwise + // otherwise upsert label entity const interpretedLabel = encodeLabelHash(labelHash) as InterpretedLabel; await context.db .insert(schema.label) diff --git a/apps/ensindexer/src/lib/ensv2/registrar-lib.ts b/apps/ensindexer/src/lib/ensv2/registrar-lib.ts index f43eb70b1..82a77e613 100644 --- a/apps/ensindexer/src/lib/ensv2/registrar-lib.ts +++ b/apps/ensindexer/src/lib/ensv2/registrar-lib.ts @@ -24,7 +24,9 @@ const lineanamesNameWrapper = maybeGetDatasourceContract( "NameWrapper", ); -// TODO: need to handle namespace-specific remap +/** + * Mapping of RegistrarManagedName to its related Registrar and Registrar-adjacent contracts. + */ const REGISTRAR_CONTRACTS_BY_MANAGED_NAME: Record = { eth: [ getDatasourceContract( @@ -82,6 +84,10 @@ const REGISTRAR_CONTRACTS_BY_MANAGED_NAME: Record = { ].filter((c) => !!c), }; +/** + * Certain RegistrarManagedNames are different depending on the ENSNamespace — this encodes that + * relationship. + */ const RMN_NAMESPACE_OVERRIDE: Partial>> = { sepolia: { "base.eth": "basetest.eth", @@ -89,6 +95,9 @@ const RMN_NAMESPACE_OVERRIDE: Partial> }, }; +/** + * Given a `contract`, identify its RegistrarManagedName. + */ export const getRegistrarManagedName = (contract: AccountId) => { for (const [managedName, contracts] of Object.entries(REGISTRAR_CONTRACTS_BY_MANAGED_NAME)) { const isAnyOfTheContracts = contracts.some((_contract) => accountIdEqual(_contract, contract)); @@ -103,6 +112,9 @@ export const getRegistrarManagedName = (contract: AccountId) => { throw new Error("never"); }; +/** + * Determines whether `contract` is the NameWrapper. + */ export function isNameWrapper(contract: AccountId) { if (accountIdEqual(ethnamesNameWrapper, contract)) return true; if (lineanamesNameWrapper && accountIdEqual(lineanamesNameWrapper, contract)) return true; diff --git a/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts index 3c3a4137a..65d9227b8 100644 --- a/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts @@ -6,7 +6,7 @@ import { type DomainId, makeLatestRegistrationId, makeRegistrationId } from "@en import { toJson } from "@/lib/json-stringify-with-bigints"; /** - * TODO: find the most recent registration, active or otherwise + * Gets the latest Regsitration for the provided `domainId`. */ export async function getLatestRegistration(context: Context, domainId: DomainId) { return context.db.find(schema.registration, { id: makeLatestRegistrationId(domainId) }); diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index 6c30db59a..f19306258 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -1,5 +1,6 @@ /** * TODO TODAY + * - move registration expiration shared logic to sdk/ens * - self-review and document where needed * * TODO LATER @@ -15,6 +16,9 @@ * - will need to observe the correct event and then override the existing domain/registratioon info * - for MigratedWrappedNameRegistries, need to check name expiry during resolution and avoid resolving expired names * - autocomplete api + * - Query.permissions(by: { contract: { } }) + * - custom wrapper for resolveCursorConnection with typesafety that applies defaults and auto-decodes cursors to the indicated type + * - Pothos envelop plugins (aliases, depth, tokens, whatever) * * PENDING ENS TEAM * - DedicatedResolver moving to EAC From c906736f8b73abf56dcf2ce4fbfeb121d4e4f3fb Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 21 Nov 2025 16:23:07 -0600 Subject: [PATCH 063/102] docs --- .../src/lib/ensv2/registration-db-helpers.ts | 24 +++++++- .../ensindexer/src/lib/get-this-account-id.ts | 6 ++ .../src/lib/json-stringify-with-bigints.ts | 3 + ...domain-resolver-relationship-db-helpers.ts | 5 ++ .../ensv2/handlers/ensv1/BaseRegistrar.ts | 29 +++++----- .../ensv2/handlers/ensv1/ENSv1Registry.ts | 39 ++++--------- .../ensv2/handlers/ensv1/NameWrapper.ts | 4 +- .../handlers/ensv1/RegistrarController.ts | 2 +- .../ensv2/handlers/ensv2/ENSv2Registry.ts | 56 ++++++++----------- apps/ensindexer/src/plugins/ensv2/plugin.ts | 1 + .../schemas/protocol-acceleration.schema.ts | 6 +- 11 files changed, 96 insertions(+), 79 deletions(-) diff --git a/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts index 65d9227b8..23d58fe19 100644 --- a/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts @@ -5,6 +5,27 @@ import { type DomainId, makeLatestRegistrationId, makeRegistrationId } from "@en import { toJson } from "@/lib/json-stringify-with-bigints"; +/** + * Latest Registration & Renewals + * + * We store a one-to-many relationship of Domain -> Registration and a one-to-many relationship of + * Registration -> Renewal, but must frequently access the latest Registration or Renewal in our + * indexing logic. If we were to access these entities via a custom sql query like "SELECT * from + * registrations WHERE domainId= $domainId ORDER BY index DESC", ponder's in-memory cache would have + * to be flushed to postgres every time we access the latest Registration/Renewal, which is pretty + * frequent. To avoid this, we use the special key path /latest (instead of /:index) to access the + * latest Registration/Renewal, turning the operation into an O(1) lookup compatible with Ponder's + * in-memory cacheable db api. + * + * Then, when a new Registration/Renewal is to be created, the current latest is 'superceded': its id + * that is currently /latest is replaced by /:index and the new /latest is inserted. To make this + * compatible with Ponder's cacheable api, instead of updating the id, we delete the /latest entity + * and insert a new entity (with all of the same columns) under the new id. See `supercedeLatestRegistration` + * for implementation. + * + * This same logic applies to Renewals. + */ + /** * Gets the latest Regsitration for the provided `domainId`. */ @@ -13,7 +34,8 @@ export async function getLatestRegistration(context: Context, domainId: DomainId } /** - * TODO + * Supercedes the latest Registration, changing its id to be indexed, making room in the set for + * a new latest Registration. */ export async function supercedeLatestRegistration( context: Context, diff --git a/apps/ensindexer/src/lib/get-this-account-id.ts b/apps/ensindexer/src/lib/get-this-account-id.ts index 701879f9c..86b023b44 100644 --- a/apps/ensindexer/src/lib/get-this-account-id.ts +++ b/apps/ensindexer/src/lib/get-this-account-id.ts @@ -4,5 +4,11 @@ import type { AccountId } from "@ensnode/ensnode-sdk"; import type { LogEvent } from "@/lib/ponder-helpers"; +/** + * Retrieves the AccountId representing the contract on this chain under which `event` was emitted. + * + * @example + * const { chainId, address } = getThisAccountId(context, event); + */ export const getThisAccountId = (context: Context, event: Pick) => ({ chainId: context.chain.id, address: event.log.address }) satisfies AccountId; diff --git a/apps/ensindexer/src/lib/json-stringify-with-bigints.ts b/apps/ensindexer/src/lib/json-stringify-with-bigints.ts index 059d147c4..1cebc88f4 100644 --- a/apps/ensindexer/src/lib/json-stringify-with-bigints.ts +++ b/apps/ensindexer/src/lib/json-stringify-with-bigints.ts @@ -1,4 +1,7 @@ import { replaceBigInts } from "ponder"; +/** + * JSON.stringify with bigints replaced. + */ export const toJson = (value: unknown, pretty = true) => JSON.stringify(replaceBigInts(value, String), null, pretty ? 2 : undefined); diff --git a/apps/ensindexer/src/lib/protocol-acceleration/domain-resolver-relationship-db-helpers.ts b/apps/ensindexer/src/lib/protocol-acceleration/domain-resolver-relationship-db-helpers.ts index 52b78fc37..cd3701ca0 100644 --- a/apps/ensindexer/src/lib/protocol-acceleration/domain-resolver-relationship-db-helpers.ts +++ b/apps/ensindexer/src/lib/protocol-acceleration/domain-resolver-relationship-db-helpers.ts @@ -4,6 +4,11 @@ import { type Address, isAddressEqual, zeroAddress } from "viem"; import { type AccountId, type DomainId, makeResolverId } from "@ensnode/ensnode-sdk"; +/** + * Ensures that the Domain-Resolver Relationship for the provided `domainId` in `registry` is set + * to `resolver`. If `resolver` is zeroAddress, it is interpreted as a deletion, and the relationship + * is removed. + */ export async function ensureDomainResolverRelation( context: Context, registry: AccountId, diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts index f5e122afc..c6d55673e 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts @@ -26,18 +26,19 @@ import type { EventWithArgs } from "@/lib/ponder-helpers"; const pluginName = PluginName.ENSv2; -// legacy always updates registry so domain is guaranteed to exist -// wrapped always updates registry so domain is guaranteed to exist -// unwrapped always updates registry so domain is guaranteed to exist - -// technically all BaseRegistry-derived contracts have the ability to `registerOnly` and therefore -// don't represent a valid ENS name. then they can be re-registered -// renewals work on registered names because that's obvious -// and in this model it's valid for a registration to reference a domain that does not exist -// and we _don't_ update owner -// that's why registrars manage their own set of owners (registrants), which can be desynced from a domain's owner -// so in the registrar handlers we should never touch schema.domain, only reference them. - +/** + * In ENSv1, all BaseRegistrar-derived Registrar contracts (& their controllers) have the ability to + * `registerOnly`, creating a 'preminted' name (a label with a Registration but no Domain in the + * ENSv1 Registry). The .eth Registrar doesn't do this, but Basenames and Lineanames do. + * + * Because they all technically have this ability, this logic avoids the invariant that an associated + * v1Domain must exist and the v1Domain.owner is conditionally materialized. + * + * Technically each BaseRegistrar Registration also has an associated owner that we could keep track + * of, but because we're materializing the v1Domain's effective owner, we need not explicitly track + * it. When a preminted name is actually registered, the indexing logic will see that the v1Domain + * exists and materialize its effective owner correctly. + */ export default function () { ponder.on( namespaceContract(pluginName, "BaseRegistrar:Transfer"), @@ -64,7 +65,7 @@ export default function () { // b) re-registering a name that has expired, and it will emit NameRegistered directly afterwards, or // c) user intentionally burning their registration token by transferring to zeroAddress. // - // in all such cases, a Registration is expected and we can + // in all such cases, a Registration is expected and we can conditionally materialize Domain owner const labelHash = registrarTokenIdToLabelHash(tokenId); const registrar = getThisAccountId(context, event); @@ -180,7 +181,7 @@ export default function () { // update the registration await context.db.update(schema.registration, { id: registration.id }).set({ expiry }); - // TODO: insert renewal & reference registration + // TODO(renewals): insert renewal & reference registration }, ); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts index 3a5f333f3..65af6858c 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts @@ -7,8 +7,7 @@ import { type Address, isAddressEqual, zeroAddress } from "viem"; import { ADDR_REVERSE_NODE, getENSRootChainId, - getRootRegistry, - getRootRegistryId, + interpretAddress, type LabelHash, makeENSv1DomainId, makeSubdomainNode, @@ -31,19 +30,6 @@ const pluginName = PluginName.ENSv2; * - piggybacks Protocol Resolution plugin's Node Migration status */ export default function () { - /** - * Sets up the ENSv2 Root Registry - */ - ponder.on(namespaceContract(pluginName, "ENSv1RegistryOld:setup"), async ({ context }) => { - // ensures that the Root Registry (which is eventually backed by the ENSv2 Root Registry) is - // populated in the database - await context.db.insert(schema.registry).values({ - id: getRootRegistryId(config.namespace), - type: "RegistryContract", - ...getRootRegistry(config.namespace), - }); - }); - /** * Registry#NewOwner is either a new Domain OR the owner of the parent changing the owner of the child. */ @@ -113,26 +99,25 @@ export default function () { context: Context; event: EventWithArgs<{ node: Node; owner: Address }>; }) { - const { node, owner } = event.args; + const { node, owner: _owner } = event.args; + const owner = interpretAddress(_owner); // ENSv2 model does not include root node, no-op if (node === ROOT_NODE) return; const domainId = makeENSv1DomainId(node); - const isDeletion = isAddressEqual(zeroAddress, owner); - if (isDeletion) { + if (owner === null) { await context.db.delete(schema.v1Domain, { id: domainId }); - return; + } else { + // materialize domain owner + // NOTE: despite Domain.ownerId being materialized from other sources of truth (i.e. Registrars + // like BaseRegistrars & NameWrapper) it's ok to always set it here because the Registrar-emitted + // events occur _after_ the Registry events. So when a name is wrapped, for example, the Registry's + // owner changes to that of the NameWrapper but then the NameWrapper emits NameWrapped, and this + // indexing code re-materializes the Domain.ownerId to the NameWraper-emitted value. + await materializeENSv1DomainEffectiveOwner(context, domainId, owner); } - - // materialize domain owner - // NOTE: despite Domain.ownerId being materialized from other sources of truth (i.e. Registrars - // like BaseRegistrars & NameWrapper) it's ok to always set it here because the Registrar-emitted - // events occur _after_ the Registry events. So when a name is wrapped, for example, the Registry's - // owner changes to that of the NameWrapper but then the NameWrapper emits NameWrapped, and this - // indexing code re-materializes the Domain.ownerId to the NameWraper-emitted value. - await materializeENSv1DomainEffectiveOwner(context, domainId, owner); } /** diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts index e9f5618df..35086a6be 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts @@ -45,7 +45,7 @@ const pluginName = PluginName.ENSv2; const tokenIdToNode = (tokenId: bigint): Node => uint256ToHex32(tokenId); /** - * NameWrapper emits expiry but 0 means 'doesn't expire', we represent as null. + * NameWrapper emits expiry as 0 to mean 'doesn't expire', so we interpret as null. */ const interpretExpiry = (expiry: bigint): bigint | null => (expiry === 0n ? null : expiry); @@ -358,6 +358,8 @@ export default function () { } await context.db.update(schema.registration, { id: registration.id }).set({ expiry }); + + // TODO(renewals): insert Renewal if NameWrapper Registration, otherwise handled by BaseRegistrar }, ); diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts index abf06e7e0..9e26fc0eb 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts @@ -101,7 +101,7 @@ export default function () { ); } - // TODO: update renewal with base/premium + // TODO(renewals): update renewal with base/premium // const renewal = await getLatestRenewal(context, registration.id); // if (!renewal) invariant // await context.db.update(schema.renewal, { id: renewal.id }).set({ baseCost, premium, referrer }) diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts index 5f0355cb8..bfdca3c7d 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts @@ -1,8 +1,8 @@ /** biome-ignore-all lint/correctness/noUnusedVariables: ignore for now */ + import { type Context, ponder } from "ponder:registry"; import schema from "ponder:schema"; -import { replaceBigInts } from "ponder"; -import { type Address, hexToBigInt, isAddressEqual, labelhash, zeroAddress } from "viem"; +import { type Address, hexToBigInt, labelhash } from "viem"; import { type AccountId, @@ -56,17 +56,14 @@ export default function () { // Sanity Check: Canonical Id must match emitted label if (canonicalId !== getCanonicalId(hexToBigInt(labelhash(label)))) { throw new Error( - `Sanity Check: Domain's Canonical Id !== getCanonicalId(uint256(labelhash(label)))\n${JSON.stringify( - replaceBigInts( - { - tokenId, - canonicalId, - label, - labelHash, - hexToBigInt: hexToBigInt(labelhash(label)), - }, - String, - ), + `Sanity Check: Domain's Canonical Id !== getCanonicalId(uint256(labelhash(label)))\n${toJson( + { + tokenId, + canonicalId, + label, + labelHash, + hexToBigInt: hexToBigInt(labelhash(label)), + }, )}`, ); } @@ -160,7 +157,7 @@ export default function () { // update Registration await context.db.update(schema.registration, { id: registration.id }).set({ expiry }); - // TODO: insert Renewal + // TODO(renewals): insert Renewal }, ); @@ -176,15 +173,15 @@ export default function () { subregistry: Address; }>; }) => { - const { tokenId, subregistry } = event.args; + const { tokenId, subregistry: _subregistry } = event.args; + const subregistry = interpretAddress(_subregistry); const registryAccountId = getThisAccountId(context, event); const canonicalId = getCanonicalId(tokenId); const domainId = makeENSv2DomainId(registryAccountId, canonicalId); // update domain's subregistry - const isDeletion = isAddressEqual(zeroAddress, subregistry); - if (isDeletion) { + if (subregistry === null) { await context.db.update(schema.v2Domain, { id: domainId }).set({ subregistryId: null }); } else { const subregistryAccountId: AccountId = { chainId: context.chain.id, address: subregistry }; @@ -210,7 +207,7 @@ export default function () { }) => { const { oldTokenId, newTokenId, resource } = event.args; - // Invariant: CanonicalIds match + // Invariant: CanonicalIds must match if (getCanonicalId(oldTokenId) !== getCanonicalId(newTokenId)) { throw new Error(`Invariant(ENSv2Registry:TokenRegenerated): Canonical ID Malformed.`); } @@ -236,8 +233,13 @@ export default function () { const { id: tokenId, to: owner } = event.args; const canonicalId = getCanonicalId(tokenId); - const registryAccountId = getThisAccountId(context, event); - const domainId = makeENSv2DomainId(registryAccountId, canonicalId); + const registry = getThisAccountId(context, event); + const domainId = makeENSv2DomainId(registry, canonicalId); + + // TODO(signals): remove this + const registryId = makeRegistryId(registry); + const exists = await context.db.find(schema.registry, { id: registryId }); + if (!exists) return; // no-op non-Registry ERC1155 Transfers // just update the owner // any _burns are always followed by a _mint, which would set the owner correctly @@ -246,19 +248,7 @@ export default function () { .set({ ownerId: interpretAddress(owner) }); } - ponder.on( - namespaceContract(pluginName, "ENSv2Registry:TransferSingle"), - async ({ context, event }) => { - const registryAccountId = getThisAccountId(context, event); - const registryId = makeRegistryId(registryAccountId); - - // TODO(registry-announcement): ideally remove this - const registry = await context.db.find(schema.registry, { id: registryId }); - if (registry === null) return; // no-op non-Registry ERC1155 Transfers - - await handleTransferSingle({ context, event }); - }, - ); + ponder.on(namespaceContract(pluginName, "ENSv2Registry:TransferSingle"), handleTransferSingle); ponder.on( namespaceContract(pluginName, "ENSv2Registry:TransferBatch"), async ({ context, event }) => { diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index f19306258..c67211cc1 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -4,6 +4,7 @@ * - self-review and document where needed * * TODO LATER + * - re-asses NameWrapper expiry logic — compare to subgraph implementation & see if we can simplify * - indexes based on graphql queries, ask claude to compile recommendations * - modify Registration schema to more closely match ENSv2, map v1 into it * - Renewals (v1, v2) diff --git a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts index e7b7f6987..bfb263950 100644 --- a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts +++ b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts @@ -7,8 +7,6 @@ import type { Address } from "viem"; import type { ChainId, DomainId, Node, ResolverId, ResolverRecordsId } from "@ensnode/ensnode-sdk"; -// TODO: implement resolverType & polymorphic field availability - /** * Tracks an Account's ENSIP-19 Reverse Name Records by CoinType. * @@ -80,6 +78,10 @@ export const domainResolverRelation_relations = relations(domainResolverRelation }), })); +/** + * Resolver represents an individual IResolver contract. It tracks metadata about the Resolver as well, + * for example whether it is an IExtendedResolver, IDedicatedResolver, a BridgedResolver, etc). + */ export const resolver = onchainTable( "resolvers", (t) => ({ From 9f3beab853a0561bd37894c97ea6b33f480dc3a4 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 21 Nov 2025 16:25:06 -0600 Subject: [PATCH 064/102] fix: lint --- .../handlers/ThreeDNSToken.ts | 1 - .../src/abis/ensv2/ETHRegistrar.ts | 704 +++++++++--------- 2 files changed, 352 insertions(+), 353 deletions(-) diff --git a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ThreeDNSToken.ts b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ThreeDNSToken.ts index 2f705890c..66236871e 100644 --- a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ThreeDNSToken.ts +++ b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/ThreeDNSToken.ts @@ -8,7 +8,6 @@ import { type ChainId, type LabelHash, makeENSv1DomainId, - makeResolverId, makeSubdomainNode, type Node, PluginName, diff --git a/packages/datasources/src/abis/ensv2/ETHRegistrar.ts b/packages/datasources/src/abis/ensv2/ETHRegistrar.ts index f9415f916..84ab344e4 100644 --- a/packages/datasources/src/abis/ensv2/ETHRegistrar.ts +++ b/packages/datasources/src/abis/ensv2/ETHRegistrar.ts @@ -1,534 +1,534 @@ export const ETHRegistrar = [ { - "inputs": [ + inputs: [ { - "internalType": "bytes32", - "name": "commitment", - "type": "bytes32" + internalType: "bytes32", + name: "commitment", + type: "bytes32", }, { - "internalType": "uint64", - "name": "validFrom", - "type": "uint64" + internalType: "uint64", + name: "validFrom", + type: "uint64", }, { - "internalType": "uint64", - "name": "blockTimestamp", - "type": "uint64" - } + internalType: "uint64", + name: "blockTimestamp", + type: "uint64", + }, ], - "name": "CommitmentTooNew", - "type": "error" + name: "CommitmentTooNew", + type: "error", }, { - "inputs": [ + inputs: [ { - "internalType": "bytes32", - "name": "commitment", - "type": "bytes32" + internalType: "bytes32", + name: "commitment", + type: "bytes32", }, { - "internalType": "uint64", - "name": "validTo", - "type": "uint64" + internalType: "uint64", + name: "validTo", + type: "uint64", }, { - "internalType": "uint64", - "name": "blockTimestamp", - "type": "uint64" - } + internalType: "uint64", + name: "blockTimestamp", + type: "uint64", + }, ], - "name": "CommitmentTooOld", - "type": "error" + name: "CommitmentTooOld", + type: "error", }, { - "inputs": [ + inputs: [ { - "internalType": "uint64", - "name": "duration", - "type": "uint64" + internalType: "uint64", + name: "duration", + type: "uint64", }, { - "internalType": "uint64", - "name": "minDuration", - "type": "uint64" - } + internalType: "uint64", + name: "minDuration", + type: "uint64", + }, ], - "name": "DurationTooShort", - "type": "error" + name: "DurationTooShort", + type: "error", }, { - "inputs": [], - "name": "MaxCommitmentAgeTooLow", - "type": "error" + inputs: [], + name: "MaxCommitmentAgeTooLow", + type: "error", }, { - "inputs": [ + inputs: [ { - "internalType": "string", - "name": "label", - "type": "string" - } + internalType: "string", + name: "label", + type: "string", + }, ], - "name": "NameAlreadyRegistered", - "type": "error" + name: "NameAlreadyRegistered", + type: "error", }, { - "inputs": [ + inputs: [ { - "internalType": "string", - "name": "label", - "type": "string" - } + internalType: "string", + name: "label", + type: "string", + }, ], - "name": "NameNotRegistered", - "type": "error" + name: "NameNotRegistered", + type: "error", }, { - "inputs": [ + inputs: [ { - "internalType": "string", - "name": "label", - "type": "string" - } + internalType: "string", + name: "label", + type: "string", + }, ], - "name": "NotValid", - "type": "error" + name: "NotValid", + type: "error", }, { - "inputs": [ + inputs: [ { - "internalType": "contract IERC20", - "name": "paymentToken", - "type": "address" - } + internalType: "contract IERC20", + name: "paymentToken", + type: "address", + }, ], - "name": "PaymentTokenNotSupported", - "type": "error" + name: "PaymentTokenNotSupported", + type: "error", }, { - "inputs": [ + inputs: [ { - "internalType": "bytes32", - "name": "commitment", - "type": "bytes32" - } + internalType: "bytes32", + name: "commitment", + type: "bytes32", + }, ], - "name": "UnexpiredCommitmentExists", - "type": "error" + name: "UnexpiredCommitmentExists", + type: "error", }, { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "bytes32", - "name": "commitment", - "type": "bytes32" - } + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "bytes32", + name: "commitment", + type: "bytes32", + }, ], - "name": "CommitmentMade", - "type": "event" + name: "CommitmentMade", + type: "event", }, { - "anonymous": false, - "inputs": [ + anonymous: false, + inputs: [ { - "indexed": true, - "internalType": "uint256", - "name": "tokenId", - "type": "uint256" + indexed: true, + internalType: "uint256", + name: "tokenId", + type: "uint256", }, { - "indexed": false, - "internalType": "string", - "name": "label", - "type": "string" + indexed: false, + internalType: "string", + name: "label", + type: "string", }, { - "indexed": false, - "internalType": "address", - "name": "owner", - "type": "address" + indexed: false, + internalType: "address", + name: "owner", + type: "address", }, { - "indexed": false, - "internalType": "contract IRegistry", - "name": "subregistry", - "type": "address" + indexed: false, + internalType: "contract IRegistry", + name: "subregistry", + type: "address", }, { - "indexed": false, - "internalType": "address", - "name": "resolver", - "type": "address" + indexed: false, + internalType: "address", + name: "resolver", + type: "address", }, { - "indexed": false, - "internalType": "uint64", - "name": "duration", - "type": "uint64" + indexed: false, + internalType: "uint64", + name: "duration", + type: "uint64", }, { - "indexed": false, - "internalType": "contract IERC20", - "name": "paymentToken", - "type": "address" + indexed: false, + internalType: "contract IERC20", + name: "paymentToken", + type: "address", }, { - "indexed": false, - "internalType": "bytes32", - "name": "referrer", - "type": "bytes32" + indexed: false, + internalType: "bytes32", + name: "referrer", + type: "bytes32", }, { - "indexed": false, - "internalType": "uint256", - "name": "base", - "type": "uint256" + indexed: false, + internalType: "uint256", + name: "base", + type: "uint256", }, { - "indexed": false, - "internalType": "uint256", - "name": "premium", - "type": "uint256" - } + indexed: false, + internalType: "uint256", + name: "premium", + type: "uint256", + }, ], - "name": "NameRegistered", - "type": "event" + name: "NameRegistered", + type: "event", }, { - "anonymous": false, - "inputs": [ + anonymous: false, + inputs: [ { - "indexed": true, - "internalType": "uint256", - "name": "tokenId", - "type": "uint256" + indexed: true, + internalType: "uint256", + name: "tokenId", + type: "uint256", }, { - "indexed": false, - "internalType": "string", - "name": "label", - "type": "string" + indexed: false, + internalType: "string", + name: "label", + type: "string", }, { - "indexed": false, - "internalType": "uint64", - "name": "duration", - "type": "uint64" + indexed: false, + internalType: "uint64", + name: "duration", + type: "uint64", }, { - "indexed": false, - "internalType": "uint64", - "name": "newExpiry", - "type": "uint64" + indexed: false, + internalType: "uint64", + name: "newExpiry", + type: "uint64", }, { - "indexed": false, - "internalType": "contract IERC20", - "name": "paymentToken", - "type": "address" + indexed: false, + internalType: "contract IERC20", + name: "paymentToken", + type: "address", }, { - "indexed": false, - "internalType": "bytes32", - "name": "referrer", - "type": "bytes32" + indexed: false, + internalType: "bytes32", + name: "referrer", + type: "bytes32", }, { - "indexed": false, - "internalType": "uint256", - "name": "base", - "type": "uint256" - } + indexed: false, + internalType: "uint256", + name: "base", + type: "uint256", + }, ], - "name": "NameRenewed", - "type": "event" + name: "NameRenewed", + type: "event", }, { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "contract IERC20", - "name": "paymentToken", - "type": "address" - } + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "contract IERC20", + name: "paymentToken", + type: "address", + }, ], - "name": "PaymentTokenAdded", - "type": "event" + name: "PaymentTokenAdded", + type: "event", }, { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "contract IERC20", - "name": "paymentToken", - "type": "address" - } + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "contract IERC20", + name: "paymentToken", + type: "address", + }, ], - "name": "PaymentTokenRemoved", - "type": "event" + name: "PaymentTokenRemoved", + type: "event", }, { - "inputs": [ + inputs: [ { - "internalType": "bytes32", - "name": "commitment", - "type": "bytes32" - } + internalType: "bytes32", + name: "commitment", + type: "bytes32", + }, ], - "name": "commit", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" + name: "commit", + outputs: [], + stateMutability: "nonpayable", + type: "function", }, { - "inputs": [ + inputs: [ { - "internalType": "bytes32", - "name": "commitment", - "type": "bytes32" - } + internalType: "bytes32", + name: "commitment", + type: "bytes32", + }, ], - "name": "commitmentAt", - "outputs": [ + name: "commitmentAt", + outputs: [ { - "internalType": "uint64", - "name": "", - "type": "uint64" - } + internalType: "uint64", + name: "", + type: "uint64", + }, ], - "stateMutability": "view", - "type": "function" + stateMutability: "view", + type: "function", }, { - "inputs": [ + inputs: [ { - "internalType": "string", - "name": "label", - "type": "string" - } + internalType: "string", + name: "label", + type: "string", + }, ], - "name": "isAvailable", - "outputs": [ + name: "isAvailable", + outputs: [ { - "internalType": "bool", - "name": "", - "type": "bool" - } + internalType: "bool", + name: "", + type: "bool", + }, ], - "stateMutability": "view", - "type": "function" + stateMutability: "view", + type: "function", }, { - "inputs": [ + inputs: [ { - "internalType": "contract IERC20", - "name": "paymentToken", - "type": "address" - } + internalType: "contract IERC20", + name: "paymentToken", + type: "address", + }, ], - "name": "isPaymentToken", - "outputs": [ + name: "isPaymentToken", + outputs: [ { - "internalType": "bool", - "name": "", - "type": "bool" - } + internalType: "bool", + name: "", + type: "bool", + }, ], - "stateMutability": "view", - "type": "function" + stateMutability: "view", + type: "function", }, { - "inputs": [ + inputs: [ { - "internalType": "string", - "name": "label", - "type": "string" - } + internalType: "string", + name: "label", + type: "string", + }, ], - "name": "isValid", - "outputs": [ + name: "isValid", + outputs: [ { - "internalType": "bool", - "name": "", - "type": "bool" - } + internalType: "bool", + name: "", + type: "bool", + }, ], - "stateMutability": "view", - "type": "function" + stateMutability: "view", + type: "function", }, { - "inputs": [ + inputs: [ { - "internalType": "string", - "name": "label", - "type": "string" + internalType: "string", + name: "label", + type: "string", }, { - "internalType": "address", - "name": "owner", - "type": "address" + internalType: "address", + name: "owner", + type: "address", }, { - "internalType": "bytes32", - "name": "secret", - "type": "bytes32" + internalType: "bytes32", + name: "secret", + type: "bytes32", }, { - "internalType": "contract IRegistry", - "name": "subregistry", - "type": "address" + internalType: "contract IRegistry", + name: "subregistry", + type: "address", }, { - "internalType": "address", - "name": "resolver", - "type": "address" + internalType: "address", + name: "resolver", + type: "address", }, { - "internalType": "uint64", - "name": "duration", - "type": "uint64" + internalType: "uint64", + name: "duration", + type: "uint64", }, { - "internalType": "bytes32", - "name": "referrer", - "type": "bytes32" - } + internalType: "bytes32", + name: "referrer", + type: "bytes32", + }, ], - "name": "makeCommitment", - "outputs": [ + name: "makeCommitment", + outputs: [ { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } + internalType: "bytes32", + name: "", + type: "bytes32", + }, ], - "stateMutability": "pure", - "type": "function" + stateMutability: "pure", + type: "function", }, { - "inputs": [ + inputs: [ { - "internalType": "string", - "name": "label", - "type": "string" + internalType: "string", + name: "label", + type: "string", }, { - "internalType": "address", - "name": "owner", - "type": "address" + internalType: "address", + name: "owner", + type: "address", }, { - "internalType": "bytes32", - "name": "secret", - "type": "bytes32" + internalType: "bytes32", + name: "secret", + type: "bytes32", }, { - "internalType": "contract IRegistry", - "name": "subregistry", - "type": "address" + internalType: "contract IRegistry", + name: "subregistry", + type: "address", }, { - "internalType": "address", - "name": "resolver", - "type": "address" + internalType: "address", + name: "resolver", + type: "address", }, { - "internalType": "uint64", - "name": "duration", - "type": "uint64" + internalType: "uint64", + name: "duration", + type: "uint64", }, { - "internalType": "contract IERC20", - "name": "paymentToken", - "type": "address" + internalType: "contract IERC20", + name: "paymentToken", + type: "address", }, { - "internalType": "bytes32", - "name": "referrer", - "type": "bytes32" - } + internalType: "bytes32", + name: "referrer", + type: "bytes32", + }, ], - "name": "register", - "outputs": [ + name: "register", + outputs: [ { - "internalType": "uint256", - "name": "", - "type": "uint256" - } + internalType: "uint256", + name: "", + type: "uint256", + }, ], - "stateMutability": "nonpayable", - "type": "function" + stateMutability: "nonpayable", + type: "function", }, { - "inputs": [ + inputs: [ { - "internalType": "string", - "name": "label", - "type": "string" + internalType: "string", + name: "label", + type: "string", }, { - "internalType": "uint64", - "name": "duration", - "type": "uint64" + internalType: "uint64", + name: "duration", + type: "uint64", }, { - "internalType": "contract IERC20", - "name": "paymentToken", - "type": "address" + internalType: "contract IERC20", + name: "paymentToken", + type: "address", }, { - "internalType": "bytes32", - "name": "referrer", - "type": "bytes32" - } + internalType: "bytes32", + name: "referrer", + type: "bytes32", + }, ], - "name": "renew", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" + name: "renew", + outputs: [], + stateMutability: "nonpayable", + type: "function", }, { - "inputs": [ + inputs: [ { - "internalType": "string", - "name": "label", - "type": "string" + internalType: "string", + name: "label", + type: "string", }, { - "internalType": "address", - "name": "owner", - "type": "address" + internalType: "address", + name: "owner", + type: "address", }, { - "internalType": "uint64", - "name": "duration", - "type": "uint64" + internalType: "uint64", + name: "duration", + type: "uint64", }, { - "internalType": "contract IERC20", - "name": "paymentToken", - "type": "address" - } + internalType: "contract IERC20", + name: "paymentToken", + type: "address", + }, ], - "name": "rentPrice", - "outputs": [ + name: "rentPrice", + outputs: [ { - "internalType": "uint256", - "name": "base", - "type": "uint256" + internalType: "uint256", + name: "base", + type: "uint256", }, { - "internalType": "uint256", - "name": "premium", - "type": "uint256" - } + internalType: "uint256", + name: "premium", + type: "uint256", + }, ], - "stateMutability": "view", - "type": "function" - } + stateMutability: "view", + type: "function", + }, ] as const; From 9f420db25d36026f22bd0eeb5e57fd9145d0f1e4 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 21 Nov 2025 18:15:56 -0600 Subject: [PATCH 065/102] fix: ensapi init log --- apps/ensapi/src/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/ensapi/src/index.ts b/apps/ensapi/src/index.ts index 1bc2adc6d..8604fe6eb 100644 --- a/apps/ensapi/src/index.ts +++ b/apps/ensapi/src/index.ts @@ -67,9 +67,7 @@ const server = serve( port: config.port, }, async (info) => { - logger.info( - `ENSApi listening on port ${info.port} with config:\n${prettyPrintJson(redactEnsApiConfig(config))}`, - ); + logger.info({ config: redactEnsApiConfig(config) }, `ENSApi listening on port ${info.port}`); // self-healthcheck to connect to ENSIndexer & warm Indexing Status / Can Accelerate cache await app.request("/health"); From 321aa60b4751b9424f10619dbfb821cfb4024145 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 21 Nov 2025 18:38:59 -0600 Subject: [PATCH 066/102] docs --- apps/ensindexer/src/plugins/ensv2/plugin.ts | 7 +++---- packages/ensnode-sdk/src/ens/fuses.ts | 6 ++++++ packages/ensnode-sdk/src/ensv2/ids-lib.ts | 14 +++++++------- packages/ensnode-sdk/src/ensv2/ids.ts | 12 ++++++------ packages/ensnode-sdk/src/shared/root-registry.ts | 10 +++++----- 5 files changed, 27 insertions(+), 22 deletions(-) diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index c67211cc1..399a24e3b 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -1,9 +1,8 @@ /** - * TODO TODAY + * TODO * - move registration expiration shared logic to sdk/ens - * - self-review and document where needed - * - * TODO LATER + * - update isRegistrationFullyExpired todo in ensapi somewhere + * - RequiredAndNotNull opposite type: RequiredToBeNull for constraining polymorphic entities in graphql schema * - re-asses NameWrapper expiry logic — compare to subgraph implementation & see if we can simplify * - indexes based on graphql queries, ask claude to compile recommendations * - modify Registration schema to more closely match ENSv2, map v1 into it diff --git a/packages/ensnode-sdk/src/ens/fuses.ts b/packages/ensnode-sdk/src/ens/fuses.ts index 013b0b220..bacfa2464 100644 --- a/packages/ensnode-sdk/src/ens/fuses.ts +++ b/packages/ensnode-sdk/src/ens/fuses.ts @@ -1,4 +1,10 @@ +/** + * The NameWrapper's PARENT_CANNOT_CONTROL fuse. + */ const PARENT_CANNOT_CONTROL = 0x10000; +/** + * Determines whether `fuses` has set ('burnt') the PARENT_CANNOT_CONTROL fuse. + */ export const isPccFuseSet = (fuses: number) => (fuses & PARENT_CANNOT_CONTROL) === PARENT_CANNOT_CONTROL; diff --git a/packages/ensnode-sdk/src/ensv2/ids-lib.ts b/packages/ensnode-sdk/src/ensv2/ids-lib.ts index 051a0e3d0..afffdd360 100644 --- a/packages/ensnode-sdk/src/ensv2/ids-lib.ts +++ b/packages/ensnode-sdk/src/ensv2/ids-lib.ts @@ -61,13 +61,13 @@ export const makePermissionsId = (contract: AccountId) => serializeAccountId(contract) as PermissionsId; /** - * + * Constructs a PermissionsResourceId for a given `contract`'s `resource`. */ export const makePermissionsResourceId = (contract: AccountId, resource: bigint) => `${makePermissionsId(contract)}/${resource}` as PermissionsResourceId; /** - * + * Constructs a PermissionsUserId for a given `contract`'s `resource`'s `user`. */ export const makePermissionsUserId = (contract: AccountId, resource: bigint, user: Address) => `${makePermissionsId(contract)}/${resource}/${user}` as PermissionsUserId; @@ -78,19 +78,19 @@ export const makePermissionsUserId = (contract: AccountId, resource: bigint, use export const makeResolverId = (contract: AccountId) => serializeAccountId(contract) as ResolverId; /** - * + * Constructs a ResolverRecordsId for a given `node` under `resolver`. */ -export const makeResolverRecordsId = (contract: AccountId, node: Node) => - `${makeResolverId(contract)}/${node}` as ResolverRecordsId; +export const makeResolverRecordsId = (resolver: AccountId, node: Node) => + `${makeResolverId(resolver)}/${node}` as ResolverRecordsId; /** - * + * Constructs a RegistrationId for a `domainId`'s `index`'thd Registration. */ export const makeRegistrationId = (domainId: DomainId, index: number = 0) => `${domainId}/${index}` as RegistrationId; /** - * + * Constructs a RegistrationId denoting the latest Registration using the /latest keypath. */ export const makeLatestRegistrationId = (domainId: DomainId) => `${domainId}/latest` as RegistrationId; diff --git a/packages/ensnode-sdk/src/ensv2/ids.ts b/packages/ensnode-sdk/src/ensv2/ids.ts index b12483e32..1ce87aac0 100644 --- a/packages/ensnode-sdk/src/ensv2/ids.ts +++ b/packages/ensnode-sdk/src/ensv2/ids.ts @@ -26,31 +26,31 @@ export type ENSv2DomainId = string & { __brand: "ENSv2DomainId" }; export type DomainId = ENSv1DomainId | ENSv2DomainId; /** - * + * Uniquely identifies a Permissions entity. */ export type PermissionsId = SerializedAccountId & { __brand: "PermissionsId" }; /** - * + * Uniquely identifies a PermissionsResource entity. */ export type PermissionsResourceId = string & { __brand: "PermissionsResourceId" }; /** - * + * Uniquely identifies a PermissionsUser entity. */ export type PermissionsUserId = string & { __brand: "PermissionsUserId" }; /** - * + * Uniquely identifies a Resolver entity. */ export type ResolverId = SerializedAccountId & { __brand: "ResolverId" }; /** - * + * Uniquely identifies a ResolverRecords entity. */ export type ResolverRecordsId = string & { __brand: "ResolverRecordsId" }; /** - * + * Uniquely identifies a Registration entity. */ export type RegistrationId = string & { __brand: "RegistrationId" }; diff --git a/packages/ensnode-sdk/src/shared/root-registry.ts b/packages/ensnode-sdk/src/shared/root-registry.ts index fb71cbf08..5d5d45efb 100644 --- a/packages/ensnode-sdk/src/shared/root-registry.ts +++ b/packages/ensnode-sdk/src/shared/root-registry.ts @@ -2,7 +2,7 @@ import { DatasourceNames, type ENSNamespaceId, getDatasource } from "@ensnode/da import { type AccountId, accountIdEqual, makeRegistryId } from "@ensnode/ensnode-sdk"; /** - * TODO + * Gets the AccountId representing the ENSv2 Root Registry in the selected `namespace`. */ export const getRootRegistry = (namespace: ENSNamespaceId) => { const ensroot = getDatasource(namespace, DatasourceNames.ENSRoot); @@ -14,13 +14,13 @@ export const getRootRegistry = (namespace: ENSNamespaceId) => { }; /** - * TODO + * Gets the RegistryId representing the ENSv2 Root Registry in the selected `namespace`. */ export const getRootRegistryId = (namespace: ENSNamespaceId) => makeRegistryId(getRootRegistry(namespace)); /** - * TODO + * Determines whether `contract` is the ENSv2 Root Registry in `namespace`. */ -export const isRootRegistry = (namespace: ENSNamespaceId, accountId: AccountId) => - accountIdEqual(getRootRegistry(namespace), accountId); +export const isRootRegistry = (namespace: ENSNamespaceId, contract: AccountId) => + accountIdEqual(getRootRegistry(namespace), contract); From 078590be60bbf0b9be4be6f00eea634cf7ee3131 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 21 Nov 2025 18:40:01 -0600 Subject: [PATCH 067/102] pnpm to latest --- .tool-versions | 2 +- apps/ensadmin/package.json | 2 +- docs/ensnode.io/package.json | 2 +- docs/ensrainbow.io/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.tool-versions b/.tool-versions index ae9c7b993..cad8ba816 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ nodejs 22.14.0 -pnpm 10.20.0 +pnpm 10.23.0 diff --git a/apps/ensadmin/package.json b/apps/ensadmin/package.json index 411b09282..dc615fbc5 100644 --- a/apps/ensadmin/package.json +++ b/apps/ensadmin/package.json @@ -5,7 +5,7 @@ "type": "module", "description": "Explore the ENS Protocol like never before", "license": "MIT", - "packageManager": "pnpm@10.20.0", + "packageManager": "pnpm@10.23.0", "repository": { "type": "git", "url": "git+https://github.com/namehash/ensnode.git", diff --git a/docs/ensnode.io/package.json b/docs/ensnode.io/package.json index 3f286c3a5..0301810fb 100644 --- a/docs/ensnode.io/package.json +++ b/docs/ensnode.io/package.json @@ -3,7 +3,7 @@ "private": true, "type": "module", "version": "0.36.0", - "packageManager": "pnpm@10.20.0", + "packageManager": "pnpm@10.23.0", "scripts": { "dev": "astro dev", "start": "astro dev", diff --git a/docs/ensrainbow.io/package.json b/docs/ensrainbow.io/package.json index 10d5fc7f8..4a05544e1 100644 --- a/docs/ensrainbow.io/package.json +++ b/docs/ensrainbow.io/package.json @@ -2,7 +2,7 @@ "name": "@docs/ensrainbow", "type": "module", "version": "0.36.0", - "packageManager": "pnpm@10.20.0", + "packageManager": "pnpm@10.23.0", "private": true, "scripts": { "dev": "astro dev", From 1311939ab7928e4f3c544255c4b38e534795bdca Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 24 Nov 2025 10:08:05 -0800 Subject: [PATCH 068/102] feat: point to new seploia namespace (temp) --- ...s-db-helpers.ts => resolver-db-helpers.ts} | 18 +- apps/ensindexer/src/plugins/ensv2/plugin.ts | 3 + .../handlers/Resolver.ts | 2 +- packages/datasources/src/sepolia.ts | 305 +++--------------- 4 files changed, 65 insertions(+), 263 deletions(-) rename apps/ensindexer/src/lib/protocol-acceleration/{resolver-records-db-helpers.ts => resolver-db-helpers.ts} (95%) diff --git a/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts b/apps/ensindexer/src/lib/protocol-acceleration/resolver-db-helpers.ts similarity index 95% rename from apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts rename to apps/ensindexer/src/lib/protocol-acceleration/resolver-db-helpers.ts index d723eaeda..d64e7fc9d 100644 --- a/apps/ensindexer/src/lib/protocol-acceleration/resolver-records-db-helpers.ts +++ b/apps/ensindexer/src/lib/protocol-acceleration/resolver-db-helpers.ts @@ -79,15 +79,15 @@ export async function ensureResolver(context: Context, resolver: AccountId) { : null; // TODO: remove this in favor of EAC - let ownerId: Address | null = null; - try { - const rawOwner = await context.client.readContract({ - address: resolver.address, - abi: ResolverABI, - functionName: "owner", - }); - ownerId = interpretAddress(rawOwner); - } catch {} + const ownerId: Address | null = null; + // try { + // const rawOwner = await context.client.readContract({ + // address: resolver.address, + // abi: ResolverABI, + // functionName: "owner", + // }); + // ownerId = interpretAddress(rawOwner); + // } catch {} // ensure Resolver await context.db.insert(schema.resolver).values({ diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index 399a24e3b..fe861432e 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -1,5 +1,7 @@ /** * TODO + * - root can be inserted on setup or could be discovered naturally — see how that affects traversal/graphql api + * - probably easier to just insert it ahead of time like previously * - move registration expiration shared logic to sdk/ens * - update isRegistrationFullyExpired todo in ensapi somewhere * - RequiredAndNotNull opposite type: RequiredToBeNull for constraining polymorphic entities in graphql schema @@ -19,6 +21,7 @@ * - Query.permissions(by: { contract: { } }) * - custom wrapper for resolveCursorConnection with typesafety that applies defaults and auto-decodes cursors to the indicated type * - Pothos envelop plugins (aliases, depth, tokens, whatever) + * - BEFORE MERGE: revert sepolia.ts namespace back to original, including ensv2 stubs * * PENDING ENS TEAM * - DedicatedResolver moving to EAC diff --git a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Resolver.ts b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Resolver.ts index 248a2c72f..e3b05d518 100644 --- a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Resolver.ts +++ b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Resolver.ts @@ -20,7 +20,7 @@ import { handleResolverOwnerUpdate, handleResolverTextRecordUpdate, makeResolverRecordsCompositeKey, -} from "@/lib/protocol-acceleration/resolver-records-db-helpers"; +} from "@/lib/protocol-acceleration/resolver-db-helpers"; const pluginName = PluginName.ProtocolAcceleration; diff --git a/packages/datasources/src/sepolia.ts b/packages/datasources/src/sepolia.ts index 1397894ec..891321edd 100644 --- a/packages/datasources/src/sepolia.ts +++ b/packages/datasources/src/sepolia.ts @@ -1,3 +1,4 @@ +import { zeroAddress } from "viem"; import { arbitrumSepolia, baseSepolia, @@ -55,37 +56,37 @@ export default { contracts: { ENSv1RegistryOld: { abi: root_Registry, // Registry was redeployed, same abi - address: "0x94f523b8261b815b87effcf4d18e6abef18d6e4b", - startBlock: 3702721, + address: "0x4355f1c6b5b59818dc56e336d1584df35d47ad86", + startBlock: 9374708, }, ENSv1Registry: { abi: root_Registry, // Registry was redeployed, same abi - address: "0x00000000000c2e074ec69a0dfb2997ba6c7d2e1e", - startBlock: 3702728, + address: "0x17795c119b8155ab9d3357c77747ba509695d7cb", + startBlock: 9374709, }, Resolver: { abi: ResolverABI, - startBlock: 3702721, // ignores any Resolver events prior to `startBlock` of ENSv1RegistryOld on Sepolia + startBlock: 9374708, // ignores any Resolver events prior to `startBlock` of ENSv1RegistryOld on Sepolia }, BaseRegistrar: { abi: root_BaseRegistrar, - address: "0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85", - startBlock: 3702731, + address: "0xb16870800de7444f6b2ebd885465412a5e581614", + startBlock: 9374751, }, LegacyEthRegistrarController: { abi: root_LegacyEthRegistrarController, - address: "0x7e02892cfc2bfd53a75275451d73cf620e793fc0", - startBlock: 3790197, + address: "0x25da9aa54dae4afa6534ba829c6288039d4f5ebb", + startBlock: 9374756, }, WrappedEthRegistrarController: { abi: root_WrappedEthRegistrarController, - address: "0xfed6a969aaa60e4961fcd3ebf1a2e8913ac65b72", - startBlock: 3790244, + address: "0x4f1d36f2c1382a01006077a42de53f7c843d1a83", + startBlock: 9374767, }, UnwrappedEthRegistrarController: { abi: root_UnwrappedEthRegistrarController, - address: "0xfb3ce5d01e0f33f41dbb39035db9745962f1f968", - startBlock: 8579988, + address: "0x99e517db3db5ec5424367b8b50cd11ddcb0008f1", + startBlock: 9374773, }, UniversalRegistrarRenewalWithReferrer: { abi: root_UniversalRegistrarRenewalWithReferrer, @@ -94,34 +95,34 @@ export default { }, NameWrapper: { abi: root_NameWrapper, - address: "0x0635513f179d50a207757e05759cbd106d7dfce8", - startBlock: 3790153, + address: "0xca7e6d0ddc5f373197bbe6fc2f09c2314399f028", + startBlock: 9374764, }, UniversalResolver: { abi: root_UniversalResolver, - address: "0xb7b7dadf4d42a08b3ec1d3a1079959dfbc8cffcc", - startBlock: 8515717, + address: "0x198827b2316e020c48b500fc3cebdbcaf58787ce", + startBlock: 9374794, }, // ETHRegistry: { abi: Registry, - address: "0x1291be112d480055dafd8a610b7d1e203891c274", - startBlock: 9629999, + address: "0x89db31efa19c29c2510db56d8c213b3f960ca256", + startBlock: 9685062, }, RootRegistry: { abi: Registry, - address: "0x8a791620dd6260079bf849dc5567adc3f2fdc318", - startBlock: 9629999, + address: "0x52c3eec93cb33451985c29c1e3f80a40ab071360", + startBlock: 9684796, }, Registry: { abi: Registry, - startBlock: 9629999, + startBlock: 9684796, }, EnhancedAccessControl: { abi: EnhancedAccessControl, - startBlock: 9629999, + startBlock: 9684796, }, }, }, @@ -131,137 +132,25 @@ export default { contracts: { Resolver: { abi: ResolverABI, - startBlock: 9629999, - }, - Registry: { - abi: Registry, - startBlock: 9629999, - }, - EnhancedAccessControl: { - abi: EnhancedAccessControl, - startBlock: 9629999, + startBlock: 9374708, // temporary: match same-network Resolver in ENSRoot above }, ETHRegistry: { abi: Registry, - address: "0x5fc8d32690cc91d4c39d9d3abcbd16989f875707", - startBlock: 9629999, + address: "0x0f3eb298470639a96bd548cea4a648bc80b2cee2", + startBlock: 9683977, }, ETHRegistrar: { abi: ETHRegistrar, - address: "0xa513e6e4b8f2a923d98304ec87f64353c4d5c853", - startBlock: 9629999, + address: "0x774faadcd7e8c4b7441aa2927f10845fea083ea1", + startBlock: 9374809, }, - }, - }, - - /** - * Basenames Datasource - * - * Addresses and Start Blocks from Basenames - * https://github.com/base-org/basenames - */ - [DatasourceNames.Basenames]: { - /** - * As of 5-Jun-2025 the Resolver for 'basetest.eth' in the Sepolia ENS namespace is - * 0x084D10C07EfEecD9fFc73DEb38ecb72f9eEb65aB. - * - * This Resolver uses ENSIP-10 (Wildcard Resolution) and EIP-3668 (CCIP Read) to delegate - * the forward resolution of data associated with subnames of 'basetest.eth' to an offchain - * gateway server operated by Coinbase that uses the following subregistry contracts on - * Base Sepolia as its source of truth. - * - * The owner of 'basetest.eth' in the ENS Registry on the Sepolia ENS namespace - * (e.g. Coinbase) has the ability to change this configuration at any time. - * - * See the reference documentation for additional context: - * docs/ensnode/src/content/docs/reference/mainnet-registered-subnames-of-subregistries.mdx - */ - chain: baseSepolia, - contracts: { Registry: { - abi: base_Registry, - address: "0x1493b2567056c2181630115660963e13a8e32735", - startBlock: 13012458, - }, - Resolver: { - abi: ResolverABI, - startBlock: 13012458, - }, - BaseRegistrar: { - abi: base_BaseRegistrar, - address: "0xa0c70ec36c010b55e3c434d6c6ebeec50c705794", - startBlock: 13012465, - }, - EARegistrarController: { - abi: base_EARegistrarController, - address: "0x3a0e8c2a0a28f396a5e5b69edb2e630311f1517a", - startBlock: 13041164, - }, - RegistrarController: { - abi: base_RegistrarController, - address: "0x49ae3cc2e3aa768b1e5654f5d3c6002144a59581", - startBlock: 13298580, - }, - /** - * This controller was added to BaseRegistrar contract - * with the following tx: - * https://sepolia.basescan.org/tx/0x648d984c1a379a6c300851b9561fe98a9b5282a26ca8c2c7660b11c53f0564bc - */ - UpgradeableRegistrarController: { - abi: base_UpgradeableRegistrarController, - address: "0x82c858cdf64b3d893fe54962680edfddc37e94c8", // a proxy contract - startBlock: 29896051, - }, - }, - }, - - /** - * Lineanames Datasource - * - * Addresses and Start Blocks from Lineanames - * https://github.com/Consensys/linea-ens - */ - [DatasourceNames.Lineanames]: { - /** - * As of 5-Jun-2025 the Resolver for 'linea-sepolia.eth' in the Sepolia ENS namespace is - * 0x64884ED06241c059497aEdB2C7A44CcaE6bc7937. - * - * This Resolver uses ENSIP-10 (Wildcard Resolution) and EIP-3668 (CCIP Read) to delegate - * the forward resolution of data associated with subnames of 'linea-sepolia.eth' to an offchain - * gateway server operated by Consensys that uses the following subregistry contracts on - * Linea Sepolia as its source of truth. - * - * The owner of 'linea-sepolia.eth' in the ENS Registry on the Sepolia ENS namespace - * (e.g. Consensys) has the ability to change this configuration at any time. - * - * See the reference documentation for additional context: - * docs/ensnode/src/content/docs/reference/mainnet-registered-subnames-of-subregistries.mdx - */ - chain: lineaSepolia, - contracts: { - Registry: { - abi: linea_Registry, - address: "0x5b2636f0f2137b4ae722c01dd5122d7d3e9541f7", - startBlock: 2395094, - }, - Resolver: { - abi: ResolverABI, - startBlock: 2395094, // based on startBlock of Registry on Linea Sepolia - }, - BaseRegistrar: { - abi: linea_BaseRegistrar, - address: "0x83475a84c0ea834f06c8e636a62631e7d2e07a44", - startBlock: 2395099, - }, - EthRegistrarController: { - abi: linea_EthRegistrarController, - address: "0x0f81e3b3a32dfe1b8a08d3c0061d852337a09338", - startBlock: 2395231, + abi: Registry, + startBlock: 9374809, }, - NameWrapper: { - abi: linea_NameWrapper, - address: "0xf127de9e039a789806fed4c6b1c0f3affea9425e", - startBlock: 2395202, + EnhancedAccessControl: { + abi: EnhancedAccessControl, + startBlock: 9374809, }, }, }, @@ -274,145 +163,55 @@ export default { contracts: { DefaultReverseRegistrar: { abi: StandaloneReverseRegistrar, - address: "0x4f382928805ba0e23b30cfb75fc9e848e82dfd47", - startBlock: 8579966, + address: "0xf7fca8d7b8b802d07a1011b69a5e39395197b730", + startBlock: 9374772, }, - DefaultReverseResolver1: { - abi: ResolverABI, - address: "0x8fade66b79cc9f707ab26799354482eb93a5b7dd", - startBlock: 3790251, - }, - DefaultReverseResolver2: { - abi: ResolverABI, - address: "0x8948458626811dd0c23eb25cc74291247077cc51", - startBlock: 7035086, - }, DefaultReverseResolver3: { abi: ResolverABI, - address: "0x9dc60e7bd81ccc96774c55214ff389d42ae5e9ac", - startBlock: 8580041, + address: "0xa238d3aca667210d272391a119125d38816af4b1", + startBlock: 9374791, }, - DefaultPublicResolver1: { - abi: ResolverABI, - address: "0x8fade66b79cc9f707ab26799354482eb93a5b7dd", - startBlock: 3790251, - }, DefaultPublicResolver2: { abi: ResolverABI, - address: "0x8948458626811dd0c23eb25cc74291247077cc51", - startBlock: 7035086, + address: "0x9c97031854a11e41289a33e2fa5749c468c08820", + startBlock: 9374783, }, DefaultPublicResolver3: { abi: ResolverABI, - address: "0xe99638b40e4fff0129d56f03b55b6bbc4bbe49b5", - startBlock: 8580001, + address: "0x0e14ee0592da66bb4c8a8090066bc8a5af15f3e6", + startBlock: 9374784, }, BaseReverseResolver: { abi: ResolverABI, - // https://adraffy.github.io/ens-normalize.js/test/resolver.html?sepolia#80014a34.reverse - address: "0xaf3b3f636be80b6709f5bd3a374d6ac0d0a7c7aa", - startBlock: 8580004, + address: "0xf849bc9d818ac09a629ae981b03bcbcdca750e8f", + startBlock: 9374708, }, LineaReverseResolver: { abi: ResolverABI, - // https://adraffy.github.io/ens-normalize.js/test/resolver.html?sepolia#8000e705.reverse - address: "0x083da1dbc0f379ccda6ac81a934207c3d8a8a205", - startBlock: 8580005, + address: "0xc8e393f59be1ec4d44ea9190e6831d3c4a94dfa7", + startBlock: 9374708, }, OptimismReverseResolver: { abi: ResolverABI, - // https://adraffy.github.io/ens-normalize.js/test/resolver.html?sepolia#80aa37dc.reverse - address: "0xc9ae189772bd48e01410ab3be933637ee9d3aa5f", - startBlock: 8580026, + address: "0x05e889ba6c7a2399ea9ce4e9666f1e863b0f1728", + startBlock: 9374708, }, ArbitrumReverseResolver: { abi: ResolverABI, - // https://adraffy.github.io/ens-normalize.js/test/resolver.html?sepolia#80066eee.reverse - address: "0x926f94d2adc77c86cb0050892097d49aadd02e8b", - startBlock: 8580003, + address: "0x18b9b7158c16194b6d4c4fde85de92b035a3ce77", + startBlock: 9374708, }, ScrollReverseResolver: { abi: ResolverABI, - // https://adraffy.github.io/ens-normalize.js/test/resolver.html?sepolia#8008274f.reverse - address: "0x9fa59673e43f15bdb8722fdaf5c2107574b99062", - startBlock: 8580040, - }, - }, - }, - - /** - * Contracts that power Reverse Resolution on Base Sepolia. - */ - [DatasourceNames.ReverseResolverBase]: { - chain: baseSepolia, - contracts: { - L2ReverseRegistrar: { - abi: ResolverABI, - address: "0x00000beef055f7934784d6d81b6bc86665630dba", - startBlock: 21788010, - }, - }, - }, - - /** - * Contracts that power Reverse Resolution on Optimism Sepolia. - */ - [DatasourceNames.ReverseResolverOptimism]: { - chain: optimismSepolia, - contracts: { - L2ReverseRegistrar: { - abi: ResolverABI, - address: "0x00000beef055f7934784d6d81b6bc86665630dba", - startBlock: 23770766, - }, - }, - }, - - /** - * Contracts that power Reverse Resolution on Arbitrum Sepolia. - */ - [DatasourceNames.ReverseResolverArbitrum]: { - chain: arbitrumSepolia, - contracts: { - L2ReverseRegistrar: { - abi: ResolverABI, - address: "0x00000beef055f7934784d6d81b6bc86665630dba", - startBlock: 123142726, - }, - }, - }, - - /** - * Contracts that power Reverse Resolution on Scroll Sepolia. - */ - [DatasourceNames.ReverseResolverScroll]: { - chain: scrollSepolia, - contracts: { - L2ReverseRegistrar: { - abi: ResolverABI, - address: "0x00000beef055f7934784d6d81b6bc86665630dba", - startBlock: 8175276, - }, - }, - }, - - /** - * Contracts that power Reverse Resolution on Linea Sepolia. - */ - [DatasourceNames.ReverseResolverLinea]: { - chain: lineaSepolia, - contracts: { - L2ReverseRegistrar: { - abi: ResolverABI, - address: "0x00000beef055f7934784d6d81b6bc86665630dba", - startBlock: 9267966, + address: "0xd854f312888d0a5d64b646932a2ed8e8bad8de87", + startBlock: 9374708, }, }, }, From c7d397c0239ba4ec2f0bd5f0f3dba1470d468fdb Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 8 Dec 2025 12:22:57 -0600 Subject: [PATCH 069/102] checkpoint --- apps/ensapi/src/index.ts | 2 - .../resolver-db-helpers.ts | 28 ---- .../handlers/ensv2/EnhancedAccessControl.ts | 125 ++++-------------- .../handlers/Resolver.ts | 24 +--- .../src/abis/ensv2/EnhancedAccessControl.ts | 53 ++------ .../datasources/src/abis/shared/Ownable.ts | 34 ----- packages/datasources/src/lib/ResolverABI.ts | 9 +- packages/datasources/src/sepolia.ts | 21 +-- .../schemas/protocol-acceleration.schema.ts | 9 -- packages/ensnode-sdk/src/shared/cache.test.ts | 2 +- 10 files changed, 40 insertions(+), 267 deletions(-) delete mode 100644 packages/datasources/src/abis/shared/Ownable.ts diff --git a/apps/ensapi/src/index.ts b/apps/ensapi/src/index.ts index 48f97b7c2..c41abf3ea 100644 --- a/apps/ensapi/src/index.ts +++ b/apps/ensapi/src/index.ts @@ -5,8 +5,6 @@ import { serve } from "@hono/node-server"; import { otel } from "@hono/otel"; import { cors } from "hono/cors"; -import { prettyPrintJson } from "@ensnode/ensnode-sdk/internal"; - import { redactEnsApiConfig } from "@/config/redact"; import { errorResponse } from "@/lib/handlers/error-response"; import { factory } from "@/lib/hono-factory"; diff --git a/apps/ensindexer/src/lib/protocol-acceleration/resolver-db-helpers.ts b/apps/ensindexer/src/lib/protocol-acceleration/resolver-db-helpers.ts index d64e7fc9d..3af940e85 100644 --- a/apps/ensindexer/src/lib/protocol-acceleration/resolver-db-helpers.ts +++ b/apps/ensindexer/src/lib/protocol-acceleration/resolver-db-helpers.ts @@ -2,11 +2,9 @@ import type { Context } from "ponder:registry"; import schema from "ponder:schema"; import type { Address } from "viem"; -import { ResolverABI } from "@ensnode/datasources"; import { type AccountId, type CoinType, - interpretAddress, makeResolverId, makeResolverRecordsId, type Node, @@ -78,22 +76,10 @@ export async function ensureResolver(context: Context, resolver: AccountId) { ? staticResolverImplementsAddressRecordDefaulting(resolver) : null; - // TODO: remove this in favor of EAC - const ownerId: Address | null = null; - // try { - // const rawOwner = await context.client.readContract({ - // address: resolver.address, - // abi: ResolverABI, - // functionName: "owner", - // }); - // ownerId = interpretAddress(rawOwner); - // } catch {} - // ensure Resolver await context.db.insert(schema.resolver).values({ id: resolverId, ...resolver, - ownerId, isExtended, isDedicated, isStatic, @@ -209,17 +195,3 @@ export async function handleResolverTextRecordUpdate( .onConflictDoUpdate({ value: interpretedValue }); } } - -/** - * Updates the resolver's `owner`, interpreting zeroAddress as null. - */ -export async function handleResolverOwnerUpdate( - context: Context, - resolver: AccountId, - owner: Address, -) { - // upsert owner, interpreting zeroAddress as null - await context.db - .update(schema.resolver, { id: makeResolverId(resolver) }) - .set({ ownerId: interpretAddress(owner) }); -} diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/EnhancedAccessControl.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/EnhancedAccessControl.ts index 074f947eb..5f4f8fe05 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/EnhancedAccessControl.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/EnhancedAccessControl.ts @@ -19,14 +19,6 @@ import type { EventWithArgs } from "@/lib/ponder-helpers"; */ type PermissionsCompositeKey = Pick; -/** - * Infer the type of the PermissionsUsers entity's composite key. - */ -type PermissionsUsersCompositeKey = Pick< - typeof schema.permissionsUser.$inferInsert, - "chainId" | "address" | "resource" | "user" ->; - const ensurePermissionsResource = async ( context: Context, contract: PermissionsCompositeKey, @@ -50,60 +42,9 @@ const ensurePermissionsResource = async ( const isZeroRoles = (roles: bigint) => roles === 0n; -async function upsertNewRoles(context: Context, key: PermissionsUsersCompositeKey, roles: bigint) { - const permissionsUserId = makePermissionsUserId( - { chainId: key.chainId, address: key.address }, - key.resource, - key.user, - ); - - if (isZeroRoles(roles)) { - // ensure deleted - await context.db.delete(schema.permissionsUser, { id: permissionsUserId }); - } else { - // ensure upserted - await context.db - .insert(schema.permissionsUser) - .values({ id: permissionsUserId, ...key, roles }) - .onConflictDoUpdate({ roles }); - } -} - export default function () { ponder.on( - namespaceContract(PluginName.ENSv2, "EnhancedAccessControl:EACRolesGranted"), - async ({ - context, - event, - }: { - context: Context; - event: EventWithArgs<{ - resource: bigint; - roleBitmap: bigint; - account: Address; - }>; - }) => { - const { resource, roleBitmap: roles, account: user } = event.args; - - // ignore roles for zeroAddress - if (isAddressEqual(zeroAddress, user)) return; - - await ensureAccount(context, user); - - const accountId = getThisAccountId(context, event); - const permissionsUserId = makePermissionsUserId(accountId, resource, user); - - await ensurePermissionsResource(context, accountId, resource); - const existing = await context.db.find(schema.permissionsUser, { id: permissionsUserId }); - - // https://github.com/ensdomains/namechain/blob/main/contracts/src/common/access-control/EnhancedAccessControl.sol#L292 - const newRoles = (existing?.roles ?? 0n) | roles; - await upsertNewRoles(context, { ...accountId, resource, user }, newRoles); - }, - ); - - ponder.on( - namespaceContract(PluginName.ENSv2, "EnhancedAccessControl:EACRolesRevoked"), + namespaceContract(PluginName.ENSv2, "EnhancedAccessControl:EACRolesChanged"), async ({ context, event, @@ -111,52 +52,38 @@ export default function () { context: Context; event: EventWithArgs<{ resource: bigint; - roleBitmap: bigint; account: Address; + oldRoleBitmap: bigint; + newRoleBitmap: bigint; }>; }) => { - const { resource, roleBitmap: roles, account: user } = event.args; - - // ignore roles for zeroAddress - if (isAddressEqual(zeroAddress, user)) return; - - await ensureAccount(context, user); + // biome-ignore lint/correctness/noUnusedVariables: TODO: use oldRoleBitmap at all? + const { resource, account: user, oldRoleBitmap, newRoleBitmap } = event.args; - const accountId = getThisAccountId(context, event); - const permissionsUserId = makePermissionsUserId(accountId, resource, user); - - await ensurePermissionsResource(context, accountId, resource); - const existing = await context.db.find(schema.permissionsUser, { id: permissionsUserId }); - - // https://github.com/ensdomains/namechain/blob/main/contracts/src/common/access-control/EnhancedAccessControl.sol#L325 - const newRoles = (existing?.roles ?? 0n) & ~roles; - await upsertNewRoles(context, { ...accountId, resource, user }, newRoles); - }, - ); - - ponder.on( - namespaceContract(PluginName.ENSv2, "EnhancedAccessControl:EACAllRolesRevoked"), - async ({ - context, - event, - }: { - context: Context; - event: EventWithArgs<{ - resource: bigint; - account: Address; - }>; - }) => { - const { resource, account: user } = event.args; + // Invariant: EAC reverts EACInvalidAccount if account === zeroAddress + if (isAddressEqual(zeroAddress, user)) { + throw new Error( + `Invariant(EnhancedAccessControl:EACRolesChanged): EACRolesChanged emitted for zeroAddress, should have reverted.`, + ); + } - // ignore roles for zeroAddress - if (isAddressEqual(zeroAddress, user)) return; + const contract = getThisAccountId(context, event); + const permissionsUserId = makePermissionsUserId(contract, resource, user); await ensureAccount(context, user); - - const accountId = getThisAccountId(context, event); - await ensurePermissionsResource(context, accountId, resource); - - await upsertNewRoles(context, { ...accountId, resource, user }, 0n); + await ensurePermissionsResource(context, contract, resource); + + const roles = newRoleBitmap; + if (isZeroRoles(roles)) { + // ensure deleted + await context.db.delete(schema.permissionsUser, { id: permissionsUserId }); + } else { + // ensure upserted + await context.db + .insert(schema.permissionsUser) + .values({ id: permissionsUserId, ...contract, resource, user, roles }) + .onConflictDoUpdate({ roles }); + } }, ); } diff --git a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Resolver.ts b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Resolver.ts index e3b05d518..dd8d9bea3 100644 --- a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Resolver.ts +++ b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/Resolver.ts @@ -1,13 +1,6 @@ import { ponder } from "ponder:registry"; -import schema from "ponder:schema"; -import { - bigintToCoinType, - type CoinType, - ETH_COIN_TYPE, - makeResolverId, - PluginName, -} from "@ensnode/ensnode-sdk"; +import { bigintToCoinType, type CoinType, ETH_COIN_TYPE, PluginName } from "@ensnode/ensnode-sdk"; import { parseDnsTxtRecordArgs } from "@/lib/dns-helpers"; import { getThisAccountId } from "@/lib/get-this-account-id"; @@ -17,7 +10,6 @@ import { ensureResolverRecords, handleResolverAddressRecordUpdate, handleResolverNameUpdate, - handleResolverOwnerUpdate, handleResolverTextRecordUpdate, makeResolverRecordsCompositeKey, } from "@/lib/protocol-acceleration/resolver-db-helpers"; @@ -182,18 +174,4 @@ export default function () { await handleResolverTextRecordUpdate(context, resolverRecordsKey, key, null); }, ); - - ponder.on( - namespaceContract(pluginName, "Resolver:OwnershipTransferred"), - async ({ context, event }) => { - // ignore OwnershipTransferred events that are not about Resolvers we're aware of - const resolver = getThisAccountId(context, event); - const resolverId = makeResolverId(resolver); - const existing = await context.db.find(schema.resolver, { id: resolverId }); - if (!existing) return; - - const { newOwner } = event.args; - await handleResolverOwnerUpdate(context, resolver, newOwner); - }, - ); } diff --git a/packages/datasources/src/abis/ensv2/EnhancedAccessControl.ts b/packages/datasources/src/abis/ensv2/EnhancedAccessControl.ts index db90a209a..e4753b00c 100644 --- a/packages/datasources/src/abis/ensv2/EnhancedAccessControl.ts +++ b/packages/datasources/src/abis/ensv2/EnhancedAccessControl.ts @@ -41,6 +41,11 @@ export const EnhancedAccessControl = [ name: "EACCannotRevokeRoles", type: "error", }, + { + inputs: [], + name: "EACInvalidAccount", + type: "error", + }, { inputs: [ { @@ -114,69 +119,31 @@ export const EnhancedAccessControl = [ anonymous: false, inputs: [ { - indexed: false, - internalType: "uint256", - name: "resource", - type: "uint256", - }, - { - indexed: false, - internalType: "address", - name: "account", - type: "address", - }, - ], - name: "EACAllRolesRevoked", - type: "event", - }, - { - anonymous: false, - inputs: [ - { - indexed: false, + indexed: true, internalType: "uint256", name: "resource", type: "uint256", }, { - indexed: false, - internalType: "uint256", - name: "roleBitmap", - type: "uint256", - }, - { - indexed: false, + indexed: true, internalType: "address", name: "account", type: "address", }, - ], - name: "EACRolesGranted", - type: "event", - }, - { - anonymous: false, - inputs: [ { indexed: false, internalType: "uint256", - name: "resource", + name: "oldRoleBitmap", type: "uint256", }, { indexed: false, internalType: "uint256", - name: "roleBitmap", + name: "newRoleBitmap", type: "uint256", }, - { - indexed: false, - internalType: "address", - name: "account", - type: "address", - }, ], - name: "EACRolesRevoked", + name: "EACRolesChanged", type: "event", }, { diff --git a/packages/datasources/src/abis/shared/Ownable.ts b/packages/datasources/src/abis/shared/Ownable.ts deleted file mode 100644 index 74d613369..000000000 --- a/packages/datasources/src/abis/shared/Ownable.ts +++ /dev/null @@ -1,34 +0,0 @@ -export const Ownable = [ - { - type: "event", - name: "OwnershipTransferred", - inputs: [ - { - name: "previousOwner", - type: "address", - indexed: true, - internalType: "address", - }, - { - name: "newOwner", - type: "address", - indexed: true, - internalType: "address", - }, - ], - anonymous: false, - }, - { - type: "function", - name: "owner", - inputs: [], - outputs: [ - { - name: "", - type: "address", - internalType: "address", - }, - ], - stateMutability: "view", - }, -] as const; diff --git a/packages/datasources/src/lib/ResolverABI.ts b/packages/datasources/src/lib/ResolverABI.ts index b21613648..edf7ccab9 100644 --- a/packages/datasources/src/lib/ResolverABI.ts +++ b/packages/datasources/src/lib/ResolverABI.ts @@ -1,8 +1,6 @@ import { mergeAbis } from "@ponder/utils"; -// import { EnhancedAccessControl } from "../abis/ensv2/EnhancedAccessControl"; import { LegacyPublicResolver } from "../abis/shared/LegacyPublicResolver"; -import { Ownable } from "../abis/shared/Ownable"; import { Resolver } from "../abis/shared/Resolver"; /** @@ -17,9 +15,4 @@ import { Resolver } from "../abis/shared/Resolver"; * A Resolver contract is a contract that emits _any_ (not _all_) of the events specified here and * may or may not support any number of the methods available in this ABI. */ -export const ResolverABI = mergeAbis([ - LegacyPublicResolver, - Resolver, - Ownable, - // EnhancedAccessControl, // TODO: mmove this to EAC -]); +export const ResolverABI = mergeAbis([LegacyPublicResolver, Resolver]); diff --git a/packages/datasources/src/sepolia.ts b/packages/datasources/src/sepolia.ts index 891321edd..d8017147c 100644 --- a/packages/datasources/src/sepolia.ts +++ b/packages/datasources/src/sepolia.ts @@ -1,28 +1,9 @@ -import { zeroAddress } from "viem"; -import { - arbitrumSepolia, - baseSepolia, - lineaSepolia, - optimismSepolia, - scrollSepolia, - sepolia, -} from "viem/chains"; +import { sepolia } from "viem/chains"; -// ABIs for Basenames Datasource -import { BaseRegistrar as base_BaseRegistrar } from "./abis/basenames/BaseRegistrar"; -import { EarlyAccessRegistrarController as base_EARegistrarController } from "./abis/basenames/EARegistrarController"; -import { RegistrarController as base_RegistrarController } from "./abis/basenames/RegistrarController"; -import { Registry as base_Registry } from "./abis/basenames/Registry"; -import { UpgradeableRegistrarController as base_UpgradeableRegistrarController } from "./abis/basenames/UpgradeableRegistrarController"; // ABIs for Namechain import { EnhancedAccessControl } from "./abis/ensv2/EnhancedAccessControl"; import { ETHRegistrar } from "./abis/ensv2/ETHRegistrar"; import { Registry } from "./abis/ensv2/Registry"; -// ABIs for Lineanames Datasource -import { BaseRegistrar as linea_BaseRegistrar } from "./abis/lineanames/BaseRegistrar"; -import { EthRegistrarController as linea_EthRegistrarController } from "./abis/lineanames/EthRegistrarController"; -import { NameWrapper as linea_NameWrapper } from "./abis/lineanames/NameWrapper"; -import { Registry as linea_Registry } from "./abis/lineanames/Registry"; // ABIs for ENSRoot Datasource import { BaseRegistrar as root_BaseRegistrar } from "./abis/root/BaseRegistrar"; import { LegacyEthRegistrarController as root_LegacyEthRegistrarController } from "./abis/root/LegacyEthRegistrarController"; diff --git a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts index bfb263950..e37bb5b1f 100644 --- a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts +++ b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts @@ -91,15 +91,6 @@ export const resolver = onchainTable( chainId: t.integer().notNull().$type(), address: t.hex().notNull().$type
(), - /** - * A Resolver may have an `owner` via Ownable. - * - * TODO: move this to EAC - * - * Mainly relevant for DedicatedResolvers. - */ - ownerId: t.hex().$type
(), - /** * Whether the Resolver implements IExtendedResolver. */ diff --git a/packages/ensnode-sdk/src/shared/cache.test.ts b/packages/ensnode-sdk/src/shared/cache.test.ts index 9530bc8a1..69dee3f84 100644 --- a/packages/ensnode-sdk/src/shared/cache.test.ts +++ b/packages/ensnode-sdk/src/shared/cache.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { LruCache, SWRCache, TtlCache } from "./cache"; +import { LruCache, TtlCache } from "./cache"; describe("LruCache", () => { it("throws Error if capacity is not an integer", () => { From efdef663d308e116f47e0450f2ae3eeba6729792 Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 8 Dec 2025 13:09:22 -0600 Subject: [PATCH 070/102] fix internal circ dep --- apps/ensapi/src/graphql-api/schema/account.ts | 23 +------------------ .../ensapi/src/graphql-api/schema/resolver.ts | 15 ++++++------ apps/ensindexer/src/config/validations.ts | 5 +--- .../src/api/indexing-status/zod-schemas.ts | 2 +- .../src/api/registrar-actions/zod-schemas.ts | 3 ++- .../src/api/shared/pagination/zod-schemas.ts | 5 +++- .../src/ensindexer/config/validations.ts | 2 +- .../src/ensindexer/config/zod-schemas.ts | 2 +- packages/ensnode-sdk/src/internal.ts | 2 ++ .../src/shared/config/validatons.ts | 2 +- .../ensnode-sdk/src/shared/zod-schemas.ts | 5 ---- packages/ensnode-sdk/src/shared/zod-types.ts | 6 +++++ 12 files changed, 27 insertions(+), 45 deletions(-) create mode 100644 packages/ensnode-sdk/src/shared/zod-types.ts diff --git a/apps/ensapi/src/graphql-api/schema/account.ts b/apps/ensapi/src/graphql-api/schema/account.ts index a578976ee..b88866ba9 100644 --- a/apps/ensapi/src/graphql-api/schema/account.ts +++ b/apps/ensapi/src/graphql-api/schema/account.ts @@ -142,27 +142,6 @@ AccountRef.implement({ ////////////////////////////// // Account.dedicatedResolvers ////////////////////////////// - dedicatedResolvers: t.connection({ - description: "TODO", - type: ResolverRef, - resolve: (parent, args) => - // TODO(EAC) — migrate to Permissions lookup - resolveCursorConnection( - { ...DEFAULT_CONNECTION_ARGS, args }, - ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => - db.query.resolver.findMany({ - where: (t, { lt, gt, and, eq }) => - and( - ...[ - eq(t.ownerId, parent.id), - before !== undefined && lt(t.id, cursors.decode(before)), - after !== undefined && gt(t.id, cursors.decode(after)), - ].filter((c) => !!c), - ), - orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), - limit, - }), - ), - }), + // TODO: account's dedicated resolvers via EAC }), }); diff --git a/apps/ensapi/src/graphql-api/schema/resolver.ts b/apps/ensapi/src/graphql-api/schema/resolver.ts index 5e4b81d09..0ab1c1554 100644 --- a/apps/ensapi/src/graphql-api/schema/resolver.ts +++ b/apps/ensapi/src/graphql-api/schema/resolver.ts @@ -11,7 +11,6 @@ import { import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-model-id"; -import { AccountRef } from "@/graphql-api/schema/account"; import { AccountIdInput, AccountIdRef } from "@/graphql-api/schema/account-id"; import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; import { cursors } from "@/graphql-api/schema/cursors"; @@ -148,13 +147,13 @@ DedicatedResolverMetadataRef.implement({ /////////////////////////// // DedicatedResolver.owner /////////////////////////// - owner: t.field({ - description: "TODO", - type: AccountRef, - nullable: true, - // TODO: resolve via EAC - resolve: (parent) => parent.ownerId, - }), + // owner: t.field({ + // description: "TODO", + // type: AccountRef, + // nullable: true, + // // TODO: resolve via EAC + // resolve: (parent) => parent.ownerId, + // }), ///////////////////////////////// // DedicatedResolver.permissions diff --git a/apps/ensindexer/src/config/validations.ts b/apps/ensindexer/src/config/validations.ts index e09af83a5..48f6f43b9 100644 --- a/apps/ensindexer/src/config/validations.ts +++ b/apps/ensindexer/src/config/validations.ts @@ -1,5 +1,4 @@ import { type Address, isAddress } from "viem"; -import type { z } from "zod/v4"; import { type DatasourceName, @@ -8,14 +7,12 @@ import { maybeGetDatasource, } from "@ensnode/datasources"; import { asLowerCaseAddress, PluginName, uniq } from "@ensnode/ensnode-sdk"; +import type { ZodCheckFnInput } from "@ensnode/ensnode-sdk/internal"; import { getPlugin } from "@/plugins"; import type { ENSIndexerConfig } from "./types"; -// type alias to highlight the input param of Zod's check() method -type ZodCheckFnInput = z.core.ParsePayload; - // Invariant: specified plugins' datasources are available in the specified namespace's Datasources export function invariant_requiredDatasources( ctx: ZodCheckFnInput>, diff --git a/packages/ensnode-sdk/src/api/indexing-status/zod-schemas.ts b/packages/ensnode-sdk/src/api/indexing-status/zod-schemas.ts index 5786f1dd1..4585a7123 100644 --- a/packages/ensnode-sdk/src/api/indexing-status/zod-schemas.ts +++ b/packages/ensnode-sdk/src/api/indexing-status/zod-schemas.ts @@ -1,6 +1,6 @@ import z from "zod/v4"; -import { makeRealtimeIndexingStatusProjectionSchema } from "../../internal"; +import { makeRealtimeIndexingStatusProjectionSchema } from "../../ensindexer/indexing-status/zod-schemas"; import { type IndexingStatusResponse, IndexingStatusResponseCodes, diff --git a/packages/ensnode-sdk/src/api/registrar-actions/zod-schemas.ts b/packages/ensnode-sdk/src/api/registrar-actions/zod-schemas.ts index 87c314d6c..6d76e827f 100644 --- a/packages/ensnode-sdk/src/api/registrar-actions/zod-schemas.ts +++ b/packages/ensnode-sdk/src/api/registrar-actions/zod-schemas.ts @@ -2,7 +2,8 @@ import { namehash } from "viem/ens"; import z from "zod/v4"; import type { ParsePayload } from "zod/v4/core"; -import { makeRegistrarActionSchema, makeReinterpretedNameSchema } from "../../internal"; +import { makeRegistrarActionSchema } from "../../registrars/zod-schemas"; +import { makeReinterpretedNameSchema } from "../../shared/zod-schemas"; import { ErrorResponseSchema } from "../shared/errors/zod-schemas"; import { type NamedRegistrarAction, RegistrarActionsResponseCodes } from "./response"; diff --git a/packages/ensnode-sdk/src/api/shared/pagination/zod-schemas.ts b/packages/ensnode-sdk/src/api/shared/pagination/zod-schemas.ts index c80811b8b..0beac53f5 100644 --- a/packages/ensnode-sdk/src/api/shared/pagination/zod-schemas.ts +++ b/packages/ensnode-sdk/src/api/shared/pagination/zod-schemas.ts @@ -1,7 +1,10 @@ import z from "zod/v4"; import type { ParsePayload } from "zod/v4/core"; -import { makeNonNegativeIntegerSchema, makePositiveIntegerSchema } from "../../../internal"; +import { + makeNonNegativeIntegerSchema, + makePositiveIntegerSchema, +} from "../../../shared/zod-schemas"; import { RECORDS_PER_PAGE_MAX, RequestPageParams } from "./request"; import { ResponsePageContext, diff --git a/packages/ensnode-sdk/src/ensindexer/config/validations.ts b/packages/ensnode-sdk/src/ensindexer/config/validations.ts index 5c3809f3d..cc7567529 100644 --- a/packages/ensnode-sdk/src/ensindexer/config/validations.ts +++ b/packages/ensnode-sdk/src/ensindexer/config/validations.ts @@ -1,4 +1,4 @@ -import type { ZodCheckFnInput } from "../../shared/zod-schemas"; +import type { ZodCheckFnInput } from "../../shared/zod-types"; import type { ENSIndexerVersionInfo } from "./types"; /** diff --git a/packages/ensnode-sdk/src/ensindexer/config/zod-schemas.ts b/packages/ensnode-sdk/src/ensindexer/config/zod-schemas.ts index b8dc3cd6e..21625b91f 100644 --- a/packages/ensnode-sdk/src/ensindexer/config/zod-schemas.ts +++ b/packages/ensnode-sdk/src/ensindexer/config/zod-schemas.ts @@ -14,8 +14,8 @@ import { makeENSNamespaceIdSchema, makeNonNegativeIntegerSchema, makePositiveIntegerSchema, - type ZodCheckFnInput, } from "../../shared/zod-schemas"; +import type { ZodCheckFnInput } from "../../shared/zod-types"; import { isSubgraphCompatible } from "./is-subgraph-compatible"; import type { ENSIndexerPublicConfig } from "./types"; import { PluginName } from "./types"; diff --git a/packages/ensnode-sdk/src/internal.ts b/packages/ensnode-sdk/src/internal.ts index d7cae94cb..23e81e606 100644 --- a/packages/ensnode-sdk/src/internal.ts +++ b/packages/ensnode-sdk/src/internal.ts @@ -27,7 +27,9 @@ export * from "./shared/config/redacting"; export * from "./shared/config/rpc-configs-from-env"; export * from "./shared/config/types"; export * from "./shared/config/validatons"; +export * from "./shared/config/zod-schemas"; export * from "./shared/datasources-with-resolvers"; export * from "./shared/interpretation/interpret-record-values"; export * from "./shared/log-level"; export * from "./shared/zod-schemas"; +export * from "./shared/zod-types"; diff --git a/packages/ensnode-sdk/src/shared/config/validatons.ts b/packages/ensnode-sdk/src/shared/config/validatons.ts index 25e798236..c12805617 100644 --- a/packages/ensnode-sdk/src/shared/config/validatons.ts +++ b/packages/ensnode-sdk/src/shared/config/validatons.ts @@ -3,7 +3,7 @@ import type { z } from "zod/v4"; import { getENSRootChainId } from "@ensnode/datasources"; import { isHttpProtocol, isWebSocketProtocol } from "../url"; -import type { ZodCheckFnInput } from "../zod-schemas"; +import type { ZodCheckFnInput } from "../zod-types"; import type { ENSNamespaceSchema, RpcConfigsSchema } from "./zod-schemas"; /** diff --git a/packages/ensnode-sdk/src/shared/zod-schemas.ts b/packages/ensnode-sdk/src/shared/zod-schemas.ts index e4f1bc7ef..92be74f8a 100644 --- a/packages/ensnode-sdk/src/shared/zod-schemas.ts +++ b/packages/ensnode-sdk/src/shared/zod-schemas.ts @@ -26,11 +26,6 @@ import type { UnixTimestamp, } from "./types"; -/** - * Zod `.check()` function input. - */ -export type ZodCheckFnInput = z.core.ParsePayload; - /** * Parses a string value as a boolean. */ diff --git a/packages/ensnode-sdk/src/shared/zod-types.ts b/packages/ensnode-sdk/src/shared/zod-types.ts new file mode 100644 index 000000000..166c95a89 --- /dev/null +++ b/packages/ensnode-sdk/src/shared/zod-types.ts @@ -0,0 +1,6 @@ +import type { z } from "zod/v4"; + +/** + * Zod `.check()` function input. + */ +export type ZodCheckFnInput = z.core.ParsePayload; From 5896625254488ffd5580e97c5c3361442234fef5 Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 8 Dec 2025 13:09:47 -0600 Subject: [PATCH 071/102] fix lint --- apps/ensapi/src/graphql-api/schema/account.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/ensapi/src/graphql-api/schema/account.ts b/apps/ensapi/src/graphql-api/schema/account.ts index b88866ba9..7417b92c9 100644 --- a/apps/ensapi/src/graphql-api/schema/account.ts +++ b/apps/ensapi/src/graphql-api/schema/account.ts @@ -4,7 +4,7 @@ import { unionAll } from "drizzle-orm/pg-core"; import type { Address } from "viem"; import * as schema from "@ensnode/ensnode-schema"; -import type { PermissionsUserId, ResolverId } from "@ensnode/ensnode-sdk"; +import type { PermissionsUserId } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-model-id"; @@ -13,7 +13,6 @@ import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; import { cursors } from "@/graphql-api/schema/cursors"; import { DomainInterfaceRef } from "@/graphql-api/schema/domain"; import { PermissionsUserRef } from "@/graphql-api/schema/permissions"; -import { ResolverRef } from "@/graphql-api/schema/resolver"; import { db } from "@/lib/db"; export const AccountRef = builder.loadableObjectRef("Account", { From c99a53fa1ffc13e8c326ab51bd2a5042aa6aab98 Mon Sep 17 00:00:00 2001 From: shrugs Date: Tue, 9 Dec 2025 16:49:35 -0600 Subject: [PATCH 072/102] ENSv2 forwards all resolution api requests to the universal resolver v2 --- .vscode/launch.json | 18 + .../src/lib/resolution/forward-resolution.ts | 43 +- .../resolution/resolve-calls-and-results.ts | 1 + .../resolve-with-universal-resolver.ts | 113 +++++ .../middleware/can-accelerate.middleware.ts | 26 +- .../middleware/indexing-status.middleware.ts | 15 +- .../lib/heal-addr-reverse-subname-label.ts | 7 +- .../ensv2/handlers/ensv2/ENSv2Registry.ts | 2 +- .../src/abis/root/UniversalResolver.ts | 404 ++++++------------ .../abis/shared/AbstractReverseResolver.ts | 123 ++++++ packages/datasources/src/ens-test-env.ts | 22 +- packages/datasources/src/lib/ResolverABI.ts | 7 +- .../ensindexer/indexing-status/validations.ts | 2 +- 13 files changed, 483 insertions(+), 300 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 apps/ensapi/src/lib/resolution/resolve-with-universal-resolver.ts create mode 100644 packages/datasources/src/abis/shared/AbstractReverseResolver.ts diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..e285a5f8a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug Script: ENSApi Dev", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["run", "dev"], + "cwd": "${workspaceFolder}/apps/ensapi", + "env": { + "NODE_ENV": "development" + }, + "console": "integratedTerminal", + "outputCapture": "std" + } + ] +} diff --git a/apps/ensapi/src/lib/resolution/forward-resolution.ts b/apps/ensapi/src/lib/resolution/forward-resolution.ts index fb8421efa..a206def75 100644 --- a/apps/ensapi/src/lib/resolution/forward-resolution.ts +++ b/apps/ensapi/src/lib/resolution/forward-resolution.ts @@ -5,6 +5,7 @@ import { replaceBigInts } from "ponder"; import { namehash } from "viem"; import { normalize } from "viem/ens"; +import { DatasourceNames, getDatasource } from "@ensnode/datasources"; import { type AccountId, type ForwardResolutionArgs, @@ -15,6 +16,7 @@ import { isSelectionEmpty, makeResolverId, type Node, + PluginName, parseReverseName, type ResolverRecordsResponse, type ResolverRecordsSelection, @@ -39,12 +41,12 @@ import { interpretRawCallsAndResults, makeResolveCalls, } from "@/lib/resolution/resolve-calls-and-results"; +import { executeResolveCallsWithUniversalResolver } from "@/lib/resolution/resolve-with-universal-resolver"; import { withActiveSpanAsync, withSpanAsync } from "@/lib/tracing/auto-span"; import { addProtocolStepEvent, withProtocolStepAsync } from "@/lib/tracing/protocol-tracing"; const logger = makeLogger("forward-resolution"); const tracer = trace.getTracer("forward-resolution"); -// const metric = metrics.getMeter("forward-resolution"); // NOTE: normalize generic name to force the normalization lib to lazy-load itself (otherwise the // first trace generated here would be unusually slow) @@ -93,9 +95,8 @@ export async function resolveForward } /** - * Internal Forward Resolution implementation. - * - * NOTE: uses `chainId` parameter for internal Protocol Acceleration behavior (see recursive call below). + * Internal Forward Resolution implementation for a given `name`, beginning from the specified + * `registry`. */ async function _resolveForward( name: ForwardResolutionArgs["name"], @@ -155,13 +156,41 @@ async function _resolveForward( ); } + // create an un-cached viem#PublicClient separate from ponder's cached/logged clients + const publicClient = getPublicClient(chainId); + + //////////////////////////// + /// Temporary ENSv2 Bailout + //////////////////////////// + // TODO: re-enable protocol acceleration for ENSv2 + if (config.ensIndexerPublicConfig.plugins.includes(PluginName.ENSv2)) { + // execute each record's call against the UniversalResolver + const rawResults = await withProtocolStepAsync( + TraceableENSProtocol.ForwardResolution, + ForwardResolutionProtocolStep.ExecuteResolveCalls, + {}, + () => + executeResolveCallsWithUniversalResolver({ + name, + calls, + publicClient, + }), + ); + + span.setAttribute("rawResults", JSON.stringify(replaceBigInts(rawResults, String))); + + // additional semantic interpretation of the raw results from the chain + const results = interpretRawCallsAndResults(rawResults); + span.setAttribute("results", JSON.stringify(replaceBigInts(results, String))); + + // return record values + return makeRecordsResponseFromResolveResults(selection, results); + } + ////////////////////////////////////////////////// // 1. Identify the active resolver for the name on the specified chain. ////////////////////////////////////////////////// - // create an un-cached viem#PublicClient separate from ponder's cached/logged clients - const publicClient = getPublicClient(chainId); - const { activeName, activeResolver, requiresWildcardSupport } = await withProtocolStepAsync( TraceableENSProtocol.ForwardResolution, diff --git a/apps/ensapi/src/lib/resolution/resolve-calls-and-results.ts b/apps/ensapi/src/lib/resolution/resolve-calls-and-results.ts index 629f59146..185774953 100644 --- a/apps/ensapi/src/lib/resolution/resolve-calls-and-results.ts +++ b/apps/ensapi/src/lib/resolution/resolve-calls-and-results.ts @@ -117,6 +117,7 @@ export async function executeResolveCalls { const ResolverContract = { abi: ResolverABI, address: resolverAddress } as const; + // NOTE: automatically multicalled by viem return await Promise.all( calls.map(async (call) => { try { diff --git a/apps/ensapi/src/lib/resolution/resolve-with-universal-resolver.ts b/apps/ensapi/src/lib/resolution/resolve-with-universal-resolver.ts new file mode 100644 index 000000000..2031332a3 --- /dev/null +++ b/apps/ensapi/src/lib/resolution/resolve-with-universal-resolver.ts @@ -0,0 +1,113 @@ +import config from "@/config"; + +import { + bytesToHex, + ContractFunctionExecutionError, + ContractFunctionRevertedError, + decodeAbiParameters, + decodeErrorResult, + encodeFunctionData, + getAbiItem, + type Hex, + hexToBytes, + type PublicClient, + parseAbiItem, + size, +} from "viem"; +import { packetToBytes } from "viem/ens"; + +import { DatasourceNames, getDatasource, ResolverABI } from "@ensnode/datasources"; +import { + type DNSEncodedLiteralName, + decodeDNSEncodedLiteralName, + decodeDNSEncodedName, + literalLabelsToInterpretedName, + type Name, + type ResolverRecordsSelection, +} from "@ensnode/ensnode-sdk"; + +import type { + ResolveCalls, + ResolveCallsAndRawResults, +} from "@/lib/resolution/resolve-calls-and-results"; + +const ensroot = getDatasource(config.namespace, DatasourceNames.ENSRoot); + +/** + * Execute a set of ResolveCalls for `name` + */ +export async function executeResolveCallsWithUniversalResolver< + SELECTION extends ResolverRecordsSelection, +>({ + name, + calls, + publicClient, +}: { + name: Name; + calls: ResolveCalls; + publicClient: PublicClient; +}): Promise> { + // NOTE: automatically multicalled by viem + return await Promise.all( + calls.map(async (call) => { + try { + const encodedName = bytesToHex(packetToBytes(name)); // DNS-encode `name` for resolve() + const encodedMethod = encodeFunctionData({ abi: ResolverABI, ...call }); + + const [value] = await publicClient.readContract({ + abi: ensroot.contracts.UniversalResolver.abi, + address: ensroot.contracts.UniversalResolver.address, + functionName: "resolve", + args: [encodedName, encodedMethod], + }); + + // if resolve() returned empty bytes or reverted, coalece to null + if (size(value) === 0) { + return { call, result: null, reason: "returned empty response" }; + } + + // ENSIP-10 — resolve() always returns bytes that need to be decoded + const results = decodeAbiParameters( + getAbiItem({ abi: ResolverABI, name: call.functionName, args: call.args }).outputs, + value, + ); + + // NOTE: results is type-guaranteed to have at least 1 result (because each abi item's outputs.length >= 1) + const result = results[0]; + + return { + call, + result: result, + reason: `.resolve(${call.functionName}, ${call.args})`, + }; + } catch (error) { + // in general, reverts are expected behavior + if (error instanceof ContractFunctionExecutionError) { + if (error.cause instanceof ContractFunctionRevertedError) { + if (error.cause.data?.errorName === "ResolverError") { + const decoded = decodeErrorResult({ + abi: ResolverABI, + // biome-ignore lint/style/noNonNullAssertion: allow + data: error.cause.data!.args![0] as Hex, + }); + + const _encodedName = decoded.args[0]; + const _name = literalLabelsToInterpretedName( + decodeDNSEncodedLiteralName(_encodedName as DNSEncodedLiteralName), + ); + + console.log( + `UniversalResolver#resolve(${name}) reverted ResolverError() with internal error ${decoded.errorName} and name ${_name}`, + ); + } + } + + return { call, result: null, reason: error.shortMessage }; + } + + // otherwise, rethrow + throw error; + } + }), + ); +} diff --git a/apps/ensapi/src/middleware/can-accelerate.middleware.ts b/apps/ensapi/src/middleware/can-accelerate.middleware.ts index ea4e4e2fd..9aee7188b 100644 --- a/apps/ensapi/src/middleware/can-accelerate.middleware.ts +++ b/apps/ensapi/src/middleware/can-accelerate.middleware.ts @@ -11,6 +11,7 @@ export type CanAccelerateMiddlewareVariables = { canAccelerate: boolean }; // TODO: expand this datamodel to include 'reasons' acceleration was disabled to drive ui +let didWarnCannotAccelerateENSv2 = false; let didWarnNoProtocolAccelerationPlugin = false; let didInitialCanAccelerate = false; let prevCanAccelerate = false; @@ -28,9 +29,26 @@ export const canAccelerateMiddleware = factory.createMiddleware(async (c, next) throw new Error(`Invariant(canAccelerateMiddleware): isRealtime middleware required`); } - ///////////////////////////////////////////// + //////////////////////////// + /// Temporary ENSv2 Bailout + //////////////////////////// + // TODO: re-enable acceleration for ensv2 once implemented + if (config.ensIndexerPublicConfig.plugins.includes(PluginName.ENSv2)) { + if (!didWarnCannotAccelerateENSv2) { + logger.warn( + `ENSApi is currently unable to accelerate Resolution API requests while indexing ENSv2. Protocol Acceleration is DISABLED.`, + ); + + didWarnCannotAccelerateENSv2 = true; + } + + c.set("canAccelerate", false); + return await next(); + } + + ////////////////////////////////////////////// /// Protocol Acceleration Plugin Availability - ///////////////////////////////////////////// + ////////////////////////////////////////////// const hasProtocolAccelerationPlugin = config.ensIndexerPublicConfig.plugins.includes( PluginName.ProtocolAcceleration, @@ -45,9 +63,9 @@ export const canAccelerateMiddleware = factory.createMiddleware(async (c, next) didWarnNoProtocolAccelerationPlugin = true; } - ///////////////////////////// + ////////////////////////////// /// Can Accelerate Derivation - ///////////////////////////// + ////////////////////////////// // the Resolution API can accelerate requests if // a) ENSIndexer reports that it is within MAX_REALTIME_DISTANCE_TO_ACCELERATE of realtime, and diff --git a/apps/ensapi/src/middleware/indexing-status.middleware.ts b/apps/ensapi/src/middleware/indexing-status.middleware.ts index 4a87a1c0a..3a1c427c9 100644 --- a/apps/ensapi/src/middleware/indexing-status.middleware.ts +++ b/apps/ensapi/src/middleware/indexing-status.middleware.ts @@ -17,6 +17,8 @@ import { makeLogger } from "@/lib/logger"; const logger = makeLogger("indexing-status.middleware"); const client = new ENSNodeClient({ url: config.ensIndexerUrl }); +let hasWarnedOnFailure = false; + export const indexingStatusCache = await SWRCache.create({ fn: async () => client @@ -41,10 +43,14 @@ export const indexingStatusCache = await SWRCache.create({ // Therefore, throw an error so that this current invocation of `readCache` will: // - Reject the newly fetched response (if any) such that it won't be cached. // - Return the most recently cached value from prior invocations, or `null` if no prior invocation successfully cached a value. - logger.error( - error, - "Error occurred while fetching a new indexing status snapshot. The cached indexing status snapshot (if any) will not be updated.", - ); + if (!hasWarnedOnFailure) { + logger.error( + error, + "Error occurred while fetching a new indexing status snapshot. The cached indexing status snapshot (if any) will not be updated.", + ); + + hasWarnedOnFailure = true; + } throw error; }), ttl: 5, // 5 seconds @@ -102,7 +108,6 @@ export const indexingStatusMiddleware = factory.createMiddleware(async (c, next) const errorMessage = "Unable to generate a new indexing status projection. No indexing status snapshots have been successfully fetched and stored into cache since service startup. This may indicate the ENSIndexer service is unreachable or in an error state."; const error = new Error(errorMessage); - logger.error(error); promiseResult = await pReflect(Promise.reject(error)); } else { // An indexing status snapshot has been cached successfully. diff --git a/apps/ensindexer/src/lib/heal-addr-reverse-subname-label.ts b/apps/ensindexer/src/lib/heal-addr-reverse-subname-label.ts index 348995acc..89b46dcfa 100644 --- a/apps/ensindexer/src/lib/heal-addr-reverse-subname-label.ts +++ b/apps/ensindexer/src/lib/heal-addr-reverse-subname-label.ts @@ -72,13 +72,13 @@ export async function healAddrReverseSubnameLabel( // https://etherscan.io/tx/0x9a6a5156f9f1fc6b1d5551483b97930df32e802f2f9229b35572170f1111134d // The `debug_traceTransaction` RPC call is cached by Ponder - const traces = await context.client.request({ + const trace = await context.client.request({ method: "debug_traceTransaction", params: [event.transaction.hash, { tracer: "callTracer" }], }); // extract all addresses from the traces - const allAddressesInTransaction = getAddressesFromTrace(traces); + const allAddressesInTransaction = getAddressesFromTrace(trace); // iterate over all addresses in the transaction traces // and try to heal the label with each address @@ -87,6 +87,9 @@ export async function healAddrReverseSubnameLabel( if (healedFromTrace !== null) return healedFromTrace; } + // TODO: this trace-based derivation is failing in ens-test-env for some reason + if (config.namespace === "ens-test-env") return "whatever" as LiteralLabel; + // Invariant: by this point, we should have healed all subnames of addr.reverse throw new Error( `Invariant(healAddrReverseSubnameLabel): Unable to heal the label for subname of addr.reverse with labelHash '${labelHash}'.`, diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts index bfdca3c7d..56fa92fa1 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts @@ -2,7 +2,7 @@ import { type Context, ponder } from "ponder:registry"; import schema from "ponder:schema"; -import { type Address, hexToBigInt, labelhash } from "viem"; +import { type Address, hexToBigInt, labelhash, numberToHex } from "viem"; import { type AccountId, diff --git a/packages/datasources/src/abis/root/UniversalResolver.ts b/packages/datasources/src/abis/root/UniversalResolver.ts index 42cd9e3c8..c89ecac2e 100644 --- a/packages/datasources/src/abis/root/UniversalResolver.ts +++ b/packages/datasources/src/abis/root/UniversalResolver.ts @@ -2,14 +2,14 @@ export const UniversalResolver = [ { inputs: [ { - internalType: "contract ENS", - name: "ens", + internalType: "contract IRegistry", + name: "root", type: "address", }, { - internalType: "string[]", - name: "gateways", - type: "string[]", + internalType: "contract IGatewayProvider", + name: "batchGatewayProvider", + type: "address", }, ], stateMutability: "nonpayable", @@ -94,6 +94,22 @@ export const UniversalResolver = [ name: "OffchainLookup", type: "error", }, + { + inputs: [ + { + internalType: "uint256", + name: "offset", + type: "uint256", + }, + { + internalType: "uint256", + name: "length", + type: "uint256", + }, + ], + name: "OffsetOutOfBoundsError", + type: "error", + }, { inputs: [ { @@ -160,38 +176,26 @@ export const UniversalResolver = [ type: "error", }, { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: "address", - name: "previousOwner", - type: "address", - }, + inputs: [], + name: "ROOT_REGISTRY", + outputs: [ { - indexed: true, - internalType: "address", - name: "newOwner", + internalType: "contract IRegistry", + name: "", type: "address", }, ], - name: "OwnershipTransferred", - type: "event", + stateMutability: "view", + type: "function", }, { - inputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - name: "batchGateways", + inputs: [], + name: "batchGatewayProvider", outputs: [ { - internalType: "string", + internalType: "contract IGatewayProvider", name: "", - type: "string", + type: "address", }, ], stateMutability: "view", @@ -368,60 +372,46 @@ export const UniversalResolver = [ type: "bytes", }, ], - name: "findResolver", + name: "findRegistries", outputs: [ { - internalType: "address", + internalType: "contract IRegistry[]", name: "", - type: "address", - }, - { - internalType: "bytes32", - name: "", - type: "bytes32", - }, - { - internalType: "uint256", - name: "", - type: "uint256", + type: "address[]", }, ], stateMutability: "view", type: "function", }, { - inputs: [], - name: "owner", - outputs: [ + inputs: [ { - internalType: "address", - name: "", - type: "address", + internalType: "bytes", + name: "name", + type: "bytes", }, ], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "registry", + name: "findResolver", outputs: [ { - internalType: "contract ENS", - name: "", + internalType: "address", + name: "resolver", type: "address", }, + { + internalType: "bytes32", + name: "node", + type: "bytes32", + }, + { + internalType: "uint256", + name: "offset", + type: "uint256", + }, ], stateMutability: "view", type: "function", }, - { - inputs: [], - name: "renounceOwnership", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, { inputs: [ { @@ -518,63 +508,9 @@ export const UniversalResolver = [ { inputs: [ { - components: [ - { - internalType: "bytes", - name: "name", - type: "bytes", - }, - { - internalType: "uint256", - name: "offset", - type: "uint256", - }, - { - internalType: "bytes32", - name: "node", - type: "bytes32", - }, - { - internalType: "address", - name: "resolver", - type: "address", - }, - { - internalType: "bool", - name: "extended", - type: "bool", - }, - ], - internalType: "struct AbstractUniversalResolver.ResolverInfo", - name: "info", - type: "tuple", - }, - { - components: [ - { - internalType: "address", - name: "target", - type: "address", - }, - { - internalType: "bytes", - name: "call", - type: "bytes", - }, - { - internalType: "bytes", - name: "data", - type: "bytes", - }, - { - internalType: "uint256", - name: "flags", - type: "uint256", - }, - ], - internalType: "struct CCIPBatcher.Lookup[]", - name: "lookups", - type: "tuple[]", + internalType: "bytes", + name: "response", + type: "bytes", }, { internalType: "bytes", @@ -586,18 +522,54 @@ export const UniversalResolver = [ outputs: [ { internalType: "bytes", - name: "result", + name: "", type: "bytes", }, { internalType: "address", - name: "resolver", + name: "", type: "address", }, ], stateMutability: "pure", type: "function", }, + { + inputs: [ + { + internalType: "bytes", + name: "response", + type: "bytes", + }, + { + internalType: "bytes", + name: "extraData", + type: "bytes", + }, + ], + name: "resolveDirectCallback", + outputs: [], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes", + name: "response", + type: "bytes", + }, + { + internalType: "bytes", + name: "", + type: "bytes", + }, + ], + name: "resolveDirectCallbackError", + outputs: [], + stateMutability: "pure", + type: "function", + }, { inputs: [ { @@ -620,14 +592,48 @@ export const UniversalResolver = [ outputs: [ { internalType: "bytes", - name: "", + name: "result", type: "bytes", }, { internalType: "address", - name: "", + name: "resolver", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "resolver", type: "address", }, + { + internalType: "bytes", + name: "name", + type: "bytes", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + { + internalType: "string[]", + name: "gateways", + type: "string[]", + }, + ], + name: "resolveWithResolver", + outputs: [ + { + internalType: "bytes", + name: "", + type: "bytes", + }, ], stateMutability: "view", type: "function", @@ -669,63 +675,9 @@ export const UniversalResolver = [ { inputs: [ { - components: [ - { - internalType: "bytes", - name: "name", - type: "bytes", - }, - { - internalType: "uint256", - name: "offset", - type: "uint256", - }, - { - internalType: "bytes32", - name: "node", - type: "bytes32", - }, - { - internalType: "address", - name: "resolver", - type: "address", - }, - { - internalType: "bool", - name: "extended", - type: "bool", - }, - ], - internalType: "struct AbstractUniversalResolver.ResolverInfo", - name: "info", - type: "tuple", - }, - { - components: [ - { - internalType: "address", - name: "target", - type: "address", - }, - { - internalType: "bytes", - name: "call", - type: "bytes", - }, - { - internalType: "bytes", - name: "data", - type: "bytes", - }, - { - internalType: "uint256", - name: "flags", - type: "uint256", - }, - ], - internalType: "struct CCIPBatcher.Lookup[]", - name: "lookups", - type: "tuple[]", + internalType: "bytes", + name: "response", + type: "bytes", }, { internalType: "bytes", @@ -757,63 +709,9 @@ export const UniversalResolver = [ { inputs: [ { - components: [ - { - internalType: "bytes", - name: "name", - type: "bytes", - }, - { - internalType: "uint256", - name: "offset", - type: "uint256", - }, - { - internalType: "bytes32", - name: "node", - type: "bytes32", - }, - { - internalType: "address", - name: "resolver", - type: "address", - }, - { - internalType: "bool", - name: "extended", - type: "bool", - }, - ], - internalType: "struct AbstractUniversalResolver.ResolverInfo", - name: "infoRev", - type: "tuple", - }, - { - components: [ - { - internalType: "address", - name: "target", - type: "address", - }, - { - internalType: "bytes", - name: "call", - type: "bytes", - }, - { - internalType: "bytes", - name: "data", - type: "bytes", - }, - { - internalType: "uint256", - name: "flags", - type: "uint256", - }, - ], - internalType: "struct CCIPBatcher.Lookup[]", - name: "lookups", - type: "tuple[]", + internalType: "bytes", + name: "response", + type: "bytes", }, { internalType: "bytes", @@ -864,36 +762,23 @@ export const UniversalResolver = [ outputs: [ { internalType: "string", - name: "", + name: "primary", type: "string", }, { internalType: "address", - name: "", + name: "resolver", type: "address", }, { internalType: "address", - name: "", + name: "reverseResolver", type: "address", }, ], stateMutability: "view", type: "function", }, - { - inputs: [ - { - internalType: "string[]", - name: "gateways", - type: "string[]", - }, - ], - name: "setBatchGateways", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, { inputs: [ { @@ -913,17 +798,4 @@ export const UniversalResolver = [ stateMutability: "view", type: "function", }, - { - inputs: [ - { - internalType: "address", - name: "newOwner", - type: "address", - }, - ], - name: "transferOwnership", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, ] as const; diff --git a/packages/datasources/src/abis/shared/AbstractReverseResolver.ts b/packages/datasources/src/abis/shared/AbstractReverseResolver.ts new file mode 100644 index 000000000..d06955fd1 --- /dev/null +++ b/packages/datasources/src/abis/shared/AbstractReverseResolver.ts @@ -0,0 +1,123 @@ +export const AbstractReverseResolver = [ + { + inputs: [ + { + internalType: "bytes", + name: "dns", + type: "bytes", + }, + ], + name: "DNSDecodingFailed", + type: "error", + }, + { + inputs: [ + { + internalType: "bytes", + name: "name", + type: "bytes", + }, + ], + name: "UnreachableName", + type: "error", + }, + { + inputs: [ + { + internalType: "bytes4", + name: "selector", + type: "bytes4", + }, + ], + name: "UnsupportedResolverProfile", + type: "error", + }, + { + inputs: [], + name: "chainId", + outputs: [ + { + internalType: "uint32", + name: "", + type: "uint32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "coinType", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes", + name: "name", + type: "bytes", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + name: "resolve", + outputs: [ + { + internalType: "bytes", + name: "result", + type: "bytes", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address[]", + name: "addrs", + type: "address[]", + }, + ], + name: "resolveNames", + outputs: [ + { + internalType: "string[]", + name: "names", + type: "string[]", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes4", + name: "interfaceId", + type: "bytes4", + }, + ], + name: "supportsInterface", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, +] as const; diff --git a/packages/datasources/src/ens-test-env.ts b/packages/datasources/src/ens-test-env.ts index ed2e0e67f..90da2bf4b 100644 --- a/packages/datasources/src/ens-test-env.ts +++ b/packages/datasources/src/ens-test-env.ts @@ -43,12 +43,12 @@ export default { contracts: { ENSv1RegistryOld: { abi: root_Registry, // Registry was redeployed, same abi - address: "0xa51c1fc2f0d1a1b8494ed1fe312d7c3a78ed91c0", + address: "0x0dcd1bf9a1b36ce34237eeafef220932846bcd82", startBlock: 0, }, ENSv1Registry: { abi: root_Registry, // Registry was redeployed, same abi - address: "0x0dcd1bf9a1b36ce34237eeafef220932846bcd82", + address: "0x9a676e781a523b5d0c0e43731313a708cb607508", startBlock: 0, }, Resolver: { @@ -57,12 +57,12 @@ export default { }, BaseRegistrar: { abi: root_BaseRegistrar, - address: "0x851356ae760d987e095750cceb3bc6014560891c", + address: "0xb7278a61aa25c888815afc32ad3cc52ff24fe575", startBlock: 0, }, LegacyEthRegistrarController: { abi: root_LegacyEthRegistrarController, - address: "0x172076e0166d1f9cc711c77adf8488051744980c", + address: "0xbec49fa140acaa83533fb00a2bb19bddd0290f25", startBlock: 0, }, WrappedEthRegistrarController: { @@ -72,7 +72,7 @@ export default { }, UnwrappedEthRegistrarController: { abi: root_UnwrappedEthRegistrarController, - address: "0xd84379ceae14aa33c123af12424a37803f885889", + address: "0xfbc22278a96299d91d41c453234d97b4f5eb9b2d", startBlock: 0, }, UniversalRegistrarRenewalWithReferrer: { @@ -82,12 +82,12 @@ export default { }, NameWrapper: { abi: root_NameWrapper, - address: "0x162a433068f51e18b7d13932f27e66a3f99e6890", + address: "0xfd471836031dc5108809d173a067e8486b9047a3", startBlock: 0, }, UniversalResolver: { abi: root_UniversalResolver, - address: "0x7a9ec1d04904907de0ed7b6839ccdd59c3716ac9", + address: "0xd8a5a9b31c3c0232e196d518e89fd8bf83acad43", startBlock: 0, }, @@ -95,12 +95,12 @@ export default { ETHRegistry: { abi: Registry, - address: "0x1291be112d480055dafd8a610b7d1e203891c274", + address: "0x1613beb3b2c4f22ee086b2b38c1476a3ce7f78e8", startBlock: 0, }, RootRegistry: { abi: Registry, - address: "0x8a791620dd6260079bf849dc5567adc3f2fdc318", + address: "0x610178da211fef7d417bc0e6fed39f05609ad788", startBlock: 0, }, Registry: { @@ -131,12 +131,12 @@ export default { }, ETHRegistry: { abi: Registry, - address: "0x5fc8d32690cc91d4c39d9d3abcbd16989f875707", + address: "0x0165878a594ca255338adfa4d48449f69242eb8f", startBlock: 0, }, ETHRegistrar: { abi: ETHRegistrar, - address: "0xa513e6e4b8f2a923d98304ec87f64353c4d5c853", + address: "0x2279b7a0a67db372996a5fab50d91eaa73d2ebe6", startBlock: 0, }, }, diff --git a/packages/datasources/src/lib/ResolverABI.ts b/packages/datasources/src/lib/ResolverABI.ts index edf7ccab9..0edf07096 100644 --- a/packages/datasources/src/lib/ResolverABI.ts +++ b/packages/datasources/src/lib/ResolverABI.ts @@ -1,5 +1,6 @@ import { mergeAbis } from "@ponder/utils"; +import { AbstractReverseResolver } from "../abis/shared/AbstractReverseResolver"; import { LegacyPublicResolver } from "../abis/shared/LegacyPublicResolver"; import { Resolver } from "../abis/shared/Resolver"; @@ -9,10 +10,10 @@ import { Resolver } from "../abis/shared/Resolver"; * - TextChanged event without value * - IResolver * - modern Resolver ABI, TextChanged with value - * - DedicatedResolver - * - EnhancedAccessControl + * - ReverseResolvers + * - AbstractReverseResolver * * A Resolver contract is a contract that emits _any_ (not _all_) of the events specified here and * may or may not support any number of the methods available in this ABI. */ -export const ResolverABI = mergeAbis([LegacyPublicResolver, Resolver]); +export const ResolverABI = mergeAbis([LegacyPublicResolver, Resolver, AbstractReverseResolver]); diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/validations.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/validations.ts index 4c8fd6454..9378f5c88 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/validations.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/validations.ts @@ -437,7 +437,7 @@ export function invariant_snapshotTimeIsTheHighestKnownBlockTimestamp( ctx.issues.push({ code: "custom", input: ctx.value, - message: `'snapshotTime' must be greater than or equal to the "highest known block timestamp" (${highestKnownBlockTimestamp})`, + message: `'snapshotTime' (${snapshotTime}) must be greater than or equal to the "highest known block timestamp" (${highestKnownBlockTimestamp})`, }); } } From f6abc034e8762aa82bbba0845fe81fcb4de6630f Mon Sep 17 00:00:00 2001 From: shrugs Date: Wed, 10 Dec 2025 13:24:58 -0600 Subject: [PATCH 073/102] try blacksmith docker actions --- .github/actions/build_docker_image/action.yml | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/actions/build_docker_image/action.yml b/.github/actions/build_docker_image/action.yml index 64ae0dff7..735548f2a 100644 --- a/.github/actions/build_docker_image/action.yml +++ b/.github/actions/build_docker_image/action.yml @@ -3,23 +3,23 @@ description: builds multi-arch docker image inputs: image: - description: 'Target Docker image name' + description: "Target Docker image name" required: true dockerfile: - description: 'Target Dockerfile path' + description: "Target Dockerfile path" required: true tags: - description: 'Docker Image Tags' + description: "Docker Image Tags" required: false registry_user: - description: 'Username for Docker registry' + description: "Username for Docker registry" required: true registry_token: - description: 'Registry token for Docker registry authentication' + description: "Registry token for Docker registry authentication" required: true build_args: @@ -46,11 +46,11 @@ runs: - name: Set up QEMU uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + - name: Setup Docker Builder + uses: useblacksmith/setup-docker-builder@v1 - name: Build & Push - uses: docker/build-push-action@v6 + uses: useblacksmith/build-push-action@v2 with: context: . file: ${{ inputs.dockerfile }} @@ -59,5 +59,3 @@ runs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: ${{ inputs.build_args }} - # cache-from: type=gha - # cache-to: type=gha,mode=max From fd6de3b607cae750311e69b37e55c2d3e7c3c476 Mon Sep 17 00:00:00 2001 From: shrugs Date: Wed, 10 Dec 2025 13:31:13 -0600 Subject: [PATCH 074/102] fix: route through proxy, remove logs on unreachablename --- .../resolve-with-universal-resolver.ts | 35 ++----------------- packages/datasources/src/ens-test-env.ts | 2 +- 2 files changed, 3 insertions(+), 34 deletions(-) diff --git a/apps/ensapi/src/lib/resolution/resolve-with-universal-resolver.ts b/apps/ensapi/src/lib/resolution/resolve-with-universal-resolver.ts index 2031332a3..5241d0735 100644 --- a/apps/ensapi/src/lib/resolution/resolve-with-universal-resolver.ts +++ b/apps/ensapi/src/lib/resolution/resolve-with-universal-resolver.ts @@ -3,28 +3,16 @@ import config from "@/config"; import { bytesToHex, ContractFunctionExecutionError, - ContractFunctionRevertedError, decodeAbiParameters, - decodeErrorResult, encodeFunctionData, getAbiItem, - type Hex, - hexToBytes, type PublicClient, - parseAbiItem, size, } from "viem"; import { packetToBytes } from "viem/ens"; import { DatasourceNames, getDatasource, ResolverABI } from "@ensnode/datasources"; -import { - type DNSEncodedLiteralName, - decodeDNSEncodedLiteralName, - decodeDNSEncodedName, - literalLabelsToInterpretedName, - type Name, - type ResolverRecordsSelection, -} from "@ensnode/ensnode-sdk"; +import type { Name, ResolverRecordsSelection } from "@ensnode/ensnode-sdk"; import type { ResolveCalls, @@ -34,7 +22,7 @@ import type { const ensroot = getDatasource(config.namespace, DatasourceNames.ENSRoot); /** - * Execute a set of ResolveCalls for `name` + * Execute a set of ResolveCalls for `name` against the UniversalResolver. */ export async function executeResolveCallsWithUniversalResolver< SELECTION extends ResolverRecordsSelection, @@ -83,25 +71,6 @@ export async function executeResolveCallsWithUniversalResolver< } catch (error) { // in general, reverts are expected behavior if (error instanceof ContractFunctionExecutionError) { - if (error.cause instanceof ContractFunctionRevertedError) { - if (error.cause.data?.errorName === "ResolverError") { - const decoded = decodeErrorResult({ - abi: ResolverABI, - // biome-ignore lint/style/noNonNullAssertion: allow - data: error.cause.data!.args![0] as Hex, - }); - - const _encodedName = decoded.args[0]; - const _name = literalLabelsToInterpretedName( - decodeDNSEncodedLiteralName(_encodedName as DNSEncodedLiteralName), - ); - - console.log( - `UniversalResolver#resolve(${name}) reverted ResolverError() with internal error ${decoded.errorName} and name ${_name}`, - ); - } - } - return { call, result: null, reason: error.shortMessage }; } diff --git a/packages/datasources/src/ens-test-env.ts b/packages/datasources/src/ens-test-env.ts index 90da2bf4b..574e8ffa9 100644 --- a/packages/datasources/src/ens-test-env.ts +++ b/packages/datasources/src/ens-test-env.ts @@ -87,7 +87,7 @@ export default { }, UniversalResolver: { abi: root_UniversalResolver, - address: "0xd8a5a9b31c3c0232e196d518e89fd8bf83acad43", + address: "0xdc11f7e700a4c898ae5caddb1082cffa76512add", startBlock: 0, }, From d66b3e306285457a20d154ddb151e940d9e876f8 Mon Sep 17 00:00:00 2001 From: shrugs Date: Wed, 10 Dec 2025 13:47:40 -0600 Subject: [PATCH 075/102] refactor: move registration expiration logic into sdk --- .../src/graphql-api/lib/get-domain-by-fqdn.ts | 9 ++-- .../src/lib/ensv2/registration-db-helpers.ts | 44 ------------------ .../ensv2/handlers/ensv1/BaseRegistrar.ts | 2 +- .../ensv2/handlers/ensv1/NameWrapper.ts | 6 +-- .../ensv2/handlers/ensv2/ENSv2Registry.ts | 2 +- packages/ensnode-sdk/src/registrars/index.ts | 1 + .../src/registrars/registration-expiration.ts | 45 +++++++++++++++++++ 7 files changed, 57 insertions(+), 52 deletions(-) create mode 100644 packages/ensnode-sdk/src/registrars/registration-expiration.ts diff --git a/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts index 467fa8abe..1afb6b167 100644 --- a/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts +++ b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts @@ -1,5 +1,6 @@ import config from "@/config"; +import { getUnixTime } from "date-fns"; import { Param, sql } from "drizzle-orm"; import { labelhash, namehash } from "viem"; @@ -74,11 +75,13 @@ async function v1_getDomainIdByFqdn(name: InterpretedName): Promise { const labelHashPath = interpretedNameToLabelHashPath(name); @@ -116,7 +119,7 @@ async function v2_getDomainIdByFqdn( ORDER BY depth; `); - // couldn't for the life of me figure out how to drizzle this correctly... + // couldn't for the life of me figure out how to type this result this correctly within drizzle... const rows = result.rows as { registry_id: RegistryId; domain_id: ENSv2DomainId; @@ -133,7 +136,7 @@ async function v2_getDomainIdByFqdn( // we have an exact match within ENSv2 on the ENS Root Chain const exact = rows.length === labelHashPath.length; if (exact) { - console.log(`Found ${name} in ENSv2 from Registry ${registryId}`); + console.log(`Found '${name}' in ENSv2 from Registry ${registryId}`); return leaf.domain_id; } diff --git a/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts index 23d58fe19..77b3afb0e 100644 --- a/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts @@ -57,47 +57,3 @@ export async function supercedeLatestRegistration( id: makeRegistrationId(registration.domainId, registration.index), }); } - -/** - * Returns whether Registration is expired. If the Registration includes a Grace Period, the - * Grace Period window is considered expired. - */ -export function isRegistrationExpired( - registration: typeof schema.registration.$inferSelect, - now: bigint, -) { - // no expiry, never expired - if (registration.expiry === null) return false; - - // otherwise check against now - return registration.expiry <= now; -} - -/** - * Returns whether Registration is fully expired. If the Registration includes a Grace Period, the - * Grace Period window is considered NOT fully-expired. - */ -export function isRegistrationFullyExpired( - registration: typeof schema.registration.$inferSelect, - now: bigint, -) { - // no expiry, never expired - if (registration.expiry === null) return false; - - // otherwise it is expired if now >= expiry + grace - return now >= registration.expiry + (registration.gracePeriod ?? 0n); -} - -/** - * Returns whether Registration is in grace period. - */ -export function isRegistrationInGracePeriod( - registration: typeof schema.registration.$inferSelect, - now: bigint, -) { - if (registration.expiry === null) return false; - if (registration.gracePeriod === null) return false; - - // - return registration.expiry <= now && registration.expiry + registration.gracePeriod > now; -} diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts index c6d55673e..6aea3b10f 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts @@ -5,6 +5,7 @@ import { type Address, isAddressEqual, namehash, zeroAddress } from "viem"; import { interpretAddress, + isRegistrationFullyExpired, makeENSv1DomainId, makeLatestRegistrationId, makeSubdomainNode, @@ -16,7 +17,6 @@ import { materializeENSv1DomainEffectiveOwner } from "@/lib/ensv2/domain-db-help import { getRegistrarManagedName, registrarTokenIdToLabelHash } from "@/lib/ensv2/registrar-lib"; import { getLatestRegistration, - isRegistrationFullyExpired, supercedeLatestRegistration, } from "@/lib/ensv2/registration-db-helpers"; import { getThisAccountId } from "@/lib/get-this-account-id"; diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts index 35086a6be..910f5d0b5 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts @@ -8,6 +8,9 @@ import { decodeDNSEncodedLiteralName, interpretAddress, isPccFuseSet, + isRegistrationExpired, + isRegistrationFullyExpired, + isRegistrationInGracePeriod, type LiteralLabel, labelhashLiteralLabel, makeENSv1DomainId, @@ -24,9 +27,6 @@ import { ensureLabel } from "@/lib/ensv2/label-db-helpers"; import { getRegistrarManagedName } from "@/lib/ensv2/registrar-lib"; import { getLatestRegistration, - isRegistrationExpired, - isRegistrationFullyExpired, - isRegistrationInGracePeriod, supercedeLatestRegistration, } from "@/lib/ensv2/registration-db-helpers"; import { getThisAccountId } from "@/lib/get-this-account-id"; diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts index 56fa92fa1..dafc6ba5e 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts @@ -8,6 +8,7 @@ import { type AccountId, getCanonicalId, interpretAddress, + isRegistrationFullyExpired, type LiteralLabel, makeENSv2DomainId, makeLatestRegistrationId, @@ -19,7 +20,6 @@ import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; import { ensureLabel } from "@/lib/ensv2/label-db-helpers"; import { getLatestRegistration, - isRegistrationFullyExpired, supercedeLatestRegistration, } from "@/lib/ensv2/registration-db-helpers"; import { getThisAccountId } from "@/lib/get-this-account-id"; diff --git a/packages/ensnode-sdk/src/registrars/index.ts b/packages/ensnode-sdk/src/registrars/index.ts index 319172509..0992e55a9 100644 --- a/packages/ensnode-sdk/src/registrars/index.ts +++ b/packages/ensnode-sdk/src/registrars/index.ts @@ -1,4 +1,5 @@ export * from "./ethnames-subregistry"; export * from "./registrar-action"; +export * from "./registration-expiration"; export * from "./registration-lifecycle"; export * from "./subregistry"; diff --git a/packages/ensnode-sdk/src/registrars/registration-expiration.ts b/packages/ensnode-sdk/src/registrars/registration-expiration.ts new file mode 100644 index 000000000..90e0007d7 --- /dev/null +++ b/packages/ensnode-sdk/src/registrars/registration-expiration.ts @@ -0,0 +1,45 @@ +export interface RegistrationExpiryInfo { + expiry: bigint | null; + gracePeriod: bigint | null; +} + +/** + * Returns whether Registration is expired. If the Registration includes a Grace Period, the + * Grace Period window is considered expired. + */ +export function isRegistrationExpired(registration: RegistrationExpiryInfo, now: bigint): boolean { + // no expiry, never expired + if (registration.expiry == null) return false; + + // otherwise check against now + return registration.expiry <= now; +} + +/** + * Returns whether Registration is fully expired. If the Registration includes a Grace Period, the + * Grace Period window is considered NOT fully-expired. + */ +export function isRegistrationFullyExpired( + registration: RegistrationExpiryInfo, + now: bigint, +): boolean { + // no expiry, never expired + if (registration.expiry == null) return false; + + // otherwise it is expired if now >= expiry + grace + return now >= registration.expiry + (registration.gracePeriod ?? 0n); +} + +/** + * Returns whether Registration is in grace period. + */ +export function isRegistrationInGracePeriod( + registration: RegistrationExpiryInfo, + now: bigint, +): boolean { + if (registration.expiry == null) return false; + if (registration.gracePeriod == null) return false; + + // + return registration.expiry <= now && registration.expiry + registration.gracePeriod > now; +} From 2b955e8f8b216534cda605e1558474cb759b9ac2 Mon Sep 17 00:00:00 2001 From: shrugs Date: Wed, 10 Dec 2025 13:58:28 -0600 Subject: [PATCH 076/102] add expired and isInGracePeriod methods to registration --- apps/ensapi/src/graphql-api/builder.ts | 3 +++ .../src/graphql-api/lib/get-domain-by-fqdn.ts | 6 ++--- .../src/graphql-api/schema/registration.ts | 27 ++++++++++++++++++- .../src/handlers/ensnode-graphql-api.ts | 5 ++++ 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/apps/ensapi/src/graphql-api/builder.ts b/apps/ensapi/src/graphql-api/builder.ts index a6ecb0388..f8dc7b94f 100644 --- a/apps/ensapi/src/graphql-api/builder.ts +++ b/apps/ensapi/src/graphql-api/builder.ts @@ -14,6 +14,9 @@ import type { } from "@ensnode/ensnode-sdk"; export const builder = new SchemaBuilder<{ + Context: { + now: bigint; + }; Scalars: { BigInt: { Input: bigint; Output: bigint }; Address: { Input: Address; Output: Address }; diff --git a/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts index 1afb6b167..e4d5687fd 100644 --- a/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts +++ b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts @@ -15,6 +15,7 @@ import { interpretedLabelsToInterpretedName, interpretedNameToInterpretedLabels, interpretedNameToLabelHashPath, + isRegistrationFullyExpired, type LabelHash, type LiteralLabel, labelhashLiteralLabel, @@ -81,7 +82,7 @@ async function v1_getDomainIdByFqdn(name: InterpretedName): Promise { const labelHashPath = interpretedNameToLabelHashPath(name); @@ -172,8 +173,7 @@ async function v2_getDomainIdByFqdn( const ensv1DomainId = makeENSv1DomainId(dotEth2LDNode); const registration = await getLatestRegistration(ensv1DomainId); - // TODO: && isRegistrationFullyExpired(registration,) - if (registration) { + if (registration && !isRegistrationFullyExpired(registration, now)) { console.log( `ETHTLDResolver deferring to actively registered name ${dotEth2LDNode} in ENSv1...`, ); diff --git a/apps/ensapi/src/graphql-api/schema/registration.ts b/apps/ensapi/src/graphql-api/schema/registration.ts index 2f64a6957..68abad88f 100644 --- a/apps/ensapi/src/graphql-api/schema/registration.ts +++ b/apps/ensapi/src/graphql-api/schema/registration.ts @@ -1,4 +1,9 @@ -import type { RegistrationId, RequiredAndNotNull } from "@ensnode/ensnode-sdk"; +import { + isRegistrationFullyExpired, + isRegistrationInGracePeriod, + type RegistrationId, + type RequiredAndNotNull, +} from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-model-id"; @@ -94,6 +99,16 @@ RegistrationInterfaceRef.implement({ resolve: (parent) => parent.expiry, }), + //////////////////////// + // Registration.expired + //////////////////////// + expired: t.field({ + description: "TODO", + type: "Boolean", + nullable: false, + resolve: (parent, args, context) => isRegistrationFullyExpired(parent, context.now), + }), + ///////////////////////// // Registration.referrer ///////////////////////// @@ -169,6 +184,16 @@ BaseRegistrarRegistrationRef.implement({ nullable: true, resolve: (parent) => (parent.wrapped ? parent : null), }), + + //////////////////////////////// + // Registration.isInGracePeriod + //////////////////////////////// + isInGracePeriod: t.field({ + description: "TODO", + type: "Boolean", + nullable: false, + resolve: (parent, args, context) => isRegistrationInGracePeriod(parent, context.now), + }), }), }); diff --git a/apps/ensapi/src/handlers/ensnode-graphql-api.ts b/apps/ensapi/src/handlers/ensnode-graphql-api.ts index 0896385f2..869cd5ff8 100644 --- a/apps/ensapi/src/handlers/ensnode-graphql-api.ts +++ b/apps/ensapi/src/handlers/ensnode-graphql-api.ts @@ -2,6 +2,7 @@ // import { maxDepthPlugin } from "@escape.tech/graphql-armor-max-depth"; // import { maxTokensPlugin } from "@escape.tech/graphql-armor-max-tokens"; +import { getUnixTime } from "date-fns"; import { createYoga } from "graphql-yoga"; import { schema } from "@/graphql-api/schema"; @@ -14,6 +15,10 @@ const logger = makeLogger("ensnode-graphql"); const yoga = createYoga({ graphqlEndpoint: "*", schema, + context: async () => ({ + // generate a bigint UnixTimestamp per-request for handlers to use + now: BigInt(getUnixTime(new Date())), + }), graphiql: { defaultQuery: `query DomainsByOwner { account(address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266") { From 43dd193c46bb05e8ca5ab341b79fef36c6ad5ff6 Mon Sep 17 00:00:00 2001 From: shrugs Date: Wed, 10 Dec 2025 14:49:53 -0600 Subject: [PATCH 077/102] feat: implement EAC joins for resolvers, registyr permissions --- .../schema/account-registries-permissions.ts | 51 ++++++++++ .../schema/account-resolver-permissions.ts | 51 ++++++++++ apps/ensapi/src/graphql-api/schema/account.ts | 96 +++++++++++++++++-- 3 files changed, 192 insertions(+), 6 deletions(-) create mode 100644 apps/ensapi/src/graphql-api/schema/account-registries-permissions.ts create mode 100644 apps/ensapi/src/graphql-api/schema/account-resolver-permissions.ts diff --git a/apps/ensapi/src/graphql-api/schema/account-registries-permissions.ts b/apps/ensapi/src/graphql-api/schema/account-registries-permissions.ts new file mode 100644 index 000000000..1402063af --- /dev/null +++ b/apps/ensapi/src/graphql-api/schema/account-registries-permissions.ts @@ -0,0 +1,51 @@ +import type * as schema from "@ensnode/ensnode-schema"; + +import { builder } from "@/graphql-api/builder"; +import { RegistryRef } from "@/graphql-api/schema/registry"; + +/** + * Represents an account-specific reference to a `registry` and the account's PermissionsUser for + * that registry. + */ +export interface AccountRegistryPermissionsRef { + permissionsUser: typeof schema.permissionsUser.$inferSelect; + registry: typeof schema.registry.$inferSelect; +} + +export const AccountRegistryPermissionsRef = builder.objectRef( + "AccountRegistryPermissions", +); + +AccountRegistryPermissionsRef.implement({ + fields: (t) => ({ + /////////////////////////////////////// + // AccountRegistryPermissions.registry + /////////////////////////////////////// + registry: t.field({ + description: "TODO", + type: RegistryRef, + nullable: false, + resolve: (parent) => parent.registry, + }), + + /////////////////////////////////////// + // AccountRegistryPermissions.resource + /////////////////////////////////////// + resource: t.field({ + description: "TODO", + type: "BigInt", + nullable: false, + resolve: (parent) => parent.permissionsUser.resource, + }), + + //////////////////////////////////// + // AccountRegistryPermissions.roles + //////////////////////////////////// + roles: t.field({ + description: "TODO", + type: "BigInt", + nullable: false, + resolve: (parent) => parent.permissionsUser.roles, + }), + }), +}); diff --git a/apps/ensapi/src/graphql-api/schema/account-resolver-permissions.ts b/apps/ensapi/src/graphql-api/schema/account-resolver-permissions.ts new file mode 100644 index 000000000..b0e37b884 --- /dev/null +++ b/apps/ensapi/src/graphql-api/schema/account-resolver-permissions.ts @@ -0,0 +1,51 @@ +import type * as schema from "@ensnode/ensnode-schema"; + +import { builder } from "@/graphql-api/builder"; +import { ResolverRef } from "@/graphql-api/schema/resolver"; + +/** + * Represents an account-specific reference to a `resolver` and the account's PermissionsUser for + * that resolver. + */ +export interface AccountResolverPermissions { + permissionsUser: typeof schema.permissionsUser.$inferSelect; + resolver: typeof schema.resolver.$inferSelect; +} + +export const AccountResolverPermissionsRef = builder.objectRef( + "AccountResolverPermissions", +); + +AccountResolverPermissionsRef.implement({ + fields: (t) => ({ + /////////////////////////////////////// + // AccountResolverPermissions.resolver + /////////////////////////////////////// + resolver: t.field({ + description: "TODO", + type: ResolverRef, + nullable: false, + resolve: (parent) => parent.resolver, + }), + + /////////////////////////////////////// + // AccountResolverPermissions.resource + /////////////////////////////////////// + resource: t.field({ + description: "TODO", + type: "BigInt", + nullable: false, + resolve: (parent) => parent.permissionsUser.resource, + }), + + //////////////////////////////////// + // AccountResolverPermissions.roles + //////////////////////////////////// + roles: t.field({ + description: "TODO", + type: "BigInt", + nullable: false, + resolve: (parent) => parent.permissionsUser.roles, + }), + }), +}); diff --git a/apps/ensapi/src/graphql-api/schema/account.ts b/apps/ensapi/src/graphql-api/schema/account.ts index 7417b92c9..ad8b528d9 100644 --- a/apps/ensapi/src/graphql-api/schema/account.ts +++ b/apps/ensapi/src/graphql-api/schema/account.ts @@ -9,6 +9,8 @@ import type { PermissionsUserId } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-model-id"; import { rejectAnyErrors } from "@/graphql-api/lib/reject-any-errors"; +import { AccountRegistryPermissionsRef } from "@/graphql-api/schema/account-registries-permissions"; +import { AccountResolverPermissionsRef } from "@/graphql-api/schema/account-resolver-permissions"; import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; import { cursors } from "@/graphql-api/schema/cursors"; import { DomainInterfaceRef } from "@/graphql-api/schema/domain"; @@ -135,12 +137,94 @@ AccountRef.implement({ ////////////////////// // Account.registries ////////////////////// - // TODO: account's registries via EAC - // similar logic for dedicatedResolvers + // TODO: this should probably be called registryPermissions... + // TODO: this returns all permissions in a registry, perhaps can provide api for non-token resources... + registries: t.connection({ + description: "TODO", + type: AccountRegistryPermissionsRef, + resolve: (parent, args, context) => + resolveCursorConnection( + { ...DEFAULT_CONNECTION_ARGS, args }, + async ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => { + const results = await db + .select({ + permissionsUser: schema.permissionsUser, + registry: schema.registry, + }) + .from(schema.permissionsUser) + .innerJoin( + schema.registry, + and( + eq(schema.permissionsUser.chainId, schema.registry.chainId), + eq(schema.permissionsUser.address, schema.registry.address), + ), + ) + .where( + and( + ...[ + eq(schema.permissionsUser.user, parent.id), + before !== undefined && + lt(schema.permissionsUser.id, cursors.decode(before)), + after !== undefined && + gt(schema.permissionsUser.id, cursors.decode(after)), + ].filter((c) => !!c), + ), + ) + .orderBy(inverted ? desc(schema.permissionsUser.id) : asc(schema.permissionsUser.id)) + .limit(limit); + + return results.map((result) => ({ + id: result.permissionsUser.id, + ...result, + })); + }, + ), + }), - ////////////////////////////// - // Account.dedicatedResolvers - ////////////////////////////// - // TODO: account's dedicated resolvers via EAC + ///////////////////// + // Account.resolvers + ///////////////////// + // TODO: this should probably be called resolverPermissions... + resolvers: t.connection({ + description: "TODO", + type: AccountResolverPermissionsRef, + resolve: (parent, args, context) => + resolveCursorConnection( + { ...DEFAULT_CONNECTION_ARGS, args }, + async ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => { + const results = await db + .select({ + permissionsUser: schema.permissionsUser, + resolver: schema.resolver, + }) + .from(schema.permissionsUser) + .innerJoin( + schema.resolver, + and( + eq(schema.permissionsUser.chainId, schema.resolver.chainId), + eq(schema.permissionsUser.address, schema.resolver.address), + ), + ) + .where( + and( + ...[ + eq(schema.permissionsUser.user, parent.id), + before !== undefined && + lt(schema.permissionsUser.id, cursors.decode(before)), + after !== undefined && + gt(schema.permissionsUser.id, cursors.decode(after)), + ].filter((c) => !!c), + ), + ) + .orderBy(inverted ? desc(schema.permissionsUser.id) : asc(schema.permissionsUser.id)) + .limit(limit); + + return results.map((result) => ({ + id: result.permissionsUser.id, + ...result, + })); + }, + ), + }), }), }); From e96781a07a811a15637ac05b01c31f20351039e5 Mon Sep 17 00:00:00 2001 From: shrugs Date: Wed, 10 Dec 2025 15:00:50 -0600 Subject: [PATCH 078/102] feat: allow filtering permissions by accountid --- .../src/graphql-api/lib/get-domain-by-fqdn.ts | 5 ++--- apps/ensapi/src/graphql-api/schema/account.ts | 15 +++++++++------ apps/ensapi/src/graphql-api/schema/permissions.ts | 5 ++--- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts index e4d5687fd..b1787710f 100644 --- a/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts +++ b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts @@ -52,7 +52,6 @@ export async function getDomainIdByInterpretedName( name: InterpretedName, ): Promise { // Domains addressable in v2 are preferred, but v1 lookups are cheap, so just do them both ahead of time - // TODO: when v2 names are the majority, we can unroll this into a v2 then v1 lookup. const [v1DomainId, v2DomainId] = await Promise.all([ v1_getDomainIdByFqdn(name), v2_getDomainIdByFqdn(ROOT_REGISTRY_ID, name), @@ -89,7 +88,7 @@ async function v2_getDomainIdByFqdn( // https://github.com/drizzle-team/drizzle-orm/issues/1289#issuecomment-2688581070 const rawLabelHashPathArray = sql`${new Param(labelHashPath)}::text[]`; - // TODO: need to join latest registration and confirm that it's not expired, otherwise should treat the domain as not existing + // TODO: need to join latest registration and confirm that it's not expired, if expired should treat the domain as not existing const result = await db.execute(sql` WITH RECURSIVE path AS ( @@ -146,7 +145,7 @@ async function v2_getDomainIdByFqdn( // we did not find an exact match for the Domain within ENSv2 on the ENS Root Chain // if the path terminates at the .eth Registry, we must implement the logic in ETHTLDResolver - // TODO: we could ad an additional invariant that the .eth v2 Registry does indeed have the ETHTLDResolver + // TODO: we could add an additional invariant that the .eth v2 Registry does indeed have the ETHTLDResolver // set as its resolver, but that is unnecessary at the moment and incurs additional db requests or a join against // domain_resolver_relationships // TODO: generalize this into other future bridging resolvers depending on how basenames etc do it diff --git a/apps/ensapi/src/graphql-api/schema/account.ts b/apps/ensapi/src/graphql-api/schema/account.ts index ad8b528d9..55dc008aa 100644 --- a/apps/ensapi/src/graphql-api/schema/account.ts +++ b/apps/ensapi/src/graphql-api/schema/account.ts @@ -9,6 +9,7 @@ import type { PermissionsUserId } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-model-id"; import { rejectAnyErrors } from "@/graphql-api/lib/reject-any-errors"; +import { AccountIdInput } from "@/graphql-api/schema/account-id"; import { AccountRegistryPermissionsRef } from "@/graphql-api/schema/account-registries-permissions"; import { AccountResolverPermissionsRef } from "@/graphql-api/schema/account-resolver-permissions"; import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; @@ -86,7 +87,7 @@ AccountRef.implement({ .where( and( ...[ - // TODO: using any because drizzle infers id as ENSv1DomainId + // NOTE: using any because drizzle infers id as ENSv1DomainId before && lt(domains.id, cursors.decode(before)), after && gt(domains.id, cursors.decode(after)), ].filter((c) => !!c), @@ -110,11 +111,9 @@ AccountRef.implement({ permissions: t.connection({ description: "TODO", type: PermissionsUserRef, - // TODO: allow permissions(in: { contract: { chainId, address } }) - // or permissions(type: 'Registry' | 'Resolver') - // and then join (chainId, address) against Registry/Resolver index to see what it refers to - // and then filter on that — pretty expensive-sounding - // args: {}, + args: { + in: t.arg({ type: AccountIdInput }), + }, resolve: (parent, args, context) => resolveCursorConnection( { ...DEFAULT_CONNECTION_ARGS, args }, @@ -123,7 +122,11 @@ AccountRef.implement({ where: (t, { lt, gt, and, eq }) => and( ...[ + // this user's permissions eq(t.user, parent.id), + // optionally filtered by contract + args.in && and(eq(t.chainId, args.in.chainId), eq(t.address, args.in.address)), + // optionall filtered by cursor before !== undefined && lt(t.id, cursors.decode(before)), after !== undefined && gt(t.id, cursors.decode(after)), ].filter((c) => !!c), diff --git a/apps/ensapi/src/graphql-api/schema/permissions.ts b/apps/ensapi/src/graphql-api/schema/permissions.ts index 57e6520e4..b86f6e5cc 100644 --- a/apps/ensapi/src/graphql-api/schema/permissions.ts +++ b/apps/ensapi/src/graphql-api/schema/permissions.ts @@ -202,10 +202,9 @@ PermissionsUserRef.implement({ //////////////////////////// resource: t.field({ description: "TODO", - type: PermissionsResourceRef, + type: "BigInt", nullable: false, - resolve: ({ chainId, address, resource }) => - makePermissionsResourceId({ chainId, address }, resource), + resolve: (parent) => parent.resource, }), //////////////////////// From fe2c55834b0e1d4afcf7278af63d11114c38bf0f Mon Sep 17 00:00:00 2001 From: shrugs Date: Wed, 10 Dec 2025 16:12:25 -0600 Subject: [PATCH 079/102] ix: permissions names --- apps/ensapi/src/graphql-api/schema/account.ts | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/apps/ensapi/src/graphql-api/schema/account.ts b/apps/ensapi/src/graphql-api/schema/account.ts index 55dc008aa..832212815 100644 --- a/apps/ensapi/src/graphql-api/schema/account.ts +++ b/apps/ensapi/src/graphql-api/schema/account.ts @@ -137,12 +137,11 @@ AccountRef.implement({ ), }), - ////////////////////// - // Account.registries - ////////////////////// - // TODO: this should probably be called registryPermissions... + /////////////////////////////// + // Account.registryPermissions + /////////////////////////////// // TODO: this returns all permissions in a registry, perhaps can provide api for non-token resources... - registries: t.connection({ + registryPermissions: t.connection({ description: "TODO", type: AccountRegistryPermissionsRef, resolve: (parent, args, context) => @@ -176,19 +175,15 @@ AccountRef.implement({ .orderBy(inverted ? desc(schema.permissionsUser.id) : asc(schema.permissionsUser.id)) .limit(limit); - return results.map((result) => ({ - id: result.permissionsUser.id, - ...result, - })); + return results.map((result) => ({ id: result.permissionsUser.id, ...result })); }, ), }), - ///////////////////// - // Account.resolvers - ///////////////////// - // TODO: this should probably be called resolverPermissions... - resolvers: t.connection({ + /////////////////////////////// + // Account.resolverPermissions + /////////////////////////////// + resolverPermissions: t.connection({ description: "TODO", type: AccountResolverPermissionsRef, resolve: (parent, args, context) => @@ -222,10 +217,7 @@ AccountRef.implement({ .orderBy(inverted ? desc(schema.permissionsUser.id) : asc(schema.permissionsUser.id)) .limit(limit); - return results.map((result) => ({ - id: result.permissionsUser.id, - ...result, - })); + return results.map((result) => ({ id: result.permissionsUser.id, ...result })); }, ), }), From a2278c5f2ae1994c0a29e240950c404348639e94 Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 15 Dec 2025 15:59:28 -0600 Subject: [PATCH 080/102] add extended to resolver --- .../ensapi/src/graphql-api/schema/resolver.ts | 30 ++++++++++++++----- .../schemas/protocol-acceleration.schema.ts | 6 ++-- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/apps/ensapi/src/graphql-api/schema/resolver.ts b/apps/ensapi/src/graphql-api/schema/resolver.ts index 0ab1c1554..c9883b2ad 100644 --- a/apps/ensapi/src/graphql-api/schema/resolver.ts +++ b/apps/ensapi/src/graphql-api/schema/resolver.ts @@ -2,6 +2,7 @@ import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@poth import { namehash } from "viem"; import { + makePermissionsId, makeResolverRecordsId, NODE_ANY, type RequiredAndNotNull, @@ -11,10 +12,12 @@ import { import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-model-id"; +import { AccountRef } from "@/graphql-api/schema/account"; import { AccountIdInput, AccountIdRef } from "@/graphql-api/schema/account-id"; import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; import { cursors } from "@/graphql-api/schema/cursors"; import { NameOrNodeInput } from "@/graphql-api/schema/name-or-node"; +import { PermissionsRef } from "@/graphql-api/schema/permissions"; import { ResolverRecordsRef } from "@/graphql-api/schema/resolver-records"; import { db } from "@/lib/db"; @@ -107,6 +110,16 @@ ResolverRef.implement({ }, }), + ///////////////////// + // Resolver.extended + ///////////////////// + extended: t.field({ + description: "TODO", + type: "Boolean", + nullable: false, + resolve: (parent) => parent.isExtended, + }), + ////////////////////// // Resolver.dedicated ////////////////////// @@ -147,25 +160,26 @@ DedicatedResolverMetadataRef.implement({ /////////////////////////// // DedicatedResolver.owner /////////////////////////// + // TODO: lookup via PermissionsUser, but isn't this technically an [AccountRef] type? // owner: t.field({ // description: "TODO", // type: AccountRef, // nullable: true, // // TODO: resolve via EAC - // resolve: (parent) => parent.ownerId, + // resolve: async (parent) => {}, // }), ///////////////////////////////// // DedicatedResolver.permissions ///////////////////////////////// // TODO(EAC) — support DedicatedResolver.permissions after EAC change - // permissions: t.field({ - // description: "TODO", - // type: PermissionsRef, - // nullable: false, - // // TODO: render a DedicatedResolverPermissions model that parses the backing permissions into dedicated-resolver-semantic roles? - // resolve: ({ chainId, address }) => makePermissionsId({ chainId, address }), - // }), + permissions: t.field({ + description: "TODO", + type: PermissionsRef, + nullable: false, + // TODO: render a DedicatedResolverPermissions model that parses the backing permissions into dedicated-resolver-semantic roles? + resolve: ({ chainId, address }) => makePermissionsId({ chainId, address }), + }), ///////////////////////////// // Resolver.dedicatedRecords diff --git a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts index e37bb5b1f..97e00244f 100644 --- a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts +++ b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts @@ -94,17 +94,17 @@ export const resolver = onchainTable( /** * Whether the Resolver implements IExtendedResolver. */ - isExtended: t.boolean().default(false), + isExtended: t.boolean().notNull().default(false), /** * Whether the Resolver implements IDedicatedResolver. */ - isDedicated: t.boolean().default(false), + isDedicated: t.boolean().notNull().default(false), /** * Whether the Resolver is an Onchain Static Resolver. */ - isStatic: t.boolean().default(false), + isStatic: t.boolean().notNull().default(false), /** * Whether the Resolver is an ENSIP19ReverseResolver. From 39c26cefc5e63a6139fd1f7c4645a0345278a5bb Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 15 Dec 2025 16:05:51 -0600 Subject: [PATCH 081/102] fixes --- apps/ensapi/src/handlers/ensnode-graphql-api.ts | 2 +- apps/ensapi/src/lib/resolution/forward-resolution.ts | 5 ++--- apps/ensindexer/src/lib/heal-addr-reverse-subname-label.ts | 3 --- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/apps/ensapi/src/handlers/ensnode-graphql-api.ts b/apps/ensapi/src/handlers/ensnode-graphql-api.ts index 869cd5ff8..e00f0c9bc 100644 --- a/apps/ensapi/src/handlers/ensnode-graphql-api.ts +++ b/apps/ensapi/src/handlers/ensnode-graphql-api.ts @@ -15,7 +15,7 @@ const logger = makeLogger("ensnode-graphql"); const yoga = createYoga({ graphqlEndpoint: "*", schema, - context: async () => ({ + context: () => ({ // generate a bigint UnixTimestamp per-request for handlers to use now: BigInt(getUnixTime(new Date())), }), diff --git a/apps/ensapi/src/lib/resolution/forward-resolution.ts b/apps/ensapi/src/lib/resolution/forward-resolution.ts index 4e597cbbe..8ee6fe06e 100644 --- a/apps/ensapi/src/lib/resolution/forward-resolution.ts +++ b/apps/ensapi/src/lib/resolution/forward-resolution.ts @@ -158,7 +158,6 @@ async function _resolveForward( ); } - // create an un-cached viem#PublicClient separate from ponder's cached/logged clients const publicClient = getPublicClient(chainId); //////////////////////////// @@ -294,8 +293,8 @@ async function _resolveForward( } ////////////////////////////////////////////////// - // Protocol Acceleration: CCIP-Read Shadow Registry Resolvers - // If the activeResolver is a CCIP-Read Shadow Registry Resolver, + // Protocol Acceleration: Bridged Resolvers + // If the activeResolver is a Bridged Resolver, // then we can short-circuit the CCIP-Read and defer resolution to the indicated (shadow)Registry. ////////////////////////////////////////////////// if ( diff --git a/apps/ensindexer/src/lib/heal-addr-reverse-subname-label.ts b/apps/ensindexer/src/lib/heal-addr-reverse-subname-label.ts index 89b46dcfa..d42457a72 100644 --- a/apps/ensindexer/src/lib/heal-addr-reverse-subname-label.ts +++ b/apps/ensindexer/src/lib/heal-addr-reverse-subname-label.ts @@ -87,9 +87,6 @@ export async function healAddrReverseSubnameLabel( if (healedFromTrace !== null) return healedFromTrace; } - // TODO: this trace-based derivation is failing in ens-test-env for some reason - if (config.namespace === "ens-test-env") return "whatever" as LiteralLabel; - // Invariant: by this point, we should have healed all subnames of addr.reverse throw new Error( `Invariant(healAddrReverseSubnameLabel): Unable to heal the label for subname of addr.reverse with labelHash '${labelHash}'.`, From 3ade5374d66d95d82e0813ce74d46ced53d0617c Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 15 Dec 2025 16:09:38 -0600 Subject: [PATCH 082/102] refactor registration expiration arg name --- .../src/registrars/registration-expiration.ts | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/packages/ensnode-sdk/src/registrars/registration-expiration.ts b/packages/ensnode-sdk/src/registrars/registration-expiration.ts index 90e0007d7..b81e7d696 100644 --- a/packages/ensnode-sdk/src/registrars/registration-expiration.ts +++ b/packages/ensnode-sdk/src/registrars/registration-expiration.ts @@ -7,39 +7,32 @@ export interface RegistrationExpiryInfo { * Returns whether Registration is expired. If the Registration includes a Grace Period, the * Grace Period window is considered expired. */ -export function isRegistrationExpired(registration: RegistrationExpiryInfo, now: bigint): boolean { +export function isRegistrationExpired(info: RegistrationExpiryInfo, now: bigint): boolean { // no expiry, never expired - if (registration.expiry == null) return false; + if (info.expiry == null) return false; // otherwise check against now - return registration.expiry <= now; + return info.expiry <= now; } /** * Returns whether Registration is fully expired. If the Registration includes a Grace Period, the * Grace Period window is considered NOT fully-expired. */ -export function isRegistrationFullyExpired( - registration: RegistrationExpiryInfo, - now: bigint, -): boolean { +export function isRegistrationFullyExpired(info: RegistrationExpiryInfo, now: bigint): boolean { // no expiry, never expired - if (registration.expiry == null) return false; + if (info.expiry == null) return false; // otherwise it is expired if now >= expiry + grace - return now >= registration.expiry + (registration.gracePeriod ?? 0n); + return now >= info.expiry + (info.gracePeriod ?? 0n); } /** * Returns whether Registration is in grace period. */ -export function isRegistrationInGracePeriod( - registration: RegistrationExpiryInfo, - now: bigint, -): boolean { - if (registration.expiry == null) return false; - if (registration.gracePeriod == null) return false; +export function isRegistrationInGracePeriod(info: RegistrationExpiryInfo, now: bigint): boolean { + if (info.expiry == null) return false; + if (info.gracePeriod == null) return false; - // - return registration.expiry <= now && registration.expiry + registration.gracePeriod > now; + return info.expiry <= now && info.expiry + info.gracePeriod > now; } From a5c3bfbf94a6408b47fa6070799c239d6aef2476 Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 15 Dec 2025 16:13:03 -0600 Subject: [PATCH 083/102] fix: remove test from merge conflict --- packages/ensnode-sdk/src/shared/cache.test.ts | 210 ------------------ .../src/shared/cache/swr-cache.test.ts | 2 +- .../src/shared/cache/ttl-cache.test.ts | 2 +- 3 files changed, 2 insertions(+), 212 deletions(-) delete mode 100644 packages/ensnode-sdk/src/shared/cache.test.ts diff --git a/packages/ensnode-sdk/src/shared/cache.test.ts b/packages/ensnode-sdk/src/shared/cache.test.ts deleted file mode 100644 index 69dee3f84..000000000 --- a/packages/ensnode-sdk/src/shared/cache.test.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -import { LruCache, TtlCache } from "./cache"; - -describe("LruCache", () => { - it("throws Error if capacity is not an integer", () => { - expect(() => { - new LruCache(1.5); - }).toThrow(); - }); - - it("throws Error if capacity < 0", () => { - expect(() => { - new LruCache(-1); - }).toThrow(); - }); - - it("enforces capacity 0", () => { - const lru = new LruCache(0); - - lru.set("key1", "value"); - - expect(lru.size).toBe(0); - expect(lru.capacity).toBe(0); - expect(lru.get("key1")).toBeUndefined(); - }); - - it("enforces capacity 1", () => { - const lru = new LruCache(1); - - lru.set("key1", "value"); - lru.set("key2", "value"); - - expect(lru.size).toBe(1); - expect(lru.capacity).toBe(1); - expect(lru.get("key1")).toBeUndefined(); - expect(lru.get("key2")).toBeDefined(); - }); - - it("enforces capacity > 1", () => { - const lru = new LruCache(2); - - lru.set("key1", "value"); - lru.set("key2", "value"); - lru.set("key3", "value"); - - expect(lru.size).toBe(2); - expect(lru.capacity).toBe(2); - expect(lru.get("key1")).toBeUndefined(); - expect(lru.get("key2")).toBeDefined(); - expect(lru.get("key3")).toBeDefined(); - }); - - it("remembers up to capacity most recently read keys", () => { - const lru = new LruCache(2); - - lru.set("key1", "value"); - lru.set("key2", "value"); - lru.get("key1"); - lru.set("key3", "value"); - - expect(lru.size).toBe(2); - expect(lru.capacity).toBe(2); - expect(lru.get("key1")).toBeDefined(); - expect(lru.get("key2")).toBeUndefined(); - expect(lru.get("key3")).toBeDefined(); - }); - - it("clears cached values", () => { - const lru = new LruCache(1); - lru.set("key1", "value"); - lru.set("key2", "value"); - lru.clear(); - expect(lru.size).toBe(0); - expect(lru.capacity).toBe(1); - expect(lru.get("key1")).toBeUndefined(); - expect(lru.get("key2")).toBeUndefined(); - }); -}); - -describe("TtlCache", () => { - beforeEach(() => { - vi.useFakeTimers({ shouldAdvanceTime: true, now: new Date(2024, 0, 1) }); - }); - - afterEach(() => { - vi.useRealTimers(); - }); - - it("stores and retrieves values within TTL", () => { - const ttl = new TtlCache(1); // 1 second - - ttl.set("key1", "value1"); - expect(ttl.get("key1")).toBe("value1"); - expect(ttl.size).toBe(1); - }); - - it("expires values after TTL", () => { - const ttl = new TtlCache(1); // 1 second - - ttl.set("key1", "value1"); - expect(ttl.get("key1")).toBe("value1"); - - vi.advanceTimersByTime(1001); // Advance by 1001ms (1 second + 1ms) - - expect(ttl.get("key1")).toBeUndefined(); - expect(ttl.size).toBe(0); - }); - - it("has method returns true for existing non-expired values", () => { - const ttl = new TtlCache(1); // 1 second - - ttl.set("key1", "value1"); - expect(ttl.has("key1")).toBe(true); - }); - - it("has method returns false for non-existent keys", () => { - const ttl = new TtlCache(1); // 1 second - - expect(ttl.has("nonexistent")).toBe(false); - }); - - it("has method returns false for expired values", () => { - const ttl = new TtlCache(1); // 1 second - - ttl.set("key1", "value1"); - expect(ttl.has("key1")).toBe(true); - - vi.advanceTimersByTime(1001); // Advance by 1001ms (1 second + 1ms) - - expect(ttl.has("key1")).toBe(false); - expect(ttl.size).toBe(0); - }); - - it("delete method removes values and returns true if key existed", () => { - const ttl = new TtlCache(1); // 1 second - - ttl.set("key1", "value1"); - expect(ttl.has("key1")).toBe(true); - - const deleted = ttl.delete("key1"); - expect(deleted).toBe(true); - expect(ttl.has("key1")).toBe(false); - expect(ttl.get("key1")).toBeUndefined(); - expect(ttl.size).toBe(0); - }); - - it("delete method returns false if key does not exist", () => { - const ttl = new TtlCache(1); // 1 second - - const deleted = ttl.delete("nonexistent"); - expect(deleted).toBe(false); - }); - - it("clears all cached values", () => { - const ttl = new TtlCache(1); // 1 second - - ttl.set("key1", "value1"); - ttl.set("key2", "value2"); - expect(ttl.size).toBe(2); - - ttl.clear(); - expect(ttl.size).toBe(0); - expect(ttl.get("key1")).toBeUndefined(); - expect(ttl.get("key2")).toBeUndefined(); - }); - - it("capacity returns MAX_SAFE_INTEGER", () => { - const ttl = new TtlCache(1); // 1 second - expect(ttl.capacity).toBe(Number.MAX_SAFE_INTEGER); - }); - - it("automatically cleans up expired entries on size access", () => { - const ttl = new TtlCache(20); // 20 seconds - - ttl.set("key1", "value1"); - ttl.set("key2", "value2"); - expect(ttl.size).toBe(2); - - vi.advanceTimersByTime(10000); // Advance by 10000ms (10 seconds) - ttl.set("key3", "value3"); - expect(ttl.size).toBe(3); - - vi.advanceTimersByTime(11000); // Advance by 11000ms (11 seconds) - total 21 seconds - - expect(ttl.size).toBe(1); - expect(ttl.get("key1")).toBeUndefined(); - expect(ttl.get("key2")).toBeUndefined(); - expect(ttl.get("key3")).toBe("value3"); - }); - - it("refreshes TTL on each set operation", () => { - const ttl = new TtlCache(20); // 20 seconds - - ttl.set("key1", "value1"); - - vi.advanceTimersByTime(10000); // Advance by 10000ms (10 seconds) - ttl.set("key1", "value1-updated"); - - vi.advanceTimersByTime(10000); // Advance by 10000ms (10 seconds) - total 20 seconds from first set, 10 seconds from second set - - expect(ttl.get("key1")).toBe("value1-updated"); - expect(ttl.has("key1")).toBe(true); - - vi.advanceTimersByTime(11000); // Advance by 11000ms (11 seconds) - total 21 seconds from second set - - expect(ttl.get("key1")).toBeUndefined(); - expect(ttl.has("key1")).toBe(false); - }); -}); diff --git a/packages/ensnode-sdk/src/shared/cache/swr-cache.test.ts b/packages/ensnode-sdk/src/shared/cache/swr-cache.test.ts index 3041ab9da..9fedb710a 100644 --- a/packages/ensnode-sdk/src/shared/cache/swr-cache.test.ts +++ b/packages/ensnode-sdk/src/shared/cache/swr-cache.test.ts @@ -4,7 +4,7 @@ import { SWRCache } from "./swr-cache"; describe("SWRCache", () => { beforeEach(() => { - vi.useFakeTimers(); + vi.useFakeTimers({ shouldAdvanceTime: true, now: new Date(2024, 0, 1) }); }); afterEach(() => { diff --git a/packages/ensnode-sdk/src/shared/cache/ttl-cache.test.ts b/packages/ensnode-sdk/src/shared/cache/ttl-cache.test.ts index 4e717a2f7..1cc3a0bf7 100644 --- a/packages/ensnode-sdk/src/shared/cache/ttl-cache.test.ts +++ b/packages/ensnode-sdk/src/shared/cache/ttl-cache.test.ts @@ -4,7 +4,7 @@ import { TtlCache } from "./ttl-cache"; describe("TtlCache", () => { beforeEach(() => { - vi.useFakeTimers(); + vi.useFakeTimers({ shouldAdvanceTime: true, now: new Date(2024, 0, 1) }); }); afterEach(() => { From be02b8a982d6d487fa5564b0a9b9395f3222799e Mon Sep 17 00:00:00 2001 From: shrugs Date: Wed, 17 Dec 2025 10:48:23 -0600 Subject: [PATCH 084/102] simplify drizzle logging --- apps/ensapi/src/lib/handlers/drizzle.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/apps/ensapi/src/lib/handlers/drizzle.ts b/apps/ensapi/src/lib/handlers/drizzle.ts index 8ff71b44a..a5d8f36da 100644 --- a/apps/ensapi/src/lib/handlers/drizzle.ts +++ b/apps/ensapi/src/lib/handlers/drizzle.ts @@ -1,7 +1,5 @@ import { setDatabaseSchema } from "@ponder/client"; -import type { Logger } from "drizzle-orm/logger"; import { drizzle } from "drizzle-orm/node-postgres"; -import type pino from "pino"; import { makeLogger } from "@/lib/logger"; @@ -9,14 +7,6 @@ type Schema = { [name: string]: unknown }; const logger = makeLogger("drizzle"); -class PinoDrizzleLogger implements Logger { - constructor(private readonly logger: pino.Logger) {} - - logQuery(query: string, params: unknown[]): void { - this.logger.debug({ params }, query); - } -} - /** * Makes a Drizzle DB object. */ @@ -35,6 +25,8 @@ export const makeDrizzle = ({ return drizzle(databaseUrl, { schema, casing: "snake_case", - logger: new PinoDrizzleLogger(logger), + logger: { + logQuery: (query, params) => logger.trace({ params }, query), + }, }); }; From 172123a16604e69cd52626435edc1bb0ba0fd2fe Mon Sep 17 00:00:00 2001 From: shrugs Date: Thu, 18 Dec 2025 13:21:16 -0600 Subject: [PATCH 085/102] pnpm to latest --- .tool-versions | 2 +- apps/ensadmin/package.json | 2 +- docs/ensnode.io/package.json | 2 +- docs/ensrainbow.io/package.json | 2 +- package.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.tool-versions b/.tool-versions index 735950c57..c914eff2a 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ nodejs 22.14.0 -pnpm 10.25.0 +pnpm 10.26.0 diff --git a/apps/ensadmin/package.json b/apps/ensadmin/package.json index 4c9792216..b5ce33565 100644 --- a/apps/ensadmin/package.json +++ b/apps/ensadmin/package.json @@ -5,7 +5,7 @@ "type": "module", "description": "Explore the ENS Protocol like never before", "license": "MIT", - "packageManager": "pnpm@10.25.0", + "packageManager": "pnpm@10.26.0", "repository": { "type": "git", "url": "git+https://github.com/namehash/ensnode.git", diff --git a/docs/ensnode.io/package.json b/docs/ensnode.io/package.json index 5765df808..f6bf219b9 100644 --- a/docs/ensnode.io/package.json +++ b/docs/ensnode.io/package.json @@ -3,7 +3,7 @@ "private": true, "type": "module", "version": "1.3.1", - "packageManager": "pnpm@10.25.0", + "packageManager": "pnpm@10.26.0", "scripts": { "dev": "astro dev", "start": "astro dev", diff --git a/docs/ensrainbow.io/package.json b/docs/ensrainbow.io/package.json index 75ca13ba5..631e51fae 100644 --- a/docs/ensrainbow.io/package.json +++ b/docs/ensrainbow.io/package.json @@ -2,7 +2,7 @@ "name": "@docs/ensrainbow", "type": "module", "version": "1.3.1", - "packageManager": "pnpm@10.25.0", + "packageManager": "pnpm@10.26.0", "private": true, "scripts": { "dev": "astro dev", diff --git a/package.json b/package.json index 47112dd07..3068c9af6 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "ensnode-monorepo", "version": "0.0.1", "private": true, - "packageManager": "pnpm@10.25.0", + "packageManager": "pnpm@10.26.0", "scripts": { "lint": "biome check --write .", "lint:ci": "biome ci", From f94875e9355c63a6ea7fd0553cb79ffffe5cdcbd Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 22 Dec 2025 09:48:09 -0600 Subject: [PATCH 086/102] fix: use correct registry ref in new subgraph plugin.ts lol oops --- .../src/plugins/subgraph/plugins/subgraph/plugin.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/plugin.ts b/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/plugin.ts index 74c226e0c..3f576f70d 100644 --- a/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/plugin.ts +++ b/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/plugin.ts @@ -43,11 +43,7 @@ export default createPlugin({ abi: contracts.ENSv1RegistryOld.abi, }, [namespaceContract(pluginName, "ENSv1Registry")]: { - chain: chainConfigForContract( - config.globalBlockrange, - chain.id, - contracts.ENSv1RegistryOld, - ), + chain: chainConfigForContract(config.globalBlockrange, chain.id, contracts.ENSv1Registry), abi: contracts.ENSv1Registry.abi, }, [namespaceContract(pluginName, "BaseRegistrar")]: { From 54993868975355650ce3aa117a9f8b5adedef77b Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 22 Dec 2025 14:34:12 -0600 Subject: [PATCH 087/102] feat: Registration and Renewals improvements --- .../src/lib/ensv2/registration-db-helpers.ts | 75 ++++++-- .../ensv2/handlers/ensv1/BaseRegistrar.ts | 37 +++- .../ensv2/handlers/ensv1/NameWrapper.ts | 37 +++- .../handlers/ensv1/RegistrarController.ts | 27 ++- .../ensv2/handlers/ensv2/ENSv2Registry.ts | 15 +- .../ensv2/handlers/ensv2/ETHRegistrar.ts | 171 +++++++++++++++++- .../src/abis/ensv2/ETHRegistrar.ts | 13 ++ .../src/schemas/ensv2.schema.ts | 56 +++++- packages/ensnode-sdk/src/ensv2/ids-lib.ts | 29 ++- packages/ensnode-sdk/src/ensv2/ids.ts | 5 + 10 files changed, 422 insertions(+), 43 deletions(-) diff --git a/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts index 77b3afb0e..14c760683 100644 --- a/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts @@ -1,7 +1,13 @@ import type { Context } from "ponder:registry"; import schema from "ponder:schema"; -import { type DomainId, makeLatestRegistrationId, makeRegistrationId } from "@ensnode/ensnode-sdk"; +import { + type DomainId, + makeLatestRegistrationId, + makeLatestRenewalId, + makeRegistrationId, + makeRenewalId, +} from "@ensnode/ensnode-sdk"; import { toJson } from "@/lib/json-stringify-with-bigints"; @@ -9,11 +15,18 @@ import { toJson } from "@/lib/json-stringify-with-bigints"; * Latest Registration & Renewals * * We store a one-to-many relationship of Domain -> Registration and a one-to-many relationship of - * Registration -> Renewal, but must frequently access the latest Registration or Renewal in our - * indexing logic. If we were to access these entities via a custom sql query like "SELECT * from - * registrations WHERE domainId= $domainId ORDER BY index DESC", ponder's in-memory cache would have - * to be flushed to postgres every time we access the latest Registration/Renewal, which is pretty - * frequent. To avoid this, we use the special key path /latest (instead of /:index) to access the + * Registration -> Renewal, but must efficiently access the latest Registration or Renewal in our + * indexing logic. The concrete reason for this is that information regarding the latest Registration + * is spread across event handlers: in ENSv1, the .eth BaseRegistrar emits an event that omits pricing + * information. This pricing information is only knowable until the RegistrarController emits an event + * (directly afterwards) including said info. If we were to only index the RegistrarController, however, + * we could theoretically miss Registrations or Renewals created by a RegistrarController that we don't + * index for whatever reason. + * + * If we were to access these entities via a custom sql query like "SELECT * from registrations + * WHERE domainId= $domainId ORDER BY index DESC", ponder's in-memory cache would have to be flushed + * to postgres every time we access the latest Registration/Renewal, which is pretty frequent. + * To avoid this, we use the special key path /latest (instead of /:index) to access the * latest Registration/Renewal, turning the operation into an O(1) lookup compatible with Ponder's * in-memory cacheable db api. * @@ -23,7 +36,13 @@ import { toJson } from "@/lib/json-stringify-with-bigints"; * and insert a new entity (with all of the same columns) under the new id. See `supercedeLatestRegistration` * for implementation. * - * This same logic applies to Renewals. + * This same logic applies to Renewals. Note that the foreign key for a Renewal's Registration is NOT + * the RegistrationId (which changes from /latest to /:index as discussed) but is + * (domainId, registrationIndex, index). The renewals_relationships shows how the composite key + * (domainId, registrationIndex) is used to join Registrations and Renewals. + * + * Finally, RenewalIds must use the 'pinned' RegistrationId (i.e. /:index) at all times, to avoid + * uniqueness collisions when Registrations are superceded. */ /** @@ -34,8 +53,7 @@ export async function getLatestRegistration(context: Context, domainId: DomainId } /** - * Supercedes the latest Registration, changing its id to be indexed, making room in the set for - * a new latest Registration. + * Supercedes the latest Registration, pinning its, making room in the set for a new latest Registration. */ export async function supercedeLatestRegistration( context: Context, @@ -44,16 +62,51 @@ export async function supercedeLatestRegistration( // Invariant: Must be the latest Registration if (registration.id !== makeLatestRegistrationId(registration.domainId)) { throw new Error( - `Invariant(supercedeRegistration): Attempted to supercede non-latest Registration:\n${toJson(registration)}`, + `Invariant(supercedeLatestRegistration): Attempted to supercede non-latest Registration:\n${toJson(registration)}`, ); } // delete latest await context.db.delete(schema.registration, { id: registration.id }); - // insert existing data into new Registration w/ indexed id + // insert existing data into new Registration w/ pinned RegistrationId await context.db.insert(schema.registration).values({ ...registration, id: makeRegistrationId(registration.domainId, registration.index), }); } + +/** + * Gets the latest Renewal. + */ +export async function getLatestRenewal( + context: Context, + domainId: DomainId, + registrationIndex: number, +) { + return context.db.find(schema.renewal, { id: makeLatestRenewalId(domainId, registrationIndex) }); +} + +/** + * Supercedes the latest Renewal, pinning its id, making room in the set for a new latest Renewal. + */ +export async function supercedeLatestRenewal( + context: Context, + renewal: typeof schema.renewal.$inferSelect, +) { + // Invariant: Must be the latest Renewal + if (renewal.id !== makeLatestRenewalId(renewal.domainId, renewal.registrationIndex)) { + throw new Error( + `Invariant(supercedeLatestRenewal): Attempted to supercede non-latest Renewal:\n${toJson(renewal)}`, + ); + } + + // delete latest + await context.db.delete(schema.renewal, { id: renewal.id }); + + // insert existing data into new Renewal w/ 'pinned' RenewalId + await context.db.insert(schema.renewal).values({ + ...renewal, + id: makeRenewalId(renewal.domainId, renewal.registrationIndex, renewal.index), + }); +} diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts index 6aea3b10f..6d0e4774f 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts @@ -8,6 +8,7 @@ import { isRegistrationFullyExpired, makeENSv1DomainId, makeLatestRegistrationId, + makeLatestRenewalId, makeSubdomainNode, PluginName, } from "@ensnode/ensnode-sdk"; @@ -17,7 +18,9 @@ import { materializeENSv1DomainEffectiveOwner } from "@/lib/ensv2/domain-db-help import { getRegistrarManagedName, registrarTokenIdToLabelHash } from "@/lib/ensv2/registrar-lib"; import { getLatestRegistration, + getLatestRenewal, supercedeLatestRegistration, + supercedeLatestRenewal, } from "@/lib/ensv2/registration-db-helpers"; import { getThisAccountId } from "@/lib/get-this-account-id"; import { toJson } from "@/lib/json-stringify-with-bigints"; @@ -170,18 +173,48 @@ export default function () { ); } + // Invariant: Must be BaseRegistrar Registration + if (registration.type !== "BaseRegistrar") { + throw new Error( + `Invariant(BaseRegistrar:NameRenewed): NameRenewed emitted for a non-BaseRegistrar registration:\n${toJson(registration)}`, + ); + } + + // Invariant: Because it is a BaseRegistrar Registration, it must have an expiry. + if (registration.expiry === null) { + throw new Error( + `Invariant(BaseRegistrar:NameRenewed): NameRenewed emitted for a BaseRegistrar registration that has a null expiry:\n${toJson(registration)}`, + ); + } + // Invariant: The Registation must not be fully expired. // https://github.com/ensdomains/ens-contracts/blob/b6cb0e26/contracts/ethregistrar/BaseRegistrarImplementation.sol#L161 if (isRegistrationFullyExpired(registration, event.block.timestamp)) { throw new Error( - `Invariant(BaseRegistrar:NameRenewed): NameRenewed emitted but no unexpired registration\n${toJson({ registration, timestamp: event.block.timestamp })}`, + `Invariant(BaseRegistrar:NameRenewed): NameRenewed emitted but registration is expired:\n${toJson({ registration, timestamp: event.block.timestamp })}`, ); } + // infer duration + const duration = expiry - registration.expiry; + // update the registration await context.db.update(schema.registration, { id: registration.id }).set({ expiry }); - // TODO(renewals): insert renewal & reference registration + // get latest Renewal and supercede if exists + const renewal = await getLatestRenewal(context, domainId, registration.index); + if (renewal) await supercedeLatestRenewal(context, renewal); + + // insert latest Renewal + await context.db.insert(schema.renewal).values({ + id: makeLatestRenewalId(domainId, registration.index), + domainId, + registrationIndex: registration.index, + index: renewal ? renewal.index + 1 : 0, + duration, + // NOTE: no pricing information from BaseRegistrar#NameRenewed. in ENSv1, this info is + // indexed from the Registrar Controllers, see apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts + }); }, ); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts index 910f5d0b5..9e33eae2f 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts @@ -15,6 +15,7 @@ import { labelhashLiteralLabel, makeENSv1DomainId, makeLatestRegistrationId, + makeLatestRenewalId, makeSubdomainNode, type Node, PluginName, @@ -27,7 +28,9 @@ import { ensureLabel } from "@/lib/ensv2/label-db-helpers"; import { getRegistrarManagedName } from "@/lib/ensv2/registrar-lib"; import { getLatestRegistration, + getLatestRenewal, supercedeLatestRegistration, + supercedeLatestRenewal, } from "@/lib/ensv2/registration-db-helpers"; import { getThisAccountId } from "@/lib/get-this-account-id"; import { toJson } from "@/lib/json-stringify-with-bigints"; @@ -359,7 +362,39 @@ export default function () { await context.db.update(schema.registration, { id: registration.id }).set({ expiry }); - // TODO(renewals): insert Renewal if NameWrapper Registration, otherwise handled by BaseRegistrar + // if this is a NameWrapper Registration, this is a Renewal event. otherwise, this is a wrapped + // BaseRegistrar Registration, and the Renewal is already being managed + + if (registration.type !== "NameWrapper") return; + + // if the Registration will no longer expire, this isn't really a Renewal, so no-op + if (expiry === null) return; + + // If: + // a) the Registration previously did not expire, and + // b) the new expiry is before the current block timestamp, + // Then it wasn't really renewed, now, was it? And calculating Renewal.duration is more or less + // impossible. + const now = event.block.timestamp; + if (registration.expiry === null && expiry < now) return; + + // if the Registration previously did not expire but now does, we can calculate duration + // as 'time added since now' (which could be 0 seconds) + const duration = expiry - (registration.expiry ?? event.block.timestamp); + + // get latest Renewal and supercede if exists + const renewal = await getLatestRenewal(context, domainId, registration.index); + if (renewal) await supercedeLatestRenewal(context, renewal); + + // insert latest Renewal + await context.db.insert(schema.renewal).values({ + id: makeLatestRenewalId(domainId, registration.index), + domainId, + registrationIndex: registration.index, + index: renewal ? renewal.index + 1 : 0, + duration, + // NOTE: NameWrapper does not include pricing information + }); }, ); diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts index 9e26fc0eb..9c68c6b2e 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts @@ -16,8 +16,9 @@ import { import { ensureLabel, ensureUnknownLabel } from "@/lib/ensv2/label-db-helpers"; import { getRegistrarManagedName } from "@/lib/ensv2/registrar-lib"; -import { getLatestRegistration } from "@/lib/ensv2/registration-db-helpers"; +import { getLatestRegistration, getLatestRenewal } from "@/lib/ensv2/registration-db-helpers"; import { getThisAccountId } from "@/lib/get-this-account-id"; +import { toJson } from "@/lib/json-stringify-with-bigints"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; @@ -37,7 +38,7 @@ export default function () { referrer?: EncodedReferrer; }>; }) { - const { label: _label, labelHash, baseCost, premium, referrer } = event.args; + const { label: _label, labelHash, baseCost: base, premium, referrer } = event.args; const label = _label as LiteralLabel | undefined; // Invariant: If emitted, label must align with labelHash @@ -67,10 +68,11 @@ export default function () { await ensureUnknownLabel(context, labelHash); } - // update registration's baseCost/premium + // update registration's base/premium + // TODO(paymentToken): add payment token tracking here await context.db .update(schema.registration, { id: registration.id }) - .set({ baseCost, premium, referrer }); + .set({ base, premium, referrer }); } async function handleNameRenewedByController({ @@ -85,7 +87,7 @@ export default function () { referrer?: EncodedReferrer; }>; }) { - const { label: _label, baseCost, premium, referrer } = event.args; + const { label: _label, baseCost: base, premium, referrer } = event.args; const label = _label as LiteralLabel; const controller = getThisAccountId(context, event); @@ -94,17 +96,22 @@ export default function () { const node = makeSubdomainNode(labelHash, managedNode); const domainId = makeENSv1DomainId(node); const registration = await getLatestRegistration(context, domainId); - if (!registration) { throw new Error( `Invariant(RegistrarController:NameRenewed): NameRenewed but no Registration.`, ); } - // TODO(renewals): update renewal with base/premium - // const renewal = await getLatestRenewal(context, registration.id); - // if (!renewal) invariant - // await context.db.update(schema.renewal, { id: renewal.id }).set({ baseCost, premium, referrer }) + const renewal = await getLatestRenewal(context, domainId, registration.index); + if (!renewal) { + throw new Error( + `Invariant(RegistrarController:NameRenewed): NameRenewed but no Renewal for Registration\n${toJson(registration)}`, + ); + } + + // update renewal info + // TODO(paymentToken): add payment token tracking here + await context.db.update(schema.renewal, { id: renewal.id }).set({ base, premium, referrer }); } ////////////////////////////////////// diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts index 73ee6ff9d..a82c3536c 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts @@ -1,5 +1,3 @@ -/** biome-ignore-all lint/correctness/noUnusedVariables: ignore for now */ - import { type Context, ponder } from "ponder:registry"; import schema from "ponder:schema"; import { type Address, hexToBigInt, labelhash } from "viem"; @@ -95,10 +93,10 @@ export default function () { const isFullyExpired = registration && isRegistrationFullyExpired(registration, event.block.timestamp); - // Invariant: If there is an existing Registration, it must be expired. + // Invariant: If the latest Registration exists, it must be fully expired if (registration && !isFullyExpired) { throw new Error( - `Invariant(ENSv2Registry:NameRegistered): Existing unexpired registration found in NameRegistered, expected none or expired.\n${toJson(registration)}`, + `Invariant(ENSv2Registry:NameRegistered): Existing unexpired ENSv2Registry Registration found in NameRegistered, expected none or expired.\n${toJson(registration)}`, ); } @@ -134,7 +132,8 @@ export default function () { changedBy: Address; }>; }) => { - const { tokenId, newExpiry: expiry, changedBy: renewer } = event.args; + // biome-ignore lint/correctness/noUnusedVariables: not sure if we care to index changedBy + const { tokenId, newExpiry: expiry, changedBy } = event.args; const registry = getThisAccountId(context, event); const canonicalId = getCanonicalId(tokenId); @@ -157,7 +156,10 @@ export default function () { // update Registration await context.db.update(schema.registration, { id: registration.id }).set({ expiry }); - // TODO(renewals): insert Renewal + // if newExpiry is 0, this is an `unregister` call, related to ejecting + // https://github.com/ensdomains/namechain/blob/9e31679f4ee6d8abb4d4e840cdf06f2d653a706b/contracts/src/L1/bridge/L1BridgeController.sol#L141 + // TODO(migration): maybe do something special with this state? + // if (expiry === 0n) return; }, ); @@ -205,6 +207,7 @@ export default function () { resource: bigint; }>; }) => { + // biome-ignore lint/correctness/noUnusedVariables: TODO: use resource const { oldTokenId, newTokenId, resource } = event.args; // Invariant: CanonicalIds must match diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ETHRegistrar.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ETHRegistrar.ts index 86e070439..9c5d43f22 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ETHRegistrar.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ETHRegistrar.ts @@ -1,24 +1,181 @@ -/** biome-ignore-all lint/correctness/noUnusedFunctionParameters: ignore for now */ -import { ponder } from "ponder:registry"; +import { type Context, ponder } from "ponder:registry"; +import schema from "ponder:schema"; +import type { Address } from "viem"; -import { PluginName } from "@ensnode/ensnode-sdk"; +import { + type AccountId, + type EncodedReferrer, + getCanonicalId, + interpretAddress, + isRegistrationFullyExpired, + makeENSv2DomainId, + makeLatestRenewalId, + PluginName, + type TokenId, +} from "@ensnode/ensnode-sdk"; +import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; +import { + getLatestRegistration, + getLatestRenewal, + supercedeLatestRenewal, +} from "@/lib/ensv2/registration-db-helpers"; +import { getThisAccountId } from "@/lib/get-this-account-id"; +import { toJson } from "@/lib/json-stringify-with-bigints"; import { namespaceContract } from "@/lib/plugin-helpers"; +import type { EventWithArgs, LogEvent } from "@/lib/ponder-helpers"; const pluginName = PluginName.ENSv2; +async function getRegistrarAndRegistry(context: Context, event: LogEvent) { + const registrar = getThisAccountId(context, event); + const registry: AccountId = { + chainId: context.chain.id, + // ETHRegistrar (this contract) provides a handle to its backing Registry + address: await context.client.readContract({ + abi: context.contracts[namespaceContract(pluginName, "ETHRegistrar")].abi, + address: event.log.address, + functionName: "REGISTRY", + }), + }; + + return { registrar, registry }; +} + export default function () { ponder.on( namespaceContract(pluginName, "ETHRegistrar:NameRegistered"), - async ({ context, event }) => { - // TODO add to existing Registration, override registrant (BaseRegistrar uses msg.sender) + async ({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ + tokenId: TokenId; + label: string; + owner: Address; + subregistry: Address; + resolver: Address; + duration: bigint; + referrer: EncodedReferrer; + paymentToken: Address; + base: bigint; + premium: bigint; + }>; + }) => { + // biome-ignore lint/correctness/noUnusedVariables: TODO(paymentToken) + const { tokenId, owner, referrer, paymentToken, base, premium } = event.args; + + // NOTE: Label and Domain operations are handled by ENSv2Registry:NameRegistered + // (see apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts) which occurs + // _before_ this event. This event upserts the latest Registration with payment info. + + const { registrar, registry } = await getRegistrarAndRegistry(context, event); + const canonicalId = getCanonicalId(tokenId); + const domainId = makeENSv2DomainId(registry, canonicalId); + + const registration = await getLatestRegistration(context, domainId); + + // Invariant: must have latest Registration + if (!registration) { + throw new Error( + `Invariant(ETHRegistrar:NameRegistered): Registration expected, none found.`, + ); + } + + // Invariant: must be ENSv2Registry Registration + if (registration.type !== "ENSv2Registry") { + throw new Error( + `Invariant(ETHRegistrar:NameRegistered): Registration found but not ENSv2Registry Registration:\n${toJson(registration)}`, + ); + } + + // Invariant: must not be expired + const isFullyExpired = isRegistrationFullyExpired(registration, event.block.timestamp); + if (isFullyExpired) { + throw new Error( + `Invariant(ETHRegistrar:NameRegistered): Registration found but expired:\n${toJson(registration)}`, + ); + } + + // upsert registrant + await ensureAccount(context, owner); + + // update latest Registration + await context.db.update(schema.registration, { id: registration.id }).set({ + // TODO: reconsider 'Registration.registrant' if ENSv2 doesn't provide explicit 'registrant' + registrantId: interpretAddress(owner), + + // we now know the correct registrar to attribute to, so overwrite + registrarChainId: registrar.chainId, + registrarAddress: registrar.address, + + referrer, + + // TODO(paymentToken): add payment token tracking here + base, + premium, + }); }, ); ponder.on( namespaceContract(pluginName, "ETHRegistrar:NameRenewed"), - async ({ context, event }) => { - // TODO add to existing Renewal ditto above + async ({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ + tokenId: TokenId; + label: string; + duration: bigint; + newExpiry: bigint; + referrer: EncodedReferrer; + paymentToken: Address; + base: bigint; + }>; + }) => { + // biome-ignore lint/correctness/noUnusedVariables: TODO(paymentToken) + const { tokenId, duration, referrer, paymentToken, base } = event.args; + + // this event occurs _after_ ENSv2Registry:ExpiryUpdated and therefore does not need to + // update Registration.expiry, it just needs to update the latest Renewal + + const { registry } = await getRegistrarAndRegistry(context, event); + const canonicalId = getCanonicalId(tokenId); + const domainId = makeENSv2DomainId(registry, canonicalId); + + const registration = await getLatestRegistration(context, domainId); + + // Invariant: There must be a Registration to renew. + if (!registration) { + throw new Error(`Invariant(ETHRegistrar:NameRenewed): No Registration to renew.`); + } + + // Invariant: Must be ENSv2Registry Registration + if (registration.type !== "ENSv2Registry") { + throw new Error( + `Invariant(ETHRegistrar:NameRenewed): Registration found but not ENSv2Registry Registration:\n${toJson(registration)}`, + ); + } + + // get latest Renewal and supercede if exists + const renewal = await getLatestRenewal(context, domainId, registration.index); + if (renewal) await supercedeLatestRenewal(context, renewal); + + // insert latest Renewal + await context.db.insert(schema.renewal).values({ + id: makeLatestRenewalId(domainId, registration.index), + domainId, + registrationIndex: registration.index, + index: renewal ? renewal.index + 1 : 0, + duration, + referrer, + + // TODO(paymentToken) + base, + }); }, ); } diff --git a/packages/datasources/src/abis/ensv2/ETHRegistrar.ts b/packages/datasources/src/abis/ensv2/ETHRegistrar.ts index 84ab344e4..d621bfeb1 100644 --- a/packages/datasources/src/abis/ensv2/ETHRegistrar.ts +++ b/packages/datasources/src/abis/ensv2/ETHRegistrar.ts @@ -1,4 +1,17 @@ export const ETHRegistrar = [ + { + inputs: [], + name: "REGISTRY", + outputs: [ + { + internalType: "contract IPermissionedRegistry", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, { inputs: [ { diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/schemas/ensv2.schema.ts index 1a3f6d6f9..355f89e08 100644 --- a/packages/ensnode-schema/src/schemas/ensv2.schema.ts +++ b/packages/ensnode-schema/src/schemas/ensv2.schema.ts @@ -14,6 +14,7 @@ import type { PermissionsUserId, RegistrationId, RegistryId, + RenewalId, } from "@ensnode/ensnode-sdk"; /** @@ -239,6 +240,7 @@ export const relations_v2Domain = relations(v2Domain, ({ one, many }) => ({ ///////////////// export const registrationType = onchainEnum("RegistrationType", [ + // TODO: prefix these with ENSv1, maybe excluding ThreeDNS "NameWrapper", "BaseRegistrar", "ThreeDNS", @@ -277,8 +279,12 @@ export const registration = onchainTable( // may have fuses (NameWrapper, Wrapped BaseRegistrar) fuses: t.integer(), - // may have baseCost/premium (BaseRegistrar) - baseCost: t.bigint(), + // TODO(paymentToken): add payment token tracking here + + // may have base cost (ENSv2Registrar) + base: t.bigint(), + + // may have a premium (BaseRegistrar) premium: t.bigint(), // may be Wrapped (BaseRegistrar) @@ -289,7 +295,7 @@ export const registration = onchainTable( }), ); -export const registration_relations = relations(registration, ({ one }) => ({ +export const registration_relations = relations(registration, ({ one, many }) => ({ // belongs to either v1Domain or v2Domain v1Domain: one(v1Domain, { fields: [registration.domainId], @@ -306,6 +312,50 @@ export const registration_relations = relations(registration, ({ one }) => ({ references: [account.id], relationName: "registrant", }), + + // has many renewals + renewals: many(renewal), +})); + +//////////// +// Renewals +//////////// + +export const renewal = onchainTable( + "renewals", + (t) => ({ + // keyed by (registrationId, index) + id: t.text().primaryKey().$type(), + + domainId: t.text().notNull().$type(), + registrationIndex: t.integer().notNull().default(0), + index: t.integer().notNull().default(0), + + // all renewals have a duration + duration: t.bigint().notNull(), + + // may have a referrer + referrer: t.hex().$type(), + + // TODO(paymentToken): add payment token tracking here + + // may have base cost + base: t.bigint(), + + // may have a premium (ENSv1 RegistrarControllers) + premium: t.bigint(), + }), + (t) => ({ + byId: uniqueIndex().on(t.domainId, t.index), + }), +); + +export const renewal_relations = relations(renewal, ({ one }) => ({ + // belongs to registration + registration: one(registration, { + fields: [renewal.domainId, renewal.registrationIndex], + references: [registration.domainId, registration.index], + }), })); /////////////// diff --git a/packages/ensnode-sdk/src/ensv2/ids-lib.ts b/packages/ensnode-sdk/src/ensv2/ids-lib.ts index c7b2606cb..dab81ef9a 100644 --- a/packages/ensnode-sdk/src/ensv2/ids-lib.ts +++ b/packages/ensnode-sdk/src/ensv2/ids-lib.ts @@ -19,6 +19,7 @@ import type { PermissionsUserId, RegistrationId, RegistryId, + RenewalId, ResolverId, ResolverRecordsId, } from "./ids"; @@ -85,14 +86,36 @@ export const makeResolverId = (contract: AccountId) => formatAccountId(contract) export const makeResolverRecordsId = (resolver: AccountId, node: Node) => `${makeResolverId(resolver)}/${node}` as ResolverRecordsId; +/** + * Constructs a RegistrationId for a `domainId`'s latest Registration. + * + * @dev See apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts for more info. + */ +export const makeLatestRegistrationId = (domainId: DomainId) => + `${domainId}/latest` as RegistrationId; + /** * Constructs a RegistrationId for a `domainId`'s `index`'thd Registration. + * + * @dev See apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts for more info. */ export const makeRegistrationId = (domainId: DomainId, index: number = 0) => `${domainId}/${index}` as RegistrationId; /** - * Constructs a RegistrationId denoting the latest Registration using the /latest keypath. + * Constructs a RenewalId for a `domainId`'s `registrationIndex`thd Registration's latest Renewal. + * + * @dev Forces usage of the 'pinned' RegistrationId to avoid collisions, see + * apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts for more info. */ -export const makeLatestRegistrationId = (domainId: DomainId) => - `${domainId}/latest` as RegistrationId; +export const makeLatestRenewalId = (domainId: DomainId, registrationIndex: number) => + `${makeRegistrationId(domainId, registrationIndex)}/latest` as RenewalId; + +/** + * Constructs a RenewalId for a `domainId`'s `registrationIndex`thd Registration's `index`'thd Renewal. + * + * @dev Forces usage of the 'pinned' RegistrationId to avoid collisions, see + * apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts for more info. + */ +export const makeRenewalId = (domainId: DomainId, registrationIndex: number, index: number = 0) => + `${makeRegistrationId(domainId, registrationIndex)}/${index}` as RenewalId; diff --git a/packages/ensnode-sdk/src/ensv2/ids.ts b/packages/ensnode-sdk/src/ensv2/ids.ts index 714e2aa70..102f05237 100644 --- a/packages/ensnode-sdk/src/ensv2/ids.ts +++ b/packages/ensnode-sdk/src/ensv2/ids.ts @@ -54,3 +54,8 @@ export type ResolverRecordsId = string & { __brand: "ResolverRecordsId" }; * Uniquely identifies a Registration entity. */ export type RegistrationId = string & { __brand: "RegistrationId" }; + +/** + * Uniquely identifies a Renewal entity. + */ +export type RenewalId = string & { __brand: "RenewalId" }; From 65c0521a57a523f8cc23570057c615c3e8953aeb Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 22 Dec 2025 17:03:43 -0600 Subject: [PATCH 088/102] feat: renewals in the api --- apps/ensapi/src/graphql-api/schema/query.ts | 26 +++++++ .../src/graphql-api/schema/registration.ts | 30 ++++++++ apps/ensapi/src/graphql-api/schema/renewal.ts | 69 +++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 apps/ensapi/src/graphql-api/schema/renewal.ts diff --git a/apps/ensapi/src/graphql-api/schema/query.ts b/apps/ensapi/src/graphql-api/schema/query.ts index c406c8417..57bbcd48b 100644 --- a/apps/ensapi/src/graphql-api/schema/query.ts +++ b/apps/ensapi/src/graphql-api/schema/query.ts @@ -9,6 +9,7 @@ import { makePermissionsId, makeRegistryId, makeResolverId, + type RegistrationId, type ResolverId, } from "@ensnode/ensnode-sdk"; @@ -25,6 +26,7 @@ import { ENSv2DomainRef, } from "@/graphql-api/schema/domain"; import { PermissionsRef } from "@/graphql-api/schema/permissions"; +import { RegistrationInterfaceRef } from "@/graphql-api/schema/registration"; import { RegistryIdInput, RegistryRef } from "@/graphql-api/schema/registry"; import { ResolverIdInput, ResolverRef } from "@/graphql-api/schema/resolver"; import { db } from "@/lib/db"; @@ -108,6 +110,30 @@ builder.queryType({ }), ), }), + + ///////////////////////////////// + // Query.registrations (Testing) + ///////////////////////////////// + registrations: t.connection({ + description: "TODO", + type: RegistrationInterfaceRef, + resolve: (parent, args, context) => + resolveCursorConnection( + { ...DEFAULT_CONNECTION_ARGS, args }, + ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => + db.query.registration.findMany({ + where: (t, { lt, gt, and }) => + and( + ...[ + before !== undefined && lt(t.id, cursors.decode(before)), + after !== undefined && gt(t.id, cursors.decode(after)), + ].filter((c) => !!c), + ), + orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), + limit, + }), + ), + }), }), ////////////////////////////////// diff --git a/apps/ensapi/src/graphql-api/schema/registration.ts b/apps/ensapi/src/graphql-api/schema/registration.ts index 68abad88f..33b8fa818 100644 --- a/apps/ensapi/src/graphql-api/schema/registration.ts +++ b/apps/ensapi/src/graphql-api/schema/registration.ts @@ -1,16 +1,22 @@ +import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay"; + import { isRegistrationFullyExpired, isRegistrationInGracePeriod, type RegistrationId, + type RenewalId, type RequiredAndNotNull, } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-model-id"; import { AccountIdRef } from "@/graphql-api/schema/account-id"; +import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; import { DomainInterfaceRef } from "@/graphql-api/schema/domain"; +import { RenewalRef } from "@/graphql-api/schema/renewal"; import { WrappedBaseRegistrarRegistrationRef } from "@/graphql-api/schema/wrapped-baseregistrar-registration"; import { db } from "@/lib/db"; +import { cursors } from "@/graphql-api/schema/cursors"; export const RegistrationInterfaceRef = builder.loadableInterfaceRef("Registration", { load: (ids: RegistrationId[]) => @@ -118,6 +124,30 @@ RegistrationInterfaceRef.implement({ nullable: true, resolve: (parent) => parent.referrer, }), + + ///////////////////////// + // Registration.renewals + ///////////////////////// + renewals: t.connection({ + description: "TODO", + type: RenewalRef, + resolve: (parent, args, context) => + resolveCursorConnection( + { ...DEFAULT_CONNECTION_ARGS, args }, + ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => + db.query.renewal.findMany({ + where: (t, { lt, gt, and }) => + and( + ...[ + before !== undefined && lt(t.id, cursors.decode(before)), + after !== undefined && gt(t.id, cursors.decode(after)), + ].filter((c) => !!c), + ), + orderBy: (t, { asc, desc }) => (inverted ? desc(t.id) : asc(t.id)), + limit, + }), + ), + }), }), }); diff --git a/apps/ensapi/src/graphql-api/schema/renewal.ts b/apps/ensapi/src/graphql-api/schema/renewal.ts new file mode 100644 index 000000000..daf0f36ad --- /dev/null +++ b/apps/ensapi/src/graphql-api/schema/renewal.ts @@ -0,0 +1,69 @@ +import * as schema from "@ensnode/ensnode-schema"; +import type { RenewalId } from "@ensnode/ensnode-sdk"; + +import { builder } from "@/graphql-api/builder"; +import { getModelId } from "@/graphql-api/lib/get-model-id"; +import { db } from "@/lib/db"; + +export const RenewalRef = builder.loadableObjectRef("Renewal", { + load: (ids: RenewalId[]) => + db.query.renewal.findMany({ + where: (t, { inArray }) => inArray(t.id, ids), + }), + toKey: getModelId, + cacheResolved: true, + sort: true, +}); + +export type Renewal = Exclude; + +/////////// +// Renewal +/////////// +RenewalRef.implement({ + description: "TODO", + fields: (t) => ({ + ////////////// + // Renewal.id + ////////////// + id: t.expose("id", { + description: "TODO", + type: "ID", + nullable: false, + }), + + // all renewals have a duration + // duration: t.bigint().notNull(), + + // // may have a referrer + // referrer: t.hex().$type(), + + // // TODO(paymentToken): add payment token tracking here + + // // may have base cost + // base: t.bigint(), + + // // may have a premium (ENSv1 RegistrarControllers) + // premium: t.bigint(), + + //////////////////// + // Renewal.duration + //////////////////// + duration: t.field({ + description: "TODO", + type: "BigInt", + nullable: false, + resolve: (parent) => parent.duration, + }), + + //////////////////// + // Renewal.referrer + //////////////////// + referrer: t.field({ + description: "TODO", + type: "Hex", + nullable: true, + resolve: (parent) => parent.referrer, + }), + }), +}); From 78fac585c68799ce0fd8e70b336632b3cfce1055 Mon Sep 17 00:00:00 2001 From: shrugs Date: Tue, 23 Dec 2025 12:38:47 -0600 Subject: [PATCH 089/102] refactor expose to just field --- apps/ensapi/src/graphql-api/schema/account.ts | 3 +- apps/ensapi/src/graphql-api/schema/domain.ts | 5 ++- .../src/graphql-api/schema/registration.ts | 5 ++- .../ensapi/src/graphql-api/schema/registry.ts | 5 ++- apps/ensapi/src/graphql-api/schema/renewal.ts | 40 +++++++++++-------- .../graphql-api/schema/resolver-records.ts | 3 +- .../ensapi/src/graphql-api/schema/resolver.ts | 6 +-- 7 files changed, 40 insertions(+), 27 deletions(-) diff --git a/apps/ensapi/src/graphql-api/schema/account.ts b/apps/ensapi/src/graphql-api/schema/account.ts index 832212815..61cf6060c 100644 --- a/apps/ensapi/src/graphql-api/schema/account.ts +++ b/apps/ensapi/src/graphql-api/schema/account.ts @@ -39,10 +39,11 @@ AccountRef.implement({ ////////////// // Account.id ////////////// - id: t.expose("id", { + id: t.field({ description: "TODO", type: "Address", nullable: false, + resolve: (parent) => parent.id, }), /////////////////// diff --git a/apps/ensapi/src/graphql-api/schema/domain.ts b/apps/ensapi/src/graphql-api/schema/domain.ts index 05135c9ff..4bc150e20 100644 --- a/apps/ensapi/src/graphql-api/schema/domain.ts +++ b/apps/ensapi/src/graphql-api/schema/domain.ts @@ -81,10 +81,11 @@ DomainInterfaceRef.implement({ ////////////////////// // Domain.id ////////////////////// - id: t.expose("id", { - type: "ID", + id: t.field({ description: "TODO", + type: "DomainId", nullable: false, + resolve: (parent) => parent.id, }), ////////////////////// diff --git a/apps/ensapi/src/graphql-api/schema/registration.ts b/apps/ensapi/src/graphql-api/schema/registration.ts index 33b8fa818..4faf4aef6 100644 --- a/apps/ensapi/src/graphql-api/schema/registration.ts +++ b/apps/ensapi/src/graphql-api/schema/registration.ts @@ -12,11 +12,11 @@ import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-model-id"; import { AccountIdRef } from "@/graphql-api/schema/account-id"; import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; +import { cursors } from "@/graphql-api/schema/cursors"; import { DomainInterfaceRef } from "@/graphql-api/schema/domain"; import { RenewalRef } from "@/graphql-api/schema/renewal"; import { WrappedBaseRegistrarRegistrationRef } from "@/graphql-api/schema/wrapped-baseregistrar-registration"; import { db } from "@/lib/db"; -import { cursors } from "@/graphql-api/schema/cursors"; export const RegistrationInterfaceRef = builder.loadableInterfaceRef("Registration", { load: (ids: RegistrationId[]) => @@ -59,10 +59,11 @@ RegistrationInterfaceRef.implement({ ////////////////////// // Registration.id ////////////////////// - id: t.expose("id", { + id: t.field({ description: "TODO", type: "ID", nullable: false, + resolve: (parent) => parent.id, }), /////////////////////// diff --git a/apps/ensapi/src/graphql-api/schema/registry.ts b/apps/ensapi/src/graphql-api/schema/registry.ts index 4d6fc49f1..8eb66e6bf 100644 --- a/apps/ensapi/src/graphql-api/schema/registry.ts +++ b/apps/ensapi/src/graphql-api/schema/registry.ts @@ -27,10 +27,11 @@ RegistryRef.implement({ ////////////////////// // Registry.id ////////////////////// - id: t.expose("id", { + id: t.field({ description: "TODO", - type: "ID", + type: "RegistryId", nullable: false, + resolve: (parent) => parent.id, }), //////////////////// diff --git a/apps/ensapi/src/graphql-api/schema/renewal.ts b/apps/ensapi/src/graphql-api/schema/renewal.ts index daf0f36ad..0c00b99dd 100644 --- a/apps/ensapi/src/graphql-api/schema/renewal.ts +++ b/apps/ensapi/src/graphql-api/schema/renewal.ts @@ -1,4 +1,3 @@ -import * as schema from "@ensnode/ensnode-schema"; import type { RenewalId } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; @@ -26,26 +25,13 @@ RenewalRef.implement({ ////////////// // Renewal.id ////////////// - id: t.expose("id", { + id: t.field({ description: "TODO", type: "ID", nullable: false, + resolve: (parent) => parent.id, }), - // all renewals have a duration - // duration: t.bigint().notNull(), - - // // may have a referrer - // referrer: t.hex().$type(), - - // // TODO(paymentToken): add payment token tracking here - - // // may have base cost - // base: t.bigint(), - - // // may have a premium (ENSv1 RegistrarControllers) - // premium: t.bigint(), - //////////////////// // Renewal.duration //////////////////// @@ -65,5 +51,27 @@ RenewalRef.implement({ nullable: true, resolve: (parent) => parent.referrer, }), + + //////////////// + // Renewal.base + //////////////// + base: t.field({ + description: "TODO", + type: "BigInt", + nullable: true, + resolve: (parent) => parent.base, + }), + + /////////////////// + // Renewal.premium + /////////////////// + premium: t.field({ + description: "TODO", + type: "BigInt", + nullable: true, + resolve: (parent) => parent.premium, + }), + + // TODO(paymentToken): add payment token tracking here }), }); diff --git a/apps/ensapi/src/graphql-api/schema/resolver-records.ts b/apps/ensapi/src/graphql-api/schema/resolver-records.ts index c2c7daf64..22217e4ee 100644 --- a/apps/ensapi/src/graphql-api/schema/resolver-records.ts +++ b/apps/ensapi/src/graphql-api/schema/resolver-records.ts @@ -23,10 +23,11 @@ ResolverRecordsRef.implement({ ////////////////////// // ResolverRecords.id ////////////////////// - id: t.expose("id", { + id: t.field({ description: "TODO", type: "ID", nullable: false, + resolve: (parent) => parent.id, }), //////////////////////// diff --git a/apps/ensapi/src/graphql-api/schema/resolver.ts b/apps/ensapi/src/graphql-api/schema/resolver.ts index c9883b2ad..41ab7f030 100644 --- a/apps/ensapi/src/graphql-api/schema/resolver.ts +++ b/apps/ensapi/src/graphql-api/schema/resolver.ts @@ -12,7 +12,6 @@ import { import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-model-id"; -import { AccountRef } from "@/graphql-api/schema/account"; import { AccountIdInput, AccountIdRef } from "@/graphql-api/schema/account-id"; import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; import { cursors } from "@/graphql-api/schema/cursors"; @@ -53,10 +52,11 @@ ResolverRef.implement({ /////////////// // Resolver.id /////////////// - id: t.expose("id", { - type: "ID", + id: t.field({ description: "TODO", + type: "ResolverId", nullable: false, + resolve: (parent) => parent.id, }), ///////////////////// From c4ce1c0074df3539cf435f5bd08d41d9cbbf87ce Mon Sep 17 00:00:00 2001 From: shrugs Date: Wed, 24 Dec 2025 12:13:25 -0600 Subject: [PATCH 090/102] fix: update addresses, handle re-registration --- .../resolve-with-universal-resolver.ts | 4 +++ .../ensv2/handlers/ensv2/ENSv2Registry.ts | 27 ++++++++++++------- packages/datasources/src/ens-test-env.ts | 8 +++--- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/apps/ensapi/src/lib/resolution/resolve-with-universal-resolver.ts b/apps/ensapi/src/lib/resolution/resolve-with-universal-resolver.ts index 5241d0735..6f30011d2 100644 --- a/apps/ensapi/src/lib/resolution/resolve-with-universal-resolver.ts +++ b/apps/ensapi/src/lib/resolution/resolve-with-universal-resolver.ts @@ -63,12 +63,16 @@ export async function executeResolveCallsWithUniversalResolver< // NOTE: results is type-guaranteed to have at least 1 result (because each abi item's outputs.length >= 1) const result = results[0]; + console.log(`.resolve(${call.functionName}, ${call.args}) -> ${result}`); + return { call, result: result, reason: `.resolve(${call.functionName}, ${call.args})`, }; } catch (error) { + console.log(`.resolve(${call.functionName}, ${call.args}) -> ${error}`); + // in general, reverts are expected behavior if (error instanceof ContractFunctionExecutionError) { return { call, result: null, reason: error.shortMessage }; diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts index a82c3536c..75c140503 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts @@ -80,26 +80,31 @@ export default function () { // ensure discovered Label await ensureLabel(context, label); - // insert v2Domain - await context.db.insert(schema.v2Domain).values({ - id: domainId, - tokenId, - registryId, - labelHash, - // NOTE: ownerId omitted, Transfer* events are sole source of ownership - }); - const registration = await getLatestRegistration(context, domainId); const isFullyExpired = registration && isRegistrationFullyExpired(registration, event.block.timestamp); - // Invariant: If the latest Registration exists, it must be fully expired + // Invariant: If a Registration for this v2Domain exists, it must be fully expired if (registration && !isFullyExpired) { throw new Error( `Invariant(ENSv2Registry:NameRegistered): Existing unexpired ENSv2Registry Registration found in NameRegistered, expected none or expired.\n${toJson(registration)}`, ); } + // insert or update v2Domain + // console.log(`NameRegistered: '${label}'\n ↳ ${domainId}`); + await context.db + .insert(schema.v2Domain) + .values({ + id: domainId, + tokenId, + registryId, + labelHash, + // NOTE: ownerId omitted, Transfer* events are sole source of ownership + }) + // if the v2Domain exists, this is a re-register after expiration and tokenId may have changed + .onConflictDoUpdate({ tokenId }); + // supercede the latest Registration if exists if (registration) await supercedeLatestRegistration(context, registration); @@ -182,6 +187,8 @@ export default function () { const canonicalId = getCanonicalId(tokenId); const domainId = makeENSv2DomainId(registryAccountId, canonicalId); + // console.log(`SubregistryUpdated: ${subregistry} \n ↳ ${domainId}`); + // update domain's subregistry if (subregistry === null) { await context.db.update(schema.v2Domain, { id: domainId }).set({ subregistryId: null }); diff --git a/packages/datasources/src/ens-test-env.ts b/packages/datasources/src/ens-test-env.ts index 574e8ffa9..b020b2e85 100644 --- a/packages/datasources/src/ens-test-env.ts +++ b/packages/datasources/src/ens-test-env.ts @@ -95,12 +95,12 @@ export default { ETHRegistry: { abi: Registry, - address: "0x1613beb3b2c4f22ee086b2b38c1476a3ce7f78e8", + address: "0x0b306bf915c4d645ff596e518faf3f9669b97016", startBlock: 0, }, RootRegistry: { abi: Registry, - address: "0x610178da211fef7d417bc0e6fed39f05609ad788", + address: "0x9a676e781a523b5d0c0e43731313a708cb607508", startBlock: 0, }, Registry: { @@ -131,12 +131,12 @@ export default { }, ETHRegistry: { abi: Registry, - address: "0x0165878a594ca255338adfa4d48449f69242eb8f", + address: "0xdc64a140aa3e981100a9beca4e685f962f0cf6c9", startBlock: 0, }, ETHRegistrar: { abi: ETHRegistrar, - address: "0x2279b7a0a67db372996a5fab50d91eaa73d2ebe6", + address: "0xa51c1fc2f0d1a1b8494ed1fe312d7c3a78ed91c0", startBlock: 0, }, }, From c70bcf6f3249ef336e9b236c1b14d7acacd790e5 Mon Sep 17 00:00:00 2001 From: shrugs Date: Wed, 24 Dec 2025 12:37:33 -0600 Subject: [PATCH 091/102] fix: universalresolver works in docker-compose now --- apps/ensapi/src/lib/public-client.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/ensapi/src/lib/public-client.ts b/apps/ensapi/src/lib/public-client.ts index b726162e4..58fffef79 100644 --- a/apps/ensapi/src/lib/public-client.ts +++ b/apps/ensapi/src/lib/public-client.ts @@ -1,7 +1,8 @@ import config from "@/config"; -import { createPublicClient, fallback, http, type PublicClient } from "viem"; +import { ccipRequest, createPublicClient, fallback, http, type PublicClient } from "viem"; +import { ensTestEnvL1Chain } from "@ensnode/datasources"; import type { ChainId } from "@ensnode/ensnode-sdk"; const _cache = new Map(); @@ -23,6 +24,17 @@ export function getPublicClient(chainId: ChainId): PublicClient { // Create an viem#PublicClient that uses a fallback() transport with all specified HTTP RPCs createPublicClient({ transport: fallback(rpcConfig.httpRPCs.map((url) => http(url.toString()))), + ccipRead: { + async request({ data, sender, urls }) { + // Inject the ens-test-env docker-compose URG url as a fallback if http://localhost:8547 fails + if (chainId === ensTestEnvL1Chain.id) { + return ccipRequest({ data, sender, urls: [...urls, "http://devnet:8547"] }); + } + + // otherwise, handle as normal + return ccipRequest({ data, sender, urls }); + }, + }, }), ); } From 0959ed437b6cb48f443afe132e6c8a9c9d01a422 Mon Sep 17 00:00:00 2001 From: shrugs Date: Thu, 25 Dec 2025 10:08:10 -0600 Subject: [PATCH 092/102] fix: reset to normal sepolia --- packages/datasources/src/sepolia.ts | 324 +++++++++++++++++++++------- 1 file changed, 247 insertions(+), 77 deletions(-) diff --git a/packages/datasources/src/sepolia.ts b/packages/datasources/src/sepolia.ts index d8017147c..3632c33a0 100644 --- a/packages/datasources/src/sepolia.ts +++ b/packages/datasources/src/sepolia.ts @@ -1,9 +1,23 @@ -import { sepolia } from "viem/chains"; +import { + arbitrumSepolia, + baseSepolia, + lineaSepolia, + optimismSepolia, + scrollSepolia, + sepolia, +} from "viem/chains"; -// ABIs for Namechain -import { EnhancedAccessControl } from "./abis/ensv2/EnhancedAccessControl"; -import { ETHRegistrar } from "./abis/ensv2/ETHRegistrar"; -import { Registry } from "./abis/ensv2/Registry"; +// ABIs for Basenames Datasource +import { BaseRegistrar as base_BaseRegistrar } from "./abis/basenames/BaseRegistrar"; +import { EarlyAccessRegistrarController as base_EARegistrarController } from "./abis/basenames/EARegistrarController"; +import { RegistrarController as base_RegistrarController } from "./abis/basenames/RegistrarController"; +import { Registry as base_Registry } from "./abis/basenames/Registry"; +import { UpgradeableRegistrarController as base_UpgradeableRegistrarController } from "./abis/basenames/UpgradeableRegistrarController"; +// ABIs for Lineanames Datasource +import { BaseRegistrar as linea_BaseRegistrar } from "./abis/lineanames/BaseRegistrar"; +import { EthRegistrarController as linea_EthRegistrarController } from "./abis/lineanames/EthRegistrarController"; +import { NameWrapper as linea_NameWrapper } from "./abis/lineanames/NameWrapper"; +import { Registry as linea_Registry } from "./abis/lineanames/Registry"; // ABIs for ENSRoot Datasource import { BaseRegistrar as root_BaseRegistrar } from "./abis/root/BaseRegistrar"; import { LegacyEthRegistrarController as root_LegacyEthRegistrarController } from "./abis/root/LegacyEthRegistrarController"; @@ -16,7 +30,7 @@ import { WrappedEthRegistrarController as root_WrappedEthRegistrarController } f import { Seaport as Seaport1_5 } from "./abis/seaport/Seaport1.5"; // Shared ABIs import { StandaloneReverseRegistrar } from "./abis/shared/StandaloneReverseRegistrar"; -import { ResolverABI } from "./lib/ResolverABI"; +import { ResolverABI, ResolverFilter } from "./lib/resolver"; // Types import { DatasourceNames, type ENSNamespace } from "./lib/types"; @@ -35,39 +49,40 @@ export default { [DatasourceNames.ENSRoot]: { chain: sepolia, contracts: { - ENSv1RegistryOld: { + RegistryOld: { abi: root_Registry, // Registry was redeployed, same abi - address: "0x4355f1c6b5b59818dc56e336d1584df35d47ad86", - startBlock: 9374708, + address: "0x94f523b8261b815b87effcf4d18e6abef18d6e4b", + startBlock: 3702721, }, - ENSv1Registry: { + Registry: { abi: root_Registry, // Registry was redeployed, same abi - address: "0x17795c119b8155ab9d3357c77747ba509695d7cb", - startBlock: 9374709, + address: "0x00000000000c2e074ec69a0dfb2997ba6c7d2e1e", + startBlock: 3702728, }, Resolver: { abi: ResolverABI, - startBlock: 9374708, // ignores any Resolver events prior to `startBlock` of ENSv1RegistryOld on Sepolia + filter: ResolverFilter, + startBlock: 3702721, // ignores any Resolver events prior to `startBlock` of RegistryOld on Sepolia }, BaseRegistrar: { abi: root_BaseRegistrar, - address: "0xb16870800de7444f6b2ebd885465412a5e581614", - startBlock: 9374751, + address: "0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85", + startBlock: 3702731, }, LegacyEthRegistrarController: { abi: root_LegacyEthRegistrarController, - address: "0x25da9aa54dae4afa6534ba829c6288039d4f5ebb", - startBlock: 9374756, + address: "0x7e02892cfc2bfd53a75275451d73cf620e793fc0", + startBlock: 3790197, }, WrappedEthRegistrarController: { abi: root_WrappedEthRegistrarController, - address: "0x4f1d36f2c1382a01006077a42de53f7c843d1a83", - startBlock: 9374767, + address: "0xfed6a969aaa60e4961fcd3ebf1a2e8913ac65b72", + startBlock: 3790244, }, UnwrappedEthRegistrarController: { abi: root_UnwrappedEthRegistrarController, - address: "0x99e517db3db5ec5424367b8b50cd11ddcb0008f1", - startBlock: 9374773, + address: "0xfb3ce5d01e0f33f41dbb39035db9745962f1f968", + startBlock: 8579988, }, UniversalRegistrarRenewalWithReferrer: { abi: root_UniversalRegistrarRenewalWithReferrer, @@ -76,62 +91,127 @@ export default { }, NameWrapper: { abi: root_NameWrapper, - address: "0xca7e6d0ddc5f373197bbe6fc2f09c2314399f028", - startBlock: 9374764, + address: "0x0635513f179d50a207757e05759cbd106d7dfce8", + startBlock: 3790153, }, UniversalResolver: { abi: root_UniversalResolver, - address: "0x198827b2316e020c48b500fc3cebdbcaf58787ce", - startBlock: 9374794, + address: "0xb7b7dadf4d42a08b3ec1d3a1079959dfbc8cffcc", + startBlock: 8515717, }, + }, + }, - // - - ETHRegistry: { - abi: Registry, - address: "0x89db31efa19c29c2510db56d8c213b3f960ca256", - startBlock: 9685062, + /** + * Basenames Datasource + * + * Addresses and Start Blocks from Basenames + * https://github.com/base-org/basenames + */ + [DatasourceNames.Basenames]: { + /** + * As of 5-Jun-2025 the Resolver for 'basetest.eth' in the Sepolia ENS namespace is + * 0x084D10C07EfEecD9fFc73DEb38ecb72f9eEb65aB. + * + * This Resolver uses ENSIP-10 (Wildcard Resolution) and EIP-3668 (CCIP Read) to delegate + * the forward resolution of data associated with subnames of 'basetest.eth' to an offchain + * gateway server operated by Coinbase that uses the following subregistry contracts on + * Base Sepolia as its source of truth. + * + * The owner of 'basetest.eth' in the ENS Registry on the Sepolia ENS namespace + * (e.g. Coinbase) has the ability to change this configuration at any time. + * + * See the reference documentation for additional context: + * docs/ensnode/src/content/docs/reference/mainnet-registered-subnames-of-subregistries.mdx + */ + chain: baseSepolia, + contracts: { + Registry: { + abi: base_Registry, + address: "0x1493b2567056c2181630115660963e13a8e32735", + startBlock: 13012458, }, - RootRegistry: { - abi: Registry, - address: "0x52c3eec93cb33451985c29c1e3f80a40ab071360", - startBlock: 9684796, + Resolver: { + abi: ResolverABI, + filter: ResolverFilter, + startBlock: 13012458, }, - Registry: { - abi: Registry, - startBlock: 9684796, + BaseRegistrar: { + abi: base_BaseRegistrar, + address: "0xa0c70ec36c010b55e3c434d6c6ebeec50c705794", + startBlock: 13012465, + }, + EARegistrarController: { + abi: base_EARegistrarController, + address: "0x3a0e8c2a0a28f396a5e5b69edb2e630311f1517a", + startBlock: 13041164, }, - EnhancedAccessControl: { - abi: EnhancedAccessControl, - startBlock: 9684796, + RegistrarController: { + abi: base_RegistrarController, + address: "0x49ae3cc2e3aa768b1e5654f5d3c6002144a59581", + startBlock: 13298580, + }, + /** + * This controller was added to BaseRegistrar contract + * with the following tx: + * https://sepolia.basescan.org/tx/0x648d984c1a379a6c300851b9561fe98a9b5282a26ca8c2c7660b11c53f0564bc + */ + UpgradeableRegistrarController: { + abi: base_UpgradeableRegistrarController, + address: "0x82c858cdf64b3d893fe54962680edfddc37e94c8", // a proxy contract + startBlock: 29896051, }, }, }, - [DatasourceNames.Namechain]: { - chain: sepolia, + /** + * Lineanames Datasource + * + * Addresses and Start Blocks from Lineanames + * https://github.com/Consensys/linea-ens + */ + [DatasourceNames.Lineanames]: { + /** + * As of 5-Jun-2025 the Resolver for 'linea-sepolia.eth' in the Sepolia ENS namespace is + * 0x64884ED06241c059497aEdB2C7A44CcaE6bc7937. + * + * This Resolver uses ENSIP-10 (Wildcard Resolution) and EIP-3668 (CCIP Read) to delegate + * the forward resolution of data associated with subnames of 'linea-sepolia.eth' to an offchain + * gateway server operated by Consensys that uses the following subregistry contracts on + * Linea Sepolia as its source of truth. + * + * The owner of 'linea-sepolia.eth' in the ENS Registry on the Sepolia ENS namespace + * (e.g. Consensys) has the ability to change this configuration at any time. + * + * See the reference documentation for additional context: + * docs/ensnode/src/content/docs/reference/mainnet-registered-subnames-of-subregistries.mdx + */ + chain: lineaSepolia, contracts: { + Registry: { + abi: linea_Registry, + address: "0x5b2636f0f2137b4ae722c01dd5122d7d3e9541f7", + startBlock: 2395094, + }, Resolver: { abi: ResolverABI, - startBlock: 9374708, // temporary: match same-network Resolver in ENSRoot above + filter: ResolverFilter, + startBlock: 2395094, // based on startBlock of Registry on Linea Sepolia }, - ETHRegistry: { - abi: Registry, - address: "0x0f3eb298470639a96bd548cea4a648bc80b2cee2", - startBlock: 9683977, + BaseRegistrar: { + abi: linea_BaseRegistrar, + address: "0x83475a84c0ea834f06c8e636a62631e7d2e07a44", + startBlock: 2395099, }, - ETHRegistrar: { - abi: ETHRegistrar, - address: "0x774faadcd7e8c4b7441aa2927f10845fea083ea1", - startBlock: 9374809, + EthRegistrarController: { + abi: linea_EthRegistrarController, + address: "0x0f81e3b3a32dfe1b8a08d3c0061d852337a09338", + startBlock: 2395231, }, - Registry: { - abi: Registry, - startBlock: 9374809, - }, - EnhancedAccessControl: { - abi: EnhancedAccessControl, - startBlock: 9374809, + NameWrapper: { + abi: linea_NameWrapper, + address: "0xf127de9e039a789806fed4c6b1c0f3affea9425e", + startBlock: 2395202, }, }, }, @@ -144,55 +224,145 @@ export default { contracts: { DefaultReverseRegistrar: { abi: StandaloneReverseRegistrar, - address: "0xf7fca8d7b8b802d07a1011b69a5e39395197b730", - startBlock: 9374772, + address: "0x4f382928805ba0e23b30cfb75fc9e848e82dfd47", + startBlock: 8579966, }, + DefaultReverseResolver1: { + abi: ResolverABI, + address: "0x8fade66b79cc9f707ab26799354482eb93a5b7dd", + startBlock: 3790251, + }, + DefaultReverseResolver2: { + abi: ResolverABI, + address: "0x8948458626811dd0c23eb25cc74291247077cc51", + startBlock: 7035086, + }, DefaultReverseResolver3: { abi: ResolverABI, - address: "0xa238d3aca667210d272391a119125d38816af4b1", - startBlock: 9374791, + address: "0x9dc60e7bd81ccc96774c55214ff389d42ae5e9ac", + startBlock: 8580041, }, + DefaultPublicResolver1: { + abi: ResolverABI, + address: "0x8fade66b79cc9f707ab26799354482eb93a5b7dd", + startBlock: 3790251, + }, DefaultPublicResolver2: { abi: ResolverABI, - address: "0x9c97031854a11e41289a33e2fa5749c468c08820", - startBlock: 9374783, + address: "0x8948458626811dd0c23eb25cc74291247077cc51", + startBlock: 7035086, }, DefaultPublicResolver3: { abi: ResolverABI, - address: "0x0e14ee0592da66bb4c8a8090066bc8a5af15f3e6", - startBlock: 9374784, + address: "0xe99638b40e4fff0129d56f03b55b6bbc4bbe49b5", + startBlock: 8580001, }, BaseReverseResolver: { abi: ResolverABI, - address: "0xf849bc9d818ac09a629ae981b03bcbcdca750e8f", - startBlock: 9374708, + // https://adraffy.github.io/ens-normalize.js/test/resolver.html?sepolia#80014a34.reverse + address: "0xaf3b3f636be80b6709f5bd3a374d6ac0d0a7c7aa", + startBlock: 8580004, }, LineaReverseResolver: { abi: ResolverABI, - address: "0xc8e393f59be1ec4d44ea9190e6831d3c4a94dfa7", - startBlock: 9374708, + // https://adraffy.github.io/ens-normalize.js/test/resolver.html?sepolia#8000e705.reverse + address: "0x083da1dbc0f379ccda6ac81a934207c3d8a8a205", + startBlock: 8580005, }, OptimismReverseResolver: { abi: ResolverABI, - address: "0x05e889ba6c7a2399ea9ce4e9666f1e863b0f1728", - startBlock: 9374708, + // https://adraffy.github.io/ens-normalize.js/test/resolver.html?sepolia#80aa37dc.reverse + address: "0xc9ae189772bd48e01410ab3be933637ee9d3aa5f", + startBlock: 8580026, }, ArbitrumReverseResolver: { abi: ResolverABI, - address: "0x18b9b7158c16194b6d4c4fde85de92b035a3ce77", - startBlock: 9374708, + // https://adraffy.github.io/ens-normalize.js/test/resolver.html?sepolia#80066eee.reverse + address: "0x926f94d2adc77c86cb0050892097d49aadd02e8b", + startBlock: 8580003, }, ScrollReverseResolver: { abi: ResolverABI, - address: "0xd854f312888d0a5d64b646932a2ed8e8bad8de87", - startBlock: 9374708, + // https://adraffy.github.io/ens-normalize.js/test/resolver.html?sepolia#8008274f.reverse + address: "0x9fa59673e43f15bdb8722fdaf5c2107574b99062", + startBlock: 8580040, + }, + }, + }, + + /** + * Contracts that power Reverse Resolution on Base Sepolia. + */ + [DatasourceNames.ReverseResolverBase]: { + chain: baseSepolia, + contracts: { + L2ReverseRegistrar: { + abi: ResolverABI, + address: "0x00000beef055f7934784d6d81b6bc86665630dba", + startBlock: 21788010, + }, + }, + }, + + /** + * Contracts that power Reverse Resolution on Optimism Sepolia. + */ + [DatasourceNames.ReverseResolverOptimism]: { + chain: optimismSepolia, + contracts: { + L2ReverseRegistrar: { + abi: ResolverABI, + address: "0x00000beef055f7934784d6d81b6bc86665630dba", + startBlock: 23770766, + }, + }, + }, + + /** + * Contracts that power Reverse Resolution on Arbitrum Sepolia. + */ + [DatasourceNames.ReverseResolverArbitrum]: { + chain: arbitrumSepolia, + contracts: { + L2ReverseRegistrar: { + abi: ResolverABI, + address: "0x00000beef055f7934784d6d81b6bc86665630dba", + startBlock: 123142726, + }, + }, + }, + + /** + * Contracts that power Reverse Resolution on Scroll Sepolia. + */ + [DatasourceNames.ReverseResolverScroll]: { + chain: scrollSepolia, + contracts: { + L2ReverseRegistrar: { + abi: ResolverABI, + address: "0x00000beef055f7934784d6d81b6bc86665630dba", + startBlock: 8175276, + }, + }, + }, + + /** + * Contracts that power Reverse Resolution on Linea Sepolia. + */ + [DatasourceNames.ReverseResolverLinea]: { + chain: lineaSepolia, + contracts: { + L2ReverseRegistrar: { + abi: ResolverABI, + address: "0x00000beef055f7934784d6d81b6bc86665630dba", + startBlock: 9267966, }, }, }, From d452f0200bcf00ffd3f8784e2d53896b3cecc0dd Mon Sep 17 00:00:00 2001 From: shrugs Date: Thu, 25 Dec 2025 10:10:45 -0600 Subject: [PATCH 093/102] fix: update old sepolia datasource to new naming and add ensv2 stubs --- packages/datasources/src/sepolia.ts | 61 ++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/packages/datasources/src/sepolia.ts b/packages/datasources/src/sepolia.ts index 3632c33a0..bbfb1ecfc 100644 --- a/packages/datasources/src/sepolia.ts +++ b/packages/datasources/src/sepolia.ts @@ -13,6 +13,9 @@ import { EarlyAccessRegistrarController as base_EARegistrarController } from "./ import { RegistrarController as base_RegistrarController } from "./abis/basenames/RegistrarController"; import { Registry as base_Registry } from "./abis/basenames/Registry"; import { UpgradeableRegistrarController as base_UpgradeableRegistrarController } from "./abis/basenames/UpgradeableRegistrarController"; +import { EnhancedAccessControl } from "./abis/ensv2/EnhancedAccessControl"; +import { ETHRegistrar } from "./abis/ensv2/ETHRegistrar"; +import { Registry } from "./abis/ensv2/Registry"; // ABIs for Lineanames Datasource import { BaseRegistrar as linea_BaseRegistrar } from "./abis/lineanames/BaseRegistrar"; import { EthRegistrarController as linea_EthRegistrarController } from "./abis/lineanames/EthRegistrarController"; @@ -30,7 +33,7 @@ import { WrappedEthRegistrarController as root_WrappedEthRegistrarController } f import { Seaport as Seaport1_5 } from "./abis/seaport/Seaport1.5"; // Shared ABIs import { StandaloneReverseRegistrar } from "./abis/shared/StandaloneReverseRegistrar"; -import { ResolverABI, ResolverFilter } from "./lib/resolver"; +import { ResolverABI } from "./lib/ResolverABI"; // Types import { DatasourceNames, type ENSNamespace } from "./lib/types"; @@ -49,19 +52,18 @@ export default { [DatasourceNames.ENSRoot]: { chain: sepolia, contracts: { - RegistryOld: { + ENSv1RegistryOld: { abi: root_Registry, // Registry was redeployed, same abi address: "0x94f523b8261b815b87effcf4d18e6abef18d6e4b", startBlock: 3702721, }, - Registry: { + ENSv1Registry: { abi: root_Registry, // Registry was redeployed, same abi address: "0x00000000000c2e074ec69a0dfb2997ba6c7d2e1e", startBlock: 3702728, }, Resolver: { abi: ResolverABI, - filter: ResolverFilter, startBlock: 3702721, // ignores any Resolver events prior to `startBlock` of RegistryOld on Sepolia }, BaseRegistrar: { @@ -99,6 +101,55 @@ export default { address: "0xb7b7dadf4d42a08b3ec1d3a1079959dfbc8cffcc", startBlock: 8515717, }, + + // + + ETHRegistry: { + abi: Registry, + address: "0x1291be112d480055dafd8a610b7d1e203891c274", + startBlock: 23794084, + }, + RootRegistry: { + abi: Registry, + address: "0x8a791620dd6260079bf849dc5567adc3f2fdc318", + startBlock: 23794084, + }, + Registry: { + abi: Registry, + startBlock: 23794084, + }, + EnhancedAccessControl: { + abi: EnhancedAccessControl, + startBlock: 23794084, + }, + }, + }, + + [DatasourceNames.Namechain]: { + chain: sepolia, + contracts: { + Resolver: { + abi: ResolverABI, + startBlock: 23794084, + }, + Registry: { + abi: Registry, + startBlock: 23794084, + }, + EnhancedAccessControl: { + abi: EnhancedAccessControl, + startBlock: 23794084, + }, + ETHRegistry: { + abi: Registry, + address: "0x5fc8d32690cc91d4c39d9d3abcbd16989f875707", + startBlock: 23794084, + }, + ETHRegistrar: { + abi: ETHRegistrar, + address: "0xa513e6e4b8f2a923d98304ec87f64353c4d5c853", + startBlock: 23794084, + }, }, }, @@ -133,7 +184,6 @@ export default { }, Resolver: { abi: ResolverABI, - filter: ResolverFilter, startBlock: 13012458, }, BaseRegistrar: { @@ -195,7 +245,6 @@ export default { }, Resolver: { abi: ResolverABI, - filter: ResolverFilter, startBlock: 2395094, // based on startBlock of Registry on Linea Sepolia }, BaseRegistrar: { From af8319ae853e49de663bd82d6f5e658ef73bcc00 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 26 Dec 2025 14:26:25 -0600 Subject: [PATCH 094/102] feat: another self review, add Resolver.dedicated.owner --- apps/ensapi/src/graphql-api/schema/domain.ts | 16 ++++----- .../src/graphql-api/schema/registration.ts | 4 +-- .../ensapi/src/graphql-api/schema/resolver.ts | 36 ++++++++++++++----- apps/ensapi/src/lib/public-client.ts | 6 +++- .../src/schemas/ensv2.schema.ts | 2 +- 5 files changed, 42 insertions(+), 22 deletions(-) diff --git a/apps/ensapi/src/graphql-api/schema/domain.ts b/apps/ensapi/src/graphql-api/schema/domain.ts index 4bc150e20..9fefd7014 100644 --- a/apps/ensapi/src/graphql-api/schema/domain.ts +++ b/apps/ensapi/src/graphql-api/schema/domain.ts @@ -22,9 +22,9 @@ import { db } from "@/lib/db"; const isENSv1Domain = (domain: Domain): domain is ENSv1Domain => "parentId" in domain; -////////////////////// -// Refs -////////////////////// +///////////////////////////// +// ENSv1Domain & ENSv2Domain +///////////////////////////// export const ENSv1DomainRef = builder.loadableObjectRef("ENSv1Domain", { load: (ids: ENSv1DomainId[]) => @@ -101,13 +101,7 @@ DomainInterfaceRef.implement({ //////////////////// // Domain.canonical //////////////////// - // canonical: t.loadable({ - // description: "TODO", - // type: "Name", - // nullable: true, - // load: (ids: DomainId[], context) => context.loadPosts(ids), - // resolve: (user, args) => user.lastPostID, - // }), + // TODO: pending ENS team canonicalName implementation // canonical: t.field({ // description: "TODO", // type: "Name", @@ -134,6 +128,7 @@ DomainInterfaceRef.implement({ ////////////////// // Domain.parents ////////////////// + // TODO: pending ENS team canonicalName implementation // parents: t.field({ // description: "TODO", // type: [DomainInterfaceRef], @@ -154,6 +149,7 @@ DomainInterfaceRef.implement({ ////////////////// // Domain.aliases ////////////////// + // TODO: pending ENS team canonicalName implementation, maybe impossible to implement // aliases: t.field({ // description: "TODO", // type: ["Name"], diff --git a/apps/ensapi/src/graphql-api/schema/registration.ts b/apps/ensapi/src/graphql-api/schema/registration.ts index 4faf4aef6..d8333b78c 100644 --- a/apps/ensapi/src/graphql-api/schema/registration.ts +++ b/apps/ensapi/src/graphql-api/schema/registration.ts @@ -242,9 +242,9 @@ ThreeDNSRegistrationRef.implement({ }), }); -/////////////////////////// +///////////////////////////// // ENSv2RegistryRegistration -/////////////////////////// +///////////////////////////// export const ENSv2RegistryRegistrationRef = builder.objectRef( "ENSv2RegistryRegistration", ); diff --git a/apps/ensapi/src/graphql-api/schema/resolver.ts b/apps/ensapi/src/graphql-api/schema/resolver.ts index 41ab7f030..b34fce8d7 100644 --- a/apps/ensapi/src/graphql-api/schema/resolver.ts +++ b/apps/ensapi/src/graphql-api/schema/resolver.ts @@ -8,10 +8,12 @@ import { type RequiredAndNotNull, type ResolverId, type ResolverRecordsId, + ROOT_RESOURCE, } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-model-id"; +import { AccountRef } from "@/graphql-api/schema/account"; import { AccountIdInput, AccountIdRef } from "@/graphql-api/schema/account-id"; import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; import { cursors } from "@/graphql-api/schema/cursors"; @@ -145,6 +147,15 @@ ResolverRef.implement({ }; }, }), + + //////////////////////// + // Resolver.permissions + //////////////////////// + permissions: t.field({ + description: "TODO", + type: PermissionsRef, + resolve: ({ chainId, address }) => makePermissionsId({ chainId, address }), + }), }), }); @@ -160,14 +171,23 @@ DedicatedResolverMetadataRef.implement({ /////////////////////////// // DedicatedResolver.owner /////////////////////////// - // TODO: lookup via PermissionsUser, but isn't this technically an [AccountRef] type? - // owner: t.field({ - // description: "TODO", - // type: AccountRef, - // nullable: true, - // // TODO: resolve via EAC - // resolve: async (parent) => {}, - // }), + owner: t.field({ + description: "TODO", + type: AccountRef, + nullable: true, + resolve: async (parent) => { + const permissionsUser = await db.query.permissionsUser.findFirst({ + where: (t, { eq, and }) => + and( + eq(t.chainId, parent.chainId), + eq(t.address, parent.address), + eq(t.resource, ROOT_RESOURCE), + ), + }); + + return permissionsUser?.user; + }, + }), ///////////////////////////////// // DedicatedResolver.permissions diff --git a/apps/ensapi/src/lib/public-client.ts b/apps/ensapi/src/lib/public-client.ts index 58fffef79..7cfa125e9 100644 --- a/apps/ensapi/src/lib/public-client.ts +++ b/apps/ensapi/src/lib/public-client.ts @@ -26,7 +26,11 @@ export function getPublicClient(chainId: ChainId): PublicClient { transport: fallback(rpcConfig.httpRPCs.map((url) => http(url.toString()))), ccipRead: { async request({ data, sender, urls }) { - // Inject the ens-test-env docker-compose URG url as a fallback if http://localhost:8547 fails + // When running in Docker, ENSApi's viem should fetch the UniversalResolverGateway at + // http://devnet:8547 rather than the default of http://localhost:8547, which is unreachable + // from within the Docker container. So here, if we're handling a CCIP-Read request on + // the ens-test-env L1 Chain, we add the ens-test-env's docker-compose-specific url as + // a fallback if the default (http://localhost:8547) fails. if (chainId === ensTestEnvL1Chain.id) { return ccipRequest({ data, sender, urls: [...urls, "http://devnet:8547"] }); } diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/schemas/ensv2.schema.ts index 355f89e08..670a44c1f 100644 --- a/packages/ensnode-schema/src/schemas/ensv2.schema.ts +++ b/packages/ensnode-schema/src/schemas/ensv2.schema.ts @@ -281,7 +281,7 @@ export const registration = onchainTable( // TODO(paymentToken): add payment token tracking here - // may have base cost (ENSv2Registrar) + // may have base cost (BaseRegistrar, ENSv2Registrar) base: t.bigint(), // may have a premium (BaseRegistrar) From b743f207b96d75765e90612a6d69c01988711d70 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 26 Dec 2025 14:31:59 -0600 Subject: [PATCH 095/102] fix: incorrectly calling isInterpertedLabel --- .../src/shared/interpretation/interpreted-names-and-labels.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ensnode-sdk/src/shared/interpretation/interpreted-names-and-labels.ts b/packages/ensnode-sdk/src/shared/interpretation/interpreted-names-and-labels.ts index da38157e6..dada395d9 100644 --- a/packages/ensnode-sdk/src/shared/interpretation/interpreted-names-and-labels.ts +++ b/packages/ensnode-sdk/src/shared/interpretation/interpreted-names-and-labels.ts @@ -111,7 +111,7 @@ export function isInterpretedName(name: Name): name is InterpretedName { export function interpretedNameToLabelHashPath(name: InterpretedName): LabelHashPath { return interpretedNameToInterpretedLabels(name) .map((label) => { - if (!isInterpetedLabel) { + if (!isInterpetedLabel(label)) { throw new Error( `Invariant(interpretedNameToLabelHashPath): Expected InterpretedLabel, received '${label}'.`, ); From 6321796796c8d983d956c07b6815ebada222ae9e Mon Sep 17 00:00:00 2001 From: shrugs Date: Sat, 27 Dec 2025 10:00:37 -0600 Subject: [PATCH 096/102] fix: forward-resolution starts at ensv1 registry for now --- .../src/graphql-api/lib/get-canonical-path.ts | 4 +- .../src/graphql-api/lib/get-domain-by-fqdn.ts | 4 +- apps/ensapi/src/graphql-api/schema/query.ts | 4 +- .../protocol-acceleration/find-resolver.ts | 31 ++++---- .../src/lib/resolution/forward-resolution.ts | 4 +- .../middleware/can-accelerate.middleware.ts | 77 ++----------------- ...domain-resolver-relationship-db-helpers.ts | 10 +-- .../schemas/protocol-acceleration.schema.ts | 10 +-- .../ensnode-sdk/src/shared/root-registry.ts | 47 +++++++---- 9 files changed, 72 insertions(+), 119 deletions(-) diff --git a/apps/ensapi/src/graphql-api/lib/get-canonical-path.ts b/apps/ensapi/src/graphql-api/lib/get-canonical-path.ts index 8323f35d3..87428b37b 100644 --- a/apps/ensapi/src/graphql-api/lib/get-canonical-path.ts +++ b/apps/ensapi/src/graphql-api/lib/get-canonical-path.ts @@ -6,14 +6,14 @@ import * as schema from "@ensnode/ensnode-schema"; import { type CanonicalPath, type DomainId, - getRootRegistryId, + getENSv2RootRegistryId, type RegistryId, } from "@ensnode/ensnode-sdk"; import { db } from "@/lib/db"; const MAX_DEPTH = 16; -const ROOT_REGISTRY_ID = getRootRegistryId(config.namespace); +const ROOT_REGISTRY_ID = getENSv2RootRegistryId(config.namespace); /** * Provide the canonical parents from the Root Registry to `domainId`. diff --git a/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts index b1787710f..dc02aab27 100644 --- a/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts +++ b/apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts @@ -10,7 +10,7 @@ import { type DomainId, type ENSv2DomainId, ETH_NODE, - getRootRegistryId, + getENSv2RootRegistryId, type InterpretedName, interpretedLabelsToInterpretedName, interpretedNameToInterpretedLabels, @@ -33,7 +33,7 @@ const namechain = getDatasource(config.namespace, DatasourceNames.Namechain); const ETH_LABELHASH = labelhashLiteralLabel("eth" as LiteralLabel); -const ROOT_REGISTRY_ID = getRootRegistryId(config.namespace); +const ROOT_REGISTRY_ID = getENSv2RootRegistryId(config.namespace); const ENS_ROOT_V2_ETH_REGISTRY_ID = makeRegistryId({ chainId: ensroot.chain.id, diff --git a/apps/ensapi/src/graphql-api/schema/query.ts b/apps/ensapi/src/graphql-api/schema/query.ts index 57bbcd48b..417b033ba 100644 --- a/apps/ensapi/src/graphql-api/schema/query.ts +++ b/apps/ensapi/src/graphql-api/schema/query.ts @@ -5,7 +5,7 @@ import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@poth import { type ENSv1DomainId, type ENSv2DomainId, - getRootRegistryId, + getENSv2RootRegistryId, makePermissionsId, makeRegistryId, makeResolverId, @@ -203,7 +203,7 @@ builder.queryType({ description: "TODO", type: RegistryRef, nullable: false, - resolve: () => getRootRegistryId(config.namespace), + resolve: () => getENSv2RootRegistryId(config.namespace), }), }), }); diff --git a/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts b/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts index 91b367e08..bd17fe2bc 100644 --- a/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts +++ b/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts @@ -17,7 +17,7 @@ import { type AccountId, type ENSv1DomainId, getNameHierarchy, - isRootRegistry, + isENSv1Registry, type Name, type Node, type NormalizedName, @@ -46,8 +46,11 @@ const tracer = trace.getTracer("find-resolver"); /** * Identifies `name`'s active resolver in `registry`. * - * Note that any `registry` that is not the ENS Root Chain's Registry is a Shadow Registry like - * Basenames' or Lineanames' (shadow)Registry contracts. + * Registry can be: + * - ENSv1 Root Chain Registry + * - ENSv1 Basenames (shadow) Registry + * - ENSv1 Lineanames (shadow) Registry + * - TODO: any ENSv2 Registry */ export async function findResolver({ registry, @@ -74,7 +77,7 @@ export async function findResolver({ } // Invariant: UniversalResolver#findResolver only works for ENS Root Registry - if (!isRootRegistry(config.namespace, registry)) { + if (!isENSv1Registry(config.namespace, registry)) { throw new Error( `Invariant(findResolver): UniversalResolver#findResolver only identifies active resolvers agains the ENs Root Registry, but a different Registry contract was passed: ${JSON.stringify(registry)}.`, ); @@ -204,30 +207,30 @@ async function findResolverWithIndex( eq(t.address, registry.address), // exclusively for the requested registry inArray(t.domainId, domainIds), // find Relations for the following Domains ), - columns: { domainId: true, address: true }, + columns: { domainId: true, resolver: true }, }); + // 3.1 sort into the same order as `domainIds`, db results are not guaranteed to match `inArray` order + records.sort(sortByArrayOrder(domainIds, (drr) => drr.domainId)); + // cast into our semantic types - return records as { domainId: ENSv1DomainId; address: Address }[]; + return records as { domainId: ENSv1DomainId; resolver: Address }[]; }, ); - // 3.1 sort into the same order as `nodes`, db results are not guaranteed to match `inArray` order - domainResolverRelations.sort(sortByArrayOrder(domainIds, (nrr) => nrr.domainId)); - // 4. iterate up the hierarchy and return the first valid resolver - for (const { domainId, address } of domainResolverRelations) { + for (const { domainId, resolver } of domainResolverRelations) { // NOTE: this zeroAddress check is not strictly necessary, as the ProtocolAcceleration plugin // encodes a zeroAddress resolver as the _absence_ of a Node-Resolver relation, so there is // no case where a Node-Resolver relation exists and the resolverAddress is zeroAddress, but // we include this invariant here to encode that expectation explicitly. - if (isAddressEqual(zeroAddress, address)) { + if (isAddressEqual(zeroAddress, resolver)) { throw new Error( - `Invariant(findResolverWithIndex): Encountered a zeroAddress resolverAddress for Domain ${domainId}, which should be impossible: check ProtocolAcceleration Node-Resolver Relation indexing logic.`, + `Invariant(findResolverWithIndex): Encountered a zeroAddress resolverAddress for Domain ${domainId}, which should be impossible: check ProtocolAcceleration Domain-Resolver Relation indexing logic.`, ); } - // map the relation's `node` back to its name in `names` + // map the relation's `domainId` back to its name in `names` const indexInHierarchy = domainIds.indexOf(domainId); const activeName = names[indexInHierarchy]; @@ -240,7 +243,7 @@ async function findResolverWithIndex( return { activeName, - activeResolver: address, + activeResolver: resolver, // this resolver must have wildcard support if it was not for the first node in our hierarchy requiresWildcardSupport: indexInHierarchy > 0, }; diff --git a/apps/ensapi/src/lib/resolution/forward-resolution.ts b/apps/ensapi/src/lib/resolution/forward-resolution.ts index 8ee6fe06e..efc556bb3 100644 --- a/apps/ensapi/src/lib/resolution/forward-resolution.ts +++ b/apps/ensapi/src/lib/resolution/forward-resolution.ts @@ -10,7 +10,7 @@ import { type ForwardResolutionArgs, ForwardResolutionProtocolStep, type ForwardResolutionResult, - getRootRegistry, + getENSv1Registry, isNormalizedName, isSelectionEmpty, makeResolverId, @@ -92,7 +92,7 @@ export async function resolveForward // initially be ENS Root Chain's Registry: see `_resolveForward` for additional context. return _resolveForward(name, selection, { ...options, - registry: getRootRegistry(config.namespace), + registry: getENSv1Registry(config.namespace), }); } diff --git a/apps/ensapi/src/middleware/can-accelerate.middleware.ts b/apps/ensapi/src/middleware/can-accelerate.middleware.ts index 9aee7188b..42144cdbb 100644 --- a/apps/ensapi/src/middleware/can-accelerate.middleware.ts +++ b/apps/ensapi/src/middleware/can-accelerate.middleware.ts @@ -11,10 +11,10 @@ export type CanAccelerateMiddlewareVariables = { canAccelerate: boolean }; // TODO: expand this datamodel to include 'reasons' acceleration was disabled to drive ui -let didWarnCannotAccelerateENSv2 = false; -let didWarnNoProtocolAccelerationPlugin = false; -let didInitialCanAccelerate = false; -let prevCanAccelerate = false; +const didWarnCannotAccelerateENSv2 = false; +const didWarnNoProtocolAccelerationPlugin = false; +const didInitialCanAccelerate = false; +const prevCanAccelerate = false; /** * Middleware that determines if protocol acceleration can be enabled for the current request. @@ -24,73 +24,8 @@ let prevCanAccelerate = false; * resolution handlers. */ export const canAccelerateMiddleware = factory.createMiddleware(async (c, next) => { - // context must be set by the required middleware - if (c.var.isRealtime === undefined) { - throw new Error(`Invariant(canAccelerateMiddleware): isRealtime middleware required`); - } - - //////////////////////////// - /// Temporary ENSv2 Bailout - //////////////////////////// - // TODO: re-enable acceleration for ensv2 once implemented - if (config.ensIndexerPublicConfig.plugins.includes(PluginName.ENSv2)) { - if (!didWarnCannotAccelerateENSv2) { - logger.warn( - `ENSApi is currently unable to accelerate Resolution API requests while indexing ENSv2. Protocol Acceleration is DISABLED.`, - ); - - didWarnCannotAccelerateENSv2 = true; - } - - c.set("canAccelerate", false); + if (true) { + c.set("canAccelerate", true); return await next(); } - - ////////////////////////////////////////////// - /// Protocol Acceleration Plugin Availability - ////////////////////////////////////////////// - - const hasProtocolAccelerationPlugin = config.ensIndexerPublicConfig.plugins.includes( - PluginName.ProtocolAcceleration, - ); - - // log one warning to the console if !hasProtocolAccelerationPlugin - if (!didWarnNoProtocolAccelerationPlugin && !hasProtocolAccelerationPlugin) { - logger.warn( - `ENSApi is connected to an ENSIndexer that does NOT include the ${PluginName.ProtocolAcceleration} plugin: ENSApi will NOT be able to accelerate Resolution API requests, even if ?accelerate=true. Resolution requests will abide by the full Forward/Reverse Resolution specification, including RPC calls and CCIP-Read requests to external CCIP-Read Gateways.`, - ); - - didWarnNoProtocolAccelerationPlugin = true; - } - - ////////////////////////////// - /// Can Accelerate Derivation - ////////////////////////////// - - // the Resolution API can accelerate requests if - // a) ENSIndexer reports that it is within MAX_REALTIME_DISTANCE_TO_ACCELERATE of realtime, and - // b) ENSIndexer reports that it has the ProtocolAcceleration plugin enabled. - const canAccelerate = hasProtocolAccelerationPlugin && c.var.isRealtime; - - // log notice when acceleration begins - if ( - (!didInitialCanAccelerate && canAccelerate) || // first time - (didInitialCanAccelerate && !prevCanAccelerate && canAccelerate) // future change in status - ) { - logger.info(`Protocol Acceleration is now ENABLED.`); - } - - // log notice when acceleration ends - if ( - (!didInitialCanAccelerate && !canAccelerate) || // first time - (didInitialCanAccelerate && prevCanAccelerate && !canAccelerate) // future change in status - ) { - logger.info(`Protocol Acceleration is DISABLED.`); - } - - prevCanAccelerate = canAccelerate; - didInitialCanAccelerate = true; - - c.set("canAccelerate", canAccelerate); - await next(); }); diff --git a/apps/ensindexer/src/lib/protocol-acceleration/domain-resolver-relationship-db-helpers.ts b/apps/ensindexer/src/lib/protocol-acceleration/domain-resolver-relationship-db-helpers.ts index cd3701ca0..28e33a533 100644 --- a/apps/ensindexer/src/lib/protocol-acceleration/domain-resolver-relationship-db-helpers.ts +++ b/apps/ensindexer/src/lib/protocol-acceleration/domain-resolver-relationship-db-helpers.ts @@ -2,7 +2,7 @@ import type { Context } from "ponder:registry"; import schema from "ponder:schema"; import { type Address, isAddressEqual, zeroAddress } from "viem"; -import { type AccountId, type DomainId, makeResolverId } from "@ensnode/ensnode-sdk"; +import type { AccountId, DomainId } from "@ensnode/ensnode-sdk"; /** * Ensures that the Domain-Resolver Relationship for the provided `domainId` in `registry` is set @@ -15,14 +15,12 @@ export async function ensureDomainResolverRelation( domainId: DomainId, resolver: Address, ) { - const isZeroResolver = isAddressEqual(zeroAddress, resolver); - if (isZeroResolver) { + if (isAddressEqual(zeroAddress, resolver)) { await context.db.delete(schema.domainResolverRelation, { ...registry, domainId }); } else { - const resolverId = makeResolverId({ chainId: registry.chainId, address: resolver }); await context.db .insert(schema.domainResolverRelation) - .values({ ...registry, domainId, resolverId }) - .onConflictDoUpdate({ resolverId }); + .values({ ...registry, domainId, resolver }) + .onConflictDoUpdate({ resolver }); } } diff --git a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts index 97e00244f..9b9415ee4 100644 --- a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts +++ b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts @@ -61,10 +61,8 @@ export const domainResolverRelation = onchainTable( address: t.hex().notNull().$type
(), domainId: t.hex().notNull().$type(), - /** - * The Domain's assigned Resolver address within the Registry identified by (chainId, address). - */ - resolverId: t.hex().notNull().$type(), + // The Domain's assigned Resolver's address (NOTE: always scoped to chainId) + resolver: t.hex().notNull().$type
(), }), (t) => ({ pk: primaryKey({ columns: [t.chainId, t.address, t.domainId] }), @@ -73,8 +71,8 @@ export const domainResolverRelation = onchainTable( export const domainResolverRelation_relations = relations(domainResolverRelation, ({ one }) => ({ resolver: one(resolver, { - fields: [domainResolverRelation.resolverId], - references: [resolver.id], + fields: [domainResolverRelation.chainId, domainResolverRelation.resolver], + references: [resolver.chainId, resolver.address], }), })); diff --git a/packages/ensnode-sdk/src/shared/root-registry.ts b/packages/ensnode-sdk/src/shared/root-registry.ts index 5d5d45efb..02cc02434 100644 --- a/packages/ensnode-sdk/src/shared/root-registry.ts +++ b/packages/ensnode-sdk/src/shared/root-registry.ts @@ -1,26 +1,45 @@ -import { DatasourceNames, type ENSNamespaceId, getDatasource } from "@ensnode/datasources"; -import { type AccountId, accountIdEqual, makeRegistryId } from "@ensnode/ensnode-sdk"; +import { DatasourceNames, type ENSNamespaceId } from "@ensnode/datasources"; +import { + type AccountId, + accountIdEqual, + getDatasourceContract, + makeRegistryId, +} from "@ensnode/ensnode-sdk"; + +////////////// +// ENSv1 +////////////// /** - * Gets the AccountId representing the ENSv2 Root Registry in the selected `namespace`. + * Gets the AccountId representing the ENSv1 Registry in the selected `namespace`. + */ +export const getENSv1Registry = (namespace: ENSNamespaceId) => + getDatasourceContract(namespace, DatasourceNames.ENSRoot, "ENSv1Registry"); + +/** + * Determines whether `contract` is the ENSv1 Registry in `namespace`. */ -export const getRootRegistry = (namespace: ENSNamespaceId) => { - const ensroot = getDatasource(namespace, DatasourceNames.ENSRoot); +export const isENSv1Registry = (namespace: ENSNamespaceId, contract: AccountId) => + accountIdEqual(getENSv1Registry(namespace), contract); - return { - chainId: ensroot.chain.id, - address: ensroot.contracts.RootRegistry.address, - } satisfies AccountId; -}; +////////////// +// ENSv2 +////////////// + +/** + * Gets the AccountId representing the ENSv2 Root Registry in the selected `namespace`. + */ +export const getENSv2RootRegistry = (namespace: ENSNamespaceId) => + getDatasourceContract(namespace, DatasourceNames.ENSRoot, "RootRegistry"); /** * Gets the RegistryId representing the ENSv2 Root Registry in the selected `namespace`. */ -export const getRootRegistryId = (namespace: ENSNamespaceId) => - makeRegistryId(getRootRegistry(namespace)); +export const getENSv2RootRegistryId = (namespace: ENSNamespaceId) => + makeRegistryId(getENSv2RootRegistry(namespace)); /** * Determines whether `contract` is the ENSv2 Root Registry in `namespace`. */ -export const isRootRegistry = (namespace: ENSNamespaceId, contract: AccountId) => - accountIdEqual(getRootRegistry(namespace), contract); +export const isENSv2RootRegistry = (namespace: ENSNamespaceId, contract: AccountId) => + accountIdEqual(getENSv2RootRegistry(namespace), contract); From fad3f84801dc79e5d9300541e5c3592834e4cf61 Mon Sep 17 00:00:00 2001 From: shrugs Date: Sat, 27 Dec 2025 10:01:10 -0600 Subject: [PATCH 097/102] fix: revert can-accelerate testing --- .../middleware/can-accelerate.middleware.ts | 77 +++++++++++++++++-- 1 file changed, 71 insertions(+), 6 deletions(-) diff --git a/apps/ensapi/src/middleware/can-accelerate.middleware.ts b/apps/ensapi/src/middleware/can-accelerate.middleware.ts index 42144cdbb..9aee7188b 100644 --- a/apps/ensapi/src/middleware/can-accelerate.middleware.ts +++ b/apps/ensapi/src/middleware/can-accelerate.middleware.ts @@ -11,10 +11,10 @@ export type CanAccelerateMiddlewareVariables = { canAccelerate: boolean }; // TODO: expand this datamodel to include 'reasons' acceleration was disabled to drive ui -const didWarnCannotAccelerateENSv2 = false; -const didWarnNoProtocolAccelerationPlugin = false; -const didInitialCanAccelerate = false; -const prevCanAccelerate = false; +let didWarnCannotAccelerateENSv2 = false; +let didWarnNoProtocolAccelerationPlugin = false; +let didInitialCanAccelerate = false; +let prevCanAccelerate = false; /** * Middleware that determines if protocol acceleration can be enabled for the current request. @@ -24,8 +24,73 @@ const prevCanAccelerate = false; * resolution handlers. */ export const canAccelerateMiddleware = factory.createMiddleware(async (c, next) => { - if (true) { - c.set("canAccelerate", true); + // context must be set by the required middleware + if (c.var.isRealtime === undefined) { + throw new Error(`Invariant(canAccelerateMiddleware): isRealtime middleware required`); + } + + //////////////////////////// + /// Temporary ENSv2 Bailout + //////////////////////////// + // TODO: re-enable acceleration for ensv2 once implemented + if (config.ensIndexerPublicConfig.plugins.includes(PluginName.ENSv2)) { + if (!didWarnCannotAccelerateENSv2) { + logger.warn( + `ENSApi is currently unable to accelerate Resolution API requests while indexing ENSv2. Protocol Acceleration is DISABLED.`, + ); + + didWarnCannotAccelerateENSv2 = true; + } + + c.set("canAccelerate", false); return await next(); } + + ////////////////////////////////////////////// + /// Protocol Acceleration Plugin Availability + ////////////////////////////////////////////// + + const hasProtocolAccelerationPlugin = config.ensIndexerPublicConfig.plugins.includes( + PluginName.ProtocolAcceleration, + ); + + // log one warning to the console if !hasProtocolAccelerationPlugin + if (!didWarnNoProtocolAccelerationPlugin && !hasProtocolAccelerationPlugin) { + logger.warn( + `ENSApi is connected to an ENSIndexer that does NOT include the ${PluginName.ProtocolAcceleration} plugin: ENSApi will NOT be able to accelerate Resolution API requests, even if ?accelerate=true. Resolution requests will abide by the full Forward/Reverse Resolution specification, including RPC calls and CCIP-Read requests to external CCIP-Read Gateways.`, + ); + + didWarnNoProtocolAccelerationPlugin = true; + } + + ////////////////////////////// + /// Can Accelerate Derivation + ////////////////////////////// + + // the Resolution API can accelerate requests if + // a) ENSIndexer reports that it is within MAX_REALTIME_DISTANCE_TO_ACCELERATE of realtime, and + // b) ENSIndexer reports that it has the ProtocolAcceleration plugin enabled. + const canAccelerate = hasProtocolAccelerationPlugin && c.var.isRealtime; + + // log notice when acceleration begins + if ( + (!didInitialCanAccelerate && canAccelerate) || // first time + (didInitialCanAccelerate && !prevCanAccelerate && canAccelerate) // future change in status + ) { + logger.info(`Protocol Acceleration is now ENABLED.`); + } + + // log notice when acceleration ends + if ( + (!didInitialCanAccelerate && !canAccelerate) || // first time + (didInitialCanAccelerate && prevCanAccelerate && !canAccelerate) // future change in status + ) { + logger.info(`Protocol Acceleration is DISABLED.`); + } + + prevCanAccelerate = canAccelerate; + didInitialCanAccelerate = true; + + c.set("canAccelerate", canAccelerate); + await next(); }); From 66f0425efb518d3ff5e5ad979fbf14b89affb2b4 Mon Sep 17 00:00:00 2001 From: shrugs Date: Sun, 28 Dec 2025 10:14:05 -0600 Subject: [PATCH 098/102] move protocol-acceleration logic to shared and do runtime checks in forward-resolution --- .../get-records-from-index.ts | 7 +- .../src/lib/resolution/forward-resolution.ts | 238 +++++++++--------- .../resolver-db-helpers.ts | 20 +- packages/ensnode-sdk/src/internal.ts | 3 + .../is-bridged-resolver.ts | 20 +- .../is-ensip-19-reverse-resolver.ts | 11 +- .../is-static-resolver.ts | 15 +- 7 files changed, 157 insertions(+), 157 deletions(-) rename {apps/ensindexer/src/lib => packages/ensnode-sdk/src/shared}/protocol-acceleration/is-bridged-resolver.ts (79%) rename {apps/ensindexer/src/lib => packages/ensnode-sdk/src/shared}/protocol-acceleration/is-ensip-19-reverse-resolver.ts (78%) rename {apps/ensindexer/src/lib => packages/ensnode-sdk/src/shared}/protocol-acceleration/is-static-resolver.ts (80%) diff --git a/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts b/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts index 8eb1d2ff4..f7022606d 100644 --- a/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts +++ b/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts @@ -20,9 +20,8 @@ export async function getRecordsFromIndex { - const resolverId = makeResolverId(_resolver); const resolver = await db.query.resolver.findFirst({ - where: (t, { eq }) => eq(t.id, resolverId), + where: (t, { eq }) => eq(t.id, makeResolverId(_resolver)), }); if (!resolver) return null; @@ -44,7 +43,9 @@ export async function getRecordsFromIndex record.coinType === DEFAULT_EVM_COIN_TYPE_BIGINT, diff --git a/apps/ensapi/src/lib/resolution/forward-resolution.ts b/apps/ensapi/src/lib/resolution/forward-resolution.ts index efc556bb3..6e0b1ccdd 100644 --- a/apps/ensapi/src/lib/resolution/forward-resolution.ts +++ b/apps/ensapi/src/lib/resolution/forward-resolution.ts @@ -13,7 +13,6 @@ import { getENSv1Registry, isNormalizedName, isSelectionEmpty, - makeResolverId, type Node, PluginName, parseReverseName, @@ -21,9 +20,13 @@ import { type ResolverRecordsSelection, TraceableENSProtocol, } from "@ensnode/ensnode-sdk"; -import { isExtendedResolver } from "@ensnode/ensnode-sdk/internal"; +import { + isBridgedResolver, + isExtendedResolver, + isKnownENSIP19ReverseResolver, + isStaticResolver, +} from "@ensnode/ensnode-sdk/internal"; -import { db } from "@/lib/db"; import { withActiveSpanAsync, withSpanAsync } from "@/lib/instrumentation/auto-span"; import { makeLogger } from "@/lib/logger"; import { findResolver } from "@/lib/protocol-acceleration/find-resolver"; @@ -237,138 +240,123 @@ async function _resolveForward( // Protocol Acceleration ////////////////////////////////////////////////// if (accelerate && canAccelerate) { - const resolverId = makeResolverId({ chainId, address: activeResolver }); - const resolver = await db.query.resolver.findFirst({ - where: (t, { eq }) => eq(t.id, resolverId), - }); - - // Must have an indexed Resolver in order to accelerate further — otherwise fall back to - // Forward Resolution - if (resolver) { - ////////////////////////////////////////////////// - // Protocol Acceleration: ENSIP-19 Reverse Resolvers - // If the activeResolver is a Known ENSIP-19 Reverse Resolver, - // then we can just read the name record value directly from the index. - ////////////////////////////////////////////////// - if (resolver.isENSIP19ReverseResolver) { - return withEnsProtocolStep( - TraceableENSProtocol.ForwardResolution, - ForwardResolutionProtocolStep.AccelerateENSIP19ReverseResolver, - {}, - async () => { - // Invariant: consumer must be selecting the `name` record at this point - if (selection.name !== true) { - throw new Error( - `Invariant(ENSIP-19 Reverse Resolvers Protocol Acceleration): expected 'name' record in selection but instead received: ${JSON.stringify(selection)}.`, - ); - } - - // Sanity Check: This should only happen in the context of Reverse Resolution, and - // the selection should just be `{ name: true }`, but technically not prohibited to - // select more records than just 'name', so just warn if that happens. - if (selection.addresses !== undefined || selection.texts !== undefined) { - logger.warn( - `Sanity Check(ENSIP-19 Reverse Resolvers Protocol Acceleration): expected a selection of exactly '{ name: true }' but received ${JSON.stringify(selection)}.`, - ); - } - - // Invariant: the name in question should be an ENSIP-19 Reverse Name that we're able to parse - const parsed = parseReverseName(name); - if (!parsed) { - throw new Error( - `Invariant(ENSIP-19 Reverse Resolvers Protocol Acceleration): expected a valid ENSIP-19 Reverse Name but recieved '${name}'.`, - ); - } - - // retrieve the name record from the index - const nameRecordValue = await getENSIP19ReverseNameRecordFromIndex( - parsed.address, - parsed.coinType, + // NOTE: because Resolvers can exist without emitting events (and therefore may or may + // not actually exist in the index), we have to do runtime validation of the Resolver's + // metadata (i.e. whether it's an ENSIP-19 Reverse Resolver, a Bridged Resolver, etc) + // ex: BasenamesL1Resolver need not emit events to function properly + // https://etherscan.io/address/0xde9049636f4a1dfe0a64d1bfe3155c0a14c54f31#code + const resolver = { chainId, address: activeResolver }; + + ////////////////////////////////////////////////// + // Protocol Acceleration: ENSIP-19 Reverse Resolvers + // If the activeResolver is a Known ENSIP-19 Reverse Resolver, + // then we can just read the name record value directly from the index. + ////////////////////////////////////////////////// + if (isKnownENSIP19ReverseResolver(config.namespace, resolver)) { + return withEnsProtocolStep( + TraceableENSProtocol.ForwardResolution, + ForwardResolutionProtocolStep.AccelerateENSIP19ReverseResolver, + {}, + async () => { + // Invariant: consumer must be selecting the `name` record at this point + if (selection.name !== true) { + throw new Error( + `Invariant(ENSIP-19 Reverse Resolvers Protocol Acceleration): expected 'name' record in selection but instead received: ${JSON.stringify(selection)}.`, + ); + } + + // Sanity Check: This should only happen in the context of Reverse Resolution, and + // the selection should just be `{ name: true }`, but technically not prohibited to + // select more records than just 'name', so just warn if that happens. + if (selection.addresses !== undefined || selection.texts !== undefined) { + logger.warn( + `Sanity Check(ENSIP-19 Reverse Resolvers Protocol Acceleration): expected a selection of exactly '{ name: true }' but received ${JSON.stringify(selection)}.`, + ); + } + + // Invariant: the name in question should be an ENSIP-19 Reverse Name that we're able to parse + const parsed = parseReverseName(name); + if (!parsed) { + throw new Error( + `Invariant(ENSIP-19 Reverse Resolvers Protocol Acceleration): expected a valid ENSIP-19 Reverse Name but recieved '${name}'.`, ); + } + + // retrieve the name record from the index + const nameRecordValue = await getENSIP19ReverseNameRecordFromIndex( + parsed.address, + parsed.coinType, + ); + + // NOTE: typecast is ok because of sanity checks above + return { name: nameRecordValue } as ResolverRecordsResponse; + }, + ); + } - // NOTE: typecast is ok because of sanity checks above - return { name: nameRecordValue } as ResolverRecordsResponse; - }, - ); - } - - ////////////////////////////////////////////////// - // Protocol Acceleration: Bridged Resolvers - // If the activeResolver is a Bridged Resolver, - // then we can short-circuit the CCIP-Read and defer resolution to the indicated (shadow)Registry. - ////////////////////////////////////////////////// - if ( - resolver.bridgesToRegistryChainId !== null && - resolver.bridgesToRegistryAddress !== null - ) { - return withEnsProtocolStep( - TraceableENSProtocol.ForwardResolution, - ForwardResolutionProtocolStep.AccelerateKnownOffchainLookupResolver, - {}, - () => - _resolveForward(name, selection, { - ...options, - registry: { - // biome-ignore lint/style/noNonNullAssertion: null check above - chainId: resolver.bridgesToRegistryChainId!, - // biome-ignore lint/style/noNonNullAssertion: null check above - address: resolver.bridgesToRegistryAddress!, - }, - }), - ); - } - - addEnsProtocolStepEvent( - protocolTracingSpan, + ////////////////////////////////////////////////// + // Protocol Acceleration: Bridged Resolvers + // If the activeResolver is a Bridged Resolver, + // then we can short-circuit the CCIP-Read and defer resolution to the indicated (shadow)Registry. + ////////////////////////////////////////////////// + const bridgesTo = isBridgedResolver(config.namespace, resolver); + if (bridgesTo) { + return withEnsProtocolStep( TraceableENSProtocol.ForwardResolution, ForwardResolutionProtocolStep.AccelerateKnownOffchainLookupResolver, - false, + {}, + () => _resolveForward(name, selection, { ...options, registry: bridgesTo }), ); + } - ////////////////////////////////////////////////// - // Protocol Acceleration: Known On-Chain Static Resolvers - // If: - // 1) the ProtocolAcceleration Plugin indexes records for all Resolver contracts on - // this chain, and - // 2) the activeResolver is a Static Resolver, - // then we can retrieve records directly from the database. - ////////////////////////////////////////////////// - const resolverRecordsAreIndexed = - areResolverRecordsIndexedByProtocolAccelerationPluginOnChainId( - config.namespace, - chainId, - ); - - if (resolverRecordsAreIndexed && resolver.isStatic) { - return withEnsProtocolStep( - TraceableENSProtocol.ForwardResolution, - ForwardResolutionProtocolStep.AccelerateKnownOnchainStaticResolver, - {}, - async () => { - const resolver = await getRecordsFromIndex({ - resolver: { chainId, address: activeResolver }, - node, - selection, - }); - - // if resolver doesn't exist here, there are no records in the index - if (!resolver) { - return makeEmptyResolverRecordsResponse(selection); - } - - // format into RecordsResponse and return - return makeRecordsResponseFromIndexedRecords(selection, resolver); - }, - ); - } - - addEnsProtocolStepEvent( - protocolTracingSpan, + addEnsProtocolStepEvent( + protocolTracingSpan, + TraceableENSProtocol.ForwardResolution, + ForwardResolutionProtocolStep.AccelerateKnownOffchainLookupResolver, + false, + ); + + ////////////////////////////////////////////////// + // Protocol Acceleration: Known On-Chain Static Resolvers + // If: + // 1) the ProtocolAcceleration Plugin indexes records for all Resolver contracts on + // this chain, and + // 2) the activeResolver is a Static Resolver, + // then we can retrieve records directly from the database. + ////////////////////////////////////////////////// + const resolverRecordsAreIndexed = + areResolverRecordsIndexedByProtocolAccelerationPluginOnChainId( + config.namespace, + chainId, + ); + + if (resolverRecordsAreIndexed && isStaticResolver(config.namespace, resolver)) { + return withEnsProtocolStep( TraceableENSProtocol.ForwardResolution, ForwardResolutionProtocolStep.AccelerateKnownOnchainStaticResolver, - false, + {}, + async () => { + const records = await getRecordsFromIndex({ + resolver: { chainId, address: activeResolver }, + node, + selection, + }); + + // if resolver doesn't exist here, there are no records in the index + if (!records) return makeEmptyResolverRecordsResponse(selection); + + // otherwise, format into RecordsResponse and return + return makeRecordsResponseFromIndexedRecords(selection, records); + }, ); } + + addEnsProtocolStepEvent( + protocolTracingSpan, + TraceableENSProtocol.ForwardResolution, + ForwardResolutionProtocolStep.AccelerateKnownOnchainStaticResolver, + false, + ); } ////////////////////////////////////////////////// diff --git a/apps/ensindexer/src/lib/protocol-acceleration/resolver-db-helpers.ts b/apps/ensindexer/src/lib/protocol-acceleration/resolver-db-helpers.ts index 3af940e85..cb4aeb4d0 100644 --- a/apps/ensindexer/src/lib/protocol-acceleration/resolver-db-helpers.ts +++ b/apps/ensindexer/src/lib/protocol-acceleration/resolver-db-helpers.ts @@ -1,3 +1,5 @@ +import config from "@/config"; + import type { Context } from "ponder:registry"; import schema from "ponder:schema"; import type { Address } from "viem"; @@ -14,17 +16,15 @@ import { interpretNameRecordValue, interpretTextRecordKey, interpretTextRecordValue, + isBridgedResolver, isDedicatedResolver, isExtendedResolver, + isKnownENSIP19ReverseResolver, + isStaticResolver, + staticResolverImplementsAddressRecordDefaulting, } from "@ensnode/ensnode-sdk/internal"; import type { EventWithArgs } from "@/lib/ponder-helpers"; -import { isBridgedResolver } from "@/lib/protocol-acceleration/is-bridged-resolver"; -import { isKnownENSIP19ReverseResolver } from "@/lib/protocol-acceleration/is-ensip-19-reverse-resolver"; -import { - isStaticResolver, - staticResolverImplementsAddressRecordDefaulting, -} from "@/lib/protocol-acceleration/is-static-resolver"; /** * Infer the type of the ResolverRecord entity's composite key. @@ -68,12 +68,12 @@ export async function ensureResolver(context: Context, resolver: AccountId) { publicClient: context.client, }); - const isENSIP19ReverseResolver = isKnownENSIP19ReverseResolver(resolver); - const bridgesToRegistry = isBridgedResolver(resolver); - const isStatic = isStaticResolver(resolver); + const isENSIP19ReverseResolver = isKnownENSIP19ReverseResolver(config.namespace, resolver); + const bridgesToRegistry = isBridgedResolver(config.namespace, resolver); + const isStatic = isStaticResolver(config.namespace, resolver); const implementsAddressRecordDefaulting = isStatic - ? staticResolverImplementsAddressRecordDefaulting(resolver) + ? staticResolverImplementsAddressRecordDefaulting(config.namespace, resolver) : null; // ensure Resolver diff --git a/packages/ensnode-sdk/src/internal.ts b/packages/ensnode-sdk/src/internal.ts index e1faf4c87..4eb50bc54 100644 --- a/packages/ensnode-sdk/src/internal.ts +++ b/packages/ensnode-sdk/src/internal.ts @@ -33,6 +33,9 @@ export * from "./shared/config-templates"; export * from "./shared/datasources-with-resolvers"; export * from "./shared/interpretation/interpret-record-values"; export * from "./shared/log-level"; +export * from "./shared/protocol-acceleration/is-bridged-resolver"; +export * from "./shared/protocol-acceleration/is-ensip-19-reverse-resolver"; +export * from "./shared/protocol-acceleration/is-static-resolver"; export * from "./shared/thegraph"; export * from "./shared/zod-schemas"; export * from "./shared/zod-types"; diff --git a/apps/ensindexer/src/lib/protocol-acceleration/is-bridged-resolver.ts b/packages/ensnode-sdk/src/shared/protocol-acceleration/is-bridged-resolver.ts similarity index 79% rename from apps/ensindexer/src/lib/protocol-acceleration/is-bridged-resolver.ts rename to packages/ensnode-sdk/src/shared/protocol-acceleration/is-bridged-resolver.ts index 9468368f6..99377cfe5 100644 --- a/apps/ensindexer/src/lib/protocol-acceleration/is-bridged-resolver.ts +++ b/packages/ensnode-sdk/src/shared/protocol-acceleration/is-bridged-resolver.ts @@ -1,7 +1,10 @@ -import config from "@/config"; - import { DatasourceNames } from "@ensnode/datasources"; -import { type AccountId, getDatasourceContract, makeContractMatcher } from "@ensnode/ensnode-sdk"; +import { + type AccountId, + type ENSNamespaceId, + getDatasourceContract, + makeContractMatcher, +} from "@ensnode/ensnode-sdk"; /** * For a given `resolver`, if it is a known Bridged Resolver, return the @@ -26,17 +29,20 @@ import { type AccountId, getDatasourceContract, makeContractMatcher } from "@ens * * TODO: these relationships could/should be encoded in an ENSIP */ -export function isBridgedResolver(resolver: AccountId): AccountId | null { - const resolverEq = makeContractMatcher(config.namespace, resolver); +export function isBridgedResolver( + namespace: ENSNamespaceId, + resolver: AccountId, +): AccountId | null { + const resolverEq = makeContractMatcher(namespace, resolver); // the ENSRoot's BasenamesL1Resolver bridges to the Basenames (shadow)Registry if (resolverEq(DatasourceNames.ENSRoot, "BasenamesL1Resolver")) { - return getDatasourceContract(config.namespace, DatasourceNames.Basenames, "Registry"); + return getDatasourceContract(namespace, DatasourceNames.Basenames, "Registry"); } // the ENSRoot's LineanamesL1Resolver bridges to the Lineanames (shadow)Registry if (resolverEq(DatasourceNames.ENSRoot, "LineanamesL1Resolver")) { - return getDatasourceContract(config.namespace, DatasourceNames.Lineanames, "Registry"); + return getDatasourceContract(namespace, DatasourceNames.Lineanames, "Registry"); } // TODO: ThreeDNS diff --git a/apps/ensindexer/src/lib/protocol-acceleration/is-ensip-19-reverse-resolver.ts b/packages/ensnode-sdk/src/shared/protocol-acceleration/is-ensip-19-reverse-resolver.ts similarity index 78% rename from apps/ensindexer/src/lib/protocol-acceleration/is-ensip-19-reverse-resolver.ts rename to packages/ensnode-sdk/src/shared/protocol-acceleration/is-ensip-19-reverse-resolver.ts index 05e08564f..57b271884 100644 --- a/apps/ensindexer/src/lib/protocol-acceleration/is-ensip-19-reverse-resolver.ts +++ b/packages/ensnode-sdk/src/shared/protocol-acceleration/is-ensip-19-reverse-resolver.ts @@ -1,7 +1,5 @@ -import config from "@/config"; - import { DatasourceNames } from "@ensnode/datasources"; -import { type AccountId, makeContractMatcher } from "@ensnode/ensnode-sdk"; +import { type AccountId, type ENSNamespaceId, makeContractMatcher } from "@ensnode/ensnode-sdk"; /** * ENSIP-19 Reverse Resolvers (i.e. DefaultReverseResolver or ChainReverseResolver) simply: @@ -10,8 +8,11 @@ import { type AccountId, makeContractMatcher } from "@ensnode/ensnode-sdk"; * * We encode this behavior here, for the purposes of Protocol Acceleration. */ -export function isKnownENSIP19ReverseResolver(resolver: AccountId): boolean { - const resolverEq = makeContractMatcher(config.namespace, resolver); +export function isKnownENSIP19ReverseResolver( + namespace: ENSNamespaceId, + resolver: AccountId, +): boolean { + const resolverEq = makeContractMatcher(namespace, resolver); return [ // DefaultReverseResolver (default.reverse) diff --git a/apps/ensindexer/src/lib/protocol-acceleration/is-static-resolver.ts b/packages/ensnode-sdk/src/shared/protocol-acceleration/is-static-resolver.ts similarity index 80% rename from apps/ensindexer/src/lib/protocol-acceleration/is-static-resolver.ts rename to packages/ensnode-sdk/src/shared/protocol-acceleration/is-static-resolver.ts index 5c5c1be9b..c1b519b77 100644 --- a/apps/ensindexer/src/lib/protocol-acceleration/is-static-resolver.ts +++ b/packages/ensnode-sdk/src/shared/protocol-acceleration/is-static-resolver.ts @@ -1,7 +1,5 @@ -import config from "@/config"; - import { DatasourceNames } from "@ensnode/datasources"; -import { type AccountId, makeContractMatcher } from "@ensnode/ensnode-sdk"; +import { type AccountId, type ENSNamespaceId, makeContractMatcher } from "@ensnode/ensnode-sdk"; /** * Returns whether `resolver` is an Static Resolver. @@ -16,8 +14,8 @@ import { type AccountId, makeContractMatcher } from "@ensnode/ensnode-sdk"; * * TODO: these relationships could be encoded in an ENSIP */ -export function isStaticResolver(resolver: AccountId): boolean { - const resolverEq = makeContractMatcher(config.namespace, resolver); +export function isStaticResolver(namespace: ENSNamespaceId, resolver: AccountId): boolean { + const resolverEq = makeContractMatcher(namespace, resolver); return [ // ENS Root Chain @@ -36,8 +34,11 @@ export function isStaticResolver(resolver: AccountId): boolean { * * @see https://docs.ens.domains/ensip/19/#default-address */ -export function staticResolverImplementsAddressRecordDefaulting(resolver: AccountId): boolean { - const resolverEq = makeContractMatcher(config.namespace, resolver); +export function staticResolverImplementsAddressRecordDefaulting( + namespace: ENSNamespaceId, + resolver: AccountId, +): boolean { + const resolverEq = makeContractMatcher(namespace, resolver); return [ // ENS Root Chain From fdd5dfe62e59f1fa2fc6b1994f1e5155557394d4 Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 29 Dec 2025 12:16:00 -0600 Subject: [PATCH 099/102] fix: remove runtime-dependent resolver logic from index --- apps/ensapi/src/graphql-api/builder.ts | 2 + apps/ensapi/src/graphql-api/schema/query.ts | 4 +- .../ensapi/src/graphql-api/schema/resolver.ts | 34 ++++++++--------- .../src/handlers/ensnode-graphql-api.ts | 5 +++ .../get-records-from-index.ts | 7 ++-- .../resolver-db-helpers.ts | 38 +++++-------------- .../src/schemas/ensv2.schema.ts | 4 -- .../schemas/protocol-acceleration.schema.ts | 31 ++++----------- 8 files changed, 45 insertions(+), 80 deletions(-) diff --git a/apps/ensapi/src/graphql-api/builder.ts b/apps/ensapi/src/graphql-api/builder.ts index f8dc7b94f..e659cc071 100644 --- a/apps/ensapi/src/graphql-api/builder.ts +++ b/apps/ensapi/src/graphql-api/builder.ts @@ -7,6 +7,7 @@ import type { ChainId, CoinType, DomainId, + ENSNamespaceId, InterpretedName, Node, RegistryId, @@ -15,6 +16,7 @@ import type { export const builder = new SchemaBuilder<{ Context: { + namespace: ENSNamespaceId; now: bigint; }; Scalars: { diff --git a/apps/ensapi/src/graphql-api/schema/query.ts b/apps/ensapi/src/graphql-api/schema/query.ts index 417b033ba..95bb8bf0d 100644 --- a/apps/ensapi/src/graphql-api/schema/query.ts +++ b/apps/ensapi/src/graphql-api/schema/query.ts @@ -1,5 +1,3 @@ -import config from "@/config"; - import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay"; import { @@ -203,7 +201,7 @@ builder.queryType({ description: "TODO", type: RegistryRef, nullable: false, - resolve: () => getENSv2RootRegistryId(config.namespace), + resolve: (parent, args, context) => getENSv2RootRegistryId(context.namespace), }), }), }); diff --git a/apps/ensapi/src/graphql-api/schema/resolver.ts b/apps/ensapi/src/graphql-api/schema/resolver.ts index b34fce8d7..100face5d 100644 --- a/apps/ensapi/src/graphql-api/schema/resolver.ts +++ b/apps/ensapi/src/graphql-api/schema/resolver.ts @@ -5,11 +5,11 @@ import { makePermissionsId, makeResolverRecordsId, NODE_ANY, - type RequiredAndNotNull, type ResolverId, type ResolverRecordsId, ROOT_RESOURCE, } from "@ensnode/ensnode-sdk"; +import { isBridgedResolver } from "@ensnode/ensnode-sdk/internal"; import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-model-id"; @@ -22,11 +22,18 @@ import { PermissionsRef } from "@/graphql-api/schema/permissions"; import { ResolverRecordsRef } from "@/graphql-api/schema/resolver-records"; import { db } from "@/lib/db"; -const isDedicatedResolver = (resolver: Resolver): resolver is DedicatedResolver => - resolver.isDedicated === true; - -const isBridgedResolver = (resolver: Resolver): resolver is BridgedResolver => - resolver.bridgesToRegistryChainId !== null && resolver.bridgesToRegistryAddress !== null; +/** + * Note that this indexed Resolver entity represents not _all_ Resolver contracts that exist onchain, + * but the set of Resolver contracts that have emitted at least one event that we are able to index. + * + * This means that if one were to access a Resolver contract that _does_ exist on-chain, but hasn't + * emitted any events (ex: BasenamesL1Resolver), this API (which retrieves data from the index) + * would say that it doesn't exist. + * + * This limitation has always been the case, including for the legacy ENS Subgraph, and would require + * an RPC call to the chain be performed in the case that a Resolver doesn't exist in the index, which + * is prohibitive in both cost and latency. As such we acknowledge this limitation here, for now. + */ export const ResolverRef = builder.loadableObjectRef("Resolver", { load: (ids: ResolverId[]) => @@ -39,11 +46,6 @@ export const ResolverRef = builder.loadableObjectRef("Resolver", { }); export type Resolver = Exclude; -export type DedicatedResolver = Omit & { isDedicated: true }; -export type BridgedResolver = RequiredAndNotNull< - Resolver, - "bridgesToRegistryChainId" | "bridgesToRegistryAddress" ->; //////////// // Resolver @@ -129,7 +131,7 @@ ResolverRef.implement({ description: "TODO", type: DedicatedResolverMetadataRef, nullable: true, - resolve: (parent) => (isDedicatedResolver(parent) ? parent : null), + resolve: (parent) => (parent.isDedicated ? parent : null), }), //////////////////// @@ -139,13 +141,7 @@ ResolverRef.implement({ description: "TODO", type: AccountIdRef, nullable: true, - resolve: (parent) => { - if (!isBridgedResolver(parent)) return null; - return { - chainId: parent.bridgesToRegistryChainId, - address: parent.bridgesToRegistryAddress, - }; - }, + resolve: (parent, args, context) => isBridgedResolver(context.namespace, parent), }), //////////////////////// diff --git a/apps/ensapi/src/handlers/ensnode-graphql-api.ts b/apps/ensapi/src/handlers/ensnode-graphql-api.ts index e00f0c9bc..3005c9e5f 100644 --- a/apps/ensapi/src/handlers/ensnode-graphql-api.ts +++ b/apps/ensapi/src/handlers/ensnode-graphql-api.ts @@ -2,6 +2,8 @@ // import { maxDepthPlugin } from "@escape.tech/graphql-armor-max-depth"; // import { maxTokensPlugin } from "@escape.tech/graphql-armor-max-tokens"; +import config from "@/config"; + import { getUnixTime } from "date-fns"; import { createYoga } from "graphql-yoga"; @@ -16,6 +18,9 @@ const yoga = createYoga({ graphqlEndpoint: "*", schema, context: () => ({ + // inject config's namespace into context, feel cleaner than accessing from @/config directly + namespace: config.namespace, + // generate a bigint UnixTimestamp per-request for handlers to use now: BigInt(getUnixTime(new Date())), }), diff --git a/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts b/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts index f7022606d..ef7ef6896 100644 --- a/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts +++ b/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts @@ -1,3 +1,5 @@ +import config from "@/config"; + import { type AccountId, DEFAULT_EVM_COIN_TYPE, @@ -5,6 +7,7 @@ import { type Node, type ResolverRecordsSelection, } from "@ensnode/ensnode-sdk"; +import { staticResolverImplementsAddressRecordDefaulting } from "@ensnode/ensnode-sdk/internal"; import { db } from "@/lib/db"; import type { IndexedResolverRecords } from "@/lib/resolution/make-records-response"; @@ -43,9 +46,7 @@ export async function getRecordsFromIndex record.coinType === DEFAULT_EVM_COIN_TYPE_BIGINT, diff --git a/apps/ensindexer/src/lib/protocol-acceleration/resolver-db-helpers.ts b/apps/ensindexer/src/lib/protocol-acceleration/resolver-db-helpers.ts index cb4aeb4d0..b48f65c43 100644 --- a/apps/ensindexer/src/lib/protocol-acceleration/resolver-db-helpers.ts +++ b/apps/ensindexer/src/lib/protocol-acceleration/resolver-db-helpers.ts @@ -1,5 +1,3 @@ -import config from "@/config"; - import type { Context } from "ponder:registry"; import schema from "ponder:schema"; import type { Address } from "viem"; @@ -16,12 +14,8 @@ import { interpretNameRecordValue, interpretTextRecordKey, interpretTextRecordValue, - isBridgedResolver, isDedicatedResolver, isExtendedResolver, - isKnownENSIP19ReverseResolver, - isStaticResolver, - staticResolverImplementsAddressRecordDefaulting, } from "@ensnode/ensnode-sdk/internal"; import type { EventWithArgs } from "@/lib/ponder-helpers"; @@ -58,23 +52,16 @@ export async function ensureResolver(context: Context, resolver: AccountId) { const existing = await context.db.find(schema.resolver, { id: resolverId }); if (existing) return; - const isExtended = await isExtendedResolver({ - address: resolver.address, - publicClient: context.client, - }); - - const isDedicated = await isDedicatedResolver({ - address: resolver.address, - publicClient: context.client, - }); - - const isENSIP19ReverseResolver = isKnownENSIP19ReverseResolver(config.namespace, resolver); - const bridgesToRegistry = isBridgedResolver(config.namespace, resolver); - const isStatic = isStaticResolver(config.namespace, resolver); - - const implementsAddressRecordDefaulting = isStatic - ? staticResolverImplementsAddressRecordDefaulting(config.namespace, resolver) - : null; + const [isExtended, isDedicated] = await Promise.all([ + isExtendedResolver({ + address: resolver.address, + publicClient: context.client, + }), + isDedicatedResolver({ + address: resolver.address, + publicClient: context.client, + }), + ]); // ensure Resolver await context.db.insert(schema.resolver).values({ @@ -82,11 +69,6 @@ export async function ensureResolver(context: Context, resolver: AccountId) { ...resolver, isExtended, isDedicated, - isStatic, - isENSIP19ReverseResolver, - implementsAddressRecordDefaulting, - bridgesToRegistryChainId: bridgesToRegistry?.chainId ?? null, - bridgesToRegistryAddress: bridgesToRegistry?.address ?? null, }); } diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/schemas/ensv2.schema.ts index 670a44c1f..490e45b13 100644 --- a/packages/ensnode-schema/src/schemas/ensv2.schema.ts +++ b/packages/ensnode-schema/src/schemas/ensv2.schema.ts @@ -48,10 +48,6 @@ import type { * * Many datamodels are sharable between ENSv1 and ENSv2, including Registrations, Renewals, and Resolvers. * - * Resolvers implement 'extensions' more so than polymorphism — a Resovler can abide by many - * permutations of behavior (IExtendedResolver, IDedicatedResolver, BridgedResolver, ...etc), that are - * not technically mutually exclusive, as they'd be with a truly polymorphic entity. - * * Registrations are polymorphic between the defined RegistrationTypes, depending on the associated * guarantees (for example, ENSv1 BaseRegistrar Registrations may have a gracePeriod, but ENSv2 * Registry Registrations do not). diff --git a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts index 9b9415ee4..6a685436c 100644 --- a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts +++ b/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts @@ -77,8 +77,14 @@ export const domainResolverRelation_relations = relations(domainResolverRelation })); /** - * Resolver represents an individual IResolver contract. It tracks metadata about the Resolver as well, - * for example whether it is an IExtendedResolver, IDedicatedResolver, a BridgedResolver, etc). + * Resolver represents an individual IResolver contract that has emitted at least 1 event. + * Note that Resolver contracts can exist on-chain but not emit any events and still function + * properly, so checks against a Resolver's existence and metadata must be done at runtime. + * + * We index whether a Resolver is an IExtendedResolver or an IDedicatedResolver, to minimize RPC + * requests at the API layer, when resolving resources, but note that runtime operations like + * Forward Resolution _must_ query a Resolver's existence against the chain, because the indexed set + * of Resolvers is a subset of the theoretical set of functional Resolvers deployed to a given chain. */ export const resolver = onchainTable( "resolvers", @@ -98,27 +104,6 @@ export const resolver = onchainTable( * Whether the Resolver implements IDedicatedResolver. */ isDedicated: t.boolean().notNull().default(false), - - /** - * Whether the Resolver is an Onchain Static Resolver. - */ - isStatic: t.boolean().notNull().default(false), - - /** - * Whether the Resolver is an ENSIP19ReverseResolver. - */ - isENSIP19ReverseResolver: t.boolean().default(false), - - /** - * If dedicated or static, whether the Resolver implements Address Record Defaulting. - */ - implementsAddressRecordDefaulting: t.boolean(), - - /** - * If set, the Resolver is a Bridged Resolver that bridges to the AccountId indicated. - */ - bridgesToRegistryChainId: t.text().$type(), - bridgesToRegistryAddress: t.hex().$type
(), }), (t) => ({ byId: uniqueIndex().on(t.chainId, t.address), From 5e00ae5f7614c9f37ae1b3c521bcb94fb471f175 Mon Sep 17 00:00:00 2001 From: shrugs Date: Tue, 30 Dec 2025 13:07:25 -0600 Subject: [PATCH 100/102] fix: update RegistryOld to new naming --- .../src/lib/protocol-acceleration/find-resolver.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts b/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts index eb7485770..0e37f6115 100644 --- a/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts +++ b/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts @@ -44,7 +44,11 @@ const NULL_RESULT: FindResolverResult = { const tracer = trace.getTracer("find-resolver"); -const RegistryOld = getDatasourceContract(config.namespace, DatasourceNames.ENSRoot, "RegistryOld"); +const ENSv1RegistryOld = getDatasourceContract( + config.namespace, + DatasourceNames.ENSRoot, + "ENSv1RegistryOld", +); /** * Identifies `name`'s active resolver in `registry`. @@ -217,7 +221,10 @@ async function findResolverWithIndex( and(eq(t.chainId, registry.chainId), eq(t.address, registry.address)), // OR, if the registry is the ENS Root Registry, also include records from RegistryOld isENSv1Registry(config.namespace, registry) && - and(eq(t.chainId, RegistryOld.chainId), eq(t.address, RegistryOld.address)), + and( + eq(t.chainId, ENSv1RegistryOld.chainId), + eq(t.address, ENSv1RegistryOld.address), + ), ].filter((c) => !!c), ), // filter for Domain-Resolver Relations for the following DomainIds From f860fddda255752b6503056c74458deb852d40af Mon Sep 17 00:00:00 2001 From: shrugs Date: Tue, 30 Dec 2025 16:07:00 -0600 Subject: [PATCH 101/102] fix: use raw params correctly --- apps/ensapi/src/handlers/resolution-api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ensapi/src/handlers/resolution-api.ts b/apps/ensapi/src/handlers/resolution-api.ts index 42c05558c..c131d17be 100644 --- a/apps/ensapi/src/handlers/resolution-api.ts +++ b/apps/ensapi/src/handlers/resolution-api.ts @@ -69,7 +69,7 @@ app.get( "query", z .object({ - selection: params.selection, + ...params.selectionParams.shape, trace: params.trace, accelerate: params.accelerate, }) From d0d0e02eef8ee1df55efe1de2b8fc45a6406eb97 Mon Sep 17 00:00:00 2001 From: shrugs Date: Wed, 31 Dec 2025 16:42:50 -0600 Subject: [PATCH 102/102] don't need to fetch resolver entity silly --- .../get-records-from-index.ts | 61 ++++++++----------- 1 file changed, 27 insertions(+), 34 deletions(-) diff --git a/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts b/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts index ef7ef6896..5b3c1969f 100644 --- a/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts +++ b/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts @@ -15,7 +15,7 @@ import type { IndexedResolverRecords } from "@/lib/resolution/make-records-respo const DEFAULT_EVM_COIN_TYPE_BIGINT = BigInt(DEFAULT_EVM_COIN_TYPE); export async function getRecordsFromIndex({ - resolver: _resolver, + resolver, node, selection, }: { @@ -23,49 +23,42 @@ export async function getRecordsFromIndex { - const resolver = await db.query.resolver.findFirst({ - where: (t, { eq }) => eq(t.id, makeResolverId(_resolver)), - }); - - if (!resolver) return null; - - const records = await db.query.resolverRecords.findFirst({ - where: (resolver, { and, eq }) => + const records = (await db.query.resolverRecords.findFirst({ + where: (t, { and, eq }) => and( - eq(resolver.chainId, resolver.chainId), - eq(resolver.address, resolver.address), - eq(resolver.node, node), + // filter by specific resolver + eq(t.chainId, resolver.chainId), + eq(t.address, resolver.address), + // filter by specific node + eq(t.node, node), ), columns: { name: true }, with: { addressRecords: true, textRecords: true }, - }); + })) as IndexedResolverRecords | undefined; - const resolverRecords = records as IndexedResolverRecords | undefined; + // no records found + if (!records) return null; - if (!resolverRecords) return null; + // if the resolver doesn't implement address record defaulting, return records as-is + if (!staticResolverImplementsAddressRecordDefaulting(config.namespace, resolver)) return records; - // if the resolver implements address record defaulting, materialize all selected address records - // that do not yet exist - if (staticResolverImplementsAddressRecordDefaulting(config.namespace, resolver)) { - if (selection.addresses) { - const defaultRecord = resolverRecords.addressRecords.find( - (record) => record.coinType === DEFAULT_EVM_COIN_TYPE_BIGINT, - ); + // otherwise, materialize all selected address records that do not yet exist + if (selection.addresses) { + const defaultRecord = records.addressRecords.find( + (record) => record.coinType === DEFAULT_EVM_COIN_TYPE_BIGINT, + ); - for (const coinType of selection.addresses) { - const _coinType = BigInt(coinType); - const existing = resolverRecords.addressRecords.find( - (record) => record.coinType === _coinType, - ); - if (!existing && defaultRecord) { - resolverRecords.addressRecords.push({ - value: defaultRecord.value, - coinType: _coinType, - }); - } + for (const coinType of selection.addresses) { + const _coinType = BigInt(coinType); + const existing = records.addressRecords.find((record) => record.coinType === _coinType); + if (!existing && defaultRecord) { + records.addressRecords.push({ + value: defaultRecord.value, + coinType: _coinType, + }); } } } - return resolverRecords; + return records; }