Skip to content
Draft
Changes from all commits
Commits
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
80 changes: 60 additions & 20 deletions apps/dashboard/shared/hooks/useEnsData.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,37 @@
"use client";

import { useQuery, useQueries } from "@tanstack/react-query";
import { Address, isAddress } from "viem";
import { normalize } from "viem/ens";
import { publicClient } from "@/shared/services/wallet/wallet";
import axios from "axios";
import { Address } from "viem";

const getEnsUrl = (address: Address | `${string}.eth`) => {
return `https://api.ethfollow.xyz/api/v1/users/${address}/ens`;
};

type EnsRecords = {
avatar?: string;
"com.discord"?: string;
"com.github"?: string;
"com.twitter"?: string;
description?: string;
email?: string;
header?: string;
location?: string;
name?: string;
"org.telegram"?: string;
url?: string;
[key: string]: string | undefined;
};

type EnsApiResponse = {
ens: {
name: string;
address: Address;
avatar: string | null;
records: EnsRecords | null;
updated_at: string;
};
};

type EnsData = {
address: Address;
Expand All @@ -14,7 +42,7 @@ type EnsData = {

/**
* Hook to fetch ENS data for a single address
* @param address - Ethereum address (e.g., "0x123...")
* @param address - Ethereum address (e.g., "0x123..." )
* @returns Object containing ENS data, error, and loading state
*/
export const useEnsData = (address: Address | null | undefined) => {
Expand All @@ -35,32 +63,35 @@ export const useEnsData = (address: Address | null | undefined) => {
};

/**
* Fetches ENS data using viem for a single address
* Fetches ENS data from the API for a single address
* @param address - Ethereum address
* @returns Promise resolving to EnsData
* @throws Error if the API request fails or response is invalid
*/
export const fetchEnsDataFromAddress = async ({
address,
}: {
address: Address;
}): Promise<EnsData> => {
let ensName: string | null = null;
let avatarUrl: string | null = null;
const response = await axios.get<EnsApiResponse>(getEnsUrl(address));
const data = response.data;

if (isAddress(address)) {
ensName = await publicClient.getEnsName({ address });
// Validate response structure
if (!data?.ens) {
throw new Error("Invalid ENS API response: missing ens field");
}

// Get avatar URL if we have an ENS name
if (ensName) {
avatarUrl = await publicClient.getEnsAvatar({ name: normalize(ensName) });
if (!data.ens.address) {
throw new Error("Invalid ENS API response: missing address field");
}
Comment on lines +80 to 86
Copy link
Member

@pikonha pikonha Jan 5, 2026

Choose a reason for hiding this comment

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

what happens with the UI when this is thrown?

if a problem happens, we should handle it silently

Copy link
Member Author

Choose a reason for hiding this comment

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

Nothing, UI falls back silently


// Empty name is valid (means no ENS name exists for this address)
// Transform API response to match expected EnsData structure
return {
address: address,
avatar_url: avatarUrl,
ens: ensName || "",
avatar: avatarUrl,
address: data.ens.address,
avatar_url: data.ens.avatar,
ens: data.ens.name,
Copy link

Choose a reason for hiding this comment

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

Missing null fallback for ens.name field

The old code used ensName || "" to guarantee the ens field is always a string, providing defensive handling for null/undefined values. The new code directly assigns data.ens.name without a fallback. If the ethfollow API returns null for the name field (which external APIs can do despite the TypeScript type definition), the returned EnsData would violate its type contract where ens is typed as string, potentially causing unexpected behavior in consumers.

Fix in Cursor Fix in Web

avatar: data.ens.avatar,
};
};

Expand All @@ -69,10 +100,19 @@ export const fetchAddressFromEnsName = async ({
}: {
ensName: `${string}.eth`;
}): Promise<Address | null> => {
const address = await publicClient.getEnsAddress({
name: normalize(ensName),
});
return address || null;
const response = await axios.get<EnsApiResponse>(getEnsUrl(ensName));
const data = response.data;

// Validate response structure
if (!data?.ens) {
return null;
}

if (!data.ens.address) {
return null;
}

return data.ens.address;
Copy link

Choose a reason for hiding this comment

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

Function throws instead of returning null, breaking callers

fetchAddressFromEnsName now throws errors when the API response is missing expected fields, but the function signature still promises Promise<Address | null> and callers expect null for unresolved ENS names. The callers in DelegateDelegationHistoryTable.tsx and BalanceHistoryTable.tsx call this function without try-catch blocks, so any thrown error will cause an unhandled promise rejection and break the UI. As the PR reviewer noted, errors need to be handled silently. The function needs to either return null for API failures/missing data or callers need error handling.

Fix in Cursor Fix in Web

};

/**
Expand Down