From 618e8d30d9722c0f7b8938338b42660713a1fa2c Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Sun, 12 Jan 2025 08:44:22 +0100 Subject: [PATCH 01/41] 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 b0ddcdf..b6d1167 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 d291439..6b370f7 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 a0e2880..efeaebd 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 6da6530..980cfff 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 3212e10..0d7914c 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 e57005d..707fc37 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 16df285..6dc5f9d 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 33e3219..7744011 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 0dcaa8a..535376d 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 02/41] 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 0abac19..911beba 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 6340a2c..e4ed62e 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 7744011..3d70395 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 535376d..1268343 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 03/41] 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 980cfff..eaec7a2 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 e864be6..58177f9 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 0c0c490..f609667 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 04/41] 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 efeaebd..63ab9b0 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 05/41] 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 6252eb7..64a5f99 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 63ab9b0..744a0bc 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 707fc37..7cafdc4 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 991fc8f..38ac070 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 e4ed62e..b4394ce 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 f609667..58de1a5 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 1268343..0ee670e 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 06/41] 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 6dc5f9d..6d0e1c5 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 3d70395..6a9164a 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 07/41] 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 64a5f99..543c312 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 744a0bc..07dab93 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 911beba..6391161 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 b2837a1..528979c 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 6d0e1c5..e1942cd 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 6a9164a..248a29d 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 08/41] 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 7cafdc4..f65715d 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)); } From 748dfa41c6707bb37499c78cc9e11ff50388e86a Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Mon, 13 Jan 2025 21:27:59 +0100 Subject: [PATCH 09/41] fix(schema): use proper schema import --- src/plugins/linea.eth/handlers/EthRegistrar.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/linea.eth/handlers/EthRegistrar.ts b/src/plugins/linea.eth/handlers/EthRegistrar.ts index 248a29d..2e48590 100644 --- a/src/plugins/linea.eth/handlers/EthRegistrar.ts +++ b/src/plugins/linea.eth/handlers/EthRegistrar.ts @@ -1,5 +1,5 @@ import { type Context, ponder } from "ponder:registry"; -import { domains } from "ponder:schema"; +import schema from "ponder:schema"; import { type Block } from "ponder"; import { Hex, zeroAddress } from "viem"; import { makeRegistrarHandlers } from "../../../handlers/Registrar"; @@ -33,7 +33,7 @@ function handleDomainNameInitialized({ const label = tokenIdToLabel(id); const node = makeSubnodeNamehash(ownedSubnameNode, label); return context.db - .insert(domains) + .insert(schema.domain) .values({ id: node, ownerId: owner, From 5365dd66f49f054f572571fcddd3e8825f0d7433 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Mon, 13 Jan 2025 21:28:47 +0100 Subject: [PATCH 10/41] feat(utils): import deep merge npm lib --- package.json | 1 + pnpm-lock.yaml | 9 +++++++++ src/lib/helpers.ts | 40 ++-------------------------------------- 3 files changed, 12 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index b6d1167..8da9ec5 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@ensdomains/ensjs": "^4.0.2", "hono": "^4.6.14", "ponder": "^0.8.24", + "ts-deepmerge": "^7.0.2", "viem": "^2.21.57" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6b370f7..62adf5f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: ponder: 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)) + ts-deepmerge: + specifier: ^7.0.2 + version: 7.0.2 viem: specifier: ^2.21.57 version: 2.21.57(typescript@5.7.2) @@ -1734,6 +1737,10 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + ts-deepmerge@7.0.2: + resolution: {integrity: sha512-akcpDTPuez4xzULo5NwuoKwYRtjQJ9eoNfBACiBMaXwNAx7B1PKfe5wqUFJuW5uKzQ68YjDFwPaWHDG1KnFGsA==} + engines: {node: '>=14.13.1'} + ts-pattern@5.6.0: resolution: {integrity: sha512-SL8u60X5+LoEy9tmQHWCdPc2hhb2pKI6I1tU5Jue3v8+iRqZdcT3mWPwKKJy1fMfky6uha82c8ByHAE8PMhKHw==} @@ -3505,6 +3512,8 @@ snapshots: tr46@0.0.3: {} + ts-deepmerge@7.0.2: {} + ts-pattern@5.6.0: {} tsconfck@3.1.4(typescript@5.7.2): diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 0d7914c..d6e89c5 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -1,4 +1,5 @@ import type { ContractConfig } from "ponder"; +import { merge as tsDeepMerge } from 'ts-deepmerge'; export const uniq = (arr: T[]): T[] => [...new Set(arr)]; @@ -24,47 +25,10 @@ type AnyObject = { [key: string]: any }; * @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; + return tsDeepMerge(target, source) as T & U; } From 43b1dc756b3790c135ef56b3314b175ef917fbad Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Mon, 13 Jan 2025 22:15:14 +0100 Subject: [PATCH 11/41] fix(codestyle): apply formatting --- src/lib/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index d6e89c5..3be9423 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -1,5 +1,5 @@ import type { ContractConfig } from "ponder"; -import { merge as tsDeepMerge } from 'ts-deepmerge'; +import { merge as tsDeepMerge } from "ts-deepmerge"; export const uniq = (arr: T[]): T[] => [...new Set(arr)]; From ac141f59922eaae6eb73c41b5ed344e682263a1a Mon Sep 17 00:00:00 2001 From: Tomek Kopacki Date: Tue, 14 Jan 2025 14:05:57 +0100 Subject: [PATCH 12/41] docs: apply text edits Apply PR suggestion regarding the in-code documentation Co-authored-by: lightwalker.eth <126201998+lightwalker-eth@users.noreply.github.com> --- .env.local.example | 5 +++-- ponder.config.ts | 6 +++--- src/lib/plugin-helpers.ts | 4 ++-- src/plugins/README.md | 6 +++--- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.env.local.example b/.env.local.example index 543c312..d8993e0 100644 --- a/.env.local.example +++ b/.env.local.example @@ -5,7 +5,8 @@ RPC_URL_1=https://ethereum-rpc.publicnode.com RPC_URL_8453=https://base-rpc.publicnode.com RPC_URL_59144=https://linea-rpc.publicnode.com -# Identify which indexer plugin to activate (see `src/plugins` for available plugins) +# Identify which indexer plugins to activate (see `src/plugins` for available plugins) +# This is a comma separated list of one or more available plugin names. # ACTIVE_PLUGINS=eth,base.eth,linea.eth # ACTIVE_PLUGINS=base.eth,linea.eth @@ -18,7 +19,7 @@ ACTIVE_PLUGINS=eth # # Keeping the database schema unique to the indexer instance is important to # 1) speed up indexing after a restart -# 2) prevent data corruption +# 2) prevent data corruption from multiple indexer app instances writing state concurrently to the same db schema # # No two indexer instances can use the same database schema at the same time. # diff --git a/ponder.config.ts b/ponder.config.ts index 07dab93..bd983c9 100644 --- a/ponder.config.ts +++ b/ponder.config.ts @@ -12,8 +12,8 @@ const plugins = [baseEthPlugin, ethPlugin, lineaEthPlugin] as const; // of the indexing handlers type AllPluginsConfig = IntersectionOf<(typeof plugins)[number]["config"]>; -// Activates the indexing handlers included in selected active plugins and -// returns and intersection of their combined config. +// Activates the indexing handlers of activated plugins and +// returns the intersection of their combined config. function getActivePluginsConfig(): AllPluginsConfig { const activePlugins = getActivePlugins(plugins); @@ -28,5 +28,5 @@ function getActivePluginsConfig(): AllPluginsConfig { } // The type of the default export is the intersection of all available plugin -// configs to each plugin can be correctly typechecked +// configs so that each plugin can be correctly typechecked export default getActivePluginsConfig(); diff --git a/src/lib/plugin-helpers.ts b/src/lib/plugin-helpers.ts index f65715d..0789c39 100644 --- a/src/lib/plugin-helpers.ts +++ b/src/lib/plugin-helpers.ts @@ -79,11 +79,11 @@ type PluginNamespacePath = | `/${string}` | `/${string}${T}`; -/** @var the requested active plugin name (see `src/plugins` for available plugins) */ +/** @var comma separated list of the requested active plugin names (see `src/plugins` for available plugins) */ export const ACTIVE_PLUGINS = process.env.ACTIVE_PLUGINS; /** - * Returns the active plugins list based on the `ACTIVE_PLUGINS` environment variable. + * Returns the list of 1 or more active plugins based on the `ACTIVE_PLUGINS` environment variable. * * The `ACTIVE_PLUGINS` environment variable is a comma-separated list of plugin * names. The function returns the plugins that are included in the list. diff --git a/src/plugins/README.md b/src/plugins/README.md index 38ac070..a5003e5 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_PLUGINS` env variable to select the active plugin, for example: +This directory contains plugins which define subname-specific processing of blockchain events. +One or more plugins are activated at a time. Use the `ACTIVE_PLUGINS` env variable to select the active plugins, for example: ``` -ACTIVE_PLUGINS=base.eth +ACTIVE_PLUGINS=eth,base.eth,linea.eth ``` \ No newline at end of file From 46ab32ddfeea14e8b8dc331fe59104d6e84aa97b Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 14 Jan 2025 14:47:55 +0100 Subject: [PATCH 13/41] feat(database-entites): ensure domain entity exists Create an idepotenet function that takes a domain token ID and makes sure there is a database entity created for it. --- src/handlers/Registry.ts | 19 +++---- src/lib/plugin-helpers.ts | 4 +- src/lib/upserts.ts | 22 ++++++++ src/plugins/base.eth/handlers/Registrar.ts | 32 +++++++----- .../linea.eth/handlers/EthRegistrar.ts | 52 +++++-------------- 5 files changed, 61 insertions(+), 68 deletions(-) diff --git a/src/handlers/Registry.ts b/src/handlers/Registry.ts index edcd475..c1c59b9 100644 --- a/src/handlers/Registry.ts +++ b/src/handlers/Registry.ts @@ -5,7 +5,7 @@ import { Block } from "ponder"; import { type Hex, zeroAddress } from "viem"; import { makeResolverId } from "../lib/ids"; import { ROOT_NODE, makeSubnodeNamehash } from "../lib/subname-helpers"; -import { upsertAccount } from "../lib/upserts"; +import { ensureDomainExists, upsertAccount } from "../lib/upserts"; /** * Initialize the ENS root node with the zeroAddress as the owner. @@ -15,15 +15,12 @@ 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(schema.domain) - .values({ - id: ROOT_NODE, - ownerId: zeroAddress, - createdAt: 0n, - isMigrated: false, - }) - .onConflictDoNothing(); + await ensureDomainExists(context, { + id: ROOT_NODE, + ownerId: zeroAddress, + createdAt: 0n, + isMigrated: false, + }); } function isDomainEmpty(domain: typeof schema.domain.$inferSelect) { @@ -104,7 +101,7 @@ export const handleNewOwner = await context.db.update(schema.domain, { id: domain.id }).set({ ownerId: owner, isMigrated }); } else { // otherwise create the domain - domain = await context.db.insert(schema.domain).values({ + domain = await ensureDomainExists(context, { id: subnode, ownerId: owner, parentId: node, diff --git a/src/lib/plugin-helpers.ts b/src/lib/plugin-helpers.ts index 0789c39..1e873f3 100644 --- a/src/lib/plugin-helpers.ts +++ b/src/lib/plugin-helpers.ts @@ -92,9 +92,7 @@ export const ACTIVE_PLUGINS = process.env.ACTIVE_PLUGINS; * @returns the active plugins */ export function getActivePlugins(plugins: readonly T[]): T[] { - const pluginsToActivateByOwnedName = ACTIVE_PLUGINS - ? ACTIVE_PLUGINS.split(",").map((p) => p.toLowerCase()) - : []; + const pluginsToActivateByOwnedName = ACTIVE_PLUGINS ? ACTIVE_PLUGINS.split(",") : []; if (!pluginsToActivateByOwnedName.length) { throw new Error("No active plugins found. Please set the ACTIVE_PLUGINS environment variable."); diff --git a/src/lib/upserts.ts b/src/lib/upserts.ts index aa12290..c098ff1 100644 --- a/src/lib/upserts.ts +++ b/src/lib/upserts.ts @@ -19,3 +19,25 @@ export async function upsertRegistration( ) { return context.db.insert(schema.registration).values(values).onConflictDoUpdate(values); } + +/** + * Idempotent handler to ensure a domain entity for requested node exists in + * the database. It inserts a domain entity if it does not exist. Otherwise, + * just returns the existing domain entity from the db. + * + * @param context ponder context object + * @param values domain properties + * @returns domain database entity + */ +export async function ensureDomainExists( + context: Context, + values: typeof schema.domain.$inferInsert, +): Promise { + const domainEntity = await context.db.insert(schema.domain).values(values).onConflictDoNothing(); + + if (!domainEntity) { + throw new Error("domain expected"); + } + + return domainEntity; +} diff --git a/src/plugins/base.eth/handlers/Registrar.ts b/src/plugins/base.eth/handlers/Registrar.ts index f25349a..1cdae23 100644 --- a/src/plugins/base.eth/handlers/Registrar.ts +++ b/src/plugins/base.eth/handlers/Registrar.ts @@ -4,7 +4,7 @@ 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"; +import { ensureDomainExists, upsertAccount } from "../../../lib/upserts"; import { ownedName, pluginNamespace } from "../ponder.config"; const { @@ -56,7 +56,11 @@ export default function () { // 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. - await handleDomainNameInitialized({ context, event }); + await ensureDomainExists(context, { + id: makeSubnodeNamehash(ownedSubnameNode, tokenIdToLabel(event.args.id)), + ownerId: event.args.owner, + createdAt: event.block.timestamp, + }); // after ensuring the domain exists, continue with the standard handler return handleNameRegistered({ context, event }); @@ -64,27 +68,27 @@ export default function () { ponder.on(pluginNamespace("BaseRegistrar:NameRenewed"), handleNameRenewed); ponder.on(pluginNamespace("BaseRegistrar:Transfer"), async ({ context, event }) => { - const { id, from, to } = event.args; + // base.eth's BaseRegistrar uses `id` instead of `tokenId` + const { id: tokenId, from, to } = event.args; 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 }, - }, + // minted, there's no domain entity in the database yet. The very first + // transfer event has to ensure the domain entity for the requested + // token ID has been inserted into the database. This is a workaround to + // meet expectations of the `handleNameTransferred` subgraph + // implementation. + await ensureDomainExists(context, { + id: makeSubnodeNamehash(ownedSubnameNode, tokenIdToLabel(tokenId)), + ownerId: to, + createdAt: event.block.timestamp, }); } - // base.eth's BaseRegistrar uses `id` instead of `tokenId` await handleNameTransferred({ context, - args: { from, to, tokenId: id }, + args: { from, to, tokenId }, }); }); diff --git a/src/plugins/linea.eth/handlers/EthRegistrar.ts b/src/plugins/linea.eth/handlers/EthRegistrar.ts index 2e48590..599fe4f 100644 --- a/src/plugins/linea.eth/handlers/EthRegistrar.ts +++ b/src/plugins/linea.eth/handlers/EthRegistrar.ts @@ -1,9 +1,8 @@ -import { type Context, ponder } from "ponder:registry"; -import schema from "ponder:schema"; -import { type Block } from "ponder"; -import { Hex, zeroAddress } from "viem"; +import { ponder } from "ponder:registry"; +import { zeroAddress } from "viem"; import { makeRegistrarHandlers } from "../../../handlers/Registrar"; import { makeSubnodeNamehash, tokenIdToLabel } from "../../../lib/subname-helpers"; +import { ensureDomainExists } from "../../../lib/upserts"; import { ownedName, pluginNamespace } from "../ponder.config"; const { @@ -15,33 +14,6 @@ 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(schema.domain) - .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); @@ -52,15 +24,15 @@ export default function () { 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 }, - }, + // minted, there's no domain entity in the database yet. The very first + // transfer event has to ensure the domain entity for the requested + // token ID has been inserted into the database. This is a workaround to + // meet expectations of the `handleNameTransferred` subgraph + // implementation. + await ensureDomainExists(context, { + id: makeSubnodeNamehash(ownedSubnameNode, tokenIdToLabel(tokenId)), + ownerId: to, + createdAt: event.block.timestamp, }); } From 273cd63721a906f28d1a66aa8504b4e3b1826914 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 14 Jan 2025 15:56:50 +0100 Subject: [PATCH 14/41] fix(upserts): make `ensureDomainExists` to always return a DB entity` -m "Had to replace `.onConflictDoNothing()` with `.onConflictDoUpdate({})` as the former would not return a DB entity if there was a conflict. The later would apply the delta on conflict, but since delta is an empty object, there will be no update at all. And yet, the existing DB entity will be always returned." --- src/lib/upserts.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/lib/upserts.ts b/src/lib/upserts.ts index c098ff1..e5b15f2 100644 --- a/src/lib/upserts.ts +++ b/src/lib/upserts.ts @@ -33,11 +33,8 @@ export async function ensureDomainExists( context: Context, values: typeof schema.domain.$inferInsert, ): Promise { - const domainEntity = await context.db.insert(schema.domain).values(values).onConflictDoNothing(); - - if (!domainEntity) { - throw new Error("domain expected"); - } - - return domainEntity; + // `onConflictDoUpdate({}}` makes no change to the existing entity + // (the delta is an empty object, which means no updates to apply) + // and it always returns the existing entity + return context.db.insert(schema.domain).values(values).onConflictDoUpdate({}); } From 5e1b695b19ee6009e94c832913e100459e7a9686 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 14 Jan 2025 16:06:51 +0100 Subject: [PATCH 15/41] docs: update description of `ensureDomainExists` --- src/lib/upserts.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib/upserts.ts b/src/lib/upserts.ts index e5b15f2..622de11 100644 --- a/src/lib/upserts.ts +++ b/src/lib/upserts.ts @@ -21,12 +21,12 @@ export async function upsertRegistration( } /** - * Idempotent handler to ensure a domain entity for requested node exists in - * the database. It inserts a domain entity if it does not exist. Otherwise, - * just returns the existing domain entity from the db. + * A function to ensure a domain entity for requested node value exists in + * the database. It inserts the provided domain entity if it does not exist. + * Otherwise, just returns the existing domain entity from the db. * - * @param context ponder context object - * @param values domain properties + * @param context ponder context to interact with database + * @param values domain properties, where `id` is the node value * @returns domain database entity */ export async function ensureDomainExists( From f63ba6a474cac121426538184e559f54410057dc Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 14 Jan 2025 16:07:57 +0100 Subject: [PATCH 16/41] fix(defaults): make all available plugins active Declare all plugins active with the default `ACTIVE_PLUGINS` env var. --- .env.local.example | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.env.local.example b/.env.local.example index d8993e0..7fb2496 100644 --- a/.env.local.example +++ b/.env.local.example @@ -8,9 +8,7 @@ RPC_URL_59144=https://linea-rpc.publicnode.com # Identify which indexer plugins to activate (see `src/plugins` for available plugins) # This is a comma separated list of one or more available plugin names. -# ACTIVE_PLUGINS=eth,base.eth,linea.eth -# ACTIVE_PLUGINS=base.eth,linea.eth -ACTIVE_PLUGINS=eth +ACTIVE_PLUGINS=base.eth,linea.eth # Database configuration From 8d2bb1b48a178f75f6bd6be826c697dad71cc3d1 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 14 Jan 2025 16:38:14 +0100 Subject: [PATCH 17/41] feat(deps): update ponder --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 8da9ec5..2cda7f4 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "dependencies": { "@ensdomains/ensjs": "^4.0.2", "hono": "^4.6.14", - "ponder": "^0.8.24", + "ponder": "^0.8.26", "ts-deepmerge": "^7.0.2", "viem": "^2.21.57" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62adf5f..62c531e 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.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)) + specifier: ^0.8.26 + version: 0.8.26(@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)) ts-deepmerge: specifier: ^7.0.2 version: 7.0.2 @@ -1455,8 +1455,8 @@ packages: resolution: {integrity: sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==} hasBin: true - ponder@0.8.24: - resolution: {integrity: sha512-WMj9FmlY+A2Wb07rHbhekai9Z/JsCFz31+7+Zfjg5I933LbV3FeWYy/q277A4h7ai9o/yrVBfkL8kbUmO40Y7g==} + ponder@0.8.26: + resolution: {integrity: sha512-K1fAaJK8eGdAnur+X6TGW/3y7hJc+3HSB5KJMFYiUNReExgFG9kxDT6+La/myv8IHm3SE2BB7BAWAXmaUYy2Cg==} engines: {node: '>=18.14'} hasBin: true peerDependencies: @@ -3164,7 +3164,7 @@ snapshots: sonic-boom: 3.8.1 thread-stream: 2.7.0 - 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)): + ponder@0.8.26(@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) From c478ad538ee8edbb7959d06b500d9a6dafce13f9 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 14 Jan 2025 16:56:52 +0100 Subject: [PATCH 18/41] docs(handlers): update Registry descriptions --- src/handlers/Registry.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/handlers/Registry.ts b/src/handlers/Registry.ts index c1c59b9..7216a2b 100644 --- a/src/handlers/Registry.ts +++ b/src/handlers/Registry.ts @@ -9,6 +9,24 @@ import { ensureDomainExists, upsertAccount } from "../lib/upserts"; /** * Initialize the ENS root node with the zeroAddress as the owner. + * Any permutation of plugins might be activated and multiple plugins expect + * the ENS root to exist. This behavior is consistent with the ens-subgraph, + * which initializes the ENS root node in the same way. However, + * the ens-subgraph does not have independent plugins. In our case, we have + * multiple plugins that might be activated independently. Regardless of + * the permutation of active plugins, they all expect the ENS root to exist. + * + * This function has to be used a callback on the Ponder's setup event handler. + * https://ponder.sh/docs/api-reference/indexing-functions#setup-event + * In case there are multiple setup handlers defined, the order of execution + * is guaranteed to be the same, and it is the reverse order of + * the network name configured on a given contract that the setup event was + * registered for. + * + * For example, for setup events registered for contracts on: base, linea, mainnet + * The order of execution will be: mainnet, linea, base. And if the contracts + * were registered on: base, ethereum, linea, the order of execution will be: + * linea, ethereum, base. */ export async function setupRootNode({ context }: { context: Context }) { // ensure we have an account for the zeroAddress @@ -101,7 +119,7 @@ export const handleNewOwner = await context.db.update(schema.domain, { id: domain.id }).set({ ownerId: owner, isMigrated }); } else { // otherwise create the domain - domain = await ensureDomainExists(context, { + domain = await context.db.insert(schema.domain).values({ id: subnode, ownerId: owner, parentId: node, From 6e19c7d28ae195058dd89c4afeaf593d252099e3 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 14 Jan 2025 17:00:33 +0100 Subject: [PATCH 19/41] fix(handlers-base.eth): remove dead code --- src/plugins/base.eth/handlers/Registrar.ts | 27 ---------------------- 1 file changed, 27 deletions(-) diff --git a/src/plugins/base.eth/handlers/Registrar.ts b/src/plugins/base.eth/handlers/Registrar.ts index 1cdae23..b2bcc74 100644 --- a/src/plugins/base.eth/handlers/Registrar.ts +++ b/src/plugins/base.eth/handlers/Registrar.ts @@ -16,33 +16,6 @@ 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(schema.domain) - .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 }) => From 38e9c15de74a0bb7a8ef9a85abe9bf05ccc251a3 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 14 Jan 2025 17:21:41 +0100 Subject: [PATCH 20/41] feat(rpc): define env vars with rate limits This change allows defining env vars with rate limtis for selected RPC nodes. --- .env.local.example | 11 ++++++--- src/handlers/Registry.ts | 6 ++--- src/lib/helpers.ts | 31 ++++++++++++++++++++++++++ src/plugins/base.eth/ponder.config.ts | 4 ++-- src/plugins/eth/ponder.config.ts | 4 ++-- src/plugins/linea.eth/ponder.config.ts | 4 ++-- 6 files changed, 48 insertions(+), 12 deletions(-) diff --git a/.env.local.example b/.env.local.example index 7fb2496..10892e8 100644 --- a/.env.local.example +++ b/.env.local.example @@ -1,9 +1,13 @@ # RPC configuration -# Follow the format: RPC_URL_{chainId}={rpcUrl} - +# For RPC URL, follow the format: RPC_URL_{chainId}={rpcUrl} RPC_URL_1=https://ethereum-rpc.publicnode.com RPC_URL_8453=https://base-rpc.publicnode.com RPC_URL_59144=https://linea-rpc.publicnode.com +# For RPC rate limiting, follow the format: +# RPC_REQUEST_RATE_LIMIT_{chainId}={rateLimitInRequestsPerSecond} +RPC_REQUEST_RATE_LIMIT_1=50 +RPC_REQUEST_RATE_LIMIT_8453=20 +RPC_REQUEST_RATE_LIMIT_59144=20 # Identify which indexer plugins to activate (see `src/plugins` for available plugins) # This is a comma separated list of one or more available plugin names. @@ -17,7 +21,8 @@ ACTIVE_PLUGINS=base.eth,linea.eth # # Keeping the database schema unique to the indexer instance is important to # 1) speed up indexing after a restart -# 2) prevent data corruption from multiple indexer app instances writing state concurrently to the same db schema +# 2) prevent data corruption from multiple indexer app instances writing state +# concurrently to the same db schema # # No two indexer instances can use the same database schema at the same time. # diff --git a/src/handlers/Registry.ts b/src/handlers/Registry.ts index 7216a2b..af31bf1 100644 --- a/src/handlers/Registry.ts +++ b/src/handlers/Registry.ts @@ -15,14 +15,14 @@ import { ensureDomainExists, upsertAccount } from "../lib/upserts"; * the ens-subgraph does not have independent plugins. In our case, we have * multiple plugins that might be activated independently. Regardless of * the permutation of active plugins, they all expect the ENS root to exist. - * + * * This function has to be used a callback on the Ponder's setup event handler. * https://ponder.sh/docs/api-reference/indexing-functions#setup-event * In case there are multiple setup handlers defined, the order of execution * is guaranteed to be the same, and it is the reverse order of * the network name configured on a given contract that the setup event was - * registered for. - * + * registered for. + * * For example, for setup events registered for contracts on: base, linea, mainnet * The order of execution will be: mainnet, linea, base. And if the contracts * were registered on: base, ethereum, linea, the order of execution will be: diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 3be9423..f633277 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -18,6 +18,37 @@ export const blockConfig = ( endBlock: end, }); +/** + * Reads the RPC request rate limit for a given chain ID from the environment + * variable that follow naming convention: RPC_REQUEST_RATE_LIMIT_{chianId}. + * For example, for Ethereum mainnet the chainId is `1`, so the env variable + * can be set as `RPC_REQUEST_RATE_LIMIT_1=400`. This will set the rate limit + * for the mainnet (chainId=1) to 400 requests per second. If the environment + * variable is not set for the requested chain ID, the default rate limit is 50 rps. + * + * The rate limit is the maximum number of requests per second that can be made + * to the RPC endpoint. For public RPC endpoints, it is recommended to set + * a rate limit to low values (i.e. below 30 rps) to avoid being rate limited. + * For private RPC endpoints, the rate limit can be set to higher values, + * depending on the capacity of the endpoint. For example, 500 rps. + * + * @param chainId the chain ID to read the rate limit for from the environment variable + * @returns the rate limit in requests per second (rps) + */ +export const rpcRequestRateLimit = (chainId: number): number | undefined => { + if (typeof process.env[`RPC_REQUEST_RATE_LIMIT_${chainId}`] === "string") { + try { + return parseInt(process.env[`RPC_REQUEST_RATE_LIMIT_${chainId}`]!, 10); + } catch (e) { + throw new Error( + `Invalid RPC_REQUEST_RATE_LIMIT_${chainId} value: ${e}. Please provide a valid number.`, + ); + } + } + + return 50; +}; + type AnyObject = { [key: string]: any }; /** diff --git a/src/plugins/base.eth/ponder.config.ts b/src/plugins/base.eth/ponder.config.ts index b4394ce..fb4eda2 100644 --- a/src/plugins/base.eth/ponder.config.ts +++ b/src/plugins/base.eth/ponder.config.ts @@ -2,7 +2,7 @@ import { type ContractConfig, createConfig, factory } from "ponder"; import { http, getAbiItem } from "viem"; import { base } from "viem/chains"; -import { blockConfig } from "../../lib/helpers"; +import { blockConfig, rpcRequestRateLimit } from "../../lib/helpers"; import { createPluginNamespace } from "../../lib/plugin-helpers"; import { BaseRegistrar } from "./abis/BaseRegistrar"; import { EarlyAccessRegistrarController } from "./abis/EARegistrarController"; @@ -24,7 +24,7 @@ export const config = createConfig({ base: { chainId: base.id, transport: http(process.env[`RPC_URL_${base.id}`]), - maxRequestsPerSecond: 250, + maxRequestsPerSecond: rpcRequestRateLimit(base.id), }, }, contracts: { diff --git a/src/plugins/eth/ponder.config.ts b/src/plugins/eth/ponder.config.ts index 58de1a5..de40ae1 100644 --- a/src/plugins/eth/ponder.config.ts +++ b/src/plugins/eth/ponder.config.ts @@ -2,7 +2,7 @@ import { ContractConfig, createConfig, factory, mergeAbis } from "ponder"; import { http, getAbiItem } from "viem"; import { mainnet } from "viem/chains"; -import { blockConfig } from "../../lib/helpers"; +import { blockConfig, rpcRequestRateLimit } from "../../lib/helpers"; import { createPluginNamespace } from "../../lib/plugin-helpers"; import { BaseRegistrar } from "./abis/BaseRegistrar"; import { EthRegistrarController } from "./abis/EthRegistrarController"; @@ -28,7 +28,7 @@ export const config = createConfig({ mainnet: { chainId: mainnet.id, transport: http(process.env[`RPC_URL_${mainnet.id}`]), - maxRequestsPerSecond: 250, + maxRequestsPerSecond: rpcRequestRateLimit(mainnet.id), }, }, contracts: { diff --git a/src/plugins/linea.eth/ponder.config.ts b/src/plugins/linea.eth/ponder.config.ts index 0ee670e..25626a8 100644 --- a/src/plugins/linea.eth/ponder.config.ts +++ b/src/plugins/linea.eth/ponder.config.ts @@ -2,7 +2,7 @@ import { ContractConfig, createConfig, factory, mergeAbis } from "ponder"; import { http, getAbiItem } from "viem"; import { linea } from "viem/chains"; -import { blockConfig } from "../../lib/helpers"; +import { blockConfig, rpcRequestRateLimit } from "../../lib/helpers"; import { createPluginNamespace } from "../../lib/plugin-helpers"; import { BaseRegistrar } from "./abis/BaseRegistrar"; import { EthRegistrarController } from "./abis/EthRegistrarController"; @@ -24,7 +24,7 @@ export const config = createConfig({ linea: { chainId: linea.id, transport: http(process.env[`RPC_URL_${linea.id}`]), - maxRequestsPerSecond: 250, + maxRequestsPerSecond: rpcRequestRateLimit(linea.id), }, }, contracts: { From fd2d56714c5895da86f0f33d0213ace7d5d57517 Mon Sep 17 00:00:00 2001 From: Tomek Kopacki Date: Tue, 14 Jan 2025 19:20:17 +0100 Subject: [PATCH 21/41] docs: update descriptions as suggested by PR feedback Co-authored-by: lightwalker.eth <126201998+lightwalker-eth@users.noreply.github.com> --- .env.local.example | 6 +++--- src/lib/helpers.ts | 2 +- src/lib/upserts.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.env.local.example b/.env.local.example index 10892e8..03e378c 100644 --- a/.env.local.example +++ b/.env.local.example @@ -1,9 +1,9 @@ # RPC configuration -# For RPC URL, follow the format: RPC_URL_{chainId}={rpcUrl} +# For the RPC URL of each chain, follow the format: RPC_URL_{chainId}={rpcUrl} RPC_URL_1=https://ethereum-rpc.publicnode.com RPC_URL_8453=https://base-rpc.publicnode.com RPC_URL_59144=https://linea-rpc.publicnode.com -# For RPC rate limiting, follow the format: +# For the RPC rate limits of each chain, follow the format: # RPC_REQUEST_RATE_LIMIT_{chainId}={rateLimitInRequestsPerSecond} RPC_REQUEST_RATE_LIMIT_1=50 RPC_REQUEST_RATE_LIMIT_8453=20 @@ -12,7 +12,7 @@ RPC_REQUEST_RATE_LIMIT_59144=20 # Identify which indexer plugins to activate (see `src/plugins` for available plugins) # This is a comma separated list of one or more available plugin names. -ACTIVE_PLUGINS=base.eth,linea.eth +ACTIVE_PLUGINS=eth,base.eth,linea.eth # Database configuration diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index f633277..0059656 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -20,7 +20,7 @@ export const blockConfig = ( /** * Reads the RPC request rate limit for a given chain ID from the environment - * variable that follow naming convention: RPC_REQUEST_RATE_LIMIT_{chianId}. + * variable: RPC_REQUEST_RATE_LIMIT_{chainId}. * For example, for Ethereum mainnet the chainId is `1`, so the env variable * can be set as `RPC_REQUEST_RATE_LIMIT_1=400`. This will set the rate limit * for the mainnet (chainId=1) to 400 requests per second. If the environment diff --git a/src/lib/upserts.ts b/src/lib/upserts.ts index 622de11..bb1fc7a 100644 --- a/src/lib/upserts.ts +++ b/src/lib/upserts.ts @@ -21,7 +21,7 @@ export async function upsertRegistration( } /** - * A function to ensure a domain entity for requested node value exists in + * Ensure that some domain entity value (not necessarily the provided value) for the requested node exists in * the database. It inserts the provided domain entity if it does not exist. * Otherwise, just returns the existing domain entity from the db. * From 6da8bebb35eb4affad0a23810ba4e5241d6195a4 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 14 Jan 2025 19:23:16 +0100 Subject: [PATCH 22/41] docs: update .env.local.example Organize env vars in logical groups. Also, update the public RPC URLs to be the dRPC ones. --- .env.local.example | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/.env.local.example b/.env.local.example index 03e378c..06934a5 100644 --- a/.env.local.example +++ b/.env.local.example @@ -1,21 +1,15 @@ # RPC configuration # For the RPC URL of each chain, follow the format: RPC_URL_{chainId}={rpcUrl} -RPC_URL_1=https://ethereum-rpc.publicnode.com -RPC_URL_8453=https://base-rpc.publicnode.com -RPC_URL_59144=https://linea-rpc.publicnode.com +RPC_URL_1=https://eth.drpc.org +RPC_URL_8453=https://base.drpc.org +RPC_URL_59144=https://linea.drpc.org # For the RPC rate limits of each chain, follow the format: # RPC_REQUEST_RATE_LIMIT_{chainId}={rateLimitInRequestsPerSecond} RPC_REQUEST_RATE_LIMIT_1=50 RPC_REQUEST_RATE_LIMIT_8453=20 RPC_REQUEST_RATE_LIMIT_59144=20 -# Identify which indexer plugins to activate (see `src/plugins` for available plugins) -# This is a comma separated list of one or more available plugin names. - -ACTIVE_PLUGINS=eth,base.eth,linea.eth - # Database configuration - # 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. # @@ -29,7 +23,11 @@ ACTIVE_PLUGINS=eth,base.eth,linea.eth # 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 + +# Plugins configuration +# Identify which indexer plugins to activate (see `src/plugins` for available plugins) +# This is a comma separated list of one or more available plugin names. +ACTIVE_PLUGINS=eth,base.eth,linea.eth \ No newline at end of file From b6e75af37eb11aac3d645f0146d920179bfa416c Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 14 Jan 2025 19:29:30 +0100 Subject: [PATCH 23/41] docs(helpers): define default values as named consts Introduce `DEFAULT_RPC_RATE_LIMIT` const to describe a defult rate limit setting. --- src/lib/helpers.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 0059656..713fc81 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -18,13 +18,17 @@ export const blockConfig = ( endBlock: end, }); +// default rate limit for request per second to RPC endpoints +const DEFAULT_RPC_RATE_LIMIT = 50; + /** * Reads the RPC request rate limit for a given chain ID from the environment * variable: RPC_REQUEST_RATE_LIMIT_{chainId}. * For example, for Ethereum mainnet the chainId is `1`, so the env variable * can be set as `RPC_REQUEST_RATE_LIMIT_1=400`. This will set the rate limit * for the mainnet (chainId=1) to 400 requests per second. If the environment - * variable is not set for the requested chain ID, the default rate limit is 50 rps. + * variable is not set for the requested chain ID, use `DEFAULT_RPC_RATE_LIMIT` + * as the default value. * * The rate limit is the maximum number of requests per second that can be made * to the RPC endpoint. For public RPC endpoints, it is recommended to set @@ -35,7 +39,7 @@ export const blockConfig = ( * @param chainId the chain ID to read the rate limit for from the environment variable * @returns the rate limit in requests per second (rps) */ -export const rpcRequestRateLimit = (chainId: number): number | undefined => { +export const rpcRequestRateLimit = (chainId: number): number => { if (typeof process.env[`RPC_REQUEST_RATE_LIMIT_${chainId}`] === "string") { try { return parseInt(process.env[`RPC_REQUEST_RATE_LIMIT_${chainId}`]!, 10); @@ -46,7 +50,7 @@ export const rpcRequestRateLimit = (chainId: number): number | undefined => { } } - return 50; + return DEFAULT_RPC_RATE_LIMIT; }; type AnyObject = { [key: string]: any }; From 7334d9d272e37a7a3861232d759610760599fecd Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 14 Jan 2025 19:32:42 +0100 Subject: [PATCH 24/41] refactor(db-helpers): rename `upserts into db-helpers The new name describes the file contents better. There are some upsert helpers, but also some idemptent-insert ones. --- src/handlers/NameWrapper.ts | 2 +- src/handlers/Registrar.ts | 2 +- src/handlers/Registry.ts | 2 +- src/handlers/Resolver.ts | 2 +- src/lib/{upserts.ts => db-helpers.ts} | 0 src/plugins/base.eth/handlers/Registrar.ts | 2 +- src/plugins/linea.eth/handlers/EthRegistrar.ts | 2 +- 7 files changed, 6 insertions(+), 6 deletions(-) rename src/lib/{upserts.ts => db-helpers.ts} (100%) diff --git a/src/handlers/NameWrapper.ts b/src/handlers/NameWrapper.ts index 5330c49..bd1191a 100644 --- a/src/handlers/NameWrapper.ts +++ b/src/handlers/NameWrapper.ts @@ -2,10 +2,10 @@ import { type Context, type Event, type EventNames } from "ponder:registry"; import schema from "ponder:schema"; import { checkPccBurned } from "@ensdomains/ensjs/utils"; import { type Address, type Hex, hexToBytes, namehash } from "viem"; +import { upsertAccount } from "../lib/db-helpers"; import { bigintMax } from "../lib/helpers"; import { makeEventId } from "../lib/ids"; import { decodeDNSPacketBytes, tokenIdToLabel } from "../lib/subname-helpers"; -import { upsertAccount } from "../lib/upserts"; // if the wrappedDomain in question has pcc burned (?) and a higher (?) expiry date, update the domain's expiryDate async function materializeDomainExpiryDate(context: Context, node: Hex) { diff --git a/src/handlers/Registrar.ts b/src/handlers/Registrar.ts index 2bf35be..eb3057b 100644 --- a/src/handlers/Registrar.ts +++ b/src/handlers/Registrar.ts @@ -2,8 +2,8 @@ import { type Context } from "ponder:registry"; import schema from "ponder:schema"; import { Block } from "ponder"; import { type Hex, namehash } from "viem"; +import { upsertAccount, upsertRegistration } from "../lib/db-helpers"; import { isLabelIndexable, makeSubnodeNamehash, tokenIdToLabel } from "../lib/subname-helpers"; -import { upsertAccount, upsertRegistration } from "../lib/upserts"; const GRACE_PERIOD_SECONDS = 7776000n; // 90 days in seconds diff --git a/src/handlers/Registry.ts b/src/handlers/Registry.ts index af31bf1..0204d1c 100644 --- a/src/handlers/Registry.ts +++ b/src/handlers/Registry.ts @@ -3,9 +3,9 @@ import schema from "ponder:schema"; import { encodeLabelhash } from "@ensdomains/ensjs/utils"; import { Block } from "ponder"; import { type Hex, zeroAddress } from "viem"; +import { ensureDomainExists, upsertAccount } from "../lib/db-helpers"; import { makeResolverId } from "../lib/ids"; import { ROOT_NODE, makeSubnodeNamehash } from "../lib/subname-helpers"; -import { ensureDomainExists, upsertAccount } from "../lib/upserts"; /** * Initialize the ENS root node with the zeroAddress as the owner. diff --git a/src/handlers/Resolver.ts b/src/handlers/Resolver.ts index a70605d..d77167a 100644 --- a/src/handlers/Resolver.ts +++ b/src/handlers/Resolver.ts @@ -2,9 +2,9 @@ import { type Context } from "ponder:registry"; import schema from "ponder:schema"; import { Log } from "ponder"; import { Hex } from "viem"; +import { upsertAccount, upsertResolver } from "../lib/db-helpers"; import { hasNullByte, uniq } from "../lib/helpers"; import { makeResolverId } from "../lib/ids"; -import { upsertAccount, upsertResolver } from "../lib/upserts"; export async function handleAddrChanged({ context, diff --git a/src/lib/upserts.ts b/src/lib/db-helpers.ts similarity index 100% rename from src/lib/upserts.ts rename to src/lib/db-helpers.ts diff --git a/src/plugins/base.eth/handlers/Registrar.ts b/src/plugins/base.eth/handlers/Registrar.ts index b2bcc74..5777e2b 100644 --- a/src/plugins/base.eth/handlers/Registrar.ts +++ b/src/plugins/base.eth/handlers/Registrar.ts @@ -3,8 +3,8 @@ import schema from "ponder:schema"; import { Block } from "ponder"; import { Hex, zeroAddress } from "viem"; import { makeRegistrarHandlers } from "../../../handlers/Registrar"; +import { ensureDomainExists, upsertAccount } from "../../../lib/db-helpers"; import { makeSubnodeNamehash, tokenIdToLabel } from "../../../lib/subname-helpers"; -import { ensureDomainExists, upsertAccount } from "../../../lib/upserts"; import { ownedName, pluginNamespace } from "../ponder.config"; const { diff --git a/src/plugins/linea.eth/handlers/EthRegistrar.ts b/src/plugins/linea.eth/handlers/EthRegistrar.ts index 599fe4f..d4ce1d5 100644 --- a/src/plugins/linea.eth/handlers/EthRegistrar.ts +++ b/src/plugins/linea.eth/handlers/EthRegistrar.ts @@ -1,8 +1,8 @@ import { ponder } from "ponder:registry"; import { zeroAddress } from "viem"; import { makeRegistrarHandlers } from "../../../handlers/Registrar"; +import { ensureDomainExists } from "../../../lib/db-helpers"; import { makeSubnodeNamehash, tokenIdToLabel } from "../../../lib/subname-helpers"; -import { ensureDomainExists } from "../../../lib/upserts"; import { ownedName, pluginNamespace } from "../ponder.config"; const { From dc94a323662e41902933273ac3e9d0fd41d88fb7 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 14 Jan 2025 20:09:13 +0100 Subject: [PATCH 25/41] refactor(db-helpers): drop `ensureDomainExists` Use plain DB inserts and apply conflict resolution method if applicatble. --- src/handlers/Registry.ts | 19 ++++++++++++------- src/lib/db-helpers.ts | 19 ------------------- src/plugins/base.eth/handlers/Registrar.ts | 13 ++++++------- .../linea.eth/handlers/EthRegistrar.ts | 4 ++-- 4 files changed, 20 insertions(+), 35 deletions(-) diff --git a/src/handlers/Registry.ts b/src/handlers/Registry.ts index 0204d1c..3b1a5fe 100644 --- a/src/handlers/Registry.ts +++ b/src/handlers/Registry.ts @@ -3,7 +3,7 @@ import schema from "ponder:schema"; import { encodeLabelhash } from "@ensdomains/ensjs/utils"; import { Block } from "ponder"; import { type Hex, zeroAddress } from "viem"; -import { ensureDomainExists, upsertAccount } from "../lib/db-helpers"; +import { upsertAccount } from "../lib/db-helpers"; import { makeResolverId } from "../lib/ids"; import { ROOT_NODE, makeSubnodeNamehash } from "../lib/subname-helpers"; @@ -33,12 +33,17 @@ 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 ensureDomainExists(context, { - id: ROOT_NODE, - ownerId: zeroAddress, - createdAt: 0n, - isMigrated: false, - }); + await context.db + .insert(schema.domain) + .values({ + id: ROOT_NODE, + ownerId: zeroAddress, + createdAt: 0n, + isMigrated: false, + }) + // make sure to only insert the domain entity into database + // only if it doesn't already exist + .onConflictDoNothing(); } function isDomainEmpty(domain: typeof schema.domain.$inferSelect) { diff --git a/src/lib/db-helpers.ts b/src/lib/db-helpers.ts index bb1fc7a..aa12290 100644 --- a/src/lib/db-helpers.ts +++ b/src/lib/db-helpers.ts @@ -19,22 +19,3 @@ export async function upsertRegistration( ) { return context.db.insert(schema.registration).values(values).onConflictDoUpdate(values); } - -/** - * Ensure that some domain entity value (not necessarily the provided value) for the requested node exists in - * the database. It inserts the provided domain entity if it does not exist. - * Otherwise, just returns the existing domain entity from the db. - * - * @param context ponder context to interact with database - * @param values domain properties, where `id` is the node value - * @returns domain database entity - */ -export async function ensureDomainExists( - context: Context, - values: typeof schema.domain.$inferInsert, -): Promise { - // `onConflictDoUpdate({}}` makes no change to the existing entity - // (the delta is an empty object, which means no updates to apply) - // and it always returns the existing entity - return context.db.insert(schema.domain).values(values).onConflictDoUpdate({}); -} diff --git a/src/plugins/base.eth/handlers/Registrar.ts b/src/plugins/base.eth/handlers/Registrar.ts index 5777e2b..71b387f 100644 --- a/src/plugins/base.eth/handlers/Registrar.ts +++ b/src/plugins/base.eth/handlers/Registrar.ts @@ -1,9 +1,8 @@ -import { Context, ponder } from "ponder:registry"; +import { ponder } from "ponder:registry"; import schema from "ponder:schema"; -import { Block } from "ponder"; -import { Hex, zeroAddress } from "viem"; +import { zeroAddress } from "viem"; import { makeRegistrarHandlers } from "../../../handlers/Registrar"; -import { ensureDomainExists, upsertAccount } from "../../../lib/db-helpers"; +import { upsertAccount } from "../../../lib/db-helpers"; import { makeSubnodeNamehash, tokenIdToLabel } from "../../../lib/subname-helpers"; import { ownedName, pluginNamespace } from "../ponder.config"; @@ -27,9 +26,9 @@ export default function () { // 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, + // in order to avoid prematurely drifting from subgraph equivalancy, we insert the domain entity here, // allowing the base indexer to progress. - await ensureDomainExists(context, { + await context.db.insert(schema.domain).values({ id: makeSubnodeNamehash(ownedSubnameNode, tokenIdToLabel(event.args.id)), ownerId: event.args.owner, createdAt: event.block.timestamp, @@ -52,7 +51,7 @@ export default function () { // token ID has been inserted into the database. This is a workaround to // meet expectations of the `handleNameTransferred` subgraph // implementation. - await ensureDomainExists(context, { + await context.db.insert(schema.domain).values({ id: makeSubnodeNamehash(ownedSubnameNode, tokenIdToLabel(tokenId)), ownerId: to, createdAt: event.block.timestamp, diff --git a/src/plugins/linea.eth/handlers/EthRegistrar.ts b/src/plugins/linea.eth/handlers/EthRegistrar.ts index d4ce1d5..6643003 100644 --- a/src/plugins/linea.eth/handlers/EthRegistrar.ts +++ b/src/plugins/linea.eth/handlers/EthRegistrar.ts @@ -1,7 +1,7 @@ import { ponder } from "ponder:registry"; +import schema from "ponder:schema"; import { zeroAddress } from "viem"; import { makeRegistrarHandlers } from "../../../handlers/Registrar"; -import { ensureDomainExists } from "../../../lib/db-helpers"; import { makeSubnodeNamehash, tokenIdToLabel } from "../../../lib/subname-helpers"; import { ownedName, pluginNamespace } from "../ponder.config"; @@ -29,7 +29,7 @@ export default function () { // token ID has been inserted into the database. This is a workaround to // meet expectations of the `handleNameTransferred` subgraph // implementation. - await ensureDomainExists(context, { + await context.db.insert(schema.domain).values({ id: makeSubnodeNamehash(ownedSubnameNode, tokenIdToLabel(tokenId)), ownerId: to, createdAt: event.block.timestamp, From eb7aa6be880f78410246868f07193779549374b5 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Wed, 15 Jan 2025 17:13:05 +0100 Subject: [PATCH 26/41] chore: trigger rebuild with fresh deployment id From e18f392c9bf35ab16e7c08bea99e569bbf89d631 Mon Sep 17 00:00:00 2001 From: Tomek Kopacki Date: Thu, 16 Jan 2025 16:43:29 +0100 Subject: [PATCH 27/41] fix(readme): update title --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2364136..797d9d4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ens-multichain indexer +# ENSNode > powered by ponder From 7a7a70bb395b40533396df0a2fa8224bf56ad713 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 17 Jan 2025 05:57:54 +0100 Subject: [PATCH 28/41] refactor(helpers): update `rpcRequestRateLimit` config facotry --- src/lib/helpers.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 713fc81..a315e25 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -40,17 +40,19 @@ const DEFAULT_RPC_RATE_LIMIT = 50; * @returns the rate limit in requests per second (rps) */ export const rpcRequestRateLimit = (chainId: number): number => { - if (typeof process.env[`RPC_REQUEST_RATE_LIMIT_${chainId}`] === "string") { - try { - return parseInt(process.env[`RPC_REQUEST_RATE_LIMIT_${chainId}`]!, 10); - } catch (e) { - throw new Error( - `Invalid RPC_REQUEST_RATE_LIMIT_${chainId} value: ${e}. Please provide a valid number.`, - ); - } + const envVarName = `RPC_REQUEST_RATE_LIMIT_${chainId}`; + + if (!process.env[envVarName]) { + return DEFAULT_RPC_RATE_LIMIT; } - return DEFAULT_RPC_RATE_LIMIT; + try { + return parseInt(process.env[envVarName], 10); + } catch (e) { + throw new Error( + `Invalid ${envVarName} value: ${e}. Please provide a valid number.`, + ); + } }; type AnyObject = { [key: string]: any }; From a42be859e06c699561d3731cb01909abd37936c7 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 17 Jan 2025 06:04:19 +0100 Subject: [PATCH 29/41] docs: typos --- .env.local.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.local.example b/.env.local.example index 06934a5..35b27db 100644 --- a/.env.local.example +++ b/.env.local.example @@ -27,7 +27,7 @@ DATABASE_SCHEMA=ens # It should be in the format of `postgresql://:@:/` DATABASE_URL=postgresql://dbuser:abcd1234@localhost:5432/my_database -# Plugins configuration +# Plugin configuration # Identify which indexer plugins to activate (see `src/plugins` for available plugins) # This is a comma separated list of one or more available plugin names. ACTIVE_PLUGINS=eth,base.eth,linea.eth \ No newline at end of file From 03529a34d15be900f1aa6d9f6fd213260b4ffaf4 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 17 Jan 2025 06:05:07 +0100 Subject: [PATCH 30/41] docs(helpers): move the docs around --- .env.local.example | 5 +++++ src/lib/helpers.ts | 29 +++++++++++++---------------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/.env.local.example b/.env.local.example index 35b27db..b23e6ee 100644 --- a/.env.local.example +++ b/.env.local.example @@ -5,6 +5,11 @@ RPC_URL_8453=https://base.drpc.org RPC_URL_59144=https://linea.drpc.org # For the RPC rate limits of each chain, follow the format: # RPC_REQUEST_RATE_LIMIT_{chainId}={rateLimitInRequestsPerSecond} +# The rate limit is the maximum number of requests per second that can be made +# to the RPC endpoint. For public RPC endpoints, it is recommended to set +# a rate limit to low values (i.e. below 30 rps) to avoid being rate limited. +# For private RPC endpoints, the rate limit can be set to higher values, +# depending on the capacity of the endpoint. For example, 500 rps. RPC_REQUEST_RATE_LIMIT_1=50 RPC_REQUEST_RATE_LIMIT_8453=20 RPC_REQUEST_RATE_LIMIT_59144=20 diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index a315e25..fe5866a 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -22,36 +22,33 @@ export const blockConfig = ( const DEFAULT_RPC_RATE_LIMIT = 50; /** - * Reads the RPC request rate limit for a given chain ID from the environment - * variable: RPC_REQUEST_RATE_LIMIT_{chainId}. - * For example, for Ethereum mainnet the chainId is `1`, so the env variable - * can be set as `RPC_REQUEST_RATE_LIMIT_1=400`. This will set the rate limit - * for the mainnet (chainId=1) to 400 requests per second. If the environment - * variable is not set for the requested chain ID, use `DEFAULT_RPC_RATE_LIMIT` - * as the default value. - * - * The rate limit is the maximum number of requests per second that can be made - * to the RPC endpoint. For public RPC endpoints, it is recommended to set - * a rate limit to low values (i.e. below 30 rps) to avoid being rate limited. - * For private RPC endpoints, the rate limit can be set to higher values, - * depending on the capacity of the endpoint. For example, 500 rps. + * Creates the RPC request rate limit for a given chain ID. * * @param chainId the chain ID to read the rate limit for from the environment variable * @returns the rate limit in requests per second (rps) */ export const rpcRequestRateLimit = (chainId: number): number => { + /** + * Reads the RPC request rate limit for a given chain ID from the environment + * variable: RPC_REQUEST_RATE_LIMIT_{chainId}. + * For example, for Ethereum mainnet the chainId is `1`, so the env variable + * can be set as `RPC_REQUEST_RATE_LIMIT_1=400`. This will set the rate limit + * for the mainnet (chainId=1) to 400 requests per second. + */ const envVarName = `RPC_REQUEST_RATE_LIMIT_${chainId}`; + // no rate limit provided in env var if (!process.env[envVarName]) { + // apply default rate limit value return DEFAULT_RPC_RATE_LIMIT; } + // otherwise try { + // parse the rate limit value from the environment variable return parseInt(process.env[envVarName], 10); } catch (e) { - throw new Error( - `Invalid ${envVarName} value: ${e}. Please provide a valid number.`, - ); + throw new Error(`Invalid ${envVarName} value: ${e}. Please provide a valid number.`); } }; From 19394cb2d4d249b3355bd9581d167b40f02ca3eb Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 17 Jan 2025 06:07:07 +0100 Subject: [PATCH 31/41] refactor(ponder.config): renames --- ponder.config.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ponder.config.ts b/ponder.config.ts index bd983c9..41b3645 100644 --- a/ponder.config.ts +++ b/ponder.config.ts @@ -10,11 +10,11 @@ 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"]>; +type AllPluginConfigs = IntersectionOf<(typeof plugins)[number]["config"]>; // Activates the indexing handlers of activated plugins and // returns the intersection of their combined config. -function getActivePluginsConfig(): AllPluginsConfig { +function getActivePluginConfigs(): AllPluginConfigs { const activePlugins = getActivePlugins(plugins); // load indexing handlers from the active plugins into the runtime @@ -22,11 +22,11 @@ function getActivePluginsConfig(): AllPluginsConfig { const config = activePlugins .map((plugin) => plugin.config) - .reduce((acc, val) => deepMergeRecursive(acc, val), {} as AllPluginsConfig); + .reduce((acc, val) => deepMergeRecursive(acc, val), {} as AllPluginConfigs); - return config as AllPluginsConfig; + return config as AllPluginConfigs; } -// The type of the default export is the intersection of all available plugin +// The type of the default export is the intersection of all active plugin configs // configs so that each plugin can be correctly typechecked -export default getActivePluginsConfig(); +export default getActivePluginConfigs(); From b0575f505bcb19f1e3ff0145aaee5f98dc3a9a8b Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 17 Jan 2025 06:15:41 +0100 Subject: [PATCH 32/41] docs(handlers): update registry setup description --- src/handlers/Registry.ts | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/handlers/Registry.ts b/src/handlers/Registry.ts index 3b1a5fe..8a3c04a 100644 --- a/src/handlers/Registry.ts +++ b/src/handlers/Registry.ts @@ -9,24 +9,25 @@ import { ROOT_NODE, makeSubnodeNamehash } from "../lib/subname-helpers"; /** * Initialize the ENS root node with the zeroAddress as the owner. - * Any permutation of plugins might be activated and multiple plugins expect - * the ENS root to exist. This behavior is consistent with the ens-subgraph, - * which initializes the ENS root node in the same way. However, - * the ens-subgraph does not have independent plugins. In our case, we have - * multiple plugins that might be activated independently. Regardless of - * the permutation of active plugins, they all expect the ENS root to exist. + * Any permutation of plugins might be activated (except no plugins activated) + * and multiple plugins expect the ENS root to exist. This behavior is + * consistent with the ens-subgraph, which initializes the ENS root node in + * the same way. However, the ens-subgraph does not have independent plugins. + * In our case, we have multiple plugins that might be activated independently. + * Regardless of the permutation of active plugins, they all expect + * the ENS root to exist. * - * This function has to be used a callback on the Ponder's setup event handler. + * This function should be used as the setup event handler for registry + * (or shadow registry) contracts. * https://ponder.sh/docs/api-reference/indexing-functions#setup-event - * In case there are multiple setup handlers defined, the order of execution - * is guaranteed to be the same, and it is the reverse order of - * the network name configured on a given contract that the setup event was - * registered for. - * - * For example, for setup events registered for contracts on: base, linea, mainnet - * The order of execution will be: mainnet, linea, base. And if the contracts - * were registered on: base, ethereum, linea, the order of execution will be: - * linea, ethereum, base. + * In case there are multiple plugins activated, `setupRootNode` will be + * executed multiple times. The order of execution of `setupRootNode` is + * deterministic based on the reverse order of the network names of the given + * contracts associated with the activated plugins. For example, + * if the network name were: `base`, `linea`, `mainnet`, the order of execution + * will be: `mainnet`, `linea`, `base`. + * And if the network name were: `base`, `ethereum`, `linea`, the order of + * execution will be: `linea`, `ethereum`, `base`. */ export async function setupRootNode({ context }: { context: Context }) { // ensure we have an account for the zeroAddress From 141e57c98cccedc82901435755be588ba48599f2 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 17 Jan 2025 06:22:45 +0100 Subject: [PATCH 33/41] docs: replacements --- src/lib/helpers.ts | 6 +++--- src/plugins/README.md | 2 +- src/plugins/base.eth/ponder.config.ts | 2 +- src/plugins/eth/ponder.config.ts | 2 +- src/plugins/linea.eth/ponder.config.ts | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index fe5866a..5d8dd65 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -18,13 +18,13 @@ export const blockConfig = ( endBlock: end, }); -// default rate limit for request per second to RPC endpoints +// default request per second rate limit for RPC endpoints const DEFAULT_RPC_RATE_LIMIT = 50; /** - * Creates the RPC request rate limit for a given chain ID. + * Gets the RPC request rate limit for a given chain ID. * - * @param chainId the chain ID to read the rate limit for from the environment variable + * @param chainId the chain ID to get the rate limit for * @returns the rate limit in requests per second (rps) */ export const rpcRequestRateLimit = (chainId: number): number => { diff --git a/src/plugins/README.md b/src/plugins/README.md index a5003e5..c21aa33 100644 --- a/src/plugins/README.md +++ b/src/plugins/README.md @@ -1,6 +1,6 @@ # Indexer plugins -This directory contains plugins which define subname-specific processing of blockchain events. +This directory contains plugins that define subname-specific processing of blockchain events. One or more plugins are activated at a time. Use the `ACTIVE_PLUGINS` env variable to select the active plugins, for example: ``` diff --git a/src/plugins/base.eth/ponder.config.ts b/src/plugins/base.eth/ponder.config.ts index fb4eda2..2bf7f04 100644 --- a/src/plugins/base.eth/ponder.config.ts +++ b/src/plugins/base.eth/ponder.config.ts @@ -14,7 +14,7 @@ export const ownedName = "base.eth" as const; export const pluginNamespace = createPluginNamespace(ownedName); -// constrain the ponder indexing between the following start/end blocks +// constrain 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; diff --git a/src/plugins/eth/ponder.config.ts b/src/plugins/eth/ponder.config.ts index de40ae1..5c4a216 100644 --- a/src/plugins/eth/ponder.config.ts +++ b/src/plugins/eth/ponder.config.ts @@ -18,7 +18,7 @@ export const ownedName = "eth"; export const pluginNamespace = createPluginNamespace(ownedName); -// constrain the ponder indexing between the following start/end blocks +// constrain 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; diff --git a/src/plugins/linea.eth/ponder.config.ts b/src/plugins/linea.eth/ponder.config.ts index 25626a8..56972b6 100644 --- a/src/plugins/linea.eth/ponder.config.ts +++ b/src/plugins/linea.eth/ponder.config.ts @@ -14,7 +14,7 @@ export const ownedName = "linea.eth"; export const pluginNamespace = createPluginNamespace(ownedName); -// constrain the ponder indexing between the following start/end blocks +// constrain 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; From 77cc78bc6e767279c4e57e5428e6f8a53b562251 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 17 Jan 2025 06:37:59 +0100 Subject: [PATCH 34/41] feat(helpers): introduce `rpcEndpointUrl` factory --- src/lib/helpers.ts | 29 ++++++++++++++++++++++++++ src/plugins/base.eth/ponder.config.ts | 4 ++-- src/plugins/eth/ponder.config.ts | 4 ++-- src/plugins/linea.eth/ponder.config.ts | 4 ++-- 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 5d8dd65..b37993e 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -18,6 +18,35 @@ export const blockConfig = ( endBlock: end, }); +/** + * Gets the RPC endpoint URL for a given chain ID. + * + * @param chainId the chain ID to get the RPC URL for + * @returns the URL of the RPC endpoint + */ +export const rpcEndpointUrl = (chainId: number): string => { + /** + * Reads the RPC URL for a given chain ID from the environment variable: + * RPC_URL_{chainId}. For example, for Ethereum mainnet the chainId is `1`, + * so the env variable can be set as `RPC_URL_1=https://eth.drpc.org`. + */ + const envVarName = `RPC_URL_${chainId}`; + + // no RPC URL provided in env var + if (!process.env[envVarName]) { + // throw an error, as the RPC URL is required and no defaults apply + throw new Error(`Missing '${envVarName}' environment variable`); + } + + try { + return new URL(process.env[envVarName] as string).toString(); + } catch (e) { + throw new Error( + `Invalid '${envVarName}' environment variable. Please provide a valid URL.`, + ); + } +}; + // default request per second rate limit for RPC endpoints const DEFAULT_RPC_RATE_LIMIT = 50; diff --git a/src/plugins/base.eth/ponder.config.ts b/src/plugins/base.eth/ponder.config.ts index 2bf7f04..338f782 100644 --- a/src/plugins/base.eth/ponder.config.ts +++ b/src/plugins/base.eth/ponder.config.ts @@ -2,7 +2,7 @@ import { type ContractConfig, createConfig, factory } from "ponder"; import { http, getAbiItem } from "viem"; import { base } from "viem/chains"; -import { blockConfig, rpcRequestRateLimit } from "../../lib/helpers"; +import { blockConfig, rpcEndpointUrl, rpcRequestRateLimit } from "../../lib/helpers"; import { createPluginNamespace } from "../../lib/plugin-helpers"; import { BaseRegistrar } from "./abis/BaseRegistrar"; import { EarlyAccessRegistrarController } from "./abis/EARegistrarController"; @@ -23,7 +23,7 @@ export const config = createConfig({ networks: { base: { chainId: base.id, - transport: http(process.env[`RPC_URL_${base.id}`]), + transport: http(rpcEndpointUrl(base.id)), maxRequestsPerSecond: rpcRequestRateLimit(base.id), }, }, diff --git a/src/plugins/eth/ponder.config.ts b/src/plugins/eth/ponder.config.ts index 5c4a216..8ecc94b 100644 --- a/src/plugins/eth/ponder.config.ts +++ b/src/plugins/eth/ponder.config.ts @@ -2,7 +2,7 @@ import { ContractConfig, createConfig, factory, mergeAbis } from "ponder"; import { http, getAbiItem } from "viem"; import { mainnet } from "viem/chains"; -import { blockConfig, rpcRequestRateLimit } from "../../lib/helpers"; +import { blockConfig, rpcEndpointUrl, rpcRequestRateLimit } from "../../lib/helpers"; import { createPluginNamespace } from "../../lib/plugin-helpers"; import { BaseRegistrar } from "./abis/BaseRegistrar"; import { EthRegistrarController } from "./abis/EthRegistrarController"; @@ -27,7 +27,7 @@ export const config = createConfig({ networks: { mainnet: { chainId: mainnet.id, - transport: http(process.env[`RPC_URL_${mainnet.id}`]), + transport: http(rpcEndpointUrl(mainnet.id)), maxRequestsPerSecond: rpcRequestRateLimit(mainnet.id), }, }, diff --git a/src/plugins/linea.eth/ponder.config.ts b/src/plugins/linea.eth/ponder.config.ts index 56972b6..0b13359 100644 --- a/src/plugins/linea.eth/ponder.config.ts +++ b/src/plugins/linea.eth/ponder.config.ts @@ -2,7 +2,7 @@ import { ContractConfig, createConfig, factory, mergeAbis } from "ponder"; import { http, getAbiItem } from "viem"; import { linea } from "viem/chains"; -import { blockConfig, rpcRequestRateLimit } from "../../lib/helpers"; +import { blockConfig, rpcEndpointUrl, rpcRequestRateLimit } from "../../lib/helpers"; import { createPluginNamespace } from "../../lib/plugin-helpers"; import { BaseRegistrar } from "./abis/BaseRegistrar"; import { EthRegistrarController } from "./abis/EthRegistrarController"; @@ -23,7 +23,7 @@ export const config = createConfig({ networks: { linea: { chainId: linea.id, - transport: http(process.env[`RPC_URL_${linea.id}`]), + transport: http(rpcEndpointUrl(linea.id)), maxRequestsPerSecond: rpcRequestRateLimit(linea.id), }, }, From f35a828a83d90902328e0a4f428e3ba7c455d3d7 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 17 Jan 2025 06:40:58 +0100 Subject: [PATCH 35/41] fix(codestyle): apply auto-formatting --- src/lib/helpers.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index b37993e..0e38776 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -41,9 +41,7 @@ export const rpcEndpointUrl = (chainId: number): string => { try { return new URL(process.env[envVarName] as string).toString(); } catch (e) { - throw new Error( - `Invalid '${envVarName}' environment variable. Please provide a valid URL.`, - ); + throw new Error(`Invalid '${envVarName}' environment variable. Please provide a valid URL.`); } }; From 7d060beedac487bf424f2b7efe582d1035d86e07 Mon Sep 17 00:00:00 2001 From: Tomek Kopacki Date: Fri, 17 Jan 2025 15:27:49 +0100 Subject: [PATCH 36/41] docs: update descriptions Co-authored-by: lightwalker.eth <126201998+lightwalker-eth@users.noreply.github.com> --- src/handlers/Registry.ts | 6 +++--- src/lib/helpers.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/handlers/Registry.ts b/src/handlers/Registry.ts index 8a3c04a..5195ff3 100644 --- a/src/handlers/Registry.ts +++ b/src/handlers/Registry.ts @@ -24,9 +24,9 @@ import { ROOT_NODE, makeSubnodeNamehash } from "../lib/subname-helpers"; * executed multiple times. The order of execution of `setupRootNode` is * deterministic based on the reverse order of the network names of the given * contracts associated with the activated plugins. For example, - * if the network name were: `base`, `linea`, `mainnet`, the order of execution + * if the network names were: `base`, `linea`, `mainnet`, the order of execution * will be: `mainnet`, `linea`, `base`. - * And if the network name were: `base`, `ethereum`, `linea`, the order of + * And if the network names were: `base`, `ethereum`, `linea`, the order of * execution will be: `linea`, `ethereum`, `base`. */ export async function setupRootNode({ context }: { context: Context }) { @@ -42,7 +42,7 @@ export async function setupRootNode({ context }: { context: Context }) { createdAt: 0n, isMigrated: false, }) - // make sure to only insert the domain entity into database + // only insert the domain entity into the database if it doesn't already exist // only if it doesn't already exist .onConflictDoNothing(); } diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 0e38776..9bb363e 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -35,13 +35,13 @@ export const rpcEndpointUrl = (chainId: number): string => { // no RPC URL provided in env var if (!process.env[envVarName]) { // throw an error, as the RPC URL is required and no defaults apply - throw new Error(`Missing '${envVarName}' environment variable`); + throw new Error(`Missing '${envVarName}' environment variable. The RPC URL for chainId ${chainId} is required.`); } try { return new URL(process.env[envVarName] as string).toString(); } catch (e) { - throw new Error(`Invalid '${envVarName}' environment variable. Please provide a valid URL.`); + throw new Error(`Invalid '${envVarName}' environment variable value: '${process.env[envVarName]}'. Please provide a valid RPC URL for chainId ${chainId}.`); } }; From b3f51460111515740f7c64540cbe7e229bb1672e Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 17 Jan 2025 15:41:41 +0100 Subject: [PATCH 37/41] fix(helpers): update env var parsing logic --- src/lib/helpers.ts | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 9bb363e..24fd168 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -31,17 +31,22 @@ export const rpcEndpointUrl = (chainId: number): string => { * so the env variable can be set as `RPC_URL_1=https://eth.drpc.org`. */ const envVarName = `RPC_URL_${chainId}`; + const envVarValue = process.env[envVarName]; // no RPC URL provided in env var - if (!process.env[envVarName]) { + if (!envVarValue) { // throw an error, as the RPC URL is required and no defaults apply - throw new Error(`Missing '${envVarName}' environment variable. The RPC URL for chainId ${chainId} is required.`); + throw new Error( + `Missing '${envVarName}' environment variable. The RPC URL for chainId ${chainId} is required.`, + ); } try { - return new URL(process.env[envVarName] as string).toString(); + return new URL(envVarValue).toString(); } catch (e) { - throw new Error(`Invalid '${envVarName}' environment variable value: '${process.env[envVarName]}'. Please provide a valid RPC URL for chainId ${chainId}.`); + throw new Error( + `Invalid '${envVarName}' environment variable value: '${envVarValue}'. Please provide a valid RPC URL.`, + ); } }; @@ -63,9 +68,10 @@ export const rpcRequestRateLimit = (chainId: number): number => { * for the mainnet (chainId=1) to 400 requests per second. */ const envVarName = `RPC_REQUEST_RATE_LIMIT_${chainId}`; + const envVarValue = process.env[envVarName]; // no rate limit provided in env var - if (!process.env[envVarName]) { + if (!envVarValue) { // apply default rate limit value return DEFAULT_RPC_RATE_LIMIT; } @@ -73,9 +79,19 @@ export const rpcRequestRateLimit = (chainId: number): number => { // otherwise try { // parse the rate limit value from the environment variable - return parseInt(process.env[envVarName], 10); + const rpcRequestRateLimit = parseInt(envVarValue, 10); + + if (Number.isNaN(rpcRequestRateLimit)) { + throw new Error(`Could not parse rate limit value '${rpcRequestRateLimit}'`); + } + + return rpcRequestRateLimit; } catch (e) { - throw new Error(`Invalid ${envVarName} value: ${e}. Please provide a valid number.`); + console.log(e); + + throw new Error( + `Invalid '${envVarName}' environment variable value: '${envVarValue}'. Please provide a valid RPC RATE LIMIT integer.`, + ); } }; From 4b586ebf80a7dad3509f4eb439936c1936b03699 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 17 Jan 2025 16:57:52 +0100 Subject: [PATCH 38/41] docs: being more specific and correct --- .env.local.example | 2 +- ponder.config.ts | 16 ++++----- src/handlers/Registry.ts | 1 - src/lib/helpers.ts | 18 +++++----- src/lib/plugin-helpers.ts | 34 ++++++++++++------- src/plugins/base.eth/handlers/Registrar.ts | 20 +++++------ src/plugins/base.eth/ponder.config.ts | 4 +-- src/plugins/eth/ponder.config.ts | 4 +-- .../linea.eth/handlers/EthRegistrar.ts | 12 +++---- src/plugins/linea.eth/ponder.config.ts | 4 +-- 10 files changed, 60 insertions(+), 55 deletions(-) diff --git a/.env.local.example b/.env.local.example index b23e6ee..5a0577f 100644 --- a/.env.local.example +++ b/.env.local.example @@ -34,5 +34,5 @@ DATABASE_URL=postgresql://dbuser:abcd1234@localhost:5432/my_database # Plugin configuration # Identify which indexer plugins to activate (see `src/plugins` for available plugins) -# This is a comma separated list of one or more available plugin names. +# This is a comma separated list of one or more available plugin names (case-sensitive). ACTIVE_PLUGINS=eth,base.eth,linea.eth \ No newline at end of file diff --git a/ponder.config.ts b/ponder.config.ts index 41b3645..4808277 100644 --- a/ponder.config.ts +++ b/ponder.config.ts @@ -5,28 +5,28 @@ 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; +// any available plugin can be activated at runtime +const availablePlugins = [baseEthPlugin, ethPlugin, lineaEthPlugin] as const; // intersection of all available plugin configs to support correct typechecking // of the indexing handlers -type AllPluginConfigs = IntersectionOf<(typeof plugins)[number]["config"]>; +type AllPluginConfigs = IntersectionOf<(typeof availablePlugins)[number]["config"]>; // Activates the indexing handlers of activated plugins and // returns the intersection of their combined config. -function getActivePluginConfigs(): AllPluginConfigs { - const activePlugins = getActivePlugins(plugins); +function activatePluginsAndGetConfig(): AllPluginConfigs { + const activePlugins = getActivePlugins(availablePlugins); // load indexing handlers from the active plugins into the runtime activePlugins.forEach((plugin) => plugin.activate()); - const config = activePlugins + const activePluginsConfig = activePlugins .map((plugin) => plugin.config) .reduce((acc, val) => deepMergeRecursive(acc, val), {} as AllPluginConfigs); - return config as AllPluginConfigs; + return activePluginsConfig as AllPluginConfigs; } // The type of the default export is the intersection of all active plugin configs // configs so that each plugin can be correctly typechecked -export default getActivePluginConfigs(); +export default activatePluginsAndGetConfig(); diff --git a/src/handlers/Registry.ts b/src/handlers/Registry.ts index 5195ff3..c1dc146 100644 --- a/src/handlers/Registry.ts +++ b/src/handlers/Registry.ts @@ -43,7 +43,6 @@ export async function setupRootNode({ context }: { context: Context }) { isMigrated: false, }) // only insert the domain entity into the database if it doesn't already exist - // only if it doesn't already exist .onConflictDoNothing(); } diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 24fd168..71bf25e 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -59,7 +59,7 @@ const DEFAULT_RPC_RATE_LIMIT = 50; * @param chainId the chain ID to get the rate limit for * @returns the rate limit in requests per second (rps) */ -export const rpcRequestRateLimit = (chainId: number): number => { +export const rpcMaxRequestsPerSecond = (chainId: number): number => { /** * Reads the RPC request rate limit for a given chain ID from the environment * variable: RPC_REQUEST_RATE_LIMIT_{chainId}. @@ -79,18 +79,16 @@ export const rpcRequestRateLimit = (chainId: number): number => { // otherwise try { // parse the rate limit value from the environment variable - const rpcRequestRateLimit = parseInt(envVarValue, 10); - - if (Number.isNaN(rpcRequestRateLimit)) { - throw new Error(`Could not parse rate limit value '${rpcRequestRateLimit}'`); + const parsedEnvVarValue = parseInt(envVarValue, 10); + + if (Number.isNaN(parsedEnvVarValue) || parsedEnvVarValue <= 0) { + throw new Error(`Rate limit value must be an integer greater than 0.`); } - return rpcRequestRateLimit; - } catch (e) { - console.log(e); - + return parsedEnvVarValue; + } catch (e: any) { throw new Error( - `Invalid '${envVarName}' environment variable value: '${envVarValue}'. Please provide a valid RPC RATE LIMIT integer.`, + `Invalid '${envVarName}' environment variable value: '${envVarValue}'. ${e.message}`, ); } }; diff --git a/src/lib/plugin-helpers.ts b/src/lib/plugin-helpers.ts index 1e873f3..81dbc84 100644 --- a/src/lib/plugin-helpers.ts +++ b/src/lib/plugin-helpers.ts @@ -79,28 +79,29 @@ type PluginNamespacePath = | `/${string}` | `/${string}${T}`; -/** @var comma separated list of the requested active plugin names (see `src/plugins` for available plugins) */ -export const ACTIVE_PLUGINS = process.env.ACTIVE_PLUGINS; - /** - * Returns the list of 1 or more active plugins based on the `ACTIVE_PLUGINS` environment variable. + * Returns a list of 1 or more distinct active plugins based on the `ACTIVE_PLUGINS` environment variable. * * 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 + * @param availablePlugins is a list of available plugins * @returns the active plugins */ -export function getActivePlugins(plugins: readonly T[]): T[] { - const pluginsToActivateByOwnedName = ACTIVE_PLUGINS ? ACTIVE_PLUGINS.split(",") : []; +export function getActivePlugins( + availablePlugins: readonly T[], +): T[] { + /** @var comma separated list of the requested plugin names (see `src/plugins` for available plugins) */ + const requestedPluginsEnvVar = process.env.ACTIVE_PLUGINS; + const requestedPlugins = requestedPluginsEnvVar ? requestedPluginsEnvVar.split(",") : []; - if (!pluginsToActivateByOwnedName.length) { - throw new Error("No active plugins found. Please set the ACTIVE_PLUGINS environment variable."); + if (!requestedPlugins.length) { + throw new Error("Set the ACTIVE_PLUGINS environment variable to activate one or more plugins."); } // Check if the requested plugins are valid and can become active - const invalidPlugins = pluginsToActivateByOwnedName.filter( - (plugin) => !plugins.some((p) => p.ownedName === plugin), + const invalidPlugins = requestedPlugins.filter( + (plugin) => !availablePlugins.some((p) => p.ownedName === plugin), ); if (invalidPlugins.length) { @@ -112,8 +113,15 @@ export function getActivePlugins(plugins: reado ); } - // Return the active plugins - return plugins.filter((plugin) => pluginsToActivateByOwnedName.includes(plugin.ownedName)); + const uniquePluginsToActivate = availablePlugins.reduce((acc, plugin) => { + // Only add the plugin if it's not already in the map + if (acc.has(plugin.ownedName) === false) { + acc.set(plugin.ownedName, plugin); + } + return acc; + }, new Map()); + + return Array.from(uniquePluginsToActivate.values()); } // Helper type to get the intersection of all config types diff --git a/src/plugins/base.eth/handlers/Registrar.ts b/src/plugins/base.eth/handlers/Registrar.ts index 71b387f..c26f7cc 100644 --- a/src/plugins/base.eth/handlers/Registrar.ts +++ b/src/plugins/base.eth/handlers/Registrar.ts @@ -23,10 +23,10 @@ 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 insert the domain entity here, + // Base has 'preminted' names via Registrar#registerOnly, which explicitly + // does not update the Registry. This breaks a subgraph assumption, as it + // expects a domain to exist (via Registry:NewOwner) before any + // Registrar:NameRegistered events. We insert the domain entity here, // allowing the base indexer to progress. await context.db.insert(schema.domain).values({ id: makeSubnodeNamehash(ownedSubnameNode, tokenIdToLabel(event.args.id)), @@ -45,12 +45,12 @@ export default function () { 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 entity in the database yet. The very first - // transfer event has to ensure the domain entity for the requested - // token ID has been inserted into the database. This is a workaround to - // meet expectations of the `handleNameTransferred` subgraph - // implementation. + // assumes an indexed record for the domain already exists. However, + // when an NFT token is minted (transferred from `0x0` address), + // there's no domain entity in the database yet. That very first transfer + // event has to ensure the domain entity for the requested token ID + // has been inserted into the database. This is a workaround to meet + // expectations of the `handleNameTransferred` subgraph implementation. await context.db.insert(schema.domain).values({ id: makeSubnodeNamehash(ownedSubnameNode, tokenIdToLabel(tokenId)), ownerId: to, diff --git a/src/plugins/base.eth/ponder.config.ts b/src/plugins/base.eth/ponder.config.ts index 338f782..aadd96e 100644 --- a/src/plugins/base.eth/ponder.config.ts +++ b/src/plugins/base.eth/ponder.config.ts @@ -2,7 +2,7 @@ import { type ContractConfig, createConfig, factory } from "ponder"; import { http, getAbiItem } from "viem"; import { base } from "viem/chains"; -import { blockConfig, rpcEndpointUrl, rpcRequestRateLimit } from "../../lib/helpers"; +import { blockConfig, rpcEndpointUrl, rpcMaxRequestsPerSecond } from "../../lib/helpers"; import { createPluginNamespace } from "../../lib/plugin-helpers"; import { BaseRegistrar } from "./abis/BaseRegistrar"; import { EarlyAccessRegistrarController } from "./abis/EARegistrarController"; @@ -24,7 +24,7 @@ export const config = createConfig({ base: { chainId: base.id, transport: http(rpcEndpointUrl(base.id)), - maxRequestsPerSecond: rpcRequestRateLimit(base.id), + maxRequestsPerSecond: rpcMaxRequestsPerSecond(base.id), }, }, contracts: { diff --git a/src/plugins/eth/ponder.config.ts b/src/plugins/eth/ponder.config.ts index 8ecc94b..98134ce 100644 --- a/src/plugins/eth/ponder.config.ts +++ b/src/plugins/eth/ponder.config.ts @@ -2,7 +2,7 @@ import { ContractConfig, createConfig, factory, mergeAbis } from "ponder"; import { http, getAbiItem } from "viem"; import { mainnet } from "viem/chains"; -import { blockConfig, rpcEndpointUrl, rpcRequestRateLimit } from "../../lib/helpers"; +import { blockConfig, rpcEndpointUrl, rpcMaxRequestsPerSecond } from "../../lib/helpers"; import { createPluginNamespace } from "../../lib/plugin-helpers"; import { BaseRegistrar } from "./abis/BaseRegistrar"; import { EthRegistrarController } from "./abis/EthRegistrarController"; @@ -28,7 +28,7 @@ export const config = createConfig({ mainnet: { chainId: mainnet.id, transport: http(rpcEndpointUrl(mainnet.id)), - maxRequestsPerSecond: rpcRequestRateLimit(mainnet.id), + maxRequestsPerSecond: rpcMaxRequestsPerSecond(mainnet.id), }, }, contracts: { diff --git a/src/plugins/linea.eth/handlers/EthRegistrar.ts b/src/plugins/linea.eth/handlers/EthRegistrar.ts index 6643003..ff17d05 100644 --- a/src/plugins/linea.eth/handlers/EthRegistrar.ts +++ b/src/plugins/linea.eth/handlers/EthRegistrar.ts @@ -23,12 +23,12 @@ export default function () { 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 entity in the database yet. The very first - // transfer event has to ensure the domain entity for the requested - // token ID has been inserted into the database. This is a workaround to - // meet expectations of the `handleNameTransferred` subgraph - // implementation. + // assumes an indexed record for the domain already exists. However, + // when an NFT token is minted (transferred from `0x0` address), + // there's no domain entity in the database yet. That very first transfer + // event has to ensure the domain entity for the requested token ID + // has been inserted into the database. This is a workaround to meet + // expectations of the `handleNameTransferred` subgraph implementation. await context.db.insert(schema.domain).values({ id: makeSubnodeNamehash(ownedSubnameNode, tokenIdToLabel(tokenId)), ownerId: to, diff --git a/src/plugins/linea.eth/ponder.config.ts b/src/plugins/linea.eth/ponder.config.ts index 0b13359..17fdb41 100644 --- a/src/plugins/linea.eth/ponder.config.ts +++ b/src/plugins/linea.eth/ponder.config.ts @@ -2,7 +2,7 @@ import { ContractConfig, createConfig, factory, mergeAbis } from "ponder"; import { http, getAbiItem } from "viem"; import { linea } from "viem/chains"; -import { blockConfig, rpcEndpointUrl, rpcRequestRateLimit } from "../../lib/helpers"; +import { blockConfig, rpcEndpointUrl, rpcMaxRequestsPerSecond } from "../../lib/helpers"; import { createPluginNamespace } from "../../lib/plugin-helpers"; import { BaseRegistrar } from "./abis/BaseRegistrar"; import { EthRegistrarController } from "./abis/EthRegistrarController"; @@ -24,7 +24,7 @@ export const config = createConfig({ linea: { chainId: linea.id, transport: http(rpcEndpointUrl(linea.id)), - maxRequestsPerSecond: rpcRequestRateLimit(linea.id), + maxRequestsPerSecond: rpcMaxRequestsPerSecond(linea.id), }, }, contracts: { From a7099a492862dc187daa69c176e13bb226ac56a4 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 17 Jan 2025 17:36:35 +0100 Subject: [PATCH 39/41] fix(plugin-helpers): rename types --- ponder.config.ts | 6 +++--- src/lib/plugin-helpers.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ponder.config.ts b/ponder.config.ts index 4808277..6a5ad6c 100644 --- a/ponder.config.ts +++ b/ponder.config.ts @@ -1,5 +1,5 @@ import { deepMergeRecursive } from "./src/lib/helpers"; -import { type IntersectionOf, getActivePlugins } from "./src/lib/plugin-helpers"; +import { type MergedTypes, 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"; @@ -8,9 +8,9 @@ import * as lineaEthPlugin from "./src/plugins/linea.eth/ponder.config"; // any available plugin can be activated at runtime const availablePlugins = [baseEthPlugin, ethPlugin, lineaEthPlugin] as const; -// intersection of all available plugin configs to support correct typechecking +// merge of all available plugin configs to support correct typechecking // of the indexing handlers -type AllPluginConfigs = IntersectionOf<(typeof availablePlugins)[number]["config"]>; +type AllPluginConfigs = MergedTypes<(typeof availablePlugins)[number]["config"]>; // Activates the indexing handlers of activated plugins and // returns the intersection of their combined config. diff --git a/src/lib/plugin-helpers.ts b/src/lib/plugin-helpers.ts index 81dbc84..41c387e 100644 --- a/src/lib/plugin-helpers.ts +++ b/src/lib/plugin-helpers.ts @@ -124,8 +124,8 @@ export function getActivePlugins( return Array.from(uniquePluginsToActivate.values()); } -// Helper type to get the intersection of all config types -export type IntersectionOf = (T extends any ? (x: T) => void : never) extends ( +// Helper type to merge multiple types into one +export type MergedTypes = (T extends any ? (x: T) => void : never) extends ( x: infer R, ) => void ? R From b7704f43e4c4aecb7c60164983df1c1471cee2e6 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 17 Jan 2025 18:26:18 +0100 Subject: [PATCH 40/41] fix: apply pr feedback --- ponder.config.ts | 7 ++++--- src/lib/plugin-helpers.ts | 7 +++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ponder.config.ts b/ponder.config.ts index 6a5ad6c..8a96830 100644 --- a/ponder.config.ts +++ b/ponder.config.ts @@ -12,8 +12,9 @@ const availablePlugins = [baseEthPlugin, ethPlugin, lineaEthPlugin] as const; // of the indexing handlers type AllPluginConfigs = MergedTypes<(typeof availablePlugins)[number]["config"]>; -// Activates the indexing handlers of activated plugins and -// returns the intersection of their combined config. +// Activates the indexing handlers of activated plugins. +// Statically typed as the merge of all available plugin configs. However at +// runtime returns the merge of all activated plugin configs. function activatePluginsAndGetConfig(): AllPluginConfigs { const activePlugins = getActivePlugins(availablePlugins); @@ -27,6 +28,6 @@ function activatePluginsAndGetConfig(): AllPluginConfigs { return activePluginsConfig as AllPluginConfigs; } -// The type of the default export is the intersection of all active plugin configs +// 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 activatePluginsAndGetConfig(); diff --git a/src/lib/plugin-helpers.ts b/src/lib/plugin-helpers.ts index 41c387e..36063a7 100644 --- a/src/lib/plugin-helpers.ts +++ b/src/lib/plugin-helpers.ts @@ -101,7 +101,8 @@ export function getActivePlugins( // Check if the requested plugins are valid and can become active const invalidPlugins = requestedPlugins.filter( - (plugin) => !availablePlugins.some((p) => p.ownedName === plugin), + (requestedPlugin) => + !availablePlugins.some((availablePlugin) => availablePlugin.ownedName === requestedPlugin), ); if (invalidPlugins.length) { @@ -125,8 +126,6 @@ export function getActivePlugins( } // Helper type to merge multiple types into one -export type MergedTypes = (T extends any ? (x: T) => void : never) extends ( - x: infer R, -) => void +export type MergedTypes = (T extends any ? (x: T) => void : never) extends (x: infer R) => void ? R : never; From ba17d36c165386e7664d9713072a9393c35fc75f Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 17 Jan 2025 18:48:52 +0100 Subject: [PATCH 41/41] docs: update env example --- .env.local.example | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.env.local.example b/.env.local.example index 5a0577f..bb97466 100644 --- a/.env.local.example +++ b/.env.local.example @@ -10,6 +10,8 @@ RPC_URL_59144=https://linea.drpc.org # a rate limit to low values (i.e. below 30 rps) to avoid being rate limited. # For private RPC endpoints, the rate limit can be set to higher values, # depending on the capacity of the endpoint. For example, 500 rps. +# If no rate limit is set for a given chainId, the DEFAULT_RPC_RATE_LIMIT value +# will be applied. RPC_REQUEST_RATE_LIMIT_1=50 RPC_REQUEST_RATE_LIMIT_8453=20 RPC_REQUEST_RATE_LIMIT_59144=20