Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cross-chain-indexing): enable multiple concurrent plugins #24

Merged
merged 42 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
618e8d3
feat(cross-subname-indexing): allow activating multiple plugins together
tk-o Jan 12, 2025
7d8f978
feat(cross-subname-indexing): demonstrate retrieving cross-subname index
tk-o Jan 12, 2025
76754e4
refactor(handlers): stremline logic
tk-o Jan 12, 2025
0ffa78c
refactor(ponder.config): simplify structure
tk-o Jan 12, 2025
b118dfb
fix: apply pr feedback
tk-o Jan 13, 2025
c71479f
fix: typos
tk-o Jan 13, 2025
ceb03cc
fix: apply pr feedback
tk-o Jan 13, 2025
fa57a34
feat(plugins): introduce ACTIVE_PLUGINS validation
tk-o Jan 13, 2025
3b2c07b
Merge remote-tracking branch 'origin/main' into feat/cross-chain-inde…
tk-o Jan 13, 2025
748dfa4
fix(schema): use proper schema import
tk-o Jan 13, 2025
5365dd6
feat(utils): import deep merge npm lib
tk-o Jan 13, 2025
43b1dc7
fix(codestyle): apply formatting
tk-o Jan 13, 2025
ac141f5
docs: apply text edits
tk-o Jan 14, 2025
46ab32d
feat(database-entites): ensure domain entity exists
tk-o Jan 14, 2025
273cd63
fix(upserts): make `ensureDomainExists` to always return a DB entity`…
tk-o Jan 14, 2025
5e1b695
docs: update description of `ensureDomainExists`
tk-o Jan 14, 2025
f63ba6a
fix(defaults): make all available plugins active
tk-o Jan 14, 2025
8d2bb1b
feat(deps): update ponder
tk-o Jan 14, 2025
c478ad5
docs(handlers): update Registry descriptions
tk-o Jan 14, 2025
6e19c7d
fix(handlers-base.eth): remove dead code
tk-o Jan 14, 2025
38e9c15
feat(rpc): define env vars with rate limits
tk-o Jan 14, 2025
fd2d567
docs: update descriptions as suggested by PR feedback
tk-o Jan 14, 2025
6da8beb
docs: update .env.local.example
tk-o Jan 14, 2025
b6e75af
docs(helpers): define default values as named consts
tk-o Jan 14, 2025
7334d9d
refactor(db-helpers): rename `upserts into db-helpers
tk-o Jan 14, 2025
dc94a32
refactor(db-helpers): drop `ensureDomainExists`
tk-o Jan 14, 2025
eb7aa6b
chore: trigger rebuild with fresh deployment id
tk-o Jan 15, 2025
e18f392
fix(readme): update title
tk-o Jan 16, 2025
7a7a70b
refactor(helpers): update `rpcRequestRateLimit` config facotry
tk-o Jan 17, 2025
a42be85
docs: typos
tk-o Jan 17, 2025
03529a3
docs(helpers): move the docs around
tk-o Jan 17, 2025
19394cb
refactor(ponder.config): renames
tk-o Jan 17, 2025
b0575f5
docs(handlers): update registry setup description
tk-o Jan 17, 2025
141e57c
docs: replacements
tk-o Jan 17, 2025
77cc78b
feat(helpers): introduce `rpcEndpointUrl` factory
tk-o Jan 17, 2025
f35a828
fix(codestyle): apply auto-formatting
tk-o Jan 17, 2025
7d060be
docs: update descriptions
tk-o Jan 17, 2025
b3f5146
fix(helpers): update env var parsing logic
tk-o Jan 17, 2025
4b586eb
docs: being more specific and correct
tk-o Jan 17, 2025
a7099a4
fix(plugin-helpers): rename types
tk-o Jan 17, 2025
b7704f4
fix: apply pr feedback
tk-o Jan 17, 2025
ba17d36
docs: update env example
tk-o Jan 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
#
Expand Down
6 changes: 3 additions & 3 deletions ponder.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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();
19 changes: 8 additions & 11 deletions src/handlers/Registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 3 additions & 5 deletions src/lib/plugin-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,11 @@ type PluginNamespacePath<T extends 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.
Expand All @@ -92,9 +92,7 @@ export const ACTIVE_PLUGINS = process.env.ACTIVE_PLUGINS;
* @returns the active plugins
*/
export function getActivePlugins<T extends { ownedName: string }>(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.");
Expand Down
22 changes: 22 additions & 0 deletions src/lib/upserts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof schema.domain.$inferSelect> {
const domainEntity = await context.db.insert(schema.domain).values(values).onConflictDoNothing();

if (!domainEntity) {
throw new Error("domain expected");
}

return domainEntity;
}
6 changes: 3 additions & 3 deletions src/plugins/README.md
Original file line number Diff line number Diff line change
@@ -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
```
32 changes: 18 additions & 14 deletions src/plugins/base.eth/handlers/Registrar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -56,35 +56,39 @@ 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 });
});
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 },
});
});

Expand Down
52 changes: 12 additions & 40 deletions src/plugins/linea.eth/handlers/EthRegistrar.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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);
Expand All @@ -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,
});
}

Expand Down
Loading