diff --git a/apps/api/.gitignore b/apps/api/.gitignore new file mode 100644 index 000000000..506e4c37e --- /dev/null +++ b/apps/api/.gitignore @@ -0,0 +1,2 @@ +# deps +node_modules/ diff --git a/apps/api/LICENSE b/apps/api/LICENSE new file mode 100644 index 000000000..24d66814d --- /dev/null +++ b/apps/api/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 NameHash + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/api/README.md b/apps/api/README.md new file mode 100644 index 000000000..ebb5b228f --- /dev/null +++ b/apps/api/README.md @@ -0,0 +1,13 @@ +# ENSNode API + +API Server for ENSNode + +## Documentation + +For detailed documentation and guides, see the [ENSNode Documentation](https://ensnode.io/ensnode). + +## License + +Licensed under the MIT License, Copyright © 2025-present [NameHash Labs](https://namehashlabs.org). + +See [LICENSE](./LICENSE) for more information. diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 000000000..c1a10cc6b --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,23 @@ +{ + "name": "@ensnode/api", + "type": "module", + "scripts": { + "start": "bun run src/index.ts", + "dev": "bun run --hot src/index.ts" + }, + "dependencies": { + "@ensdomains/ensjs": "^4.0.2", + "@ensnode/ponder-schema": "workspace:*", + "@ensnode/utils": "workspace:*", + "@ponder/client": "catalog:", + "@ponder/utils": "catalog:", + "bun": "^1.2.2", + "drizzle-orm": "catalog:", + "hono": "catalog:", + "viem": "catalog:" + }, + "devDependencies": { + "@ensnode/shared-configs": "workspace:*", + "@types/bun": "latest" + } +} diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts new file mode 100644 index 000000000..1672e36d6 --- /dev/null +++ b/apps/api/src/index.ts @@ -0,0 +1,21 @@ +import { Hono } from "hono"; +import { cors } from "hono/cors"; +import { proxy } from "hono/proxy"; + +import v1 from "./v1"; + +const app = new Hono(); + +// use cors +app.use(cors({ origin: "*" })); + +// TODO: ENSNode-api should be the exclusive api entrypoint for ENSNode +// https://hono.dev/examples/proxy +// - proxy /ponder, /subgraph, /sql/* endpoints to ensindexer + +app.route("/api/v1", v1); + +export default { + port: 3289, + fetch: app.fetch, +}; diff --git a/apps/api/src/lib/db.ts b/apps/api/src/lib/db.ts new file mode 100644 index 000000000..d16411694 --- /dev/null +++ b/apps/api/src/lib/db.ts @@ -0,0 +1,20 @@ +import * as _schema from "@ensnode/ponder-schema"; +import { Table, is } from "drizzle-orm"; +// import { setDatabaseSchema } from "@ponder/client"; +import { drizzle } from "drizzle-orm/node-postgres"; + +const setDatabaseSchema = ( + schema: T, + schemaName: string, +): T => { + for (const table of Object.values(schema)) { + if (is(table, Table)) { + // Use type assertion to fix the TypeScript error + (table as any)[Symbol.for("drizzle:Schema")] = schemaName; + } + } + return schema; +}; + +export const schema = setDatabaseSchema(_schema, Bun.env.DATABASE_SCHEMA || "public"); +export const db = drizzle(Bun.env.DATABASE_URL, { schema, casing: "snake_case", logger: true }); diff --git a/apps/api/src/lib/get-domain.ts b/apps/api/src/lib/get-domain.ts new file mode 100644 index 000000000..50e8acbea --- /dev/null +++ b/apps/api/src/lib/get-domain.ts @@ -0,0 +1,103 @@ +import { sql } from "drizzle-orm"; + +import { CAIP10AccountId, LabelHash } from "@ensnode/utils/types"; +import { HTTPException } from "hono/http-exception"; +import { hexToBigInt } from "viem"; +import { db, schema } from "./db"; +import { parseName } from "./parse-name"; + +// TODO: configure this correctly, likely constructing the root registry id from the relevant ens deployment +const ROOT_REGISTRY = "eip155:11155111:0xc44D7201065190B290Aaaf6efaDFD49d530547A3"; + +// TODO: de-duplicate these helpers with @ensnode/utils +const LABEL_HASH_MASK = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000n; +const maskTokenId = (tokenId: bigint) => tokenId & LABEL_HASH_MASK; +const labelHashToTokenId = (labelHash: LabelHash) => hexToBigInt(labelHash, { size: 32 }); + +/** + * gets a Domain from the tree if it exists using recursive CTE, traversing from RootRegistry + */ +export async function getDomainAndPath(name: string) { + const tokenIds = parseName(name) // given a set of labelhashes + .toReversed() // reverse for path + .map((labelHash) => maskTokenId(labelHashToTokenId(labelHash))); // convert to masked bigint tokenId + + if (tokenIds.length === 0) { + throw new Error(`getDomainAndPath: name "${name}" did not contain any segments?`); + } + + console.log({ + name, + tokenIdsReversed: tokenIds, + }); + + // https://github.com/drizzle-team/drizzle-orm/issues/1289 + // https://github.com/drizzle-team/drizzle-orm/issues/1589 + const rawTokenIdsArray = sql.raw(`ARRAY[${tokenIds.join(", ")}]::numeric[]`); + + const result = await db.execute(sql` + WITH RECURSIVE path_traversal AS ( + -- Base case: Start with RootRegistry + SELECT + r.id AS "registry_id", + NULL::text AS "domain_id", + NULL::numeric(78,0) AS "masked_token_id", + NULL::numeric(78,0) AS "token_id", + NULL::text AS "label", + 0 AS depth + -- ARRAY[]::numeric[] AS traversed_path + FROM + ${schema.v2_registry} r + WHERE + r.id = ${ROOT_REGISTRY} + + UNION ALL + + -- Recursive case: Find matching domain + SELECT + d."subregistry_id" AS "registry_id", + d.id AS "domain_id", + d."masked_token_id", + d."token_id", + d.label, + pt.depth + 1 AS depth + -- pt.traversed_path || d."masked_token_id": :numeric AS traversed_path + FROM + path_traversal pt + JOIN + ${schema.v2_domain} d ON d."registry_id" = pt."registry_id" + WHERE + d."masked_token_id" = (${rawTokenIdsArray})[pt.depth + 1] + AND pt.depth < array_length(${rawTokenIdsArray}, 1) + ) + + SELECT * FROM path_traversal + WHERE domain_id IS NOT NULL -- only return domains, not root registry + ORDER BY depth + `); + + // TODO: idk type this correctly + const rows = result.rows as { + registry_id: CAIP10AccountId; + domain_id: string; + masked_token_id: string; + token_id: string; + label: string; + depth: number; + }[]; + + // the domain in question was found iff the path has exactly the correct number of nodes + const exists = rows.length > 0 && rows.length === tokenIds.length; + if (!exists) throw new HTTPException(404, { message: "Domain not found." }); + + const lastRow = rows[rows.length - 1]!; // NOTE: must exist given length check above + if (lastRow.domain_id === null) throw new Error(`Expected domain_id`); + + // the last element is the node and it exists in the tree + return { + path: rows, + domain: await db.query.v2_domain.findFirst({ + where: (t, { eq }) => eq(t.id, lastRow.domain_id), + }), + }; +} diff --git a/apps/api/src/lib/get-records.ts b/apps/api/src/lib/get-records.ts new file mode 100644 index 000000000..4fc1284c3 --- /dev/null +++ b/apps/api/src/lib/get-records.ts @@ -0,0 +1,10 @@ +import { Node } from "@ensnode/utils/types"; +import { db } from "./db"; + +export async function getResolverRecords(resolverId: string, node: Node) { + return await db.query.v2_resolverRecords.findFirst({ + // TODO: put id generation into @ensnode/utils and re-use it here for faster lookups + where: (t, { eq, and }) => and(eq(t.resolverId, resolverId), eq(t.node, node)), + with: { addresses: true }, + }); +} diff --git a/apps/api/src/lib/parse-name.ts b/apps/api/src/lib/parse-name.ts new file mode 100644 index 000000000..7733e609c --- /dev/null +++ b/apps/api/src/lib/parse-name.ts @@ -0,0 +1,32 @@ +import { LabelHash } from "@ensnode/utils/types"; +import { Hex, isHex } from "viem"; +import { labelhash } from "viem/ens"; + +// https://github.com/wevm/viem/blob/main/src/utils/ens/encodedLabelToLabelhash.ts +export function encodedLabelToLabelhash(label: string): Hex | 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; +} + +/** + * parses a name into labelHash segments. name may contain encoded labelHashes + */ +export function parseName(name: string): LabelHash[] { + return name.split(".").map((segment) => { + const labelHash = segment.startsWith("[") + ? encodedLabelToLabelhash(segment) + : labelhash(segment); + + if (!labelHash) { + throw new Error( + `parseName: name "${name}" segment "${segment}" is not a valid encoded labelHash`, + ); + } + + return labelHash; + }); +} diff --git a/apps/api/src/v1.ts b/apps/api/src/v1.ts new file mode 100644 index 000000000..8fe9ca7a1 --- /dev/null +++ b/apps/api/src/v1.ts @@ -0,0 +1,56 @@ +import { encodeLabelhash } from "@ensdomains/ensjs/utils"; +import { uint256ToHex32 } from "@ensnode/utils/subname-helpers"; +import { replaceBigInts } from "@ponder/utils"; +import { Hono } from "hono"; +import { namehash } from "viem"; + +import { getDomainAndPath } from "./lib/get-domain.js"; + +const app = new Hono(); + +/** + * Finds a Domain by its `name` in the nametree. + */ +app.get("/domain/:name", async (c) => { + const nameParam = c.req.param("name"); + + // fetches a domain by name and the concrete path in the nametree + const { domain, path } = await getDomainAndPath(nameParam); + + // identify any unknown labels in the name + const unknownSegments = path.filter((segment) => segment.label === undefined); + + // TODO: attempt heal with ENSRainbow batch + const knownOrEncodedSegments = (await Promise.all(unknownSegments)).reduce< + Record + >((memo, segment) => { + memo[segment.token_id] === encodeLabelhash(uint256ToHex32(BigInt(segment.token_id))); + return memo; + }, {}); + + // construct the domain's name to the best of our abilities + const name = path + // reverse to name-order + .toReversed() + // return known label or ens rainbow result + .map((segment) => segment.label ?? knownOrEncodedSegments[segment.token_id]) + // join into name + .join("."); + + const node = namehash(name); + + // TODO: type this when we're more confident in what we want + const result = { + domain: { + ...domain, + // add constructed name and node to domain response + name, + node, + }, + path, + }; + + return c.json(replaceBigInts(result, (v) => String(v))); +}); + +export default app; diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 000000000..21fd3b621 --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@ensnode/shared-configs/tsconfig.ponder.json", + "include": ["./**/*.ts"], + "exclude": ["node_modules"], + "compilerOptions": { + "strict": true, + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx" + } +} diff --git a/apps/ensindexer/package.json b/apps/ensindexer/package.json index 20213d8b1..0088ae501 100644 --- a/apps/ensindexer/package.json +++ b/apps/ensindexer/package.json @@ -25,11 +25,12 @@ "dependencies": { "@ensdomains/ensjs": "^4.0.2", "@ensnode/ens-deployments": "workspace:*", - "@ensnode/utils": "workspace:*", "@ensnode/ensrainbow-sdk": "workspace:*", "@ensnode/ponder-metadata": "workspace:*", "@ensnode/ponder-schema": "workspace:*", "@ensnode/ponder-subgraph": "workspace:*", + "@ensnode/utils": "workspace:*", + "caip": "^1.1.1", "hono": "catalog:", "ponder": "catalog:", "ts-deepmerge": "^7.0.2", diff --git a/apps/ensindexer/ponder.config.ts b/apps/ensindexer/ponder.config.ts index 8d0d62f45..4310afd53 100644 --- a/apps/ensindexer/ponder.config.ts +++ b/apps/ensindexer/ponder.config.ts @@ -9,7 +9,7 @@ import * as ethPlugin from "./src/plugins/eth/ponder.plugin"; import * as lineaEthPlugin from "./src/plugins/linea/ponder.plugin"; //////// -// First, generate AllPluginConfigs type representing the merged types of each plugin's `config`, +// Generate AllPluginConfigs type representing the merged types of each plugin's `config`, // so ponder's typechecking of the indexing handlers and their event arguments is correct. //////// @@ -18,31 +18,33 @@ const ALL_PLUGINS = [ethPlugin, baseEthPlugin, lineaEthPlugin, ensV2Plugin] as c type AllPluginConfigs = MergedTypes<(typeof ALL_PLUGINS)[number]["config"]>; //////// -// Next, filter ALL_PLUGINS by those that are available and that the user has activated. +// Filter ALL_PLUGINS by those that are 'available' (i.e. defined by the SELECTED_DEPLOYMENT_CONFIG) //////// -// the available PluginNames are those that the selected ENS Deployment defines as available const availablePluginNames = Object.keys(SELECTED_DEPLOYMENT_CONFIG) as PluginName[]; -// filter the set of available plugins by those that are 'active' in the env +//////// +// Filter ALL_PLUGINS by those that are 'active' in the env (i.e. via ACTIVE_PLUGINS) +//////// + const activePlugins = getActivePlugins(ALL_PLUGINS, availablePluginNames); //////// -// Next, merge the plugins' configs into a single ponder config and activate their handlers. +// Merge the plugins' configs into a single ponder config. //////// -// merge the resulting configs const activePluginsMergedConfig = activePlugins .map((plugin) => plugin.config) .reduce((acc, val) => deepMergeRecursive(acc, val), {}) as AllPluginConfigs; -// load indexing handlers from the active plugins into the runtime +//////// +// 'activate' each plugin, registering its indexing handlers with ponder +//////// + activePlugins.forEach((plugin) => plugin.activate()); //////// -// Finally, return the merged config for ponder to use for type inference and runtime behavior. +// Finally, return the merged config (typed as AllPluginConfigs) for ponder. //////// -// The type of the default export is a merge of all active plugin configs -// configs so that each plugin can be correctly typechecked export default activePluginsMergedConfig; diff --git a/apps/ensindexer/src/handlers/Registrar.ts b/apps/ensindexer/src/handlers/Registrar.ts index 185f3176e..4a45e33fb 100644 --- a/apps/ensindexer/src/handlers/Registrar.ts +++ b/apps/ensindexer/src/handlers/Registrar.ts @@ -1,7 +1,7 @@ import { type Context } from "ponder:registry"; import schema from "ponder:schema"; import { isLabelIndexable, makeSubnodeNamehash } from "@ensnode/utils/subname-helpers"; -import type { Labelhash } from "@ensnode/utils/types"; +import type { LabelHash } from "@ensnode/utils/types"; import { type Hex, labelhash as _labelhash, namehash } from "viem"; import { createSharedEventValues, upsertAccount, upsertRegistration } from "../lib/db-helpers"; import { labelByHash } from "../lib/graphnode-helpers"; @@ -23,7 +23,7 @@ export const makeRegistrarHandlers = (ownedName: OwnedName) => { async function setNamePreimage( context: Context, name: string, - labelhash: Labelhash, + labelhash: LabelHash, cost: bigint, ) { // NOTE: ponder intentionally removes null bytes to spare users the footgun of @@ -68,7 +68,7 @@ export const makeRegistrarHandlers = (ownedName: OwnedName) => { }: { context: Context; event: EventWithArgs<{ - labelhash: Labelhash; + labelhash: LabelHash; owner: Hex; expires: bigint; }>; @@ -131,7 +131,7 @@ export const makeRegistrarHandlers = (ownedName: OwnedName) => { context: Context; event: EventWithArgs<{ name: string; - label: Labelhash; + label: LabelHash; cost: bigint; }>; }) { @@ -144,7 +144,7 @@ export const makeRegistrarHandlers = (ownedName: OwnedName) => { event, }: { context: Context; - event: EventWithArgs<{ name: string; label: Labelhash; cost: bigint }>; + event: EventWithArgs<{ name: string; label: LabelHash; cost: bigint }>; }) { const { name, label, cost } = event.args; await setNamePreimage(context, name, label, cost); @@ -155,7 +155,7 @@ export const makeRegistrarHandlers = (ownedName: OwnedName) => { event, }: { context: Context; - event: EventWithArgs<{ labelhash: Labelhash; expires: bigint }>; + event: EventWithArgs<{ labelhash: LabelHash; expires: bigint }>; }) { const { labelhash, expires } = event.args; @@ -186,7 +186,7 @@ export const makeRegistrarHandlers = (ownedName: OwnedName) => { event, }: { context: Context; - event: EventWithArgs<{ labelhash: Labelhash; from: Hex; to: Hex }>; + event: EventWithArgs<{ labelhash: LabelHash; from: Hex; to: Hex }>; }) { const { labelhash, to } = event.args; await upsertAccount(context, to); diff --git a/apps/ensindexer/src/handlers/Registry.ts b/apps/ensindexer/src/handlers/Registry.ts index 5e8127fa0..11f613d55 100644 --- a/apps/ensindexer/src/handlers/Registry.ts +++ b/apps/ensindexer/src/handlers/Registry.ts @@ -2,7 +2,7 @@ import { Context } from "ponder:registry"; import schema from "ponder:schema"; import { encodeLabelhash } from "@ensdomains/ensjs/utils"; import { ROOT_NODE, isLabelIndexable, makeSubnodeNamehash } from "@ensnode/utils/subname-helpers"; -import type { Labelhash, Node } from "@ensnode/utils/types"; +import type { LabelHash, Node } from "@ensnode/utils/types"; import { type Hex, zeroAddress } from "viem"; import { createSharedEventValues, upsertAccount, upsertResolver } from "../lib/db-helpers"; import { labelByHash } from "../lib/graphnode-helpers"; @@ -84,7 +84,7 @@ export const makeRegistryHandlers = (ownedName: OwnedName) => { event, }: { context: Context; - event: EventWithArgs<{ node: Node; label: Labelhash; owner: Hex }>; + event: EventWithArgs<{ node: Node; label: LabelHash; owner: Hex }>; }) => { const { label: labelhash, node, owner } = event.args; diff --git a/apps/ensindexer/src/lib/globals.ts b/apps/ensindexer/src/lib/globals.ts index 0c0f7aff5..21830d2e9 100644 --- a/apps/ensindexer/src/lib/globals.ts +++ b/apps/ensindexer/src/lib/globals.ts @@ -14,21 +14,28 @@ import { getEnsDeploymentChain } from "./ponder-helpers"; export const SELECTED_DEPLOYMENT_CONFIG = DeploymentConfigs[getEnsDeploymentChain()]; /** - * Note that here, we define the global DEPLOYMENT_CONFIG as the _merge_ of all possible deployments - * (therefore fully specifying all plugin configs), overrided with the SELECTED_DEPLOYMENT_CONFIG. - * - * This ensures that at type-check-time and in `ALL_PLUGINS` every plugin's `config` has valid values - * (and therefore its type can continue to be inferred). This means that initially upon building the - * plugin configs, if the user is selecting a deployment that does not fully specify every available - * plugin, the plugins that are not in that deployment's specification are technically pointing at - * the mainnet deployment. This is never an issue, however, as those plugin are filtered out - * (see ponder.config.ts and `getActivePlugins`) and never activated. + * Here, we define the global MERGED_DEPLOYMENT_CONIG as the merge of all possible deployment configs + * (therefore fully specifying all possible AddressBooks). */ -export const DEPLOYMENT_CONFIG = { +const MERGED_DEPLOYMENT_CONIG = { ...DeploymentConfigs.mainnet, ...DeploymentConfigs.sepolia, ...DeploymentConfigs.holesky, ...DeploymentConfigs["ens-test-env"], +}; + +/** + * Here we override the MERGED_DEPLOYMENT_CONIG object with the SELECTED_DEPLOYMENT_CONFIG. + * + * This ensures that at type-check-time every plugin's `config` has valid values (and therefore its + * type can be inferred). This means that initially upon building the plugin configs, if the user is + * selecting a deployment that does not fully specify every available plugin, the plugins that are + * not in that deployment's specification are technically referencing an AddressBook from another + * deployment. This is never an issue, however, as those plugin are filtered out at runtime + * and never activated (see ponder.config.ts and `getActivePlugins`). + */ +export const DEPLOYMENT_CONFIG = { + ...MERGED_DEPLOYMENT_CONIG, ...SELECTED_DEPLOYMENT_CONFIG, }; diff --git a/apps/ensindexer/src/lib/graphnode-helpers.ts b/apps/ensindexer/src/lib/graphnode-helpers.ts index 66d657128..5335b6dfe 100644 --- a/apps/ensindexer/src/lib/graphnode-helpers.ts +++ b/apps/ensindexer/src/lib/graphnode-helpers.ts @@ -1,5 +1,5 @@ import { EnsRainbowApiClient, ErrorCode, isHealError } from "@ensnode/ensrainbow-sdk"; -import type { Labelhash } from "@ensnode/utils/types"; +import type { LabelHash } from "@ensnode/utils/types"; import { ensRainbowEndpointUrl } from "./ponder-helpers"; const ensRainbowApiClient = new EnsRainbowApiClient({ @@ -24,7 +24,7 @@ if ( * @returns the original label if found, or null if not found for the labelhash. * @throws if the labelhash is not correctly formatted, or server error occurs, or connection error occurs. **/ -export async function labelByHash(labelhash: Labelhash): Promise { +export async function labelByHash(labelhash: LabelHash): Promise { const healResponse = await ensRainbowApiClient.heal(labelhash); if (!isHealError(healResponse)) { diff --git a/apps/ensindexer/src/lib/ids.ts b/apps/ensindexer/src/lib/ids.ts index 6480f9e9b..415e2ab1c 100644 --- a/apps/ensindexer/src/lib/ids.ts +++ b/apps/ensindexer/src/lib/ids.ts @@ -1,4 +1,4 @@ -import type { Labelhash, Node } from "@ensnode/utils/types"; +import type { LabelHash, Node } from "@ensnode/utils/types"; import type { Address, Hex } from "viem"; // NOTE: subgraph uses lowercase address here, viem provides us checksummed, so we lowercase it @@ -59,7 +59,7 @@ export const makeEventId = ( * @param node the node of the full name that was registered * @returns a unique registration id */ -export const makeRegistrationId = (registrarName: string, labelHash: Labelhash, node: Node) => { +export const makeRegistrationId = (registrarName: string, labelHash: LabelHash, node: Node) => { if (registrarName === "eth") { // For the "v1" of ENSIndexer (at a minimum) we want to preserve backwards // compatibility with Registration id's issued by the ENS Subgraph. diff --git a/apps/ensindexer/src/lib/plugin-helpers.ts b/apps/ensindexer/src/lib/plugin-helpers.ts index 66dbbcd7b..88b26db57 100644 --- a/apps/ensindexer/src/lib/plugin-helpers.ts +++ b/apps/ensindexer/src/lib/plugin-helpers.ts @@ -1,4 +1,4 @@ -import type { SubregistryContractConfig } from "@ensnode/ens-deployments"; +import type { ContractConfig } from "@ensnode/ens-deployments"; import type { NetworkConfig } from "ponder"; import { http, Chain } from "viem"; import { END_BLOCK, START_BLOCK } from "./globals"; @@ -221,7 +221,7 @@ export function networksConfigForChain(chain: Chain) { * Defines a `ponder#ContractConfig['network']` given a contract's config, injecting the global * start/end blocks to constrain indexing range. */ -export function networkConfigForContract( +export function networkConfigForContract( chain: Chain, contractConfig: CONTRACT_CONFIG, ) { diff --git a/apps/ensindexer/src/lib/types.ts b/apps/ensindexer/src/lib/types.ts index 3e16a3db7..7ce4ab022 100644 --- a/apps/ensindexer/src/lib/types.ts +++ b/apps/ensindexer/src/lib/types.ts @@ -9,10 +9,18 @@ export type OwnedName = string; /** - * In this project we use the notion of 'plugins' to describe which registries and subregistries - * of a given ENS deployment are being indexed by ponder. In this project, a plugin's name is the - * name of the subregistry it indexes. Note that this type definition is 1:1 with that of - * @ensnode/ens-deployments SubregistryName, simplifying the relationship between an ENSDeploymentConfig - * and the plugins in this project. + * In ENSIndexer we use the notion of 'plugins' to describe a relationship between a set of contracts + * and the handler logic that indexes their events. For ENSv1, a plugin's name is the + * name of the subregistry it indexes ('eth', 'base', 'linea'). + * + * The ENSv2 plugin ('ens-v2') represents the configuration and indexing logic associated with + * indexing ENSv2. + * + * The PluginName type is necessary for uniquely identifying each plugin's config and filtering + * which are 'activated' at runtime. + * + * Note that this type is an exact equivalent of `keyof ENSDeploymentConfig`, which we over-specify + * to illustrate the connection between a ENSIndexer plugin and the AddressBook that it is configured + * with. */ export type PluginName = "eth" | "base" | "linea" | "ens-v2"; diff --git a/apps/ensindexer/src/plugins/base/handlers/Registrar.ts b/apps/ensindexer/src/plugins/base/handlers/Registrar.ts index beee6923b..7e371b2bc 100644 --- a/apps/ensindexer/src/plugins/base/handlers/Registrar.ts +++ b/apps/ensindexer/src/plugins/base/handlers/Registrar.ts @@ -1,7 +1,7 @@ import { ponder } from "ponder:registry"; import schema from "ponder:schema"; import { makeSubnodeNamehash, uint256ToHex32 } from "@ensnode/utils/subname-helpers"; -import type { Labelhash } from "@ensnode/utils/types"; +import type { LabelHash } from "@ensnode/utils/types"; import { zeroAddress } from "viem"; import { makeRegistrarHandlers } from "../../../handlers/Registrar"; import { upsertAccount } from "../../../lib/db-helpers"; @@ -14,7 +14,7 @@ import { PonderENSPluginHandlerArgs } from "../../../lib/plugin-helpers"; * subname of base.eth that was registered. * https://github.com/base/basenames/blob/1b5c1ad/src/L2/RegistrarController.sol#L488 */ -const tokenIdToLabelhash = (tokenId: bigint): Labelhash => uint256ToHex32(tokenId); +const tokenIdToLabelhash = (tokenId: bigint): LabelHash => uint256ToHex32(tokenId); export default function ({ ownedName, namespace }: PonderENSPluginHandlerArgs<"base.eth">) { const { diff --git a/apps/ensindexer/src/plugins/ens-v2/handlers/EthRegistry.ts b/apps/ensindexer/src/plugins/ens-v2/handlers/EthRegistry.ts index 28909e880..be947f8d2 100644 --- a/apps/ensindexer/src/plugins/ens-v2/handlers/EthRegistry.ts +++ b/apps/ensindexer/src/plugins/ens-v2/handlers/EthRegistry.ts @@ -1,54 +1,16 @@ import { ponder } from "ponder:registry"; -import schema from "ponder:schema"; import { PonderENSPluginHandlerArgs } from "../../../lib/plugin-helpers"; -import { createDomainId, createEventId, generateTokenId, updateDomainLabel } from "../v2-lib"; +import { + handleNewSubname, + handleTransferBatch, + handleTransferSingle, + handleURI, +} from "./shared/Registry"; export default function ({ namespace }: PonderENSPluginHandlerArgs<"ens-v2">) { - ponder.on(namespace("EthRegistry:TransferSingle"), async ({ event, context }) => { - console.log("EthRegistry:TransferSingle", event.transaction.to); - const timestamp = event.block.timestamp; - const labelHash = event.args.id.toString(); - const domainId = createDomainId(event.transaction.to?.toString(), labelHash); - - await context.db.insert(schema.v2_domain).values({ - id: domainId, - labelHash: labelHash, - owner: event.args.to.toString(), - registry: event.transaction.to?.toString(), - createdAt: timestamp, - updatedAt: timestamp, - }); - - // Store the event data - const eventId = createEventId(event); - await context.db.insert(schema.v2_transferSingleEvent).values({ - id: eventId, - registryId: event.transaction.to?.toString(), - tokenId: event.args.id.toString(), - from: event.args.from.toString(), - to: event.args.to.toString(), - value: event.args.value, - source: "EthRegistry", - createdAt: timestamp, - updatedAt: timestamp, - }); - }); - - ponder.on(namespace("EthRegistry:NewSubname"), async ({ event, context }) => { - console.log("EthRegistry:NewSubname", event.transaction.to); - const tokenId = generateTokenId(event.args.label); - const registryId = event.transaction.to?.toString(); - const domainId = createDomainId(registryId, tokenId); - - await updateDomainLabel( - context, - domainId, - event.args.label, - tokenId, - event.block.timestamp, - event, - "EthRegistry", - ); - }); + ponder.on(namespace("EthRegistry:NewSubname"), handleNewSubname); + ponder.on(namespace("EthRegistry:TransferSingle"), handleTransferSingle); + ponder.on(namespace("EthRegistry:TransferBatch"), handleTransferBatch); + ponder.on(namespace("EthRegistry:URI"), handleURI); } diff --git a/apps/ensindexer/src/plugins/ens-v2/handlers/OwnedResolver.ts b/apps/ensindexer/src/plugins/ens-v2/handlers/OwnedResolver.ts deleted file mode 100644 index ae2db4f69..000000000 --- a/apps/ensindexer/src/plugins/ens-v2/handlers/OwnedResolver.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ponder } from "ponder:registry"; -import schema from "ponder:schema"; - -import { PonderENSPluginHandlerArgs } from "../../../lib/plugin-helpers"; - -export default function ({ namespace }: PonderENSPluginHandlerArgs<"ens-v2">) { - ponder.on(namespace("OwnedResolver:AddressChanged"), async ({ event, context }) => { - const timestamp = event.block.timestamp; - const resolverId = event.transaction.to?.toString(); - if (!resolverId) return; - - console.log("OwnedResolver:AddressChanged", event.args, resolverId); - const record = await context.db.find(schema.v2_resolver, { id: resolverId }); - if (record) { - console.log("OwnedResolver:AddressChanged", "Record found", record); - await context.db.update(schema.v2_resolver, { id: record.id }).set({ - ...record, - address: event.args.newAddress.toString(), - updatedAt: timestamp, - node: event.args.node.toString(), - }); - } else { - console.log("OwnedResolver:AddressChanged", "No record found"); - } - }); -} diff --git a/apps/ensindexer/src/plugins/ens-v2/handlers/RegistryDatastore.ts b/apps/ensindexer/src/plugins/ens-v2/handlers/RegistryDatastore.ts index 8254be625..8f3daec24 100644 --- a/apps/ensindexer/src/plugins/ens-v2/handlers/RegistryDatastore.ts +++ b/apps/ensindexer/src/plugins/ens-v2/handlers/RegistryDatastore.ts @@ -2,70 +2,121 @@ import { ponder } from "ponder:registry"; import schema from "ponder:schema"; import { PonderENSPluginHandlerArgs } from "../../../lib/plugin-helpers"; -import { createEventId } from "../v2-lib"; +import { makeContractId, makeDomainId } from "../v2-lib"; export default function ({ namespace }: PonderENSPluginHandlerArgs<"ens-v2">) { + // NOTE: can arrive in any order, must upsert all relevant entities ponder.on(namespace("RegistryDatastore:SubregistryUpdate"), async ({ context, event }) => { - console.log("RegistryDatastore:SubregistryUpdate", event.args); - const timestamp = event.block.timestamp; - await context.db.insert(schema.v2_registry).values({ - id: event.args.registry.toString(), - labelHash: event.args.labelHash.toString(), - subregistryId: event.args.subregistry, - flags: event.args.flags, - createdAt: timestamp, - updatedAt: timestamp, - }); - console.log(event); - const eventId = createEventId(event); - await context.db.insert(schema.v2_subregistryUpdateEvent).values({ - id: eventId, - registryId: event.args.registry.toString(), - labelHash: event.args.labelHash.toString(), - subregistryId: event.args.subregistry, - flags: event.args.flags, - createdAt: timestamp, - updatedAt: timestamp, + const { + registry: registryAddress, + labelHash: maskedTokenId, // NOTE: this variable is called labelHash but is actually masked tokenId + subregistry: subregistryAddress, + flags, + } = event.args; + + const registryId = makeContractId(context.network.chainId, registryAddress); + const domainId = makeDomainId(registryId, maskedTokenId); + const subregistryId = makeContractId(context.network.chainId, subregistryAddress); + + console.table({ + on: "RegistryDatastore:SubregistryUpdate", + registryId, + tokenId: maskedTokenId, + domainId, + subregistryId, + hash: event.transaction.hash, }); + + // NOTE(registry-domain-uniq): if subregistry is already linked to a domain, ignore this update + // (implements first-write-wins for registry-domain relations) + const existingSubregistry = await context.db.find(schema.v2_registry, { id: subregistryId }); + if (existingSubregistry?.domainId) { + console.log( + `tx ${event.transaction.hash} wanted to set the subregistry for ${domainId} to ${subregistryId} but that registry is already linked to another domain (${existingSubregistry.domainId}) — ignoring.`, + ); + return; + } + + // ensure registry entity + await context.db.insert(schema.v2_registry).values({ id: registryId }).onConflictDoNothing(); + + // ensure subregistry entity + // TODO(registry-domain-uniq): also update the reverse-mapping on the subregistry to point to this domain + await context.db + .insert(schema.v2_registry) + .values({ id: subregistryId, domainId }) + .onConflictDoUpdate({ domainId }); + + await context.db + .insert(schema.v2_domain) + // insert domain with subregistry info + .values({ + id: domainId, + registryId, + maskedTokenId, + // NOTE: this is technically incorrect but this event does not have access to the token's unmasked + // tokenId. in the event that the Domain already exists, this create will not occur so this isn't an issue. + // in the event that the Domain does not exist, it is created with an incorrect tokenId BUT + // according to IRegistry, NewSubname will follow, which ensures that the correct tokenId + // is always set. solutions to this issue include: + // 1. event-order guarantees (NewSubname then RegistryDatastore events) + // 2. RegistryDatastore events emitting the un-masked tokenId and expecting clients to do the + // subsequent masking + tokenId: maskedTokenId, + subregistryId, + subregistryFlags: flags, + }) + // or update existing domain with subregistry info + .onConflictDoUpdate({ subregistryId, subregistryFlags: flags }); }); + // NOTE: can arrive in any order, must upsert all relevant entities ponder.on(namespace("RegistryDatastore:ResolverUpdate"), async ({ context, event }) => { - console.log("RegistryDatastore:ResolverUpdate", event.args); - const timestamp = event.block.timestamp; - const record2 = await context.db.find(schema.v2_registry, { - id: event.args.registry.toString(), - }); - if (record2) { - console.log("RegistryDatastore:ResolverUpdate", "Record found", record2); - await context.db - .update(schema.v2_registry, { id: record2.id }) - .set({ ...record2, resolver: event.args.resolver.toString() }); + const { + registry: registryAddress, + labelHash: maskedTokenId, // NOTE: this variable is called labelHash but is actually masked tokenId + resolver: resolverAddress, + flags, + } = event.args; - const record3 = await context.db.find(schema.v2_resolver, { - id: event.args.resolver.toString(), - }); - if (!record3) { - console.log("RegistryDatastore:ResolverUpdate", "Creating new resolver record"); - await context.db.insert(schema.v2_resolver).values({ - id: event.args.resolver.toString(), - createdAt: timestamp, - updatedAt: timestamp, - }); - } - } else { - console.log("RegistryDatastore:ResolverUpdate", "No record found"); - } + const registryId = makeContractId(context.network.chainId, registryAddress); + const domainId = makeDomainId(registryId, maskedTokenId); + const resolverId = makeContractId(context.network.chainId, resolverAddress); - // Store the event data - const eventId = createEventId(event); - await context.db.insert(schema.v2_resolverUpdateEvent).values({ - id: eventId, - registryId: event.args.registry.toString(), - labelHash: event.args.labelHash.toString(), - resolverId: event.args.resolver.toString(), - flags: event.args.flags, - createdAt: timestamp, - updatedAt: timestamp, + console.table({ + on: "RegistryDatastore:ResolverUpdate", + registryId, + tokenId: maskedTokenId, + domainId, + resolverId, }); + + // ensure registry entity + await context.db.insert(schema.v2_registry).values({ id: registryId }).onConflictDoNothing(); + + // ensure resolver entity + await context.db.insert(schema.v2_resolver).values({ id: resolverId }).onConflictDoNothing(); + + await context.db + .insert(schema.v2_domain) + // insert domain with resolver info + .values({ + id: domainId, + registryId, + maskedTokenId, + // NOTE: this is technically incorrect but this event does not have access to the token's unmasked + // tokenId. in the event that the Domain already exists, this create will not occur so this isn't an issue. + // in the event that the Domain does not exist, it is created with an incorrect tokenId BUT + // according to IRegistry, NewSubname will follow, which ensures that the correct tokenId + // is always set. solutions to this issue include: + // 1. event-order guarantees (NewSubname then RegistryDatastore events) + // 2. RegistryDatastore events emitting the un-masked tokenId and expecting clients to do the + // subsequent masking + tokenId: maskedTokenId, + resolverId, + resolverFlags: flags, + }) + // or update existing domain with resolver info + .onConflictDoUpdate({ resolverId, resolverFlags: flags }); }); } diff --git a/apps/ensindexer/src/plugins/ens-v2/handlers/Resolver.ts b/apps/ensindexer/src/plugins/ens-v2/handlers/Resolver.ts new file mode 100644 index 000000000..d207de31e --- /dev/null +++ b/apps/ensindexer/src/plugins/ens-v2/handlers/Resolver.ts @@ -0,0 +1,73 @@ +import { Context, ponder } from "ponder:registry"; +import schema from "ponder:schema"; + +import { Node } from "@ensnode/utils/types"; +import { Address } from "viem"; +import { PonderENSPluginHandlerArgs } from "../../../lib/plugin-helpers"; +import { EventWithArgs } from "../../../lib/ponder-helpers"; +import { makeContractId, makeResolverRecordsAddressId, makeResolverRecordsId } from "../v2-lib"; + +export async function upsertResolver( + context: Context, + values: typeof schema.v2_resolver.$inferInsert, +) { + return context.db.insert(schema.v2_resolver).values(values).onConflictDoUpdate(values); +} + +export async function upsertResolverRecords( + context: Context, + values: typeof schema.v2_resolverRecords.$inferInsert, +) { + return context.db.insert(schema.v2_resolverRecords).values(values).onConflictDoUpdate(values); +} + +export default function ({ namespace }: PonderENSPluginHandlerArgs<"ens-v2">) { + async function handleAddressChanged({ + context, + event, + }: { + context: Context; + event: EventWithArgs<{ + node: Node; + coinType: bigint; + newAddress: Address; + }>; + }) { + const { node, coinType, newAddress } = event.args; + + const resolverId = makeContractId(context.network.chainId, event.log.address); + const resolverRecordsId = makeResolverRecordsId(resolverId, node); + const resolverRecordsAddressId = makeResolverRecordsAddressId(resolverRecordsId, coinType); + + await upsertResolver(context, { id: resolverId }); + await upsertResolverRecords(context, { id: resolverRecordsId, resolverId, node }); + + await context.db + .insert(schema.v2_resolverRecordsAddress) + // create a new address entity + .values({ + id: resolverRecordsAddressId, + resolverRecordsId, + coinType, + address: newAddress, + }) + // or update the existing one + .onConflictDoUpdate({ address: newAddress }); + } + + ponder.on(namespace("Resolver:AddrChanged"), async ({ context, event }) => { + await handleAddressChanged({ + context, + event: { + ...event, + args: { + node: event.args.node, + newAddress: event.args.a, + coinType: 60n, // TODO: move to utils/constants + }, + }, + }); + }); + + ponder.on(namespace("Resolver:AddressChanged"), handleAddressChanged); +} diff --git a/apps/ensindexer/src/plugins/ens-v2/handlers/RootRegistry.ts b/apps/ensindexer/src/plugins/ens-v2/handlers/RootRegistry.ts index a5f1d973a..eacc94427 100644 --- a/apps/ensindexer/src/plugins/ens-v2/handlers/RootRegistry.ts +++ b/apps/ensindexer/src/plugins/ens-v2/handlers/RootRegistry.ts @@ -1,54 +1,16 @@ import { ponder } from "ponder:registry"; -import schema from "ponder:schema"; import { PonderENSPluginHandlerArgs } from "../../../lib/plugin-helpers"; -import { createDomainId, createEventId, generateTokenId, updateDomainLabel } from "../v2-lib"; +import { + handleNewSubname, + handleTransferBatch, + handleTransferSingle, + handleURI, +} from "./shared/Registry"; export default function ({ namespace }: PonderENSPluginHandlerArgs<"ens-v2">) { - ponder.on(namespace("RootRegistry:TransferSingle"), async ({ event, context }) => { - const timestamp = event.block.timestamp; - const tokenId = event.args.id.toString(); - const registryId = event.transaction.to?.toString(); - const domainId = createDomainId(registryId, tokenId); - const values = { - id: domainId, - labelHash: tokenId, - owner: event.args.to.toString(), - registry: registryId, - createdAt: timestamp, - updatedAt: timestamp, - }; - console.log("RootRegistry:TransferSingle", values); - await context.db.insert(schema.v2_domain).values(values); - // Store the event data - const eventId = createEventId(event); - await context.db.insert(schema.v2_transferSingleEvent).values({ - id: eventId, - registryId: registryId, - tokenId: tokenId, - from: event.args.from.toString(), - to: event.args.to.toString(), - value: event.args.value, - source: "RootRegistry", - createdAt: timestamp, - updatedAt: timestamp, - }); - }); - - ponder.on(namespace("RootRegistry:NewSubname"), async ({ event, context }) => { - console.log("RootRegistry:NewSubname", event.transaction.to); - const tokenId = generateTokenId(event.args.label); - const registryId = event.transaction.to?.toString(); - const domainId = createDomainId(registryId, tokenId); - - await updateDomainLabel( - context, - domainId, - event.args.label, - tokenId, - event.block.timestamp, - event, - "RootRegistry", - ); - }); + ponder.on(namespace("RootRegistry:NewSubname"), handleNewSubname); + ponder.on(namespace("RootRegistry:TransferSingle"), handleTransferSingle); + ponder.on(namespace("RootRegistry:TransferBatch"), handleTransferBatch); + ponder.on(namespace("RootRegistry:URI"), handleURI); } diff --git a/apps/ensindexer/src/plugins/ens-v2/handlers/shared/Registry.ts b/apps/ensindexer/src/plugins/ens-v2/handlers/shared/Registry.ts new file mode 100644 index 000000000..20baafdbf --- /dev/null +++ b/apps/ensindexer/src/plugins/ens-v2/handlers/shared/Registry.ts @@ -0,0 +1,166 @@ +/// +/// Shared Registry Handlers +/// + +import { Context } from "ponder:registry"; +import schema from "ponder:schema"; +import { isLabelIndexable } from "@ensnode/utils/subname-helpers"; +import { Address, getAddress, labelhash, zeroAddress } from "viem"; +import { EventWithArgs } from "../../../../lib/ponder-helpers"; +import { labelHashToTokenId, makeContractId, makeDomainId, maskTokenId } from "../../v2-lib"; + +// NewSubname and DataStore events may arrive in any order +export async function handleNewSubname({ + context, + event, +}: { + context: Context; + event: EventWithArgs<{ label: string }>; +}) { + const { label } = event.args; + + const registryId = makeContractId(context.network.chainId, event.log.address); + const tokenId = labelHashToTokenId(labelhash(label)); + const maskedTokenId = maskTokenId(tokenId); + const domainId = makeDomainId(registryId, maskedTokenId); + + console.table({ on: "NewSubname", registryId, tokenId, domainId }); + + // ensure that this registry exists + await context.db.insert(schema.v2_registry).values({ id: registryId }).onConflictDoNothing(); + + const indexableLabel = isLabelIndexable(label) ? label : null; + + await context.db + .insert(schema.v2_domain) + // insert new Domain with `label` value + .values({ + id: domainId, + registryId, + tokenId, + maskedTokenId, + label: indexableLabel, + }) + // or upsert existing Domain's `label` value + .onConflictDoUpdate({ + label: indexableLabel, + // NOTE: we enforce that tokenId is set here to avoid event-order issues specified in RegistryDatastore + tokenId, + }); +} + +export async function handleURI({ + context, + event, +}: { + context: Context; + event: EventWithArgs<{ + id: bigint; + value: string; + }>; +}) { + const { id: tokenId, value: uri } = event.args; + + const registryId = makeContractId(context.network.chainId, event.log.address); + const maskedTokenId = maskTokenId(tokenId); + const domainId = makeDomainId(registryId, maskedTokenId); + + console.table({ on: "URI", registryId, tokenId, domainId, uri }); + + await context.db + .insert(schema.v2_domain) + // insert new Domain with uri + .values({ + id: domainId, + registryId, + tokenId, + maskedTokenId, + uri, + }) + // or update uri of existing Domain + .onConflictDoUpdate({ + uri, + // NOTE: we enforce that tokenId is set here to avoid event-order issues specified in RegistryDatastore + tokenId, + }); +} + +// ERC1155 Transfer events may arrive in any order +async function handleTransfer({ + context, + event, +}: { context: Context; event: EventWithArgs<{ id: bigint; to: Address }> }) { + const { id: tokenId, to } = event.args; + + const registryId = makeContractId(context.network.chainId, event.log.address); + const maskedTokenId = maskTokenId(tokenId); + const domainId = makeDomainId(registryId, maskedTokenId); + const owner = getAddress(to); // NOTE: ensures that owner is checksummed + + console.table({ on: "handleTransfer", registryId, tokenId, domainId, owner }); + + const isBurn = owner === zeroAddress; + if (isBurn) { + // to remove a Domain from the tree, we need only delete the Domain entity + // NOTE(registry-domain-uniq): we must also remove the reverse relationship on its subregistry + // because we store that information bi-directionally + const domain = await context.db.find(schema.v2_domain, { id: domainId }); + if (domain?.subregistryId) { + await context.db + .update(schema.v2_registry, { id: domain.subregistryId }) + .set({ domainId: null }); + } + + // delete the relevant Domain entity, removing it and its subtree from the namespace + await context.db.delete(schema.v2_domain, { id: domainId }); + } else { + // this is a mint or update event + await context.db + .insert(schema.v2_domain) + // insert new Domain with owner + .values({ + id: domainId, + registryId, + tokenId, + maskedTokenId, + owner, + }) + // or update owner of existing Domain + .onConflictDoUpdate({ + owner, + // NOTE: we enforce that tokenId is set here to avoid event-order issues specified in RegistryDatastore + tokenId, + }); + } +} + +export async function handleTransferSingle({ + context, + event, +}: { + context: Context; + event: EventWithArgs<{ id: bigint; to: Address }>; +}) { + await handleTransfer({ context, event }); +} + +export async function handleTransferBatch({ + context, + event, +}: { + context: Context; + event: EventWithArgs<{ ids: readonly bigint[]; to: Address }>; +}) { + for (const [i, id] of event.args.ids.entries()) { + await handleTransfer({ + context, + event: { + ...event, + args: { + id, + to: event.args.to, + }, + }, + }); + } +} diff --git a/apps/ensindexer/src/plugins/ens-v2/ponder.plugin.ts b/apps/ensindexer/src/plugins/ens-v2/ponder.plugin.ts index bc0eca30f..cb2278528 100644 --- a/apps/ensindexer/src/plugins/ens-v2/ponder.plugin.ts +++ b/apps/ensindexer/src/plugins/ens-v2/ponder.plugin.ts @@ -15,21 +15,21 @@ const namespace = createPluginNamespace(pluginName); export const config = createConfig({ networks: networksConfigForChain(chain), contracts: { - [namespace("EthRegistry")]: { - network: networkConfigForContract(chain, contracts.EthRegistry), - abi: contracts.EthRegistry.abi, - }, [namespace("RegistryDatastore")]: { network: networkConfigForContract(chain, contracts.RegistryDatastore), abi: contracts.RegistryDatastore.abi, }, + [namespace("Resolver")]: { + network: networkConfigForContract(chain, contracts.Resolver), + abi: contracts.Resolver.abi, + }, [namespace("RootRegistry")]: { network: networkConfigForContract(chain, contracts.RootRegistry), abi: contracts.RootRegistry.abi, }, - [namespace("OwnedResolver")]: { - network: networkConfigForContract(chain, contracts.OwnedResolver), - abi: contracts.OwnedResolver.abi, + [namespace("EthRegistry")]: { + network: networkConfigForContract(chain, contracts.EthRegistry), + abi: contracts.EthRegistry.abi, }, }, }); @@ -41,6 +41,6 @@ export const activate = activateHandlers({ import("./handlers/EthRegistry"), import("./handlers/RegistryDatastore"), import("./handlers/RootRegistry"), - import("./handlers/OwnedResolver"), + import("./handlers/Resolver"), ], }); diff --git a/apps/ensindexer/src/plugins/ens-v2/v2-lib.ts b/apps/ensindexer/src/plugins/ens-v2/v2-lib.ts index 0e7d77100..f2ec62a91 100644 --- a/apps/ensindexer/src/plugins/ens-v2/v2-lib.ts +++ b/apps/ensindexer/src/plugins/ens-v2/v2-lib.ts @@ -2,122 +2,93 @@ * This file temporarily located here for prototyping—should be moved to ensnode-utils. */ -import { Context, Event } from "ponder:registry"; +import { Context } from "ponder:registry"; import schema from "ponder:schema"; -import { eq } from "ponder"; -import { keccak256, toBytes } from "viem"; +import { LabelHash, Node } from "@ensnode/utils/types"; +import { AccountId } from "caip"; +import { Address, getAddress, hexToBigInt, toHex } from "viem"; const LABEL_HASH_MASK = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000n; -// Utility functions -export function createEventId(event: Event): string { - return [event.block.number, event.log.logIndex].join("-"); -} - -export function generateTokenId(label: string): string { - const hash = keccak256(toBytes(label)); +/// +/// UPSERTS +/// - // Convert the hash to BigInt and perform the bitwise operation - const hashBigInt = BigInt(hash); - const mask = BigInt(0x7); - const tokenId = hashBigInt & ~mask; // Equivalent to & ~0x7 - console.log("generateTokenId", label, hash, tokenId); - return tokenId.toString(); -} - -export function createDomainId(registryId: string | undefined, tokenId: string): string { - return `${registryId}-${tokenId}`; +export async function upsertRegistry( + context: Context, + values: typeof schema.v2_registry.$inferInsert, +) { + return await context.db.insert(schema.v2_registry).values(values).onConflictDoUpdate(values); } -export async function updateDomainLabel( +export async function upsertResolverRecords( context: Context, - domainId: string, - label: string, - tokenId: string, - timestamp: bigint, - event: any, - source: string, + values: typeof schema.v2_resolverRecords.$inferInsert, ) { - const domainRecord = await context.db.find(schema.v2_domain, { id: domainId }); - if (!domainRecord) { - console.log("Domain not found:", domainId); - return; - } - - console.log("Updating domain label:", domainRecord); + // ensure Resolver entity + await context.db + .insert(schema.v2_resolver) + .values({ id: values.resolverId }) + .onConflictDoNothing(); + + // ensure ResolverRecords entity + return context.db // + .insert(schema.v2_resolverRecords) + .values(values) + .onConflictDoUpdate(values); +} - // Update registry database if exists - const labelHash = BigInt(tokenId) & LABEL_HASH_MASK; - const registryRecord = await context.db.sql.query.v2_registry.findFirst({ - where: eq(schema.v2_registry.labelHash, labelHash.toString()), - }); +/// +/// IDS +/// - if (registryRecord) { - console.log("Registry record found:", registryRecord); - await context.db - .update(schema.v2_registry, { id: registryRecord.id }) - .set({ ...registryRecord, label: label }); +/** + * Encodes a contract's cross-chain unique address using a CAIP-10 AccountId. + * + * @param chainId source chain id + * @param address contract address + * @returns + */ +export const makeContractId = (chainId: number, address: Address) => { + // ensure checksummed + if (address !== getAddress(address)) { + throw new Error(`makeContractId: "${address}" is not checksummed`); } - let name = label; - if (source != "RootRegistry") { - let currentRegistryId = registryRecord!.id; - let currentName = name; - while (true) { - const parentRegistryRecord = await context.db.sql.query.v2_registry.findFirst({ - where: eq(schema.v2_registry.subregistryId, currentRegistryId), - }); + return new AccountId({ + chainId: { + namespace: "eip155", // ENSIndexer only ever indexes EVM chains namespaced by eip155 + reference: chainId.toString(), + }, + address, + }).toString(); +}; - if (!parentRegistryRecord) { - break; // We've reached the top level - } +export const makeResolverRecordsId = (resolverId: string, node: Node) => + [resolverId, node].join("-"); - console.log("Parent registry record found:", parentRegistryRecord); - let parentDomainRecord = await context.db.sql.query.v2_domain.findFirst({ - where: eq(schema.v2_domain.registry, parentRegistryRecord.id), - }); +export const makeDomainId = (registryId: string, maskedTokenId: bigint) => + [registryId, maskedTokenId].join("-"); - if (!parentDomainRecord) break; +export const makeResolverRecordsAddressId = (resolverRecordsId: string, coinType: bigint) => + [resolverRecordsId, coinType].join("-"); - console.log("Parent domain record found:", parentDomainRecord); +/// +/// UTILS +/// - if (parentDomainRecord.isTld) { - currentName = currentName + "." + parentDomainRecord.label; - console.log("Reached TLD. Final name:", currentName); - break; - } +/** + * masks a given tokenId + */ - currentName = currentName + "." + parentDomainRecord.label; - currentRegistryId = parentRegistryRecord.id; - console.log("Current name:", currentName); - } +export const maskTokenId = (tokenId: bigint) => tokenId & LABEL_HASH_MASK; - name = currentName; - } - // Update the domain record - const nameArray = domainRecord.name ? [...domainRecord.name, name] : [name]; - const newDomainRecord = { - ...domainRecord, - label: label, - name: nameArray, - labelHash: tokenId, - isTld: source === "RootRegistry" ? true : false, - updatedAt: timestamp, - }; - - await context.db.update(schema.v2_domain, { id: domainId }).set(newDomainRecord); - - // Store the event data - const eventId = createEventId(event); - await context.db.insert(schema.v2_newSubnameEvent).values({ - id: eventId, - registryId: domainRecord.registry, - label: label, - labelHash: tokenId, - source: source, - createdAt: timestamp, - updatedAt: timestamp, - }); - - console.log("Domain updated:", domainId); -} +/** + * encodes a hex labelHash as bigint, masking the lower 32 bits + */ +export const labelHashToTokenId = (labelHash: LabelHash) => hexToBigInt(labelHash, { size: 32 }); + +/** + * decodes a bigint tokenId into a hex labelHash + */ +export const tokenIdToLabelHash = (tokenId: bigint): LabelHash => toHex(tokenId, { size: 32 }); diff --git a/apps/ensindexer/src/plugins/eth/handlers/EthRegistrar.ts b/apps/ensindexer/src/plugins/eth/handlers/EthRegistrar.ts index d00729fc0..2c4c2c9bc 100644 --- a/apps/ensindexer/src/plugins/eth/handlers/EthRegistrar.ts +++ b/apps/ensindexer/src/plugins/eth/handlers/EthRegistrar.ts @@ -1,6 +1,6 @@ import { ponder } from "ponder:registry"; import { uint256ToHex32 } from "@ensnode/utils/subname-helpers"; -import type { Labelhash } from "@ensnode/utils/types"; +import type { LabelHash } from "@ensnode/utils/types"; import { makeRegistrarHandlers } from "../../../handlers/Registrar"; import { PonderENSPluginHandlerArgs } from "../../../lib/plugin-helpers"; @@ -11,7 +11,7 @@ import { PonderENSPluginHandlerArgs } from "../../../lib/plugin-helpers"; * direct subname of .eth that was registered. * https://github.com/ensdomains/ens-contracts/blob/db613bc/contracts/ethregistrar/ETHRegistrarController.sol#L215 */ -const tokenIdToLabelhash = (tokenId: bigint): Labelhash => uint256ToHex32(tokenId); +const tokenIdToLabelhash = (tokenId: bigint): LabelHash => uint256ToHex32(tokenId); export default function ({ ownedName, namespace }: PonderENSPluginHandlerArgs<"eth">) { const { diff --git a/apps/ensindexer/src/plugins/linea/handlers/EthRegistrar.ts b/apps/ensindexer/src/plugins/linea/handlers/EthRegistrar.ts index 845681478..ec7c1a107 100644 --- a/apps/ensindexer/src/plugins/linea/handlers/EthRegistrar.ts +++ b/apps/ensindexer/src/plugins/linea/handlers/EthRegistrar.ts @@ -1,7 +1,7 @@ import { ponder } from "ponder:registry"; import schema from "ponder:schema"; import { makeSubnodeNamehash, uint256ToHex32 } from "@ensnode/utils/subname-helpers"; -import type { Labelhash } from "@ensnode/utils/types"; +import type { LabelHash } from "@ensnode/utils/types"; import { zeroAddress } from "viem"; import { makeRegistrarHandlers } from "../../../handlers/Registrar"; import { upsertAccount } from "../../../lib/db-helpers"; @@ -14,7 +14,7 @@ import { PonderENSPluginHandlerArgs } from "../../../lib/plugin-helpers"; * direct subname of linea.eth that was registered. * https://github.com/Consensys/linea-ens/blob/3a4f02f/packages/linea-ens-contracts/contracts/ethregistrar/ETHRegistrarController.sol#L447 */ -const tokenIdToLabelhash = (tokenId: bigint): Labelhash => uint256ToHex32(tokenId); +const tokenIdToLabelhash = (tokenId: bigint): LabelHash => uint256ToHex32(tokenId); export default function ({ ownedName, namespace }: PonderENSPluginHandlerArgs<"linea.eth">) { const { diff --git a/apps/ensindexer/test/graphnode-helpers.spec.ts b/apps/ensindexer/test/graphnode-helpers.spec.ts index 7937a53a3..3e3230c8a 100644 --- a/apps/ensindexer/test/graphnode-helpers.spec.ts +++ b/apps/ensindexer/test/graphnode-helpers.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import type { Labelhash } from "@ensnode/utils/types"; +import type { LabelHash } from "@ensnode/utils/types"; import { labelByHash } from "../src/lib/graphnode-helpers"; describe("labelByHash", () => { @@ -39,7 +39,7 @@ describe("labelByHash", () => { it("throws an error for an invalid labelhash missing 0x prefix", async () => { await expect( labelByHash( - "12ca5d0b4ef1129e04bfe7d35ac9def2f4f91daeb202cbe6e613f1dd17b2da0600" as Labelhash, + "12ca5d0b4ef1129e04bfe7d35ac9def2f4f91daeb202cbe6e613f1dd17b2da0600" as LabelHash, ), ).rejects.toThrow("Labelhash must be 0x-prefixed"); }); diff --git a/docs/ensnode.io/src/content/docs/ensnode/reference/ensnode-v2-notes.mdx b/docs/ensnode.io/src/content/docs/ensnode/reference/ensnode-v2-notes.mdx index 92b6aad09..b27d56bac 100644 --- a/docs/ensnode.io/src/content/docs/ensnode/reference/ensnode-v2-notes.mdx +++ b/docs/ensnode.io/src/content/docs/ensnode/reference/ensnode-v2-notes.mdx @@ -103,3 +103,7 @@ In theory (and likely in practice) any normalized name will never become invalid - removes centralized dependency on the CCIP Gateway - flaky test experience with .cb.id name gateway - also helps indexer discovery + +# Notes Scratchpad + +- multichain primary names diff --git a/docs/ensnode.io/src/content/docs/ensnode/reference/subgraph-compatibility.mdx b/docs/ensnode.io/src/content/docs/ensnode/reference/subgraph-compatibility.mdx index 104e31e51..c56b8f6af 100644 --- a/docs/ensnode.io/src/content/docs/ensnode/reference/subgraph-compatibility.mdx +++ b/docs/ensnode.io/src/content/docs/ensnode/reference/subgraph-compatibility.mdx @@ -56,6 +56,7 @@ If you'd like to contribute to these features, please [open a Pull Request on Gi The following features of the subgraph graphql api are explicitly unsupported and are not planned. - [1-level-nested Entity `_orderBy` param](https://thegraph.com/docs/en/subgraphs/querying/graphql-api/#example-for-nested-entity-sorting) +- nested Entity filter params (i.e. `entity(where: { children_: { ... } }) { ... }`) (thegraph docs not available) - [time travel queries](https://thegraph.com/docs/en/subgraphs/querying/graphql-api/#time-travel-queries) - [_change_block filtering](https://thegraph.com/docs/en/subgraphs/querying/graphql-api/#example-for-block-filtering) diff --git a/packages/ens-deployments/package.json b/packages/ens-deployments/package.json index 54c660717..027bc03a5 100644 --- a/packages/ens-deployments/package.json +++ b/packages/ens-deployments/package.json @@ -39,6 +39,6 @@ "viem": "catalog:" }, "dependencies": { - "@ponder/utils": "^0.2.3" + "@ponder/utils": "catalog:" } } diff --git a/packages/ens-deployments/src/abis/ens-v2/OwnedResolver.ts b/packages/ens-deployments/src/abis/ens-v2/Resolver.ts similarity index 99% rename from packages/ens-deployments/src/abis/ens-v2/OwnedResolver.ts rename to packages/ens-deployments/src/abis/ens-v2/Resolver.ts index 2e161c1e6..c4716a9f8 100644 --- a/packages/ens-deployments/src/abis/ens-v2/OwnedResolver.ts +++ b/packages/ens-deployments/src/abis/ens-v2/Resolver.ts @@ -1,4 +1,4 @@ -export const OwnedResolver = [ +export const Resolver = [ { inputs: [ { diff --git a/packages/ens-deployments/src/filters.ts b/packages/ens-deployments/src/filters.ts index 34246d6a9..f39cfe20a 100644 --- a/packages/ens-deployments/src/filters.ts +++ b/packages/ens-deployments/src/filters.ts @@ -1,4 +1,4 @@ -import { SubregistryContractConfig } from "./types"; +import { ContractConfig } from "./types"; export const ETHResolverFilter = [ { event: "AddrChanged", args: {} }, @@ -21,7 +21,7 @@ export const ETHResolverFilter = [ { event: "DNSRecordChanged", args: {} }, { event: "DNSRecordDeleted", args: {} }, { event: "DNSZonehashChanged", args: {} }, -] as const satisfies SubregistryContractConfig["filter"]; +] as const satisfies ContractConfig["filter"]; export const BaseResolverFilter = [ { event: "AddrChanged", args: {} }, @@ -36,7 +36,7 @@ export const BaseResolverFilter = [ { event: "DNSRecordChanged", args: {} }, { event: "DNSRecordDeleted", args: {} }, { event: "DNSZonehashChanged", args: {} }, -] as const satisfies SubregistryContractConfig["filter"]; +] as const satisfies ContractConfig["filter"]; export const LineaResolverFilter = [ { event: "AddrChanged", args: {} }, @@ -51,4 +51,4 @@ export const LineaResolverFilter = [ { event: "DNSRecordChanged", args: {} }, { event: "DNSRecordDeleted", args: {} }, { event: "DNSZonehashChanged", args: {} }, -] as const satisfies SubregistryContractConfig["filter"]; +] as const satisfies ContractConfig["filter"]; diff --git a/packages/ens-deployments/src/sepolia.ts b/packages/ens-deployments/src/sepolia.ts index 0f0a8abab..7e969d3e3 100644 --- a/packages/ens-deployments/src/sepolia.ts +++ b/packages/ens-deployments/src/sepolia.ts @@ -16,8 +16,8 @@ import { Resolver as eth_Resolver } from "./abis/eth/Resolver"; // ENS v2 ABIs import { ETHRegistry as ensV2_ETHRegistry } from "./abis/ens-v2/ETHRegistry"; -import { OwnedResolver as ensV2_OwnedResolver } from "./abis/ens-v2/OwnedResolver"; import { RegistryDatastore as ensV2_RegistryDatastore } from "./abis/ens-v2/RegistryDatastore"; +import { Resolver as ensV2_Resolver } from "./abis/ens-v2/Resolver"; import { RootRegistry as ensV2_RootRegistry } from "./abis/ens-v2/RootRegistry"; /** @@ -76,32 +76,25 @@ export default { // Addresses and Start Blocks from ens-ponder // https://github.com/ensdomains/ens-ponder contracts: { - EthRegistry: { - abi: ensV2_ETHRegistry, - address: "0xFd8562F0B884b5f8d137ff50D25fc26b34868172", - startBlock: 7699319, - }, RegistryDatastore: { abi: ensV2_RegistryDatastore, address: "0x73308B430b61958e3d8C4a6db08153372d5eb125", startBlock: 7699319, }, + Resolver: { + abi: ensV2_Resolver, + startBlock: 7699319, + }, + EthRegistry: { + abi: ensV2_ETHRegistry, + address: "0xFd8562F0B884b5f8d137ff50D25fc26b34868172", + startBlock: 7699319, + }, RootRegistry: { abi: ensV2_RootRegistry, address: "0xc44D7201065190B290Aaaf6efaDFD49d530547A3", startBlock: 7699319, }, - OwnedResolver: { - abi: ensV2_OwnedResolver, - factory: { - address: "0x33d438bb85B76C9211c4F259109D94Fe83F5A5eC", - event: parseAbiItem( - "event ProxyDeployed(address indexed sender, address indexed proxyAddress, uint256 salt, address implementation)", - ), - parameter: "proxyAddress", - }, - startBlock: 7699319, - }, }, }, /** diff --git a/packages/ens-deployments/src/types.ts b/packages/ens-deployments/src/types.ts index c8eb3295a..e632b3ee9 100644 --- a/packages/ens-deployments/src/types.ts +++ b/packages/ens-deployments/src/types.ts @@ -1,76 +1,48 @@ -import type { factory } from "ponder"; -import type { Abi, Address, Chain } from "viem"; +import type { ContractConfig as PonderContractConfig } from "ponder"; +import type { Chain } from "viem"; /** * Encodes a set of chains known to provide an "ENS deployment". * - * Each "ENS deployment" is a single, unified namespace of ENS names with: - * - A root Registry deployed to the "ENS Deployment" chain. - * - A capability to expand from that root Registry across any number of chains, subregistries, and offchain resources. + * Each "ENS deployment" is a single, unified, and isolated namespace of ENS names. * - * 'ens-test-env' represents an "ENS deployment" running on a local Anvil chain for testing - * protocol changes, running deterministic test suites, and local development. + * A deployment include ENSv1 contracts with: + * - A root Registry deployed to the L1 indicated by the `ENSDeploymentChain`. + * - A capability to expand from that root Registry across any number of chains, subregistries, and + * offchain resources. + * + * A deployment may include ENSv2 contracts. + * + * NOTE: the 'ens-test-env' deployment represents an ENS deployment running on a local Anvil chain + * for testing protocol changes, running deterministic test suites, and local development. * https://github.com/ensdomains/ens-test-env */ export type ENSDeploymentChain = "mainnet" | "sepolia" | "holesky" | "ens-test-env"; -/** - * Encodes a set of known subregistries. - */ -export type SubregistryName = "eth" | "base" | "linea" | "ens-v2"; - -/** - * EventFilter specifies a given event's name and arguments to filter that event by. - * It is intentionally a subset of ponder's `ContractConfig['filter']`. - */ -export interface EventFilter { - event: string; - args: Record; -} - /** * Defines the abi, address, filter, and startBlock of a contract relevant to indexing a subregistry. - * A contract is located on-chain either by a static `address` or the event signatures (`filter`) - * one should filter the chain for. + * See Ponder's [Contracts and Networks Documentation](https://ponder.sh/docs/contracts-and-networks) + * for more information. * * @param abi - the ABI of the contract - * @param address - (optional) address of the contract + * @param address - (optional) address of the contract or a factory spec * @param filter - (optional) array of event signatures to filter the log by - * @param startBlock - block number the contract was deployed in + * @param startBlock - (required) block to start indexing from */ -export type SubregistryContractConfig = - | { - readonly abi: Abi; - readonly address: Address; - readonly filter?: never; - readonly factory?: never; - readonly startBlock: number; - } - | { - readonly abi: Abi; - readonly address?: never; - readonly filter: EventFilter[]; - readonly factory?: never; - readonly startBlock: number; - } - | { - readonly abi: Abi; - readonly address?: never; - readonly filter?: never; - readonly factory: Parameters[0]; - readonly startBlock: number; - }; +export type ContractConfig = Pick & { + startBlock: number; +}; /** - * Encodes the deployment of a subregistry, including the target chain and contracts. + * Encodes a set of contract configs on a given chain. */ -export interface SubregistryDeploymentConfig { +export interface AddressBook { chain: Chain; - contracts: Record; + contracts: Record; } /** - * Encodes the set of known subregistries for an "ENS deployment". + * Encodes the set of known contract configs for a given "ENS deployment" root */ export type ENSDeploymentConfig = { /** @@ -78,26 +50,26 @@ export type ENSDeploymentConfig = { * * Required for each "ENS deployment". */ - eth: SubregistryDeploymentConfig; + eth: AddressBook; /** * Subregistry for direct subnames of 'base.eth'. * * Optional for each "ENS deployment". */ - base?: SubregistryDeploymentConfig; + base?: AddressBook; /** * Subregistry for direct subnames of 'linea.eth'. * * Optional for each "ENS deployment". */ - linea?: SubregistryDeploymentConfig; + linea?: AddressBook; /** * ENS v2 Contracts - * TODO: the naming in this package is no longer accurate — perhaps this should be 'AddressBook' - * or something like that. + * + * Optional for each "ENS deployment". */ - "ens-v2"?: SubregistryDeploymentConfig; + "ens-v2"?: AddressBook; }; diff --git a/packages/ensnode-utils/src/types.ts b/packages/ensnode-utils/src/types.ts index ecc526111..41c566d0a 100644 --- a/packages/ensnode-utils/src/types.ts +++ b/packages/ensnode-utils/src/types.ts @@ -14,8 +14,15 @@ export type Node = Hex; /** * A hash value that identifies only a single part or "label" of an ENS name. - * The labelhash is just the Keccak-256 output for the label. + * The labelHash is just the Keccak-256 output for the label. * * @link https://docs.ens.domains/ensip/1#labelhash-algorithm */ -export type Labelhash = Hex; +export type LabelHash = Hex; + +/** + * Represents a CAIP-10 AccountId + * + * @link https://chainagnostic.org/CAIPs/caip-10 + */ +export type CAIP10AccountId = string; diff --git a/packages/ensrainbow-sdk/src/client.ts b/packages/ensrainbow-sdk/src/client.ts index 1baeef165..87a61dbdc 100644 --- a/packages/ensrainbow-sdk/src/client.ts +++ b/packages/ensrainbow-sdk/src/client.ts @@ -1,6 +1,6 @@ import type { Cache } from "@ensnode/utils/cache"; import { LruCache } from "@ensnode/utils/cache"; -import type { Labelhash } from "@ensnode/utils/types"; +import type { LabelHash } from "@ensnode/utils/types"; import { DEFAULT_ENSRAINBOW_URL, ErrorCode, StatusCode } from "./consts"; export namespace EnsRainbow { @@ -9,7 +9,7 @@ export namespace EnsRainbow { export interface ApiClient { count(): Promise; - heal(labelhash: Labelhash): Promise; + heal(labelhash: LabelHash): Promise; health(): Promise; @@ -132,7 +132,7 @@ export interface EnsRainbowApiClientOptions { */ export class EnsRainbowApiClient implements EnsRainbow.ApiClient { private readonly options: EnsRainbowApiClientOptions; - private readonly cache: Cache; + private readonly cache: Cache; public static readonly DEFAULT_CACHE_CAPACITY = 1000; @@ -154,7 +154,7 @@ export class EnsRainbowApiClient implements EnsRainbow.ApiClient { ...options, }; - this.cache = new LruCache( + this.cache = new LruCache( this.options.cacheCapacity, ); } @@ -201,7 +201,7 @@ export class EnsRainbowApiClient implements EnsRainbow.ApiClient { * // } * ``` */ - async heal(labelhash: Labelhash): Promise { + async heal(labelhash: LabelHash): Promise { const cachedResult = this.cache.get(labelhash); if (cachedResult) { diff --git a/packages/ensrainbow-sdk/src/label-utils.ts b/packages/ensrainbow-sdk/src/label-utils.ts index 894583fe9..ce160c94b 100644 --- a/packages/ensrainbow-sdk/src/label-utils.ts +++ b/packages/ensrainbow-sdk/src/label-utils.ts @@ -1,4 +1,4 @@ -import type { Labelhash } from "@ensnode/utils/types"; +import type { LabelHash } from "@ensnode/utils/types"; import { ByteArray, hexToBytes } from "viem"; /** @@ -7,7 +7,7 @@ import { ByteArray, hexToBytes } from "viem"; * @returns A ByteArray containing the bytes * @throws Error if `labelHash` is not a valid 32-byte hex string */ -export function labelHashToBytes(labelHash: Labelhash): ByteArray { +export function labelHashToBytes(labelHash: LabelHash): ByteArray { try { if (labelHash.length !== 66) { throw new Error(`Invalid labelhash length ${labelHash.length} characters (expected 66)`); diff --git a/packages/ponder-schema/package.json b/packages/ponder-schema/package.json index 128c64804..07e45321e 100644 --- a/packages/ponder-schema/package.json +++ b/packages/ponder-schema/package.json @@ -35,6 +35,7 @@ "viem": "catalog:" }, "devDependencies": { + "@ensnode/utils": "workspace:", "@biomejs/biome": "catalog:", "@ensnode/shared-configs": "workspace:", "tsup": "catalog:", diff --git a/packages/ponder-schema/src/ponder.schema.ts b/packages/ponder-schema/src/ponder.schema.ts index f672c6b7d..1114255e7 100644 --- a/packages/ponder-schema/src/ponder.schema.ts +++ b/packages/ponder-schema/src/ponder.schema.ts @@ -1,5 +1,6 @@ -import { index, onchainTable, relations } from "ponder"; -import type { Address } from "viem"; +import { CAIP10AccountId } from "@ensnode/utils/types"; +import { index, onchainTable, relations, uniqueIndex } from "ponder"; +import { type Address, zeroAddress } from "viem"; import { monkeypatchCollate } from "./collate"; /** @@ -685,99 +686,266 @@ export const versionChangedRelations = relations(versionChanged, ({ one }) => ({ * NOTE: These entities kept namespaced for rapid prototyping—see v2 plans for additional context. * https://www.ensnode.io/ensnode/reference/ensnode-v2-notes/ * - * Original Schema from https://github.com/ensdomains/ens-ponder + * The core design principle here is that a Registry references many Domains which each reference a + * (sub)Registry which references many Domains... etc. This accurately represents the nature of the + * on-chain contracts and supports the dynamic re-arrangement of the hierarchical namespace as + * proposed by ENSv2. + * + * For example, when a subregistry is updated for a given Domain, the tree now represent's that + * subregistry's Domains, without and bulk creation/deletion being necessary. + * + * open questions: + * - how do v1 subregistries other than .eth handle the migration? + * - what does v2's .eth registry `reliquishing` accomplish, from an indexing perspective? + * - a 'registry' is... any contract tht implements ERC1155 and NewSubname()? may mean that we need to + * index every instance of those events... not very good at all... would be nice if a Registry contract + * emitted an event like NewRegistry() in constructor that indicated whether it was an ENS registry. + * or if it were required to announce itself against some singleton address and we could use the + * factory pattern to index those addresses. otherwise we'll need to track every ERC1155 contract. + * (ponder doesn't handle dynamic address indexing for efficiency reasons) + * - could multiple tokenIds in the ENSv2 system have the same subregistry address? seems yes because + * datastore does not enforce uniqueness, but this results in a many-many mapping between labels + * and subregistries which seems very annoying to work with, in particular for upward traversals. + * the indexer could enforce uniqueness and if a name sets a registry address that's already assigned + * we could ignore that subtree? not ideal. since ENSv2 is on L2 we can likely include uniqueness + * check without too much of a penalty? + * search TODO(registry-domain-uniq): in codebase to see locations where this is noted + * - event order guarantees would be really nice as part of the v2 spec, but technically not needed + * - i.e. registries must emit NewSubname before any ERC1155 events, must emit NewSubname before + * calling datastore, etc. indexers love an event that's guaranteed to be first in order to setup entity + * - should token ids within registry contracts not be masked? what happens if registry mints + * multiple tokens with conflicting tokenIds? they'll have the same state in datastore but there will + * be multiple tokens in the 1155 contract with different owners, etc. how should the indexer represent this? + * - we _need_ to be able to configure ponder's handling of null values in order to correctly index ENSv2 + * because ENSv2 doesn't emit the namehash/labelhash, only human-readable args, which may contain null bytes + * https://github.com/ponder-sh/ponder/issues/1456 + * + * todo in schema: + * - events & event relations + * - store node on label in order to set up resolver record references? + * - createdAt and updatedAt values across the board + * - could use composite schemas more frequently instead of concatenated ids */ -export const v2_domain = onchainTable("v2_domain", (t) => ({ - id: t.text().primaryKey(), - label: t.text(), - name: t.text().array(), // Will store serialized array as JSON string - labelHash: t.text(), - owner: t.text(), - registry: t.text(), - isTld: t.boolean(), - createdAt: t.bigint("createdAt").notNull(), - updatedAt: t.bigint("updatedAt").notNull(), -})); +/** + * A Registry represents a Registry _contract_ on-chain, and is keyed by its chain-specific address. + */ +export const v2_registry = onchainTable( + "v2_registries", + (t) => ({ + /** + * Registry is keyed by [CAIP-10](https://chainagnostic.org/CAIPs/caip-10) + */ + id: t.text().primaryKey().$type(), -export const v2_domainRelations = relations(v2_domain, ({ one }) => ({ - registry: one(v2_registry, { - fields: [v2_domain.registry], - references: [v2_registry.id], + /** + * A Registry can be the subregistry of exactly one Domain. + * NOTE: we duplicate this reference in order to make cachable traversals trivial. + * TODO(registry-domain-uniq): see above + */ + domainId: t.text(), + + // TODO: reference registry-specific logic entities here (i.e. .eth registry expiries) }), -})); + (t) => ({}), +); -export const v2_registry = onchainTable("v2_registry", (t) => ({ - id: t.text().primaryKey(), - labelHash: t.text(), - label: t.text(), - subregistryId: t.text(), - resolver: t.text(), - flags: t.bigint(), - createdAt: t.bigint("createdAt").notNull(), - updatedAt: t.bigint("updatedAt").notNull(), -})); +export const v2_registryRelations = relations(v2_registry, ({ one, many }) => ({ + /** + * a registry has one domain (i.e is that domain's subregistry) + * + * TODO(registry-domain-uniq): see above + */ + domain: one(v2_domain, { + fields: [v2_registry.domainId], + references: [v2_domain.id], + relationName: "isSubregistryOfDomain", + }), -export const v2_subregistryUpdateEvent = onchainTable("v2_subregistryUpdateEvent", (t) => ({ - id: t.text().primaryKey(), - registryId: t.text(), - labelHash: t.text(), - subregistryId: t.text(), - flags: t.bigint(), - createdAt: t.bigint("createdAt").notNull(), - updatedAt: t.bigint("updatedAt").notNull(), + // a registry has many labels by label.registryId + domains: many(v2_domain, { + relationName: "managedDomains", + }), })); -export const v2_resolverUpdateEvent = onchainTable("v2_resolverUpdateEvent", (t) => ({ - id: t.text().primaryKey(), - registryId: t.text(), - labelHash: t.text(), - resolverId: t.text(), - flags: t.bigint(), - createdAt: t.bigint("createdAt").notNull(), - updatedAt: t.bigint("updatedAt").notNull(), -})); +/** + * A Domain entity represents a subname in the hierarchical namespace. + * + * In ENSv2 this maps 1:1 with a Registry contract's tokens. + * NOTE: we key Domain by maskedTokenId to avoid collisions. + */ +export const v2_domain = onchainTable( + "v2_domains", + (t) => ({ + /** + * Domains are unique by (registryId, maskedTokenId), encoded as `${registryId}-${maskedTokenId}` + */ + id: t.text().primaryKey(), -export const v2_newSubnameEvent = onchainTable("v2_newSubnameEvent", (t) => ({ - id: t.text().primaryKey(), - registryId: t.text(), - label: t.text(), - labelHash: t.text(), - source: t.text(), // "EthRegistry" or "RootRegistry" - createdAt: t.bigint("createdAt").notNull(), - updatedAt: t.bigint("updatedAt").notNull(), -})); + /** + * A Domain belongs to a Registry. + */ + registryId: t.text().notNull(), + + /** + * A Domain entity represents a given labelHash value i.e. the result of `labelhash()`, encoded + * as a bigint 'tokenId', within a given Registry, with the lower 32 bits masked. + */ + maskedTokenId: t.bigint().notNull(), + + /** + * A Domain stores its un-masked tokenId for calculating `node`. + * + * tokenId alone is _not_ a UUID value, and collisions are expected (i.e. there will be a Domain + * entity representing the `hello` in `hello.example.eth` and a Domain representing the `hello` + * in `hello.eth` that have identical tokenId values). + * + * Domain entities are unique by (registryId, tokenId), enforced by ERC1155. Note that this tokenId + * value is _not_ masked. + */ + tokenId: t.bigint().notNull(), + + /** + * The human-readable representation of a given name segment. + * + * In ENSv1, labels may or may not be known, hence this field is optional. + * In ENSv2, this `label` is always known. + */ + label: t.text(), + + /** + * A Domain may have an URI. + */ + uri: t.text(), + + /** + * A Domain has an `owner` address, potentially zeroAddress. + * + * NOTE: stored as text with type Address to ensure checksumming is persisted + */ + owner: t.text().notNull().default(zeroAddress).$type
(), + + /** + * A Domain can be assigned a (sub)Registry with flags. + * NOTE: we include bi-directonal references in order to make cachable traversals trivial. + */ + subregistryId: t.text(), + subregistryFlags: t.bigint().notNull().default(0n), + + /** + * A Domain can be configured with a given Resolver with flags. + */ + resolverId: t.text(), + resolverFlags: t.bigint().notNull().default(0n), + }), + (t) => ({ + // a Domain is unique by (registryId, maskedTokenId) (indexer-invariant) + registryTokenIdIndex: uniqueIndex().on(t.registryId, t.maskedTokenId), + // a Domain is unique by (registryId, tokenId) (ERC1155-invariant) + registryMaskedTokenIdIndex: uniqueIndex().on(t.registryId, t.tokenId), + }), +); + +export const v2_domainRelations = relations(v2_domain, ({ one, many }) => ({ + // domain belongs to one (parent)registry + registry: one(v2_registry, { + fields: [v2_domain.registryId], + references: [v2_registry.id], + }), -export const v2_registryRelations = relations(v2_registry, ({ one }) => ({ + // domain references one (sub)registry subregistry: one(v2_registry, { - fields: [v2_registry.subregistryId], + fields: [v2_domain.subregistryId], references: [v2_registry.id], }), + + // domain references one resolver + resolver: one(v2_resolver, { + fields: [v2_domain.resolverId], + references: [v2_resolver.id], + }), })); -export const v2_resolver = onchainTable("v2_resolver", (t) => ({ - id: t.text().primaryKey(), - address: t.text(), - node: t.text(), - createdAt: t.bigint("createdAt").notNull(), - updatedAt: t.bigint("updatedAt").notNull(), +/** + * A Resolver represents a given Resolver _contract_ on-chain. + */ +export const v2_resolver = onchainTable( + "v2_resolvers", + (t) => ({ + /** + * Resolver are keyed by [CAIP-10](https://chainagnostic.org/CAIPs/caip-10). + */ + id: t.text().primaryKey().$type(), + }), + (t) => ({}), +); + +export const v2_resolverRelations = relations(v2_resolver, ({ one, many }) => ({ + // any number of domains can reference a given Resolver + domains: many(v2_domain), + + // resolver has many ResolverRecords + records: many(v2_resolverRecords), })); -export const v2_registryResolverRelations = relations(v2_registry, ({ one }) => ({ +/** + * A ResolverRecords represents a pairwise relationship between a Resolver entity/contract + * and a given `node`. + */ +export const v2_resolverRecords = onchainTable( + "v2_resolver_records", + (t) => ({ + /** + * A ResolverRecords is keyed by (resolverId, node), encoded as `${resolverId}-${node}`. + */ + id: t.text().primaryKey(), + + /** + * A ResolverRecords maintains references to the Resolver contract and which node it stores + * records for. + */ + resolverId: t.text().notNull(), + node: t.hex().notNull(), + + // TODO: implement all record storage here + }), + (t) => ({ + // uniquely index against the composite key + idxResolverNode: uniqueIndex().on(t.resolverId, t.node), + }), +); + +export const v2_resolverRecordRelations = relations(v2_resolverRecords, ({ one, many }) => ({ + // records belongs to Resolver resolver: one(v2_resolver, { - fields: [v2_registry.resolver], + fields: [v2_resolverRecords.resolverId], references: [v2_resolver.id], }), -})); -export const v2_transferSingleEvent = onchainTable("v2_transferSingleEvent", (t) => ({ - id: t.text().primaryKey(), - registryId: t.text(), - tokenId: t.text(), - from: t.text(), - to: t.text(), - value: t.bigint(), - source: t.text(), // "EthRegistry" or "RootRegistry" - createdAt: t.bigint("createdAt").notNull(), - updatedAt: t.bigint("updatedAt").notNull(), + // records has many addresses + addresses: many(v2_resolverRecordsAddress), })); + +export const v2_resolverRecordsAddress = onchainTable( + "v2_resolver_records_addresses", + (t) => ({ + id: t.text().primaryKey(), + resolverRecordsId: t.text().notNull(), + coinType: t.bigint().notNull(), + address: t.text().notNull(), + }), + (t) => ({ + byCoinType: index().on(t.id, t.coinType), + }), +); + +export const v2_resolverRecordsAddressRelations = relations( + v2_resolverRecordsAddress, + ({ one, many }) => ({ + // resolverrecordsaddress belongs to resolver records + records: one(v2_resolverRecords, { + fields: [v2_resolverRecordsAddress.resolverRecordsId], + references: [v2_resolverRecords.id], + }), + }), +); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa812c2ca..056247b38 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,8 +28,8 @@ catalogs: specifier: ^0.39.3 version: 0.39.3 hono: - specifier: ^4.6.14 - version: 4.6.17 + specifier: ^4.7.4 + version: 4.7.4 ponder: specifier: ^0.9.27 version: 0.9.27 @@ -74,6 +74,43 @@ importers: specifier: 'catalog:' version: 5.7.3 + apps/api: + dependencies: + '@ensdomains/ensjs': + specifier: ^4.0.2 + version: 4.0.2(typescript@5.7.3)(viem@2.22.13(typescript@5.7.3)(zod@3.24.2))(zod@3.24.2) + '@ensnode/ponder-schema': + specifier: workspace:* + version: link:../../packages/ponder-schema + '@ensnode/utils': + specifier: workspace:* + version: link:../../packages/ensnode-utils + '@ponder/client': + specifier: 'catalog:' + version: 0.9.27(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.7.0)(bun-types@1.2.5)(kysely@0.26.3)(pg@8.11.3)(typescript@5.7.3) + '@ponder/utils': + specifier: 'catalog:' + version: 0.2.3(typescript@5.7.3)(viem@2.22.13(typescript@5.7.3)(zod@3.24.2)) + bun: + specifier: ^1.2.2 + version: 1.2.2 + drizzle-orm: + specifier: 'catalog:' + version: 0.39.3(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.7.0)(bun-types@1.2.5)(kysely@0.26.3)(pg@8.11.3) + hono: + specifier: 'catalog:' + version: 4.7.4 + viem: + specifier: 'catalog:' + version: 2.22.13(typescript@5.7.3)(zod@3.24.2) + devDependencies: + '@ensnode/shared-configs': + specifier: workspace:* + version: link:../../packages/shared-configs + '@types/bun': + specifier: latest + version: 1.2.5 + apps/ensadmin: dependencies: '@graphiql/react': @@ -199,10 +236,10 @@ importers: version: 0.11.1(@types/node@22.13.5)(graphql-ws@5.16.2(graphql@16.10.0))(graphql@16.10.0) '@ponder/client': specifier: 'catalog:' - version: 0.9.27(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.7.0)(kysely@0.26.3)(pg@8.11.3)(typescript@5.7.3) + version: 0.9.27(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.7.0)(bun-types@1.2.5)(kysely@0.26.3)(pg@8.11.3)(typescript@5.7.3) '@ponder/react': specifier: 'catalog:' - version: 0.9.27(@ponder/client@0.9.27(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.7.0)(kysely@0.26.3)(pg@8.11.3)(typescript@5.7.3))(@tanstack/react-query@5.66.9(react@18.3.1))(react@18.3.1)(typescript@5.7.3) + version: 0.9.27(@ponder/client@0.9.27(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.7.0)(bun-types@1.2.5)(kysely@0.26.3)(pg@8.11.3)(typescript@5.7.3))(@tanstack/react-query@5.66.9(react@18.3.1))(react@18.3.1)(typescript@5.7.3) '@ponder/utils': specifier: 'catalog:' version: 0.2.3(typescript@5.7.3)(viem@2.22.13(typescript@5.7.3)(zod@3.24.2)) @@ -300,12 +337,15 @@ importers: '@ensnode/utils': specifier: workspace:* version: link:../../packages/ensnode-utils + caip: + specifier: ^1.1.1 + version: 1.1.1 hono: specifier: 'catalog:' - version: 4.6.17 + version: 4.7.4 ponder: specifier: 'catalog:' - version: 0.9.27(@opentelemetry/api@1.7.0)(@types/node@20.17.14)(hono@4.6.17)(lightningcss@1.29.1)(typescript@5.7.3)(viem@2.22.13(typescript@5.7.3)(zod@3.24.2))(zod@3.24.2) + version: 0.9.27(@opentelemetry/api@1.7.0)(@types/node@20.17.14)(bun-types@1.2.5)(hono@4.7.4)(lightningcss@1.29.1)(typescript@5.7.3)(viem@2.22.13(typescript@5.7.3)(zod@3.24.2))(zod@3.24.2) ts-deepmerge: specifier: ^7.0.2 version: 7.0.2 @@ -339,7 +379,7 @@ importers: version: link:../../packages/ensnode-utils '@hono/node-server': specifier: ^1.4.1 - version: 1.13.3(hono@4.6.17) + version: 1.13.3(hono@4.7.4) bun: specifier: ^1.2.2 version: 1.2.2 @@ -348,7 +388,7 @@ importers: version: 1.4.1 hono: specifier: 'catalog:' - version: 4.6.17 + version: 4.7.4 pino: specifier: ^8.19.0 version: 8.21.0 @@ -429,7 +469,7 @@ importers: packages/ens-deployments: dependencies: '@ponder/utils': - specifier: ^0.2.3 + specifier: 'catalog:' version: 0.2.3(typescript@5.7.3)(viem@2.22.13(typescript@5.7.3)(zod@3.24.2)) devDependencies: '@biomejs/biome': @@ -440,7 +480,7 @@ importers: version: link:../shared-configs ponder: specifier: 'catalog:' - version: 0.9.27(@opentelemetry/api@1.7.0)(@types/node@22.13.5)(hono@4.6.17)(lightningcss@1.29.1)(typescript@5.7.3)(viem@2.22.13(typescript@5.7.3)(zod@3.24.2))(zod@3.24.2) + version: 0.9.27(@opentelemetry/api@1.7.0)(@types/node@22.13.5)(bun-types@1.2.5)(hono@4.7.4)(lightningcss@1.29.1)(typescript@5.7.3)(viem@2.22.13(typescript@5.7.3)(zod@3.24.2))(zod@3.24.2) tsup: specifier: 'catalog:' version: 8.3.6(jiti@2.4.2)(postcss@8.5.1)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) @@ -504,7 +544,7 @@ importers: dependencies: drizzle-orm: specifier: 'catalog:' - version: 0.39.3(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.7.0)(kysely@0.26.3)(pg@8.11.3) + version: 0.39.3(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.7.0)(bun-types@1.2.5)(kysely@0.26.3)(pg@8.11.3) parse-prometheus-text-format: specifier: ^1.1.1 version: 1.1.1 @@ -523,10 +563,10 @@ importers: version: 20.17.14 hono: specifier: 'catalog:' - version: 4.6.17 + version: 4.7.4 ponder: specifier: 'catalog:' - version: 0.9.27(@opentelemetry/api@1.7.0)(@types/node@20.17.14)(hono@4.6.17)(lightningcss@1.29.1)(typescript@5.7.3)(viem@2.22.13(typescript@5.7.3)(zod@3.24.2))(zod@3.24.2) + version: 0.9.27(@opentelemetry/api@1.7.0)(@types/node@20.17.14)(bun-types@1.2.5)(hono@4.7.4)(lightningcss@1.29.1)(typescript@5.7.3)(viem@2.22.13(typescript@5.7.3)(zod@3.24.2))(zod@3.24.2) tsup: specifier: 'catalog:' version: 8.3.6(jiti@2.4.2)(postcss@8.5.1)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) @@ -541,7 +581,7 @@ importers: dependencies: ponder: specifier: 'catalog:' - version: 0.9.27(@opentelemetry/api@1.7.0)(@types/node@22.13.5)(hono@4.6.17)(lightningcss@1.29.1)(typescript@5.7.3)(viem@2.22.13(typescript@5.7.3)(zod@3.24.2))(zod@3.24.2) + version: 0.9.27(@opentelemetry/api@1.7.0)(@types/node@22.13.5)(bun-types@1.2.5)(hono@4.7.4)(lightningcss@1.29.1)(typescript@5.7.3)(viem@2.22.13(typescript@5.7.3)(zod@3.24.2))(zod@3.24.2) viem: specifier: 'catalog:' version: 2.22.13(typescript@5.7.3)(zod@3.24.2) @@ -552,6 +592,9 @@ importers: '@ensnode/shared-configs': specifier: 'workspace:' version: link:../shared-configs + '@ensnode/utils': + specifier: 'workspace:' + version: link:../ensnode-utils tsup: specifier: 'catalog:' version: 8.3.6(jiti@2.4.2)(postcss@8.5.1)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) @@ -575,7 +618,7 @@ importers: version: 2.2.3 drizzle-orm: specifier: 'catalog:' - version: 0.39.3(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.7.0)(kysely@0.26.3)(pg@8.11.3) + version: 0.39.3(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.7.0)(bun-types@1.2.5)(kysely@0.26.3)(pg@8.11.3) graphql: specifier: ^16.10.0 version: 16.10.0 @@ -597,7 +640,7 @@ importers: version: 20.17.14 hono: specifier: 'catalog:' - version: 4.6.17 + version: 4.7.4 tsup: specifier: 'catalog:' version: 8.3.6(jiti@2.4.2)(postcss@8.5.1)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) @@ -2250,6 +2293,9 @@ packages: '@types/babel__traverse@7.20.6': resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + '@types/bun@1.2.5': + resolution: {integrity: sha512-w2OZTzrZTVtbnJew1pdFmgV99H0/L+Pvw+z1P67HaR18MHOzYnTYOi6qzErhK8HyT+DB782ADVPPE92Xu2/Opg==} + '@types/codemirror@0.0.90': resolution: {integrity: sha512-8Z9+tSg27NPRGubbUPUCrt5DDG/OWzLph5BvcDykwR5D7RyZh5mhHG0uS1ePKV1YFCA+/cwc4Ey2AJAEFfV3IA==} @@ -2342,6 +2388,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/ws@8.5.14': + resolution: {integrity: sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==} + '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -2721,8 +2770,12 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bun-types@1.2.5: + resolution: {integrity: sha512-3oO6LVGGRRKI4kHINx5PIdIgnLRb7l/SprhzqXapmoYkFl5m4j6EvALvbDVuuBFaamB46Ap6HCUxIXNLCGy+tg==} + bun@1.2.2: resolution: {integrity: sha512-RUc8uVVTw8WoASUzXaEQJR1s7mnwoHm3P871qBUIqSaoOpuwcU+bSVX151/xoqDwnyv38SjOX7yQ3oO0IeT73g==} + cpu: [arm64, x64, aarch64] os: [darwin, linux, win32] hasBin: true @@ -2740,6 +2793,9 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + caip@1.1.1: + resolution: {integrity: sha512-a3v5lteUUOoyRI0U6qe5ayCCGkF2mCmJ5zQMDnOD2vRjgRg6sm9p8TsRC2h4D4beyqRN9RYniphAPnj/+jQC6g==} + call-bind-apply-helpers@1.0.1: resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==} engines: {node: '>= 0.4'} @@ -3858,8 +3914,8 @@ packages: hey-listen@1.0.8: resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==} - hono@4.6.17: - resolution: {integrity: sha512-Kbh4M0so2RzLiIg6iP33DoTU68TdvP2O/kb1Hhhdwa37fazuf402ig8ZRfjkz2dqXwiWl2dAgh0f++TuKAdOtQ==} + hono@4.7.4: + resolution: {integrity: sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg==} engines: {node: '>=16.9.0'} html-escaper@2.0.2: @@ -7249,9 +7305,9 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@hono/node-server@1.13.3(hono@4.6.17)': + '@hono/node-server@1.13.3(hono@4.7.4)': dependencies: - hono: 4.6.17 + hono: 4.7.4 '@humanfs/core@0.19.1': {} @@ -7573,9 +7629,9 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@ponder/client@0.9.27(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.7.0)(kysely@0.26.3)(pg@8.11.3)(typescript@5.7.3)': + '@ponder/client@0.9.27(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.7.0)(bun-types@1.2.5)(kysely@0.26.3)(pg@8.11.3)(typescript@5.7.3)': dependencies: - drizzle-orm: 0.39.3(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.7.0)(kysely@0.26.3)(pg@8.11.3) + drizzle-orm: 0.39.3(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.7.0)(bun-types@1.2.5)(kysely@0.26.3)(pg@8.11.3) eventsource: 3.0.5 superjson: 2.2.2 optionalDependencies: @@ -7609,9 +7665,9 @@ snapshots: - sql.js - sqlite3 - '@ponder/react@0.9.27(@ponder/client@0.9.27(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.7.0)(kysely@0.26.3)(pg@8.11.3)(typescript@5.7.3))(@tanstack/react-query@5.66.9(react@18.3.1))(react@18.3.1)(typescript@5.7.3)': + '@ponder/react@0.9.27(@ponder/client@0.9.27(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.7.0)(bun-types@1.2.5)(kysely@0.26.3)(pg@8.11.3)(typescript@5.7.3))(@tanstack/react-query@5.66.9(react@18.3.1))(react@18.3.1)(typescript@5.7.3)': dependencies: - '@ponder/client': 0.9.27(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.7.0)(kysely@0.26.3)(pg@8.11.3)(typescript@5.7.3) + '@ponder/client': 0.9.27(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.7.0)(bun-types@1.2.5)(kysely@0.26.3)(pg@8.11.3)(typescript@5.7.3) '@tanstack/react-query': 5.66.9(react@18.3.1) react: 18.3.1 superjson: 2.2.2 @@ -8201,6 +8257,10 @@ snapshots: dependencies: '@babel/types': 7.26.9 + '@types/bun@1.2.5': + dependencies: + bun-types: 1.2.5 + '@types/codemirror@0.0.90': dependencies: '@types/tern': 0.23.9 @@ -8298,6 +8358,10 @@ snapshots: '@types/unist@3.0.3': {} + '@types/ws@8.5.14': + dependencies: + '@types/node': 22.13.5 + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.33': @@ -8845,6 +8909,11 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bun-types@1.2.5: + dependencies: + '@types/node': 22.13.5 + '@types/ws': 8.5.14 + bun@1.2.2: optionalDependencies: '@oven/bun-darwin-aarch64': 1.2.2 @@ -8870,6 +8939,8 @@ snapshots: cac@6.7.14: {} + caip@1.1.1: {} + call-bind-apply-helpers@1.0.1: dependencies: es-errors: 1.3.0 @@ -9203,10 +9274,11 @@ snapshots: dotenv@8.6.0: {} - drizzle-orm@0.39.3(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.7.0)(kysely@0.26.3)(pg@8.11.3): + drizzle-orm@0.39.3(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.7.0)(bun-types@1.2.5)(kysely@0.26.3)(pg@8.11.3): optionalDependencies: '@electric-sql/pglite': 0.2.13 '@opentelemetry/api': 1.7.0 + bun-types: 1.2.5 kysely: 0.26.3 pg: 8.11.3 @@ -9410,7 +9482,7 @@ snapshots: '@typescript-eslint/parser': 8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) eslint: 9.20.1(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@9.20.1(jiti@2.4.2)) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-typescript@3.8.3)(eslint@9.20.1(jiti@2.4.2)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.20.1(jiti@2.4.2)) eslint-plugin-react: 7.37.4(eslint@9.20.1(jiti@2.4.2)) @@ -9430,7 +9502,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)): + eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0)(eslint@9.20.1(jiti@2.4.2)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0 @@ -9445,14 +9517,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.20.1(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) eslint: 9.20.1(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@9.20.1(jiti@2.4.2)) transitivePeerDependencies: - supports-color @@ -9467,7 +9539,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.20.1(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.20.1(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -10212,7 +10284,7 @@ snapshots: hey-listen@1.0.8: {} - hono@4.6.17: {} + hono@4.7.4: {} html-escaper@2.0.2: {} @@ -11565,7 +11637,7 @@ snapshots: dependencies: find-up: 4.1.0 - ponder@0.9.27(@opentelemetry/api@1.7.0)(@types/node@20.17.14)(hono@4.6.17)(lightningcss@1.29.1)(typescript@5.7.3)(viem@2.22.13(typescript@5.7.3)(zod@3.24.2))(zod@3.24.2): + ponder@0.9.27(@opentelemetry/api@1.7.0)(@types/node@20.17.14)(bun-types@1.2.5)(hono@4.7.4)(lightningcss@1.29.1)(typescript@5.7.3)(viem@2.22.13(typescript@5.7.3)(zod@3.24.2))(zod@3.24.2): dependencies: '@babel/code-frame': 7.26.2 '@commander-js/extra-typings': 12.1.0(commander@12.1.0) @@ -11573,7 +11645,7 @@ snapshots: '@escape.tech/graphql-armor-max-aliases': 2.6.0 '@escape.tech/graphql-armor-max-depth': 2.4.0 '@escape.tech/graphql-armor-max-tokens': 2.5.0 - '@hono/node-server': 1.13.3(hono@4.6.17) + '@hono/node-server': 1.13.3(hono@4.7.4) '@ponder/utils': 0.2.3(typescript@5.7.3)(viem@2.22.13(typescript@5.7.3)(zod@3.24.2)) abitype: 0.10.3(typescript@5.7.3)(zod@3.24.2) ansi-escapes: 7.0.0 @@ -11582,11 +11654,11 @@ snapshots: dataloader: 2.2.3 detect-package-manager: 3.0.2 dotenv: 16.4.7 - drizzle-orm: 0.39.3(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.7.0)(kysely@0.26.3)(pg@8.11.3) + drizzle-orm: 0.39.3(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.7.0)(bun-types@1.2.5)(kysely@0.26.3)(pg@8.11.3) glob: 10.4.5 graphql: 16.10.0 graphql-yoga: 5.10.10(graphql@16.10.0) - hono: 4.6.17 + hono: 4.7.4 http-terminator: 3.2.0 kysely: 0.26.3 pg: 8.11.3 @@ -11643,7 +11715,7 @@ snapshots: - terser - zod - ponder@0.9.27(@opentelemetry/api@1.7.0)(@types/node@22.13.5)(hono@4.6.17)(lightningcss@1.29.1)(typescript@5.7.3)(viem@2.22.13(typescript@5.7.3)(zod@3.24.2))(zod@3.24.2): + ponder@0.9.27(@opentelemetry/api@1.7.0)(@types/node@22.13.5)(bun-types@1.2.5)(hono@4.7.4)(lightningcss@1.29.1)(typescript@5.7.3)(viem@2.22.13(typescript@5.7.3)(zod@3.24.2))(zod@3.24.2): dependencies: '@babel/code-frame': 7.26.2 '@commander-js/extra-typings': 12.1.0(commander@12.1.0) @@ -11651,7 +11723,7 @@ snapshots: '@escape.tech/graphql-armor-max-aliases': 2.6.0 '@escape.tech/graphql-armor-max-depth': 2.4.0 '@escape.tech/graphql-armor-max-tokens': 2.5.0 - '@hono/node-server': 1.13.3(hono@4.6.17) + '@hono/node-server': 1.13.3(hono@4.7.4) '@ponder/utils': 0.2.3(typescript@5.7.3)(viem@2.22.13(typescript@5.7.3)(zod@3.24.2)) abitype: 0.10.3(typescript@5.7.3)(zod@3.24.2) ansi-escapes: 7.0.0 @@ -11660,11 +11732,11 @@ snapshots: dataloader: 2.2.3 detect-package-manager: 3.0.2 dotenv: 16.4.7 - drizzle-orm: 0.39.3(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.7.0)(kysely@0.26.3)(pg@8.11.3) + drizzle-orm: 0.39.3(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.7.0)(bun-types@1.2.5)(kysely@0.26.3)(pg@8.11.3) glob: 10.4.5 graphql: 16.10.0 graphql-yoga: 5.10.10(graphql@16.10.0) - hono: 4.6.17 + hono: 4.7.4 http-terminator: 3.2.0 kysely: 0.26.3 pg: 8.11.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9cf91135b..9527ed2ae 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -11,7 +11,7 @@ catalog: "@types/node": ^20.10.0 "@vitest/coverage-v8": ^3.0.5 drizzle-orm: ^0.39.3 - hono: ^4.6.14 + hono: ^4.7.4 typescript: ^5.7.3 viem: ^2.22.13 vitest: ^3.0.5