Skip to content

feat(cross-subname-indexing): allow activating multiple plugins together - #21

Closed
tk-o wants to merge 8 commits into
mainfrom
feat/cross-subname-indexing
Closed

feat(cross-subname-indexing): allow activating multiple plugins together#21
tk-o wants to merge 8 commits into
mainfrom
feat/cross-subname-indexing

Conversation

@tk-o

@tk-o tk-o commented Jan 12, 2025

Copy link
Copy Markdown
Member

This PR allows cross-subname indexing. For example. with this change, it's possible to get a list of all names a given address owns across multiple subnames (i.e. base.eth, linea.eth).

Example query

query GetOwnedDomains($ownerAddreess: String!) {
  domainss(
    where: {OR: [{ownerId: $ownerAddreess}, {wrappedOwnerId: $ownerAddreess}]}
  ) {
    items {
      labelName
      name
      ownerId
      wrappedOwnerId
      createdAt
      expiryDate
    }
  }
}

Preview

Domain names under eth, base.eth, and linea.eth:
image

Comment thread src/handlers/Registrar.ts
await context.db
.update(domains, { id: node })
.set({ labelName: name, name: `${name}${ownedName}` });
.set({ labelName: name, name: `${name}.${ownedName}` });

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a bug, that existed since we dropped . from the ownedName param on plugin's configuration object.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks.

Comment thread src/handlers/Registry.ts
createdAt: 0n,
isMigrated: false,
})
.onConflictDoNothing();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to introduce .onConflictDoNothing() as multiple plugins run the setupRootNode() function, so every call apart from the initial one would cause an error.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tk-o Hey really appreciate your PR comment on this! Could you please also document this idea on each place in the code where this idea applies? This way the relevant documentation will live on in our code in an easy to find place after this PR is merged.

Taking a step-back, I'm a bit confused on why there might be conflicts caused by other plugins here. I suppose the conflict might be caused by how we are enabling the base.eth or linea.eth plugins to be activated without the .eth plugin being activated. In other words, we don't have any concept of plugin dependencies implemented, such that the activation of plugin X also requires the activation of plugin Y.

Do I understand that correctly? Appreciate your advice.

I'm ok to not introduce plugin dependencies yet. I think we can come back to this as a separate goal in the future. It would add complexity and I'm not sure if its even the best idea. For now just trying to understand the current situation better. Thanks 👍

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each plugin requires a root node to be included in the domains collection so that all TLDs could be attached the parent node (the root node). Each plugin needs the root domain entry to be added before the rest of indexing happens. No plugin can expect any other plugin to do that job, so each plugin tries to do it on its own. If multiple plugins are activated, the only one of them will effectively insert the root domain entry into the domains table, while other plugins will do nothing.

Comment thread src/plugins/base.eth/ponder.config.ts Outdated
@tk-o
tk-o force-pushed the feat/cross-subname-indexing branch from 720ed9f to 7d8f978 Compare January 12, 2025 11:54
Comment thread ponder.config.ts Outdated
const activePlugins = loadActivePlugins();

pluginToActivate.activate();
export default (() =>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you just return createConfig(loadActivePlugins()) as AllPluginsConfig? feels like spreading contracts: {...activePlugins.contracts} isn't doing much?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this suggestion, now it looks way cleaner ;)

Comment thread src/lib/helpers.ts
* const obj3 = deepMergeRecursive(obj1, obj2);
* // { a: 4, b: 2, c: { d: 3, e: 5 } }
*/
export function deepMergeRecursive<T extends AnyObject, U extends AnyObject>(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'd actually prefer to depend on a package for this, what do you think? no dependency is nice but this looks complex enough and generic enough that a well-tested dependency seems a-ok to me. i found this one for example https://github.com/voodoocreation/ts-deepmerge

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use lib, I tried ts-deepmerge and deepmerge — couldn't get the types to work easily, so gave up. On the other hand, this one works well and does its job.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tk-o Thanks for proposing this. Agreed with @shrugs that a well-tested library for this sounds attractive. My head hurts thinking for code review of testing on this.

Appreciate if it might take some time to figure out how to get one of those libraries to work. If we can find a solution to that in this PR, then great 👍 If it needs more time, then please feel welcome to create a new GitHub Issue for us to track this goal and we can come back to it in a separate PR.

Comment thread src/handlers/Registry.ts
Comment on lines +135 to +139
if (await incrementSubdomainCountOnParent()) {
await context.db
.update(domains, { id: parent!.id })
.set((row) => ({ subdomainCount: row.subdomainCount + 1 }));
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shrugs, I hope you'll like this change. It allows not updating the parent domain entry if it does not exist. Useful during partial block-range indexing, and neutral to full block-range indexing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tk-o Hey thanks for your PR comment above. I see that comment is specific to lines 135-139. What about all the other changes being proposed to handleNewOwner in this PR? Can you please help us understand the bigger goal for these changes?

While we're working on our "V1" milestone I'm cautious about changes to indexing logic that are difficult for me to easily review. Open to making them if there's a good reason 👍

@lightwalker-eth lightwalker-eth left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tk-o Super exciting updates in this PR! 🚀 Reviewed and shared feedback.

/**
* 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* 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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really appreciate the comment here! Very happy to see 👍

Could you please add even more background info into the comment here?

For example:

  1. Exactly what is causing the Transfer event to occur before the NameRegistered event?
  2. How does this relate to the event.args.from being the zeroAddress?
  3. Please describe the big idea behind what this new code is doing? What does it represent in the big picture? I see we're inserting some new domain, but it isn't clear why or what we're trying to achieve.

@tk-o tk-o Jan 13, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. The contract emits the Transfer event before emitting the NameRegistered event. It's how the contract implementation is done. Our BaseRegistrar:Transfer handler previously assumed that the domain name would already be included in the domains table, but that's not always the case. Registering a new subname first transfers an NFT token from the zero address to the owner, and then emits the NameRegistered event.

  2. Minting the subname NFT is effectively transferring it from the zero address to the owner.

  3. This code is ensuring that the BaseRegistrar:NameRegistered handler can assume that the domain name has been for sure present in the database.

@@ -46,7 +47,28 @@ export default function () {

// Base's BaseRegistrar uses `id` instead of `tokenId`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Base's BaseRegistrar uses `id` instead of `tokenId`
// base.eth's BaseRegistrar uses `id` instead of `tokenId`

// Base's BaseRegistrar uses `id` instead of `tokenId`
ponder.on(pluginNamespace("BaseRegistrar:Transfer"), async ({ context, event }) => {
return await handleNameTransferred({
if (event.args.from === zeroAddress) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see related feedback I shared for this on related code for linea.eth

Comment thread src/lib/plugin-helpers.ts Outdated
Comment thread src/handlers/Registry.ts
createdAt: 0n,
isMigrated: false,
})
.onConflictDoNothing();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tk-o Hey really appreciate your PR comment on this! Could you please also document this idea on each place in the code where this idea applies? This way the relevant documentation will live on in our code in an easy to find place after this PR is merged.

Taking a step-back, I'm a bit confused on why there might be conflicts caused by other plugins here. I suppose the conflict might be caused by how we are enabling the base.eth or linea.eth plugins to be activated without the .eth plugin being activated. In other words, we don't have any concept of plugin dependencies implemented, such that the activation of plugin X also requires the activation of plugin Y.

Do I understand that correctly? Appreciate your advice.

I'm ok to not introduce plugin dependencies yet. I think we can come back to this as a separate goal in the future. It would add complexity and I'm not sure if its even the best idea. For now just trying to understand the current situation better. Thanks 👍

Comment thread ponder.config.ts Outdated
// 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few questions:

  1. What is "the type of the exported default"?
  2. When you write "all plugin configs" or AllPluginsConfig, is this all activated plugins, or all available plugins? Appreciate if you could clarify terminology.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I should say "the type of the default export"
  2. "all plugins config" mean an intersection type of the available plugins (regardless if active or not). This type is required to support all defined indexing handlers type-safety.

Comment thread ponder.config.ts
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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const plugins = [baseEthPlugin, ethPlugin, lineaEthPlugin] as const;
const availablePlugins = [baseEthPlugin, ethPlugin, lineaEthPlugin] as const;

Suggesting this to help us give more distinct naming to all available plugins vs all activated plugins.

Comment thread src/handlers/Registrar.ts
await context.db
.update(domains, { id: node })
.set({ labelName: name, name: `${name}${ownedName}` });
.set({ labelName: name, name: `${name}.${ownedName}` });

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks.

Comment thread src/handlers/Registry.ts
Comment on lines +135 to +139
if (await incrementSubdomainCountOnParent()) {
await context.db
.update(domains, { id: parent!.id })
.set((row) => ({ subdomainCount: row.subdomainCount + 1 }));
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tk-o Hey thanks for your PR comment above. I see that comment is specific to lines 135-139. What about all the other changes being proposed to handleNewOwner in this PR? Can you please help us understand the bigger goal for these changes?

While we're working on our "V1" milestone I'm cautious about changes to indexing logic that are difficult for me to easily review. Open to making them if there's a good reason 👍

Comment thread src/lib/plugin-helpers.ts Outdated
* @returns the active plugins
*/
export function getActivePlugins<T extends { ownedName: string }>(plugins: readonly T[]): T[] {
const pluginsToActivateByOwnedName = ACTIVE_PLUGIN

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's rename this to ACTIVE_PLUGINS now that it's a plural array, and make sure to update .env.local.example as well

@tk-o

tk-o commented Jan 13, 2025

Copy link
Copy Markdown
Member Author

Closing this PR in favour of:

@tk-o tk-o closed this Jan 13, 2025
@tk-o
tk-o deleted the feat/cross-subname-indexing branch January 15, 2025 06:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants