From d05b41ca178411691583a0c73becc72a57098969 Mon Sep 17 00:00:00 2001 From: Bruno Barbieri <1247834+brunobar79@users.noreply.github.com> Date: Wed, 5 Jun 2024 15:37:21 -0400 Subject: [PATCH 1/6] more Safemath fixes (#5818) * more fixes * revert the revert * number dot case --- src/__swaps__/safe-math/SafeMath.ts | 34 ++++++++++++++++--- .../safe-math/__tests__/SafeMath.test.ts | 5 ++- .../Swap/hooks/useSwapInputsController.ts | 1 - 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/__swaps__/safe-math/SafeMath.ts b/src/__swaps__/safe-math/SafeMath.ts index be161c34fcf..af5062e562b 100644 --- a/src/__swaps__/safe-math/SafeMath.ts +++ b/src/__swaps__/safe-math/SafeMath.ts @@ -1,15 +1,35 @@ // Utility function to remove the decimal point and keep track of the number of decimal places const removeDecimalWorklet = (num: string): [bigint, number] => { 'worklet'; - const parts = num.split('.'); - const decimalPlaces = parts.length === 2 ? parts[1].length : 0; - const bigIntNum = BigInt(parts.join('')); + let decimalPlaces = 0; + let bigIntNum: bigint; + + if (/[eE]/.test(num)) { + const [base, exponent] = num.split(/[eE]/); + const exp = Number(exponent); + const parts = base.split('.'); + const baseDecimalPlaces = parts.length === 2 ? parts[1].length : 0; + const bigIntBase = BigInt(parts.join('')); + + if (exp >= 0) { + decimalPlaces = baseDecimalPlaces - exp; + bigIntNum = bigIntBase * BigInt(10) ** BigInt(exp); + } else { + decimalPlaces = baseDecimalPlaces - exp; + bigIntNum = bigIntBase; + } + } else { + const parts = num.split('.'); + decimalPlaces = parts.length === 2 ? parts[1].length : 0; + bigIntNum = BigInt(parts.join('')); + } + return [bigIntNum, decimalPlaces]; }; const isNumberStringWorklet = (value: string): boolean => { 'worklet'; - return /^-?\d+(\.\d+)?$/.test(value); + return /^-?\d+(\.\d+)?([eE][-+]?\d+)?$/.test(value); }; const isZeroWorklet = (value: string): boolean => { @@ -43,7 +63,11 @@ const formatResultWorklet = (result: bigint): string => { // Helper function to handle string and number input types const toStringWorklet = (value: string | number): string => { 'worklet'; - return typeof value === 'number' ? value.toString() : value; + const ret = typeof value === 'number' ? value.toString() : value; + if (/^\d+\.$/.test(ret)) { + return ret.slice(0, -1); + } + return ret; }; // Converts a numeric string to a scaled integer string, preserving the specified decimal places diff --git a/src/__swaps__/safe-math/__tests__/SafeMath.test.ts b/src/__swaps__/safe-math/__tests__/SafeMath.test.ts index 60933f3fb98..a374b7d6b51 100644 --- a/src/__swaps__/safe-math/__tests__/SafeMath.test.ts +++ b/src/__swaps__/safe-math/__tests__/SafeMath.test.ts @@ -32,6 +32,7 @@ const RESULTS = { floor: '1243425', toScaledInteger: '57464009350560633', negativePow: '0.001', + negativeExp: '6.0895415516156', }; const VALUE_A = '1243425.345'; @@ -39,7 +40,8 @@ const VALUE_B = '3819.24'; const VALUE_C = '2'; const VALUE_D = '1243425.745'; const VALUE_E = '0.057464009350560633'; -const VALUE_F = '0.001'; +const VALUE_F = '147887324'; +const VALUE_G = '4.11769e-8'; const NEGATIVE_VALUE = '-2412.12'; const ZERO = '0'; const ONE = '1'; @@ -80,6 +82,7 @@ describe('SafeMath', () => { expect(mulWorklet(VALUE_A, VALUE_B)).toBe(RESULTS.mul); expect(mulWorklet(Number(VALUE_A), VALUE_B)).toBe(RESULTS.mul); expect(mulWorklet(VALUE_A, Number(VALUE_B))).toBe(RESULTS.mul); + expect(mulWorklet(VALUE_F, VALUE_G)).toBe(RESULTS.negativeExp); }); test('divWorklet', () => { diff --git a/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts b/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts index f1aa2851d3d..3bd64647059 100644 --- a/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts +++ b/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts @@ -166,7 +166,6 @@ export function useSwapInputsController({ if (inputMethod.value === 'outputAmount' || typeof inputValues.value.outputAmount === 'string') { return addCommasToNumber(inputValues.value.outputAmount, '0'); } - return valueBasedDecimalFormatter({ amount: inputValues.value.outputAmount, usdTokenPrice: outputNativePrice.value, From 43690779e79a9199f559cf218fed28c8250fdfff Mon Sep 17 00:00:00 2001 From: Ben Goldberg Date: Wed, 5 Jun 2024 13:10:30 -0700 Subject: [PATCH 2/6] fix fasterimage border radius android (#5816) --- .../screens/Swap/components/AnimatedSwapCoinIcon.tsx | 10 ++++++---- src/components/images/ImgixImage.tsx | 8 ++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/__swaps__/screens/Swap/components/AnimatedSwapCoinIcon.tsx b/src/__swaps__/screens/Swap/components/AnimatedSwapCoinIcon.tsx index 3b1368924df..8ab9b815f66 100644 --- a/src/__swaps__/screens/Swap/components/AnimatedSwapCoinIcon.tsx +++ b/src/__swaps__/screens/Swap/components/AnimatedSwapCoinIcon.tsx @@ -1,6 +1,6 @@ /* eslint-disable no-nested-ternary */ import React from 'react'; -import { StyleSheet, View, ViewStyle } from 'react-native'; +import { PixelRatio, StyleSheet, View, ViewStyle } from 'react-native'; import { borders } from '@/styles'; import { useTheme } from '@/theme'; import Animated, { SharedValue, useAnimatedProps, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; @@ -11,7 +11,9 @@ import { AnimatedChainImage } from './AnimatedChainImage'; import { fadeConfig } from '../constants'; import { SwapCoinIconTextFallback } from './SwapCoinIconTextFallback'; import { Box } from '@/design-system'; -import { IS_ANDROID } from '@/env'; +import { IS_ANDROID, IS_IOS } from '@/env'; + +const PIXEL_RATIO = PixelRatio.get(); const fallbackIconStyle = { ...borders.buildCircleAsObject(32), @@ -49,7 +51,7 @@ export const AnimatedSwapCoinIcon = React.memo(function FeedCoinIcon({ return { source: { ...DEFAULT_FASTER_IMAGE_CONFIG, - borderRadius: IS_ANDROID ? size / 2 : undefined, + borderRadius: IS_ANDROID ? (size / 2) * PIXEL_RATIO : undefined, transitionDuration: 0, url: asset.value?.icon_url ?? '', }, @@ -118,7 +120,7 @@ export const AnimatedSwapCoinIcon = React.memo(function FeedCoinIcon({ style={[ sx.coinIcon, { - borderRadius: size / 2, + borderRadius: IS_IOS ? size / 2 : undefined, height: size, width: size, }, diff --git a/src/components/images/ImgixImage.tsx b/src/components/images/ImgixImage.tsx index 46ead019030..700105a0e50 100644 --- a/src/components/images/ImgixImage.tsx +++ b/src/components/images/ImgixImage.tsx @@ -1,8 +1,9 @@ import { FasterImageView, ImageOptions } from '@candlefinance/faster-image'; import * as React from 'react'; -import { StyleSheet } from 'react-native'; +import { PixelRatio, StyleSheet } from 'react-native'; import FastImage, { FastImageProps, Source } from 'react-native-fast-image'; import { maybeSignSource } from '../../handlers/imgix'; +import { IS_IOS } from '@/env'; export type ImgixImageProps = FastImageProps & { readonly Component?: React.ElementType; @@ -29,6 +30,8 @@ type HiddenImgixImageProps = { }; type MergedImgixImageProps = ImgixImageProps & HiddenImgixImageProps; +const PIXEL_RATIO = PixelRatio.get(); + // ImgixImage must be a class Component to support Animated.createAnimatedComponent. class ImgixImage extends React.PureComponent { static getDerivedStateFromProps(props: MergedImgixImageProps) { @@ -49,7 +52,8 @@ class ImgixImage extends React.PureComponent Date: Thu, 6 Jun 2024 09:35:43 -0400 Subject: [PATCH 3/6] add type to SearchAsset type and add into type possibilities (#5820) * add type to SearchAsset type and add into type possibilities * fix lint --- src/__swaps__/types/search.ts | 3 ++- src/__swaps__/utils/assets.ts | 2 +- src/components/sheet/sheet-action-buttons/SwapActionButton.tsx | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/__swaps__/types/search.ts b/src/__swaps__/types/search.ts index c370a4a5242..335b8bb847b 100644 --- a/src/__swaps__/types/search.ts +++ b/src/__swaps__/types/search.ts @@ -1,6 +1,6 @@ import { Address } from 'viem'; -import { AddressOrEth, ParsedAsset, UniqueId } from '@/__swaps__/types/assets'; +import { AddressOrEth, AssetType, ParsedAsset, UniqueId } from '@/__swaps__/types/assets'; import { ChainId } from '@/__swaps__/types/chains'; export type TokenSearchAssetKey = keyof ParsedAsset; @@ -29,5 +29,6 @@ export type SearchAsset = { }; rainbowMetadataId?: number; symbol: string; + type?: AssetType; uniqueId: UniqueId; }; diff --git a/src/__swaps__/utils/assets.ts b/src/__swaps__/utils/assets.ts index 49089c544b5..ea3b6a767e8 100644 --- a/src/__swaps__/utils/assets.ts +++ b/src/__swaps__/utils/assets.ts @@ -302,7 +302,7 @@ export const parseSearchAsset = ({ balance: userAsset?.balance || { amount: '0', display: '0.00' }, icon_url: userAsset?.icon_url || assetWithPrice?.icon_url || searchAsset?.icon_url, colors: userAsset?.colors || assetWithPrice?.colors || searchAsset?.colors, - type: userAsset?.type || assetWithPrice?.type, + type: userAsset?.type || assetWithPrice?.type || searchAsset?.type, }); export function filterAsset(asset: ZerionAsset) { diff --git a/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx b/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx index c6ae33c2f1c..8daef82baa5 100644 --- a/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx +++ b/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx @@ -60,6 +60,7 @@ function SwapActionButton({ asset, color: givenColor, inputType, label, fromDisc isVerified: asset.isVerified ?? false, mainnetAddress: (asset.mainnet_address ?? '') as AddressOrEth, networks: asset.networks ?? [], + type: asset.type as AssetType, }, userAsset, }); From 984fabed77a8fe20e25edef7d0de7b119ea72b18 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Thu, 6 Jun 2024 11:17:38 -0400 Subject: [PATCH 4/6] Disable flashbots toggle on appropriate chains (#5812) * disable flashbots on all chains other than mainnet * . * add networkobj check * whoops * fix lint --- .../Swap/components/AnimatedSwitch.tsx | 6 ++++- .../screens/Swap/components/ReviewPanel.tsx | 26 ++++++++++++++----- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/__swaps__/screens/Swap/components/AnimatedSwitch.tsx b/src/__swaps__/screens/Swap/components/AnimatedSwitch.tsx index 7a344be6a26..9f2032ca3a6 100644 --- a/src/__swaps__/screens/Swap/components/AnimatedSwitch.tsx +++ b/src/__swaps__/screens/Swap/components/AnimatedSwitch.tsx @@ -12,9 +12,10 @@ type AnimatedSwitchProps = { value: SharedValue; activeLabel?: string; inactiveLabel?: string; + disabled?: boolean; } & Omit; -export function AnimatedSwitch({ value, onToggle, activeLabel, inactiveLabel, ...props }: AnimatedSwitchProps) { +export function AnimatedSwitch({ value, onToggle, activeLabel, inactiveLabel, disabled = false, ...props }: AnimatedSwitchProps) { const { isDarkMode } = useColorMode(); const inactiveBg = useForegroundColor('fillSecondary'); @@ -27,6 +28,7 @@ export function AnimatedSwitch({ value, onToggle, activeLabel, inactiveLabel, .. ? withTiming(opacityWorklet(inactiveBg, 0.12), TIMING_CONFIGS.fadeConfig) : withTiming(opacityWorklet(activeBg, 0.64), TIMING_CONFIGS.fadeConfig), borderColor: opacityWorklet(border, 0.06), + opacity: disabled ? 0.4 : 1, }; }); @@ -41,6 +43,8 @@ export function AnimatedSwitch({ value, onToggle, activeLabel, inactiveLabel, .. }); const labelItem = useDerivedValue(() => { + if (disabled) return; + if (!activeLabel && !inactiveLabel) { return; } diff --git a/src/__swaps__/screens/Swap/components/ReviewPanel.tsx b/src/__swaps__/screens/Swap/components/ReviewPanel.tsx index 4ebca35d4a7..8343d2f4170 100644 --- a/src/__swaps__/screens/Swap/components/ReviewPanel.tsx +++ b/src/__swaps__/screens/Swap/components/ReviewPanel.tsx @@ -34,6 +34,7 @@ import { useNavigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; import { ethereumUtils } from '@/utils'; import { getNativeAssetForNetwork } from '@/utils/ethereumUtils'; +import { getNetworkObj } from '@/networks'; import { chainNameFromChainId } from '@/__swaps__/utils/chains'; const UNKNOWN_LABEL = i18n.t(i18n.l.swap.unknown); @@ -119,6 +120,24 @@ function EstimatedArrivalTime() { ); } +function FlashbotsToggle() { + const { SwapSettings } = useSwapContext(); + + const inputAssetChainId = swapsStore(state => state.inputAsset?.chainId) ?? ChainId.mainnet; + const isFlashbotsEnabledForNetwork = getNetworkObj(ethereumUtils.getNetworkFromChainId(inputAssetChainId)).features.flashbots; + const flashbotsToggleValue = useDerivedValue(() => isFlashbotsEnabledForNetwork && SwapSettings.flashbots.value); + + return ( + + ); +} + export function ReviewPanel() { const { navigate } = useNavigation(); const { isDarkMode } = useColorMode(); @@ -266,12 +285,7 @@ export function ReviewPanel() { - + From 60bffb16bd89cb3aa612fe4e92546fdb78f32466 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Thu, 6 Jun 2024 17:36:49 -0400 Subject: [PATCH 5/6] fix wrong chainname (#5823) --- src/__swaps__/screens/Swap/components/ReviewPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__swaps__/screens/Swap/components/ReviewPanel.tsx b/src/__swaps__/screens/Swap/components/ReviewPanel.tsx index 8343d2f4170..1b804f6c208 100644 --- a/src/__swaps__/screens/Swap/components/ReviewPanel.tsx +++ b/src/__swaps__/screens/Swap/components/ReviewPanel.tsx @@ -145,7 +145,7 @@ export function ReviewPanel() { const unknown = i18n.t(i18n.l.swap.unknown); - const chainName = useDerivedValue(() => ChainNameDisplay[internalSelectedOutputAsset.value?.chainId ?? ChainId.mainnet]); + const chainName = useDerivedValue(() => ChainNameDisplay[internalSelectedInputAsset.value?.chainId ?? ChainId.mainnet]); const minimumReceived = useDerivedValue(() => { if (!SwapInputController.formattedOutputAmount.value || !internalSelectedOutputAsset.value?.symbol) { From 66422314d44cfe92c1fe9a5d67c90e6f10fec3bd Mon Sep 17 00:00:00 2001 From: Christian Baroni <7061887+christianbaroni@users.noreply.github.com> Date: Fri, 7 Jun 2024 11:32:54 -0400 Subject: [PATCH 6/6] Performance: limit Sentry tracking, fix NFT hooks (#5819) --- index.js | 3 + src/App.js | 15 +- src/components/DappBrowser/search/Search.tsx | 5 +- .../search/results/SearchResults.tsx | 218 ++++++++++-------- src/components/asset-list/AssetList.js | 1 - .../UniqueTokenExpandedState.tsx | 4 +- src/components/toasts/Toast.tsx | 6 +- src/handlers/ens.ts | 18 -- src/hooks/index.ts | 2 - src/hooks/useAccountEmptyState.ts | 25 -- src/hooks/useCollectible.ts | 23 +- src/hooks/useTrackENSProfile.ts | 63 ----- src/logger/sentry.ts | 28 +-- src/resources/nfts/index.ts | 105 +++++---- src/screens/WalletScreen/index.tsx | 42 +--- .../points/components/LeaderboardRow.tsx | 8 +- src/utils/logger.ts | 3 +- 17 files changed, 227 insertions(+), 342 deletions(-) delete mode 100644 src/hooks/useAccountEmptyState.ts delete mode 100644 src/hooks/useTrackENSProfile.ts diff --git a/index.js b/index.js index 5720753a824..74db2fc363f 100644 --- a/index.js +++ b/index.js @@ -5,11 +5,14 @@ It needs to be an import statement because otherwise it doesn't load properly likely because of typescript. */ import '@walletconnect/react-native-compat'; +import { initSentry } from '@/logger/sentry'; import { analytics } from './src/analytics'; import { StartTime } from './src/performance/start-time'; import { PerformanceTracking } from './src/performance/tracking'; import { PerformanceMetrics } from './src/performance/tracking/types/PerformanceMetrics'; +initSentry(); + analytics.track('Started executing JavaScript bundle'); PerformanceTracking.logDirectly(PerformanceMetrics.loadJSBundle, Date.now() - StartTime.START_TIME); PerformanceTracking.startMeasuring(PerformanceMetrics.loadRootAppComponent); diff --git a/src/App.js b/src/App.js index 6a8d8db8850..3825e243a8a 100644 --- a/src/App.js +++ b/src/App.js @@ -42,7 +42,6 @@ import { InitialRouteContext } from '@/navigation/initialRoute'; import Routes from '@/navigation/routesNames'; import { Portal } from '@/react-native-cool-modals/Portal'; import { NotificationsHandler } from '@/notifications/NotificationsHandler'; -import { initSentry, sentryRoutingInstrumentation } from '@/logger/sentry'; import { analyticsV2 } from '@/analytics'; import { getOrCreateDeviceId, securelyHashWalletAddress } from '@/analytics/utils'; import { logger, RainbowError } from '@/logger'; @@ -57,9 +56,10 @@ import { handleReviewPromptAction } from '@/utils/reviewAlert'; import { RemotePromoSheetProvider } from '@/components/remote-promo-sheet/RemotePromoSheetProvider'; import { RemoteCardProvider } from '@/components/cards/remote-cards'; import { initializeRemoteConfig } from '@/model/remoteConfig'; +import { IS_DEV } from './env'; import { checkIdentifierOnLaunch } from './model/backup'; -if (__DEV__) { +if (IS_DEV) { reactNativeDisableYellowBox && LogBox.ignoreAllLogs(); (showNetworkRequests || showNetworkResponses) && monitorNetwork(showNetworkRequests, showNetworkResponses); } @@ -218,10 +218,6 @@ class OldApp extends Component { updateBalancesAfter(isL2 ? 10000 : 5000, isL2, network); }; - handleSentryNavigationIntegration = () => { - sentryRoutingInstrumentation?.registerNavigationContainer(this.navigatorRef); - }; - render() { return ( @@ -230,7 +226,7 @@ class OldApp extends Component { - + @@ -257,9 +253,7 @@ function Root() { React.useEffect(() => { async function initializeApplication() { - await initSentry(); // must be set up immediately await initializeRemoteConfig(); - // must happen immediately, but after Sentry await migrate(); const isReturningUser = ls.device.get(['isReturningUser']); @@ -339,7 +333,7 @@ function Root() { // init complete, load the rest of the app setInitializing(false); }) - .catch(e => { + .catch(() => { logger.error(new RainbowError(`initializeApplication failed`)); // for failure, continue to rest of the app for now @@ -369,6 +363,7 @@ function Root() { ); } +/** Wrapping Root allows Sentry to accurately track startup times */ const RootWithSentry = Sentry.wrap(Root); const PlaygroundWithReduxStore = () => ( diff --git a/src/components/DappBrowser/search/Search.tsx b/src/components/DappBrowser/search/Search.tsx index 59a433604ab..df60db17000 100644 --- a/src/components/DappBrowser/search/Search.tsx +++ b/src/components/DappBrowser/search/Search.tsx @@ -17,7 +17,6 @@ import { useBrowserWorkletsContext } from '../BrowserWorkletsContext'; import { SearchResults } from './results/SearchResults'; import { useSearchContext } from './SearchContext'; import { useSyncSharedValue } from '@/hooks/reanimated/useSyncSharedValue'; -import { DappsContextProvider } from '@/resources/metadata/dapps'; import { useSharedValueState } from '@/hooks/reanimated/useSharedValueState'; export const Search = () => { @@ -125,9 +124,7 @@ export const Search = () => { <> - - - + diff --git a/src/components/DappBrowser/search/results/SearchResults.tsx b/src/components/DappBrowser/search/results/SearchResults.tsx index c4df358842a..e7bf159980f 100644 --- a/src/components/DappBrowser/search/results/SearchResults.tsx +++ b/src/components/DappBrowser/search/results/SearchResults.tsx @@ -3,7 +3,7 @@ import { ScrollView, StyleSheet } from 'react-native'; import Animated, { SharedValue, useAnimatedReaction, useAnimatedStyle, withSpring } from 'react-native-reanimated'; import { ButtonPressAnimation } from '@/components/animations'; import { Bleed, Box, Inline, Inset, Stack, Text, TextIcon, globalColors, useColorMode, useForegroundColor } from '@/design-system'; -import { Dapp, useDappsContext } from '@/resources/metadata/dapps'; +import { Dapp, DappsContextProvider, useDappsContext } from '@/resources/metadata/dapps'; import { useBrowserContext } from '../../BrowserContext'; import { SEARCH_BAR_HEIGHT } from '../../search-input/SearchInput'; import { useSearchContext } from '../SearchContext'; @@ -88,7 +88,6 @@ export const SearchResults = React.memo(function SearchResults({ const { isDarkMode } = useColorMode(); const { searchViewProgress } = useBrowserContext(); const { inputRef, keyboardHeight, searchQuery, searchResults } = useSearchContext(); - const { dapps } = useDappsContext(); const separatorSecondary = useForegroundColor('separatorSecondary'); const separatorTertiary = useForegroundColor('separatorTertiary'); @@ -102,20 +101,6 @@ export const SearchResults = React.memo(function SearchResults({ inputRef?.current?.blur(); }, [inputRef]); - useAnimatedReaction( - () => searchQuery.value, - (result, previous) => { - if (result !== previous && isFocused.value) { - searchResults.modify(value => { - const results = search(result, dapps, 4); - value.splice(0, value.length); - value.push(...results); - return value; - }); - } - } - ); - const allResultsAnimatedStyle = useAnimatedStyle(() => ({ display: searchQuery.value ? 'flex' : 'none', })); @@ -143,95 +128,128 @@ export const SearchResults = React.memo(function SearchResults({ })); return ( - - - - - 􀆄 - - - - - - - 􀊫 - - - {i18n.t(i18n.l.dapp_browser.search.find_apps_and_more)} - - - - - - - - - - - - - - - - - - - - - 􀊫 - - - {i18n.t(i18n.l.dapp_browser.search.more_results)} - - - - - - - - - - + <> + + + + + + + + 􀆄 + + + + + + + 􀊫 + + + {i18n.t(i18n.l.dapp_browser.search.find_apps_and_more)} + + + + + + + + + + + + + - - - - - + + + + + + + 􀊫 + + + {i18n.t(i18n.l.dapp_browser.search.more_results)} + + + + + + + + + + + + + + + + + - + ); }); +const DappsDataSync = ({ + isFocused, + searchQuery, + searchResults, +}: { + isFocused: SharedValue; + searchQuery: SharedValue; + searchResults: SharedValue; +}) => { + const { dapps } = useDappsContext(); + + useAnimatedReaction( + () => searchQuery.value, + (result, previous) => { + if (result !== previous && isFocused.value) { + searchResults.modify(value => { + const results = search(result, dapps, 4); + value.splice(0, value.length); + value.push(...results); + return value; + }); + } + } + ); + + return null; +}; + const styles = StyleSheet.create({ closeButton: { height: 32, diff --git a/src/components/asset-list/AssetList.js b/src/components/asset-list/AssetList.js index b96cecce68a..c221e4f8661 100644 --- a/src/components/asset-list/AssetList.js +++ b/src/components/asset-list/AssetList.js @@ -12,7 +12,6 @@ const FabSizeWithPadding = FloatingActionButtonSize + FabWrapperBottomPosition * const AssetList = ({ accentColor, hideHeader, - isEmpty, isLoading, isWalletEthZero, network, diff --git a/src/components/expanded-state/UniqueTokenExpandedState.tsx b/src/components/expanded-state/UniqueTokenExpandedState.tsx index 228d04107b3..a1c92d99192 100644 --- a/src/components/expanded-state/UniqueTokenExpandedState.tsx +++ b/src/components/expanded-state/UniqueTokenExpandedState.tsx @@ -226,8 +226,8 @@ const UniqueTokenExpandedState = ({ asset: passedAsset, external }: UniqueTokenE const { navigate, setOptions } = useNavigation(); const { colors, isDarkMode } = useTheme(); const { isReadOnlyWallet } = useWallets(); - const collecible = useCollectible(passedAsset?.uniqueId); - const asset = external ? passedAsset : collecible; + const collectible = useCollectible(passedAsset?.uniqueId); + const asset = external ? passedAsset : collectible; const { data: { nftOffers }, } = useNFTOffers({ diff --git a/src/components/toasts/Toast.tsx b/src/components/toasts/Toast.tsx index f6895711651..8b6852a5375 100644 --- a/src/components/toasts/Toast.tsx +++ b/src/components/toasts/Toast.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, PropsWithChildren, useLayoutEffect } from 'react'; +import React, { Fragment, PropsWithChildren, memo, useLayoutEffect } from 'react'; import { Insets, ViewProps } from 'react-native'; import Animated, { interpolate, useAnimatedStyle, useSharedValue, withSpring, WithSpringConfig } from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; @@ -53,7 +53,7 @@ type Props = PropsWithChildren<{ }> & Pick; -export default function Toast({ children, color, distance = 90, targetTranslate = 0, icon, isVisible, testID, text, textColor }: Props) { +function Toast({ children, color, distance = 90, targetTranslate = 0, icon, isVisible, testID, text, textColor }: Props) { const { colors, isDarkMode } = useTheme(); const { width: deviceWidth } = useDimensions(); const insets = useSafeAreaInsets(); @@ -89,3 +89,5 @@ export default function Toast({ children, color, distance = 90, targetTranslate ); } + +export default memo(Toast); diff --git a/src/handlers/ens.ts b/src/handlers/ens.ts index cfb31cf92c3..e29076a878f 100644 --- a/src/handlers/ens.ts +++ b/src/handlers/ens.ts @@ -463,24 +463,6 @@ export const fetchAccountPrimary = async (accountAddress: string) => { }; }; -export function prefetchENSIntroData() { - const ensMarqueeQueryData = queryClient.getQueryData<{ - ensMarquee: EnsMarqueeAccount[]; - }>([ENS_MARQUEE_QUERY_KEY]); - - if (ensMarqueeQueryData?.ensMarquee) { - const ensMarqueeAccounts = ensMarqueeQueryData.ensMarquee.map((account: EnsMarqueeAccount) => account.name); - - for (const name of ensMarqueeAccounts) { - prefetchENSAddress({ name }, { staleTime: Infinity }); - prefetchENSAvatar(name, { cacheFirst: true }); - prefetchENSCover(name, { cacheFirst: true }); - prefetchENSRecords(name, { cacheFirst: true }); - prefetchFirstTransactionTimestamp({ addressOrName: name }); - } - } -} - export const estimateENSCommitGasLimit = async ({ name, ownerAddress, duration, rentPrice, salt }: ENSActionParameters) => estimateENSTransactionGasLimit({ duration, diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 77a279b144a..048802245d6 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -2,7 +2,6 @@ export { useChartDataLabels, useChartThrottledPoints } from './charts'; export { default as useDelayedValueWithLayoutAnimation } from './useDelayedValueWithLayoutAnimation'; export { default as useAccountAsset } from './useAccountAsset'; export { default as useFrameDelayedValue } from './useFrameDelayedValue'; -export { default as useAccountEmptyState } from './useAccountEmptyState'; export { default as useAccountENSDomains, prefetchAccountENSDomains } from './useAccountENSDomains'; export { default as useAndroidScrollViewGestureHandler } from './useAndroidScrollViewGestureHandler'; export { default as useAccountProfile } from './useAccountProfile'; @@ -33,7 +32,6 @@ export { default as useENSResolver, prefetchENSResolver } from './useENSResolver export { default as useENSRegistrant, prefetchENSRegistrant } from './useENSRegistrant'; export { default as useENSRecords, prefetchENSRecords, ensRecordsQueryKey } from './useENSRecords'; export { default as useFadeImage } from './useFadeImage'; -export { default as useTrackENSProfile } from './useTrackENSProfile'; export { default as useENSRecordDisplayProperties } from './useENSRecordDisplayProperties'; export { default as useENSRegistration } from './useENSRegistration'; export { default as useENSModifiedRegistration } from './useENSModifiedRegistration'; diff --git a/src/hooks/useAccountEmptyState.ts b/src/hooks/useAccountEmptyState.ts deleted file mode 100644 index d86210480de..00000000000 --- a/src/hooks/useAccountEmptyState.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { useEffect, useMemo } from 'react'; -import { getAccountEmptyState, saveAccountEmptyState } from '../handlers/localstorage/accountLocal'; -import useAccountSettings from './useAccountSettings'; - -export default function useAccountEmptyState(isSectionsEmpty: boolean, isLoadingUserAssets: boolean) { - const { network, accountAddress } = useAccountSettings(); - const isAccountEmptyInStorage = useMemo(() => getAccountEmptyState(accountAddress, network), [accountAddress, network]); - const isEmpty: { [address: string]: boolean | undefined } = useMemo( - () => ({ - ...isEmpty, - [accountAddress]: isLoadingUserAssets ? isAccountEmptyInStorage : isSectionsEmpty, - }), - [accountAddress, isAccountEmptyInStorage, isLoadingUserAssets, isSectionsEmpty] - ); - - useEffect(() => { - if (!isLoadingUserAssets) { - saveAccountEmptyState(false, accountAddress, network); - } - }, [accountAddress, isLoadingUserAssets, isSectionsEmpty, network]); - - return { - isEmpty: isEmpty[accountAddress], - }; -} diff --git a/src/hooks/useCollectible.ts b/src/hooks/useCollectible.ts index 780d12ad619..30e4fd9af35 100644 --- a/src/hooks/useCollectible.ts +++ b/src/hooks/useCollectible.ts @@ -1,24 +1,19 @@ import { useMemo } from 'react'; -import { ParsedAddressAsset } from '@/entities'; import { useLegacyNFTs } from '@/resources/nfts'; import { useAccountSettings } from '.'; export default function useCollectible(uniqueId: string, externalAddress?: string) { const { accountAddress } = useAccountSettings(); - const { - data: { nftsMap: selfNFTsMap }, - } = useLegacyNFTs({ address: accountAddress }); - const { - data: { nftsMap: externalNFTsMap }, - } = useLegacyNFTs({ - address: externalAddress ?? '', - }); + const isExternal = Boolean(externalAddress); - // Use the appropriate tokens based on if the user is viewing the - // current accounts tokens, or external tokens (e.g. ProfileSheet) - const uniqueTokensMap = useMemo(() => (isExternal ? externalNFTsMap : selfNFTsMap), [externalNFTsMap, isExternal, selfNFTsMap]); + const address = isExternal ? externalAddress ?? '' : accountAddress; - const asset = uniqueTokensMap?.[uniqueId]; + const { data: asset } = useLegacyNFTs({ + address, + config: { + select: data => data.nftsMap[uniqueId], + }, + }); - return { ...asset, isExternal }; + return useMemo(() => ({ ...asset, isExternal }), [asset, isExternal]); } diff --git a/src/hooks/useTrackENSProfile.ts b/src/hooks/useTrackENSProfile.ts deleted file mode 100644 index 0a0345a0360..00000000000 --- a/src/hooks/useTrackENSProfile.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { useCallback, useMemo } from 'react'; -import { fetchENSRecords } from './useENSRecords'; -import useWallets from './useWallets'; -import { analyticsV2 } from '@/analytics'; -import { EthereumAddress } from '@/entities'; -import { fetchAccountDomains } from '@/handlers/ens'; -import { ENS_RECORDS } from '@/helpers/ens'; -import walletTypes from '@/helpers/walletTypes'; -import { RainbowWallet } from '@/model/wallet'; - -export default function useTrackENSProfile() { - const { walletNames, wallets } = useWallets(); - - const addresses = useMemo( - () => - Object.values(wallets || {}) - .filter(wallet => wallet?.type !== walletTypes.readOnly) - .reduce( - (addresses: EthereumAddress[], wallet: RainbowWallet | undefined) => - addresses.concat(wallet?.addresses.map(({ address }: { address: EthereumAddress }) => address)!), - [] - ), - [wallets] - ); - - const getTrackProfilesData = useCallback(async () => { - const data = { - numberOfENSOwned: 0, - numberOfENSWithAvatarOrCoverSet: 0, - numberOfENSWithOtherMetadataSet: 0, - numberOfENSWithPrimaryNameSet: 0, - }; - for (const i in addresses) { - const ens = walletNames[addresses[i]]; - if (ens) { - const { records } = await fetchENSRecords(ens); - const domains = await fetchAccountDomains(addresses[i]); - data.numberOfENSOwned += domains?.account?.registrations?.length || 0; - data.numberOfENSWithAvatarOrCoverSet += records?.avatar || records?.header ? 1 : 0; - - data.numberOfENSWithOtherMetadataSet = Object.keys(records ?? {}).some( - key => key !== ENS_RECORDS.header && key !== ENS_RECORDS.avatar - ) - ? 1 - : 0; - data.numberOfENSWithPrimaryNameSet += 1; - } - } - return data; - }, [addresses, walletNames]); - - const { data, isSuccess } = useQuery(['getTrackProfilesData', [addresses]], getTrackProfilesData, { - enabled: Boolean(addresses.length), - retry: 0, - }); - - const trackENSProfile = useCallback(() => { - isSuccess && data && analyticsV2.identify(data); - }, [isSuccess, data]); - - return { trackENSProfile }; -} diff --git a/src/logger/sentry.ts b/src/logger/sentry.ts index d236e9ae087..05b7ceaac05 100644 --- a/src/logger/sentry.ts +++ b/src/logger/sentry.ts @@ -6,27 +6,21 @@ import { IS_PROD, IS_TEST } from '@/env'; import { logger, RainbowError } from '@/logger'; import isTestFlight from '@/helpers/isTestFlight'; -/** - * We need to disable React Navigation instrumentation for E2E tests because - * detox doesn't like setTimeout calls that are used inside When enabled detox - * hangs and timeouts on all test cases - */ -export const sentryRoutingInstrumentation = IS_PROD ? new Sentry.ReactNavigationInstrumentation() : undefined; - -export const defaultOptions = { +export const defaultOptions: Sentry.ReactNativeOptions = { + attachStacktrace: true, + defaultIntegrations: false, dsn: SENTRY_ENDPOINT, - enableAutoSessionTracking: true, + enableAppHangTracking: false, + enableAutoPerformanceTracing: false, + enableAutoSessionTracking: false, + enableTracing: false, environment: isTestFlight ? 'Testflight' : SENTRY_ENVIRONMENT, - integrations: [ - new Sentry.ReactNativeTracing({ - routingInstrumentation: sentryRoutingInstrumentation, - tracingOrigins: ['localhost', /^\//], - }), - ], - tracesSampleRate: 0.2, + integrations: [], + maxBreadcrumbs: 5, + tracesSampleRate: 0, }; -export async function initSentry() { +export function initSentry() { if (IS_TEST) { logger.debug(`Sentry is disabled for test environment`); return; diff --git a/src/resources/nfts/index.ts b/src/resources/nfts/index.ts index e49337ef00b..d7b1c88c198 100644 --- a/src/resources/nfts/index.ts +++ b/src/resources/nfts/index.ts @@ -1,20 +1,20 @@ -import { useQuery } from '@tanstack/react-query'; -import { createQueryKey } from '@/react-query'; +import { QueryFunction, useQuery } from '@tanstack/react-query'; +import { QueryConfigWithSelect, createQueryKey } from '@/react-query'; import { NFT } from '@/resources/nfts/types'; import { fetchSimpleHashNFTListing } from '@/resources/nfts/simplehash'; -import { useMemo } from 'react'; import { simpleHashNFTToUniqueAsset } from '@/resources/nfts/simplehash/utils'; import { useSelector } from 'react-redux'; import { AppState } from '@/redux/store'; import { Network } from '@/helpers'; import { UniqueAsset } from '@/entities'; import { arcClient } from '@/graphql'; +import { createSelector } from 'reselect'; -const NFTS_STALE_TIME = 300000; // 5 minutes +const NFTS_STALE_TIME = 600000; // 10 minutes const NFTS_CACHE_TIME_EXTERNAL = 3600000; // 1 hour const NFTS_CACHE_TIME_INTERNAL = 604800000; // 1 week -export const nftsQueryKey = ({ address }: { address: string }) => createQueryKey('nfts', { address }, { persisterVersion: 2 }); +export const nftsQueryKey = ({ address }: { address: string }) => createQueryKey('nfts', { address }, { persisterVersion: 3 }); export const nftListingQueryKey = ({ contractAddress, @@ -26,53 +26,74 @@ export const nftListingQueryKey = ({ network: Omit; }) => createQueryKey('nftListing', { contractAddress, tokenId, network }); -export function useNFTs(): NFT[] { - // normal react query where we get new NFT formatted data - return []; +const walletsSelector = (state: AppState) => state.wallets?.wallets; + +const isImportedWalletSelector = createSelector( + walletsSelector, + (_: AppState, address: string) => address, + (wallets, address) => { + if (!wallets) { + return false; + } + for (const wallet of Object.values(wallets)) { + if (wallet.addresses.some(account => account.address === address)) { + return true; + } + } + return false; + } +); + +interface NFTData { + nfts: UniqueAsset[]; + nftsMap: Record; } -export function useLegacyNFTs({ address }: { address: string }) { - const { wallets } = useSelector((state: AppState) => state.wallets); +type NFTQueryKey = ReturnType; - const walletAddresses = useMemo( - () => (wallets ? Object.values(wallets).flatMap(wallet => wallet.addresses.map(account => account.address)) : []), - [wallets] - ); - const isImportedWallet = walletAddresses.includes(address); - - const { data, error, isFetching } = useQuery({ - queryKey: nftsQueryKey({ address }), - queryFn: async () => { - const queryResponse = await arcClient.getNFTs({ walletAddress: address }); - const nfts = queryResponse?.nfts?.map(nft => simpleHashNFTToUniqueAsset(nft, address)); - return nfts; +const fetchNFTData: QueryFunction = async ({ queryKey }) => { + const [{ address }] = queryKey; + const queryResponse = await arcClient.getNFTs({ walletAddress: address }); + + const nfts = queryResponse?.nfts?.map(nft => simpleHashNFTToUniqueAsset(nft, address)); + + // ⚠️ TODO: Delete this and rework the code that uses it + const nftsMap = nfts?.reduce( + (acc, nft) => { + // Track down why these both exist - we should not be doing this + acc[nft.uniqueId] = nft; + acc[nft.fullUniqueId] = nft; + return acc; }, - staleTime: NFTS_STALE_TIME, - retry: 3, + {} as Record + ); + + return { nfts: nfts ?? [], nftsMap: nftsMap ?? {} }; +}; + +const FALLBACK_DATA: NFTData = { nfts: [], nftsMap: {} }; + +export function useLegacyNFTs({ + address, + config, +}: { + address: string; + config?: QueryConfigWithSelect; +}) { + const isImportedWallet = useSelector((state: AppState) => isImportedWalletSelector(state, address)); + + const { data, error, isFetching } = useQuery(nftsQueryKey({ address }), fetchNFTData, { cacheTime: isImportedWallet ? NFTS_CACHE_TIME_INTERNAL : NFTS_CACHE_TIME_EXTERNAL, enabled: !!address, + retry: 3, + staleTime: NFTS_STALE_TIME, + ...config, }); - const nfts = useMemo(() => data ?? [], [data]); - - const nftsMap = useMemo( - () => - nfts.reduce( - (acc, nft) => { - // index by both uniqueId and fullUniqueId bc why not - acc[nft.uniqueId] = nft; - acc[nft.fullUniqueId] = nft; - return acc; - }, - {} as { [key: string]: UniqueAsset } - ), - [nfts] - ); - return { - data: { nfts, nftsMap }, + data: (config?.select ? data ?? config.select(FALLBACK_DATA) : data ?? FALLBACK_DATA) as TSelected, error, - isInitialLoading: !data?.length && isFetching, + isInitialLoading: !data && isFetching, }; } diff --git a/src/screens/WalletScreen/index.tsx b/src/screens/WalletScreen/index.tsx index 1672d5bd227..1b982d1cace 100644 --- a/src/screens/WalletScreen/index.tsx +++ b/src/screens/WalletScreen/index.tsx @@ -6,13 +6,10 @@ import { Page } from '../../components/layout'; import { Network } from '@/helpers'; import { useRemoveFirst } from '@/navigation/useRemoveFirst'; import { settingsUpdateNetwork } from '@/redux/settings'; -import useExperimentalFlag, { PROFILES } from '@/config/experimentalHooks'; -import { prefetchENSIntroData } from '@/handlers/ens'; import { navbarHeight } from '@/components/navbar/Navbar'; import { Box } from '@/design-system'; import { useAccountAccentColor, - useAccountEmptyState, useAccountSettings, useInitializeAccountData, useInitializeWallet, @@ -20,7 +17,6 @@ import { useLoadAccountLateData, useLoadGlobalLateData, useResetAccountState, - useTrackENSProfile, useWalletSectionsData, } from '@/hooks'; import Routes from '@rainbow-me/routes'; @@ -34,19 +30,20 @@ import { addressCopiedToastAtom } from '@/recoil/addressCopiedToastAtom'; import { usePositions } from '@/resources/defi/PositionsQuery'; import styled from '@/styled-thing'; import { UserAssetsSync } from '@/__swaps__/screens/Swap/components/UserAssetsSync'; +import { IS_ANDROID } from '@/env'; const WalletPage = styled(Page)({ ...position.sizeAsObject('100%'), flex: 1, }); +// eslint-disable-next-line @typescript-eslint/no-explicit-any const WalletScreen: React.FC = ({ navigation, route }) => { const { params } = route; const { setParams, getState: dangerouslyGetState, getParent: dangerouslyGetParent } = navigation; const removeFirst = useRemoveFirst(); const [initialized, setInitialized] = useState(!!params?.initialized); const initializeWallet = useInitializeWallet(); - const { trackENSProfile } = useTrackENSProfile(); const { network: currentNetwork, accountAddress, appIcon, nativeCurrency } = useAccountSettings(); usePositions({ address: accountAddress, currency: nativeCurrency }); @@ -75,17 +72,12 @@ const WalletScreen: React.FC = ({ navigation, route }) => { }, [currentNetwork, revertToMainnet]); const walletReady = useSelector(({ appState: { walletReady } }: AppState) => walletReady); - const { - isWalletEthZero, - isEmpty: isSectionsEmpty, - isLoadingUserAssets, - briefSectionsData: walletBriefSectionsData, - } = useWalletSectionsData(); + const { isWalletEthZero, isLoadingUserAssets, briefSectionsData: walletBriefSectionsData } = useWalletSectionsData(); useEffect(() => { // This is the fix for Android wallet creation problem. // We need to remove the welcome screen from the stack. - if (ios) { + if (!IS_ANDROID) { return; } const isWelcomeScreen = dangerouslyGetParent()?.getState().routes[0].name === Routes.WELCOME_SCREEN; @@ -94,14 +86,6 @@ const WalletScreen: React.FC = ({ navigation, route }) => { } }, [dangerouslyGetState, removeFirst]); - const { isEmpty: isAccountEmpty } = useAccountEmptyState(isSectionsEmpty, isLoadingUserAssets); - - const { addressSocket } = useSelector(({ explorer: { addressSocket } }: AppState) => ({ - addressSocket, - })); - - const profilesEnabled = useExperimentalFlag(PROFILES); - useEffect(() => { const initializeAndSetParams = async () => { // @ts-expect-error messed up initializeWallet types @@ -123,19 +107,6 @@ const WalletScreen: React.FC = ({ navigation, route }) => { } }, [loadAccountLateData, loadGlobalLateData, walletReady]); - useEffect(() => { - if (walletReady && profilesEnabled) { - InteractionManager.runAfterInteractions(() => { - // We are not prefetching intro profiles data on Android - // as the RPC call queue is considerably slower. - if (ios) { - prefetchENSIntroData(); - } - trackENSProfile(); - }); - } - }, [profilesEnabled, trackENSProfile, walletReady]); - // track current app icon useEffect(() => { analyticsV2.identify({ appIcon }); @@ -160,8 +131,7 @@ const WalletScreen: React.FC = ({ navigation, route }) => { = ({ navigation, route }) => { - {/* NOTE: The components below render null and are solely for keeping react-query and Zustand in sync */} + {/* NOTE: The component below renders null and is solely for keeping react-query and Zustand in sync */} diff --git a/src/screens/points/components/LeaderboardRow.tsx b/src/screens/points/components/LeaderboardRow.tsx index 6aac7b2e24f..6892a058fd0 100644 --- a/src/screens/points/components/LeaderboardRow.tsx +++ b/src/screens/points/components/LeaderboardRow.tsx @@ -1,5 +1,5 @@ import * as i18n from '@/languages'; -import React, { useCallback, useMemo } from 'react'; +import React, { memo, useCallback, useMemo } from 'react'; import { Keyboard, Share } from 'react-native'; import { MenuActionConfig } from 'react-native-ios-context-menu'; import ContextMenuButton from '@/components/native-context-menu/contextMenu'; @@ -30,7 +30,7 @@ const ACTIONS = { SHARE: 'share', }; -export const LeaderboardRow = ({ +export const LeaderboardRow = memo(function LeaderboardRow({ address, ens, avatarURL, @@ -42,7 +42,7 @@ export const LeaderboardRow = ({ avatarURL?: string; points: number; rank: number; -}) => { +}) { const { switchToWalletWithAddress, selectedWallet } = useWallets(); const { isWatching } = useWatchWallet({ address }); const { colors } = useTheme(); @@ -253,4 +253,4 @@ export const LeaderboardRow = ({ ); -}; +}); diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 955c36edff1..c69ff91a70f 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -72,8 +72,7 @@ const Logger = { console.log(...args); // eslint-disable-line no-console } if (args.length === 1 && typeof args[0] === 'string') { - // @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. - sentryUtils.addInfoBreadcrumb.apply(null, args); + sentryUtils.addInfoBreadcrumb.apply(null, [args[0]]); } else { const safeData = safelyStringifyWithFormat(args[1]); sentryUtils.addDataBreadcrumb(args[0], safeData);