From 3c3ab49cc1f8bc0d576514defce80e8cb25bdd1d Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Wed, 28 Aug 2024 17:29:34 -0400 Subject: [PATCH] Revert "Transaction Simulation Cleanup (#5977)" (#6057) This reverts commit 254e06db205c247d7472ba38278142de81c3d52e. --- globals.d.ts | 1 - ios/Podfile.lock | 12 - package.json | 1 - src/App.tsx | 167 +- src/components/AppStateChangeHandler.tsx | 38 - src/components/DeeplinkHandler.tsx | 65 - src/components/FadeGradient.tsx | 47 - src/components/FadedScrollCard.tsx | 281 --- .../MobileWalletProtocolListener.tsx | 51 - .../Transactions/TransactionDetailsCard.tsx | 127 -- .../Transactions/TransactionDetailsRow.tsx | 90 - .../Transactions/TransactionIcons.tsx | 196 -- .../Transactions/TransactionMessageCard.tsx | 113 - .../TransactionSimulatedEventRow.tsx | 109 - .../TransactionSimulationCard.tsx | 315 --- src/components/Transactions/constants.ts | 121 -- src/components/Transactions/types.ts | 18 - src/handlers/deeplinks.ts | 19 +- src/hooks/useApplicationSetup.ts | 55 - src/hooks/useCalculateGasLimit.ts | 74 - src/hooks/useConfirmTransaction.ts | 27 - src/hooks/useHasEnoughBalance.ts | 47 - src/hooks/useNonceForDisplay.ts | 32 - src/hooks/useProviderSetup.ts | 50 - src/hooks/useSubmitTransaction.ts | 55 - src/hooks/useTransactionSetup.ts | 76 - src/languages/en_US.json | 1 - src/navigation/Routes.android.tsx | 29 +- src/navigation/Routes.ios.tsx | 29 +- src/navigation/config.tsx | 3 +- src/notifications/tokens.ts | 2 +- src/parsers/requests.js | 6 +- src/redux/requests.ts | 6 +- src/redux/walletconnect.ts | 4 +- .../transactions/transactionSimulation.ts | 148 -- src/screens/SignTransactionSheet.tsx | 1920 +++++++++++++++-- src/screens/WalletScreen/index.tsx | 6 - src/state/performance/operations.ts | 1 - src/storage/index.ts | 51 +- src/utils/formatDate.ts | 28 - src/utils/requestNavigationHandlers.ts | 274 +-- src/walletConnect/index.tsx | 2 +- tsconfig.json | 1 - yarn.lock | 34 - 44 files changed, 1901 insertions(+), 2831 deletions(-) delete mode 100644 src/components/AppStateChangeHandler.tsx delete mode 100644 src/components/DeeplinkHandler.tsx delete mode 100644 src/components/FadeGradient.tsx delete mode 100644 src/components/FadedScrollCard.tsx delete mode 100644 src/components/MobileWalletProtocolListener.tsx delete mode 100644 src/components/Transactions/TransactionDetailsCard.tsx delete mode 100644 src/components/Transactions/TransactionDetailsRow.tsx delete mode 100644 src/components/Transactions/TransactionIcons.tsx delete mode 100644 src/components/Transactions/TransactionMessageCard.tsx delete mode 100644 src/components/Transactions/TransactionSimulatedEventRow.tsx delete mode 100644 src/components/Transactions/TransactionSimulationCard.tsx delete mode 100644 src/components/Transactions/constants.ts delete mode 100644 src/components/Transactions/types.ts delete mode 100644 src/hooks/useApplicationSetup.ts delete mode 100644 src/hooks/useCalculateGasLimit.ts delete mode 100644 src/hooks/useConfirmTransaction.ts delete mode 100644 src/hooks/useHasEnoughBalance.ts delete mode 100644 src/hooks/useNonceForDisplay.ts delete mode 100644 src/hooks/useProviderSetup.ts delete mode 100644 src/hooks/useSubmitTransaction.ts delete mode 100644 src/hooks/useTransactionSetup.ts delete mode 100644 src/resources/transactions/transactionSimulation.ts delete mode 100644 src/utils/formatDate.ts diff --git a/globals.d.ts b/globals.d.ts index 068c5a86e74..29d1107dba2 100644 --- a/globals.d.ts +++ b/globals.d.ts @@ -109,5 +109,4 @@ declare module 'react-native-dotenv' { export const REACT_NATIVE_RUDDERSTACK_WRITE_KEY: string; export const RUDDERSTACK_DATA_PLANE_URL: string; export const SILENCE_EMOJI_WARNINGS: boolean; - export const MWP_ENCRYPTION_KEY: string; } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f28ca0bae70..1c400b0f1c1 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -4,9 +4,6 @@ PODS: - BVLinearGradient (2.8.3): - React-Core - CocoaAsyncSocket (7.6.5) - - CoinbaseWalletSDK/Client (1.1.0) - - CoinbaseWalletSDK/Host (1.1.0): - - CoinbaseWalletSDK/Client - DoubleConversion (1.1.6) - FasterImage (1.6.2): - FasterImage/Nuke (= 1.6.2) @@ -174,9 +171,6 @@ PODS: - MMKV (1.3.9): - MMKVCore (~> 1.3.9) - MMKVCore (1.3.9) - - mobile-wallet-protocol-host (0.1.7): - - CoinbaseWalletSDK/Host - - React-Core - MultiplatformBleAdapter (0.1.9) - nanopb (2.30910.0): - nanopb/decode (= 2.30910.0) @@ -1832,7 +1826,6 @@ DEPENDENCIES: - GoogleUtilities - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - libwebp - - "mobile-wallet-protocol-host (from `../node_modules/@coinbase/mobile-wallet-protocol-host`)" - nanopb - PanModal (from `https://github.com/rainbow-me/PanModal`, commit `ab97d74279ba28c2891b47a5dc767ed4dd7cf994`) - Permission-Camera (from `../node_modules/react-native-permissions/ios/Camera`) @@ -1963,7 +1956,6 @@ SPEC REPOS: https://github.com/CocoaPods/Specs.git: - Branch - CocoaAsyncSocket - - CoinbaseWalletSDK - Firebase - FirebaseABTesting - FirebaseAnalytics @@ -2015,8 +2007,6 @@ EXTERNAL SOURCES: hermes-engine: :podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" :tag: hermes-2024-06-28-RNv0.74.3-7bda0c267e76d11b68a585f84cfdd65000babf85 - mobile-wallet-protocol-host: - :path: "../node_modules/@coinbase/mobile-wallet-protocol-host" PanModal: :commit: ab97d74279ba28c2891b47a5dc767ed4dd7cf994 :git: https://github.com/rainbow-me/PanModal @@ -2267,7 +2257,6 @@ SPEC CHECKSUMS: Branch: d99436c6f3d5b2529ba948d273e47e732830f207 BVLinearGradient: 880f91a7854faff2df62518f0281afb1c60d49a3 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 - CoinbaseWalletSDK: bd6aa4f5a6460d4279e09e115969868e134126fb DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5 FasterImage: af05a76f042ca3654c962b658fdb01cb4d31caee FBLazyVector: 7e977dd099937dc5458851233141583abba49ff2 @@ -2293,7 +2282,6 @@ SPEC CHECKSUMS: MetricsReporter: 99596ee5003c69949ed2f50acc34aee83c42f843 MMKV: 817ba1eea17421547e01e087285606eb270a8dcb MMKVCore: af055b00e27d88cd92fad301c5fecd1ff9b26dd9 - mobile-wallet-protocol-host: 8ed897dcf4f846d39b35767540e6a695631cab73 MultiplatformBleAdapter: 5a6a897b006764392f9cef785e4360f54fb9477d nanopb: 438bc412db1928dac798aa6fd75726007be04262 PanModal: 421fe72d4af5b7e9016aaa3b4db94a2fb71756d3 diff --git a/package.json b/package.json index 7ff3845f425..533313474f1 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,6 @@ "@bradgarropy/use-countdown": "1.4.1", "@candlefinance/faster-image": "1.6.2", "@capsizecss/core": "3.0.0", - "@coinbase/mobile-wallet-protocol-host": "0.1.7", "@ensdomains/address-encoder": "0.2.16", "@ensdomains/content-hash": "2.5.7", "@ensdomains/eth-ens-namehash": "2.0.15", diff --git a/src/App.tsx b/src/App.tsx index 128324099b8..02c836a846e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,9 @@ import './languages'; import * as Sentry from '@sentry/react-native'; -import React, { lazy, Suspense, useCallback, useEffect, useState } from 'react'; -import { AppRegistry, Dimensions, LogBox, StyleSheet, View } from 'react-native'; -import { MobileWalletProtocolProvider } from '@coinbase/mobile-wallet-protocol-host'; -import { DeeplinkHandler } from '@/components/DeeplinkHandler'; -import { AppStateChangeHandler } from '@/components/AppStateChangeHandler'; -import { useApplicationSetup } from '@/hooks/useApplicationSetup'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { AppRegistry, AppState, AppStateStatus, Dimensions, InteractionManager, Linking, LogBox, View } from 'react-native'; +import branch from 'react-native-branch'; + import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { enableScreens } from 'react-native-screens'; @@ -17,16 +15,26 @@ import { OfflineToast } from './components/toasts'; import { designSystemPlaygroundEnabled, reactNativeDisableYellowBox, showNetworkRequests, showNetworkResponses } from './config/debug'; import monitorNetwork from './debugging/network'; import { Playground } from './design-system/playground/Playground'; +import handleDeeplink from './handlers/deeplinks'; +import { runWalletBackupStatusChecks } from './handlers/walletReadyEvents'; import RainbowContextWrapper from './helpers/RainbowContext'; +import isTestFlight from './helpers/isTestFlight'; import * as keychain from '@/model/keychain'; +import { loadAddress } from './model/wallet'; import { Navigation } from './navigation'; import RoutesComponent from './navigation/Routes'; +import { PerformanceContextMap } from './performance/PerformanceContextMap'; +import { PerformanceTracking } from './performance/tracking'; +import { PerformanceMetrics } from './performance/tracking/types/PerformanceMetrics'; import { PersistQueryClientProvider, persistOptions, queryClient } from './react-query'; import store from './redux/store'; +import { walletConnectLoadState } from './redux/walletconnect'; import { MainThemeProvider } from './theme/ThemeContext'; +import { branchListener } from './utils/branch'; import { addressKey } from './utils/keychainConstants'; import { SharedValuesProvider } from '@/helpers/SharedValuesContext'; import { InitialRoute, InitialRouteContext } from '@/navigation/initialRoute'; +import Routes from '@/navigation/routesNames'; import { Portal } from '@/react-native-cool-modals/Portal'; import { NotificationsHandler } from '@/notifications/NotificationsHandler'; import { analyticsV2 } from '@/analytics'; @@ -34,17 +42,19 @@ import { getOrCreateDeviceId, securelyHashWalletAddress } from '@/analytics/util import { logger, RainbowError } from '@/logger'; import * as ls from '@/storage'; import { migrate } from '@/migrations'; +import { initListeners as initWalletConnectListeners } from '@/walletConnect'; +import { saveFCMToken } from '@/notifications/tokens'; import { initializeReservoirClient } from '@/resources/reservoir/client'; import { ReviewPromptAction } from '@/storage/schema'; +import { handleReviewPromptAction } from '@/utils/reviewAlert'; import { initializeRemoteConfig } from '@/model/remoteConfig'; import { NavigationContainerRef } from '@react-navigation/native'; import { RootStackParamList } from './navigation/types'; import { Address } from 'viem'; import { IS_DEV } from './env'; +import { checkIdentifierOnLaunch } from './model/backup'; import { prefetchDefaultFavorites } from './resources/favorites'; -const LazyRoutesComponent = lazy(() => import('./navigation/Routes')); - if (IS_DEV) { reactNativeDisableYellowBox && LogBox.ignoreAllLogs(); (showNetworkRequests || showNetworkResponses) && monitorNetwork(showNetworkRequests, showNetworkResponses); @@ -52,40 +62,123 @@ if (IS_DEV) { enableScreens(); -const sx = StyleSheet.create({ - container: { - flex: 1, - }, -}); +const containerStyle = { flex: 1 }; interface AppProps { walletReady: boolean; } function App({ walletReady }: AppProps) { - const { initialRoute } = useApplicationSetup(); + const [appState, setAppState] = useState(AppState.currentState); + const [initialRoute, setInitialRoute] = useState(null); + const eventSubscription = useRef | null>(null); + const branchListenerRef = useRef | null>(null); + const navigatorRef = useRef | null>(null); + + const setupDeeplinking = useCallback(async () => { + const initialUrl = await Linking.getInitialURL(); + + branchListenerRef.current = await branchListener(url => { + logger.debug(`[App]: Branch: listener called`, {}, logger.DebugContext.deeplinks); + try { + handleDeeplink(url, initialRoute); + } catch (error) { + if (error instanceof Error) { + logger.error(new RainbowError(`[App]: Error opening deeplink`), { + message: error.message, + url, + }); + } else { + logger.error(new RainbowError(`[App]: Error opening deeplink`), { + message: 'Unknown error', + url, + }); + } + } + }); + + if (initialUrl) { + logger.debug(`[App]: has initial URL, opening with Branch`, { initialUrl }); + branch.openURL(initialUrl); + } + }, [initialRoute]); + + const identifyFlow = useCallback(async () => { + const address = await loadAddress(); + if (address) { + setTimeout(() => { + InteractionManager.runAfterInteractions(() => { + handleReviewPromptAction(ReviewPromptAction.TimesLaunchedSinceInstall); + }); + }, 10_000); + + InteractionManager.runAfterInteractions(checkIdentifierOnLaunch); + } + + setInitialRoute(address ? Routes.SWIPE_LAYOUT : Routes.WELCOME_SCREEN); + PerformanceContextMap.set('initialRoute', address ? Routes.SWIPE_LAYOUT : Routes.WELCOME_SCREEN); + }, []); + + const handleAppStateChange = useCallback( + (nextAppState: AppStateStatus) => { + if (appState === 'background' && nextAppState === 'active') { + store.dispatch(walletConnectLoadState()); + } + setAppState(nextAppState); + analyticsV2.track(analyticsV2.event.appStateChange, { + category: 'app state', + label: nextAppState, + }); + }, + [appState] + ); const handleNavigatorRef = useCallback((ref: NavigationContainerRef) => { + navigatorRef.current = ref; Navigation.setTopLevelNavigator(ref); }, []); + useEffect(() => { + if (!__DEV__ && isTestFlight) { + logger.debug(`[App]: Test flight usage - ${isTestFlight}`); + } + identifyFlow(); + eventSubscription.current = AppState.addEventListener('change', handleAppStateChange); + + const p1 = analyticsV2.initializeRudderstack(); + const p2 = setupDeeplinking(); + const p3 = saveFCMToken(); + Promise.all([p1, p2, p3]).then(() => { + initWalletConnectListeners(); + PerformanceTracking.finishMeasuring(PerformanceMetrics.loadRootAppComponent); + analyticsV2.track(analyticsV2.event.applicationDidMount); + }); + + return () => { + eventSubscription.current?.remove(); + branchListenerRef.current?.(); + }; + }, []); + + useEffect(() => { + if (walletReady) { + logger.debug(`[App]: ✅ Wallet ready!`); + runWalletBackupStatusChecks(); + } + }, [walletReady]); + return ( - + {initialRoute && ( - }> - {/* @ts-expect-error - Property 'ref' does not exist on the 'IntrinsicAttributes' object */} - - + )} - - ); } @@ -99,9 +192,9 @@ const AppWithRedux = connect(state => }))(App); function Root() { - const [initializing, setInitializing] = useState(true); + const [initializing, setInitializing] = React.useState(true); - useEffect(() => { + React.useEffect(() => { async function initializeApplication() { await initializeRemoteConfig(); await migrate(); @@ -207,21 +300,19 @@ function Root() { prefetchDefaultFavorites(); }} > - - - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/src/components/AppStateChangeHandler.tsx b/src/components/AppStateChangeHandler.tsx deleted file mode 100644 index ee8b29d2ebb..00000000000 --- a/src/components/AppStateChangeHandler.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; -import { AppState, AppStateStatus, Linking } from 'react-native'; -import { analyticsV2 } from '@/analytics'; -import store from '@/redux/store'; -import { walletConnectLoadState } from '@/redux/walletconnect'; - -type AppStateChangeHandlerProps = { - walletReady: boolean; -}; - -export function AppStateChangeHandler({ walletReady }: AppStateChangeHandlerProps) { - const [appState, setAppState] = useState(AppState.currentState); - const eventSubscription = useRef | null>(null); - - const handleAppStateChange = useCallback( - (nextAppState: AppStateStatus) => { - if (appState === 'background' && nextAppState === 'active') { - store.dispatch(walletConnectLoadState()); - } - setAppState(nextAppState); - analyticsV2.track(analyticsV2.event.appStateChange, { - category: 'app state', - label: nextAppState, - }); - }, - [appState] - ); - - useEffect(() => { - if (!walletReady) return; - - eventSubscription.current = AppState.addEventListener('change', handleAppStateChange); - - return () => eventSubscription.current?.remove(); - }, [handleAppStateChange]); - - return null; -} diff --git a/src/components/DeeplinkHandler.tsx b/src/components/DeeplinkHandler.tsx deleted file mode 100644 index e34f7ef4e8b..00000000000 --- a/src/components/DeeplinkHandler.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useCallback, useEffect, useRef } from 'react'; -import { Linking } from 'react-native'; -import branch from 'react-native-branch'; -import { useMobileWalletProtocolHost } from '@coinbase/mobile-wallet-protocol-host'; -import handleDeeplink from '@/handlers/deeplinks'; -import { InitialRoute } from '@/navigation/initialRoute'; -import { logger, RainbowError } from '@/logger'; -import { branchListener } from '@/utils/branch'; - -type DeeplinkHandlerProps = { - initialRoute: InitialRoute; - walletReady: boolean; -}; - -export function DeeplinkHandler({ initialRoute, walletReady }: DeeplinkHandlerProps) { - const branchListenerRef = useRef | null>(null); - const { handleRequestUrl, sendFailureToClient } = useMobileWalletProtocolHost(); - - const setupDeeplinking = useCallback(async () => { - const initialUrl = await Linking.getInitialURL(); - - branchListenerRef.current = await branchListener(async url => { - logger.debug(`[App]: Branch listener called`, {}, logger.DebugContext.deeplinks); - - try { - handleDeeplink({ - url, - initialRoute, - handleRequestUrl, - sendFailureToClient, - }); - } catch (error) { - if (error instanceof Error) { - logger.error(new RainbowError('Error opening deeplink'), { - message: error.message, - url, - }); - } else { - logger.error(new RainbowError('Error opening deeplink'), { - message: 'Unknown error', - url, - }); - } - } - }); - - if (initialUrl) { - logger.debug(`[App]: has initial URL, opening with Branch`, { initialUrl }); - branch.openURL(initialUrl); - } - }, [handleRequestUrl, initialRoute, sendFailureToClient]); - - useEffect(() => { - if (!walletReady) return; - - setupDeeplinking(); - return () => { - if (branchListenerRef.current) { - branchListenerRef.current(); - } - }; - }, [setupDeeplinking, walletReady]); - - return null; -} diff --git a/src/components/FadeGradient.tsx b/src/components/FadeGradient.tsx deleted file mode 100644 index 88fdade67a7..00000000000 --- a/src/components/FadeGradient.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import { StyleProp, ViewStyle } from 'react-native'; -import LinearGradient from 'react-native-linear-gradient'; -import Animated from 'react-native-reanimated'; - -import { Box, globalColors } from '@/design-system'; - -import { useTheme } from '@/theme'; - -type FadeGradientProps = { side: 'top' | 'bottom'; style?: StyleProp>> }; - -export const FadeGradient = ({ side, style }: FadeGradientProps) => { - const { colors, isDarkMode } = useTheme(); - - const isTop = side === 'top'; - const solidColor = isDarkMode ? globalColors.white10 : '#FBFCFD'; - const transparentColor = colors.alpha(solidColor, 0); - - return ( - - - - ); -}; diff --git a/src/components/FadedScrollCard.tsx b/src/components/FadedScrollCard.tsx deleted file mode 100644 index bf8a1ed9c39..00000000000 --- a/src/components/FadedScrollCard.tsx +++ /dev/null @@ -1,281 +0,0 @@ -import React, { useCallback, useState } from 'react'; -import { TouchableWithoutFeedback } from 'react-native'; -import Animated, { - Easing, - SharedValue, - interpolate, - interpolateColor, - measure, - runOnJS, - runOnUI, - useAnimatedReaction, - useAnimatedRef, - useAnimatedStyle, - useSharedValue, - withTiming, -} from 'react-native-reanimated'; - -import { globalColors } from '@/design-system'; - -import { useTheme } from '@/theme'; - -import { useDimensions } from '@/hooks'; -import { FadeGradient } from '@/components/FadeGradient'; - -const COLLAPSED_CARD_HEIGHT = 56; -const MAX_CARD_HEIGHT = 176; - -const CARD_BORDER_WIDTH = 1.5; - -const timingConfig = { - duration: 300, - easing: Easing.bezier(0.2, 0, 0, 1), -}; - -type FadedScrollCardProps = { - cardHeight: SharedValue; - children: React.ReactNode; - contentHeight: SharedValue; - expandedCardBottomInset?: number; - expandedCardTopInset?: number; - initialScrollEnabled?: boolean; - isExpanded: boolean; - onPressCollapsedCard?: () => void; - skipCollapsedState?: boolean; -}; - -export const FadedScrollCard = ({ - cardHeight, - children, - contentHeight, - expandedCardBottomInset = 120, - expandedCardTopInset = 120, - initialScrollEnabled, - isExpanded, - onPressCollapsedCard, - skipCollapsedState, -}: FadedScrollCardProps) => { - const { height: deviceHeight, width: deviceWidth } = useDimensions(); - const { isDarkMode } = useTheme(); - - const cardRef = useAnimatedRef(); - - const [scrollEnabled, setScrollEnabled] = useState(initialScrollEnabled); - const [isFullyExpanded, setIsFullyExpanded] = useState(false); - - const yPosition = useSharedValue(0); - - const maxExpandedHeight = deviceHeight - (expandedCardBottomInset + expandedCardTopInset); - - const containerStyle = useAnimatedStyle(() => { - return { - height: - cardHeight.value > MAX_CARD_HEIGHT || !skipCollapsedState - ? interpolate( - cardHeight.value, - [MAX_CARD_HEIGHT, MAX_CARD_HEIGHT, maxExpandedHeight], - [cardHeight.value, MAX_CARD_HEIGHT, MAX_CARD_HEIGHT], - 'clamp' - ) - : undefined, - zIndex: interpolate(cardHeight.value, [0, MAX_CARD_HEIGHT, MAX_CARD_HEIGHT + 1], [1, 1, 2], 'clamp'), - }; - }); - - const backdropStyle = useAnimatedStyle(() => { - const canExpandFully = contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT; - return { - opacity: canExpandFully && isFullyExpanded ? withTiming(1, timingConfig) : withTiming(0, timingConfig), - }; - }); - - const cardStyle = useAnimatedStyle(() => { - const canExpandFully = contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT; - const expandedCardHeight = Math.min(contentHeight.value + CARD_BORDER_WIDTH * 2, maxExpandedHeight); - - const outputRange = [0, 0]; - - const yPos = -yPosition.value; - const offset = - deviceHeight - (expandedCardBottomInset + expandedCardTopInset) - expandedCardHeight - (yPosition.value + expandedCardHeight); - - if (yPos + expandedCardTopInset + offset >= deviceHeight - expandedCardBottomInset) { - outputRange.push(0); - } else { - outputRange.push(deviceHeight - expandedCardBottomInset - yPosition.value - expandedCardHeight); - } - - return { - borderColor: interpolateColor( - cardHeight.value, - [0, MAX_CARD_HEIGHT, expandedCardHeight], - isDarkMode ? ['#1F2023', '#1F2023', '#242527'] : ['#F5F7F8', '#F5F7F8', '#FBFCFD'] - ), - height: cardHeight.value > MAX_CARD_HEIGHT ? cardHeight.value : undefined, - position: canExpandFully && isFullyExpanded ? 'absolute' : 'relative', - transform: [ - { - translateY: interpolate(cardHeight.value, [0, MAX_CARD_HEIGHT, expandedCardHeight], outputRange), - }, - ], - }; - }); - - const centerVerticallyWhenCollapsedStyle = useAnimatedStyle(() => { - return { - transform: skipCollapsedState - ? undefined - : [ - { - translateY: interpolate( - cardHeight.value, - [ - 0, - COLLAPSED_CARD_HEIGHT, - contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT - ? MAX_CARD_HEIGHT - : contentHeight.value + CARD_BORDER_WIDTH * 2, - maxExpandedHeight, - ], - [-2, -2, 0, 0] - ), - }, - ], - }; - }); - - const shadowStyle = useAnimatedStyle(() => { - const canExpandFully = contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT; - return { - shadowOpacity: canExpandFully && isFullyExpanded ? withTiming(isDarkMode ? 0.9 : 0.16, timingConfig) : withTiming(0, timingConfig), - }; - }); - - const handleContentSizeChange = useCallback( - (width: number, height: number) => { - contentHeight.value = Math.round(height); - }, - [contentHeight] - ); - - const handleOnLayout = useCallback(() => { - runOnUI(() => { - if (cardHeight.value === MAX_CARD_HEIGHT) { - const measurement = measure(cardRef); - if (measurement === null) { - return; - } - if (yPosition.value !== measurement.pageY) { - yPosition.value = measurement.pageY; - } - } - })(); - }, [cardHeight, cardRef, yPosition]); - - useAnimatedReaction( - () => ({ contentHeight: contentHeight.value, isExpanded, isFullyExpanded }), - ({ contentHeight, isExpanded, isFullyExpanded }, previous) => { - if ( - isFullyExpanded !== previous?.isFullyExpanded || - isExpanded !== previous?.isExpanded || - contentHeight !== previous?.contentHeight - ) { - if (isFullyExpanded) { - const expandedCardHeight = - contentHeight + CARD_BORDER_WIDTH * 2 > maxExpandedHeight ? maxExpandedHeight : contentHeight + CARD_BORDER_WIDTH * 2; - if (contentHeight + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT && cardHeight.value >= MAX_CARD_HEIGHT) { - cardHeight.value = withTiming(expandedCardHeight, timingConfig); - } else { - runOnJS(setIsFullyExpanded)(false); - } - } else if (isExpanded) { - cardHeight.value = withTiming( - contentHeight + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT ? MAX_CARD_HEIGHT : contentHeight + CARD_BORDER_WIDTH * 2, - timingConfig - ); - } else { - cardHeight.value = withTiming(COLLAPSED_CARD_HEIGHT, timingConfig); - } - - const enableScroll = isExpanded && contentHeight + CARD_BORDER_WIDTH * 2 > (isFullyExpanded ? maxExpandedHeight : MAX_CARD_HEIGHT); - runOnJS(setScrollEnabled)(enableScroll); - } - } - ); - - return ( - - { - if (isFullyExpanded) { - setIsFullyExpanded(false); - } - }} - pointerEvents={isFullyExpanded ? 'auto' : 'none'} - style={[ - { - backgroundColor: 'rgba(0, 0, 0, 0.6)', - height: deviceHeight * 3, - left: -deviceWidth * 0.5, - position: 'absolute', - top: -deviceHeight, - width: deviceWidth * 2, - zIndex: -1, - }, - backdropStyle, - ]} - /> - - - - { - if (!isFullyExpanded) { - setIsFullyExpanded(true); - } else setIsFullyExpanded(false); - } - } - > - {children} - - - - - - - - ); -}; diff --git a/src/components/MobileWalletProtocolListener.tsx b/src/components/MobileWalletProtocolListener.tsx deleted file mode 100644 index 395c58fd6b4..00000000000 --- a/src/components/MobileWalletProtocolListener.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { useEffect, useRef } from 'react'; -import { addDiagnosticLogListener, getAndroidIntentUrl, useMobileWalletProtocolHost } from '@coinbase/mobile-wallet-protocol-host'; -import { handleMobileWalletProtocolRequest } from '@/utils/requestNavigationHandlers'; -import { logger, RainbowError } from '@/logger'; -import { IS_ANDROID, IS_DEV } from '@/env'; - -export const MobileWalletProtocolListener = () => { - const { message, handleRequestUrl, sendFailureToClient, ...mwpProps } = useMobileWalletProtocolHost(); - const lastMessageUuidRef = useRef(null); - - useEffect(() => { - if (message && lastMessageUuidRef.current !== message.uuid) { - lastMessageUuidRef.current = message.uuid; - try { - handleMobileWalletProtocolRequest({ request: message, ...mwpProps }); - } catch (error) { - logger.error(new RainbowError('Error handling Mobile Wallet Protocol request'), { - error, - }); - } - } - }, [message, mwpProps]); - - useEffect(() => { - if (IS_DEV) { - const removeListener = addDiagnosticLogListener(event => { - console.log('Event:', JSON.stringify(event, null, 2)); - }); - - return () => removeListener(); - } - }, []); - - useEffect(() => { - if (IS_ANDROID) { - (async function handleAndroidIntent() { - const intentUrl = await getAndroidIntentUrl(); - if (intentUrl) { - const response = await handleRequestUrl(intentUrl); - if (response.error) { - // Return error to client app if session is expired or invalid - const { errorMessage, decodedRequest } = response.error; - await sendFailureToClient(errorMessage, decodedRequest); - } - } - })(); - } - }, [handleRequestUrl, sendFailureToClient]); - - return null; -}; diff --git a/src/components/Transactions/TransactionDetailsCard.tsx b/src/components/Transactions/TransactionDetailsCard.tsx deleted file mode 100644 index 8e00ee8e78a..00000000000 --- a/src/components/Transactions/TransactionDetailsCard.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import React, { useState } from 'react'; -import * as i18n from '@/languages'; -import Animated, { interpolate, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'; - -import { Box, Inline, Stack, Text } from '@/design-system'; -import { TextColor } from '@/design-system/color/palettes'; - -import { abbreviations, ethereumUtils } from '@/utils'; -import { TransactionSimulationMeta } from '@/graphql/__generated__/metadataPOST'; -import { ChainId } from '@/__swaps__/types/chains'; - -import { getNetworkObj, getNetworkObject } from '@/networks'; -import { TransactionDetailsRow } from '@/components/Transactions/TransactionDetailsRow'; -import { FadedScrollCard } from '@/components/FadedScrollCard'; -import { IconContainer } from '@/components/Transactions/TransactionIcons'; -import { formatDate } from '@/utils/formatDate'; -import { - COLLAPSED_CARD_HEIGHT, - MAX_CARD_HEIGHT, - CARD_ROW_HEIGHT, - CARD_BORDER_WIDTH, - EXPANDED_CARD_TOP_INSET, -} from '@/components/Transactions/constants'; - -interface TransactionDetailsCardProps { - currentChainId: ChainId; - expandedCardBottomInset: number; - isBalanceEnough: boolean | undefined; - isLoading: boolean; - meta: TransactionSimulationMeta | undefined; - methodName: string; - noChanges: boolean; - nonce: string | undefined; - toAddress: string; -} - -export const TransactionDetailsCard = ({ - currentChainId, - expandedCardBottomInset, - isBalanceEnough, - isLoading, - meta, - methodName, - noChanges, - nonce, - toAddress, -}: TransactionDetailsCardProps) => { - const cardHeight = useSharedValue(COLLAPSED_CARD_HEIGHT); - const contentHeight = useSharedValue(COLLAPSED_CARD_HEIGHT - CARD_BORDER_WIDTH * 2); - const [isExpanded, setIsExpanded] = useState(false); - - const currentNetwork = getNetworkObject({ chainId: currentChainId }); - - const listStyle = useAnimatedStyle(() => ({ - opacity: interpolate( - cardHeight.value, - [ - COLLAPSED_CARD_HEIGHT, - contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT ? MAX_CARD_HEIGHT : contentHeight.value + CARD_BORDER_WIDTH * 2, - ], - [0, 1] - ), - })); - - const collapsedTextColor: TextColor = isLoading ? 'labelQuaternary' : 'blue'; - - const showFunctionRow = meta?.to?.function || (methodName && methodName.substring(0, 2) !== '0x'); - const isContract = showFunctionRow || meta?.to?.created || meta?.to?.sourceCodeStatus; - const showTransferToRow = !!meta?.transferTo?.address; - // Hide DetailsCard if balance is insufficient once loaded - if (!isLoading && isBalanceEnough === false) { - return <>; - } - return ( - setIsExpanded(true)} - > - - - - - - 􁙠 - - - - {i18n.t(i18n.l.walletconnect.simulation.details_card.title)} - - - - - - {} - {!!(meta?.to?.address || toAddress || showTransferToRow) && ( - - ethereumUtils.openAddressInBlockExplorer( - meta?.to?.address || toAddress || meta?.transferTo?.address || '', - currentChainId - ) - } - value={ - meta?.to?.name || - abbreviations.address(meta?.to?.address || toAddress, 4, 6) || - meta?.to?.address || - toAddress || - meta?.transferTo?.address || - '' - } - /> - )} - {showFunctionRow && } - {!!meta?.to?.sourceCodeStatus && } - {!!meta?.to?.created && } - {nonce && } - - - - - ); -}; diff --git a/src/components/Transactions/TransactionDetailsRow.tsx b/src/components/Transactions/TransactionDetailsRow.tsx deleted file mode 100644 index bbe2a8394de..00000000000 --- a/src/components/Transactions/TransactionDetailsRow.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react'; -import * as i18n from '@/languages'; -import { TouchableWithoutFeedback } from 'react-native'; - -import { ButtonPressAnimation } from '@/components/animations'; -import { ChainImage } from '@/components/coin-icon/ChainImage'; -import { Box, Inline, Text } from '@/design-system'; - -import { DetailIcon, DetailBadge, IconContainer } from '@/components/Transactions/TransactionIcons'; -import { SMALL_CARD_ROW_HEIGHT } from '@/components/Transactions/constants'; -import { DetailType, DetailInfo } from '@/components/Transactions/types'; -import { ChainId } from '@/__swaps__/types/chains'; - -interface TransactionDetailsRowProps { - currentChainId?: ChainId; - detailType: DetailType; - onPress?: () => void; - value: string; -} - -export const TransactionDetailsRow = ({ currentChainId, detailType, onPress, value }: TransactionDetailsRowProps) => { - const detailInfo: DetailInfo = infoForDetailType[detailType]; - - return ( - - - - - - {detailInfo.label} - - - - {detailType === 'function' && } - {detailType === 'sourceCodeVerification' && ( - - )} - {detailType === 'chain' && currentChainId && } - {detailType !== 'function' && detailType !== 'sourceCodeVerification' && ( - - {value} - - )} - {(detailType === 'contract' || detailType === 'to') && ( - - - - - 􀂄 - - - - - )} - - - - ); -}; - -const infoForDetailType: { [key: string]: DetailInfo } = { - chain: { - icon: '􀤆', - label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.chain), - }, - contract: { - icon: '􀉆', - label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.contract), - }, - to: { - icon: '􀉩', - label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.to), - }, - function: { - icon: '􀡅', - label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.function), - }, - sourceCodeVerification: { - icon: '􀕹', - label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.source_code), - }, - dateCreated: { - icon: '􀉉', - label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.contract_created), - }, - nonce: { - icon: '􀆃', - label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.nonce), - }, -}; diff --git a/src/components/Transactions/TransactionIcons.tsx b/src/components/Transactions/TransactionIcons.tsx deleted file mode 100644 index 53ef21fcd6e..00000000000 --- a/src/components/Transactions/TransactionIcons.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import React from 'react'; -import { AnimatePresence, MotiView } from 'moti'; - -import { Bleed, Box, Text, globalColors, useForegroundColor } from '@/design-system'; -import { TextColor } from '@/design-system/color/palettes'; -import { infoForEventType, motiTimingConfig } from '@/components/Transactions/constants'; - -import { useTheme } from '@/theme'; -import { DetailInfo, EventInfo, EventType } from '@/components/Transactions/types'; - -export const EventIcon = ({ eventType }: { eventType: EventType }) => { - const eventInfo: EventInfo = infoForEventType[eventType]; - - const hideInnerFill = eventType === 'approve' || eventType === 'revoke'; - const isWarningIcon = - eventType === 'failed' || eventType === 'insufficientBalance' || eventType === 'MALICIOUS' || eventType === 'WARNING'; - - return ( - - {!hideInnerFill && ( - - )} - - {eventInfo.icon} - - - ); -}; - -export const DetailIcon = ({ detailInfo }: { detailInfo: DetailInfo }) => { - return ( - - - {detailInfo.icon} - - - ); -}; - -export const DetailBadge = ({ type, value }: { type: 'function' | 'unknown' | 'unverified' | 'verified'; value: string }) => { - const { colors, isDarkMode } = useTheme(); - const separatorTertiary = useForegroundColor('separatorTertiary'); - - const infoForBadgeType: { - [key: string]: { - backgroundColor: string; - borderColor: string; - label?: string; - text: TextColor; - textOpacity?: number; - }; - } = { - function: { - backgroundColor: 'transparent', - borderColor: isDarkMode ? separatorTertiary : colors.alpha(separatorTertiary, 0.025), - text: 'labelQuaternary', - }, - unknown: { - backgroundColor: 'transparent', - borderColor: isDarkMode ? separatorTertiary : colors.alpha(separatorTertiary, 0.025), - label: 'Unknown', - text: 'labelQuaternary', - }, - unverified: { - backgroundColor: isDarkMode ? colors.alpha(colors.red, 0.05) : globalColors.red10, - borderColor: colors.alpha(colors.red, 0.02), - label: 'Unverified', - text: 'red', - textOpacity: 0.76, - }, - verified: { - backgroundColor: isDarkMode ? colors.alpha(colors.green, 0.05) : globalColors.green10, - borderColor: colors.alpha(colors.green, 0.02), - label: 'Verified', - text: 'green', - textOpacity: 0.76, - }, - }; - - return ( - - - - {infoForBadgeType[type].label || value} - - - - ); -}; - -export const VerifiedBadge = () => { - return ( - - - - - 􀇻 - - - - ); -}; - -export const AnimatedCheckmark = ({ visible }: { visible: boolean }) => { - return ( - - {visible && ( - - - - - - 􀁣 - - - - - )} - - ); -}; - -export const IconContainer = ({ - children, - hitSlop, - opacity, - size = 20, -}: { - children: React.ReactNode; - hitSlop?: number; - opacity?: number; - size?: number; -}) => { - // Prevent wide icons from being clipped - const extraHorizontalSpace = 4; - - return ( - - - {children} - - - ); -}; diff --git a/src/components/Transactions/TransactionMessageCard.tsx b/src/components/Transactions/TransactionMessageCard.tsx deleted file mode 100644 index 3e946bf6d6d..00000000000 --- a/src/components/Transactions/TransactionMessageCard.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React, { useCallback, useMemo, useState } from 'react'; -import * as i18n from '@/languages'; -import { TouchableWithoutFeedback } from 'react-native'; -import { useSharedValue } from 'react-native-reanimated'; - -import { ButtonPressAnimation } from '@/components/animations'; -import { Bleed, Box, Inline, Stack, Text } from '@/design-system'; - -import { useClipboard } from '@/hooks'; -import { logger } from '@/logger'; -import { isSignTypedData } from '@/utils/signingMethods'; - -import { RPCMethod } from '@/walletConnect/types'; -import { sanitizeTypedData } from '@/utils/signingUtils'; -import { - estimateMessageHeight, - MAX_CARD_HEIGHT, - CARD_ROW_HEIGHT, - CARD_BORDER_WIDTH, - EXPANDED_CARD_TOP_INSET, -} from '@/components/Transactions/constants'; -import { FadedScrollCard } from '@/components/FadedScrollCard'; -import { AnimatedCheckmark, IconContainer } from '@/components/Transactions/TransactionIcons'; - -type TransactionMessageCardProps = { - expandedCardBottomInset: number; - message: string; - method: RPCMethod; -}; - -export const TransactionMessageCard = ({ expandedCardBottomInset, message, method }: TransactionMessageCardProps) => { - const { setClipboard } = useClipboard(); - const [didCopy, setDidCopy] = useState(false); - - let displayMessage = message; - if (isSignTypedData(method)) { - try { - const parsedMessage = JSON.parse(message); - const sanitizedMessage = sanitizeTypedData(parsedMessage); - displayMessage = sanitizedMessage; - } catch (error) { - logger.warn(`[TransactionMessageCard]: Error parsing signed typed data for ${method}`, { - error, - }); - } - - displayMessage = JSON.stringify(displayMessage, null, 4); - } - - const estimatedMessageHeight = useMemo(() => estimateMessageHeight(displayMessage), [displayMessage]); - - const cardHeight = useSharedValue( - estimatedMessageHeight > MAX_CARD_HEIGHT ? MAX_CARD_HEIGHT : estimatedMessageHeight + CARD_BORDER_WIDTH * 2 - ); - const contentHeight = useSharedValue(estimatedMessageHeight); - - const handleCopyPress = useCallback( - (message: string) => { - if (didCopy) return; - setClipboard(message); - setDidCopy(true); - const copyTimer = setTimeout(() => { - setDidCopy(false); - }, 2000); - return () => clearTimeout(copyTimer); - }, - [didCopy, setClipboard] - ); - - return ( - MAX_CARD_HEIGHT} - isExpanded - skipCollapsedState - > - - - - - - 􀙤 - - - - {i18n.t(i18n.l.walletconnect.simulation.message_card.title)} - - - - handleCopyPress(message)}> - - - - - - {i18n.t(i18n.l.walletconnect.simulation.message_card.copy)} - - - - - - - - - {displayMessage} - - - - ); -}; diff --git a/src/components/Transactions/TransactionSimulatedEventRow.tsx b/src/components/Transactions/TransactionSimulatedEventRow.tsx deleted file mode 100644 index ce4d26052eb..00000000000 --- a/src/components/Transactions/TransactionSimulatedEventRow.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React, { useMemo } from 'react'; -import * as i18n from '@/languages'; -import { Image, PixelRatio } from 'react-native'; - -import { Bleed, Box, Inline, Text } from '@/design-system'; - -import { useTheme } from '@/theme'; -import { TransactionAssetType, TransactionSimulationAsset } from '@/graphql/__generated__/metadataPOST'; -import { Network } from '@/networks/types'; -import { convertAmountToNativeDisplay, convertRawAmountToBalance } from '@/helpers/utilities'; - -import { useAccountSettings } from '@/hooks'; - -import { maybeSignUri } from '@/handlers/imgix'; -import RainbowCoinIcon from '@/components/coin-icon/RainbowCoinIcon'; -import { useExternalToken } from '@/resources/assets/externalAssetsQuery'; -import { EventInfo, EventType } from '@/components/Transactions/types'; -import { infoForEventType, CARD_ROW_HEIGHT } from '@/components/Transactions/constants'; -import { EventIcon } from '@/components/Transactions/TransactionIcons'; -import { ethereumUtils } from '@/utils'; - -type TransactionSimulatedEventRowProps = { - amount: string | 'unlimited'; - asset: TransactionSimulationAsset | undefined; - eventType: EventType; - price?: number | undefined; -}; - -export const TransactionSimulatedEventRow = ({ amount, asset, eventType, price }: TransactionSimulatedEventRowProps) => { - const theme = useTheme(); - const { nativeCurrency } = useAccountSettings(); - - const chainId = ethereumUtils.getChainIdFromNetwork((asset?.network as Network) || Network.mainnet); - - const { data: externalAsset } = useExternalToken({ - address: asset?.assetCode || '', - chainId, - currency: nativeCurrency, - }); - - const eventInfo: EventInfo = infoForEventType[eventType]; - - const formattedAmount = useMemo(() => { - if (!asset) return; - - const nftFallbackSymbol = parseFloat(amount) > 1 ? 'NFTs' : 'NFT'; - const assetDisplayName = - asset?.type === TransactionAssetType.Nft ? asset?.name || asset?.symbol || nftFallbackSymbol : asset?.symbol || asset?.name; - const shortenedDisplayName = assetDisplayName.length > 12 ? `${assetDisplayName.slice(0, 12).trim()}…` : assetDisplayName; - - const displayAmount = - asset?.decimals === 0 - ? `${amount}${shortenedDisplayName ? ' ' + shortenedDisplayName : ''}` - : convertRawAmountToBalance(amount, { decimals: asset?.decimals || 18, symbol: shortenedDisplayName }, 3, true).display; - - const unlimitedApproval = `${i18n.t(i18n.l.walletconnect.simulation.simulation_card.event_row.unlimited)} ${asset?.symbol}`; - - return `${eventInfo.amountPrefix}${amount === 'UNLIMITED' ? unlimitedApproval : displayAmount}`; - }, [amount, asset, eventInfo?.amountPrefix]); - - const url = maybeSignUri(asset?.iconURL, { - fm: 'png', - w: 16 * PixelRatio.get(), - }); - - const showUSD = (eventType === 'send' || eventType === 'receive') && !!price; - - const formattedPrice = price && convertAmountToNativeDisplay(price, nativeCurrency); - - return ( - - - - - - - {eventInfo.label} - - {showUSD && ( - - {formattedPrice} - - )} - - - - - {asset?.type !== TransactionAssetType.Nft ? ( - - ) : ( - - )} - - - {formattedAmount} - - - - - ); -}; diff --git a/src/components/Transactions/TransactionSimulationCard.tsx b/src/components/Transactions/TransactionSimulationCard.tsx deleted file mode 100644 index 51244f5b030..00000000000 --- a/src/components/Transactions/TransactionSimulationCard.tsx +++ /dev/null @@ -1,315 +0,0 @@ -import React, { useMemo } from 'react'; -import * as i18n from '@/languages'; -import Animated, { - interpolate, - useAnimatedReaction, - useAnimatedStyle, - useSharedValue, - withRepeat, - withTiming, -} from 'react-native-reanimated'; - -import { Box, Inline, Stack, Text } from '@/design-system'; -import { TextColor } from '@/design-system/color/palettes'; - -import { TransactionErrorType, TransactionSimulationResult, TransactionScanResultType } from '@/graphql/__generated__/metadataPOST'; -import { Network } from '@/networks/types'; - -import { getNetworkObj, getNetworkObject } from '@/networks'; -import { isEmpty } from 'lodash'; -import { TransactionSimulatedEventRow } from '@/components/Transactions/TransactionSimulatedEventRow'; -import { FadedScrollCard } from '@/components/FadedScrollCard'; -import { EventIcon, IconContainer } from '@/components/Transactions/TransactionIcons'; -import { - COLLAPSED_CARD_HEIGHT, - MAX_CARD_HEIGHT, - CARD_ROW_HEIGHT, - CARD_BORDER_WIDTH, - EXPANDED_CARD_TOP_INSET, - rotationConfig, - timingConfig, -} from '@/components/Transactions/constants'; -import { ChainId } from '@/__swaps__/types/chains'; - -interface TransactionSimulationCardProps { - currentChainId: ChainId; - expandedCardBottomInset: number; - isBalanceEnough: boolean | undefined; - isLoading: boolean; - txSimulationApiError: unknown; - isPersonalSignRequest: boolean; - noChanges: boolean; - simulation: TransactionSimulationResult | undefined; - simulationError: TransactionErrorType | undefined; - simulationScanResult: TransactionScanResultType | undefined; - walletBalance: { - amount: string | number; - display: string; - isLoaded: boolean; - symbol: string; - }; -} - -export const TransactionSimulationCard = ({ - currentChainId, - expandedCardBottomInset, - isBalanceEnough, - isLoading, - txSimulationApiError, - isPersonalSignRequest, - noChanges, - simulation, - simulationError, - simulationScanResult, - walletBalance, -}: TransactionSimulationCardProps) => { - const cardHeight = useSharedValue(COLLAPSED_CARD_HEIGHT); - const contentHeight = useSharedValue(COLLAPSED_CARD_HEIGHT - CARD_BORDER_WIDTH * 2); - const spinnerRotation = useSharedValue(0); - - const listStyle = useAnimatedStyle(() => ({ - opacity: noChanges - ? withTiming(1, timingConfig) - : interpolate( - cardHeight.value, - [ - COLLAPSED_CARD_HEIGHT, - contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT ? MAX_CARD_HEIGHT : contentHeight.value + CARD_BORDER_WIDTH * 2, - ], - [0, 1] - ), - })); - - const spinnerStyle = useAnimatedStyle(() => { - return { - transform: [{ rotate: `${spinnerRotation.value}deg` }], - }; - }); - - useAnimatedReaction( - () => ({ isLoading, isPersonalSignRequest }), - ({ isLoading, isPersonalSignRequest }, previous = { isLoading: false, isPersonalSignRequest: false }) => { - if (isLoading && !previous?.isLoading) { - spinnerRotation.value = withRepeat(withTiming(360, rotationConfig), -1, false); - } else if ( - (!isLoading && previous?.isLoading) || - (isPersonalSignRequest && !previous?.isPersonalSignRequest && previous?.isLoading) - ) { - spinnerRotation.value = withTiming(360, timingConfig); - } - }, - [isLoading, isPersonalSignRequest] - ); - const renderSimulationEventRows = useMemo(() => { - if (isBalanceEnough === false) return null; - - return ( - <> - {simulation?.approvals?.map(change => { - return ( - - ); - })} - {simulation?.out?.map(change => { - return ( - - ); - })} - {simulation?.in?.map(change => { - return ( - - ); - })} - - ); - }, [isBalanceEnough, simulation]); - - const titleColor: TextColor = useMemo(() => { - if (isLoading) { - return 'label'; - } - if (isBalanceEnough === false) { - return 'blue'; - } - if (noChanges || isPersonalSignRequest || txSimulationApiError) { - return 'labelQuaternary'; - } - if (simulationScanResult === TransactionScanResultType.Warning) { - return 'orange'; - } - if (simulationError || simulationScanResult === TransactionScanResultType.Malicious) { - return 'red'; - } - return 'label'; - }, [isBalanceEnough, isLoading, noChanges, simulationError, simulationScanResult, isPersonalSignRequest, txSimulationApiError]); - - const titleText = useMemo(() => { - if (isLoading) { - return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.simulating); - } - if (isBalanceEnough === false) { - return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.not_enough_native_balance, { symbol: walletBalance?.symbol }); - } - if (txSimulationApiError || isPersonalSignRequest) { - return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.simulation_unavailable); - } - if (simulationScanResult === TransactionScanResultType.Warning) { - return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.proceed_carefully); - } - if (simulationScanResult === TransactionScanResultType.Malicious) { - return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.suspicious_transaction); - } - if (noChanges) { - return i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.no_changes); - } - if (simulationError) { - return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.likely_to_fail); - } - return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.simulation_result); - }, [ - isBalanceEnough, - isLoading, - noChanges, - simulationError, - simulationScanResult, - isPersonalSignRequest, - txSimulationApiError, - walletBalance?.symbol, - ]); - - const isExpanded = useMemo(() => { - if (isLoading || isPersonalSignRequest) { - return false; - } - const shouldExpandOnLoad = isBalanceEnough === false || (!isEmpty(simulation) && !noChanges) || !!simulationError; - return shouldExpandOnLoad; - }, [isBalanceEnough, isLoading, isPersonalSignRequest, noChanges, simulation, simulationError]); - - return ( - - - - - {!isLoading && (simulationError || isBalanceEnough === false || simulationScanResult !== TransactionScanResultType.Ok) ? ( - - ) : ( - - {!isLoading && noChanges && !isPersonalSignRequest ? ( - - {/* The extra space avoids icon clipping */} - {'􀻾 '} - - ) : ( - - - 􀬨 - - - )} - - )} - - {titleText} - - - {/* TODO: Unhide once we add explainer sheets */} - {/* - - - - - 􀁜 - - - - - */} - - - - {isBalanceEnough === false ? ( - - {i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.need_more_native, { - symbol: walletBalance?.symbol, - network: getNetworkObject({ chainId: currentChainId }).name, - })} - - ) : ( - <> - {isPersonalSignRequest && ( - - - {i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.unavailable_personal_sign)} - - - )} - {txSimulationApiError && ( - - {i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.tx_api_error)} - - )} - {simulationError && ( - - {i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.failed_to_simulate)} - - )} - {simulationScanResult === TransactionScanResultType.Warning && ( - - {i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.warning)}{' '} - - )} - {simulationScanResult === TransactionScanResultType.Malicious && ( - - {i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.malicious)} - - )} - - )} - {renderSimulationEventRows} - - - - - ); -}; diff --git a/src/components/Transactions/constants.ts b/src/components/Transactions/constants.ts deleted file mode 100644 index 79e6e0d8df5..00000000000 --- a/src/components/Transactions/constants.ts +++ /dev/null @@ -1,121 +0,0 @@ -import * as i18n from '@/languages'; -import { Screens } from '@/state/performance/operations'; -import { safeAreaInsetValues } from '@/utils'; -import { TransitionConfig } from 'moti'; -import { Easing } from 'react-native-reanimated'; -import { EventInfo } from '@/components/Transactions/types'; -import { RequestSource } from '@/utils/requestNavigationHandlers'; - -export const SCREEN_BOTTOM_INSET = safeAreaInsetValues.bottom + 20; -export const GAS_BUTTON_SPACE = - 30 + // GasSpeedButton height - 24; // Between GasSpeedButton and bottom of sheet - -export const EXPANDED_CARD_BOTTOM_INSET = - SCREEN_BOTTOM_INSET + - 24 + // Between bottom of sheet and bottom of Cancel/Confirm - 52 + // Cancel/Confirm height - 24 + // Between Cancel/Confirm and wallet avatar row - 44 + // Wallet avatar row height - 24; // Between wallet avatar row and bottom of expandable area - -export const COLLAPSED_CARD_HEIGHT = 56; -export const MAX_CARD_HEIGHT = 176; - -export const CARD_ROW_HEIGHT = 12; -export const SMALL_CARD_ROW_HEIGHT = 10; -export const CARD_BORDER_WIDTH = 1.5; - -export const EXPANDED_CARD_TOP_INSET = safeAreaInsetValues.top + 72; - -export const rotationConfig = { - duration: 2100, - easing: Easing.linear, -}; - -export const timingConfig = { - duration: 300, - easing: Easing.bezier(0.2, 0, 0, 1), -}; - -export const motiTimingConfig: TransitionConfig = { - duration: 225, - easing: Easing.bezier(0.2, 0, 0, 1), - type: 'timing', -}; - -export const SCREEN_FOR_REQUEST_SOURCE = { - [RequestSource.BROWSER]: Screens.DAPP_BROWSER, - [RequestSource.WALLETCONNECT]: Screens.WALLETCONNECT, - [RequestSource.MOBILE_WALLET_PROTOCOL]: Screens.MOBILE_WALLET_PROTOCOL, -}; - -export const CHARACTERS_PER_LINE = 40; -export const LINE_HEIGHT = 11; -export const LINE_GAP = 9; - -export const estimateMessageHeight = (message: string) => { - const estimatedLines = Math.ceil(message.length / CHARACTERS_PER_LINE); - const messageHeight = estimatedLines * LINE_HEIGHT + (estimatedLines - 1) * LINE_GAP + CARD_ROW_HEIGHT + 24 * 3 - CARD_BORDER_WIDTH * 2; - - return messageHeight; -}; - -export const infoForEventType: { [key: string]: EventInfo } = { - send: { - amountPrefix: '- ', - icon: '􀁷', - iconColor: 'red', - label: i18n.t(i18n.l.walletconnect.simulation.simulation_card.event_row.types.send), - textColor: 'red', - }, - receive: { - amountPrefix: '+ ', - icon: '􀁹', - iconColor: 'green', - label: i18n.t(i18n.l.walletconnect.simulation.simulation_card.event_row.types.receive), - textColor: 'green', - }, - approve: { - amountPrefix: '', - icon: '􀎤', - iconColor: 'green', - label: i18n.t(i18n.l.walletconnect.simulation.simulation_card.event_row.types.approve), - textColor: 'label', - }, - revoke: { - amountPrefix: '', - icon: '􀎠', - iconColor: 'red', - label: i18n.t(i18n.l.walletconnect.simulation.simulation_card.event_row.types.revoke), - textColor: 'label', - }, - failed: { - amountPrefix: '', - icon: '􀇿', - iconColor: 'red', - label: i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.likely_to_fail), - textColor: 'red', - }, - insufficientBalance: { - amountPrefix: '', - icon: '􀇿', - iconColor: 'blue', - label: '', - textColor: 'blue', - }, - MALICIOUS: { - amountPrefix: '', - icon: '􀇿', - iconColor: 'red', - label: '', - textColor: 'red', - }, - WARNING: { - amountPrefix: '', - icon: '􀇿', - iconColor: 'orange', - label: '', - textColor: 'orange', - }, -}; diff --git a/src/components/Transactions/types.ts b/src/components/Transactions/types.ts deleted file mode 100644 index ce9eaa8b3c3..00000000000 --- a/src/components/Transactions/types.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { TextColor } from '@/design-system/color/palettes'; - -export type EventType = 'send' | 'receive' | 'approve' | 'revoke' | 'failed' | 'insufficientBalance' | 'MALICIOUS' | 'WARNING'; - -export type EventInfo = { - amountPrefix: string; - icon: string; - iconColor: TextColor; - label: string; - textColor: TextColor; -}; - -export type DetailType = 'chain' | 'contract' | 'to' | 'function' | 'sourceCodeVerification' | 'dateCreated' | 'nonce'; - -export type DetailInfo = { - icon: string; - label: string; -}; diff --git a/src/handlers/deeplinks.ts b/src/handlers/deeplinks.ts index 33316a8c0a0..c8914e691bd 100644 --- a/src/handlers/deeplinks.ts +++ b/src/handlers/deeplinks.ts @@ -19,13 +19,6 @@ import { FiatProviderName } from '@/entities/f2c'; import { getPoapAndOpenSheetWithQRHash, getPoapAndOpenSheetWithSecretWord } from '@/utils/poaps'; import { queryClient } from '@/react-query'; import { pointsReferralCodeQueryKey } from '@/resources/points'; -import { useMobileWalletProtocolHost } from '@coinbase/mobile-wallet-protocol-host'; -import { InitialRoute } from '@/navigation/initialRoute'; - -interface DeeplinkHandlerProps extends Pick, 'handleRequestUrl' | 'sendFailureToClient'> { - url: string; - initialRoute: InitialRoute; -} /* * You can test these deeplinks with the following command: @@ -33,7 +26,7 @@ interface DeeplinkHandlerProps extends Pick(null); - - const identifyFlow = useCallback(async () => { - const address = await loadAddress(); - if (address) { - setTimeout(() => { - InteractionManager.runAfterInteractions(() => { - handleReviewPromptAction(ReviewPromptAction.TimesLaunchedSinceInstall); - }); - }, 10_000); - - InteractionManager.runAfterInteractions(checkIdentifierOnLaunch); - } - - setInitialRoute(address ? Routes.SWIPE_LAYOUT : Routes.WELCOME_SCREEN); - PerformanceContextMap.set('initialRoute', address ? Routes.SWIPE_LAYOUT : Routes.WELCOME_SCREEN); - }, []); - - useEffect(() => { - if (!IS_DEV && isTestFlight) { - logger.debug(`[App]: Test flight usage - ${isTestFlight}`); - } - identifyFlow(); - - Promise.all([analyticsV2.initializeRudderstack(), saveFCMToken()]) - .catch(error => { - logger.error(new RainbowError('Failed to initialize rudderstack or save FCM token', error)); - }) - .finally(() => { - initWalletConnectListeners(); - PerformanceTracking.finishMeasuring(PerformanceMetrics.loadRootAppComponent); - analyticsV2.track(analyticsV2.event.applicationDidMount); - }); - }, [identifyFlow]); - - return { initialRoute }; -} diff --git a/src/hooks/useCalculateGasLimit.ts b/src/hooks/useCalculateGasLimit.ts deleted file mode 100644 index b0c6fb098bb..00000000000 --- a/src/hooks/useCalculateGasLimit.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { useEffect, useRef, useCallback } from 'react'; -import { estimateGas, web3Provider, toHex } from '@/handlers/web3'; -import { convertHexToString, omitFlatten } from '@/helpers/utilities'; -import { logger, RainbowError } from '@/logger'; -import { getNetworkObject } from '@/networks'; -import { ethereumUtils } from '@/utils'; -import { hexToNumber, isHex } from 'viem'; -import { isEmpty } from 'lodash'; -import { InteractionManager } from 'react-native'; -import { GasFeeParamsBySpeed } from '@/entities'; -import { StaticJsonRpcProvider } from '@ethersproject/providers'; -import { useGas } from '@/hooks'; -import { ChainId } from '@/__swaps__/types/chains'; - -type CalculateGasLimitProps = { - isMessageRequest: boolean; - gasFeeParamsBySpeed: GasFeeParamsBySpeed; - provider: StaticJsonRpcProvider | null; - req: any; - updateTxFee: ReturnType['updateTxFee']; - currentChainId: ChainId; -}; - -export const useCalculateGasLimit = ({ - isMessageRequest, - gasFeeParamsBySpeed, - provider, - req, - updateTxFee, - currentChainId, -}: CalculateGasLimitProps) => { - const calculatingGasLimit = useRef(false); - - const calculateGasLimit = useCallback(async () => { - calculatingGasLimit.current = true; - const txPayload = req; - if (isHex(txPayload?.type)) { - txPayload.type = hexToNumber(txPayload?.type); - } - let gas = txPayload.gasLimit || txPayload.gas; - - try { - logger.debug('WC: Estimating gas limit', { gas }, logger.DebugContext.walletconnect); - const cleanTxPayload = omitFlatten(txPayload, ['gas', 'gasLimit', 'gasPrice', 'maxFeePerGas', 'maxPriorityFeePerGas']); - const rawGasLimit = await estimateGas(cleanTxPayload, provider); - logger.debug('WC: Estimated gas limit', { rawGasLimit }, logger.DebugContext.walletconnect); - if (rawGasLimit) { - gas = toHex(rawGasLimit); - } - } catch (error) { - logger.error(new RainbowError('WC: error estimating gas'), { error }); - } finally { - logger.debug('WC: Setting gas limit to', { gas: convertHexToString(gas) }, logger.DebugContext.walletconnect); - - const networkObject = getNetworkObject({ chainId: currentChainId }); - if (currentChainId && networkObject.gas.OptimismTxFee) { - const l1GasFeeOptimism = await ethereumUtils.calculateL1FeeOptimism(txPayload, provider || web3Provider); - updateTxFee(gas, null, l1GasFeeOptimism); - } else { - updateTxFee(gas, null); - } - } - }, [currentChainId, req, updateTxFee, provider]); - - useEffect(() => { - if (!isEmpty(gasFeeParamsBySpeed) && !calculatingGasLimit.current && !isMessageRequest && provider) { - InteractionManager.runAfterInteractions(() => { - calculateGasLimit(); - }); - } - }, [calculateGasLimit, gasFeeParamsBySpeed, isMessageRequest, provider]); - - return { calculateGasLimit }; -}; diff --git a/src/hooks/useConfirmTransaction.ts b/src/hooks/useConfirmTransaction.ts deleted file mode 100644 index 6600cead84b..00000000000 --- a/src/hooks/useConfirmTransaction.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useCallback } from 'react'; - -type UseConfirmTransactionProps = { - isMessageRequest: boolean; - isBalanceEnough: boolean | undefined; - isValidGas: boolean; - handleSignMessage: () => void; - handleConfirmTransaction: () => void; -}; - -export const useConfirmTransaction = ({ - isMessageRequest, - isBalanceEnough, - isValidGas, - handleSignMessage, - handleConfirmTransaction, -}: UseConfirmTransactionProps) => { - const onConfirm = useCallback(async () => { - if (isMessageRequest) { - return handleSignMessage(); - } - if (!isBalanceEnough || !isValidGas) return; - return handleConfirmTransaction(); - }, [isMessageRequest, isBalanceEnough, isValidGas, handleConfirmTransaction, handleSignMessage]); - - return { onConfirm }; -}; diff --git a/src/hooks/useHasEnoughBalance.ts b/src/hooks/useHasEnoughBalance.ts deleted file mode 100644 index 56c5c766ae1..00000000000 --- a/src/hooks/useHasEnoughBalance.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { useEffect, useState } from 'react'; -import { fromWei, greaterThanOrEqualTo } from '@/helpers/utilities'; -import BigNumber from 'bignumber.js'; -import { SelectedGasFee } from '@/entities'; -import { ChainId } from '@/__swaps__/types/chains'; - -type WalletBalance = { - amount: string | number; - display: string; - isLoaded: boolean; - symbol: string; -}; - -type BalanceCheckParams = { - isMessageRequest: boolean; - walletBalance: WalletBalance; - currentChainId: ChainId; - selectedGasFee: SelectedGasFee; - req: any; -}; - -export const useHasEnoughBalance = ({ isMessageRequest, walletBalance, currentChainId, selectedGasFee, req }: BalanceCheckParams) => { - const [isBalanceEnough, setIsBalanceEnough] = useState(); - - useEffect(() => { - if (isMessageRequest) { - setIsBalanceEnough(true); - return; - } - - const { gasFee } = selectedGasFee; - if (!walletBalance.isLoaded || !currentChainId || !gasFee?.estimatedFee) { - return; - } - - const txFeeAmount = fromWei(gasFee?.maxFee?.value?.amount ?? 0); - const balanceAmount = walletBalance.amount; - const value = req?.value ?? 0; - - const totalAmount = new BigNumber(fromWei(value)).plus(txFeeAmount); - const isEnough = greaterThanOrEqualTo(balanceAmount, totalAmount); - - setIsBalanceEnough(isEnough); - }, [isMessageRequest, currentChainId, selectedGasFee, walletBalance, req]); - - return { isBalanceEnough }; -}; diff --git a/src/hooks/useNonceForDisplay.ts b/src/hooks/useNonceForDisplay.ts deleted file mode 100644 index 34ba719c5e9..00000000000 --- a/src/hooks/useNonceForDisplay.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useEffect, useState } from 'react'; -import { getNextNonce } from '@/state/nonces'; -import { ChainId } from '@/__swaps__/types/chains'; -import { ethereumUtils } from '@/utils'; - -type UseNonceParams = { - isMessageRequest: boolean; - currentAddress: string; - currentChainId: ChainId; -}; - -export const useNonceForDisplay = ({ isMessageRequest, currentAddress, currentChainId }: UseNonceParams) => { - const [nonceForDisplay, setNonceForDisplay] = useState(); - - useEffect(() => { - if (!isMessageRequest && !nonceForDisplay) { - (async () => { - try { - const nonce = await getNextNonce({ address: currentAddress, network: ethereumUtils.getNetworkFromChainId(currentChainId) }); - if (nonce || nonce === 0) { - const nonceAsString = nonce.toString(); - setNonceForDisplay(nonceAsString); - } - } catch (error) { - console.error('Failed to get nonce for display:', error); - } - })(); - } - }, [currentAddress, currentChainId, isMessageRequest, nonceForDisplay]); - - return { nonceForDisplay }; -}; diff --git a/src/hooks/useProviderSetup.ts b/src/hooks/useProviderSetup.ts deleted file mode 100644 index 83b372bf1a0..00000000000 --- a/src/hooks/useProviderSetup.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { useEffect, useState } from 'react'; -import { getFlashbotsProvider, getProvider } from '@/handlers/web3'; -import { StaticJsonRpcProvider } from '@ethersproject/providers'; -import { ethereumUtils } from '@/utils'; -import { getOnchainAssetBalance } from '@/handlers/assets'; -import { ParsedAddressAsset } from '@/entities'; -import { ChainId } from '@/__swaps__/types/chains'; - -export const useProviderSetup = (currentChainId: ChainId, accountAddress: string) => { - const [provider, setProvider] = useState(null); - const [nativeAsset, setNativeAsset] = useState(null); - - useEffect(() => { - const initProvider = async () => { - let p; - if (currentChainId === ChainId.mainnet) { - p = await getFlashbotsProvider(); - } else { - p = getProvider({ chainId: currentChainId }); - } - setProvider(p); - }; - initProvider(); - }, [currentChainId]); - - useEffect(() => { - const fetchNativeAsset = async () => { - if (provider) { - const asset = await ethereumUtils.getNativeAssetForNetwork(currentChainId, accountAddress); - if (asset) { - const balance = await getOnchainAssetBalance( - asset, - accountAddress, - ethereumUtils.getNetworkFromChainId(currentChainId), - provider - ); - if (balance) { - const assetWithOnchainBalance: ParsedAddressAsset = { ...asset, balance }; - setNativeAsset(assetWithOnchainBalance); - } else { - setNativeAsset(asset); - } - } - } - }; - fetchNativeAsset(); - }, [accountAddress, currentChainId, provider]); - - return { provider, nativeAsset }; -}; diff --git a/src/hooks/useSubmitTransaction.ts b/src/hooks/useSubmitTransaction.ts deleted file mode 100644 index 166f2f4004f..00000000000 --- a/src/hooks/useSubmitTransaction.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { useCallback, useState } from 'react'; -import { performanceTracking, TimeToSignOperation } from '@/state/performance/performance'; -import Routes from '@/navigation/routesNames'; -import { useNavigation } from '@/navigation'; -import { RequestSource } from '@/utils/requestNavigationHandlers'; -import { SCREEN_FOR_REQUEST_SOURCE } from '@/components/Transactions/constants'; - -export const useTransactionSubmission = ({ - isBalanceEnough, - accountInfo, - onConfirm, - source, -}: { - isBalanceEnough: boolean | undefined; - accountInfo: { isHardwareWallet: boolean }; - onConfirm: () => Promise; - source: RequestSource; -}) => { - const [isAuthorizing, setIsAuthorizing] = useState(false); - const { navigate } = useNavigation(); - - const onPressSend = useCallback(async () => { - if (isAuthorizing) return; - try { - setIsAuthorizing(true); - await onConfirm(); - } catch (error) { - console.error('Error while sending transaction:', error); - } finally { - setIsAuthorizing(false); - } - }, [isAuthorizing, onConfirm]); - - const submitFn = useCallback( - () => - performanceTracking.getState().executeFn({ - fn: async () => { - if (!isBalanceEnough) { - navigate(Routes.ADD_CASH_SHEET); - return; - } - if (accountInfo.isHardwareWallet) { - navigate(Routes.HARDWARE_WALLET_TX_NAVIGATOR, { submit: onPressSend }); - } else { - await onPressSend(); - } - }, - operation: TimeToSignOperation.CallToAction, - screen: SCREEN_FOR_REQUEST_SOURCE[source], - })(), - [accountInfo.isHardwareWallet, isBalanceEnough, navigate, onPressSend, source] - ); - - return { submitFn, isAuthorizing }; -}; diff --git a/src/hooks/useTransactionSetup.ts b/src/hooks/useTransactionSetup.ts deleted file mode 100644 index 6bc4f9528ab..00000000000 --- a/src/hooks/useTransactionSetup.ts +++ /dev/null @@ -1,76 +0,0 @@ -import * as i18n from '@/languages'; -import { useCallback, useEffect, useState } from 'react'; -import { InteractionManager } from 'react-native'; -import useGas from '@/hooks/useGas'; -import { methodRegistryLookupAndParse } from '@/utils/methodRegistry'; -import { analytics } from '@/analytics'; -import { event } from '@/analytics/event'; -import { RequestSource } from '@/utils/requestNavigationHandlers'; -import { ChainId } from '@/__swaps__/types/chains'; -import { ethereumUtils } from '@/utils'; - -type TransactionSetupParams = { - currentChainId: ChainId; - startPollingGasFees: ReturnType['startPollingGasFees']; - stopPollingGasFees: ReturnType['stopPollingGasFees']; - isMessageRequest: boolean; - transactionDetails: any; - source: RequestSource; -}; - -export const useTransactionSetup = ({ - currentChainId, - startPollingGasFees, - stopPollingGasFees, - isMessageRequest, - transactionDetails, - source, -}: TransactionSetupParams) => { - const [methodName, setMethodName] = useState(null); - - const fetchMethodName = useCallback( - async (data: string) => { - const methodSignaturePrefix = data.substr(0, 10); - try { - const { name } = await methodRegistryLookupAndParse(methodSignaturePrefix, currentChainId); - if (name) { - setMethodName(name); - } - } catch (e) { - setMethodName(data); - } - }, - [currentChainId] - ); - - useEffect(() => { - InteractionManager.runAfterInteractions(() => { - if (currentChainId) { - if (!isMessageRequest) { - const network = ethereumUtils.getNetworkFromChainId(currentChainId); - startPollingGasFees(network); - fetchMethodName(transactionDetails?.payload?.params?.[0].data); - } else { - setMethodName(i18n.t(i18n.l.wallet.message_signing.request)); - } - analytics.track(event.txRequestShownSheet, { source }); - } - }); - - return () => { - if (!isMessageRequest) { - stopPollingGasFees(); - } - }; - }, [ - isMessageRequest, - currentChainId, - transactionDetails?.payload?.params, - source, - fetchMethodName, - startPollingGasFees, - stopPollingGasFees, - ]); - - return { methodName }; -}; diff --git a/src/languages/en_US.json b/src/languages/en_US.json index 4f157ecc5c2..5130d449fc6 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -2835,7 +2835,6 @@ "unavailable_personal_sign": "Simulation for personal signs is not yet supported", "unavailable_zora_network": "Simulation on Zora is not yet supported", "failed_to_simulate": "The simulation failed, which suggests your transaction is likely to fail. This may be an issue with the app you’re using.", - "tx_api_error": "We are unable to determine whether or not your transaction will succeed or fail. Proceed with caution.", "warning": "No known malicious behavior was detected, but this transaction has characteristics that may pose a risk to your wallet.", "malicious": "Signing this transaction could result in losing access to everything in your wallet." }, diff --git a/src/navigation/Routes.android.tsx b/src/navigation/Routes.android.tsx index 007652f2302..39d424b8c06 100644 --- a/src/navigation/Routes.android.tsx +++ b/src/navigation/Routes.android.tsx @@ -1,5 +1,5 @@ /* eslint-disable react/jsx-props-no-spreading */ -import { NavigationContainer, NavigationContainerRef } from '@react-navigation/native'; +import { NavigationContainer } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; import React, { useContext } from 'react'; import { AddCashSheet } from '../screens/AddCash'; @@ -90,7 +90,6 @@ import { SwapScreen } from '@/__swaps__/screens/Swap/Swap'; import { useRemoteConfig } from '@/model/remoteConfig'; import { ControlPanel } from '@/components/DappBrowser/control-panel/ControlPanel'; import { ClaimRewardsPanel } from '@/screens/points/claim-flow/ClaimRewardsPanel'; -import { RootStackParamList } from './types'; const Stack = createStackNavigator(); const OuterStack = createStackNavigator(); @@ -270,13 +269,25 @@ function AuthNavigator() { ); } -const AppContainerWithAnalytics = React.forwardRef, { onReady: () => void }>((props, ref) => ( - - - - - -)); +const AppContainerWithAnalytics = React.forwardRef( + ( + props: { + onReady: () => void; + }, + ref + ) => ( + + + + + + ) +); AppContainerWithAnalytics.displayName = 'AppContainerWithAnalytics'; diff --git a/src/navigation/Routes.ios.tsx b/src/navigation/Routes.ios.tsx index 4f1a8e96407..c468af96ecb 100644 --- a/src/navigation/Routes.ios.tsx +++ b/src/navigation/Routes.ios.tsx @@ -1,5 +1,5 @@ /* eslint-disable react/jsx-props-no-spreading */ -import { NavigationContainer, NavigationContainerRef } from '@react-navigation/native'; +import { NavigationContainer } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; import React, { useContext } from 'react'; import { AddCashSheet } from '../screens/AddCash'; @@ -104,7 +104,6 @@ import { useRemoteConfig } from '@/model/remoteConfig'; import CheckIdentifierScreen from '@/screens/CheckIdentifierScreen'; import { ControlPanel } from '@/components/DappBrowser/control-panel/ControlPanel'; import { ClaimRewardsPanel } from '@/screens/points/claim-flow/ClaimRewardsPanel'; -import { RootStackParamList } from './types'; const Stack = createStackNavigator(); const NativeStack = createNativeStackNavigator(); @@ -293,13 +292,25 @@ function NativeStackNavigator() { ); } -const AppContainerWithAnalytics = React.forwardRef, { onReady: () => void }>((props, ref) => ( - - - - - -)); +const AppContainerWithAnalytics = React.forwardRef( + ( + props: { + onReady: () => void; + }, + ref + ) => ( + + + + + + ) +); AppContainerWithAnalytics.displayName = 'AppContainerWithAnalytics'; diff --git a/src/navigation/config.tsx b/src/navigation/config.tsx index 8324c450a34..3e9de2383ff 100644 --- a/src/navigation/config.tsx +++ b/src/navigation/config.tsx @@ -29,7 +29,6 @@ import { BottomSheetNavigationOptions } from '@/navigation/bottom-sheet/types'; import { Box } from '@/design-system'; import { IS_ANDROID } from '@/env'; import { SignTransactionSheetRouteProp } from '@/screens/SignTransactionSheet'; -import { RequestSource } from '@/utils/requestNavigationHandlers'; export const sharedCoolModalTopOffset = safeAreaInsetValues.top; @@ -278,7 +277,7 @@ export const signTransactionSheetConfig = { options: ({ route }: { route: SignTransactionSheetRouteProp }) => ({ ...buildCoolModalConfig({ ...route.params, - backgroundOpacity: route?.params?.source === RequestSource.WALLETCONNECT ? 1 : 0.7, + backgroundOpacity: route?.params?.source === 'walletconnect' ? 1 : 0.7, cornerRadius: 0, springDamping: 1, topOffset: 0, diff --git a/src/notifications/tokens.ts b/src/notifications/tokens.ts index 4bc8b78a88d..8c88b9ee1bf 100644 --- a/src/notifications/tokens.ts +++ b/src/notifications/tokens.ts @@ -2,7 +2,7 @@ import messaging from '@react-native-firebase/messaging'; import { getLocal, saveLocal } from '@/handlers/localstorage/common'; import { getPermissionStatus } from '@/notifications/permissions'; -import { logger } from '@/logger'; +import { logger, RainbowError } from '@/logger'; export const registerTokenRefreshListener = () => messaging().onTokenRefresh(fcmToken => { diff --git a/src/parsers/requests.js b/src/parsers/requests.js index a3fa2ac7dc6..ae7da2d87b6 100644 --- a/src/parsers/requests.js +++ b/src/parsers/requests.js @@ -9,7 +9,7 @@ import { isSignTypedData, SIGN, PERSONAL_SIGN, SEND_TRANSACTION, SIGN_TRANSACTIO import { isAddress } from '@ethersproject/address'; import { toUtf8String } from '@ethersproject/strings'; -export const getRequestDisplayDetails = async (payload, nativeCurrency, chainId) => { +export const getRequestDisplayDetails = (payload, nativeCurrency, chainId) => { const timestampInMs = Date.now(); if (payload.method === SEND_TRANSACTION || payload.method === SIGN_TRANSACTION) { const transaction = Object.assign(payload?.params?.[0] ?? null); @@ -75,9 +75,9 @@ const getMessageDisplayDetails = (message, timestampInMs) => ({ timestampInMs, }); -const getTransactionDisplayDetails = async (transaction, nativeCurrency, timestampInMs, chainId) => { +const getTransactionDisplayDetails = (transaction, nativeCurrency, timestampInMs, chainId) => { const tokenTransferHash = smartContractMethods.token_transfer.hash; - const nativeAsset = await ethereumUtils.getNativeAssetForNetwork(chainId); + const nativeAsset = ethereumUtils.getNativeAssetForNetwork(chainId); if (transaction.data === '0x') { const value = fromWei(convertHexToString(transaction.value)); const priceUnit = nativeAsset?.price?.value ?? 0; diff --git a/src/redux/requests.ts b/src/redux/requests.ts index 05350697d48..aa5196b9cd4 100644 --- a/src/redux/requests.ts +++ b/src/redux/requests.ts @@ -71,7 +71,7 @@ export interface WalletconnectRequestData extends RequestData { /** * Display details loaded for a request. */ -export interface RequestDisplayDetails { +interface RequestDisplayDetails { /** * Data loaded for the request, depending on the type of request. */ @@ -154,7 +154,7 @@ export const addRequestToApprove = icons?: string[]; } ) => - async (dispatch: Dispatch, getState: AppGetState) => { + (dispatch: Dispatch, getState: AppGetState) => { const { requests } = getState().requests; const { walletConnectors } = getState().walletconnect; const { accountAddress, network, nativeCurrency } = getState().settings; @@ -163,7 +163,7 @@ export const addRequestToApprove = const chainId = walletConnector._chainId; // @ts-expect-error "_accounts" is private. const address = walletConnector._accounts[0]; - const displayDetails = await getRequestDisplayDetails(payload, nativeCurrency, chainId); + const displayDetails = getRequestDisplayDetails(payload, nativeCurrency, chainId); const oneHourAgoTs = Date.now() - EXPIRATION_THRESHOLD_IN_MS; // @ts-expect-error This fails to compile as `displayDetails` does not // always return an object with `timestampInMs`. Still, the error thrown diff --git a/src/redux/walletconnect.ts b/src/redux/walletconnect.ts index fa1b19b631a..da0ea48400c 100644 --- a/src/redux/walletconnect.ts +++ b/src/redux/walletconnect.ts @@ -559,9 +559,7 @@ const listenOnNewMessages = return; } const { requests: pendingRequests } = getState().requests; - const request = !pendingRequests[requestId] - ? await dispatch(addRequestToApprove(clientId, peerId, requestId, payload, peerMeta)) - : null; + const request = !pendingRequests[requestId] ? dispatch(addRequestToApprove(clientId, peerId, requestId, payload, peerMeta)) : null; if (request) { handleWalletConnectRequest(request); InteractionManager.runAfterInteractions(() => { diff --git a/src/resources/transactions/transactionSimulation.ts b/src/resources/transactions/transactionSimulation.ts deleted file mode 100644 index 01d76df5de2..00000000000 --- a/src/resources/transactions/transactionSimulation.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { createQueryKey, QueryConfig, QueryFunctionArgs } from '@/react-query'; -import { useQuery } from '@tanstack/react-query'; -import { RainbowError, logger } from '@/logger'; -import { metadataPOSTClient } from '@/graphql'; -import { TransactionErrorType, TransactionScanResultType, TransactionSimulationResult } from '@/graphql/__generated__/metadataPOST'; -import { isNil } from 'lodash'; -import { RequestData } from '@/redux/requests'; -import { ChainId } from '@/__swaps__/types/chains'; - -type SimulationArgs = { - accountAddress: string; - currentChainId: ChainId; - isMessageRequest: boolean; - nativeCurrency: string; - req: any; // Replace 'any' with the correct type for 'req' - requestMessage: string; - simulationUnavailable: boolean; - transactionDetails: RequestData; -}; - -type SimulationResult = { - simulationData: TransactionSimulationResult | undefined; - simulationError: TransactionErrorType | undefined; - simulationScanResult: TransactionScanResultType | undefined; -}; - -const simulationQueryKey = ({ - accountAddress, - currentChainId, - isMessageRequest, - nativeCurrency, - req, - requestMessage, - simulationUnavailable, - transactionDetails, -}: SimulationArgs) => - createQueryKey( - 'txSimulation', - { - accountAddress, - currentChainId, - isMessageRequest, - nativeCurrency, - req, - requestMessage, - simulationUnavailable, - transactionDetails, - }, - { persisterVersion: 1 } - ); - -const fetchSimulation = async ({ - queryKey: [ - { accountAddress, currentChainId, isMessageRequest, nativeCurrency, req, requestMessage, simulationUnavailable, transactionDetails }, - ], -}: QueryFunctionArgs): Promise => { - try { - let simulationData; - - if (isMessageRequest) { - simulationData = await metadataPOSTClient.simulateMessage({ - address: accountAddress, - chainId: currentChainId, - message: { - method: transactionDetails?.payload?.method, - params: [requestMessage], - }, - domain: transactionDetails?.dappUrl, - }); - - if (isNil(simulationData?.simulateMessage?.simulation) && isNil(simulationData?.simulateMessage?.error)) { - return { - simulationData: { in: [], out: [], approvals: [] }, - simulationError: undefined, - simulationScanResult: simulationData?.simulateMessage?.scanning?.result, - }; - } else if (simulationData?.simulateMessage?.error && !simulationUnavailable) { - return { - simulationData: undefined, - simulationError: simulationData?.simulateMessage?.error?.type, - simulationScanResult: simulationData?.simulateMessage?.scanning?.result, - }; - } else if (simulationData.simulateMessage?.simulation && !simulationUnavailable) { - return { - simulationData: simulationData.simulateMessage?.simulation, - simulationError: undefined, - simulationScanResult: simulationData?.simulateMessage?.scanning?.result, - }; - } - } else { - simulationData = await metadataPOSTClient.simulateTransactions({ - chainId: currentChainId, - currency: nativeCurrency?.toLowerCase(), - transactions: [ - { - from: req?.from, - to: req?.to, - data: req?.data || '0x', - value: req?.value || '0x0', - }, - ], - domain: transactionDetails?.dappUrl, - }); - - if (isNil(simulationData?.simulateTransactions?.[0]?.simulation) && isNil(simulationData?.simulateTransactions?.[0]?.error)) { - return { - simulationData: { in: [], out: [], approvals: [] }, - simulationError: undefined, - simulationScanResult: simulationData?.simulateTransactions?.[0]?.scanning?.result, - }; - } else if (simulationData?.simulateTransactions?.[0]?.error) { - return { - simulationData: undefined, - simulationError: simulationData?.simulateTransactions?.[0]?.error?.type, - simulationScanResult: simulationData?.simulateTransactions[0]?.scanning?.result, - }; - } else if (simulationData.simulateTransactions?.[0]?.simulation) { - return { - simulationData: simulationData.simulateTransactions[0]?.simulation, - simulationError: undefined, - simulationScanResult: simulationData?.simulateTransactions[0]?.scanning?.result, - }; - } - } - - return { - simulationData: undefined, - simulationError: undefined, - simulationScanResult: undefined, - }; - } catch (error) { - logger.error(new RainbowError('Error while simulating'), { error }); - throw error; - } -}; - -export const useSimulation = ( - args: SimulationArgs, - config: QueryConfig> = {} -) => { - return useQuery(simulationQueryKey(args), fetchSimulation, { - enabled: !!args.accountAddress && !!args.currentChainId, - retry: 3, - refetchOnWindowFocus: false, - staleTime: Infinity, - ...config, - }); -}; diff --git a/src/screens/SignTransactionSheet.tsx b/src/screens/SignTransactionSheet.tsx index c3961c962ef..49dfac6f483 100644 --- a/src/screens/SignTransactionSheet.tsx +++ b/src/screens/SignTransactionSheet.tsx @@ -1,31 +1,68 @@ -import React, { useCallback, useMemo } from 'react'; -import { AnimatePresence, MotiView } from 'moti'; +/* eslint-disable no-nested-ternary */ +import BigNumber from 'bignumber.js'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { AnimatePresence, MotiView, TransitionConfig } from 'moti'; import * as i18n from '@/languages'; -import { Image, InteractionManager, PixelRatio, ScrollView } from 'react-native'; -import Animated from 'react-native-reanimated'; +import { Image, InteractionManager, PixelRatio, ScrollView, StyleProp, TouchableWithoutFeedback, ViewStyle } from 'react-native'; +import LinearGradient from 'react-native-linear-gradient'; +import Animated, { + Easing, + SharedValue, + interpolate, + interpolateColor, + measure, + runOnJS, + runOnUI, + useAnimatedReaction, + useAnimatedRef, + useAnimatedStyle, + useSharedValue, + withRepeat, + withTiming, +} from 'react-native-reanimated'; import { Transaction } from '@ethersproject/transactions'; +import { ButtonPressAnimation } from '@/components/animations'; import { ChainImage } from '@/components/coin-icon/ChainImage'; import { SheetActionButton } from '@/components/sheet'; import { Bleed, Box, Columns, Inline, Inset, Stack, Text, globalColors, useBackgroundColor, useForegroundColor } from '@/design-system'; -import { NewTransaction } from '@/entities'; +import { TextColor } from '@/design-system/color/palettes'; +import { NewTransaction, ParsedAddressAsset } from '@/entities'; import { useNavigation } from '@/navigation'; import { useTheme } from '@/theme'; -import { deviceUtils, ethereumUtils } from '@/utils'; +import { abbreviations, deviceUtils, ethereumUtils, safeAreaInsetValues } from '@/utils'; import { PanGestureHandler } from 'react-native-gesture-handler'; import { RouteProp, useRoute } from '@react-navigation/native'; -import { TransactionScanResultType } from '@/graphql/__generated__/metadataPOST'; +import { metadataPOSTClient } from '@/graphql'; +import { + TransactionAssetType, + TransactionErrorType, + TransactionSimulationAsset, + TransactionSimulationMeta, + TransactionSimulationResult, + TransactionScanResultType, +} from '@/graphql/__generated__/metadataPOST'; import { Network } from '@/networks/types'; -import { convertHexToString, delay, greaterThan, omitFlatten } from '@/helpers/utilities'; +import { + convertAmountToNativeDisplay, + convertHexToString, + convertRawAmountToBalance, + delay, + fromWei, + greaterThan, + greaterThanOrEqualTo, + omitFlatten, +} from '@/helpers/utilities'; import { findWalletWithAccount } from '@/helpers/findWalletWithAccount'; import { getAccountProfileInfo } from '@/helpers/accountInfo'; -import { useAccountSettings, useGas, useSwitchWallet, useWallets } from '@/hooks'; +import { useAccountSettings, useClipboard, useDimensions, useGas, useSwitchWallet, useWallets } from '@/hooks'; import ImageAvatar from '@/components/contacts/ImageAvatar'; import { ContactAvatar } from '@/components/contacts'; import { IS_IOS } from '@/env'; -import { estimateGasWithPadding, getProvider, getProviderForNetwork, toHex } from '@/handlers/web3'; +import { estimateGas, estimateGasWithPadding, getFlashbotsProvider, getProvider, toHex } from '@/handlers/web3'; +import { StaticJsonRpcProvider } from '@ethersproject/providers'; import { GasSpeedButton } from '@/components/gas'; import { getNetworkObj, getNetworkObject } from '@/networks'; import { RainbowError, logger } from '@/logger'; @@ -35,45 +72,71 @@ import { SIGN_TYPED_DATA, SIGN_TYPED_DATA_V4, isMessageDisplayType, - isPersonalSign, + isPersonalSign as checkIsPersonalSign, + isSignTypedData, } from '@/utils/signingMethods'; -import { isNil } from 'lodash'; +import { isEmpty, isNil } from 'lodash'; +import Routes from '@/navigation/routesNames'; import { parseGasParamsForTransaction } from '@/parsers/gas'; import { loadWallet, sendTransaction, signPersonalMessage, signTransaction, signTypedDataMessage } from '@/model/wallet'; import { analyticsV2 as analytics } from '@/analytics'; import { maybeSignUri } from '@/handlers/imgix'; +import { RPCMethod } from '@/walletConnect/types'; import { isAddress } from '@ethersproject/address'; +import { methodRegistryLookupAndParse } from '@/utils/methodRegistry'; +import { sanitizeTypedData } from '@/utils/signingUtils'; import { hexToNumber, isHex } from 'viem'; import { addNewTransaction } from '@/state/pendingTransactions'; import { getNextNonce } from '@/state/nonces'; +import RainbowCoinIcon from '@/components/coin-icon/RainbowCoinIcon'; +import { useExternalToken } from '@/resources/assets/externalAssetsQuery'; import { RequestData } from '@/redux/requests'; import { RequestSource } from '@/utils/requestNavigationHandlers'; import { event } from '@/analytics/event'; -import { performanceTracking, TimeToSignOperation } from '@/state/performance/performance'; -import { useSimulation } from '@/resources/transactions/transactionSimulation'; -import { TransactionSimulationCard } from '@/components/Transactions/TransactionSimulationCard'; -import { TransactionDetailsCard } from '@/components/Transactions/TransactionDetailsCard'; -import { TransactionMessageCard } from '@/components/Transactions/TransactionMessageCard'; -import { VerifiedBadge } from '@/components/Transactions/TransactionIcons'; -import { - SCREEN_FOR_REQUEST_SOURCE, - EXPANDED_CARD_BOTTOM_INSET, - GAS_BUTTON_SPACE, - motiTimingConfig, - SCREEN_BOTTOM_INSET, - infoForEventType, -} from '@/components/Transactions/constants'; -import { useCalculateGasLimit } from '@/hooks/useCalculateGasLimit'; -import { useTransactionSetup } from '@/hooks/useTransactionSetup'; -import { useHasEnoughBalance } from '@/hooks/useHasEnoughBalance'; -import { useNonceForDisplay } from '@/hooks/useNonceForDisplay'; -import { useProviderSetup } from '@/hooks/useProviderSetup'; -import { useTransactionSubmission } from '@/hooks/useSubmitTransaction'; -import { useConfirmTransaction } from '@/hooks/useConfirmTransaction'; +import { getOnchainAssetBalance } from '@/handlers/assets'; +import { performanceTracking, Screens, TimeToSignOperation } from '@/state/performance/performance'; import { ChainId } from '@/__swaps__/types/chains'; +const COLLAPSED_CARD_HEIGHT = 56; +const MAX_CARD_HEIGHT = 176; + +const CARD_ROW_HEIGHT = 12; +const SMALL_CARD_ROW_HEIGHT = 10; +const CARD_BORDER_WIDTH = 1.5; + +const EXPANDED_CARD_TOP_INSET = safeAreaInsetValues.top + 72; +const SCREEN_BOTTOM_INSET = safeAreaInsetValues.bottom + 20; + +const GAS_BUTTON_SPACE = + 30 + // GasSpeedButton height + 24; // Between GasSpeedButton and bottom of sheet + +const EXPANDED_CARD_BOTTOM_INSET = + SCREEN_BOTTOM_INSET + + 24 + // Between bottom of sheet and bottom of Cancel/Confirm + 52 + // Cancel/Confirm height + 24 + // Between Cancel/Confirm and wallet avatar row + 44 + // Wallet avatar row height + 24; // Between wallet avatar row and bottom of expandable area + +const rotationConfig = { + duration: 2100, + easing: Easing.linear, +}; + +const timingConfig = { + duration: 300, + easing: Easing.bezier(0.2, 0, 0, 1), +}; + +const motiTimingConfig: TransitionConfig = { + duration: 225, + easing: Easing.bezier(0.2, 0, 0, 1), + type: 'timing', +}; + type SignTransactionSheetParams = { transactionDetails: RequestData; onSuccess: (hash: string) => void; @@ -85,12 +148,20 @@ type SignTransactionSheetParams = { source: RequestSource; }; +const SCREEN_FOR_REQUEST_SOURCE = { + browser: Screens.DAPP_BROWSER, + walletconnect: Screens.WALLETCONNECT, +}; + export type SignTransactionSheetRouteProp = RouteProp<{ SignTransactionSheet: SignTransactionSheetParams }, 'SignTransactionSheet'>; export const SignTransactionSheet = () => { - const { goBack } = useNavigation(); + const { goBack, navigate } = useNavigation(); const { colors, isDarkMode } = useTheme(); const { accountAddress, nativeCurrency } = useAccountSettings(); + const [simulationData, setSimulationData] = useState(); + const [simulationError, setSimulationError] = useState(undefined); + const [simulationScanResult, setSimulationScanResult] = useState(undefined); const { params: routeParams } = useRoute(); const { wallets, walletNames } = useWallets(); @@ -106,14 +177,22 @@ export const SignTransactionSheet = () => { source, } = routeParams; - const { provider, nativeAsset } = useProviderSetup(currentChainId, accountAddress); - const isMessageRequest = isMessageDisplayType(transactionDetails.payload.method); - const isPersonalSignRequest = isPersonalSign(transactionDetails.payload.method); + + const isPersonalSign = checkIsPersonalSign(transactionDetails.payload.method); const label = useForegroundColor('label'); const surfacePrimary = useBackgroundColor('surfacePrimary'); + const [provider, setProvider] = useState(null); + const [isAuthorizing, setIsAuthorizing] = useState(false); + const [isLoading, setIsLoading] = useState(!isPersonalSign); + const [methodName, setMethodName] = useState(null); + const calculatingGasLimit = useRef(false); + const [isBalanceEnough, setIsBalanceEnough] = useState(); + const [nonceForDisplay, setNonceForDisplay] = useState(); + + const [nativeAsset, setNativeAsset] = useState(null); const formattedDappUrl = useMemo(() => { try { const { hostname } = new URL(transactionDetails?.dappUrl); @@ -123,16 +202,107 @@ export const SignTransactionSheet = () => { } }, [transactionDetails]); + const { + gasLimit, + isValidGas, + startPollingGasFees, + stopPollingGasFees, + isSufficientGas, + updateTxFee, + selectedGasFee, + gasFeeParamsBySpeed, + } = useGas(); + + const simulationUnavailable = isPersonalSign; + + const itemCount = (simulationData?.in?.length || 0) + (simulationData?.out?.length || 0) + (simulationData?.approvals?.length || 0); + + const noChanges = !!(simulationData && itemCount === 0) && simulationScanResult === TransactionScanResultType.Ok; + const req = transactionDetails?.payload?.params?.[0]; const request = useMemo(() => { return isMessageRequest - ? { message: transactionDetails?.displayDetails?.request || '' } + ? { message: transactionDetails?.displayDetails?.request } : { ...transactionDetails?.displayDetails?.request, - nativeAsset, + nativeAsset: nativeAsset, }; }, [isMessageRequest, transactionDetails?.displayDetails?.request, nativeAsset]); + const calculateGasLimit = useCallback(async () => { + calculatingGasLimit.current = true; + const txPayload = req; + if (isHex(txPayload?.type)) { + txPayload.type = hexToNumber(txPayload?.type); + } + // use the default + let gas = txPayload.gasLimit || txPayload.gas; + + const provider = getProvider({ chainId: currentChainId }); + try { + // attempt to re-run estimation + logger.debug('[SignTransactionSheet]: Estimating gas limit', { gas }, logger.DebugContext.walletconnect); + // safety precaution: we want to ensure these properties are not used for gas estimation + const cleanTxPayload = omitFlatten(txPayload, ['gas', 'gasLimit', 'gasPrice', 'maxFeePerGas', 'maxPriorityFeePerGas']); + const rawGasLimit = await estimateGas(cleanTxPayload, provider); + logger.debug('[SignTransactionSheet]: Estimated gas limit', { rawGasLimit }, logger.DebugContext.walletconnect); + if (rawGasLimit) { + gas = toHex(rawGasLimit); + } + } catch (error) { + logger.error(new RainbowError('[SignTransactionSheet]: error estimating gas'), { error }); + } finally { + logger.debug('[SignTransactionSheet]: Setting gas limit to', { gas: convertHexToString(gas) }, logger.DebugContext.walletconnect); + const networkObject = getNetworkObject({ chainId: currentChainId }); + if (networkObject && networkObject.gas.OptimismTxFee) { + const l1GasFeeOptimism = await ethereumUtils.calculateL1FeeOptimism(txPayload, provider); + updateTxFee(gas, null, l1GasFeeOptimism); + } else { + updateTxFee(gas, null); + } + } + }, [currentChainId, req, updateTxFee]); + + const fetchMethodName = useCallback( + async (data: string) => { + const methodSignaturePrefix = data.substr(0, 10); + try { + const { name } = await methodRegistryLookupAndParse(methodSignaturePrefix, currentChainId); + if (name) { + setMethodName(name); + } + } catch (e) { + setMethodName(data); + } + }, + [currentChainId] + ); + + // start polling for gas and get fn name + useEffect(() => { + InteractionManager.runAfterInteractions(() => { + if (currentChainId) { + if (!isMessageRequest) { + const network = ethereumUtils.getNetworkFromChainId(currentChainId); + startPollingGasFees(network); + fetchMethodName(transactionDetails?.payload?.params[0].data); + } else { + setMethodName(i18n.t(i18n.l.wallet.message_signing.request)); + } + analytics.track(event.txRequestShownSheet), { source }; + } + }); + }, [isMessageRequest, startPollingGasFees, fetchMethodName, transactionDetails?.payload?.params, source, currentChainId]); + + // get gas limit + useEffect(() => { + if (!isEmpty(gasFeeParamsBySpeed) && !calculatingGasLimit.current && !isMessageRequest && provider) { + InteractionManager.runAfterInteractions(() => { + calculateGasLimit(); + }); + } + }, [calculateGasLimit, gasLimit, gasFeeParamsBySpeed, isMessageRequest, provider, updateTxFee]); + const walletBalance = useMemo(() => { return { amount: nativeAsset?.balance?.amount || 0, @@ -142,67 +312,34 @@ export const SignTransactionSheet = () => { }; }, [nativeAsset?.balance?.amount, nativeAsset?.balance?.display, nativeAsset?.symbol]); - const { gasLimit, isValidGas, startPollingGasFees, stopPollingGasFees, updateTxFee, selectedGasFee, gasFeeParamsBySpeed } = useGas(); - - const { methodName } = useTransactionSetup({ - currentChainId, - startPollingGasFees, - stopPollingGasFees, - isMessageRequest, - transactionDetails, - source, - }); + // check native balance is sufficient + useEffect(() => { + if (isMessageRequest) { + setIsBalanceEnough(true); + return; + } - const { isBalanceEnough } = useHasEnoughBalance({ - isMessageRequest, - walletBalance, - currentChainId, - selectedGasFee, - req, - }); + const { gasFee } = selectedGasFee; + if (!walletBalance?.isLoaded || !currentChainId || !gasFee?.estimatedFee) { + return; + } - useCalculateGasLimit({ - isMessageRequest, - gasFeeParamsBySpeed, - provider, - req, - updateTxFee, - currentChainId, - }); + // Get the TX fee Amount + const txFeeAmount = fromWei(gasFee?.maxFee?.value?.amount ?? 0); - const { nonceForDisplay } = useNonceForDisplay({ - isMessageRequest, - currentAddress, - currentChainId, - }); + // Get the ETH balance + const balanceAmount = walletBalance?.amount ?? 0; - const { - data: simulationResult, - isLoading: txSimulationLoading, - error: txSimulationApiError, - } = useSimulation( - { - accountAddress, - currentChainId, - isMessageRequest, - nativeCurrency, - req, - requestMessage: request.message, - simulationUnavailable: isPersonalSignRequest, - transactionDetails, - }, - { - enabled: !isPersonalSignRequest, - } - ); + // Get the TX value + const txPayload = req; + const value = txPayload?.value ?? 0; - const itemCount = - (simulationResult?.simulationData?.in?.length || 0) + - (simulationResult?.simulationData?.out?.length || 0) + - (simulationResult?.simulationData?.approvals?.length || 0); + // Check that there's enough ETH to pay for everything! + const totalAmount = new BigNumber(fromWei(value)).plus(txFeeAmount); + const isEnough = greaterThanOrEqualTo(balanceAmount, totalAmount); - const noChanges = - !!(simulationResult?.simulationData && itemCount === 0) && simulationResult?.simulationScanResult === TransactionScanResultType.Ok; + setIsBalanceEnough(isEnough); + }, [isMessageRequest, isSufficientGas, selectedGasFee, walletBalance, req, currentChainId]); const accountInfo = useMemo(() => { const selectedWallet = wallets ? findWalletWithAccount(wallets, currentAddress) : undefined; @@ -214,6 +351,135 @@ export const SignTransactionSheet = () => { }; }, [wallets, currentAddress, walletNames]); + useEffect(() => { + const initProvider = async () => { + let p; + // check on this o.O + if (currentChainId === ChainId.mainnet) { + p = await getFlashbotsProvider(); + } else { + p = getProvider({ chainId: currentChainId }); + } + + setProvider(p); + }; + initProvider(); + }, [currentChainId, setProvider]); + + useEffect(() => { + (async () => { + const asset = await ethereumUtils.getNativeAssetForNetwork(currentChainId, accountInfo.address); + if (asset && provider) { + const balance = await getOnchainAssetBalance( + asset, + accountInfo.address, + ethereumUtils.getNetworkFromChainId(currentChainId), + provider + ); + if (balance) { + const assetWithOnchainBalance: ParsedAddressAsset = { ...asset, balance }; + setNativeAsset(assetWithOnchainBalance); + } else { + setNativeAsset(asset); + } + } + })(); + }, [accountInfo.address, currentChainId, provider]); + + useEffect(() => { + (async () => { + if (!isMessageRequest && !nonceForDisplay) { + try { + const nonce = await getNextNonce({ address: currentAddress, network: ethereumUtils.getNetworkFromChainId(currentChainId) }); + if (nonce || nonce === 0) { + const nonceAsString = nonce.toString(); + setNonceForDisplay(nonceAsString); + } + } catch (error) { + console.error('Failed to get nonce for display:', error); + } + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [accountInfo.address, currentChainId, getNextNonce, isMessageRequest]); + + useEffect(() => { + const timeout = setTimeout(async () => { + try { + let simulationData; + if (isMessageRequest) { + // Message Signing + simulationData = await metadataPOSTClient.simulateMessage({ + address: accountAddress, + chainId: currentChainId, + message: { + method: transactionDetails?.payload?.method, + params: [request.message], + }, + domain: transactionDetails?.dappUrl, + }); + // Handle message simulation response + if (isNil(simulationData?.simulateMessage?.simulation) && isNil(simulationData?.simulateMessage?.error)) { + setSimulationData({ in: [], out: [], approvals: [] }); + setSimulationScanResult(simulationData?.simulateMessage?.scanning?.result); + } else if (simulationData?.simulateMessage?.error && !simulationUnavailable) { + setSimulationError(simulationData?.simulateMessage?.error?.type); + setSimulationScanResult(simulationData?.simulateMessage?.scanning?.result); + setSimulationData(undefined); + } else if (simulationData.simulateMessage?.simulation && !simulationUnavailable) { + setSimulationData(simulationData.simulateMessage?.simulation); + setSimulationScanResult(simulationData?.simulateMessage?.scanning?.result); + } + } else { + // TX Signing + simulationData = await metadataPOSTClient.simulateTransactions({ + chainId: currentChainId, + currency: nativeCurrency?.toLowerCase(), + transactions: [ + { + from: req?.from, + to: req?.to, + data: req?.data, + value: req?.value || '0x0', + }, + ], + domain: transactionDetails?.dappUrl, + }); + // Handle TX simulation response + if (isNil(simulationData?.simulateTransactions?.[0]?.simulation) && isNil(simulationData?.simulateTransactions?.[0]?.error)) { + setSimulationData({ in: [], out: [], approvals: [] }); + setSimulationScanResult(simulationData?.simulateTransactions?.[0]?.scanning?.result); + } else if (simulationData?.simulateTransactions?.[0]?.error) { + setSimulationError(simulationData?.simulateTransactions?.[0]?.error?.type); + setSimulationData(undefined); + setSimulationScanResult(simulationData?.simulateTransactions[0]?.scanning?.result); + } else if (simulationData.simulateTransactions?.[0]?.simulation) { + setSimulationData(simulationData.simulateTransactions[0]?.simulation); + setSimulationScanResult(simulationData?.simulateTransactions[0]?.scanning?.result); + } + } + } catch (error) { + logger.error(new RainbowError('[SignTransactionSheet]: Error while simulating'), { error }); + } finally { + setIsLoading(false); + } + }, 750); + + return () => { + clearTimeout(timeout); + }; + }, [ + accountAddress, + currentChainId, + isMessageRequest, + isPersonalSign, + nativeCurrency, + req, + request.message, + simulationUnavailable, + transactionDetails, + ]); + const closeScreen = useCallback( (canceled: boolean) => performanceTracking.getState().executeFn({ @@ -260,6 +526,80 @@ export const SignTransactionSheet = () => { [accountInfo.isHardwareWallet, closeScreen, onCancelCallback, source, transactionDetails?.payload?.method] ); + const handleSignMessage = useCallback(async () => { + const message = transactionDetails?.payload?.params.find((p: string) => !isAddress(p)); + let response = null; + + const provider = getProvider({ chainId: currentChainId }); + if (!provider) { + return; + } + + const existingWallet = await performanceTracking.getState().executeFn({ + fn: loadWallet, + screen: SCREEN_FOR_REQUEST_SOURCE[source], + operation: TimeToSignOperation.KeychainRead, + })({ + address: accountInfo.address, + provider, + timeTracking: { + screen: SCREEN_FOR_REQUEST_SOURCE[source], + operation: TimeToSignOperation.Authentication, + }, + }); + + if (!existingWallet) { + return; + } + switch (transactionDetails?.payload?.method) { + case PERSONAL_SIGN: + response = await performanceTracking.getState().executeFn({ + fn: signPersonalMessage, + screen: SCREEN_FOR_REQUEST_SOURCE[source], + operation: TimeToSignOperation.SignTransaction, + })(message, existingWallet); + break; + case SIGN_TYPED_DATA_V4: + case SIGN_TYPED_DATA: + response = await performanceTracking.getState().executeFn({ + fn: signTypedDataMessage, + screen: SCREEN_FOR_REQUEST_SOURCE[source], + operation: TimeToSignOperation.SignTransaction, + })(message, existingWallet); + break; + default: + break; + } + + if (response?.result) { + analytics.track(event.txRequestApprove, { + source, + requestType: 'signature', + dappName: transactionDetails?.dappName, + dappUrl: transactionDetails?.dappUrl, + isHardwareWallet: accountInfo.isHardwareWallet, + network: ethereumUtils.getNetworkFromChainId(currentChainId), + }); + onSuccessCallback?.(response.result); + + closeScreen(false); + } else { + await onCancel(response?.error); + } + }, [ + transactionDetails?.payload?.params, + transactionDetails?.payload?.method, + transactionDetails?.dappName, + transactionDetails?.dappUrl, + currentChainId, + accountInfo.address, + accountInfo.isHardwareWallet, + source, + onSuccessCallback, + closeScreen, + onCancel, + ]); + const handleConfirmTransaction = useCallback(async () => { const sendInsteadOfSign = transactionDetails.payload.method === SEND_TRANSACTION; const txPayload = req; @@ -473,94 +813,44 @@ export const SignTransactionSheet = () => { onCancel, ]); - const handleSignMessage = useCallback(async () => { - const message = transactionDetails?.payload?.params.find((p: string) => !isAddress(p)); - let response = null; - - const provider = getProvider({ chainId: currentChainId }); - if (!provider) { - return; + const onConfirm = useCallback(async () => { + if (isMessageRequest) { + return handleSignMessage(); } + if (!isBalanceEnough || !isValidGas) return; + return handleConfirmTransaction(); + }, [handleConfirmTransaction, handleSignMessage, isBalanceEnough, isMessageRequest, isValidGas]); - const existingWallet = await performanceTracking.getState().executeFn({ - fn: loadWallet, - screen: SCREEN_FOR_REQUEST_SOURCE[source], - operation: TimeToSignOperation.KeychainRead, - })({ - address: accountInfo.address, - provider, - timeTracking: { - screen: SCREEN_FOR_REQUEST_SOURCE[source], - operation: TimeToSignOperation.Authentication, - }, - }); - - if (!existingWallet) { - return; - } - switch (transactionDetails?.payload?.method) { - case PERSONAL_SIGN: - response = await performanceTracking.getState().executeFn({ - fn: signPersonalMessage, - screen: SCREEN_FOR_REQUEST_SOURCE[source], - operation: TimeToSignOperation.SignTransaction, - })(message, existingWallet); - break; - case SIGN_TYPED_DATA_V4: - case SIGN_TYPED_DATA: - response = await performanceTracking.getState().executeFn({ - fn: signTypedDataMessage, - screen: SCREEN_FOR_REQUEST_SOURCE[source], - operation: TimeToSignOperation.SignTransaction, - })(message, existingWallet); - break; - default: - break; - } - - if (response?.result) { - analytics.track(event.txRequestApprove, { - source, - requestType: 'signature', - dappName: transactionDetails?.dappName, - dappUrl: transactionDetails?.dappUrl, - isHardwareWallet: accountInfo.isHardwareWallet, - network: ethereumUtils.getNetworkFromChainId(currentChainId), - }); - onSuccessCallback?.(response.result); - - closeScreen(false); - } else { - await onCancel(response?.error); + const onPressSend = useCallback(async () => { + if (isAuthorizing) return; + setIsAuthorizing(true); + try { + await onConfirm(); + setIsAuthorizing(false); + } catch (error) { + setIsAuthorizing(false); } - }, [ - transactionDetails?.payload?.params, - transactionDetails?.payload?.method, - transactionDetails?.dappName, - transactionDetails?.dappUrl, - currentChainId, - accountInfo.address, - accountInfo.isHardwareWallet, - source, - onSuccessCallback, - closeScreen, - onCancel, - ]); + }, [isAuthorizing, onConfirm]); - const { onConfirm } = useConfirmTransaction({ - isMessageRequest, - isBalanceEnough, - isValidGas, - handleSignMessage, - handleConfirmTransaction, - }); - - const { submitFn } = useTransactionSubmission({ - isBalanceEnough, - accountInfo, - onConfirm, - source, - }); + const submitFn = useCallback( + () => + performanceTracking.getState().executeFn({ + fn: async () => { + if (!isBalanceEnough) { + navigate(Routes.ADD_CASH_SHEET); + return; + } + if (accountInfo.isHardwareWallet) { + navigate(Routes.HARDWARE_WALLET_TX_NAVIGATOR, { submit: onPressSend }); + } else { + await onPressSend(); + } + }, + operation: TimeToSignOperation.CallToAction, + screen: SCREEN_FOR_REQUEST_SOURCE[source], + })(), + [accountInfo.isHardwareWallet, isBalanceEnough, navigate, onPressSend, source] + ); const onPressCancel = useCallback(() => onCancel(), [onCancel]); @@ -615,9 +905,8 @@ export const SignTransactionSheet = () => { { > {transactionDetails.dappName} - {source === RequestSource.BROWSER && } + {source === 'browser' && } {isMessageRequest @@ -638,36 +927,33 @@ export const SignTransactionSheet = () => { - {isMessageRequest ? ( - ) : ( - { /> { disabled={!canPressConfirm} size="big" weight="heavy" - color={ - simulationResult?.simulationError || - (simulationResult?.simulationScanResult && simulationResult?.simulationScanResult !== TransactionScanResultType.Ok) - ? simulationResult?.simulationScanResult === TransactionScanResultType.Warning - ? 'orange' - : colors.red - : undefined - } + // eslint-disable-next-line react/jsx-props-no-spreading + {...((simulationError || (simulationScanResult && simulationScanResult !== TransactionScanResultType.Ok)) && { + color: simulationScanResult === TransactionScanResultType.Warning ? 'orange' : colors.red, + })} /> @@ -767,7 +1049,7 @@ export const SignTransactionSheet = () => { )} - {source === RequestSource.BROWSER && ( + {source === 'browser' && ( { theme={'dark'} marginBottom={0} asset={undefined} - fallbackColor={simulationResult?.simulationError ? colors.red : undefined} + fallbackColor={simulationError ? colors.red : undefined} testID={undefined} showGasOptions={undefined} validateGasParams={undefined} @@ -801,3 +1083,1221 @@ export const SignTransactionSheet = () => { ); }; + +interface SimulationCardProps { + currentNetwork: Network; + expandedCardBottomInset: number; + isBalanceEnough: boolean | undefined; + isLoading: boolean; + isPersonalSign: boolean; + noChanges: boolean; + simulation: TransactionSimulationResult | undefined; + simulationError: TransactionErrorType | undefined; + simulationScanResult: TransactionScanResultType | undefined; + walletBalance: { + amount: string | number; + display: string; + isLoaded: boolean; + symbol: string; + }; +} + +const SimulationCard = ({ + currentNetwork, + expandedCardBottomInset, + isBalanceEnough, + isLoading, + isPersonalSign, + noChanges, + simulation, + simulationError, + simulationScanResult, + walletBalance, +}: SimulationCardProps) => { + const cardHeight = useSharedValue(COLLAPSED_CARD_HEIGHT); + const contentHeight = useSharedValue(COLLAPSED_CARD_HEIGHT - CARD_BORDER_WIDTH * 2); + const spinnerRotation = useSharedValue(0); + + const simulationUnavailable = isPersonalSign; + + const listStyle = useAnimatedStyle(() => ({ + opacity: noChanges + ? withTiming(1, timingConfig) + : interpolate( + cardHeight.value, + [ + COLLAPSED_CARD_HEIGHT, + contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT ? MAX_CARD_HEIGHT : contentHeight.value + CARD_BORDER_WIDTH * 2, + ], + [0, 1] + ), + })); + + const spinnerStyle = useAnimatedStyle(() => { + return { + transform: [{ rotate: `${spinnerRotation.value}deg` }], + }; + }); + + useAnimatedReaction( + () => ({ isLoading, simulationUnavailable }), + ({ isLoading, simulationUnavailable }, previous = { isLoading: false, simulationUnavailable: false }) => { + if (isLoading && !previous?.isLoading) { + spinnerRotation.value = withRepeat(withTiming(360, rotationConfig), -1, false); + } else if ( + (!isLoading && previous?.isLoading) || + (simulationUnavailable && !previous?.simulationUnavailable && previous?.isLoading) + ) { + spinnerRotation.value = withTiming(360, timingConfig); + } + }, + [isLoading, simulationUnavailable] + ); + const renderSimulationEventRows = useMemo(() => { + if (isBalanceEnough === false) return null; + + return ( + <> + {simulation?.approvals?.map(change => { + return ( + + ); + })} + {simulation?.out?.map(change => { + return ( + + ); + })} + {simulation?.in?.map(change => { + return ( + + ); + })} + + ); + }, [isBalanceEnough, simulation]); + + const titleColor: TextColor = useMemo(() => { + if (isLoading) { + return 'label'; + } + if (isBalanceEnough === false) { + return 'blue'; + } + if (noChanges || simulationUnavailable) { + return 'labelQuaternary'; + } + if (simulationScanResult === TransactionScanResultType.Warning) { + return 'orange'; + } + if (simulationError || simulationScanResult === TransactionScanResultType.Malicious) { + return 'red'; + } + return 'label'; + }, [isBalanceEnough, isLoading, noChanges, simulationError, simulationScanResult, simulationUnavailable]); + + const titleText = useMemo(() => { + if (isLoading) { + return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.simulating); + } + if (isBalanceEnough === false) { + return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.not_enough_native_balance, { symbol: walletBalance?.symbol }); + } + if (simulationUnavailable) { + return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.simulation_unavailable); + } + if (simulationScanResult === TransactionScanResultType.Warning) { + return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.proceed_carefully); + } + if (simulationScanResult === TransactionScanResultType.Malicious) { + return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.suspicious_transaction); + } + if (noChanges) { + return i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.no_changes); + } + if (simulationError) { + return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.likely_to_fail); + } + return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.simulation_result); + }, [isBalanceEnough, isLoading, noChanges, simulationError, simulationScanResult, simulationUnavailable, walletBalance?.symbol]); + + const isExpanded = useMemo(() => { + if (isLoading || isPersonalSign) { + return false; + } + const shouldExpandOnLoad = isBalanceEnough === false || (!isEmpty(simulation) && !noChanges) || !!simulationError; + return shouldExpandOnLoad; + }, [isBalanceEnough, isLoading, isPersonalSign, noChanges, simulation, simulationError]); + + return ( + + + + + {!isLoading && (simulationError || isBalanceEnough === false || simulationScanResult !== TransactionScanResultType.Ok) ? ( + + ) : ( + + {!isLoading && noChanges && !simulationUnavailable ? ( + + {/* The extra space avoids icon clipping */} + {'􀻾 '} + + ) : ( + + + 􀬨 + + + )} + + )} + + {titleText} + + + {/* TODO: Unhide once we add explainer sheets */} + {/* + + + + + 􀁜 + + + + + */} + + + + {isBalanceEnough === false ? ( + + {i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.need_more_native, { + symbol: walletBalance?.symbol, + network: getNetworkObj(currentNetwork).name, + })} + + ) : ( + <> + {simulationUnavailable && isPersonalSign && ( + + + {i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.unavailable_personal_sign)} + + + )} + {simulationError && ( + + {i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.failed_to_simulate)} + + )} + {simulationScanResult === TransactionScanResultType.Warning && ( + + {i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.warning)}{' '} + + )} + {simulationScanResult === TransactionScanResultType.Malicious && ( + + {i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.malicious)} + + )} + + )} + {renderSimulationEventRows} + + + + + ); +}; + +interface DetailsCardProps { + currentNetwork: Network; + expandedCardBottomInset: number; + isBalanceEnough: boolean | undefined; + isLoading: boolean; + meta: TransactionSimulationMeta | undefined; + methodName: string; + noChanges: boolean; + nonce: string | undefined; + toAddress: string; +} + +const DetailsCard = ({ + currentNetwork, + expandedCardBottomInset, + isBalanceEnough, + isLoading, + meta, + methodName, + noChanges, + nonce, + toAddress, +}: DetailsCardProps) => { + const cardHeight = useSharedValue(COLLAPSED_CARD_HEIGHT); + const contentHeight = useSharedValue(COLLAPSED_CARD_HEIGHT - CARD_BORDER_WIDTH * 2); + const [isExpanded, setIsExpanded] = useState(false); + + const listStyle = useAnimatedStyle(() => ({ + opacity: interpolate( + cardHeight.value, + [ + COLLAPSED_CARD_HEIGHT, + contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT ? MAX_CARD_HEIGHT : contentHeight.value + CARD_BORDER_WIDTH * 2, + ], + [0, 1] + ), + })); + + const collapsedTextColor: TextColor = isLoading ? 'labelQuaternary' : 'blue'; + + const showFunctionRow = meta?.to?.function || (methodName && methodName.substring(0, 2) !== '0x'); + const isContract = showFunctionRow || meta?.to?.created || meta?.to?.sourceCodeStatus; + const showTransferToRow = !!meta?.transferTo?.address; + // Hide DetailsCard if balance is insufficient once loaded + if (!isLoading && isBalanceEnough === false) { + return <>; + } + return ( + setIsExpanded(true)} + > + + + + + + 􁙠 + + + + {i18n.t(i18n.l.walletconnect.simulation.details_card.title)} + + + + + + {} + {!!(meta?.to?.address || toAddress || showTransferToRow) && ( + + ethereumUtils.openAddressInBlockExplorer( + meta?.to?.address || toAddress || meta?.transferTo?.address || '', + ethereumUtils.getChainIdFromNetwork(currentNetwork) + ) + } + value={ + meta?.to?.name || + abbreviations.address(meta?.to?.address || toAddress, 4, 6) || + meta?.to?.address || + toAddress || + meta?.transferTo?.address || + '' + } + /> + )} + {showFunctionRow && } + {!!meta?.to?.sourceCodeStatus && } + {!!meta?.to?.created && } + {nonce && } + + + + + ); +}; + +const MessageCard = ({ + expandedCardBottomInset, + message, + method, +}: { + expandedCardBottomInset: number; + message: string; + method: RPCMethod; +}) => { + const { setClipboard } = useClipboard(); + const [didCopy, setDidCopy] = useState(false); + + let displayMessage = message; + if (isSignTypedData(method)) { + try { + const parsedMessage = JSON.parse(message); + const sanitizedMessage = sanitizeTypedData(parsedMessage); + displayMessage = sanitizedMessage; + // eslint-disable-next-line no-empty + } catch (e) { + logger.warn('[SignTransactionSheet]: Error while parsing message'); + } + + displayMessage = JSON.stringify(displayMessage, null, 4); + } + + const estimatedMessageHeight = useMemo(() => estimateMessageHeight(displayMessage), [displayMessage]); + + const cardHeight = useSharedValue( + estimatedMessageHeight > MAX_CARD_HEIGHT ? MAX_CARD_HEIGHT : estimatedMessageHeight + CARD_BORDER_WIDTH * 2 + ); + const contentHeight = useSharedValue(estimatedMessageHeight); + + const handleCopyPress = useCallback( + (message: string) => { + if (didCopy) return; + setClipboard(message); + setDidCopy(true); + const copyTimer = setTimeout(() => { + setDidCopy(false); + }, 2000); + return () => clearTimeout(copyTimer); + }, + [didCopy, setClipboard] + ); + + return ( + MAX_CARD_HEIGHT} + isExpanded + skipCollapsedState + > + + + + + + 􀙤 + + + + {i18n.t(i18n.l.walletconnect.simulation.message_card.title)} + + + + handleCopyPress(message)}> + + + + + + {i18n.t(i18n.l.walletconnect.simulation.message_card.copy)} + + + + + + + + + {displayMessage} + + + + ); +}; + +const SimulatedEventRow = ({ + amount, + asset, + eventType, + price, +}: { + amount: string | 'unlimited'; + asset: TransactionSimulationAsset | undefined; + eventType: EventType; + price?: number | undefined; +}) => { + const theme = useTheme(); + const { nativeCurrency } = useAccountSettings(); + const { data: externalAsset } = useExternalToken({ + address: asset?.assetCode || '', + chainId: ethereumUtils.getChainIdFromNetwork((asset?.network as Network) || Network.mainnet), + currency: nativeCurrency, + }); + + const eventInfo: EventInfo = infoForEventType[eventType]; + + const formattedAmount = useMemo(() => { + if (!asset) return; + + const nftFallbackSymbol = parseFloat(amount) > 1 ? 'NFTs' : 'NFT'; + const assetDisplayName = + asset?.type === TransactionAssetType.Nft ? asset?.name || asset?.symbol || nftFallbackSymbol : asset?.symbol || asset?.name; + const shortenedDisplayName = assetDisplayName.length > 12 ? `${assetDisplayName.slice(0, 12).trim()}…` : assetDisplayName; + + const displayAmount = + asset?.decimals === 0 + ? `${amount}${shortenedDisplayName ? ' ' + shortenedDisplayName : ''}` + : convertRawAmountToBalance(amount, { decimals: asset?.decimals || 18, symbol: shortenedDisplayName }, 3, true).display; + + const unlimitedApproval = `${i18n.t(i18n.l.walletconnect.simulation.simulation_card.event_row.unlimited)} ${asset?.symbol}`; + + return `${eventInfo.amountPrefix}${amount === 'UNLIMITED' ? unlimitedApproval : displayAmount}`; + }, [amount, asset, eventInfo?.amountPrefix]); + + const url = maybeSignUri(asset?.iconURL, { + fm: 'png', + w: 16 * PixelRatio.get(), + }); + + const showUSD = (eventType === 'send' || eventType === 'receive') && !!price; + + const formattedPrice = price && convertAmountToNativeDisplay(price, nativeCurrency); + + return ( + + + + + + + {eventInfo.label} + + {showUSD && ( + + {formattedPrice} + + )} + + + + + {asset?.type !== TransactionAssetType.Nft ? ( + + ) : ( + + )} + + + {formattedAmount} + + + + + ); +}; + +const DetailRow = ({ + currentNetwork, + detailType, + onPress, + value, +}: { + currentNetwork?: Network; + detailType: DetailType; + onPress?: () => void; + value: string; +}) => { + const detailInfo: DetailInfo = infoForDetailType[detailType]; + + return ( + + + + + + {detailInfo.label} + + + + {detailType === 'function' && } + {detailType === 'sourceCodeVerification' && ( + + )} + {detailType === 'chain' && currentNetwork && ( + + )} + {detailType !== 'function' && detailType !== 'sourceCodeVerification' && ( + + {value} + + )} + {(detailType === 'contract' || detailType === 'to') && ( + + + + + 􀂄 + + + + + )} + + + + ); +}; + +const EventIcon = ({ eventType }: { eventType: EventType }) => { + const eventInfo: EventInfo = infoForEventType[eventType]; + + const hideInnerFill = eventType === 'approve' || eventType === 'revoke'; + const isWarningIcon = + eventType === 'failed' || eventType === 'insufficientBalance' || eventType === 'MALICIOUS' || eventType === 'WARNING'; + + return ( + + {!hideInnerFill && ( + + )} + + {eventInfo.icon} + + + ); +}; + +const DetailIcon = ({ detailInfo }: { detailInfo: DetailInfo }) => { + return ( + + + {detailInfo.icon} + + + ); +}; + +const DetailBadge = ({ type, value }: { type: 'function' | 'unknown' | 'unverified' | 'verified'; value: string }) => { + const { colors, isDarkMode } = useTheme(); + const separatorTertiary = useForegroundColor('separatorTertiary'); + + const infoForBadgeType: { + [key: string]: { + backgroundColor: string; + borderColor: string; + label?: string; + text: TextColor; + textOpacity?: number; + }; + } = { + function: { + backgroundColor: 'transparent', + borderColor: isDarkMode ? separatorTertiary : colors.alpha(separatorTertiary, 0.025), + text: 'labelQuaternary', + }, + unknown: { + backgroundColor: 'transparent', + borderColor: isDarkMode ? separatorTertiary : colors.alpha(separatorTertiary, 0.025), + label: 'Unknown', + text: 'labelQuaternary', + }, + unverified: { + backgroundColor: isDarkMode ? colors.alpha(colors.red, 0.05) : globalColors.red10, + borderColor: colors.alpha(colors.red, 0.02), + label: 'Unverified', + text: 'red', + textOpacity: 0.76, + }, + verified: { + backgroundColor: isDarkMode ? colors.alpha(colors.green, 0.05) : globalColors.green10, + borderColor: colors.alpha(colors.green, 0.02), + label: 'Verified', + text: 'green', + textOpacity: 0.76, + }, + }; + + return ( + + + + {infoForBadgeType[type].label || value} + + + + ); +}; + +const VerifiedBadge = () => { + return ( + + + + + 􀇻 + + + + ); +}; + +const AnimatedCheckmark = ({ visible }: { visible: boolean }) => { + return ( + + {visible && ( + + + + + + 􀁣 + + + + + )} + + ); +}; + +const FadedScrollCard = ({ + cardHeight, + children, + contentHeight, + expandedCardBottomInset = 120, + expandedCardTopInset = 120, + initialScrollEnabled, + isExpanded, + onPressCollapsedCard, + skipCollapsedState, +}: { + cardHeight: SharedValue; + children: React.ReactNode; + contentHeight: SharedValue; + expandedCardBottomInset?: number; + expandedCardTopInset?: number; + initialScrollEnabled?: boolean; + isExpanded: boolean; + onPressCollapsedCard?: () => void; + skipCollapsedState?: boolean; +}) => { + const { height: deviceHeight, width: deviceWidth } = useDimensions(); + const { isDarkMode } = useTheme(); + + const cardRef = useAnimatedRef(); + + const [scrollEnabled, setScrollEnabled] = useState(initialScrollEnabled); + const [isFullyExpanded, setIsFullyExpanded] = useState(false); + + const yPosition = useSharedValue(0); + + const maxExpandedHeight = deviceHeight - (expandedCardBottomInset + expandedCardTopInset); + + const containerStyle = useAnimatedStyle(() => { + return { + height: + cardHeight.value > MAX_CARD_HEIGHT || !skipCollapsedState + ? interpolate( + cardHeight.value, + [MAX_CARD_HEIGHT, MAX_CARD_HEIGHT, maxExpandedHeight], + [cardHeight.value, MAX_CARD_HEIGHT, MAX_CARD_HEIGHT], + 'clamp' + ) + : undefined, + zIndex: interpolate(cardHeight.value, [0, MAX_CARD_HEIGHT, MAX_CARD_HEIGHT + 1], [1, 1, 2], 'clamp'), + }; + }); + + const backdropStyle = useAnimatedStyle(() => { + const canExpandFully = contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT; + return { + opacity: canExpandFully && isFullyExpanded ? withTiming(1, timingConfig) : withTiming(0, timingConfig), + }; + }); + + const cardStyle = useAnimatedStyle(() => { + const canExpandFully = contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT; + const expandedCardHeight = Math.min(contentHeight.value + CARD_BORDER_WIDTH * 2, maxExpandedHeight); + return { + borderColor: interpolateColor( + cardHeight.value, + [0, MAX_CARD_HEIGHT, expandedCardHeight], + isDarkMode ? ['#1F2023', '#1F2023', '#242527'] : ['#F5F7F8', '#F5F7F8', '#FBFCFD'] + ), + height: cardHeight.value > MAX_CARD_HEIGHT ? cardHeight.value : undefined, + position: canExpandFully && isFullyExpanded ? 'absolute' : 'relative', + transform: [ + { + translateY: interpolate( + cardHeight.value, + [0, MAX_CARD_HEIGHT, expandedCardHeight], + [ + 0, + 0, + -yPosition.value + + expandedCardTopInset + + (deviceHeight - (expandedCardBottomInset + expandedCardTopInset) - expandedCardHeight) - + (yPosition.value + expandedCardHeight >= deviceHeight - expandedCardBottomInset + ? 0 + : deviceHeight - expandedCardBottomInset - yPosition.value - expandedCardHeight), + ] + ), + }, + ], + }; + }); + + const centerVerticallyWhenCollapsedStyle = useAnimatedStyle(() => { + return { + transform: skipCollapsedState + ? undefined + : [ + { + translateY: interpolate( + cardHeight.value, + [ + 0, + COLLAPSED_CARD_HEIGHT, + contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT + ? MAX_CARD_HEIGHT + : contentHeight.value + CARD_BORDER_WIDTH * 2, + maxExpandedHeight, + ], + [-2, -2, 0, 0] + ), + }, + ], + }; + }); + + const shadowStyle = useAnimatedStyle(() => { + const canExpandFully = contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT; + return { + shadowOpacity: canExpandFully && isFullyExpanded ? withTiming(isDarkMode ? 0.9 : 0.16, timingConfig) : withTiming(0, timingConfig), + }; + }); + + const handleContentSizeChange = useCallback( + (width: number, height: number) => { + contentHeight.value = Math.round(height); + }, + [contentHeight] + ); + + const handleOnLayout = useCallback(() => { + runOnUI(() => { + if (cardHeight.value === MAX_CARD_HEIGHT) { + const measurement = measure(cardRef); + if (measurement === null) { + return; + } + if (yPosition.value !== measurement.pageY) { + yPosition.value = measurement.pageY; + } + } + })(); + }, [cardHeight, cardRef, yPosition]); + + useAnimatedReaction( + () => ({ contentHeight: contentHeight.value, isExpanded, isFullyExpanded }), + ({ contentHeight, isExpanded, isFullyExpanded }, previous) => { + if ( + isFullyExpanded !== previous?.isFullyExpanded || + isExpanded !== previous?.isExpanded || + contentHeight !== previous?.contentHeight + ) { + if (isFullyExpanded) { + const expandedCardHeight = + contentHeight + CARD_BORDER_WIDTH * 2 > maxExpandedHeight ? maxExpandedHeight : contentHeight + CARD_BORDER_WIDTH * 2; + if (contentHeight + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT && cardHeight.value >= MAX_CARD_HEIGHT) { + cardHeight.value = withTiming(expandedCardHeight, timingConfig); + } else { + runOnJS(setIsFullyExpanded)(false); + } + } else if (isExpanded) { + cardHeight.value = withTiming( + contentHeight + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT ? MAX_CARD_HEIGHT : contentHeight + CARD_BORDER_WIDTH * 2, + timingConfig + ); + } else { + cardHeight.value = withTiming(COLLAPSED_CARD_HEIGHT, timingConfig); + } + + const enableScroll = isExpanded && contentHeight + CARD_BORDER_WIDTH * 2 > (isFullyExpanded ? maxExpandedHeight : MAX_CARD_HEIGHT); + runOnJS(setScrollEnabled)(enableScroll); + } + } + ); + + return ( + + { + if (isFullyExpanded) { + setIsFullyExpanded(false); + } + }} + pointerEvents={isFullyExpanded ? 'auto' : 'none'} + style={[ + { + backgroundColor: 'rgba(0, 0, 0, 0.6)', + height: deviceHeight * 3, + left: -deviceWidth * 0.5, + position: 'absolute', + top: -deviceHeight, + width: deviceWidth * 2, + zIndex: -1, + }, + backdropStyle, + ]} + /> + + + + { + if (!isFullyExpanded) { + setIsFullyExpanded(true); + } else setIsFullyExpanded(false); + } + } + > + {children} + + + + + + + + ); +}; + +const FadeGradient = ({ side, style }: { side: 'top' | 'bottom'; style?: StyleProp>> }) => { + const { colors, isDarkMode } = useTheme(); + + const isTop = side === 'top'; + const solidColor = isDarkMode ? globalColors.white10 : '#FBFCFD'; + const transparentColor = colors.alpha(solidColor, 0); + + return ( + + + + ); +}; + +const IconContainer = ({ + children, + hitSlop, + opacity, + size = 20, +}: { + children: React.ReactNode; + hitSlop?: number; + opacity?: number; + size?: number; +}) => { + // Prevent wide icons from being clipped + const extraHorizontalSpace = 4; + + return ( + + + {children} + + + ); +}; + +type EventType = 'send' | 'receive' | 'approve' | 'revoke' | 'failed' | 'insufficientBalance' | 'MALICIOUS' | 'WARNING'; + +type EventInfo = { + amountPrefix: string; + icon: string; + iconColor: TextColor; + label: string; + textColor: TextColor; +}; + +const infoForEventType: { [key: string]: EventInfo } = { + send: { + amountPrefix: '- ', + icon: '􀁷', + iconColor: 'red', + label: i18n.t(i18n.l.walletconnect.simulation.simulation_card.event_row.types.send), + textColor: 'red', + }, + receive: { + amountPrefix: '+ ', + icon: '􀁹', + iconColor: 'green', + label: i18n.t(i18n.l.walletconnect.simulation.simulation_card.event_row.types.receive), + textColor: 'green', + }, + approve: { + amountPrefix: '', + icon: '􀎤', + iconColor: 'green', + label: i18n.t(i18n.l.walletconnect.simulation.simulation_card.event_row.types.approve), + textColor: 'label', + }, + revoke: { + amountPrefix: '', + icon: '􀎠', + iconColor: 'red', + label: i18n.t(i18n.l.walletconnect.simulation.simulation_card.event_row.types.revoke), + textColor: 'label', + }, + failed: { + amountPrefix: '', + icon: '􀇿', + iconColor: 'red', + label: i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.likely_to_fail), + textColor: 'red', + }, + insufficientBalance: { + amountPrefix: '', + icon: '􀇿', + iconColor: 'blue', + label: '', + textColor: 'blue', + }, + MALICIOUS: { + amountPrefix: '', + icon: '􀇿', + iconColor: 'red', + label: '', + textColor: 'red', + }, + WARNING: { + amountPrefix: '', + icon: '􀇿', + iconColor: 'orange', + label: '', + textColor: 'orange', + }, +}; + +type DetailType = 'chain' | 'contract' | 'to' | 'function' | 'sourceCodeVerification' | 'dateCreated' | 'nonce'; + +type DetailInfo = { + icon: string; + label: string; +}; + +const infoForDetailType: { [key: string]: DetailInfo } = { + chain: { + icon: '􀤆', + label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.chain), + }, + contract: { + icon: '􀉆', + label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.contract), + }, + to: { + icon: '􀉩', + label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.to), + }, + function: { + icon: '􀡅', + label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.function), + }, + sourceCodeVerification: { + icon: '􀕹', + label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.source_code), + }, + dateCreated: { + icon: '􀉉', + label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.contract_created), + }, + nonce: { + icon: '􀆃', + label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.nonce), + }, +}; + +const CHARACTERS_PER_LINE = 40; +const LINE_HEIGHT = 11; +const LINE_GAP = 9; + +const estimateMessageHeight = (message: string) => { + const estimatedLines = Math.ceil(message.length / CHARACTERS_PER_LINE); + const messageHeight = estimatedLines * LINE_HEIGHT + (estimatedLines - 1) * LINE_GAP + CARD_ROW_HEIGHT + 24 * 3 - CARD_BORDER_WIDTH * 2; + + return messageHeight; +}; + +const formatDate = (dateString: string) => { + const date = new Date(dateString); + const now = new Date(); + const diffTime = Math.abs(now.getTime() - date.getTime()); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + const diffWeeks = Math.floor(diffDays / 7); + const diffMonths = Math.floor(diffDays / 30.44); + + if (diffDays === 0) { + return i18n.t(i18n.l.walletconnect.simulation.formatted_dates.today); + } else if (diffDays === 1) { + return `${diffDays} ${i18n.t(i18n.l.walletconnect.simulation.formatted_dates.day_ago)}`; + } else if (diffDays < 7) { + return `${diffDays} ${i18n.t(i18n.l.walletconnect.simulation.formatted_dates.days_ago)}`; + } else if (diffWeeks === 1) { + return `${diffWeeks} ${i18n.t(i18n.l.walletconnect.simulation.formatted_dates.week_ago)}`; + } else if (diffDays < 30.44) { + return `${diffWeeks} ${i18n.t(i18n.l.walletconnect.simulation.formatted_dates.weeks_ago)}`; + } else if (diffMonths === 1) { + return `${diffMonths} ${i18n.t(i18n.l.walletconnect.simulation.formatted_dates.month_ago)}`; + } else if (diffDays < 365.25) { + return `${diffMonths} ${i18n.t(i18n.l.walletconnect.simulation.formatted_dates.months_ago)}`; + } else { + return date.toLocaleString('default', { month: 'short', year: 'numeric' }); + } +}; diff --git a/src/screens/WalletScreen/index.tsx b/src/screens/WalletScreen/index.tsx index 6c4fd429c2c..02ea352ec5c 100644 --- a/src/screens/WalletScreen/index.tsx +++ b/src/screens/WalletScreen/index.tsx @@ -33,8 +33,6 @@ import { IS_ANDROID } from '@/env'; import { RemoteCardsSync } from '@/state/sync/RemoteCardsSync'; import { RemotePromoSheetSync } from '@/state/sync/RemotePromoSheetSync'; import { UserAssetsSync } from '@/state/sync/UserAssetsSync'; -import { MobileWalletProtocolListener } from '@/components/MobileWalletProtocolListener'; -import { runWalletBackupStatusChecks } from '@/handlers/walletReadyEvents'; const WalletPage = styled(Page)({ ...position.sizeAsObject('100%'), @@ -108,7 +106,6 @@ const WalletScreen: React.FC = ({ navigation, route }) => { if (walletReady) { loadAccountLateData(); loadGlobalLateData(); - runWalletBackupStatusChecks(); } }, [loadAccountLateData, loadGlobalLateData, walletReady]); @@ -150,9 +147,6 @@ const WalletScreen: React.FC = ({ navigation, route }) => { - - {/* NOTE: This component listens for Mobile Wallet Protocol requests and handles them */} - ); diff --git a/src/state/performance/operations.ts b/src/state/performance/operations.ts index a042200fc26..f30133ce5c6 100644 --- a/src/state/performance/operations.ts +++ b/src/state/performance/operations.ts @@ -4,7 +4,6 @@ export enum Screens { SEND = 'Send', SEND_ENS = 'SendENS', WALLETCONNECT = 'WalletConnect', - MOBILE_WALLET_PROTOCOL = 'MobileWalletProtocol', } type RouteValues = (typeof Screens)[keyof typeof Screens]; diff --git a/src/storage/index.ts b/src/storage/index.ts index c9d9639552a..824c7288ce7 100644 --- a/src/storage/index.ts +++ b/src/storage/index.ts @@ -3,7 +3,6 @@ import { MMKV } from 'react-native-mmkv'; import { Account, Cards, Campaigns, Device, Review } from '@/storage/schema'; import { EthereumAddress, RainbowTransaction } from '@/entities'; import { Network } from '@/networks/types'; -import { SecureStorage } from '@coinbase/mobile-wallet-protocol-host'; /** * Generic storage class. DO NOT use this directly. Instead, use the exported @@ -13,8 +12,8 @@ export class Storage { protected sep = ':'; protected store: MMKV; - constructor({ id, encryptionKey }: { id: string; encryptionKey?: string }) { - this.store = new MMKV({ id, encryptionKey }); + constructor({ id }: { id: string }) { + this.store = new MMKV({ id }); } /** @@ -51,13 +50,6 @@ export class Storage { this.store.delete(scopes.join(this.sep)); } - /** - * Clear all values from storage - */ - clear() { - this.store.clearAll(); - } - /** * Remove many values from the same storage scope by keys * @@ -67,21 +59,6 @@ export class Storage { removeMany(scopes: [...Scopes], keys: Key[]) { keys.forEach(key => this.remove([...scopes, key])); } - - /** - * Encrypt the storage with a new key - * @param newEncryptionKey - The new encryption key - */ - encrypt(newEncryptionKey: string): void { - this.store.recrypt(newEncryptionKey); - } - - /** - * Remove encryption from the storage - */ - removeEncryption(): void { - this.store.recrypt(undefined); - } } /** @@ -111,27 +88,3 @@ export const cards = new Storage<[], Cards>({ id: 'cards' }); export const identifier = new Storage<[], { identifier: string }>({ id: 'identifier', }); - -/** - * Mobile Wallet Protocol storage - * - * @todo - fix any type here - */ -const mwpStorage = new Storage<[], any>({ id: 'mwp', encryptionKey: process.env.MWP_ENCRYPTION_KEY }); - -export const mwp: SecureStorage = { - get: async function (key: string): Promise { - const dataJson = mwpStorage.get([key]); - if (dataJson === undefined) { - return undefined; - } - return Promise.resolve(JSON.parse(dataJson) as T); - }, - set: async function (key: string, value: T): Promise { - const encoded = JSON.stringify(value); - mwpStorage.set([key], encoded); - }, - remove: async function (key: string): Promise { - mwpStorage.remove([key]); - }, -}; diff --git a/src/utils/formatDate.ts b/src/utils/formatDate.ts deleted file mode 100644 index 8bb40dc1897..00000000000 --- a/src/utils/formatDate.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as i18n from '@/languages'; - -export const formatDate = (dateString: string) => { - const date = new Date(dateString); - const now = new Date(); - const diffTime = Math.abs(now.getTime() - date.getTime()); - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); - const diffWeeks = Math.floor(diffDays / 7); - const diffMonths = Math.floor(diffDays / 30.44); - - if (diffDays === 0) { - return i18n.t(i18n.l.walletconnect.simulation.formatted_dates.today); - } else if (diffDays === 1) { - return `${diffDays} ${i18n.t(i18n.l.walletconnect.simulation.formatted_dates.day_ago)}`; - } else if (diffDays < 7) { - return `${diffDays} ${i18n.t(i18n.l.walletconnect.simulation.formatted_dates.days_ago)}`; - } else if (diffWeeks === 1) { - return `${diffWeeks} ${i18n.t(i18n.l.walletconnect.simulation.formatted_dates.week_ago)}`; - } else if (diffDays < 30.44) { - return `${diffWeeks} ${i18n.t(i18n.l.walletconnect.simulation.formatted_dates.weeks_ago)}`; - } else if (diffMonths === 1) { - return `${diffMonths} ${i18n.t(i18n.l.walletconnect.simulation.formatted_dates.month_ago)}`; - } else if (diffDays < 365.25) { - return `${diffMonths} ${i18n.t(i18n.l.walletconnect.simulation.formatted_dates.months_ago)}`; - } else { - return date.toLocaleString('default', { month: 'short', year: 'numeric' }); - } -}; diff --git a/src/utils/requestNavigationHandlers.ts b/src/utils/requestNavigationHandlers.ts index 6ecc8ad16fc..8a6f193a480 100644 --- a/src/utils/requestNavigationHandlers.ts +++ b/src/utils/requestNavigationHandlers.ts @@ -13,7 +13,7 @@ import { import { InteractionManager } from 'react-native'; import { SEND_TRANSACTION } from './signingMethods'; import { handleSessionRequestResponse } from '@/walletConnect'; -import ethereumUtils, { getNetworkFromChainId } from './ethereumUtils'; +import ethereumUtils from './ethereumUtils'; import { getRequestDisplayDetails } from '@/parsers'; import { RainbowNetworks } from '@/networks'; import { maybeSignUri } from '@/handlers/imgix'; @@ -22,249 +22,8 @@ import { findWalletWithAccount } from '@/helpers/findWalletWithAccount'; import { enableActionsOnReadOnlyWallet } from '@/config'; import walletTypes from '@/helpers/walletTypes'; import watchingAlert from './watchingAlert'; -import { - EthereumAction, - isEthereumAction, - isHandshakeAction, - PersonalSignAction, - RequestMessage, - useMobileWalletProtocolHost, -} from '@coinbase/mobile-wallet-protocol-host'; -import { ChainId } from '@/__swaps__/types/chains'; -import { logger, RainbowError } from '@/logger'; -import { noop } from 'lodash'; -import { toUtf8String } from '@ethersproject/strings'; -import { BigNumber } from '@ethersproject/bignumber'; - -export enum RequestSource { - WALLETCONNECT = 'walletconnect', - BROWSER = 'browser', - MOBILE_WALLET_PROTOCOL = 'mobile-wallet-protocol', -} - -// Mobile Wallet Protocol - -interface HandleMobileWalletProtocolRequestProps - extends Omit, 'message' | 'handleRequestUrl' | 'sendFailureToClient'> { - request: RequestMessage; -} - -const constructEthereumActionPayload = (action: EthereumAction) => { - if (action.method === 'eth_sendTransaction') { - const { weiValue, fromAddress, toAddress, actionSource, gasPriceInWei, ...rest } = action.params; - return [ - { - ...rest, - from: fromAddress, - to: toAddress, - value: weiValue, - }, - ]; - } - - return Object.values(action.params); -}; - -const supportedMobileWalletProtocolActions: string[] = [ - 'eth_requestAccounts', - 'eth_sendTransaction', - 'eth_signTypedData_v4', - 'personal_sign', - 'wallet_switchEthereumChain', -]; - -export const handleMobileWalletProtocolRequest = async ({ - request, - fetchClientAppMetadata, - approveHandshake, - rejectHandshake, - approveAction, - rejectAction, - session, -}: HandleMobileWalletProtocolRequestProps): Promise => { - logger.debug(`Handling Mobile Wallet Protocol request: ${request.uuid}`); - - const { selected } = store.getState().wallets; - const { accountAddress } = store.getState().settings; - - const isReadOnlyWallet = selected?.type === walletTypes.readOnly; - if (isReadOnlyWallet && !enableActionsOnReadOnlyWallet) { - logger.debug('Rejecting request due to read-only wallet'); - watchingAlert(); - return Promise.reject(new Error('This wallet is read-only.')); - } - - const handleAction = async (currentIndex: number): Promise => { - const action = request.actions[currentIndex]; - logger.debug(`Handling action: ${action.kind}`); - - if (isHandshakeAction(action)) { - logger.debug(`Processing handshake action for ${action.appId}`); - - const chainIds = RainbowNetworks.filter(network => network.enabled && network.networkType !== 'testnet').map(network => network.id); - const receivedTimestamp = Date.now(); - - const dappMetadata = await fetchClientAppMetadata(); - return new Promise((resolve, reject) => { - const routeParams: WalletconnectApprovalSheetRouteParams = { - receivedTimestamp, - meta: { - chainIds, - dappName: dappMetadata?.appName || dappMetadata?.appUrl || action.appName || action.appIconUrl || action.appId || '', - dappUrl: dappMetadata?.appUrl || action.appId || '', - imageUrl: maybeSignUri(dappMetadata?.iconUrl || action.appIconUrl), - isWalletConnectV2: false, - peerId: '', - dappScheme: action.callback, - proposedChainId: request.account?.networkId ?? ChainId.mainnet, - proposedAddress: request.account?.address || accountAddress, - }, - source: RequestSource.MOBILE_WALLET_PROTOCOL, - timedOut: false, - callback: async approved => { - if (approved) { - logger.debug(`Handshake approved for ${action.appId}`); - const success = await approveHandshake(dappMetadata); - resolve(success); - } else { - logger.debug(`Handshake rejected for ${action.appId}`); - await rejectHandshake('User rejected the handshake'); - reject('User rejected the handshake'); - } - }, - }; - - Navigation.handleAction( - Routes.WALLET_CONNECT_APPROVAL_SHEET, - routeParams, - getActiveRoute()?.name === Routes.WALLET_CONNECT_APPROVAL_SHEET - ); - }); - } else if (isEthereumAction(action)) { - logger.debug(`Processing ethereum action: ${action.method}`); - if (!supportedMobileWalletProtocolActions.includes(action.method)) { - logger.error(new RainbowError(`[handleMobileWalletProtocolRequest]: Unsupported action type ${action.method}`)); - await rejectAction(action, { - message: 'Unsupported action type', - code: 4001, - }); - return false; - } - - if (action.method === 'wallet_switchEthereumChain') { - const isSupportedChain = RainbowNetworks.find(network => network.id === BigNumber.from(action.params.chainId).toNumber()); - if (!isSupportedChain) { - await rejectAction(action, { - message: 'Unsupported chain', - code: 4001, - }); - return false; - } - - await approveAction(action, { value: 'null' }); - return true; - } - - // NOTE: This is a workaround to approve the eth_requestAccounts action if the previous action was a handshake action. - const previousAction = request.actions[currentIndex - 1]; - if (previousAction && isHandshakeAction(previousAction)) { - logger.debug('Approving eth_requestAccounts'); - await approveAction(action, { - value: JSON.stringify({ - chain: request.account?.chain ?? 'eth', - networkId: request.account?.networkId ?? ChainId.mainnet, - address: accountAddress, - }), - }); - return true; - } - - const nativeCurrency = store.getState().settings.nativeCurrency; - - // @ts-expect-error - coinbase host protocol types are NOT correct e.g. {"data": [72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100], "type": "Buffer"} - if ((action as PersonalSignAction).params.message && (action as PersonalSignAction).params.message.type === 'Buffer') { - // @ts-expect-error - coinbase host protocol types are NOT correct e.g. {"data": [72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100], "type": "Buffer"} - const messageFromBuffer = toUtf8String(Buffer.from((action as PersonalSignAction).params.message.data, 'hex')); - (action as PersonalSignAction).params.message = messageFromBuffer; - } - - const payload = { - method: action.method, - params: constructEthereumActionPayload(action), - }; - - const displayDetails = await getRequestDisplayDetails(payload, nativeCurrency, request.account?.networkId ?? ChainId.mainnet); - - const requestWithDetails: RequestData = { - dappName: session?.dappName ?? session?.dappId ?? '', - dappUrl: session?.dappURL ?? '', - imageUrl: session?.dappImageURL ?? '', - address: (action as PersonalSignAction).params.address ?? accountAddress, - chainId: request.account?.networkId ?? ChainId.mainnet, - payload, - displayDetails, - }; - - return new Promise((resolve, reject) => { - const onSuccess = async (result: string) => { - logger.debug(`Ethereum action approved: [${action.method}]: ${result}`); - const success = await approveAction(action, { value: JSON.stringify(result) }); - resolve(success); - }; - const onCancel = async (error?: Error) => { - if (error) { - logger.debug(`Ethereum action rejected: [${action.method}]: ${error.message}`); - await rejectAction(action, { - message: error.message, - code: 4001, - }); - reject(error.message); - } else { - logger.debug(`Ethereum action rejected: [${action.method}]: User rejected request`); - await rejectAction(action, { - message: 'User rejected request', - code: 4001, - }); - reject('User rejected request'); - } - }; - - Navigation.handleAction(Routes.CONFIRM_REQUEST, { - transactionDetails: requestWithDetails, - onSuccess, - onCancel, - onCloseScreen: noop, - chainId: request.account?.networkId ?? ChainId.mainnet, - address: accountAddress, - source: RequestSource.MOBILE_WALLET_PROTOCOL, - }); - }); - } else { - logger.error(new RainbowError(`[handleMobileWalletProtocolRequest]: Unsupported action type, ${action}`)); - return false; - } - }; - - const handleActions = async (actions: typeof request.actions, currentIndex: number = 0): Promise => { - if (currentIndex >= actions.length) { - logger.debug(`All actions completed successfully: ${actions.length}`); - return true; - } - - logger.debug(`Processing action ${currentIndex + 1} of ${actions.length}`); - const success = await handleAction(currentIndex); - if (success) { - return handleActions(actions, currentIndex + 1); - } else { - // stop processing if an action fails - return false; - } - }; - - // start processing actions starting at index 0 - return handleActions(request.actions); -}; +export type RequestSource = 'walletconnect' | 'browser'; // Dapp Browser @@ -293,7 +52,7 @@ export const handleDappBrowserConnectionPrompt = (dappData: DappConnectionData): proposedChainId: dappData.chainId, proposedAddress: dappData.address, }, - source: RequestSource.BROWSER, + source: 'browser', timedOut: false, callback: async (approved, approvedChainId, accountAddress) => { if (approved) { @@ -318,31 +77,16 @@ export const handleDappBrowserConnectionPrompt = (dappData: DappConnectionData): }); }; -const findWalletForAddress = async (address: string) => { - if (!address.trim()) { - return Promise.reject(new Error('Invalid address')); - } - +export const handleDappBrowserRequest = async (request: Omit): Promise => { const { wallets } = store.getState().wallets; - const selectedWallet = findWalletWithAccount(wallets!, address); - if (!selectedWallet) { - return Promise.reject(new Error('Wallet not found')); - } - - const isReadOnlyWallet = selectedWallet.type === walletTypes.readOnly; + const selectedWallet = findWalletWithAccount(wallets!, request.address); + const isReadOnlyWallet = selectedWallet!.type === walletTypes.readOnly; if (isReadOnlyWallet && !enableActionsOnReadOnlyWallet) { watchingAlert(); return Promise.reject(new Error('This wallet is read-only.')); } - - return selectedWallet; -}; - -export const handleDappBrowserRequest = async (request: Omit): Promise => { - await findWalletForAddress(request.address); - const nativeCurrency = store.getState().settings.nativeCurrency; - const displayDetails = await getRequestDisplayDetails(request.payload, nativeCurrency, request.chainId); + const displayDetails = getRequestDisplayDetails(request.payload, nativeCurrency, request.chainId); const requestWithDetails: RequestData = { ...request, @@ -374,7 +118,7 @@ export const handleDappBrowserRequest = async (request: Omit