Files
sre-05-dex/packages/nextjs/components/scaffold-eth/Input/AddressInput.tsx
2026-01-21 20:45:23 +07:00

121 lines
3.8 KiB
TypeScript

import { useEffect, useState } from "react";
import { blo } from "blo";
import { useDebounceValue } 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";
/**
* Address input with ENS name resolution
*/
export const AddressInput = ({ value, name, placeholder, onChange, disabled }: CommonInputProps<Address | 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] = useDebounceValue(value, 500);
const debouncedValue = isAddress(value) ? value : _debouncedValue;
const isDebouncedValueLive = debouncedValue === value;
// 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,
isError: isEnsAddressError,
isSuccess: isEnsAddressSuccess,
} = useEnsAddress({
name: settledValue,
chainId: 1,
query: {
gcTime: 30_000,
enabled: isDebouncedValueLive && isENS(debouncedValue),
},
});
const [enteredEnsName, setEnteredEnsName] = useState<string>();
const {
data: ensName,
isLoading: isEnsNameLoading,
isError: isEnsNameError,
isSuccess: isEnsNameSuccess,
} = useEnsName({
address: settledValue as Address,
chainId: 1,
query: {
enabled: isAddress(debouncedValue),
gcTime: 30_000,
},
});
const { data: ensAvatar, isLoading: isEnsAvatarLoading } = useEnsAvatar({
name: ensName ? normalize(ensName) : undefined,
chainId: 1,
query: {
enabled: Boolean(ensName),
gcTime: 30_000,
},
});
// ens => address
useEffect(() => {
if (!ensAddress) return;
// ENS resolved successfully
setEnteredEnsName(debouncedValue);
onChange(ensAddress);
}, [ensAddress, onChange, debouncedValue]);
useEffect(() => {
setEnteredEnsName(undefined);
}, [value]);
const reFocus =
isEnsAddressError ||
isEnsNameError ||
isEnsNameSuccess ||
isEnsAddressSuccess ||
ensName === null ||
ensAddress === null;
return (
<InputBase<Address>
name={name}
placeholder={placeholder}
error={ensAddress === null}
value={value as Address}
onChange={onChange}
disabled={isEnsAddressLoading || isEnsNameLoading || disabled}
reFocus={reFocus}
prefix={
ensName ? (
<div className="flex bg-base-300 rounded-l-full items-center">
{isEnsAvatarLoading && <div className="skeleton bg-base-200 w-[35px] h-[35px] rounded-full shrink-0"></div>}
{ensAvatar ? (
<span className="w-[35px]">
{
// eslint-disable-next-line
<img className="w-full rounded-full" src={ensAvatar} alt={`${ensAddress} avatar`} />
}
</span>
) : null}
<span className="text-accent px-2">{enteredEnsName ?? ensName}</span>
</div>
) : (
(isEnsNameLoading || isEnsAddressLoading) && (
<div className="flex bg-base-300 rounded-l-full items-center gap-2 pr-2">
<div className="skeleton bg-base-200 w-[35px] h-[35px] rounded-full shrink-0"></div>
<div className="skeleton bg-base-200 h-3 w-20"></div>
</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-full!" src={blo(value as `0x${string}`)} width="35" height="35" />
}
/>
);
};