Skip to content

Commit

Permalink
Use ensdata api to reduce api calls
Browse files Browse the repository at this point in the history
  • Loading branch information
portdeveloper committed Dec 28, 2024
1 parent cc27388 commit a38887f
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 85 deletions.
36 changes: 4 additions & 32 deletions packages/nextjs/components/scaffold-eth/Address.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { useEffect, useState } from "react";
import { useState } from "react";
import { CopyToClipboard } from "react-copy-to-clipboard";
import { Address as AddressType, isAddress } from "viem";
import { hardhat } from "viem/chains";
import { normalize } from "viem/ens";
import { useEnsAvatar, useEnsName } from "wagmi";
import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline";
import { BlockieAvatar } from "~~/components/scaffold-eth";
import { useEnsData } from "~~/hooks/useEnsData";
import { useGlobalState } from "~~/services/store/store";
import { getBlockExplorerAddressLink } from "~~/utils/scaffold-eth";

Expand All @@ -30,36 +29,9 @@ const blockieSizeMap = {
* Displays an address (or ENS) with a Blockie image and option to copy address.
*/
export const Address = ({ address, disableAddressLink, format, size = "base" }: AddressProps) => {
const [ens, setEns] = useState<string | null>();
const [ensAvatar, setEnsAvatar] = useState<string | null>();
const [addressCopied, setAddressCopied] = useState(false);

const targetNetwork = useGlobalState(state => state.targetNetwork);

const { data: fetchedEns } = useEnsName({
address: address,
chainId: 1,
query: {
enabled: isAddress(address ?? ""),
},
});
const { data: fetchedEnsAvatar } = useEnsAvatar({
name: fetchedEns ? normalize(fetchedEns) : undefined,
chainId: 1,
query: {
enabled: Boolean(fetchedEns),
gcTime: 30_000,
},
});

// We need to apply this pattern to avoid Hydration errors.
useEffect(() => {
setEns(fetchedEns);
}, [fetchedEns]);

useEffect(() => {
setEnsAvatar(fetchedEnsAvatar);
}, [fetchedEnsAvatar]);
const { ens, avatar_url } = useEnsData(address ?? "");

// Skeleton UI
if (!address) {
Expand Down Expand Up @@ -91,7 +63,7 @@ export const Address = ({ address, disableAddressLink, format, size = "base" }:
<div className="flex-shrink-0">
<BlockieAvatar
address={address}
ensImage={ensAvatar}
ensImage={avatar_url}
size={(blockieSizeMap[size] * 24) / blockieSizeMap["base"]}
/>
</div>
Expand Down
82 changes: 29 additions & 53 deletions packages/nextjs/components/scaffold-eth/Input/AddressInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,64 +2,37 @@ import { useCallback, useEffect, useState } from "react";
import { blo } from "blo";
import { useDebounce } from "usehooks-ts";
import { Address, isAddress } from "viem";
import { normalize } from "viem/ens";
import { useEnsAddress, useEnsAvatar, useEnsName } from "wagmi";
import { CommonInputProps, InputBase, isENS } from "~~/components/scaffold-eth";
import { CommonInputProps, InputBase } from "~~/components/scaffold-eth";
import { useEnsData } from "~~/hooks/useEnsData";

/**
* Address input with ENS name resolution
*/
export const AddressInput = ({ value, name, placeholder, onChange, disabled }: CommonInputProps<Address | string>) => {
const [displayValue, setDisplayValue] = useState<string>(value as string);

// Debounce the input to keep clean RPC calls when resolving ENS names
// If the input is an address, we don't need to debounce it
const _debouncedValue = useDebounce(value, 500);
const debouncedValue = isAddress(value) ? value : _debouncedValue;
const isDebouncedValueLive = debouncedValue === value;
const _debouncedValue = useDebounce(displayValue, 500);
const debouncedValue = isAddress(displayValue) ? displayValue : _debouncedValue;
const isDebouncedValueLive = debouncedValue === displayValue;

// If the user changes the input after an ENS name is already resolved, we want to remove the stale result
const settledValue = isDebouncedValueLive ? debouncedValue : undefined;

const { data: ensAddress, isLoading: isEnsAddressLoading } = useEnsAddress({
name: settledValue,
chainId: 1,
query: {
gcTime: 30_000,
enabled: isDebouncedValueLive && isENS(debouncedValue),
},
});

const [enteredEnsName, setEnteredEnsName] = useState<string>();
const { data: ensName, isLoading: isEnsNameLoading } = useEnsName({
address: settledValue as Address,
chainId: 1,
query: {
enabled: isAddress(debouncedValue),
gcTime: 30_000,
},
});

const { data: ensAvatar } = useEnsAvatar({
name: ensName ? normalize(ensName) : undefined,
chainId: 1,
query: {
enabled: Boolean(ensName),
gcTime: 30_000,
},
});
const { ens: ensName, avatar_url: ensAvatar, address: ensAddress } = useEnsData(settledValue as string);

// ens => address
useEffect(() => {
if (!ensAddress) return;
if (ensName && ensAddress) {
setDisplayValue(ensAddress);
onChange(ensAddress as Address);
}
}, [ensName, onChange, settledValue, ensAddress]);

// ENS resolved successfully
setEnteredEnsName(debouncedValue);
onChange(ensAddress);
}, [ensAddress, onChange, debouncedValue]);

const handleChange = useCallback(
(newValue: Address) => {
setEnteredEnsName(undefined);
onChange(newValue);
const handleInputChange = useCallback(
(newValue: string) => {
setDisplayValue(newValue);
onChange(newValue as Address);
},
[onChange],
);
Expand All @@ -68,29 +41,32 @@ export const AddressInput = ({ value, name, placeholder, onChange, disabled }: C
<InputBase<Address>
name={name}
placeholder={placeholder}
error={ensAddress === null}
value={value as Address}
onChange={handleChange}
disabled={isEnsAddressLoading || isEnsNameLoading || disabled}
error={ensName === null}
value={displayValue as Address}
onChange={handleInputChange}
disabled={disabled}
prefix={
ensName && (
<div className="flex bg-base-100 rounded-l-lg items-center">
{ensAvatar ? (
<span className="w-[35px]">
{
// eslint-disable-next-line
<img className="w-full rounded-lg" src={ensAvatar} alt={`${ensAddress} avatar`} />
<img className="w-full rounded-lg" src={ensAvatar} alt={`${ensName} avatar`} />
}
</span>
) : null}
<span className="text-accent px-2">{enteredEnsName ?? ensName}</span>
<span className="text-accent px-2">{ensName}</span>
</div>
)
}
suffix={
// Don't want to use nextJS Image here (and adding remote patterns for the URL)
// eslint-disable-next-line @next/next/no-img-element
value && <img alt="" className="!rounded-lg" src={blo(value as `0x${string}`)} width="35" height="35" />
isAddress(displayValue) &&
!ensAvatar && (
// Don't want to use nextJS Image here (and adding remote patterns for the URL)
// eslint-disable-next-line @next/next/no-img-element
<img alt="" className="!rounded-lg" src={blo(displayValue as `0x${string}`)} width="35" height="35" />
)
}
/>
);
Expand Down
32 changes: 32 additions & 0 deletions packages/nextjs/hooks/useEnsData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useQuery } from "@tanstack/react-query";
import { Address as AddressType, isAddress } from "viem";

const isEns = (value: string) => value.endsWith(".eth") || value.endsWith(".xyz");

export const useEnsData = (addressOrEns: AddressType | string) => {
const { data: ensData } = useQuery({
queryKey: ["ensData", addressOrEns],
queryFn: async () => {
if (!addressOrEns || (!isAddress(addressOrEns) && !isEns(addressOrEns))) return {};

const response = await fetch(`https://ensdata.net/${addressOrEns}`);
const data = await response.json();

if (data.error) {
return { error: true };
}

return {
ens: data.ens,
address: data.address,
avatar_url: data.avatar_url || data.records_primary.avatar_small,
};
},
staleTime: 24 * 60 * 60 * 1000, // 24 hours
gcTime: 7 * 24 * 60 * 60 * 1000, // 7 days
enabled: Boolean(addressOrEns) && (isEns(addressOrEns) || isAddress(addressOrEns)),
retry: false,
});

return ensData || {};
};

0 comments on commit a38887f

Please sign in to comment.