diff --git a/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts b/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts index 9e66e59fda1..722676e79f5 100644 --- a/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts +++ b/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts @@ -1,7 +1,7 @@ import { useCallback } from 'react'; import { SharedValue, runOnJS, runOnUI, useAnimatedReaction, useDerivedValue, useSharedValue, withSpring } from 'react-native-reanimated'; import { useDebouncedCallback } from 'use-debounce'; -import { MAXIMUM_SIGNIFICANT_DECIMALS, SCRUBBER_WIDTH, SLIDER_WIDTH, snappySpringConfig } from '@/__swaps__/screens/Swap/constants'; +import { SCRUBBER_WIDTH, SLIDER_WIDTH, snappySpringConfig } from '@/__swaps__/screens/Swap/constants'; import { RequestNewQuoteParams, inputKeys, inputMethods, inputValuesType } from '@/__swaps__/types/swap'; import { addCommasToNumber, @@ -105,6 +105,8 @@ export function useSwapInputsController({ }); const inputMethod = useSharedValue('slider'); + const maxSwappableAmount = useDerivedValue(() => internalSelectedInputAsset.value?.maxSwappableAmount); + const percentageToSwap = useDerivedValue(() => { return Math.round(clamp((sliderXPosition.value - SCRUBBER_WIDTH / SLIDER_WIDTH) / SLIDER_WIDTH, 0, 1) * 100) / 100; }); @@ -559,6 +561,24 @@ export function useSwapInputsController({ }); }; + // update the input amount & quote if swapping max amount & maxSwappableAmount changes + useAnimatedReaction( + () => maxSwappableAmount.value, + maxSwappableAmount => { + const isSwappingMaxBalance = internalSelectedInputAsset.value && inputMethod.value === 'slider' && percentageToSwap.value >= 1; + if (maxSwappableAmount && isSwappingMaxBalance) { + inputValues.modify(prev => { + return { + ...prev, + inputAmount: +maxSwappableAmount, + inputNativeValue: +mulWorklet(maxSwappableAmount, inputNativePrice.value), + }; + }); + fetchQuoteAndAssetPrices(); + } + } + ); + const quoteFetchingInterval = useAnimatedInterval({ intervalMs: 12_000, onIntervalWorklet: fetchQuoteAndAssetPrices, diff --git a/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx b/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx index f8ac1ebb873..9b9c4a10f90 100644 --- a/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx +++ b/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx @@ -17,7 +17,7 @@ import { useUserNativeNetworkAsset } from '@/resources/assets/useUserAsset'; import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; import { debounce } from 'lodash'; import { useEffect } from 'react'; -import { SharedValue, runOnJS, useAnimatedReaction } from 'react-native-reanimated'; +import { runOnJS, useAnimatedReaction, useSharedValue } from 'react-native-reanimated'; import { formatUnits } from 'viem'; import { create } from 'zustand'; import { calculateGasFee } from '../hooks/useEstimatedGasFee'; @@ -25,6 +25,8 @@ import { useSelectedGas } from '../hooks/useSelectedGas'; import { useSwapEstimatedGasLimit } from '../hooks/useSwapEstimatedGasLimit'; import { useSwapContext } from './swap-provider'; +const BUFFER_RATIO = 0.25; + type InternalSyncedSwapState = { assetToBuy: ExtendedAnimatedAssetWithColors | undefined; assetToSell: ExtendedAnimatedAssetWithColors | undefined; @@ -84,55 +86,69 @@ const getHasEnoughFundsForGas = (quote: Quote, gasFee: string, nativeNetworkAsse return lessThanOrEqualToWorklet(totalNativeSpentInTx, userBalance); }; -const BUFFER_FACTOR = 1.3; -function updateMaxSwappableAmount(internalSelectedInputAsset: SharedValue, gasFee: string) { - internalSelectedInputAsset.modify(asset => { - 'worklet'; - - if (!asset?.isNativeAsset) return asset; - - const gasFeeNativeCurrency = divWorklet(gasFee, powWorklet(10, asset.decimals)); - const gasFeeWithBuffer = toFixedWorklet(mulWorklet(gasFeeNativeCurrency, BUFFER_FACTOR), asset.decimals); - const maxSwappableAmount = subWorklet(asset.balance.amount, gasFeeWithBuffer); - - return { - ...asset, - maxSwappableAmount: lessThanWorklet(maxSwappableAmount, 0) ? '0' : maxSwappableAmount, - }; - }); -} - export function SyncGasStateToSharedValues() { - const { hasEnoughFundsForGas, internalSelectedInputAsset, SwapInputController } = useSwapContext(); + const { hasEnoughFundsForGas, internalSelectedInputAsset } = useSwapContext(); const { assetToSell, chainId = ChainId.mainnet, quote } = useSyncedSwapQuoteStore(); const gasSettings = useSelectedGas(chainId); const { data: userNativeNetworkAsset } = useUserNativeNetworkAsset(chainId); - const { data: estimatedGasLimit, isFetching } = useSwapEstimatedGasLimit({ chainId, assetToSell, quote }); + const { data: estimatedGasLimit } = useSwapEstimatedGasLimit({ chainId, assetToSell, quote }); + + const gasFeeRange = useSharedValue<[string, string] | null>(null); + + useAnimatedReaction( + () => ({ inputAsset: internalSelectedInputAsset.value, bufferRange: gasFeeRange.value }), + (current, previous) => { + const { inputAsset: currInputAsset, bufferRange: currBufferRange } = current; + const { inputAsset: prevInputAsset, bufferRange: prevBufferRange } = previous || {}; + + const currBuffer = currBufferRange?.[1]; + const prevBuffer = prevBufferRange?.[1]; + + if (currInputAsset?.chainId !== prevInputAsset?.chainId) { + // reset gas fee range when input chain changes + gasFeeRange.value = null; + } else if (currBuffer && (currBuffer !== prevBuffer || currInputAsset?.uniqueId !== prevInputAsset?.uniqueId)) { + // update maxSwappableAmount when gas fee range is set and there is a change to input asset or gas fee range + internalSelectedInputAsset.modify(asset => { + 'worklet'; + if (!asset || !asset.isNativeAsset) return asset; + return { + ...asset, + maxSwappableAmount: subWorklet(asset.balance.amount, currBuffer), + }; + }); + } + } + ); useEffect(() => { hasEnoughFundsForGas.value = undefined; - if (!gasSettings || !estimatedGasLimit || !quote || 'error' in quote) return; + if (!gasSettings || !estimatedGasLimit || !quote || 'error' in quote || !userNativeNetworkAsset) return; const gasFee = calculateGasFee(gasSettings, estimatedGasLimit); - updateMaxSwappableAmount(internalSelectedInputAsset, gasFee); + const nativeGasFee = divWorklet(gasFee, powWorklet(10, userNativeNetworkAsset.decimals)); + + const isEstimateOutsideRange = !!( + gasFeeRange.value && + (lessThanWorklet(nativeGasFee, gasFeeRange.value[0]) || greaterThanWorklet(nativeGasFee, gasFeeRange.value[1])) + ); + + // If the gas fee range hasn't been set or the estimated fee is outside the range, calculate the range based on the gas fee + if (nativeGasFee && (!gasFeeRange.value || isEstimateOutsideRange)) { + const lowerBound = toFixedWorklet(mulWorklet(nativeGasFee, 1 - BUFFER_RATIO), userNativeNetworkAsset.decimals); + const upperBound = toFixedWorklet(mulWorklet(nativeGasFee, 1 + BUFFER_RATIO), userNativeNetworkAsset.decimals); + gasFeeRange.value = [lowerBound, upperBound]; + } + hasEnoughFundsForGas.value = getHasEnoughFundsForGas(quote, gasFee, userNativeNetworkAsset); return () => { hasEnoughFundsForGas.value = undefined; }; - }, [ - estimatedGasLimit, - gasSettings, - hasEnoughFundsForGas, - quote, - internalSelectedInputAsset, - SwapInputController.inputValues.value.inputAmount, - userNativeNetworkAsset, - isFetching, - ]); + }, [estimatedGasLimit, gasFeeRange, gasSettings, hasEnoughFundsForGas, quote, userNativeNetworkAsset]); return null; }