-
Notifications
You must be signed in to change notification settings - Fork 16
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
Changes from 5 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 | ||||
|---|---|---|---|---|---|---|
|
|
@@ -305,5 +305,20 @@ 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", { | ||||||
|
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
|
||||||
| ownerAddress: "EVM address of the owner of an EFP List Token", | ||||||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||||||
| listStorageLocation: "A reference to the related ListStorageLocation entity", | ||||||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||||||
| }), | ||||||
| ...generateTypeDocSetWithTypeName("efp_listStorageLocation", "EFP List Storage Location", { | ||||||
|
lightwalker-eth marked this conversation as 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 |
||||||
| chainId: "The 32-byte EVM chain ID of the chain where the list is stored", | ||||||
| listRecordsAddress: "The 20-byte EVM address of the contract where the list is stored", | ||||||
| slot: "A 32-byte value that specifies the storage slot of the list within the contract. This disambiguates multiple lists stored within the same contract and de-couples it from the EFP List NFT token id which is stored on Ethereum and inaccessible on L2s.", | ||||||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||||||
| listTokenId: "Unique identifier for this EFP list token", | ||||||
| listToken: "A reference to the related ListToken entity", | ||||||
| }), | ||||||
| }); | ||||||
| }; | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| /** | ||
| * The EFP plugin describes indexing behavior for the Ethereum Follow Protocol. | ||
| * | ||
| * NOTE: this is an early version of the experimental EFP plugin and is not complete or production ready. | ||
| */ | ||
|
|
||
| import type { ENSIndexerConfig } from "@/config/types"; | ||
| import { | ||
| type ENSIndexerPlugin, | ||
| activateHandlers, | ||
| makePluginNamespace, | ||
| networkConfigForContract, | ||
| networksConfigForChain, | ||
| } from "@/lib/plugin-helpers"; | ||
| import { DatasourceName } from "@ensnode/ens-deployments"; | ||
| import { PluginName } from "@ensnode/ensnode-sdk"; | ||
| import { createConfig } from "ponder"; | ||
|
|
||
| const pluginName = PluginName.EFP; | ||
|
|
||
| // Define the Datasources required by the plugin | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| const requiredDatasources = [DatasourceName.EFPBase]; | ||
|
|
||
| // construct a unique contract namespace for this plugin | ||
| const namespace = makePluginNamespace(pluginName); | ||
|
|
||
| // config object factory used to derive PluginConfig type | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| function createPonderConfig(appConfig: ENSIndexerConfig) { | ||
| const { ensDeployment } = appConfig; | ||
| // extract the chain and contract configs for root Datasource in order to build ponder config | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| const { chain, contracts } = ensDeployment[DatasourceName.EFPBase]; | ||
|
|
||
| return createConfig({ | ||
| networks: networksConfigForChain(chain.id), | ||
| contracts: { | ||
| [namespace("EFPListRegistry")]: { | ||
| network: networkConfigForContract(chain, contracts.EFPListRegistry), | ||
| abi: contracts.EFPListRegistry.abi, | ||
| }, | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| // Implicitly define the type returned by createPluginConfig | ||
| type PonderConfig = ReturnType<typeof createPonderConfig>; | ||
|
|
||
| export default { | ||
| /** | ||
| * Activate the plugin handlers for indexing. | ||
| */ | ||
| activate: activateHandlers({ | ||
| pluginName, | ||
| namespace, | ||
| handlers: () => [import("./handlers/EFPListRegistry")], | ||
| }), | ||
|
|
||
| /** | ||
| * Load the plugin configuration lazily to prevent premature execution of | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| * nested factory functions, i.e. to ensure that the plugin configuration | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| * is only built when the plugin is activated. | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| */ | ||
| createPonderConfig, | ||
|
|
||
| /** The unique plugin name */ | ||
| pluginName, | ||
|
|
||
| /** The plugin's required Datasources */ | ||
| requiredDatasources, | ||
| } as const satisfies ENSIndexerPlugin<PluginName.EFP, PonderConfig>; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| 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 { DatasourceName } from "@ensnode/ens-deployments"; | ||
| import { PluginName } from "@ensnode/ensnode-sdk"; | ||
| import { zeroAddress } from "viem"; | ||
| import type { Address } from "viem/accounts"; | ||
| import { getAddress } from "viem/utils"; | ||
|
|
||
| const efpListRegistryContract = | ||
| config.ensDeployment[DatasourceName.EFPBase].contracts["EFPListRegistry"]; | ||
|
|
||
| export default function ({ namespace }: ENSIndexerPluginHandlerArgs<PluginName.EFP>) { | ||
| /// | ||
| /// EFPListRegistry Handlers | ||
| /// | ||
| ponder.on( | ||
| namespace("EFPListRegistry:Transfer"), | ||
| async function handleListTokenMint({ context, event }) { | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| // We can only index a new List Token after a mint event happened. | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| // This means the `from` address of the Transfer event points to | ||
| // the zero address. Otherwise, we skip event handling | ||
| if (event.args.from !== zeroAddress) { | ||
| return; | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| // As this is a token mint event, create a new List Token with an owner | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| const listToken = await context.db.insert(efp_listToken).values({ | ||
| id: event.args.tokenId, | ||
| ownerAddress: event.args.to, | ||
| }); | ||
|
|
||
| // Read the List Storage Location for that newly created List Token | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| const listStorageLocationRaw = await context.client.readContract({ | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| abi: efpListRegistryContract.abi, | ||
| address: efpListRegistryContract.address, | ||
| functionName: "getListStorageLocation", | ||
| args: [listToken.id], | ||
| }); | ||
|
|
||
| // Index the List Storage Location linked to the List Token | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| try { | ||
| const parsedListStorageLocation = parseListStorageLocation(listStorageLocationRaw); | ||
|
|
||
| // Index the parsed List Storage Location data with a reference to the List Token | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| // created with the currently handled EVM event | ||
| await context.db | ||
| .insert(efp_listStorageLocation) | ||
|
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. Ah very important!! We need to make very special note of how it is possible to have "orphaned" LSL. In other words, some LSL may exist but not have any active relationship with a List Token. Additionally, as I understand this means it is possible for a List Token to update its LSL so that multiple List Tokens point to the same LSL? Is that correct? It's really important for us to completely master this data model. Appreciate your advice!
Member
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 like us to ask such questions to the EFP team. Will note it down 👍 |
||
| .values({ | ||
| ...parsedListStorageLocation, | ||
| listTokenId: listToken.id, | ||
| }) | ||
| // TODO: decide what needs to do in a case of violated unique constraint | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| // For example, it happens for List Storage Location fetched after | ||
| // this transaction | ||
| // https://basescan.org/tx/0x5f64037fedd56a3a874f598a38a48ea6f8f5f9815223dac955e2c18eff1ab173 | ||
| // | ||
| // NOTE: For now, we do update the reference to the List Token | ||
| .onConflictDoUpdate({ | ||
| listTokenId: listToken.id, | ||
| }); | ||
| } catch (error) { | ||
| const errorMessage = error instanceof Error ? error.message : "Unknown error"; | ||
|
|
||
| console.error( | ||
| `Could not create list storage location for tx ${event.transaction.hash}. Error: ${errorMessage}`, | ||
| ); | ||
| } | ||
| }, | ||
| ); | ||
| } | ||
|
|
||
| // NOTE: copied from https://github.com/ethereumfollowprotocol/onchain/blob/f3c970e/src/efp.ts#L95-L123 | ||
| /** | ||
| * Parses a List Storage Location string and returns a ListStorageLocation object. | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| * | ||
| * @param lsl - The List Storage Location string to parse. | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| * @throws An error if parsing could not be completed successfully. | ||
| * @returns A {@link ListStorageLocation} object with parsed data. | ||
| */ | ||
| export function parseListStorageLocation(lsl: string): ListStorageLocation { | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| if (lsl.length != 174) { | ||
| throw new Error("List Storage Location value must be 174-character long string"); | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| const lslVersion = lsl.slice(2, 4); // Extract the first byte after the 0x (2 hex characters = 1 byte) | ||
| const lslType = lsl.slice(4, 6); // Extract the second byte | ||
| const lslChainId = BigInt("0x" + lsl.slice(6, 70)); // Extract the next 32 bytes to get the chain id | ||
| // NOTE: updated parser | ||
| const lslListRecordsContract = getAddress("0x" + lsl.slice(70, 110)); // Extract the address (40 hex characters = 20 bytes) | ||
| const lslSlot = BigInt("0x" + lsl.slice(110, 174)); // Extract the slot | ||
| return { | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| version: lslVersion, | ||
| type: lslType, | ||
| chainId: lslChainId, | ||
| listRecordsAddress: lslListRecordsContract, | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| slot: lslSlot, | ||
| }; | ||
| } | ||
|
|
||
| // NOTE: copied from https://github.com/ethereumfollowprotocol/onchain/blob/598ab49/src/types.ts#L41-L47 | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| // Documented based on https://docs.efp.app/design/list-storage-location/ | ||
| /** | ||
| * List Storage Location is encoded in a versioned, flexible data structure. | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| * | ||
| * Each List Storage Location is encoded as a bytes array with the following structure: | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| * - `version`: A uint8 representing the version of the List Storage Location. This is used to ensure compatibility and facilitate future upgrades. | ||
| * - `location_type`: A uint8 indicating the type of list storage location. This serves as an identifier for the kind of data the data field contains. | ||
| * - `data:` A bytes array containing the actual data of the list storage location. The structure of this data depends on the location type. | ||
| * | ||
| * The version is always 1. | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| * The location type is always 1. | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| */ | ||
| type ListStorageLocation = { | ||
| /** | ||
| * A `uint8` representing the version of the List Storage Location. | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| * This is used to ensure compatibility and facilitate future upgrades. | ||
| */ | ||
| version: string; | ||
|
|
||
| /** | ||
| * A uint8 indicating the type of list storage location. | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| * This serves as an identifier for the kind of data the data field contains. | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| */ | ||
| type: string; | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
|
|
||
| /** | ||
| * The 32-byte EVM chain ID of the chain where the list is stored. | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| * A.k.a. `chain_id` | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| */ | ||
| chainId: bigint; | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
|
|
||
| /** | ||
| * The 20-byte EVM address of the contract where the list is stored. | ||
| * A.k.a. `contract_address` | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| * | ||
| * NOTE: updated type from `string` to `Address` | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| */ | ||
| listRecordsAddress: Address; | ||
|
|
||
| /** | ||
| * A 32-byte value that specifies the storage slot of the list within the contract. | ||
|
lightwalker-eth marked this conversation as resolved.
Outdated
|
||
| * This disambiguates multiple lists stored within the same contract and | ||
| * de-couples it from the EFP List NFT token id which is stored on Ethereum and | ||
| * inaccessible on L2s. | ||
| */ | ||
| slot: bigint; | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.