Skip to content

Commit

Permalink
Gas (#5757)
Browse files Browse the repository at this point in the history
* types improvements

* useSwapsStore

* number & currency formatter

* gasss

* dont keep previous data

* use chainId from input token

* hm

* decimal separator

* add gas fee to review panel

* 😕

* save

* fix (#5758)

* consolidated verified assets fetches (#5759)

* fix (#5761)

* chore: i18n updates (#5762)

* APP-1500 (#5763)

* fix

* optional chaining just in case

* allow message signing tx w/o loaded balance/gas

* aaaaaa

* uhu

* add border back to review gas button

* remove color todo tags

* gasPrice

---------

Co-authored-by: Matthew Wall <[email protected]>
Co-authored-by: Ben Goldberg <[email protected]>
Co-authored-by: Daniel Sinclair <[email protected]>
  • Loading branch information
4 people authored May 24, 2024
1 parent 1817a43 commit 341c192
Show file tree
Hide file tree
Showing 18 changed files with 1,004 additions and 778 deletions.
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

0 comments on commit 341c192

Please sign in to comment.