Skip to content

Commit

Permalink
Gas optimizations (#5779)
Browse files Browse the repository at this point in the history
* perf

* ✨

* useWhyDidYouUpdate

* EstimatedSwapGasFee

* keepPreviousData

* AnimatedText

* isSameAddress
  • Loading branch information
greg-schrammel committed May 29, 2024
1 parent c2bea4f commit 56aecb3
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 77 deletions.
36 changes: 36 additions & 0 deletions src/__swaps__/screens/Swap/components/EstimatedSwapGasFee.tsx
Original file line number Diff line number Diff line change
@@ -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<Pick<TextProps, 'align' | 'color' | 'size' | 'weight' | 'tabularNumbers'>>) {
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 (
<AnimatedText
style={animatedOpacity}
staticText={estimatedGasFee}
align={align}
color={color}
size={size}
weight={weight}
tabularNumbers={tabularNumbers}
/>
);
});
35 changes: 17 additions & 18 deletions src/__swaps__/screens/Swap/components/GasButton.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Inline alignVertical="center" space="4px">
<TextIcon color="labelQuaternary" height={10} size="icon 11px" weight="heavy" width={16}>
􀵟
</TextIcon>
<Text color="labelTertiary" size="15pt" weight="bold">
{estimatedGasFee}
</Text>
<EstimatedSwapGasFee gasSettings={gasSettings} />
</Inline>
);
}
Expand Down Expand Up @@ -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<const T extends string>(obj: Record<T, any> | undefined) {
Expand Down Expand Up @@ -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,
Expand All @@ -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;

Expand Down Expand Up @@ -211,10 +210,10 @@ export function ReviewGasButton() {
export const GasButton = () => {
return (
<GasMenu>
<Stack space="12px">
<Box gap={12}>
<SelectedGas />
<EstimatedGasFee />
</Stack>
</Box>
</GasMenu>
);
};
Expand Down
41 changes: 25 additions & 16 deletions src/__swaps__/screens/Swap/components/GasPanel.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -144,6 +144,9 @@ function CurrentBaseFee() {

// loading state?

const isEIP1559 = useIsChainEIP1559(chainId);
if (!isEIP1559) return null;

return (
<Inline horizontalSpace="10px" alignVertical="center" alignHorizontal="justify">
<PressableLabel
Expand Down Expand Up @@ -180,17 +183,19 @@ function useGasPanelState<
const state = useGasPanelStore(select);

const chainId = useSwapsStore(s => 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<GasPanelState>) => {
Expand Down Expand Up @@ -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 (
<Inline horizontalSpace="10px" alignVertical="center" alignHorizontal="justify">
Expand All @@ -279,9 +284,13 @@ function MaxTransactionFee() {
</Inline>

<Inline horizontalSpace="6px">
<Text align="right" color={isDarkMode ? 'labelSecondary' : 'label'} size="15pt" weight="heavy">
{maxTransactionFee}
</Text>
<EstimatedSwapGasFee
gasSettings={gasSettings}
align="right"
color={isDarkMode ? 'labelSecondary' : 'label'}
size="15pt"
weight="heavy"
/>
</Inline>
</Inline>
);
Expand All @@ -290,7 +299,9 @@ function MaxTransactionFee() {
function EditableGasSettings() {
const chainId = useSwapsStore(s => s.inputAsset?.chainId || ChainId.mainnet);
const isEIP1559 = useIsChainEIP1559(chainId);

if (!isEIP1559) return <EditGasPrice />;

return (
<>
<EditMaxBaseFee />
Expand All @@ -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();
Expand Down
9 changes: 2 additions & 7 deletions src/__swaps__/screens/Swap/components/ReviewPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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 (
<Text align="left" color={'label'} size="15pt" weight="heavy">
{estimatedGasFee}
</Text>
);
return <EstimatedSwapGasFee gasSettings={gasSettings} align="left" color="label" size="15pt" weight="heavy" />;
}

function EstimatedArrivalTime() {
Expand Down
56 changes: 45 additions & 11 deletions src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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]);
}
Loading

0 comments on commit 56aecb3

Please sign in to comment.