diff --git a/src/__swaps__/screens/Swap/components/SwapActionButton.tsx b/src/__swaps__/screens/Swap/components/SwapActionButton.tsx index 3eac5683f65..2556d5fc8b5 100644 --- a/src/__swaps__/screens/Swap/components/SwapActionButton.tsx +++ b/src/__swaps__/screens/Swap/components/SwapActionButton.tsx @@ -3,10 +3,10 @@ import React from 'react'; import { StyleProp, StyleSheet, TextStyle, ViewStyle } from 'react-native'; import Animated, { DerivedValue, useAnimatedStyle, useDerivedValue } from 'react-native-reanimated'; -import { ButtonPressAnimation } from '@/components/animations'; import { AnimatedText, Box, Column, Columns, globalColors, useColorMode, useForegroundColor } from '@/design-system'; import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets'; import { getColorValueForThemeWorklet } from '@/__swaps__/utils/swaps'; +import { GestureHandlerV1Button } from './GestureHandlerV1Button'; export const SwapActionButton = ({ asset, @@ -16,7 +16,6 @@ export const SwapActionButton = ({ icon, iconStyle, label, - onLongPress, onPress, outline, rightIcon, @@ -31,7 +30,6 @@ export const SwapActionButton = ({ icon?: string | DerivedValue; iconStyle?: StyleProp; label: string | DerivedValue; - onLongPress?: () => void; onPress?: () => void; outline?: boolean; rightIcon?: string; @@ -98,9 +96,8 @@ export const SwapActionButton = ({ }); return ( - - + ); }; diff --git a/src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx b/src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx index 2170757a807..587d27f292c 100644 --- a/src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx +++ b/src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import Animated, { runOnUI, useAnimatedStyle, withSpring } from 'react-native-reanimated'; +import Animated, { 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'; @@ -74,7 +74,7 @@ export function SwapBottomPanel() { runOnUI(SwapNavigation.handleSwapAction)()} + onPress={SwapNavigation.handleSwapAction} asset={internalSelectedOutputAsset} icon={confirmButtonIcon} iconStyle={confirmButtonIconStyle} diff --git a/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx b/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx index 6ca074c82a6..8e2c16acb57 100644 --- a/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx +++ b/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx @@ -34,7 +34,7 @@ function SwapInputActionButton() { disableShadow={isDarkMode} hugContent label={label} - onPress={runOnUI(SwapNavigation.handleInputPress)} + onPress={SwapNavigation.handleInputPress} rightIcon={'􀆏'} small /> diff --git a/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx b/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx index 8b3ebf05028..6ce449c4d0f 100644 --- a/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx +++ b/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx @@ -34,7 +34,7 @@ function SwapOutputActionButton() { disableShadow={isDarkMode} hugContent label={label} - onPress={runOnUI(SwapNavigation.handleOutputPress)} + onPress={SwapNavigation.handleOutputPress} rightIcon={'􀆏'} small /> diff --git a/src/__swaps__/screens/Swap/hooks/useSelectedGas.ts b/src/__swaps__/screens/Swap/hooks/useSelectedGas.ts index 0b61b0c1047..1e8cfe3ec3d 100644 --- a/src/__swaps__/screens/Swap/hooks/useSelectedGas.ts +++ b/src/__swaps__/screens/Swap/hooks/useSelectedGas.ts @@ -36,6 +36,14 @@ export function useSelectedGas(chainId: ChainId) { return useGasSettings(chainId, selectedGasSpeed); } +export function getGasSettingsBySpeed(chainId: ChainId) { + const suggestions = getCachedGasSuggestions(chainId); + return { + ...suggestions, + custom: getCustomGasSettings(chainId), + }; +} + export function getGasSettings(speed: GasSpeed, chainId: ChainId) { if (speed === 'custom') return getCustomGasSettings(chainId); return getCachedGasSuggestions(chainId)?.[speed]; diff --git a/src/__swaps__/screens/Swap/hooks/useSwapNavigation.ts b/src/__swaps__/screens/Swap/hooks/useSwapNavigation.ts index 567f71737ad..135f2c0987d 100644 --- a/src/__swaps__/screens/Swap/hooks/useSwapNavigation.ts +++ b/src/__swaps__/screens/Swap/hooks/useSwapNavigation.ts @@ -16,11 +16,13 @@ export function useSwapNavigation({ inputProgress, outputProgress, configProgress, + executeSwap, }: { SwapInputController: ReturnType; inputProgress: SharedValue; outputProgress: SharedValue; configProgress: SharedValue; + executeSwap: () => void; }) { const navigateBackToReview = useSharedValue(false); @@ -147,12 +149,12 @@ export function useSwapNavigation({ handleDismissGas(); } } else if (configProgress.value === NavigationSteps.SHOW_REVIEW) { - // TODO: Handle executing swap - handleDismissReview(); + // TODO: Handle long press + executeSwap(); } else { handleShowReview(); } - }, [configProgress, handleDismissGas, handleDismissReview, handleShowReview, navigateBackToReview]); + }, [configProgress.value, executeSwap, handleDismissGas, handleShowReview, navigateBackToReview]); return { navigateBackToReview, diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index d54ddc4f660..453a36d543b 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -1,33 +1,57 @@ // @refresh -import { INITIAL_SLIDER_POSITION, SLIDER_COLLAPSED_HEIGHT, SLIDER_HEIGHT, SLIDER_WIDTH } from '@/__swaps__/screens/Swap/constants'; -import { useAnimatedSwapStyles } from '@/__swaps__/screens/Swap/hooks/useAnimatedSwapStyles'; -import { useSwapInputsController } from '@/__swaps__/screens/Swap/hooks/useSwapInputsController'; -import { NavigationSteps, useSwapNavigation } from '@/__swaps__/screens/Swap/hooks/useSwapNavigation'; -import { useSwapTextStyles } from '@/__swaps__/screens/Swap/hooks/useSwapTextStyles'; -import { useSwapWarning } from '@/__swaps__/screens/Swap/hooks/useSwapWarning'; -import { ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '@/__swaps__/types/assets'; -import { ChainId } from '@/__swaps__/types/chains'; -import { SwapAssetType, inputKeys } from '@/__swaps__/types/swap'; -import { isSameAsset } from '@/__swaps__/utils/assets'; -import { parseAssetAndExtend } from '@/__swaps__/utils/swaps'; -import { logger } from '@/logger'; -import { swapsStore } from '@/state/swaps/swapsStore'; -import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; -import React, { ReactNode, createContext, useContext, useEffect } from 'react'; -import { StyleProp, TextInput, TextStyle } from 'react-native'; +import React, { createContext, useContext, ReactNode, useEffect } from 'react'; +import { StyleProp, TextStyle, TextInput, NativeModules } from 'react-native'; import { AnimatedRef, SharedValue, + runOnJS, runOnUI, useAnimatedRef, useAnimatedStyle, useDerivedValue, useSharedValue, } from 'react-native-reanimated'; -import { useSwapSettings } from '../hooks/useSwapSettings'; + +import * as i18n from '@/languages'; +import { SwapAssetType, inputKeys } from '@/__swaps__/types/swap'; +import { INITIAL_SLIDER_POSITION, SLIDER_COLLAPSED_HEIGHT, SLIDER_HEIGHT, SLIDER_WIDTH } from '@/__swaps__/screens/Swap/constants'; +import { useAnimatedSwapStyles } from '@/__swaps__/screens/Swap/hooks/useAnimatedSwapStyles'; +import { useSwapTextStyles } from '@/__swaps__/screens/Swap/hooks/useSwapTextStyles'; +import { useSwapNavigation, NavigationSteps } from '@/__swaps__/screens/Swap/hooks/useSwapNavigation'; +import { useSwapInputsController } from '@/__swaps__/screens/Swap/hooks/useSwapInputsController'; +import { ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '@/__swaps__/types/assets'; +import { useSwapWarning } from '@/__swaps__/screens/Swap/hooks/useSwapWarning'; +import { useSwapSettings } from '@/__swaps__/screens/Swap/hooks/useSwapSettings'; +import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; +import { swapsStore } from '@/state/swaps/swapsStore'; +import { isSameAsset } from '@/__swaps__/utils/assets'; +import { parseAssetAndExtend } from '@/__swaps__/utils/swaps'; +import { ChainId } from '@/__swaps__/types/chains'; +import { RainbowError, logger } from '@/logger'; +import { QuoteTypeMap, RapSwapActionParameters } from '@/raps/references'; +import { Navigation } from '@/navigation'; +import { WrappedAlert as Alert } from '@/helpers/alert'; +import Routes from '@/navigation/routesNames'; +import { ethereumUtils } from '@/utils'; +import { getCachedProviderForNetwork, isHardHat } from '@/handlers/web3'; +import { loadWallet } from '@/model/wallet'; +import { walletExecuteRap } from '@/raps/execute'; +import { queryClient } from '@/react-query'; +import { userAssetsQueryKey } from '@/resources/assets/UserAssetsQuery'; +import { useAccountSettings } from '@/hooks'; +import { getGasSettingsBySpeed, getSelectedGas } from '../hooks/useSelectedGas'; +import { LegacyTransactionGasParamAmounts, TransactionGasParamAmounts } from '@/entities'; + +const swapping = i18n.t(i18n.l.swap.actions.swapping); +const tapToSwap = i18n.t(i18n.l.swap.actions.tap_to_swap); +const save = i18n.t(i18n.l.swap.actions.save); +const enterAmount = i18n.t(i18n.l.swap.actions.enter_amount); +const review = i18n.t(i18n.l.swap.actions.review); +const fetchingPrices = i18n.t(i18n.l.swap.actions.fetching_prices); interface SwapContextType { isFetching: SharedValue; + isSwapping: SharedValue; isQuoteStale: SharedValue; searchInputRef: AnimatedRef; @@ -50,6 +74,7 @@ interface SwapContextType { setAsset: ({ type, asset }: { type: SwapAssetType; asset: ParsedSearchAsset }) => void; quote: SharedValue; + executeSwap: () => void; SwapSettings: ReturnType; SwapInputController: ReturnType; @@ -70,7 +95,10 @@ interface SwapProviderProps { } export const SwapProvider = ({ children }: SwapProviderProps) => { + const { nativeCurrency } = useAccountSettings(); + const isFetching = useSharedValue(false); + const isSwapping = useSharedValue(false); const isQuoteStale = useSharedValue(0); const searchInputRef = useAnimatedRef(); @@ -109,6 +137,127 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { quote, }); + const getNonceAndPerformSwap = async ({ + type, + parameters, + }: { + type: 'swap' | 'crosschainSwap'; + parameters: Omit, 'gasParams' | 'gasFeeParamsBySpeed' | 'selectedGasFee'>; + }) => { + const NotificationManager = ios ? NativeModules.NotificationManager : null; + NotificationManager?.postNotification('rapInProgress'); + + const resetSwappingStatus = () => { + 'worklet'; + isSwapping.value = false; + }; + + const network = ethereumUtils.getNetworkFromChainId(parameters.chainId); + const provider = getCachedProviderForNetwork(network); + const providerUrl = provider?.connection?.url; + const connectedToHardhat = isHardHat(providerUrl); + + const wallet = await loadWallet(parameters.quote.from, false, provider); + if (!wallet) { + runOnUI(resetSwappingStatus)(); + Alert.alert(i18n.t(i18n.l.swap.unable_to_load_wallet)); + return; + } + + const selectedGas = getSelectedGas(parameters.chainId); + if (!selectedGas) { + runOnUI(resetSwappingStatus)(); + // TODO: Show alert or something but this should never happen technically + Alert.alert(i18n.t(i18n.l.swap.unable_to_load_wallet)); + return; + } + + const gasFeeParamsBySpeed = getGasSettingsBySpeed(parameters.chainId); + + let gasParams: TransactionGasParamAmounts | LegacyTransactionGasParamAmounts = {} as + | TransactionGasParamAmounts + | LegacyTransactionGasParamAmounts; + + if (selectedGas.isEIP1559) { + gasParams = { + maxFeePerGas: selectedGas.maxBaseFee, + maxPriorityFeePerGas: selectedGas.maxPriorityFee, + }; + } else { + gasParams = { + gasPrice: selectedGas.gasPrice, + }; + } + + const { errorMessage } = await walletExecuteRap(wallet, type, { + ...parameters, + gasParams, + gasFeeParamsBySpeed: gasFeeParamsBySpeed as any, + }); + runOnUI(resetSwappingStatus)(); + + if (errorMessage) { + SwapInputController.quoteFetchingInterval.start(); + + if (errorMessage !== 'handled') { + logger.error(new RainbowError(`[getNonceAndPerformSwap]: Error executing swap: ${errorMessage}`)); + const extractedError = errorMessage.split('[')[0]; + Alert.alert(i18n.t(i18n.l.swap.error_executing_swap), extractedError); + return; + } + } + + queryClient.invalidateQueries({ + queryKey: userAssetsQueryKey({ + address: parameters.quote.from, + currency: nativeCurrency, + connectedToHardhat, + }), + }); + + // TODO: Analytics + NotificationManager?.postNotification('rapCompleted'); + Navigation.handleAction(Routes.PROFILE_SCREEN, {}); + }; + + const executeSwap = () => { + 'worklet'; + + // TODO: Analytics + if (configProgress.value !== NavigationSteps.SHOW_REVIEW) return; + + const inputAsset = internalSelectedInputAsset.value; + const outputAsset = internalSelectedOutputAsset.value; + const q = quote.value; + + // TODO: What other checks do we need here? + if (!inputAsset || !outputAsset || !q || (q as QuoteError)?.error) { + return; + } + + isSwapping.value = true; + SwapInputController.quoteFetchingInterval.stop(); + + const type = inputAsset.chainId !== outputAsset.chainId ? 'crosschainSwap' : 'swap'; + const quoteData = q as QuoteTypeMap[typeof type]; + const flashbots = (SwapSettings.flashbots.value && inputAsset.chainId === ChainId.mainnet) ?? false; + + const parameters: Omit, 'gasParams' | 'gasFeeParamsBySpeed' | 'selectedGasFee'> = { + sellAmount: quoteData.sellAmount?.toString(), + buyAmount: quoteData.buyAmount?.toString(), + chainId: inputAsset.chainId, + assetToSell: inputAsset, + assetToBuy: outputAsset, + quote: quoteData, + flashbots, + }; + + runOnJS(getNonceAndPerformSwap)({ + type, + parameters, + }); + }; + const SwapTextStyles = useSwapTextStyles({ inputMethod: SwapInputController.inputMethod, inputValues: SwapInputController.inputValues, @@ -127,6 +276,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { inputProgress, outputProgress, configProgress, + executeSwap, }); const SwapWarning = useSwapWarning({ @@ -245,6 +395,10 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { }; const confirmButtonIcon = useDerivedValue(() => { + if (isSwapping.value) { + return ''; + } + if (configProgress.value === NavigationSteps.SHOW_REVIEW) { return '􀎽'; } else if (configProgress.value === NavigationSteps.SHOW_GAS) { @@ -267,29 +421,34 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { } }); + // TODO: i18n these const confirmButtonLabel = useDerivedValue(() => { + if (isSwapping.value) { + return swapping; + } + if (configProgress.value === NavigationSteps.SHOW_REVIEW) { - return 'Hold to Swap'; + return tapToSwap; } else if (configProgress.value === NavigationSteps.SHOW_GAS) { - return 'Save'; + return save; } if (isFetching.value) { - return 'Fetching prices'; + return fetchingPrices; } const isInputZero = Number(SwapInputController.inputValues.value.inputAmount) === 0; const isOutputZero = Number(SwapInputController.inputValues.value.outputAmount) === 0; if (SwapInputController.inputMethod.value !== 'slider' && (isInputZero || isOutputZero) && !isFetching.value) { - return 'Enter Amount'; + return enterAmount; } else if ( SwapInputController.inputMethod.value === 'slider' && (SwapInputController.percentageToSwap.value === 0 || isInputZero || isOutputZero) ) { - return 'Enter Amount'; + return enterAmount; } else { - return 'Review'; + return review; } }); @@ -327,6 +486,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { { setAsset, quote, + executeSwap, SwapSettings, SwapInputController, diff --git a/src/components/coin-row/FastTransactionCoinRow.tsx b/src/components/coin-row/FastTransactionCoinRow.tsx index 42f07be0353..b852cd9822d 100644 --- a/src/components/coin-row/FastTransactionCoinRow.tsx +++ b/src/components/coin-row/FastTransactionCoinRow.tsx @@ -3,7 +3,7 @@ import { StyleSheet, View } from 'react-native'; import { ButtonPressAnimation } from '../animations'; import FastTransactionStatusBadge from './FastTransactionStatusBadge'; import { Bleed, Box, Inline, Text, globalColors, useForegroundColor } from '@/design-system'; -import { NativeCurrencyKey, RainbowTransaction } from '@/entities'; +import { NativeCurrencyKey, NewTransaction, RainbowTransaction } from '@/entities'; import { ThemeContextProps } from '@/theme'; import { useNavigation } from '@/navigation'; import Routes from '@rainbow-me/routes'; @@ -17,7 +17,9 @@ import { convertAmountAndPriceToNativeDisplay, convertAmountToBalanceDisplay, convertRawAmountToBalance, + convertRawAmountToDecimalFormat, greaterThan, + handleSignificantDecimals, } from '@/helpers/utilities'; import { TwoCoinsIcon } from '../coin-icon/TwoCoinsIcon'; import Spinner from '../Spinner'; @@ -45,10 +47,19 @@ const approvalTypeValues = (transaction: RainbowTransaction) => { return [transaction.protocol || '', getApprovalLabel(transaction)]; }; -const swapTypeValues = (changes: RainbowTransaction['changes']) => { +const swapTypeValues = (changes: RainbowTransaction['changes'], status: RainbowTransaction['status']) => { const tokenIn = changes?.filter(c => c?.direction === 'in')[0]; const tokenOut = changes?.filter(c => c?.direction === 'out')[0]; + // NOTE: For pending txns let's use the change values instead of + // the transaction balance change since that hasn't happened yet + if (status === 'pending') { + const valueOut = `${handleSignificantDecimals(convertRawAmountToDecimalFormat(tokenOut?.value?.toString() || '0', tokenOut?.asset.decimals || 18), tokenOut?.asset.decimals || 18)} ${tokenOut?.asset.symbol}`; + const valueIn = `+${handleSignificantDecimals(convertRawAmountToDecimalFormat(tokenIn?.value?.toString() || '0', tokenIn?.asset.decimals || 18), tokenIn?.asset.decimals || 18)} ${tokenIn?.asset.symbol}`; + + return [valueOut, valueIn]; + } + if (!tokenIn?.asset.balance?.amount || !tokenOut?.asset.balance?.amount) return; const valueOut = `${convertAmountToBalanceDisplay(tokenOut?.asset.balance?.amount, { ...tokenOut?.asset })}`; @@ -58,11 +69,11 @@ const swapTypeValues = (changes: RainbowTransaction['changes']) => { }; const activityValues = (transaction: RainbowTransaction, nativeCurrency: NativeCurrencyKey) => { - const { changes, direction, type } = transaction; - if (['swap', 'wrap', 'unwrap'].includes(type)) return swapTypeValues(changes); - if (['approve', 'revoke'].includes(type)) return approvalTypeValues(transaction); + const { changes, direction, type, status } = transaction; + if (['swap', 'wrap', 'unwrap'].includes(type)) return swapTypeValues(changes, status); + if (['approve', 'revoke'].includes(type)) return approvalTypeValues(transaction as RainbowTransaction); - const asset = changes?.filter(c => c?.direction === direction && c?.asset.type !== 'nft')[0]?.asset; + const change = changes?.filter(c => c?.direction === direction && c?.asset.type !== 'nft')[0]; let valueSymbol = direction === 'out' ? '-' : '+'; if (type === 'send') { @@ -72,14 +83,13 @@ const activityValues = (transaction: RainbowTransaction, nativeCurrency: NativeC valueSymbol = '+'; } - if (!asset) return; + if (!change) return; - const { balance } = asset; - if (balance?.amount === '0') return; + const { balance } = change.asset; - const assetValue = convertAmountToBalanceDisplay(balance?.amount || '0', asset); + const assetValue = convertAmountToBalanceDisplay(balance?.amount || '0', change.asset); - const nativeBalance = convertAmountAndPriceToNativeDisplay(balance?.amount || '0', asset?.price?.value || '0', nativeCurrency); + const nativeBalance = convertAmountAndPriceToNativeDisplay(balance?.amount || '0', change.asset.price?.value || '0', nativeCurrency); const assetNativeValue = greaterThan(nativeBalance.amount, '0') ? `${valueSymbol}${nativeBalance?.display}` : lang.t(lang.l.transactions.no_value); diff --git a/src/hooks/useAccountTransactions.ts b/src/hooks/useAccountTransactions.ts index 1fadc46434e..68ac1f9d468 100644 --- a/src/hooks/useAccountTransactions.ts +++ b/src/hooks/useAccountTransactions.ts @@ -17,12 +17,7 @@ export const NOE_PAGE = 30; export default function useAccountTransactions() { const { accountAddress, nativeCurrency } = useAccountSettings(); - const storePendingTransactions = usePendingTransactionsStore(state => state.pendingTransactions); - - const pendingTransactions = useMemo(() => { - const txs = storePendingTransactions[accountAddress] || []; - return txs; - }, [accountAddress, storePendingTransactions]); + const pendingTransactions = usePendingTransactionsStore(state => state.pendingTransactions[accountAddress] || []); const { data, fetchNextPage, hasNextPage } = useConsolidatedTransactions({ address: accountAddress, diff --git a/src/languages/en_US.json b/src/languages/en_US.json index ad5082df096..01be7ac0bb9 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -1907,9 +1907,18 @@ "too_many_signup_request": "Too many signup requests, please try again later" }, "swap": { + "actions": { + "tap_to_swap": "Tap to Swap", + "save": "Save", + "enter_amount": "Enter Amount", + "review": "Review", + "fetching_prices": "Fetching Prices", + "swapping": "Swapping" + }, "aggregators": { "rainbow": "Rainbow" }, + "error_executing_swap": "Error executing swap", "tokens_input": { "tokens": "Tokens", "sort": "Sort", @@ -1945,6 +1954,7 @@ "withdraw_symbol": "Withdraw %{symbol}" }, "unknown": "Unknown", + "unable_to_load_wallet": "Unable to load wallet", "warning": { "cost": { "are_you_sure_title": "Are you sure?", diff --git a/src/raps/actions/crosschainSwap.ts b/src/raps/actions/crosschainSwap.ts index bd559cc6a01..724d84068b7 100644 --- a/src/raps/actions/crosschainSwap.ts +++ b/src/raps/actions/crosschainSwap.ts @@ -23,6 +23,7 @@ import { import { ethereumUtils } from '@/utils'; import { TokenColors } from '@/graphql/__generated__/metadata'; import { ParsedAsset } from '@/resources/assets/types'; +import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets'; const getCrosschainSwapDefaultGasLimit = (quote: CrosschainQuote) => quote?.routes?.[0]?.userTxs?.[0]?.gasFees?.gasLimit; @@ -109,7 +110,6 @@ export const crosschainSwap = async ({ const { quote, chainId, requiresApprove } = parameters; let gasParamsToUse = gasParams; - // let gasParams = parseGasParamAmounts(selectedGasFee); if (currentRap.actions.length - 1 > index) { gasParamsToUse = overrideWithFastSpeedIfNeeded({ gasParams, @@ -156,6 +156,18 @@ export const crosschainSwap = async ({ // TODO: MARK - Replace this once we migrate network => chainId const network = ethereumUtils.getNetworkFromChainId(parameters.chainId); + const nativePriceForAssetToBuy = (parameters.assetToBuy as ExtendedAnimatedAssetWithColors)?.nativePrice + ? { + value: (parameters.assetToBuy as ExtendedAnimatedAssetWithColors)?.nativePrice, + } + : parameters.assetToBuy.price; + + const nativePriceForAssetToSell = (parameters.assetToSell as ExtendedAnimatedAssetWithColors)?.nativePrice + ? { + value: (parameters.assetToSell as ExtendedAnimatedAssetWithColors)?.nativePrice, + } + : parameters.assetToSell.price; + const transaction = { data: parameters.quote.data, value: parameters.quote.value?.toString(), @@ -163,6 +175,7 @@ export const crosschainSwap = async ({ ...parameters.assetToBuy, network: ethereumUtils.getNetworkFromChainId(parameters.assetToBuy.chainId), colors: parameters.assetToBuy.colors as TokenColors, + price: nativePriceForAssetToBuy, } as ParsedAsset, changes: [ { @@ -173,6 +186,7 @@ export const crosschainSwap = async ({ ...parameters.assetToSell, network: ethereumUtils.getNetworkFromChainId(parameters.assetToSell.chainId), colors: parameters.assetToSell.colors as TokenColors, + price: nativePriceForAssetToSell, }, value: quote.sellAmount.toString(), }, @@ -184,8 +198,9 @@ export const crosschainSwap = async ({ ...parameters.assetToBuy, network: ethereumUtils.getNetworkFromChainId(parameters.assetToBuy.chainId), colors: parameters.assetToBuy.colors as TokenColors, + price: nativePriceForAssetToBuy, }, - value: quote.buyAmount.toString(), + value: quote.buyAmountMinusFees.toString(), }, ], from: parameters.quote.from as Address, diff --git a/src/raps/actions/swap.ts b/src/raps/actions/swap.ts index b87045ed804..50d65999d43 100644 --- a/src/raps/actions/swap.ts +++ b/src/raps/actions/swap.ts @@ -6,6 +6,7 @@ import { ETH_ADDRESS as ETH_ADDRESS_AGGREGATORS, Quote, ChainId as SwapChainId, + SwapType, WRAPPED_ASSET, fillQuote, getQuoteExecutionDetails, @@ -45,6 +46,7 @@ import { populateApprove } from './unlock'; import { TokenColors } from '@/graphql/__generated__/metadata'; import { swapMetadataStorage } from '../common'; import { ParsedAsset } from '@/resources/assets/types'; +import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets'; const WRAP_GAS_PADDING = 1.002; @@ -290,7 +292,22 @@ export const swap = async ({ throw e; } - if (!swap) throw new RainbowError('swap: error executeSwap'); + if (!swap || !swap?.hash) throw new RainbowError('swap: error executeSwap'); + + // TODO: MARK - Replace this once we migrate network => chainId + const network = ethereumUtils.getNetworkFromChainId(parameters.chainId); + + const nativePriceForAssetToBuy = (parameters.assetToBuy as ExtendedAnimatedAssetWithColors)?.nativePrice + ? { + value: (parameters.assetToBuy as ExtendedAnimatedAssetWithColors)?.nativePrice, + } + : parameters.assetToBuy.price; + + const nativePriceForAssetToSell = (parameters.assetToSell as ExtendedAnimatedAssetWithColors)?.nativePrice + ? { + value: (parameters.assetToSell as ExtendedAnimatedAssetWithColors)?.nativePrice, + } + : parameters.assetToSell.price; const transaction = { data: swap.data, @@ -303,6 +320,7 @@ export const swap = async ({ ...parameters.assetToBuy, network: ethereumUtils.getNetworkFromChainId(parameters.assetToBuy.chainId), colors: parameters.assetToBuy.colors as TokenColors, + price: nativePriceForAssetToBuy, } as ParsedAsset, changes: [ { @@ -313,6 +331,7 @@ export const swap = async ({ ...parameters.assetToSell, network: ethereumUtils.getNetworkFromChainId(parameters.assetToSell.chainId), colors: parameters.assetToSell.colors as TokenColors, + price: nativePriceForAssetToSell, }, value: quote.sellAmount.toString(), }, @@ -323,11 +342,13 @@ export const swap = async ({ asset: { ...parameters.assetToBuy, network: ethereumUtils.getNetworkFromChainId(parameters.assetToBuy.chainId), - colors: parameters.assetToSell.colors as TokenColors, + colors: parameters.assetToBuy.colors as TokenColors, + price: nativePriceForAssetToBuy, }, - value: quote.buyAmount.toString(), + value: quote.buyAmountMinusFees.toString(), }, ], + gasLimit, hash: swap.hash as TxHash, // TODO: MARK - Replace this once we migrate network => chainId network: ethereumUtils.getNetworkFromChainId(parameters.chainId), @@ -335,17 +356,26 @@ export const swap = async ({ nonce: swap.nonce, status: 'pending', type: 'swap', + swap: { + type: SwapType.normal, + fromChainId: parameters.assetToSell.chainId as unknown as SwapChainId, + toChainId: parameters.assetToBuy.chainId as unknown as SwapChainId, + + // TODO: Is this right? + isBridge: + parameters.assetToBuy.chainId !== parameters.assetToSell.chainId && + parameters.assetToSell.address === parameters.assetToBuy.address, + }, flashbots: parameters.flashbots, - ...gasParams, + ...gasParamsToUse, } satisfies NewTransaction; - // TODO: MARK - Replace this once we migrate network => chainId - const network = ethereumUtils.getNetworkFromChainId(parameters.chainId); - if (parameters.meta && swap.hash) { swapMetadataStorage.set(swap.hash.toLowerCase(), JSON.stringify({ type: 'swap', data: parameters.meta })); } + console.log(JSON.stringify(transaction, null, 2)); + addNewTransaction({ address: parameters.quote.from as Address, // chainId: parameters.chainId as ChainId, diff --git a/src/screens/transaction-details/TransactionDetails.tsx b/src/screens/transaction-details/TransactionDetails.tsx index 386ac5bcce0..e81de10c8b3 100644 --- a/src/screens/transaction-details/TransactionDetails.tsx +++ b/src/screens/transaction-details/TransactionDetails.tsx @@ -26,9 +26,8 @@ export const TransactionDetails = () => { const navigation = useNavigation(); const route = useRoute>(); const { setParams } = navigation; - const { transaction: tx } = route.params; + const { transaction } = route.params; - const transaction = tx; const [sheetHeight, setSheetHeight] = useState(0); const [statusIconHidden, setStatusIconHidden] = useState(false); const { presentedToast, presentToastFor } = useTransactionDetailsToasts(); diff --git a/src/screens/transaction-details/components/TransactionDetailsStatusActionsAndTimestampSection.tsx b/src/screens/transaction-details/components/TransactionDetailsStatusActionsAndTimestampSection.tsx index 7aaf0ce0307..d3df660b79d 100644 --- a/src/screens/transaction-details/components/TransactionDetailsStatusActionsAndTimestampSection.tsx +++ b/src/screens/transaction-details/components/TransactionDetailsStatusActionsAndTimestampSection.tsx @@ -11,7 +11,7 @@ import { StyleSheet } from 'react-native'; import { ButtonPressAnimation } from '@/components/animations'; import { haptics } from '@/utils'; import Routes from '@rainbow-me/routes'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import { AppState } from '@/redux/store'; import { useNavigation } from '@react-navigation/native'; import * as i18n from '@/languages'; @@ -25,8 +25,7 @@ type Props = { export const TransactionDetailsStatusActionsAndTimestampSection: React.FC = ({ transaction, hideIcon }) => { const { minedAt, status, type, from } = transaction; - const dispatch = useDispatch(); - const { navigate, goBack } = useNavigation(); + const { navigate } = useNavigation(); const accountAddress = useSelector((state: AppState) => state.settings.accountAddress); const date = formatTransactionDetailsDate(minedAt ?? undefined); const { colors } = useTheme(); diff --git a/src/screens/transaction-details/components/TransactionDetailsValueAndFeeSection.tsx b/src/screens/transaction-details/components/TransactionDetailsValueAndFeeSection.tsx index 7f60bfe1c26..bcc68853810 100644 --- a/src/screens/transaction-details/components/TransactionDetailsValueAndFeeSection.tsx +++ b/src/screens/transaction-details/components/TransactionDetailsValueAndFeeSection.tsx @@ -31,6 +31,8 @@ export const TransactionDetailsValueAndFeeSection: React.FC = ({ transact const assetData = transaction?.asset; const change = transaction?.changes?.[0]; + const isPendingSwap = ['swap', 'wrap', 'unwrap'].includes(transaction.type) && transaction.status === 'pending'; + const value = change?.value || transaction.balance?.display; const valueDisplay = value ? convertRawAmountToBalance(value || '', assetData!).display : ''; const nativeCurrencyValue = change?.asset?.price?.value @@ -39,7 +41,7 @@ export const TransactionDetailsValueAndFeeSection: React.FC = ({ transact const feeValue = fee?.value.display ?? ''; const feeNativeCurrencyValue = fee?.native?.display ?? ''; - if (!value && !fee) return null; + if ((!value && !fee) || isPendingSwap) return null; return ( <> diff --git a/src/screens/transaction-details/components/TransactionMasthead.tsx b/src/screens/transaction-details/components/TransactionMasthead.tsx index 5d815e11dbf..3ab623b421e 100644 --- a/src/screens/transaction-details/components/TransactionMasthead.tsx +++ b/src/screens/transaction-details/components/TransactionMasthead.tsx @@ -1,7 +1,7 @@ // @refresh reset import React, { useEffect, useMemo, useState } from 'react'; -import { NativeCurrencyKey, ParsedAddressAsset, RainbowTransaction } from '@/entities'; +import { ParsedAddressAsset, RainbowTransaction } from '@/entities'; import { Bleed, Box, Columns, Cover, Row, Rows, Separator, Stack, Text, TextProps } from '@/design-system'; @@ -11,18 +11,23 @@ import { ThemeContextProps, useTheme } from '@/theme'; import { usePersistentDominantColorFromImage } from '@/hooks/usePersistentDominantColorFromImage'; import RowWithMargins from '@/components/layout/RowWithMargins'; import { IS_ANDROID } from '@/env'; -import { convertAmountAndPriceToNativeDisplay, convertAmountToBalanceDisplay } from '@/helpers/utilities'; +import { + convertAmountAndPriceToNativeDisplay, + convertAmountToBalanceDisplay, + convertRawAmountToDecimalFormat, + handleSignificantDecimals, +} from '@/helpers/utilities'; import { fetchENSAvatar } from '@/hooks/useENSAvatar'; import { fetchReverseRecord } from '@/handlers/ens'; -import { address, formatAddressForDisplay } from '@/utils/abbreviations'; +import { formatAddressForDisplay } from '@/utils/abbreviations'; import { ContactAvatar } from '@/components/contacts'; import { isLowerCaseMatch } from '@/utils'; import { useSelector } from 'react-redux'; import { AppState } from '@/redux/store'; import { useContacts, useUserAccounts } from '@/hooks'; import { useTiming } from 'react-native-redash'; -import Animated, { Easing, SharedValue, interpolate, useAnimatedStyle } from 'react-native-reanimated'; +import Animated, { Easing, interpolate, useAnimatedStyle } from 'react-native-reanimated'; import { removeFirstEmojiFromString, returnStringFirstEmoji } from '@/helpers/emojiHandler'; import { addressHashedColorIndex, addressHashedEmoji } from '@/utils/profileUtils'; import ImageAvatar from '@/components/contacts/ImageAvatar'; @@ -172,7 +177,7 @@ function CurrencyTile({ ) : ( <> - {/*add coin icon*/} + {/* add coin icon*/} state.settings.nativeCurrency); const inputAsset = useMemo(() => { - const inAsset = transaction?.changes?.find(a => a?.direction === 'in')?.asset; - if (!inAsset) return undefined; + const change = transaction?.changes?.find(a => a?.direction === 'in'); + + if (!change?.asset) return undefined; + + // NOTE: For pending transactions let's use the change value + // since the balance hasn't been updated yet. + if (['swap', 'wrap', 'unwrap'].includes(transaction.type) && transaction.status === 'pending') { + const inAssetValueDisplay = `${handleSignificantDecimals(convertRawAmountToDecimalFormat(change?.value?.toString() || '0', change?.asset.decimals || 18), change?.asset.decimals || 18)} ${change?.asset.symbol}`; + return { + inAssetValueDisplay, + inAssetNativeDisplay: change?.asset.price?.value + ? convertAmountAndPriceToNativeDisplay( + convertRawAmountToDecimalFormat(change?.value?.toString() || '0', change?.asset.decimals || 18), + change?.asset.price?.value || '0', + nativeCurrency + )?.display + : '-', + ...change.asset, + }; + } + + const inAsset = change.asset; return { inAssetValueDisplay: convertAmountToBalanceDisplay(inAsset?.balance?.amount || '0', inAsset), @@ -289,8 +314,28 @@ export default function TransactionMasthead({ transaction }: { transaction: Rain }, []); const outputAsset = useMemo(() => { - const outAsset = transaction?.changes?.find(a => a?.direction === 'out')?.asset; - if (!outAsset) return undefined; + const change = transaction?.changes?.find(a => a?.direction === 'out'); + + if (!change?.asset) return undefined; + + // NOTE: For pending transactions let's use the change value + // since the balance hasn't been updated yet. + if (['swap', 'wrap', 'unwrap'].includes(transaction.type) && transaction.status === 'pending') { + const inAssetValueDisplay = `${handleSignificantDecimals(convertRawAmountToDecimalFormat(change?.value?.toString() || '0', change?.asset.decimals || 18), change?.asset.decimals || 18)} ${change?.asset.symbol}`; + return { + inAssetValueDisplay, + inAssetNativeDisplay: change?.asset.price?.value + ? convertAmountAndPriceToNativeDisplay( + convertRawAmountToDecimalFormat(change?.value?.toString() || '0', change?.asset.decimals || 18), + change?.asset.price?.value || '0', + nativeCurrency + )?.display + : '-', + ...change?.asset, + }; + } + + const outAsset = change.asset; return { image: outAsset?.icon_url || '',