Problem
Portal's /addr/$addr/resolution view ("names pointing to this address") today is served by ensjs subgraph's getResolvedNamesForAddress. As Portal migrates off subgraph stitching, Omnigraph needs to cover this query.
Proposal
Expose forward-resolution lookup on Account as a Relay connection over a new NameReference type, with an optional coinType filter:
type Account {
nameReferences(
where: AccountNameReferencesWhereInput
first: Int, last: Int, before: String, after: String
): NameReferenceConnection!
}
input AccountNameReferencesWhereInput {
coinType: CoinType!
}
type NameReference {
name: String! # InterpretedName of the Domain whose addr() points here
coinType: CoinType! # the CoinType of the matching addr() record
match: Boolean! # ENSIP-19 reverse(address, coinType) === name (indexed-only)
normalized: Boolean! # isNormalizedName(name)
resolverAddress: Address! # the Resolver holding the matching addr() record
resolverChainId: ChainId! # chain where that Resolver lives
}
Notes
where is optional — when omitted, return matches across all coinTypes; when supplied, scope to a single coinType. Default-coinType records (coinType = DEFAULT_EVM_COIN_TYPE) are returned as ordinary rows, not specially expanded.
- v1 reflects literally-indexed records only: no Forward Resolution / CCIP-Read, no ENSIP-19 default address record expansion, no
*.addr.reverse-based primary name walk for match. match uses the indexed StandaloneReverseRegistrar shortcut (default.reverse / [coinType].reverse).
- v1 does NOT enforce active-resolver consistency — records on resolvers no longer assigned to the Domain may surface. (Tracked separately.)
- New
(value, coinType) index on resolver_address_records for the access pattern.
Blocker / Open question: node → Domain reverse mapping
The natural query reads resolver_address_records (keyed by (chainId, resolver, node, coinType)) and joins to a Domain to attach a Name. In ENSv1, subgraph_domain.id = node (and domain.node is set), so the join is trivial. In ENSv2, node is no longer a functional reference to a Domain — it's only knowable when you start from the Name and namehash forward. The corresponding Domain entities exist conceptually but their namehash is not materialized in the database.
Confirmed empirically against ens-test-env (v2-native, default SUBGRAPH_COMPAT=false): 15 records for (value=DEVNET_OWNER, coinType=60), 0 join matches.
To support reverse lookup natively, we'd need to materialize a canonicalNode for every Domain (the namehash of its full canonical path) and maintain it as the canonical path changes. It is not obvious this is feasible in Ponder at ENS scale: every parent canonical-path mutation cascades to all descendants' canonicalNode values — potentially a very large fan-out write per event, hot in the indexer critical path. Designs to consider: incremental/lazy recompute, deferred recompute jobs, cap-and-warn on deep subtrees, etc. This needs its own investigation before this PR can ship its intended shape.
Consumers
apps/portal/ /addr/$addr/resolution
Alternative shapes if canonicalNode materialization is infeasible
If the reverse mapping can't be made sustainable, the endpoint can still be useful in narrower forms — these are not interchangeable with Portal's current discovery UX, but each has real callers:
- Records-only primitive. Drop
name from the response; return (resolverChainId, resolverAddress, node, coinType) and let the caller hydrate names via their own index. Useless for Portal's specific UI; useful as a building block for tools that already have a name index.
- Filter-by-candidates. Flip the direction:
Account.nameReferences(where: { coinType, names: [InterpretedName!]! }). Caller supplies candidate names; we namehash each and check the records. Solves "verify these names point at this address"; does not solve discovery ("find names I don't already know about").
- Owned-and-pointing intersection. Extend
Account.domains with where: { resolvesTo: AccountAddressInput }. Filters owned Domains down to those whose addr record points to a given address. Limited to names the user owns, but covers the common case for an address page.
- Subgraph-compat-only. Ship the field gated on
SUBGRAPH_COMPAT=true; document that v2-native deployments return empty until canonicalNode materialization lands. Useful for deployments running the legacy compat plugin; silent footgun in v2-native deployments unless we error visibly when not compat-enabled.
Open question: which of these (or which combination) actually serves Portal — and is it worth shipping any of them before the canonicalNode work, or do we wait?
Prior context
Problem
Portal's
/addr/$addr/resolutionview ("names pointing to this address") today is served by ensjs subgraph'sgetResolvedNamesForAddress. As Portal migrates off subgraph stitching, Omnigraph needs to cover this query.Proposal
Expose forward-resolution lookup on
Accountas a Relay connection over a newNameReferencetype, with an optionalcoinTypefilter:Notes
whereis optional — when omitted, return matches across all coinTypes; when supplied, scope to a single coinType. Default-coinType records (coinType = DEFAULT_EVM_COIN_TYPE) are returned as ordinary rows, not specially expanded.*.addr.reverse-based primary name walk formatch.matchuses the indexed StandaloneReverseRegistrar shortcut (default.reverse/[coinType].reverse).(value, coinType)index onresolver_address_recordsfor the access pattern.Blocker / Open question: node → Domain reverse mapping
The natural query reads
resolver_address_records(keyed by(chainId, resolver, node, coinType)) and joins to a Domain to attach aName. In ENSv1,subgraph_domain.id = node(anddomain.nodeis set), so the join is trivial. In ENSv2,nodeis no longer a functional reference to a Domain — it's only knowable when you start from the Name and namehash forward. The corresponding Domain entities exist conceptually but their namehash is not materialized in the database.Confirmed empirically against
ens-test-env(v2-native, defaultSUBGRAPH_COMPAT=false): 15 records for(value=DEVNET_OWNER, coinType=60), 0 join matches.To support reverse lookup natively, we'd need to materialize a
canonicalNodefor every Domain (the namehash of its full canonical path) and maintain it as the canonical path changes. It is not obvious this is feasible in Ponder at ENS scale: every parent canonical-path mutation cascades to all descendants'canonicalNodevalues — potentially a very large fan-out write per event, hot in the indexer critical path. Designs to consider: incremental/lazy recompute, deferred recompute jobs, cap-and-warn on deep subtrees, etc. This needs its own investigation before this PR can ship its intended shape.Consumers
apps/portal//addr/$addr/resolutionAlternative shapes if
canonicalNodematerialization is infeasibleIf the reverse mapping can't be made sustainable, the endpoint can still be useful in narrower forms — these are not interchangeable with Portal's current discovery UX, but each has real callers:
namefrom the response; return(resolverChainId, resolverAddress, node, coinType)and let the caller hydrate names via their own index. Useless for Portal's specific UI; useful as a building block for tools that already have a name index.Account.nameReferences(where: { coinType, names: [InterpretedName!]! }). Caller supplies candidate names; we namehash each and check the records. Solves "verify these names point at this address"; does not solve discovery ("find names I don't already know about").Account.domainswithwhere: { resolvesTo: AccountAddressInput }. Filters owned Domains down to those whose addr record points to a given address. Limited to names the user owns, but covers the common case for an address page.SUBGRAPH_COMPAT=true; document that v2-native deployments return empty untilcanonicalNodematerialization lands. Useful for deployments running the legacy compat plugin; silent footgun in v2-native deployments unless we error visibly when not compat-enabled.Open question: which of these (or which combination) actually serves Portal — and is it worth shipping any of them before the
canonicalNodework, or do we wait?Prior context
domain_resolver_relations.domain_id = resolver_address_records.node, implicitly assuming the ENSv1 model wheredomainId ≡ namehash. That silently returns zero rows in any v2-native deployment.