diff --git a/package.json b/package.json index 2a84d3419..70dc14377 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@ledgerhq/react-native-hw-transport-ble": "6.27.2", "@metaplex-foundation/js": "0.17.6", "@metaplex-foundation/mpl-bubblegum": "0.6.0", + "@onsol/tldparser": "^0.5.3", "@pythnetwork/client": "^2.17.0", "@react-native-async-storage/async-storage": "1.18.1", "@react-native-community/blur": "4.3.0", @@ -291,29 +292,29 @@ "tls": false }, "browser": { - "zlib": "browserify-zlib", + "_stream_duplex": "readable-stream/duplex", + "_stream_passthrough": "readable-stream/passthrough", + "_stream_readable": "readable-stream/readable", + "_stream_transform": "readable-stream/transform", + "_stream_writable": "readable-stream/writable", "console": "console-browserify", "constants": "constants-browserify", "crypto": "react-native-crypto", + "dgram": "react-native-udp", "dns": "dns.js", - "net": "react-native-tcp", "domain": "domain-browser", + "fs": "react-native-level-fs", "http": "@tradle/react-native-http", "https": "https-browserify", + "net": "react-native-tcp", "os": "react-native-os", "path": "path-browserify", "querystring": "querystring-es3", - "fs": "react-native-level-fs", - "_stream_transform": "readable-stream/transform", - "_stream_readable": "readable-stream/readable", - "_stream_writable": "readable-stream/writable", - "_stream_duplex": "readable-stream/duplex", - "_stream_passthrough": "readable-stream/passthrough", - "dgram": "react-native-udp", "stream": "stream-browserify", "timers": "timers-browserify", + "tls": false, "tty": "tty-browserify", "vm": "vm-browserify", - "tls": false + "zlib": "browserify-zlib" } } diff --git a/src/features/addressBook/ContactDetails.tsx b/src/features/addressBook/ContactDetails.tsx index 8471fe956..f84ef9026 100644 --- a/src/features/addressBook/ContactDetails.tsx +++ b/src/features/addressBook/ContactDetails.tsx @@ -28,6 +28,8 @@ import useAlert from '@hooks/useAlert' import CloseButton from '@components/CloseButton' import { solAddressIsValid, accountNetType } from '@utils/accountUtils' import { heliumAddressFromSolAddress } from '@helium/spl-utils' +import { useDebounce } from 'use-debounce' +import { fetchDomainOwner } from '@utils/getDomainOwner' import { HomeNavigationProp } from '../home/homeTypes' import { useAccountStorage } from '../../storage/AccountStorageProvider' import { @@ -37,6 +39,7 @@ import { import { useAppStorage } from '../../storage/AppStorageProvider' import AddressExtra from './AddressExtra' import { CSAccount } from '../../storage/cloudStorage' +import { useSolana } from '../../solana/SolanaProvider' const BUTTON_HEIGHT = 55 @@ -62,6 +65,9 @@ const ContactDetails = ({ action, contact }: Props) => { const { scannedAddress, setScannedAddress } = useAppStorage() const spacing = useSpacing() const { showOKCancelAlert } = useAlert() + const { connection } = useSolana() + // debounce is needed to avoid unneccessary rpc calls + const [debouncedAddress] = useDebounce(address, 800) useEffect(() => { if (route.params?.address) { @@ -71,6 +77,30 @@ const ContactDetails = ({ action, contact }: Props) => { } }, [contact, route]) + const handleDomainAddress = useCallback( + async ({ domain }: { domain: string }) => { + if (!connection) return + return fetchDomainOwner(connection, domain) + }, + [connection], + ) + + useEffect(() => { + // only parse addresses which include dots. + if (debouncedAddress.split('.').length === 2) { + handleDomainAddress({ domain: debouncedAddress }).then( + (resolvedAddress) => { + // owner was not found so we do not set the owner address + if (!resolvedAddress) return + setAddress(resolvedAddress) + // if nickname was previously set we ignore setting the domain as nickname + if (nickname) return + setNickname(debouncedAddress) + }, + ) + } + }, [debouncedAddress, handleDomainAddress, nickname]) + const onRequestClose = useCallback(() => { homeNav.goBack() }, [homeNav]) diff --git a/src/features/payment/PaymentItem.tsx b/src/features/payment/PaymentItem.tsx index 4465956f3..575631603 100644 --- a/src/features/payment/PaymentItem.tsx +++ b/src/features/payment/PaymentItem.tsx @@ -23,6 +23,7 @@ import { NativeSyntheticEvent, TextInputEndEditingEventData, } from 'react-native' +import { useDebouncedCallback } from 'use-debounce' import { CSAccount } from '../../storage/cloudStorage' import { useBalance } from '../../utils/Balance' import { accountNetType, ellipsizeAddress } from '../../utils/accountUtils' @@ -120,14 +121,12 @@ const PaymentItem = ({ onToggleMax({ address, index }) }, [address, index, onToggleMax]) - const handleEditAddress = useCallback( - (text?: string) => { - onEditAddress({ address: text || '', index }) - }, - [index, onEditAddress], - ) + // debounce is needed to avoid unneccessary rpc calls + const handleEditAddress = useDebouncedCallback((text?: string) => { + onEditAddress({ address: text || '', index }) + }, 800) - const handleAddressBlur = useCallback( + const handleAddressBlur = useDebouncedCallback( (event?: NativeSyntheticEvent) => { const text = event?.nativeEvent.text handleAddressError({ @@ -136,7 +135,7 @@ const PaymentItem = ({ isHotspotOrValidator: false, }) }, - [handleAddressError, index], + 800, ) const handleRemove = useCallback(() => { diff --git a/src/features/payment/PaymentScreen.tsx b/src/features/payment/PaymentScreen.tsx index 9e1f6b677..63e35392d 100644 --- a/src/features/payment/PaymentScreen.tsx +++ b/src/features/payment/PaymentScreen.tsx @@ -49,6 +49,7 @@ import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view import { useSafeAreaInsets } from 'react-native-safe-area-context' import Toast from 'react-native-simple-toast' import { useSelector } from 'react-redux' +import { fetchDomainOwner } from '@utils/getDomainOwner' import useSubmitTxn from '../../hooks/useSubmitTxn' import { RootNavigationProp } from '../../navigation/rootTypes' import { useSolana } from '../../solana/SolanaProvider' @@ -131,7 +132,7 @@ const PaymentScreen = () => { return new BN(balanceBigint.toString()) } }, [balanceBigint]) - const { anchorProvider } = useSolana() + const { anchorProvider, connection } = useSolana() const appDispatch = useAppDispatch() const navigation = useNavigation() @@ -489,6 +490,14 @@ const PaymentScreen = () => { [dispatch], ) + const handleDomainAddress = useCallback( + async ({ domain }: { domain: string }) => { + if (!connection) return + return fetchDomainOwner(connection, domain) + }, + [connection], + ) + const handleAddressError = useCallback( ({ index, @@ -505,6 +514,12 @@ const PaymentScreen = () => { } let invalidAddress = false + // only handle address which include dots. + if (address.split('.').length === 2) { + // retrieve the address which has been set previously by handleEditAddress. + /* eslint-disable-next-line no-param-reassign */ + address = paymentState.payments[index].address || '' + } invalidAddress = !!address && !solAddressIsValid(address) const wrongNetType = @@ -513,20 +528,28 @@ const PaymentScreen = () => { accountNetType(address) !== networkType handleSetPaymentError(index, invalidAddress || wrongNetType) }, - [handleSetPaymentError, networkType], + [handleSetPaymentError, networkType, paymentState], ) const handleEditAddress = useCallback( async ({ index, address }: { index: number; address: string }) => { if (index === undefined || !currentAccount || !anchorProvider) return - + let domain = '' + if (address.split('.').length === 2) { + const resolvedAddress = + (await handleDomainAddress({ domain: address })) || '' + /* eslint-disable-next-line no-param-reassign */ + address = resolvedAddress + // if the address is resolved then the domain could also be an alias/nickname of the address. + if (resolvedAddress) domain = address + } const allAccounts = unionBy( contacts, Object.values(accounts || {}), ({ address: addr }) => addr, ) let contact = allAccounts.find((c) => c.address === address) - if (!contact) contact = { address, netType: networkType, alias: '' } + if (!contact) contact = { address, netType: networkType, alias: domain } const createTokenAccountFee = await calcCreateAssociatedTokenAccountAccountFee( @@ -551,6 +574,7 @@ const PaymentScreen = () => { networkType, anchorProvider, mint, + handleDomainAddress, ], ) diff --git a/src/utils/getDomainOwner.ts b/src/utils/getDomainOwner.ts new file mode 100644 index 000000000..305584ba9 --- /dev/null +++ b/src/utils/getDomainOwner.ts @@ -0,0 +1,16 @@ +import { TldParser } from '@onsol/tldparser' +import { Connection } from '@solana/web3.js' + +// retrives AllDomain domain owner. +// the domain must include the dot +export async function fetchDomainOwner(connection: Connection, domain: string) { + try { + const parser = new TldParser(connection) + const owner = await parser.getOwnerFromDomainTld(domain) + if (!owner) return + return owner.toBase58() + } catch (e) { + // Handle the error here if needed + // console.error("Error fetching domain owner:", e); + } +} diff --git a/yarn.lock b/yarn.lock index 2f750a3af..d4885e5fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3320,6 +3320,14 @@ mkdirp "^1.0.4" rimraf "^3.0.2" +"@onsol/tldparser@^0.5.3": + version "0.5.3" + resolved "https://registry.yarnpkg.com/@onsol/tldparser/-/tldparser-0.5.3.tgz#f5a0a06fa69af0e8a2783464bbd32b3e88ad0b1a" + integrity sha512-rICUDhYPwDuO81wo4HI7QSCf6kQiaM0mSv3HKBJPrRxliIvgwanAoU5H0p54HEdAKeS3pmeLi5wB6ROpGxTZ/A== + dependencies: + "@ethersproject/sha2" "^5.7.0" + "@metaplex-foundation/beet-solana" "^0.4.0" + "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"