From 4973d964b3a3ae01e230286ce6b35dd0043f1207 Mon Sep 17 00:00:00 2001 From: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:10:52 +1100 Subject: [PATCH] wip --- .../MultiHopTradeConfirm.tsx | 34 ++++----- .../MultiHopTradeConfirm/components/Hop.tsx | 10 +-- .../MultiHopTradeConfirm/components/Hops.tsx | 12 ++-- .../components/StepperStep.tsx | 6 +- .../hooks/useIsApprovalInitiallyNeeded.tsx | 22 +++--- .../hooks/useTradeExecution.tsx | 22 +++--- .../useTradeNetworkFeeCryptoBaseUnit.tsx | 16 ++--- .../components/TradeConfirm/EtaStep.tsx | 10 +-- .../TradeConfirm/ExpandableStepperSteps.tsx | 10 +-- .../components/TradeConfirm/TradeConfirm.tsx | 32 +++++---- .../TradeConfirmSummary.tsx | 10 +-- .../TradeConfirm/TradeFooterButton.tsx | 10 +-- .../TradeConfirm/hooks/useCurrentHopIndex.tsx | 6 +- .../TradeConfirm/hooks/useStepperSteps.tsx | 4 +- .../hooks/useStreamingProgress.tsx | 11 +-- .../hooks/useTradeButtonProps.tsx | 10 +-- .../components/TradeInput/TradeInput.tsx | 19 ++++- .../components/TradeSettingsMenu.tsx | 9 ++- .../useGetTradeQuotes/useGetTradeQuotes.tsx | 7 +- src/state/slices/tradeInputSlice/selectors.ts | 61 ++++------------ src/state/slices/tradeQuoteSlice/constants.ts | 1 + src/state/slices/tradeQuoteSlice/selectors.ts | 70 +++++++++---------- .../slices/tradeQuoteSlice/tradeQuoteSlice.ts | 12 +++- src/state/slices/tradeQuoteSlice/types.ts | 9 ++- src/test/mocks/store.ts | 1 + 25 files changed, 209 insertions(+), 205 deletions(-) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx index 2d217a2dab6..e057a04b555 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx @@ -12,7 +12,7 @@ import { Text } from 'components/Text' import { bnOrZero } from 'lib/bignumber/bignumber' import { fromBaseUnit } from 'lib/math' import { - selectActiveQuote, + selectConfirmedQuote, selectConfirmedTradeExecutionState, selectLastHop, } from 'state/slices/tradeQuoteSlice/selectors' @@ -38,25 +38,27 @@ export const MultiHopTradeConfirm = memo(() => { const history = useHistory() const [shouldShowWarningAcknowledgement, setShouldShowWarningAcknowledgement] = useState(false) - const activeQuote = useAppSelector(selectActiveQuote) - const { isModeratePriceImpact, priceImpactPercentage } = usePriceImpact(activeQuote) + const confirmedQuote = useAppSelector(selectConfirmedQuote) + const { isModeratePriceImpact, priceImpactPercentage } = usePriceImpact(confirmedQuote) const lastHop = useAppSelector(selectLastHop) - const initialActiveTradeIdRef = useRef(activeQuote?.id ?? '') + const initialActiveTradeIdRef = useRef(confirmedQuote?.id ?? '') const { isLoading } = useIsApprovalInitiallyNeeded() const isArbitrumBridgeWithdraw = useMemo(() => { - return isArbitrumBridgeTradeQuoteOrRate(activeQuote) && activeQuote.direction === 'withdrawal' - }, [activeQuote]) + return ( + isArbitrumBridgeTradeQuoteOrRate(confirmedQuote) && confirmedQuote?.direction === 'withdrawal' + ) + }, [confirmedQuote]) useEffect(() => { - if (isLoading || !activeQuote) return + if (isLoading || !confirmedQuote) return // Only set the trade to initialized if it was actually initializing previously. Now that we shove quotes in at confirm time, we can't rely on this effect only running once. if (confirmedTradeExecutionState !== TradeExecutionState.Initializing) return - dispatch(tradeQuoteSlice.actions.setTradeInitialized(activeQuote.id)) - }, [dispatch, isLoading, activeQuote, confirmedTradeExecutionState]) + dispatch(tradeQuoteSlice.actions.setTradeInitialized(confirmedQuote.id)) + }, [dispatch, isLoading, confirmedQuote, confirmedTradeExecutionState]) const isTradeComplete = useMemo( () => confirmedTradeExecutionState === TradeExecutionState.TradeComplete, @@ -93,9 +95,9 @@ export const MultiHopTradeConfirm = memo(() => { ]) const handleTradeConfirm = useCallback(() => { - if (!activeQuote) return - dispatch(tradeQuoteSlice.actions.confirmTrade(activeQuote.id)) - }, [dispatch, activeQuote]) + if (!confirmedQuote) return + dispatch(tradeQuoteSlice.actions.confirmTrade(confirmedQuote.id)) + }, [dispatch, confirmedQuote]) const handleSubmit = useCallback(() => { if (isModeratePriceImpact) { @@ -139,17 +141,17 @@ export const MultiHopTradeConfirm = memo(() => { - {isTradeComplete && activeQuote && lastHop ? ( + {isTradeComplete && confirmedQuote && lastHop ? ( { - return isArbitrumBridgeTradeQuoteOrRate(activeQuote) && activeQuote.direction === 'withdrawal' - }, [activeQuote]) + return ( + isArbitrumBridgeTradeQuoteOrRate(confirmedQuote) && confirmedQuote.direction === 'withdrawal' + ) + }, [confirmedQuote]) const hopExecutionMetadataFilter = useMemo(() => { return { diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/Hops.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/Hops.tsx index de161f19f78..3827542f964 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/Hops.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/Hops.tsx @@ -1,8 +1,8 @@ import { Box, Stack } from '@chakra-ui/react' import { memo, useMemo } from 'react' import { - selectActiveQuote, selectActiveSwapperName, + selectConfirmedQuote, selectFirstHop, selectIsActiveQuoteMultiHop, selectLastHop, @@ -30,7 +30,7 @@ export const Hops = memo((props: HopsProps) => { const swapperName = useAppSelector(selectActiveSwapperName) const firstHop = useAppSelector(selectFirstHop) const lastHop = useAppSelector(selectLastHop) - const activeQuote = useAppSelector(selectActiveQuote) + const confirmedQuote = useAppSelector(selectConfirmedQuote) const isMultiHopTrade = useAppSelector(selectIsActiveQuoteMultiHop) const divider = useMemo( @@ -38,9 +38,9 @@ export const Hops = memo((props: HopsProps) => { [], ) - if (!activeQuote || !firstHop || !swapperName) return null + if (!confirmedQuote || !firstHop || !swapperName) return null - const activeTradeId = activeQuote.id + const activeTradeId = confirmedQuote.id return ( @@ -50,7 +50,7 @@ export const Hops = memo((props: HopsProps) => { hopIndex={0} isOpen={isFirstHopOpen} onToggleIsOpen={onToggleFirstHop} - slippageTolerancePercentageDecimal={activeQuote.slippageTolerancePercentageDecimal} + slippageTolerancePercentageDecimal={confirmedQuote.slippageTolerancePercentageDecimal} activeTradeId={activeTradeId} initialActiveTradeId={initialActiveTradeId} /> @@ -61,7 +61,7 @@ export const Hops = memo((props: HopsProps) => { hopIndex={1} isOpen={isSecondHopOpen} onToggleIsOpen={onToggleSecondHop} - slippageTolerancePercentageDecimal={activeQuote.slippageTolerancePercentageDecimal} + slippageTolerancePercentageDecimal={confirmedQuote.slippageTolerancePercentageDecimal} activeTradeId={activeTradeId} initialActiveTradeId={initialActiveTradeId} /> diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StepperStep.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StepperStep.tsx index 338b787bdd2..0344dc5343f 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StepperStep.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StepperStep.tsx @@ -13,7 +13,7 @@ import { } from '@chakra-ui/react' import { InlineCopyButton } from 'components/InlineCopyButton' import { MiddleEllipsis } from 'components/MiddleEllipsis/MiddleEllipsis' -import { selectActiveQuote } from 'state/slices/tradeQuoteSlice/selectors' +import { selectConfirmedQuote } from 'state/slices/tradeQuoteSlice/selectors' import { useAppSelector } from 'state/store' const stepStyle = { @@ -54,8 +54,8 @@ export type StepperStepProps = { } const LastStepTag = () => { - const activeQuote = useAppSelector(selectActiveQuote) - const receiveAddress = activeQuote?.receiveAddress + const confirmedQuote = useAppSelector(selectConfirmedQuote) + const receiveAddress = confirmedQuote?.receiveAddress if (!receiveAddress) return null diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useIsApprovalInitiallyNeeded.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useIsApprovalInitiallyNeeded.tsx index a72fc831bc7..39a3aa1588b 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useIsApprovalInitiallyNeeded.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useIsApprovalInitiallyNeeded.tsx @@ -9,7 +9,7 @@ import { selectSecondHopSellAccountId, } from 'state/slices/tradeInputSlice/selectors' import { - selectActiveQuote, + selectConfirmedQuote, selectFirstHop, selectIsActiveQuoteMultiHop, selectSecondHop, @@ -107,7 +107,7 @@ const useIsAllowanceResetInitiallyRequiredForHop = ( export const useIsApprovalInitiallyNeeded = () => { const dispatch = useAppDispatch() - const activeQuote = useAppSelector(selectActiveQuote) + const confirmedQuote = useAppSelector(selectConfirmedQuote) const firstHop = useAppSelector(selectFirstHop) const secondHop = useAppSelector(selectSecondHop) const isMultiHopTrade = useAppSelector(selectIsActiveQuoteMultiHop) @@ -117,18 +117,18 @@ export const useIsApprovalInitiallyNeeded = () => { const { isLoading: isFirstHopAllowanceApprovalRequirementsLoading, isApprovalInitiallyNeeded: isApprovalInitiallyNeededForFirstHop, - } = useIsApprovalInitiallyNeededForHop(activeQuote?.id, firstHop, firstHopSellAssetAccountId) + } = useIsApprovalInitiallyNeededForHop(confirmedQuote?.id, firstHop, firstHopSellAssetAccountId) const { isLoading: isSecondHopAllowanceApprovalRequirementsLoading, isApprovalInitiallyNeeded: isApprovalInitiallyNeededForSecondHop, - } = useIsApprovalInitiallyNeededForHop(activeQuote?.id, secondHop, secondHopSellAssetAccountId) + } = useIsApprovalInitiallyNeededForHop(confirmedQuote?.id, secondHop, secondHopSellAssetAccountId) const { isLoading: isFirstHopAllowanceResetRequirementsLoading, isAllowanceResetNeeded: isAllowanceResetNeededForFirstHop, } = useIsAllowanceResetInitiallyRequiredForHop( - activeQuote?.id, + confirmedQuote?.id, firstHop, firstHopSellAssetAccountId, ) @@ -137,7 +137,7 @@ export const useIsApprovalInitiallyNeeded = () => { isLoading: isSecondHopAllowanceResetRequirementsLoading, isAllowanceResetNeeded: isAllowanceResetNeededForSecondHop, } = useIsAllowanceResetInitiallyRequiredForHop( - activeQuote?.id, + confirmedQuote?.id, secondHop, secondHopSellAssetAccountId, ) @@ -157,11 +157,11 @@ export const useIsApprovalInitiallyNeeded = () => { useEffect(() => { if (isFirstHopLoading || (secondHop !== undefined && isSecondHopLoading)) return - if (!activeQuote?.id) return + if (!confirmedQuote?.id) return dispatch( tradeQuoteSlice.actions.setInitialApprovalRequirements({ - id: activeQuote.id, + id: confirmedQuote.id, firstHop: isApprovalInitiallyNeededForFirstHop ?? false, secondHop: isApprovalInitiallyNeededForSecondHop ?? false, }), @@ -169,7 +169,7 @@ export const useIsApprovalInitiallyNeeded = () => { dispatch( tradeQuoteSlice.actions.setAllowanceResetRequirements({ - id: activeQuote.id, + id: confirmedQuote.id, firstHop: isAllowanceResetNeededForFirstHop ?? false, secondHop: isAllowanceResetNeededForSecondHop ?? false, }), @@ -177,13 +177,13 @@ export const useIsApprovalInitiallyNeeded = () => { dispatch( tradeQuoteSlice.actions.setPermit2Requirements({ - id: activeQuote.id, + id: confirmedQuote.id, firstHop: isPermit2RequiredForFirstHop, secondHop: isPermit2RequiredForSecondHop, }), ) }, [ - activeQuote?.id, + confirmedQuote?.id, dispatch, isAllowanceResetNeededForFirstHop, isAllowanceResetNeededForSecondHop, diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useTradeExecution.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useTradeExecution.tsx index 5df4f9ef6ff..8085ae1a70c 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useTradeExecution.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useTradeExecution.tsx @@ -41,8 +41,8 @@ import { assertGetUtxoChainAdapter } from 'lib/utils/utxo' import { selectThorVotingPower } from 'state/apis/snapshot/selectors' import { selectAssetById, selectPortfolioAccountMetadataByAccountId } from 'state/slices/selectors' import { - selectActiveQuote, selectActiveSwapperName, + selectConfirmedQuote, selectHopExecutionMetadata, selectHopSellAccountId, selectTradeSlippagePercentageDecimal, @@ -101,7 +101,7 @@ export const useTradeExecution = ( selectPortfolioAccountMetadataByAccountId(state, accountMetadataFilter), ) const swapperName = useAppSelector(selectActiveSwapperName) - const tradeQuote = useAppSelector(selectActiveQuote) + const tradeQuote = useAppSelector(selectConfirmedQuote) // This is ugly, but we need to use refs to get around the fact that the // poll fn effectively creates a closure and will hold stale variables forever @@ -277,7 +277,7 @@ export const useTradeExecution = ( const output = await execution.execEvmMessage({ swapperName, - tradeQuote, + tradeQuote: tradeQuote as TradeQuote, stepIndex: hopIndex, slippageTolerancePercentageDecimal, from, @@ -307,8 +307,8 @@ export const useTradeExecution = ( const receiverAddress = stepBuyAssetAssetId === bchAssetId - ? tradeQuote.receiveAddress.replace('bitcoincash:', '') - : tradeQuote.receiveAddress + ? (tradeQuote as TradeQuote).receiveAddress?.replace('bitcoincash:', '') + : (tradeQuote as TradeQuote).receiveAddress switch (stepSellAssetChainNamespace) { case CHAIN_NAMESPACE.Evm: { @@ -318,7 +318,7 @@ export const useTradeExecution = ( const output = await execution.execEvmTransaction({ swapperName, - tradeQuote, + tradeQuote: tradeQuote as TradeQuote, stepIndex: hopIndex, slippageTolerancePercentageDecimal, from, @@ -359,7 +359,7 @@ export const useTradeExecution = ( const output = await execution.execUtxoTransaction({ swapperName, - tradeQuote, + tradeQuote: tradeQuote as TradeQuote, stepIndex: hopIndex, slippageTolerancePercentageDecimal, xpub, @@ -389,7 +389,7 @@ export const useTradeExecution = ( const from = await adapter.getAddress({ accountNumber, wallet }) const output = await execution.execCosmosSdkTransaction({ swapperName, - tradeQuote, + tradeQuote: tradeQuote as TradeQuote, stepIndex: hopIndex, slippageTolerancePercentageDecimal, from, @@ -415,7 +415,7 @@ export const useTradeExecution = ( }) const output = await adapter.broadcastTransaction({ senderAddress: from, - receiverAddress: tradeQuote.receiveAddress, + receiverAddress: (tradeQuote as TradeQuote).receiveAddress, hex: signedTx, }) @@ -431,7 +431,7 @@ export const useTradeExecution = ( const from = await adapter.getAddress({ accountNumber, wallet }) const output = await execution.execSolanaTransaction({ swapperName, - tradeQuote, + tradeQuote: tradeQuote as TradeQuote, stepIndex: hopIndex, slippageTolerancePercentageDecimal, from, @@ -442,7 +442,7 @@ export const useTradeExecution = ( }) const output = await adapter.broadcastTransaction({ senderAddress: from, - receiverAddress: tradeQuote.receiveAddress, + receiverAddress: (tradeQuote as TradeQuote).receiveAddress, hex: signedTx, }) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useTradeNetworkFeeCryptoBaseUnit.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useTradeNetworkFeeCryptoBaseUnit.tsx index 8eba3341e1f..c289272e0c6 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useTradeNetworkFeeCryptoBaseUnit.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useTradeNetworkFeeCryptoBaseUnit.tsx @@ -1,6 +1,6 @@ import { bchAssetId, CHAIN_NAMESPACE, fromChainId } from '@shapeshiftoss/caip' import { supportsETH } from '@shapeshiftoss/hdwallet-core' -import type { SupportedTradeQuoteStepIndex } from '@shapeshiftoss/swapper' +import type { SupportedTradeQuoteStepIndex, TradeQuote } from '@shapeshiftoss/swapper' import { getHopByIndex, isExecutableTradeQuote, @@ -18,8 +18,8 @@ import { assertGetSolanaChainAdapter } from 'lib/utils/solana' import { assertGetUtxoChainAdapter } from 'lib/utils/utxo' import { selectPortfolioAccountMetadataByAccountId } from 'state/slices/selectors' import { - selectActiveQuote, selectActiveSwapperName, + selectConfirmedQuote, selectConfirmedTradeExecution, selectHopExecutionMetadata, selectHopSellAccountId, @@ -57,12 +57,12 @@ export const useTradeNetworkFeeCryptoBaseUnit = ({ selectPortfolioAccountMetadataByAccountId(state, accountMetadataFilter), ) const swapperName = useAppSelector(selectActiveSwapperName) - const tradeQuote = useAppSelector(selectActiveQuote) + const tradeQuote = useAppSelector(selectConfirmedQuote) const hop = useMemo(() => getHopByIndex(tradeQuote, hopIndex), [tradeQuote, hopIndex]) const swapper = useMemo(() => (swapperName ? swappers[swapperName] : undefined), [swapperName]) - const activeTrade = useAppSelector(selectActiveQuote) + const activeTrade = useAppSelector(selectConfirmedQuote) const activeTradeId = activeTrade?.id const hopExecutionMetadataFilter = useMemo(() => { @@ -119,7 +119,7 @@ export const useTradeNetworkFeeCryptoBaseUnit = ({ const output = await swapper.getEvmTransactionFees({ chainId: hop.sellAsset.chainId, - tradeQuote, + tradeQuote: tradeQuote as TradeQuote, stepIndex: hopIndex, slippageTolerancePercentageDecimal, // permit2Signature is zrx-specific and always on the first hop @@ -148,7 +148,7 @@ export const useTradeNetworkFeeCryptoBaseUnit = ({ : _senderAddress const output = await swapper.getUtxoTransactionFees({ - tradeQuote, + tradeQuote: tradeQuote as TradeQuote, xpub, accountType, stepIndex: hopIndex, @@ -169,7 +169,7 @@ export const useTradeNetworkFeeCryptoBaseUnit = ({ const output = await swapper.getCosmosSdkTransactionFees({ stepIndex: hopIndex, - tradeQuote, + tradeQuote: tradeQuote as TradeQuote, chainId: hop.sellAsset.chainId, from, config: getConfig(), @@ -186,7 +186,7 @@ export const useTradeNetworkFeeCryptoBaseUnit = ({ const from = await adapter.getAddress({ accountNumber, wallet }) const output = await swapper.getSolanaTransactionFees({ - tradeQuote, + tradeQuote: tradeQuote as TradeQuote, from, stepIndex: hopIndex, slippageTolerancePercentageDecimal, diff --git a/src/components/MultiHopTrade/components/TradeConfirm/EtaStep.tsx b/src/components/MultiHopTrade/components/TradeConfirm/EtaStep.tsx index 12483967b89..93f74b23788 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/EtaStep.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/EtaStep.tsx @@ -2,7 +2,7 @@ import { ArrowDownIcon } from '@chakra-ui/icons' import prettyMilliseconds from 'pretty-ms' import { useMemo } from 'react' import { useTranslate } from 'react-polyglot' -import { selectActiveQuote } from 'state/slices/tradeQuoteSlice/selectors' +import { selectConfirmedQuote } from 'state/slices/tradeQuoteSlice/selectors' import { useAppSelector } from 'state/store' import { StepperStep } from '../MultiHopTradeConfirm/components/StepperStep' @@ -11,15 +11,15 @@ const etaStepProps = { py: 0 } export const EtaStep = () => { const translate = useTranslate() - const activeQuote = useAppSelector(selectActiveQuote) + const confirmedQuote = useAppSelector(selectConfirmedQuote) const totalEstimatedExecutionTimeMs = useMemo( () => - activeQuote?.steps.reduce((acc, step) => { + confirmedQuote?.steps.reduce((acc, step) => { return acc + (step.estimatedExecutionTimeMs ?? 0) }, 0), - [activeQuote?.steps], + [confirmedQuote?.steps], ) - const swapperName = activeQuote?.steps[0].source + const swapperName = confirmedQuote?.steps[0].source const stepIndicator = useMemo(() => { return diff --git a/src/components/MultiHopTrade/components/TradeConfirm/ExpandableStepperSteps.tsx b/src/components/MultiHopTrade/components/TradeConfirm/ExpandableStepperSteps.tsx index 62633213b9c..80d594cdcdd 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/ExpandableStepperSteps.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/ExpandableStepperSteps.tsx @@ -5,8 +5,8 @@ import { AnimatedCheck } from 'components/AnimatedCheck' import { CircularProgress } from 'components/CircularProgress/CircularProgress' import { Text } from 'components/Text' import { - selectActiveQuote, selectActiveQuoteErrors, + selectConfirmedQuote, selectConfirmedTradeExecutionState, selectHopExecutionMetadata, } from 'state/slices/tradeQuoteSlice/selectors' @@ -40,8 +40,8 @@ export const ExpandableStepperSteps = ({ [isExpanded], ) const currentHopIndex = useCurrentHopIndex() - const activeTradeQuote = useAppSelector(selectActiveQuote) - const activeTradeId = activeTradeQuote?.id + const confirmedQuote = useAppSelector(selectConfirmedQuote) + const activeTradeId = confirmedQuote?.id const activeQuoteErrors = useAppSelector(selectActiveQuoteErrors) const activeQuoteError = useMemo(() => activeQuoteErrors?.[0], [activeQuoteErrors]) const hopExecutionMetadataFilter = useMemo(() => { @@ -50,7 +50,7 @@ export const ExpandableStepperSteps = ({ hopIndex: currentHopIndex ?? 0, } }, [activeTradeId, currentHopIndex]) - const swapperName = activeTradeQuote?.steps[0].source + const swapperName = confirmedQuote?.steps[0].source const { state: hopExecutionState, swap: { state: swapTxState }, @@ -121,7 +121,7 @@ export const ExpandableStepperSteps = ({ /> - {activeTradeQuote && } + {confirmedQuote && } diff --git a/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirm.tsx b/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirm.tsx index cb61b255d10..c43b9f18c52 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirm.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirm.tsx @@ -6,7 +6,7 @@ import { TradeRoutePaths } from 'components/MultiHopTrade/types' import type { TextPropTypes } from 'components/Text/Text' import { fromBaseUnit } from 'lib/math' import { - selectActiveQuote, + selectConfirmedQuote, selectConfirmedTradeExecutionState, selectFirstHop, selectLastHop, @@ -27,8 +27,8 @@ export const TradeConfirm = () => { const { isLoading } = useIsApprovalInitiallyNeeded() const history = useHistory() const dispatch = useAppDispatch() - const activeQuote = useAppSelector(selectActiveQuote) - const activeTradeId = activeQuote?.id + const confirmedQuote = useAppSelector(selectConfirmedQuote) + const activeTradeId = confirmedQuote?.id const currentHopIndex = useCurrentHopIndex() const tradeQuoteFirstHop = useAppSelector(selectFirstHop) const tradeQuoteLastHop = useAppSelector(selectLastHop) @@ -61,36 +61,38 @@ export const TradeConfirm = () => { }, [confirmedTradeExecutionState]) useEffect(() => { - if (isLoading || !activeQuote) return + if (isLoading || !confirmedQuote) return // Only set the trade to initialized if it was actually initializing previously. Now that we shove quotes in at confirm time, we can't rely on this effect only running once. if (confirmedTradeExecutionState !== TradeExecutionState.Initializing) return - dispatch(tradeQuoteSlice.actions.setTradeInitialized(activeQuote.id)) - }, [dispatch, isLoading, activeQuote, confirmedTradeExecutionState]) + dispatch(tradeQuoteSlice.actions.setTradeInitialized(confirmedQuote.id)) + }, [dispatch, isLoading, confirmedQuote, confirmedTradeExecutionState]) const footer = useMemo(() => { - if (isTradeComplete && activeQuote && tradeQuoteLastHop) return null + if (isTradeComplete && confirmedQuote && tradeQuoteLastHop) return null if (!tradeQuoteStep || !activeTradeId) return null return - }, [isTradeComplete, activeQuote, tradeQuoteLastHop, tradeQuoteStep, activeTradeId]) + }, [isTradeComplete, confirmedQuote, tradeQuoteLastHop, tradeQuoteStep, activeTradeId]) const isArbitrumBridgeWithdraw = useMemo(() => { - return isArbitrumBridgeTradeQuoteOrRate(activeQuote) && activeQuote.direction === 'withdrawal' - }, [activeQuote]) + return ( + isArbitrumBridgeTradeQuoteOrRate(confirmedQuote) && confirmedQuote?.direction === 'withdrawal' + ) + }, [confirmedQuote]) const body = useMemo(() => { - if (isTradeComplete && activeQuote && tradeQuoteLastHop) + if (isTradeComplete && confirmedQuote && tradeQuoteLastHop) return ( { ) return - }, [activeQuote, handleBack, isArbitrumBridgeWithdraw, isTradeComplete, tradeQuoteLastHop]) + }, [confirmedQuote, handleBack, isArbitrumBridgeWithdraw, isTradeComplete, tradeQuoteLastHop]) if (!headerTranslation) return null diff --git a/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirmFooterContent/TradeConfirmSummary.tsx b/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirmFooterContent/TradeConfirmSummary.tsx index f8810218466..fcd1fdd71d4 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirmFooterContent/TradeConfirmSummary.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirmFooterContent/TradeConfirmSummary.tsx @@ -35,9 +35,9 @@ import { selectIsActiveQuoteMultiHop, } from 'state/slices/tradeInputSlice/selectors' import { - selectActiveQuote, selectActiveSwapperName, selectBuyAmountAfterFeesCryptoPrecision, + selectConfirmedQuote, selectFirstHop, selectFirstHopNetworkFeeCryptoBaseUnit, selectFirstHopNetworkFeeUserCurrency, @@ -89,14 +89,14 @@ const ShowMoreButton = (props: ButtonProps) => ( export const TradeConfirmSummary = () => { const swapperName = useAppSelector(selectActiveSwapperName) - const activeQuote = useAppSelector(selectActiveQuote) + const confirmedQuote = useAppSelector(selectConfirmedQuote) const buyAsset = useAppSelector(selectInputBuyAsset) const sellAsset = useAppSelector(selectInputSellAsset) const buyAmountAfterFeesCryptoPrecision = useAppSelector(selectBuyAmountAfterFeesCryptoPrecision) const slippagePercentageDecimal = useAppSelector(selectTradeSlippagePercentageDecimal) const totalProtocolFees = useAppSelector(selectTotalProtocolFeeByAsset) const firstHopFeeAsset = useSelectorWithArgs(selectFeeAssetById, sellAsset.assetId) - const secondHop = getHopByIndex(activeQuote, 1) + const secondHop = getHopByIndex(confirmedQuote, 1) const secondHopFeeAsset = useSelectorWithArgs( selectFeeAssetById, secondHop?.sellAsset.assetId ?? '', @@ -113,12 +113,12 @@ export const TradeConfirmSummary = () => { const secondHopNetworkFeeCryptoBaseUnit = useAppSelector(selectSecondHopNetworkFeeCryptoBaseUnit) const tradeQuoteFirstHop = useAppSelector(selectFirstHop) const translate = useTranslate() - const { priceImpactPercentage } = usePriceImpact(activeQuote) + const { priceImpactPercentage } = usePriceImpact(confirmedQuote) const { isLoading } = useIsApprovalInitiallyNeeded() const greenColor = useColorModeValue('green.600', 'green.200') const [showFeeModal, setShowFeeModal] = useState(false) const thorVotingPower = useAppSelector(selectThorVotingPower) - const receiveAddress = activeQuote?.receiveAddress + const receiveAddress = confirmedQuote?.receiveAddress const swapSource = tradeQuoteFirstHop?.source const rate = tradeQuoteFirstHop?.rate const sellAssetSymbol = sellAsset.symbol diff --git a/src/components/MultiHopTrade/components/TradeConfirm/TradeFooterButton.tsx b/src/components/MultiHopTrade/components/TradeConfirm/TradeFooterButton.tsx index 0e2e616dd6a..309086ace66 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/TradeFooterButton.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/TradeFooterButton.tsx @@ -21,9 +21,9 @@ import type { TextPropTypes } from 'components/Text/Text' import { bnOrZero } from 'lib/bignumber/bignumber' import { assertUnreachable } from 'lib/utils' import { - selectActiveQuote, selectActiveQuoteErrors, selectActiveSwapperName, + selectConfirmedQuote, selectConfirmedTradeExecutionState, selectHopExecutionMetadata, selectLastHopBuyAsset, @@ -64,15 +64,15 @@ export const TradeFooterButton: FC = ({ const swapperName = useAppSelector(selectActiveSwapperName) const lastHopBuyAsset = useAppSelector(selectLastHopBuyAsset) const confirmedTradeExecutionState = useAppSelector(selectConfirmedTradeExecutionState) - const activeQuote = useAppSelector(selectActiveQuote) - const { isModeratePriceImpact, priceImpactPercentage } = usePriceImpact(activeQuote) + const confirmedQuote = useAppSelector(selectConfirmedQuote) + const { isModeratePriceImpact, priceImpactPercentage } = usePriceImpact(confirmedQuote) const firstHopMetadata = useSelectorWithArgs(selectHopExecutionMetadata, { - tradeId: activeQuote?.id ?? '', + tradeId: confirmedQuote?.id ?? '', hopIndex: 0, }) const secondHopMetadata = useSelectorWithArgs(selectHopExecutionMetadata, { - tradeId: activeQuote?.id ?? '', + tradeId: confirmedQuote?.id ?? '', hopIndex: 1, }) const networkFeeUserCurrency = useAppSelector(selectTotalNetworkFeeUserCurrency) diff --git a/src/components/MultiHopTrade/components/TradeConfirm/hooks/useCurrentHopIndex.tsx b/src/components/MultiHopTrade/components/TradeConfirm/hooks/useCurrentHopIndex.tsx index 906899d7d7f..1e11abcebc8 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/hooks/useCurrentHopIndex.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/hooks/useCurrentHopIndex.tsx @@ -1,6 +1,6 @@ import type { SupportedTradeQuoteStepIndex } from '@shapeshiftoss/swapper' import { - selectActiveQuote, + selectConfirmedQuote, selectHopExecutionMetadata, selectIsActiveQuoteMultiHop, } from 'state/slices/tradeQuoteSlice/selectors' @@ -12,11 +12,11 @@ import { useAppSelector, useSelectorWithArgs } from 'state/store' * A hop is considered "current" if it's in an active state (awaiting user action or executing) */ export const useCurrentHopIndex: () => SupportedTradeQuoteStepIndex = () => { - const activeQuote = useAppSelector(selectActiveQuote) + const confirmedQuote = useAppSelector(selectConfirmedQuote) const isMultiHop = useAppSelector(selectIsActiveQuoteMultiHop) const firstHopMetadata = useSelectorWithArgs(selectHopExecutionMetadata, { - tradeId: activeQuote?.id ?? '', + tradeId: confirmedQuote?.id ?? '', hopIndex: 0, }) diff --git a/src/components/MultiHopTrade/components/TradeConfirm/hooks/useStepperSteps.tsx b/src/components/MultiHopTrade/components/TradeConfirm/hooks/useStepperSteps.tsx index b89970e0f88..11b7fb1d9de 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/hooks/useStepperSteps.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/hooks/useStepperSteps.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react' import { selectIsActiveQuoteMultiHop } from 'state/slices/tradeInputSlice/selectors' import { - selectActiveQuote, + selectConfirmedQuote, selectHopExecutionMetadata, } from 'state/slices/tradeQuoteSlice/selectors' import { useAppSelector } from 'state/store' @@ -15,7 +15,7 @@ import { import { useCurrentHopIndex } from './useCurrentHopIndex' export const useStepperSteps = () => { - const activeTradeId = useAppSelector(selectActiveQuote)?.id + const activeTradeId = useAppSelector(selectConfirmedQuote)?.id const isMultiHopTrade = useAppSelector(selectIsActiveQuoteMultiHop) const firstHopExecutionMetadataFilter = useMemo( diff --git a/src/components/MultiHopTrade/components/TradeConfirm/hooks/useStreamingProgress.tsx b/src/components/MultiHopTrade/components/TradeConfirm/hooks/useStreamingProgress.tsx index 6ad7d033c5d..b4663508ede 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/hooks/useStreamingProgress.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/hooks/useStreamingProgress.tsx @@ -1,6 +1,9 @@ import type { TradeQuoteStep } from '@shapeshiftoss/swapper' import { SwapperName } from '@shapeshiftoss/swapper' -import { selectActiveQuote, selectActiveSwapperName } from 'state/slices/tradeQuoteSlice/selectors' +import { + selectActiveSwapperName, + selectConfirmedQuote, +} from 'state/slices/tradeQuoteSlice/selectors' import { useAppSelector } from 'state/store' import { useChainflipStreamingProgress } from '../../MultiHopTradeConfirm/hooks/useChainflipStreamingProgress' @@ -12,11 +15,11 @@ type UseStreamingProgressProps = { } export const useStreamingProgress = ({ hopIndex, tradeQuoteStep }: UseStreamingProgressProps) => { - const activeQuote = useAppSelector(selectActiveQuote) + const confirmedQuote = useAppSelector(selectConfirmedQuote) const currentSwapperName = useAppSelector(selectActiveSwapperName) - const isStreamingSwap = activeQuote?.isStreaming || false + const isStreamingSwap = confirmedQuote?.isStreaming || false const isThorchainSwap = currentSwapperName === SwapperName.Thorchain - const confirmedTradeId = activeQuote?.id + const confirmedTradeId = confirmedQuote?.id const streamingProgressArgs = { tradeQuoteStep, diff --git a/src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeButtonProps.tsx b/src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeButtonProps.tsx index fea0c352306..e6edd5969e6 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeButtonProps.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeButtonProps.tsx @@ -5,7 +5,7 @@ import { useGetTradeQuotes } from 'components/MultiHopTrade/hooks/useGetTradeQuo import { TradeRoutePaths } from 'components/MultiHopTrade/types' import { assertUnreachable } from 'lib/utils' import { - selectActiveQuote, + selectConfirmedQuote, selectConfirmedTradeExecutionState, selectHopExecutionMetadata, } from 'state/slices/tradeQuoteSlice/selectors' @@ -44,7 +44,7 @@ export const useTradeButtonProps = ({ const dispatch = useAppDispatch() const history = useHistory() const confirmedTradeExecutionState = useAppSelector(selectConfirmedTradeExecutionState) - const activeQuote = useAppSelector(selectActiveQuote) + const confirmedQuote = useAppSelector(selectConfirmedQuote) const { isFetching, data: tradeQuoteQueryData } = useGetTradeQuotes() const { handleSignAllowanceApproval, @@ -61,9 +61,9 @@ export const useTradeButtonProps = ({ }) const handleTradeConfirm = useCallback(() => { - if (!activeQuote) return - dispatch(tradeQuoteSlice.actions.confirmTrade(activeQuote.id)) - }, [dispatch, activeQuote]) + if (!confirmedQuote) return + dispatch(tradeQuoteSlice.actions.confirmTrade(confirmedQuote.id)) + }, [dispatch, confirmedQuote]) const hopExecutionMetadataFilter = useMemo(() => { return { diff --git a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx index cefe5f6cfd1..46728b4c6c6 100644 --- a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx @@ -25,7 +25,7 @@ import { useWallet } from 'hooks/useWallet/useWallet' import { fromBaseUnit } from 'lib/math' import { getMixPanel } from 'lib/mixpanel/mixPanelSingleton' import { MixPanelEvent } from 'lib/mixpanel/types' -import { selectIsVotingPowerLoading } from 'state/apis/snapshot/selectors' +import { selectCalculatedFees, selectIsVotingPowerLoading } from 'state/apis/snapshot/selectors' import type { ApiQuote } from 'state/apis/swapper/types' import { selectIsAnyAccountMetadataLoadedForChainId, @@ -49,11 +49,12 @@ import { selectFirstHop, selectIsTradeQuoteRequestAborted, selectIsUnsafeActiveQuote, + selectQuoteSellAmountUsd, selectShouldShowTradeQuoteOrAwaitInput, selectSortedTradeQuotes, } from 'state/slices/tradeQuoteSlice/selectors' import { tradeQuoteSlice } from 'state/slices/tradeQuoteSlice/tradeQuoteSlice' -import { store, useAppDispatch, useAppSelector } from 'state/store' +import { store, useAppDispatch, useAppSelector, useSelectorWithArgs } from 'state/store' import { useAccountIds } from '../../hooks/useAccountIds' import { SharedTradeInput } from '../SharedTradeInput/SharedTradeInput' @@ -107,6 +108,7 @@ export const TradeInput = ({ isCompact, tradeInputRef, onChangeTab }: TradeInput const activeQuoteMeta = useAppSelector(selectActiveQuoteMeta) const sellAmountCryptoPrecision = useAppSelector(selectInputSellAmountCryptoPrecision) const sellAmountUserCurrency = useAppSelector(selectInputSellAmountUserCurrency) + const sellAmountUsd = useAppSelector(selectQuoteSellAmountUsd) const buyAmountAfterFeesCryptoPrecision = useAppSelector(selectBuyAmountAfterFeesCryptoPrecision) const buyAmountAfterFeesUserCurrency = useAppSelector(selectBuyAmountAfterFeesUserCurrency) const shouldShowTradeQuoteOrAwaitInput = useAppSelector(selectShouldShowTradeQuoteOrAwaitInput) @@ -126,6 +128,14 @@ export const TradeInput = ({ isCompact, tradeInputRef, onChangeTab }: TradeInput selectIsAnyAccountMetadataLoadedForChainId(state, isAnyAccountMetadataLoadedForChainIdFilter), ) const walletId = useAppSelector(selectWalletId) + const calculatedFeesParams = useMemo( + () => ({ + feeModel: 'SWAPPER', + inputAmountUsd: sellAmountUsd, + }), + [sellAmountUsd], + ) + const calculatedFees = useSelectorWithArgs(selectCalculatedFees, calculatedFeesParams) const sellAssetUsdRate = useAppSelector(state => selectUsdRateByAssetId(state, sellAsset.assetId)) const buyAssetUsdRate = useAppSelector(state => selectUsdRateByAssetId(state, buyAsset.assetId)) @@ -255,7 +265,9 @@ export const TradeInput = ({ isCompact, tradeInputRef, onChangeTab }: TradeInput dispatch(tradeQuoteSlice.actions.setActiveQuote(bestQuote)) } - dispatch(tradeQuoteSlice.actions.setConfirmedQuote(activeQuote)) + // Set the confirmed quote for execution, with a snapshot of the affiliate fees for display after the trade is executed. + // This is done to handle the fox power calculation changing due to FOX balance changes after the trade is executed. + dispatch(tradeQuoteSlice.actions.setConfirmedQuote({ quote: activeQuote, calculatedFees })) dispatch(tradeQuoteSlice.actions.clearQuoteExecutionState(activeQuote.id)) if (isLedger(wallet)) { @@ -273,6 +285,7 @@ export const TradeInput = ({ isCompact, tradeInputRef, onChangeTab }: TradeInput }, [ activeQuote, activeQuoteMeta, + calculatedFees, dispatch, handleConnect, history, diff --git a/src/components/MultiHopTrade/components/TradeInput/components/TradeSettingsMenu.tsx b/src/components/MultiHopTrade/components/TradeInput/components/TradeSettingsMenu.tsx index 94629868090..fa53c41a187 100644 --- a/src/components/MultiHopTrade/components/TradeInput/components/TradeSettingsMenu.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/components/TradeSettingsMenu.tsx @@ -2,7 +2,10 @@ import { useMediaQuery } from '@chakra-ui/react' import { DEFAULT_GET_TRADE_QUOTE_POLLING_INTERVAL, swappers } from '@shapeshiftoss/swapper' import { useMemo } from 'react' import { selectIsTradeQuoteApiQueryPending } from 'state/apis/swapper/selectors' -import { selectActiveQuote, selectActiveSwapperName } from 'state/slices/tradeQuoteSlice/selectors' +import { + selectActiveSwapperName, + selectConfirmedQuote, +} from 'state/slices/tradeQuoteSlice/selectors' import { useAppSelector } from 'state/store' import { breakpoints } from 'theme/theme' @@ -16,7 +19,7 @@ type TradeSettingsMenuProps = { export const TradeSettingsMenu = ({ isCompact, isLoading }: TradeSettingsMenuProps) => { const [isSmallerThanXl] = useMediaQuery(`(max-width: ${breakpoints.xl})`, { ssr: false }) - const activeQuote = useAppSelector(selectActiveQuote) + const confirmedQuote = useAppSelector(selectConfirmedQuote) const activeSwapperName = useAppSelector(selectActiveSwapperName) const isTradeQuoteApiQueryPending = useAppSelector(selectIsTradeQuoteApiQueryPending) @@ -32,7 +35,7 @@ export const TradeSettingsMenu = ({ isCompact, isLoading }: TradeSettingsMenuPro return ( <> - {activeQuote && (isCompact || isSmallerThanXl) && ( + {confirmedQuote && (isCompact || isSmallerThanXl) && ( )} diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index 6eecf646203..38e63240410 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -48,8 +48,8 @@ import { selectUserSlippagePercentageDecimal, } from 'state/slices/tradeInputSlice/selectors' import { - selectActiveQuote, selectActiveQuoteMetaOrDefault, + selectConfirmedQuote, selectConfirmedTradeExecution, selectHopExecutionMetadata, selectIsAnyTradeQuoteLoading, @@ -137,7 +137,7 @@ export const useGetTradeQuotes = () => { } = useWallet() const sortedTradeQuotes = useAppSelector(selectSortedTradeQuotes) - const activeTrade = useAppSelector(selectActiveQuote) + const activeTrade = useAppSelector(selectConfirmedQuote) const activeTradeId = activeTrade?.id const activeRateRef = useRef() const activeTradeIdRef = useRef() @@ -390,9 +390,8 @@ export const useGetTradeQuotes = () => { executionMetadata: confirmedTradeExecution, }), ) - // Set as both confirmed *and* active + // Set as active dispatch(tradeQuoteSlice.actions.setActiveQuote(quoteData)) - dispatch(tradeQuoteSlice.actions.setConfirmedQuote(quoteData.quote)) }, [activeTrade, activeQuoteMeta, dispatch, queryStateMeta.data, confirmedTradeExecution]) // TODO: move to separate hook so we don't need to pull quote data into here diff --git a/src/state/slices/tradeInputSlice/selectors.ts b/src/state/slices/tradeInputSlice/selectors.ts index 5107998f902..6a7d024aa44 100644 --- a/src/state/slices/tradeInputSlice/selectors.ts +++ b/src/state/slices/tradeInputSlice/selectors.ts @@ -1,16 +1,13 @@ import { createSelector } from '@reduxjs/toolkit' -import type { SwapperName, TradeQuote, TradeRate } from '@shapeshiftoss/swapper' +import type { TradeQuote, TradeRate } from '@shapeshiftoss/swapper' import { isExecutableTradeStep } from '@shapeshiftoss/swapper' import { bn } from '@shapeshiftoss/utils' import type { Selector } from 'react-redux' -import type { ApiQuote } from 'state/apis/swapper/types' import type { ReduxState } from 'state/reducer' import { createDeepEqualOutputSelector } from 'state/selector-utils' import { createTradeInputBaseSelectors } from '../common/tradeInputBase/createTradeInputBaseSelectors' import { selectAccountIdByAccountNumberAndChainId } from '../portfolioSlice/selectors' -import { getActiveQuoteMetaOrDefault, sortTradeQuotes } from '../tradeQuoteSlice/helpers' -import type { ActiveQuoteMeta } from '../tradeQuoteSlice/types' import type { TradeInputState } from './tradeInputSlice' // Shared selectors from the base trade input slice that handle common functionality like input @@ -51,57 +48,23 @@ export const selectLastHopBuyAccountId = selectBuyAccountId // and allow selectSecondHopSellAccountId to keep a pwetty API const selectTradeQuoteSlice = (state: ReduxState) => state.tradeQuoteSlice -const selectActiveQuoteMeta: Selector = createSelector( - selectTradeQuoteSlice, - tradeQuoteSlice => tradeQuoteSlice.activeQuoteMeta, -) -const selectTradeQuotes = createDeepEqualOutputSelector( - selectTradeQuoteSlice, - tradeQuoteSlice => tradeQuoteSlice.tradeQuotes, -) -const selectSortedTradeQuotes = createDeepEqualOutputSelector(selectTradeQuotes, tradeQuotes => - sortTradeQuotes(tradeQuotes), -) - -const selectActiveQuoteMetaOrDefault: Selector< - ReduxState, - { swapperName: SwapperName; identifier: string } | undefined -> = createSelector(selectActiveQuoteMeta, selectSortedTradeQuotes, getActiveQuoteMetaOrDefault) - -const selectActiveSwapperApiResponse: Selector = - createDeepEqualOutputSelector( - selectTradeQuotes, - selectActiveQuoteMetaOrDefault, - (tradeQuotes, activeQuoteMetaOrDefault) => { - // If the active quote was reset, we do NOT want to return a stale quote as an "active" quote - if (activeQuoteMetaOrDefault === undefined) return undefined - return tradeQuotes[activeQuoteMetaOrDefault.swapperName]?.[ - activeQuoteMetaOrDefault.identifier - ] - }, - ) -const selectConfirmedQuote: Selector = +// Return the confirmed quote for trading. If it doesn't exist, it's not safe to trade. +// This mechanism prevents the quote changing during trade execution, but has implications on the UI +// displaying stale data. To prevent displaying stale data, we must ensure to clear the +// confirmedQuote when not executing a trade. +export const selectConfirmedQuote: Selector = createDeepEqualOutputSelector(selectTradeQuoteSlice, tradeQuote => tradeQuote.confirmedQuote) -const selectActiveQuote: Selector = - createDeepEqualOutputSelector( - selectActiveSwapperApiResponse, - selectConfirmedQuote, - (response, confirmedQuote) => { - // Return the confirmed quote for trading, if it exists. - // This prevents the quote changing during trade execution, but has implications on the UI - // displaying stale data. To prevent displaying stale data, we must ensure to clear the - // confirmedQuote when not executing a trade. - if (confirmedQuote) return confirmedQuote - return response?.quote - }, - ) const selectSecondHop: Selector = - createDeepEqualOutputSelector(selectActiveQuote, quote => (quote ? quote.steps[1] : undefined)) + createDeepEqualOutputSelector(selectConfirmedQuote, confirmedQuote => + confirmedQuote ? confirmedQuote.steps[1] : undefined, + ) export const selectIsActiveQuoteMultiHop: Selector = - createSelector(selectActiveQuote, quote => (quote ? quote?.steps.length > 1 : undefined)) + createSelector(selectConfirmedQuote, confirmedQuote => + confirmedQuote ? confirmedQuote.steps.length > 1 : undefined, + ) export const selectUserSlippagePercentage = createSelector( selectBaseSlice, diff --git a/src/state/slices/tradeQuoteSlice/constants.ts b/src/state/slices/tradeQuoteSlice/constants.ts index aef62d69ee2..aeed808cbd4 100644 --- a/src/state/slices/tradeQuoteSlice/constants.ts +++ b/src/state/slices/tradeQuoteSlice/constants.ts @@ -30,6 +30,7 @@ export const initialTradeExecutionState = { export const initialState: TradeQuoteSliceState = { activeQuoteMeta: undefined, confirmedQuote: undefined, + confirmedFees: undefined, activeStep: undefined, tradeExecution: {}, tradeQuotes: {}, diff --git a/src/state/slices/tradeQuoteSlice/selectors.ts b/src/state/slices/tradeQuoteSlice/selectors.ts index 0d49c8912e8..b820c09d417 100644 --- a/src/state/slices/tradeQuoteSlice/selectors.ts +++ b/src/state/slices/tradeQuoteSlice/selectors.ts @@ -16,8 +16,8 @@ import type { Asset } from '@shapeshiftoss/types' import { identity } from 'lodash' import type { Selector } from 'reselect' import { bn, bnOrZero } from 'lib/bignumber/bignumber' +import type { CalculateFeeBpsReturn } from 'lib/fees/model' import { fromBaseUnit } from 'lib/math' -import { selectCalculatedFees } from 'state/apis/snapshot/selectors' import { validateQuoteRequest } from 'state/apis/swapper/helpers/validateQuoteRequest' import { selectIsTradeQuoteApiQueryPending } from 'state/apis/swapper/selectors' import type { ApiQuote, ErrorWithMeta, TradeQuoteError } from 'state/apis/swapper/types' @@ -200,11 +200,16 @@ export const selectActiveStepOrDefault: Selector = createSel tradeQuote => tradeQuote.activeStep ?? 0, ) -const selectConfirmedQuote: Selector = +export const selectConfirmedQuote: Selector = createDeepEqualOutputSelector(selectTradeQuoteSlice, tradeQuoteState => { return tradeQuoteState.confirmedQuote }) +export const selectConfirmedFees: Selector = + createDeepEqualOutputSelector(selectTradeQuoteSlice, tradeQuoteState => { + return tradeQuoteState.confirmedFees + }) + export const selectActiveQuoteMetaOrDefault: Selector< ReduxState, { swapperName: SwapperName; identifier: string } | undefined @@ -252,7 +257,7 @@ export const selectActiveQuote: Selector = - createSelector(selectActiveQuote, activeQuote => { + createSelector(selectConfirmedQuote, activeQuote => { return activeQuote?.slippageTolerancePercentageDecimal }) @@ -283,7 +288,7 @@ export const selectActiveQuoteWarnings: Selector< export const selectHopTotalProtocolFeesFiatPrecision: Selector = createSelector( - selectActiveQuote, + selectConfirmedQuote, selectUserCurrencyToUsdRate, selectMarketDataUsd, (_state: ReduxState, stepIndex: SupportedTradeQuoteStepIndex) => stepIndex, @@ -296,35 +301,41 @@ export const selectHopTotalProtocolFeesFiatPrecision: Selector = - createSelector(selectActiveQuote, selectActiveSwapperName, quote => - quote ? getBuyAmountAfterFeesCryptoPrecision({ quote }) : undefined, + createSelector(selectConfirmedQuote, selectActiveSwapperName, confirmedQuote => + confirmedQuote ? getBuyAmountAfterFeesCryptoPrecision({ quote: confirmedQuote }) : undefined, ) export const selectTotalProtocolFeeByAsset: Selector< ReduxState, Record | undefined -> = createDeepEqualOutputSelector(selectActiveQuote, quote => - quote ? getTotalProtocolFeeByAsset(quote) : undefined, +> = createDeepEqualOutputSelector(selectConfirmedQuote, confirmedQuote => + confirmedQuote ? getTotalProtocolFeeByAsset(confirmedQuote) : undefined, ) export const selectIsActiveQuoteMultiHop: Selector = - createSelector(selectActiveQuote, quote => (quote ? quote?.steps.length > 1 : undefined)) + createSelector(selectConfirmedQuote, confirmedQuote => + confirmedQuote ? confirmedQuote.steps.length > 1 : undefined, + ) export const selectFirstHop: Selector = - createDeepEqualOutputSelector(selectActiveQuote, quote => getHopByIndex(quote, 0)) + createDeepEqualOutputSelector(selectConfirmedQuote, confirmedQuote => + confirmedQuote ? getHopByIndex(confirmedQuote, 0) : undefined, + ) export const selectLastHop: Selector< ReduxState, TradeQuote['steps'][SupportedTradeQuoteStepIndex] | undefined -> = createDeepEqualOutputSelector(selectActiveQuote, quote => { - if (!quote) return - const stepIndex = (quote.steps.length - 1) as SupportedTradeQuoteStepIndex - return getHopByIndex(quote, stepIndex) +> = createDeepEqualOutputSelector(selectConfirmedQuote, confirmedQuote => { + if (!confirmedQuote) return + const stepIndex = (confirmedQuote?.steps.length - 1) as SupportedTradeQuoteStepIndex + return getHopByIndex(confirmedQuote, stepIndex) }) // selects the second hop if it exists. This is different to "last hop" export const selectSecondHop: Selector = - createDeepEqualOutputSelector(selectActiveQuote, quote => getHopByIndex(quote, 1)) + createDeepEqualOutputSelector(selectConfirmedQuote, confirmedQuote => + confirmedQuote ? getHopByIndex(confirmedQuote, 1) : undefined, + ) export const selectFirstHopSellAsset: Selector = createDeepEqualOutputSelector(selectFirstHop, firstHop => @@ -552,20 +563,16 @@ export const selectQuoteSellAmountUserCurrency = createSelector( ) export const selectActiveQuoteAffiliateBps: Selector = - createSelector(selectActiveQuote, activeQuote => { - if (!activeQuote) return - return activeQuote.affiliateBps + createSelector(selectConfirmedQuote, confirmedQuote => { + if (!confirmedQuote) return + return confirmedQuote.affiliateBps }) export const selectTradeQuoteAffiliateFeeAfterDiscountUsd = createSelector( - (state: ReduxState) => - selectCalculatedFees(state, { - feeModel: 'SWAPPER', - inputAmountUsd: selectQuoteSellAmountUsd(state), - }), + selectConfirmedFees, selectActiveQuoteAffiliateBps, (calculatedFees, affiliateBps) => { - if (!affiliateBps) return + if (!affiliateBps || !calculatedFees) return if (affiliateBps === '0') return bn(0) return calculatedFees.feeUsd @@ -573,17 +580,10 @@ export const selectTradeQuoteAffiliateFeeAfterDiscountUsd = createSelector( ) export const selectTradeQuoteAffiliateFeeDiscountUsd = createSelector( - (state: ReduxState) => - selectCalculatedFees(state, { - feeModel: 'SWAPPER', - inputAmountUsd: selectQuoteSellAmountUsd(state), - }), - selectActiveQuoteAffiliateBps, - (calculatedFees, affiliateBps) => { - if (!affiliateBps) return - if (affiliateBps === '0') return bn(0) - - return calculatedFees.foxDiscountUsd + selectConfirmedFees, + confirmedFees => { + if (!confirmedFees) return + return confirmedFees.foxDiscountUsd }, ) diff --git a/src/state/slices/tradeQuoteSlice/tradeQuoteSlice.ts b/src/state/slices/tradeQuoteSlice/tradeQuoteSlice.ts index c8568c00016..1b207ce3c97 100644 --- a/src/state/slices/tradeQuoteSlice/tradeQuoteSlice.ts +++ b/src/state/slices/tradeQuoteSlice/tradeQuoteSlice.ts @@ -3,6 +3,7 @@ import { createSlice } from '@reduxjs/toolkit' import type { SwapperName, TradeQuote, TradeRate } from '@shapeshiftoss/swapper' import { orderBy, uniqBy } from 'lodash' import type { InterpolationOptions } from 'node-polyglot' +import type { CalculateFeeBpsReturn } from 'lib/fees/model' import type { ApiQuote } from 'state/apis/swapper/types' import { initialState, initialTradeExecutionState } from './constants' @@ -52,8 +53,15 @@ export const tradeQuoteSlice = createSlice({ } } }, - setConfirmedQuote: (state, action: PayloadAction) => { - state.confirmedQuote = action.payload + setConfirmedQuote: ( + state, + action: PayloadAction<{ + quote: TradeQuote | TradeRate + calculatedFees: CalculateFeeBpsReturn + }>, + ) => { + state.confirmedQuote = action.payload.quote + state.confirmedFees = action.payload.calculatedFees }, clearQuoteExecutionState: (state, action: PayloadAction) => { state.tradeExecution[action.payload] = initialTradeExecutionState diff --git a/src/state/slices/tradeQuoteSlice/types.ts b/src/state/slices/tradeQuoteSlice/types.ts index 7cecfd2874c..f6e76e92e30 100644 --- a/src/state/slices/tradeQuoteSlice/types.ts +++ b/src/state/slices/tradeQuoteSlice/types.ts @@ -1,14 +1,21 @@ import type { SwapperName, TradeQuote, TradeRate } from '@shapeshiftoss/swapper' import type { PartialRecord } from '@shapeshiftoss/types' import type { InterpolationOptions } from 'node-polyglot' +import type { CalculateFeeBpsReturn } from 'lib/fees/model' import type { ApiQuote } from 'state/apis/swapper/types' -export type ActiveQuoteMeta = { swapperName: SwapperName; identifier: string } +export type ActiveQuoteMeta = { + swapperName: SwapperName + identifier: string +} export type TradeQuoteSliceState = { activeStep: number | undefined // Make sure to actively check for undefined vs. falsy here. 0 is the first step, undefined means no active step yet activeQuoteMeta: ActiveQuoteMeta | undefined // the selected quote metadata used to find the active quote in the api responses confirmedQuote: TradeQuote | TradeRate | undefined // the quote being executed + // Used to display the "You saved" message in the TradeSuccess component. This needs to be stored + // here because trading fox will affect the calculation after the trade has been executed. + confirmedFees: CalculateFeeBpsReturn | undefined tradeExecution: Record tradeQuotes: PartialRecord> // mapping from swapperName to quoteId to ApiQuote tradeQuoteDisplayCache: ApiQuote[] diff --git a/src/test/mocks/store.ts b/src/test/mocks/store.ts index 46dd9a31ac9..7297a50aacd 100644 --- a/src/test/mocks/store.ts +++ b/src/test/mocks/store.ts @@ -266,6 +266,7 @@ export const mockStore: ReduxState = { tradeQuoteSlice: { activeQuoteMeta: undefined, confirmedQuote: undefined, + confirmedFees: undefined, activeStep: undefined, tradeExecution: {}, tradeQuotes: {},