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 all 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
51 changes: 36 additions & 15 deletions .env.local.example
Original file line number Diff line number Diff line change
@@ -1,19 +1,40 @@
# RPC configuration
# 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

# Identify which indexer plugin to activate (see `src/plugins` for available plugins)

ACTIVE_PLUGIN=base.eth
# For the RPC URL of each chain, follow the format: RPC_URL_{chainId}={rpcUrl}
RPC_URL_1=https://eth.drpc.org
RPC_URL_8453=https://base.drpc.org
RPC_URL_59144=https://linea.drpc.org
tk-o marked this conversation as resolved.
Show resolved Hide resolved
# 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.
# 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

# 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 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.
#
# 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://<username>:<password>@<host>:<port>/<database>`
tk-o marked this conversation as resolved.
Show resolved Hide resolved
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 (case-sensitive).
ACTIVE_PLUGINS=eth,base.eth,linea.eth
lightwalker-eth marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# ens-multichain indexer
# ENSNode

> powered by ponder

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"dependencies": {
"@ensdomains/ensjs": "^4.0.2",
"hono": "^4.6.14",
"ponder": "^0.8.17",
"ponder": "^0.8.26",
"ts-deepmerge": "^7.0.2",
"viem": "^2.21.57"
},
"devDependencies": {
Expand Down
19 changes: 14 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 24 additions & 20 deletions ponder.config.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
import { ACTIVE_PLUGIN } from "./src/lib/plugin-helpers";
import { deepMergeRecursive } from "./src/lib/helpers";
import { type MergedTypes, getActivePlugins } from "./src/lib/plugin-helpers";
tk-o marked this conversation as resolved.
Show resolved Hide resolved
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;
// list of all available plugins
// any available plugin can be activated at runtime
const availablePlugins = [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);
// merge of all available plugin configs to support correct typechecking
// of the indexing handlers
type AllPluginConfigs = MergedTypes<(typeof availablePlugins)[number]["config"]>;

if (!pluginToActivate) {
throw new Error(`Unsupported ACTIVE_PLUGIN: ${ACTIVE_PLUGIN}`);
}
// 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);

pluginToActivate.activate();
// load indexing handlers from the active plugins into the runtime
activePlugins.forEach((plugin) => plugin.activate());

return pluginToActivate.config as AllConfigs;
})();
const activePluginsConfig = activePlugins
.map((plugin) => plugin.config)
.reduce((acc, val) => deepMergeRecursive(acc, val), {} as AllPluginConfigs);

// Helper type to get the intersection of all config types
type IntersectionOf<T> = (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"]>;
return activePluginsConfig as AllPluginConfigs;
}

// 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();
2 changes: 1 addition & 1 deletion src/handlers/NameWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
10 changes: 5 additions & 5 deletions src/handlers/Registrar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -23,7 +23,7 @@ export const makeRegistrarHandlers = (ownedName: `${string}eth`) => {
if (domain.labelName !== name) {
await context.db
.update(schema.domain, { id: node })
.set({ labelName: name, name: `${name}${ownedName}` });
.set({ labelName: name, name: `${name}.${ownedName}` });
tk-o marked this conversation as resolved.
Show resolved Hide resolved
}

await context.db.update(schema.registration, { id: label }).set({ labelName: name, cost });
Expand Down Expand Up @@ -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({
Expand All @@ -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({
Expand Down Expand Up @@ -117,7 +117,7 @@ export const makeRegistrarHandlers = (ownedName: `${string}eth`) => {

async handleNameTransferred({
context,
args: { tokenId, from, to },
args: { tokenId, to },
}: {
context: Context;
args: {
Expand Down
37 changes: 30 additions & 7 deletions src/handlers/Registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,47 @@ import schema from "ponder:schema";
import { encodeLabelhash } from "@ensdomains/ensjs/utils";
import { Block } from "ponder";
import { type Hex, zeroAddress } from "viem";
import { upsertAccount } from "../lib/db-helpers";
import { makeResolverId } from "../lib/ids";
import { ROOT_NODE, makeSubnodeNamehash } from "../lib/subname-helpers";
import { upsertAccount } from "../lib/upserts";

/**
* Initialize the ENS root node with the zeroAddress as the owner.
* 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 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 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 names were: `base`, `linea`, `mainnet`, the order of execution
* will be: `mainnet`, `linea`, `base`.
* 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 }) {
// ensure we have an account for the zeroAddress
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,
});
await context.db
.insert(schema.domain)
.values({
id: ROOT_NODE,
ownerId: zeroAddress,
createdAt: 0n,
isMigrated: false,
})
// only insert the domain entity into the database if it doesn't already exist
.onConflictDoNothing();
lightwalker-eth marked this conversation as resolved.
Show resolved Hide resolved
}

function isDomainEmpty(domain: typeof schema.domain.$inferSelect) {
Expand Down
2 changes: 1 addition & 1 deletion src/handlers/Resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions src/lib/upserts.ts → src/lib/db-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ import schema from "ponder:schema";
import type { Address } from "viem";

export async function upsertAccount(context: Context, address: Address) {
return await context.db.insert(schema.account).values({ id: address }).onConflictDoNothing();
return context.db.insert(schema.account).values({ id: address }).onConflictDoNothing();
}

export async function upsertResolver(
context: Context,
values: typeof schema.resolver.$inferInsert,
) {
return await context.db.insert(schema.resolver).values(values).onConflictDoUpdate(values);
return context.db.insert(schema.resolver).values(values).onConflictDoUpdate(values);
}

export async function upsertRegistration(
context: Context,
values: typeof schema.registration.$inferInsert,
) {
return await context.db.insert(schema.registration).values(values).onConflictDoUpdate(values);
return context.db.insert(schema.registration).values(values).onConflictDoUpdate(values);
}
Loading
Loading