From fcaaf87e9a527ea7ec0aae55555bb313789819e6 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 19 Jun 2026 14:32:14 -0700 Subject: [PATCH 1/5] feat(docs): EFP Omnigraph example queries, skill, and plugin docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds EFP (Ethereum Follow Protocol) example queries to the shared SDK collection and surfaces them across the docs and agent skills. - ensnode-sdk: add `efp-follow-graph` / `efp-who-follows` / `efp-list` example queries; tag every GraphqlApiExampleQuery with the `plugin` it requires (existing → unigraph, new → efp). - ensskills: generate the efp-protocol skill's examples from the SDK (AUTOGEN:EXAMPLES, filtered to efp); filter the base omnigraph skill to unigraph. - docs: new "EFP" Omnigraph page (how to enable the plugin, API surface), three standalone EFP example pages each with an "EFP plugin required" tip, the efp plugin in the Existing Plugins table and on the hosted Alpha instance. Omnigraph examples snapshot refreshed to v1.16.0 (EFP responses recorded from a local efp-enabled instance). Closes #2324 --- .changeset/efp-example-queries.md | 5 + .changeset/efp-omnigraph-docs.md | 5 + .changeset/efp-skill-examples.md | 5 + .../starlight/sidebar-topics/integrate.ts | 4 + .../content/docs/docs/hosted-instances.mdx | 2 +- .../integration-options/ensnode-plugins.mdx | 25 +- .../docs/docs/integrate/omnigraph/efp.mdx | 62 ++ .../omnigraph/examples/efp-follow-graph.mdx | 11 + .../integrate/omnigraph/examples/efp-list.mdx | 11 + .../omnigraph/examples/efp-who-follows.mdx | 11 + .../src/data/omnigraph-examples/config.ts | 27 + .../src/data/omnigraph-examples/examples.json | 25 +- .../data/omnigraph-examples/responses.json | 564 ++++++++++++++++++ .../data/omnigraph-examples/schema.graphql | 457 ++++++++++++++ .../src/data/omnigraph-examples/snapshot.json | 10 +- .../src/omnigraph-api/example-queries.ts | 121 ++++ packages/ensskills/scripts/generate.ts | 36 +- .../ensskills/skills/efp-protocol/SKILL.md | 39 +- 18 files changed, 1392 insertions(+), 28 deletions(-) create mode 100644 .changeset/efp-example-queries.md create mode 100644 .changeset/efp-omnigraph-docs.md create mode 100644 .changeset/efp-skill-examples.md create mode 100644 docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/efp.mdx create mode 100644 docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/examples/efp-follow-graph.mdx create mode 100644 docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/examples/efp-list.mdx create mode 100644 docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/examples/efp-who-follows.mdx diff --git a/.changeset/efp-example-queries.md b/.changeset/efp-example-queries.md new file mode 100644 index 000000000..712ebcab8 --- /dev/null +++ b/.changeset/efp-example-queries.md @@ -0,0 +1,5 @@ +--- +"@ensnode/ensnode-sdk": minor +--- + +Add EFP Omnigraph example queries (`efp-follow-graph`, `efp-who-follows`, `efp-list`) and tag every `GraphqlApiExampleQuery` with the `plugin` (`PluginName`) it requires. Existing queries are tagged `unigraph`; the new EFP queries are tagged `efp`. diff --git a/.changeset/efp-omnigraph-docs.md b/.changeset/efp-omnigraph-docs.md new file mode 100644 index 000000000..b364d0b5f --- /dev/null +++ b/.changeset/efp-omnigraph-docs.md @@ -0,0 +1,5 @@ +--- +"@docs/ensnode": patch +--- + +Document EFP in the ENS Omnigraph API: a new "EFP" page covering how to enable the `efp` plugin and its API surface, standalone example-query pages (follow graph, who-follows, list) with an "EFP plugin required" tip, the `efp` plugin in the Existing Plugins table, and the `efp` plugin on the hosted Alpha instance. The Omnigraph examples snapshot is refreshed to v1.16.0. diff --git a/.changeset/efp-skill-examples.md b/.changeset/efp-skill-examples.md new file mode 100644 index 000000000..ca18a1659 --- /dev/null +++ b/.changeset/efp-skill-examples.md @@ -0,0 +1,5 @@ +--- +"ensskills": patch +--- + +The `efp-protocol` skill's example queries are now generated from the shared ENSNode SDK example-query collection (an `AUTOGEN:EXAMPLES` region), filtered to the `efp` plugin. The base `omnigraph` skill's examples are now filtered to the `unigraph` plugin, so each skill ships only the examples that run against the plugin it documents. diff --git a/docs/ensnode.io/config/integrations/starlight/sidebar-topics/integrate.ts b/docs/ensnode.io/config/integrations/starlight/sidebar-topics/integrate.ts index 0fc00d46e..1e6f35355 100644 --- a/docs/ensnode.io/config/integrations/starlight/sidebar-topics/integrate.ts +++ b/docs/ensnode.io/config/integrations/starlight/sidebar-topics/integrate.ts @@ -100,6 +100,10 @@ export const integrateSidebarTopic = { label: "Protocol Acceleration", link: "/docs/integrate/omnigraph/protocol-acceleration", }, + { + label: "EFP", + link: "/docs/integrate/omnigraph/efp", + }, { label: "FAQ", link: "/docs/integrate/omnigraph/faq", diff --git a/docs/ensnode.io/src/content/docs/docs/hosted-instances.mdx b/docs/ensnode.io/src/content/docs/docs/hosted-instances.mdx index 5fe581916..c3684c991 100644 --- a/docs/ensnode.io/src/content/docs/docs/hosted-instances.mdx +++ b/docs/ensnode.io/src/content/docs/docs/hosted-instances.mdx @@ -101,7 +101,7 @@ These ENSNode instances do not constrain themselves to exclusively activating th connectWithENSAdminURL="https://admin.ensnode.io/connection?connection=https%3A%2F%2Fapi.alpha.ensnode.io" namespace="mainnet" ensVersions="ENSv1 only (not also ENSv2 yet)" - plugins={`${pluginLink("unigraph")}, ${pluginLink("protocol-acceleration")}, ${pluginLink("subgraph")}, ${pluginLink("basenames")}, ${pluginLink("lineanames")}, ${pluginLink("threedns")}, ${pluginLink("registrars")}, ${pluginLink("tokenscope")}`} + plugins={`${pluginLink("unigraph")}, ${pluginLink("protocol-acceleration")}, ${pluginLink("subgraph")}, ${pluginLink("basenames")}, ${pluginLink("lineanames")}, ${pluginLink("threedns")}, ${pluginLink("registrars")}, ${pluginLink("tokenscope")}, ${pluginLink("efp")}`} > diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/ensnode-plugins.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/ensnode-plugins.mdx index 20099c149..cef75a1dc 100644 --- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/ensnode-plugins.mdx +++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/ensnode-plugins.mdx @@ -30,23 +30,20 @@ When new ENSNode plugins are created, it becomes possible for the [ENS Omnigraph [ENSIndexer](/docs/services/ensindexer) is a reference implementation of an [ENSDb Writer](/docs/services/ensdb/concepts/glossary#ensdb-writer) that implements multiple existing ENSNode Plugin specifications. The code for each plugin inside ENSIndexer is a reference implementation of that plugin's abstract specification. [The ENSNode Plugins implemented in ENSIndexer today](https://github.com/namehash/ensnode/tree/main/apps/ensindexer/src/plugins) are: -| Plugin | Description | -| ----------------------- | ---------------------------------------------------------- | -| `unigraph` | Unified ENSv1 + ENSv2 data model with polymorphic entities | -| `protocol-acceleration` | Accelerated lookups for ENS resolution | -| `subgraph` | Legacy ENS Subgraph-compatible data model | -| `basenames` | Basenames (`.base.eth`) subname indexing | -| `lineanames` | Lineanames (`.linea.eth`) subname indexing | -| `threedns` | 3DNS (`.box`) name indexing | -| `registrars` | Registration and renewal lifecycle tracking | -| `tokenscope` | NFT tokenization of ENS names | +| Plugin | Description | +| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `unigraph` | Unified ENSv1 + ENSv2 data model with polymorphic entities | +| `protocol-acceleration` | Accelerated lookups for ENS resolution | +| `subgraph` | Legacy ENS Subgraph-compatible data model | +| `basenames` | Basenames (`.base.eth`) subname indexing | +| `lineanames` | Lineanames (`.linea.eth`) subname indexing | +| `threedns` | 3DNS (`.box`) name indexing | +| `registrars` | Registration and renewal lifecycle tracking | +| `tokenscope` | NFT tokenization of ENS names | +| `efp` | [Ethereum Follow Protocol](https://efp.app/) onchain social graph (lists, records, tags) — see [EFP in the Omnigraph](/docs/integrate/omnigraph/efp) | ## New Plugins -### Under Development - -An `efp` plugin for the [Ethereum Follow Protocol](https://efp.app/) is [already under active development](https://github.com/Quantumlyy/efpnode/tree/main/packages/ensnode-plugin-efp) by the [EthId team](https://ethid.org/). - ### Build Your Own If you are interested in defining a new ENSNode plugin or implementing an existing one, reach out to the NameHash Labs team — we are happy to provide support and additional info. Join us on [Telegram](https://t.me/ensnode) for any questions. diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/efp.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/efp.mdx new file mode 100644 index 000000000..42616f01b --- /dev/null +++ b/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/efp.mdx @@ -0,0 +1,62 @@ +--- +title: Ethereum Follow Protocol (EFP) +description: Query the Ethereum Follow Protocol (EFP) onchain social graph through the ENS Omnigraph API by enabling the efp plugin. +sidebar: + label: EFP +--- + +import { LinkCard } from "@astrojs/starlight/components"; +import OmnigraphStaticExampleSet from "@components/organisms/OmnigraphStaticExampleSet.astro"; + +The [Ethereum Follow Protocol (EFP)](https://efp.app) is an onchain social graph for Ethereum accounts — a decentralized "who follows whom" primitive. Because EFP composes directly with ENS identity, the ENS Omnigraph API exposes it alongside ENS data, so you can read an account's follow graph **and** resolve each followed account's ENS name in a single query. + +## Enabling the plugin + +EFP is indexed by the **`efp` plugin**. The Omnigraph's `Query.efp` and `Account.efp` fields return data only when the connected ENSIndexer has this plugin enabled; otherwise they resolve to `null` — treat that like any other unsupported-instance case, not a query error. + +To enable it on your own instance, add `efp` to the ENSIndexer `PLUGINS` environment variable (a comma-separated list), for example: + +```bash +PLUGINS=unigraph,efp +``` + +The `efp` plugin's datasources are defined for the **mainnet** namespace (the EFP contracts on Base, OP Mainnet, and Ethereum Mainnet) and the local **ens-test-env** devnet. + +:::note[Already enabled on the Alpha instance] +The hosted [ENSNode 'Alpha'](/docs/hosted-instances#ensnode-alpha) instance has the `efp` plugin enabled, so you can run every query on this page against it without hosting anything yourself. +::: + +## What it indexes + +The plugin indexes the full EFP onchain state into ENSDb: + +- **Lists** — the ERC-721 NFTs that hold follow relationships, with their `owner` / `manager` / `user` roles and storage location. +- **List records** — the individual follows (address records), each with its **tags** (`block`, `mute`, `top8`, and custom tags). +- **Account metadata** — the onchain `(address, key) → value` store, including the `primary-list` key used to validate an account's primary list. + +## The API surface + +EFP is exposed through two entry points: + +- **`Account.efp`** — an account's EFP presence: validated, block/mute-filtered `following` / `followers` connections (whose edges are full `Account`s you can resolve straight into ENS names), the validated `primaryList`, and raw account `metadata`. +- **`Query.efp`** — list-centric queries: `list(by: { tokenId })`, `lists(where:)` by role address, and `listRecords(where:)` (the raw record set, including `block`/`mute`). + +Prefer `Account.efp.following` / `followers` for the social-graph answer — they apply EFP's primary-list validation and block/mute filtering for you. Drop to the raw `Query.efp.listRecords` / `EfpList.records` only when you specifically need tags, storage location, or non-primary lists. + +## Example + + + +## Learn more + + + + diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/examples/efp-follow-graph.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/examples/efp-follow-graph.mdx new file mode 100644 index 000000000..48c7640cf --- /dev/null +++ b/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/examples/efp-follow-graph.mdx @@ -0,0 +1,11 @@ +--- +title: EFP Follow Graph +--- + +import OmnigraphStaticExampleSet from "@components/organisms/OmnigraphStaticExampleSet.astro"; + +:::tip[EFP plugin required] +This query reads [Ethereum Follow Protocol (EFP)](/docs/integrate/omnigraph/efp) data, which requires the [`efp` plugin](/docs/integrate/omnigraph/efp) to be enabled on the connected ENSIndexer. It is enabled on the hosted [ENSNode 'Alpha'](/docs/hosted-instances#ensnode-alpha) instance. +::: + + diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/examples/efp-list.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/examples/efp-list.mdx new file mode 100644 index 000000000..b3dbf91e3 --- /dev/null +++ b/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/examples/efp-list.mdx @@ -0,0 +1,11 @@ +--- +title: EFP List +--- + +import OmnigraphStaticExampleSet from "@components/organisms/OmnigraphStaticExampleSet.astro"; + +:::tip[EFP plugin required] +This query reads [Ethereum Follow Protocol (EFP)](/docs/integrate/omnigraph/efp) data, which requires the [`efp` plugin](/docs/integrate/omnigraph/efp) to be enabled on the connected ENSIndexer. It is enabled on the hosted [ENSNode 'Alpha'](/docs/hosted-instances#ensnode-alpha) instance. +::: + + diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/examples/efp-who-follows.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/examples/efp-who-follows.mdx new file mode 100644 index 000000000..593539bc2 --- /dev/null +++ b/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/examples/efp-who-follows.mdx @@ -0,0 +1,11 @@ +--- +title: EFP Who Follows +--- + +import OmnigraphStaticExampleSet from "@components/organisms/OmnigraphStaticExampleSet.astro"; + +:::tip[EFP plugin required] +This query reads [Ethereum Follow Protocol (EFP)](/docs/integrate/omnigraph/efp) data, which requires the [`efp` plugin](/docs/integrate/omnigraph/efp) to be enabled on the connected ENSIndexer. It is enabled on the hosted [ENSNode 'Alpha'](/docs/hosted-instances#ensnode-alpha) instance. +::: + + diff --git a/docs/ensnode.io/src/data/omnigraph-examples/config.ts b/docs/ensnode.io/src/data/omnigraph-examples/config.ts index 1aaa478cb..5f08fe47b 100644 --- a/docs/ensnode.io/src/data/omnigraph-examples/config.ts +++ b/docs/ensnode.io/src/data/omnigraph-examples/config.ts @@ -213,6 +213,33 @@ export const OMNIGRAPH_EXAMPLES_CONFIG: OmnigraphExampleConfig[] = [ namespace: ENSNamespaceIds.Mainnet, hostSeparatePage: false, }, + { + id: "efp-follow-graph", + title: "EFP Follow Graph", + description: + "This query loads an account's validated EFP social graph — its primary list, following, and followers — and resolves each followed account's ENS primary name. Requires the efp plugin.", + category: "EFP", + namespace: ENSNamespaceIds.Mainnet, + hostSeparatePage: true, + }, + { + id: "efp-who-follows", + title: "EFP Who Follows", + description: + 'This query finds the raw EFP list records pointing at an address (any list, including block/mute tags), to answer "which lists follow this address?". Requires the efp plugin.', + category: "EFP", + namespace: ENSNamespaceIds.Mainnet, + hostSeparatePage: true, + }, + { + id: "efp-list", + title: "EFP List", + description: + "This query loads an EFP list by tokenId — its roles, storage location, and raw records with tags. Requires the efp plugin.", + category: "EFP", + namespace: ENSNamespaceIds.Mainnet, + hostSeparatePage: true, + }, ]; export const OMNIGRAPH_EXAMPLES_INDEX_PATH = "/docs/integrate/omnigraph/examples" as const; diff --git a/docs/ensnode.io/src/data/omnigraph-examples/examples.json b/docs/ensnode.io/src/data/omnigraph-examples/examples.json index 6b8b4bef5..0851bdc97 100644 --- a/docs/ensnode.io/src/data/omnigraph-examples/examples.json +++ b/docs/ensnode.io/src/data/omnigraph-examples/examples.json @@ -109,7 +109,7 @@ "variables": { "registry": { "chainId": 11155111, - "address": "0x64c81210d0e580cfc7746f3fb910bf0e8f6378e1" + "address": "0xdedb92913a25abe1f7bcdd85d8a344a43b398b67" } } }, @@ -119,7 +119,7 @@ "variables": { "contract": { "chainId": 11155111, - "address": "0x64c81210d0e580cfc7746f3fb910bf0e8f6378e1" + "address": "0xdedb92913a25abe1f7bcdd85d8a344a43b398b67" } } }, @@ -167,5 +167,26 @@ "variables": { "address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" } + }, + { + "id": "efp-follow-graph", + "query": "query EfpFollowGraph($address: Address!) {\n account(by: { address: $address }) {\n efp {\n # The validated primary list, or null if unset/unvalidated.\n primaryList { tokenId }\n # following/followers are validated and block/mute-filtered, so the\n # edges are full Accounts you can walk straight into ENS names.\n following(first: 10) {\n totalCount\n edges {\n node {\n address\n resolve {\n primaryName(by: { chainName: ETHEREUM }) {\n name { beautified }\n }\n }\n }\n }\n }\n followers(first: 10) {\n totalCount\n edges { node { address } }\n }\n }\n }\n}", + "variables": { + "address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" + } + }, + { + "id": "efp-who-follows", + "query": "# Raw record view: includes block/mute and non-primary lists — inspect `tags`.\n# For the validated social graph, prefer Account.efp.followers instead.\nquery EfpWhoFollows($address: Address!) {\n efp {\n listRecords(where: { recordData: $address }, first: 25) {\n totalCount\n edges {\n node {\n tags\n list { tokenId user }\n }\n }\n }\n }\n}", + "variables": { + "address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" + } + }, + { + "id": "efp-list", + "query": "query EfpList($tokenId: TokenId!) {\n efp {\n list(by: { tokenId: $tokenId }) {\n tokenId\n owner\n user\n manager\n storageLocation { chainId address slot }\n # Raw records (any tag, no primary-list validation).\n records(first: 25) {\n totalCount\n edges { node { recordData tags } }\n }\n }\n }\n}", + "variables": { + "tokenId": "1" + } } ] diff --git a/docs/ensnode.io/src/data/omnigraph-examples/responses.json b/docs/ensnode.io/src/data/omnigraph-examples/responses.json index febc201df..44b8a295b 100644 --- a/docs/ensnode.io/src/data/omnigraph-examples/responses.json +++ b/docs/ensnode.io/src/data/omnigraph-examples/responses.json @@ -6470,5 +6470,569 @@ } } } + }, + "efp-follow-graph": { + "data": { + "account": { + "efp": { + "primaryList": { + "tokenId": "6509" + }, + "following": { + "totalCount": 10, + "edges": [ + { + "node": { + "address": "0x1b63142628311395ceafeea5667e7c9026c862ca", + "resolve": { + "primaryName": { + "name": { + "beautified": "tgerring.eth" + } + } + } + } + }, + { + "node": { + "address": "0x2b888954421b424c5d3d9ce9bb67c9bd47537d12", + "resolve": { + "primaryName": { + "name": { + "beautified": "lefteris.eth" + } + } + } + } + }, + { + "node": { + "address": "0x50ec05ade8280758e2077fcbc08d878d4aef79c3", + "resolve": { + "primaryName": { + "name": { + "beautified": "hayden.eth" + } + } + } + } + }, + { + "node": { + "address": "0x54becc7560a7be76d72ed76a1f5fee6c5a2a7ab6", + "resolve": { + "primaryName": { + "name": { + "beautified": "simona.eth" + } + } + } + } + }, + { + "node": { + "address": "0x5b76f5b8fc9d700624f78208132f91ad4e61a1f0", + "resolve": { + "primaryName": { + "name": { + "beautified": "barmstrong.eth" + } + } + } + } + }, + { + "node": { + "address": "0x648aa14e4424e0825a5ce739c8c68610e143fb79", + "resolve": { + "primaryName": { + "name": { + "beautified": "sassal.eth" + } + } + } + } + }, + { + "node": { + "address": "0x983110309620d911731ac0932219af06091b6744", + "resolve": { + "primaryName": { + "name": { + "beautified": "brantly.eth" + } + } + } + } + }, + { + "node": { + "address": "0xb8c2c29ee19d8307cb7255e1cd9cbde883a267d5", + "resolve": { + "primaryName": { + "name": { + "beautified": "nick.eth" + } + } + } + } + }, + { + "node": { + "address": "0xd7029bdea1c17493893aafe29aad69ef892b8ff2", + "resolve": { + "primaryName": { + "name": null + } + } + } + }, + { + "node": { + "address": "0xf56345338cb4cddaf915ebef3bfde63e70fe3053", + "resolve": { + "primaryName": { + "name": { + "beautified": "bored.eth" + } + } + } + } + } + ] + }, + "followers": { + "edges": [ + { + "node": { + "address": "0x00000066ced4c62a0f740de74eb08bc2a1552764" + } + }, + { + "node": { + "address": "0x000000dcf1190af44f7149b85299f18ce7221024" + } + }, + { + "node": { + "address": "0x000066b5a8e1e35b7e2f64e24715829647f80000" + } + }, + { + "node": { + "address": "0x00076993122809055293d8210a60d8265a7e17f3" + } + }, + { + "node": { + "address": "0x000f16ca1e73a0b6b10e1ed4cc5f8232336c1482" + } + }, + { + "node": { + "address": "0x0011e9582ac18d9b80204817eaa2e3b544e727aa" + } + }, + { + "node": { + "address": "0x00173c4cb23e6d876fcb036ba954a2f9cfcafa19" + } + }, + { + "node": { + "address": "0x00197ff7abdac8ad42152ff9d5bd9e521701a944" + } + }, + { + "node": { + "address": "0x002153708f11f2651215059eea30820ee4d49ff3" + } + }, + { + "node": { + "address": "0x003e7b75ff09615ad4a6f763dc7d28e48ed513d9" + } + } + ], + "totalCount": 5338 + } + } + } + } + }, + "efp-who-follows": { + "data": { + "efp": { + "listRecords": { + "totalCount": 6085, + "edges": [ + { + "node": { + "tags": [], + "list": null + } + }, + { + "node": { + "tags": [], + "list": null + } + }, + { + "node": { + "tags": [], + "list": null + } + }, + { + "node": { + "tags": [], + "list": null + } + }, + { + "node": { + "tags": ["top8"], + "list": { + "tokenId": "2400", + "user": "0x9760721a78fcff74a61440a06dbcb2a0ab2e4f89" + } + } + }, + { + "node": { + "tags": [], + "list": { + "tokenId": "31294", + "user": "0x3a4bbb42c18e1bb24edbd6e00aef4ea023da960c" + } + } + }, + { + "node": { + "tags": [], + "list": null + } + }, + { + "node": { + "tags": [], + "list": null + } + }, + { + "node": { + "tags": [], + "list": { + "tokenId": "39897", + "user": "0x8e79ef9e545fa14e205d89970d50e7caa3456683" + } + } + }, + { + "node": { + "tags": [], + "list": { + "tokenId": "388", + "user": "0xbe4f0cdf3834bd876813a1037137dcfad79acd99" + } + } + }, + { + "node": { + "tags": [], + "list": { + "tokenId": "55439", + "user": "0xe4ab754481e555fdd76d93d85b42dcbf56a8ab1e" + } + } + }, + { + "node": { + "tags": [], + "list": { + "tokenId": "39191", + "user": "0xff2d23446f7726da0512c19525262612c4e83d27" + } + } + }, + { + "node": { + "tags": [], + "list": null + } + }, + { + "node": { + "tags": [], + "list": null + } + }, + { + "node": { + "tags": [], + "list": { + "tokenId": "16149", + "user": "0x225f137127d9067788314bc7fcc1f36746a3c3b5" + } + } + }, + { + "node": { + "tags": [], + "list": { + "tokenId": "33148", + "user": "0x3ce7241a8c5c291e50a0d027dd9badff9db1fd7c" + } + } + }, + { + "node": { + "tags": [], + "list": { + "tokenId": "5056", + "user": "0x52fc2a810b96b333c303e48f95f6f88a2acf5d69" + } + } + }, + { + "node": { + "tags": [], + "list": null + } + }, + { + "node": { + "tags": [], + "list": { + "tokenId": "52365", + "user": "0xb5a9b9e2aec901d1b857138fc93c932f350a60ed" + } + } + }, + { + "node": { + "tags": [], + "list": { + "tokenId": "55011", + "user": "0x2a811f1e11636c144a2a062d3d402245a43d4074" + } + } + }, + { + "node": { + "tags": [], + "list": null + } + }, + { + "node": { + "tags": [], + "list": { + "tokenId": "45610", + "user": "0x7f7720bdb2cb5c13dd30a0c8ab8d0dd553b31caa" + } + } + }, + { + "node": { + "tags": [], + "list": { + "tokenId": "43738", + "user": "0xdf095ca41af452ed9ed390d8fac260fbdad20976" + } + } + }, + { + "node": { + "tags": [], + "list": null + } + }, + { + "node": { + "tags": [], + "list": { + "tokenId": "45079", + "user": "0x2e6728f76d949753f2e7dcee33f8136cf7e4e5f0" + } + } + } + ] + } + } + } + }, + "efp-list": { + "data": { + "efp": { + "list": { + "tokenId": "1", + "owner": "0xc983ebc9db969782d994627bdffec0ae6efee1b3", + "user": "0xc983ebc9db969782d994627bdffec0ae6efee1b3", + "manager": "0xc983ebc9db969782d994627bdffec0ae6efee1b3", + "storageLocation": { + "chainId": 8453, + "address": "0x41aa48ef3c0446b46a5b1cc6337ff3d3716e2a33", + "slot": "0x5659892f4e1f565ebd9f7504a28177ca927325e4104d0c6787c7e8a00dcda60c" + }, + "records": { + "totalCount": 452, + "edges": [ + { + "node": { + "recordData": "0x0000ce08fa224696a819877070bf378e8b131acf", + "tags": [] + } + }, + { + "node": { + "recordData": "0x000f16ca1e73a0b6b10e1ed4cc5f8232336c1482", + "tags": [] + } + }, + { + "node": { + "recordData": "0x009ae2d1a7240cfc89095fab06ed24f1641d9169", + "tags": [] + } + }, + { + "node": { + "recordData": "0x00c7ecc4187f239e09d4cfea5abe6d3c18f923bc", + "tags": [] + } + }, + { + "node": { + "recordData": "0x021021ccee934b346160342f8d7f59f514c08c56", + "tags": [] + } + }, + { + "node": { + "recordData": "0x025376e7e7f161a198fb5fc90a220a553836d11a", + "tags": [] + } + }, + { + "node": { + "recordData": "0x027fc636db3ced7342581baed6eee79f7d1123c3", + "tags": [] + } + }, + { + "node": { + "recordData": "0x02c5b7c40daaaf3aac3dd2ff39013cdd0f1fb4ae", + "tags": [] + } + }, + { + "node": { + "recordData": "0x0331969e189d63fbc31d771bb04ab44227d748d8", + "tags": [] + } + }, + { + "node": { + "recordData": "0x035ebd096afa6b98372494c7f08f3402324117d3", + "tags": [] + } + }, + { + "node": { + "recordData": "0x041d311ac26e478ea6e1753ec354676c452db9d8", + "tags": [] + } + }, + { + "node": { + "recordData": "0x0446bc2beabb16aa542cd99eaff3e63af37125b4", + "tags": [] + } + }, + { + "node": { + "recordData": "0x049569adb8a1e8a9349e9f1111c7b7993a4612eb", + "tags": [] + } + }, + { + "node": { + "recordData": "0x054becf10c79afa4e60f75b187837be9fb83867d", + "tags": [] + } + }, + { + "node": { + "recordData": "0x05977b2fb9b7ab3f3733b34350044a01a388579c", + "tags": [] + } + }, + { + "node": { + "recordData": "0x05a1ff0a32bc24265bcb39499d0c5d9a6cb2011c", + "tags": [] + } + }, + { + "node": { + "recordData": "0x06ce84f051ce93c2fe99344bc8c9d2bf92e64409", + "tags": [] + } + }, + { + "node": { + "recordData": "0x074470b9a32f68de86fac393a10d5cea01c54269", + "tags": [] + } + }, + { + "node": { + "recordData": "0x0865c5de7fe349aa5b7f6ef721f8c2b9cfe5e40d", + "tags": [] + } + }, + { + "node": { + "recordData": "0x092ad3842dced68b5b6ef9cc602be66924baecdd", + "tags": [] + } + }, + { + "node": { + "recordData": "0x09a78e1abe7a893c43986dbf7864c79d03860bde", + "tags": [] + } + }, + { + "node": { + "recordData": "0x0c077910e1dc0b5bea1054cce14143e0971f95ce", + "tags": [] + } + }, + { + "node": { + "recordData": "0x0d1f2c041b7f9702903b89057c1f57f1f01a79af", + "tags": [] + } + }, + { + "node": { + "recordData": "0x0d3f5a7a1ee78e743e25c18e66942fcbcd84ccad", + "tags": [] + } + }, + { + "node": { + "recordData": "0x0de5a5d50453e702ad66d3fcf7c986f408019ffd", + "tags": [] + } + } + ] + } + } + } + } } } diff --git a/docs/ensnode.io/src/data/omnigraph-examples/schema.graphql b/docs/ensnode.io/src/data/omnigraph-examples/schema.graphql index 9fbe48746..ed4321927 100644 --- a/docs/ensnode.io/src/data/omnigraph-examples/schema.graphql +++ b/docs/ensnode.io/src/data/omnigraph-examples/schema.graphql @@ -34,6 +34,11 @@ type Account { where: AccountDomainsWhereInput ): AccountDomainsConnection + """ + This Account's Ethereum Follow Protocol (EFP) presence: its lists, validated primary list, and account metadata. Null when the connected ENSIndexer does not have the `efp` plugin enabled. + """ + efp: AccountEfp + """ All Events for which this Account is the HCA-aware `sender` (i.e. `Event.sender`). """ @@ -50,6 +55,17 @@ type Account { """ id: Address! + """ + The Names whose indexed `addr()` record points at this Account, optionally scoped to a single CoinType. Reflects literally-indexed, Canonical Domains only: records whose node has no Canonical Domain are omitted. + """ + nameReferences( + after: String + before: String + first: Int + last: Int + where: AccountNameReferencesWhereInput + ): AccountNameReferencesConnection + """ The Permissions granted to this Account, optionally filtered to Permissions in a specific contract. """ @@ -132,6 +148,85 @@ input AccountDomainsWhereInput { version: ENSProtocolVersion } +""" +An account's Ethereum Follow Protocol (EFP) presence. +""" +type AccountEfp { + """ + The accounts that follow this account: those whose validated primary EFP list holds this account as a non-`block`/`mute` record. + """ + followers(after: String, before: String, first: Int, last: Int): AccountEfpFollowersConnection + + """ + The accounts this account follows: the address records in its validated primary EFP list, excluding `block`/`mute`-tagged records. Empty when the account has no validated primary list. + """ + following(after: String, before: String, first: Int, last: Int): AccountEfpFollowingConnection + + """ + The EFP lists this account is the `user` of (the lists representing it). + """ + lists(after: String, before: String, first: Int, last: Int): AccountEfpListsConnection + + """ + Get one of this account's EFP account-metadata values by key (e.g. "primary-list"). + """ + metadata(key: String!): EfpAccountMetadata + + """ + All of this account's EFP account-metadata entries. + """ + metadatas(after: String, before: String, first: Int, last: Int): AccountEfpMetadatasConnection + + """ + The account's validated primary EFP list: the list named by its `primary-list` metadata, returned only if that list's `user` role matches the account (the EFP two-step Primary List validation). Null if unset or unvalidated. + """ + primaryList: EfpList +} + +type AccountEfpFollowersConnection { + edges: [AccountEfpFollowersConnectionEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type AccountEfpFollowersConnectionEdge { + cursor: String! + node: Account! +} + +type AccountEfpFollowingConnection { + edges: [AccountEfpFollowingConnectionEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type AccountEfpFollowingConnectionEdge { + cursor: String! + node: Account! +} + +type AccountEfpListsConnection { + edges: [AccountEfpListsConnectionEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type AccountEfpListsConnectionEdge { + cursor: String! + node: EfpList! +} + +type AccountEfpMetadatasConnection { + edges: [AccountEfpMetadatasConnectionEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type AccountEfpMetadatasConnectionEdge { + cursor: String! + node: EfpAccountMetadata! +} + type AccountEventsConnection { edges: [AccountEventsConnectionEdge!]! pageInfo: PageInfo! @@ -179,6 +274,27 @@ input AccountIdInput { chainId: ChainId! } +type AccountNameReferencesConnection { + edges: [AccountNameReferencesConnectionEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type AccountNameReferencesConnectionEdge { + cursor: String! + node: NameReference! +} + +""" +Filter for Account.nameReferences. +""" +input AccountNameReferencesWhereInput { + """ + If set, scopes matches to a single CoinType. When omitted, returns matches across all CoinTypes. + """ + coinType: CoinType +} + type AccountPermissionsConnection { edges: [AccountPermissionsConnectionEdge!]! pageInfo: PageInfo! @@ -379,6 +495,19 @@ CoinType represents an enssdk#CoinType. """ scalar CoinType +""" +The ENSIP-7 contenthash protocol type (e.g. IPFS, IPNS, Swarm, Arweave). +""" +enum ContenthashProtocol { + ARWEAVE + IPFS + IPNS + ONION + ONION3 + SKYNET + SWARM +} + """ DogecoinAddress represents a Base58Check-encoded Dogecoin address (coin type 3). """ @@ -561,6 +690,11 @@ type DomainProfile { """ avatar: ProfileAvatar + """ + The interpreted ENSIP-7 contenthash on the profile of an ENS name. Returns null when the raw contenthash record is unset, empty, or cannot be decoded (unknown codec or malformed bytes). See https://docs.ens.domains/ensip/7. + """ + contenthash: ProfileContenthash + """ The interpreted description on the profile of an ENS name, or null when unset. """ @@ -1166,6 +1300,269 @@ type ENSv2RegistryReservation implements Registration { unregistrant: Account } +""" +An EFP `(address, key) -> value` account-metadata entry (e.g. "primary-list"). +""" +type EfpAccountMetadata { + """ + The account this metadata belongs to. + """ + address: Address! + + """ + The CAIP-10 account id of the AccountMetadata contract. + """ + contract: AccountId! + + """ + When this metadata entry was first indexed (Unix timestamp, seconds). + """ + createdAt: BigInt! + id: ID! + + """ + The metadata key (UTF-8 string). + """ + key: String! + + """ + When this metadata entry was last updated (Unix timestamp, seconds). + """ + updatedAt: BigInt! + + """ + The metadata value (raw bytes). + """ + value: Hex! +} + +""" +An EFP list NFT (a ListRegistry token) and the records it holds. +""" +type EfpList { + """ + When this list was first indexed (Unix timestamp, seconds). + """ + createdAt: BigInt! + + """ + The address allowed to administer this list. + """ + manager: Address + + """ + The list NFT's AccountId. + """ + nft: AccountId! + + """ + The current ERC-721 owner of the list NFT. + """ + owner: Address! + + """ + The records currently in this list (the addresses it follows). + """ + records(after: String, before: String, first: Int, last: Int): EfpListRecordsConnection + + """ + The decoded onchain location of this list's records, or null if not known. + """ + storageLocation: EfpListStorageLocation + + """ + The ERC-721 TokenId of the list NFT. + """ + tokenId: TokenId! + + """ + When this list was last updated (Unix timestamp, seconds). + """ + updatedAt: BigInt! + + """ + The address allowed to post records to this list. + """ + user: Address +} + +""" +Identify an EFP list by its NFT token id. +""" +input EfpListByInput { + """ + The ERC-721 token id of the list NFT. + """ + tokenId: TokenId! +} + +""" +A single record within an EFP list (an address it follows), with its tags. +""" +type EfpListRecord { + """ + The Account this record points to (its `recordData`). Always resolvable: an Account exists for any address (see `query.account`), so a record's target can always be walked into its ENS names and own EFP presence. + """ + account: Account! + + """ + The CAIP-10 account id of the ListRecords contract holding this record. + """ + contract: AccountId! + + """ + When this record was first indexed (Unix timestamp, seconds). + """ + createdAt: BigInt! + id: ID! + + """ + The EFP list this record belongs to. + """ + list: EfpList + + """ + Canonical record bytes (version | type | address), 0x-prefixed (exactly 22 bytes), with any trailing junk after the 20-byte address truncated. + """ + record: Hex! + + """ + The followed/target address (the record's 20-byte payload). EFP indexes only address records (recordType 1). + """ + recordData: Address! + + """ + The EFP record type (1 = address). + """ + recordType: Int! + + """ + The list's storage slot (bytes32) within the ListRecords contract. + """ + slot: Hex! + + """ + UTF-8 tags attached to this record (e.g. "close-friend", "block"). + """ + tags: [String!]! +} + +type EfpListRecordsConnection { + edges: [EfpListRecordsConnectionEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type EfpListRecordsConnectionEdge { + cursor: String! + node: EfpListRecord! +} + +""" +Filter EFP list records. +""" +input EfpListRecordsWhereInput { + """ + The target address of an address record (recordType 1). Filtering by this answers 'which lists follow this address?'. + """ + recordData: Address + + """ + The EFP record type (1 = address). + """ + recordType: Int +} + +""" +The decoded onchain location of a list's records: a (chainId, ListRecords contract address, slot) tuple. +""" +type EfpListStorageLocation { + """ + The ListRecords contract address holding the list's records. + """ + address: Address! + chainId: ChainId! + + """ + The list's storage slot (bytes32) within the ListRecords contract. + """ + slot: Hex! +} + +""" +Filter EFP lists by their owner, user, or manager address. +""" +input EfpListsWhereInput { + """ + The address allowed to administer the list. + """ + manager: Address + + """ + The ERC-721 owner of the list NFT. + """ + owner: Address + + """ + The address allowed to post records. + """ + user: Address +} + +""" +Queries for Ethereum Follow Protocol (EFP) data. +""" +type EfpQuery { + """ + Get an EFP list by its NFT token id. + """ + list(by: EfpListByInput!): EfpList + + """ + Find EFP list records. Filter by `recordData` to answer 'which lists follow this address?'. + """ + listRecords( + after: String + before: String + first: Int + last: Int + where: EfpListRecordsWhereInput + ): EfpQueryListRecordsConnection + + """ + Find EFP lists, optionally filtered by owner / user / manager. + """ + lists( + after: String + before: String + first: Int + last: Int + where: EfpListsWhereInput + ): EfpQueryListsConnection +} + +type EfpQueryListRecordsConnection { + edges: [EfpQueryListRecordsConnectionEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type EfpQueryListRecordsConnectionEdge { + cursor: String! + node: EfpListRecord! +} + +type EfpQueryListsConnection { + edges: [EfpQueryListsConnectionEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type EfpQueryListsConnectionEdge { + cursor: String! + node: EfpList! +} + """ Email represents a validated contact email address. """ @@ -1432,6 +1829,31 @@ input NameOrNodeInput @oneOf { node: Node } +""" +A Name whose indexed `addr()` record points at an Account. +""" +type NameReference { + """ + The CoinType of the matching `addr()` record. + """ + coinType: CoinType! + + """ + The canonical Domain whose `addr(coinType)` record points at this Account. + """ + domain: Domain! + + """ + Whether this name is the ENSIP-19 Primary Name of this Account for this `coinType`. i.e. whether reverse resolution of `(address, coinType)` resolves to this exact name. Protocol Accelerated when supported. + """ + match: Boolean! + + """ + The Resolver holding the matching `addr()` record. + """ + resolver: Resolver! +} + """ A NameWrapperRegistration represents a Registration initiated by the ENSv1 NameWrapper. """ @@ -1816,6 +2238,31 @@ type ProfileAvatar { httpUrl: String } +""" +The interpreted ENSIP-7 contenthash on the profile of an ENS name. +""" +type ProfileContenthash { + """ + The decoded, human-readable content identifier (e.g. a CID for IPFS, transaction ID for Arweave). + """ + decoded: String! + + """ + The default public HTTP gateway URL for fetching this content in a browser (e.g. https://ipfs.io/ipfs/…). Null for protocols with no well-known public gateway (onion, skynet). + """ + httpUrl: String + + """ + The ENSIP-7 contenthash protocol type (e.g. IPFS, IPNS, Swarm, Arweave). + """ + protocolType: ContenthashProtocol! + + """ + The canonical protocol-native URI (e.g. "ipfs://bafy…", "ar://…", "bzz://…"). + """ + uri: String! +} + """ The interpreted header image on the profile of an ENS name. """ @@ -1904,6 +2351,11 @@ type Query { where: DomainsWhereInput! ): QueryDomainsConnection + """ + Ethereum Follow Protocol (EFP) queries. Null when the connected ENSIndexer does not have the `efp` plugin enabled. + """ + efp: EfpQuery + """ Identify Permissions by ID or AccountId. """ @@ -2558,6 +3010,11 @@ type ThreeDNSRegistration implements Registration { unregistrant: Account } +""" +TokenId represents an enssdk#TokenId. +""" +scalar TokenId + """ A resolvable-but-unindexed Domain: not present in the index, but resolvable because an ancestor in its namegraph path has an ENSIP-10 wildcard Resolver (e.g. off-chain / CCIP-Read names, unindexed 3DNS names, wildcard subnames). """ diff --git a/docs/ensnode.io/src/data/omnigraph-examples/snapshot.json b/docs/ensnode.io/src/data/omnigraph-examples/snapshot.json index dae7fee69..34791c9ff 100644 --- a/docs/ensnode.io/src/data/omnigraph-examples/snapshot.json +++ b/docs/ensnode.io/src/data/omnigraph-examples/snapshot.json @@ -1,7 +1,7 @@ { - "version": "v1.15.2", - "commit": "97b5093a6", - "sdkVersion": "1.15.2", - "schemaTag": "v1.15.2", - "snapshottedAt": "2026-06-10" + "version": "v1.16.0", + "commit": "9fd6918dc", + "sdkVersion": "1.16.0", + "schemaTag": "v1.16.0", + "snapshottedAt": "2026-06-19" } diff --git a/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts b/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts index 1dee7a614..bf4b392c7 100644 --- a/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts +++ b/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts @@ -3,6 +3,7 @@ import { asInterpretedName, toNormalizedAddress } from "enssdk"; import { DatasourceNames, ENSNamespaceIds } from "@ensnode/datasources"; import { accounts } from "@ensnode/integration-test-env/devnet"; +import { PluginName } from "../ensindexer/config/types"; import { getDatasourceContract } from "../shared/datasource-contract"; import type { NamespaceSpecificValue } from "../shared/namespace-specific-value"; @@ -63,6 +64,8 @@ const SEPOLIA_V2_RESOLVER_WITH_RECORDS = { export type GraphqlApiExampleQuery = { id: string; + /** The plugin that must be enabled on the connected ENSIndexer for this query to return data. */ + plugin: PluginName; query: string; variables: NamespaceSpecificValue>; }; @@ -81,6 +84,7 @@ export const GRAPHQL_API_EXAMPLE_QUERIES: GraphqlApiExampleQuery[] = [ //////////////// { id: "hello-world", + plugin: PluginName.Unigraph, query: `query HelloWorld($address: Address!) { # Lookup an Account by address. account(by: { address: $address }) { @@ -128,6 +132,7 @@ export const GRAPHQL_API_EXAMPLE_QUERIES: GraphqlApiExampleQuery[] = [ ///////////////// { id: "find-domains", + plugin: PluginName.Unigraph, query: ` query FindDomains( $name: DomainsNameFilter! @@ -165,6 +170,7 @@ query FindDomains( { id: "domain-by-name", + plugin: PluginName.Unigraph, query: ` query DomainByName($name: InterpretedName!) { domain(by: { name: $name }) { @@ -192,6 +198,7 @@ query DomainByName($name: InterpretedName!) { //////////////////////////////// { id: "domain-by-name-type-condition", + plugin: PluginName.Unigraph, query: ` query DomainByName($name: InterpretedName!) { domain(by: {name: $name}) { @@ -218,6 +225,7 @@ query DomainByName($name: InterpretedName!) { /////////////////////// { id: "domain-registration", + plugin: PluginName.Unigraph, query: ` query DomainRegistration($name: InterpretedName!) { domain(by: { name: $name }) { @@ -264,6 +272,7 @@ query DomainRegistration($name: InterpretedName!) { //////////////////// { id: "domain-records", + plugin: PluginName.Unigraph, query: ` query DomainRecords($name: InterpretedName!) { domain(by: {name: $name}) { @@ -300,6 +309,7 @@ query DomainRecords($name: InterpretedName!) { { id: "domain-profile", + plugin: PluginName.Unigraph, query: ` query DomainProfile($name: InterpretedName!) { domain(by: {name: $name}) { @@ -341,6 +351,7 @@ query DomainProfile($name: InterpretedName!) { { id: "domain-profile-and-records", + plugin: PluginName.Unigraph, query: ` query DomainProfileAndRecords($name: InterpretedName!) { domain(by: { name: $name }) { @@ -388,6 +399,7 @@ query DomainProfileAndRecords($name: InterpretedName!) { //////////////////////////////////// { id: "offchain-name", + plugin: PluginName.Unigraph, query: ` query OffchainName($name: InterpretedName!) { domain(by: { name: $name }) { @@ -418,6 +430,7 @@ query OffchainName($name: InterpretedName!) { ////////////////////// { id: "domain-subdomains", + plugin: PluginName.Unigraph, query: ` query DomainSubdomains($name: InterpretedName!) { domain(by: {name: $name}) { @@ -446,6 +459,7 @@ query DomainSubdomains($name: InterpretedName!) { //////////////////////////////////// { id: "domain-subdomains-recently-registered", + plugin: PluginName.Unigraph, query: ` query RecentlyRegisteredSubdomains($name: InterpretedName!) { domain(by: {name: $name}) { @@ -467,6 +481,7 @@ query RecentlyRegisteredSubdomains($name: InterpretedName!) { //////////////////////// { id: "subdomains-pagination", + plugin: PluginName.Unigraph, query: ` query SubdomainsPagination($first: Int!, $after: String) { domain(by: { name: "eth" }) { @@ -493,6 +508,7 @@ query SubdomainsPagination($first: Int!, $after: String) { ///////////////// { id: "domain-events", + plugin: PluginName.Unigraph, query: ` query DomainEvents($name: InterpretedName!) { domain(by: {name: $name}) { @@ -522,6 +538,7 @@ query DomainEvents($name: InterpretedName!) { //////////////////// { id: "domains-by-address", + plugin: PluginName.Unigraph, query: ` query AccountDomains( $address: Address! @@ -549,6 +566,7 @@ query AccountDomains( ///////////////////////// { id: "account-primary-names", + plugin: PluginName.Unigraph, query: ` query AccountPrimaryNames($address: Address!) { account(by: { address: $address }) { @@ -579,6 +597,7 @@ query AccountPrimaryNames($address: Address!) { }, { id: "account-primary-name-records", + plugin: PluginName.Unigraph, query: ` query AccountPrimaryNameRecords($address: Address!) { account(by: { address: $address }) { @@ -611,6 +630,7 @@ query AccountPrimaryNameRecords($address: Address!) { //////////////////// { id: "account-events", + plugin: PluginName.Unigraph, query: ` query AccountEvents( $address: Address! @@ -631,6 +651,7 @@ query AccountEvents( ///////////////////// { id: "registry-domains", + plugin: PluginName.Unigraph, query: ` query RegistryDomains( $registry: AccountIdInput! @@ -658,6 +679,7 @@ query RegistryDomains( //////////////////////////// { id: "permissions-by-contract", + plugin: PluginName.Unigraph, query: ` query PermissionsByContract( $contract: AccountIdInput! @@ -695,6 +717,7 @@ query PermissionsByContract( //////////////////////// { id: "permissions-by-user", + plugin: PluginName.Unigraph, query: ` query PermissionsByUser($address: Address!) { account(by: { address: $address }) { @@ -719,6 +742,7 @@ query PermissionsByUser($address: Address!) { ////////////////////////////////// { id: "account-resolver-permissions", + plugin: PluginName.Unigraph, query: ` query AccountResolverPermissions($address: Address!) { account(by: { address: $address }) { @@ -746,6 +770,7 @@ query AccountResolverPermissions($address: Address!) { ////////////////////////////// { id: "domain-resolver", + plugin: PluginName.Unigraph, query: ` query DomainResolver($name: InterpretedName!) { domain(by: { name: $name }) { @@ -774,6 +799,7 @@ query DomainResolver($name: InterpretedName!) { //////////////////////// { id: "resolver-by-address", + plugin: PluginName.Unigraph, query: ` query ResolverByAddress($contract: AccountIdInput!) { resolver(by: { contract: $contract }) { @@ -807,6 +833,7 @@ query ResolverByAddress($contract: AccountIdInput!) { ////////////// { id: "namegraph", + plugin: PluginName.Unigraph, query: ` query Namegraph { domain(by: { name: "eth" }) { @@ -832,6 +859,7 @@ query Namegraph { ///////////////////////////// { id: "account-migrated-names", + plugin: PluginName.Unigraph, query: ` query AccountMigratedNames($address: Address!) { account(by: { address: $address }) { @@ -849,6 +877,7 @@ query AccountMigratedNames($address: Address!) { }, { id: "eth-by-version", + plugin: PluginName.Unigraph, query: ` query GetEthDomains { domains(where: { name: { eq: "eth" } }) { @@ -864,6 +893,7 @@ query GetEthDomains { }, { id: "accelerate-resolve", + plugin: PluginName.Unigraph, query: ` query AccelerateResolve($address: Address!) { account(by: { address: $address }) { @@ -887,6 +917,97 @@ query AccelerateResolve($address: Address!) { [ENSNamespaceIds.SepoliaV2]: { address: SEPOLIA_V2_ACCOUNT_WITH_V1_AND_V2 }, }, }, + + /////////////////////////////////////// + // Ethereum Follow Protocol (EFP) + // + // Require the `efp` plugin on the connected ENSIndexer; `Query.efp` / `Account.efp` + // are null otherwise. Indexed only in the mainnet namespace. + /////////////////////////////////////// + { + id: "efp-follow-graph", + plugin: PluginName.EFP, + query: ` +query EfpFollowGraph($address: Address!) { + account(by: { address: $address }) { + efp { + # The validated primary list, or null if unset/unvalidated. + primaryList { tokenId } + # following/followers are validated and block/mute-filtered, so the + # edges are full Accounts you can walk straight into ENS names. + following(first: 10) { + totalCount + edges { + node { + address + resolve { + primaryName(by: { chainName: ETHEREUM }) { + name { beautified } + } + } + } + } + } + followers(first: 10) { + totalCount + edges { node { address } } + } + } + } +}`, + variables: { + default: { address: VITALIK_ADDRESS }, + }, + }, + + { + id: "efp-who-follows", + plugin: PluginName.EFP, + query: ` +# Raw record view: includes block/mute and non-primary lists — inspect \`tags\`. +# For the validated social graph, prefer Account.efp.followers instead. +query EfpWhoFollows($address: Address!) { + efp { + listRecords(where: { recordData: $address }, first: 25) { + totalCount + edges { + node { + tags + list { tokenId user } + } + } + } + } +}`, + variables: { + default: { address: VITALIK_ADDRESS }, + }, + }, + + { + id: "efp-list", + plugin: PluginName.EFP, + query: ` +query EfpList($tokenId: TokenId!) { + efp { + list(by: { tokenId: $tokenId }) { + tokenId + owner + user + manager + storageLocation { chainId address slot } + # Raw records (any tag, no primary-list validation). + records(first: 25) { + totalCount + edges { node { recordData tags } } + } + } + } +}`, + variables: { + default: { tokenId: "1" }, + }, + }, ]; const graphqlApiExampleQueryById = new Map( diff --git a/packages/ensskills/scripts/generate.ts b/packages/ensskills/scripts/generate.ts index 076b480d5..13703ee3b 100644 --- a/packages/ensskills/scripts/generate.ts +++ b/packages/ensskills/scripts/generate.ts @@ -27,15 +27,18 @@ import prettier from "prettier"; const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); const SDL_PATH = resolve(SCRIPT_DIR, "../../enssdk/src/omnigraph/generated/schema.graphql"); const SKILL_PATH = resolve(SCRIPT_DIR, "../skills/omnigraph/SKILL.md"); +const EFP_SKILL_PATH = resolve(SCRIPT_DIR, "../skills/efp-protocol/SKILL.md"); const EXAMPLE_QUERIES_PATH = resolve( SCRIPT_DIR, "../../ensnode-sdk/src/omnigraph-api/example-queries.ts", ); +const PLUGIN_TYPES_PATH = resolve(SCRIPT_DIR, "../../ensnode-sdk/src/ensindexer/config/types.ts"); const ENSCLI_SKILL_PATH = resolve(SCRIPT_DIR, "../skills/enscli/SKILL.md"); const ENSCLI_EXAMPLE_COMMANDS_PATH = resolve(SCRIPT_DIR, "../../enscli/src/example-commands.ts"); interface ExampleQuery { id: string; + plugin: string; query: string; variables: { default: Record }; } @@ -125,13 +128,20 @@ function buildSchemaReference(schema: GraphQLSchema): string { return sections.join("\n\n"); } -async function buildExamples(): Promise { +/** + * Renders the example queries tagged with `plugin` into a markdown block. The base `omnigraph` skill + * gets the `unigraph` queries; the `efp-protocol` skill gets the `efp` queries — so each skill ships + * only the examples that run against the plugin it documents. + */ +async function buildExamples(plugin: string): Promise { // load dynamically to avoid tsconfig root error const { GRAPHQL_API_EXAMPLE_QUERIES } = (await import(EXAMPLE_QUERIES_PATH)) as { GRAPHQL_API_EXAMPLE_QUERIES: ExampleQuery[]; }; // Skip "hello-world": it's the playground welcome blurb, not a reusable query pattern. - return GRAPHQL_API_EXAMPLE_QUERIES.filter((example) => example.id !== "hello-world") + return GRAPHQL_API_EXAMPLE_QUERIES.filter( + (example) => example.plugin === plugin && example.id !== "hello-world", + ) .map((example) => { const query = example.query.trim(); const variables = JSON.stringify(example.variables.default, null, 2); @@ -200,13 +210,33 @@ async function writeFormatted(path: string, content: string): Promise { } async function main(): Promise { + // load dynamically to avoid tsconfig root error + const { PluginName } = (await import(PLUGIN_TYPES_PATH)) as { + PluginName: { Unigraph: string; EFP: string }; + }; + const schema = buildSchema(readFileSync(SDL_PATH, "utf8")); let content = readFileSync(SKILL_PATH, "utf8"); content = replaceRegion(content, "SCHEMA", buildSchemaReference(schema), SKILL_PATH); - content = replaceRegion(content, "EXAMPLES", await buildExamples(), SKILL_PATH); + content = replaceRegion( + content, + "EXAMPLES", + await buildExamples(PluginName.Unigraph), + SKILL_PATH, + ); await writeFormatted(SKILL_PATH, content); console.log(`Updated ${SKILL_PATH}`); + let efpContent = readFileSync(EFP_SKILL_PATH, "utf8"); + efpContent = replaceRegion( + efpContent, + "EXAMPLES", + await buildExamples(PluginName.EFP), + EFP_SKILL_PATH, + ); + await writeFormatted(EFP_SKILL_PATH, efpContent); + console.log(`Updated ${EFP_SKILL_PATH}`); + let enscliContent = readFileSync(ENSCLI_SKILL_PATH, "utf8"); for (const [group, block] of await buildEnscliExamplesByGroup()) { enscliContent = replaceRegion(enscliContent, group, block, ENSCLI_SKILL_PATH); diff --git a/packages/ensskills/skills/efp-protocol/SKILL.md b/packages/ensskills/skills/efp-protocol/SKILL.md index 973ddfc98..eaf8fd16f 100644 --- a/packages/ensskills/skills/efp-protocol/SKILL.md +++ b/packages/ensskills/skills/efp-protocol/SKILL.md @@ -96,15 +96,20 @@ Prefer this for "who does X follow / who follows X" — it applies primary-list Run these with the **omnigraph**/**enscli** skill. They assume an instance with the `efp` plugin enabled. -### Following & followers, with names + + +### efp-follow-graph ```graphql query EfpFollowGraph($address: Address!) { account(by: { address: $address }) { efp { + # The validated primary list, or null if unset/unvalidated. primaryList { tokenId } + # following/followers are validated and block/mute-filtered, so the + # edges are full Accounts you can walk straight into ENS names. following(first: 10) { totalCount edges { @@ -133,10 +138,19 @@ query EfpFollowGraph($address: Address!) { } ``` -### Who follows this address (raw records, with tags) +Variables: + +```json +{ + "address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" +} +``` + +### efp-who-follows ```graphql # Raw record view: includes block/mute and non-primary lists — inspect `tags`. +# For the validated social graph, prefer Account.efp.followers instead. query EfpWhoFollows($address: Address!) { efp { listRecords(where: { recordData: $address }, first: 25) { @@ -155,7 +169,15 @@ query EfpWhoFollows($address: Address!) { } ``` -### A list by token id, with its records and storage location +Variables: + +```json +{ + "address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" +} +``` + +### efp-list ```graphql query EfpList($tokenId: TokenId!) { @@ -170,6 +192,7 @@ query EfpList($tokenId: TokenId!) { address slot } + # Raw records (any tag, no primary-list validation). records(first: 25) { totalCount edges { @@ -184,6 +207,16 @@ query EfpList($tokenId: TokenId!) { } ``` +Variables: + +```json +{ + "tokenId": "1" +} +``` + + + ## Related skills - **omnigraph** — the GraphQL API EFP is exposed through; author and run EFP queries there. From ad03dc2af276addf1f5a6fe438163be71e79dbbb Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 19 Jun 2026 14:34:40 -0700 Subject: [PATCH 2/5] chore: drop changesets --- .changeset/efp-example-queries.md | 5 ----- .changeset/efp-omnigraph-docs.md | 5 ----- .changeset/efp-skill-examples.md | 5 ----- 3 files changed, 15 deletions(-) delete mode 100644 .changeset/efp-example-queries.md delete mode 100644 .changeset/efp-omnigraph-docs.md delete mode 100644 .changeset/efp-skill-examples.md diff --git a/.changeset/efp-example-queries.md b/.changeset/efp-example-queries.md deleted file mode 100644 index 712ebcab8..000000000 --- a/.changeset/efp-example-queries.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@ensnode/ensnode-sdk": minor ---- - -Add EFP Omnigraph example queries (`efp-follow-graph`, `efp-who-follows`, `efp-list`) and tag every `GraphqlApiExampleQuery` with the `plugin` (`PluginName`) it requires. Existing queries are tagged `unigraph`; the new EFP queries are tagged `efp`. diff --git a/.changeset/efp-omnigraph-docs.md b/.changeset/efp-omnigraph-docs.md deleted file mode 100644 index b364d0b5f..000000000 --- a/.changeset/efp-omnigraph-docs.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@docs/ensnode": patch ---- - -Document EFP in the ENS Omnigraph API: a new "EFP" page covering how to enable the `efp` plugin and its API surface, standalone example-query pages (follow graph, who-follows, list) with an "EFP plugin required" tip, the `efp` plugin in the Existing Plugins table, and the `efp` plugin on the hosted Alpha instance. The Omnigraph examples snapshot is refreshed to v1.16.0. diff --git a/.changeset/efp-skill-examples.md b/.changeset/efp-skill-examples.md deleted file mode 100644 index ca18a1659..000000000 --- a/.changeset/efp-skill-examples.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"ensskills": patch ---- - -The `efp-protocol` skill's example queries are now generated from the shared ENSNode SDK example-query collection (an `AUTOGEN:EXAMPLES` region), filtered to the `efp` plugin. The base `omnigraph` skill's examples are now filtered to the `unigraph` plugin, so each skill ships only the examples that run against the plugin it documents. From 07080cd94a6d9832d70c07070133ceb320984b10 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 19 Jun 2026 14:46:28 -0700 Subject: [PATCH 3/5] docs: EFP SQL schema reference + category-driven example aside - Document the efp_* tables and indexes in their own section of the ENSDb Database Schemas reference. - Abstract the 'EFP plugin required' tip into a reusable ExampleRequirementAside component driven by an example's category; render it from OmnigraphStaticExampleSet so every example page shows the right requirement notice (or none) automatically. - Drop a stray backtick from the efp-who-follows query comment that broke generated TS snippet typechecking. --- .../ExampleRequirementAside.astro | 32 +++++++ .../organisms/OmnigraphStaticExampleSet.astro | 3 + .../omnigraph/examples/efp-follow-graph.mdx | 4 - .../integrate/omnigraph/examples/efp-list.mdx | 4 - .../omnigraph/examples/efp-who-follows.mdx | 4 - .../ensdb/concepts/database-schemas.mdx | 95 +++++++++++++++++++ .../src/data/omnigraph-examples/examples.json | 2 +- .../src/data/omnigraph-examples/snapshot.json | 2 +- .../src/omnigraph-api/example-queries.ts | 2 +- .../ensskills/skills/efp-protocol/SKILL.md | 2 +- 10 files changed, 134 insertions(+), 16 deletions(-) create mode 100644 docs/ensnode.io/src/components/molecules/omnigraph-static-example/ExampleRequirementAside.astro diff --git a/docs/ensnode.io/src/components/molecules/omnigraph-static-example/ExampleRequirementAside.astro b/docs/ensnode.io/src/components/molecules/omnigraph-static-example/ExampleRequirementAside.astro new file mode 100644 index 000000000..9809d5fef --- /dev/null +++ b/docs/ensnode.io/src/components/molecules/omnigraph-static-example/ExampleRequirementAside.astro @@ -0,0 +1,32 @@ +--- +// Renders a plugin-requirement Aside for an Omnigraph example, driven by the example's category. +// Example pages pass their `id`; the component looks the example up and renders the matching aside +// (or nothing) so requirement notices stay consistent and live next to the example definition. +import { Aside } from "@astrojs/starlight/components"; + +import { getOmnigraphExampleConfigById } from "@data/omnigraph-examples/config"; + +interface Props { + id: string; +} + +const { id } = Astro.props; +const category = getOmnigraphExampleConfigById(id)?.category; +--- + +{ + category === "EFP" && ( + + ) +} diff --git a/docs/ensnode.io/src/components/organisms/OmnigraphStaticExampleSet.astro b/docs/ensnode.io/src/components/organisms/OmnigraphStaticExampleSet.astro index 885e2a6b3..1e0a5d4a1 100644 --- a/docs/ensnode.io/src/components/organisms/OmnigraphStaticExampleSet.astro +++ b/docs/ensnode.io/src/components/organisms/OmnigraphStaticExampleSet.astro @@ -6,6 +6,7 @@ import StaticExampleCard from "@components/molecules/omnigraph-static-example/St import CurlStaticCodeExample from "@components/molecules/omnigraph-static-example/CurlStaticCodeExample.astro"; import SdkStaticCodeExample from "@components/molecules/omnigraph-static-example/SdkStaticCodeExample.astro"; import GraphqlStaticQueryExample from "@components/molecules/omnigraph-static-example/GraphqlStaticQueryExample.astro"; +import ExampleRequirementAside from "@components/molecules/omnigraph-static-example/ExampleRequirementAside.astro"; import { exampleTabClass } from "@components/molecules/omnigraph-static-example/constants"; import { resolveOmnigraphStaticExample } from "@lib/examples/omnigraph/resolve-static-example"; @@ -37,6 +38,8 @@ const { description } = getOmnigraphExampleById(id); const data = resolveOmnigraphStaticExample(id); --- + + { !hideDescription && (
diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/examples/efp-follow-graph.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/examples/efp-follow-graph.mdx index 48c7640cf..5f5420e43 100644 --- a/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/examples/efp-follow-graph.mdx +++ b/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/examples/efp-follow-graph.mdx @@ -4,8 +4,4 @@ title: EFP Follow Graph import OmnigraphStaticExampleSet from "@components/organisms/OmnigraphStaticExampleSet.astro"; -:::tip[EFP plugin required] -This query reads [Ethereum Follow Protocol (EFP)](/docs/integrate/omnigraph/efp) data, which requires the [`efp` plugin](/docs/integrate/omnigraph/efp) to be enabled on the connected ENSIndexer. It is enabled on the hosted [ENSNode 'Alpha'](/docs/hosted-instances#ensnode-alpha) instance. -::: - diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/examples/efp-list.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/examples/efp-list.mdx index b3dbf91e3..ab0fc7ab2 100644 --- a/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/examples/efp-list.mdx +++ b/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/examples/efp-list.mdx @@ -4,8 +4,4 @@ title: EFP List import OmnigraphStaticExampleSet from "@components/organisms/OmnigraphStaticExampleSet.astro"; -:::tip[EFP plugin required] -This query reads [Ethereum Follow Protocol (EFP)](/docs/integrate/omnigraph/efp) data, which requires the [`efp` plugin](/docs/integrate/omnigraph/efp) to be enabled on the connected ENSIndexer. It is enabled on the hosted [ENSNode 'Alpha'](/docs/hosted-instances#ensnode-alpha) instance. -::: - diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/examples/efp-who-follows.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/examples/efp-who-follows.mdx index 593539bc2..eeee2bea6 100644 --- a/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/examples/efp-who-follows.mdx +++ b/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/examples/efp-who-follows.mdx @@ -4,8 +4,4 @@ title: EFP Who Follows import OmnigraphStaticExampleSet from "@components/organisms/OmnigraphStaticExampleSet.astro"; -:::tip[EFP plugin required] -This query reads [Ethereum Follow Protocol (EFP)](/docs/integrate/omnigraph/efp) data, which requires the [`efp` plugin](/docs/integrate/omnigraph/efp) to be enabled on the connected ENSIndexer. It is enabled on the hosted [ENSNode 'Alpha'](/docs/hosted-instances#ensnode-alpha) instance. -::: - diff --git a/docs/ensnode.io/src/content/docs/docs/services/ensdb/concepts/database-schemas.mdx b/docs/ensnode.io/src/content/docs/docs/services/ensdb/concepts/database-schemas.mdx index a9f7a322f..f38f3bcb6 100644 --- a/docs/ensnode.io/src/content/docs/docs/services/ensdb/concepts/database-schemas.mdx +++ b/docs/ensnode.io/src/content/docs/docs/services/ensdb/concepts/database-schemas.mdx @@ -528,3 +528,98 @@ After an NFT is indexed, it is never deleted from the index. When an indexed NFT | `mint_status` | `text` | no | Either `minted` or `burned`. | **Indexes:** `domain_id`, `owner`. + +--- + +### EFP + +Defined in [`efp.schema.ts`](https://github.com/namehash/ensnode/blob/main/packages/ensdb-sdk/src/ensindexer-abstract/efp.schema.ts). + +Indexes the [Ethereum Follow Protocol (EFP)](/docs/integrate/omnigraph/efp) onchain social graph — list NFTs, the records and tags they hold, and account metadata. Tables are prefixed `efp_`. The model mirrors the [ethereumfollowprotocol/api-v2](https://github.com/ethereumfollowprotocol/api) reference indexer, with two adaptations for ENSNode's primary-key-only access pattern: `efp_list_storage_locations` is a reverse index so list-metadata events resolve the owning list NFT by primary key, and a record's tags are embedded as an array on `efp_list_records` (rather than a separate join table) so removing a record is a single primary-key delete. See the [EFP Omnigraph reference](/docs/integrate/omnigraph/efp) for the higher-level API over this data. + +#### efp_lists + +One row per minted `ListRegistry` NFT (a "list"). EFP separates the NFT `owner`, the `user` allowed to post records, and the `manager` allowed to administer the list. The `list_storage_location_*` columns describe which `(chain_id, contract_address, slot)` tuple in `efp_list_records` stores this list's records. + +| Column | Type | Nullable | Description | +| ---------------------------------------- | ------------- | -------- | ---------------------------------------------------------------------- | +| `id` | `numeric(78)` | no | ERC-721 token id of the list NFT (a `uint256`). Primary key. | +| `owner` | `text` | no | Current ERC-721 owner of the list NFT. | +| `nft_chain_id` | `bigint` | no | Chain id of the `ListRegistry` NFT (Base / `8453` on mainnet). | +| `nft_contract_address` | `text` | no | `ListRegistry` contract address on `nft_chain_id`. | +| `list_storage_location` | `text` | yes | Raw `UpdateListStorageLocation` payload. | +| `list_storage_location_chain_id` | `bigint` | yes | Decoded list storage location: target chain id. `null` if undecodable. | +| `list_storage_location_contract_address` | `text` | yes | Decoded list storage location: target contract address. | +| `list_storage_location_slot` | `text` | yes | Decoded list storage location: target slot (`bytes32`). | +| `user` | `text` | yes | Address allowed to post records to this list (the EFP "user"). | +| `manager` | `text` | yes | Address allowed to administer this list (the EFP "manager"). | +| `created_at` | `numeric(78)` | no | Unix timestamp when the list was first indexed. | +| `updated_at` | `numeric(78)` | no | Unix timestamp of the most recent update. | + +**Indexes:** `owner`, `user`, `manager`, `(list_storage_location_chain_id, list_storage_location_contract_address, list_storage_location_slot)`. The `numeric` primary key already orders numerically, so list pagination needs no extra index. + +#### efp_list_storage_locations + +Reverse index from a storage location `(chain_id, contract_address, slot)` to the list NFT that points at it, so `UpdateListMetadata` events (keyed only by slot) can resolve the owning list NFT by primary key instead of scanning `efp_lists`. The slot is arbitrary, attacker-settable bytes, so multiple list NFTs can point at the same location; this records the first list to claim it (first writer wins). + +| Column | Type | Nullable | Description | +| ------------------ | ------------- | -------- | ---------------------------------------------------------------------------- | +| `id` | `text` | no | Composite key `{chain_id}-{contract_address}-{slot}`. Primary key. | +| `chain_id` | `bigint` | no | Target chain id of the storage location. | +| `contract_address` | `text` | no | `ListRecords` contract address. | +| `slot` | `text` | no | Storage slot (`bytes32`). | +| `token_id` | `numeric(78)` | no | Token id of the list NFT that owns this reverse mapping (first writer wins). | +| `updated_at` | `numeric(78)` | no | Unix timestamp of the most recent update. | + +#### efp_list_records + +One row per record currently in a list. `record` is the canonical `version | type | address` 22-byte prefix (any trailing junk after the address truncated), which is also what tag and remove ops reference. A record's `tags` are embedded here as a set, so removing a record drops its tags in the same primary-key delete. + +| Column | Type | Nullable | Description | +| ------------------ | ------------- | -------- | --------------------------------------------------------------------------------------- | +| `id` | `text` | no | Composite key `{chain_id}-{contract_address}-{slot}-{record}`. Primary key. | +| `chain_id` | `bigint` | no | Chain id where the records are stored. | +| `contract_address` | `text` | no | `ListRecords` contract address. | +| `slot` | `text` | no | Storage slot (`bytes32`) of the owning list. | +| `record` | `text` | no | Canonical record prefix `version \| type \| address` (22 bytes). | +| `record_version` | `integer` | no | Decoded record header — version byte. | +| `record_type` | `integer` | no | Decoded record header — type byte (only `1` = address record is indexed). | +| `record_data` | `text` | no | Decoded record data — the 20-byte target address. | +| `tags` | `text[]` | no | UTF-8 tags attached to this record (a set; e.g. `block`, `mute`, `top8`). Default `[]`. | +| `created_at` | `numeric(78)` | no | Unix timestamp when the record was added. | + +**Indexes:** `(chain_id, contract_address, slot)`, `record_data`. + +#### efp_account_metadata + +Most-recent `value` per `(address, key)` account-metadata pair (today only `primary-list`). + +| Column | Type | Nullable | Description | +| ----------------------- | ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `id` | `text` | no | Composite key `{chain_id}-{address}-{key}`. Primary key. | +| `chain_id` | `bigint` | no | Chain id of the `AccountMetadata` contract. | +| `contract_address` | `text` | no | `AccountMetadata` contract address. | +| `address` | `text` | no | Account whose metadata this is. | +| `key` | `text` | no | Metadata key (UTF-8 string), e.g. `primary-list`. | +| `value` | `text` | no | Metadata value (raw bytes). | +| `primary_list_token_id` | `numeric(78)` | yes | For the `primary-list` key only: the decoded token id of the account's primary list. `null` for any other key, or a malformed value. | +| `created_at` | `numeric(78)` | no | Unix timestamp when first indexed. | +| `updated_at` | `numeric(78)` | no | Unix timestamp of the most recent update. | + +**Indexes:** `address`, `(address, key)`, `primary_list_token_id`. + +#### efp_list_metadata + +EFP List Metadata (`user` / `manager`), keyed by the storage location it is set at (`{chain_id}-{contract_address}-{slot}-{key}`), not by list NFT. `UpdateListMetadata` is emitted on the `ListRecords` contract while the storage-location mapping is created by `UpdateListStorageLocation` on the `ListRegistry` contract (a different contract, sometimes on a different chain), so the two can arrive in either order. The value here is durable: it survives a list re-pointing its storage location. + +| Column | Type | Nullable | Description | +| ------------------ | ------------- | -------- | ------------------------------------------------------------------------ | +| `id` | `text` | no | Composite key `{chain_id}-{contract_address}-{slot}-{key}`. Primary key. | +| `chain_id` | `bigint` | no | Target chain id of the storage location. | +| `contract_address` | `text` | no | `ListRecords` contract address. | +| `slot` | `text` | no | Storage slot (`bytes32`). | +| `key` | `text` | no | Metadata key (`user` or `manager`). | +| `value` | `text` | no | Metadata value (raw bytes). | +| `created_at` | `numeric(78)` | no | Unix timestamp when first indexed. | + +**Indexes:** `(chain_id, contract_address, slot)`. diff --git a/docs/ensnode.io/src/data/omnigraph-examples/examples.json b/docs/ensnode.io/src/data/omnigraph-examples/examples.json index 0851bdc97..614439f4b 100644 --- a/docs/ensnode.io/src/data/omnigraph-examples/examples.json +++ b/docs/ensnode.io/src/data/omnigraph-examples/examples.json @@ -177,7 +177,7 @@ }, { "id": "efp-who-follows", - "query": "# Raw record view: includes block/mute and non-primary lists — inspect `tags`.\n# For the validated social graph, prefer Account.efp.followers instead.\nquery EfpWhoFollows($address: Address!) {\n efp {\n listRecords(where: { recordData: $address }, first: 25) {\n totalCount\n edges {\n node {\n tags\n list { tokenId user }\n }\n }\n }\n }\n}", + "query": "# Raw record view: includes block/mute and non-primary lists — inspect the tags.\n# For the validated social graph, prefer Account.efp.followers instead.\nquery EfpWhoFollows($address: Address!) {\n efp {\n listRecords(where: { recordData: $address }, first: 25) {\n totalCount\n edges {\n node {\n tags\n list { tokenId user }\n }\n }\n }\n }\n}", "variables": { "address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" } diff --git a/docs/ensnode.io/src/data/omnigraph-examples/snapshot.json b/docs/ensnode.io/src/data/omnigraph-examples/snapshot.json index 34791c9ff..7667419bf 100644 --- a/docs/ensnode.io/src/data/omnigraph-examples/snapshot.json +++ b/docs/ensnode.io/src/data/omnigraph-examples/snapshot.json @@ -1,6 +1,6 @@ { "version": "v1.16.0", - "commit": "9fd6918dc", + "commit": "ad03dc2af", "sdkVersion": "1.16.0", "schemaTag": "v1.16.0", "snapshottedAt": "2026-06-19" diff --git a/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts b/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts index bf4b392c7..2d0636a7d 100644 --- a/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts +++ b/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts @@ -964,7 +964,7 @@ query EfpFollowGraph($address: Address!) { id: "efp-who-follows", plugin: PluginName.EFP, query: ` -# Raw record view: includes block/mute and non-primary lists — inspect \`tags\`. +# Raw record view: includes block/mute and non-primary lists — inspect the tags. # For the validated social graph, prefer Account.efp.followers instead. query EfpWhoFollows($address: Address!) { efp { diff --git a/packages/ensskills/skills/efp-protocol/SKILL.md b/packages/ensskills/skills/efp-protocol/SKILL.md index eaf8fd16f..6199cfa7f 100644 --- a/packages/ensskills/skills/efp-protocol/SKILL.md +++ b/packages/ensskills/skills/efp-protocol/SKILL.md @@ -149,7 +149,7 @@ Variables: ### efp-who-follows ```graphql -# Raw record view: includes block/mute and non-primary lists — inspect `tags`. +# Raw record view: includes block/mute and non-primary lists — inspect the tags. # For the validated social graph, prefer Account.efp.followers instead. query EfpWhoFollows($address: Address!) { efp { From 606ef7301221bb1d3e506ea09451f60b64c92e0c Mon Sep 17 00:00:00 2001 From: shrugs Date: Tue, 23 Jun 2026 13:05:31 -0500 Subject: [PATCH 4/5] docs: add ERC-8004 Complementary Protocols page; group EFP under it Take over the ERC-8004 (AI Agent Discovery) page from #2309 and align both it and the EFP page to Lightwalker's terminology choices: - New nav group "Complementary Protocols" (collapsed), containing "EFP (Social Graph)" and "AI Agents / ERC-8004"; no SOON badge in nav. - ERC-8004 page: title "ERC-8004 (AI Agent Identity, Discovery, and Reputation)", "A complementary protocol for ENS" framing callout, plugin-named `erc8004` field (was `erc8004agents`), nested `agent` (was `erc8004agent`), "Example Queries" H2 with H3 examples, hyphenated filename erc-8004.mdx. Consistent "ENS Omnigraph" naming. - Move EFP page to omnigraph/complementary/efp (redirect added) and add the parallel "complementary to ENS" framing callout; sidebar label "EFP (Social Graph)". - Update inbound links (ensnode-plugins, database-schemas, example aside). --- docs/ensnode.io/astro.config.mjs | 2 + .../starlight/sidebar-topics/integrate.ts | 14 +- .../ExampleRequirementAside.astro | 6 +- .../integration-options/ensnode-plugins.mdx | 22 +- .../omnigraph/{ => complementary}/efp.mdx | 18 +- .../omnigraph/complementary/erc-8004.mdx | 197 ++++++++++++++++++ .../ensdb/concepts/database-schemas.mdx | 2 +- 7 files changed, 242 insertions(+), 19 deletions(-) rename docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/{ => complementary}/efp.mdx (79%) create mode 100644 docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/complementary/erc-8004.mdx diff --git a/docs/ensnode.io/astro.config.mjs b/docs/ensnode.io/astro.config.mjs index 9ed94d704..32cd25578 100644 --- a/docs/ensnode.io/astro.config.mjs +++ b/docs/ensnode.io/astro.config.mjs @@ -35,6 +35,8 @@ export default defineConfig({ redirects: { "/docs": "/docs/integrate", "/ensnode": "/docs/integrate", + // EFP page moved under the Complementary Protocols group + "/docs/integrate/omnigraph/efp": "/docs/integrate/omnigraph/complementary/efp", "/examples": "/docs/integrate/omnigraph/examples", "/ensnode/deploying/railway": "/docs/services/ensrainbow/deploying/railway", // ENS Subgraph docs moved from /docs/integrate/subgraph to /docs/integrate/ens-subgraph diff --git a/docs/ensnode.io/config/integrations/starlight/sidebar-topics/integrate.ts b/docs/ensnode.io/config/integrations/starlight/sidebar-topics/integrate.ts index 1e6f35355..c1fe2ebdf 100644 --- a/docs/ensnode.io/config/integrations/starlight/sidebar-topics/integrate.ts +++ b/docs/ensnode.io/config/integrations/starlight/sidebar-topics/integrate.ts @@ -101,8 +101,18 @@ export const integrateSidebarTopic = { link: "/docs/integrate/omnigraph/protocol-acceleration", }, { - label: "EFP", - link: "/docs/integrate/omnigraph/efp", + label: "Complementary Protocols", + collapsed: true, + items: [ + { + label: "EFP (Social Graph)", + link: "/docs/integrate/omnigraph/complementary/efp", + }, + { + label: "AI Agents / ERC-8004", + link: "/docs/integrate/omnigraph/complementary/erc-8004", + }, + ], }, { label: "FAQ", diff --git a/docs/ensnode.io/src/components/molecules/omnigraph-static-example/ExampleRequirementAside.astro b/docs/ensnode.io/src/components/molecules/omnigraph-static-example/ExampleRequirementAside.astro index 9809d5fef..0221e78e0 100644 --- a/docs/ensnode.io/src/components/molecules/omnigraph-static-example/ExampleRequirementAside.astro +++ b/docs/ensnode.io/src/components/molecules/omnigraph-static-example/ExampleRequirementAside.astro @@ -19,9 +19,9 @@ const category = getOmnigraphExampleConfigById(id)?.category;