From 618e8d30d9722c0f7b8938338b42660713a1fa2c Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Sun, 12 Jan 2025 08:44:22 +0100 Subject: [PATCH 1/8] feat(cross-subname-indexing): allow activating multiple plugins together --- package.json | 2 +- pnpm-lock.yaml | 10 ++-- ponder.config.ts | 48 ++++++++++------- src/handlers/Registry.ts | 15 +++--- src/lib/helpers.ts | 52 +++++++++++++++++++ src/lib/plugin-helpers.ts | 30 +++++++++++ src/plugins/base.eth/handlers/Registrar.ts | 24 ++++++++- .../linea.eth/handlers/EthRegistrar.ts | 24 +++++++++ src/plugins/linea.eth/ponder.config.ts | 2 +- 9 files changed, 174 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index b0ddcdf4a7..b6d1167d8e 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "dependencies": { "@ensdomains/ensjs": "^4.0.2", "hono": "^4.6.14", - "ponder": "^0.8.17", + "ponder": "^0.8.24", "viem": "^2.21.57" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d291439ef0..6b370f71b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^4.6.14 version: 4.6.14 ponder: - specifier: ^0.8.17 - version: 0.8.17(@opentelemetry/api@1.9.0)(@types/node@20.17.10)(@types/pg@8.11.10)(hono@4.6.14)(typescript@5.7.2)(viem@2.21.57(typescript@5.7.2)) + specifier: ^0.8.24 + version: 0.8.24(@opentelemetry/api@1.9.0)(@types/node@20.17.10)(@types/pg@8.11.10)(hono@4.6.14)(typescript@5.7.2)(viem@2.21.57(typescript@5.7.2)) viem: specifier: ^2.21.57 version: 2.21.57(typescript@5.7.2) @@ -1452,8 +1452,8 @@ packages: resolution: {integrity: sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==} hasBin: true - ponder@0.8.17: - resolution: {integrity: sha512-p0gvs0CJpdJ6sf5OOQYXaIfmIeUVoTMkCbPVAJ1jK1O2m62ZnTlxpnGrPp5ZAWYxdlCSQQCpZpNhdsYGejGK+g==} + ponder@0.8.24: + resolution: {integrity: sha512-WMj9FmlY+A2Wb07rHbhekai9Z/JsCFz31+7+Zfjg5I933LbV3FeWYy/q277A4h7ai9o/yrVBfkL8kbUmO40Y7g==} engines: {node: '>=18.14'} hasBin: true peerDependencies: @@ -3157,7 +3157,7 @@ snapshots: sonic-boom: 3.8.1 thread-stream: 2.7.0 - ponder@0.8.17(@opentelemetry/api@1.9.0)(@types/node@20.17.10)(@types/pg@8.11.10)(hono@4.6.14)(typescript@5.7.2)(viem@2.21.57(typescript@5.7.2)): + ponder@0.8.24(@opentelemetry/api@1.9.0)(@types/node@20.17.10)(@types/pg@8.11.10)(hono@4.6.14)(typescript@5.7.2)(viem@2.21.57(typescript@5.7.2)): dependencies: '@babel/code-frame': 7.26.2 '@commander-js/extra-typings': 12.1.0(commander@12.1.0) diff --git a/ponder.config.ts b/ponder.config.ts index a0e288061f..efeaebd716 100644 --- a/ponder.config.ts +++ b/ponder.config.ts @@ -1,29 +1,39 @@ -import { ACTIVE_PLUGIN } from "./src/lib/plugin-helpers"; +import { createConfig } from "ponder"; +import { deepMergeRecursive } from "./src/lib/helpers"; +import { type IntersectionOf, getActivePlugins } from "./src/lib/plugin-helpers"; import * as baseEthPlugin from "./src/plugins/base.eth/ponder.config"; import * as ethPlugin from "./src/plugins/eth/ponder.config"; import * as lineaEthPlugin from "./src/plugins/linea.eth/ponder.config"; const plugins = [baseEthPlugin, ethPlugin, lineaEthPlugin] as const; -// here we export only a single 'plugin's config, by type it as every config -// this makes all of the mapping types happy at typecheck-time, but only the -// relevant config is run at runtime -export default ((): AllConfigs => { - const pluginToActivate = plugins.find((p) => p.ownedName === ACTIVE_PLUGIN); +// The type of the exported default is the intersection of all plugin configs to +// each plugin can be correctly typechecked +type AllPluginsConfig = IntersectionOf<(typeof plugins)[number]["config"]>; - if (!pluginToActivate) { - throw new Error(`Unsupported ACTIVE_PLUGIN: ${ACTIVE_PLUGIN}`); - } +const activePlugins = loadActivePlugins(); - pluginToActivate.activate(); +export default (() => + createConfig({ + contracts: { + ...activePlugins.contracts, + }, + networks: { + ...activePlugins.networks, + }, + }) as AllPluginsConfig)(); - return pluginToActivate.config as AllConfigs; -})(); +/** + * Activates the indexing handlers included in selected active plugins and returns their combined config. + */ +function loadActivePlugins() { + const activePlugins = getActivePlugins(plugins); -// Helper type to get the intersection of all config types -type IntersectionOf = (T extends any ? (x: T) => void : never) extends (x: infer R) => void - ? R - : never; -// The type of the exported default is the intersection of all plugin configs to -// each plugin can be correctly typechecked -type AllConfigs = IntersectionOf<(typeof plugins)[number]["config"]>; + activePlugins.forEach((plugin) => plugin.activate()); + + const config = activePlugins + .map((plugin) => plugin.config) + .reduce((acc, val) => deepMergeRecursive(acc, val), {} as AllPluginsConfig); + + return config; +} diff --git a/src/handlers/Registry.ts b/src/handlers/Registry.ts index 6da6530521..980cfffa9d 100644 --- a/src/handlers/Registry.ts +++ b/src/handlers/Registry.ts @@ -15,12 +15,15 @@ export async function setupRootNode({ context }: { context: Context }) { await upsertAccount(context, zeroAddress); // initialize the ENS root to be owned by the zeroAddress and not migrated - await context.db.insert(domains).values({ - id: ROOT_NODE, - ownerId: zeroAddress, - createdAt: 0n, - isMigrated: false, - }); + await context.db + .insert(domains) + .values({ + id: ROOT_NODE, + ownerId: zeroAddress, + createdAt: 0n, + isMigrated: false, + }) + .onConflictDoNothing(); } function isDomainEmpty(domain: typeof domains.$inferSelect) { diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 3212e1068e..0d7914c918 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -16,3 +16,55 @@ export const blockConfig = ( startBlock: Math.min(Math.max(start || 0, startBlock), end || Number.MAX_SAFE_INTEGER), endBlock: end, }); + +type AnyObject = { [key: string]: any }; + +/** + * Deep merge two objects recursively. + * @param target The target object to merge into. + * @param source The source object to merge from. + * @returns The merged object. + * @see https://stackoverflow.com/a/48218209 + * @example + * const obj1 = { a: 1, b: 2, c: { d: 3 } }; + * const obj2 = { a: 4, c: { e: 5 } }; + * const obj3 = deepMergeRecursive(obj1, obj2); + * // { a: 4, b: 2, c: { d: 3, e: 5 } } + */ +export function deepMergeRecursive( + target: T, + source: U, +): T & U { + const output = { ...target } as T & U; + + function isObject(item: any): item is AnyObject { + return item && typeof item === "object" && !Array.isArray(item); + } + + if (isObject(target) && isObject(source)) { + Object.keys(source).forEach((key) => { + if (isObject(source[key])) { + if (!(key in target)) { + Object.assign(output, { [key]: source[key] }); + } else { + (output as AnyObject)[key] = deepMergeRecursive( + (target as AnyObject)[key], + (source as AnyObject)[key], + ); + } + } else if (Array.isArray(source[key])) { + if (!(key in target)) { + Object.assign(output, { [key]: source[key] }); + } else { + (output as AnyObject)[key] = Array.isArray((target as AnyObject)[key]) + ? [...(target as AnyObject)[key], ...source[key]] + : source[key]; + } + } else { + Object.assign(output, { [key]: source[key] }); + } + }); + } + + return output; +} diff --git a/src/lib/plugin-helpers.ts b/src/lib/plugin-helpers.ts index e57005d6f3..707fc376f4 100644 --- a/src/lib/plugin-helpers.ts +++ b/src/lib/plugin-helpers.ts @@ -81,3 +81,33 @@ type PluginNamespacePath = /** @var the requested active plugin name (see `src/plugins` for available plugins) */ export const ACTIVE_PLUGIN = process.env.ACTIVE_PLUGIN; + +/** + * Returns the active plugins list based on the `ACTIVE_PLUGIN` environment variable. + * + * The `ACTIVE_PLUGIN` environment variable is a comma-separated list of plugin + * names. The function returns the plugins that are included in the list. + * + * @param plugins is a list of available plugins + * @returns the active plugins + */ +export function getActivePlugins(plugins: readonly T[]): T[] { + const pluginsToActivateByOwnedName = ACTIVE_PLUGIN + ? ACTIVE_PLUGIN.split(",").map((p) => p.toLowerCase()) + : []; + + if (!pluginsToActivateByOwnedName.length) { + throw new Error("No active plugins found. Please set the ACTIVE_PLUGIN environment variable."); + } + + return plugins.filter((plugin) => + pluginsToActivateByOwnedName.includes(plugin.ownedName.toLowerCase()), + ); +} + +// Helper type to get the intersection of all config types +export type IntersectionOf = (T extends any ? (x: T) => void : never) extends ( + x: infer R, +) => void + ? R + : never; diff --git a/src/plugins/base.eth/handlers/Registrar.ts b/src/plugins/base.eth/handlers/Registrar.ts index 16df285be9..6dc5f9d46a 100644 --- a/src/plugins/base.eth/handlers/Registrar.ts +++ b/src/plugins/base.eth/handlers/Registrar.ts @@ -1,5 +1,6 @@ import { ponder } from "ponder:registry"; import { domains } from "ponder:schema"; +import { zeroAddress } from "viem"; import { makeRegistrarHandlers } from "../../../handlers/Registrar"; import { makeSubnodeNamehash, tokenIdToLabel } from "../../../lib/subname-helpers"; import { upsertAccount } from "../../../lib/upserts"; @@ -46,7 +47,28 @@ export default function () { // Base's BaseRegistrar uses `id` instead of `tokenId` ponder.on(pluginNamespace("BaseRegistrar:Transfer"), async ({ context, event }) => { - return await handleNameTransferred({ + if (event.args.from === zeroAddress) { + /** + * Address the issue where in the same transaction the Transfer event occurs before the NameRegistered event. + * Example: https://basescan.org/tx/0x4d478a75710fb1edcfad5289b9e5ba76c4d1a0e8d897e2e89adf6d8107aadd66#eventlog + * Code: https://github.com/base-org/basenames/blob/1b5c1ad464f061c557c33b60b1821f75dae924cc/src/L2/BaseRegistrar.sol#L272-L273 + */ + + const { id, to: owner } = event.args; + const label = tokenIdToLabel(id); + const node = makeSubnodeNamehash(ownedSubnameNode, label); + + await context.db + .insert(domains) + .values({ + id: node, + ownerId: owner, + createdAt: event.block.timestamp, + }) + .onConflictDoNothing(); + } + + await handleNameTransferred({ context, args: { ...event.args, tokenId: event.args.id }, }); diff --git a/src/plugins/linea.eth/handlers/EthRegistrar.ts b/src/plugins/linea.eth/handlers/EthRegistrar.ts index 33e3219355..7744011807 100644 --- a/src/plugins/linea.eth/handlers/EthRegistrar.ts +++ b/src/plugins/linea.eth/handlers/EthRegistrar.ts @@ -1,5 +1,8 @@ import { ponder } from "ponder:registry"; +import { domains } from "ponder:schema"; +import { zeroAddress } from "viem"; import { makeRegistrarHandlers } from "../../../handlers/Registrar"; +import { makeSubnodeNamehash, tokenIdToLabel } from "../../../lib/subname-helpers"; import { ownedName, pluginNamespace } from "../ponder.config"; const { @@ -8,6 +11,7 @@ const { handleNameRenewedByController, handleNameRenewed, handleNameTransferred, + ownedSubnameNode, } = makeRegistrarHandlers(ownedName); export default function () { @@ -15,6 +19,26 @@ export default function () { ponder.on(pluginNamespace("BaseRegistrar:NameRenewed"), handleNameRenewed); ponder.on(pluginNamespace("BaseRegistrar:Transfer"), async ({ context, event }) => { + if (event.args.from === zeroAddress) { + /** + * Address the issue where in the same transaction the Transfer event occurs before the NameRegistered event. + * Example: https://basescan.org/tx/0x4d478a75710fb1edcfad5289b9e5ba76c4d1a0e8d897e2e89adf6d8107aadd66#eventlog + * Code: https://github.com/base-org/basenames/blob/1b5c1ad464f061c557c33b60b1821f75dae924cc/src/L2/BaseRegistrar.sol#L272-L273 + */ + + const { tokenId: id, to: owner } = event.args; + const label = tokenIdToLabel(id); + const node = makeSubnodeNamehash(ownedSubnameNode, label); + + await context.db + .insert(domains) + .values({ + id: node, + ownerId: owner, + createdAt: event.block.timestamp, + }) + .onConflictDoNothing(); + } return await handleNameTransferred({ context, args: event.args }); }); diff --git a/src/plugins/linea.eth/ponder.config.ts b/src/plugins/linea.eth/ponder.config.ts index 0dcaa8ad79..535376d446 100644 --- a/src/plugins/linea.eth/ponder.config.ts +++ b/src/plugins/linea.eth/ponder.config.ts @@ -17,7 +17,7 @@ export const pluginNamespace = createPluginNamespace(ownedName); // constrain the ponder indexing between the following start/end blocks // https://ponder.sh/0_6/docs/contracts-and-networks#block-range const START_BLOCK: ContractConfig["startBlock"] = undefined; -const END_BLOCK: ContractConfig["endBlock"] = undefined; +const END_BLOCK: ContractConfig["endBlock"] = 8398631; export const config = createConfig({ networks: { From 7d8f9787dcaa0f3d924cc3f9e8a66708cc7f86c6 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Sun, 12 Jan 2025 12:39:35 +0100 Subject: [PATCH 2/8] feat(cross-subname-indexing): demonstrate retrieving cross-subname index --- src/handlers/Registrar.ts | 2 +- src/plugins/base.eth/ponder.config.ts | 18 ++++++++++++------ src/plugins/linea.eth/handlers/EthRegistrar.ts | 4 ++-- src/plugins/linea.eth/ponder.config.ts | 4 ++-- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/handlers/Registrar.ts b/src/handlers/Registrar.ts index 0abac197ad..911beba6d3 100644 --- a/src/handlers/Registrar.ts +++ b/src/handlers/Registrar.ts @@ -23,7 +23,7 @@ export const makeRegistrarHandlers = (ownedName: `${string}eth`) => { if (domain.labelName !== name) { await context.db .update(domains, { id: node }) - .set({ labelName: name, name: `${name}${ownedName}` }); + .set({ labelName: name, name: `${name}.${ownedName}` }); } await context.db.update(registrations, { id: label }).set({ labelName: name, cost }); diff --git a/src/plugins/base.eth/ponder.config.ts b/src/plugins/base.eth/ponder.config.ts index 6340a2c844..e4ed62eafa 100644 --- a/src/plugins/base.eth/ponder.config.ts +++ b/src/plugins/base.eth/ponder.config.ts @@ -1,7 +1,8 @@ -import { createConfig, factory } from "ponder"; +import { type ContractConfig, createConfig, factory } from "ponder"; import { http, getAbiItem } from "viem"; import { base } from "viem/chains"; +import { blockConfig } from "../../lib/helpers"; import { createPluginNamespace } from "../../lib/plugin-helpers"; import { BaseRegistrar } from "./abis/BaseRegistrar"; import { EarlyAccessRegistrarController } from "./abis/EARegistrarController"; @@ -13,6 +14,11 @@ export const ownedName = "base.eth" as const; export const pluginNamespace = createPluginNamespace(ownedName); +// constrain the ponder indexing between the following start/end blocks +// https://ponder.sh/0_6/docs/contracts-and-networks#block-range +const START_BLOCK: ContractConfig["startBlock"] = 24944146; +const END_BLOCK: ContractConfig["endBlock"] = undefined; + export const config = createConfig({ networks: { base: { @@ -25,7 +31,7 @@ export const config = createConfig({ network: "base", abi: Registry, address: "0xb94704422c2a1e396835a571837aa5ae53285a95", - startBlock: 17571480, + ...blockConfig(START_BLOCK, 17571480, END_BLOCK), }, [pluginNamespace("Resolver")]: { network: "base", @@ -35,25 +41,25 @@ export const config = createConfig({ event: getAbiItem({ abi: Registry, name: "NewResolver" }), parameter: "resolver", }), - startBlock: 17575714, + ...blockConfig(START_BLOCK, 17575714, END_BLOCK), }, [pluginNamespace("BaseRegistrar")]: { network: "base", abi: BaseRegistrar, address: "0x03c4738Ee98aE44591e1A4A4F3CaB6641d95DD9a", - startBlock: 17571486, + ...blockConfig(START_BLOCK, 17571486, END_BLOCK), }, [pluginNamespace("EARegistrarController")]: { network: "base", abi: EarlyAccessRegistrarController, address: "0xd3e6775ed9b7dc12b205c8e608dc3767b9e5efda", - startBlock: 17575699, + ...blockConfig(START_BLOCK, 17575699, END_BLOCK), }, [pluginNamespace("RegistrarController")]: { network: "base", abi: RegistrarController, address: "0x4cCb0BB02FCABA27e82a56646E81d8c5bC4119a5", - startBlock: 18619035, + ...blockConfig(START_BLOCK, 18619035, END_BLOCK), }, }, }); diff --git a/src/plugins/linea.eth/handlers/EthRegistrar.ts b/src/plugins/linea.eth/handlers/EthRegistrar.ts index 7744011807..3d70395b28 100644 --- a/src/plugins/linea.eth/handlers/EthRegistrar.ts +++ b/src/plugins/linea.eth/handlers/EthRegistrar.ts @@ -22,8 +22,8 @@ export default function () { if (event.args.from === zeroAddress) { /** * Address the issue where in the same transaction the Transfer event occurs before the NameRegistered event. - * Example: https://basescan.org/tx/0x4d478a75710fb1edcfad5289b9e5ba76c4d1a0e8d897e2e89adf6d8107aadd66#eventlog - * Code: https://github.com/base-org/basenames/blob/1b5c1ad464f061c557c33b60b1821f75dae924cc/src/L2/BaseRegistrar.sol#L272-L273 + * Example: https://lineascan.build/tx/0x2211c5d857d16b7ac111088c57fb346ab94049cb297f02b0dda7aaf4c14d305b#eventlog + * Code: hhttps://github.com/Consensys/linea-ens/blob/main/packages/linea-ens-contracts/contracts/ethregistrar/BaseRegistrarImplementation.sol#L155-L160 */ const { tokenId: id, to: owner } = event.args; diff --git a/src/plugins/linea.eth/ponder.config.ts b/src/plugins/linea.eth/ponder.config.ts index 535376d446..1268343da2 100644 --- a/src/plugins/linea.eth/ponder.config.ts +++ b/src/plugins/linea.eth/ponder.config.ts @@ -16,8 +16,8 @@ export const pluginNamespace = createPluginNamespace(ownedName); // constrain the ponder indexing between the following start/end blocks // https://ponder.sh/0_6/docs/contracts-and-networks#block-range -const START_BLOCK: ContractConfig["startBlock"] = undefined; -const END_BLOCK: ContractConfig["endBlock"] = 8398631; +const START_BLOCK: ContractConfig["startBlock"] = 14490289; +const END_BLOCK: ContractConfig["endBlock"] = undefined; export const config = createConfig({ networks: { From 76754e40cef75861dc6fff52fb3cb12cd5313ec1 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Sun, 12 Jan 2025 20:20:06 +0100 Subject: [PATCH 3/8] refactor(handlers): stremline logic --- src/handlers/Registry.ts | 42 ++++++++++++++---------- src/plugins/eth/handlers/EthRegistrar.ts | 10 +++--- src/plugins/eth/ponder.config.ts | 2 +- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/handlers/Registry.ts b/src/handlers/Registry.ts index 980cfffa9d..eaec7a2e77 100644 --- a/src/handlers/Registry.ts +++ b/src/handlers/Registry.ts @@ -92,36 +92,38 @@ export const handleNewOwner = }) => { const { label, node, owner } = event.args; + const parent = await context.db.find(domains, { id: node }); const subnode = makeSubnodeNamehash(node, label); + const incrementSubdomainCountOnParent = async () => + // only increment subdomainCount + Boolean( + // if the parent domain exists and + parent !== null && + // the subdomain doesn't exist yet + (await context.db.find(domains, { id: subnode })), + ); + // ensure owner await upsertAccount(context, owner); - // note that we set isMigrated so that if this domain is being interacted with on the new registry, its migration status is set here - let domain = await context.db.find(domains, { id: subnode }); - if (domain) { - // if the domain already exists, this is just an update of the owner record (& isMigrated) - await context.db.update(domains, { id: domain.id }).set({ ownerId: owner, isMigrated }); - } else { - // otherwise create the domain - domain = await context.db.insert(domains).values({ + const domain = await context.db + .insert(domains) + .values({ id: subnode, ownerId: owner, parentId: node, createdAt: event.block.timestamp, isMigrated, - }); - - // and increment parent subdomainCount - await context.db - .update(domains, { id: node }) - .set((row) => ({ subdomainCount: row.subdomainCount + 1 })); - } + }) + .onConflictDoUpdate(() => ({ + // if the domain already exists, this is just an update of the owner record (& isMigrated) + ownerId: owner, + isMigrated, + })); // if the domain doesn't yet have a name, construct it here if (!domain.name) { - const parent = await context.db.find(domains, { id: node }); - // TODO: implement sync rainbow table lookups // https://github.com/ensdomains/ens-subgraph/blob/master/src/ensRegistry.ts#L111 const labelName = encodeLabelhash(label); @@ -130,6 +132,12 @@ export const handleNewOwner = await context.db.update(domains, { id: domain.id }).set({ name, labelName }); } + if (await incrementSubdomainCountOnParent()) { + await context.db + .update(domains, { id: parent!.id }) + .set((row) => ({ subdomainCount: row.subdomainCount + 1 })); + } + // garbage collect newly 'empty' domain iff necessary if (owner === zeroAddress) { await recursivelyRemoveEmptyDomainFromParentSubdomainCount(context, domain.id); diff --git a/src/plugins/eth/handlers/EthRegistrar.ts b/src/plugins/eth/handlers/EthRegistrar.ts index e864be6f73..58177f9d60 100644 --- a/src/plugins/eth/handlers/EthRegistrar.ts +++ b/src/plugins/eth/handlers/EthRegistrar.ts @@ -15,20 +15,20 @@ export default function () { ponder.on(pluginNamespace("BaseRegistrar:NameRenewed"), handleNameRenewed); ponder.on(pluginNamespace("BaseRegistrar:Transfer"), async ({ context, event }) => { - return await handleNameTransferred({ context, args: event.args }); + await handleNameTransferred({ context, args: event.args }); }); ponder.on( pluginNamespace("EthRegistrarControllerOld:NameRegistered"), async ({ context, event }) => { // the old registrar controller just had `cost` param - return await handleNameRegisteredByController({ context, args: event.args }); + await handleNameRegisteredByController({ context, args: event.args }); }, ); ponder.on( pluginNamespace("EthRegistrarControllerOld:NameRenewed"), async ({ context, event }) => { - return await handleNameRenewedByController({ context, args: event.args }); + await handleNameRenewedByController({ context, args: event.args }); }, ); @@ -36,7 +36,7 @@ export default function () { pluginNamespace("EthRegistrarController:NameRegistered"), async ({ context, event }) => { // the new registrar controller uses baseCost + premium to compute cost - return await handleNameRegisteredByController({ + await handleNameRegisteredByController({ context, args: { ...event.args, @@ -46,6 +46,6 @@ export default function () { }, ); ponder.on(pluginNamespace("EthRegistrarController:NameRenewed"), async ({ context, event }) => { - return await handleNameRenewedByController({ context, args: event.args }); + await handleNameRenewedByController({ context, args: event.args }); }); } diff --git a/src/plugins/eth/ponder.config.ts b/src/plugins/eth/ponder.config.ts index 0c0c490f57..f609667386 100644 --- a/src/plugins/eth/ponder.config.ts +++ b/src/plugins/eth/ponder.config.ts @@ -20,7 +20,7 @@ export const pluginNamespace = createPluginNamespace(ownedName); // constrain the ponder indexing between the following start/end blocks // https://ponder.sh/0_6/docs/contracts-and-networks#block-range -const START_BLOCK: ContractConfig["startBlock"] = undefined; +const START_BLOCK: ContractConfig["startBlock"] = 21_610_000; const END_BLOCK: ContractConfig["endBlock"] = undefined; export const config = createConfig({ From 0ffa78c818a5aee5f088ea649018d9db2a8bfa16 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Sun, 12 Jan 2025 20:20:27 +0100 Subject: [PATCH 4/8] refactor(ponder.config): simplify structure --- ponder.config.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/ponder.config.ts b/ponder.config.ts index efeaebd716..63ab9b08e2 100644 --- a/ponder.config.ts +++ b/ponder.config.ts @@ -1,4 +1,3 @@ -import { createConfig } from "ponder"; import { deepMergeRecursive } from "./src/lib/helpers"; import { type IntersectionOf, getActivePlugins } from "./src/lib/plugin-helpers"; import * as baseEthPlugin from "./src/plugins/base.eth/ponder.config"; @@ -11,22 +10,12 @@ const plugins = [baseEthPlugin, ethPlugin, lineaEthPlugin] as const; // each plugin can be correctly typechecked type AllPluginsConfig = IntersectionOf<(typeof plugins)[number]["config"]>; -const activePlugins = loadActivePlugins(); - -export default (() => - createConfig({ - contracts: { - ...activePlugins.contracts, - }, - networks: { - ...activePlugins.networks, - }, - }) as AllPluginsConfig)(); +export default loadActivePlugins(); /** * Activates the indexing handlers included in selected active plugins and returns their combined config. */ -function loadActivePlugins() { +function loadActivePlugins(): AllPluginsConfig { const activePlugins = getActivePlugins(plugins); activePlugins.forEach((plugin) => plugin.activate()); @@ -35,5 +24,5 @@ function loadActivePlugins() { .map((plugin) => plugin.config) .reduce((acc, val) => deepMergeRecursive(acc, val), {} as AllPluginsConfig); - return config; + return config as AllPluginsConfig; } From b118dfb87f80e9efa29be8e23e7735bc792dfe0a Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Mon, 13 Jan 2025 17:51:22 +0100 Subject: [PATCH 5/8] fix: apply pr feedback --- .env.local.example | 2 +- ponder.config.ts | 9 +++++---- src/lib/plugin-helpers.ts | 18 +++++++++--------- src/plugins/README.md | 4 ++-- src/plugins/base.eth/ponder.config.ts | 3 ++- src/plugins/eth/ponder.config.ts | 3 ++- src/plugins/linea.eth/ponder.config.ts | 3 ++- 7 files changed, 23 insertions(+), 19 deletions(-) diff --git a/.env.local.example b/.env.local.example index 6252eb771b..64a5f993b5 100644 --- a/.env.local.example +++ b/.env.local.example @@ -7,7 +7,7 @@ RPC_URL_59144=https://linea-rpc.publicnode.com # Identify which indexer plugin to activate (see `src/plugins` for available plugins) -ACTIVE_PLUGIN=base.eth +ACTIVE_PLUGINS=base.eth # Database configuration diff --git a/ponder.config.ts b/ponder.config.ts index 63ab9b08e2..744a0bc0dc 100644 --- a/ponder.config.ts +++ b/ponder.config.ts @@ -4,14 +4,11 @@ import * as baseEthPlugin from "./src/plugins/base.eth/ponder.config"; import * as ethPlugin from "./src/plugins/eth/ponder.config"; import * as lineaEthPlugin from "./src/plugins/linea.eth/ponder.config"; +/** list of all available plugins — any of them can be activated in the runtime */ const plugins = [baseEthPlugin, ethPlugin, lineaEthPlugin] as const; -// The type of the exported default is the intersection of all plugin configs to -// each plugin can be correctly typechecked type AllPluginsConfig = IntersectionOf<(typeof plugins)[number]["config"]>; -export default loadActivePlugins(); - /** * Activates the indexing handlers included in selected active plugins and returns their combined config. */ @@ -26,3 +23,7 @@ function loadActivePlugins(): AllPluginsConfig { return config as AllPluginsConfig; } + +// The type of the defuexporte is the intersection of all plugin configs to +// each plugin can be correctly typechecked +export default loadActivePlugins(); diff --git a/src/lib/plugin-helpers.ts b/src/lib/plugin-helpers.ts index 707fc376f4..7cafdc489f 100644 --- a/src/lib/plugin-helpers.ts +++ b/src/lib/plugin-helpers.ts @@ -80,29 +80,29 @@ type PluginNamespacePath = | `/${string}${T}`; /** @var the requested active plugin name (see `src/plugins` for available plugins) */ -export const ACTIVE_PLUGIN = process.env.ACTIVE_PLUGIN; +export const ACTIVE_PLUGINS = process.env.ACTIVE_PLUGINS; /** - * Returns the active plugins list based on the `ACTIVE_PLUGIN` environment variable. + * Returns the active plugins list based on the `ACTIVE_PLUGINS` environment variable. * - * The `ACTIVE_PLUGIN` environment variable is a comma-separated list of plugin + * The `ACTIVE_PLUGINS` environment variable is a comma-separated list of plugin * names. The function returns the plugins that are included in the list. * * @param plugins is a list of available plugins * @returns the active plugins */ export function getActivePlugins(plugins: readonly T[]): T[] { - const pluginsToActivateByOwnedName = ACTIVE_PLUGIN - ? ACTIVE_PLUGIN.split(",").map((p) => p.toLowerCase()) + const pluginsToActivateByOwnedName = ACTIVE_PLUGINS + ? ACTIVE_PLUGINS.split(",").map((p) => p.toLowerCase()) : []; if (!pluginsToActivateByOwnedName.length) { - throw new Error("No active plugins found. Please set the ACTIVE_PLUGIN environment variable."); + throw new Error("No active plugins found. Please set the ACTIVE_PLUGINS environment variable."); } - return plugins.filter((plugin) => - pluginsToActivateByOwnedName.includes(plugin.ownedName.toLowerCase()), - ); + // TODO: drop an error if the plugin is not found + + return plugins.filter((plugin) => pluginsToActivateByOwnedName.includes(plugin.ownedName)); } // Helper type to get the intersection of all config types diff --git a/src/plugins/README.md b/src/plugins/README.md index 991fc8fca1..38ac070495 100644 --- a/src/plugins/README.md +++ b/src/plugins/README.md @@ -1,8 +1,8 @@ # Indexer plugins This directory contains plugins which allow defining subname-specific processing of blockchain events. -Only one plugin can be active at a time. Use the `ACTIVE_PLUGIN` env variable to select the active plugin, for example: +Only one plugin can be active at a time. Use the `ACTIVE_PLUGINS` env variable to select the active plugin, for example: ``` -ACTIVE_PLUGIN=base.eth +ACTIVE_PLUGINS=base.eth ``` \ No newline at end of file diff --git a/src/plugins/base.eth/ponder.config.ts b/src/plugins/base.eth/ponder.config.ts index e4ed62eafa..b4394ceda8 100644 --- a/src/plugins/base.eth/ponder.config.ts +++ b/src/plugins/base.eth/ponder.config.ts @@ -16,7 +16,7 @@ export const pluginNamespace = createPluginNamespace(ownedName); // constrain the ponder indexing between the following start/end blocks // https://ponder.sh/0_6/docs/contracts-and-networks#block-range -const START_BLOCK: ContractConfig["startBlock"] = 24944146; +const START_BLOCK: ContractConfig["startBlock"] = undefined; const END_BLOCK: ContractConfig["endBlock"] = undefined; export const config = createConfig({ @@ -24,6 +24,7 @@ export const config = createConfig({ base: { chainId: base.id, transport: http(process.env[`RPC_URL_${base.id}`]), + maxRequestsPerSecond: 250, }, }, contracts: { diff --git a/src/plugins/eth/ponder.config.ts b/src/plugins/eth/ponder.config.ts index f609667386..58de1a5a49 100644 --- a/src/plugins/eth/ponder.config.ts +++ b/src/plugins/eth/ponder.config.ts @@ -20,7 +20,7 @@ export const pluginNamespace = createPluginNamespace(ownedName); // constrain the ponder indexing between the following start/end blocks // https://ponder.sh/0_6/docs/contracts-and-networks#block-range -const START_BLOCK: ContractConfig["startBlock"] = 21_610_000; +const START_BLOCK: ContractConfig["startBlock"] = undefined; const END_BLOCK: ContractConfig["endBlock"] = undefined; export const config = createConfig({ @@ -28,6 +28,7 @@ export const config = createConfig({ mainnet: { chainId: mainnet.id, transport: http(process.env[`RPC_URL_${mainnet.id}`]), + maxRequestsPerSecond: 250, }, }, contracts: { diff --git a/src/plugins/linea.eth/ponder.config.ts b/src/plugins/linea.eth/ponder.config.ts index 1268343da2..0ee670ee46 100644 --- a/src/plugins/linea.eth/ponder.config.ts +++ b/src/plugins/linea.eth/ponder.config.ts @@ -16,7 +16,7 @@ export const pluginNamespace = createPluginNamespace(ownedName); // constrain the ponder indexing between the following start/end blocks // https://ponder.sh/0_6/docs/contracts-and-networks#block-range -const START_BLOCK: ContractConfig["startBlock"] = 14490289; +const START_BLOCK: ContractConfig["startBlock"] = undefined; const END_BLOCK: ContractConfig["endBlock"] = undefined; export const config = createConfig({ @@ -24,6 +24,7 @@ export const config = createConfig({ linea: { chainId: linea.id, transport: http(process.env[`RPC_URL_${linea.id}`]), + maxRequestsPerSecond: 250, }, }, contracts: { From c71479fd35a1011e30d7750cf2ec92ff5a2f7d43 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Mon, 13 Jan 2025 17:55:36 +0100 Subject: [PATCH 6/8] fix: typos --- src/plugins/base.eth/handlers/Registrar.ts | 2 +- src/plugins/linea.eth/handlers/EthRegistrar.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/base.eth/handlers/Registrar.ts b/src/plugins/base.eth/handlers/Registrar.ts index 6dc5f9d46a..6d0e1c50c4 100644 --- a/src/plugins/base.eth/handlers/Registrar.ts +++ b/src/plugins/base.eth/handlers/Registrar.ts @@ -45,7 +45,7 @@ export default function () { }); ponder.on(pluginNamespace("BaseRegistrar:NameRenewed"), handleNameRenewed); - // Base's BaseRegistrar uses `id` instead of `tokenId` + // base.eth's BaseRegistrar uses `id` instead of `tokenId` ponder.on(pluginNamespace("BaseRegistrar:Transfer"), async ({ context, event }) => { if (event.args.from === zeroAddress) { /** diff --git a/src/plugins/linea.eth/handlers/EthRegistrar.ts b/src/plugins/linea.eth/handlers/EthRegistrar.ts index 3d70395b28..6a9164ab78 100644 --- a/src/plugins/linea.eth/handlers/EthRegistrar.ts +++ b/src/plugins/linea.eth/handlers/EthRegistrar.ts @@ -23,7 +23,7 @@ export default function () { /** * Address the issue where in the same transaction the Transfer event occurs before the NameRegistered event. * Example: https://lineascan.build/tx/0x2211c5d857d16b7ac111088c57fb346ab94049cb297f02b0dda7aaf4c14d305b#eventlog - * Code: hhttps://github.com/Consensys/linea-ens/blob/main/packages/linea-ens-contracts/contracts/ethregistrar/BaseRegistrarImplementation.sol#L155-L160 + * Code: https://github.com/Consensys/linea-ens/blob/main/packages/linea-ens-contracts/contracts/ethregistrar/BaseRegistrarImplementation.sol#L155-L160 */ const { tokenId: id, to: owner } = event.args; From ceb03cccc831c88f467450f97f9e12a03f5b0a99 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Mon, 13 Jan 2025 20:48:36 +0100 Subject: [PATCH 7/8] fix: apply pr feedback --- .env.local.example | 24 +++-- ponder.config.ts | 19 ++-- src/handlers/Registrar.ts | 6 +- src/lib/upserts.ts | 6 +- src/plugins/base.eth/handlers/Registrar.ts | 88 +++++++++++-------- .../linea.eth/handlers/EthRegistrar.ts | 78 ++++++++++------ 6 files changed, 139 insertions(+), 82 deletions(-) diff --git a/.env.local.example b/.env.local.example index 64a5f993b5..543c312af4 100644 --- a/.env.local.example +++ b/.env.local.example @@ -7,13 +7,25 @@ RPC_URL_59144=https://linea-rpc.publicnode.com # Identify which indexer plugin to activate (see `src/plugins` for available plugins) -ACTIVE_PLUGINS=base.eth +# ACTIVE_PLUGINS=eth,base.eth,linea.eth +# ACTIVE_PLUGINS=base.eth,linea.eth +ACTIVE_PLUGINS=eth # Database configuration -# This is where the indexer will create the tables defined in ponder.schema.ts -# No two indexer instances can use the same database schema at the same time. This prevents data corruption. -# @link https://ponder.sh/docs/api-reference/database#database-schema-rules -DATABASE_SCHEMA=subname_index_base.eth -# The indexer will use Postgres with that as the connection string. If not defined, the indexer will use PSlite. +# This is a namespace for the tables that the indexer will create to store indexed data. +# It should be a string that is unique to the running indexer instance. +# +# Keeping the database schema unique to the indexer instance is important to +# 1) speed up indexing after a restart +# 2) prevent data corruption +# +# No two indexer instances can use the same database schema at the same time. +# +# Read more about database schema rules here: +# https://ponder.sh/docs/api-reference/database#database-schema-rules +DATABASE_SCHEMA=ens + +# This is the connection string for the database that the indexer will use to store data. +# It should be in the format of `postgresql://:@:/` DATABASE_URL=postgresql://dbuser:abcd1234@localhost:5432/my_database diff --git a/ponder.config.ts b/ponder.config.ts index 744a0bc0dc..07dab93b18 100644 --- a/ponder.config.ts +++ b/ponder.config.ts @@ -4,17 +4,20 @@ import * as baseEthPlugin from "./src/plugins/base.eth/ponder.config"; import * as ethPlugin from "./src/plugins/eth/ponder.config"; import * as lineaEthPlugin from "./src/plugins/linea.eth/ponder.config"; -/** list of all available plugins — any of them can be activated in the runtime */ +// list of all available plugins +// any of them can be activated in the runtime const plugins = [baseEthPlugin, ethPlugin, lineaEthPlugin] as const; +// intersection of all available plugin configs to support correct typechecking +// of the indexing handlers type AllPluginsConfig = IntersectionOf<(typeof plugins)[number]["config"]>; -/** - * Activates the indexing handlers included in selected active plugins and returns their combined config. - */ -function loadActivePlugins(): AllPluginsConfig { +// Activates the indexing handlers included in selected active plugins and +// returns and intersection of their combined config. +function getActivePluginsConfig(): AllPluginsConfig { const activePlugins = getActivePlugins(plugins); + // load indexing handlers from the active plugins into the runtime activePlugins.forEach((plugin) => plugin.activate()); const config = activePlugins @@ -24,6 +27,6 @@ function loadActivePlugins(): AllPluginsConfig { return config as AllPluginsConfig; } -// The type of the defuexporte is the intersection of all plugin configs to -// each plugin can be correctly typechecked -export default loadActivePlugins(); +// The type of the default export is the intersection of all available plugin +// configs to each plugin can be correctly typechecked +export default getActivePluginsConfig(); diff --git a/src/handlers/Registrar.ts b/src/handlers/Registrar.ts index 911beba6d3..63911616e3 100644 --- a/src/handlers/Registrar.ts +++ b/src/handlers/Registrar.ts @@ -79,7 +79,7 @@ export const makeRegistrarHandlers = (ownedName: `${string}eth`) => { context: Context; args: { name: string; label: Hex; cost: bigint }; }) { - return await setNamePreimage(context, name, label, cost); + await setNamePreimage(context, name, label, cost); }, async handleNameRenewedByController({ @@ -89,7 +89,7 @@ export const makeRegistrarHandlers = (ownedName: `${string}eth`) => { context: Context; args: { name: string; label: Hex; cost: bigint }; }) { - return await setNamePreimage(context, name, label, cost); + await setNamePreimage(context, name, label, cost); }, async handleNameRenewed({ @@ -117,7 +117,7 @@ export const makeRegistrarHandlers = (ownedName: `${string}eth`) => { async handleNameTransferred({ context, - args: { tokenId, from, to }, + args: { tokenId, to }, }: { context: Context; args: { diff --git a/src/lib/upserts.ts b/src/lib/upserts.ts index b2837a1837..528979c726 100644 --- a/src/lib/upserts.ts +++ b/src/lib/upserts.ts @@ -3,16 +3,16 @@ import { accounts, registrations, resolvers } from "ponder:schema"; import type { Address } from "viem"; export async function upsertAccount(context: Context, address: Address) { - return await context.db.insert(accounts).values({ id: address }).onConflictDoNothing(); + return context.db.insert(accounts).values({ id: address }).onConflictDoNothing(); } export async function upsertResolver(context: Context, values: typeof resolvers.$inferInsert) { - return await context.db.insert(resolvers).values(values).onConflictDoUpdate(values); + return context.db.insert(resolvers).values(values).onConflictDoUpdate(values); } export async function upsertRegistration( context: Context, values: typeof registrations.$inferInsert, ) { - return await context.db.insert(registrations).values(values).onConflictDoUpdate(values); + return context.db.insert(registrations).values(values).onConflictDoUpdate(values); } diff --git a/src/plugins/base.eth/handlers/Registrar.ts b/src/plugins/base.eth/handlers/Registrar.ts index 6d0e1c50c4..e1942cd263 100644 --- a/src/plugins/base.eth/handlers/Registrar.ts +++ b/src/plugins/base.eth/handlers/Registrar.ts @@ -1,6 +1,7 @@ -import { ponder } from "ponder:registry"; +import { Context, ponder } from "ponder:registry"; import { domains } from "ponder:schema"; -import { zeroAddress } from "viem"; +import { Block } from "ponder"; +import { Hex, zeroAddress } from "viem"; import { makeRegistrarHandlers } from "../../../handlers/Registrar"; import { makeSubnodeNamehash, tokenIdToLabel } from "../../../lib/subname-helpers"; import { upsertAccount } from "../../../lib/upserts"; @@ -15,6 +16,33 @@ const { ownedSubnameNode, } = makeRegistrarHandlers(ownedName); +/** + * Idempotent handler to insert a domain record when a new domain is + * initialized. For example, right after an NFT token for the domain + * is minted. + * + * @returns a newly created domain record + */ +function handleDomainNameInitialized({ + context, + event, +}: { + context: Context; + event: { args: { id: bigint; owner: Hex }; block: Block }; +}) { + const { id, owner } = event.args; + const label = tokenIdToLabel(id); + const node = makeSubnodeNamehash(ownedSubnameNode, label); + return context.db + .insert(domains) + .values({ + id: node, + ownerId: owner, + createdAt: event.block.timestamp, + }) + .onConflictDoNothing(); +} + export default function () { // support NameRegisteredWithRecord for BaseRegistrar as it used by Base's RegistrarControllers ponder.on(pluginNamespace("BaseRegistrar:NameRegisteredWithRecord"), async ({ context, event }) => @@ -22,62 +50,48 @@ export default function () { ); ponder.on(pluginNamespace("BaseRegistrar:NameRegistered"), async ({ context, event }) => { + await upsertAccount(context, event.args.owner); // base has 'preminted' names via Registrar#registerOnly, which explicitly does not update Registry. // this breaks a subgraph assumption, as it expects a domain to exist (via Registry:NewOwner) before // any Registrar:NameRegistered events. in the future we will likely happily upsert domains, but // in order to avoid prematurely drifting from subgraph equivalancy, we upsert the domain here, // allowing the base indexer to progress. - const { id, owner } = event.args; - const label = tokenIdToLabel(id); - const node = makeSubnodeNamehash(ownedSubnameNode, label); - await upsertAccount(context, owner); - await context.db - .insert(domains) - .values({ - id: node, - ownerId: owner, - createdAt: event.block.timestamp, - }) - .onConflictDoNothing(); + await handleDomainNameInitialized({ context, event }); // after ensuring the domain exists, continue with the standard handler return handleNameRegistered({ context, event }); }); ponder.on(pluginNamespace("BaseRegistrar:NameRenewed"), handleNameRenewed); - // base.eth's BaseRegistrar uses `id` instead of `tokenId` ponder.on(pluginNamespace("BaseRegistrar:Transfer"), async ({ context, event }) => { - if (event.args.from === zeroAddress) { - /** - * Address the issue where in the same transaction the Transfer event occurs before the NameRegistered event. - * Example: https://basescan.org/tx/0x4d478a75710fb1edcfad5289b9e5ba76c4d1a0e8d897e2e89adf6d8107aadd66#eventlog - * Code: https://github.com/base-org/basenames/blob/1b5c1ad464f061c557c33b60b1821f75dae924cc/src/L2/BaseRegistrar.sol#L272-L273 - */ + const { id, from, to } = event.args; - const { id, to: owner } = event.args; - const label = tokenIdToLabel(id); - const node = makeSubnodeNamehash(ownedSubnameNode, label); - - await context.db - .insert(domains) - .values({ - id: node, - ownerId: owner, - createdAt: event.block.timestamp, - }) - .onConflictDoNothing(); + if (event.args.from === zeroAddress) { + // The ens-subgraph `handleNameTransferred` handler implementation + // assumes the domain record exists. However, when an NFT token is + // minted, there's no domain record yet created. The very first transfer + // event has to initialize the domain record. This is a workaround to + // meet the subgraph implementation expectations. + await handleDomainNameInitialized({ + context, + event: { + ...event, + args: { id, owner: to }, + }, + }); } + // base.eth's BaseRegistrar uses `id` instead of `tokenId` await handleNameTransferred({ context, - args: { ...event.args, tokenId: event.args.id }, + args: { from, to, tokenId: id }, }); }); ponder.on(pluginNamespace("EARegistrarController:NameRegistered"), async ({ context, event }) => { // TODO: registration expected here - return handleNameRegisteredByController({ + await handleNameRegisteredByController({ context, args: { ...event.args, cost: 0n }, }); @@ -86,14 +100,14 @@ export default function () { ponder.on(pluginNamespace("RegistrarController:NameRegistered"), async ({ context, event }) => { // TODO: registration expected here - return handleNameRegisteredByController({ + await handleNameRegisteredByController({ context, args: { ...event.args, cost: 0n }, }); }); ponder.on(pluginNamespace("RegistrarController:NameRenewed"), async ({ context, event }) => { - return handleNameRenewedByController({ + await handleNameRenewedByController({ context, args: { ...event.args, cost: 0n }, }); diff --git a/src/plugins/linea.eth/handlers/EthRegistrar.ts b/src/plugins/linea.eth/handlers/EthRegistrar.ts index 6a9164ab78..248a29d164 100644 --- a/src/plugins/linea.eth/handlers/EthRegistrar.ts +++ b/src/plugins/linea.eth/handlers/EthRegistrar.ts @@ -1,6 +1,7 @@ -import { ponder } from "ponder:registry"; +import { type Context, ponder } from "ponder:registry"; import { domains } from "ponder:schema"; -import { zeroAddress } from "viem"; +import { type Block } from "ponder"; +import { Hex, zeroAddress } from "viem"; import { makeRegistrarHandlers } from "../../../handlers/Registrar"; import { makeSubnodeNamehash, tokenIdToLabel } from "../../../lib/subname-helpers"; import { ownedName, pluginNamespace } from "../ponder.config"; @@ -14,39 +15,66 @@ const { ownedSubnameNode, } = makeRegistrarHandlers(ownedName); +/** + * Idempotent handler to insert a domain record when a new domain is + * initialized. For example, right after an NFT token for the domain + * is minted. + * + * @returns a newly created domain record + */ +function handleDomainNameInitialized({ + context, + event, +}: { + context: Context; + event: { args: { id: bigint; owner: Hex }; block: Block }; +}) { + const { id, owner } = event.args; + const label = tokenIdToLabel(id); + const node = makeSubnodeNamehash(ownedSubnameNode, label); + return context.db + .insert(domains) + .values({ + id: node, + ownerId: owner, + createdAt: event.block.timestamp, + }) + .onConflictDoNothing(); +} + export default function () { ponder.on(pluginNamespace("BaseRegistrar:NameRegistered"), handleNameRegistered); ponder.on(pluginNamespace("BaseRegistrar:NameRenewed"), handleNameRenewed); ponder.on(pluginNamespace("BaseRegistrar:Transfer"), async ({ context, event }) => { - if (event.args.from === zeroAddress) { - /** - * Address the issue where in the same transaction the Transfer event occurs before the NameRegistered event. - * Example: https://lineascan.build/tx/0x2211c5d857d16b7ac111088c57fb346ab94049cb297f02b0dda7aaf4c14d305b#eventlog - * Code: https://github.com/Consensys/linea-ens/blob/main/packages/linea-ens-contracts/contracts/ethregistrar/BaseRegistrarImplementation.sol#L155-L160 - */ + const { tokenId, from, to } = event.args; - const { tokenId: id, to: owner } = event.args; - const label = tokenIdToLabel(id); - const node = makeSubnodeNamehash(ownedSubnameNode, label); - - await context.db - .insert(domains) - .values({ - id: node, - ownerId: owner, - createdAt: event.block.timestamp, - }) - .onConflictDoNothing(); + if (event.args.from === zeroAddress) { + // The ens-subgraph `handleNameTransferred` handler implementation + // assumes the domain record exists. However, when an NFT token is + // minted, there's no domain record yet created. The very first transfer + // event has to initialize the domain record. This is a workaround to + // meet the subgraph implementation expectations. + await handleDomainNameInitialized({ + context, + event: { + ...event, + args: { id: tokenId, owner: to }, + }, + }); } - return await handleNameTransferred({ context, args: event.args }); + + await handleNameTransferred({ + context, + args: { from, to, tokenId }, + }); }); // Linea allows the owner of the EthRegistrarController to register subnames for free ponder.on( pluginNamespace("EthRegistrarController:OwnerNameRegistered"), async ({ context, event }) => { - return handleNameRegisteredByController({ + await handleNameRegisteredByController({ context, args: { ...event.args, @@ -60,7 +88,7 @@ export default function () { ponder.on( pluginNamespace("EthRegistrarController:PohNameRegistered"), async ({ context, event }) => { - return handleNameRegisteredByController({ + await handleNameRegisteredByController({ context, args: { ...event.args, @@ -74,7 +102,7 @@ export default function () { pluginNamespace("EthRegistrarController:NameRegistered"), async ({ context, event }) => { // the new registrar controller uses baseCost + premium to compute cost - return await handleNameRegisteredByController({ + await handleNameRegisteredByController({ context, args: { ...event.args, @@ -84,6 +112,6 @@ export default function () { }, ); ponder.on(pluginNamespace("EthRegistrarController:NameRenewed"), async ({ context, event }) => { - return await handleNameRenewedByController({ context, args: event.args }); + await handleNameRenewedByController({ context, args: event.args }); }); } From fa57a34d0c5b2f5f918c9f864b5ba9e4443609b1 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Mon, 13 Jan 2025 20:52:58 +0100 Subject: [PATCH 8/8] feat(plugins): introduce ACTIVE_PLUGINS validation --- src/lib/plugin-helpers.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/lib/plugin-helpers.ts b/src/lib/plugin-helpers.ts index 7cafdc489f..f65715dadb 100644 --- a/src/lib/plugin-helpers.ts +++ b/src/lib/plugin-helpers.ts @@ -100,8 +100,21 @@ export function getActivePlugins(plugins: reado throw new Error("No active plugins found. Please set the ACTIVE_PLUGINS environment variable."); } - // TODO: drop an error if the plugin is not found + // Check if the requested plugins are valid and can become active + const invalidPlugins = pluginsToActivateByOwnedName.filter( + (plugin) => !plugins.some((p) => p.ownedName === plugin), + ); + if (invalidPlugins.length) { + // Throw an error if there are invalid plugins + throw new Error( + `Invalid plugin names found: ${invalidPlugins.join( + ", ", + )}. Please check the ACTIVE_PLUGINS environment variable.`, + ); + } + + // Return the active plugins return plugins.filter((plugin) => pluginsToActivateByOwnedName.includes(plugin.ownedName)); }