diff --git a/src/app/components/elements/ContactAutocomplete.tsx b/src/app/components/elements/ContactAutocomplete.tsx index baa161694..1367bb7e8 100644 --- a/src/app/components/elements/ContactAutocomplete.tsx +++ b/src/app/components/elements/ContactAutocomplete.tsx @@ -26,7 +26,7 @@ import { LOAD_MORE_ON_CONTACTS_DROPDOWN_FROM_END, } from "app/defaults"; import { useContacts } from "app/hooks/contacts"; -import { useAccounts, useEns } from "app/hooks"; +import { useAccounts, useEns, useRns } from "app/hooks"; import ScrollAreaContainer from "./ScrollAreaContainer"; import AddressField, { AddressFieldProps } from "./AddressField"; import WalletName from "./WalletName"; @@ -160,10 +160,12 @@ const ContactAutocomplete = forwardRef< const { paste } = usePasteFromClipboard(setValue); const { getAddressByEns, watchEns } = useEns(); + const { getAddressByRns, watchRns } = useRns(); useEffect(() => { watchEns(value, setValue); - }, [value, setValue, getAddressByEns, watchEns]); + watchRns(value, setValue); + }, [value, setValue, getAddressByEns, watchEns, getAddressByRns, watchRns]); const pasteButton = useMemo(() => { return ( diff --git a/src/app/components/elements/WalletName.tsx b/src/app/components/elements/WalletName.tsx index 0eaf26e87..b5167d585 100644 --- a/src/app/components/elements/WalletName.tsx +++ b/src/app/components/elements/WalletName.tsx @@ -3,7 +3,7 @@ import classNames from "clsx"; import { TReplace } from "lib/ext/i18n/react"; import { Account, AccountSource } from "core/types"; -import { useEns } from "app/hooks"; +import { useEns, useRns } from "app/hooks"; import { ReactComponent as GoogleIcon } from "app/icons/google.svg"; import { ReactComponent as FacebookIcon } from "app/icons/facebook.svg"; @@ -27,13 +27,16 @@ const WalletName: FC = ({ iconClassName, }) => { const { getEnsName } = useEns(); + const { getRnsName } = useRns(); const [ensName, setEnsName] = useState(null); useEffect(() => { const fetchEnsName = async () => { try { - const name = await getEnsName(wallet.address); + const name = + (await getEnsName(wallet.address)) || + (await getRnsName(wallet.address)); setEnsName(name); } catch (error) { console.error(error); @@ -41,7 +44,7 @@ const WalletName: FC = ({ }; fetchEnsName(); - }, [getEnsName, wallet.address]); + }, [getEnsName, getRnsName, wallet.address]); return ( hex.slice(2); + +const RNS_REGISTRY_ABI = [ + "function resolver(bytes32 node) public view returns (address)", +]; + +const RNS_ADDR_RESOLVER_ABI = [ + "function addr(bytes32 node) public view returns (address)", +]; + +const RNS_NAME_RESOLVER_ABI = [ + "function name(bytes32 node) external view returns (string)", +]; + +const RNSProvider = new JsonRpcProvider(ROOTSTOCK_RPC_NODE); +const rnsRegistryContract = new Contract( + RNS_REGISTRY_ADDRESS, + RNS_REGISTRY_ABI, + RNSProvider, +); + +const resolveRnsName = async (name: string): Promise => { + const nameHash = namehash(name); + const resolverAddress = await rnsRegistryContract.resolver(nameHash); + + if (resolverAddress === AddressZero) { + return null; + } + + const addrResolverContract = new Contract( + resolverAddress, + RNS_ADDR_RESOLVER_ABI, + RNSProvider, + ); + + const address = await addrResolverContract.addr(nameHash); + + if (address === undefined || address === null) { + return null; + } + + return address.toLowerCase(); +}; + +const lookupAddress = async (address: string): Promise => { + const reverseRecordHash = namehash(`${stripHexPrefix(address)}.addr.reverse`); + + const resolverAddress = await rnsRegistryContract.resolver(reverseRecordHash); + + if (resolverAddress === AddressZero) { + return null; + } + + const nameResolverContract = new Contract( + resolverAddress, + RNS_NAME_RESOLVER_ABI, + RNSProvider, + ); + + const name = await nameResolverContract.name(reverseRecordHash); + + if (name === undefined) { + return null; + } + + return name; +}; + +const useRns = () => { + const getRnsName = useCallback(async (address: string) => { + const rnsNameLS = localStorage.getItem(`RNS_${address}`); + const parsedData = rnsNameLS ? JSON.parse(rnsNameLS) : null; + if (parsedData && parsedData.expirationTimestamp > Date.now()) { + return parsedData.ensName; + } else { + const rnsName = await lookupAddress(address); + + if (rnsName) { + const data = { + rnsName, + expirationTimestamp: Date.now() + ONE_DAY, + }; + localStorage.setItem(`RNS_${address}`, JSON.stringify(data)); + return rnsName; + } else { + return null; + } + } + }, []); + + const getAddressByRns = useCallback(async (rnsName: string) => { + const address = await resolveRnsName(rnsName); + + if (address) { + return address; + } else { + return null; + } + }, []); + + const watchRns = useCallback( + async (value: any, cb: (address: string) => void) => { + const ethereumAddressOrRNSRegex = + /^(0x[a-fA-F0-9]{40})|([a-zA-Z0-9-]+\.rsk)$/; + if (value && typeof value == "string") { + const isValid = ethereumAddressOrRNSRegex.test(value); + if (isValid && value.includes(".rsk")) { + const response = await getAddressByRns(value); + if (response) { + cb(response); + } + } + } + }, + [getAddressByRns], + ); + + return useMemo( + () => ({ + getRnsName, + getAddressByRns, + watchRns, + }), + [getRnsName, getAddressByRns, watchRns], + ); +}; + +export { useRns };