Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
59a4c2f
feat: useAvatarUrl with manual fallback
notrab Oct 2, 2025
dfbc7e2
Update packages/ensnode-react/src/hooks/useAvatarUrl.ts
notrab Oct 2, 2025
a584559
feat: useAvatarUrl with fallback (#1143)
notrab Oct 2, 2025
4251e40
apply feedback
notrab Oct 2, 2025
018f43d
move buildUrl
notrab Oct 5, 2025
40192ae
apply feedback
notrab Oct 5, 2025
8cc88a0
update jsdoc for avatar
notrab Oct 5, 2025
a233cf4
apply changes
notrab Oct 5, 2025
35bc13a
fix: missing generics for useQuery;
notrab Oct 5, 2025
43d8902
docs(changeset): useAvatarUrl
notrab Oct 5, 2025
b128d60
docs(changeset): Added useAvatarUrl hook
notrab Oct 5, 2025
41e9f11
docs(changeset): Added buildUrl
notrab Oct 5, 2025
66dc496
individual changesets
notrab Oct 5, 2025
cd5f6c5
export correct types for use
notrab Oct 5, 2025
b190da0
apply suggestion
notrab Oct 6, 2025
ddc875b
apply feedback
notrab Oct 6, 2025
8c1c4bd
Update packages/ensnode-react/src/hooks/useAvatarUrl.ts
notrab Oct 6, 2025
65bdc7f
Update packages/ensnode-react/src/hooks/useAvatarUrl.ts
notrab Oct 6, 2025
a10e774
toBrowserSupportedUrl
notrab Oct 6, 2025
0b89a9d
update comments for normalizeAvatarUrl
notrab Oct 6, 2025
8e42580
remove datasources pkg
notrab Oct 6, 2025
5c0c095
move comment to above property
notrab Oct 8, 2025
a5aeabd
remove redundant enabled propetty
notrab Oct 8, 2025
4c0891d
update loading state
notrab Oct 8, 2025
f064b24
use buildUrl
notrab Oct 10, 2025
c71a0a6
use toBrowserSupportedUrl only
notrab Oct 10, 2025
f844aed
call toBrowserSupportedUrl
notrab Oct 10, 2025
0b888b8
revisit feedback
notrab Oct 12, 2025
2baeb88
apply feedback
notrab Oct 12, 2025
a25a686
toString
notrab Oct 12, 2025
1285379
Merge branch 'main' into use-avatar-url
notrab Oct 12, 2025
2493e5e
add docs to readme
notrab Oct 12, 2025
a7b412b
Apply suggestions from code review
notrab Oct 14, 2025
7abe501
apply missed usesProxy
notrab Oct 14, 2025
4c9fd42
apply feedback
notrab Oct 14, 2025
34d131b
apply example feedback
notrab Oct 14, 2025
22aab5f
fix async proxy
notrab Oct 14, 2025
c230242
more fallback > proxy terminology
notrab Oct 14, 2025
b0c6adc
toBrowserSupportedUrl jsdoc improvements
notrab Oct 14, 2025
4c98d73
catch and buildUrl
notrab Oct 14, 2025
1faed00
fix: use browserSupportedAvatarUrlProxy
notrab Oct 14, 2025
03ceb2c
Merge branch 'main' into use-avatar-url
notrab Oct 14, 2025
1cec6c9
lint
notrab Oct 14, 2025
5d484b9
fix merge conflict
notrab Oct 14, 2025
9675b64
Update packages/ensnode-react/README.md
notrab Oct 14, 2025
9933fad
apply doc changes
notrab Oct 14, 2025
d6bf7cd
ipfs example
notrab Oct 14, 2025
994166c
remove notes
notrab Oct 15, 2025
6264add
handle data protocol
notrab Oct 15, 2025
efb4806
cleanup
notrab Oct 15, 2025
72158da
ignore eip155 use avatar url (#1182)
notrab Oct 16, 2025
4150634
Avatar mock input (#1183)
notrab Oct 16, 2025
741d716
build URL
notrab Oct 17, 2025
578a8c5
Update packages/ensnode-react/README.md
notrab Oct 17, 2025
0165bce
Update packages/ensnode-react/src/hooks/useAvatarUrl.ts
notrab Oct 17, 2025
70f2b43
Update packages/ensnode-react/src/hooks/useAvatarUrl.ts
notrab Oct 17, 2025
98ee6be
Update packages/ensnode-react/src/hooks/useAvatarUrl.ts
notrab Oct 17, 2025
9e79135
Update packages/ensnode-react/src/hooks/useAvatarUrl.ts
notrab Oct 17, 2025
e204547
Update packages/ensnode-react/src/hooks/useAvatarUrl.ts
notrab Oct 17, 2025
ef145e0
Update packages/ensnode-react/src/hooks/useAvatarUrl.ts
notrab Oct 17, 2025
4d46cc3
mock page updates
notrab Oct 17, 2025
a30ef90
rename variables again
notrab Oct 17, 2025
839a328
move files
notrab Oct 17, 2025
cb03bf8
caip
notrab Oct 18, 2025
ebe9df7
terminology
notrab Oct 18, 2025
adc847e
update mocks
notrab Oct 18, 2025
21c91ef
old tests
notrab Oct 19, 2025
4151338
replace old tests
notrab Oct 20, 2025
9912ed2
replace old tests
notrab Oct 20, 2025
a012f44
Merge branch 'main' into use-avatar-url
notrab Oct 20, 2025
b4b91d3
handle invalid URLs
notrab Oct 20, 2025
5b58014
Merge branch 'main' into use-avatar-url
notrab Oct 31, 2025
050c8a6
merge conflicts
notrab Oct 31, 2025
7ab5cb8
revert changes to buildEnsMetadataServiceAvatarUrl
notrab Oct 31, 2025
112c20e
lint
notrab Oct 31, 2025
2c8ea1e
Merge branch 'main' into use-avatar-url
lightwalker-eth Nov 4, 2025
a0c58de
simplify mock loading state
notrab Nov 4, 2025
b7e971e
fix loading overloads
notrab Nov 4, 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
7 changes: 7 additions & 0 deletions .changeset/cold-donuts-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@ensnode/ensnode-sdk": minor
---

Added `buildEnsMetadataServiceAvatarUrl` function to generate ENS Metadata Service avatar URLs for supported namespaces
Added `buildUrl` utility function to normalize URLs with implicit `https://` protocol handling
Exported metadata service utilities from `ens` module
7 changes: 7 additions & 0 deletions .changeset/evil-numbers-shine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"ensadmin": minor
---

Refactored avatar URL handling to use centralized utilities from `ensnode-sdk`
Removed duplicate `buildEnsMetadataServiceAvatarUrl` and `buildUrl` functions in favor of SDK exports
Updated `ens-avatar` component to use new avatar URL utilities
10 changes: 10 additions & 0 deletions .changeset/stale-dots-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@ensnode/ensnode-react": minor
---

Added `useAvatarUrl` hook for resolving ENS avatar URLs with browser-supported protocols
Added `UseAvatarUrlResult` interface for avatar URL query results
Added `UseAvatarUrlParameters` interface for hook configuration
Added `AvatarUrl` type alias for avatar URL objects
Added support for custom fallback functions when avatar uses non-http/https protocols (e.g., `ipfs://`, `ar://`)
Added automatic fallback to ENS Metadata Service for unsupported protocol
2 changes: 1 addition & 1 deletion apps/ensadmin/biome.jsonc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.1/schema.json",
"$schema": "https://biomejs.dev/schemas/2.3.2/schema.json",
"extends": "//",
"css": {
"parser": {
Expand Down
25 changes: 25 additions & 0 deletions apps/ensadmin/src/app/@breadcrumbs/mock/ens-avatar/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use client";

import {
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { useRawConnectionUrlParam } from "@/hooks/use-connection-url-param";

export default function Page() {
const { retainCurrentRawConnectionUrlParam } = useRawConnectionUrlParam();
const uiMocksBaseHref = retainCurrentRawConnectionUrlParam("/mock");
return (
<>
<BreadcrumbLink href={uiMocksBaseHref} className="hidden md:block">
UI Mocks
</BreadcrumbLink>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>ENS Avatar</BreadcrumbPage>
</BreadcrumbItem>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ export function RenderRequestsOutput<KEY extends string>({
{
message: focused.error.message,
...(focused.error instanceof ClientError &&
!!focused.error.details && { details: focused.error.details }),
!!focused.error.details && {
details: focused.error.details,
}),
},
null,
2,
Expand Down
295 changes: 295 additions & 0 deletions apps/ensadmin/src/app/mock/ens-avatar/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
"use client";

import { AlertCircle, Check, X } from "lucide-react";
import { useMemo, useState } from "react";

import { ENSNamespaceIds } from "@ensnode/datasources";
import { useAvatarUrl } from "@ensnode/ensnode-react";
import { buildBrowserSupportedAvatarUrl, ENSNamespaceId, Name } from "@ensnode/ensnode-sdk";

import { EnsAvatar, EnsAvatarDisplay } from "@/components/ens-avatar";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";

const TEST_NAMES: Name[] = [
"lightwalker.eth",
"brantly.eth",
"ada.eth",
"jesse.base.eth",
"skeleton.mfpurrs.eth",
"vitalik.eth",
];

interface AvatarTestCardProps {
name: Name;
}

function AvatarTestCard({ name }: AvatarTestCardProps) {
const { data, isLoading, error } = useAvatarUrl({ name });

const hasAvatar = data?.browserSupportedAvatarUrl !== null;
const hasRawUrl = data?.rawAvatarTextRecord !== null;
const hasError = !!error;

return (
<Card
className={hasError ? "border-red-200" : hasAvatar ? "border-green-200" : "border-gray-200"}
>
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
{hasError ? (
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0" />
) : hasAvatar ? (
<Check className="h-5 w-5 text-green-600 flex-shrink-0" />
) : (
<X className="h-5 w-5 text-gray-400 flex-shrink-0" />
)}
<span>{name}</span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-center">
<EnsAvatar name={name} className="h-32 w-32" />
</div>

{error && (
<div className="p-2 bg-red-50 border border-red-200 rounded">
<p className="text-sm text-red-600">{error.message}</p>
</div>
)}

<div className="space-y-3">
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">Raw Avatar URL:</span>
{isLoading ? null : hasRawUrl ? (
<Check className="h-4 w-4 text-green-600" />
) : (
<X className="h-4 w-4 text-gray-400" />
)}
</div>
{isLoading || !data ? (
<Skeleton className="h-8 w-full rounded" />
) : (
<div className="text-xs text-muted-foreground break-all bg-muted p-2 rounded">
{data.rawAvatarTextRecord || "Not set"}
</div>
)}
</div>

<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">Browser-Supported URL:</span>
{isLoading ? null : hasAvatar ? (
<Check className="h-4 w-4 text-green-600" />
) : (
<X className="h-4 w-4 text-gray-400" />
)}
</div>
{isLoading || !data ? (
<Skeleton className="h-8 w-full rounded" />
) : (
<div className="text-xs text-muted-foreground break-all bg-muted p-2 rounded">
{data.browserSupportedAvatarUrl?.toString() || "Not available"}
</div>
)}
</div>

<div className="flex items-center justify-between p-2 bg-muted rounded">
<span className="text-sm font-medium">Uses Proxy:</span>
{isLoading || !data ? (
<Skeleton className="h-4 w-12 rounded" />
) : (
<div className="flex items-center gap-2">
{data.usesProxy ? (
<>
<Check className="h-4 w-4 text-green-600" />
<span className="text-sm text-green-600">Yes</span>
</>
) : (
<>
<X className="h-4 w-4 text-gray-400" />
<span className="text-sm text-gray-600">No</span>
</>
)}
</div>
)}
</div>
</div>
</CardContent>
</Card>
);
}

/**
* Wrapper component that resolves and renders an avatar using a custom raw avatar text record.
* Does not make any requests - only uses the provided inputs for resolution.
*/
function CustomAvatarWrapper({
rawAssetTextRecord,
name,
namespaceId,
}: {
rawAssetTextRecord: string;
name: Name;
namespaceId: ENSNamespaceId;
}) {
// Resolve the avatar URL using the same logic as useAvatarUrl
// This does NOT make any network requests - it only processes the provided raw text record
const resolvedData = useMemo(() => {
return buildBrowserSupportedAvatarUrl(rawAssetTextRecord, name, namespaceId);
}, [rawAssetTextRecord, name, namespaceId]);

const hasAvatar = resolvedData.browserSupportedAssetUrl !== null;

return (
<div className="space-y-4">
<div className="flex justify-center">
<EnsAvatarDisplay
name={name}
avatarUrl={resolvedData.browserSupportedAssetUrl}
className="h-32 w-32"
/>
</div>

{/* Display the resolution information */}
<div className="space-y-3">
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">Raw Avatar Text Record:</span>
{resolvedData.rawAssetTextRecord ? (
<Check className="h-4 w-4 text-green-600" />
) : (
<X className="h-4 w-4 text-gray-400" />
)}
</div>
<div className="text-xs text-muted-foreground break-all bg-muted p-2 rounded">
{resolvedData.rawAssetTextRecord || "Not set"}
</div>
</div>

<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">Browser-Supported URL:</span>
{hasAvatar ? (
<Check className="h-4 w-4 text-green-600" />
) : (
<X className="h-4 w-4 text-gray-400" />
)}
</div>
<div className="text-xs text-muted-foreground break-all bg-muted p-2 rounded">
{resolvedData.browserSupportedAssetUrl?.toString() || "Not available"}
</div>
</div>
</div>
</div>
);
}

function CustomAvatarUrlTestCard() {
const [namespaceId, setNamespaceId] = useState<ENSNamespaceId>(ENSNamespaceIds.Mainnet);
const [name, setName] = useState<Name>("" as Name);
const [rawAssetTextRecord, setRawAssetTextRecord] = useState("");

return (
<Card className="border-blue-200">
<CardHeader>
<CardTitle className="text-lg">Custom Avatar Text Record</CardTitle>
<CardDescription>
Enter an ENS namespace, name, and raw avatar text record to test resolution and display.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* Namespace Selection */}
<div className="space-y-2">
<Label htmlFor="namespace">ENS Namespace</Label>
<Select
value={namespaceId}
onValueChange={(value) => setNamespaceId(value as ENSNamespaceId)}
>
<SelectTrigger id="namespace">
<SelectValue placeholder="Select namespace" />
</SelectTrigger>
<SelectContent>
<SelectItem value={ENSNamespaceIds.Mainnet}>Mainnet</SelectItem>
<SelectItem value={ENSNamespaceIds.Sepolia}>Sepolia</SelectItem>
<SelectItem value={ENSNamespaceIds.Holesky}>Holesky</SelectItem>
<SelectItem value={ENSNamespaceIds.EnsTestEnv}>ENS Test Env</SelectItem>
</SelectContent>
</Select>
</div>

{/* Name Input */}
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
type="text"
placeholder="vitalik.eth"
value={name}
onChange={(e) => setName(e.target.value as Name)}
/>
</div>

{/* Avatar URL Input */}
<div className="space-y-2">
<Label htmlFor="avatar-url">Raw Avatar Text Record</Label>
<Input
id="avatar-url"
type="text"
placeholder="https://example.com/avatar.jpg, ipfs://..., eip155:1/erc721:..., etc."
value={rawAssetTextRecord}
onChange={(e) => setRawAssetTextRecord(e.target.value)}
/>
</div>

{/* Avatar Display */}
{rawAssetTextRecord && name && (
<CustomAvatarWrapper
rawAssetTextRecord={rawAssetTextRecord}
name={name}
namespaceId={namespaceId}
/>
)}
</CardContent>
</Card>
);
}

export default function MockAvatarUrlPage() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a UI where I can input the following:

  • An ENSNamespace
  • A Name
  • A raw avatar text record

And then view the result of this. Note how this functionality I'm requesting should make 0 requests for the real avatar text record of the name and therefore does not need to care at all about the active namespace of the connected ENSNode instance.

return (
<section className="flex flex-col gap-6 p-6 max-sm:p-4">
<Card>
<CardHeader>
<CardTitle className="text-2xl leading-normal">Mock: ENS Avatar</CardTitle>
<CardDescription>
Displays avatar images, raw URLs, browser-supported URLs, and proxy usage for each ENS
name.
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{/* Custom URL Test Section */}
<CustomAvatarUrlTestCard />

{/* Existing Test Names Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{TEST_NAMES.map((name) => (
<AvatarTestCard key={name} name={name} />
))}
</div>
</div>
</CardContent>
</Card>
</section>
);
}
3 changes: 3 additions & 0 deletions apps/ensadmin/src/app/mock/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export default function MockList() {
DisplayIdentity
</Link>
</Button>
<Button asChild variant="outline" size="sm">
<Link href={retainCurrentRawConnectionUrlParam("/mock/ens-avatar")}>ENS Avatar</Link>
</Button>
</div>
</CardContent>
</Card>
Expand Down
6 changes: 1 addition & 5 deletions apps/ensadmin/src/app/name/_components/ProfileHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,7 @@ export function ProfileHeader({ name, namespaceId, headerImage, websiteUrl }: Pr
<CardContent className="pt-6">
<div className="flex items-start justify-between">
<div className="flex items-center gap-4">
<EnsAvatar
className="-mt-16 h-20 w-20 ring-4 ring-white"
name={name}
namespaceId={namespaceId}
/>
<EnsAvatar className="-mt-16 h-20 w-20 ring-4 ring-white bg-white" name={name} />
<div className="flex-1">
<h1>
<NameDisplay className="text-3xl font-bold" name={name} />
Expand Down
Loading