-
Notifications
You must be signed in to change notification settings - Fork 15
feat(ensindexer): introduce initial efp plugin demo
#771
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
base: main
Are you sure you want to change the base?
Changes from all commits
5cb40d1
114021e
797e7de
4dc678c
57ef002
2ac7ddb
9ba0591
cbfa373
cd0e030
2445b09
40a5c2e
45701e3
dcaf69b
6f7443e
260bca0
d8ecb6e
68e6066
5a90cc6
1d91624
0509953
7d714bc
9f422a5
c02f7c0
ee6c1b3
6d6e956
b52033f
c49360f
c242463
d1942ac
90a85d1
a1962a7
42e18e0
d57197f
fd4bfc2
03d55bc
0040037
a35833c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| --- | ||
| "@ensnode/datasources": minor | ||
| "@ensnode/ensnode-schema": minor | ||
| "@ensnode/ensnode-sdk": minor | ||
| "ensindexer": minor | ||
| --- | ||
|
|
||
| Introduce initial `efp` plugin demo that indexes EFP List Tokens and EFP List Storage Locations. |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -305,5 +305,22 @@ const makeApiDocumentation = (isSubgraph: boolean) => { | |||||
| value: "Value of the text record", | ||||||
| }, | ||||||
| ), | ||||||
| /** | ||||||
| * The following is documentation for packages/ensnode-schema/src/efp.schema.ts | ||||||
| */ | ||||||
| ...generateTypeDocSetWithTypeName("efp_listToken", "EFP List Token", { | ||||||
lightwalker-eth marked this conversation as resolved.
Show resolved
Hide resolved
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| id: "Unique token ID for an EFP List Token", | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| owner: "The address of the current owner of the EFP List Token (always lowercase)", | ||||||
| lslId: | ||||||
| "Value of `EncodedLsl` type (optional, lowercase if present). Stores the ID of the List Storage Location. If the List Storage Location was never created or not in a recognized format, this field value will be `null`.", | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A few concerns / suggestions here:
|
||||||
| }), | ||||||
| ...generateTypeDocSetWithTypeName("efp_listStorageLocation", "EFP List Storage Location", { | ||||||
lightwalker-eth marked this conversation as resolved.
Show resolved
Hide resolved
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Is that fair?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm. Maybe we aren't defining the data model here correctly? Previously I was thinking we would build a foreign key relationship from However now I'm thinking that's not right and that we should actually build this foreign key relationship using the EFP List Token Id. In other words, the primary key of both What do you think? I think we still want to keep the suggested |
||||||
| id: "ListStorageLocation ID, an `EncodedLsl` value (always lowercase).", | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
What do you think? I find it nice to avoid introducing additional terminology for "lsl id". Instead, in my current understanding, we have lsl, and a subset of lsl we index will be "recognized". All lsl we index will be found in the |
||||||
| chainId: | ||||||
| "EVM chain ID of the chain where the EFP list records are stored, an `EFPDeploymentChainId` value.", | ||||||
| listRecordsAddress: | ||||||
| "Contract address on chainId where the EFP list records are stored (always lowercase).", | ||||||
| slot: "A unique identifier within the List Storage Location, distinguishes between multiple EFP lists stored in the same `EFPListRecords` smart contract by serving as the key in mappings for list operations and metadata.", | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Is that fair? |
||||||
| }), | ||||||
| }); | ||||||
| }; | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| import { ponder } from "ponder:registry"; | ||
| import { efp_listStorageLocation, efp_listToken } from "ponder:schema"; | ||
|
|
||
| import config from "@/config"; | ||
| import type { ENSIndexerPluginHandlerArgs } from "@/lib/plugin-helpers"; | ||
| import { PluginName } from "@ensnode/ensnode-sdk"; | ||
| import { zeroAddress } from "viem"; | ||
| import { | ||
| decodeListStorageLocationContract, | ||
| isEncodedLslContract, | ||
| parseEncodedLsl, | ||
| } from "../lib/lsl"; | ||
| import { parseEvmAddress } from "../lib/utils"; | ||
|
|
||
| export default function ({ pluginNamespace: ns }: ENSIndexerPluginHandlerArgs<PluginName.EFP>) { | ||
| /// | ||
| /// EFPListRegistry Handlers | ||
| /// | ||
| ponder.on( | ||
| ns("EFPListRegistry:Transfer"), | ||
| async function handleEFPListTokenTransfer({ context, event }) { | ||
| const { tokenId, from: fromAddress, to: toAddress } = event.args; | ||
|
|
||
| // The mint event represents the transfer of ownership of | ||
| // an EFP List token from the zeroAddress to the new owner's address. | ||
| if (fromAddress === zeroAddress) { | ||
| // Create a new EFP List Token with the owner initialized as the token recipient | ||
| await context.db.insert(efp_listToken).values({ | ||
| id: tokenId, | ||
| owner: parseEvmAddress(toAddress), | ||
| }); | ||
| } | ||
|
|
||
| // The burn event represents the transfer of ownership of | ||
| // an EFP List token from the owner's address to the zero address. | ||
| else if (toAddress === zeroAddress) { | ||
| // Delete the burnt EFP List Token | ||
| await context.db.delete(efp_listToken, { id: tokenId }); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about other cascading relationships? For example, what if this burnt list token was also associated with a list storage location? Don't we need to ensure that record is also deleted? If Postgres is automatically doing those cascading deletes for us, please document that here. Thanks
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd keep the LSL updates (and deletes) to be handled with I don't know the EFP protocol to the extent that would let me confidently say what needs to be done when a list token is burnt. This is a great question to ask to the EFP team. I'd like us to focus on optimising what we know. Things we don't know about the EFP protocol should not be optimised in this PR. I think we can leave this example to highlight: here's how updates and deletions look like.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Revisiting this discussion. Please see my other comments suggesting that the primary key of both the If you agree with that idea then I believe we should also perform a delete on |
||
| } | ||
|
|
||
| // If the transfer is not from the zeroAddress, | ||
| // and the transfer is not to the zeroAddress, | ||
| // this transfer represents an ownership change. | ||
| else { | ||
| // Update the owner of the the List Token that changed ownership | ||
| await context.db.update(efp_listToken, { id: tokenId }).set({ | ||
| owner: parseEvmAddress(toAddress), | ||
| }); | ||
| } | ||
| }, | ||
| ); | ||
|
|
||
| ponder.on( | ||
| ns("EFPListRegistry:UpdateListStorageLocation"), | ||
| async function handleEFPListStorageLocationUpdate({ context, event }) { | ||
| const { listStorageLocation: encodedListStorageLocation, tokenId } = event.args; | ||
| const listToken = await context.db.find(efp_listToken, { id: tokenId }); | ||
|
|
||
| // invariant: List Token must exist before its List Storage Location can be updated | ||
| if (!listToken) { | ||
| throw new Error( | ||
| `Cannot update List Storage Location for nonexisting List Token (id: ${tokenId})`, | ||
| ); | ||
| } | ||
|
|
||
| const encodedLsl = parseEncodedLsl(encodedListStorageLocation); | ||
| const lslId = encodedLsl; | ||
|
|
||
| // Update the List Token with the new List Storage Location | ||
| await context.db.update(efp_listToken, { id: tokenId }).set({ | ||
| lslId, | ||
| }); | ||
|
|
||
| // Update the List Storage Location associated with the List Token | ||
| // if the List Storage Location is in a recognized format | ||
| if (isEncodedLslContract(encodedLsl)) { | ||
| try { | ||
| const lslContract = decodeListStorageLocationContract(config.namespace, encodedLsl); | ||
|
|
||
| // Index the decoded List Storage Location data with a reference to the List Token | ||
| // created with the currently handled EVM event. | ||
| // Note: if the List Storage Location with the same ID already exists, | ||
| // no new changes will be made to the database. | ||
| await context.db | ||
| .insert(efp_listStorageLocation) | ||
| .values({ | ||
| id: lslId, | ||
| chainId: lslContract.chainId, | ||
| listRecordsAddress: lslContract.listRecordsAddress, | ||
| slot: lslContract.slot, | ||
| }) | ||
| .onConflictDoNothing(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please see my other comments about suggested data model changes. I think we want to remove this |
||
| } catch { | ||
| // The `encodedLsl` value could not be decoded. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please see my other comments about suggested data model changes. Based on those suggestions, I believe here we want to perform a delete operation in the |
||
| // We can ignore this case, as we have already captured the `encodedLsl` value | ||
| // in the `efp_listToken` table as the `lslId` value. | ||
| } | ||
| } | ||
| }, | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,56 @@ | ||||||
| /** | ||||||
| * This Hono application enables the ENSIndexer API to handle requests related to the Ethereum Follow Protocol. | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| */ | ||||||
|
|
||||||
| import { efp_listToken } from "ponder:schema"; | ||||||
| import { Hono } from "hono"; | ||||||
| import { type ReadonlyDrizzle, eq } from "ponder"; | ||||||
| import { type ListTokenId, parseListTokenId } from "./utils"; | ||||||
|
|
||||||
| interface HonoEFP { | ||||||
| db: ReadonlyDrizzle<Record<string, unknown>>; | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not so familiar with Drizzle. Are we supposed to pass in the overall schema we define in another file here to help give Drizzle full type system knowledge? Appreciate your advice. |
||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Creates a Hono application with EFP routes. | ||||||
| * @returns | ||||||
| */ | ||||||
| export function honoEFP({ db }: HonoEFP): Hono { | ||||||
| const app = new Hono(); | ||||||
|
|
||||||
| // Route to handle EFP-related requests | ||||||
| app.get("/list/:listTokenId", async function getListTokenById(ctx) { | ||||||
| let listTokenId: ListTokenId; | ||||||
|
|
||||||
| // Parse the listTokenId from the request parameter | ||||||
| try { | ||||||
| listTokenId = parseListTokenId(ctx.req.param("listTokenId")); | ||||||
| } catch (error) { | ||||||
| const errorMessage = error instanceof Error ? error.message : "Unknown error"; | ||||||
| return ctx.json({ error: `Invalid list token ID. ${errorMessage}` }, 400); | ||||||
| } | ||||||
|
|
||||||
| // Fetch the EFP List Token from the database | ||||||
| const listTokens = await db | ||||||
| .select() | ||||||
| .from(efp_listToken) | ||||||
| .where(eq(efp_listToken.id, listTokenId)); | ||||||
|
|
||||||
| const listToken = listTokens[0]; | ||||||
|
|
||||||
| if (!listToken) { | ||||||
| return ctx.json({ error: `List token with ID ${listTokenId} not found.` }, 404); | ||||||
| } | ||||||
|
|
||||||
| // Prepare the response DTO | ||||||
| // Note: The DTO is a simplified version of the EFP List Token schema. | ||||||
| const listTokenDto = { | ||||||
| owner: listToken.owner, | ||||||
| encodedLsl: listToken.lslId, | ||||||
| }; | ||||||
|
|
||||||
| return ctx.json(listTokenDto, 200); | ||||||
| }); | ||||||
|
|
||||||
| return app; | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import type { ENSNamespaceId } from "@ensnode/datasources"; | ||
| import { base, baseSepolia, mainnet, optimism, optimismSepolia, sepolia } from "viem/chains"; | ||
|
|
||
| /** | ||
| * The ChainId of an EFP Deployment.. | ||
| * | ||
| * EFP has an allowlisted set of supported chains. This allowlisted set is a function of the ENSNamespace. | ||
| * See {@link getEFPDeploymentChainIds}. | ||
| */ | ||
| export type EFPDeploymentChainId = number; | ||
lightwalker-eth marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * Get the list of EFP Deployment Chain IDs for the ENS Namespace ID. | ||
| * | ||
| * @param ensNamespaceId - ENS Namespace ID to get the EFP Deployment Chain IDs for | ||
| * @returns list of EFP Deployment Chain IDs on the associated ENS Namespace | ||
| */ | ||
| export function getEFPDeploymentChainIds(ensNamespaceId: ENSNamespaceId): EFPDeploymentChainId[] { | ||
| switch (ensNamespaceId) { | ||
| case "mainnet": | ||
| return [base.id, optimism.id, mainnet.id]; | ||
| case "sepolia": | ||
| return [baseSepolia.id, optimismSepolia.id, sepolia.id]; | ||
| default: | ||
| throw new Error( | ||
| `EFP Deployment chainIds are not defined for the ${ensNamespaceId} ENS Namespace ID`, | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.