From 0cb7aa8f6e2fe97022589829c324541353898f08 Mon Sep 17 00:00:00 2001 From: Apotheosis <0xapotheosis@gmail.com> Date: Fri, 20 Dec 2024 19:28:46 +1100 Subject: [PATCH] feat: incremental tweaks to new swapper (#8408) --- src/assets/translations/en/main.json | 6 +- src/components/Amount/Amount.tsx | 16 +-- .../components/TradeConfirm/EtaStep.tsx | 12 ++- .../TradeConfirm/ExpandableTradeSteps.tsx | 3 +- .../TradeConfirm/ExpandedTradeSteps.tsx | 102 +++++++++++------- .../TradeConfirm/TradeConfirmFooter.tsx | 94 +++++++++++----- .../TradeConfirmSummary.tsx | 61 +++++++++-- .../hooks/useActiveTradeAllowance.tsx | 23 +--- 8 files changed, 207 insertions(+), 110 deletions(-) diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index 189acd37dc9..10e19262a2b 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -1011,17 +1011,19 @@ }, "hopTitle": { "bridge": "Bridge via %{swapperName}", - "swap": "Swap via %{swapperName}" + "swap": "Swap via %{swapperName}", + "swapEta": "Swap via %{swapperName} in ~%{eta}" }, "transactionFailed": { "bridge": "A problem occurred executing the bridge", "swap": "A problem occurred executing the swap" }, "transactionTitle": { - "bridge": "Bridge from %{sellChainName} to %{buyChainName} via %{swapperName}", + "bridge": "Swap from %{sellChainName} to %{buyChainName} via %{swapperName}", "swap": "Swap on %{sellChainName} via %{swapperName}" }, "approvalTitle": "Token allowance approval", + "permit2Title": "Permit token transfer", "resetTitle": "Token allowance reset", "fiatAmountOnChain": "%{amountFiatFormatted} on %{chainName}", "quote": { diff --git a/src/components/Amount/Amount.tsx b/src/components/Amount/Amount.tsx index 80c7268ffa5..2fa6d424bf1 100644 --- a/src/components/Amount/Amount.tsx +++ b/src/components/Amount/Amount.tsx @@ -14,6 +14,7 @@ export type AmountProps = { abbreviated?: boolean truncateLargeNumbers?: boolean maximumFractionDigits?: number + noSpace?: boolean } & TextProps export function Amount({ @@ -23,6 +24,7 @@ export function Amount({ maximumFractionDigits, omitDecimalTrailingZeros = false, abbreviated = false, + noSpace = false, ...props }: any): React.ReactElement { const { @@ -31,9 +33,9 @@ export function Amount({ return ( - {prefix} + {prefix && `${prefix}${noSpace ? '' : ' '}`} {toString(value, { maximumFractionDigits, omitDecimalTrailingZeros, abbreviated })} - {suffix} + {suffix && `${noSpace ? '' : ' '}${suffix}`} ) } @@ -62,6 +64,7 @@ const Crypto = ({ omitDecimalTrailingZeros = false, abbreviated = false, truncateLargeNumbers = false, + noSpace = false, ...props }: CryptoAmountProps) => { const { @@ -77,9 +80,9 @@ const Crypto = ({ return ( - {prefix && `${prefix} `} + {prefix && `${prefix}${noSpace ? '' : ' '}`} {crypto} - {suffix && ` ${suffix}`} + {suffix && `${noSpace ? '' : ' '}${suffix}`} ) } @@ -92,6 +95,7 @@ const Fiat = ({ maximumFractionDigits, omitDecimalTrailingZeros = false, abbreviated = false, + noSpace = false, ...props }: FiatAmountProps) => { const { @@ -107,9 +111,9 @@ const Fiat = ({ return ( - {prefix && `${prefix} `} + {prefix && `${prefix}${noSpace ? '' : ' '}`} {fiat} - {suffix && ` ${suffix}`} + {suffix && `${noSpace ? '' : ' '}${suffix}`} ) } diff --git a/src/components/MultiHopTrade/components/TradeConfirm/EtaStep.tsx b/src/components/MultiHopTrade/components/TradeConfirm/EtaStep.tsx index 3dd0c7dbd65..35528c39ad8 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/EtaStep.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/EtaStep.tsx @@ -1,6 +1,7 @@ import { ArrowDownIcon } from '@chakra-ui/icons' import prettyMilliseconds from 'pretty-ms' import { useMemo } from 'react' +import { useTranslate } from 'react-polyglot' import { selectIsActiveQuoteMultiHop } from 'state/slices/tradeInputSlice/selectors' import { selectFirstHop, selectLastHop } from 'state/slices/tradeQuoteSlice/selectors' import { useAppSelector } from 'state/store' @@ -10,6 +11,7 @@ import { StepperStep } from '../MultiHopTradeConfirm/components/StepperStep' const etaStepProps = { alignItems: 'center', py: 2 } export const EtaStep = () => { + const translate = useTranslate() const tradeQuoteFirstHop = useAppSelector(selectFirstHop) const tradeQuoteLastHop = useAppSelector(selectLastHop) const isMultiHopTrade = useAppSelector(selectIsActiveQuoteMultiHop) @@ -21,15 +23,19 @@ export const EtaStep = () => { ? tradeQuoteFirstHop.estimatedExecutionTimeMs + tradeQuoteLastHop.estimatedExecutionTimeMs : tradeQuoteFirstHop.estimatedExecutionTimeMs }, [isMultiHopTrade, tradeQuoteFirstHop, tradeQuoteLastHop]) + const swapperName = tradeQuoteFirstHop?.source const stepIndicator = useMemo(() => { return }, []) const title = useMemo(() => { return totalEstimatedExecutionTimeMs - ? `Estimated completion ${prettyMilliseconds(totalEstimatedExecutionTimeMs)}` - : 'Estimated completion time unknown' - }, [totalEstimatedExecutionTimeMs]) + ? translate('trade.hopTitle.swapEta', { + swapperName, + eta: prettyMilliseconds(totalEstimatedExecutionTimeMs), + }) + : translate('trade.hopTitle.swap', { swapperName }) + }, [totalEstimatedExecutionTimeMs, swapperName, translate]) return ( { hopIndex: currentHopIndex ?? 0, } }, [activeTradeId, currentHopIndex]) - const swapperName = useAppSelector(selectActiveSwapperName) + const swapperName = activeTradeQuote?.steps[0].source const { state: hopExecutionState } = useSelectorWithArgs( selectHopExecutionMetadata, hopExecutionMetadataFilter, diff --git a/src/components/MultiHopTrade/components/TradeConfirm/ExpandedTradeSteps.tsx b/src/components/MultiHopTrade/components/TradeConfirm/ExpandedTradeSteps.tsx index cbd467fc7fa..30d1c5df4c7 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/ExpandedTradeSteps.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/ExpandedTradeSteps.tsx @@ -1,18 +1,27 @@ import { CheckCircleIcon, WarningIcon } from '@chakra-ui/icons' -import { Flex, HStack, Stepper, StepStatus, Tag, VStack } from '@chakra-ui/react' +import { + Box, + Flex, + HStack, + Icon, + Stepper, + StepStatus, + Tag, + Tooltip, + VStack, +} from '@chakra-ui/react' import type { TradeQuote, TradeRate } from '@shapeshiftoss/swapper' import { useMemo } from 'react' +import { FaInfoCircle } from 'react-icons/fa' import { useTranslate } from 'react-polyglot' import { RawText, Text } from 'components/Text' import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingleton' import { selectFirstHopSellAccountId, - selectIsActiveQuoteMultiHop, selectSecondHopSellAccountId, } from 'state/slices/tradeInputSlice/selectors' import { selectActiveQuoteErrors, - selectActiveSwapperName, selectHopExecutionMetadata, } from 'state/slices/tradeQuoteSlice/selectors' import { useAppSelector, useSelectorWithArgs } from 'state/store' @@ -34,14 +43,13 @@ type ExpandedTradeStepsProps = { export const ExpandedTradeSteps = ({ activeTradeQuote }: ExpandedTradeStepsProps) => { const translate = useTranslate() - const swapperName = useAppSelector(selectActiveSwapperName) // this is the account we're selling from - assume this is the AccountId of the approval Tx const firstHopSellAccountId = useAppSelector(selectFirstHopSellAccountId) const lastHopSellAccountId = useAppSelector(selectSecondHopSellAccountId) - const isMultiHopTrade = useAppSelector(selectIsActiveQuoteMultiHop) const tradeQuoteFirstHop = activeTradeQuote.steps[0] const tradeQuoteLastHop = activeTradeQuote.steps[1] const activeTradeId = activeTradeQuote.id + const swapperName = tradeQuoteFirstHop?.source const firstHopStreamingProgress = useStreamingProgress({ hopIndex: 0, @@ -160,8 +168,14 @@ export const ExpandedTradeSteps = ({ activeTradeQuote }: ExpandedTradeStepsProps return ( {firstHopPermit2.isRequired === true ? ( - // TODO: Add permit2 tooltip - + <> + + + + + + + ) : ( <> @@ -181,6 +195,7 @@ export const ExpandedTradeSteps = ({ activeTradeQuote }: ExpandedTradeStepsProps firstHopPermit2.isRequired, firstHopSellAccountId, tradeQuoteFirstHop, + translate, ]) const firstHopActionTitle = useMemo(() => { @@ -242,8 +257,14 @@ export const ExpandedTradeSteps = ({ activeTradeQuote }: ExpandedTradeStepsProps return ( {lastHopPermit2.isRequired === true ? ( - // TODO: Add permit2 tooltip - + <> + + + + + + + ) : ( <> @@ -263,6 +284,7 @@ export const ExpandedTradeSteps = ({ activeTradeQuote }: ExpandedTradeStepsProps lastHopPermit2.isRequired, lastHopSellAccountId, tradeQuoteLastHop, + translate, ]) const lastHopActionTitle = useMemo(() => { @@ -337,38 +359,36 @@ export const ExpandedTradeSteps = ({ activeTradeQuote }: ExpandedTradeStepsProps isError={activeQuoteError && currentTradeStep === TradeStep.FirstHopSwap} stepIndicatorVariant='innerSteps' /> - {isMultiHopTrade && ( - <> - {tradeSteps[TradeStep.LastHopReset] ? ( - - ) : null} - {tradeSteps[TradeStep.LastHopApproval] ? ( - - ) : null} - - - )} + {tradeSteps[TradeStep.LastHopReset] ? ( + + ) : null} + {tradeSteps[TradeStep.LastHopApproval] ? ( + + ) : null} + {tradeSteps[TradeStep.LastHopSwap] ? ( + + ) : null} ) } diff --git a/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirmFooter.tsx b/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirmFooter.tsx index 824e7360328..8676f252176 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirmFooter.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirmFooter.tsx @@ -1,4 +1,4 @@ -import { Skeleton, Stack, Switch } from '@chakra-ui/react' +import { HStack, Skeleton, Stack, Switch } from '@chakra-ui/react' import type { TradeQuoteStep } from '@shapeshiftoss/swapper' import type { FC } from 'react' import { useMemo, useState } from 'react' @@ -10,10 +10,7 @@ import { bnOrZero } from 'lib/bignumber/bignumber' import { fromBaseUnit } from 'lib/math' import { selectFeeAssetById } from 'state/slices/assetsSlice/selectors' import { selectMarketDataByAssetIdUserCurrency } from 'state/slices/marketDataSlice/selectors' -import { - selectHopNetworkFeeUserCurrency, - selectIsActiveSwapperQuoteLoading, -} from 'state/slices/tradeQuoteSlice/selectors' +import { selectIsActiveSwapperQuoteLoading } from 'state/slices/tradeQuoteSlice/selectors' import { useAppSelector, useSelectorWithArgs } from 'state/store' import { isPermit2Hop } from '../MultiHopTradeConfirm/hooks/helpers' @@ -37,19 +34,21 @@ export const TradeConfirmFooter: FC = ({ const [isExactAllowance, toggleIsExactAllowance] = useToggle(true) const [hasClickedButton, setHasClickedButton] = useState(false) const currentHopIndex = useCurrentHopIndex() - const tradeNetworkFeeFiatUserCurrency = useSelectorWithArgs(selectHopNetworkFeeUserCurrency, { - hopIndex: currentHopIndex, - }) + const networkFeeCryptoBaseUnit = tradeQuoteStep.feeData.networkFeeCryptoBaseUnit + const feeAsset = useSelectorWithArgs(selectFeeAssetById, tradeQuoteStep.sellAsset.assetId) + const networkFeeCryptoPrecision = fromBaseUnit(networkFeeCryptoBaseUnit, feeAsset?.precision ?? 0) + const feeAssetUserCurrencyRate = useSelectorWithArgs( + selectMarketDataByAssetIdUserCurrency, + feeAsset?.assetId ?? '', + ) + const networkFeeUserCurrency = bnOrZero(networkFeeCryptoPrecision) + .times(feeAssetUserCurrencyRate.price) + .toFixed() const isActiveSwapperQuoteLoading = useAppSelector(selectIsActiveSwapperQuoteLoading) const sellChainFeeAsset = useSelectorWithArgs( selectFeeAssetById, tradeQuoteStep.sellAsset.assetId, ) - const buyChainFeeAsset = useSelectorWithArgs(selectFeeAssetById, tradeQuoteStep.buyAsset.assetId) - const sellChainFeeAssetUserCurrencyRate = useSelectorWithArgs( - selectMarketDataByAssetIdUserCurrency, - sellChainFeeAsset?.assetId ?? '', - ) const { allowanceResetNetworkFeeCryptoBaseUnit, @@ -62,22 +61,22 @@ export const TradeConfirmFooter: FC = ({ activeTradeId, }) - const allowanceResetNetworkFeeCryptoHuman = fromBaseUnit( + const allowanceResetNetworkFeeCryptoPrecision = fromBaseUnit( allowanceResetNetworkFeeCryptoBaseUnit, sellChainFeeAsset?.precision ?? 0, ) - const allowanceResetNetworkFeeUserCurrency = bnOrZero(allowanceResetNetworkFeeCryptoHuman) - .times(sellChainFeeAssetUserCurrencyRate.price) + const allowanceResetNetworkFeeUserCurrency = bnOrZero(allowanceResetNetworkFeeCryptoPrecision) + .times(feeAssetUserCurrencyRate.price) .toFixed() - const approvalNetworkFeeCryptoHuman = fromBaseUnit( + const approvalNetworkFeeCryptoPrecision = fromBaseUnit( approvalNetworkFeeCryptoBaseUnit, - buyChainFeeAsset?.precision ?? 0, + sellChainFeeAsset?.precision ?? 0, ) - const approvalNetworkFeeUserCurrency = bnOrZero(approvalNetworkFeeCryptoHuman) - .times(sellChainFeeAssetUserCurrencyRate.price) + const approvalNetworkFeeUserCurrency = bnOrZero(approvalNetworkFeeCryptoPrecision) + .times(feeAssetUserCurrencyRate.price) .toFixed() const { currentTradeStep } = useTradeSteps() @@ -91,13 +90,30 @@ export const TradeConfirmFooter: FC = ({ - + + + + ) - }, [allowanceResetNetworkFeeUserCurrency, isAllowanceResetLoading]) + }, [ + allowanceResetNetworkFeeCryptoPrecision, + allowanceResetNetworkFeeUserCurrency, + isAllowanceResetLoading, + sellChainFeeAsset?.symbol, + ]) const isPermit2 = useMemo(() => { return isPermit2Hop(tradeQuoteStep) @@ -116,7 +132,19 @@ export const TradeConfirmFooter: FC = ({ - + + + + @@ -148,6 +176,8 @@ export const TradeConfirmFooter: FC = ({ ) }, [ isAllowanceApprovalLoading, + sellChainFeeAsset?.symbol, + approvalNetworkFeeCryptoPrecision, approvalNetworkFeeUserCurrency, isPermit2, isExactAllowance, @@ -164,13 +194,27 @@ export const TradeConfirmFooter: FC = ({ - + + + + ) - }, [tradeNetworkFeeFiatUserCurrency, isActiveSwapperQuoteLoading]) + }, [ + feeAsset?.symbol, + isActiveSwapperQuoteLoading, + networkFeeCryptoPrecision, + networkFeeUserCurrency, + ]) const tradeDetail = useMemo(() => { switch (currentTradeStep) { diff --git a/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirmFooterContent/TradeConfirmSummary.tsx b/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirmFooterContent/TradeConfirmSummary.tsx index a1714be73e1..4b1b6401924 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirmFooterContent/TradeConfirmSummary.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirmFooterContent/TradeConfirmSummary.tsx @@ -12,6 +12,7 @@ import { Tooltip, useColorModeValue, } from '@chakra-ui/react' +import type { AssetId } from '@shapeshiftoss/caip' import type { AmountDisplayMeta } from '@shapeshiftoss/swapper' import { bnOrZero, fromBaseUnit, isSome } from '@shapeshiftoss/utils' import { useCallback, useMemo, useState } from 'react' @@ -26,6 +27,7 @@ import { useToggle } from 'hooks/useToggle/useToggle' import { THORSWAP_MAXIMUM_YEAR_TRESHOLD, THORSWAP_UNIT_THRESHOLD } from 'lib/fees/model' import { middleEllipsis } from 'lib/utils' import { selectThorVotingPower } from 'state/apis/snapshot/selectors' +import { selectMarketDataUserCurrency } from 'state/slices/marketDataSlice/selectors' import { selectInputBuyAsset, selectInputSellAmountUsd, @@ -49,13 +51,21 @@ import { MaxSlippage } from '../../TradeInput/components/MaxSlippage' import { SwapperIcon } from '../../TradeInput/components/SwapperIcon/SwapperIcon' import { useTradeReceiveAddress } from '../../TradeInput/hooks/useTradeReceiveAddress' -const parseAmountDisplayMeta = (items: AmountDisplayMeta[]) => { +type ProtocolFee = { + assetId: AssetId | undefined + chainName: string | undefined + amountCryptoPrecision: string + symbol: string +} + +const parseAmountDisplayMeta = (items: AmountDisplayMeta[]): ProtocolFee[] => { return items .filter(({ amountCryptoBaseUnit }) => bnOrZero(amountCryptoBaseUnit).gt(0)) .map(({ amountCryptoBaseUnit, asset }: AmountDisplayMeta) => ({ - symbol: asset.symbol, + assetId: asset.assetId, chainName: getChainAdapterManager().get(asset.chainId)?.getDisplayName(), amountCryptoPrecision: fromBaseUnit(amountCryptoBaseUnit, asset.precision), + symbol: asset.symbol, })) } @@ -104,12 +114,12 @@ export const TradeConfirmSummary = () => { const affiliateFeeAfterDiscountUserCurrency = useAppSelector( selectTradeQuoteAffiliateFeeAfterDiscountUserCurrency, ) + const marketDataUserCurrency = useAppSelector(selectMarketDataUserCurrency) const totalNetworkFeeFiatUserCurrency = useAppSelector(selectTotalNetworkFeeUserCurrency) const tradeQuoteFirstHop = useAppSelector(selectFirstHop) const translate = useTranslate() const { priceImpactPercentage } = usePriceImpact(activeQuote) const { isLoading } = useIsApprovalInitiallyNeeded() - const redColor = useColorModeValue('red.500', 'red.300') const greenColor = useColorModeValue('green.600', 'green.200') const { manualReceiveAddress, walletReceiveAddress } = useTradeReceiveAddress() const [showFeeModal, setShowFeeModal] = useState(false) @@ -126,7 +136,24 @@ export const TradeConfirmSummary = () => { const hasIntermediaryTransactionOutputs = intermediaryTransactionOutputsParsed && intermediaryTransactionOutputsParsed.length > 0 const protocolFeesParsed = totalProtocolFees - ? parseAmountDisplayMeta(Object.values(totalProtocolFees).filter(isSome)) + ? Object.values( + parseAmountDisplayMeta(Object.values(totalProtocolFees).filter(isSome)).reduce( + (acc, fee) => { + const key = `${fee.assetId}-${fee.chainName}` + if (acc[key]) { + // If we already have this symbol+chain combination, add the amounts + acc[key].amountCryptoPrecision = bnOrZero(acc[key].amountCryptoPrecision) + .plus(fee.amountCryptoPrecision) + .toString() + } else { + // First time seeing this symbol+chain combination + acc[key] = { ...fee } + } + return acc + }, + {} as Record, + ), + ) : undefined const hasProtocolFees = protocolFeesParsed && protocolFeesParsed.length > 0 @@ -191,13 +218,25 @@ export const TradeConfirmSummary = () => { - {protocolFeesParsed?.map(({ amountCryptoPrecision, symbol }) => ( - + {protocolFeesParsed?.map(({ amountCryptoPrecision, assetId, symbol }) => ( + + + {assetId && ( + + )} + ))} diff --git a/src/components/MultiHopTrade/components/TradeConfirm/hooks/useActiveTradeAllowance.tsx b/src/components/MultiHopTrade/components/TradeConfirm/hooks/useActiveTradeAllowance.tsx index 7bbc4d92830..ee1dd188934 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/hooks/useActiveTradeAllowance.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/hooks/useActiveTradeAllowance.tsx @@ -1,5 +1,5 @@ import type { TradeQuoteStep } from '@shapeshiftoss/swapper' -import { useCallback, useMemo } from 'react' +import { useMemo } from 'react' import { useGetTradeQuotes } from 'components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes' import { AllowanceType } from 'hooks/queries/useApprovalFees' import { selectHopExecutionMetadata } from 'state/slices/tradeQuoteSlice/selectors' @@ -22,7 +22,6 @@ export const useActiveTradeAllowance = ({ isExactAllowance, activeTradeId, }: UseSignAllowanceApprovalProps) => { - // TODO: confirm this is actually needed in the new flow // DO NOT REMOVE ME. Fetches and upserts permit2 quotes at pre-permit2-signing time useGetTradeQuotes() @@ -91,25 +90,9 @@ export const useActiveTradeAllowance = ({ allowanceReset.isInitiallyRequired, ) - const handleSignAllowanceApproval = useCallback(async () => { - try { - await approveMutation.mutateAsync() - } catch (error) { - console.error(error) - } - }, [approveMutation]) - - const handleSignAllowanceReset = useCallback(async () => { - try { - await allowanceResetMutation.mutateAsync() - } catch (error) { - console.error(error) - } - }, [allowanceResetMutation]) - return { - handleSignAllowanceApproval, - handleSignAllowanceReset, + handleSignAllowanceApproval: approveMutation.mutate, + handleSignAllowanceReset: allowanceResetMutation.mutate, isAllowanceApprovalLoading, isAllowanceApprovalPending: approveMutation.isPending, isAllowanceResetLoading,