Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gas #5757

Merged
merged 23 commits into from
May 24, 2024
Merged

Gas #5757

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
409 changes: 186 additions & 223 deletions src/__swaps__/screens/Swap/components/GasButton.tsx

Large diffs are not rendered by default.

545 changes: 309 additions & 236 deletions src/__swaps__/screens/Swap/components/GasPanel.tsx

Large diffs are not rendered by default.

61 changes: 43 additions & 18 deletions src/__swaps__/screens/Swap/components/ReviewPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import * as i18n from '@/languages';
import React, { useCallback } from 'react';

import { ReviewGasButton } from '@/__swaps__/screens/Swap/components/GasButton';
import { ChainId } from '@/__swaps__/types/chains';
import { AnimatedText, Box, Inline, Separator, Stack, Text, globalColors, useColorMode } from '@/design-system';
import { StyleSheet, View } from 'react-native';

import Animated, {
Expand All @@ -9,22 +14,22 @@ import Animated, {
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import { fadeConfig } from '../constants';
import { NavigationSteps, useSwapContext } from '../providers/swap-provider';
import { AnimatedSwitch } from './AnimatedSwitch';

import { AnimatedText, Box, Inline, Separator, Stack, Text, globalColors, useColorMode } from '@/design-system';
import * as i18n from '@/languages';
import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps';
import { useAccountSettings } from '@/hooks';
import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps';

import { NavigationSteps, useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider';
import { fadeConfig } from '@/__swaps__/screens/Swap/constants';
import { ChainId } from '@/__swaps__/types/chains';
import { chainNameForChainIdWithMainnetSubstitutionWorklet } from '@/__swaps__/utils/chains';
import { AnimatedSwitch } from '@/__swaps__/screens/Swap/components/AnimatedSwitch';
import { GasButton } from '@/__swaps__/screens/Swap/components/GasButton';
import { GestureHandlerV1Button } from '@/__swaps__/screens/Swap/components/GestureHandlerV1Button';
import { AnimatedChainImage } from '@/__swaps__/screens/Swap/components/AnimatedChainImage';
import { convertRawAmountToBalance, convertRawAmountToNativeDisplay, handleSignificantDecimals, multiply } from '@/__swaps__/utils/numbers';
import { GestureHandlerV1Button } from '@/__swaps__/screens/Swap/components/GestureHandlerV1Button';
import { useNativeAssetForChain } from '@/__swaps__/screens/Swap/hooks/useNativeAssetForChain';
import { chainNameForChainIdWithMainnetSubstitutionWorklet } from '@/__swaps__/utils/chains';
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';

const unknown = i18n.t(i18n.l.swap.unknown);

Expand Down Expand Up @@ -81,6 +86,30 @@ 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>
);
}

function EstimatedArrivalTime() {
const chainId = useSwapsStore(s => s.inputAsset?.chainId || ChainId.mainnet);
const speed = useSelectedGasSpeed(chainId);
const { data: estimatedTime } = useEstimatedTime({ chainId, speed });
if (!estimatedTime) return null;
return (
<Text align="right" color={'labelTertiary'} size="15pt" weight="bold">
{estimatedTime}
</Text>
);
}

export function ReviewPanel() {
const { isDarkMode } = useColorMode();
const { configProgress, SwapSettings, SwapInputController, internalSelectedInputAsset, internalSelectedOutputAsset } = useSwapContext();
Expand Down Expand Up @@ -109,10 +138,6 @@ export function ReviewPanel() {
SwapSettings.onUpdateSlippage('plus');
};

// TODO: Comes from gas store
const estimatedGasFee = useSharedValue('$2.25');
const estimatedArrivalTime = useSharedValue('~4 sec');

const styles = useAnimatedStyle(() => {
return {
display: configProgress.value !== NavigationSteps.SHOW_REVIEW ? 'none' : 'flex',
Expand Down Expand Up @@ -296,8 +321,8 @@ export function ReviewPanel() {
<AnimatedChainImage showMainnetBadge asset={internalSelectedInputAsset} size={16} />
</View>
<Inline horizontalSpace="4px">
<AnimatedText align="left" color={'label'} size="15pt" weight="heavy" text={estimatedGasFee} />
<AnimatedText align="right" color={'labelTertiary'} size="15pt" weight="bold" text={estimatedArrivalTime} />
<EstimatedGasFee />
<EstimatedArrivalTime />
</Inline>
</Inline>

Expand All @@ -312,7 +337,7 @@ export function ReviewPanel() {
</Stack>

<Inline alignVertical="center" horizontalSpace="8px">
<GasButton isReviewing />
<ReviewGasButton />
</Inline>
</Inline>
</Stack>
Expand Down
13 changes: 7 additions & 6 deletions src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import React from 'react';
import Animated, { runOnUI, useAnimatedStyle, withSpring } from 'react-native-reanimated';
import { StyleSheet } from 'react-native';
import { getSoftMenuBarHeight } from 'react-native-extra-dimensions-android';
import { PanGestureHandler } from 'react-native-gesture-handler';

import { Box, Column, Columns, Separator, globalColors, useColorMode } from '@/design-system';
import { safeAreaInsetValues } from '@/utils';

import { SwapActionButton } from './SwapActionButton';
import { GasButton } from './GasButton';
import { LIGHT_SEPARATOR_COLOR, SEPARATOR_COLOR, THICK_BORDER_WIDTH, springConfig } from '@/__swaps__/screens/Swap/constants';
import { IS_ANDROID } from '@/env';
import { useSwapContext, NavigationSteps } from '@/__swaps__/screens/Swap/providers/swap-provider';
import Animated, { runOnUI, useAnimatedStyle, withSpring } from 'react-native-reanimated';
import { StyleSheet } from 'react-native';

import { opacity } from '@/__swaps__/utils/swaps';
import { ReviewPanel } from './ReviewPanel';
import { GasPanel } from './GasPanel';
import { useBottomPanelGestureHandler } from '../hooks/useBottomPanelGestureHandler';
import { GasButton } from './GasButton';
import { GasPanel } from './GasPanel';
import { ReviewPanel } from './ReviewPanel';
import { SwapActionButton } from './SwapActionButton';

export function SwapBottomPanel() {
const { isDarkMode } = useColorMode();
Expand Down
31 changes: 31 additions & 0 deletions src/__swaps__/screens/Swap/hooks/formatNumber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import store from '@/redux/store';
import { supportedNativeCurrencies } from '@/references';

const decimalSeparator = '.';
export const formatNumber = (value: string, options?: { decimals?: number }) => {
if (!+value) return `0${decimalSeparator}00`;
if (+value < 0.0001) return `<0${decimalSeparator}0001`;

const [whole, fraction = ''] = value.split(decimalSeparator);
const decimals = options?.decimals;
const paddedFraction = `${fraction.padEnd(decimals || 4, '0')}`;

if (decimals) {
if (decimals === 0) return whole;
return `${whole}${decimalSeparator}${paddedFraction.slice(0, decimals)}`;
}

if (+whole > 0) return `${whole}${decimalSeparator}${paddedFraction.slice(0, 2)}`;
return `0${decimalSeparator}${paddedFraction.slice(0, 4)}`;
};

const getUserPreferredCurrency = () => {
const currency = store.getState().settings.nativeCurrency;
return supportedNativeCurrencies[currency];
};

export const formatCurrency = (value: string, currency = getUserPreferredCurrency()) => {
const formatted = formatNumber(value);
if (currency.alignment === 'left') return `${currency.symbol}${formatted}`;
return `${formatted} ${currency.symbol}`;
};
174 changes: 35 additions & 139 deletions src/__swaps__/screens/Swap/hooks/useCustomGas.ts
Original file line number Diff line number Diff line change
@@ -1,141 +1,37 @@
import { useGas } from '@/hooks';
import { useCallback } from 'react';
import { runOnJS, useAnimatedReaction, useDerivedValue, useSharedValue } from 'react-native-reanimated';
import { gasUtils } from '@/utils';
import { greaterThan } from '@/__swaps__/utils/numbers';
import { gweiToWei, parseGasFeeParam } from '@/parsers';
import { GasSpeed } from '@/__swaps__/types/gas';

export enum CUSTOM_GAS_FIELDS {
MAX_BASE_FEE = 'maxBaseFee',
PRIORITY_FEE = 'priorityFee',
}

const { CUSTOM } = gasUtils;

/**
* TODO: Work left to do for custom gas
* 1. Need to add in currentBaseFee for current network
* 2. Show current gas trend on custom gas panel UI somewhere
* 3. Allow user to type in both fields (make both animated textinput component)
* - will have to figure out how to handle prompting keyboard and dismissing
* 4. Handle long press on both fields
* 5. Handle showing warnings (overpaying, might fail, etc.)
*/

export function useCustomGas() {
const { selectedGasFee, currentBlockParams, updateToCustomGasFee, updateGasFeeOption, gasFeeParamsBySpeed } = useGas();

const currentBaseFee = useSharedValue<string>(currentBlockParams?.baseFeePerGas?.gwei || 'Unknown');
const maxBaseFee = useSharedValue<string>(currentBlockParams?.baseFeePerGas?.amount || '1');
const priorityFee = useSharedValue<string>('1');

const maxTransactionFee = useDerivedValue(() => {
const gasPrice = gasFeeParamsBySpeed?.[GasSpeed.CUSTOM]?.maxBaseFee.gwei || '';
if (gasPrice.trim() === '') return 'Unknown';
return gasPrice;
import { ChainId } from '@/__swaps__/types/chains';
import { createRainbowStore } from '@/state/internal/createRainbowStore';

export type EIP1159GasSettings = {
isEIP1559: true;
maxBaseFee: string;
maxPriorityFee: string;
};

export type LegacyGasSettings = {
isEIP1559: false;
gasPrice: string;
};

export type GasSettings = EIP1159GasSettings | LegacyGasSettings;

export type CustomGasStoreState = { [c in ChainId]?: GasSettings };
export const useCustomGasStore = createRainbowStore<CustomGasStoreState>(() => ({}));

export const useCustomGasSettings = (chainId: ChainId) => useCustomGasStore(s => s[chainId]);
export const getCustomGasSettings = (chainId: ChainId) => useCustomGasStore.getState()[chainId];

export const setCustomGasSettings = (chainId: ChainId, update: Partial<GasSettings>) => {
useCustomGasStore.setState(s => {
const state = s[chainId] || {
isEIP1559: !('gasPrice' in update && !!update.gasPrice),
maxBaseFee: '0',
maxPriorityFee: '0',
gasPrice: '0',
};
return { [chainId]: { ...state, ...update } as GasSettings };
});
};

useAnimatedReaction(
() => currentBlockParams?.baseFeePerGas?.gwei,
current => {
currentBaseFee.value = current;
}
);

const updateCustomFieldValue = useCallback(
(field: CUSTOM_GAS_FIELDS, value: string) => {
switch (field) {
case CUSTOM_GAS_FIELDS.MAX_BASE_FEE: {
const maxBaseFee = parseGasFeeParam(gweiToWei(value || 0));
const newGasParams = {
...selectedGasFee.gasFeeParams,
maxBaseFee,
};
updateToCustomGasFee(newGasParams);
break;
}

case CUSTOM_GAS_FIELDS.PRIORITY_FEE: {
const priorityFee = parseGasFeeParam(gweiToWei(value || 0));
const newGasParams = {
...selectedGasFee.gasFeeParams,
maxPriorityFeePerGas: priorityFee,
};
updateToCustomGasFee(newGasParams);
break;
}
}
},
[selectedGasFee.gasFeeParams, updateToCustomGasFee]
);

const onUpdateField = useCallback(
(field: CUSTOM_GAS_FIELDS, operation: 'increment' | 'decrement', step = 1) => {
'worklet';

switch (field) {
case CUSTOM_GAS_FIELDS.MAX_BASE_FEE: {
const text = maxBaseFee.value;

if (operation === 'decrement' && greaterThan(1, Number(text) - step)) {
maxBaseFee.value = String(1);
runOnJS(updateCustomFieldValue)(CUSTOM_GAS_FIELDS.MAX_BASE_FEE, String(1));
return;
}

const maxBaseFeeNumber = Number(text);
maxBaseFee.value = operation === 'increment' ? String(maxBaseFeeNumber + step) : String(maxBaseFeeNumber - step);
runOnJS(updateCustomFieldValue)(CUSTOM_GAS_FIELDS.MAX_BASE_FEE, maxBaseFee.value);
break;
}

case CUSTOM_GAS_FIELDS.PRIORITY_FEE: {
const text = priorityFee.value;

if (operation === 'decrement' && greaterThan(1, Number(text) - step)) {
priorityFee.value = String(1);
runOnJS(updateCustomFieldValue)(CUSTOM_GAS_FIELDS.PRIORITY_FEE, String(1));
return;
}

const priorityFeeNumber = Number(text);
priorityFee.value = operation === 'increment' ? String(priorityFeeNumber + step) : String(priorityFeeNumber - step);
runOnJS(updateCustomFieldValue)(CUSTOM_GAS_FIELDS.PRIORITY_FEE, priorityFee.value);
break;
}
}
},
[maxBaseFee, priorityFee, updateCustomFieldValue]
);

const updateCustomGas = ({ priorityFee, baseFee }: { priorityFee: string; baseFee: string }) => {
updateGasFeeOption(CUSTOM);
const maxPriorityFeePerGas = parseGasFeeParam(gweiToWei(priorityFee || 0));
const maxBaseFee = parseGasFeeParam(gweiToWei(baseFee || 0));

updateToCustomGasFee({
...selectedGasFee.gasFeeParams,
maxPriorityFeePerGas,
maxBaseFee,
});
};

const onSaveCustomGas = () => {
'worklet';

runOnJS(updateCustomGas)({
priorityFee: priorityFee.value,
baseFee: maxBaseFee.value,
});
};

return {
currentBaseFee,
maxBaseFee,
priorityFee,
maxTransactionFee,
onUpdateField,
onSaveCustomGas,
};
}
export const setCustomMaxBaseFee = (chainId: ChainId, maxBaseFee = '0') => setCustomGasSettings(chainId, { maxBaseFee });
export const setCustomMaxPriorityFee = (chainId: ChainId, maxPriorityFee = '0') => setCustomGasSettings(chainId, { maxPriorityFee });
export const setCustomGasPrice = (chainId: ChainId, gasPrice = '0') => setCustomGasSettings(chainId, { gasPrice });
47 changes: 47 additions & 0 deletions src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ChainId } from '@/__swaps__/types/chains';
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 { formatCurrency, formatNumber } from './formatNumber';
import { GasSettings } from './useCustomGas';
import { useSwapEstimatedGasLimit } from './useSwapEstimatedGasLimit';

export function useEstimatedGasFee({
chainId,
gasLimit,
gasSettings,
}: {
chainId: ChainId;
gasLimit: string | undefined;
gasSettings: GasSettings | undefined;
}) {
const network = ethereumUtils.getNetworkFromChainId(chainId);
const nativeNetworkAsset = useNativeAssetForNetwork(network);

if (!gasLimit || !gasSettings || !nativeNetworkAsset) return 'Loading...'; // TODO: loading state

const amount = gasSettings.isEIP1559 ? add(gasSettings.maxBaseFee, gasSettings.maxPriorityFee) : gasSettings.gasPrice;

const totalWei = multiply(gasLimit, amount);
const nativePrice = nativeNetworkAsset.price.value?.toString();

if (!nativePrice) return `${formatNumber(weiToGwei(totalWei))} Gwei`;

const gasAmount = formatUnits(BigInt(totalWei), nativeNetworkAsset.decimals).toString();
const feeInUserCurrency = multiply(nativePrice, gasAmount);

return formatCurrency(feeInUserCurrency);
}

export function useSwapEstimatedGasFee(gasSettings: GasSettings | undefined) {
const chainId = useSwapsStore(s => s.inputAsset?.chainId || ChainId.mainnet);

const assetToSell = useSwapsStore(s => s.inputAsset);
const quote = useSwapsStore(s => s.quote);

const { data: gasLimit } = useSwapEstimatedGasLimit({ chainId, quote, assetToSell }, { enabled: !!quote });

return useEstimatedGasFee({ chainId, gasLimit, gasSettings });
}
Loading
Loading