diff --git a/packages/starknet-snap/src/rpcs/get-addr-from-starkname.ts b/packages/starknet-snap/src/rpcs/get-addr-from-starkname.ts index 540ec72f..4c7afe23 100644 --- a/packages/starknet-snap/src/rpcs/get-addr-from-starkname.ts +++ b/packages/starknet-snap/src/rpcs/get-addr-from-starkname.ts @@ -30,7 +30,7 @@ export type GetAddrFromStarkNameResponse = Infer< >; /** - * The RPC handler to add a ERC20 asset. + * The RPC handler to get a StarkName by a Starknet address. */ export class GetAddrFromStarkNameRpc extends RpcController< GetAddrFromStarkNameParams, @@ -81,9 +81,10 @@ export class GetAddrFromStarkNameRpc extends RpcController< const { chainId, starkName } = params; const network = await this.getNetworkFromChainId(chainId); - const getAddressResp = await getAddrFromStarkNameUtil(network, starkName); - return getAddressResp; + const address = await getAddrFromStarkNameUtil(network, starkName); + + return address; } } diff --git a/packages/starknet-snap/src/utils/starknetUtils.test.ts b/packages/starknet-snap/src/utils/starknetUtils.test.ts index 307729d3..2bfc56e7 100644 --- a/packages/starknet-snap/src/utils/starknetUtils.test.ts +++ b/packages/starknet-snap/src/utils/starknetUtils.test.ts @@ -170,3 +170,37 @@ describe('getEstimatedFees', () => { }); }); }); + +describe('isValidStarkName', () => { + it.each([ + { starkName: 'valid.stark', expected: true }, + { starkName: 'valid-name.stark', expected: true }, + { starkName: 'valid123.stark', expected: true }, + { starkName: 'valid-name123.stark', expected: true }, + { starkName: 'valid.subdomain.stark', expected: true }, + { starkName: '1-valid.stark', expected: true }, + { + starkName: 'valid-name-with-many-subdomains.valid.subdomain.stark', + expected: true, + }, + { + starkName: 'too-long-stark-domain-name-more-than-48-characters.stark', + expected: false, + }, + { starkName: 'invalid..stark', expected: false }, + { starkName: 'invalid@stark', expected: false }, + { starkName: 'invalid_name.stark', expected: false }, + { starkName: 'invalid space.stark', expected: false }, + { starkName: 'invalid.starknet', expected: false }, + { starkName: '.invalid.stark', expected: false }, + { starkName: 'invalid.', expected: false }, + { starkName: 'invalid.stark.', expected: false }, + { starkName: '', expected: false }, + ])( + 'validates `$starkName` correctly and returns $expected', + ({ starkName, expected }) => { + const result = starknetUtils.isValidStarkName(starkName); + expect(result).toBe(expected); + }, + ); +}); diff --git a/packages/wallet-ui/src/components/ui/molecule/AddressInput/AddressInput.view.tsx b/packages/wallet-ui/src/components/ui/molecule/AddressInput/AddressInput.view.tsx index 220e7a59..8023a34a 100644 --- a/packages/wallet-ui/src/components/ui/molecule/AddressInput/AddressInput.view.tsx +++ b/packages/wallet-ui/src/components/ui/molecule/AddressInput/AddressInput.view.tsx @@ -1,4 +1,4 @@ -import { KeyboardEvent, ChangeEvent } from 'react'; +import { KeyboardEvent, ChangeEvent, useEffect } from 'react'; import { InputHTMLAttributes, useRef, @@ -24,13 +24,11 @@ import { Wrapper, } from './AddressInput.style'; import { STARKNET_ADDRESS_LENGTH } from 'utils/constants'; -import { useStarkNetSnap } from 'services'; -import { useAppSelector } from 'hooks/redux'; interface Props extends InputHTMLAttributes { label?: string; setIsValidAddress?: Dispatch>; - onResolvedAddress?: (address: string) => void; + resolvedAddress?: string; } export const AddressInputView = ({ @@ -38,18 +36,30 @@ export const AddressInputView = ({ onChange, label, setIsValidAddress, - onResolvedAddress, + resolvedAddress, ...otherProps }: Props) => { - const networks = useAppSelector((state) => state.networks); - const chainId = networks?.items[networks.activeNetwork]?.chainId; - const { getAddrFromStarkName } = useStarkNetSnap(); const [focused, setFocused] = useState(false); const inputRef = useRef(null); const [error, setError] = useState(''); const [valid, setValid] = useState(false); const [info, setInfo] = useState(''); + useEffect(() => { + if (!inputRef.current || !resolvedAddress) { + return; + } + + const { valid, error, info } = validateAddress( + inputRef.current.value, + resolvedAddress, + ); + + setValid(valid); + setError(error); + setInfo(info); + }, [resolvedAddress]); + const displayIcon = () => { return valid || error !== ''; }; @@ -68,39 +78,37 @@ export const AddressInputView = ({ //Check if valid address onChange && onChange(event); - if (!inputRef.current) { - return; - } - const isValid = - inputRef.current.value !== '' && isValidAddress(inputRef.current.value); - if (isValid) { - setValid(true); - setError(''); - onResolvedAddress?.(inputRef.current.value); - } else if (isValidStarkName(inputRef.current.value)) { - setValid(false); - setError(''); + if (!inputRef.current) return; - getAddrFromStarkName(inputRef.current.value, chainId).then((address) => { - if (isValidAddress(address)) { - setValid(true); - setError(''); - setInfo(shortenAddress(address as string, 12) as string); - onResolvedAddress?.(address as string); - } else { - setValid(false); - setError('.stark name not found'); - setInfo(''); - } - }); - } else { - setValid(false); - setError('Invalid address format'); - setInfo(''); - } + const { valid, error, info } = validateAddress( + inputRef.current.value, + resolvedAddress, + ); + + setValid(valid); + setError(error); + setInfo(info); if (setIsValidAddress) { - setIsValidAddress(isValid); + setIsValidAddress(valid); + } + }; + + const validateAddress = (value: string, addr: string | undefined) => { + if (value !== '' && isValidAddress(value)) { + return { valid: true, error: '', info: '' }; + } else if (isValidStarkName(value)) { + if (addr && isValidAddress(addr)) { + return { + valid: true, + error: '', + info: shortenAddress(addr, 12) as string, + }; + } else { + return { valid: false, error: '.stark name not found', info: '' }; + } + } else { + return { valid: false, error: 'Invalid address format', info: '' }; } }; diff --git a/packages/wallet-ui/src/components/ui/organism/Header/SendModal/SendModal.view.tsx b/packages/wallet-ui/src/components/ui/organism/Header/SendModal/SendModal.view.tsx index 8b839e6b..bd262c1b 100644 --- a/packages/wallet-ui/src/components/ui/organism/Header/SendModal/SendModal.view.tsx +++ b/packages/wallet-ui/src/components/ui/organism/Header/SendModal/SendModal.view.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useRef, useState } from 'react'; import { AmountInput } from 'components/ui/molecule/AmountInput'; import { SendSummaryModal } from '../SendSummaryModal'; import { @@ -43,6 +43,7 @@ export const SendModalView = ({ closeModal }: Props) => { }); const [errors, setErrors] = useState({ amount: '', address: '' }); const [resolvedAddress, setResolvedAddress] = useState(''); + const controllerRef = useRef(null); const handleChange = (fieldName: string, fieldValue: string) => { //Check if input amount does not exceed user balance @@ -69,19 +70,46 @@ export const SendModalView = ({ closeModal }: Props) => { case 'address': if (fieldValue !== '') { if (isValidAddress(fieldValue)) { + setResolvedAddress(fieldValue); break; } else if (isValidStarkName(fieldValue)) { - getAddrFromStarkName(fieldValue, chainId).then((address) => { - if (isValidAddress(address)) { - setResolvedAddress(address); - } else { - setErrors((prevErrors) => ({ - ...prevErrors, - address: '.stark name doesn’t exist', - })); - } - }); + // Cancel previous request + controllerRef.current?.abort(); + + // Create a new AbortController for this request + const abortController = new AbortController(); + controllerRef.current = abortController; + + getAddrFromStarkNameQuery(fieldValue, { + signal: abortController.signal, + }) + .then((address) => { + if (abortController.signal.aborted) return; + if (isValidAddress(address)) { + setResolvedAddress(address); + } else { + setResolvedAddress(''); + setErrors((prevErrors) => ({ + ...prevErrors, + address: '.stark name doesn’t exist', + })); + } + }) + .catch((error) => { + if (error.name !== 'AbortError') { + setResolvedAddress(''); + setErrors((prevErrors) => ({ + ...prevErrors, + address: '.stark name doesn’t exist', + })); + } + }) + .finally(() => { + if (controllerRef.current === abortController) + controllerRef.current = null; + }); } else { + setResolvedAddress(''); setErrors((prevErrors) => ({ ...prevErrors, address: 'Invalid address format', @@ -111,6 +139,33 @@ export const SendModalView = ({ closeModal }: Props) => { ); }; + const getAddrFromStarkNameQuery = async ( + starkName: string, + options?: { signal?: AbortSignal }, + ): Promise => { + const { signal } = options || {}; + + return new Promise((resolve, reject) => { + // Check if the request is already aborted + if (signal?.aborted) { + return reject(new DOMException('Request aborted', 'AbortError')); + } + + const abortHandler = () => { + reject(new DOMException('Request aborted', 'AbortError')); + }; + + signal?.addEventListener('abort', abortHandler); + + getAddrFromStarkName(starkName, chainId) + .then(resolve) + .catch(reject) + .finally(() => { + signal?.removeEventListener('abort', abortHandler); + }); + }); + }; + return ( <> {!summaryModalOpen && ( @@ -127,7 +182,7 @@ export const SendModalView = ({ closeModal }: Props) => { label="To" placeholder="Paste recipient address or .stark name here" onChange={(value) => handleChange('address', value.target.value)} - onResolvedAddress={(address) => setResolvedAddress(address)} + resolvedAddress={resolvedAddress} />