Skip to content
This repository was archived by the owner on Aug 5, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
a0a72f7
feat(packages/ensnode-sdk): resolution client
notrab Jul 20, 2025
346ee66
chore: dump pnpm
notrab Jul 20, 2025
9007ee2
feat(packages/ensnode-react): initial concept with tanstack query
notrab Jul 20, 2025
7490ef5
lint
notrab Jul 21, 2025
261fed7
lock
notrab Jul 21, 2025
2feae4c
ENSNode client
notrab Jul 22, 2025
953e374
remove namespaces
notrab Jul 24, 2025
81e4405
docs(changeset): ensnode-sdk and react hooks for name and address lookup
notrab Jul 25, 2025
f0ee1ea
remove index and config hooks
notrab Jul 25, 2025
2a608e0
apply changes
notrab Jul 27, 2025
be45838
ensnodeconfig
notrab Jul 28, 2025
3060cf8
chores
notrab Jul 30, 2025
989dce3
Merge branch 'main' into resolution-client
notrab Aug 4, 2025
ef0121e
Update packages/ensnode-sdk/src/client.test.ts
notrab Aug 4, 2025
cebc1e3
Update packages/ensnode-react/README.md
notrab Aug 4, 2025
e1f1a44
Apply suggestions from code review
notrab Aug 4, 2025
7c4e3da
docs(changeset): add ensnode-react package provider and hooks based o…
notrab Aug 4, 2025
c5f913a
docs(changeset): add ensnode react hooks and provider based on ensnod…
notrab Aug 4, 2025
b3a1990
apply some changes
notrab Aug 4, 2025
1914729
update readme
notrab Aug 4, 2025
106e5bc
separate changesets
notrab Aug 4, 2025
f43ff66
update readme
notrab Aug 4, 2025
0c8999f
remove name from docs
notrab Aug 4, 2025
2499136
remove type import
notrab Aug 4, 2025
a59e4bb
export forwardresolutionselection
notrab Aug 4, 2025
809e444
use ForwardResolutionSelection
notrab Aug 4, 2025
d6f13f2
Merge branch 'main' into resolution-client
shrugs Aug 5, 2025
59e2bae
fix: import issue after merge
shrugs Aug 5, 2025
bbbdaad
feat: move api typing to shared sdk, move resolution api typings to s…
shrugs Aug 5, 2025
7475b3d
feat: tidy up client behavior
shrugs Aug 5, 2025
4a66af6
checkpoint: refactoring shared types, hook behavior
shrugs Aug 5, 2025
3cdd179
fix: rename debug to trace
shrugs Aug 5, 2025
fe48e0f
first pass on hook readme
shrugs Aug 5, 2025
46168c8
remove automatic resolution for the moment
shrugs Aug 5, 2025
71202b5
remove .js ending
shrugs Aug 5, 2025
7722416
nit: rename finalOptions to options
shrugs Aug 5, 2025
b4a50ef
update examples
shrugs Aug 5, 2025
e8e73d8
checkpoint: update sdk for new naming
shrugs Aug 6, 2025
2f949df
checkpoint: update naming of hooks
shrugs Aug 6, 2025
0e55727
checkpoint: update readmes for new naming
shrugs Aug 6, 2025
1a5414b
checkpoint: add required chainId parameter to all api methods
shrugs Aug 6, 2025
637db61
fix: pnpm lock
shrugs Aug 6, 2025
3f089bd
chain id type, pr updates
shrugs Aug 6, 2025
446aef2
fix: reverse resolution api just returns name now
shrugs Aug 6, 2025
e27fdc0
pr notes
shrugs Aug 6, 2025
31b9df7
fix data example
shrugs Aug 6, 2025
56c2886
pr notes
shrugs Aug 6, 2025
6976a6b
Merge branch 'main' into resolution-client
shrugs Aug 6, 2025
e54438a
pr notes
shrugs Aug 6, 2025
f1e5a88
pr notes
shrugs Aug 6, 2025
a8c313e
pr notes
shrugs Aug 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/late-keys-heal.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
"ensindexer": minor
---

implements protocol tracing outputs for the resolution apis via ?debug query param
implements protocol tracing outputs for the resolution apis via ?trace query param
5 changes: 5 additions & 0 deletions .changeset/polite-actors-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ensnode/ensnode-react": minor
---

Adds ENSNode React hooks and providers based on ENSNode-SDK for use inside React apps
5 changes: 5 additions & 0 deletions .changeset/stale-horses-stand.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ensnode/ensnode-sdk": minor
---

Adds `resolveRecords` and `resolvePrimaryName` methods to ENSNode-SDK.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ generated
# Terraform
.terraform*
terraform.tfstate*

# CLAUDE.md
CLAUDE.md
36 changes: 0 additions & 36 deletions apps/ensindexer/src/api/lib/automatic-resolution.ts

This file was deleted.

8 changes: 4 additions & 4 deletions apps/ensindexer/src/api/lib/find-resolver.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { db, publicClients } from "ponder:api";
import { DatasourceNames, getDatasource, getENSRootChainId } from "@ensnode/datasources";
import { type Name, type Node, PluginName, getNameHierarchy } from "@ensnode/ensnode-sdk";
import { ChainId, type Name, type Node, PluginName, getNameHierarchy } from "@ensnode/ensnode-sdk";
import { SpanStatusCode, trace } from "@opentelemetry/api";
import { type Address, isAddressEqual, namehash, toHex, zeroAddress } from "viem";
import { packetToBytes } from "viem/ens";
Expand All @@ -25,7 +25,7 @@ const NULL_RESULT: FindResolverResult = {

const tracer = trace.getTracer("find-resolver");

export async function findResolver(chainId: number, name: Name) {
export async function findResolver(chainId: ChainId, name: Name) {
// TODO: Accelerate names that are subnames of well-known registrar managed names (i.e. base.eth, .linea.eth)
// .base.eth -> ensroot.BasenamesL1Resolver.address;
// .linea.eth -> ensroot.LineanamesL1Resolver.address;
Expand Down Expand Up @@ -54,7 +54,7 @@ export async function findResolver(chainId: number, name: Name) {
* via RPC.
*/
async function findResolverWithUniversalResolver(
chainId: number,
chainId: ChainId,
name: Name,
): Promise<FindResolverResult> {
return withActiveSpanAsync(
Expand Down Expand Up @@ -132,7 +132,7 @@ async function findResolverWithUniversalResolver(
* // Returns: "0x123..." or null if no resolver found
* ```
*/
async function findResolverWithIndex(chainId: number, name: Name): Promise<FindResolverResult> {
async function findResolverWithIndex(chainId: ChainId, name: Name): Promise<FindResolverResult> {
return withActiveSpanAsync(tracer, "findResolverWithIndex", { chainId, name }, async (span) => {
// 1. construct a hierarchy of names. i.e. sub.example.eth -> [sub.example.eth, example.eth, eth]
const names = getNameHierarchy(name);
Expand Down
44 changes: 30 additions & 14 deletions apps/ensindexer/src/api/lib/forward-resolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
ForwardResolutionProtocolStep,
type Name,
Node,
ResolverRecordsResponse,
ResolverRecordsSelection,
TraceableENSProtocol,
} from "@ensnode/ensnode-sdk";
import { trace } from "@opentelemetry/api";
Expand All @@ -15,21 +17,19 @@ import { supportsENSIP10Interface } from "@/api/lib/ensip-10";
import { findResolver } from "@/api/lib/find-resolver";
import { possibleKnownOffchainLookupResolverDefersTo } from "@/api/lib/known-offchain-lookup-resolver";
import { getKnownOnchainStaticResolverAddresses } from "@/api/lib/known-onchain-static-resolver";
import {
IndexedResolverRecords,
makeEmptyResolverRecordsResponse,
makeRecordsResponseFromIndexedRecords,
makeRecordsResponseFromResolveResults,
} from "@/api/lib/make-records-response";
import { addProtocolStepEvent, withProtocolStepAsync } from "@/api/lib/protocol-tracing";
import {
executeResolveCalls,
interpretRawCallsAndResults,
makeResolveCalls,
} from "@/api/lib/resolve-calls-and-results";
import { areResolverRecordsIndexedOnChain } from "@/api/lib/resolver-records-indexed-on-chain";
import {
IndexedResolverRecords,
ResolverRecordsResponse,
makeEmptyResolverRecordsResponse,
makeRecordsResponseFromIndexedRecords,
makeRecordsResponseFromResolveResults,
} from "@/api/lib/resolver-records-response";
import { ResolverRecordsSelection } from "@/api/lib/resolver-records-selection";
import config from "@/config";
import { withActiveSpanAsync, withSpan, withSpanAsync } from "@/lib/auto-span";
import { makeResolverId } from "@/lib/ids";
Expand All @@ -44,11 +44,12 @@ const ensRootChainId = getENSRootChainId(config.namespace);
normalize("example.eth");

/**
* Implements Forward Resolution of an ENS name, for a selection of records, on a specified chainId.
* Implements Forward Resolution of record values for a specified ENS Name.
*
* @param name the ENS name to resolve
* @param selection selection specifying which records to resolve
* @param chainId optional, the chain id from which to resolve records
* @param options Optional settings
* @param options.accelerate Whether to accelerate resolution (default: true)
*
* @example
* await resolveForward("jesse.base.eth", {
Expand All @@ -59,7 +60,7 @@ normalize("example.eth");
*
* // results in
* {
* name: { name: 'jesse.base.eth' },
* name: 'jesse.base.eth',
* addresses: {
* 60: '0x849151d7D0bF1F34b70d5caD5149D28CC2308bf1',
* 2147492101: null
Expand All @@ -73,9 +74,24 @@ normalize("example.eth");
export async function resolveForward<SELECTION extends ResolverRecordsSelection>(
name: Name,
selection: SELECTION,
options: { chainId?: number; accelerate?: boolean } = {},
{ accelerate }: { accelerate?: boolean } = {},
): Promise<ResolverRecordsResponse<SELECTION>> {
// NOTE: `resolveForward` is just `_resolveForward` with the enforcement that `chainId` must
// initially be `ensRootChainId`: see `_resolveForward` for additional context.
return _resolveForward(name, selection, { chainId: ensRootChainId, accelerate });
}

/**
* Internal Forward Resolution implementation.
*
* NOTE: uses `chainId` parameter for internal Protocol Acceleration behavior (see recursive call below).
*/
async function _resolveForward<SELECTION extends ResolverRecordsSelection>(
name: Name,
selection: SELECTION,
options: { chainId: number; accelerate?: boolean },
): Promise<ResolverRecordsResponse<SELECTION>> {
const { chainId = ensRootChainId, accelerate = true } = options;
const { chainId, accelerate = true } = options;

// trace for external consumers
return withProtocolStepAsync(
Expand Down Expand Up @@ -188,7 +204,7 @@ export async function resolveForward<SELECTION extends ResolverRecordsSelection>
return withProtocolStepAsync(
TraceableENSProtocol.ForwardResolution,
ForwardResolutionProtocolStep.AccelerateKnownOffchainLookupResolver,
() => resolveForward(name, selection, { ...options, chainId: defers.chainId }),
() => _resolveForward(name, selection, { ...options, chainId: defers.chainId }),
);
}

Expand Down
21 changes: 13 additions & 8 deletions apps/ensindexer/src/api/lib/known-offchain-lookup-resolver.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import config from "@/config";
import { ENSNamespace, getENSNamespace } from "@ensnode/datasources";
import { PluginName } from "@ensnode/ensnode-sdk";
import { ChainId, PluginName } from "@ensnode/ensnode-sdk";
import { Address, isAddressEqual } from "viem";

// NOTE: typing as ENSNamespace so we can access possibly undefined Datasources
Expand Down Expand Up @@ -29,19 +29,23 @@ const ensNamespace = getENSNamespace(config.namespace) as ENSNamespace;
* resolverAddress to (sub-)Registry on a specified chain.
*/
export function possibleKnownOffchainLookupResolverDefersTo(
chainId: number,
chainId: ChainId,
resolverAddress: Address,
): { pluginName: PluginName; chainId: number | undefined } | null {
): { pluginName: PluginName; chainId: ChainId } | null {
// on the ENS Deployment Chain
if (chainId === ensNamespace.ensroot.chain.id) {
const basenamesL1ResolverAddress = ensNamespace.ensroot.contracts.BasenamesL1Resolver
?.address as Address | undefined;

// the ENSRoot's BasenamesL1Resolver, if exists, defers to the Basenames plugin
if (basenamesL1ResolverAddress && isAddressEqual(resolverAddress, basenamesL1ResolverAddress)) {
// the ENSRoot's BasenamesL1Resolver, if exists, defers to the Basenames plugin,
if (
basenamesL1ResolverAddress &&
isAddressEqual(resolverAddress, basenamesL1ResolverAddress) &&
ensNamespace.basenames
) {
return {
pluginName: PluginName.Basenames,
chainId: ensNamespace.basenames?.chain.id,
chainId: ensNamespace.basenames.chain.id,
};
}

Expand All @@ -51,11 +55,12 @@ export function possibleKnownOffchainLookupResolverDefersTo(
// the ENSRoot's BasenamesL1Resolver, if exists, defers to the Lineanames plugin
if (
lineanamesL1ResolverAddress &&
isAddressEqual(resolverAddress, lineanamesL1ResolverAddress)
isAddressEqual(resolverAddress, lineanamesL1ResolverAddress) &&
ensNamespace.lineanames
) {
return {
pluginName: PluginName.Lineanames,
chainId: ensNamespace.lineanames?.chain.id,
chainId: ensNamespace.lineanames.chain.id,
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import config from "@/config";
import { ENSNamespace, getENSNamespace } from "@ensnode/datasources";
import { ChainId } from "@ensnode/ensnode-sdk";
import { Address } from "viem";

// NOTE: typing as ENSNamespace so we can access possibly undefined Datasources
Expand All @@ -19,7 +20,7 @@ const ensNamespace = getENSNamespace(config.namespace) as ENSNamespace;
*
* TODO: these relationships could/should be encoded in an ENSIP
*/
export function getKnownOnchainStaticResolverAddresses(chainId: number): Address[] {
export function getKnownOnchainStaticResolverAddresses(chainId: ChainId): Address[] {
// on the ENS Deployment Chain
if (chainId === ensNamespace.ensroot.chain.id) {
return [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { ResolveCallsAndResults } from "@/api/lib/resolve-calls-and-results";
import { ResolverRecordsSelection } from "@/api/lib/resolver-records-selection";
import { CoinType, bigintToCoinType } from "@ensnode/ensnode-sdk";
import {
ResolverRecordsResponse,
ResolverRecordsResponseBase,
ResolverRecordsSelection,
bigintToCoinType,
} from "@ensnode/ensnode-sdk";

// TODO: replace with some sort of inferred typing from dizzle
Comment thread
shrugs marked this conversation as resolved.
export interface IndexedResolverRecords {
Expand All @@ -9,55 +13,6 @@ export interface IndexedResolverRecords {
textRecords: { key: string; value: string }[];
}

type ResolverRecordsResponseBase = {
/**
* The name record.
*/
name: string | null;

/**
* Address records, keyed by CoinType.
*/
addresses: Record<CoinType, string | null>;

/**
* Text records, keyed by key.
*/
texts: Record<string, string | null>;
};

/**
* Example usage of ResolverRecordsResponse type:
*
* ```typescript
* const selection = {
* name: true,
* addresses: [60],
* texts: ["com.twitter", "avatar"],
* } as const satisfies ResolverRecordsSelection;
*
* type Response = ResolverRecordsResponse<typeof selection>;
*
* // results in the following type
* type Response = {
* readonly name: string | null;
* readonly addresses: Record<"60", string | null>;
* readonly texts: Record<"avatar" | "com.twitter", string | null>;
* }
* ```
*/
export type ResolverRecordsResponse<T extends ResolverRecordsSelection = ResolverRecordsSelection> =
{
[K in keyof T as T[K] extends true | any[] ? K : never]: K extends "addresses"
? Record<
`${T["addresses"] extends readonly CoinType[] ? T["addresses"][number] : never}`,
string | null
>
: K extends "texts"
? Record<T["texts"] extends readonly string[] ? T["texts"][number] : never, string | null>
: ResolverRecordsResponseBase[K & keyof ResolverRecordsResponseBase];
};

/**
* Formats IndexedResolverRecords into a ResolverRecordsResponse based on the provided selection.
*
Expand All @@ -69,7 +24,7 @@ export function makeRecordsResponseFromIndexedRecords<SELECTION extends Resolver
selection: SELECTION,
records: IndexedResolverRecords,
): ResolverRecordsResponse<SELECTION> {
const response: Partial<ResolverRecordsResponse<any>> = {};
const response: Partial<ResolverRecordsResponseBase> = {};

if (selection.name) {
response.name = records.name;
Expand Down Expand Up @@ -97,14 +52,15 @@ export function makeRecordsResponseFromIndexedRecords<SELECTION extends Resolver
);
}

// cast response as the inferred type based on SELECTION
return response as ResolverRecordsResponse<SELECTION>;
}

export function makeRecordsResponseFromResolveResults<SELECTION extends ResolverRecordsSelection>(
selection: SELECTION,
results: ResolveCallsAndResults<SELECTION>,
): ResolverRecordsResponse<SELECTION> {
const response: Partial<ResolverRecordsResponse<any>> = {};
const response: Partial<ResolverRecordsResponseBase> = {};

if (selection.name) {
const nameResult = results.find(({ call: { functionName } }) => functionName === "name");
Expand Down Expand Up @@ -139,6 +95,7 @@ export function makeRecordsResponseFromResolveResults<SELECTION extends Resolver
);
}

// cast response as the inferred type based on SELECTION
return response as ResolverRecordsResponse<SELECTION>;
}

Expand Down
2 changes: 1 addition & 1 deletion apps/ensindexer/src/api/lib/protocol-tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export function addProtocolStepEvent<
*/
export async function captureTrace<Fn extends () => Promise<any>>(
fn: Fn,
): Promise<{ trace: ProtocolTrace; result: ReturnType<Awaited<Fn>> }> {
): Promise<{ trace: ProtocolTrace; result: Awaited<ReturnType<Fn>> }> {
// TODO: make sure there are no race conditions here, not sure how hono & otel work
const traceId = trace.getActiveSpan()?.spanContext().traceId;

Expand Down
Loading