Skip to content

Commit

Permalink
custom gas warnings (#5838)
Browse files Browse the repository at this point in the history
* remove "more info" icon from max tx fee

* 🀌

* warning

* warnings & input steps

* a

* πŸ‘

---------

Co-authored-by: Matthew Wall <[email protected]>
  • Loading branch information
greg-schrammel and walmat authored Jun 18, 2024
1 parent ec157e3 commit 9d1c0c5
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 75 deletions.
192 changes: 127 additions & 65 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, ReactNode, useMemo } from 'react';
import React, { PropsWithChildren, ReactNode, useCallback, useMemo } from 'react';
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated';

import { fadeConfig } from '@/__swaps__/screens/Swap/constants';
Expand All @@ -8,13 +8,16 @@ import { ChainId } from '@/__swaps__/types/chains';
import { GasSpeed } from '@/__swaps__/types/gas';
import { gweiToWei, weiToGwei } from '@/__swaps__/utils/ethereum';
import {
GasSuggestion,
getCachedCurrentBaseFee,
getSelectedSpeedSuggestion,
useBaseFee,
useGasTrend,
useIsChainEIP1559,
useMeteorologySuggestion,
useMeteorologySuggestions,
} from '@/__swaps__/utils/meteorology';
import { add, formatNumber, subtract } from '@/__swaps__/utils/numbers';
import { add, formatNumber, greaterThan, multiply, subtract } from '@/__swaps__/utils/numbers';
import { opacity } from '@/__swaps__/utils/swaps';
import { ButtonPressAnimation } from '@/components/animations';
import { Bleed, Box, Inline, Separator, Stack, Text, globalColors, useColorMode, useForegroundColor } from '@/design-system';
Expand All @@ -23,25 +26,18 @@ import { lessThan } from '@/helpers/utilities';
import { useNavigation } from '@/navigation';
import Routes from '@/navigation/routesNames';
import { createRainbowStore } from '@/state/internal/createRainbowStore';
import { useSwapsStore } from '@/state/swaps/swapsStore';
import { swapsStore, useSwapsStore } from '@/state/swaps/swapsStore';
import { gasUtils } from '@/utils';
import { upperFirst } from 'lodash';
import { GasSettings, getCustomGasSettings, setCustomGasSettings, useCustomGasStore } from '../hooks/useCustomGas';
import { setSelectedGasSpeed, useSelectedGasSpeed } from '../hooks/useSelectedGas';
import { getSelectedGas, setSelectedGasSpeed, useSelectedGasSpeed } from '../hooks/useSelectedGas';
import { EstimatedSwapGasFee, EstimatedSwapGasFeeSlot } from './EstimatedSwapGasFee';
import { UnmountOnAnimatedReaction } from './UnmountOnAnimatedReaction';

const { GAS_TRENDS } = gasUtils;

const MINER_TIP_TYPE = 'minerTip';
const MAX_BASE_FEE_TYPE = 'maxBaseFee';
const HIGH_ALERT = 'HIGH_ALERT';
const LOW_ALERT = 'LOW_ALERT';

type AlertInfo = {
type: typeof LOW_ALERT | typeof HIGH_ALERT;
message: string;
} | null;

function UnmountWhenGasPanelIsClosed({ placeholder, children }: PropsWithChildren<{ placeholder: ReactNode }>) {
const { configProgress } = useSwapContext();
Expand Down Expand Up @@ -94,15 +90,16 @@ function NumericInputButton({ children, onPress }: PropsWithChildren<{ onPress:
style={{
justifyContent: 'center',
alignItems: 'center',
paddingTop: 1,
paddingLeft: 1,
borderWidth: 1,
borderColor: isDarkMode ? globalColors.white10 : globalColors.grey100,
borderColor: isDarkMode ? globalColors.white10 : globalColors.grey20,
backgroundColor: opacity(fillSecondary, 0.12),
}}
height={{ custom: 16 }}
width={{ custom: 20 }}
borderRadius={100}
paddingVertical="1px (Deprecated)"
gap={10}
>
<Text weight="black" size="icon 10px" color={{ custom: opacity(labelTertiary, 0.56) }}>
{children}
Expand All @@ -112,7 +109,17 @@ function NumericInputButton({ children, onPress }: PropsWithChildren<{ onPress:
);
}

const INPUT_STEP = gweiToWei('0.1');
const minStep = gweiToWei('0.0001');
const getStep = () => {
const chainId = useSwapsStore.getState().inputAsset?.chainId;
if (!chainId) return minStep;

const baseFee = getCachedCurrentBaseFee(chainId);
if (!baseFee) return minStep;

const step = 10 ** (baseFee.length - 2);
return step;
};
function GasSettingInput({
onChange,
min = '0',
Expand All @@ -129,18 +136,26 @@ function GasSettingInput({
<Inline wrap={false} horizontalSpace="8px" alignVertical="center">
<NumericInputButton
onPress={() => {
const newValue = subtract(value, INPUT_STEP);
onChange(lessThan(newValue, min) ? min : newValue);
const step = getStep();
const newValue = subtract(value, step);
onChange(lessThan(newValue, min) || lessThan(newValue, minStep) ? min : newValue);
}}
>
τ€…½
</NumericInputButton>

<Text size="15pt" weight="bold" color="labelSecondary">
<Text size="15pt" weight="bold" color="labelSecondary" tabularNumbers>
{formatNumber(weiToGwei(value))}
</Text>

<NumericInputButton onPress={() => onChange(add(value, INPUT_STEP))}>τ€…Ό</NumericInputButton>
<NumericInputButton
onPress={() => {
const step = getStep();
onChange(add(value, step));
}}
>
τ€…Ό
</NumericInputButton>
</Inline>

<Text align="right" color={isDarkMode ? 'labelSecondary' : 'label'} size="15pt" weight="heavy">
Expand All @@ -150,7 +165,7 @@ function GasSettingInput({
);
}

const selectWeiToGwei = (s: string | undefined) => s && weiToGwei(s);
const selectBaseFee = (s: string | undefined = '0') => formatNumber(weiToGwei(s));

function CurrentBaseFeeSlot({ baseFee, gasTrend = 'notrend' }: { baseFee?: string; gasTrend?: keyof typeof GAS_TRENDS }) {
const { isDarkMode } = useColorMode();
Expand All @@ -159,18 +174,12 @@ function CurrentBaseFeeSlot({ baseFee, gasTrend = 'notrend' }: { baseFee?: strin
const label = useForegroundColor('label');
const labelSecondary = useForegroundColor('labelSecondary');

const chainId = useSwapsStore(s => s.inputAsset?.chainId || ChainId.mainnet);
const trendType = 'currentBaseFee' + upperFirst(gasTrend);

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

const onPressLabel = () => {
if (!baseFee || !gasTrend) return;
navigate(Routes.EXPLAIN_SHEET, {
currentBaseFee: baseFee,
currentGasTrend: gasTrend,
type: trendType,
type: 'currentBaseFee' + upperFirst(gasTrend),
});
};

Expand All @@ -197,7 +206,7 @@ function CurrentBaseFeeSlot({ baseFee, gasTrend = 'notrend' }: { baseFee?: strin
weight="heavy"
style={{ textTransform: 'capitalize' }}
>
{formatNumber(baseFee || '0')}
{baseFee}
</Text>
</Stack>
</Bleed>
Expand All @@ -207,7 +216,7 @@ function CurrentBaseFeeSlot({ baseFee, gasTrend = 'notrend' }: { baseFee?: strin

function CurrentBaseFee() {
const chainId = useSwapsStore(s => s.inputAsset?.chainId || ChainId.mainnet);
const { data: baseFee } = useBaseFee({ chainId, select: selectWeiToGwei });
const { data: baseFee } = useBaseFee({ chainId, select: selectBaseFee });
const { data: gasTrend } = useGasTrend({ chainId });
return <CurrentBaseFeeSlot baseFee={baseFee} gasTrend={gasTrend} />;
}
Expand All @@ -216,25 +225,15 @@ type GasPanelState = { gasPrice?: string; maxBaseFee?: string; maxPriorityFee?:
const useGasPanelStore = createRainbowStore<GasPanelState | undefined>(() => undefined);

function useGasPanelState<
Key extends 'maxBaseFee' | 'maxPriorityFee' | 'gasPrice' | undefined = undefined,
Selected = Key extends string ? string : GasPanelState,
>(key?: Key, select: (s: GasPanelState | undefined) => Selected = s => (key ? s?.[key] : s) as Selected) {
Option extends 'maxBaseFee' | 'maxPriorityFee' | 'gasPrice' | undefined = undefined,
Selected = Option extends string ? string : GasPanelState,
>(opt?: Option, select: (s: GasPanelState | undefined) => Selected = s => (opt ? s?.[opt] : s) as Selected) {
const state = useGasPanelStore(select);

const chainId = useSwapsStore(s => s.inputAsset?.chainId || ChainId.mainnet);

const currentGasSettings = useCustomGasStore(s => select(s?.[chainId]));

const speed = useSelectedGasSpeed(chainId);
const { data: suggestion } = useMeteorologySuggestion({
chainId,
speed,
select,
enabled: !!state,
notifyOnChangeProps: !!state && speed !== 'custom' ? ['data'] : [],
});

return useMemo(() => state ?? currentGasSettings ?? suggestion, [currentGasSettings, state, suggestion]);
return useMemo(() => state ?? currentGasSettings, [currentGasSettings, state]);
}

const setGasPanelState = (update: Partial<GasPanelState>) => {
Expand All @@ -247,58 +246,120 @@ const setGasPanelState = (update: Partial<GasPanelState>) => {
useGasPanelStore.setState({ ...suggestion, ...update });
};

function useMetereologySuggested<Option extends 'maxBaseFee' | 'maxPriorityFee' | 'gasPrice'>(
option: Option,
{ enabled = true }: { enabled?: boolean } = {}
) {
const chainId = useSwapsStore(s => s.inputAsset?.chainId || ChainId.mainnet);
const speed = useSelectedGasSpeed(chainId);
const { data: suggestion } = useMeteorologySuggestion({
chainId,
speed,
select: useCallback((d?: GasSuggestion) => d?.[option], [option]),
enabled,
notifyOnChangeProps: !!enabled && speed !== 'custom' ? ['data'] : [],
});
return suggestion;
}

const likely_to_fail = i18n.t(i18n.l.gas.likely_to_fail);
const higher_than_suggested = i18n.t(i18n.l.gas.higher_than_suggested);
const lower_than_suggested = i18n.t(i18n.l.gas.lower_than_suggested);

const useMaxBaseFeeWarning = (maxBaseFee: string | undefined) => {
const chainId = useSwapsStore(s => s.inputAsset?.chainId || ChainId.mainnet);
const { data: suggestions } = useMeteorologySuggestions({ chainId, enabled: !!maxBaseFee });
const { data: currentBaseFee = '0' } = useBaseFee({ chainId });

if (!maxBaseFee) return null;

// likely to get stuck if less than 20% of current base fee
if (lessThan(maxBaseFee, multiply(currentBaseFee, 0.2))) return likely_to_fail;

// suggestions
const { urgent, normal } = suggestions || {};
const highThreshold = urgent?.maxBaseFee && multiply(urgent.maxBaseFee, 1.1);
const lowThreshold = normal?.maxBaseFee && multiply(normal.maxBaseFee, 0.9);
if (highThreshold && greaterThan(maxBaseFee, highThreshold)) return higher_than_suggested;
if (lowThreshold && lessThan(maxBaseFee, lowThreshold)) return lower_than_suggested;

return null;
};

function Warning({ children }: { children: string }) {
const [prefix, description] = children.split('Β·');

return (
<Text color="orange" size="13pt" weight="medium">
{prefix.trim()}
{' Β· '}
<Text color="labelQuaternary" size="13pt" weight="medium">
{description.trim()}
</Text>
</Text>
);
}

function EditMaxBaseFee() {
const maxBaseFee = useGasPanelState('maxBaseFee');
const { navigate } = useNavigation();

const maxBaseFee = useGasPanelState('maxBaseFee');
const placeholder = useMetereologySuggested('maxBaseFee', { enabled: !maxBaseFee });

const warning = useMaxBaseFeeWarning(maxBaseFee);

return (
<Inline horizontalSpace="10px" alignVertical="center" alignHorizontal="justify">
{/* TODO: Add error and warning values here */}
<PressableLabel onPress={() => navigate(Routes.EXPLAIN_SHEET, { type: MAX_BASE_FEE_TYPE })}>
{i18n.t(i18n.l.gas.max_base_fee)}
</PressableLabel>
<GasSettingInput value={maxBaseFee} onChange={maxBaseFee => setGasPanelState({ maxBaseFee })} />
</Inline>
<Box flexDirection="row" alignItems="center" justifyContent="space-between">
<Box gap={8} style={{ marginTop: warning ? -10 : 0, marginBottom: warning ? -10 : 0 }}>
<PressableLabel onPress={() => navigate(Routes.EXPLAIN_SHEET, { type: MAX_BASE_FEE_TYPE })}>
{i18n.t(i18n.l.gas.max_base_fee)}
</PressableLabel>
{warning && <Warning>{warning}</Warning>}
</Box>
<GasSettingInput value={maxBaseFee || placeholder} onChange={maxBaseFee => setGasPanelState({ maxBaseFee })} />
</Box>
);
}

const MIN_FLASHBOTS_PRIORITY_FEE = gweiToWei('6');
function EditPriorityFee() {
const maxPriorityFee = useGasPanelState('maxPriorityFee');
const { navigate } = useNavigation();

const isFlashbotsEnabled = useSwapsStore(s => s.flashbots);
// TODO: THIS FLASHBOTS INPUT LOGIC IS FLAWED REVIEW LATER
const min = isFlashbotsEnabled ? MIN_FLASHBOTS_PRIORITY_FEE : '0';

const maxPriorityFee = useGasPanelState('maxPriorityFee');
const placeholder = useMetereologySuggested('maxPriorityFee', { enabled: !maxPriorityFee });

return (
<Inline horizontalSpace="10px" alignVertical="center" alignHorizontal="justify">
{/* TODO: Add error and warning values here */}
<PressableLabel onPress={() => navigate(Routes.EXPLAIN_SHEET, { type: MINER_TIP_TYPE })}>
{i18n.t(i18n.l.gas.miner_tip)}
</PressableLabel>
<GasSettingInput value={maxPriorityFee} onChange={maxPriorityFee => setGasPanelState({ maxPriorityFee })} min={min} />
<GasSettingInput value={maxPriorityFee || placeholder} onChange={maxPriorityFee => setGasPanelState({ maxPriorityFee })} min={min} />
</Inline>
);
}

function EditGasPrice() {
const gasPrice = useGasPanelState('gasPrice');
const { navigate } = useNavigation();

const gasPrice = useGasPanelState('gasPrice');
const placeholder = useMetereologySuggested('gasPrice', { enabled: !gasPrice });

return (
<Inline horizontalSpace="10px" alignVertical="center" alignHorizontal="justify">
{/* TODO: Add error and warning values here */}
<PressableLabel onPress={() => navigate(Routes.EXPLAIN_SHEET, { type: MAX_BASE_FEE_TYPE })}>
{i18n.t(i18n.l.gas.max_base_fee)}
</PressableLabel>
<GasSettingInput value={gasPrice} onChange={gasPrice => setGasPanelState({ gasPrice })} />
<GasSettingInput value={gasPrice || placeholder} onChange={gasPrice => setGasPanelState({ gasPrice })} />
</Inline>
);
}

const stateToGasSettings = (s: GasPanelState | undefined): GasSettings | undefined => {
if (!s) return;
if (!s) return getSelectedGas(swapsStore.getState().inputAsset?.chainId || ChainId.mainnet);
if (s.gasPrice) return { isEIP1559: false, gasPrice: s.gasPrice || '0' };
return { isEIP1559: true, maxBaseFee: s.maxBaseFee || '0', maxPriorityFee: s.maxPriorityFee || '0' };
};
Expand All @@ -311,13 +372,9 @@ function MaxTransactionFee() {

return (
<Inline horizontalSpace="10px" alignVertical="center" alignHorizontal="justify">
<Inline horizontalSpace="12px">
<Inline horizontalSpace="4px">
<Text color="labelTertiary" weight="semibold" size="15pt">
{i18n.t(i18n.l.gas.max_transaction_fee)}
</Text>
</Inline>
</Inline>
<Text color="labelTertiary" weight="semibold" size="15pt">
{i18n.t(i18n.l.gas.max_transaction_fee)}
</Text>

<Inline horizontalSpace="6px">
<UnmountWhenGasPanelIsClosed
Expand All @@ -338,12 +395,15 @@ function MaxTransactionFee() {
);
}

const chainsThatIgnoreThePriorityFee = [ChainId.arbitrum, ChainId.arbitrumNova, ChainId.arbitrumSepolia];
function EditableGasSettings() {
const chainId = useSwapsStore(s => s.inputAsset?.chainId || ChainId.mainnet);
const isEIP1559 = useIsChainEIP1559(chainId);

if (!isEIP1559) return <EditGasPrice />;

if (chainsThatIgnoreThePriorityFee.includes(chainId)) return <EditMaxBaseFee />;

return (
<>
<EditMaxBaseFee />
Expand Down Expand Up @@ -395,7 +455,9 @@ export function GasPanel() {
<CurrentBaseFee />
</UnmountWhenGasPanelIsClosed>

<EditableGasSettings />
<Box gap={24} height="64px">
<EditableGasSettings />
</Box>

<Separator color="separatorSecondary" />

Expand Down
Loading

0 comments on commit 9d1c0c5

Please sign in to comment.