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

Swaps performance improvements, logic fixes #6050

Merged
merged 24 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9d6fc28
Clean up swap coin icons, fix flashes
christianbaroni Aug 27, 2024
1e47176
Fix useAnimatedReaction dependencies in swaps
christianbaroni Aug 27, 2024
156dc6d
Show disabled paste button when clipboard is empty
christianbaroni Aug 27, 2024
e708ef0
Make hasClipboardData react to clipboard changes
christianbaroni Aug 27, 2024
5ae9e14
Remove redundant Flashbots disabling
christianbaroni Aug 27, 2024
cdc7b0f
Expose runOnUIImmediately
christianbaroni Aug 27, 2024
50cbbf3
Remove redundant resetting of hasEnoughFundsForGas
christianbaroni Aug 27, 2024
150cfe4
Clean up swap confirm button logic to reduce jank
christianbaroni Aug 27, 2024
3d68519
Stop auto-fetching quotes for certain error cases
christianbaroni Aug 27, 2024
436e792
Use runOnUIImmediately for time-sensitive interactions
christianbaroni Aug 27, 2024
5b16cb3
Reduce hold to swap time from 500ms to 400ms
christianbaroni Aug 27, 2024
1d4037d
Fix max button edge cases
christianbaroni Aug 27, 2024
8257b00
Migrate token lists from FlashList to FlatList
christianbaroni Aug 27, 2024
4db9dee
Reduce jank when the input asset's chain changes
christianbaroni Aug 27, 2024
7d8b52a
Improve the inputValues useAnimatedReaction equality check
christianbaroni Aug 27, 2024
bd982fe
Fix outputAmount zero value handling
christianbaroni Aug 27, 2024
7b42b0d
Add missing hold_to_bridge i18n string
christianbaroni Aug 27, 2024
7581858
Move gas calculations to the UI thread
christianbaroni Aug 27, 2024
25cf20c
Add missing export
christianbaroni Aug 27, 2024
f97e138
Fix userAssets RegExp bug
christianbaroni Aug 27, 2024
a585e7a
Revert "Expose runOnUIImmediately"
christianbaroni Aug 28, 2024
4c93906
Revert "Use runOnUIImmediately for time-sensitive interactions"
christianbaroni Aug 28, 2024
934b4a5
Merge branch 'develop' into @christian/swaps-improvements
christianbaroni Aug 29, 2024
6c3dd8d
Merge branch 'develop' into @christian/swaps-improvements
christianbaroni Aug 29, 2024
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
3 changes: 2 additions & 1 deletion src/__swaps__/screens/Swap/Swap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ const WalletAddressObserver = () => {
if (didWalletAddressChange) {
runOnJS(setNewInputAsset)();
}
}
},
[]
);

return null;
Expand Down
66 changes: 18 additions & 48 deletions src/__swaps__/screens/Swap/components/AnimatedChainImage.ios.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,14 @@ import AvalancheBadge from '@/assets/badges/avalanche.png';
import BlastBadge from '@/assets/badges/blast.png';
import DegenBadge from '@/assets/badges/degen.png';
import { ChainId } from '@/networks/types';
import { useAnimatedProps } from 'react-native-reanimated';
import { AddressOrEth } from '@/__swaps__/types/assets';
import { useAnimatedProps, useDerivedValue } from 'react-native-reanimated';
import { AnimatedFasterImage } from '@/components/AnimatedComponents/AnimatedFasterImage';
import { DEFAULT_FASTER_IMAGE_CONFIG } from '@/components/images/ImgixImage';
import { globalColors } from '@/design-system';
import { customChainIdsToAssetNames } from '@/__swaps__/utils/chains';
import { AddressZero } from '@ethersproject/constants';
import { ETH_ADDRESS } from '@/references';
import { IS_ANDROID } from '@/env';
import { PIXEL_RATIO } from '@/utils/deviceUtils';
import { useSwapContext } from '../providers/swap-provider';
import { BLANK_BASE64_PIXEL } from '@/components/DappBrowser/constants';

const networkBadges = {
[ChainId.mainnet]: Image.resolveAssetSource(EthereumBadge).uri,
Expand All @@ -47,19 +44,6 @@ const networkBadges = {
[ChainId.degen]: Image.resolveAssetSource(DegenBadge).uri,
};

const getCustomChainIconUrlWorklet = (chainId: ChainId, address: AddressOrEth) => {
'worklet';

if (!chainId || !customChainIdsToAssetNames[chainId]) return '';
const baseUrl = 'https://raw.githubusercontent.com/rainbow-me/assets/master/blockchains/';

if (address === AddressZero || address === ETH_ADDRESS) {
return `${baseUrl}${customChainIdsToAssetNames[chainId]}/info/logo.png`;
} else {
return `${baseUrl}${customChainIdsToAssetNames[chainId]}/assets/${address}/logo.png`;
}
};

export function AnimatedChainImage({
assetType,
showMainnetBadge = false,
Expand All @@ -70,42 +54,28 @@ export function AnimatedChainImage({
size?: number;
}) {
const { internalSelectedInputAsset, internalSelectedOutputAsset } = useSwapContext();
const asset = assetType === 'input' ? internalSelectedInputAsset : internalSelectedOutputAsset;

const animatedIconSource = useAnimatedProps(() => {
const base = {
source: {
...DEFAULT_FASTER_IMAGE_CONFIG,
borderRadius: IS_ANDROID ? (size / 2) * PIXEL_RATIO : size / 2,
url: '',
},
};
if (!asset?.value) {
if (!showMainnetBadge) {
return base;
}

base.source.url = networkBadges[ChainId.mainnet];
return base;
}
const url = useDerivedValue(() => {
const asset = assetType === 'input' ? internalSelectedInputAsset : internalSelectedOutputAsset;
const chainId = asset?.value?.chainId;

if (networkBadges[asset.value.chainId]) {
if (!showMainnetBadge && asset.value.chainId === ChainId.mainnet) {
return base;
}
base.source.url = networkBadges[asset.value.chainId];
return base;
}
let url = 'eth';

const url = getCustomChainIconUrlWorklet(asset.value.chainId, asset.value.address);
if (url) {
base.source.url = url;
return base;
if (chainId !== undefined && !(!showMainnetBadge && chainId === ChainId.mainnet)) {
url = networkBadges[chainId];
}

return base;
return url;
});

const animatedIconSource = useAnimatedProps(() => ({
source: {
...DEFAULT_FASTER_IMAGE_CONFIG,
base64Placeholder: BLANK_BASE64_PIXEL,
borderRadius: IS_ANDROID ? (size / 2) * PIXEL_RATIO : size / 2,
url: url.value,
},
}));

return (
<View style={[sx.badge, { borderRadius: size / 2, height: size, width: size }]}>
{/* ⚠️ TODO: This works but we should figure out how to type this correctly to avoid this error */}
Expand Down
69 changes: 27 additions & 42 deletions src/__swaps__/screens/Swap/components/AnimatedSwapCoinIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,69 +41,54 @@ export const AnimatedSwapCoinIcon = memo(function FeedCoinIcon({
showBadge?: boolean;
}) {
const { isDarkMode, colors } = useTheme();

const { internalSelectedInputAsset, internalSelectedOutputAsset } = useSwapContext();

const asset = assetType === 'input' ? internalSelectedInputAsset : internalSelectedOutputAsset;
const size = small ? 16 : large ? 36 : 32;

const didErrorForUniqueId = useSharedValue<string | undefined>(undefined);

const size = small ? 16 : large ? 36 : 32;

// Shield animated props from unnecessary updates to avoid flicker
const coinIconUrl = useDerivedValue(() => asset.value?.icon_url ?? '');
const coinIconUrl = useDerivedValue(() => asset.value?.icon_url || '');

const animatedIconSource = useAnimatedProps(() => {
return {
source: {
...DEFAULT_FASTER_IMAGE_CONFIG,
borderRadius: IS_ANDROID ? (size / 2) * PIXEL_RATIO : undefined,
transitionDuration: 0,
url: coinIconUrl.value,
},
};
});

const animatedCoinIconWrapperStyles = useAnimatedStyle(() => {
const showEmptyState = !asset.value?.uniqueId;
const showFallback = didErrorForUniqueId.value === asset.value?.uniqueId;
const shouldDisplay = !showFallback && !showEmptyState;

return {
shadowColor: shouldDisplay ? (isDarkMode ? colors.shadow : asset.value?.shadowColor['light']) : 'transparent',
};
});

const animatedCoinIconStyles = useAnimatedStyle(() => {
const showEmptyState = !asset.value?.uniqueId;
const showFallback = didErrorForUniqueId.value === asset.value?.uniqueId;
const shouldDisplay = !showFallback && !showEmptyState;

return {
display: shouldDisplay ? 'flex' : 'none',
pointerEvents: shouldDisplay ? 'auto' : 'none',
opacity: withTiming(shouldDisplay ? 1 : 0, fadeConfig),
};
});

const animatedEmptyStateStyles = useAnimatedStyle(() => {
const visibility = useDerivedValue(() => {
const showEmptyState = !asset.value?.uniqueId;
const showFallback = !showEmptyState && (didErrorForUniqueId.value === asset.value?.uniqueId || !asset.value?.icon_url);
const showCoinIcon = !showFallback && !showEmptyState;

return {
display: showEmptyState ? 'flex' : 'none',
opacity: withTiming(showEmptyState ? 1 : 0, fadeConfig),
};
return { showCoinIcon, showEmptyState, showFallback };
});

const animatedFallbackStyles = useAnimatedStyle(() => {
const showEmptyState = !asset.value?.uniqueId;
const showFallback = !showEmptyState && didErrorForUniqueId.value === asset.value?.uniqueId;

return {
display: showFallback ? 'flex' : 'none',
pointerEvents: showFallback ? 'auto' : 'none',
opacity: withTiming(showFallback ? 1 : 0, fadeConfig),
};
});
const animatedCoinIconWrapperStyles = useAnimatedStyle(() => ({
shadowColor: visibility.value.showCoinIcon ? (isDarkMode ? colors.shadow : asset.value?.shadowColor['light']) : 'transparent',
}));

const animatedCoinIconStyles = useAnimatedStyle(() => ({
display: visibility.value.showCoinIcon ? 'flex' : 'none',
pointerEvents: visibility.value.showCoinIcon ? 'auto' : 'none',
opacity: withTiming(visibility.value.showCoinIcon ? 1 : 0, fadeConfig),
}));

const animatedEmptyStateStyles = useAnimatedStyle(() => ({
display: visibility.value.showEmptyState ? 'flex' : 'none',
opacity: withTiming(visibility.value.showEmptyState ? 1 : 0, fadeConfig),
}));

const animatedFallbackStyles = useAnimatedStyle(() => ({
display: visibility.value.showFallback ? 'flex' : 'none',
pointerEvents: visibility.value.showFallback ? 'auto' : 'none',
opacity: withTiming(visibility.value.showFallback ? 1 : 0, fadeConfig),
}));

return (
<View style={small ? sx.containerSmall : large ? sx.containerLarge : sx.container}>
Expand Down
3 changes: 2 additions & 1 deletion src/__swaps__/screens/Swap/components/ExchangeRateBubble.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ export const ExchangeRateBubble = () => {
break;
}
}
}
},
[]
);

const bubbleVisibilityWrapper = useAnimatedStyle(() => {
Expand Down
3 changes: 2 additions & 1 deletion src/__swaps__/screens/Swap/components/GasPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,8 @@ export function GasPanel() {
if (previous === NavigationSteps.SHOW_GAS && current !== NavigationSteps.SHOW_GAS) {
runOnJS(saveCustomGasSettings)();
}
}
},
[]
);

const styles = useAnimatedStyle(() => {
Expand Down
18 changes: 4 additions & 14 deletions src/__swaps__/screens/Swap/components/ReviewPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { AnimatedChainImage } from '@/__swaps__/screens/Swap/components/AnimatedChainImage';
import { ReviewGasButton } from '@/__swaps__/screens/Swap/components/GasButton';
import { GestureHandlerButton } from '@/__swaps__/screens/Swap/components/GestureHandlerButton';
import { useNativeAssetForChain } from '@/__swaps__/screens/Swap/hooks/useNativeAssetForChain';
import { ChainNameDisplay, ChainId } from '@/networks/types';
import { useEstimatedTime } from '@/__swaps__/utils/meteorology';
import {
Expand All @@ -27,11 +26,9 @@ import {
useColorMode,
useForegroundColor,
} from '@/design-system';
import { useAccountSettings } from '@/hooks';
import * as i18n from '@/languages';
import { useNavigation } from '@/navigation';
import Routes from '@/navigation/routesNames';
import { getNetworkObject } from '@/networks';
import { swapsStore, useSwapsStore } from '@/state/swaps/swapsStore';
import { getNativeAssetForNetwork } from '@/utils/ethereumUtils';
import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps';
Expand Down Expand Up @@ -64,11 +61,8 @@ const MAX_SLIPPAGE_LABEL = i18n.t(i18n.l.exchange.slippage_tolerance);
const ESTIMATED_NETWORK_FEE_LABEL = i18n.t(i18n.l.gas.network_fee);

const RainbowFee = () => {
const { nativeCurrency } = useAccountSettings();
const { isDarkMode } = useColorMode();
const { isFetching, isQuoteStale, quote, internalSelectedInputAsset } = useSwapContext();

const { nativeAsset } = useNativeAssetForChain({ inputAsset: internalSelectedInputAsset });
const { isFetching, isQuoteStale, quote } = useSwapContext();

const index = useSharedValue(0);
const rainbowFee = useSharedValue<string[]>([UNKNOWN_LABEL, UNKNOWN_LABEL]);
Expand Down Expand Up @@ -104,7 +98,8 @@ const RainbowFee = () => {
if (!current.isQuoteStale && !current.isFetching && current.quote && !(current.quote as QuoteError)?.error) {
runOnJS(calculateRainbowFeeFromQuoteData)(current.quote as Quote | CrosschainQuote);
}
}
},
[]
);

return (
Expand Down Expand Up @@ -139,15 +134,10 @@ function EstimatedArrivalTime() {
function FlashbotsToggle() {
const { SwapSettings } = useSwapContext();

const inputAssetChainId = swapsStore(state => state.inputAsset?.chainId) ?? ChainId.mainnet;
const isFlashbotsEnabledForNetwork = getNetworkObject({ chainId: inputAssetChainId }).features.flashbots;
const flashbotsToggleValue = useDerivedValue(() => isFlashbotsEnabledForNetwork && SwapSettings.flashbots.value);

return (
<AnimatedSwitch
onToggle={SwapSettings.onToggleFlashbots}
disabled={!isFlashbotsEnabledForNetwork}
value={flashbotsToggleValue}
value={SwapSettings.flashbots}
activeLabel={i18n.t(i18n.l.expanded_state.swap.on)}
inactiveLabel={i18n.t(i18n.l.expanded_state.swap.off)}
/>
Expand Down
3 changes: 2 additions & 1 deletion src/__swaps__/screens/Swap/components/SearchInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ export const SearchInput = ({
if (output) runOnJS(onOutputSearchQueryChange)('');
else runOnJS(onInputSearchQueryChange)('');
}
}
},
[]
);

return (
Expand Down
55 changes: 35 additions & 20 deletions src/__swaps__/screens/Swap/components/SearchInputButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as i18n from '@/languages';
import { THICK_BORDER_WIDTH } from '../constants';
import { useClipboard } from '@/hooks';
import { TIMING_CONFIGS } from '@/components/animations/animationConfigs';
import { triggerHapticFeedback } from '@/screens/points/constants';

const CANCEL_LABEL = i18n.t(i18n.l.button.cancel);
const CLOSE_LABEL = i18n.t(i18n.l.button.close);
Expand Down Expand Up @@ -51,31 +52,45 @@ export const SearchInputButton = ({
return PASTE_LABEL;
});

const onPaste = useCallback(() => {
Clipboard.getString().then(text => {
// to prevent users from mistakingly pasting long ass texts when copying the wrong thing
// we slice the string to 42 which is the size of a eth address,
// no token name query search should be that big anyway
const v = text.trim().slice(0, 42);
pastedSearchInputValue.value = v;
useSwapsStore.setState({ outputSearchQuery: v });
});
}, []);
const onPaste = useCallback(
(isPasteDisabled: boolean) => {
if (isPasteDisabled) {
triggerHapticFeedback('notificationError');
return;
}

const buttonVisibilityStyle = useAnimatedStyle(() => {
Clipboard.getString().then(text => {
// Slice the pasted text to the length of an ETH address
const v = text.trim().slice(0, 42);
pastedSearchInputValue.value = v;
useSwapsStore.setState({ outputSearchQuery: v });
});
},
[pastedSearchInputValue]
);

const buttonInfo = useDerivedValue(() => {
const isInputSearchFocused = inputProgress.value === NavigationSteps.SEARCH_FOCUSED;
const isOutputSearchFocused = outputProgress.value === NavigationSteps.SEARCH_FOCUSED;
const isOutputTokenListFocused = outputProgress.value === NavigationSteps.TOKEN_LIST_FOCUSED;

const isVisible =
isInputSearchFocused ||
isOutputSearchFocused ||
(output && (internalSelectedOutputAsset.value || hasClipboardData)) ||
(!output && internalSelectedInputAsset.value);
const isVisible = isInputSearchFocused || isOutputSearchFocused || output || (!output && !!internalSelectedInputAsset.value);

const isPasteDisabled = output && !internalSelectedOutputAsset.value && isOutputTokenListFocused && !hasClipboardData;
const visibleOpacity = isPasteDisabled ? 0.4 : 1;

return {
isPasteDisabled,
isVisible,
visibleOpacity,
};
});

const buttonVisibilityStyle = useAnimatedStyle(() => {
return {
display: isVisible ? 'flex' : 'none',
opacity: isVisible ? withTiming(1, TIMING_CONFIGS.slowerFadeConfig) : 0,
pointerEvents: isVisible ? 'auto' : 'none',
display: buttonInfo.value.isVisible ? 'flex' : 'none',
opacity: buttonInfo.value.isVisible ? withTiming(buttonInfo.value.visibleOpacity, TIMING_CONFIGS.tabPressConfig) : 0,
pointerEvents: buttonInfo.value.isVisible ? 'auto' : 'none',
};
});

Expand All @@ -88,7 +103,7 @@ export const SearchInputButton = ({
onPressWorklet={() => {
'worklet';
if (output && outputProgress.value === NavigationSteps.TOKEN_LIST_FOCUSED && !internalSelectedOutputAsset.value) {
runOnJS(onPaste)();
runOnJS(onPaste)(buttonInfo.value.isPasteDisabled);
}

if (isSearchFocused.value || (output && internalSelectedOutputAsset.value) || (!output && internalSelectedInputAsset.value)) {
Expand Down
3 changes: 2 additions & 1 deletion src/__swaps__/screens/Swap/components/SwapActionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,8 @@ const HoldProgress = ({ holdProgress }: { holdProgress: SharedValue<number> }) =
if (current && current !== previous) {
runOnJS(transformColor)(getColorValueForThemeWorklet(current, isDarkMode, true));
}
}
},
[]
);

return (
Expand Down
Loading
Loading