agsola-react-ens-address
Version:
React ENS address component
369 lines (329 loc) • 11.9 kB
text/typescript
import { normalize } from 'viem/ens'
import React, {
useState,
useEffect,
useRef,
isValidElementType,
useCallback,
} from 'react'
import PropTypes from 'prop-types'
import _ from 'lodash'
import {
getEthAddressType,
isAddress,
ETH_ADDRESS_TYPE,
} from './utils/address.js'
import Loader from './Loader.js'
import { SingleNameBlockies } from './Blockies.js'
import { PublicClient } from 'viem'
//import './style.css' Import in your own website
const ENS_NOT_FOUND = 'ENS name not found'
function Address(props) {
const [resolvedAddress, setResolvedAddress] = useState(null)
const inputValue = props.inputValue;
const setInputValue = props.setterFunction;
//const [inputValue, setInputValue] = useState('')
const [isResolvingInProgressValue, setIsResolvingInProgress] = useState(0);
const isResolvingInProgress = isResolvingInProgressValue > 0;
const [error, setError] = useState(null)
const [avatarURL, setAvatarURL] = useState(null)
//const [ENS, setENS] = useState(null)
const [provider, setProvider] = useState<PublicClient>(null);
const currentInput = useRef()
const inputDebouncerHandler = async (input) => {
try {
// Regular expression to check if input is a domain
const domainRegex = new RegExp('^[^.]{1,63}(\\.[^.]{2,3})+$');
const starProtocolDomainRegex = new RegExp('^[^.]{1,63}\\.(zk|linea|line|base|bas|ba|lin|li)$');
const spaceIdDomainRegex = new RegExp('^[^.]{1,63}\\.(bnb|arb|bn|ar)$');
if (starProtocolDomainRegex.test(input)) {
if (input.endsWith('.zk')) {
const urlToFetch = `https://omniapi.zkns.app/domain-resolver/getRecord/${input}`;
// console.log("1urlToFetch", urlToFetch);
const fetchData = await fetch(urlToFetch);
const data = await fetchData.text();
// console.log("starProtocolDomain data: ", data);
if (isAddress(data)) {
setError(null);
setResolvedAddress(data);
props.onResolve({
address: data,
name: input,
type: ETH_ADDRESS_TYPE.address,
});
props.onError(null);
} else {
console.error("starProtocolDomain: Invalid domain");
}
} else if (input.endsWith('.linea') | input.endsWith('.base')) {
const urlToFetch = `https://mainnet-api.sns.so/domain/getRecord/${input}`;
// console.log("2urlToFetch", urlToFetch);
const result = await fetch(urlToFetch);
// console.log("starProtocolDomain result: ", result);
const json = await result.json();
// console.log("starProtocolDomain json: ", json);
const { data, code, message } = json;
// console.log("starProtocolDomain", data, code, message);
if (code === 200 && isAddress(data)) {
setError(null);
setResolvedAddress(data);
props.onResolve({
address: data,
name: input,
type: ETH_ADDRESS_TYPE.address,
});
props.onError(null);
} else {
console.error("starProtocolDomain: Invalid domain");
}
} else {
// Todavía no ha llegado aquí.
setError(null);
}
} else if (spaceIdDomainRegex.test(input)) {
if (input.endsWith('.arb') || input.endsWith('.bnb')) {
let tld;
if (input.endsWith('.arb')) {
tld = "arb1";
} else {
tld = "bnb";
}
const urlToFetch = `https://api.prd.space.id/v1/getAddress?tld=${tld}&domain=${input}`;
// console.log("spaceId urlToFetch:", urlToFetch);
const result = await fetch(urlToFetch);
// console.log("spaceId result: ", result);
const json = await result.json();
// console.log("spaceId json: ", json);
const { code, address } = json;
// console.log("spaceId code, address: ", code, address);
if (code === 0 && isAddress(address) && address !== "0x0000000000000000000000000000000000000000") {
setError(null);
setResolvedAddress(address);
props.onResolve({
address: address,
name: input,
type: ETH_ADDRESS_TYPE.address,
});
props.onError(null);
} else {
console.error("starProtocolDomain: Invalid domain");
}
}
} else if (domainRegex.test(input)) {
const result = await resolveName(input)
if (input === currentInput.current) {
if (error !== null) setError(null);
const { address, type, name, avatar } = result;
if (type === ETH_ADDRESS_TYPE.name) {
setResolvedAddress(address)
} else if (type === ETH_ADDRESS_TYPE.address) {
setResolvedAddress(name)
}
setAvatarURL(avatar)
props.onResolve(result)
props.onError(null)
}
} else {
if (error) setError(null);
}
//if newest continue, otherwise ignore
} catch (error) {
setError(error.toString())
setResolvedAddress(null)
setAvatarURL(null)
props.onResolve({
address: input,
name: null,
type: null,
})
props.onError(error)
}
}
const inputDebouncer = _.debounce(inputDebouncerHandler, 500)
useEffect(() => {
setProvider(props.provider);
/* async function setup() {
const options = {}
if(props.ensAddress){
options.ensAddress = props.ensAddress
}
if (props.provider) {
options.customProvider = props.provider
}
const { ens } = await setupENS(options)
setENS(ens)
}
setup() */
}, [props.provider])
const handleInput = useCallback(
async (address) => {
if (!address || address.length === 0) {
setInputValue('')
setError(null)
setResolvedAddress(null)
if (inputDebouncer) {
inputDebouncer.cancel()
}
}
setInputValue(address)
if (inputDebouncer) {
inputDebouncer(address)
}
},
[inputDebouncer]
)
useEffect(() => {
if (props.presetValue.length !== 0) {
handleInput(props.presetValue)
}
}, [props.presetValue])
useEffect(() => {
if (props.inputValue.length === 0) {
handleInput(props.inputValue)
}
}, [props.inputValue])
if (!provider) {
return <Loader className="loader" />
}
const handleResolver = async (fn) => {
try {
// console.log("ags BEFORE RESOLVER");
setIsResolvingInProgress((current) => current+1);
setResolvedAddress(null)
return await fn()
} catch (error) {
if (error && error.message && error.message === ENS_NOT_FOUND) return
throw error
} finally {
setIsResolvingInProgress((current) => current-1);
// console.log("ags AFTER RESOLVER");
}
}
const resolveName = async (inputValue) => {
// update latest input resolving
currentInput.current = inputValue
const addressType = getEthAddressType(inputValue)
const publicClient: PublicClient = provider;
if (addressType === ETH_ADDRESS_TYPE.name) {
return await handleResolver(async () => {
const resolvedAddress = await publicClient.getEnsResolver({ name: normalize(inputValue)});
const resolvedAvatar = await publicClient.getEnsAvatar({ name: normalize(inputValue)});
return {
input: inputValue,
address: resolvedAddress,
avatar: resolvedAvatar,
name: inputValue,
type: 'name',
}})
} else if (addressType === ETH_ADDRESS_TYPE.address) {
return await handleResolver(async () => {
// const tempName = await provider.lookupAddress(inputValue);
const tempName = await publicClient.getEnsName({ address: inputValue});
let tempAvatar;
if (tempName) {
// tempAvatar = await provider.getAvatar(tempName)
tempAvatar = await publicClient.getEnsAvatar({ name: tempName});
}
return {
input: inputValue,
name: tempName,
avatar: tempAvatar,
address: inputValue,
type: 'address',
}})
}
setAvatarURL(null)
throw 'Incorrect address or name'
}
const isResolveNameNotFound = () => {
// console.log("ags resolvedAddress", resolvedAddress);
// console.log("ags inputValue", inputValue);
// console.log("ags isResolvingInProgress", isResolvingInProgress);
return (
!resolvedAddress &&
inputValue &&
!isResolvingInProgress &&
getEthAddressType(inputValue) !== ETH_ADDRESS_TYPE.address
)
}
const showBlockies = () => {
if (props.showBlockies) {
let address
if (isAddress(inputValue)) {
address = inputValue
} else if (isAddress(resolvedAddress)) {
address = resolvedAddress
}
if (address) {
return (
<SingleNameBlockies
address={address.toLowerCase()}
imageSize={30}
className="blockies"
avatarURL={avatarURL}
/>
)
}
}
}
return (
<div className={`cmp-address-wrapper ${props.className}`}>
<div
className={`w-full cmp-address ${resolvedAddress ? 'resolved' : ''} ${
error ? 'error' : ''
}`}
>
<div className="input-wrapper">
<div className="indicator">
{isResolvingInProgress && <Loader className="loader" />}
{!isResolvingInProgress && showBlockies()}
{isResolveNameNotFound() && (
<div
alt="warning icon"
className="icon-wrapper error-icon mt-2 ml-2"
>{<svg width="19" height="17" xmlns="http://www.w3.org/2000/svg"><path d="M9.2456 5.0302c.5001-.1232 1.021.1232 1.2294.5954.0625.1643.1042.3285.1042.5133-.0209.5133-.0625 1.047-.1042 1.5604-.0417.8007-.1042 1.622-.1459 2.4227-.0208.2669-.0208.2874-.0208.5543-.0208.4312-.3542.7597-.7918.7597-.4376 0-.771-.308-.7918-.7391-.0625-1.2525-.1458-2.2996-.2083-3.552-.0209-.3285-.0417-.657-.0625-1.006-.0209-.5133.2917-.965.7917-1.1087m.271 9.4444c-.5835 0-1.0627-.4722-1.0627-1.047 0-.575.4792-1.0472 1.0626-1.0472.5834 0 1.0627.4723 1.0418 1.0677.0209.5543-.4792 1.0265-1.0418 1.0265M2.849 17h13.2936c2.1878 0 3.563-2.3611 2.4795-4.209L11.9544 1.4168c-1.0835-1.889-3.834-1.889-4.9174 0L.3694 12.791C-.6933 14.6594.661 17 2.8489 17" fill="#DC2E2E" fillRule="evenodd"/></svg>}
</div>
)}
{props.DefaultIcon && !inputValue && <DefaultIcon />}
</div>
<input
disabled={props.disabled}
value={inputValue}
onChange={(e) => handleInput(e.currentTarget.value)}
placeholder={props.placeholder}
spellCheck={false}
name="ethereum"
/>
</div>
<div className="info-wrapper">
{resolvedAddress && <div className="resolved">{resolvedAddress}</div>}
</div>
</div>
</div>
)
}
Address.propTypes = {
provider: PropTypes.object.isRequired,
placeholder: PropTypes.string,
showBlockies: PropTypes.bool,
DefaultIcon: (props, propName) => {
if (props[propName] && !isValidElementType(props[propName])) {
return new Error(
`Invalid prop 'component' supplied to 'Route': the prop is not a valid React component`
)
}
},
onError: PropTypes.func,
onResolve: PropTypes.func,
className: PropTypes.string,
}
Address.defaultProps = {
presetValue: '',
placeholder: 'Enter Ethereum name or address',
showBlockies: true,
DefaultIcon: null,
className: '',
onError: function () {},
onResolve: function () {},
}
export default Address