From 46162ee4668b697bcb452c3920b576e5afc96d21 Mon Sep 17 00:00:00 2001 From: gregs Date: Wed, 29 May 2024 15:29:18 -0300 Subject: [PATCH 01/25] wip --- .../Swap/components/SwapActionButton.tsx | 29 +++++---- .../Swap/components/SwapBottomPanel.tsx | 24 ++++---- .../Swap/components/SwapInputAsset.tsx | 10 ++-- .../screens/Swap/providers/swap-provider.tsx | 59 +++++++------------ 4 files changed, 55 insertions(+), 67 deletions(-) diff --git a/src/__swaps__/screens/Swap/components/SwapActionButton.tsx b/src/__swaps__/screens/Swap/components/SwapActionButton.tsx index 3eac5683f65..e3b49a33c7c 100644 --- a/src/__swaps__/screens/Swap/components/SwapActionButton.tsx +++ b/src/__swaps__/screens/Swap/components/SwapActionButton.tsx @@ -1,12 +1,12 @@ /* eslint-disable no-nested-ternary */ import React from 'react'; import { StyleProp, StyleSheet, TextStyle, ViewStyle } from 'react-native'; -import Animated, { DerivedValue, useAnimatedStyle, useDerivedValue } from 'react-native-reanimated'; +import Animated, { DerivedValue, useAnimatedProps, useAnimatedStyle, useDerivedValue } from 'react-native-reanimated'; -import { ButtonPressAnimation } from '@/components/animations'; -import { AnimatedText, Box, Column, Columns, globalColors, useColorMode, useForegroundColor } from '@/design-system'; import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets'; import { getColorValueForThemeWorklet } from '@/__swaps__/utils/swaps'; +import { ButtonPressAnimation } from '@/components/animations'; +import { AnimatedText, Box, Column, Columns, globalColors, useColorMode, useForegroundColor } from '@/design-system'; export const SwapActionButton = ({ asset, @@ -23,6 +23,7 @@ export const SwapActionButton = ({ scaleTo, small, style, + disabled, }: { asset: DerivedValue; borderRadius?: number; @@ -38,6 +39,7 @@ export const SwapActionButton = ({ scaleTo?: number; small?: boolean; style?: ViewStyle; + disabled?: DerivedValue; }) => { const { isDarkMode } = useColorMode(); const fallbackColor = useForegroundColor('label'); @@ -80,6 +82,7 @@ export const SwapActionButton = ({ }, shadowOpacity: isDarkMode ? 0.2 : small ? 0.2 : 0.36, shadowRadius: isDarkMode ? 26 : small ? 9 : 15, + opacity: disabled?.value ? 0.5 : 1, }; }); @@ -97,15 +100,21 @@ export const SwapActionButton = ({ return rightIcon; }); + const a = useAnimatedProps(() => { + console.log(disabled?.value || false); + return { + isDisabled: disabled?.value || false, + }; + }); + + console.log('ASDSADASDA', a.isDisabled); + return ( confirmButtonProps.value.icon); + const label = useDerivedValue(() => confirmButtonProps.value.label); + const disabled = useDerivedValue(() => confirmButtonProps.value.disabled); + return ( // @ts-expect-error Property 'children' does not exist on type @@ -76,9 +73,10 @@ export function SwapBottomPanel() { runOnUI(SwapNavigation.handleSwapAction)()} asset={internalSelectedOutputAsset} - icon={confirmButtonIcon} + icon={icon} iconStyle={confirmButtonIconStyle} - label={confirmButtonLabel} + label={label} + disabled={disabled} scaleTo={0.9} /> diff --git a/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx b/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx index 6ca074c82a6..753e597905d 100644 --- a/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx +++ b/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx @@ -1,22 +1,22 @@ import MaskedView from '@react-native-masked-view/masked-view'; import React from 'react'; -import { StyleSheet, StatusBar } from 'react-native'; +import { StatusBar, StyleSheet } from 'react-native'; import Animated, { runOnUI, useDerivedValue } from 'react-native-reanimated'; import { ScreenCornerRadius } from 'react-native-screen-corner-radius'; import { AnimatedText, Box, Column, Columns, Stack, useColorMode } from '@/design-system'; +import { BalanceBadge } from '@/__swaps__/screens/Swap/components/BalanceBadge'; +import { FadeMask } from '@/__swaps__/screens/Swap/components/FadeMask'; import { GestureHandlerV1Button } from '@/__swaps__/screens/Swap/components/GestureHandlerV1Button'; import { SwapActionButton } from '@/__swaps__/screens/Swap/components/SwapActionButton'; -import { FadeMask } from '@/__swaps__/screens/Swap/components/FadeMask'; import { SwapInput } from '@/__swaps__/screens/Swap/components/SwapInput'; -import { BalanceBadge } from '@/__swaps__/screens/Swap/components/BalanceBadge'; import { TokenList } from '@/__swaps__/screens/Swap/components/TokenList/TokenList'; import { BASE_INPUT_WIDTH, INPUT_INNER_WIDTH, INPUT_PADDING, THICK_BORDER_WIDTH } from '@/__swaps__/screens/Swap/constants'; -import { IS_ANDROID } from '@/env'; -import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider'; import { useAssetsToSell } from '@/__swaps__/screens/Swap/hooks/useAssetsToSell'; +import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider'; import { isSameAssetWorklet } from '@/__swaps__/utils/assets'; +import { IS_ANDROID } from '@/env'; import { AmimatedSwapCoinIcon } from './AnimatedSwapCoinIcon'; function SwapInputActionButton() { diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index d54ddc4f660..414f0825818 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -58,8 +58,13 @@ interface SwapContextType { SwapNavigation: ReturnType; SwapWarning: ReturnType; - confirmButtonIcon: Readonly>; - confirmButtonLabel: Readonly>; + confirmButtonProps: Readonly< + SharedValue<{ + label: string; + icon?: string; + disabled?: boolean; + }> + >; confirmButtonIconStyle: StyleProp; } @@ -244,53 +249,30 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { runOnUI(updateAssetValue)({ type, asset: parseAssetAndExtend({ asset }) }); }; - const confirmButtonIcon = useDerivedValue(() => { + const confirmButtonProps = useDerivedValue(() => { if (configProgress.value === NavigationSteps.SHOW_REVIEW) { - return '􀎽'; - } else if (configProgress.value === NavigationSteps.SHOW_GAS) { - return '􀆅'; + return { icon: '􀎽', label: 'Hold to Swap', disabled: false }; } - if (isFetching.value) { - return ''; - } - - const isInputZero = Number(SwapInputController.inputValues.value.inputAmount) === 0; - const isOutputZero = Number(SwapInputController.inputValues.value.outputAmount) === 0; - - if (SwapInputController.inputMethod.value !== 'slider' && (isInputZero || isOutputZero) && !isFetching.value) { - return ''; - } else if (SwapInputController.inputMethod.value === 'slider' && SwapInputController.percentageToSwap.value === 0) { - return ''; - } else { - return '􀕹'; - } - }); - - const confirmButtonLabel = useDerivedValue(() => { - if (configProgress.value === NavigationSteps.SHOW_REVIEW) { - return 'Hold to Swap'; - } else if (configProgress.value === NavigationSteps.SHOW_GAS) { - return 'Save'; + if (configProgress.value === NavigationSteps.SHOW_GAS) { + return { icon: '􀆅', label: 'Save', disabled: false }; } if (isFetching.value) { - return 'Fetching prices'; + return { label: 'Fetching...', disabled: true }; } + const hasSelectedAssets = internalSelectedInputAsset.value && internalSelectedOutputAsset.value; + if (!hasSelectedAssets) return { label: 'Select Token', disabled: true }; + const isInputZero = Number(SwapInputController.inputValues.value.inputAmount) === 0; const isOutputZero = Number(SwapInputController.inputValues.value.outputAmount) === 0; - if (SwapInputController.inputMethod.value !== 'slider' && (isInputZero || isOutputZero) && !isFetching.value) { - return 'Enter Amount'; - } else if ( - SwapInputController.inputMethod.value === 'slider' && - (SwapInputController.percentageToSwap.value === 0 || isInputZero || isOutputZero) - ) { - return 'Enter Amount'; - } else { - return 'Review'; + if (SwapInputController.percentageToSwap.value === 0 || isInputZero || isOutputZero) { + return { label: 'Enter Amount', disabled: true }; } + + return { icon: '􀕹', label: 'Review', disabled: false }; }); const confirmButtonIconStyle = useAnimatedStyle(() => { @@ -356,8 +338,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { SwapNavigation, SwapWarning, - confirmButtonIcon, - confirmButtonLabel, + confirmButtonProps, confirmButtonIconStyle, }} > From 3a3c341d6d837c75407b3bf0aee47a0a5fa5367d Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Wed, 29 May 2024 15:07:02 -0400 Subject: [PATCH 02/25] fix gesture button states --- .../components/GestureHandlerV1Button.tsx | 3 ++ .../Swap/components/SwapActionButton.tsx | 29 +++++++++---------- .../Swap/components/SwapBottomPanel.tsx | 4 +-- .../Swap/components/SwapInputAsset.tsx | 2 +- .../Swap/components/SwapOutputAsset.tsx | 2 +- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/__swaps__/screens/Swap/components/GestureHandlerV1Button.tsx b/src/__swaps__/screens/Swap/components/GestureHandlerV1Button.tsx index 833cfed209e..2a3d2bb8cdb 100644 --- a/src/__swaps__/screens/Swap/components/GestureHandlerV1Button.tsx +++ b/src/__swaps__/screens/Swap/components/GestureHandlerV1Button.tsx @@ -76,11 +76,14 @@ export const GestureHandlerV1Button = React.forwardRef(function GestureHandlerV1 if (onPressStartWorklet) onPressStartWorklet(); }, onActive: () => { + console.log('here'); if (onPressWorklet) onPressWorklet(); if (onPressJS) runOnJS(onPressJS)(); }, }); + console.log({ disabled }); + return ( ; iconStyle?: StyleProp; label: string | DerivedValue; - onLongPress?: () => void; - onPress?: () => void; + onPressWorklet?: () => void; outline?: boolean; rightIcon?: string; scaleTo?: number; @@ -82,7 +82,7 @@ export const SwapActionButton = ({ }, shadowOpacity: isDarkMode ? 0.2 : small ? 0.2 : 0.36, shadowRadius: isDarkMode ? 26 : small ? 9 : 15, - opacity: disabled?.value ? 0.5 : 1, + opacity: disabled?.value ? 0.6 : 1, }; }); @@ -100,20 +100,17 @@ export const SwapActionButton = ({ return rightIcon; }); - const a = useAnimatedProps(() => { - console.log(disabled?.value || false); + const buttonAnimatedProps = useAnimatedProps(() => { return { - isDisabled: disabled?.value || false, + disabled: disabled?.value, + scaleTo: disabled?.value ? 1 : scaleTo || (hugContent ? undefined : 0.925), }; }); - console.log('ASDSADASDA', a.isDisabled); - return ( - - + ); }; diff --git a/src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx b/src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx index 049a862a05c..d78e1ce0a3d 100644 --- a/src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx +++ b/src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { StyleSheet } from 'react-native'; import { getSoftMenuBarHeight } from 'react-native-extra-dimensions-android'; import { PanGestureHandler } from 'react-native-gesture-handler'; -import Animated, { runOnUI, useAnimatedStyle, useDerivedValue, withSpring } from 'react-native-reanimated'; +import Animated, { useAnimatedStyle, useDerivedValue, withSpring } from 'react-native-reanimated'; import { Box, Column, Columns, Separator, globalColors, useColorMode } from '@/design-system'; import { safeAreaInsetValues } from '@/utils'; @@ -71,7 +71,7 @@ export function SwapBottomPanel() { runOnUI(SwapNavigation.handleSwapAction)()} + onPressWorklet={SwapNavigation.handleSwapAction} asset={internalSelectedOutputAsset} icon={icon} iconStyle={confirmButtonIconStyle} diff --git a/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx b/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx index 753e597905d..3a8d1c86641 100644 --- a/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx +++ b/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx @@ -34,7 +34,7 @@ function SwapInputActionButton() { disableShadow={isDarkMode} hugContent label={label} - onPress={runOnUI(SwapNavigation.handleInputPress)} + onPressWorklet={SwapNavigation.handleInputPress} rightIcon={'􀆏'} small /> diff --git a/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx b/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx index 8b3ebf05028..55de1edcd98 100644 --- a/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx +++ b/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx @@ -34,7 +34,7 @@ function SwapOutputActionButton() { disableShadow={isDarkMode} hugContent label={label} - onPress={runOnUI(SwapNavigation.handleOutputPress)} + onPressWorklet={SwapNavigation.handleOutputPress} rightIcon={'􀆏'} small /> From b8635be9336d4c8d5abbd2fd4bd80563bfd9b871 Mon Sep 17 00:00:00 2001 From: Bruno Barbieri <1247834+brunobar79@users.noreply.github.com> Date: Wed, 29 May 2024 13:08:33 -0400 Subject: [PATCH 03/25] Safemath pt 2 (#5778) * add more fns * accept string or number * update errors --- src/__swaps__/safe-math/SafeMath.ts | 270 +++++++++++++----- .../safe-math/__tests__/SafeMath.test.ts | 106 +++++-- 2 files changed, 285 insertions(+), 91 deletions(-) diff --git a/src/__swaps__/safe-math/SafeMath.ts b/src/__swaps__/safe-math/SafeMath.ts index c98ea7536fa..4d0eb500112 100644 --- a/src/__swaps__/safe-math/SafeMath.ts +++ b/src/__swaps__/safe-math/SafeMath.ts @@ -40,21 +40,30 @@ const formatResultWorklet = (result: bigint): string => { return isNegative ? `-${formattedResult}` : formattedResult; }; +// Helper function to handle string and number input types +const toStringWorklet = (value: string | number): string => { + 'worklet'; + return typeof value === 'number' ? value.toString() : value; +}; + // Sum function -export function sumWorklet(num1: string, num2: string): string { +export function sumWorklet(num1: string | number, num2: string | number): string { 'worklet'; - if (!isNumberStringWorklet(num1) || !isNumberStringWorklet(num2)) { - throw new Error('Arguments must be a numeric string'); + const num1Str = toStringWorklet(num1); + const num2Str = toStringWorklet(num2); + + if (!isNumberStringWorklet(num1Str) || !isNumberStringWorklet(num2Str)) { + throw new Error('Arguments must be a numeric string or number'); } - if (isZeroWorklet(num1)) { - return num2; + if (isZeroWorklet(num1Str)) { + return num2Str; } - if (isZeroWorklet(num2)) { - return num1; + if (isZeroWorklet(num2Str)) { + return num1Str; } - const [bigInt1, decimalPlaces1] = removeDecimalWorklet(num1); - const [bigInt2, decimalPlaces2] = removeDecimalWorklet(num2); + const [bigInt1, decimalPlaces1] = removeDecimalWorklet(num1Str); + const [bigInt2, decimalPlaces2] = removeDecimalWorklet(num2Str); const scaledBigInt1 = scaleUpWorklet(bigInt1, decimalPlaces1); const scaledBigInt2 = scaleUpWorklet(bigInt2, decimalPlaces2); const result = scaledBigInt1 + scaledBigInt2; @@ -62,18 +71,21 @@ export function sumWorklet(num1: string, num2: string): string { } // Subtract function -export function subWorklet(num1: string, num2: string): string { +export function subWorklet(num1: string | number, num2: string | number): string { 'worklet'; - if (!isNumberStringWorklet(num1) || !isNumberStringWorklet(num2)) { - throw new Error('Arguments must be a numeric string'); + const num1Str = toStringWorklet(num1); + const num2Str = toStringWorklet(num2); + + if (!isNumberStringWorklet(num1Str) || !isNumberStringWorklet(num2Str)) { + throw new Error('Arguments must be a numeric string or number'); } - if (isZeroWorklet(num2)) { - return num1; + if (isZeroWorklet(num2Str)) { + return num1Str; } - const [bigInt1, decimalPlaces1] = removeDecimalWorklet(num1); - const [bigInt2, decimalPlaces2] = removeDecimalWorklet(num2); + const [bigInt1, decimalPlaces1] = removeDecimalWorklet(num1Str); + const [bigInt2, decimalPlaces2] = removeDecimalWorklet(num2Str); const scaledBigInt1 = scaleUpWorklet(bigInt1, decimalPlaces1); const scaledBigInt2 = scaleUpWorklet(bigInt2, decimalPlaces2); const result = scaledBigInt1 - scaledBigInt2; @@ -81,16 +93,19 @@ export function subWorklet(num1: string, num2: string): string { } // Multiply function -export function mulWorklet(num1: string, num2: string): string { +export function mulWorklet(num1: string | number, num2: string | number): string { 'worklet'; - if (!isNumberStringWorklet(num1) || !isNumberStringWorklet(num2)) { - throw new Error('Arguments must be a numeric string'); + const num1Str = toStringWorklet(num1); + const num2Str = toStringWorklet(num2); + + if (!isNumberStringWorklet(num1Str) || !isNumberStringWorklet(num2Str)) { + throw new Error('Arguments must be a numeric string or number'); } - if (isZeroWorklet(num1) || isZeroWorklet(num2)) { + if (isZeroWorklet(num1Str) || isZeroWorklet(num2Str)) { return '0'; } - const [bigInt1, decimalPlaces1] = removeDecimalWorklet(num1); - const [bigInt2, decimalPlaces2] = removeDecimalWorklet(num2); + const [bigInt1, decimalPlaces1] = removeDecimalWorklet(num1Str); + const [bigInt2, decimalPlaces2] = removeDecimalWorklet(num2Str); const scaledBigInt1 = scaleUpWorklet(bigInt1, decimalPlaces1); const scaledBigInt2 = scaleUpWorklet(bigInt2, decimalPlaces2); const result = (scaledBigInt1 * scaledBigInt2) / BigInt(10) ** BigInt(20); @@ -98,19 +113,22 @@ export function mulWorklet(num1: string, num2: string): string { } // Divide function -export function divWorklet(num1: string, num2: string): string { +export function divWorklet(num1: string | number, num2: string | number): string { 'worklet'; - if (!isNumberStringWorklet(num1) || !isNumberStringWorklet(num2)) { - throw new Error('Arguments must be a numeric string'); + const num1Str = toStringWorklet(num1); + const num2Str = toStringWorklet(num2); + + if (!isNumberStringWorklet(num1Str) || !isNumberStringWorklet(num2Str)) { + throw new Error('Arguments must be a numeric string or number'); } - if (isZeroWorklet(num2)) { + if (isZeroWorklet(num2Str)) { throw new Error('Division by zero'); } - if (isZeroWorklet(num1)) { + if (isZeroWorklet(num1Str)) { return '0'; } - const [bigInt1, decimalPlaces1] = removeDecimalWorklet(num1); - const [bigInt2, decimalPlaces2] = removeDecimalWorklet(num2); + const [bigInt1, decimalPlaces1] = removeDecimalWorklet(num1Str); + const [bigInt2, decimalPlaces2] = removeDecimalWorklet(num2Str); const scaledBigInt1 = scaleUpWorklet(bigInt1, decimalPlaces1); const scaledBigInt2 = scaleUpWorklet(bigInt2, decimalPlaces2); const result = (scaledBigInt1 * BigInt(10) ** BigInt(20)) / scaledBigInt2; @@ -118,19 +136,22 @@ export function divWorklet(num1: string, num2: string): string { } // Modulus function -export function modWorklet(num1: string, num2: string): string { +export function modWorklet(num1: string | number, num2: string | number): string { 'worklet'; - if (!isNumberStringWorklet(num1) || !isNumberStringWorklet(num2)) { - throw new Error('Arguments must be a numeric string'); + const num1Str = toStringWorklet(num1); + const num2Str = toStringWorklet(num2); + + if (!isNumberStringWorklet(num1Str) || !isNumberStringWorklet(num2Str)) { + throw new Error('Arguments must be a numeric string or number'); } - if (isZeroWorklet(num2)) { + if (isZeroWorklet(num2Str)) { throw new Error('Division by zero'); } - if (isZeroWorklet(num1)) { + if (isZeroWorklet(num1Str)) { return '0'; } - const [bigInt1, decimalPlaces1] = removeDecimalWorklet(num1); - const [bigInt2, decimalPlaces2] = removeDecimalWorklet(num2); + const [bigInt1, decimalPlaces1] = removeDecimalWorklet(num1Str); + const [bigInt2, decimalPlaces2] = removeDecimalWorklet(num2Str); const scaledBigInt1 = scaleUpWorklet(bigInt1, decimalPlaces1); const scaledBigInt2 = scaleUpWorklet(bigInt2, decimalPlaces2); const result = scaledBigInt1 % scaledBigInt2; @@ -138,84 +159,197 @@ export function modWorklet(num1: string, num2: string): string { } // Power function -export function powWorklet(base: string, exponent: string): string { +export function powWorklet(base: string | number, exponent: string | number): string { 'worklet'; - if (!isNumberStringWorklet(base) || !isNumberStringWorklet(exponent)) { - throw new Error('Arguments must be a numeric string'); + const baseStr = toStringWorklet(base); + const exponentStr = toStringWorklet(exponent); + + if (!isNumberStringWorklet(baseStr) || !isNumberStringWorklet(exponentStr)) { + throw new Error('Arguments must be a numeric string or number'); } - if (isZeroWorklet(base)) { + if (isZeroWorklet(baseStr)) { return '0'; } - if (isZeroWorklet(exponent)) { + if (isZeroWorklet(exponentStr)) { return '1'; } - const [bigIntBase, decimalPlaces] = removeDecimalWorklet(base); + const [bigIntBase, decimalPlaces] = removeDecimalWorklet(baseStr); const scaledBigIntBase = scaleUpWorklet(bigIntBase, decimalPlaces); - const result = scaledBigIntBase ** BigInt(exponent) / BigInt(10) ** BigInt(20); + const result = scaledBigIntBase ** BigInt(exponentStr) / BigInt(10) ** BigInt(20); return formatResultWorklet(result); } +// Logarithm base 10 function +export function log10Worklet(num: string | number): string { + 'worklet'; + const numStr = toStringWorklet(num); + + if (!isNumberStringWorklet(numStr)) { + throw new Error('Arguments must be a numeric string or number'); + } + if (isZeroWorklet(numStr)) { + throw new Error('Argument must be greater than 0'); + } + + const [bigIntNum, decimalPlaces] = removeDecimalWorklet(numStr); + const scaledBigIntNum = scaleUpWorklet(bigIntNum, decimalPlaces); + const result = Math.log10(Number(scaledBigIntNum)) - 20; // Adjust the scale factor for log10 + const resultBigInt = BigInt(result * 10 ** 20); + return formatResultWorklet(resultBigInt); +} + // Equality function -export function equalWorklet(num1: string, num2: string): boolean { +export function equalWorklet(num1: string | number, num2: string | number): boolean { 'worklet'; - if (!isNumberStringWorklet(num1) || !isNumberStringWorklet(num2)) { - throw new Error('Arguments must be a numeric string'); + const num1Str = toStringWorklet(num1); + const num2Str = toStringWorklet(num2); + + if (!isNumberStringWorklet(num1Str) || !isNumberStringWorklet(num2Str)) { + throw new Error('Arguments must be a numeric string or number'); } - const [bigInt1, decimalPlaces1] = removeDecimalWorklet(num1); - const [bigInt2, decimalPlaces2] = removeDecimalWorklet(num2); + const [bigInt1, decimalPlaces1] = removeDecimalWorklet(num1Str); + const [bigInt2, decimalPlaces2] = removeDecimalWorklet(num2Str); const scaledBigInt1 = scaleUpWorklet(bigInt1, decimalPlaces1); const scaledBigInt2 = scaleUpWorklet(bigInt2, decimalPlaces2); return scaledBigInt1 === scaledBigInt2; } // Greater than function -export function greaterThanWorklet(num1: string, num2: string): boolean { +export function greaterThanWorklet(num1: string | number, num2: string | number): boolean { 'worklet'; - if (!isNumberStringWorklet(num1) || !isNumberStringWorklet(num2)) { - throw new Error('Arguments must be a numeric string'); + const num1Str = toStringWorklet(num1); + const num2Str = toStringWorklet(num2); + + if (!isNumberStringWorklet(num1Str) || !isNumberStringWorklet(num2Str)) { + throw new Error('Arguments must be a numeric string or number'); } - const [bigInt1, decimalPlaces1] = removeDecimalWorklet(num1); - const [bigInt2, decimalPlaces2] = removeDecimalWorklet(num2); + const [bigInt1, decimalPlaces1] = removeDecimalWorklet(num1Str); + const [bigInt2, decimalPlaces2] = removeDecimalWorklet(num2Str); const scaledBigInt1 = scaleUpWorklet(bigInt1, decimalPlaces1); const scaledBigInt2 = scaleUpWorklet(bigInt2, decimalPlaces2); return scaledBigInt1 > scaledBigInt2; } // Greater than or equal to function -export function greaterThanOrEqualToWorklet(num1: string, num2: string): boolean { +export function greaterThanOrEqualToWorklet(num1: string | number, num2: string | number): boolean { 'worklet'; - if (!isNumberStringWorklet(num1) || !isNumberStringWorklet(num2)) { - throw new Error('Arguments must be a numeric string'); + const num1Str = toStringWorklet(num1); + const num2Str = toStringWorklet(num2); + + if (!isNumberStringWorklet(num1Str) || !isNumberStringWorklet(num2Str)) { + throw new Error('Arguments must be a numeric string or number'); } - const [bigInt1, decimalPlaces1] = removeDecimalWorklet(num1); - const [bigInt2, decimalPlaces2] = removeDecimalWorklet(num2); + const [bigInt1, decimalPlaces1] = removeDecimalWorklet(num1Str); + const [bigInt2, decimalPlaces2] = removeDecimalWorklet(num2Str); const scaledBigInt1 = scaleUpWorklet(bigInt1, decimalPlaces1); const scaledBigInt2 = scaleUpWorklet(bigInt2, decimalPlaces2); return scaledBigInt1 >= scaledBigInt2; } // Less than function -export function lessThanWorklet(num1: string, num2: string): boolean { +export function lessThanWorklet(num1: string | number, num2: string | number): boolean { 'worklet'; - if (!isNumberStringWorklet(num1) || !isNumberStringWorklet(num2)) { - throw new Error('Arguments must be a numeric string'); + const num1Str = toStringWorklet(num1); + const num2Str = toStringWorklet(num2); + + if (!isNumberStringWorklet(num1Str) || !isNumberStringWorklet(num2Str)) { + throw new Error('Arguments must be a numeric string or number'); } - const [bigInt1, decimalPlaces1] = removeDecimalWorklet(num1); - const [bigInt2, decimalPlaces2] = removeDecimalWorklet(num2); + const [bigInt1, decimalPlaces1] = removeDecimalWorklet(num1Str); + const [bigInt2, decimalPlaces2] = removeDecimalWorklet(num2Str); const scaledBigInt1 = scaleUpWorklet(bigInt1, decimalPlaces1); const scaledBigInt2 = scaleUpWorklet(bigInt2, decimalPlaces2); return scaledBigInt1 < scaledBigInt2; } // Less than or equal to function -export function lessThanOrEqualToWorklet(num1: string, num2: string): boolean { +export function lessThanOrEqualToWorklet(num1: string | number, num2: string | number): boolean { 'worklet'; - if (!isNumberStringWorklet(num1) || !isNumberStringWorklet(num2)) { - throw new Error('Arguments must be a numeric string'); + const num1Str = toStringWorklet(num1); + const num2Str = toStringWorklet(num2); + + if (!isNumberStringWorklet(num1Str) || !isNumberStringWorklet(num2Str)) { + throw new Error('Arguments must be a numeric string or number'); } - const [bigInt1, decimalPlaces1] = removeDecimalWorklet(num1); - const [bigInt2, decimalPlaces2] = removeDecimalWorklet(num2); + const [bigInt1, decimalPlaces1] = removeDecimalWorklet(num1Str); + const [bigInt2, decimalPlaces2] = removeDecimalWorklet(num2Str); const scaledBigInt1 = scaleUpWorklet(bigInt1, decimalPlaces1); const scaledBigInt2 = scaleUpWorklet(bigInt2, decimalPlaces2); return scaledBigInt1 <= scaledBigInt2; } + +// toFixed function +export function toFixedWorklet(num: string | number, decimalPlaces: number): string { + 'worklet'; + const numStr = toStringWorklet(num); + + if (!isNumberStringWorklet(numStr)) { + throw new Error('Argument must be a numeric string or number'); + } + + const [bigIntNum, numDecimalPlaces] = removeDecimalWorklet(numStr); + const scaledBigIntNum = scaleUpWorklet(bigIntNum, numDecimalPlaces); + + const scaleFactor = BigInt(10) ** BigInt(20 - decimalPlaces); + const roundedBigInt = ((scaledBigIntNum + scaleFactor / BigInt(2)) / scaleFactor) * scaleFactor; + + const resultStr = roundedBigInt.toString().padStart(20 + 1, '0'); // SCALE_FACTOR decimal places + at least 1 integer place + const integerPart = resultStr.slice(0, -20) || '0'; + const fractionalPart = resultStr.slice(-20, -20 + decimalPlaces).padEnd(decimalPlaces, '0'); + + return `${integerPart}.${fractionalPart}`; +} + +// Ceil function +export function ceilWorklet(num: string | number): string { + 'worklet'; + const numStr = toStringWorklet(num); + + if (!isNumberStringWorklet(numStr)) { + throw new Error('Argument must be a numeric string or number'); + } + + const [bigIntNum, decimalPlaces] = removeDecimalWorklet(numStr); + const scaledBigIntNum = scaleUpWorklet(bigIntNum, decimalPlaces); + + const scaleFactor = BigInt(10) ** BigInt(20); + const ceilBigInt = ((scaledBigIntNum + scaleFactor - BigInt(1)) / scaleFactor) * scaleFactor; + + return formatResultWorklet(ceilBigInt); +} + +// Floor function +export function floorWorklet(num: string | number): string { + 'worklet'; + const numStr = toStringWorklet(num); + + if (!isNumberStringWorklet(numStr)) { + throw new Error('Argument must be a numeric string or number'); + } + + const [bigIntNum, decimalPlaces] = removeDecimalWorklet(numStr); + const scaledBigIntNum = scaleUpWorklet(bigIntNum, decimalPlaces); + + const scaleFactor = BigInt(10) ** BigInt(20); + const floorBigInt = (scaledBigIntNum / scaleFactor) * scaleFactor; + + return formatResultWorklet(floorBigInt); +} + +// Round function +export function roundWorklet(num: string | number): string { + 'worklet'; + const numStr = toStringWorklet(num); + + if (!isNumberStringWorklet(numStr)) { + throw new Error('Argument must be a numeric string or number'); + } + + const [bigIntNum, decimalPlaces] = removeDecimalWorklet(numStr); + const scaledBigIntNum = scaleUpWorklet(bigIntNum, decimalPlaces); + + const scaleFactor = BigInt(10) ** BigInt(20); + const roundBigInt = ((scaledBigIntNum + scaleFactor / BigInt(2)) / scaleFactor) * scaleFactor; + + return formatResultWorklet(roundBigInt); +} diff --git a/src/__swaps__/safe-math/__tests__/SafeMath.test.ts b/src/__swaps__/safe-math/__tests__/SafeMath.test.ts index a80196afeec..2db42ca5f90 100644 --- a/src/__swaps__/safe-math/__tests__/SafeMath.test.ts +++ b/src/__swaps__/safe-math/__tests__/SafeMath.test.ts @@ -1,16 +1,21 @@ import BigNumber from 'bignumber.js'; import { + ceilWorklet, divWorklet, equalWorklet, + floorWorklet, greaterThanOrEqualToWorklet, greaterThanWorklet, lessThanOrEqualToWorklet, lessThanWorklet, + log10Worklet, modWorklet, mulWorklet, powWorklet, + roundWorklet, subWorklet, sumWorklet, + toFixedWorklet, } from '../SafeMath'; const RESULTS = { @@ -20,11 +25,16 @@ const RESULTS = { div: '325.56878986395199044836', mod: '2172.345', pow: '1546106588588.369025', + log10: '0.30102999566398124032', + toFixed: '1243425.35', + ceil: '1243426', + floor: '1243425', }; const VALUE_A = '1243425.345'; const VALUE_B = '3819.24'; const VALUE_C = '2'; +const VALUE_D = '1243425.745'; const NEGATIVE_VALUE = '-2412.12'; const ZERO = '0'; const ONE = '1'; @@ -32,103 +42,153 @@ const NON_NUMERIC_STRING = 'abc'; describe('SafeMath', () => { test('sumWorklet', () => { - expect(() => sumWorklet(NON_NUMERIC_STRING, VALUE_B)).toThrow('Arguments must be a numeric string'); - expect(() => sumWorklet(VALUE_A, NON_NUMERIC_STRING)).toThrow('Arguments must be a numeric string'); + expect(() => sumWorklet(NON_NUMERIC_STRING, VALUE_B)).toThrow('Arguments must be a numeric string or number'); + expect(() => sumWorklet(VALUE_A, NON_NUMERIC_STRING)).toThrow('Arguments must be a numeric string or number'); expect(sumWorklet(ZERO, ZERO)).toBe(ZERO); expect(sumWorklet(VALUE_A, ZERO)).toBe(VALUE_A); expect(sumWorklet(ZERO, VALUE_B)).toBe(VALUE_B); expect(sumWorklet(VALUE_A, VALUE_B)).toBe(RESULTS.sum); + expect(sumWorklet(Number(VALUE_A), VALUE_B)).toBe(RESULTS.sum); + expect(sumWorklet(VALUE_A, Number(VALUE_B))).toBe(RESULTS.sum); }); test('subWorklet', () => { - expect(() => subWorklet(NON_NUMERIC_STRING, VALUE_B)).toThrow('Arguments must be a numeric string'); - expect(() => subWorklet(VALUE_A, NON_NUMERIC_STRING)).toThrow('Arguments must be a numeric string'); + expect(() => subWorklet(NON_NUMERIC_STRING, VALUE_B)).toThrow('Arguments must be a numeric string or number'); + expect(() => subWorklet(VALUE_A, NON_NUMERIC_STRING)).toThrow('Arguments must be a numeric string or number'); expect(subWorklet(ZERO, ZERO)).toBe(ZERO); expect(subWorklet(VALUE_A, ZERO)).toBe(VALUE_A); expect(subWorklet(ZERO, VALUE_B)).toBe(`-${VALUE_B}`); expect(subWorklet(NEGATIVE_VALUE, ZERO)).toBe(NEGATIVE_VALUE); expect(subWorklet(VALUE_A, VALUE_B)).toBe(RESULTS.sub); + expect(subWorklet(Number(VALUE_A), VALUE_B)).toBe(RESULTS.sub); + expect(subWorklet(VALUE_A, Number(VALUE_B))).toBe(RESULTS.sub); }); test('mulWorklet', () => { - expect(() => mulWorklet(NON_NUMERIC_STRING, VALUE_B)).toThrow('Arguments must be a numeric string'); - expect(() => mulWorklet(VALUE_A, NON_NUMERIC_STRING)).toThrow('Arguments must be a numeric string'); + expect(() => mulWorklet(NON_NUMERIC_STRING, VALUE_B)).toThrow('Arguments must be a numeric string or number'); + expect(() => mulWorklet(VALUE_A, NON_NUMERIC_STRING)).toThrow('Arguments must be a numeric string or number'); expect(mulWorklet(ZERO, ZERO)).toBe(ZERO); expect(mulWorklet(VALUE_A, ZERO)).toBe(ZERO); expect(mulWorklet(ZERO, VALUE_B)).toBe(ZERO); expect(mulWorklet(VALUE_A, VALUE_B)).toBe(RESULTS.mul); + expect(mulWorklet(Number(VALUE_A), VALUE_B)).toBe(RESULTS.mul); + expect(mulWorklet(VALUE_A, Number(VALUE_B))).toBe(RESULTS.mul); }); test('divWorklet', () => { - expect(() => divWorklet(NON_NUMERIC_STRING, VALUE_B)).toThrow('Arguments must be a numeric string'); - expect(() => divWorklet(VALUE_A, NON_NUMERIC_STRING)).toThrow('Arguments must be a numeric string'); + expect(() => divWorklet(NON_NUMERIC_STRING, VALUE_B)).toThrow('Arguments must be a numeric string or number'); + expect(() => divWorklet(VALUE_A, NON_NUMERIC_STRING)).toThrow('Arguments must be a numeric string or number'); expect(() => divWorklet(ZERO, ZERO)).toThrow('Division by zero'); expect(() => divWorklet(VALUE_A, ZERO)).toThrow('Division by zero'); expect(divWorklet(ZERO, VALUE_B)).toBe(ZERO); expect(divWorklet(VALUE_A, VALUE_B)).toBe(RESULTS.div); + expect(divWorklet(Number(VALUE_A), VALUE_B)).toBe(RESULTS.div); + expect(divWorklet(VALUE_A, Number(VALUE_B))).toBe(RESULTS.div); }); test('modWorklet', () => { - expect(() => modWorklet(NON_NUMERIC_STRING, VALUE_B)).toThrow('Arguments must be a numeric string'); - expect(() => modWorklet(VALUE_A, NON_NUMERIC_STRING)).toThrow('Arguments must be a numeric string'); + expect(() => modWorklet(NON_NUMERIC_STRING, VALUE_B)).toThrow('Arguments must be a numeric string or number'); + expect(() => modWorklet(VALUE_A, NON_NUMERIC_STRING)).toThrow('Arguments must be a numeric string or number'); expect(() => modWorklet(ZERO, ZERO)).toThrow('Division by zero'); expect(() => modWorklet(VALUE_A, ZERO)).toThrow('Division by zero'); expect(modWorklet(ZERO, VALUE_B)).toBe(ZERO); expect(modWorklet(VALUE_A, VALUE_B)).toBe(RESULTS.mod); + expect(modWorklet(Number(VALUE_A), VALUE_B)).toBe(RESULTS.mod); + expect(modWorklet(VALUE_A, Number(VALUE_B))).toBe(RESULTS.mod); }); test('powWorklet', () => { - expect(() => powWorklet(NON_NUMERIC_STRING, VALUE_B)).toThrow('Arguments must be a numeric string'); - expect(() => powWorklet(VALUE_A, NON_NUMERIC_STRING)).toThrow('Arguments must be a numeric string'); + expect(() => powWorklet(NON_NUMERIC_STRING, VALUE_B)).toThrow('Arguments must be a numeric string or number'); + expect(() => powWorklet(VALUE_A, NON_NUMERIC_STRING)).toThrow('Arguments must be a numeric string or number'); expect(powWorklet(ZERO, VALUE_B)).toBe(ZERO); expect(powWorklet(VALUE_A, ZERO)).toBe(ONE); - expect(powWorklet(ZERO, VALUE_B)).toBe(ZERO); expect(powWorklet(VALUE_A, VALUE_C)).toBe(RESULTS.pow); + expect(powWorklet(Number(VALUE_A), VALUE_C)).toBe(RESULTS.pow); + expect(powWorklet(VALUE_A, Number(VALUE_C))).toBe(RESULTS.pow); + }); + + test('log10Worklet', () => { + expect(() => log10Worklet(NON_NUMERIC_STRING)).toThrow('Arguments must be a numeric string or number'); + expect(() => log10Worklet(ZERO)).toThrow('Argument must be greater than 0'); + expect(log10Worklet(VALUE_C)).toBe(RESULTS.log10); + expect(log10Worklet(Number(VALUE_C))).toBe(RESULTS.log10); }); test('equalWorklet', () => { - expect(() => equalWorklet(NON_NUMERIC_STRING, VALUE_B)).toThrow('Arguments must be a numeric string'); - expect(() => equalWorklet(VALUE_A, NON_NUMERIC_STRING)).toThrow('Arguments must be a numeric string'); + expect(() => equalWorklet(NON_NUMERIC_STRING, VALUE_B)).toThrow('Arguments must be a numeric string or number'); + expect(() => equalWorklet(VALUE_A, NON_NUMERIC_STRING)).toThrow('Arguments must be a numeric string or number'); expect(equalWorklet(ZERO, ZERO)).toBe(true); expect(equalWorklet(VALUE_A, VALUE_A)).toBe(true); expect(equalWorklet(VALUE_A, VALUE_B)).toBe(false); expect(equalWorklet(NEGATIVE_VALUE, NEGATIVE_VALUE)).toBe(true); + expect(equalWorklet(Number(VALUE_A), VALUE_A)).toBe(true); + expect(equalWorklet(VALUE_A, Number(VALUE_A))).toBe(true); }); test('greaterThanWorklet', () => { - expect(() => greaterThanWorklet(NON_NUMERIC_STRING, VALUE_B)).toThrow('Arguments must be a numeric string'); - expect(() => greaterThanWorklet(VALUE_A, NON_NUMERIC_STRING)).toThrow('Arguments must be a numeric string'); + expect(() => greaterThanWorklet(NON_NUMERIC_STRING, VALUE_B)).toThrow('Arguments must be a numeric string or number'); + expect(() => greaterThanWorklet(VALUE_A, NON_NUMERIC_STRING)).toThrow('Arguments must be a numeric string or number'); expect(greaterThanWorklet(VALUE_A, VALUE_B)).toBe(true); expect(greaterThanWorklet(VALUE_B, VALUE_A)).toBe(false); expect(greaterThanWorklet(VALUE_A, VALUE_A)).toBe(false); expect(greaterThanWorklet(NEGATIVE_VALUE, VALUE_A)).toBe(false); + expect(greaterThanWorklet(Number(VALUE_A), VALUE_B)).toBe(true); + expect(greaterThanWorklet(VALUE_A, Number(VALUE_B))).toBe(true); }); test('greaterThanOrEqualToWorklet', () => { - expect(() => greaterThanOrEqualToWorklet(NON_NUMERIC_STRING, VALUE_B)).toThrow('Arguments must be a numeric string'); - expect(() => greaterThanOrEqualToWorklet(VALUE_A, NON_NUMERIC_STRING)).toThrow('Arguments must be a numeric string'); + expect(() => greaterThanOrEqualToWorklet(NON_NUMERIC_STRING, VALUE_B)).toThrow('Arguments must be a numeric string or number'); + expect(() => greaterThanOrEqualToWorklet(VALUE_A, NON_NUMERIC_STRING)).toThrow('Arguments must be a numeric string or number'); expect(greaterThanOrEqualToWorklet(VALUE_A, VALUE_B)).toBe(true); expect(greaterThanOrEqualToWorklet(VALUE_B, VALUE_A)).toBe(false); expect(greaterThanOrEqualToWorklet(VALUE_A, VALUE_A)).toBe(true); expect(greaterThanOrEqualToWorklet(NEGATIVE_VALUE, VALUE_A)).toBe(false); + expect(greaterThanOrEqualToWorklet(Number(VALUE_A), VALUE_B)).toBe(true); + expect(greaterThanOrEqualToWorklet(VALUE_A, Number(VALUE_B))).toBe(true); }); test('lessThanWorklet', () => { - expect(() => lessThanWorklet(NON_NUMERIC_STRING, VALUE_B)).toThrow('Arguments must be a numeric string'); - expect(() => lessThanWorklet(VALUE_A, NON_NUMERIC_STRING)).toThrow('Arguments must be a numeric string'); + expect(() => lessThanWorklet(NON_NUMERIC_STRING, VALUE_B)).toThrow('Arguments must be a numeric string or number'); + expect(() => lessThanWorklet(VALUE_A, NON_NUMERIC_STRING)).toThrow('Arguments must be a numeric string or number'); expect(lessThanWorklet(VALUE_A, VALUE_B)).toBe(false); expect(lessThanWorklet(VALUE_B, VALUE_A)).toBe(true); expect(lessThanWorklet(VALUE_A, VALUE_A)).toBe(false); expect(lessThanWorklet(NEGATIVE_VALUE, VALUE_A)).toBe(true); + expect(lessThanWorklet(Number(VALUE_A), VALUE_B)).toBe(false); + expect(lessThanWorklet(VALUE_A, Number(VALUE_B))).toBe(false); }); test('lessThanOrEqualToWorklet', () => { - expect(() => lessThanOrEqualToWorklet(NON_NUMERIC_STRING, VALUE_B)).toThrow('Arguments must be a numeric string'); - expect(() => lessThanOrEqualToWorklet(VALUE_A, NON_NUMERIC_STRING)).toThrow('Arguments must be a numeric string'); + expect(() => lessThanOrEqualToWorklet(NON_NUMERIC_STRING, VALUE_B)).toThrow('Arguments must be a numeric string or number'); + expect(() => lessThanOrEqualToWorklet(VALUE_A, NON_NUMERIC_STRING)).toThrow('Arguments must be a numeric string or number'); expect(lessThanOrEqualToWorklet(VALUE_A, VALUE_B)).toBe(false); expect(lessThanOrEqualToWorklet(VALUE_B, VALUE_A)).toBe(true); expect(lessThanOrEqualToWorklet(VALUE_A, VALUE_A)).toBe(true); expect(lessThanOrEqualToWorklet(NEGATIVE_VALUE, VALUE_A)).toBe(true); + expect(lessThanOrEqualToWorklet(Number(VALUE_A), VALUE_B)).toBe(false); + expect(lessThanOrEqualToWorklet(VALUE_A, Number(VALUE_B))).toBe(false); + }); + + test('toFixedWorklet', () => { + expect(toFixedWorklet(VALUE_A, 2)).toBe(RESULTS.toFixed); + expect(toFixedWorklet(Number(VALUE_A), 2)).toBe(RESULTS.toFixed); + }); + + test('ceilWorklet', () => { + expect(ceilWorklet(VALUE_A)).toBe(RESULTS.ceil); + expect(ceilWorklet(Number(VALUE_A))).toBe(RESULTS.ceil); + }); + + test('floorWorklet', () => { + expect(floorWorklet(VALUE_A)).toBe(RESULTS.floor); + expect(floorWorklet(Number(VALUE_A))).toBe(RESULTS.floor); + }); + + test('roundWorklet', () => { + expect(roundWorklet(VALUE_A)).toBe(RESULTS.floor); + expect(roundWorklet(VALUE_D)).toBe(RESULTS.ceil); + expect(roundWorklet(Number(VALUE_A))).toBe(RESULTS.floor); + expect(roundWorklet(Number(VALUE_D))).toBe(RESULTS.ceil); }); }); From 251f24786d93a4102c6442473a5d4449fb16d347 Mon Sep 17 00:00:00 2001 From: brdy <41711440+BrodyHughes@users.noreply.github.com> Date: Wed, 29 May 2024 14:02:51 -0400 Subject: [PATCH 04/25] fix dynamic island overlap on recieve modal (#5672) * . * oop * oop * okay ty ben * change background opacity to 1 * . * oop * . --- src/navigation/Routes.ios.tsx | 11 ++----- src/navigation/config.tsx | 11 +++++++ src/screens/ReceiveModal.js | 55 ++++++++++++++++------------------- 3 files changed, 38 insertions(+), 39 deletions(-) diff --git a/src/navigation/Routes.ios.tsx b/src/navigation/Routes.ios.tsx index ab5a1975dbd..6e2f536649a 100644 --- a/src/navigation/Routes.ios.tsx +++ b/src/navigation/Routes.ios.tsx @@ -70,6 +70,7 @@ import { positionSheetConfig, appIconUnlockSheetConfig, swapConfig, + recieveModalSheetConfig, } from './config'; import { addCashSheet, emojiPreset, emojiPresetWallet, overlayExpandedPreset, sheetPreset } from './effects'; import { InitialRouteContext } from './initialRoute'; @@ -161,15 +162,7 @@ function NativeStackNavigator() { - + ({ + ...buildCoolModalConfig({ + ...params, + backgroundOpacity: 1, + scrollEnabled: false, + springDamping: 1, + }), + }), +}; + export const qrScannerConfig: PartialNavigatorConfigOptions = { options: ({ route: { params = {} } }) => ({ ...buildCoolModalConfig({ diff --git a/src/screens/ReceiveModal.js b/src/screens/ReceiveModal.js index f943a2d47ae..dbdee78ed01 100644 --- a/src/screens/ReceiveModal.js +++ b/src/screens/ReceiveModal.js @@ -3,21 +3,23 @@ import { toLower } from 'lodash'; import React, { useCallback, useState } from 'react'; import { useSelector } from 'react-redux'; import { createSelector } from 'reselect'; -import TouchableBackdrop from '../components/TouchableBackdrop'; import { CopyFloatingEmojis } from '../components/floating-emojis'; -import { Centered, Column, ColumnWithMargins } from '../components/layout'; +import { Column, ColumnWithMargins } from '../components/layout'; import QRCode from '../components/qr-code/QRCode'; import ShareButton from '../components/qr-code/ShareButton'; -import { SheetHandle } from '../components/sheet'; import { Text, TruncatedAddress } from '../components/text'; import { CopyToast, ToastPositionContainer } from '../components/toasts'; -import { useNavigation } from '../navigation/Navigation'; -import { abbreviations, deviceUtils } from '../utils'; -import { useAccountProfile } from '@/hooks'; +import { abbreviations, deviceUtils, safeAreaInsetValues } from '../utils'; +import { useAccountProfile, useDimensions } from '@/hooks'; import styled from '@/styled-thing'; import { padding, shadow } from '@/styles'; +import { SimpleSheet } from '@/components/sheet/SimpleSheet'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { sharedCoolModalTopOffset } from '@/navigation/config'; +import { IS_ANDROID, IS_IOS } from '@/env'; +import { Box } from '@/design-system'; -const QRCodeSize = ios ? 250 : Math.min(230, deviceUtils.dimensions.width - 20); +const QRCodeSize = IS_IOS ? 250 : Math.min(230, deviceUtils.dimensions.width - 20); const AddressText = styled(TruncatedAddress).attrs(({ theme: { colors } }) => ({ align: 'center', @@ -30,24 +32,12 @@ const AddressText = styled(TruncatedAddress).attrs(({ theme: { colors } }) => ({ width: '100%', }); -const Container = styled(Centered).attrs({ - direction: 'column', -})({ - bottom: 0, - flex: 1, -}); - -const Handle = styled(SheetHandle).attrs(({ theme: { colors } }) => ({ - color: colors.whiteLabel, -}))({ - marginBottom: 19, -}); - const QRWrapper = styled(Column).attrs({ align: 'center' })(({ theme: { colors } }) => ({ ...shadow.buildAsObject(0, 10, 50, colors.shadowBlack, 0.6), ...padding.object(24), backgroundColor: colors.whiteLabel, borderRadius: 39, + margin: 24, })); const NameText = styled(Text).attrs(({ theme: { colors } }) => ({ @@ -62,7 +52,6 @@ const accountAddressSelector = state => state.settings.accountAddress; const lowercaseAccountAddressSelector = createSelector(accountAddressSelector, toLower); export default function ReceiveModal() { - const { goBack } = useNavigation(); const accountAddress = useSelector(lowercaseAccountAddressSelector); const { accountName } = useAccountProfile(); @@ -74,12 +63,18 @@ export default function ReceiveModal() { }, []); const checksummedAddress = useMemo(() => toChecksumAddress(accountAddress), [accountAddress]); + const { height: deviceHeight } = useDimensions(); + const { top } = useSafeAreaInsets(); return ( - - - - + + @@ -90,10 +85,10 @@ export default function ReceiveModal() { - - - - - + + + + + ); } From ecb04e1ecd5467cee0bf47c2e31810b9f0514331 Mon Sep 17 00:00:00 2001 From: gregs Date: Wed, 29 May 2024 15:33:45 -0300 Subject: [PATCH 05/25] Gas optimizations (#5779) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf * ✨ * useWhyDidYouUpdate * EstimatedSwapGasFee * keepPreviousData * AnimatedText * isSameAddress --- .../Swap/components/EstimatedSwapGasFee.tsx | 36 +++++++++++ .../screens/Swap/components/GasButton.tsx | 35 ++++++----- .../screens/Swap/components/GasPanel.tsx | 41 ++++++++----- .../screens/Swap/components/ReviewPanel.tsx | 9 +-- .../screens/Swap/hooks/useEstimatedGasFee.ts | 56 +++++++++++++---- .../screens/Swap/hooks/useSelectedGas.ts | 17 +++--- .../Swap/hooks/useSwapEstimatedGasLimit.ts | 11 +++- src/__swaps__/utils/meteorology.ts | 60 ++++++++++++++----- src/hooks/useWhyDidYouUpdate.ts | 47 +++++++++++++++ 9 files changed, 235 insertions(+), 77 deletions(-) create mode 100644 src/__swaps__/screens/Swap/components/EstimatedSwapGasFee.tsx create mode 100644 src/hooks/useWhyDidYouUpdate.ts diff --git a/src/__swaps__/screens/Swap/components/EstimatedSwapGasFee.tsx b/src/__swaps__/screens/Swap/components/EstimatedSwapGasFee.tsx new file mode 100644 index 00000000000..9d281c3f425 --- /dev/null +++ b/src/__swaps__/screens/Swap/components/EstimatedSwapGasFee.tsx @@ -0,0 +1,36 @@ +import { AnimatedText, TextProps } from '@/design-system'; +import React, { memo } from 'react'; +import { useAnimatedStyle, withRepeat, withSequence, withSpring, withTiming } from 'react-native-reanimated'; + +import { pulsingConfig, sliderConfig } from '../constants'; +import { GasSettings } from '../hooks/useCustomGas'; +import { useSwapEstimatedGasFee } from '../hooks/useEstimatedGasFee'; + +export const EstimatedSwapGasFee = memo(function EstimatedGasFeeA({ + gasSettings, + align, + color = 'labelTertiary', + size = '15pt', + weight = 'bold', + tabularNumbers = true, +}: { gasSettings: GasSettings | undefined } & Partial>) { + const { data: estimatedGasFee = '--', isLoading } = useSwapEstimatedGasFee(gasSettings); + + const animatedOpacity = useAnimatedStyle(() => ({ + opacity: isLoading + ? withRepeat(withSequence(withTiming(0.5, pulsingConfig), withTiming(1, pulsingConfig)), -1, true) + : withSpring(1, sliderConfig), + })); + + return ( + + ); +}); diff --git a/src/__swaps__/screens/Swap/components/GasButton.tsx b/src/__swaps__/screens/Swap/components/GasButton.tsx index ae9fa1620ae..1841c8e4005 100644 --- a/src/__swaps__/screens/Swap/components/GasButton.tsx +++ b/src/__swaps__/screens/Swap/components/GasButton.tsx @@ -1,42 +1,39 @@ import { ChainId } from '@/__swaps__/types/chains'; import { weiToGwei } from '@/__swaps__/utils/ethereum'; -import { useMeteorologySuggestions } from '@/__swaps__/utils/meteorology'; +import { getCachedCurrentBaseFee, useMeteorologySuggestions } from '@/__swaps__/utils/meteorology'; import { add } from '@/__swaps__/utils/numbers'; import { ButtonPressAnimation } from '@/components/animations'; import { ContextMenu } from '@/components/context-menu'; import { Centered } from '@/components/layout'; import ContextMenuButton from '@/components/native-context-menu/contextMenu'; -import { Box, Inline, Stack, Text, TextIcon, useColorMode, useForegroundColor } from '@/design-system'; +import { Box, Inline, Text, TextIcon, useColorMode, useForegroundColor } from '@/design-system'; import { IS_ANDROID } from '@/env'; import * as i18n from '@/languages'; import { useSwapsStore } from '@/state/swaps/swapsStore'; import styled from '@/styled-thing'; import { gasUtils } from '@/utils'; import React, { ReactNode, useCallback, useMemo } from 'react'; +import { StyleSheet } from 'react-native'; import { runOnUI } from 'react-native-reanimated'; import { ETH_COLOR, ETH_COLOR_DARK, THICK_BORDER_WIDTH } from '../constants'; import { formatNumber } from '../hooks/formatNumber'; import { GasSettings, useCustomGasSettings } from '../hooks/useCustomGas'; -import { useSwapEstimatedGasFee } from '../hooks/useEstimatedGasFee'; import { GasSpeed, setSelectedGasSpeed, useSelectedGas, useSelectedGasSpeed } from '../hooks/useSelectedGas'; import { useSwapContext } from '../providers/swap-provider'; -import { StyleSheet } from 'react-native'; +import { EstimatedSwapGasFee } from './EstimatedSwapGasFee'; const { GAS_ICONS } = gasUtils; function EstimatedGasFee() { const chainId = useSwapsStore(s => s.inputAsset?.chainId || ChainId.mainnet); const gasSettings = useSelectedGas(chainId); - const estimatedGasFee = useSwapEstimatedGasFee(gasSettings); return ( 􀵟 - - {estimatedGasFee} - + ); } @@ -66,17 +63,19 @@ const GasSpeedPagerCentered = styled(Centered).attrs(() => ({ marginHorizontal: 8, }))({}); -function getEstimatedFeeRangeInGwei(gasSettings: GasSettings | undefined, currentBaseFee?: string | undefined) { +function getEstimatedFeeRangeInGwei(gasSettings: GasSettings | undefined, currentBaseFee: string | undefined) { if (!gasSettings) return undefined; if (!gasSettings.isEIP1559) return `${formatNumber(weiToGwei(gasSettings.gasPrice))} Gwei`; const { maxBaseFee, maxPriorityFee } = gasSettings; - return `${formatNumber(weiToGwei(add(maxBaseFee, maxPriorityFee)))} Gwei`; + const maxFee = formatNumber(weiToGwei(add(maxBaseFee, maxPriorityFee))); + + if (!currentBaseFee) return `${maxFee} Gwei`; + + const minFee = formatNumber(weiToGwei(add(currentBaseFee, maxPriorityFee))); - // return `${formatNumber(weiToGwei(add(baseFee, maxPriorityFee)))} - ${formatNumber( - // weiToGwei(add(maxBaseFee, maxPriorityFee)) - // )} Gwei`; + return `${minFee} - ${maxFee} Gwei`; } function keys(obj: Record | undefined) { @@ -118,9 +117,9 @@ const GasMenu = ({ children }: { children: ReactNode }) => { const menuItems = menuOptions.map(gasOption => { if (IS_ANDROID) return gasOption; - // const currentBaseFee = getCachedCurrentBaseFee(chainId); + const currentBaseFee = getCachedCurrentBaseFee(chainId); const gasSettings = gasOption === 'custom' ? customGasSettings : metereologySuggestions.data?.[gasOption]; - const subtitle = getEstimatedFeeRangeInGwei(gasSettings); + const subtitle = getEstimatedFeeRangeInGwei(gasSettings, currentBaseFee); return { actionKey: gasOption, @@ -130,7 +129,7 @@ const GasMenu = ({ children }: { children: ReactNode }) => { }; }); return { menuItems, menuTitle: '' }; - }, [customGasSettings, menuOptions, metereologySuggestions.data]); + }, [customGasSettings, menuOptions, metereologySuggestions.data, chainId]); if (metereologySuggestions.isLoading) return children; @@ -211,10 +210,10 @@ export function ReviewGasButton() { export const GasButton = () => { return ( - + - + ); }; diff --git a/src/__swaps__/screens/Swap/components/GasPanel.tsx b/src/__swaps__/screens/Swap/components/GasPanel.tsx index 597804ec7b4..2402552e183 100644 --- a/src/__swaps__/screens/Swap/components/GasPanel.tsx +++ b/src/__swaps__/screens/Swap/components/GasPanel.tsx @@ -1,5 +1,5 @@ import * as i18n from '@/languages'; -import React, { PropsWithChildren } from 'react'; +import React, { PropsWithChildren, useMemo } from 'react'; import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'; import { fadeConfig } from '@/__swaps__/screens/Swap/constants'; @@ -11,9 +11,10 @@ import { useBaseFee, useGasTrend, useIsChainEIP1559, - useMeteorologySuggestions, + useMeteorologySuggestion, } from '@/__swaps__/utils/meteorology'; import { add, subtract } from '@/__swaps__/utils/numbers'; +import { opacity } from '@/__swaps__/utils/swaps'; import { ButtonPressAnimation } from '@/components/animations'; import { Box, Inline, Separator, Stack, Text, globalColors, useColorMode, useForegroundColor } from '@/design-system'; import { IS_ANDROID } from '@/env'; @@ -25,9 +26,8 @@ import { useSwapsStore } from '@/state/swaps/swapsStore'; import { upperFirst } from 'lodash'; import { formatNumber } from '../hooks/formatNumber'; import { GasSettings, getCustomGasSettings, setCustomGasSettings, useCustomGasStore } from '../hooks/useCustomGas'; -import { useSwapEstimatedGasFee } from '../hooks/useEstimatedGasFee'; import { setSelectedGasSpeed, useSelectedGasSpeed } from '../hooks/useSelectedGas'; -import { opacity } from '@/__swaps__/utils/swaps'; +import { EstimatedSwapGasFee } from './EstimatedSwapGasFee'; const MINER_TIP_TYPE = 'minerTip'; const MAX_BASE_FEE_TYPE = 'maxBaseFee'; @@ -144,6 +144,9 @@ function CurrentBaseFee() { // loading state? + const isEIP1559 = useIsChainEIP1559(chainId); + if (!isEIP1559) return null; + return ( s.inputAsset?.chainId || ChainId.mainnet); - const selectedSpeed = useSelectedGasSpeed(chainId); const currentGasSettings = useCustomGasStore(s => select(s?.[chainId])); - const { data: suggestion } = useMeteorologySuggestions({ + const speed = useSelectedGasSpeed(chainId); + const { data: suggestion } = useMeteorologySuggestion({ chainId, - select: d => select(selectedSpeed === 'custom' ? undefined : d[selectedSpeed]), - enabled: !state && selectedSpeed !== 'custom', + speed, + select, + enabled: !!state, + notifyOnChangeProps: !!state && speed !== 'custom' ? ['data'] : [], }); - return state ?? currentGasSettings ?? suggestion; + return useMemo(() => state ?? currentGasSettings ?? suggestion, [currentGasSettings, state, suggestion]); } const setGasPanelState = (update: Partial) => { @@ -258,12 +263,12 @@ const stateToGasSettings = (s: GasPanelState | undefined): GasSettings | undefin if (s.gasPrice) return { isEIP1559: false, gasPrice: s.gasPrice || '0' }; return { isEIP1559: true, maxBaseFee: s.maxBaseFee || '0', maxPriorityFee: s.maxPriorityFee || '0' }; }; + function MaxTransactionFee() { const { isDarkMode } = useColorMode(); const gasPanelState = useGasPanelState(); - const gasSettings = stateToGasSettings(gasPanelState); - const maxTransactionFee = useSwapEstimatedGasFee(gasSettings); + const gasSettings = useMemo(() => stateToGasSettings(gasPanelState), [gasPanelState]); return ( @@ -279,9 +284,13 @@ function MaxTransactionFee() { - - {maxTransactionFee} - + ); @@ -290,7 +299,9 @@ function MaxTransactionFee() { function EditableGasSettings() { const chainId = useSwapsStore(s => s.inputAsset?.chainId || ChainId.mainnet); const isEIP1559 = useIsChainEIP1559(chainId); + if (!isEIP1559) return ; + return ( <> @@ -300,8 +311,6 @@ function EditableGasSettings() { } function saveCustomGasSettings() { - // input is debounced if the time between editing and closing the panel is less than the debounce time (500ms) it's gonna be outdated - const unsaved = useGasPanelStore.getState(); const { inputAsset } = useSwapsStore.getState(); diff --git a/src/__swaps__/screens/Swap/components/ReviewPanel.tsx b/src/__swaps__/screens/Swap/components/ReviewPanel.tsx index 392c632d1f0..511f5f3b182 100644 --- a/src/__swaps__/screens/Swap/components/ReviewPanel.tsx +++ b/src/__swaps__/screens/Swap/components/ReviewPanel.tsx @@ -28,8 +28,8 @@ import { chainNameForChainIdWithMainnetSubstitutionWorklet } from '@/__swaps__/u import { useEstimatedTime } from '@/__swaps__/utils/meteorology'; import { convertRawAmountToBalance, convertRawAmountToNativeDisplay, handleSignificantDecimals, multiply } from '@/__swaps__/utils/numbers'; import { useSwapsStore } from '@/state/swaps/swapsStore'; -import { useSwapEstimatedGasFee } from '../hooks/useEstimatedGasFee'; import { useSelectedGas, useSelectedGasSpeed } from '../hooks/useSelectedGas'; +import { EstimatedSwapGasFee } from './EstimatedSwapGasFee'; const unknown = i18n.t(i18n.l.swap.unknown); @@ -89,13 +89,8 @@ const RainbowFee = () => { function EstimatedGasFee() { const chainId = useSwapsStore(s => s.inputAsset?.chainId || ChainId.mainnet); const gasSettings = useSelectedGas(chainId); - const estimatedGasFee = useSwapEstimatedGasFee(gasSettings); - return ( - - {estimatedGasFee} - - ); + return ; } function EstimatedArrivalTime() { diff --git a/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts b/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts index 6e01456917b..6e858ae7424 100644 --- a/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts +++ b/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts @@ -3,11 +3,21 @@ import { weiToGwei } from '@/__swaps__/utils/ethereum'; import { add, multiply } from '@/__swaps__/utils/numbers'; import { useSwapsStore } from '@/state/swaps/swapsStore'; import ethereumUtils, { useNativeAssetForNetwork } from '@/utils/ethereumUtils'; -import { formatUnits } from 'viem'; +import { ETH_ADDRESS } from '@rainbow-me/swaps'; +import { useMemo } from 'react'; +import { formatUnits, zeroAddress } from 'viem'; import { formatCurrency, formatNumber } from './formatNumber'; import { GasSettings } from './useCustomGas'; import { useSwapEstimatedGasLimit } from './useSwapEstimatedGasLimit'; +function safeBigInt(value: string) { + try { + return BigInt(value); + } catch { + return 0n; + } +} + export function useEstimatedGasFee({ chainId, gasLimit, @@ -20,28 +30,52 @@ export function useEstimatedGasFee({ const network = ethereumUtils.getNetworkFromChainId(chainId); const nativeNetworkAsset = useNativeAssetForNetwork(network); - if (!gasLimit || !gasSettings || !nativeNetworkAsset) return 'Loading...'; // TODO: loading state + return useMemo(() => { + if (!gasLimit || !gasSettings || !nativeNetworkAsset?.price) return; - const amount = gasSettings.isEIP1559 ? add(gasSettings.maxBaseFee, gasSettings.maxPriorityFee) : gasSettings.gasPrice; + const amount = gasSettings.isEIP1559 ? add(gasSettings.maxBaseFee, gasSettings.maxPriorityFee) : gasSettings.gasPrice; - const totalWei = multiply(gasLimit, amount); - const nativePrice = nativeNetworkAsset.price.value?.toString(); + const totalWei = multiply(gasLimit, amount); + const networkAssetPrice = nativeNetworkAsset.price.value?.toString(); - if (!nativePrice) return `${formatNumber(weiToGwei(totalWei))} Gwei`; + if (!networkAssetPrice) return `${formatNumber(weiToGwei(totalWei))} Gwei`; - const gasAmount = formatUnits(BigInt(totalWei), nativeNetworkAsset.decimals).toString(); - const feeInUserCurrency = multiply(nativePrice, gasAmount); + const gasAmount = formatUnits(safeBigInt(totalWei), nativeNetworkAsset.decimals).toString(); + const feeInUserCurrency = multiply(networkAssetPrice, gasAmount); - return formatCurrency(feeInUserCurrency); + return formatCurrency(feeInUserCurrency); + }, [gasLimit, gasSettings, nativeNetworkAsset]); } +const eth = ETH_ADDRESS.toLowerCase(); +const isEth = (address: string) => [eth, zeroAddress, 'eth'].includes(address.toLowerCase()); +const isSameAddress = (a: string, b: string) => { + if (isEth(a) && isEth(b)) return true; + return a.toLowerCase() === b.toLowerCase(); +}; export function useSwapEstimatedGasFee(gasSettings: GasSettings | undefined) { const chainId = useSwapsStore(s => s.inputAsset?.chainId || ChainId.mainnet); const assetToSell = useSwapsStore(s => s.inputAsset); + const assetToBuy = useSwapsStore(s => s.outputAsset); const quote = useSwapsStore(s => s.quote); - const { data: gasLimit } = useSwapEstimatedGasLimit({ chainId, quote, assetToSell }, { enabled: !!quote }); + const { data: gasLimit, isFetching } = useSwapEstimatedGasLimit( + { chainId, quote, assetToSell }, + { + enabled: + !!quote && + !!assetToSell && + !!assetToBuy && + !('error' in quote) && + // the quote and the input/output assets are not updated together, + // we shouldn't try to estimate if the assets are not the same as the quote (probably still fetching a quote) + isSameAddress(quote.sellTokenAddress, assetToSell.address) && + isSameAddress(quote.buyTokenAddress, assetToBuy.address), + } + ); + + const estimatedFee = useEstimatedGasFee({ chainId, gasLimit, gasSettings }); - return useEstimatedGasFee({ chainId, gasLimit, gasSettings }); + return useMemo(() => ({ isLoading: isFetching, data: estimatedFee }), [estimatedFee, isFetching]); } diff --git a/src/__swaps__/screens/Swap/hooks/useSelectedGas.ts b/src/__swaps__/screens/Swap/hooks/useSelectedGas.ts index 667d0b46c74..0b61b0c1047 100644 --- a/src/__swaps__/screens/Swap/hooks/useSelectedGas.ts +++ b/src/__swaps__/screens/Swap/hooks/useSelectedGas.ts @@ -18,19 +18,22 @@ export const useSelectedGasSpeed = (chainId: ChainId) => export const setSelectedGasSpeed = (chainId: ChainId, speed: GasSpeed) => useSelectedGasSpeedStore.setState({ [chainId]: speed }); export const getSelectedGasSpeed = (chainId: ChainId) => useSelectedGasSpeedStore.getState()[chainId] || 'fast'; -export function useSelectedGas(chainId: ChainId) { - const selectedGasSpeed = useSelectedGasSpeed(chainId); - +export function useGasSettings(chainId: ChainId, speed: GasSpeed) { const userCustomGasSettings = useCustomGasSettings(chainId); const { data: metereologySuggestions } = useMeteorologySuggestions({ chainId, - enabled: selectedGasSpeed !== 'custom', + enabled: speed !== 'custom', }); return useMemo(() => { - if (selectedGasSpeed === 'custom') return userCustomGasSettings; - return metereologySuggestions?.[selectedGasSpeed]; - }, [selectedGasSpeed, userCustomGasSettings, metereologySuggestions]); + if (speed === 'custom') return userCustomGasSettings; + return metereologySuggestions?.[speed]; + }, [speed, userCustomGasSettings, metereologySuggestions]); +} + +export function useSelectedGas(chainId: ChainId) { + const selectedGasSpeed = useSelectedGasSpeed(chainId); + return useGasSettings(chainId, selectedGasSpeed); } export function getGasSettings(speed: GasSpeed, chainId: ChainId) { diff --git a/src/__swaps__/screens/Swap/hooks/useSwapEstimatedGasLimit.ts b/src/__swaps__/screens/Swap/hooks/useSwapEstimatedGasLimit.ts index 905a2ae1c58..c30913f3da9 100644 --- a/src/__swaps__/screens/Swap/hooks/useSwapEstimatedGasLimit.ts +++ b/src/__swaps__/screens/Swap/hooks/useSwapEstimatedGasLimit.ts @@ -25,7 +25,7 @@ export type EstimateSwapGasLimitArgs = { // Query Key const estimateSwapGasLimitQueryKey = ({ chainId, quote, assetToSell }: EstimateSwapGasLimitArgs) => - createQueryKey('estimateSwapGasLimit', { chainId, quote, assetToSell }, { persisterVersion: 1 }); + createQueryKey('estimateSwapGasLimit', { chainId, quote, assetToSell }); type EstimateSwapGasLimitQueryKey = ReturnType; @@ -93,6 +93,13 @@ export function useSwapEstimatedGasLimit( assetToSell, }), estimateSwapGasLimitQueryFunction, - { keepPreviousData: true, staleTime: 12000, cacheTime: Infinity, ...config } + { + staleTime: 30 * 1000, // 30s + cacheTime: 60 * 1000, // 1min + notifyOnChangeProps: ['data', 'isFetching'], + keepPreviousData: true, + placeholderData: gasUnits.basic_swap[chainId], + ...config, + } ); } diff --git a/src/__swaps__/utils/meteorology.ts b/src/__swaps__/utils/meteorology.ts index 3606135cd51..cff7bc1b5f3 100644 --- a/src/__swaps__/utils/meteorology.ts +++ b/src/__swaps__/utils/meteorology.ts @@ -5,7 +5,9 @@ import { rainbowMeteorologyGetData } from '@/handlers/gasFees'; import { abs, lessThan, subtract } from '@/helpers/utilities'; import { QueryConfig, QueryFunctionArgs, QueryFunctionResult, createQueryKey, queryClient } from '@/react-query'; import { getNetworkFromChainId } from '@/utils/ethereumUtils'; -import { GasSpeed, getGasSettings, getSelectedGasSpeed } from '../screens/Swap/hooks/useSelectedGas'; +import { useCallback } from 'react'; +import { GasSettings } from '../screens/Swap/hooks/useCustomGas'; +import { GasSpeed, getSelectedGasSpeed, useGasSettings } from '../screens/Swap/hooks/useSelectedGas'; import { getMinimalTimeUnitStringForMs } from './time'; // Query Types @@ -89,7 +91,11 @@ export async function fetchMeteorology( export function useMeteorology( { chainId }: MeteorologyArgs, - { select, enabled }: { select?: (data: MeteorologyResult) => Selected; enabled?: boolean } = { select: data => data as Selected } + { + select, + enabled, + notifyOnChangeProps = ['data'], + }: { select?: (data: MeteorologyResult) => Selected; enabled?: boolean; notifyOnChangeProps?: 'data'[] } ) { return useQuery(meteorologyQueryKey({ chainId }), meteorologyQueryFunction, { select, @@ -97,7 +103,7 @@ export function useMeteorology( refetchInterval: 12_000, // 12 seconds staleTime: 12_000, // 12 seconds cacheTime: Infinity, - notifyOnChangeProps: ['data'], + notifyOnChangeProps, }); } @@ -186,19 +192,21 @@ function findClosestValue(target: string, array: string[]) { })!; } +function selectEstimatedTime({ data }: MeteorologyResult, selectedGas: GasSettings | undefined) { + if ('legacy' in data) return undefined; + if (!selectedGas?.isEIP1559) return undefined; + const value = findClosestValue(selectedGas.maxPriorityFee, Object.values(data.confirmationTimeByPriorityFee)); + const [time] = Object.entries(data.confirmationTimeByPriorityFee).find(([, v]) => v === value) || []; + if (!time) return undefined; + return `${+time >= 3600 ? '>' : '~'} ${getMinimalTimeUnitStringForMs(+time * 1000)}`; +} + export function useEstimatedTime({ chainId, speed }: { chainId: ChainId; speed: GasSpeed }) { + const selectedGas = useGasSettings(chainId, speed); return useMeteorology( { chainId }, { - select: ({ data }) => { - if ('legacy' in data) return undefined; - const gasSettings = getGasSettings(speed, chainId); - if (!gasSettings?.isEIP1559) return undefined; - const value = findClosestValue(gasSettings.maxPriorityFee, Object.values(data.confirmationTimeByPriorityFee)); - const [time] = Object.entries(data.confirmationTimeByPriorityFee).find(([, v]) => v === value) || []; - if (!time) return undefined; - return `${+time >= 3600 ? '>' : '~'} ${getMinimalTimeUnitStringForMs(+time * 1000)}`; - }, + select: useCallback((data: MeteorologyResult) => selectEstimatedTime(data, selectedGas), [selectedGas]), } ); } @@ -209,20 +217,40 @@ export const getCachedCurrentBaseFee = (chainId: ChainId) => { return selectBaseFee(data); }; -export function useMeteorologySuggestions>({ +type GasSuggestions = ReturnType; +export function useMeteorologySuggestions({ chainId, enabled }: { chainId: ChainId; enabled?: boolean }) { + return useMeteorology({ chainId }, { select: selectGasSuggestions, enabled }); +} + +export function useMeteorologySuggestion({ chainId, + speed, enabled, select = s => s as Selected, + notifyOnChangeProps = ['data'], }: { chainId: ChainId; + speed: GasSpeed; enabled?: boolean; - select?: (d: ReturnType) => Selected; + select?: (d: GasSuggestions[keyof GasSuggestions] | undefined) => Selected; + notifyOnChangeProps?: ['data'] | []; }) { - return useMeteorology({ chainId }, { select: d => select(selectGasSuggestions(d)), enabled }); + return useMeteorology( + { chainId }, + { + select: useCallback( + (d: MeteorologyResult) => select(speed === 'custom' ? undefined : selectGasSuggestions(d)[speed]), + [select, speed] + ), + enabled: enabled && speed !== 'custom', + notifyOnChangeProps, + } + ); } +const selectIsEIP1559 = ({ data }: MeteorologyResult) => !('legacy' in data); export const useIsChainEIP1559 = (chainId: ChainId) => { - const { data } = useMeteorology({ chainId }, { select: ({ data }) => !('legacy' in data) }); + const { data } = useMeteorology({ chainId }, { select: selectIsEIP1559 }); if (data === undefined) return true; return data; }; diff --git a/src/hooks/useWhyDidYouUpdate.ts b/src/hooks/useWhyDidYouUpdate.ts new file mode 100644 index 00000000000..032e778e6ee --- /dev/null +++ b/src/hooks/useWhyDidYouUpdate.ts @@ -0,0 +1,47 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useEffect, useRef } from 'react'; + +/** + * Debug hook showing which props updated between two renders + * @example + * + * const MyComponent = React.memo(props => { + * useWhyDidYouUpdate('MyComponent', props); + * return ) { + // Get a mutable ref object where we can store props ... + // ... for comparison next time this hook runs. + const previousProps = useRef() as any; + + useEffect(() => { + if (previousProps.current) { + // Get all keys from previous and current props + const allKeys = Object.keys({ ...previousProps.current, ...props }); + // Use this object to keep track of changed props + const changesObj: Record = {}; + // Iterate through keys + allKeys.forEach(key => { + // If previous is different from current + if (previousProps.current[key] !== props[key]) { + // Add to changesObj + changesObj[key] = { + from: previousProps.current[key], + to: props[key], + }; + } + }); + + // If changesObj not empty then output to console + if (Object.keys(changesObj).length) { + console.log('[why-did-you-update]', name, changesObj); + } + } + + // Finally update previousProps with current props for next hook call + previousProps.current = props; + }); +} From 7e367e1529f749458e4b76cc2c2edcaa3f7c59de Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Wed, 29 May 2024 15:29:29 -0400 Subject: [PATCH 06/25] fix other networks section (#5784) --- .../TokenList/TokenToBuySection.tsx | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/__swaps__/screens/Swap/components/TokenList/TokenToBuySection.tsx b/src/__swaps__/screens/Swap/components/TokenList/TokenToBuySection.tsx index 67a8837b0d8..9eb939a2892 100644 --- a/src/__swaps__/screens/Swap/components/TokenList/TokenToBuySection.tsx +++ b/src/__swaps__/screens/Swap/components/TokenList/TokenToBuySection.tsx @@ -1,12 +1,12 @@ import React, { useCallback } from 'react'; -import { TextStyle } from 'react-native'; +import { StyleSheet, TextStyle } from 'react-native'; import Animated, { useDerivedValue } from 'react-native-reanimated'; import { FlashList } from '@shopify/flash-list'; import * as i18n from '@/languages'; import { CoinRow } from '@/__swaps__/screens/Swap/components/CoinRow'; import { SearchAsset } from '@/__swaps__/types/search'; -import { AnimatedText, Box, Inline, Inset, Stack, Text } from '@/design-system'; +import { AnimatedText, Box, Inline, Inset, Stack, Text, useForegroundColor } from '@/design-system'; import { AssetToBuySection, AssetToBuySectionId } from '@/__swaps__/screens/Swap/hooks/useSearchCurrencyLists'; import { ChainId } from '@/__swaps__/types/chains'; import { TextColor } from '@/design-system/color/palettes'; @@ -42,12 +42,12 @@ const sectionProps: { [id in AssetToBuySectionId]: SectionProp } = { unverified: { title: i18n.t(i18n.l.token_search.section_header.unverified), symbol: '􀇿', - color: 'background: rgba(255, 218, 36, 1)', + color: 'rgba(255, 218, 36, 1)', }, other_networks: { title: i18n.t(i18n.l.token_search.section_header.on_other_networks), - symbol: 'network', - color: 'labelTertiary', + symbol: '􀊝', + color: '', }, }; @@ -68,6 +68,8 @@ const AnimatedFlashListComponent = Animated.createAnimatedComponent(FlashList { const { setAsset, selectedOutputChainId } = useSwapContext(); + const label = useForegroundColor('label'); + const handleSelectToken = useCallback( (token: SearchAsset) => { const userAsset = userAssetsStore.getState().getUserAsset(token.uniqueId); @@ -91,7 +93,10 @@ export const TokenToBuySection = ({ section }: { section: AssetToBuySection }) = const color = useDerivedValue(() => { if (section.id !== 'bridge') { - return sectionProps[section.id].color as TextColor; + if (sectionProps[section.id].color) { + return sectionProps[section.id].color as TextColor; + } + return label as TextColor; } return bridgeSectionsColorsByChain[selectedOutputChainId.value || ChainId.mainnet] as TextColor; @@ -105,9 +110,8 @@ export const TokenToBuySection = ({ section }: { section: AssetToBuySection }) = {section.id === 'other_networks' ? ( - - {/* */} - + + {i18n.t(i18n.l.swap.tokens_input.nothing_found)} @@ -154,3 +158,9 @@ export const TokenToBuySection = ({ section }: { section: AssetToBuySection }) = ); }; + +export const styles = StyleSheet.create({ + solidColorCoinIcon: { + opacity: 0.4, + }, +}); From f3307cb2879aa5680aeef675774ac483cf045129 Mon Sep 17 00:00:00 2001 From: Ben Goldberg Date: Wed, 29 May 2024 12:31:15 -0700 Subject: [PATCH 07/25] Swaps: fix favorite button press (#5782) * fix * android fix --- .../screens/Swap/components/CoinRow.tsx | 122 ++++++++++-------- 1 file changed, 66 insertions(+), 56 deletions(-) diff --git a/src/__swaps__/screens/Swap/components/CoinRow.tsx b/src/__swaps__/screens/Swap/components/CoinRow.tsx index 78c04d3298e..7f8a1eabc7a 100644 --- a/src/__swaps__/screens/Swap/components/CoinRow.tsx +++ b/src/__swaps__/screens/Swap/components/CoinRow.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react'; import { ButtonPressAnimation } from '@/components/animations'; -import { Box, HitSlop, Inline, Text } from '@/design-system'; +import { Box, Column, Columns, HitSlop, Inline, Text } from '@/design-system'; import { TextColor } from '@/design-system/color/palettes'; import { CoinRowButton } from '@/__swaps__/screens/Swap/components/CoinRowButton'; import { BalancePill } from '@/__swaps__/screens/Swap/components/BalancePill'; @@ -64,64 +64,74 @@ export const CoinRow = ({ }, [isTrending]); return ( - - - - - - - - {name} - - - - {output ? symbol : `${balance}`} - - {isTrending && percentChange && ( - - - {percentChange.prefix} + + + + + + + + + + + {name} - - {percentChange.change} - - - )} + + + {output ? symbol : `${balance}`} + + {isTrending && percentChange && ( + + + {percentChange.prefix} + + + {percentChange.change} + + + )} + + + + {!output && } + + + + + {output && ( + + + + + toggleFavorite(address)} + icon="􀋃" + weight="black" + /> - - {output ? ( - - - toggleFavorite(address)} - icon="􀋃" - weight="black" - /> - - ) : ( - - )} - - - + + )} + + ); }; From 009f887c134134c2824b159057d523ea9c6ec676 Mon Sep 17 00:00:00 2001 From: gregs Date: Wed, 29 May 2024 17:41:40 -0300 Subject: [PATCH 08/25] todo --- src/__swaps__/screens/Swap/providers/swap-provider.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index 414f0825818..0aada4d778f 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -272,6 +272,10 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { return { label: 'Enter Amount', disabled: true }; } + if (quote.value && 'error' in quote.value) return { label: 'Error', disabled: true }; + + // TODO: after the tx is built, we should check if the user has enough balance to cover the tx + return { icon: '􀕹', label: 'Review', disabled: false }; }); From 2ce9e5f617f4864faf5b60fddb33ab82bc9b5054 Mon Sep 17 00:00:00 2001 From: gregs Date: Wed, 29 May 2024 17:50:14 -0300 Subject: [PATCH 09/25] remove console logs --- .../screens/Swap/components/GestureHandlerV1Button.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/__swaps__/screens/Swap/components/GestureHandlerV1Button.tsx b/src/__swaps__/screens/Swap/components/GestureHandlerV1Button.tsx index 2a3d2bb8cdb..1e55149969e 100644 --- a/src/__swaps__/screens/Swap/components/GestureHandlerV1Button.tsx +++ b/src/__swaps__/screens/Swap/components/GestureHandlerV1Button.tsx @@ -1,10 +1,10 @@ +import { ButtonPressAnimation } from '@/components/animations'; +import { IS_IOS } from '@/env'; import ConditionalWrap from 'conditional-wrap'; import React from 'react'; import { StyleProp, ViewProps, ViewStyle } from 'react-native'; import { TapGestureHandler, TapGestureHandlerGestureEvent } from 'react-native-gesture-handler'; import Animated, { AnimatedStyle, runOnJS, useAnimatedGestureHandler } from 'react-native-reanimated'; -import { ButtonPressAnimation } from '@/components/animations'; -import { IS_IOS } from '@/env'; export type GestureHandlerButtonProps = { buttonPressWrapperStyleIOS?: StyleProp; @@ -76,14 +76,11 @@ export const GestureHandlerV1Button = React.forwardRef(function GestureHandlerV1 if (onPressStartWorklet) onPressStartWorklet(); }, onActive: () => { - console.log('here'); if (onPressWorklet) onPressWorklet(); if (onPressJS) runOnJS(onPressJS)(); }, }); - console.log({ disabled }); - return ( Date: Thu, 30 May 2024 13:20:39 -0300 Subject: [PATCH 10/25] Insufficient Funds --- .../screens/Swap/hooks/useEstimatedGasFee.ts | 25 ++++++-------- .../Swap/hooks/useSwapEstimatedGasLimit.ts | 4 +++ .../screens/Swap/providers/swap-provider.tsx | 34 +++++++++++++++++-- src/state/assets/userAssets.ts | 14 +++++++- src/utils/isSameAddress.ts | 10 ++++++ 5 files changed, 70 insertions(+), 17 deletions(-) create mode 100644 src/utils/isSameAddress.ts diff --git a/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts b/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts index 6e858ae7424..73fa26883a7 100644 --- a/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts +++ b/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts @@ -3,9 +3,9 @@ import { weiToGwei } from '@/__swaps__/utils/ethereum'; import { add, multiply } from '@/__swaps__/utils/numbers'; import { useSwapsStore } from '@/state/swaps/swapsStore'; import ethereumUtils, { useNativeAssetForNetwork } from '@/utils/ethereumUtils'; -import { ETH_ADDRESS } from '@rainbow-me/swaps'; +import { isSameAddress } from '@/utils/isSameAddress'; import { useMemo } from 'react'; -import { formatUnits, zeroAddress } from 'viem'; +import { formatUnits } from 'viem'; import { formatCurrency, formatNumber } from './formatNumber'; import { GasSettings } from './useCustomGas'; import { useSwapEstimatedGasLimit } from './useSwapEstimatedGasLimit'; @@ -18,6 +18,11 @@ function safeBigInt(value: string) { } } +export function calculateGasFee(gasSettings: GasSettings, gasLimit: string) { + const amount = gasSettings.isEIP1559 ? add(gasSettings.maxBaseFee, gasSettings.maxPriorityFee) : gasSettings.gasPrice; + return multiply(gasLimit, amount); +} + export function useEstimatedGasFee({ chainId, gasLimit, @@ -33,26 +38,18 @@ export function useEstimatedGasFee({ return useMemo(() => { if (!gasLimit || !gasSettings || !nativeNetworkAsset?.price) return; - const amount = gasSettings.isEIP1559 ? add(gasSettings.maxBaseFee, gasSettings.maxPriorityFee) : gasSettings.gasPrice; - - const totalWei = multiply(gasLimit, amount); + const fee = calculateGasFee(gasSettings, gasLimit); const networkAssetPrice = nativeNetworkAsset.price.value?.toString(); - if (!networkAssetPrice) return `${formatNumber(weiToGwei(totalWei))} Gwei`; + if (!networkAssetPrice) return `${formatNumber(weiToGwei(fee))} Gwei`; - const gasAmount = formatUnits(safeBigInt(totalWei), nativeNetworkAsset.decimals).toString(); - const feeInUserCurrency = multiply(networkAssetPrice, gasAmount); + const feeFormatted = formatUnits(safeBigInt(fee), nativeNetworkAsset.decimals).toString(); + const feeInUserCurrency = multiply(networkAssetPrice, feeFormatted); return formatCurrency(feeInUserCurrency); }, [gasLimit, gasSettings, nativeNetworkAsset]); } -const eth = ETH_ADDRESS.toLowerCase(); -const isEth = (address: string) => [eth, zeroAddress, 'eth'].includes(address.toLowerCase()); -const isSameAddress = (a: string, b: string) => { - if (isEth(a) && isEth(b)) return true; - return a.toLowerCase() === b.toLowerCase(); -}; export function useSwapEstimatedGasFee(gasSettings: GasSettings | undefined) { const chainId = useSwapsStore(s => s.inputAsset?.chainId || ChainId.mainnet); diff --git a/src/__swaps__/screens/Swap/hooks/useSwapEstimatedGasLimit.ts b/src/__swaps__/screens/Swap/hooks/useSwapEstimatedGasLimit.ts index c30913f3da9..3ec604d1e7c 100644 --- a/src/__swaps__/screens/Swap/hooks/useSwapEstimatedGasLimit.ts +++ b/src/__swaps__/screens/Swap/hooks/useSwapEstimatedGasLimit.ts @@ -103,3 +103,7 @@ export function useSwapEstimatedGasLimit( } ); } + +export const getSwapEstimatedGasLimitCachedData = (args: EstimateSwapGasLimitArgs) => { + return queryClient.getQueryData(estimateSwapGasLimitQueryKey(args)); +}; diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index 0e4ad793d31..ad84b978ed2 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -23,6 +23,7 @@ import { ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '@/__swaps__/ import { ChainId } from '@/__swaps__/types/chains'; import { SwapAssetType, inputKeys } from '@/__swaps__/types/swap'; import { isSameAsset } from '@/__swaps__/utils/assets'; +import { add, lessThan } from '@/__swaps__/utils/numbers'; import { parseAssetAndExtend } from '@/__swaps__/utils/swaps'; import { LegacyTransactionGasParamAmounts, TransactionGasParamAmounts } from '@/entities'; import { getCachedProviderForNetwork, isHardHat } from '@/handlers/web3'; @@ -37,10 +38,14 @@ import { walletExecuteRap } from '@/raps/execute'; import { QuoteTypeMap, RapSwapActionParameters } from '@/raps/references'; import { queryClient } from '@/react-query'; import { userAssetsQueryKey } from '@/resources/assets/UserAssetsQuery'; +import { getUserNativeNetworkAsset } from '@/state/assets/userAssets'; import { swapsStore } from '@/state/swaps/swapsStore'; import { ethereumUtils } from '@/utils'; +import { isEth } from '@/utils/isSameAddress'; import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; +import { calculateGasFee } from '../hooks/useEstimatedGasFee'; import { getGasSettingsBySpeed, getSelectedGas } from '../hooks/useSelectedGas'; +import { getSwapEstimatedGasLimitCachedData } from '../hooks/useSwapEstimatedGasLimit'; const swapping = i18n.t(i18n.l.swap.actions.swapping); const tapToSwap = i18n.t(i18n.l.swap.actions.tap_to_swap); @@ -426,9 +431,34 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { return { label: 'Enter Amount', disabled: true }; } - if (quote.value && 'error' in quote.value) return { label: 'Error', disabled: true }; + const _quote = quote.value; + if (_quote) { + if ('error' in _quote) return { label: 'Error', disabled: true }; - // TODO: after the tx is built, we should check if the user has enough balance to cover the tx + // TODO: after the tx is built, we should check if the user has enough balance to cover the tx + const estimatedGasLimit = getSwapEstimatedGasLimitCachedData({ + chainId: _quote.chainId, + quote: _quote, + assetToSell: internalSelectedInputAsset.value, + }); + if (!estimatedGasLimit) return { label: 'Estimating...', disabled: true }; + + const gasSettings = getSelectedGas(_quote.chainId); + if (!gasSettings) { + // this could happen if metereology is down, or some other edge cases that are not properly handled yet + return { label: 'Error', disabled: true }; + } + + const gasFee = calculateGasFee(gasSettings, estimatedGasLimit); + + const nativeAmountSelling = isEth(_quote.sellTokenAddress) ? _quote.sellAmount.toString() : '0'; + const totalNativeSpentInTx = add(add(_quote.value?.toString() || '0', gasFee), nativeAmountSelling); + + const userBalance = getUserNativeNetworkAsset(_quote.chainId)?.balance.amount || '0'; + if (lessThan(userBalance, totalNativeSpentInTx)) { + return { label: 'Insufficient Funds', disabled: true }; + } + } return { icon: '􀕹', label: 'Review', disabled: false }; }); diff --git a/src/state/assets/userAssets.ts b/src/state/assets/userAssets.ts index 68a43a13b37..52c3d1a88a3 100644 --- a/src/state/assets/userAssets.ts +++ b/src/state/assets/userAssets.ts @@ -1,9 +1,13 @@ import { Hex } from 'viem'; import { ParsedSearchAsset, UniqueId, UserAssetFilter } from '@/__swaps__/types/assets'; +import { ChainId } from '@/__swaps__/types/chains'; import { deriveAddressAndChainWithUniqueId } from '@/__swaps__/utils/address'; -import { createRainbowStore } from '@/state/internal/createRainbowStore'; +import { Network } from '@/helpers'; import { RainbowError, logger } from '@/logger'; +import { getNetworkObj } from '@/networks'; +import { createRainbowStore } from '@/state/internal/createRainbowStore'; +import { getNetworkFromChainId, getUniqueId } from '@/utils/ethereumUtils'; export interface UserAssetsState { userAssetsById: Set; @@ -167,3 +171,11 @@ export const userAssetsStore = createRainbowStore( deserializer: deserializeUserAssetsState, } ); + +export function getUserNativeNetworkAsset(chainId: ChainId) { + const network = getNetworkFromChainId(chainId); + const { nativeCurrency } = getNetworkObj(network); + const { mainnetAddress, address } = nativeCurrency; + const uniqueId = mainnetAddress ? getUniqueId(mainnetAddress, Network.mainnet) : getUniqueId(address, network); + return userAssetsStore.getState().getUserAsset(uniqueId); +} diff --git a/src/utils/isSameAddress.ts b/src/utils/isSameAddress.ts new file mode 100644 index 00000000000..05194ccc494 --- /dev/null +++ b/src/utils/isSameAddress.ts @@ -0,0 +1,10 @@ +import { ETH_ADDRESS } from '@rainbow-me/swaps'; +import { zeroAddress } from 'viem'; + +const eth = ETH_ADDRESS.toLowerCase(); +export const isEth = (address: string) => [eth, zeroAddress, 'eth'].includes(address.toLowerCase()); + +export const isSameAddress = (a: string, b: string) => { + if (isEth(a) && isEth(b)) return true; + return a.toLowerCase() === b.toLowerCase(); +}; From 9d816a86e9db918b4759f9cdc2fc865722f6ec57 Mon Sep 17 00:00:00 2001 From: gregs Date: Thu, 30 May 2024 13:21:45 -0300 Subject: [PATCH 11/25] remove todo --- src/__swaps__/screens/Swap/providers/swap-provider.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index ad84b978ed2..4c0a3e9660f 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -435,7 +435,6 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { if (_quote) { if ('error' in _quote) return { label: 'Error', disabled: true }; - // TODO: after the tx is built, we should check if the user has enough balance to cover the tx const estimatedGasLimit = getSwapEstimatedGasLimitCachedData({ chainId: _quote.chainId, quote: _quote, From a17464eac054e41771cff2ac0ef70e79758aea20 Mon Sep 17 00:00:00 2001 From: gregs Date: Sun, 2 Jun 2024 06:20:02 +0200 Subject: [PATCH 12/25] move cache getter closer to fetcher implentation --- .../screens/Swap/providers/swap-provider.tsx | 3 +- .../Swap/resources/assets/userAssets.ts | 55 +++++++++++++++++-- src/state/assets/userAssets.ts | 12 ---- 3 files changed, 51 insertions(+), 19 deletions(-) diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index 4c0a3e9660f..5003d59e7f9 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -38,7 +38,7 @@ import { walletExecuteRap } from '@/raps/execute'; import { QuoteTypeMap, RapSwapActionParameters } from '@/raps/references'; import { queryClient } from '@/react-query'; import { userAssetsQueryKey } from '@/resources/assets/UserAssetsQuery'; -import { getUserNativeNetworkAsset } from '@/state/assets/userAssets'; + import { swapsStore } from '@/state/swaps/swapsStore'; import { ethereumUtils } from '@/utils'; import { isEth } from '@/utils/isSameAddress'; @@ -46,6 +46,7 @@ import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; import { calculateGasFee } from '../hooks/useEstimatedGasFee'; import { getGasSettingsBySpeed, getSelectedGas } from '../hooks/useSelectedGas'; import { getSwapEstimatedGasLimitCachedData } from '../hooks/useSwapEstimatedGasLimit'; +import { getUserNativeNetworkAsset } from '../resources/assets/userAssets'; const swapping = i18n.t(i18n.l.swap.actions.swapping); const tapToSwap = i18n.t(i18n.l.swap.actions.tap_to_swap); diff --git a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts index 7e409673216..79f126649fe 100644 --- a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts +++ b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts @@ -1,20 +1,24 @@ import { useQuery } from '@tanstack/react-query'; -import { Address } from 'viem'; import { ADDYS_API_KEY } from 'react-native-dotenv'; +import { Address } from 'viem'; -import { QueryConfigWithSelect, QueryFunctionArgs, QueryFunctionResult, createQueryKey, queryClient } from '@/react-query'; -import { SupportedCurrencyKey, SUPPORTED_CHAIN_IDS } from '@/references'; import { ParsedAssetsDictByChain, ZerionAsset } from '@/__swaps__/types/assets'; import { ChainId } from '@/__swaps__/types/chains'; import { AddressAssetsReceivedMessage } from '@/__swaps__/types/refraction'; import { filterAsset, parseUserAsset } from '@/__swaps__/utils/assets'; import { greaterThan } from '@/__swaps__/utils/numbers'; import { RainbowError, logger } from '@/logger'; +import { QueryConfigWithSelect, QueryFunctionArgs, QueryFunctionResult, createQueryKey, queryClient } from '@/react-query'; +import { SUPPORTED_CHAIN_IDS, SupportedCurrencyKey } from '@/references'; -import { fetchUserAssetsByChain } from './userAssetsByChain'; -import { RainbowFetchClient } from '@/rainbow-fetch'; -import { useAccountSettings } from '@/hooks'; import { getCachedProviderForNetwork, isHardHat } from '@/handlers/web3'; +import { Network } from '@/helpers'; +import { useAccountSettings } from '@/hooks'; +import { getNetworkObj } from '@/networks'; +import { RainbowFetchClient } from '@/rainbow-fetch'; +import store from '@/redux/store'; +import { getNetworkFromChainId, getUniqueId } from '@/utils/ethereumUtils'; +import { fetchUserAssetsByChain } from './userAssetsByChain'; const addysHttp = new RainbowFetchClient({ baseURL: 'https://addys.p.rainbow.me/v3', @@ -219,3 +223,42 @@ export function useUserAssets( staleTime: process.env.IS_TESTING === 'true' ? 0 : 1000, }); } + +function getCachedUserAssets({ + address, + currency, + testnetMode = false, +}: { + address: Address; + currency: SupportedCurrencyKey; + testnetMode?: boolean; +}) { + return queryClient.getQueryData(userAssetsQueryKey({ address, currency, testnetMode })); +} + +const getNetworkNativeAssetUniqueId = (chainId: ChainId) => { + const network = getNetworkFromChainId(chainId); + const { nativeCurrency } = getNetworkObj(network); + const { mainnetAddress, address } = nativeCurrency; + const uniqueId = mainnetAddress ? getUniqueId(mainnetAddress, Network.mainnet) : getUniqueId(address, network); + return uniqueId; +}; + +export function getUserNativeNetworkAsset(chainId: ChainId) { + const { accountAddress: currentAddress, nativeCurrency: currentCurrency, network: currentNetwork } = store.getState().settings; + + const provider = getCachedProviderForNetwork(currentNetwork); + const providerUrl = provider?.connection?.url; + const connectedToHardhat = isHardHat(providerUrl); + + const userAssets = getCachedUserAssets({ + address: currentAddress as Address, + currency: currentCurrency, + testnetMode: connectedToHardhat, + }); + + if (!userAssets) return; + + const uniqueId = getNetworkNativeAssetUniqueId(chainId); + return userAssets[chainId][uniqueId]; +} diff --git a/src/state/assets/userAssets.ts b/src/state/assets/userAssets.ts index 52c3d1a88a3..ef5627a0372 100644 --- a/src/state/assets/userAssets.ts +++ b/src/state/assets/userAssets.ts @@ -1,13 +1,9 @@ import { Hex } from 'viem'; import { ParsedSearchAsset, UniqueId, UserAssetFilter } from '@/__swaps__/types/assets'; -import { ChainId } from '@/__swaps__/types/chains'; import { deriveAddressAndChainWithUniqueId } from '@/__swaps__/utils/address'; -import { Network } from '@/helpers'; import { RainbowError, logger } from '@/logger'; -import { getNetworkObj } from '@/networks'; import { createRainbowStore } from '@/state/internal/createRainbowStore'; -import { getNetworkFromChainId, getUniqueId } from '@/utils/ethereumUtils'; export interface UserAssetsState { userAssetsById: Set; @@ -171,11 +167,3 @@ export const userAssetsStore = createRainbowStore( deserializer: deserializeUserAssetsState, } ); - -export function getUserNativeNetworkAsset(chainId: ChainId) { - const network = getNetworkFromChainId(chainId); - const { nativeCurrency } = getNetworkObj(network); - const { mainnetAddress, address } = nativeCurrency; - const uniqueId = mainnetAddress ? getUniqueId(mainnetAddress, Network.mainnet) : getUniqueId(address, network); - return userAssetsStore.getState().getUserAsset(uniqueId); -} From 0af8ff4486f59f1568fbc503d4d36951073d30d6 Mon Sep 17 00:00:00 2001 From: gregs Date: Wed, 5 Jun 2024 15:33:38 +0200 Subject: [PATCH 13/25] fix --- .../screens/Swap/hooks/useEstimatedGasFee.ts | 57 +++++++++++++++++-- .../screens/Swap/providers/swap-provider.tsx | 51 +++++++---------- 2 files changed, 73 insertions(+), 35 deletions(-) diff --git a/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts b/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts index e3f471b5ed2..f7c8a14b9d6 100644 --- a/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts +++ b/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts @@ -1,13 +1,16 @@ import { greaterThanWorklet, toScaledIntegerWorklet } from '@/__swaps__/safe-math/SafeMath'; import { ChainId } from '@/__swaps__/types/chains'; import { weiToGwei } from '@/__swaps__/utils/ethereum'; -import { add, multiply } from '@/__swaps__/utils/numbers'; +import { add, lessThan, multiply } from '@/__swaps__/utils/numbers'; import ethereumUtils, { useNativeAssetForNetwork } from '@/utils/ethereumUtils'; -import { useMemo, useState } from 'react'; +import { isEth } from '@/utils/isSameAddress'; +import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; +import { useEffect, useMemo, useState } from 'react'; import { runOnJS, useAnimatedReaction } from 'react-native-reanimated'; import { useDebouncedCallback } from 'use-debounce'; import { formatUnits } from 'viem'; import { useSwapContext } from '../providers/swap-provider'; +import { getUserNativeNetworkAsset } from '../resources/assets/userAssets'; import { formatCurrency, formatNumber } from './formatNumber'; import { GasSettings } from './useCustomGas'; import { useSwapEstimatedGasLimit } from './useSwapEstimatedGasLimit'; @@ -52,6 +55,46 @@ export function useEstimatedGasFee({ }, [gasLimit, gasSettings, nativeNetworkAsset]); } +function useSyncSwapTransactionSharedValues({ + quote, + gasSettings, + estimatedGasLimit, +}: { + quote: Quote | CrosschainQuote | QuoteError | null; + gasSettings: GasSettings | undefined; + estimatedGasLimit: string | undefined; +}) { + const { + estimatedGasLimit: estimatedGasLimitSharedValue, + gasSettings: gasSettingsSharedValue, + userHasEnoughFundsForTx, + } = useSwapContext(); + + useEffect(() => { + if (!gasSettings || !estimatedGasLimit || !quote || 'error' in quote) { + estimatedGasLimitSharedValue.value = undefined; + gasSettingsSharedValue.value = undefined; + return; + } + + estimatedGasLimitSharedValue.value = estimatedGasLimit; + gasSettingsSharedValue.value = gasSettings; + + const gasFee = calculateGasFee(gasSettings, estimatedGasLimit); + + const nativeAmountSelling = isEth(quote.sellTokenAddress) ? quote.sellAmount.toString() : '0'; + const totalNativeSpentInTx = add(add(quote.value?.toString() || '0', gasFee), nativeAmountSelling); + + const userBalance = getUserNativeNetworkAsset(quote.chainId)?.balance.amount || '0'; + userHasEnoughFundsForTx.value = lessThan(userBalance, totalNativeSpentInTx); + + return () => { + estimatedGasLimitSharedValue.value = undefined; + gasSettingsSharedValue.value = undefined; + }; + }, [estimatedGasLimitSharedValue, estimatedGasLimit, gasSettings, gasSettingsSharedValue, userHasEnoughFundsForTx, quote]); +} + export function useSwapEstimatedGasFee(gasSettings: GasSettings | undefined) { const { internalSelectedInputAsset: assetToSell, internalSelectedOutputAsset: assetToBuy, quote } = useSwapContext(); @@ -68,7 +111,7 @@ export function useSwapEstimatedGasFee(gasSettings: GasSettings | undefined) { useAnimatedReaction( () => quote.value, (current, previous) => { - if (!assetToSell.value || !assetToBuy.value || !current || !previous || 'error' in current) return; + if (!assetToSell.value || !assetToBuy.value || !current || 'error' in current) return; const isSwappingMoreThanAvailableBalance = greaterThanWorklet( current.sellAmount.toString(), @@ -79,7 +122,7 @@ export function useSwapEstimatedGasFee(gasSettings: GasSettings | undefined) { // needed and was previously resulting in errors in useEstimatedGasFee. if (isSwappingMoreThanAvailableBalance) return; - if (current !== previous) { + if (!previous || current !== previous) { runOnJS(debouncedStateSet)({ assetToBuy: assetToBuy.value, assetToSell: assetToSell.value, @@ -90,14 +133,16 @@ export function useSwapEstimatedGasFee(gasSettings: GasSettings | undefined) { } ); - const { data: gasLimit, isFetching } = useSwapEstimatedGasLimit( + const { data: estimatedGasLimit, isFetching } = useSwapEstimatedGasLimit( { chainId: state.chainId, quote: state.quote, assetToSell: state.assetToSell }, { enabled: !!state.quote && !!state.assetToSell && !!state.assetToBuy && !('error' in quote), } ); - const estimatedFee = useEstimatedGasFee({ chainId: state.chainId, gasLimit, gasSettings }); + useSyncSwapTransactionSharedValues({ quote: state.quote, gasSettings, estimatedGasLimit }); + + const estimatedFee = useEstimatedGasFee({ chainId: state.chainId, gasLimit: estimatedGasLimit, gasSettings }); return useMemo(() => ({ isLoading: isFetching, data: estimatedFee }), [estimatedFee, isFetching]); } diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index e63b2b759f9..63185721091 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -23,7 +23,6 @@ import { useSwapWarning } from '@/__swaps__/screens/Swap/hooks/useSwapWarning'; import { ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '@/__swaps__/types/assets'; import { ChainId } from '@/__swaps__/types/chains'; import { SwapAssetType, inputKeys } from '@/__swaps__/types/swap'; -import { add, lessThan } from '@/__swaps__/utils/numbers'; import { parseAssetAndExtend } from '@/__swaps__/utils/swaps'; import { LegacyTransactionGasParamAmounts, TransactionGasParamAmounts } from '@/entities'; import { getCachedProviderForNetwork, getFlashbotsProvider, isHardHat } from '@/handlers/web3'; @@ -45,12 +44,9 @@ import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; import { equalWorklet } from '@/__swaps__/safe-math/SafeMath'; import { getNetworkObj } from '@/networks'; import { userAssetsStore } from '@/state/assets/userAssets'; -import { isEth } from '@/utils/isSameAddress'; -import { calculateGasFee } from '../hooks/useEstimatedGasFee'; +import { GasSettings } from '../hooks/useCustomGas'; import { getGasSettingsBySpeed, getSelectedGas } from '../hooks/useSelectedGas'; -import { getSwapEstimatedGasLimitCachedData } from '../hooks/useSwapEstimatedGasLimit'; import { useSwapOutputQuotesDisabled } from '../hooks/useSwapOutputQuotesDisabled'; -import { getUserNativeNetworkAsset } from '../resources/assets/userAssets'; const swapping = i18n.t(i18n.l.swap.actions.swapping); const tapToSwap = i18n.t(i18n.l.swap.actions.tap_to_swap); @@ -105,6 +101,10 @@ interface SwapContextType { }> >; confirmButtonIconStyle: StyleProp; + + estimatedGasLimit: SharedValue; + gasSettings: SharedValue; + userHasEnoughFundsForTx: SharedValue; } const SwapContext = createContext(undefined); @@ -501,6 +501,10 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { }; }, []); + const gasSettings = useSharedValue(undefined); + const estimatedGasLimit = useSharedValue(undefined); + const userHasEnoughFundsForTx = useSharedValue(true); + const confirmButtonProps = useDerivedValue(() => { if (isSwapping.value) { return { label: 'Swapping...', disabled: true }; @@ -521,6 +525,8 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { const hasSelectedAssets = internalSelectedInputAsset.value && internalSelectedOutputAsset.value; if (!hasSelectedAssets) return { label: 'Select Token', disabled: true }; + if (!quote.value || 'error' in quote.value) return { label: 'Error', disabled: true }; + const isInputZero = equalWorklet(SwapInputController.inputValues.value.inputAmount, 0); const isOutputZero = equalWorklet(SwapInputController.inputValues.value.outputAmount, 0); @@ -528,32 +534,15 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { return { label: 'Enter Amount', disabled: true }; } - const _quote = quote.value; - if (_quote) { - if ('error' in _quote) return { label: 'Error', disabled: true }; - - const estimatedGasLimit = getSwapEstimatedGasLimitCachedData({ - chainId: _quote.chainId, - quote: _quote, - assetToSell: internalSelectedInputAsset.value, - }); - if (!estimatedGasLimit) return { label: 'Estimating...', disabled: true }; - - const gasSettings = getSelectedGas(_quote.chainId); - if (!gasSettings) { - // this could happen if metereology is down, or some other edge cases that are not properly handled yet - return { label: 'Error', disabled: true }; - } - - const gasFee = calculateGasFee(gasSettings, estimatedGasLimit); + if (!estimatedGasLimit.value) return { label: 'Estimating...', disabled: true }; - const nativeAmountSelling = isEth(_quote.sellTokenAddress) ? _quote.sellAmount.toString() : '0'; - const totalNativeSpentInTx = add(add(_quote.value?.toString() || '0', gasFee), nativeAmountSelling); + if (!gasSettings.value) { + // this could happen if metereology is down, or some other edge cases that are not properly handled yet + return { label: 'Error', disabled: true }; + } - const userBalance = getUserNativeNetworkAsset(_quote.chainId)?.balance.amount || '0'; - if (lessThan(userBalance, totalNativeSpentInTx)) { - return { label: 'Insufficient Funds', disabled: true }; - } + if (!userHasEnoughFundsForTx.value) { + return { label: 'Insufficient Funds', disabled: true }; } return { icon: '􀕹', label: 'Review', disabled: false }; @@ -615,6 +604,10 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { confirmButtonProps, confirmButtonIconStyle, + + estimatedGasLimit, + gasSettings, + userHasEnoughFundsForTx, }} > {children} From 40c2055faa5708c451e7c75a4195282e8db0458a Mon Sep 17 00:00:00 2001 From: gregs Date: Wed, 5 Jun 2024 16:51:12 +0200 Subject: [PATCH 14/25] :) --- .../screens/Swap/hooks/useEstimatedGasFee.ts | 22 ++++++++++++++----- .../Swap/resources/assets/userAssets.ts | 5 ++--- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts b/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts index f7c8a14b9d6..e00ded6d76b 100644 --- a/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts +++ b/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts @@ -3,7 +3,7 @@ import { ChainId } from '@/__swaps__/types/chains'; import { weiToGwei } from '@/__swaps__/utils/ethereum'; import { add, lessThan, multiply } from '@/__swaps__/utils/numbers'; import ethereumUtils, { useNativeAssetForNetwork } from '@/utils/ethereumUtils'; -import { isEth } from '@/utils/isSameAddress'; +import { isSameAddress } from '@/utils/isSameAddress'; import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; import { useEffect, useMemo, useState } from 'react'; import { runOnJS, useAnimatedReaction } from 'react-native-reanimated'; @@ -55,6 +55,20 @@ export function useEstimatedGasFee({ }, [gasLimit, gasSettings, nativeNetworkAsset]); } +const getUserHasEnoughFundsForTx = (quote: Quote, gasFee: string) => { + const nativeAsset = getUserNativeNetworkAsset(quote.chainId); + if (!nativeAsset) return false; + const userBalance = nativeAsset.balance.amount || '0'; + + const nativeAmountSelling = isSameAddress(nativeAsset.address, quote.sellTokenAddress) ? quote.sellAmount.toString() : '0'; + const totalNativeSpentInTx = formatUnits( + safeBigInt(add(add(quote.value?.toString() || '0', gasFee), nativeAmountSelling)), + nativeAsset.decimals + ); + + return lessThan(totalNativeSpentInTx, userBalance); +}; + function useSyncSwapTransactionSharedValues({ quote, gasSettings, @@ -82,11 +96,7 @@ function useSyncSwapTransactionSharedValues({ const gasFee = calculateGasFee(gasSettings, estimatedGasLimit); - const nativeAmountSelling = isEth(quote.sellTokenAddress) ? quote.sellAmount.toString() : '0'; - const totalNativeSpentInTx = add(add(quote.value?.toString() || '0', gasFee), nativeAmountSelling); - - const userBalance = getUserNativeNetworkAsset(quote.chainId)?.balance.amount || '0'; - userHasEnoughFundsForTx.value = lessThan(userBalance, totalNativeSpentInTx); + userHasEnoughFundsForTx.value = getUserHasEnoughFundsForTx(quote, gasFee); return () => { estimatedGasLimitSharedValue.value = undefined; diff --git a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts index f5ae264307b..6ec112b24f8 100644 --- a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts +++ b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts @@ -12,12 +12,11 @@ import { QueryConfigWithSelect, QueryFunctionArgs, QueryFunctionResult, createQu import { SUPPORTED_CHAIN_IDS, SupportedCurrencyKey } from '@/references'; import { getCachedProviderForNetwork, isHardHat } from '@/handlers/web3'; -import { Network } from '@/helpers'; import { useAccountSettings } from '@/hooks'; import { getNetworkObj } from '@/networks'; import { RainbowFetchClient } from '@/rainbow-fetch'; import store from '@/redux/store'; -import { getNetworkFromChainId, getUniqueId } from '@/utils/ethereumUtils'; +import { getNetworkFromChainId } from '@/utils/ethereumUtils'; import { fetchUserAssetsByChain } from './userAssetsByChain'; const addysHttp = new RainbowFetchClient({ @@ -243,7 +242,7 @@ const getNetworkNativeAssetUniqueId = (chainId: ChainId) => { const network = getNetworkFromChainId(chainId); const { nativeCurrency } = getNetworkObj(network); const { mainnetAddress, address } = nativeCurrency; - const uniqueId = mainnetAddress ? getUniqueId(mainnetAddress, Network.mainnet) : getUniqueId(address, network); + const uniqueId = mainnetAddress ? `${mainnetAddress}_${ChainId.mainnet}` : `${address}_${chainId}`; return uniqueId; }; From c7c010222d333d10d54044725484bdad41ea72cb Mon Sep 17 00:00:00 2001 From: gregs Date: Wed, 12 Jun 2024 11:38:19 -0300 Subject: [PATCH 15/25] =?UTF-8?q?=F0=9F=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Swap/components/SwapActionButton.tsx | 6 +- .../Swap/components/SwapBottomPanel.tsx | 2 + .../screens/Swap/hooks/useCanPerformSwap.ts | 32 +++++ .../screens/Swap/hooks/useEstimatedGasFee.ts | 112 +--------------- .../Swap/hooks/useSwapEstimatedGasLimit.ts | 4 - .../SyncSwapStateAndSharedValues.tsx | 121 ++++++++++++++++++ .../screens/Swap/providers/swap-provider.tsx | 59 ++++++--- .../Swap/resources/assets/userAssets.ts | 42 ------ src/languages/en_US.json | 5 +- src/resources/assets/assetSelectors.ts | 6 +- src/resources/assets/useUserAsset.ts | 17 +++ 11 files changed, 230 insertions(+), 176 deletions(-) create mode 100644 src/__swaps__/screens/Swap/hooks/useCanPerformSwap.ts create mode 100644 src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx diff --git a/src/__swaps__/screens/Swap/components/SwapActionButton.tsx b/src/__swaps__/screens/Swap/components/SwapActionButton.tsx index 5e5bc89e7cd..d542da1c005 100644 --- a/src/__swaps__/screens/Swap/components/SwapActionButton.tsx +++ b/src/__swaps__/screens/Swap/components/SwapActionButton.tsx @@ -26,6 +26,7 @@ export const SwapActionButton = ({ small, style, disabled, + isLoading, }: { asset: DerivedValue; borderRadius?: number; @@ -42,6 +43,7 @@ export const SwapActionButton = ({ small?: boolean; style?: ViewStyle; disabled?: DerivedValue; + isLoading?: DerivedValue; }) => { const { isDarkMode } = useColorMode(); const fallbackColor = useForegroundColor('label'); @@ -84,7 +86,8 @@ export const SwapActionButton = ({ }, shadowOpacity: isDarkMode ? 0.2 : small ? 0.2 : 0.36, shadowRadius: isDarkMode ? 26 : small ? 9 : 15, - opacity: disabled?.value ? 0.6 : 1, + // we don't want to change the opacity when it's loading + opacity: !isLoading?.value && disabled?.value ? 0.6 : 1, }; }); @@ -115,7 +118,6 @@ export const SwapActionButton = ({ onPressStartWorklet={onPressWorklet} onPressJS={onPressJS} style={[hugContent && feedActionButtonStyles.buttonWrapper, style]} - scaleTo={scaleTo || (hugContent ? undefined : 0.925)} > confirmButtonProps.value.icon); const label = useDerivedValue(() => confirmButtonProps.value.label); const disabled = useDerivedValue(() => confirmButtonProps.value.disabled); + const isLoading = useDerivedValue(() => confirmButtonProps.value.isLoading); return ( // @ts-expect-error Property 'children' does not exist on type @@ -83,6 +84,7 @@ export function SwapBottomPanel() { iconStyle={confirmButtonIconStyle} label={label} disabled={disabled} + isLoading={isLoading} scaleTo={0.9} /> diff --git a/src/__swaps__/screens/Swap/hooks/useCanPerformSwap.ts b/src/__swaps__/screens/Swap/hooks/useCanPerformSwap.ts new file mode 100644 index 00000000000..6e7f309df4a --- /dev/null +++ b/src/__swaps__/screens/Swap/hooks/useCanPerformSwap.ts @@ -0,0 +1,32 @@ +import { lessThanWorklet, toScaledIntegerWorklet } from '@/__swaps__/safe-math/SafeMath'; +import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets'; +import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; +import { SharedValue, useDerivedValue, useSharedValue } from 'react-native-reanimated'; +import { GasSettings } from './useCustomGas'; + +export function useCanPerformSwap({ + quote, + inputAsset, +}: { + inputAsset: SharedValue; + quote: SharedValue; +}) { + const gasSettings = useSharedValue(undefined); + const estimatedGasLimit = useSharedValue(undefined); + const enoughFundsForGas = useSharedValue(true); + + const enoughFundsForSwap = useDerivedValue(() => { + if (!quote.value || 'error' in quote.value || !inputAsset.value) return true; + return lessThanWorklet( + quote.value.sellAmount.toString(), + toScaledIntegerWorklet(inputAsset.value.balance.amount, inputAsset.value.decimals) + ); + }); + + return { + gasSettings, + estimatedGasLimit, + enoughFundsForGas, + enoughFundsForSwap, + }; +} diff --git a/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts b/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts index e00ded6d76b..da50e54f5cb 100644 --- a/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts +++ b/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts @@ -1,16 +1,11 @@ -import { greaterThanWorklet, toScaledIntegerWorklet } from '@/__swaps__/safe-math/SafeMath'; import { ChainId } from '@/__swaps__/types/chains'; import { weiToGwei } from '@/__swaps__/utils/ethereum'; -import { add, lessThan, multiply } from '@/__swaps__/utils/numbers'; +import { add, multiply } from '@/__swaps__/utils/numbers'; import ethereumUtils, { useNativeAssetForNetwork } from '@/utils/ethereumUtils'; -import { isSameAddress } from '@/utils/isSameAddress'; -import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; -import { useEffect, useMemo, useState } from 'react'; -import { runOnJS, useAnimatedReaction } from 'react-native-reanimated'; -import { useDebouncedCallback } from 'use-debounce'; +import { useMemo } from 'react'; import { formatUnits } from 'viem'; -import { useSwapContext } from '../providers/swap-provider'; -import { getUserNativeNetworkAsset } from '../resources/assets/userAssets'; + +import { useSyncedSwapQuoteStore } from '../providers/SyncSwapStateAndSharedValues'; import { formatCurrency, formatNumber } from './formatNumber'; import { GasSettings } from './useCustomGas'; import { useSwapEstimatedGasLimit } from './useSwapEstimatedGasLimit'; @@ -55,104 +50,11 @@ export function useEstimatedGasFee({ }, [gasLimit, gasSettings, nativeNetworkAsset]); } -const getUserHasEnoughFundsForTx = (quote: Quote, gasFee: string) => { - const nativeAsset = getUserNativeNetworkAsset(quote.chainId); - if (!nativeAsset) return false; - const userBalance = nativeAsset.balance.amount || '0'; - - const nativeAmountSelling = isSameAddress(nativeAsset.address, quote.sellTokenAddress) ? quote.sellAmount.toString() : '0'; - const totalNativeSpentInTx = formatUnits( - safeBigInt(add(add(quote.value?.toString() || '0', gasFee), nativeAmountSelling)), - nativeAsset.decimals - ); - - return lessThan(totalNativeSpentInTx, userBalance); -}; - -function useSyncSwapTransactionSharedValues({ - quote, - gasSettings, - estimatedGasLimit, -}: { - quote: Quote | CrosschainQuote | QuoteError | null; - gasSettings: GasSettings | undefined; - estimatedGasLimit: string | undefined; -}) { - const { - estimatedGasLimit: estimatedGasLimitSharedValue, - gasSettings: gasSettingsSharedValue, - userHasEnoughFundsForTx, - } = useSwapContext(); - - useEffect(() => { - if (!gasSettings || !estimatedGasLimit || !quote || 'error' in quote) { - estimatedGasLimitSharedValue.value = undefined; - gasSettingsSharedValue.value = undefined; - return; - } - - estimatedGasLimitSharedValue.value = estimatedGasLimit; - gasSettingsSharedValue.value = gasSettings; - - const gasFee = calculateGasFee(gasSettings, estimatedGasLimit); - - userHasEnoughFundsForTx.value = getUserHasEnoughFundsForTx(quote, gasFee); - - return () => { - estimatedGasLimitSharedValue.value = undefined; - gasSettingsSharedValue.value = undefined; - }; - }, [estimatedGasLimitSharedValue, estimatedGasLimit, gasSettings, gasSettingsSharedValue, userHasEnoughFundsForTx, quote]); -} - export function useSwapEstimatedGasFee(gasSettings: GasSettings | undefined) { - const { internalSelectedInputAsset: assetToSell, internalSelectedOutputAsset: assetToBuy, quote } = useSwapContext(); - - const [state, setState] = useState({ - assetToBuy: assetToBuy.value, - assetToSell: assetToSell.value, - chainId: assetToSell.value?.chainId ?? ChainId.mainnet, - quote: quote.value, - }); - - const debouncedStateSet = useDebouncedCallback(setState, 100, { leading: false, trailing: true }); - - // Updates the state as a single block in response to quote changes to ensure the gas fee is cleanly updated once - useAnimatedReaction( - () => quote.value, - (current, previous) => { - if (!assetToSell.value || !assetToBuy.value || !current || 'error' in current) return; - - const isSwappingMoreThanAvailableBalance = greaterThanWorklet( - current.sellAmount.toString(), - toScaledIntegerWorklet(assetToSell.value.balance.amount, assetToSell.value.decimals) - ); - - // Skip gas fee recalculation if the user is trying to swap more than their available balance, as it isn't - // needed and was previously resulting in errors in useEstimatedGasFee. - if (isSwappingMoreThanAvailableBalance) return; - - if (!previous || current !== previous) { - runOnJS(debouncedStateSet)({ - assetToBuy: assetToBuy.value, - assetToSell: assetToSell.value, - chainId: assetToSell.value?.chainId ?? ChainId.mainnet, - quote: current, - }); - } - } - ); - - const { data: estimatedGasLimit, isFetching } = useSwapEstimatedGasLimit( - { chainId: state.chainId, quote: state.quote, assetToSell: state.assetToSell }, - { - enabled: !!state.quote && !!state.assetToSell && !!state.assetToBuy && !('error' in quote), - } - ); - - useSyncSwapTransactionSharedValues({ quote: state.quote, gasSettings, estimatedGasLimit }); + const { assetToSell, chainId = ChainId.mainnet, quote } = useSyncedSwapQuoteStore(); + const { data: estimatedGasLimit, isFetching } = useSwapEstimatedGasLimit({ chainId, assetToSell, quote }); - const estimatedFee = useEstimatedGasFee({ chainId: state.chainId, gasLimit: estimatedGasLimit, gasSettings }); + const estimatedFee = useEstimatedGasFee({ chainId, gasLimit: estimatedGasLimit, gasSettings }); return useMemo(() => ({ isLoading: isFetching, data: estimatedFee }), [estimatedFee, isFetching]); } diff --git a/src/__swaps__/screens/Swap/hooks/useSwapEstimatedGasLimit.ts b/src/__swaps__/screens/Swap/hooks/useSwapEstimatedGasLimit.ts index 3ec604d1e7c..c30913f3da9 100644 --- a/src/__swaps__/screens/Swap/hooks/useSwapEstimatedGasLimit.ts +++ b/src/__swaps__/screens/Swap/hooks/useSwapEstimatedGasLimit.ts @@ -103,7 +103,3 @@ export function useSwapEstimatedGasLimit( } ); } - -export const getSwapEstimatedGasLimitCachedData = (args: EstimateSwapGasLimitArgs) => { - return queryClient.getQueryData(estimateSwapGasLimitQueryKey(args)); -}; diff --git a/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx b/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx new file mode 100644 index 00000000000..2a2f3224346 --- /dev/null +++ b/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx @@ -0,0 +1,121 @@ +import { greaterThanWorklet, toScaledIntegerWorklet } from '@/__swaps__/safe-math/SafeMath'; +import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets'; +import { ChainId } from '@/__swaps__/types/chains'; +import { add } from '@/__swaps__/utils/numbers'; +import { ParsedAddressAsset } from '@/entities'; +import { lessThan } from '@/helpers/utilities'; +import { useUserNativeNetworkAsset } from '@/resources/assets/useUserAsset'; +import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; +import { debounce } from 'lodash'; +import { useEffect } from 'react'; +import { runOnJS, useAnimatedReaction } from 'react-native-reanimated'; +import { formatUnits } from 'viem'; +import { create } from 'zustand'; +import { calculateGasFee } from '../hooks/useEstimatedGasFee'; +import { useSelectedGas } from '../hooks/useSelectedGas'; +import { useSwapEstimatedGasLimit } from '../hooks/useSwapEstimatedGasLimit'; +import { useSwapContext } from './swap-provider'; + +const getHasEnoughFundsForGas = (quote: Quote, gasFee: string, nativeNetworkAsset: ParsedAddressAsset | undefined) => { + if (!nativeNetworkAsset) return false; + const userBalance = nativeNetworkAsset.balance?.amount || '0'; + + const quoteValue = quote.value?.toString() || '0'; + const totalNativeSpentInTx = formatUnits(BigInt(add(quoteValue, gasFee)), nativeNetworkAsset.decimals); + + return lessThan(totalNativeSpentInTx, userBalance); +}; + +type InternalSyncedSwapState = { + assetToBuy: ExtendedAnimatedAssetWithColors | undefined; + assetToSell: ExtendedAnimatedAssetWithColors | undefined; + chainId: ChainId | undefined; + quote: Quote | CrosschainQuote | QuoteError | null; +}; +export const useSyncedSwapQuoteStore = create(() => ({ + assetToBuy: undefined, + assetToSell: undefined, + chainId: undefined, + quote: null, +})); +const setInternalSyncedSwapStore = debounce((state: InternalSyncedSwapState) => useSyncedSwapQuoteStore.setState(state), 100, { + leading: false, + trailing: true, +}); + +export const SyncQuoteSharedValuesToState = () => { + const { internalSelectedInputAsset: assetToSell, internalSelectedOutputAsset: assetToBuy, quote } = useSwapContext(); + + // Updates the state as a single block in response to quote changes to ensure the gas fee is cleanly updated once + useAnimatedReaction( + () => quote.value, + (current, previous) => { + if (!assetToSell.value || !assetToBuy.value || !current || 'error' in current) return; + + const isSwappingMoreThanAvailableBalance = greaterThanWorklet( + current.sellAmount.toString(), + toScaledIntegerWorklet(assetToSell.value.balance.amount, assetToSell.value.decimals) + ); + + // Skip gas fee recalculation if the user is trying to swap more than their available balance, as it isn't + // needed and was previously resulting in errors in useEstimatedGasFee. + if (isSwappingMoreThanAvailableBalance) return; + + if (!previous || current !== previous) { + runOnJS(setInternalSyncedSwapStore)({ + assetToBuy: assetToBuy.value, + assetToSell: assetToSell.value, + chainId: assetToSell.value?.chainId, + quote: current, + }); + } + } + ); + + return null; +}; + +export function SyncGasStateToSharedValues() { + const { + estimatedGasLimit: estimatedGasLimitSharedValue, + gasSettings: gasSettingsSharedValue, + enoughFundsForGas, + internalSelectedInputAsset, + SwapInputController, + } = useSwapContext(); + + const { assetToSell, chainId = ChainId.mainnet, quote } = useSyncedSwapQuoteStore(); + + const gasSettings = useSelectedGas(chainId); + const { data: userNativeNetworkAsset } = useUserNativeNetworkAsset(chainId); + const { data: estimatedGasLimit } = useSwapEstimatedGasLimit({ chainId, assetToSell, quote }); + + useEffect(() => { + estimatedGasLimitSharedValue.value = estimatedGasLimit; + gasSettingsSharedValue.value = gasSettings; + + if (!gasSettings || !estimatedGasLimit || !quote || 'error' in quote) { + enoughFundsForGas.value = true; + return; + } + + const gasFee = calculateGasFee(gasSettings, estimatedGasLimit); + enoughFundsForGas.value = getHasEnoughFundsForGas(quote, gasFee, userNativeNetworkAsset); + + return () => { + enoughFundsForGas.value = true; + }; + }, [ + estimatedGasLimitSharedValue, + estimatedGasLimit, + gasSettings, + gasSettingsSharedValue, + enoughFundsForGas, + quote, + internalSelectedInputAsset.value?.balance.amount, + SwapInputController.inputValues.value.inputAmount, + userNativeNetworkAsset, + ]); + + return null; +} diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index 63185721091..d4b781f5be9 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -19,7 +19,7 @@ import { useSwapInputsController } from '@/__swaps__/screens/Swap/hooks/useSwapI import { NavigationSteps, useSwapNavigation } from '@/__swaps__/screens/Swap/hooks/useSwapNavigation'; import { useSwapSettings } from '@/__swaps__/screens/Swap/hooks/useSwapSettings'; import { useSwapTextStyles } from '@/__swaps__/screens/Swap/hooks/useSwapTextStyles'; -import { useSwapWarning } from '@/__swaps__/screens/Swap/hooks/useSwapWarning'; +import { SwapWarningType, useSwapWarning } from '@/__swaps__/screens/Swap/hooks/useSwapWarning'; import { ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '@/__swaps__/types/assets'; import { ChainId } from '@/__swaps__/types/chains'; import { SwapAssetType, inputKeys } from '@/__swaps__/types/swap'; @@ -44,9 +44,11 @@ import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; import { equalWorklet } from '@/__swaps__/safe-math/SafeMath'; import { getNetworkObj } from '@/networks'; import { userAssetsStore } from '@/state/assets/userAssets'; +import { useCanPerformSwap } from '../hooks/useCanPerformSwap'; import { GasSettings } from '../hooks/useCustomGas'; import { getGasSettingsBySpeed, getSelectedGas } from '../hooks/useSelectedGas'; import { useSwapOutputQuotesDisabled } from '../hooks/useSwapOutputQuotesDisabled'; +import { SyncGasStateToSharedValues, SyncQuoteSharedValuesToState } from './SyncSwapStateAndSharedValues'; const swapping = i18n.t(i18n.l.swap.actions.swapping); const tapToSwap = i18n.t(i18n.l.swap.actions.tap_to_swap); @@ -54,6 +56,9 @@ const save = i18n.t(i18n.l.swap.actions.save); const enterAmount = i18n.t(i18n.l.swap.actions.enter_amount); const review = i18n.t(i18n.l.swap.actions.review); const fetchingPrices = i18n.t(i18n.l.swap.actions.fetching_prices); +const selectToken = i18n.t(i18n.l.swap.actions.select_token); +const estimating = i18n.t(i18n.l.swap.actions.estimating); +const insufficientFunds = i18n.t(i18n.l.swap.actions.insufficient_funds); interface SwapContextType { isFetching: SharedValue; @@ -98,13 +103,14 @@ interface SwapContextType { label: string; icon?: string; disabled?: boolean; + isLoading?: boolean; }> >; confirmButtonIconStyle: StyleProp; estimatedGasLimit: SharedValue; gasSettings: SharedValue; - userHasEnoughFundsForTx: SharedValue; + enoughFundsForGas: SharedValue; } const SwapContext = createContext(undefined); @@ -501,51 +507,64 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { }; }, []); - const gasSettings = useSharedValue(undefined); - const estimatedGasLimit = useSharedValue(undefined); - const userHasEnoughFundsForTx = useSharedValue(true); + const { gasSettings, estimatedGasLimit, enoughFundsForGas, enoughFundsForSwap } = useCanPerformSwap({ + inputAsset: internalSelectedInputAsset, + quote, + }); const confirmButtonProps = useDerivedValue(() => { if (isSwapping.value) { - return { label: 'Swapping...', disabled: true }; + return { label: swapping, disabled: true }; } if (configProgress.value === NavigationSteps.SHOW_REVIEW) { - return { icon: '􀎽', label: 'Hold to Swap', disabled: false }; + return { icon: '􀎽', label: tapToSwap, disabled: false }; } if (configProgress.value === NavigationSteps.SHOW_GAS) { - return { icon: '􀆅', label: 'Save', disabled: false }; + return { icon: '􀆅', label: save, disabled: false }; } if (isFetching.value) { - return { label: 'Fetching...', disabled: true }; + return { label: fetchingPrices, isLoading: true, disabled: true }; } const hasSelectedAssets = internalSelectedInputAsset.value && internalSelectedOutputAsset.value; - if (!hasSelectedAssets) return { label: 'Select Token', disabled: true }; - - if (!quote.value || 'error' in quote.value) return { label: 'Error', disabled: true }; + if (!hasSelectedAssets) { + return { label: selectToken, disabled: true }; + } const isInputZero = equalWorklet(SwapInputController.inputValues.value.inputAmount, 0); const isOutputZero = equalWorklet(SwapInputController.inputValues.value.outputAmount, 0); - if (SwapInputController.percentageToSwap.value === 0 || isInputZero || isOutputZero) { - return { label: 'Enter Amount', disabled: true }; + const isQuoteError = quote.value && 'error' in quote.value; + + if (!isQuoteError && (SwapInputController.percentageToSwap.value === 0 || isInputZero || isOutputZero)) { + return { label: enterAmount, disabled: true }; } - if (!estimatedGasLimit.value) return { label: 'Estimating...', disabled: true }; + if (!enoughFundsForGas.value || !enoughFundsForSwap.value) { + return { label: insufficientFunds, disabled: true }; + } + + if ( + [SwapWarningType.no_quote_available, SwapWarningType.no_route_found, SwapWarningType.insufficient_liquidity].includes( + SwapWarning.swapWarning.value.type + ) + ) { + return { icon: '􀕹', label: review, disabled: true }; + } if (!gasSettings.value) { // this could happen if metereology is down, or some other edge cases that are not properly handled yet return { label: 'Error', disabled: true }; } - if (!userHasEnoughFundsForTx.value) { - return { label: 'Insufficient Funds', disabled: true }; + if (isQuoteError) { + return { label: 'Error', disabled: true }; } - return { icon: '􀕹', label: 'Review', disabled: false }; + return { icon: '􀕹', label: review, disabled: false }; }); const confirmButtonIconStyle = useAnimatedStyle(() => { @@ -607,10 +626,12 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { estimatedGasLimit, gasSettings, - userHasEnoughFundsForTx, + enoughFundsForGas, }} > {children} + + ); }; diff --git a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts index 6ec112b24f8..acfb909a756 100644 --- a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts +++ b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts @@ -13,10 +13,7 @@ import { SUPPORTED_CHAIN_IDS, SupportedCurrencyKey } from '@/references'; import { getCachedProviderForNetwork, isHardHat } from '@/handlers/web3'; import { useAccountSettings } from '@/hooks'; -import { getNetworkObj } from '@/networks'; import { RainbowFetchClient } from '@/rainbow-fetch'; -import store from '@/redux/store'; -import { getNetworkFromChainId } from '@/utils/ethereumUtils'; import { fetchUserAssetsByChain } from './userAssetsByChain'; const addysHttp = new RainbowFetchClient({ @@ -225,42 +222,3 @@ export function useUserAssets( staleTime: process.env.IS_TESTING === 'true' ? 0 : 1000, }); } - -function getCachedUserAssets({ - address, - currency, - testnetMode = false, -}: { - address: Address; - currency: SupportedCurrencyKey; - testnetMode?: boolean; -}) { - return queryClient.getQueryData(userAssetsQueryKey({ address, currency, testnetMode })); -} - -const getNetworkNativeAssetUniqueId = (chainId: ChainId) => { - const network = getNetworkFromChainId(chainId); - const { nativeCurrency } = getNetworkObj(network); - const { mainnetAddress, address } = nativeCurrency; - const uniqueId = mainnetAddress ? `${mainnetAddress}_${ChainId.mainnet}` : `${address}_${chainId}`; - return uniqueId; -}; - -export function getUserNativeNetworkAsset(chainId: ChainId) { - const { accountAddress: currentAddress, nativeCurrency: currentCurrency, network: currentNetwork } = store.getState().settings; - - const provider = getCachedProviderForNetwork(currentNetwork); - const providerUrl = provider?.connection?.url; - const connectedToHardhat = isHardHat(providerUrl); - - const userAssets = getCachedUserAssets({ - address: currentAddress as Address, - currency: currentCurrency, - testnetMode: connectedToHardhat, - }); - - if (!userAssets) return; - - const uniqueId = getNetworkNativeAssetUniqueId(chainId); - return userAssets[chainId][uniqueId]; -} diff --git a/src/languages/en_US.json b/src/languages/en_US.json index 91d5b21bc18..701242a754e 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -1935,7 +1935,10 @@ "enter_amount": "Enter Amount", "review": "Review", "fetching_prices": "Fetching", - "swapping": "Swapping" + "swapping": "Swapping", + "select_token": "Select Token", + "insufficient_funds": "Insufficient Funds", + "estimating": "Estimating" }, "aggregators": { "rainbow": "Rainbow" diff --git a/src/resources/assets/assetSelectors.ts b/src/resources/assets/assetSelectors.ts index db8a15c9c92..c5fb9bf467b 100644 --- a/src/resources/assets/assetSelectors.ts +++ b/src/resources/assets/assetSelectors.ts @@ -1,8 +1,8 @@ -import { RainbowAddressAssets } from './types'; -import isEmpty from 'lodash/isEmpty'; -import isNil from 'lodash/isNil'; import { ParsedAddressAsset } from '@/entities'; import { parseAssetsNative } from '@/parsers'; +import isEmpty from 'lodash/isEmpty'; +import isNil from 'lodash/isNil'; +import { RainbowAddressAssets } from './types'; const EMPTY_ARRAY: any = []; diff --git a/src/resources/assets/useUserAsset.ts b/src/resources/assets/useUserAsset.ts index c9777a9fa1f..adfe62ed83b 100644 --- a/src/resources/assets/useUserAsset.ts +++ b/src/resources/assets/useUserAsset.ts @@ -1,7 +1,11 @@ +import { ChainId } from '@/__swaps__/types/chains'; import { getCachedProviderForNetwork, isHardHat } from '@/handlers/web3'; +import { Network } from '@/helpers'; import { useAccountSettings } from '@/hooks'; +import { getNetworkObj } from '@/networks'; import { selectUserAssetWithUniqueId } from '@/resources/assets/assetSelectors'; import { useUserAssets } from '@/resources/assets/UserAssetsQuery'; +import { getNetworkFromChainId } from '@/utils/ethereumUtils'; export function useUserAsset(uniqueId: string) { const { accountAddress, nativeCurrency, network: currentNetwork } = useAccountSettings(); @@ -20,3 +24,16 @@ export function useUserAsset(uniqueId: string) { } ); } + +export const getNetworkNativeAssetUniqueId = (chainId: ChainId) => { + const network = getNetworkFromChainId(chainId); + const { nativeCurrency } = getNetworkObj(network); + const { mainnetAddress, address } = nativeCurrency; + const uniqueId = mainnetAddress ? `${mainnetAddress}_${Network.mainnet}` : `${address}_${network}`; + return uniqueId; +}; + +export function useUserNativeNetworkAsset(chainId: ChainId) { + const uniqueId = getNetworkNativeAssetUniqueId(chainId); + return useUserAsset(uniqueId); +} From cba4aed92e8a19dd4d64562c07eeeac2c11ce4a0 Mon Sep 17 00:00:00 2001 From: gregs Date: Wed, 12 Jun 2024 11:40:53 -0300 Subject: [PATCH 16/25] =?UTF-8?q?or=20equal=20=F0=9F=A4=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__swaps__/screens/Swap/hooks/useCanPerformSwap.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__swaps__/screens/Swap/hooks/useCanPerformSwap.ts b/src/__swaps__/screens/Swap/hooks/useCanPerformSwap.ts index 6e7f309df4a..bfcac6596ac 100644 --- a/src/__swaps__/screens/Swap/hooks/useCanPerformSwap.ts +++ b/src/__swaps__/screens/Swap/hooks/useCanPerformSwap.ts @@ -1,4 +1,4 @@ -import { lessThanWorklet, toScaledIntegerWorklet } from '@/__swaps__/safe-math/SafeMath'; +import { lessThanOrEqualToWorklet, toScaledIntegerWorklet } from '@/__swaps__/safe-math/SafeMath'; import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets'; import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; import { SharedValue, useDerivedValue, useSharedValue } from 'react-native-reanimated'; @@ -17,7 +17,7 @@ export function useCanPerformSwap({ const enoughFundsForSwap = useDerivedValue(() => { if (!quote.value || 'error' in quote.value || !inputAsset.value) return true; - return lessThanWorklet( + return lessThanOrEqualToWorklet( quote.value.sellAmount.toString(), toScaledIntegerWorklet(inputAsset.value.balance.amount, inputAsset.value.decimals) ); From 2ea366e9290c10d854687d833339e11e331caa88 Mon Sep 17 00:00:00 2001 From: gregs Date: Wed, 12 Jun 2024 11:43:26 -0300 Subject: [PATCH 17/25] remove unused isSameAddress util --- src/utils/isSameAddress.ts | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 src/utils/isSameAddress.ts diff --git a/src/utils/isSameAddress.ts b/src/utils/isSameAddress.ts deleted file mode 100644 index 05194ccc494..00000000000 --- a/src/utils/isSameAddress.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ETH_ADDRESS } from '@rainbow-me/swaps'; -import { zeroAddress } from 'viem'; - -const eth = ETH_ADDRESS.toLowerCase(); -export const isEth = (address: string) => [eth, zeroAddress, 'eth'].includes(address.toLowerCase()); - -export const isSameAddress = (a: string, b: string) => { - if (isEth(a) && isEth(b)) return true; - return a.toLowerCase() === b.toLowerCase(); -}; From e6d4382774a0fada201652c766348c69dbabb2e1 Mon Sep 17 00:00:00 2001 From: gregs Date: Wed, 12 Jun 2024 11:45:56 -0300 Subject: [PATCH 18/25] just reordering declarations --- .../SyncSwapStateAndSharedValues.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx b/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx index 2a2f3224346..8a289c08f9a 100644 --- a/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx +++ b/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx @@ -16,16 +16,6 @@ import { useSelectedGas } from '../hooks/useSelectedGas'; import { useSwapEstimatedGasLimit } from '../hooks/useSwapEstimatedGasLimit'; import { useSwapContext } from './swap-provider'; -const getHasEnoughFundsForGas = (quote: Quote, gasFee: string, nativeNetworkAsset: ParsedAddressAsset | undefined) => { - if (!nativeNetworkAsset) return false; - const userBalance = nativeNetworkAsset.balance?.amount || '0'; - - const quoteValue = quote.value?.toString() || '0'; - const totalNativeSpentInTx = formatUnits(BigInt(add(quoteValue, gasFee)), nativeNetworkAsset.decimals); - - return lessThan(totalNativeSpentInTx, userBalance); -}; - type InternalSyncedSwapState = { assetToBuy: ExtendedAnimatedAssetWithColors | undefined; assetToSell: ExtendedAnimatedAssetWithColors | undefined; @@ -75,6 +65,16 @@ export const SyncQuoteSharedValuesToState = () => { return null; }; +const getHasEnoughFundsForGas = (quote: Quote, gasFee: string, nativeNetworkAsset: ParsedAddressAsset | undefined) => { + if (!nativeNetworkAsset) return false; + const userBalance = nativeNetworkAsset.balance?.amount || '0'; + + const quoteValue = quote.value?.toString() || '0'; + const totalNativeSpentInTx = formatUnits(BigInt(add(quoteValue, gasFee)), nativeNetworkAsset.decimals); + + return lessThan(totalNativeSpentInTx, userBalance); +}; + export function SyncGasStateToSharedValues() { const { estimatedGasLimit: estimatedGasLimitSharedValue, From 31d36b67d8bb68af3f7e5cfb68dde4f9d5e48964 Mon Sep 17 00:00:00 2001 From: gregs Date: Wed, 12 Jun 2024 14:22:56 -0300 Subject: [PATCH 19/25] error i18n --- src/__swaps__/screens/Swap/providers/swap-provider.tsx | 6 +++--- src/languages/en_US.json | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index d4b781f5be9..1b563e3a885 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -57,7 +57,7 @@ const enterAmount = i18n.t(i18n.l.swap.actions.enter_amount); const review = i18n.t(i18n.l.swap.actions.review); const fetchingPrices = i18n.t(i18n.l.swap.actions.fetching_prices); const selectToken = i18n.t(i18n.l.swap.actions.select_token); -const estimating = i18n.t(i18n.l.swap.actions.estimating); +const errorLabel = i18n.t(i18n.l.swap.actions.error); const insufficientFunds = i18n.t(i18n.l.swap.actions.insufficient_funds); interface SwapContextType { @@ -557,11 +557,11 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { if (!gasSettings.value) { // this could happen if metereology is down, or some other edge cases that are not properly handled yet - return { label: 'Error', disabled: true }; + return { label: errorLabel, disabled: true }; } if (isQuoteError) { - return { label: 'Error', disabled: true }; + return { label: errorLabel, disabled: true }; } return { icon: '􀕹', label: review, disabled: false }; diff --git a/src/languages/en_US.json b/src/languages/en_US.json index 701242a754e..194f8db4b75 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -1938,7 +1938,8 @@ "swapping": "Swapping", "select_token": "Select Token", "insufficient_funds": "Insufficient Funds", - "estimating": "Estimating" + "estimating": "Estimating", + "error": "Error" }, "aggregators": { "rainbow": "Rainbow" From 76df24c3ac073a28367ad718a37fe8aeb8377aee Mon Sep 17 00:00:00 2001 From: gregs Date: Thu, 13 Jun 2024 14:33:02 -0300 Subject: [PATCH 20/25] useGasSharedValues --- .../screens/Swap/hooks/useCanPerformSwap.ts | 32 ---------------- .../screens/Swap/hooks/useGasSharedValues.ts | 14 +++++++ .../screens/Swap/providers/swap-provider.tsx | 38 +++++++++++-------- 3 files changed, 36 insertions(+), 48 deletions(-) delete mode 100644 src/__swaps__/screens/Swap/hooks/useCanPerformSwap.ts create mode 100644 src/__swaps__/screens/Swap/hooks/useGasSharedValues.ts diff --git a/src/__swaps__/screens/Swap/hooks/useCanPerformSwap.ts b/src/__swaps__/screens/Swap/hooks/useCanPerformSwap.ts deleted file mode 100644 index bfcac6596ac..00000000000 --- a/src/__swaps__/screens/Swap/hooks/useCanPerformSwap.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { lessThanOrEqualToWorklet, toScaledIntegerWorklet } from '@/__swaps__/safe-math/SafeMath'; -import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets'; -import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; -import { SharedValue, useDerivedValue, useSharedValue } from 'react-native-reanimated'; -import { GasSettings } from './useCustomGas'; - -export function useCanPerformSwap({ - quote, - inputAsset, -}: { - inputAsset: SharedValue; - quote: SharedValue; -}) { - const gasSettings = useSharedValue(undefined); - const estimatedGasLimit = useSharedValue(undefined); - const enoughFundsForGas = useSharedValue(true); - - const enoughFundsForSwap = useDerivedValue(() => { - if (!quote.value || 'error' in quote.value || !inputAsset.value) return true; - return lessThanOrEqualToWorklet( - quote.value.sellAmount.toString(), - toScaledIntegerWorklet(inputAsset.value.balance.amount, inputAsset.value.decimals) - ); - }); - - return { - gasSettings, - estimatedGasLimit, - enoughFundsForGas, - enoughFundsForSwap, - }; -} diff --git a/src/__swaps__/screens/Swap/hooks/useGasSharedValues.ts b/src/__swaps__/screens/Swap/hooks/useGasSharedValues.ts new file mode 100644 index 00000000000..adb94d4af3e --- /dev/null +++ b/src/__swaps__/screens/Swap/hooks/useGasSharedValues.ts @@ -0,0 +1,14 @@ +import { useSharedValue } from 'react-native-reanimated'; +import { GasSettings } from './useCustomGas'; + +export function useGasSharedValues() { + const gasSettings = useSharedValue(undefined); + const estimatedGasLimit = useSharedValue(undefined); + const enoughFundsForGas = useSharedValue(true); + + return { + gasSettings, + estimatedGasLimit, + enoughFundsForGas, + }; +} diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index 271f7ba1261..60143edca04 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -1,6 +1,6 @@ // @refresh import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef } from 'react'; -import { StyleProp, TextStyle, TextInput, NativeModules, InteractionManager } from 'react-native'; +import { InteractionManager, NativeModules, StyleProp, TextInput, TextStyle } from 'react-native'; import { AnimatedRef, DerivedValue, @@ -20,38 +20,38 @@ import { NavigationSteps, useSwapNavigation } from '@/__swaps__/screens/Swap/hoo import { useSwapSettings } from '@/__swaps__/screens/Swap/hooks/useSwapSettings'; import { useSwapTextStyles } from '@/__swaps__/screens/Swap/hooks/useSwapTextStyles'; import { SwapWarningType, useSwapWarning } from '@/__swaps__/screens/Swap/hooks/useSwapWarning'; +import { userAssetsQueryKey as swapsUserAssetsQueryKey } from '@/__swaps__/screens/Swap/resources/assets/userAssets'; import { AddressOrEth, ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '@/__swaps__/types/assets'; import { ChainId } from '@/__swaps__/types/chains'; import { SwapAssetType, inputKeys } from '@/__swaps__/types/swap'; import { parseAssetAndExtend } from '@/__swaps__/utils/swaps'; -import { getProviderForNetwork, getFlashbotsProvider, isHardHat } from '@/handlers/web3'; +import { getFlashbotsProvider, getProviderForNetwork, isHardHat } from '@/handlers/web3'; import { WrappedAlert as Alert } from '@/helpers/alert'; import { useAccountSettings } from '@/hooks'; import * as i18n from '@/languages'; import { RainbowError, logger } from '@/logger'; -import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; -import { swapsStore } from '@/state/swaps/swapsStore'; -import { QuoteTypeMap, RapSwapActionParameters } from '@/raps/references'; +import { loadWallet } from '@/model/wallet'; import { Navigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; -import { ethereumUtils } from '@/utils'; -import { loadWallet } from '@/model/wallet'; import { walletExecuteRap } from '@/raps/execute'; +import { QuoteTypeMap, RapSwapActionParameters } from '@/raps/references'; import { queryClient } from '@/react-query'; -import { userAssetsQueryKey as swapsUserAssetsQueryKey } from '@/__swaps__/screens/Swap/resources/assets/userAssets'; import { userAssetsQueryKey } from '@/resources/assets/UserAssetsQuery'; +import { swapsStore } from '@/state/swaps/swapsStore'; +import { ethereumUtils } from '@/utils'; +import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; -import { getGasSettingsBySpeed, getSelectedGas, getSelectedGasSpeed } from '../hooks/useSelectedGas'; +import { equalWorklet, lessThanOrEqualToWorklet, toScaledIntegerWorklet } from '@/__swaps__/safe-math/SafeMath'; +import { analyticsV2 } from '@/analytics'; import { LegacyTransactionGasParamAmounts, TransactionGasParamAmounts } from '@/entities'; -import { equalWorklet } from '@/__swaps__/safe-math/SafeMath'; import { getNetworkObj } from '@/networks'; import { userAssetsStore } from '@/state/assets/userAssets'; -import { useCanPerformSwap } from '../hooks/useCanPerformSwap'; +import { Address } from 'viem'; import { GasSettings } from '../hooks/useCustomGas'; +import { useGasSharedValues } from '../hooks/useGasSharedValues'; +import { getGasSettingsBySpeed, getSelectedGas, getSelectedGasSpeed } from '../hooks/useSelectedGas'; import { useSwapOutputQuotesDisabled } from '../hooks/useSwapOutputQuotesDisabled'; import { SyncGasStateToSharedValues, SyncQuoteSharedValuesToState } from './SyncSwapStateAndSharedValues'; -import { analyticsV2 } from '@/analytics'; -import { Address } from 'viem'; const swapping = i18n.t(i18n.l.swap.actions.swapping); const tapToSwap = i18n.t(i18n.l.swap.actions.tap_to_swap); @@ -583,9 +583,15 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { }; }, []); - const { gasSettings, estimatedGasLimit, enoughFundsForGas, enoughFundsForSwap } = useCanPerformSwap({ - inputAsset: internalSelectedInputAsset, - quote, + const { gasSettings, estimatedGasLimit, enoughFundsForGas } = useGasSharedValues(); + + const enoughFundsForSwap = useDerivedValue(() => { + const inputAsset = internalSelectedInputAsset.value; + if (!quote.value || 'error' in quote.value || !inputAsset) return true; + return lessThanOrEqualToWorklet( + quote.value.sellAmount.toString(), + toScaledIntegerWorklet(inputAsset.balance.amount, inputAsset.decimals) + ); }); const confirmButtonProps = useDerivedValue(() => { From f93b7ad6e4be78c38cf127d96cc1c2726b6103a1 Mon Sep 17 00:00:00 2001 From: gregs Date: Thu, 13 Jun 2024 14:43:33 -0300 Subject: [PATCH 21/25] remove estimating --- src/languages/en_US.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/languages/en_US.json b/src/languages/en_US.json index 78dc06c46b1..41d4841ed83 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -1942,7 +1942,6 @@ "swapping": "Swapping", "select_token": "Select Token", "insufficient_funds": "Insufficient Funds", - "estimating": "Estimating", "error": "Error" }, "aggregators": { From 769780315770e4bc03b6d25b86405788a9002a15 Mon Sep 17 00:00:00 2001 From: gregs Date: Fri, 14 Jun 2024 14:29:31 -0300 Subject: [PATCH 22/25] fix label flickering --- .../screens/Swap/hooks/useGasSharedValues.ts | 14 ----- .../SyncSwapStateAndSharedValues.tsx | 28 +++------ .../screens/Swap/providers/swap-provider.tsx | 59 +++++++++---------- 3 files changed, 37 insertions(+), 64 deletions(-) delete mode 100644 src/__swaps__/screens/Swap/hooks/useGasSharedValues.ts diff --git a/src/__swaps__/screens/Swap/hooks/useGasSharedValues.ts b/src/__swaps__/screens/Swap/hooks/useGasSharedValues.ts deleted file mode 100644 index adb94d4af3e..00000000000 --- a/src/__swaps__/screens/Swap/hooks/useGasSharedValues.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useSharedValue } from 'react-native-reanimated'; -import { GasSettings } from './useCustomGas'; - -export function useGasSharedValues() { - const gasSettings = useSharedValue(undefined); - const estimatedGasLimit = useSharedValue(undefined); - const enoughFundsForGas = useSharedValue(true); - - return { - gasSettings, - estimatedGasLimit, - enoughFundsForGas, - }; -} diff --git a/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx b/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx index 8a289c08f9a..08929120ad6 100644 --- a/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx +++ b/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx @@ -76,45 +76,33 @@ const getHasEnoughFundsForGas = (quote: Quote, gasFee: string, nativeNetworkAsse }; export function SyncGasStateToSharedValues() { - const { - estimatedGasLimit: estimatedGasLimitSharedValue, - gasSettings: gasSettingsSharedValue, - enoughFundsForGas, - internalSelectedInputAsset, - SwapInputController, - } = useSwapContext(); + const { hasEnoughFundsForGas, internalSelectedInputAsset, SwapInputController } = useSwapContext(); const { assetToSell, chainId = ChainId.mainnet, quote } = useSyncedSwapQuoteStore(); const gasSettings = useSelectedGas(chainId); const { data: userNativeNetworkAsset } = useUserNativeNetworkAsset(chainId); - const { data: estimatedGasLimit } = useSwapEstimatedGasLimit({ chainId, assetToSell, quote }); + const { data: estimatedGasLimit, isFetching } = useSwapEstimatedGasLimit({ chainId, assetToSell, quote }); useEffect(() => { - estimatedGasLimitSharedValue.value = estimatedGasLimit; - gasSettingsSharedValue.value = gasSettings; - - if (!gasSettings || !estimatedGasLimit || !quote || 'error' in quote) { - enoughFundsForGas.value = true; - return; - } + hasEnoughFundsForGas.value = undefined; + if (!gasSettings || !estimatedGasLimit || !quote || 'error' in quote) return; const gasFee = calculateGasFee(gasSettings, estimatedGasLimit); - enoughFundsForGas.value = getHasEnoughFundsForGas(quote, gasFee, userNativeNetworkAsset); + hasEnoughFundsForGas.value = getHasEnoughFundsForGas(quote, gasFee, userNativeNetworkAsset); return () => { - enoughFundsForGas.value = true; + hasEnoughFundsForGas.value = undefined; }; }, [ - estimatedGasLimitSharedValue, estimatedGasLimit, gasSettings, - gasSettingsSharedValue, - enoughFundsForGas, + hasEnoughFundsForGas, quote, internalSelectedInputAsset.value?.balance.amount, SwapInputController.inputValues.value.inputAmount, userNativeNetworkAsset, + isFetching, ]); return null; diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index 60143edca04..277fb25d0bb 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -7,6 +7,7 @@ import { SharedValue, runOnJS, runOnUI, + useAnimatedReaction, useAnimatedRef, useAnimatedStyle, useDerivedValue, @@ -47,8 +48,6 @@ import { LegacyTransactionGasParamAmounts, TransactionGasParamAmounts } from '@/ import { getNetworkObj } from '@/networks'; import { userAssetsStore } from '@/state/assets/userAssets'; import { Address } from 'viem'; -import { GasSettings } from '../hooks/useCustomGas'; -import { useGasSharedValues } from '../hooks/useGasSharedValues'; import { getGasSettingsBySpeed, getSelectedGas, getSelectedGasSpeed } from '../hooks/useSelectedGas'; import { useSwapOutputQuotesDisabled } from '../hooks/useSwapOutputQuotesDisabled'; import { SyncGasStateToSharedValues, SyncQuoteSharedValuesToState } from './SyncSwapStateAndSharedValues'; @@ -116,9 +115,7 @@ interface SwapContextType { >; confirmButtonIconStyle: StyleProp; - estimatedGasLimit: SharedValue; - gasSettings: SharedValue; - enoughFundsForGas: SharedValue; + hasEnoughFundsForGas: SharedValue; } const SwapContext = createContext(undefined); @@ -583,16 +580,15 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { }; }, []); - const { gasSettings, estimatedGasLimit, enoughFundsForGas } = useGasSharedValues(); - - const enoughFundsForSwap = useDerivedValue(() => { - const inputAsset = internalSelectedInputAsset.value; - if (!quote.value || 'error' in quote.value || !inputAsset) return true; - return lessThanOrEqualToWorklet( - quote.value.sellAmount.toString(), - toScaledIntegerWorklet(inputAsset.balance.amount, inputAsset.decimals) - ); - }); + const hasEnoughFundsForGas = useSharedValue(undefined); + useAnimatedReaction( + () => isFetching.value, + fetching => { + // if it's refetching the gas is gonna be recalculated after + // so we already set it to undefined to prevent flickering + if (fetching) hasEnoughFundsForGas.value = undefined; + } + ); const confirmButtonProps = useDerivedValue(() => { if (isSwapping.value) { @@ -607,25 +603,35 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { return { icon: '􀆅', label: save, disabled: false }; } - if (isFetching.value) { - return { label: fetchingPrices, isLoading: true, disabled: true }; - } - const hasSelectedAssets = internalSelectedInputAsset.value && internalSelectedOutputAsset.value; if (!hasSelectedAssets) { return { label: selectToken, disabled: true }; } + if (isFetching.value || hasEnoughFundsForGas.value === undefined) { + return { label: fetchingPrices, isLoading: true, disabled: true }; + } + const isInputZero = equalWorklet(SwapInputController.inputValues.value.inputAmount, 0); const isOutputZero = equalWorklet(SwapInputController.inputValues.value.outputAmount, 0); const isQuoteError = quote.value && 'error' in quote.value; - if (!isQuoteError && (SwapInputController.percentageToSwap.value === 0 || isInputZero || isOutputZero)) { + if ((!isQuoteError && SwapInputController.percentageToSwap.value === 0) || isInputZero || isOutputZero) { return { label: enterAmount, disabled: true }; } - if (!enoughFundsForGas.value || !enoughFundsForSwap.value) { + const inputAsset = internalSelectedInputAsset.value; + const sellAmount = (() => { + const inputAmount = SwapInputController.inputValues.value.inputAmount; + if (!quote.value || 'error' in quote.value) return inputAmount; + return quote.value.sellAmount.toString(); + })(); + + const enoughFundsForSwap = + inputAsset && lessThanOrEqualToWorklet(sellAmount, toScaledIntegerWorklet(inputAsset.balance.amount, inputAsset.decimals)); + + if (!hasEnoughFundsForGas.value || !enoughFundsForSwap) { return { label: insufficientFunds, disabled: true }; } @@ -637,16 +643,11 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { return { icon: '􀕹', label: review, disabled: true }; } - if (!gasSettings.value) { - // this could happen if metereology is down, or some other edge cases that are not properly handled yet - return { label: errorLabel, disabled: true }; - } - if (isQuoteError) { return { label: errorLabel, disabled: true }; } - return { icon: '􀕹', label: review, disabled: false }; + return { icon: '􀕹', label: review + 'aa', disabled: false }; }); const confirmButtonIconStyle = useAnimatedStyle(() => { @@ -708,9 +709,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { confirmButtonProps, confirmButtonIconStyle, - estimatedGasLimit, - gasSettings, - enoughFundsForGas, + hasEnoughFundsForGas, }} > {children} From da8d964fc9b546038c9ad53f6b212596d202d6dd Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Fri, 14 Jun 2024 14:06:07 -0400 Subject: [PATCH 23/25] fix review panel not prompting --- .../screens/Swap/components/GestureHandlerV1Button.tsx | 2 +- src/__swaps__/screens/Swap/components/SwapActionButton.tsx | 3 ++- src/__swaps__/screens/Swap/providers/swap-provider.tsx | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/__swaps__/screens/Swap/components/GestureHandlerV1Button.tsx b/src/__swaps__/screens/Swap/components/GestureHandlerV1Button.tsx index f4752f619a8..274acf93912 100644 --- a/src/__swaps__/screens/Swap/components/GestureHandlerV1Button.tsx +++ b/src/__swaps__/screens/Swap/components/GestureHandlerV1Button.tsx @@ -91,7 +91,7 @@ export const GestureHandlerV1Button = React.forwardRef(function GestureHandlerV1 )} > {/* @ts-expect-error Property 'children' does not exist on type */} - + {children} diff --git a/src/__swaps__/screens/Swap/components/SwapActionButton.tsx b/src/__swaps__/screens/Swap/components/SwapActionButton.tsx index d542da1c005..2a2ec23dc34 100644 --- a/src/__swaps__/screens/Swap/components/SwapActionButton.tsx +++ b/src/__swaps__/screens/Swap/components/SwapActionButton.tsx @@ -107,7 +107,7 @@ export const SwapActionButton = ({ const buttonAnimatedProps = useAnimatedProps(() => { return { - disabled: disabled?.value, + disabled: disabled?.value ?? false, scaleTo: disabled?.value ? 1 : scaleTo || (hugContent ? undefined : 0.925), }; }); @@ -117,6 +117,7 @@ export const SwapActionButton = ({ animatedProps={buttonAnimatedProps} onPressStartWorklet={onPressWorklet} onPressJS={onPressJS} + pointerEvents="auto" style={[hugContent && feedActionButtonStyles.buttonWrapper, style]} > { return { label: errorLabel, disabled: true }; } - return { icon: '􀕹', label: review + 'aa', disabled: false }; + return { icon: '􀕹', label: review, disabled: false }; }); const confirmButtonIconStyle = useAnimatedStyle(() => { From fea8f655f6e4964706a8497f747dca97e1044ddc Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Fri, 14 Jun 2024 14:32:15 -0400 Subject: [PATCH 24/25] Revert "Lint on pre-commit (#5836)" This reverts commit d56ed46e7772cd51e54a0f8214947de01e48dd47. --- .eslintignore | 10 +--------- package.json | 18 ++++++------------ tsconfig.json | 13 +------------ 3 files changed, 8 insertions(+), 33 deletions(-) diff --git a/.eslintignore b/.eslintignore index 835d18a6dd3..0b8344ee77c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -18,12 +18,4 @@ __generated__ coverage src/browser src/__swaps__/README.md -InjectedJSBundle.js -__mocks__ -config -hardhat.config.js -index.js -metro.transform.js -rainbow-scripts -react-native.config.js -scripts \ No newline at end of file +InjectedJSBundle.js \ No newline at end of file diff --git a/package.json b/package.json index 98ae21edf67..fee4475140a 100644 --- a/package.json +++ b/package.json @@ -518,18 +518,6 @@ "src/react-native-shadow-stack/**/*.*" ] }, - "husky": { - "hooks": { - "pre-commit": "lint-staged" - } - }, - "lint-staged": { - "*.{js,jsx,ts,tsx}": [ - "prettier --check", - "yarn tsc --skipLibCheck --noEmit --allowJs", - "eslint --fix" - ] - }, "lavamoat": { "allowScripts": { "$root$": false, @@ -551,5 +539,11 @@ "viem>ws>bufferutil": false, "viem>ws>utf-8-validate": false } + }, + "lint-staged": { + "*.{js,jsx,ts,tsx,graphql}": [ + "prettier --write", + "eslint --cache" + ] } } diff --git a/tsconfig.json b/tsconfig.json index d3bb4c99715..2f2c192422a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,6 @@ { "compilerOptions": { "allowJs": true, - "skipLibCheck": true, "allowSyntheticDefaultImports": true, "esModuleInterop": true, "isolatedModules": true, @@ -56,15 +55,5 @@ "resolveJsonModule": true }, "globals": ["useTheme", "ios", "android", "web", "__DEV__", "IS_DEV"], - "exclude": [ - "ios", - "src/browser", - "android", - "patches/reanimated", - "node_modules", - "babel.config.js", - "metro.config.js", - "jest.config.js" - ], - "include": ["src", "types", "globals.d.ts", "e2e"] + "exclude": ["ios", "src/browser", "android", "patches/reanimated", "node_modules", "babel.config.js", "metro.config.js", "jest.config.js"] } From 955e223d996583fd325c651cc3b08d12519d1ddf Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Fri, 14 Jun 2024 15:42:28 -0400 Subject: [PATCH 25/25] fix a bunch of shit --- .../components/GestureHandlerV1Button.tsx | 2 +- .../Swap/components/SwapActionButton.tsx | 8 +++---- .../Swap/components/SwapBottomPanel.tsx | 2 -- .../SyncSwapStateAndSharedValues.tsx | 4 ++-- .../screens/Swap/providers/swap-provider.tsx | 23 +++++++------------ 5 files changed, 15 insertions(+), 24 deletions(-) diff --git a/src/__swaps__/screens/Swap/components/GestureHandlerV1Button.tsx b/src/__swaps__/screens/Swap/components/GestureHandlerV1Button.tsx index 274acf93912..f4752f619a8 100644 --- a/src/__swaps__/screens/Swap/components/GestureHandlerV1Button.tsx +++ b/src/__swaps__/screens/Swap/components/GestureHandlerV1Button.tsx @@ -91,7 +91,7 @@ export const GestureHandlerV1Button = React.forwardRef(function GestureHandlerV1 )} > {/* @ts-expect-error Property 'children' does not exist on type */} - + {children} diff --git a/src/__swaps__/screens/Swap/components/SwapActionButton.tsx b/src/__swaps__/screens/Swap/components/SwapActionButton.tsx index 2a2ec23dc34..6bb346891e4 100644 --- a/src/__swaps__/screens/Swap/components/SwapActionButton.tsx +++ b/src/__swaps__/screens/Swap/components/SwapActionButton.tsx @@ -1,6 +1,6 @@ /* eslint-disable no-nested-ternary */ import React from 'react'; -import { StyleProp, StyleSheet, TextStyle, ViewStyle } from 'react-native'; +import { StyleProp, StyleSheet, TextStyle, ViewProps, ViewStyle } from 'react-native'; import Animated, { DerivedValue, useAnimatedProps, useAnimatedStyle, useDerivedValue } from 'react-native-reanimated'; import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets'; @@ -107,8 +107,9 @@ export const SwapActionButton = ({ const buttonAnimatedProps = useAnimatedProps(() => { return { - disabled: disabled?.value ?? false, - scaleTo: disabled?.value ? 1 : scaleTo || (hugContent ? undefined : 0.925), + pointerEvents: (disabled?.value ? 'none' : 'box-only') as ViewProps['pointerEvents'], + disableButtonPressWrapper: disabled?.value, + scaleTo: scaleTo || (hugContent ? undefined : 0.925), }; }); @@ -117,7 +118,6 @@ export const SwapActionButton = ({ animatedProps={buttonAnimatedProps} onPressStartWorklet={onPressWorklet} onPressJS={onPressJS} - pointerEvents="auto" style={[hugContent && feedActionButtonStyles.buttonWrapper, style]} > confirmButtonProps.value.icon); const label = useDerivedValue(() => confirmButtonProps.value.label); const disabled = useDerivedValue(() => confirmButtonProps.value.disabled); - const isLoading = useDerivedValue(() => confirmButtonProps.value.isLoading); return ( // @ts-expect-error Property 'children' does not exist on type @@ -84,7 +83,6 @@ export function SwapBottomPanel() { iconStyle={confirmButtonIconStyle} label={label} disabled={disabled} - isLoading={isLoading} scaleTo={0.9} /> diff --git a/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx b/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx index 08929120ad6..3557f0ddb6f 100644 --- a/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx +++ b/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx @@ -85,14 +85,14 @@ export function SyncGasStateToSharedValues() { const { data: estimatedGasLimit, isFetching } = useSwapEstimatedGasLimit({ chainId, assetToSell, quote }); useEffect(() => { - hasEnoughFundsForGas.value = undefined; + hasEnoughFundsForGas.value = false; if (!gasSettings || !estimatedGasLimit || !quote || 'error' in quote) return; const gasFee = calculateGasFee(gasSettings, estimatedGasLimit); hasEnoughFundsForGas.value = getHasEnoughFundsForGas(quote, gasFee, userNativeNetworkAsset); return () => { - hasEnoughFundsForGas.value = undefined; + hasEnoughFundsForGas.value = false; }; }, [ estimatedGasLimit, diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index e9e7fa7a3ba..7dddad7542c 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -7,7 +7,6 @@ import { SharedValue, runOnJS, runOnUI, - useAnimatedReaction, useAnimatedRef, useAnimatedStyle, useDerivedValue, @@ -110,7 +109,6 @@ interface SwapContextType { label: string; icon?: string; disabled?: boolean; - isLoading?: boolean; }> >; confirmButtonIconStyle: StyleProp; @@ -580,15 +578,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { }; }, []); - const hasEnoughFundsForGas = useSharedValue(undefined); - useAnimatedReaction( - () => isFetching.value, - fetching => { - // if it's refetching the gas is gonna be recalculated after - // so we already set it to undefined to prevent flickering - if (fetching) hasEnoughFundsForGas.value = undefined; - } - ); + const hasEnoughFundsForGas = useSharedValue(false); const confirmButtonProps = useDerivedValue(() => { if (isSwapping.value) { @@ -608,16 +598,19 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { return { label: selectToken, disabled: true }; } - if (isFetching.value || hasEnoughFundsForGas.value === undefined) { - return { label: fetchingPrices, isLoading: true, disabled: true }; + if (isFetching.value) { + return { label: fetchingPrices, disabled: true }; } const isInputZero = equalWorklet(SwapInputController.inputValues.value.inputAmount, 0); const isOutputZero = equalWorklet(SwapInputController.inputValues.value.outputAmount, 0); const isQuoteError = quote.value && 'error' in quote.value; + if (isQuoteError) { + return { label: errorLabel, disabled: true }; + } - if ((!isQuoteError && SwapInputController.percentageToSwap.value === 0) || isInputZero || isOutputZero) { + if (SwapInputController.percentageToSwap.value === 0 || isInputZero || isOutputZero) { return { label: enterAmount, disabled: true }; } @@ -631,7 +624,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { const enoughFundsForSwap = inputAsset && lessThanOrEqualToWorklet(sellAmount, toScaledIntegerWorklet(inputAsset.balance.amount, inputAsset.decimals)); - if (!hasEnoughFundsForGas.value || !enoughFundsForSwap) { + if (!isFetching && (!hasEnoughFundsForGas.value || !enoughFundsForSwap)) { return { label: insufficientFunds, disabled: true }; }