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 01/13] 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 02/13] 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 03/13] 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 04/13] 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 05/13] 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 06/13] 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); From ca22bfd60f028b9aa508b4d37ec4b0ffb54a9080 Mon Sep 17 00:00:00 2001 From: Bruno Barbieri <1247834+brunobar79@users.noreply.github.com> Date: Fri, 7 Jun 2024 14:26:57 -0400 Subject: [PATCH 07/13] reorg ens related calls (#5828) --- src/components/cards/ENSCreateProfileCard.tsx | 22 +++++++++---------- src/hooks/useOnAvatarPress.ts | 14 +++++------- src/redux/wallets.ts | 2 -- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/components/cards/ENSCreateProfileCard.tsx b/src/components/cards/ENSCreateProfileCard.tsx index c04936b76ec..e905091f6de 100644 --- a/src/components/cards/ENSCreateProfileCard.tsx +++ b/src/components/cards/ENSCreateProfileCard.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useCallback, useEffect } from 'react'; import ENSAvatarGrid from '../../assets/ensAvatarGrid.png'; import ENSIcon from '../../assets/ensIcon.png'; import { useNavigation } from '../../navigation/Navigation'; @@ -33,8 +33,15 @@ export const ENSCreateProfileCard = () => { // 40 represents the horizontal padding outside the card const imageWidth = deviceWidth - 40; - const handlePress = () => { + const { uniqueDomain } = useAccountENSDomains(); + + const handlePress = useCallback(() => { if (!isReadOnlyWallet || enableActionsOnReadOnlyWallet) { + if (uniqueDomain?.name) { + prefetchENSAvatar(uniqueDomain.name); + prefetchENSRecords(uniqueDomain.name); + } + analyticsV2.track(analyticsV2.event.cardPressed, { cardName: 'ENSCreateProfileCard', routeName, @@ -46,16 +53,7 @@ export const ENSCreateProfileCard = () => { } else { watchingAlert(); } - }; - - const { uniqueDomain } = useAccountENSDomains(); - - useEffect(() => { - if (uniqueDomain?.name) { - prefetchENSAvatar(uniqueDomain.name); - prefetchENSRecords(uniqueDomain.name); - } - }, [uniqueDomain]); + }, [isReadOnlyWallet, navigate, routeName, uniqueDomain?.name]); return ( diff --git a/src/hooks/useOnAvatarPress.ts b/src/hooks/useOnAvatarPress.ts index cf47929b0ea..4c7323bcf24 100644 --- a/src/hooks/useOnAvatarPress.ts +++ b/src/hooks/useOnAvatarPress.ts @@ -54,14 +54,6 @@ export default ({ screenType = 'transaction' }: UseOnAvatarPressProps = {}) => { const { startRegistration } = useENSRegistration(); const { setNextEmoji } = useUpdateEmoji(); - useEffect(() => { - if (accountENS) { - prefetchENSAvatar(accountENS); - prefetchENSCover(accountENS); - prefetchENSRecords(accountENS); - } - }, [accountENS]); - const onAvatarRemovePhoto = useCallback(async () => { const newWallets: typeof wallets = { ...wallets, @@ -256,6 +248,12 @@ export default ({ screenType = 'transaction' }: UseOnAvatarPressProps = {}) => { }, [accountENS, navigate]); const onAvatarPress = useCallback(() => { + if (accountENS) { + prefetchENSAvatar(accountENS); + prefetchENSCover(accountENS); + prefetchENSRecords(accountENS); + } + if (hasENSAvatar && accountENS) { onAvatarPressProfile(); } else { diff --git a/src/redux/wallets.ts b/src/redux/wallets.ts index ea733c8706f..519223b54c3 100644 --- a/src/redux/wallets.ts +++ b/src/redux/wallets.ts @@ -207,8 +207,6 @@ export const walletsLoadState = type: WALLETS_LOAD, }); - dispatch(fetchWalletNames()); - profilesEnabled && dispatch(fetchWalletENSAvatars()); return wallets; } catch (error) { logger.error(new RainbowError('Exception during walletsLoadState'), { From 6f00408829b75a93f4a52922e394e1982d3281bc Mon Sep 17 00:00:00 2001 From: brdy <41711440+BrodyHughes@users.noreply.github.com> Date: Fri, 7 Jun 2024 16:44:47 -0400 Subject: [PATCH 08/13] [e2e] Add Malicious Dapp interaction test (#5764) * . * . * . * . * final * cleanup * fix discover test * rm comment * Update e2e/7_maliciousDappConnection.spec.ts Co-authored-by: Matthew Wall * . --------- Co-authored-by: Matthew Wall --- e2e/4_discoverSheetFlow.spec.ts | 7 +-- e2e/7_maliciousDappConnection.spec.ts | 56 +++++++++++++++++++ e2e/environment.js | 2 +- e2e/helpers.ts | 17 ++++-- e2e/init.js | 2 + src/components/DappBrowser/DappBrowser.tsx | 1 + .../DappBrowser/search-input/SearchInput.tsx | 1 + src/config/experimental.ts | 3 +- src/screens/WalletConnectApprovalSheet.js | 2 +- 9 files changed, 78 insertions(+), 13 deletions(-) create mode 100644 e2e/7_maliciousDappConnection.spec.ts diff --git a/e2e/4_discoverSheetFlow.spec.ts b/e2e/4_discoverSheetFlow.spec.ts index bc4a2b4ad7c..de0ad61f37e 100644 --- a/e2e/4_discoverSheetFlow.spec.ts +++ b/e2e/4_discoverSheetFlow.spec.ts @@ -37,13 +37,12 @@ describe('Discover Screen Flow', () => { }); it('Should navigate to the Points screen after swiping left', async () => { - await swipe('profile-screen', 'left', 'slow'); + await swipe('profile-screen', 'left', 'fast'); await checkIfVisible('points-screen'); }); - it('Should navigate back to Discover screen after swiping right twice', async () => { - await swipe('points-screen', 'right', 'slow'); - await swipe('profile-screen', 'right', 'slow'); + it('Should navigate back to Discover screen after tapping Discover icon', async () => { + await waitAndTap('tab-bar-icon-DiscoverScreen'); await checkIfVisible('discover-header'); }); diff --git a/e2e/7_maliciousDappConnection.spec.ts b/e2e/7_maliciousDappConnection.spec.ts new file mode 100644 index 00000000000..0b6f49fd0bf --- /dev/null +++ b/e2e/7_maliciousDappConnection.spec.ts @@ -0,0 +1,56 @@ +import { + beforeAllcleanApp, + afterAllcleanApp, + importWalletFlow, + waitAndTap, + swipe, + checkIfVisible, + checkIfExistsByText, + typeText, + delayTime, + tapAtPoint, + checkIfExists, +} from './helpers'; +import { WALLET_VARS } from './testVariables'; + +describe('Check malicious dapp warning', () => { + beforeAll(async () => { + await beforeAllcleanApp({ hardhat: false }); + }); + + afterAll(async () => { + await afterAllcleanApp({ hardhat: false }); + }); + + it('Should be able to watch a wallet and load the wallet screen', async () => { + await importWalletFlow(WALLET_VARS.SEED_WALLET.PK); + }); + + it('Should be able to navigate to the dapp browser', async () => { + await swipe('wallet-screen', 'left', 'fast'); + await swipe('discover-sheet', 'left', 'fast'); + await checkIfVisible('browser-screen'); + }); + + it('Should be able to type on search input and go to malicious dapp', async () => { + await waitAndTap('browser-search-input'); + await checkIfExistsByText('Find apps and more'); + await typeText('browser-search-input', 'https://test-dap-welps.vercel.app/', true, false, true); + // Waiting for webpage to load + await delayTime('long'); + }); + + it('Should attempt to connect to in browser dapp', async () => { + // Detox can't query elements within a WebView within our app + // Using tapAtPoint() to tap coordinates is a workaround for now + + // Tapping connect button + await tapAtPoint('browser-screen', { x: 275, y: 80 }); + // Waiting for rainbowkit sheet to load / animate in + await delayTime('medium'); + // Tapping Rainbow button + await tapAtPoint('browser-screen', { x: 50, y: 325 }); + + await checkIfExists('malicious-dapp-warning'); + }); +}); diff --git a/e2e/environment.js b/e2e/environment.js index 74d331de25b..3d9918b8551 100644 --- a/e2e/environment.js +++ b/e2e/environment.js @@ -5,7 +5,7 @@ class CustomDetoxEnvironment extends DetoxCircusEnvironment { constructor(config, context) { super(config, context); this.launchAppTimeout = 120_000; - this.initTimeout = 360_000; + this.initTimeout = 120_000; } } module.exports = CustomDetoxEnvironment; diff --git a/e2e/helpers.ts b/e2e/helpers.ts index 206c3a7ab84..55344f6052c 100644 --- a/e2e/helpers.ts +++ b/e2e/helpers.ts @@ -28,7 +28,7 @@ export async function killHardhat() { exec('kill $(lsof -t -i:8545)'); } -export async function importWalletFlow() { +export async function importWalletFlow(customSeed?: string) { await checkIfVisible('welcome-screen'); await waitAndTap('already-have-wallet-button'); await checkIfExists('add-wallet-sheet'); @@ -36,7 +36,7 @@ export async function importWalletFlow() { await checkIfExists('import-sheet'); await clearField('import-sheet-input'); await device.disableSynchronization(); - await typeText('import-sheet-input', process.env.TEST_SEEDS, false); + await typeText('import-sheet-input', customSeed ? customSeed : process.env.TEST_SEEDS, false); await checkIfElementHasString('import-sheet-button-label', 'Continue'); await waitAndTap('import-sheet-button'); await checkIfVisible('wallet-info-modal'); @@ -52,9 +52,6 @@ export async function importWalletFlow() { } export async function beforeAllcleanApp({ hardhat }: { hardhat?: boolean }) { - // sometimes i see tests failed from the get-go - // giving an extra 15 to let things set up - await delayTime('very-long'); jest.resetAllMocks(); hardhat && (await startHardhat()); } @@ -127,7 +124,13 @@ export async function startIosSimulator() { } } -export async function typeText(elementId: string | RegExp, text: string | undefined, focus = true, syncOnAndroid = false) { +export async function typeText( + elementId: string | RegExp, + text: string | undefined, + focus = true, + syncOnAndroid = false, + hitEnterAfterText = false +) { if (text === undefined) { throw new Error(`Cannot type 'undefined' into element with id ${elementId}`); } @@ -140,6 +143,7 @@ export async function typeText(elementId: string | RegExp, text: string | undefi await device.disableSynchronization(); } await element(by.id(elementId)).typeText(text); + hitEnterAfterText && (await typeText(elementId, '\n')); if (device.getPlatform() === 'android' && !syncOnAndroid) { await device.enableSynchronization(); } @@ -147,6 +151,7 @@ export async function typeText(elementId: string | RegExp, text: string | undefi throw new Error(`Error typing "${text}" at element with id ${elementId}}: ${error}`); } } + export async function typeNumbers(elementId: string | RegExp, text: string, submitLabel: string | RegExp) { try { await element(by.id(elementId)).replaceText(text.replace('\n', '')); diff --git a/e2e/init.js b/e2e/init.js index 0778641286a..a6d016ad6b6 100644 --- a/e2e/init.js +++ b/e2e/init.js @@ -27,5 +27,7 @@ beforeAll(async () => { '.*rainbowme-res.cloudinary.com*', '.*rainbow-proxy-rpc.rainbowdotme.workers.*', '.*localhost:8081/assets/src/assets*.', + '.*arc-graphql.rainbowdotme.workers.dev*.', + '.*googleapis.com*.', ]); }); diff --git a/src/components/DappBrowser/DappBrowser.tsx b/src/components/DappBrowser/DappBrowser.tsx index e6cdc74b076..a1c41ffe873 100644 --- a/src/components/DappBrowser/DappBrowser.tsx +++ b/src/components/DappBrowser/DappBrowser.tsx @@ -157,6 +157,7 @@ const TabViewScrollView = ({ children }: { children: React.ReactNode }) => { pinchGestureEnabled={false} ref={scrollViewRef} showsVerticalScrollIndicator={false} + testID={'browser-screen'} > {children} diff --git a/src/components/DappBrowser/search-input/SearchInput.tsx b/src/components/DappBrowser/search-input/SearchInput.tsx index 61d9133f455..6e527ae9c9b 100644 --- a/src/components/DappBrowser/search-input/SearchInput.tsx +++ b/src/components/DappBrowser/search-input/SearchInput.tsx @@ -393,6 +393,7 @@ const AddressBar = React.memo(function AddressBar({ = { [REMOTE_PROMO_SHEETS]: { settings: true, value: false }, [REMOTE_CARDS]: { settings: true, value: false }, [POINTS_NOTIFICATIONS_TOGGLE]: { settings: true, value: false }, - [DAPP_BROWSER]: { settings: true, value: false }, + [DAPP_BROWSER]: { settings: true, value: IS_TEST ? true : false }, [SWAPS_V2]: { settings: true, value: false }, }; diff --git a/src/screens/WalletConnectApprovalSheet.js b/src/screens/WalletConnectApprovalSheet.js index d4a9b1fd3c6..d708e37d162 100644 --- a/src/screens/WalletConnectApprovalSheet.js +++ b/src/screens/WalletConnectApprovalSheet.js @@ -430,7 +430,7 @@ export default function WalletConnectApprovalSheet() { {isScam && ( - + From a79ec4b5bff71fa533cd63357a38919554037bc1 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Mon, 10 Jun 2024 10:37:40 -0400 Subject: [PATCH 09/13] remove info icon on max transaction fee row as we don't have that in spec (#5827) --- .../screens/Swap/components/GasPanel.tsx | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/src/__swaps__/screens/Swap/components/GasPanel.tsx b/src/__swaps__/screens/Swap/components/GasPanel.tsx index 2402552e183..17224d285c7 100644 --- a/src/__swaps__/screens/Swap/components/GasPanel.tsx +++ b/src/__swaps__/screens/Swap/components/GasPanel.tsx @@ -16,7 +16,7 @@ import { import { add, subtract } from '@/__swaps__/utils/numbers'; import { opacity } from '@/__swaps__/utils/swaps'; import { ButtonPressAnimation } from '@/components/animations'; -import { Box, Inline, Separator, Stack, Text, globalColors, useColorMode, useForegroundColor } from '@/design-system'; +import { Bleed, Box, Inline, Separator, Stack, Text, globalColors, useColorMode, useForegroundColor } from '@/design-system'; import { IS_ANDROID } from '@/env'; import { lessThan } from '@/helpers/utilities'; import { useNavigation } from '@/navigation'; @@ -24,11 +24,14 @@ import Routes from '@/navigation/routesNames'; import { createRainbowStore } from '@/state/internal/createRainbowStore'; import { useSwapsStore } from '@/state/swaps/swapsStore'; import { upperFirst } from 'lodash'; +import { gasUtils } from '@/utils'; import { formatNumber } from '../hooks/formatNumber'; import { GasSettings, getCustomGasSettings, setCustomGasSettings, useCustomGasStore } from '../hooks/useCustomGas'; import { setSelectedGasSpeed, useSelectedGasSpeed } from '../hooks/useSelectedGas'; import { EstimatedSwapGasFee } from './EstimatedSwapGasFee'; +const { GAS_TRENDS } = gasUtils; + const MINER_TIP_TYPE = 'minerTip'; const MAX_BASE_FEE_TYPE = 'maxBaseFee'; const HIGH_ALERT = 'HIGH_ALERT'; @@ -136,9 +139,12 @@ function CurrentBaseFee() { const { isDarkMode } = useColorMode(); const { navigate } = useNavigation(); + const label = useForegroundColor('label'); + const labelSecondary = useForegroundColor('labelSecondary'); + const chainId = useSwapsStore(s => s.inputAsset?.chainId || ChainId.mainnet); const { data: baseFee } = useBaseFee({ chainId, select: selectWeiToGwei }); - const { data: gasTrend } = useGasTrend({ chainId }); + const { data: gasTrend = 'notrend' } = useGasTrend({ chainId }); const trendType = 'currentBaseFee' + upperFirst(gasTrend); @@ -160,15 +166,30 @@ function CurrentBaseFee() { > {i18n.t(i18n.l.gas.current_base_fee)} - - {formatNumber(baseFee || '0')} - + + + + {GAS_TRENDS[gasTrend].label} + + + {formatNumber(baseFee || '0')} + + + ); } @@ -277,9 +298,6 @@ function MaxTransactionFee() { {i18n.t(i18n.l.gas.max_transaction_fee)} - - 􀅴 - From 9b668f8acf56b1ad5295c7a2c0a429330ca79acd Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Mon, 10 Jun 2024 20:01:21 -0400 Subject: [PATCH 10/13] Add entry point for other networks (#5824) * add entry point from other available networks * actually remove the setting of the input filter for chain selector. doesn't make sense since they don't have another asset on that chain * Update src/components/expanded-state/AvailableNetworksv2.tsx * formatting fixes * Update src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts Co-authored-by: gregs --------- Co-authored-by: gregs --- .../Swap/hooks/useSwapInputsController.ts | 2 +- .../screens/Swap/providers/swap-provider.tsx | 4 +- src/__swaps__/utils/swaps.ts | 3 + .../expanded-state/AvailableNetworksv2.tsx | 67 ++++++++++++++++++- .../asset/ChartExpandedState.js | 2 +- .../sheet-action-buttons/SwapActionButton.tsx | 7 +- 6 files changed, 79 insertions(+), 6 deletions(-) diff --git a/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts b/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts index 3bd64647059..cfb9daead9d 100644 --- a/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts +++ b/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts @@ -32,7 +32,7 @@ import { queryClient } from '@/react-query'; import { divWorklet, equalWorklet, greaterThanWorklet, mulWorklet, toFixedWorklet } from '@/__swaps__/safe-math/SafeMath'; function getInitialInputValues(initialSelectedInputAsset: ExtendedAnimatedAssetWithColors | null) { - const initialBalance = Number(initialSelectedInputAsset?.balance.amount) ?? 0; + const initialBalance = Number(initialSelectedInputAsset?.balance.amount) || 0; const initialNiceIncrement = findNiceIncrement(initialBalance); const initialDecimalPlaces = countDecimalPlaces(initialNiceIncrement); diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index b63615582da..f67193d98d6 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -127,7 +127,9 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { const selectedOutputChainId = useSharedValue(initialSelectedInputAsset?.chainId || ChainId.mainnet); const quote = useSharedValue(null); - const inputProgress = useSharedValue(NavigationSteps.INPUT_ELEMENT_FOCUSED); + const inputProgress = useSharedValue( + initialSelectedOutputAsset && !initialSelectedInputAsset ? NavigationSteps.TOKEN_LIST_FOCUSED : NavigationSteps.INPUT_ELEMENT_FOCUSED + ); const outputProgress = useSharedValue( initialSelectedOutputAsset ? NavigationSteps.INPUT_ELEMENT_FOCUSED : NavigationSteps.TOKEN_LIST_FOCUSED ); diff --git a/src/__swaps__/utils/swaps.ts b/src/__swaps__/utils/swaps.ts index dc57412ed99..501c15fd7a1 100644 --- a/src/__swaps__/utils/swaps.ts +++ b/src/__swaps__/utils/swaps.ts @@ -149,6 +149,9 @@ export const countDecimalPlaces = (number: number | string): number => { export const findNiceIncrement = (availableBalance: string | number) => { 'worklet'; + if (Number(availableBalance) === 0) { + return 0; + } // We'll use one of these factors to adjust the base increment // These factors are chosen to: diff --git a/src/components/expanded-state/AvailableNetworksv2.tsx b/src/components/expanded-state/AvailableNetworksv2.tsx index d15af381327..bc1967a0d96 100644 --- a/src/components/expanded-state/AvailableNetworksv2.tsx +++ b/src/components/expanded-state/AvailableNetworksv2.tsx @@ -17,6 +17,14 @@ import ContextMenuButton from '@/components/native-context-menu/contextMenu'; import { implementation } from '@/entities/dispersion'; import { RainbowNetworks, getNetworkObj } from '@/networks'; import { EthCoinIcon } from '../coin-icon/EthCoinIcon'; +import { SWAPS_V2, useExperimentalFlag } from '@/config'; +import { useRemoteConfig } from '@/model/remoteConfig'; +import { userAssetsStore } from '@/state/assets/userAssets'; +import { parseSearchAsset } from '@/__swaps__/utils/assets'; +import { AddressOrEth, AssetType } from '@/__swaps__/types/assets'; +import { chainNameFromChainId } from '@/__swaps__/utils/chains'; +import { swapsStore } from '@/state/swaps/swapsStore'; +import { InteractionManager } from 'react-native'; const NOOP = () => null; @@ -33,6 +41,8 @@ const AvailableNetworksv2 = ({ }) => { const { colors } = useTheme(); const { goBack, navigate } = useNavigation(); + const { swaps_v2 } = useRemoteConfig(); + const swapsV2Enabled = useExperimentalFlag(SWAPS_V2); const radialGradientProps = { center: [0, 1], @@ -62,10 +72,63 @@ const AvailableNetworksv2 = ({ // we need to convert the mainnet asset to the selected network's newAsset.mainnet_address = networks?.[ethereumUtils.getChainIdFromNetwork(Network.mainnet)]?.address ?? asset.address; newAsset.address = networks?.[ethereumUtils.getChainIdFromNetwork(chosenNetwork)].address; + newAsset.network = chosenNetwork; + + goBack(); + + if (swapsV2Enabled || swaps_v2) { + const chainId = ethereumUtils.getChainIdFromNetwork(newAsset.network); + const uniqueId = `${newAsset.address}_${chainId}`; + const userAsset = userAssetsStore.getState().userAssets.get(uniqueId); + + const parsedAsset = parseSearchAsset({ + assetWithPrice: { + ...newAsset, + uniqueId, + address: newAsset.address as AddressOrEth, + type: newAsset.type as AssetType, + chainId, + chainName: chainNameFromChainId(chainId), + isNativeAsset: false, + native: {}, + }, + searchAsset: { + ...newAsset, + uniqueId, + chainId, + chainName: chainNameFromChainId(chainId), + address: newAsset.address as AddressOrEth, + highLiquidity: newAsset.highLiquidity ?? false, + isRainbowCurated: newAsset.isRainbowCurated ?? false, + isVerified: newAsset.isVerified ?? false, + mainnetAddress: (newAsset.mainnet_address ?? '') as AddressOrEth, + networks: newAsset.networks ?? [], + type: newAsset.type as AssetType, + }, + userAsset, + }); + + const largestBalanceSameChainUserAsset = userAssetsStore + .getState() + .getUserAssets() + .find(userAsset => userAsset.chainId === chainId && userAsset.address !== newAsset.address); + if (largestBalanceSameChainUserAsset) { + swapsStore.setState({ inputAsset: largestBalanceSameChainUserAsset }); + } else { + swapsStore.setState({ inputAsset: null }); + } + swapsStore.setState({ outputAsset: parsedAsset }); + + InteractionManager.runAfterInteractions(() => { + navigate(Routes.SWAP); + }); + + return; + } + newAsset.uniqueId = `${asset.address}_${chosenNetwork}`; newAsset.type = chosenNetwork; - goBack(); navigate(Routes.EXCHANGE_MODAL, { params: { fromDiscover: true, @@ -81,7 +144,7 @@ const AvailableNetworksv2 = ({ screen: Routes.CURRENCY_SELECT_SCREEN, }); }, - [asset, goBack, navigate, networks, updateInputCurrency] + [asset, goBack, navigate, networks, swapsV2Enabled, swaps_v2, updateInputCurrency] ); const handlePressContextMenu = useCallback( diff --git a/src/components/expanded-state/asset/ChartExpandedState.js b/src/components/expanded-state/asset/ChartExpandedState.js index d947faf4254..e4db1c1c7b2 100644 --- a/src/components/expanded-state/asset/ChartExpandedState.js +++ b/src/components/expanded-state/asset/ChartExpandedState.js @@ -29,7 +29,7 @@ import { useNavigation } from '@/navigation'; import { ETH_ADDRESS } from '@/references'; import Routes from '@/navigation/routesNames'; import styled from '@/styled-thing'; -import { ethereumUtils, safeAreaInsetValues } from '@/utils'; +import { safeAreaInsetValues } from '@/utils'; import AvailableNetworksv2 from '@/components/expanded-state/AvailableNetworksv2'; import AvailableNetworksv1 from '@/components/expanded-state/AvailableNetworks'; import { Box } from '@/design-system'; diff --git a/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx b/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx index 8daef82baa5..8e5123b3394 100644 --- a/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx +++ b/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx @@ -38,11 +38,13 @@ function SwapActionButton({ asset, color: givenColor, inputType, label, fromDisc const goToSwap = useCallback(() => { if (swapsV2Enabled || swaps_v2) { const chainId = ethereumUtils.getChainIdFromNetwork(asset.network); - const userAsset = userAssetsStore.getState().userAssets.get(`${asset.address}_${chainId}`); + const uniqueId = `${asset.address}_${chainId}`; + const userAsset = userAssetsStore.getState().userAssets.get(uniqueId); const parsedAsset = parseSearchAsset({ assetWithPrice: { ...asset, + uniqueId, address: asset.address as AddressOrEth, type: asset.type as AssetType, chainId, @@ -52,6 +54,7 @@ function SwapActionButton({ asset, color: givenColor, inputType, label, fromDisc }, searchAsset: { ...asset, + uniqueId, chainId, chainName: chainNameFromChainId(chainId), address: asset.address as AddressOrEth, @@ -74,6 +77,8 @@ function SwapActionButton({ asset, color: givenColor, inputType, label, fromDisc .find(userAsset => userAsset.chainId === chainId && userAsset.address !== asset.address); if (largestBalanceSameChainUserAsset) { swapsStore.setState({ inputAsset: largestBalanceSameChainUserAsset }); + } else { + swapsStore.setState({ inputAsset: null }); } swapsStore.setState({ outputAsset: parsedAsset }); } From f2c7bfb0b5ee98bc3fb43dbb9ca5d0a62cad74fa Mon Sep 17 00:00:00 2001 From: Bruno Barbieri <1247834+brunobar79@users.noreply.github.com> Date: Mon, 10 Jun 2024 22:12:00 -0400 Subject: [PATCH 11/13] bump braces (#5831) --- package.json | 1 + yarn.lock | 94 ++++++---------------------------------------------- 2 files changed, 12 insertions(+), 83 deletions(-) diff --git a/package.json b/package.json index 4919b3a7e07..fee4475140a 100644 --- a/package.json +++ b/package.json @@ -374,6 +374,7 @@ "webpack-cli": "5.1.4" }, "resolutions": { + "**/braces": "3.0.3", "**/async": "2.6.4", "**/zod": "3.22.3", "**/file-type": "16.5.4", diff --git a/yarn.lock b/yarn.lock index a0348585153..d9ef9da7484 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6331,11 +6331,6 @@ arr-diff@^4.0.0: resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" integrity sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA== -arr-flatten@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== - arr-union@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" @@ -7087,28 +7082,12 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" - integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== - dependencies: - arr-flatten "^1.1.0" - array-unique "^0.3.2" - extend-shallow "^2.0.1" - fill-range "^4.0.0" - isobject "^3.0.1" - repeat-element "^1.1.2" - snapdragon "^0.8.1" - snapdragon-node "^2.0.1" - split-string "^3.0.2" - to-regex "^3.0.1" - -braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== +braces@3.0.3, braces@^2.3.1, braces@^3.0.2, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" brorand@^1.0.1, brorand@^1.1.0: version "1.1.0" @@ -10242,7 +10221,7 @@ extend-shallow@^2.0.1: dependencies: is-extendable "^0.1.0" -extend-shallow@^3.0.0, extend-shallow@^3.0.2: +extend-shallow@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" integrity sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q== @@ -10452,20 +10431,10 @@ filing-cabinet@^3.0.1: tsconfig-paths "^3.10.1" typescript "^3.9.7" -fill-range@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" - integrity sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ== - dependencies: - extend-shallow "^2.0.1" - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range "^2.1.0" - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -13132,7 +13101,7 @@ kind-of@^2.0.1: dependencies: is-buffer "^1.0.2" -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: +kind-of@^3.0.2, kind-of@^3.0.3: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" integrity sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ== @@ -17187,16 +17156,6 @@ remove-accents@0.4.2: resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5" integrity sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA== -repeat-element@^1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9" - integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ== - -repeat-string@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== - request@^2.79.0: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" @@ -17962,22 +17921,6 @@ smitter@1.1.1: resolved "https://registry.yarnpkg.com/smitter/-/smitter-1.1.1.tgz#cade535ccd3b2cc8ad274a9fe9b02937f50a316f" integrity sha512-6AwxCy1VfHVBpCljZb/QCGUcRmZKL6s3o5NRjJfJKAQxtiC8GCJUpy1OFs3RcJinykoj/p7jIkPrM3Z3bYmgZg== -snapdragon-node@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" - integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== - dependencies: - define-property "^1.0.0" - isobject "^3.0.0" - snapdragon-util "^3.0.1" - -snapdragon-util@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" - integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== - dependencies: - kind-of "^3.2.0" - snapdragon@^0.8.1: version "0.8.2" resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" @@ -18116,13 +18059,6 @@ split-on-first@^1.0.0: resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== -split-string@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" - integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== - dependencies: - extend-shallow "^3.0.0" - split2@^4.0.0: version "4.2.0" resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" @@ -18835,14 +18771,6 @@ to-object-path@^0.3.0: dependencies: kind-of "^3.0.2" -to-regex-range@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" - integrity sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg== - dependencies: - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" From 7cdcc7ac985ab1a775353b017fdc507ac289190d Mon Sep 17 00:00:00 2001 From: Christian Baroni <7061887+christianbaroni@users.noreply.github.com> Date: Mon, 10 Jun 2024 23:15:05 -0400 Subject: [PATCH 12/13] Fix provider recursion bug, provider function types (#5829) --- .../screens/Swap/providers/swap-provider.tsx | 2 +- .../Swap/resources/assets/userAssets.ts | 24 +++--- src/handlers/web3.ts | 73 ++++++++++--------- src/hooks/useRefreshAccountData.ts | 11 +-- src/networks/arbitrum.ts | 4 +- src/networks/avalanche.ts | 4 +- src/networks/base.ts | 4 +- src/networks/blast.ts | 4 +- src/networks/bsc.ts | 4 +- src/networks/degen.ts | 4 +- src/networks/gnosis.ts | 4 +- src/networks/goerli.ts | 4 +- src/networks/index.ts | 6 +- src/networks/mainnet.ts | 4 +- src/networks/optimism.ts | 4 +- src/networks/polygon.ts | 6 +- src/networks/types.ts | 4 +- src/networks/zora.ts | 4 +- src/resources/assets/useSortedUserAssets.ts | 8 +- src/resources/assets/useUserAsset.ts | 8 +- src/resources/assets/useUserAssetCount.ts | 8 +- src/screens/NFTSingleOfferSheet/index.tsx | 4 +- src/screens/mints/MintSheet.tsx | 4 +- src/utils/ethereumUtils.ts | 9 +-- 24 files changed, 104 insertions(+), 107 deletions(-) diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index f67193d98d6..9ca5177abfc 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -174,7 +174,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { ? await getFlashbotsProvider() : getCachedProviderForNetwork(network); const providerUrl = provider?.connection?.url; - const connectedToHardhat = isHardHat(providerUrl); + const connectedToHardhat = !!providerUrl && isHardHat(providerUrl); const selectedGas = getSelectedGas(parameters.chainId); if (!selectedGas) { diff --git a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts index 93c0c458b56..024392c1969 100644 --- a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts +++ b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts @@ -1,20 +1,19 @@ import { useQuery } from '@tanstack/react-query'; import { Address } from 'viem'; import { ADDYS_API_KEY } from 'react-native-dotenv'; - import { QueryConfigWithSelect, QueryFunctionArgs, QueryFunctionResult, createQueryKey, queryClient } from '@/react-query'; + +import { getIsHardhatConnected } from '@/handlers/web3'; +import { RainbowError, logger } from '@/logger'; +import { RainbowFetchClient } from '@/rainbow-fetch'; import { SupportedCurrencyKey, SUPPORTED_CHAIN_IDS } from '@/references'; import { ParsedAssetsDictByChain, ZerionAsset } from '@/__swaps__/types/assets'; import { ChainId } from '@/__swaps__/types/chains'; import { AddressAssetsReceivedMessage } from '@/__swaps__/types/refraction'; import { filterAsset, parseUserAsset } from '@/__swaps__/utils/assets'; import { greaterThan } from '@/__swaps__/utils/numbers'; -import { RainbowError, logger } from '@/logger'; import { fetchUserAssetsByChain } from './userAssetsByChain'; -import { RainbowFetchClient } from '@/rainbow-fetch'; -import { useAccountSettings } from '@/hooks'; -import { getCachedProviderForNetwork, isHardHat } from '@/handlers/web3'; const addysHttp = new RainbowFetchClient({ baseURL: 'https://addys.p.rainbow.me/v3', @@ -31,27 +30,27 @@ export const USER_ASSETS_STALE_INTERVAL = 30000; // Query Types export type UserAssetsArgs = { - address?: Address; + address: Address; currency: SupportedCurrencyKey; testnetMode?: boolean; }; type SetUserAssetsArgs = { - address?: Address; + address: Address; currency: SupportedCurrencyKey; userAssets?: UserAssetsResult; testnetMode?: boolean; }; type SetUserDefaultsArgs = { - address?: Address; + address: Address; currency: SupportedCurrencyKey; staleTime: number; testnetMode?: boolean; }; type FetchUserAssetsArgs = { - address?: Address; + address: Address; currency: SupportedCurrencyKey; testnetMode?: boolean; }; @@ -211,12 +210,9 @@ export function useUserAssets( { address, currency }: UserAssetsArgs, config: QueryConfigWithSelect = {} ) { - const { network: currentNetwork } = useAccountSettings(); - const provider = getCachedProviderForNetwork(currentNetwork); - const providerUrl = provider?.connection?.url; - const connectedToHardhat = isHardHat(providerUrl); + const isHardhatConnected = getIsHardhatConnected(); - return useQuery(userAssetsQueryKey({ address, currency, testnetMode: connectedToHardhat }), userAssetsQueryFunction, { + return useQuery(userAssetsQueryKey({ address, currency, testnetMode: isHardhatConnected }), userAssetsQueryFunction, { ...config, refetchInterval: USER_ASSETS_REFETCH_INTERVAL, staleTime: process.env.IS_TESTING === 'true' ? 0 : 1000, diff --git a/src/handlers/web3.ts b/src/handlers/web3.ts index 5b05dcd8775..72283159f67 100644 --- a/src/handlers/web3.ts +++ b/src/handlers/web3.ts @@ -37,15 +37,14 @@ import { ethereumUtils } from '@/utils'; import { logger, RainbowError } from '@/logger'; import { IS_IOS, RPC_PROXY_API_KEY, RPC_PROXY_BASE_URL } from '@/env'; import { getNetworkObj } from '@/networks'; +import store from '@/redux/store'; export enum TokenStandard { ERC1155 = 'ERC1155', ERC721 = 'ERC721', } -export const networkProviders: { - [network in Network]?: StaticJsonRpcProvider; -} = {}; +export const networkProviders = new Map(); /** * Creates an rpc endpoint for a given chain id using the Rainbow rpc proxy. @@ -207,8 +206,8 @@ export const getFlashbotsProvider = async () => { ); }; -export const getCachedProviderForNetwork = (network: Network = Network.mainnet) => { - return networkProviders[network]!; +export const getCachedProviderForNetwork = (network: Network = Network.mainnet): StaticJsonRpcProvider | undefined => { + return networkProviders.get(network); }; /** @@ -217,27 +216,37 @@ export const getCachedProviderForNetwork = (network: Network = Network.mainnet) * @return The provider for the network. */ export const getProviderForNetwork = async (network: Network | string = Network.mainnet): Promise => { - if (isNetworkEnum(network) && networkProviders[network]) { - return networkProviders[network]!; + const isSupportedNetwork = isNetworkEnum(network); + const cachedProvider = isSupportedNetwork ? networkProviders.get(network) : undefined; + + if (isSupportedNetwork && cachedProvider) { + return cachedProvider; } - if (!isNetworkEnum(network)) { + if (!isSupportedNetwork) { const provider = new StaticJsonRpcProvider(network, Network.mainnet); - networkProviders[Network.mainnet] = provider; + networkProviders.set(Network.mainnet, provider); return provider; } else { - const chainId = getNetworkObj(network).id; - const provider = new StaticJsonRpcProvider(getNetworkObj(network).rpc, chainId); - if (!networkProviders[network]) { - networkProviders[network] = provider; - } - await provider.ready; + const provider = new StaticJsonRpcProvider(getNetworkObj(network).rpc(), getNetworkObj(network).id); + networkProviders.set(network, provider); return provider; } }; /** - * @desc Sends an arbitrary RCP call using a given provider, or the default + * @desc Checks if the active network is Hardhat. + * @returns boolean: `true` if connected to Hardhat. + */ +export const getIsHardhatConnected = (): boolean => { + const currentNetwork = store.getState().settings.network; + const currentProviderUrl = getCachedProviderForNetwork(currentNetwork)?.connection?.url; + const connectedToHardhat = !!currentProviderUrl && isHardHat(currentProviderUrl); + return connectedToHardhat; +}; + +/** + * @desc Sends an arbitrary RPC call using a given provider, or the default * cached provider. * @param payload The payload, including a method and parameters, based on * the Ethers.js `StaticJsonRpcProvider.send` arguments. @@ -248,10 +257,10 @@ export const getProviderForNetwork = async (network: Network | string = Network. export const sendRpcCall = async ( payload: { method: string; - params: any[]; + params: unknown[]; }, provider: StaticJsonRpcProvider | null = null -): Promise => (provider || web3Provider)?.send(payload.method, payload.params); +): Promise => (provider || web3Provider)?.send(payload.method, payload.params); /** * @desc check if hex string @@ -359,9 +368,9 @@ export const estimateGas = async ( export async function estimateGasWithPadding( txPayload: TransactionRequest, contractCallEstimateGas: Contract['estimateGas'][string] | null = null, - callArguments: any[] | null = null, + callArguments: unknown[] | null = null, provider: StaticJsonRpcProvider | null = null, - paddingFactor: number = 1.1 + paddingFactor = 1.1 ): Promise { try { const p = provider || web3Provider; @@ -383,16 +392,16 @@ export async function estimateGasWithPadding( const code = to ? await p.getCode(to) : undefined; // 2 - if it's not a contract AND it doesn't have any data use the default gas limit if ((!contractCallEstimateGas && !to) || (to && !data && (!code || code === '0x'))) { - logger.info('⛽ Skipping estimates, using default', { + logger.debug('⛽ Skipping estimates, using default', { ethUnits: ethUnits.basic_tx.toString(), }); return ethUnits.basic_tx.toString(); } - logger.info('⛽ Calculating safer gas limit for last block'); + logger.debug('⛽ Calculating safer gas limit for last block'); // 3 - If it is a contract, call the RPC method `estimateGas` with a safe value const saferGasLimit = fraction(gasLimit.toString(), 19, 20); - logger.info('⛽ safer gas limit for last block is', { saferGasLimit }); + logger.debug('⛽ safer gas limit for last block is', { saferGasLimit }); txPayloadToEstimate[contractCallEstimateGas ? 'gasLimit' : 'gas'] = toHex(saferGasLimit); @@ -404,7 +413,7 @@ export async function estimateGasWithPadding( const lastBlockGasLimit = addBuffer(gasLimit.toString(), 0.9); const paddedGas = addBuffer(estimatedGas.toString(), paddingFactor.toString()); - logger.info('⛽ GAS CALCULATIONS!', { + logger.debug('⛽ GAS CALCULATIONS!', { estimatedGas: estimatedGas.toString(), gasLimit: gasLimit.toString(), lastBlockGasLimit: lastBlockGasLimit, @@ -413,26 +422,24 @@ export async function estimateGasWithPadding( // If the safe estimation is above the last block gas limit, use it if (greaterThan(estimatedGas.toString(), lastBlockGasLimit)) { - logger.info('⛽ returning orginal gas estimation', { + logger.debug('⛽ returning orginal gas estimation', { esimatedGas: estimatedGas.toString(), }); return estimatedGas.toString(); } // If the estimation is below the last block gas limit, use the padded estimate if (greaterThan(lastBlockGasLimit, paddedGas)) { - logger.info('⛽ returning padded gas estimation', { paddedGas }); + logger.debug('⛽ returning padded gas estimation', { paddedGas }); return paddedGas; } // otherwise default to the last block gas limit - logger.info('⛽ returning last block gas limit', { lastBlockGasLimit }); + logger.debug('⛽ returning last block gas limit', { lastBlockGasLimit }); return lastBlockGasLimit; - } catch (e: any) { + } catch (e) { /* * Reported ~400x per day, but if it's not actionable it might as well be a warning. */ - logger.warn('Error calculating gas limit with padding', { - message: e.message, - }); + logger.warn('Error calculating gas limit with padding', { message: e instanceof Error ? e.message : 'Unknown error' }); return null; } } @@ -522,7 +529,7 @@ export const resolveUnstoppableDomain = async (domain: string): Promise { return address; }) - .catch((error: any) => { + .catch(error => { logger.error(new RainbowError(`resolveUnstoppableDomain error`), { message: error.message, }); @@ -779,7 +786,7 @@ export const estimateGasLimit = async ( recipient: string; amount: number; }, - addPadding: boolean = false, + addPadding = false, provider: StaticJsonRpcProvider | null = null, network: Network = Network.mainnet ): Promise => { diff --git a/src/hooks/useRefreshAccountData.ts b/src/hooks/useRefreshAccountData.ts index 090ef7614dd..4de563b7108 100644 --- a/src/hooks/useRefreshAccountData.ts +++ b/src/hooks/useRefreshAccountData.ts @@ -2,8 +2,7 @@ import { captureException } from '@sentry/react-native'; import delay from 'delay'; import { useCallback, useState } from 'react'; import { useDispatch } from 'react-redux'; -import { getCachedProviderForNetwork, isHardHat } from '@/handlers/web3'; -import NetworkTypes from '../helpers/networkTypes'; +import { getIsHardhatConnected } from '@/handlers/web3'; import { walletConnectLoadState } from '../redux/walletconnect'; import { fetchWalletENSAvatars, fetchWalletNames } from '../redux/wallets'; import useAccountSettings from './useAccountSettings'; @@ -16,14 +15,12 @@ import { positionsQueryKey } from '@/resources/defi/PositionsQuery'; export default function useRefreshAccountData() { const dispatch = useDispatch(); - const { accountAddress, network, nativeCurrency } = useAccountSettings(); + const { accountAddress, nativeCurrency } = useAccountSettings(); const [isRefreshing, setIsRefreshing] = useState(false); const profilesEnabled = useExperimentalFlag(PROFILES); const fetchAccountData = useCallback(async () => { - const provider = getCachedProviderForNetwork(network); - const providerUrl = provider?.connection?.url; - const connectedToHardhat = isHardHat(providerUrl); + const connectedToHardhat = getIsHardhatConnected(); queryClient.invalidateQueries({ queryKey: nftsQueryKey({ address: accountAddress }), @@ -57,7 +54,7 @@ export default function useRefreshAccountData() { captureException(error); throw error; } - }, [accountAddress, dispatch, nativeCurrency, network, profilesEnabled]); + }, [accountAddress, dispatch, nativeCurrency, profilesEnabled]); const refresh = useCallback(async () => { if (isRefreshing) return; diff --git a/src/networks/arbitrum.ts b/src/networks/arbitrum.ts index 8a9ae182fe6..72c95c4cad5 100644 --- a/src/networks/arbitrum.ts +++ b/src/networks/arbitrum.ts @@ -24,8 +24,8 @@ export const getArbitrumNetworkObject = (): NetworkProperties => { address: ARBITRUM_ETH_ADDRESS, }, - rpc: proxyRpcEndpoint(arbitrum.id), - getProvider: getProviderForNetwork(Network.arbitrum), + rpc: () => proxyRpcEndpoint(arbitrum.id), + getProvider: () => getProviderForNetwork(Network.arbitrum), balanceCheckerAddress: '0x54A4E5800345c01455a7798E0D96438364e22723', // features diff --git a/src/networks/avalanche.ts b/src/networks/avalanche.ts index 813fce77c90..ecb8b628d99 100644 --- a/src/networks/avalanche.ts +++ b/src/networks/avalanche.ts @@ -25,8 +25,8 @@ export const getAvalancheNetworkObject = (): NetworkProperties => { address: AVAX_AVALANCHE_ADDRESS, }, - rpc: proxyRpcEndpoint(avalanche.id), - getProvider: getProviderForNetwork(Network.avalanche), + rpc: () => proxyRpcEndpoint(avalanche.id), + getProvider: () => getProviderForNetwork(Network.avalanche), // need to find balance checker address balanceCheckerAddress: '', diff --git a/src/networks/base.ts b/src/networks/base.ts index f483b210019..ea2b6163d5a 100644 --- a/src/networks/base.ts +++ b/src/networks/base.ts @@ -25,8 +25,8 @@ export const getBaseNetworkObject = (): NetworkProperties => { address: BASE_ETH_ADDRESS, }, - rpc: proxyRpcEndpoint(base.id), - getProvider: getProviderForNetwork(Network.base), + rpc: () => proxyRpcEndpoint(base.id), + getProvider: () => getProviderForNetwork(Network.base), balanceCheckerAddress: '0x1C8cFdE3Ba6eFc4FF8Dd5C93044B9A690b6CFf36', // features diff --git a/src/networks/blast.ts b/src/networks/blast.ts index a0a7ba98d44..44c63c24fb3 100644 --- a/src/networks/blast.ts +++ b/src/networks/blast.ts @@ -30,8 +30,8 @@ export const getBlastNetworkObject = (): NetworkProperties => { }, balanceCheckerAddress: '', - rpc: proxyRpcEndpoint(BLAST_CHAIN_ID), - getProvider: getProviderForNetwork(Network.blast), + rpc: () => proxyRpcEndpoint(BLAST_CHAIN_ID), + getProvider: () => getProviderForNetwork(Network.blast), // features features: { diff --git a/src/networks/bsc.ts b/src/networks/bsc.ts index f69d37a88b3..3e38d897969 100644 --- a/src/networks/bsc.ts +++ b/src/networks/bsc.ts @@ -27,8 +27,8 @@ export const getBSCNetworkObject = (): NetworkProperties => { }, // this should be refactored to have less deps - rpc: proxyRpcEndpoint(bsc.id), - getProvider: getProviderForNetwork(Network.bsc), + rpc: () => proxyRpcEndpoint(bsc.id), + getProvider: () => getProviderForNetwork(Network.bsc), balanceCheckerAddress: '0x400A9f1Bb1Db80643C33710C2232A0D74EF5CFf1', // features diff --git a/src/networks/degen.ts b/src/networks/degen.ts index de95637f4c3..bc7c43aad2d 100644 --- a/src/networks/degen.ts +++ b/src/networks/degen.ts @@ -27,8 +27,8 @@ export const getDegenNetworkObject = (): NetworkProperties => { address: DEGEN_CHAIN_DEGEN_ADDRESS, }, - rpc: proxyRpcEndpoint(degen.id), - getProvider: getProviderForNetwork(Network.degen), + rpc: () => proxyRpcEndpoint(degen.id), + getProvider: () => getProviderForNetwork(Network.degen), // need to find balance checker address balanceCheckerAddress: '', diff --git a/src/networks/gnosis.ts b/src/networks/gnosis.ts index ad90dbac2d0..f6e18c742b7 100644 --- a/src/networks/gnosis.ts +++ b/src/networks/gnosis.ts @@ -23,8 +23,8 @@ export const getGnosisNetworkObject = (): NetworkProperties => { address: ETH_ADDRESS, }, - rpc: '', - getProvider: getProviderForNetwork(Network.optimism), + rpc: () => '', + getProvider: () => getProviderForNetwork(Network.optimism), balanceCheckerAddress: '', // features diff --git a/src/networks/goerli.ts b/src/networks/goerli.ts index 82e9c501ee5..2d9423a7ac2 100644 --- a/src/networks/goerli.ts +++ b/src/networks/goerli.ts @@ -25,8 +25,8 @@ export const getGoerliNetworkObject = (): NetworkProperties => { }, // this should be refactored to have less deps - getProvider: getProviderForNetwork(Network.goerli), - rpc: proxyRpcEndpoint(goerli.id), + getProvider: () => getProviderForNetwork(Network.goerli), + rpc: () => proxyRpcEndpoint(goerli.id), balanceCheckerAddress: '0xf3352813b612a2d198e437691557069316b84ebe', // features diff --git a/src/networks/index.ts b/src/networks/index.ts index 6cf68c4e728..949a70ef828 100644 --- a/src/networks/index.ts +++ b/src/networks/index.ts @@ -38,6 +38,10 @@ export const RainbowNetworks = [ */ export function getNetworkObj(network: Network): NetworkProperties { switch (network) { + // Mainnet + case Network.mainnet: + return getMainnetNetworkObject(); + // L2s case Network.arbitrum: return getArbitrumNetworkObject(); @@ -63,7 +67,7 @@ export function getNetworkObj(network: Network): NetworkProperties { case Network.goerli: return getGoerliNetworkObject(); - // Mainnet + // Fallback default: return getMainnetNetworkObject(); } diff --git a/src/networks/mainnet.ts b/src/networks/mainnet.ts index caeab7da391..2c3644511ec 100644 --- a/src/networks/mainnet.ts +++ b/src/networks/mainnet.ts @@ -25,8 +25,8 @@ export const getMainnetNetworkObject = (): NetworkProperties => { }, // this should be refactored to have less deps - getProvider: getProviderForNetwork(Network.mainnet), - rpc: proxyRpcEndpoint(mainnet.id), + getProvider: () => getProviderForNetwork(Network.mainnet), + rpc: () => proxyRpcEndpoint(mainnet.id), balanceCheckerAddress: '0x4dcf4562268dd384fe814c00fad239f06c2a0c2b', // features diff --git a/src/networks/optimism.ts b/src/networks/optimism.ts index ade896684be..b2d6ce8c8a6 100644 --- a/src/networks/optimism.ts +++ b/src/networks/optimism.ts @@ -25,8 +25,8 @@ export const getOptimismNetworkObject = (): NetworkProperties => { address: OPTIMISM_ETH_ADDRESS, }, - rpc: proxyRpcEndpoint(optimism.id), - getProvider: getProviderForNetwork(Network.optimism), + rpc: () => proxyRpcEndpoint(optimism.id), + getProvider: () => getProviderForNetwork(Network.optimism), balanceCheckerAddress: '0x1C8cFdE3Ba6eFc4FF8Dd5C93044B9A690b6CFf36', // features diff --git a/src/networks/polygon.ts b/src/networks/polygon.ts index 91650074209..7866a77a4c4 100644 --- a/src/networks/polygon.ts +++ b/src/networks/polygon.ts @@ -26,9 +26,9 @@ export const getPolygonNetworkObject = (): NetworkProperties => { mainnetAddress: MATIC_MAINNET_ADDRESS, }, - rpc: proxyRpcEndpoint(polygon.id), - getProvider: getProviderForNetwork(Network.polygon), - balanceCheckerAddress: '0x54A4E5800345c01455a7798E0D96438364e22723', + rpc: () => proxyRpcEndpoint(polygon.id), + getProvider: () => getProviderForNetwork(Network.polygon), + balanceCheckerAddress: '0x54A4E5800345c01455a77798E0D96438364e22723', // features features: { diff --git a/src/networks/types.ts b/src/networks/types.ts index 77501e0f459..028e83450b6 100644 --- a/src/networks/types.ts +++ b/src/networks/types.ts @@ -37,8 +37,8 @@ export interface NetworkProperties extends Chain { mainnetAddress?: string; }; - rpc: string; - getProvider: Promise; + rpc: () => string; + getProvider: () => Promise; balanceCheckerAddress: EthereumAddress; // feature flags diff --git a/src/networks/zora.ts b/src/networks/zora.ts index 2bde888fd7c..e8c5da58ffd 100644 --- a/src/networks/zora.ts +++ b/src/networks/zora.ts @@ -25,8 +25,8 @@ export const getZoraNetworkObject = (): NetworkProperties => { address: ZORA_ETH_ADDRESS, }, - rpc: proxyRpcEndpoint(zora.id), - getProvider: getProviderForNetwork(Network.zora), + rpc: () => proxyRpcEndpoint(zora.id), + getProvider: () => getProviderForNetwork(Network.zora), balanceCheckerAddress: '0x1C8cFdE3Ba6eFc4FF8Dd5C93044B9A690b6CFf36', // features diff --git a/src/resources/assets/useSortedUserAssets.ts b/src/resources/assets/useSortedUserAssets.ts index ab917fa760d..96cd2e91490 100644 --- a/src/resources/assets/useSortedUserAssets.ts +++ b/src/resources/assets/useSortedUserAssets.ts @@ -1,13 +1,11 @@ -import { getCachedProviderForNetwork, isHardHat } from '@/handlers/web3'; +import { getIsHardhatConnected } from '@/handlers/web3'; import { useAccountSettings } from '@/hooks'; import { selectSortedUserAssets } from '@/resources/assets/assetSelectors'; import { useUserAssets } from '@/resources/assets/UserAssetsQuery'; export function useSortedUserAssets() { - const { accountAddress, nativeCurrency, network: currentNetwork } = useAccountSettings(); - const provider = getCachedProviderForNetwork(currentNetwork); - const providerUrl = provider?.connection?.url; - const connectedToHardhat = isHardHat(providerUrl); + const { accountAddress, nativeCurrency } = useAccountSettings(); + const connectedToHardhat = getIsHardhatConnected(); return useUserAssets( { diff --git a/src/resources/assets/useUserAsset.ts b/src/resources/assets/useUserAsset.ts index c9777a9fa1f..96c5b7a6b34 100644 --- a/src/resources/assets/useUserAsset.ts +++ b/src/resources/assets/useUserAsset.ts @@ -1,13 +1,11 @@ -import { getCachedProviderForNetwork, isHardHat } from '@/handlers/web3'; +import { getIsHardhatConnected } from '@/handlers/web3'; import { useAccountSettings } from '@/hooks'; import { selectUserAssetWithUniqueId } from '@/resources/assets/assetSelectors'; import { useUserAssets } from '@/resources/assets/UserAssetsQuery'; export function useUserAsset(uniqueId: string) { - const { accountAddress, nativeCurrency, network: currentNetwork } = useAccountSettings(); - const provider = getCachedProviderForNetwork(currentNetwork); - const providerUrl = provider?.connection?.url; - const connectedToHardhat = isHardHat(providerUrl); + const { accountAddress, nativeCurrency } = useAccountSettings(); + const connectedToHardhat = getIsHardhatConnected(); return useUserAssets( { diff --git a/src/resources/assets/useUserAssetCount.ts b/src/resources/assets/useUserAssetCount.ts index 04785d280da..bd414a8c651 100644 --- a/src/resources/assets/useUserAssetCount.ts +++ b/src/resources/assets/useUserAssetCount.ts @@ -1,4 +1,4 @@ -import { getCachedProviderForNetwork, isHardHat } from '@/handlers/web3'; +import { getIsHardhatConnected } from '@/handlers/web3'; import { useAccountSettings } from '@/hooks'; import { useUserAssets } from '@/resources/assets/UserAssetsQuery'; import { RainbowAddressAssets } from './types'; @@ -6,10 +6,8 @@ import { RainbowAddressAssets } from './types'; const countSelector = (accountAssets: RainbowAddressAssets) => accountAssets?.length; export function useUserAssetCount() { - const { accountAddress, nativeCurrency, network: currentNetwork } = useAccountSettings(); - const provider = getCachedProviderForNetwork(currentNetwork); - const providerUrl = provider?.connection?.url; - const connectedToHardhat = isHardHat(providerUrl); + const { accountAddress, nativeCurrency } = useAccountSettings(); + const connectedToHardhat = getIsHardhatConnected(); return useUserAssets( { diff --git a/src/screens/NFTSingleOfferSheet/index.tsx b/src/screens/NFTSingleOfferSheet/index.tsx index 05cce58e4f8..26901f51b5e 100644 --- a/src/screens/NFTSingleOfferSheet/index.tsx +++ b/src/screens/NFTSingleOfferSheet/index.tsx @@ -185,7 +185,7 @@ export function NFTSingleOfferSheet() { // @ts-ignore account: accountAddress, chain: networkObj, - transport: http(networkObj.rpc), + transport: http(networkObj.rpc()), }); getClient()?.actions.acceptOffer({ items: [ @@ -285,7 +285,7 @@ export function NFTSingleOfferSheet() { const signer = createWalletClient({ account, chain: networkObj, - transport: http(networkObj.rpc), + transport: http(networkObj.rpc()), }); const nonce = await getNextNonce({ address: accountAddress, network }); try { diff --git a/src/screens/mints/MintSheet.tsx b/src/screens/mints/MintSheet.tsx index ab26b3e892e..1a429d370db 100644 --- a/src/screens/mints/MintSheet.tsx +++ b/src/screens/mints/MintSheet.tsx @@ -251,7 +251,7 @@ const MintSheet = () => { const signer = createWalletClient({ account: accountAddress, chain: networkObj, - transport: http(networkObj.rpc), + transport: http(networkObj.rpc()), }); try { await getClient()?.actions.mintToken({ @@ -360,7 +360,7 @@ const MintSheet = () => { const signer = createWalletClient({ account, chain: networkObj, - transport: http(networkObj.rpc), + transport: http(networkObj.rpc()), }); const feeAddress = getRainbowFeeAddress(currentNetwork); diff --git a/src/utils/ethereumUtils.ts b/src/utils/ethereumUtils.ts index 44e766a5b38..cb150b637f6 100644 --- a/src/utils/ethereumUtils.ts +++ b/src/utils/ethereumUtils.ts @@ -24,7 +24,7 @@ import { SelectedGasFee, } from '@/entities'; import { getOnchainAssetBalance } from '@/handlers/assets'; -import { getCachedProviderForNetwork, getProviderForNetwork, isHardHat, isTestnetNetwork, toHex } from '@/handlers/web3'; +import { getIsHardhatConnected, getProviderForNetwork, isTestnetNetwork, toHex } from '@/handlers/web3'; import { Network } from '@/helpers/networkTypes'; import { convertRawAmountToDecimalFormat, fromWei, greaterThan, isZero, subtract, add } from '@/helpers/utilities'; import { Navigation } from '@/navigation'; @@ -112,12 +112,11 @@ const getAsset = (accountAssets: Record, uniqueId: E }; const getUserAssetFromCache = (uniqueId: string) => { - const { accountAddress, nativeCurrency, network } = store.getState().settings; + const { accountAddress, nativeCurrency } = store.getState().settings; + const connectedToHardhat = getIsHardhatConnected(); const cache = queryClient.getQueryCache(); - const provider = getCachedProviderForNetwork(network); - const providerUrl = provider?.connection?.url; - const connectedToHardhat = isHardHat(providerUrl); + const cachedAddressAssets = (cache.find( userAssetsQueryKey({ address: accountAddress, From 2654e864e23fd8b62a5e2466e788e146241b4d81 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Tue, 11 Jun 2024 12:08:51 -0400 Subject: [PATCH 13/13] Fix weird issues with rounding (#5822) * addresses most of the issues with stablecoins and beyond 2nd order magnitude rounding * add test suite * rm console * fix precision rounding function * fix some safe math rounding on max amount * rm old function * fix lint * clean up numberFormatter * revert change to safemath * fix some flakiness * remove logs app wide * add more tests * Apply suggestions from code review * add constants and adjust fallbacks --- src/__swaps__/safe-math/SafeMath.ts | 1 - .../NiceIncrementerFormatter.test.ts | 98 +++++++++++++++ .../components/TokenList/TokenToSellList.tsx | 2 +- src/__swaps__/screens/Swap/constants.ts | 3 + .../Swap/hooks/useSwapInputsController.ts | 26 ++++ src/__swaps__/utils/swaps.ts | 118 ++++++++++++++---- src/hooks/useSwapDerivedOutputs.ts | 6 - src/raps/actions/swap.ts | 2 - 8 files changed, 220 insertions(+), 36 deletions(-) create mode 100644 src/__swaps__/screens/Swap/__tests__/NiceIncrementerFormatter.test.ts diff --git a/src/__swaps__/safe-math/SafeMath.ts b/src/__swaps__/safe-math/SafeMath.ts index af5062e562b..74c62502fdb 100644 --- a/src/__swaps__/safe-math/SafeMath.ts +++ b/src/__swaps__/safe-math/SafeMath.ts @@ -391,7 +391,6 @@ export function floorWorklet(num: string | number): string { export function roundWorklet(num: string | number): string { 'worklet'; const numStr = toStringWorklet(num); - if (!isNumberStringWorklet(numStr)) { throw new Error('Argument must be a numeric string or number'); } diff --git a/src/__swaps__/screens/Swap/__tests__/NiceIncrementerFormatter.test.ts b/src/__swaps__/screens/Swap/__tests__/NiceIncrementerFormatter.test.ts new file mode 100644 index 00000000000..490be57ca41 --- /dev/null +++ b/src/__swaps__/screens/Swap/__tests__/NiceIncrementerFormatter.test.ts @@ -0,0 +1,98 @@ +import { niceIncrementFormatter } from '@/__swaps__/utils/swaps'; +import { SLIDER_WIDTH } from '../constants'; + +type TestCase = { + incrementDecimalPlaces: number; + inputAssetBalance: number | string; + assetBalanceDisplay: string; + inputAssetUsdPrice: number; + niceIncrement: number | string; + percentageToSwap: number; + sliderXPosition: number; + stripSeparators?: boolean; + isStablecoin?: boolean; +} & { + testName: string; + expectedResult: string; +}; + +const TEST_CASES: TestCase[] = [ + { + incrementDecimalPlaces: 0, + inputAssetBalance: 45.47364224817269, + assetBalanceDisplay: '45.47364225', + inputAssetUsdPrice: 0.9995363790000001, + niceIncrement: '1', + percentageToSwap: 0.5, + sliderXPosition: SLIDER_WIDTH / 2, + stripSeparators: true, + isStablecoin: true, + testName: 'DAI Stablecoin', + expectedResult: '22.74', + }, + { + incrementDecimalPlaces: 2, + inputAssetBalance: 100, + assetBalanceDisplay: '100.00', + inputAssetUsdPrice: 10, + niceIncrement: '0.1', + percentageToSwap: 0, + sliderXPosition: 0, + stripSeparators: false, + isStablecoin: false, + testName: 'Zero percent swap', + expectedResult: '0.00', + }, + { + incrementDecimalPlaces: 2, + inputAssetBalance: 100, + assetBalanceDisplay: '100.00', + inputAssetUsdPrice: 10, + niceIncrement: '0.1', + percentageToSwap: 1, + sliderXPosition: SLIDER_WIDTH, + stripSeparators: false, + isStablecoin: false, + testName: 'Full swap', + expectedResult: '100.00', + }, + { + incrementDecimalPlaces: 2, + inputAssetBalance: 123.456, + assetBalanceDisplay: '123.46', + inputAssetUsdPrice: 1, + niceIncrement: '0.05', + percentageToSwap: 0.25, + sliderXPosition: SLIDER_WIDTH / 4, + stripSeparators: true, + isStablecoin: false, + testName: 'Quarter swap with fractional increment', + expectedResult: '30.86', + }, + { + incrementDecimalPlaces: 0, + inputAssetBalance: '1000', + assetBalanceDisplay: '1,000', + inputAssetUsdPrice: 0.5, + niceIncrement: '100', + percentageToSwap: 0.75, + sliderXPosition: (3 * SLIDER_WIDTH) / 4, + stripSeparators: false, + isStablecoin: false, + testName: 'Large increment test', + expectedResult: '750', + }, +]; + +describe('NiceIncrementFormatter', () => { + beforeAll(() => { + jest.mock('react-native-reanimated', () => require('react-native-reanimated/mock')); + }); + + TEST_CASES.forEach(({ testName, expectedResult, ...params }, index) => { + // eslint-disable-next-line jest/valid-title + test(testName || `test-${index}`, () => { + expect(niceIncrementFormatter({ ...params })).toBe(expectedResult); + }); + }); +}); diff --git a/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx b/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx index 811a989805e..9405fd12344 100644 --- a/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx +++ b/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx @@ -57,7 +57,7 @@ const TokenToSellListComponent = () => { ListEmptyComponent={} keyExtractor={uniqueId => uniqueId} renderItem={({ item: uniqueId }) => { - return handleSelectToken(asset)} output={false} uniqueId={uniqueId} />; + return handleSelectToken(asset)} output={false} uniqueId={uniqueId} />; }} /> diff --git a/src/__swaps__/screens/Swap/constants.ts b/src/__swaps__/screens/Swap/constants.ts index ba39be48e1a..5387d1cf794 100644 --- a/src/__swaps__/screens/Swap/constants.ts +++ b/src/__swaps__/screens/Swap/constants.ts @@ -44,6 +44,9 @@ export const ETH_COLOR_DARK_ACCENT = '#9CA4AD'; export const LONG_PRESS_DELAY_DURATION = 200; export const LONG_PRESS_REPEAT_DURATION = 69; + +export const STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS = 2; +export const MAXIMUM_SIGNIFICANT_DECIMALS = 6; // // /---- END constants ----/ // diff --git a/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts b/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts index cfb9daead9d..ad6750d1fa9 100644 --- a/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts +++ b/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts @@ -33,17 +33,21 @@ import { divWorklet, equalWorklet, greaterThanWorklet, mulWorklet, toFixedWorkle function getInitialInputValues(initialSelectedInputAsset: ExtendedAnimatedAssetWithColors | null) { const initialBalance = Number(initialSelectedInputAsset?.balance.amount) || 0; + const assetBalanceDisplay = initialSelectedInputAsset?.balance.display ?? ''; const initialNiceIncrement = findNiceIncrement(initialBalance); const initialDecimalPlaces = countDecimalPlaces(initialNiceIncrement); + const isStablecoin = initialSelectedInputAsset?.type === 'stablecoin'; const initialInputAmount = niceIncrementFormatter({ incrementDecimalPlaces: initialDecimalPlaces, inputAssetBalance: initialBalance, + assetBalanceDisplay, inputAssetUsdPrice: initialSelectedInputAsset?.price?.value ?? 0, niceIncrement: initialNiceIncrement, percentageToSwap: 0.5, sliderXPosition: SLIDER_WIDTH / 2, stripSeparators: true, + isStablecoin, }); const initialInputNativeValue = addCommasToNumber( @@ -119,10 +123,13 @@ export function useSwapInputsController({ return addCommasToNumber(inputValues.value.inputAmount, '0'); } + const assetBalanceDisplay = internalSelectedInputAsset.value.balance.display ?? ''; + if (inputMethod.value === 'outputAmount') { return valueBasedDecimalFormatter({ amount: inputValues.value.inputAmount, usdTokenPrice: inputNativePrice.value, + assetBalanceDisplay, roundingMode: 'up', precisionAdjustment: -1, isStablecoin: internalSelectedInputAsset.value?.type === 'stablecoin' ?? false, @@ -131,14 +138,17 @@ export function useSwapInputsController({ } const balance = internalSelectedInputAsset.value?.balance.amount || 0; + const isStablecoin = internalSelectedInputAsset.value?.type === 'stablecoin' ?? false; return niceIncrementFormatter({ incrementDecimalPlaces: incrementDecimalPlaces.value, inputAssetBalance: balance, + assetBalanceDisplay, inputAssetUsdPrice: inputNativePrice.value, niceIncrement: niceIncrement.value, percentageToSwap: percentageToSwap.value, sliderXPosition: sliderXPosition.value, + isStablecoin, }); }); @@ -166,9 +176,13 @@ export function useSwapInputsController({ if (inputMethod.value === 'outputAmount' || typeof inputValues.value.outputAmount === 'string') { return addCommasToNumber(inputValues.value.outputAmount, '0'); } + + const assetBalanceDisplay = internalSelectedOutputAsset.value.balance.display ?? ''; + return valueBasedDecimalFormatter({ amount: inputValues.value.outputAmount, usdTokenPrice: outputNativePrice.value, + assetBalanceDisplay, roundingMode: 'down', precisionAdjustment: -1, isStablecoin: internalSelectedOutputAsset.value?.type === 'stablecoin' ?? false, @@ -674,6 +688,7 @@ export function useSwapInputsController({ if (!internalSelectedInputAsset.value) return; const balance = Number(internalSelectedInputAsset.value.balance.amount); + if (!balance) { inputValues.modify(values => { return { @@ -687,14 +702,18 @@ export function useSwapInputsController({ return; } + const assetBalanceDisplay = internalSelectedInputAsset.value.balance.display; + const inputAmount = niceIncrementFormatter({ incrementDecimalPlaces: incrementDecimalPlaces.value, inputAssetBalance: balance, + assetBalanceDisplay, inputAssetUsdPrice: inputNativePrice.value, niceIncrement: niceIncrement.value, percentageToSwap: percentageToSwap.value, sliderXPosition: sliderXPosition.value, stripSeparators: true, + isStablecoin: internalSelectedInputAsset.value?.type === 'stablecoin' ?? false, }); const inputNativeValue = mulWorklet(inputAmount, inputNativePrice.value); inputValues.modify(values => { @@ -844,14 +863,18 @@ export function useSwapInputsController({ return; } + const assetBalanceDisplay = internalSelectedInputAsset.value?.balance.display ?? ''; + const inputAmount = niceIncrementFormatter({ incrementDecimalPlaces: incrementDecimalPlaces.value, inputAssetBalance: balance, + assetBalanceDisplay, inputAssetUsdPrice: inputNativePrice.value, niceIncrement: niceIncrement.value, percentageToSwap: percentageToSwap.value, sliderXPosition: sliderXPosition.value, stripSeparators: true, + isStablecoin: internalSelectedInputAsset.value?.type === 'stablecoin' ?? false, }); const inputNativeValue = mulWorklet(inputAmount, inputNativePrice.value); @@ -869,11 +892,14 @@ export function useSwapInputsController({ const inputNativePrice = internalSelectedInputAsset.value?.nativePrice || internalSelectedInputAsset.value?.price?.value || 0; const outputNativePrice = internalSelectedOutputAsset.value?.nativePrice || internalSelectedOutputAsset.value?.price?.value || 0; + const assetBalanceDisplay = current.assetToSell?.balance.display ?? ''; + const inputAmount = Number( valueBasedDecimalFormatter({ amount: inputNativePrice > 0 ? divWorklet(inputValues.value.inputNativeValue, inputNativePrice) : inputValues.value.outputAmount, usdTokenPrice: inputNativePrice, + assetBalanceDisplay, roundingMode: 'up', precisionAdjustment: -1, isStablecoin: current.assetToSell?.type === 'stablecoin' ?? false, diff --git a/src/__swaps__/utils/swaps.ts b/src/__swaps__/utils/swaps.ts index 501c15fd7a1..3186a369bb5 100644 --- a/src/__swaps__/utils/swaps.ts +++ b/src/__swaps__/utils/swaps.ts @@ -3,7 +3,14 @@ import { SharedValue, convertToRGBA, isColor } from 'react-native-reanimated'; import * as i18n from '@/languages'; import { globalColors } from '@/design-system'; -import { ETH_COLOR, ETH_COLOR_DARK, SCRUBBER_WIDTH, SLIDER_WIDTH } from '@/__swaps__/screens/Swap/constants'; +import { + ETH_COLOR, + ETH_COLOR_DARK, + MAXIMUM_SIGNIFICANT_DECIMALS, + SCRUBBER_WIDTH, + SLIDER_WIDTH, + STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS, +} from '@/__swaps__/screens/Swap/constants'; import { chainNameFromChainId, chainNameFromChainIdWorklet } from '@/__swaps__/utils/chains'; import { ChainId, ChainName } from '@/__swaps__/types/chains'; import { RainbowConfig } from '@/model/remoteConfig'; @@ -221,41 +228,54 @@ export function trimTrailingZeros(value: string) { return withTrimmedZeros.endsWith('.') ? withTrimmedZeros.slice(0, -1) : withTrimmedZeros; } +export function precisionBasedOffMagnitude(amount: number | string, isStablecoin = false): number { + 'worklet'; + + const magnitude = -Number(floorWorklet(sumWorklet(log10Worklet(amount), 0))); + // don't let stablecoins go beneath 2nd order + if (magnitude < -2 && isStablecoin) { + return -STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS; + } + return magnitude; +} + export function valueBasedDecimalFormatter({ amount, usdTokenPrice, + assetBalanceDisplay, roundingMode, precisionAdjustment, isStablecoin, stripSeparators = true, + isMaxAmount = false, }: { amount: number | string; usdTokenPrice: number; + assetBalanceDisplay?: string; roundingMode?: 'up' | 'down'; precisionAdjustment?: number; isStablecoin?: boolean; stripSeparators?: boolean; + isMaxAmount?: boolean; }): string { 'worklet'; - function precisionBasedOffMagnitude(amount: number | string): number { - const magnitude = -Number(floorWorklet(sumWorklet(log10Worklet(amount), 1))); - return (precisionAdjustment ?? 0) + magnitude; - } - function calculateDecimalPlaces(usdTokenPrice: number): number { - const fallbackDecimalPlaces = 2; + const fallbackDecimalPlaces = STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS; if (usdTokenPrice <= 0) { return fallbackDecimalPlaces; } const unitsForOneCent = 0.01 / usdTokenPrice; if (unitsForOneCent >= 1) { - return 0; + return isStablecoin ? STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS : 0; } - return Math.max(Math.ceil(Math.log10(1 / unitsForOneCent)) + precisionBasedOffMagnitude(amount), 0); + return Math.max( + Math.ceil(Math.log10(1 / unitsForOneCent)) + (precisionAdjustment ?? 0), + isStablecoin ? STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS : 0 + ); } - const decimalPlaces = isStablecoin ? 2 : calculateDecimalPlaces(usdTokenPrice); + const decimalPlaces = calculateDecimalPlaces(usdTokenPrice); let roundedAmount; const factor = Math.pow(10, decimalPlaces); @@ -270,10 +290,35 @@ export function valueBasedDecimalFormatter({ roundedAmount = divWorklet(roundWorklet(mulWorklet(amount, factor)), factor); } + const maximumFractionDigits = () => { + // if we're selling max amount, we want to match what's displayed on the balance badge + // let's base the decimal places based on that (capped at 6) + if (isMaxAmount && assetBalanceDisplay) { + const decimals = assetBalanceDisplay.split('.'); + if (decimals.length > 1) { + const [, decimalPlacesFromDisplay] = decimals; + if (decimalPlacesFromDisplay.length < STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS && isStablecoin) { + return STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS; + } + + return Math.min(decimalPlacesFromDisplay.length, MAXIMUM_SIGNIFICANT_DECIMALS); + } + } + + if (!isNaN(decimalPlaces)) { + return isStablecoin && decimalPlaces < STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS + ? STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS + : decimalPlaces; + } + + // default to 6 precision if we have no calculation + return MAXIMUM_SIGNIFICANT_DECIMALS; + }; + // Format the number to add separators and trim trailing zeros const numberFormatter = new Intl.NumberFormat('en-US', { - minimumFractionDigits: 0, - maximumFractionDigits: !isNaN(decimalPlaces) ? decimalPlaces : 2, // Allow up to the required precision + minimumFractionDigits: isStablecoin ? STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS : 0, + maximumFractionDigits: maximumFractionDigits(), useGrouping: true, }); @@ -285,50 +330,71 @@ export function valueBasedDecimalFormatter({ export function niceIncrementFormatter({ incrementDecimalPlaces, inputAssetBalance, + assetBalanceDisplay, inputAssetUsdPrice, niceIncrement, percentageToSwap, sliderXPosition, stripSeparators, + isStablecoin = false, }: { incrementDecimalPlaces: number; inputAssetBalance: number | string; + assetBalanceDisplay: string; inputAssetUsdPrice: number; niceIncrement: number | string; percentageToSwap: number; sliderXPosition: number; stripSeparators?: boolean; + isStablecoin?: boolean; }) { 'worklet'; + if (percentageToSwap === 0) return '0'; - if (percentageToSwap === 0.25) + if (percentageToSwap === 0.25) { + const amount = mulWorklet(inputAssetBalance, 0.25); return valueBasedDecimalFormatter({ - amount: mulWorklet(inputAssetBalance, 0.25), + amount, usdTokenPrice: inputAssetUsdPrice, + assetBalanceDisplay, roundingMode: 'up', - precisionAdjustment: -3, + precisionAdjustment: precisionBasedOffMagnitude(amount, isStablecoin), + isStablecoin, }); - if (percentageToSwap === 0.5) + } + if (percentageToSwap === 0.5) { + const amount = mulWorklet(inputAssetBalance, 0.5); return valueBasedDecimalFormatter({ - amount: mulWorklet(inputAssetBalance, 0.5), + amount, usdTokenPrice: inputAssetUsdPrice, + assetBalanceDisplay, roundingMode: 'up', - precisionAdjustment: -3, + precisionAdjustment: precisionBasedOffMagnitude(amount, isStablecoin), + isStablecoin, }); - if (percentageToSwap === 0.75) + } + if (percentageToSwap === 0.75) { + const amount = mulWorklet(inputAssetBalance, 0.75); return valueBasedDecimalFormatter({ - amount: mulWorklet(inputAssetBalance, 0.75), + amount, usdTokenPrice: inputAssetUsdPrice, + assetBalanceDisplay, roundingMode: 'up', - precisionAdjustment: -3, + precisionAdjustment: precisionBasedOffMagnitude(amount, isStablecoin), + isStablecoin, }); - if (percentageToSwap === 1) + } + if (percentageToSwap === 1) { return valueBasedDecimalFormatter({ amount: inputAssetBalance, usdTokenPrice: inputAssetUsdPrice, - roundingMode: 'up', + assetBalanceDisplay, + isStablecoin, + isMaxAmount: true, }); + } + const decimals = isStablecoin ? STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS : incrementDecimalPlaces; const exactIncrement = divWorklet(inputAssetBalance, 100); const isIncrementExact = equalWorklet(niceIncrement, exactIncrement); const numberOfIncrements = divWorklet(inputAssetBalance, niceIncrement); @@ -344,12 +410,12 @@ export function niceIncrementFormatter({ const rawAmount = mulWorklet(roundWorklet(divWorklet(mulWorklet(percentage, inputAssetBalance), niceIncrement)), niceIncrement); - const amountToFixedDecimals = toFixedWorklet(rawAmount, incrementDecimalPlaces); + const amountToFixedDecimals = toFixedWorklet(rawAmount, decimals); const formattedAmount = `${Number(amountToFixedDecimals).toLocaleString('en-US', { useGrouping: true, - minimumFractionDigits: 0, - maximumFractionDigits: 8, + minimumFractionDigits: isStablecoin ? STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS : 0, + maximumFractionDigits: MAXIMUM_SIGNIFICANT_DECIMALS, })}`; if (stripSeparators) return stripCommas(formattedAmount); diff --git a/src/hooks/useSwapDerivedOutputs.ts b/src/hooks/useSwapDerivedOutputs.ts index f175946fd4c..84d6abb7ee4 100644 --- a/src/hooks/useSwapDerivedOutputs.ts +++ b/src/hooks/useSwapDerivedOutputs.ts @@ -111,9 +111,7 @@ const getInputAmount = async ( // Do not deleeeet the comment below 😤 // @ts-ignore About to get quote - console.log(JSON.stringify(quoteParams, null, 2)); const quote = await getQuote(quoteParams); - console.log(JSON.stringify(quote, null, 2)); // if no quote, if quote is error or there's no sell amount if (!quote || (quote as QuoteError).error || !(quote as Quote).sellAmount) { @@ -204,8 +202,6 @@ const getOutputAmount = async ( refuel, }; - console.log(JSON.stringify(quoteParams, null, 2)); - const rand = Math.floor(Math.random() * 100); logger.debug('[getOutputAmount]: Getting quote', { rand, quoteParams }); // Do not deleeeet the comment below 😤 @@ -213,8 +209,6 @@ const getOutputAmount = async ( const quote: Quote | CrosschainQuote | QuoteError | null = await (isCrosschainSwap ? getCrosschainQuote : getQuote)(quoteParams); logger.debug('[getOutputAmount]: Got quote', { rand, quote }); - console.log(JSON.stringify(quote, null, 2)); - if (!quote || (quote as QuoteError)?.error || !(quote as Quote)?.buyAmount) { const quoteError = quote as QuoteError; if (quoteError.error) { diff --git a/src/raps/actions/swap.ts b/src/raps/actions/swap.ts index 50d65999d43..bd1c3af9ef0 100644 --- a/src/raps/actions/swap.ts +++ b/src/raps/actions/swap.ts @@ -374,8 +374,6 @@ export const swap = async ({ swapMetadataStorage.set(swap.hash.toLowerCase(), JSON.stringify({ type: 'swap', data: parameters.meta })); } - console.log(JSON.stringify(transaction, null, 2)); - addNewTransaction({ address: parameters.quote.from as Address, // chainId: parameters.chainId as ChainId,