From a5087f2300bf28a04d6b1c1b3a302cb48ccbbae2 Mon Sep 17 00:00:00 2001 From: Ben Goldberg Date: Fri, 21 Jun 2024 17:46:56 -0700 Subject: [PATCH] Swaps: Gas fee range to use for native asset maxSwappableAmount buffer (#5881) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * fix gesture button states * Safemath pt 2 (#5778) * add more fns * accept string or number * update errors * fix dynamic island overlap on recieve modal (#5672) * . * oop * oop * okay ty ben * change background opacity to 1 * . * oop * . * Gas optimizations (#5779) * perf * ✨ * useWhyDidYouUpdate * EstimatedSwapGasFee * keepPreviousData * AnimatedText * isSameAddress * fix other networks section (#5784) * Swaps: fix favorite button press (#5782) * fix * android fix * todo * remove console logs * Insufficient Funds * remove todo * move cache getter closer to fetcher implentation * fix * :) * 🍕 * or equal 🤌 * remove unused isSameAddress util * just reordering declarations * error i18n * useGasSharedValues * remove estimating * fix label flickering * fix review panel not prompting * Revert "Lint on pre-commit (#5836)" This reverts commit d56ed46e7772cd51e54a0f8214947de01e48dd47. * fix a bunch of shit * fix? * opacity * on review panel we should show fetching status and quote errors too * remove error * useUserNativeNetworkAsset * less or equal * gas fee range * fixes * Fixes for review button states (#5873) * Fixes APP-1601: adds missing useThreshold for gas fee showing $0.00 * Tweak confirm button prop labels logic and ordering * Prep work: remove reliance of asset balance display in valueBasedDecimalFormatter * niceIncrementerFormatter returns asset balance if max swap * Update formattedInputValue Instead of using the niceIncrementFormatter for formatting slider-based values: for max: use the valueBasedDecimalFormatter on the input value which has already been set to maxSwappableAmount for everything else: add commas to the input value as it has already been formatted using the niceIncrementFormatter * merge * weird issues * fixes * fix out of sync issue * update input amount + quote when maxSwappableAmount changes * fix input formatting logic * fix wei conversion --------- Co-authored-by: gregs Co-authored-by: Matthew Wall Co-authored-by: Bruno Barbieri <1247834+brunobar79@users.noreply.github.com> Co-authored-by: brdy <41711440+BrodyHughes@users.noreply.github.com> Co-authored-by: Jin --- .../Swap/hooks/useSwapInputsController.ts | 22 ++++- .../SyncSwapStateAndSharedValues.tsx | 82 +++++++++++-------- 2 files changed, 70 insertions(+), 34 deletions(-) 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; }