diff --git a/package.json b/package.json index 8a4ff5d28ce..16c4a6d73d6 100644 --- a/package.json +++ b/package.json @@ -93,20 +93,20 @@ "@shapeshiftoss/caip": "workspace:^", "@shapeshiftoss/chain-adapters": "workspace:^", "@shapeshiftoss/errors": "workspace:^", - "@shapeshiftoss/hdwallet-coinbase": "1.52.12", - "@shapeshiftoss/hdwallet-core": "1.52.12", - "@shapeshiftoss/hdwallet-keepkey": "1.52.12", - "@shapeshiftoss/hdwallet-keepkey-webusb": "1.52.12", - "@shapeshiftoss/hdwallet-keplr": "1.52.12", - "@shapeshiftoss/hdwallet-ledger": "1.52.12", - "@shapeshiftoss/hdwallet-ledger-webhid": "1.52.12", - "@shapeshiftoss/hdwallet-ledger-webusb": "1.52.12", - "@shapeshiftoss/hdwallet-metamask": "1.52.12", - "@shapeshiftoss/hdwallet-native": "1.52.12", - "@shapeshiftoss/hdwallet-native-vault": "1.52.12", - "@shapeshiftoss/hdwallet-shapeshift-multichain": "1.52.12", - "@shapeshiftoss/hdwallet-walletconnectv2": "1.52.12", - "@shapeshiftoss/hdwallet-xdefi": "1.52.12", + "@shapeshiftoss/hdwallet-coinbase": "1.52.13", + "@shapeshiftoss/hdwallet-core": "1.52.13", + "@shapeshiftoss/hdwallet-keepkey": "1.52.13", + "@shapeshiftoss/hdwallet-keepkey-webusb": "1.52.13", + "@shapeshiftoss/hdwallet-keplr": "1.52.13", + "@shapeshiftoss/hdwallet-ledger": "1.52.13", + "@shapeshiftoss/hdwallet-ledger-webhid": "1.52.13", + "@shapeshiftoss/hdwallet-ledger-webusb": "1.52.13", + "@shapeshiftoss/hdwallet-metamask": "1.52.13", + "@shapeshiftoss/hdwallet-native": "1.52.13", + "@shapeshiftoss/hdwallet-native-vault": "1.52.13", + "@shapeshiftoss/hdwallet-shapeshift-multichain": "1.52.13", + "@shapeshiftoss/hdwallet-walletconnectv2": "1.52.13", + "@shapeshiftoss/hdwallet-xdefi": "1.52.13", "@shapeshiftoss/types": "workspace:^", "@shapeshiftoss/unchained-client": "workspace:^", "@sniptt/monads": "^0.5.10", diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index d7e9d7cebc5..8032167e03f 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -2185,6 +2185,9 @@ }, "lending": { "lending": "Lending", + "repaymentAvailable": "Repayment available", + "daysUntilRepayment": "%{numDays} days until repayment", + "repaymentDays": "%{numDays} days", "lendingBody": "A listing of all pools that can be borrowed at zero-interest and zero liquidations.", "collateralValue": "Collateral Value", "collateralValueDescription": "Current value of your asset used as collateral", @@ -2232,6 +2235,7 @@ "repayNoticeCta": "I understand", "repayNotice": "To get back your collateral you will need to repay 100% of your debt.", "feesNotice": "Fees occurred on the collateral you will receive back after repaying 100% of your debt", + "refetchQuote": "Refetch Quote", "faq": { "title": "FAQ", "lending": { @@ -2258,14 +2262,14 @@ "foxDiscounts": { "currentFoxPower": "Your FOX Power", "foxPower": "FOX Power", - "simulateTitle": "Simulate your FOX Savings", + "simulateTitle": "Simulate your FOX power discounts", "simulateBody": "Use this handy calculator to determine your total fees and the discount you'll receive on your trade. Simply adjust the sliders below to indicate your FOX holder amount and the trade size in fiat, and watch your potential savings unfold!", "tradeSize": "Trade Size", "totalFee": "Total Fee", "foxPowerDiscount": "FOX Power Discount", "basedOnFee": "Based on a fee of: %{fee}", "feeSummary": "Fee Summary", - "simulateFee": "Simulate Fee", + "simulateFee": "Simulate Discount", "freeUnderThreshold": "Trades under %{threshold} are always FREE", "breakdownHeader": "Your FOX Power Discount", "breakdownBody": "Below is a breakdown of your trade fees and the corresponding FOX Power Discount applied. Learn how much more you can save on the simulate tab.", diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx index 7ec717e67fd..079f0f46e9c 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx @@ -8,7 +8,7 @@ import { Text } from 'components/Text' import { swappers as swappersSlice } from 'state/slices/swappersSlice/swappersSlice' import { selectTradeExecutionState } from 'state/slices/tradeQuoteSlice/selectors' import { tradeQuoteSlice } from 'state/slices/tradeQuoteSlice/tradeQuoteSlice' -import { MultiHopExecutionState } from 'state/slices/tradeQuoteSlice/types' +import { TradeExecutionState } from 'state/slices/tradeQuoteSlice/types' import { useAppDispatch, useAppSelector } from 'state/store' import { TradeSuccess } from '../TradeSuccess/TradeSuccess' @@ -50,7 +50,7 @@ export const MultiHopTradeConfirm = memo(() => { useEffect(() => { if ( previousTradeExecutionState !== tradeExecutionState && - previousTradeExecutionState === MultiHopExecutionState.FirstHopAwaitingTradeExecution + previousTradeExecutionState === TradeExecutionState.FirstHop ) { if (isFirstHopOpen) onToggleFirstHop() if (!isSecondHopOpen) onToggleSecondHop() @@ -65,7 +65,7 @@ export const MultiHopTradeConfirm = memo(() => { ]) const isTradeComplete = useMemo( - () => tradeExecutionState === MultiHopExecutionState.TradeComplete, + () => tradeExecutionState === TradeExecutionState.TradeComplete, [tradeExecutionState], ) @@ -77,7 +77,7 @@ export const MultiHopTradeConfirm = memo(() => { void } export const ApprovalStep = ({ @@ -37,42 +36,45 @@ export const ApprovalStep = ({ isActive, isLastStep, isLoading, - onError: handleError, }: ApprovalStepProps) => { const { number: { toCrypto }, } = useLocaleFormatter() const dispatch = useAppDispatch() const [isExactAllowance, toggleIsExactAllowance] = useToggle(false) - const [isError, setIsError] = useState(false) const { - state: hopExecutionState, - approvalTxHash: txHash, - approvalState, + approval: { txHash, state: approvalTxState }, } = useAppSelector(selectHopExecutionMetadata)[hopIndex] + const isError = useMemo( + () => approvalTxState === TransactionExecutionState.Failed, + [approvalTxState], + ) + const { executeAllowanceApproval, approvalNetworkFeeCryptoBaseUnit } = useMockAllowanceApproval( tradeQuoteStep, - hopIndex === 0, + hopIndex, isExactAllowance, ) // TODO: use the real hook here const handleSignAllowanceApproval = useCallback(async () => { - // next state - dispatch(tradeQuoteSlice.actions.incrementTradeExecutionState()) + if (approvalTxState !== TransactionExecutionState.AwaitingConfirmation) { + console.error('attempted to execute in-progress allowance approval') + return + } + + dispatch(tradeQuoteSlice.actions.setApprovalTxPending({ hopIndex })) // execute the allowance approval const finalTxStatus = await executeAllowanceApproval() - // next state if trade was successful if (finalTxStatus === TxStatus.Confirmed) { - dispatch(tradeQuoteSlice.actions.incrementTradeExecutionState()) + dispatch(tradeQuoteSlice.actions.setApprovalTxComplete({ hopIndex })) } else if (finalTxStatus === TxStatus.Failed) { - setIsError(true) - handleError() + dispatch(tradeQuoteSlice.actions.setApprovalTxFailed({ hopIndex })) } - }, [dispatch, executeAllowanceApproval, handleError]) + }, [approvalTxState, dispatch, executeAllowanceApproval, hopIndex]) const feeAsset = selectFeeAssetById(store.getState(), tradeQuoteStep.sellAsset.assetId) const approvalNetworkFeeCryptoFormatted = @@ -83,24 +85,12 @@ export const ApprovalStep = ({ ) : '' - // the txStatus needs to be undefined before the tx is executed to handle "ready" but not "executing" status - const txStatus = - HOP_EXECUTION_STATE_ORDERED.indexOf(hopExecutionState) >= - HOP_EXECUTION_STATE_ORDERED.indexOf(HopExecutionState.AwaitingApprovalExecution) - ? approvalState - : undefined - - const stepIndicator = useMemo( - () => - txStatus !== undefined ? ( - - ) : ( -
- -
- ), - [txStatus], - ) + const stepIndicator = useMemo(() => { + const defaultIcon = + // eslint too stoopid to realize this is inside the context of useMemo already + // eslint-disable-next-line react-memo/require-usememo + return + }, [approvalTxState]) const translate = useTranslate() @@ -183,7 +173,7 @@ export const ApprovalStep = ({ size='sm' leftIcon={leftIcon} colorScheme='blue' - isLoading={hopExecutionState === HopExecutionState.AwaitingApprovalExecution} + isLoading={approvalTxState === TransactionExecutionState.Pending} onClick={handleSignAllowanceApproval} > {translate('common.approve')} @@ -192,8 +182,8 @@ export const ApprovalStep = ({ ) }, [ + approvalTxState, handleSignAllowanceApproval, - hopExecutionState, isActive, isExactAllowance, leftIcon, @@ -210,7 +200,7 @@ export const ApprovalStep = ({ content={content} isLastStep={isLastStep} isLoading={isLoading} - isError={txStatus === TxStatus.Failed} + isError={approvalTxState === TransactionExecutionState.Failed} /> ) } diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/Footer.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/Footer.tsx index d1980f8a3dd..cfa8868dc2e 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/Footer.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/Footer.tsx @@ -24,7 +24,7 @@ import { selectTradeExecutionState, } from 'state/slices/tradeQuoteSlice/selectors' import { tradeQuoteSlice } from 'state/slices/tradeQuoteSlice/tradeQuoteSlice' -import { MultiHopExecutionState } from 'state/slices/tradeQuoteSlice/types' +import { TradeExecutionState } from 'state/slices/tradeQuoteSlice/types' import { useAppDispatch, useAppSelector } from 'state/store' export const Footer = () => { @@ -38,7 +38,7 @@ export const Footer = () => { const { isModeratePriceImpact } = usePriceImpact() const handleConfirm = useCallback(() => { - dispatch(tradeQuoteSlice.actions.incrementTradeExecutionState()) + dispatch(tradeQuoteSlice.actions.confirmTrade()) }, [dispatch]) const networkFeeToTradeRatioPercentage = useMemo( @@ -105,7 +105,7 @@ export const Footer = () => { ) }, [swapperName, lastHopBuyAsset, translate]) - return tradeExecutionState === MultiHopExecutionState.Previewing ? ( + return tradeExecutionState === TradeExecutionState.Previewing ? ( () - const [isError, setIsError] = useState(false) + + const { + state: hopExecutionState, + approval: { state: approvalTxState, isRequired: isApprovalInitiallyNeeded }, + swap: { state: swapTxState }, + } = useAppSelector(selectHopExecutionMetadata)[hopIndex] + + const isError = useMemo( + () => [approvalTxState, swapTxState].includes(TransactionExecutionState.Failed), + [approvalTxState, swapTxState], + ) const rightComponent = useMemo(() => { - switch (txStatus) { - case undefined: - case TxStatus.Unknown: + switch (swapTxState) { + case TransactionExecutionState.AwaitingConfirmation: return ( tradeQuoteStep.estimatedExecutionTimeMs !== undefined && ( @@ -74,34 +81,28 @@ export const Hop = ({ ) ) - case TxStatus.Pending: + case TransactionExecutionState.Pending: return ( tradeQuoteStep.estimatedExecutionTimeMs !== undefined && ( ) ) - case TxStatus.Confirmed: + case TransactionExecutionState.Complete: return onToggleIsOpen ? ( ) : null default: return null } - }, [tradeQuoteStep.estimatedExecutionTimeMs, isOpen, onToggleIsOpen, txStatus]) - - const { state: hopExecutionState, approvalRequired: isApprovalInitiallyNeeded } = useAppSelector( - selectHopExecutionMetadata, - )[hopIndex] + }, [swapTxState, tradeQuoteStep.estimatedExecutionTimeMs, onToggleIsOpen, isOpen]) const activeStep = useMemo(() => { switch (hopExecutionState) { case HopExecutionState.Pending: return -Infinity - case HopExecutionState.AwaitingApprovalConfirmation: - case HopExecutionState.AwaitingApprovalExecution: + case HopExecutionState.AwaitingApproval: return hopIndex === 0 ? 1 : 0 - case HopExecutionState.AwaitingTradeConfirmation: - case HopExecutionState.AwaitingTradeExecution: + case HopExecutionState.AwaitingSwap: return hopIndex === 0 ? 2 : 1 case HopExecutionState.Complete: return Infinity @@ -139,10 +140,8 @@ export const Hop = ({ ) - case HopExecutionState.AwaitingApprovalConfirmation: - case HopExecutionState.AwaitingApprovalExecution: - case HopExecutionState.AwaitingTradeConfirmation: - case HopExecutionState.AwaitingTradeExecution: + case HopExecutionState.AwaitingApproval: + case HopExecutionState.AwaitingSwap: return ( @@ -157,8 +156,6 @@ export const Hop = ({ } }, [hopExecutionState, hopIndex, isError]) - const handleError = useCallback(() => setIsError(true), []) - return ( @@ -180,24 +177,15 @@ export const Hop = ({ {shouldRenderFinalSteps && } {shouldRenderFinalSteps && ( diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx index 3ab60329fa7..0ad0dda38af 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx @@ -3,7 +3,7 @@ import { Button, Card, CardBody, Link, VStack } from '@chakra-ui/react' import type { KnownChainIds } from '@shapeshiftoss/types' import { TxStatus } from '@shapeshiftoss/unchained-client' import type Polyglot from 'node-polyglot' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useMemo } from 'react' import { useTranslate } from 'react-polyglot' import { MiddleEllipsis } from 'components/MiddleEllipsis/MiddleEllipsis' import { RawText, Text } from 'components/Text' @@ -15,7 +15,7 @@ import { THORCHAIN_STREAM_SWAP_SOURCE } from 'lib/swapper/swappers/ThorchainSwap import type { SwapperName, TradeQuoteStep } from 'lib/swapper/types' import { selectHopExecutionMetadata } from 'state/slices/tradeQuoteSlice/selectors' import { tradeQuoteSlice } from 'state/slices/tradeQuoteSlice/tradeQuoteSlice' -import { HOP_EXECUTION_STATE_ORDERED, HopExecutionState } from 'state/slices/tradeQuoteSlice/types' +import { TransactionExecutionState } from 'state/slices/tradeQuoteSlice/types' import { useAppDispatch, useAppSelector } from 'state/store' import { SwapperIcon } from '../../TradeInput/components/SwapperIcon/SwapperIcon' @@ -32,8 +32,6 @@ export type HopTransactionStepProps = { isActive: boolean hopIndex: number isLastStep?: boolean - onTxStatusChange: (txStatus?: TxStatus) => void - onError: () => void } export const HopTransactionStep = ({ @@ -42,43 +40,42 @@ export const HopTransactionStep = ({ isActive, hopIndex, isLastStep, - onTxStatusChange, - onError, }: HopTransactionStepProps) => { const { number: { toCrypto }, } = useLocaleFormatter() const dispatch = useAppDispatch() const translate = useTranslate() - const [isError, setIsError] = useState(false) const { - state: hopExecutionState, - swapState: txState, - swapSellTxHash: sellTxHash, - swapBuyTxHash: buyTxHash, + swap: { state: swapTxState, sellTxHash, buyTxHash }, } = useAppSelector(selectHopExecutionMetadata)[hopIndex] + const isError = useMemo(() => swapTxState === TransactionExecutionState.Failed, [swapTxState]) + const { // TODO: use the message to better ux // message, executeTrade, - } = useMockTradeExecution(hopIndex === 0) // TODO: use the real hook here + } = useMockTradeExecution(hopIndex) // TODO: use the real hook here const handleSignTx = useCallback(async () => { - // next state - dispatch(tradeQuoteSlice.actions.incrementTradeExecutionState()) + if (swapTxState !== TransactionExecutionState.AwaitingConfirmation) { + console.error('attempted to execute in-progress swap') + return + } + + dispatch(tradeQuoteSlice.actions.setSwapTxPending({ hopIndex })) const finalTxStatus = await executeTrade() // next state if trade was successful if (finalTxStatus === TxStatus.Confirmed) { - dispatch(tradeQuoteSlice.actions.incrementTradeExecutionState()) + dispatch(tradeQuoteSlice.actions.setSwapTxComplete({ hopIndex })) } else if (finalTxStatus === TxStatus.Failed) { - setIsError(true) - onError() + dispatch(tradeQuoteSlice.actions.setSwapTxFailed({ hopIndex })) } - }, [dispatch, executeTrade, onError]) + }, [dispatch, executeTrade, hopIndex, swapTxState]) const tradeType = useMemo( () => @@ -111,29 +108,17 @@ export const HopTransactionStep = ({ return {} }, [buyTxHash, tradeQuoteStep.source, tradeQuoteStep.sellAsset.explorerTxLink, sellTxHash]) - // the txStatus needs to be undefined before the tx is executed to handle "ready" but not "executing" status - const txStatus = - HOP_EXECUTION_STATE_ORDERED.indexOf(hopExecutionState) >= - HOP_EXECUTION_STATE_ORDERED.indexOf(HopExecutionState.AwaitingTradeExecution) - ? txState - : undefined - - useEffect(() => onTxStatusChange(txStatus), [onTxStatusChange, txStatus]) - - const stepIndicator = useMemo( - () => - txStatus !== undefined ? ( - - ) : ( - - ), - [swapperName, txStatus], - ) + const stepIndicator = useMemo(() => { + const defaultIcon = + // eslint too stoopid to realize this is inside the context of useMemo already + // eslint-disable-next-line react-memo/require-usememo + return + }, [swapTxState, swapperName]) const signIcon = useMemo(() => , []) const content = useMemo(() => { - if (isActive && txStatus === undefined) { + if (isActive && swapTxState === TransactionExecutionState.AwaitingConfirmation) { return ( @@ -162,9 +147,9 @@ export const HopTransactionStep = ({ isActive, sellTxHash, signIcon, + swapTxState, tradeQuoteStep.source, translate, - txStatus, ]) const errorTranslation = useMemo( @@ -241,7 +226,7 @@ export const HopTransactionStep = ({ stepIndicator={stepIndicator} content={content} isLastStep={isLastStep} - isError={txStatus === TxStatus.Failed} + isError={swapTxState === TransactionExecutionState.Failed} /> ) } diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StatusIcon.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StatusIcon.tsx index 826dd5ec87f..c9af346fe00 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StatusIcon.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StatusIcon.tsx @@ -1,31 +1,39 @@ import { CheckCircleIcon, WarningIcon } from '@chakra-ui/icons' import { Circle } from '@chakra-ui/react' -import { TxStatus } from '@shapeshiftoss/unchained-client' import { CircularProgress } from 'components/CircularProgress/CircularProgress' +import { assertUnreachable } from 'lib/utils' +import { TransactionExecutionState } from 'state/slices/tradeQuoteSlice/types' -export const StatusIcon = (props: { txStatus: TxStatus }) => { - // TODO: proper light/dark mode colors here - switch (props.txStatus) { - case TxStatus.Confirmed: +export const StatusIcon = ({ + txStatus, + defaultIcon, +}: { + txStatus: TransactionExecutionState + defaultIcon: JSX.Element +}) => { + switch (txStatus) { + case TransactionExecutionState.Complete: return ( ) - case TxStatus.Failed: + case TransactionExecutionState.Failed: return ( ) // when the trade is submitting, treat unknown status as pending so the spinner spins - case TxStatus.Pending: - case TxStatus.Unknown: - default: + case TransactionExecutionState.Pending: return ( ) + case TransactionExecutionState.AwaitingConfirmation: + return {defaultIcon} + default: + assertUnreachable(txStatus) } } diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/mockHooks.ts b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/mockHooks.ts index d85b31719d6..7a48824c71c 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/mockHooks.ts +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/mockHooks.ts @@ -1,12 +1,12 @@ -import { TxStatus } from '@shapeshiftoss/unchained-client' import { useCallback, useEffect } from 'react' import { sleep } from 'lib/poll/poll' import type { TradeQuoteStep } from 'lib/swapper/types' import { selectHopExecutionMetadata } from 'state/slices/tradeQuoteSlice/selectors' import { tradeQuoteSlice } from 'state/slices/tradeQuoteSlice/tradeQuoteSlice' -import type { - StreamingSwapFailedSwap, - StreamingSwapMetadata, +import { + type StreamingSwapFailedSwap, + type StreamingSwapMetadata, + TransactionExecutionState, } from 'state/slices/tradeQuoteSlice/types' import { useAppDispatch, useAppSelector } from 'state/store' @@ -24,39 +24,36 @@ const DEFAULT_STREAMING_SWAP_METADATA: StreamingSwapMetadata = { // TODO: remove me export const useMockAllowanceApproval = ( _tradeQuoteStep: TradeQuoteStep, - isFirstHop: boolean, + hopIndex: number, _isExactAllowance: boolean, ) => { const dispatch = useAppDispatch() const executeAllowanceApproval = useCallback(() => { - isFirstHop - ? dispatch(tradeQuoteSlice.actions.setFirstHopApprovalState(TxStatus.Pending)) - : dispatch(tradeQuoteSlice.actions.setSecondHopApprovalState(TxStatus.Pending)) + dispatch(tradeQuoteSlice.actions.setApprovalTxPending({ hopIndex })) const promise = new Promise((resolve, _reject) => { setTimeout(() => { - isFirstHop - ? dispatch( - tradeQuoteSlice.actions.setFirstHopApprovalTxHash('first_hop_approval_tx_hash'), - ) - : dispatch( - tradeQuoteSlice.actions.setSecondHopApprovalTxHash('second_hop_approval_tx_hash'), - ) + dispatch( + tradeQuoteSlice.actions.setApprovalTxHash({ hopIndex, txHash: 'approval_tx_hash' }), + ) }, 2000) + setTimeout(() => { - const finalStatus = MOCK_FAIL_APPROVAL ? TxStatus.Failed : TxStatus.Confirmed + const finalStatus = MOCK_FAIL_APPROVAL + ? TransactionExecutionState.Failed + : TransactionExecutionState.Complete - isFirstHop - ? dispatch(tradeQuoteSlice.actions.setFirstHopApprovalState(finalStatus)) - : dispatch(tradeQuoteSlice.actions.setSecondHopApprovalState(finalStatus)) + MOCK_FAIL_APPROVAL + ? dispatch(tradeQuoteSlice.actions.setApprovalTxFailed({ hopIndex })) + : dispatch(tradeQuoteSlice.actions.setApprovalTxComplete({ hopIndex })) resolve(finalStatus) }, 5000) }) return promise - }, [dispatch, isFirstHop]) + }, [dispatch, hopIndex]) return { executeAllowanceApproval, @@ -65,31 +62,33 @@ export const useMockAllowanceApproval = ( } // TODO: remove me -export const useMockTradeExecution = (isFirstHop: boolean) => { +export const useMockTradeExecution = (hopIndex: number) => { const dispatch = useAppDispatch() const executeTrade = useCallback(() => { const promise = new Promise((resolve, _reject) => { - isFirstHop - ? dispatch(tradeQuoteSlice.actions.setFirstHopSwapState(TxStatus.Pending)) - : dispatch(tradeQuoteSlice.actions.setSecondHopSwapState(TxStatus.Pending)) + dispatch(tradeQuoteSlice.actions.setSwapTxPending({ hopIndex })) setTimeout(() => { - isFirstHop - ? dispatch(tradeQuoteSlice.actions.setFirstHopSwapSellTxHash('first_hop_sell_tx_hash')) - : dispatch(tradeQuoteSlice.actions.setSecondHopSwapSellTxHash('second_hop_sell_tx_hash')) + dispatch( + tradeQuoteSlice.actions.setSwapSellTxHash({ hopIndex, sellTxHash: 'swap_sell_tx_hash' }), + ) }, 2000) + setTimeout(() => { - const finalStatus = MOCK_FAIL_SWAP ? TxStatus.Failed : TxStatus.Confirmed - isFirstHop - ? dispatch(tradeQuoteSlice.actions.setFirstHopSwapState(finalStatus)) - : dispatch(tradeQuoteSlice.actions.setSecondHopSwapState(finalStatus)) + const finalStatus = MOCK_FAIL_SWAP + ? TransactionExecutionState.Failed + : TransactionExecutionState.Complete + + MOCK_FAIL_SWAP + ? dispatch(tradeQuoteSlice.actions.setSwapTxFailed({ hopIndex })) + : dispatch(tradeQuoteSlice.actions.setSwapTxComplete({ hopIndex })) resolve(finalStatus) }, 15000) }) return promise - }, [dispatch, isFirstHop]) + }, [dispatch, hopIndex]) return { executeTrade, @@ -106,69 +105,76 @@ export const useMockThorStreamingProgress = ( failedSwaps: StreamingSwapFailedSwap[] } => { const dispatch = useAppDispatch() - const { swapSellTxHash: sellTxHash, streamingSwap: streamingSwapMeta } = useAppSelector( - selectHopExecutionMetadata, - )[hopIndex] + const { + swap: { sellTxHash, streamingSwap: streamingSwapMeta }, + } = useAppSelector(selectHopExecutionMetadata)[hopIndex] const streamingSwapExecutionStarted = streamingSwapMeta !== undefined useEffect(() => { if (!sellTxHash || streamingSwapExecutionStarted) return ;(async () => { - const setStreamingSwapMeta = - hopIndex === 0 - ? tradeQuoteSlice.actions.setFirstHopStreamingSwapMeta - : tradeQuoteSlice.actions.setSecondHopStreamingSwapMeta - dispatch( - setStreamingSwapMeta({ - totalSwapCount: 3, - attemptedSwapCount: 0, - failedSwaps: [], + tradeQuoteSlice.actions.setStreamingSwapMeta({ + hopIndex, + streamingSwapMetadata: { + totalSwapCount: 3, + attemptedSwapCount: 0, + failedSwaps: [], + }, }), ) await sleep(1500) dispatch( - setStreamingSwapMeta({ - totalSwapCount: 3, - attemptedSwapCount: 1, - failedSwaps: [], + tradeQuoteSlice.actions.setStreamingSwapMeta({ + hopIndex, + streamingSwapMetadata: { + totalSwapCount: 3, + attemptedSwapCount: 1, + failedSwaps: [], + }, }), ) await sleep(1500) dispatch( - setStreamingSwapMeta({ - totalSwapCount: 3, - attemptedSwapCount: 2, - failedSwaps: MOCK_FAIL_STREAMING_SWAP - ? [ - { - reason: 'mock reason', - swapIndex: 1, - }, - ] - : [], + tradeQuoteSlice.actions.setStreamingSwapMeta({ + hopIndex, + streamingSwapMetadata: { + totalSwapCount: 3, + attemptedSwapCount: 2, + failedSwaps: MOCK_FAIL_STREAMING_SWAP + ? [ + { + reason: 'mock reason', + swapIndex: 1, + }, + ] + : [], + }, }), ) await sleep(1500) dispatch( - setStreamingSwapMeta({ - totalSwapCount: 3, - attemptedSwapCount: 3, - failedSwaps: MOCK_FAIL_STREAMING_SWAP - ? [ - { - reason: 'mock reason', - swapIndex: 1, - }, - ] - : [], + tradeQuoteSlice.actions.setStreamingSwapMeta({ + hopIndex, + streamingSwapMetadata: { + totalSwapCount: 3, + attemptedSwapCount: 3, + failedSwaps: MOCK_FAIL_STREAMING_SWAP + ? [ + { + reason: 'mock reason', + swapIndex: 1, + }, + ] + : [], + }, }), ) })() diff --git a/src/components/MultiHopTrade/components/TradeConfirm/ReceiveSummary.tsx b/src/components/MultiHopTrade/components/TradeConfirm/ReceiveSummary.tsx index cefbd825a12..f02e3adb70d 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/ReceiveSummary.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/ReceiveSummary.tsx @@ -30,7 +30,8 @@ import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingl import { useFeatureFlag } from 'hooks/useFeatureFlag/useFeatureFlag' import { bnOrZero } from 'lib/bignumber/bignumber' import { fromBaseUnit } from 'lib/math' -import type { AmountDisplayMeta, ProtocolFee } from 'lib/swapper/types' +import { THORCHAIN_STREAM_SWAP_SOURCE } from 'lib/swapper/swappers/ThorchainSwapper/constants' +import type { AmountDisplayMeta, ProtocolFee, SwapSource } from 'lib/swapper/types' import { SwapperName } from 'lib/swapper/types' import type { PartialRecord } from 'lib/utils' import { isSome } from 'lib/utils' @@ -61,6 +62,7 @@ type ReceiveSummaryProps = { swapperName: string donationAmountUserCurrency?: string defaultIsOpen?: boolean + swapSource?: SwapSource } & RowProps const ShapeShiftFeeModalRowHover = { textDecoration: 'underline', cursor: 'pointer' } @@ -84,6 +86,7 @@ export const ReceiveSummary: FC = memo( isLoading, donationAmountUserCurrency, defaultIsOpen = false, + swapSource, ...rest }) => { const translate = useTranslate() @@ -279,41 +282,43 @@ export const ReceiveSummary: FC = memo( )} - <> - - - - - - - - - - - {isAmountPositive && - hasIntermediaryTransactionOutputs && - intermediaryTransactionOutputsParsed?.map( - ({ amountCryptoPrecision, symbol, chainName }) => ( - - - - ), - )} - - - - + {swapSource !== THORCHAIN_STREAM_SWAP_SOURCE && ( + <> + + + + + + + + + + + {isAmountPositive && + hasIntermediaryTransactionOutputs && + intermediaryTransactionOutputsParsed?.map( + ({ amountCryptoPrecision, symbol, chainName }) => ( + + + + ), + )} + + + + + )} diff --git a/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirm.tsx b/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirm.tsx index 3a1517e2c79..18c74c0df18 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirm.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirm.tsx @@ -405,24 +405,26 @@ export const TradeConfirm = () => { fiatAmount={positiveOrZero(netBuyAmountUserCurrency).toFixed(2)} swapperName={swapperName ?? ''} intermediaryTransactionOutputs={tradeQuoteStep?.intermediaryTransactionOutputs} + swapSource={tradeQuoteStep?.source} /> ), [ - translate, - sellAmountBeforeFeesCryptoPrecision, - sellAsset?.symbol, - sellAmountBeforeFeesUserCurrency, - buyAsset?.symbol, buyAmountAfterFeesCryptoPrecision, buyAmountBeforeFeesCryptoPrecision, - tradeQuoteStep?.feeData.protocolFees, - tradeQuoteStep?.intermediaryTransactionOutputs, - shapeShiftFee, + buyAsset?.symbol, donationAmountUserCurrency, - slippageDecimal, netBuyAmountUserCurrency, + sellAmountBeforeFeesCryptoPrecision, + sellAmountBeforeFeesUserCurrency, + sellAsset?.symbol, + shapeShiftFee, + slippageDecimal, swapperName, + tradeQuoteStep?.feeData.protocolFees, + tradeQuoteStep?.intermediaryTransactionOutputs, + tradeQuoteStep?.source, + translate, ], ) diff --git a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx index 78812f9efe8..1b0aafaadef 100644 --- a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx @@ -411,6 +411,7 @@ export const TradeInput = memo(() => { slippageDecimalPercentage={slippageDecimal} swapperName={activeSwapperName ?? ''} defaultIsOpen={true} + swapSource={tradeQuoteStep?.source} /> ) : null} {isModeratePriceImpact && ( @@ -464,6 +465,7 @@ export const TradeInput = memo(() => { slippageDecimal, totalNetworkFeeFiatPrecision, totalProtocolFees, + tradeQuoteStep?.source, ], ) diff --git a/src/components/TabMenu/TabMenu.tsx b/src/components/TabMenu/TabMenu.tsx index d2d3de6440c..52e73fe41dd 100644 --- a/src/components/TabMenu/TabMenu.tsx +++ b/src/components/TabMenu/TabMenu.tsx @@ -16,7 +16,7 @@ export type TabItem = { } const flexDirTabs: ResponsiveValue = { base: 'column', md: 'row' } -const navItemPadding = { base: 6, '2xl': 8 } +const navItemPadding = { base: 4, '2xl': 8 } const navCss = { '&::-webkit-scrollbar': { display: 'none', diff --git a/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts b/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts index d2bd607c123..871009797a7 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts @@ -50,10 +50,10 @@ const expectedQuoteResponse: Omit[] = [ affiliateBps: '0', potentialAffiliateBps: '0', isStreaming: false, - rate: '137845.94361267605633802817', + rate: '144114.94366197183098591549', data: '0x', router: '0x3624525075b88B24ecc29CE226b0CEc1fFcB6976', - memo: '=:ETH.ETH:0x32DBc9Cf9E8FbCebE1e0a2ecF05Ed86Ca3096Cb6:9360638:ss:0', + memo: '=:ETH.ETH:0x32DBc9Cf9E8FbCebE1e0a2ecF05Ed86Ca3096Cb6:9786345:ss:0', tradeType: TradeType.L1ToL1, steps: [ { @@ -61,7 +61,7 @@ const expectedQuoteResponse: Omit[] = [ allowanceContract: '0x3624525075b88B24ecc29CE226b0CEc1fFcB6976', sellAmountIncludingProtocolFeesCryptoBaseUnit: '713014679420', buyAmountBeforeFeesCryptoBaseUnit: '114321610000000000', - buyAmountAfterFeesCryptoBaseUnit: '97870619965000000', + buyAmountAfterFeesCryptoBaseUnit: '102321610000000000', feeData: { protocolFees: { [ETH.assetId]: { @@ -72,7 +72,7 @@ const expectedQuoteResponse: Omit[] = [ }, networkFeeCryptoBaseUnit: '400000', }, - rate: '137845.94361267605633802817', + rate: '144114.94366197183098591549', source: SwapperName.Thorchain, buyAsset: ETH, sellAsset: FOX_MAINNET, @@ -85,10 +85,10 @@ const expectedQuoteResponse: Omit[] = [ affiliateBps: '0', potentialAffiliateBps: '0', isStreaming: true, - rate: '151555.07377464788732394366', + rate: '158199.45070422535211267606', data: '0x', router: '0x3624525075b88B24ecc29CE226b0CEc1fFcB6976', - memo: '=:ETH.ETH:0x32DBc9Cf9E8FbCebE1e0a2ecF05Ed86Ca3096Cb6:10291578/10/0:ss:0', + memo: '=:ETH.ETH:0x32DBc9Cf9E8FbCebE1e0a2ecF05Ed86Ca3096Cb6:0/10/0:ss:0', tradeType: TradeType.L1ToL1, steps: [ { @@ -96,7 +96,7 @@ const expectedQuoteResponse: Omit[] = [ allowanceContract: '0x3624525075b88B24ecc29CE226b0CEc1fFcB6976', sellAmountIncludingProtocolFeesCryptoBaseUnit: '713014679420', buyAmountBeforeFeesCryptoBaseUnit: '124321610000000000', - buyAmountAfterFeesCryptoBaseUnit: '107604102380000000', + buyAmountAfterFeesCryptoBaseUnit: '112321610000000000', feeData: { protocolFees: { [ETH.assetId]: { @@ -107,7 +107,7 @@ const expectedQuoteResponse: Omit[] = [ }, networkFeeCryptoBaseUnit: '400000', }, - rate: '151555.07377464788732394366', + rate: '158199.45070422535211267606', source: `${SwapperName.Thorchain} • Streaming`, buyAsset: ETH, sellAsset: FOX_MAINNET, @@ -170,6 +170,8 @@ describe('getTradeQuote', () => { if ((url as string).includes('streaming_interval')) { mockThorQuote.data.expected_amount_out = '11232161' mockThorQuote.data.fees.slippage_bps = 420 + mockThorQuote.data.memo = + '=:ETH.ETH:0x32DBc9Cf9E8FbCebE1e0a2ecF05Ed86Ca3096Cb6:0/10/0:ss:0' } return Promise.resolve(Ok(mockThorQuote)) diff --git a/src/lib/swapper/swappers/ThorchainSwapper/utils/addSlippageToMemo.ts b/src/lib/swapper/swappers/ThorchainSwapper/utils/addSlippageToMemo.ts index 6cc111131a0..bb919df803f 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/utils/addSlippageToMemo.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/utils/addSlippageToMemo.ts @@ -2,49 +2,41 @@ import type { ChainId } from '@shapeshiftoss/caip' import { BigNumber, bn } from 'lib/bignumber/bignumber' import { subtractBasisPointAmount } from 'state/slices/tradeQuoteSlice/utils' -import { DEFAULT_STREAMING_NUM_SWAPS, LIMIT_PART_DELIMITER, MEMO_PART_DELIMITER } from './constants' +import { MEMO_PART_DELIMITER } from './constants' import { assertIsValidMemo } from './makeSwapMemo/assertIsValidMemo' export const addSlippageToMemo = ({ expectedAmountOutThorBaseUnit, - affiliateFeesThorBaseUnit, quotedMemo, slippageBps, isStreaming, chainId, affiliateBps, - streamingInterval, }: { expectedAmountOutThorBaseUnit: string - affiliateFeesThorBaseUnit: string quotedMemo: string | undefined slippageBps: BigNumber.Value chainId: ChainId affiliateBps: string isStreaming: boolean - streamingInterval: number }) => { if (!quotedMemo) throw new Error('no memo provided') + // always use TC auto stream quote (0 limit = 5bps - 50bps, sometimes up to 100bps) + // see: https://discord.com/channels/838986635756044328/1166265575941619742/1166500062101250100 + if (isStreaming) return quotedMemo + // the missing element is the original limit with (optional, missing) streaming parameters const [prefix, pool, address, , affiliate, memoAffiliateBps] = quotedMemo.split(MEMO_PART_DELIMITER) const limitWithManualSlippage = subtractBasisPointAmount( - bn(expectedAmountOutThorBaseUnit) - .minus(affiliateFeesThorBaseUnit) - .toFixed(0, BigNumber.ROUND_DOWN), + bn(expectedAmountOutThorBaseUnit).toFixed(0, BigNumber.ROUND_DOWN), slippageBps, BigNumber.ROUND_DOWN, ) - const updatedLimitComponent = isStreaming - ? [limitWithManualSlippage, streamingInterval, DEFAULT_STREAMING_NUM_SWAPS].join( - LIMIT_PART_DELIMITER, - ) - : [limitWithManualSlippage] - - const memo = [prefix, pool, address, updatedLimitComponent, affiliate, memoAffiliateBps].join( + const memo = [prefix, pool, address, limitWithManualSlippage, affiliate, memoAffiliateBps].join( MEMO_PART_DELIMITER, ) diff --git a/src/lib/swapper/swappers/ThorchainSwapper/utils/getL1quote.ts b/src/lib/swapper/swappers/ThorchainSwapper/utils/getL1quote.ts index 56eca694d64..ba4958c5e71 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/utils/getL1quote.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/utils/getL1quote.ts @@ -22,10 +22,7 @@ import { assertGetCosmosSdkChainAdapter } from 'lib/utils/cosmosSdk' import { assertGetEvmChainAdapter } from 'lib/utils/evm' import { THOR_PRECISION } from 'lib/utils/thorchain/constants' import { assertGetUtxoChainAdapter } from 'lib/utils/utxo' -import { - convertDecimalPercentageToBasisPoints, - subtractBasisPointAmount, -} from 'state/slices/tradeQuoteSlice/utils' +import { convertDecimalPercentageToBasisPoints } from 'state/slices/tradeQuoteSlice/utils' import { THORCHAIN_STREAM_SWAP_SOURCE } from '../constants' import type { @@ -62,7 +59,7 @@ export const getL1quote = async ( const inputSlippageBps = convertDecimalPercentageToBasisPoints( slippageTolerancePercentage ?? getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Thorchain), - ).toString() + ) const maybeSwapQuote = await getQuote({ sellAsset, @@ -115,16 +112,15 @@ export const getL1quote = async ( const getRouteValues = (quote: ThornodeQuoteResponseSuccess, isStreaming: boolean) => ({ source: isStreaming ? THORCHAIN_STREAM_SWAP_SOURCE : SwapperName.Thorchain, quote, - // expected receive amount after slippage (no affiliate_fee or liquidity_fee taken out of this value) - // TODO: slippage is currently being applied on expected_amount_out which is emit_asset - outbound_fee, - // should slippage actually be applied on emit_asset? - expectedAmountOutThorBaseUnit: subtractBasisPointAmount( - quote.expected_amount_out, - quote.fees.slippage_bps, - ), + // don't take affiliate fee into account, this will be displayed as a separate line item + expectedAmountOutThorBaseUnit: bnOrZero(quote.expected_amount_out) + .plus(bnOrZero(quote.fees.affiliate)) + .toFixed(), isStreaming, affiliateBps: quote.fees.affiliate === '0' ? '0' : requestedAffiliateBps, - potentialAffiliateBps, + // always use TC auto stream quote (0 limit = 5bps - 50bps, sometimes up to 100bps) + // see: https://discord.com/channels/838986635756044328/1166265575941619742/1166500062101250100 + slippageBps: isStreaming ? bn(0) : inputSlippageBps, estimatedExecutionTimeMs: quote.total_swap_seconds ? 1000 * quote.total_swap_seconds : undefined, @@ -194,20 +190,18 @@ export const getL1quote = async ( isStreaming, estimatedExecutionTimeMs, affiliateBps, - potentialAffiliateBps, + slippageBps, }): Promise => { const rate = getRouteRate(expectedAmountOutThorBaseUnit) const buyAmountBeforeFeesCryptoBaseUnit = getRouteBuyAmount(quote) const updatedMemo = addSlippageToMemo({ expectedAmountOutThorBaseUnit, - affiliateFeesThorBaseUnit: quote.fees.affiliate, quotedMemo: quote.memo, - slippageBps: inputSlippageBps, + slippageBps, chainId: sellAsset.chainId, affiliateBps, isStreaming, - streamingInterval, }) const { data, router } = await getEvmThorTxInfo({ sellAsset, @@ -282,20 +276,18 @@ export const getL1quote = async ( isStreaming, estimatedExecutionTimeMs, affiliateBps, - potentialAffiliateBps, + slippageBps, }): Promise => { const rate = getRouteRate(expectedAmountOutThorBaseUnit) const buyAmountBeforeFeesCryptoBaseUnit = getRouteBuyAmount(quote) const updatedMemo = addSlippageToMemo({ expectedAmountOutThorBaseUnit, - affiliateFeesThorBaseUnit: quote.fees.affiliate, quotedMemo: quote.memo, - slippageBps: inputSlippageBps, + slippageBps, isStreaming, chainId: sellAsset.chainId, affiliateBps, - streamingInterval, }) const { vault, opReturnData, pubkey } = await getUtxoThorTxInfo({ sellAsset, @@ -376,7 +368,7 @@ export const getL1quote = async ( isStreaming, estimatedExecutionTimeMs, affiliateBps, - potentialAffiliateBps, + slippageBps, }): ThorTradeUtxoOrCosmosQuote => { const rate = getRouteRate(expectedAmountOutThorBaseUnit) const buyAmountBeforeFeesCryptoBaseUnit = getRouteBuyAmount(quote) @@ -389,13 +381,11 @@ export const getL1quote = async ( const updatedMemo = addSlippageToMemo({ expectedAmountOutThorBaseUnit, - affiliateFeesThorBaseUnit: quote.fees.affiliate, quotedMemo: quote.memo, - slippageBps: inputSlippageBps, + slippageBps, isStreaming, chainId: sellAsset.chainId, affiliateBps, - streamingInterval, }) return { diff --git a/src/lib/swapper/swappers/ThorchainSwapper/utils/longTailHelpers.ts b/src/lib/swapper/swappers/ThorchainSwapper/utils/longTailHelpers.ts index f6c38c7871b..d0e0b273084 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/utils/longTailHelpers.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/utils/longTailHelpers.ts @@ -39,16 +39,16 @@ export function getTradeType( buyPoolId: string | undefined, ): TradeType | undefined { switch (true) { + case !!sellAssetPool && !!buyAssetPool: + case !!buyAssetPool && !sellAssetPool && sellPoolId === 'THOR.RUNE': + case !!sellAssetPool && !buyAssetPool && buyPoolId !== 'THOR.RUNE': + return TradeType.L1ToL1 case !sellAssetPool && !buyAssetPool: return TradeType.LongTailToLongTail case !sellAssetPool && !!buyAssetPool: return TradeType.LongTailToL1 case !!sellAssetPool && !buyAssetPool: return TradeType.L1ToLongTail - case !!sellAssetPool && !!buyAssetPool: - case !!buyAssetPool && !sellAssetPool && sellPoolId === 'THOR.RUNE': - case !!sellAssetPool && !buyAssetPool && buyPoolId !== 'THOR.RUNE': - return TradeType.L1ToL1 default: return undefined } diff --git a/src/lib/utils/thorchain/lending/types.ts b/src/lib/utils/thorchain/lending/types.ts index 2155f35cb02..3bcf35dfe20 100644 --- a/src/lib/utils/thorchain/lending/types.ts +++ b/src/lib/utils/thorchain/lending/types.ts @@ -80,3 +80,39 @@ export type BorrowersResponseError = { } export type BorrowersResponse = BorrowersResponseSuccess | BorrowersResponseError + +export type LendingQuoteOpen = { + quoteCollateralAmountCryptoPrecision: string + quoteCollateralAmountFiatUserCurrency: string + quoteDebtAmountUserCurrency: string + quoteBorrowedAmountCryptoPrecision: string + quoteBorrowedAmountUserCurrency: string + quoteCollateralizationRatioPercentDecimal: string + quoteSlippageBorrowedAssetCryptoPrecision: string + quoteTotalFeesFiatUserCurrency: string + quoteInboundAddress: string + quoteMemo: string + quoteExpiry: number +} + +export type LendingQuoteClose = { + quoteLoanCollateralDecreaseCryptoPrecision: string + quoteLoanCollateralDecreaseFiatUserCurrency: string + quoteDebtRepaidAmountUserCurrency: string + quoteWithdrawnAmountAfterFeesCryptoPrecision: string + quoteWithdrawnAmountAfterFeesUserCurrency: string + quoteSlippageWithdrawndAssetCryptoPrecision: string + quoteTotalFeesFiatUserCurrency: string + quoteInboundAddress: string + quoteMemo: string + repaymentAmountCryptoPrecision: string | null + quoteExpiry: number +} + +export const isLendingQuoteOpen = ( + quote: LendingQuoteOpen | LendingQuoteClose | null, +): quote is LendingQuoteOpen => Boolean(quote && 'quoteBorrowedAmountCryptoPrecision' in quote) + +export const isLendingQuoteClose = ( + quote: LendingQuoteOpen | LendingQuoteClose | null, +): quote is LendingQuoteClose => Boolean(quote && 'quoteDebtRepaidAmountUserCurrency' in quote) diff --git a/src/pages/Lending/AvailablePools.tsx b/src/pages/Lending/AvailablePools.tsx index 63dd7f55640..86d0455786b 100644 --- a/src/pages/Lending/AvailablePools.tsx +++ b/src/pages/Lending/AvailablePools.tsx @@ -19,7 +19,20 @@ import { usePoolDataQuery } from './hooks/usePoolDataQuery' export const lendingRowGrid: GridProps['gridTemplateColumns'] = { base: 'minmax(150px, 1fr) repeat(1, minmax(40px, max-content))', - md: '200px repeat(5, 1fr)', + lg: '200px repeat(5, 1fr)', +} +const mobileDisplay = { + base: 'none', + lg: 'flex', +} + +const mobilePadding = { + base: 4, + lg: 0, +} +const listMargin = { + base: 0, + lg: -4, } type LendingPoolButtonProps = { @@ -58,7 +71,7 @@ const LendingPoolButton = ({ asset, onPoolClick }: LendingPoolButtonProps) => { onClick={handlePoolClick} > - + @@ -66,7 +79,7 @@ const LendingPoolButton = ({ asset, onPoolClick }: LendingPoolButtonProps) => { - + @@ -75,10 +88,10 @@ const LendingPoolButton = ({ asset, onPoolClick }: LendingPoolButtonProps) => { symbol={asset.symbol} /> - + - + {poolData?.totalBorrowers ?? '0'} @@ -115,25 +128,36 @@ export const AvailablePools = () => { color='text.subtle' fontWeight='bold' fontSize='sm' + px={mobilePadding} > - - - - - - + + + + + + + + + + + - - - - - - + + + + + + + + + + + - {lendingRows} + {lendingRows} ) diff --git a/src/pages/Lending/Pool/Pool.tsx b/src/pages/Lending/Pool/Pool.tsx index ea54e8d430b..c39d442e25d 100644 --- a/src/pages/Lending/Pool/Pool.tsx +++ b/src/pages/Lending/Pool/Pool.tsx @@ -31,12 +31,16 @@ import { RawText, Text } from 'components/Text' import { useRouteAssetId } from 'hooks/useRouteAssetId/useRouteAssetId' import type { Asset } from 'lib/asset-service' import { BigNumber, bnOrZero } from 'lib/bignumber/bignumber' +import { + isLendingQuoteClose, + isLendingQuoteOpen, + type LendingQuoteClose, + type LendingQuoteOpen, +} from 'lib/utils/thorchain/lending/types' import { selectAssetById } from 'state/slices/selectors' import { useAppSelector } from 'state/store' -import { useLendingQuoteCloseQuery } from '../hooks/useLendingCloseQuery' import { useLendingPositionData } from '../hooks/useLendingPositionData' -import { useLendingQuoteOpenQuery } from '../hooks/useLendingQuoteQuery' import { useRepaymentLockData } from '../hooks/useRepaymentLockData' import { Borrow } from './components/Borrow/Borrow' import { Faq } from './components/Faq' @@ -47,6 +51,7 @@ import { Repay } from './components/Repay/Repay' const containerPadding = { base: 6, '2xl': 8 } const tabSelected = { color: 'text.base' } const maxWidth = { base: '100%', md: '450px' } +const responsiveFlex = { base: 'auto', lg: 1 } const PoolHeader = () => { const translate = useTranslate() const history = useHistory() @@ -76,7 +81,7 @@ const PoolHeader = () => { ) } -const flexDirPool: ResponsiveValue = { base: 'column', lg: 'row' } +const flexDirPool: ResponsiveValue = { base: 'column-reverse', lg: 'row' } type MatchParams = { poolAccountId?: AccountId @@ -92,7 +97,9 @@ const RepaymentLockComponentWithValue = ({ isLoaded, value }: AmountProps & Skel return ( - {isRepaymentLocked ? `${value} days` : translate('lending.unlocked')} + {isRepaymentLocked + ? translate('lending.repaymentDays', { numDays: value }) + : translate('lending.unlocked')} ) @@ -102,6 +109,9 @@ export const Pool = () => { const { poolAccountId } = useParams() const [stepIndex, setStepIndex] = useState(0) const [borrowTxid, setBorrowTxid] = useState(null) + const [confirmedQuote, setConfirmedQuote] = useState( + null, + ) const [repayTxid, setRepayTxid] = useState(null) const [collateralAccountId, setCollateralAccountId] = useState(poolAccountId ?? '') const [borrowAsset, setBorrowAsset] = useState(null) @@ -139,64 +149,20 @@ export const Pool = () => { filters: { mutationKey: [borrowTxid] }, select: mutation => mutation.state.status, }) - const isBorrowPending = borrowMutationStatus?.[0] === 'pending' const isBorrowUpdated = borrowMutationStatus?.[0] === 'success' const repayMutationStatus = useMutationState({ filters: { mutationKey: [repayTxid] }, select: mutation => mutation.state.status, }) - - const isRepayPending = repayMutationStatus?.[0] === 'pending' const isRepayUpdated = repayMutationStatus?.[0] === 'success' const { data: lendingPositionData, isLoading: isLendingPositionDataLoading } = useLendingPositionData({ assetId: poolAssetId, accountId: collateralAccountId, - skip: isBorrowPending || isRepayPending, }) - const useLendingQuoteQueryArgs = useMemo( - () => ({ - collateralAssetId: poolAssetId, - collateralAccountId, - borrowAccountId, - borrowAssetId: borrowAsset?.assetId ?? '', - depositAmountCryptoPrecision: depositAmountCryptoPrecision ?? '0', - }), - [ - poolAssetId, - collateralAccountId, - borrowAccountId, - borrowAsset?.assetId, - depositAmountCryptoPrecision, - ], - ) - - const { data: lendingQuoteOpenData, isSuccess: isLendingQuoteSuccess } = - useLendingQuoteOpenQuery(useLendingQuoteQueryArgs) - - const useLendingQuoteCloseQueryArgs = useMemo( - () => ({ - collateralAssetId: poolAssetId, - collateralAccountId, - repaymentAssetId: repaymentAsset?.assetId ?? '', - repaymentPercent, - repaymentAccountId, - }), - [ - collateralAccountId, - poolAssetId, - repaymentAccountId, - repaymentAsset?.assetId, - repaymentPercent, - ], - ) - - const { data: lendingQuoteCloseData, isSuccess: isLendingQuoteCloseSuccess } = - useLendingQuoteCloseQuery(useLendingQuoteCloseQueryArgs) - const collateralBalanceComponent = useMemo( () => ( { [asset?.symbol, lendingPositionData?.collateralBalanceCryptoPrecision], ) const newCollateralCrypto = useMemo(() => { - if (isBorrowUpdated || isRepayUpdated) return {} + if (confirmedQuote && (isBorrowUpdated || isRepayUpdated)) return {} - if (stepIndex === 0 && isLendingQuoteSuccess && lendingQuoteOpenData) + if (stepIndex === 0 && lendingPositionData && isLendingQuoteOpen(confirmedQuote)) return { newValue: { value: bnOrZero(lendingPositionData?.collateralBalanceCryptoPrecision) - .plus(lendingQuoteOpenData?.quoteCollateralAmountCryptoPrecision) + .plus(confirmedQuote.quoteCollateralAmountCryptoPrecision) .toFixed(), }, } - if (stepIndex === 1 && isLendingQuoteCloseSuccess && lendingQuoteCloseData) + if (stepIndex === 1 && lendingPositionData && isLendingQuoteClose(confirmedQuote)) return { newValue: { value: bnOrZero(lendingPositionData?.collateralBalanceCryptoPrecision) - .minus(lendingQuoteCloseData?.quoteLoanCollateralDecreaseCryptoPrecision) + .minus(confirmedQuote.quoteLoanCollateralDecreaseCryptoPrecision) .toFixed(), }, } return {} - }, [ - isBorrowUpdated, - isRepayUpdated, - stepIndex, - isLendingQuoteSuccess, - lendingQuoteOpenData, - lendingPositionData?.collateralBalanceCryptoPrecision, - isLendingQuoteCloseSuccess, - lendingQuoteCloseData, - ]) + }, [confirmedQuote, isBorrowUpdated, isRepayUpdated, stepIndex, lendingPositionData]) const collateralValueComponent = useMemo( () => ( @@ -251,34 +208,27 @@ export const Pool = () => { ) const newCollateralFiat = useMemo(() => { - if (isBorrowUpdated || isRepayUpdated) return {} + if (confirmedQuote && (isBorrowUpdated || isRepayUpdated)) return {} - if (stepIndex === 0 && lendingQuoteOpenData && lendingPositionData) + if (stepIndex === 0 && lendingPositionData && isLendingQuoteOpen(confirmedQuote)) return { newValue: { value: bnOrZero(lendingPositionData.collateralBalanceFiatUserCurrency) - .plus(lendingQuoteOpenData.quoteCollateralAmountFiatUserCurrency) + .plus(confirmedQuote.quoteCollateralAmountFiatUserCurrency) .toFixed(), }, } - if (stepIndex === 1 && lendingQuoteCloseData && lendingPositionData) + if (stepIndex === 1 && lendingPositionData && isLendingQuoteClose(confirmedQuote)) return { newValue: { value: bnOrZero(lendingPositionData.collateralBalanceFiatUserCurrency) - .minus(lendingQuoteCloseData.quoteLoanCollateralDecreaseFiatUserCurrency) + .minus(confirmedQuote.quoteLoanCollateralDecreaseFiatUserCurrency) .toFixed(), }, } return {} - }, [ - isBorrowUpdated, - isRepayUpdated, - lendingPositionData, - lendingQuoteCloseData, - lendingQuoteOpenData, - stepIndex, - ]) + }, [confirmedQuote, isBorrowUpdated, isRepayUpdated, lendingPositionData, stepIndex]) const debtBalanceComponent = useMemo( () => ( @@ -292,22 +242,22 @@ export const Pool = () => { ) const newDebt = useMemo(() => { - if (isBorrowUpdated || isRepayUpdated) return {} + if (confirmedQuote && (isBorrowUpdated || isRepayUpdated)) return {} - if (stepIndex === 0 && lendingQuoteOpenData && lendingPositionData) + if (stepIndex === 0 && lendingPositionData && isLendingQuoteOpen(confirmedQuote)) return { newValue: { value: bnOrZero(lendingPositionData.debtBalanceFiatUserCurrency) - .plus(lendingQuoteOpenData.quoteDebtAmountUserCurrency) + .plus(confirmedQuote.quoteDebtAmountUserCurrency) .toFixed(), }, } - if (stepIndex === 1 && lendingQuoteCloseData && lendingPositionData) + if (stepIndex === 1 && lendingPositionData && isLendingQuoteClose(confirmedQuote)) return { newValue: { value: BigNumber.max( bnOrZero(lendingPositionData.debtBalanceFiatUserCurrency).minus( - lendingQuoteCloseData.quoteDebtRepaidAmountUsd, + confirmedQuote.quoteDebtRepaidAmountUserCurrency, ), 0, ).toFixed(), @@ -315,14 +265,7 @@ export const Pool = () => { } return {} - }, [ - isBorrowUpdated, - isRepayUpdated, - lendingPositionData, - lendingQuoteCloseData, - lendingQuoteOpenData, - stepIndex, - ]) + }, [confirmedQuote, isBorrowUpdated, isRepayUpdated, lendingPositionData, stepIndex]) const repaymentLockComponent = useMemo( () => ( @@ -335,15 +278,9 @@ export const Pool = () => { ) const newRepaymentLock = useMemo(() => { - if (isBorrowUpdated || isRepayUpdated) return {} + if (confirmedQuote && (isBorrowUpdated || isRepayUpdated)) return {} - if ( - stepIndex === 0 && - isLendingQuoteSuccess && - lendingQuoteOpenData && - isDefaultRepaymentLockSuccess && - defaultRepaymentLock - ) + if (stepIndex === 0 && confirmedQuote && isDefaultRepaymentLockSuccess && defaultRepaymentLock) return { newValue: { value: defaultRepaymentLock, @@ -351,11 +288,10 @@ export const Pool = () => { } return {} }, [ + confirmedQuote, isBorrowUpdated, isRepayUpdated, stepIndex, - isLendingQuoteSuccess, - lendingQuoteOpenData, isDefaultRepaymentLockSuccess, defaultRepaymentLock, ]) @@ -373,13 +309,13 @@ export const Pool = () => { - + { toolTipLabel={translate('lending.collateralValueDescription')} component={collateralValueComponent} isLoading={isLendingPositionDataLoading} - flex={1} + flex={responsiveFlex} {...newCollateralFiat} /> - + { toolTipLabel={translate('lending.repaymentLockDescription')} component={repaymentLockComponent} isLoading={isLendingPositionDataLoading} - flex={1} + flex={responsiveFlex} {...newRepaymentLock} /> @@ -449,6 +385,8 @@ export const Pool = () => { onBorrowAccountIdChange={setBorrowAccountId} txId={borrowTxid} setTxid={setBorrowTxid} + confirmedQuote={isLendingQuoteOpen(confirmedQuote) ? confirmedQuote : null} + setConfirmedQuote={setConfirmedQuote} /> @@ -464,6 +402,8 @@ export const Pool = () => { onRepaymentAccountIdChange={setRepaymentAccountId} txId={repayTxid} setTxid={setRepayTxid} + confirmedQuote={isLendingQuoteClose(confirmedQuote) ? confirmedQuote : null} + setConfirmedQuote={setConfirmedQuote} /> diff --git a/src/pages/Lending/Pool/components/Borrow/Borrow.tsx b/src/pages/Lending/Pool/components/Borrow/Borrow.tsx index 5db562ea1dc..5824ad4d669 100644 --- a/src/pages/Lending/Pool/components/Borrow/Borrow.tsx +++ b/src/pages/Lending/Pool/components/Borrow/Borrow.tsx @@ -5,6 +5,7 @@ import { MemoryRouter, Route, Switch, useLocation } from 'react-router' import { useRouteAssetId } from 'hooks/useRouteAssetId/useRouteAssetId' import type { Asset } from 'lib/asset-service' import { bn, bnOrZero } from 'lib/bignumber/bignumber' +import type { LendingQuoteOpen } from 'lib/utils/thorchain/lending/types' import { selectMarketDataById } from 'state/slices/selectors' import { useAppSelector } from 'state/store' @@ -36,6 +37,8 @@ type BorrowProps = { setBorrowAsset: (asset: Asset | null) => void txId: string | null setTxid: (txId: string | null) => void + confirmedQuote: LendingQuoteOpen | null + setConfirmedQuote: (quote: LendingQuoteOpen | null) => void } export const Borrow = ({ borrowAsset, @@ -49,6 +52,8 @@ export const Borrow = ({ setCryptoDepositAmount, txId, setTxid, + confirmedQuote, + setConfirmedQuote, }: BorrowProps) => { const [fiatDepositAmount, setFiatDepositAmount] = useState(null) @@ -87,6 +92,7 @@ export const Borrow = ({ setBorrowAsset={setBorrowAsset} collateralAssetId={collateralAssetId} cryptoDepositAmount={depositAmountCryptoPrecision} + setDepositAmount={setCryptoDepositAmount} fiatDepositAmount={fiatDepositAmount} onDepositAmountChange={handleDepositAmountChange} collateralAccountId={collateralAccountId} @@ -96,6 +102,8 @@ export const Borrow = ({ onBorrowAccountIdChange={handleBorrowAccountIdChange} txId={txId} setTxid={setTxid} + confirmedQuote={confirmedQuote} + setConfirmedQuote={setConfirmedQuote} /> ) @@ -106,6 +114,7 @@ type BorrowRoutesProps = { setBorrowAsset: (asset: Asset | null) => void collateralAssetId: AssetId cryptoDepositAmount: string | null + setDepositAmount: (amount: string | null) => void fiatDepositAmount: string | null onDepositAmountChange: (value: string, isFiat?: boolean) => void isAccountSelectionDisabled?: boolean @@ -115,6 +124,8 @@ type BorrowRoutesProps = { onBorrowAccountIdChange: (accountId: AccountId) => void txId: string | null setTxid: (txId: string | null) => void + confirmedQuote: LendingQuoteOpen | null + setConfirmedQuote: (quote: LendingQuoteOpen | null) => void } const BorrowRoutes = memo( @@ -123,6 +134,7 @@ const BorrowRoutes = memo( setBorrowAsset, collateralAssetId, cryptoDepositAmount, + setDepositAmount, fiatDepositAmount, isAccountSelectionDisabled, onDepositAmountChange, @@ -132,6 +144,8 @@ const BorrowRoutes = memo( onBorrowAccountIdChange: handleBorrowAccountIdChange, txId, setTxid, + confirmedQuote, + setConfirmedQuote, }: BorrowRoutesProps) => { const location = useLocation() @@ -149,6 +163,8 @@ const BorrowRoutes = memo( onDepositAmountChange={onDepositAmountChange} borrowAsset={borrowAsset} setBorrowAsset={setBorrowAsset} + confirmedQuote={confirmedQuote} + setConfirmedQuote={setConfirmedQuote} /> ), [ @@ -163,6 +179,8 @@ const BorrowRoutes = memo( onDepositAmountChange, borrowAsset, setBorrowAsset, + confirmedQuote, + setConfirmedQuote, ], ) @@ -181,21 +199,27 @@ const BorrowRoutes = memo( ), [ collateralAssetId, cryptoDepositAmount, + setDepositAmount, borrowAccountId, collateralAccountId, borrowAsset, txId, setTxid, + confirmedQuote, + setConfirmedQuote, ], ) diff --git a/src/pages/Lending/Pool/components/Borrow/BorrowConfirm.tsx b/src/pages/Lending/Pool/components/Borrow/BorrowConfirm.tsx index cc14a63ef8d..b05c5151da9 100644 --- a/src/pages/Lending/Pool/components/Borrow/BorrowConfirm.tsx +++ b/src/pages/Lending/Pool/components/Borrow/BorrowConfirm.tsx @@ -13,6 +13,7 @@ import { fromAssetId } from '@shapeshiftoss/caip' import { FeeDataKey } from '@shapeshiftoss/chain-adapters' import { TxStatus } from '@shapeshiftoss/unchained-client' import { useMutation, useMutationState } from '@tanstack/react-query' +import dayjs from 'dayjs' import { utils } from 'ethers' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslate } from 'react-polyglot' @@ -27,12 +28,15 @@ import { Row } from 'components/Row/Row' import { SlideTransition } from 'components/SlideTransition' import { RawText, Text } from 'components/Text' import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingleton' +import { queryClient } from 'context/QueryClientProvider/queryClient' import { getSupportedEvmChainIds } from 'hooks/useEvm/useEvm' +import { useInterval } from 'hooks/useInterval/useInterval' import { useWallet } from 'hooks/useWallet/useWallet' import type { Asset } from 'lib/asset-service' import { bnOrZero } from 'lib/bignumber/bignumber' import { getThorchainFromAddress, waitForThorchainUpdate } from 'lib/utils/thorchain' import { getThorchainLendingPosition } from 'lib/utils/thorchain/lending' +import type { LendingQuoteOpen } from 'lib/utils/thorchain/lending/types' import { useLendingQuoteOpenQuery } from 'pages/Lending/hooks/useLendingQuoteQuery' import { useQuoteEstimatedFeesQuery } from 'pages/Lending/hooks/useQuoteEstimatedFees' import { @@ -49,11 +53,14 @@ import { BorrowRoutePaths } from './types' type BorrowConfirmProps = { collateralAssetId: AssetId depositAmount: string | null + setDepositAmount: (amount: string | null) => void collateralAccountId: AccountId borrowAccountId: AccountId borrowAsset: Asset | null txId: string | null setTxid: (txId: string | null) => void + confirmedQuote: LendingQuoteOpen | null + setConfirmedQuote: (quote: LendingQuoteOpen | null) => void } export const BorrowConfirm = ({ @@ -64,6 +71,9 @@ export const BorrowConfirm = ({ borrowAsset, txId, setTxid, + confirmedQuote, + setConfirmedQuote, + setDepositAmount, }: BorrowConfirmProps) => { const { state: { wallet }, @@ -79,13 +89,16 @@ export const BorrowConfirm = ({ ) const { mutateAsync } = useMutation({ mutationKey: [txId], - mutationFn: (_txId: string) => + mutationFn: async (_txId: string) => { // Ensuring we wait for the outbound Tx to exist // Else, the position will update before the borrowed asset is received and users will be confused - waitForThorchainUpdate({ txId: _txId, skipOutbound: false }).promise, + await waitForThorchainUpdate({ txId: _txId, skipOutbound: false }).promise + queryClient.invalidateQueries({ queryKey: ['thorchainLendingPosition'], exact: false }) + }, }) const [isLoanPending, setIsLoanPending] = useState(false) + const [isQuoteExpired, setIsQuoteExpired] = useState(false) const lendingMutationStatus = useMutationState({ filters: { mutationKey: [txId] }, select: mutation => mutation.state.status, @@ -93,6 +106,12 @@ export const BorrowConfirm = ({ const loanTxStatus = useMemo(() => lendingMutationStatus?.[0], [lendingMutationStatus]) + const swapStatus = useMemo(() => { + if (loanTxStatus === 'success') return TxStatus.Confirmed + if (loanTxStatus === 'pending') return TxStatus.Pending + return TxStatus.Unknown + }, [loanTxStatus]) + useEffect(() => { // don't start polling until we have a tx if (!txId) return @@ -107,23 +126,6 @@ export const BorrowConfirm = ({ }, [history]) const divider = useMemo(() => , []) - const useLendingQuoteQueryArgs = useMemo( - () => ({ - collateralAssetId, - collateralAccountId, - borrowAccountId, - borrowAssetId, - depositAmountCryptoPrecision: depositAmount ?? '0', - }), - [collateralAssetId, collateralAccountId, borrowAccountId, borrowAssetId, depositAmount], - ) - const { - data: lendingQuoteData, - isSuccess: isLendingQuoteSuccess, - isLoading: isLendingQuoteLoading, - isError: isLendingQuoteError, - } = useLendingQuoteOpenQuery(useLendingQuoteQueryArgs) - const chainAdapter = getChainAdapterManager().get(fromAssetId(collateralAssetId).chainId) const selectedCurrency = useAppSelector(selectSelectedCurrency) @@ -136,11 +138,28 @@ export const BorrowConfirm = ({ } = useQuoteEstimatedFeesQuery({ collateralAssetId, collateralAccountId, - borrowAccountId, - borrowAssetId: borrowAsset?.assetId ?? '', depositAmountCryptoPrecision: depositAmount ?? '0', + confirmedQuote, }) + const useLendingQuoteQueryArgs = useMemo( + () => ({ + // Refetching at confirm step should only be done programmatically with refetch if a quote expires and a user clicks "Refetch Quote" + enabled: false, + collateralAssetId, + collateralAccountId, + borrowAccountId, + borrowAssetId: borrowAsset?.assetId ?? '', + depositAmountCryptoPrecision: depositAmount ?? '0', + }), + [borrowAccountId, borrowAsset?.assetId, collateralAccountId, collateralAssetId, depositAmount], + ) + + const { refetch: refetchLendingQuote, isRefetching: isLendingQuoteRefetching } = + useLendingQuoteOpenQuery(useLendingQuoteQueryArgs) + + const isLendingQuoteSuccess = Boolean(confirmedQuote) + const collateralAccountFilter = useMemo( () => ({ accountId: collateralAccountId }), [collateralAccountId], @@ -149,9 +168,21 @@ export const BorrowConfirm = ({ selectPortfolioAccountMetadataByAccountId(state, collateralAccountFilter), ) const handleConfirm = useCallback(async () => { - if (loanTxStatus === 'pending' || loanTxStatus === 'success') { + if (!confirmedQuote) return + + if (isQuoteExpired) { + const { data: refetchedQuote } = await refetchLendingQuote() + setConfirmedQuote(refetchedQuote ?? null) + return + } + + if (loanTxStatus === 'pending') return // no-op + + if (loanTxStatus === 'success') { // Reset values when going back to input step setTxid(null) + setConfirmedQuote(null) + setDepositAmount(null) return history.push(BorrowRoutePaths.Input) } @@ -185,20 +216,20 @@ export const BorrowConfirm = ({ const sendInput: SendInput = { cryptoAmount: depositAmount ?? '0', assetId: collateralAssetId, - to: lendingQuoteData.quoteInboundAddress, + to: confirmedQuote.quoteInboundAddress, from, sendMax: false, accountId: collateralAccountId, memo: supportedEvmChainIds.includes(fromAssetId(collateralAssetId).chainId) - ? utils.hexlify(utils.toUtf8Bytes(lendingQuoteData.quoteMemo)) - : lendingQuoteData.quoteMemo, + ? utils.hexlify(utils.toUtf8Bytes(confirmedQuote.quoteMemo)) + : confirmedQuote.quoteMemo, amountFieldError: '', estimatedFees, feeType: FeeDataKey.Fast, fiatAmount: '', fiatSymbol: selectedCurrency, vanityAddress: '', - input: lendingQuoteData.quoteInboundAddress, + input: confirmedQuote.quoteInboundAddress, } if (!sendInput) throw new Error('Error building send input') @@ -214,6 +245,8 @@ export const BorrowConfirm = ({ return maybeTxId }, [ + confirmedQuote, + isQuoteExpired, loanTxStatus, collateralAssetId, depositAmount, @@ -225,12 +258,31 @@ export const BorrowConfirm = ({ collateralAccountId, estimatedFeesData, setTxid, + refetchLendingQuote, + setConfirmedQuote, + setDepositAmount, history, - lendingQuoteData?.quoteInboundAddress, - lendingQuoteData?.quoteMemo, selectedCurrency, ]) + useInterval(() => { + // This should never happen but it may + if (!confirmedQuote) return + + // Since we run this interval check every second, subtract a few seconds to avoid + // off-by-one on the last second as well as the main thread being overloaded and running slow + const quoteExpiryUnix = dayjs.unix(confirmedQuote.quoteExpiry).subtract(5, 'second').unix() + + const isExpired = dayjs.unix(quoteExpiryUnix).isBefore(dayjs()) + setIsQuoteExpired(isExpired) + }, 1000) + + const confirmTranslation = useMemo(() => { + if (isQuoteExpired) return 'lending.refetchQuote' + + return loanTxStatus === 'success' ? 'lending.borrowAgain' : 'lending.confirmAndBorrow' + }, [isQuoteExpired, loanTxStatus]) + if (!depositAmount) return null return ( @@ -249,14 +301,14 @@ export const BorrowConfirm = ({ sellIcon={collateralAsset?.icon ?? ''} buyColor={debtAsset?.color ?? ''} sellColor={collateralAsset?.color ?? ''} - status={TxStatus.Unknown} + status={swapStatus} px={6} mb={4} /> {translate('lending.transactionInfo')} - Send + {translate('common.send')} @@ -272,26 +324,26 @@ export const BorrowConfirm = ({ - - - {translate('common.receive')} - + + {translate('common.receive')} + + - - - + + + {translate('common.feesPlusSlippage')} @@ -299,7 +351,7 @@ export const BorrowConfirm = ({ {/* Actually defined at display time, see isLoaded above */} - + @@ -314,11 +366,12 @@ export const BorrowConfirm = ({ - {translate( - loanTxStatus === 'success' ? 'lending.borrowAgain' : 'lending.confirmAndBorrow', - )} + {translate(confirmTranslation)} diff --git a/src/pages/Lending/Pool/components/Borrow/BorrowInput.tsx b/src/pages/Lending/Pool/components/Borrow/BorrowInput.tsx index 19d960ef279..91ce33c2311 100644 --- a/src/pages/Lending/Pool/components/Borrow/BorrowInput.tsx +++ b/src/pages/Lending/Pool/components/Borrow/BorrowInput.tsx @@ -27,6 +27,7 @@ import { bnOrZero } from 'lib/bignumber/bignumber' import { fromBaseUnit, toBaseUnit } from 'lib/math' import { getThorchainFromAddress } from 'lib/utils/thorchain' import { getThorchainLendingPosition } from 'lib/utils/thorchain/lending' +import type { LendingQuoteOpen } from 'lib/utils/thorchain/lending/types' import { useGetEstimatedFeesQuery } from 'pages/Lending/hooks/useGetEstimatedFeesQuery' import { useIsSweepNeededQuery } from 'pages/Lending/hooks/useIsSweepNeededQuery' import { useLendingQuoteOpenQuery } from 'pages/Lending/hooks/useLendingQuoteQuery' @@ -61,8 +62,12 @@ type BorrowInputProps = { onBorrowAccountIdChange: (accountId: AccountId) => void borrowAsset: Asset | null setBorrowAsset: (asset: Asset) => void + confirmedQuote: LendingQuoteOpen | null + setConfirmedQuote: (quote: LendingQuoteOpen | null) => void } +const percentOptions = [0] + export const BorrowInput = ({ isAccountSelectionDisabled, collateralAssetId, @@ -75,6 +80,8 @@ export const BorrowInput = ({ onBorrowAccountIdChange: handleBorrowAccountIdChange, borrowAsset, setBorrowAsset, + confirmedQuote, + setConfirmedQuote, }: BorrowInputProps) => { const [fromAddress, setFromAddress] = useState(null) @@ -86,17 +93,17 @@ export const BorrowInput = ({ const { data: borrowAssets } = useLendingSupportedAssets({ type: 'borrow' }) + const collateralAsset = useAppSelector(state => selectAssetById(state, collateralAssetId)) + useEffect(() => { - if (!borrowAssets) return + if (!(collateralAsset && borrowAssets)) return + if (borrowAsset) return - setBorrowAsset(borrowAssets[0]) - }, [borrowAssets, setBorrowAsset]) + if (!borrowAsset) setBorrowAsset(collateralAsset) + }, [borrowAsset, borrowAssets, collateralAsset, setBorrowAsset]) - const collateralAsset = useAppSelector(state => selectAssetById(state, collateralAssetId)) const swapIcon = useMemo(() => , []) - const percentOptions = useMemo(() => [0], []) - const buyAssetSearch = useModal('buyAssetSearch') const handleBorrowAssetClick = useCallback(() => { buyAssetSearch.open({ @@ -153,9 +160,8 @@ export const BorrowInput = ({ } = useQuoteEstimatedFeesQuery({ collateralAssetId, collateralAccountId, - borrowAccountId, - borrowAssetId: borrowAsset?.assetId ?? '', depositAmountCryptoPrecision: depositAmountCryptoPrecision ?? '0', + confirmedQuote, }) const balanceFilter = useMemo( @@ -275,10 +281,42 @@ export const BorrowInput = ({ isEstimatedSweepFeesDataSuccess, ]) + const useLendingQuoteQueryArgs = useMemo( + () => ({ + collateralAssetId, + collateralAccountId, + borrowAccountId, + borrowAssetId: borrowAsset?.assetId ?? '', + depositAmountCryptoPrecision: depositAmountCryptoPrecision ?? '0', + }), + [ + borrowAccountId, + borrowAsset?.assetId, + collateralAccountId, + collateralAssetId, + depositAmountCryptoPrecision, + ], + ) + const { + data, + isLoading: isLendingQuoteLoading, + isRefetching: isLendingQuoteRefetching, + isSuccess: isLendingQuoteSuccess, + isError: isLendingQuoteError, + error: lendingQuoteError, + } = useLendingQuoteOpenQuery(useLendingQuoteQueryArgs) + + const lendingQuoteData = isLendingQuoteError ? null : data + + useEffect(() => { + setConfirmedQuote(lendingQuoteData ?? null) + }, [isLendingQuoteSuccess, lendingQuoteData, setConfirmedQuote]) + const onSubmit = useCallback(() => { + if (!lendingQuoteData) return if (!isSweepNeeded) return history.push(BorrowRoutePaths.Confirm) history.push(BorrowRoutePaths.Sweep) - }, [history, isSweepNeeded]) + }, [history, isSweepNeeded, lendingQuoteData]) const collateralAssetSelectComponent = useMemo(() => { return ( @@ -301,30 +339,6 @@ export const BorrowInput = ({ ) }, [borrowAsset?.assetId, handleAssetChange, handleBorrowAssetClick]) - const useLendingQuoteQueryArgs = useMemo( - () => ({ - collateralAssetId, - collateralAccountId, - borrowAccountId, - borrowAssetId: borrowAsset?.assetId ?? '', - depositAmountCryptoPrecision: depositAmountCryptoPrecision ?? '0', - }), - [ - borrowAccountId, - borrowAsset?.assetId, - collateralAccountId, - collateralAssetId, - depositAmountCryptoPrecision, - ], - ) - const { - data, - isLoading: isLendingQuoteLoading, - isSuccess: isLendingQuoteSuccess, - isError: isLendingQuoteError, - error: lendingQuoteError, - } = useLendingQuoteOpenQuery(useLendingQuoteQueryArgs) - const quoteErrorTranslation = useMemo(() => { if ( !hasEnoughBalanceForTx || @@ -350,8 +364,6 @@ export const BorrowInput = ({ lendingQuoteError?.message, ]) - const lendingQuoteData = isLendingQuoteError ? null : data - if (!(collateralAsset && borrowAsset && feeAsset)) return null return ( @@ -399,8 +411,8 @@ export const BorrowInput = ({ isReadOnly isSendMaxDisabled={false} percentOptions={percentOptions} - showInputSkeleton={isLendingQuoteLoading} - showFiatSkeleton={isLendingQuoteLoading} + showInputSkeleton={isLendingQuoteLoading || isLendingQuoteRefetching} + showFiatSkeleton={isLendingQuoteLoading || isLendingQuoteRefetching} label={translate('lending.borrow')} onAccountIdChange={handleBorrowAccountIdChange} formControlProps={formControlProps} @@ -409,6 +421,8 @@ export const BorrowInput = ({ /> {translate('common.slippage')} - + {translate('common.gasFee')} - + @@ -461,7 +479,7 @@ export const BorrowInput = ({ {translate('common.fees')} - + @@ -485,6 +503,7 @@ export const BorrowInput = ({ onClick={onSubmit} isLoading={ isLendingQuoteLoading || + isLendingQuoteRefetching || isEstimatedFeesDataLoading || isEstimatedSweepFeesDataLoading || isEstimatedSweepFeesDataLoading || @@ -494,6 +513,7 @@ export const BorrowInput = ({ bnOrZero(depositAmountCryptoPrecision).isZero() || isLendingQuoteError || isLendingQuoteLoading || + isLendingQuoteRefetching || quoteErrorTranslation || isEstimatedFeesDataError || isEstimatedFeesDataLoading, diff --git a/src/pages/Lending/Pool/components/LoanSummary.tsx b/src/pages/Lending/Pool/components/LoanSummary.tsx index 963ad24bb57..f410975ca1f 100644 --- a/src/pages/Lending/Pool/components/LoanSummary.tsx +++ b/src/pages/Lending/Pool/components/LoanSummary.tsx @@ -12,9 +12,13 @@ import { HelperTooltip } from 'components/HelperTooltip/HelperTooltip' import { Row } from 'components/Row/Row' import { RawText } from 'components/Text' import type { Asset } from 'lib/asset-service' -import { useLendingQuoteCloseQuery } from 'pages/Lending/hooks/useLendingCloseQuery' +import { + isLendingQuoteClose, + isLendingQuoteOpen, + type LendingQuoteClose, + type LendingQuoteOpen, +} from 'lib/utils/thorchain/lending/types' import { useLendingPositionData } from 'pages/Lending/hooks/useLendingPositionData' -import { useLendingQuoteOpenQuery } from 'pages/Lending/hooks/useLendingQuoteQuery' import { useRepaymentLockData } from 'pages/Lending/hooks/useRepaymentLockData' import { selectAssetById } from 'state/slices/assetsSlice/selectors' import { useAppSelector } from 'state/store' @@ -36,6 +40,7 @@ const FromToStack: React.FC = props => { type LoanSummaryProps = { isLoading?: boolean collateralAssetId: AssetId + confirmedQuote: LendingQuoteOpen | LendingQuoteClose | null } & StackProps & ( | { @@ -79,9 +84,9 @@ export const LoanSummary: React.FC = ({ repaymentAccountId, collateralAccountId, borrowAccountId, + confirmedQuote, ...rest }) => { - const isRepay = useMemo(() => Boolean(repayAmountCryptoPrecision), [repayAmountCryptoPrecision]) const translate = useTranslate() const collateralAsset = useAppSelector(state => selectAssetById(state, collateralAssetId)) @@ -93,48 +98,6 @@ export const LoanSummary: React.FC = ({ assetId: collateralAssetId, }) - const useLendingQuoteQueryArgs = useMemo( - () => ({ - collateralAssetId, - collateralAccountId, - borrowAccountId: borrowAccountId ?? '', - borrowAssetId: borrowAssetId ?? '', - depositAmountCryptoPrecision: depositAmountCryptoPrecision ?? '0', - }), - [ - collateralAssetId, - collateralAccountId, - borrowAccountId, - borrowAssetId, - depositAmountCryptoPrecision, - ], - ) - const { - data: lendingQuoteData, - isLoading: isLendingQuoteLoading, - isError: isLendingQuoteError, - } = useLendingQuoteOpenQuery(useLendingQuoteQueryArgs) - - const useLendingQuoteCloseQueryArgs = useMemo( - () => ({ - collateralAssetId, - repaymentAssetId: repaymentAsset?.assetId ?? '', - repaymentPercent: Number(repaymentPercent), - repaymentAccountId: repaymentAccountId ?? '', - collateralAccountId: collateralAccountId ?? '', - }), - [ - collateralAccountId, - collateralAssetId, - repaymentAccountId, - repaymentAsset?.assetId, - repaymentPercent, - ], - ) - - const { data: lendingQuoteCloseData, isLoading: isLendingQuoteCloseLoading } = - useLendingQuoteCloseQuery(useLendingQuoteCloseQueryArgs) - const useRepaymentLockDataArgs = useMemo( () => ({ assetId: collateralAssetId, accountId: collateralAccountId }), [collateralAccountId, collateralAssetId], @@ -146,7 +109,7 @@ export const LoanSummary: React.FC = ({ const { data: networkRepaymentLock, isLoading: isNetworkRepaymentLockLoading } = useRepaymentLockData(useRepaymentLockNetworkDataArgs) - if (!collateralAsset || isLendingQuoteError) return null + if (!collateralAsset || !confirmedQuote) return null return ( = ({ {...rest} > {translate('lending.loanInformation')} - {(bnOrZero(lendingQuoteCloseData?.quoteLoanCollateralDecreaseCryptoPrecision).gt(0) || - bnOrZero(lendingQuoteData?.quoteCollateralAmountCryptoPrecision).gt(0)) && ( + {(bnOrZero( + (confirmedQuote as LendingQuoteClose)?.quoteLoanCollateralDecreaseCryptoPrecision, + ).gt(0) || + bnOrZero((confirmedQuote as LendingQuoteOpen)?.quoteCollateralAmountCryptoPrecision).gt( + 0, + )) && ( {translate('lending.collateral')} @@ -172,8 +139,7 @@ export const LoanSummary: React.FC = ({ isLoaded={Boolean( !isLoading && !isLendingPositionDataLoading && - !isLendingQuoteLoading && - !isLendingQuoteCloseLoading && + confirmedQuote && lendingPositionData && lendingPositionData?.collateralBalanceCryptoPrecision, )} @@ -185,12 +151,12 @@ export const LoanSummary: React.FC = ({ symbol={collateralAsset.symbol} /> = ({ = ({ value={lendingPositionData?.debtBalanceFiatUserCurrency ?? '0'} /> = ({ - {!isRepay && ( + {isLendingQuoteOpen(confirmedQuote) && ( // This doesn't make sense for repayments - repayment lock shouldn't change when repaying, and will be zero'd out when fully repaying @@ -251,7 +212,7 @@ export const LoanSummary: React.FC = ({ )} - {!isRepay && ( + {isLendingQuoteOpen(confirmedQuote) && ( // This doesn't make sense for repayments - the collateralization ratio won't change here @@ -259,15 +220,10 @@ export const LoanSummary: React.FC = ({ @@ -280,12 +236,7 @@ export const LoanSummary: React.FC = ({ {translate('lending.healthy')} diff --git a/src/pages/Lending/Pool/components/PoolInfo.tsx b/src/pages/Lending/Pool/components/PoolInfo.tsx index 996b4480ef7..8d00ce4f97a 100644 --- a/src/pages/Lending/Pool/components/PoolInfo.tsx +++ b/src/pages/Lending/Pool/components/PoolInfo.tsx @@ -13,6 +13,7 @@ import { useAppSelector } from 'state/store' import { DynamicComponent } from './PoolStat' const labelProps = { fontSize: 'sm ' } +const responsiveFlex = { base: 'auto', lg: 1 } type PoolInfoProps = { poolAssetId: AssetId @@ -71,35 +72,35 @@ export const PoolInfo = ({ poolAssetId }: PoolInfoProps) => { {translate('lending.healthy')} - + - + diff --git a/src/pages/Lending/Pool/components/Repay/Repay.tsx b/src/pages/Lending/Pool/components/Repay/Repay.tsx index 856d7b6ead4..c7e56820bbe 100644 --- a/src/pages/Lending/Pool/components/Repay/Repay.tsx +++ b/src/pages/Lending/Pool/components/Repay/Repay.tsx @@ -4,6 +4,7 @@ import { lazy, memo, Suspense, useCallback } from 'react' import { MemoryRouter, Route, Switch, useLocation } from 'react-router' import { useRouteAssetId } from 'hooks/useRouteAssetId/useRouteAssetId' import type { Asset } from 'lib/asset-service' +import type { LendingQuoteClose } from 'lib/utils/thorchain/lending/types' import { RepayRoutePaths } from './types' @@ -21,6 +22,8 @@ type RepayProps = { setRepaymentPercent: (value: number) => void txId: string | null setTxid: (txId: string | null) => void + confirmedQuote: LendingQuoteClose | null + setConfirmedQuote: (quote: LendingQuoteClose | null) => void } export const Repay = ({ @@ -35,6 +38,8 @@ export const Repay = ({ setRepaymentPercent, txId, setTxid, + confirmedQuote, + setConfirmedQuote, }: RepayProps) => { const collateralAssetId = useRouteAssetId() @@ -53,6 +58,8 @@ export const Repay = ({ onRepaymentAccountIdChange={handleRepaymentAccountIdChange} txId={txId} setTxid={setTxid} + confirmedQuote={confirmedQuote} + setConfirmedQuote={setConfirmedQuote} /> ) @@ -71,6 +78,8 @@ type RepayRoutesProps = { onRepaymentAccountIdChange: (accountId: AccountId) => void txId: string | null setTxid: (txId: string | null) => void + confirmedQuote: LendingQuoteClose | null + setConfirmedQuote: (quote: LendingQuoteClose | null) => void } const RepayInput = lazy(() => @@ -100,6 +109,8 @@ const RepayRoutes = memo( onRepaymentAccountIdChange: handleRepaymentAccountIdChange, txId, setTxid, + confirmedQuote, + setConfirmedQuote, }: RepayRoutesProps) => { const location = useLocation() @@ -116,6 +127,8 @@ const RepayRoutes = memo( onRepaymentPercentChange={onRepaymentPercentChange} repaymentAsset={repaymentAsset} setRepaymentAsset={setRepaymentAsset} + confirmedQuote={confirmedQuote} + setConfirmedQuote={setConfirmedQuote} /> ), [ @@ -129,6 +142,8 @@ const RepayRoutes = memo( onRepaymentPercentChange, repaymentAsset, setRepaymentAsset, + confirmedQuote, + setConfirmedQuote, ], ) @@ -137,21 +152,27 @@ const RepayRoutes = memo( ), [ collateralAssetId, repaymentPercent, + onRepaymentPercentChange, collateralAccountId, repaymentAccountId, repaymentAsset, txId, setTxid, + confirmedQuote, + setConfirmedQuote, ], ) diff --git a/src/pages/Lending/Pool/components/Repay/RepayConfirm.tsx b/src/pages/Lending/Pool/components/Repay/RepayConfirm.tsx index 59fde8b92c8..37304a2359c 100644 --- a/src/pages/Lending/Pool/components/Repay/RepayConfirm.tsx +++ b/src/pages/Lending/Pool/components/Repay/RepayConfirm.tsx @@ -7,6 +7,7 @@ import { Heading, Skeleton, Stack, + useInterval, } from '@chakra-ui/react' import type { AccountId, AssetId } from '@shapeshiftoss/caip' import { fromAccountId, fromAssetId, thorchainAssetId } from '@shapeshiftoss/caip' @@ -15,6 +16,7 @@ import { FeeDataKey } from '@shapeshiftoss/chain-adapters' import type { KnownChainIds } from '@shapeshiftoss/types' import { TxStatus } from '@shapeshiftoss/unchained-client' import { useMutation, useMutationState } from '@tanstack/react-query' +import dayjs from 'dayjs' import { utils } from 'ethers' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslate } from 'react-polyglot' @@ -35,6 +37,7 @@ import { useWallet } from 'hooks/useWallet/useWallet' import type { Asset } from 'lib/asset-service' import { bn, bnOrZero } from 'lib/bignumber/bignumber' import { waitForThorchainUpdate } from 'lib/utils/thorchain' +import type { LendingQuoteClose } from 'lib/utils/thorchain/lending/types' import { useLendingQuoteCloseQuery } from 'pages/Lending/hooks/useLendingCloseQuery' import { useLendingPositionData } from 'pages/Lending/hooks/useLendingPositionData' import { useQuoteEstimatedFeesQuery } from 'pages/Lending/hooks/useQuoteEstimatedFees' @@ -53,26 +56,33 @@ type RepayConfirmProps = { collateralAssetId: AssetId repaymentAsset: Asset | null repaymentPercent: number + setRepaymentPercent: (percent: number) => void collateralAccountId: AccountId repaymentAccountId: AccountId txId: string | null setTxid: (txId: string | null) => void + confirmedQuote: LendingQuoteClose | null + setConfirmedQuote: (quote: LendingQuoteClose | null) => void } export const RepayConfirm = ({ collateralAssetId, repaymentAsset, repaymentPercent, + setRepaymentPercent, collateralAccountId, repaymentAccountId, txId, setTxid, + confirmedQuote, + setConfirmedQuote, }: RepayConfirmProps) => { const { state: { wallet }, } = useWallet() const [isLoanPending, setIsLoanPending] = useState(false) + const [isQuoteExpired, setIsQuoteExpired] = useState(false) const { refetch: refetchLendingPositionData } = useLendingPositionData({ assetId: collateralAssetId, @@ -140,45 +150,54 @@ export const RepayConfirm = ({ return bnOrZero(repaymentAmountFiatUserCurrency).div(repaymentAssetMarketData.price).toFixed() }, [repaymentAmountFiatUserCurrency, repaymentAssetMarketData.price]) + const chainAdapter = getChainAdapterManager().get( + fromAssetId(repaymentAsset?.assetId ?? '').chainId, + ) + const selectedCurrency = useAppSelector(selectSelectedCurrency) + + const repaymentAccountNumberFilter = useMemo( + () => ({ accountId: repaymentAccountId }), + [repaymentAccountId], + ) + const repaymentAccountNumber = useAppSelector(state => + selectAccountNumberByAccountId(state, repaymentAccountNumberFilter), + ) + const useLendingQuoteCloseQueryArgs = useMemo( () => ({ + // Refetching at confirm step should only be done programmatically with refetch if a quote expires and a user clicks "Refetch Quote" + enabled: false, collateralAssetId, + collateralAccountId, repaymentAssetId: repaymentAsset?.assetId ?? '', repaymentPercent, repaymentAccountId, - collateralAccountId, }), [ + collateralAccountId, collateralAssetId, + repaymentAccountId, repaymentAsset?.assetId, repaymentPercent, - repaymentAccountId, - collateralAccountId, ], ) - const { - data: lendingQuoteCloseData, - isLoading: isLendingQuoteCloseLoading, - isSuccess: isLendingQuoteCloseSuccess, - } = useLendingQuoteCloseQuery(useLendingQuoteCloseQueryArgs) + const { refetch: refetchQuote, isRefetching: isLendingQuoteCloseQueryRefetching } = + useLendingQuoteCloseQuery(useLendingQuoteCloseQueryArgs) - const chainAdapter = getChainAdapterManager().get( - fromAssetId(repaymentAsset?.assetId ?? '').chainId, - ) - const selectedCurrency = useAppSelector(selectSelectedCurrency) - - const repaymentAccountNumberFilter = useMemo( - () => ({ accountId: repaymentAccountId }), - [repaymentAccountId], - ) - const repaymentAccountNumber = useAppSelector(state => - selectAccountNumberByAccountId(state, repaymentAccountNumberFilter), - ) const handleConfirm = useCallback(async () => { - if (loanTxStatus === 'pending' || loanTxStatus === 'success') { + if (isQuoteExpired) { + const { data: refetchedQuote } = await refetchQuote() + setConfirmedQuote(refetchedQuote ?? null) + return + } + + if (loanTxStatus === 'pending') return // no-op + if (loanTxStatus === 'success') { // Reset values when going back to input step setTxid(null) + setConfirmedQuote(null) + setRepaymentPercent(100) return history.push(RepayRoutePaths.Input) } @@ -187,7 +206,7 @@ export const RepayConfirm = ({ repaymentAsset && wallet && chainAdapter && - lendingQuoteCloseData && + confirmedQuote && repaymentAmountCryptoPrecision && repaymentAccountNumber !== undefined ) @@ -202,9 +221,9 @@ export const RepayConfirm = ({ cryptoAmount: repaymentAmountCryptoPrecision, assetId: repaymentAsset.assetId, memo: supportedEvmChainIds.includes(fromAssetId(repaymentAsset.assetId).chainId) - ? utils.hexlify(utils.toUtf8Bytes(lendingQuoteCloseData.quoteMemo)) - : lendingQuoteCloseData.quoteMemo, - to: lendingQuoteCloseData.quoteInboundAddress, + ? utils.hexlify(utils.toUtf8Bytes(confirmedQuote.quoteMemo)) + : confirmedQuote.quoteMemo, + to: confirmedQuote.quoteInboundAddress, sendMax: false, accountId: repaymentAccountId, contractAddress: undefined, @@ -224,7 +243,7 @@ export const RepayConfirm = ({ value: bnOrZero(repaymentAmountCryptoPrecision) .times(bn(10).pow(repaymentAsset.precision)) .toFixed(0), - memo: lendingQuoteCloseData.quoteMemo, + memo: confirmedQuote.quoteMemo, chainSpecific: { gas: (estimatedFees as FeeDataEstimate).fast .chainSpecific.gasLimit, @@ -237,7 +256,7 @@ export const RepayConfirm = ({ }) return adapter.broadcastTransaction({ senderAddress: account, - receiverAddress: lendingQuoteCloseData.quoteInboundAddress, + receiverAddress: confirmedQuote.quoteInboundAddress, hex: signedTx, }) })() @@ -248,19 +267,19 @@ export const RepayConfirm = ({ cryptoAmount: repaymentAmountCryptoPrecision, assetId: repaymentAsset.assetId, from: '', - to: lendingQuoteCloseData.quoteInboundAddress, + to: confirmedQuote.quoteInboundAddress, sendMax: false, accountId: repaymentAccountId, memo: supportedEvmChainIds.includes(fromAssetId(repaymentAsset?.assetId).chainId) - ? utils.hexlify(utils.toUtf8Bytes(lendingQuoteCloseData.quoteMemo)) - : lendingQuoteCloseData.quoteMemo, + ? utils.hexlify(utils.toUtf8Bytes(confirmedQuote.quoteMemo)) + : confirmedQuote.quoteMemo, amountFieldError: '', estimatedFees, feeType: FeeDataKey.Fast, fiatAmount: '', fiatSymbol: selectedCurrency, vanityAddress: '', - input: lendingQuoteCloseData.quoteInboundAddress, + input: confirmedQuote.quoteInboundAddress, } if (!sendInput) throw new Error('Error building send input') @@ -277,14 +296,18 @@ export const RepayConfirm = ({ return maybeTxId }, [ chainAdapter, + confirmedQuote, history, - lendingQuoteCloseData, + isQuoteExpired, loanTxStatus, + refetchQuote, repaymentAccountId, repaymentAccountNumber, repaymentAmountCryptoPrecision, repaymentAsset, selectedCurrency, + setConfirmedQuote, + setRepaymentPercent, setTxid, wallet, ]) @@ -292,13 +315,14 @@ export const RepayConfirm = ({ const { data: estimatedFeesData, isLoading: isEstimatedFeesDataLoading, + isError: isEstimatedFeesDataError, isSuccess: isEstimatedFeesDataSuccess, } = useQuoteEstimatedFeesQuery({ collateralAssetId, collateralAccountId, repaymentAccountId, - repaymentPercent, repaymentAsset, + confirmedQuote, }) const swapStatus = useMemo(() => { @@ -307,7 +331,26 @@ export const RepayConfirm = ({ return TxStatus.Unknown }, [loanTxStatus]) + useInterval(() => { + // This should never happen but it may + if (!confirmedQuote) return + + // Since we run this interval check every second, subtract a few seconds to avoid + // off-by-one on the last second as well as the main thread being overloaded and running slow + const quoteExpiryUnix = dayjs.unix(confirmedQuote.quoteExpiry).subtract(5, 'second').unix() + + const isExpired = dayjs.unix(quoteExpiryUnix).isBefore(dayjs()) + setIsQuoteExpired(isExpired) + }, 1000) + + const confirmTranslation = useMemo(() => { + if (isQuoteExpired) return 'lending.refetchQuote' + + return loanTxStatus === 'success' ? 'lending.repayAgain' : 'lending.confirmAndRepay' + }, [isQuoteExpired, loanTxStatus]) + if (!collateralAsset || !repaymentAsset) return null + return ( @@ -333,7 +376,7 @@ export const RepayConfirm = ({ {translate('common.send')} - + - - - - {translate('common.receive')} - - + + + {translate('common.receive')} + + + - - - - - - - {translate('common.feesPlusSlippage')} - - + + + + + + {translate('common.feesPlusSlippage')} + + + - - - + + + {translate('common.gasFee')} - + {/* Actually defined at display time, see isLoaded above */} @@ -399,36 +438,42 @@ export const RepayConfirm = ({ diff --git a/src/pages/Lending/Pool/components/Repay/RepayInput.tsx b/src/pages/Lending/Pool/components/Repay/RepayInput.tsx index a09d84daf7f..bba6609f9a6 100644 --- a/src/pages/Lending/Pool/components/Repay/RepayInput.tsx +++ b/src/pages/Lending/Pool/components/Repay/RepayInput.tsx @@ -27,6 +27,7 @@ import { Text } from 'components/Text' import { useModal } from 'hooks/useModal/useModal' import type { Asset } from 'lib/asset-service' import { bn, bnOrZero } from 'lib/bignumber/bignumber' +import type { LendingQuoteClose } from 'lib/utils/thorchain/lending/types' import { useLendingQuoteCloseQuery } from 'pages/Lending/hooks/useLendingCloseQuery' import { useLendingPositionData } from 'pages/Lending/hooks/useLendingPositionData' import { useLendingSupportedAssets } from 'pages/Lending/hooks/useLendingSupportedAssets' @@ -59,8 +60,12 @@ type RepayInputProps = { onRepaymentAccountIdChange: (accountId: AccountId) => void repaymentAsset: Asset | null setRepaymentAsset: (asset: Asset) => void + confirmedQuote: LendingQuoteClose | null + setConfirmedQuote: (quote: LendingQuoteClose | null) => void } +const percentOptions = [0] + export const RepayInput = ({ collateralAssetId, repaymentPercent, @@ -72,6 +77,8 @@ export const RepayInput = ({ onRepaymentAccountIdChange: handleRepaymentAccountIdChange, repaymentAsset, setRepaymentAsset, + confirmedQuote, + setConfirmedQuote, }: RepayInputProps) => { const [seenNotice, setSeenNotice] = useState(false) const translate = useTranslate() @@ -79,22 +86,6 @@ export const RepayInput = ({ const collateralAsset = useAppSelector(state => selectAssetById(state, collateralAssetId)) const feeAsset = useAppSelector(state => selectFeeAssetById(state, repaymentAsset?.assetId ?? '')) - const onSubmit = useCallback(() => { - history.push(RepayRoutePaths.Confirm) - }, [history]) - - const swapIcon = useMemo(() => , []) - - const percentOptions = useMemo(() => [0], []) - - const { data: lendingSupportedAssets } = useLendingSupportedAssets({ type: 'borrow' }) - - useEffect(() => { - if (!lendingSupportedAssets) return - - setRepaymentAsset(lendingSupportedAssets[0]) - }, [lendingSupportedAssets, setRepaymentAsset]) - const useLendingQuoteCloseQueryArgs = useMemo( () => ({ collateralAssetId, @@ -115,11 +106,33 @@ export const RepayInput = ({ const { data: lendingQuoteCloseData, isLoading: isLendingQuoteCloseLoading, + isRefetching: isLendingQuoteCloseRefetching, isSuccess: isLendingQuoteCloseSuccess, isError: isLendingQuoteCloseError, error: lendingQuoteCloseError, } = useLendingQuoteCloseQuery(useLendingQuoteCloseQueryArgs) + useEffect(() => { + setConfirmedQuote(lendingQuoteCloseData ?? null) + }, [lendingQuoteCloseData, setConfirmedQuote]) + + const onSubmit = useCallback(() => { + if (!lendingQuoteCloseData) return + setConfirmedQuote(lendingQuoteCloseData) + history.push(RepayRoutePaths.Confirm) + }, [history, lendingQuoteCloseData, setConfirmedQuote]) + + const swapIcon = useMemo(() => , []) + + const { data: lendingSupportedAssets } = useLendingSupportedAssets({ type: 'borrow' }) + + useEffect(() => { + if (!(lendingSupportedAssets && collateralAsset)) return + if (repaymentAsset) return + + setRepaymentAsset(collateralAsset) + }, [collateralAsset, lendingSupportedAssets, repaymentAsset, setRepaymentAsset]) + const buyAssetSearch = useModal('buyAssetSearch') const handleRepaymentAssetClick = useCallback(() => { if (!lendingSupportedAssets?.length) return @@ -200,8 +213,8 @@ export const RepayInput = ({ collateralAssetId, collateralAccountId, repaymentAccountId, - repaymentPercent, repaymentAsset, + confirmedQuote, }) const balanceFilter = useMemo( @@ -365,11 +378,11 @@ export const RepayInput = ({ assetIcon={collateralAsset?.icon ?? ''} // Both cryptoAmount and fiatAmount actually defined at display time, see showFiatSkeleton below cryptoAmount={lendingQuoteCloseData?.quoteWithdrawnAmountAfterFeesCryptoPrecision} - fiatAmount={lendingQuoteCloseData?.quoteDebtRepaidAmountUsd} + fiatAmount={lendingQuoteCloseData?.quoteDebtRepaidAmountUserCurrency} isAccountSelectionDisabled={isAccountSelectionDisabled} isSendMaxDisabled={false} percentOptions={percentOptions} - showInputSkeleton={isLendingQuoteCloseLoading} + showInputSkeleton={isLendingQuoteCloseLoading || isLendingQuoteCloseRefetching} showFiatSkeleton={false} label={translate('lending.unlockedCollateral')} onAccountIdChange={handleCollateralAccountIdChange} @@ -380,11 +393,15 @@ export const RepayInput = ({ layout='inline' labelPostFix={collateralAssetSelectComponent} /> - + {translate('common.slippage')} - + {translate('common.gasFee')} - + {/* Actually defined at display time, see isLoaded above */} @@ -428,7 +451,7 @@ export const RepayInput = ({ {translate('common.fees')} - + @@ -455,12 +478,16 @@ export const RepayInput = ({ mx={-2} onClick={onSubmit} isLoading={ - isLendingPositionDataLoading || isLendingQuoteCloseLoading || isEstimatedFeesDataLoading + isLendingPositionDataLoading || + isLendingQuoteCloseLoading || + isLendingQuoteCloseRefetching || + isEstimatedFeesDataLoading } isDisabled={Boolean( isLendingPositionDataLoading || isLendingPositionDataError || isLendingQuoteCloseLoading || + isLendingQuoteCloseRefetching || isEstimatedFeesDataLoading || isLendingQuoteCloseError || isEstimatedFeesDataError || diff --git a/src/pages/Lending/YourLoans.tsx b/src/pages/Lending/YourLoans.tsx index 15657e71057..d9d45884978 100644 --- a/src/pages/Lending/YourLoans.tsx +++ b/src/pages/Lending/YourLoans.tsx @@ -1,4 +1,4 @@ -import { Button, type GridProps, SimpleGrid, Skeleton, Stack } from '@chakra-ui/react' +import { Button, Flex, type GridProps, SimpleGrid, Skeleton, Stack } from '@chakra-ui/react' import type { AccountId, AssetId } from '@shapeshiftoss/caip' import { useCallback, useMemo } from 'react' import { useTranslate } from 'react-polyglot' @@ -23,7 +23,30 @@ import { useRepaymentLockData } from './hooks/useRepaymentLockData' export const lendingRowGrid: GridProps['gridTemplateColumns'] = { base: 'minmax(150px, 1fr) repeat(1, minmax(40px, max-content))', - md: 'repeat(5, 1fr)', + lg: 'repeat(5, 1fr)', +} +const reverseMobileDisplay = { + base: 'block', + lg: 'none', +} +const mobileDisplay = { + base: 'none', + lg: 'flex', +} + +const mobilePadding = { + base: 4, + lg: 0, +} + +const listMargin = { + base: 0, + lg: -4, +} + +const alignItems = { + base: 'flex-end', + lg: 'flex-start', } type LendingRowGridProps = { @@ -79,7 +102,7 @@ const LendingRowGrid = ({ asset, accountId, onPoolClick }: LendingRowGridProps) return null return ( - + @@ -188,20 +222,29 @@ export const YourLoans = () => { color='text.subtle' fontWeight='bold' fontSize='sm' + px={mobilePadding} > - - - + + + + + + - - - - - - + + + + + + + + + + + {lendingRowGrids} diff --git a/src/pages/Lending/components/LendingHeader.tsx b/src/pages/Lending/components/LendingHeader.tsx index bb940f83891..5b600d36394 100644 --- a/src/pages/Lending/components/LendingHeader.tsx +++ b/src/pages/Lending/components/LendingHeader.tsx @@ -20,6 +20,7 @@ import type { TabItem } from 'pages/Dashboard/components/DashboardHeader' import { useAllLendingPositionsData } from '../hooks/useAllLendingPositionsData' const containerPadding = { base: 6, '2xl': 8 } +const responsiveFlex = { base: 'auto', lg: 1 } export const LendingHeader = () => { const translate = useTranslate() @@ -49,8 +50,8 @@ export const LendingHeader = () => { {translate('lending.lending')} - - + + @@ -62,7 +63,7 @@ export const LendingHeader = () => { /> - + @@ -70,7 +71,12 @@ export const LendingHeader = () => { - + diff --git a/src/pages/Lending/hooks/useLendingCloseQuery.ts b/src/pages/Lending/hooks/useLendingCloseQuery.ts index 5aee86a605b..ab2ef2abfa5 100644 --- a/src/pages/Lending/hooks/useLendingCloseQuery.ts +++ b/src/pages/Lending/hooks/useLendingCloseQuery.ts @@ -2,6 +2,7 @@ import type { AccountId } from '@shapeshiftoss/caip' import { type AssetId } from '@shapeshiftoss/caip' import { bnOrZero } from '@shapeshiftoss/chain-adapters' import type { MarketData } from '@shapeshiftoss/types' +import type { QueryObserverOptions } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query' import memoize from 'lodash/memoize' import { useMemo } from 'react' @@ -10,11 +11,17 @@ import { toBaseUnit } from 'lib/math' import { fromThorBaseUnit } from 'lib/utils/thorchain' import { BASE_BPS_POINTS } from 'lib/utils/thorchain/constants' import { getMaybeThorchainLendingCloseQuote } from 'lib/utils/thorchain/lending' -import type { LendingWithdrawQuoteResponseSuccess } from 'lib/utils/thorchain/lending/types' +import type { + LendingQuoteClose, + LendingWithdrawQuoteResponseSuccess, +} from 'lib/utils/thorchain/lending/types' import { selectAssetById } from 'state/slices/assetsSlice/selectors' -import { selectMarketDataById } from 'state/slices/marketDataSlice/selectors' +import { + selectMarketDataById, + selectUserCurrencyToUsdRate, +} from 'state/slices/marketDataSlice/selectors' import { selectPortfolioAccountMetadataByAccountId } from 'state/slices/selectors' -import { useAppSelector } from 'state/store' +import { store, useAppSelector } from 'state/store' import { useLendingPositionData } from './useLendingPositionData' @@ -35,7 +42,7 @@ const selectLendingCloseQueryData = memoize( data: LendingWithdrawQuoteResponseSuccess collateralAssetMarketData: MarketData repaymentAmountCryptoPrecision: string | null - }) => { + }): LendingQuoteClose => { const quote = data const quoteLoanCollateralDecreaseCryptoPrecision = fromThorBaseUnit( @@ -46,7 +53,10 @@ const selectLendingCloseQueryData = memoize( ) .times(collateralAssetMarketData.price) .toString() - const quoteDebtRepaidAmountUsd = fromThorBaseUnit(quote.expected_debt_repaid).toString() + const userCurrencyToUsdRate = selectUserCurrencyToUsdRate(store.getState()) + const quoteDebtRepaidAmountUserCurrency = fromThorBaseUnit(quote.expected_debt_repaid) + .times(userCurrencyToUsdRate) + .toString() const quoteWithdrawnAmountAfterFeesCryptoPrecision = fromThorBaseUnit( quote.expected_amount_out, ).toString() @@ -71,17 +81,19 @@ const selectLendingCloseQueryData = memoize( const quoteInboundAddress = quote.inbound_address const quoteMemo = quote.memo + const quoteExpiry = quote.expiry return { quoteLoanCollateralDecreaseCryptoPrecision, quoteLoanCollateralDecreaseFiatUserCurrency, - quoteDebtRepaidAmountUsd, + quoteDebtRepaidAmountUserCurrency, quoteWithdrawnAmountAfterFeesCryptoPrecision, quoteWithdrawnAmountAfterFeesUserCurrency, quoteSlippageWithdrawndAssetCryptoPrecision, quoteTotalFeesFiatUserCurrency, quoteInboundAddress, quoteMemo, + quoteExpiry, repaymentAmountCryptoPrecision, } }, @@ -93,7 +105,8 @@ export const useLendingQuoteCloseQuery = ({ repaymentPercent: _repaymentPercent, repaymentAccountId: _repaymentAccountId, collateralAccountId: _collateralAccountId, -}: UseLendingQuoteCloseQueryProps) => { + enabled = true, +}: UseLendingQuoteCloseQueryProps & QueryObserverOptions) => { const repaymentPercentOrDefault = useMemo(() => { const repaymentPercentBn = bnOrZero(_repaymentPercent) // 1% buffer in case our market data differs from THOR's, to ensure 100% loan repays are actually 100% repays @@ -198,8 +211,13 @@ export const useLendingQuoteCloseQuery = ({ collateralAssetMarketData, repaymentAmountCryptoPrecision, }), + // Do not refetch if consumers explicitly set enabled to false + // They do so because the query should never run in the reactive react realm, but only programmatically with the refetch function + refetchIntervalInBackground: enabled, + refetchInterval: enabled ? 20_000 : undefined, enabled: Boolean( - lendingPositionData?.address && + enabled && + lendingPositionData?.address && bnOrZero(repaymentPercentOrDefault).gt(0) && repaymentAccountId && collateralAssetId && diff --git a/src/pages/Lending/hooks/useLendingPositionData.tsx b/src/pages/Lending/hooks/useLendingPositionData.tsx index 6fde704a2cd..041fb4e5c9b 100644 --- a/src/pages/Lending/hooks/useLendingPositionData.tsx +++ b/src/pages/Lending/hooks/useLendingPositionData.tsx @@ -12,7 +12,6 @@ import { store, useAppSelector } from 'state/store' type UseLendingPositionDataProps = { accountId: AccountId assetId: AssetId - skip?: boolean } export const thorchainLendingPositionQueryFn = async ({ @@ -25,11 +24,7 @@ export const thorchainLendingPositionQueryFn = async ({ return position } -export const useLendingPositionData = ({ - accountId, - assetId, - skip, -}: UseLendingPositionDataProps) => { +export const useLendingPositionData = ({ accountId, assetId }: UseLendingPositionDataProps) => { const lendingPositionQueryKey: [string, { accountId: AccountId; assetId: AssetId }] = useMemo( () => ['thorchainLendingPosition', { accountId, assetId }], [accountId, assetId], @@ -37,8 +32,10 @@ export const useLendingPositionData = ({ const poolAssetMarketData = useAppSelector(state => selectMarketDataById(state, assetId)) const lendingPositionData = useQuery({ - // The time before the data is considered stale, meaning firing this query after it elapses will trigger queryFn - staleTime: 300_000, + // This is on purpose. We want lending position data to be cached forever + // The only time we need new data is when doing a lending borrow/repayment + // in which case we programatically invalidate queries + staleTime: Infinity, queryKey: lendingPositionQueryKey, queryFn: async ({ queryKey }) => { const [, { accountId, assetId }] = queryKey @@ -65,10 +62,7 @@ export const useLendingPositionData = ({ address: data?.owner, } }, - enabled: Boolean(!skip && accountId && assetId && poolAssetMarketData.price !== '0'), - refetchOnMount: true, - refetchInterval: 300_000, - refetchIntervalInBackground: true, + enabled: Boolean(accountId && assetId && poolAssetMarketData.price !== '0'), }) return lendingPositionData diff --git a/src/pages/Lending/hooks/useLendingQuoteQuery.ts b/src/pages/Lending/hooks/useLendingQuoteQuery.ts index d6926eadb8a..a5ed4892b91 100644 --- a/src/pages/Lending/hooks/useLendingQuoteQuery.ts +++ b/src/pages/Lending/hooks/useLendingQuoteQuery.ts @@ -3,6 +3,7 @@ import { type AssetId, fromAccountId } from '@shapeshiftoss/caip' import { bnOrZero } from '@shapeshiftoss/chain-adapters' import { isLedger } from '@shapeshiftoss/hdwallet-ledger' import type { MarketData } from '@shapeshiftoss/types' +import type { QueryObserverOptions } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query' import memoize from 'lodash/memoize' import { useCallback, useEffect, useMemo, useState } from 'react' @@ -13,7 +14,10 @@ import { toBaseUnit } from 'lib/math' import { fromThorBaseUnit } from 'lib/utils/thorchain' import { BASE_BPS_POINTS } from 'lib/utils/thorchain/constants' import { getMaybeThorchainLendingOpenQuote } from 'lib/utils/thorchain/lending' -import type { LendingDepositQuoteResponseSuccess } from 'lib/utils/thorchain/lending/types' +import type { + LendingDepositQuoteResponseSuccess, + LendingQuoteOpen, +} from 'lib/utils/thorchain/lending/types' import { selectAssetById } from 'state/slices/assetsSlice/selectors' import { selectMarketDataById, @@ -41,7 +45,7 @@ const selectLendingQuoteQuery = memoize( data: LendingDepositQuoteResponseSuccess collateralAssetMarketData: MarketData borrowAssetMarketData: MarketData - }) => { + }): LendingQuoteOpen => { const quote = data const quoteCollateralAmountCryptoPrecision = fromThorBaseUnit( @@ -86,6 +90,7 @@ const selectLendingQuoteQuery = memoize( const quoteInboundAddress = quote.inbound_address const quoteMemo = quote.memo + const quoteExpiry = quote.expiry return { quoteCollateralAmountCryptoPrecision, @@ -98,6 +103,7 @@ const selectLendingQuoteQuery = memoize( quoteTotalFeesFiatUserCurrency, quoteInboundAddress, quoteMemo, + quoteExpiry, } }, ) @@ -108,7 +114,8 @@ export const useLendingQuoteOpenQuery = ({ borrowAccountId: _borrowAccountId, borrowAssetId: _borrowAssetId, depositAmountCryptoPrecision: _depositAmountCryptoPrecision, -}: UseLendingQuoteQueryProps) => { + enabled = true, +}: UseLendingQuoteQueryProps & QueryObserverOptions) => { const [_borrowAssetReceiveAddress, setBorrowAssetReceiveAddress] = useState(null) const wallet = useWallet().state.wallet @@ -219,8 +226,13 @@ export const useLendingQuoteOpenQuery = ({ // Failed queries go stale and don't honor "staleTime", which means smaller amounts would trigger a THOR daemon fetch from all consumers (3 currently) // vs. the failed query being considered fresh retry: false, + // Do not refetch if consumers explicitly set enabled to false + // They do so because the query should never run in the reactive react realm, but only programmatically with the refetch function + refetchIntervalInBackground: enabled, + refetchInterval: enabled ? 20_000 : undefined, enabled: Boolean( - bnOrZero(depositAmountCryptoPrecision).gt(0) && + enabled && + bnOrZero(depositAmountCryptoPrecision).gt(0) && collateralAccountId && collateralAccountId && borrowAssetId && diff --git a/src/pages/Lending/hooks/useQuoteEstimatedFees.ts b/src/pages/Lending/hooks/useQuoteEstimatedFees.ts deleted file mode 100644 index f66a5a88b17..00000000000 --- a/src/pages/Lending/hooks/useQuoteEstimatedFees.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { type AccountId, type AssetId, fromAssetId } from '@shapeshiftoss/caip' -import { useQuery } from '@tanstack/react-query' -import { utils } from 'ethers' -import { useMemo } from 'react' -import { estimateFees } from 'components/Modals/Send/utils' -import { getSupportedEvmChainIds } from 'hooks/useEvm/useEvm' -import type { Asset } from 'lib/asset-service' -import { bn, bnOrZero } from 'lib/bignumber/bignumber' -import { selectFeeAssetById, selectMarketDataById } from 'state/slices/selectors' -import { useAppSelector } from 'state/store' - -import { useLendingQuoteCloseQuery } from './useLendingCloseQuery' -import { useLendingQuoteOpenQuery } from './useLendingQuoteQuery' - -type UseQuoteEstimatedFeesProps = { - collateralAssetId: AssetId -} & ( - | { - borrowAssetId: AssetId - borrowAccountId: AccountId - collateralAccountId: AccountId - depositAmountCryptoPrecision: string - repaymentAccountId?: never - repaymentAsset?: never - repaymentPercent?: never - } - | { - borrowAssetId?: never - borrowAccountId?: never - collateralAccountId: AccountId - depositAmountCryptoPrecision?: never - repaymentAccountId: AccountId - repaymentAsset: Asset | null - repaymentPercent: number - } -) - -export const useQuoteEstimatedFeesQuery = ({ - collateralAssetId, - collateralAccountId, - borrowAccountId, - borrowAssetId, - depositAmountCryptoPrecision, - repaymentAccountId, - repaymentAsset, - repaymentPercent, -}: UseQuoteEstimatedFeesProps) => { - const useLendingQuoteQueryArgs = useMemo( - () => ({ - collateralAssetId, - collateralAccountId, - borrowAccountId: borrowAccountId ?? '', - borrowAssetId: borrowAssetId ?? '', - depositAmountCryptoPrecision: depositAmountCryptoPrecision ?? '0', - }), - [ - collateralAssetId, - collateralAccountId, - borrowAccountId, - borrowAssetId, - depositAmountCryptoPrecision, - ], - ) - const { data: lendingQuoteData } = useLendingQuoteOpenQuery(useLendingQuoteQueryArgs) - - const useLendingQuoteCloseQueryArgs = useMemo( - () => ({ - collateralAssetId, - repaymentAssetId: repaymentAsset?.assetId ?? '', - repaymentPercent: Number(repaymentPercent), - repaymentAccountId: repaymentAccountId ?? '', - collateralAccountId: collateralAccountId ?? '', - }), - [ - collateralAccountId, - collateralAssetId, - repaymentAccountId, - repaymentAsset?.assetId, - repaymentPercent, - ], - ) - - const { data: lendingQuoteCloseData } = useLendingQuoteCloseQuery(useLendingQuoteCloseQueryArgs) - - const feeAsset = useAppSelector(state => selectFeeAssetById(state, collateralAssetId)) - const feeAssetMarketData = useAppSelector(state => selectMarketDataById(state, collateralAssetId)) - const estimateFeesArgs = useMemo(() => { - const supportedEvmChainIds = getSupportedEvmChainIds() - const cryptoAmount = - depositAmountCryptoPrecision ?? lendingQuoteCloseData?.repaymentAmountCryptoPrecision ?? '0' - const assetId = repaymentAsset?.assetId ?? collateralAssetId - const quoteMemo = lendingQuoteCloseData?.quoteMemo ?? lendingQuoteData?.quoteMemo ?? '' - const memo = supportedEvmChainIds.includes(fromAssetId(assetId).chainId) - ? utils.hexlify(utils.toUtf8Bytes(quoteMemo)) - : quoteMemo - const to = - lendingQuoteCloseData?.quoteInboundAddress ?? lendingQuoteData?.quoteInboundAddress ?? '' - const accountId = repaymentAccountId ?? collateralAccountId - - return { - cryptoAmount, - assetId, - memo, - to, - accountId, - sendMax: false, - contractAddress: undefined, - } as const - }, [ - collateralAccountId, - collateralAssetId, - depositAmountCryptoPrecision, - lendingQuoteCloseData?.quoteInboundAddress, - lendingQuoteCloseData?.quoteMemo, - lendingQuoteCloseData?.repaymentAmountCryptoPrecision, - lendingQuoteData?.quoteInboundAddress, - lendingQuoteData?.quoteMemo, - repaymentAccountId, - repaymentAsset?.assetId, - ]) - - const quoteEstimatedFeesQueryKey = useMemo( - () => ['thorchainLendingQuoteEstimatedFees', estimateFeesArgs], - [estimateFeesArgs], - ) - - const useQuoteEstimatedFeesQuery = useQuery({ - queryKey: quoteEstimatedFeesQueryKey, - queryFn: async () => { - const estimatedFees = await estimateFees(estimateFeesArgs) - const txFeeFiat = bnOrZero(estimatedFees.fast.txFee) - .div(bn(10).pow(feeAsset!.precision)) // actually defined at runtime, see "enabled" below - .times(feeAssetMarketData.price) - .toString() - return { estimatedFees, txFeeFiat, txFeeCryptoBaseUnit: estimatedFees.fast.txFee } - }, - enabled: Boolean(feeAsset && (lendingQuoteData || lendingQuoteCloseData)), - retry: false, - }) - - return useQuoteEstimatedFeesQuery -} diff --git a/src/pages/Lending/hooks/useQuoteEstimatedFees/index.ts b/src/pages/Lending/hooks/useQuoteEstimatedFees/index.ts index 1111e01c320..46389d4356e 100644 --- a/src/pages/Lending/hooks/useQuoteEstimatedFees/index.ts +++ b/src/pages/Lending/hooks/useQuoteEstimatedFees/index.ts @@ -6,99 +6,52 @@ import { estimateFees } from 'components/Modals/Send/utils' import { getSupportedEvmChainIds } from 'hooks/useEvm/useEvm' import type { Asset } from 'lib/asset-service' import { bn, bnOrZero } from 'lib/bignumber/bignumber' -import { selectAssetById, selectMarketDataById } from 'state/slices/selectors' +import type { LendingQuoteClose, LendingQuoteOpen } from 'lib/utils/thorchain/lending/types' +import { selectFeeAssetById, selectMarketDataById } from 'state/slices/selectors' import { useAppSelector } from 'state/store' -import { useLendingQuoteCloseQuery } from '../useLendingCloseQuery' -import { useLendingQuoteOpenQuery } from '../useLendingQuoteQuery' - type UseQuoteEstimatedFeesProps = { collateralAssetId: AssetId } & ( | { - borrowAssetId: AssetId - borrowAccountId: AccountId + confirmedQuote: LendingQuoteOpen | null collateralAccountId: AccountId depositAmountCryptoPrecision: string repaymentAccountId?: never repaymentAsset?: never - repaymentPercent?: never } | { - borrowAssetId?: never - borrowAccountId?: never + confirmedQuote: LendingQuoteClose | null collateralAccountId: AccountId depositAmountCryptoPrecision?: never repaymentAccountId: AccountId repaymentAsset: Asset | null - repaymentPercent: number } ) export const useQuoteEstimatedFeesQuery = ({ collateralAssetId, collateralAccountId, - borrowAccountId, - borrowAssetId, depositAmountCryptoPrecision, repaymentAccountId, repaymentAsset, - repaymentPercent, + confirmedQuote, }: UseQuoteEstimatedFeesProps) => { - const useLendingQuoteQueryArgs = useMemo( - () => ({ - collateralAssetId, - collateralAccountId, - borrowAccountId: borrowAccountId ?? '', - borrowAssetId: borrowAssetId ?? '', - depositAmountCryptoPrecision: depositAmountCryptoPrecision ?? '0', - }), - [ - collateralAssetId, - collateralAccountId, - borrowAccountId, - borrowAssetId, - depositAmountCryptoPrecision, - ], - ) - const { data: lendingQuoteData } = useLendingQuoteOpenQuery(useLendingQuoteQueryArgs) - - const useLendingQuoteCloseQueryArgs = useMemo( - () => ({ - collateralAssetId, - repaymentAssetId: repaymentAsset?.assetId ?? '', - repaymentPercent: Number(repaymentPercent), - repaymentAccountId: repaymentAccountId ?? '', - collateralAccountId: collateralAccountId ?? '', - }), - [ - collateralAccountId, - collateralAssetId, - repaymentAccountId, - repaymentAsset?.assetId, - repaymentPercent, - ], - ) - - const { data: lendingQuoteCloseData } = useLendingQuoteCloseQuery(useLendingQuoteCloseQueryArgs) - - const asset = useAppSelector(state => - selectAssetById(state, repaymentAsset?.assetId ?? collateralAssetId), - ) - const assetMarketData = useAppSelector(state => - selectMarketDataById(state, repaymentAsset?.assetId ?? collateralAssetId), + const repaymentAmountCryptoPrecision = useMemo( + () => (confirmedQuote as LendingQuoteClose)?.repaymentAmountCryptoPrecision, + [confirmedQuote], ) + const feeAsset = useAppSelector(state => selectFeeAssetById(state, collateralAssetId)) + const feeAssetMarketData = useAppSelector(state => selectMarketDataById(state, collateralAssetId)) const estimateFeesArgs = useMemo(() => { const supportedEvmChainIds = getSupportedEvmChainIds() - const cryptoAmount = - depositAmountCryptoPrecision ?? lendingQuoteCloseData?.repaymentAmountCryptoPrecision ?? '0' + const cryptoAmount = depositAmountCryptoPrecision ?? repaymentAmountCryptoPrecision ?? '0' const assetId = repaymentAsset?.assetId ?? collateralAssetId - const quoteMemo = lendingQuoteCloseData?.quoteMemo ?? lendingQuoteData?.quoteMemo ?? '' + const quoteMemo = confirmedQuote?.quoteMemo ?? confirmedQuote?.quoteMemo ?? '' const memo = supportedEvmChainIds.includes(fromAssetId(assetId).chainId) ? utils.hexlify(utils.toUtf8Bytes(quoteMemo)) : quoteMemo - const to = - lendingQuoteCloseData?.quoteInboundAddress ?? lendingQuoteData?.quoteInboundAddress ?? '' + const to = confirmedQuote?.quoteInboundAddress ?? confirmedQuote?.quoteInboundAddress ?? '' const accountId = repaymentAccountId ?? collateralAccountId return { @@ -113,12 +66,10 @@ export const useQuoteEstimatedFeesQuery = ({ }, [ collateralAccountId, collateralAssetId, + confirmedQuote?.quoteInboundAddress, + confirmedQuote?.quoteMemo, + repaymentAmountCryptoPrecision, depositAmountCryptoPrecision, - lendingQuoteCloseData?.quoteInboundAddress, - lendingQuoteCloseData?.quoteMemo, - lendingQuoteCloseData?.repaymentAmountCryptoPrecision, - lendingQuoteData?.quoteInboundAddress, - lendingQuoteData?.quoteMemo, repaymentAccountId, repaymentAsset?.assetId, ]) @@ -133,12 +84,12 @@ export const useQuoteEstimatedFeesQuery = ({ queryFn: async () => { const estimatedFees = await estimateFees(estimateFeesArgs) const txFeeFiat = bnOrZero(estimatedFees.fast.txFee) - .div(bn(10).pow(asset!.precision)) // actually defined at runtime, see "enabled" below - .times(assetMarketData.price) + .div(bn(10).pow(feeAsset!.precision)) // actually defined at runtime, see "enabled" below + .times(feeAssetMarketData.price) .toString() return { estimatedFees, txFeeFiat, txFeeCryptoBaseUnit: estimatedFees.fast.txFee } }, - enabled: Boolean(asset && (lendingQuoteData || lendingQuoteCloseData)), + enabled: Boolean(feeAsset && confirmedQuote), retry: false, }) diff --git a/src/state/slices/tradeQuoteSlice/constants.ts b/src/state/slices/tradeQuoteSlice/constants.ts new file mode 100644 index 00000000000..1ce159b3ca7 --- /dev/null +++ b/src/state/slices/tradeQuoteSlice/constants.ts @@ -0,0 +1,25 @@ +import type { TradeQuoteSliceState } from './tradeQuoteSlice' +import { HopExecutionState, TradeExecutionState, TransactionExecutionState } from './types' + +const initialTransactionState = { + state: TransactionExecutionState.AwaitingConfirmation, +} + +const initialHopState = { + state: HopExecutionState.Pending, + approval: initialTransactionState, + swap: initialTransactionState, +} + +export const initialTradeExecutionState = { + state: TradeExecutionState.Previewing, + firstHop: initialHopState, + secondHop: initialHopState, +} + +export const initialState: TradeQuoteSliceState = { + activeQuoteIndex: undefined, + confirmedQuote: undefined, + activeStep: undefined, + tradeExecution: initialTradeExecutionState, +} diff --git a/src/state/slices/tradeQuoteSlice/tradeQuoteSlice.ts b/src/state/slices/tradeQuoteSlice/tradeQuoteSlice.ts index 6519502814b..7343a0a8988 100644 --- a/src/state/slices/tradeQuoteSlice/tradeQuoteSlice.ts +++ b/src/state/slices/tradeQuoteSlice/tradeQuoteSlice.ts @@ -1,34 +1,21 @@ import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' -import type { TxStatus } from '@shapeshiftoss/unchained-client' import type { TradeQuote } from 'lib/swapper/types' -import type { HopExecutionMetadata, StreamingSwapMetadata } from './types' -import { HopExecutionState, MultiHopExecutionState } from './types' -import { getHopExecutionStates, getNextTradeExecutionState } from './utils' +import { initialState, initialTradeExecutionState } from './constants' +import { + HopExecutionState, + type StreamingSwapMetadata, + type TradeExecutionMetadata, + TradeExecutionState, + TransactionExecutionState, +} from './types' 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 activeQuoteIndex: number | undefined // the selected swapper used to find the active quote in the api response confirmedQuote: TradeQuote | undefined // the quote being executed - tradeExecution: { - state: MultiHopExecutionState - firstHop: HopExecutionMetadata - secondHop: HopExecutionMetadata - } -} - -const initialTradeExecutionState = { - state: MultiHopExecutionState.Previewing, - firstHop: { state: HopExecutionState.Pending }, - secondHop: { state: HopExecutionState.Pending }, -} - -const initialState: TradeQuoteSliceState = { - activeQuoteIndex: undefined, - confirmedQuote: undefined, - activeStep: undefined, - tradeExecution: initialTradeExecutionState, + tradeExecution: TradeExecutionMetadata } export const tradeQuoteSlice = createSlice({ @@ -60,77 +47,103 @@ export const tradeQuoteSlice = createSlice({ state.confirmedQuote = undefined state.tradeExecution = initialTradeExecutionState }, - incrementTradeExecutionState: state => { - // this should never happen but if it does, exit to prevent corrupting the current state - if ( - state.tradeExecution.firstHop.approvalRequired === undefined || - state.tradeExecution.secondHop.approvalRequired === undefined - ) { - console.error('initial approval requirements not set') + confirmTrade: state => { + if (state.tradeExecution.state !== TradeExecutionState.Previewing) { + console.error('attempted to confirm an in-progress trade') return } - + state.tradeExecution.state = TradeExecutionState.FirstHop + const approvalRequired = state.tradeExecution.firstHop.approval.isRequired + state.tradeExecution.firstHop.state = approvalRequired + ? HopExecutionState.AwaitingApproval + : HopExecutionState.AwaitingSwap + }, + setApprovalTxPending: (state, action: PayloadAction<{ hopIndex: number }>) => { + const { hopIndex } = action.payload + const key = hopIndex === 0 ? 'firstHop' : 'secondHop' + state.tradeExecution[key].approval.state = TransactionExecutionState.Pending + }, + setApprovalTxFailed: (state, action: PayloadAction<{ hopIndex: number }>) => { + const { hopIndex } = action.payload + const key = hopIndex === 0 ? 'firstHop' : 'secondHop' + state.tradeExecution[key].approval.state = TransactionExecutionState.Failed + }, + setApprovalTxComplete: (state, action: PayloadAction<{ hopIndex: number }>) => { + const { hopIndex } = action.payload + const key = hopIndex === 0 ? 'firstHop' : 'secondHop' + state.tradeExecution[key].approval.state = TransactionExecutionState.Complete + state.tradeExecution[key].state = HopExecutionState.AwaitingSwap + }, + setSwapTxPending: (state, action: PayloadAction<{ hopIndex: number }>) => { + const { hopIndex } = action.payload + const key = hopIndex === 0 ? 'firstHop' : 'secondHop' + state.tradeExecution[key].swap.state = TransactionExecutionState.Pending + }, + setSwapTxFailed: (state, action: PayloadAction<{ hopIndex: number }>) => { + const { hopIndex } = action.payload + const key = hopIndex === 0 ? 'firstHop' : 'secondHop' + state.tradeExecution[key].swap.state = TransactionExecutionState.Failed + }, + setSwapTxComplete: (state, action: PayloadAction<{ hopIndex: number }>) => { + const { hopIndex } = action.payload const isMultiHopTrade = state.confirmedQuote !== undefined && state.confirmedQuote.steps.length > 1 + const isFirstHop = hopIndex === 0 - const firstHopRequiresApproval = state.tradeExecution.firstHop.approvalRequired - const secondHopRequiresApproval = state.tradeExecution.secondHop.approvalRequired + if (isFirstHop) { + // complete the first hop + state.tradeExecution.firstHop.swap.state = TransactionExecutionState.Complete + state.tradeExecution.firstHop.state = HopExecutionState.Complete - state.tradeExecution.state = getNextTradeExecutionState( - state.tradeExecution.state, - isMultiHopTrade, - firstHopRequiresApproval, - secondHopRequiresApproval, - ) + if (isMultiHopTrade) { + // first hop of multi hop trade - begin second hop + state.tradeExecution.state = TradeExecutionState.SecondHop + const approvalRequired = state.tradeExecution.secondHop.approval.isRequired + state.tradeExecution.secondHop.state = approvalRequired + ? HopExecutionState.AwaitingApproval + : HopExecutionState.AwaitingSwap + } else { + // first hop of single hop trade - trade complete + state.tradeExecution.state = TradeExecutionState.TradeComplete + } + } else { + // complete the second hop + state.tradeExecution.secondHop.swap.state = TransactionExecutionState.Complete + state.tradeExecution.secondHop.state = HopExecutionState.Complete - const { firstHop: firstHopState, secondHop: secondHopState } = getHopExecutionStates( - state.tradeExecution.state, - ) - state.tradeExecution.firstHop.state = firstHopState - state.tradeExecution.secondHop.state = secondHopState + // second hop of multi-hop trade - trade complete + state.tradeExecution.state = TradeExecutionState.TradeComplete + } }, setInitialApprovalRequirements: ( state, action: PayloadAction<{ firstHop: boolean; secondHop: boolean } | undefined>, ) => { - state.tradeExecution.firstHop.approvalRequired = action.payload?.firstHop - state.tradeExecution.secondHop.approvalRequired = action.payload?.secondHop - }, - setFirstHopApprovalTxHash: (state, action: PayloadAction) => { - state.tradeExecution.firstHop.approvalTxHash = action.payload - }, - setSecondHopApprovalTxHash: (state, action: PayloadAction) => { - state.tradeExecution.secondHop.approvalTxHash = action.payload + state.tradeExecution.firstHop.approval.isRequired = action.payload?.firstHop + state.tradeExecution.secondHop.approval.isRequired = action.payload?.secondHop }, - setFirstHopApprovalState: (state, action: PayloadAction) => { - state.tradeExecution.firstHop.approvalState = action.payload + setApprovalTxHash: (state, action: PayloadAction<{ hopIndex: number; txHash: string }>) => { + const { hopIndex, txHash } = action.payload + const key = hopIndex === 0 ? 'firstHop' : 'secondHop' + state.tradeExecution[key].approval.txHash = txHash }, - setSecondHopApprovalState: (state, action: PayloadAction) => { - state.tradeExecution.secondHop.approvalState = action.payload + setSwapSellTxHash: (state, action: PayloadAction<{ hopIndex: number; sellTxHash: string }>) => { + const { hopIndex, sellTxHash } = action.payload + const key = hopIndex === 0 ? 'firstHop' : 'secondHop' + state.tradeExecution[key].swap.sellTxHash = sellTxHash }, - setFirstHopSwapState: (state, action: PayloadAction) => { - state.tradeExecution.firstHop.swapState = action.payload + setSwapBuyTxHash: (state, action: PayloadAction<{ hopIndex: number; buyTxHash: string }>) => { + const { hopIndex, buyTxHash } = action.payload + const key = hopIndex === 0 ? 'firstHop' : 'secondHop' + state.tradeExecution[key].swap.buyTxHash = buyTxHash }, - setSecondHopSwapState: (state, action: PayloadAction) => { - state.tradeExecution.secondHop.swapState = action.payload - }, - setFirstHopSwapSellTxHash: (state, action: PayloadAction) => { - state.tradeExecution.firstHop.swapSellTxHash = action.payload - }, - setSecondHopSwapSellTxHash: (state, action: PayloadAction) => { - state.tradeExecution.secondHop.swapSellTxHash = action.payload - }, - setFirstHopSwapBuyTxHash: (state, action: PayloadAction) => { - state.tradeExecution.firstHop.swapBuyTxHash = action.payload - }, - setSecondHopSwapBuyTxHash: (state, action: PayloadAction) => { - state.tradeExecution.secondHop.swapBuyTxHash = action.payload - }, - setFirstHopStreamingSwapMeta: (state, action: PayloadAction) => { - state.tradeExecution.firstHop.streamingSwap = action.payload - }, - setSecondHopStreamingSwapMeta: (state, action: PayloadAction) => { - state.tradeExecution.secondHop.streamingSwap = action.payload + setStreamingSwapMeta: ( + state, + action: PayloadAction<{ hopIndex: number; streamingSwapMetadata: StreamingSwapMetadata }>, + ) => { + const { hopIndex, streamingSwapMetadata } = action.payload + const key = hopIndex === 0 ? 'firstHop' : 'secondHop' + state.tradeExecution[key].swap.streamingSwap = streamingSwapMetadata }, }, }) diff --git a/src/state/slices/tradeQuoteSlice/types.ts b/src/state/slices/tradeQuoteSlice/types.ts index e3919cf8d5d..4e3b3c5de78 100644 --- a/src/state/slices/tradeQuoteSlice/types.ts +++ b/src/state/slices/tradeQuoteSlice/types.ts @@ -1,47 +1,29 @@ -import type { TxStatus } from '@shapeshiftoss/unchained-client' +export enum TransactionExecutionState { + AwaitingConfirmation = 'AwaitingConfirmation', + Pending = 'Pending', + Complete = 'Complete', + Failed = 'Failed', +} export enum HopExecutionState { Pending = 'Pending', - AwaitingApprovalConfirmation = 'AwaitingApprovalConfirmation', - AwaitingApprovalExecution = 'AwaitingApprovalExecution', - AwaitingTradeConfirmation = 'AwaitingTradeConfirmation', - AwaitingTradeExecution = 'AwaitingTradeExecution', + AwaitingApproval = 'AwaitingApproval', + AwaitingSwap = 'AwaitingSwap', Complete = 'Complete', } -export const HOP_EXECUTION_STATE_ORDERED = [ - HopExecutionState.Pending, - HopExecutionState.AwaitingApprovalConfirmation, - HopExecutionState.AwaitingApprovalExecution, - HopExecutionState.AwaitingTradeConfirmation, - HopExecutionState.AwaitingTradeExecution, - HopExecutionState.Complete, -] - -export enum MultiHopExecutionState { +export enum TradeExecutionState { Previewing = 'Previewing', - FirstHopAwaitingApprovalConfirmation = `firstHop_${HopExecutionState.AwaitingApprovalConfirmation}`, - FirstHopAwaitingApprovalExecution = `firstHop_${HopExecutionState.AwaitingApprovalExecution}`, - FirstHopAwaitingTradeConfirmation = `firstHop_${HopExecutionState.AwaitingTradeConfirmation}`, - FirstHopAwaitingTradeExecution = `firstHop_${HopExecutionState.AwaitingTradeExecution}`, - SecondHopAwaitingApprovalConfirmation = `secondHop_${HopExecutionState.AwaitingApprovalConfirmation}`, - SecondHopAwaitingApprovalExecution = `secondHop_${HopExecutionState.AwaitingApprovalExecution}`, - SecondHopAwaitingTradeConfirmation = `secondHop_${HopExecutionState.AwaitingTradeConfirmation}`, - SeondHopAwaitingTradeExecution = `secondHop_${HopExecutionState.AwaitingTradeExecution}`, + FirstHop = 'FirstHop', + SecondHop = 'SecondHop', TradeComplete = 'Complete', } -export const MULTI_HOP_EXECUTION_STATE_ORDERED = [ - MultiHopExecutionState.Previewing, - MultiHopExecutionState.FirstHopAwaitingApprovalConfirmation, - MultiHopExecutionState.FirstHopAwaitingApprovalExecution, - MultiHopExecutionState.FirstHopAwaitingTradeConfirmation, - MultiHopExecutionState.FirstHopAwaitingTradeExecution, - MultiHopExecutionState.SecondHopAwaitingApprovalConfirmation, - MultiHopExecutionState.SecondHopAwaitingApprovalExecution, - MultiHopExecutionState.SecondHopAwaitingTradeConfirmation, - MultiHopExecutionState.SeondHopAwaitingTradeExecution, - MultiHopExecutionState.TradeComplete, +export const HOP_EXECUTION_STATE_ORDERED = [ + HopExecutionState.Pending, + HopExecutionState.AwaitingApproval, + HopExecutionState.AwaitingSwap, + HopExecutionState.Complete, ] export type StreamingSwapFailedSwap = { @@ -55,13 +37,27 @@ export type StreamingSwapMetadata = { failedSwaps: StreamingSwapFailedSwap[] } +export type ApprovalExecutionMetadata = { + state: TransactionExecutionState + txHash?: string + isRequired?: boolean +} + +export type SwapExecutionMetadata = { + state: TransactionExecutionState + sellTxHash?: string + buyTxHash?: string + streamingSwap?: StreamingSwapMetadata +} + export type HopExecutionMetadata = { state: HopExecutionState - approvalRequired?: boolean - approvalState?: TxStatus - approvalTxHash?: string - swapState?: TxStatus - swapSellTxHash?: string - swapBuyTxHash?: string - streamingSwap?: StreamingSwapMetadata + approval: ApprovalExecutionMetadata + swap: SwapExecutionMetadata +} + +export type TradeExecutionMetadata = { + state: TradeExecutionState + firstHop: HopExecutionMetadata + secondHop: HopExecutionMetadata } diff --git a/src/state/slices/tradeQuoteSlice/utils.ts b/src/state/slices/tradeQuoteSlice/utils.ts index ee1b6b6a3d8..2275df90630 100644 --- a/src/state/slices/tradeQuoteSlice/utils.ts +++ b/src/state/slices/tradeQuoteSlice/utils.ts @@ -4,9 +4,7 @@ import type { MarketData } from '@shapeshiftoss/types' import type { BigNumber } from 'lib/bignumber/bignumber' import { bn, bnOrZero, convertPrecision } from 'lib/bignumber/bignumber' import type { ProtocolFee } from 'lib/swapper/types' -import { assertUnreachable, type PartialRecord } from 'lib/utils' - -import { HopExecutionState, MultiHopExecutionState } from './types' +import { type PartialRecord } from 'lib/utils' export const convertBasisPointsToDecimalPercentage = (basisPoints: BigNumber.Value) => bnOrZero(basisPoints).div(10000) @@ -99,113 +97,3 @@ export const sumProtocolFeesToDenom = ({ }, bn(0)) .toString() } - -// determines the next trade execution state -// please don't abstract or enhance this - -// it's intended to be as simple as possible to prevent bugs at the cost of being very verbose -export const getNextTradeExecutionState = ( - tradeExecutionState: MultiHopExecutionState, - isMultiHopTrade: boolean, - firstHopRequiresApproval: boolean, - secondHopRequiresApproval: boolean, -) => { - switch (tradeExecutionState) { - case MultiHopExecutionState.Previewing: - if (!firstHopRequiresApproval) { - return MultiHopExecutionState.FirstHopAwaitingTradeConfirmation - } - return MultiHopExecutionState.FirstHopAwaitingApprovalConfirmation - case MultiHopExecutionState.FirstHopAwaitingApprovalConfirmation: - if (!firstHopRequiresApproval) { - return MultiHopExecutionState.FirstHopAwaitingTradeConfirmation - } - return MultiHopExecutionState.FirstHopAwaitingApprovalExecution - case MultiHopExecutionState.FirstHopAwaitingApprovalExecution: - return MultiHopExecutionState.FirstHopAwaitingTradeConfirmation - case MultiHopExecutionState.FirstHopAwaitingTradeConfirmation: - return MultiHopExecutionState.FirstHopAwaitingTradeExecution - case MultiHopExecutionState.FirstHopAwaitingTradeExecution: - if (!isMultiHopTrade) { - return MultiHopExecutionState.TradeComplete - } - if (!secondHopRequiresApproval) { - return MultiHopExecutionState.SecondHopAwaitingTradeConfirmation - } - return MultiHopExecutionState.SecondHopAwaitingApprovalConfirmation - case MultiHopExecutionState.SecondHopAwaitingApprovalConfirmation: - if (!isMultiHopTrade) { - return MultiHopExecutionState.TradeComplete - } - if (!secondHopRequiresApproval) { - return MultiHopExecutionState.SecondHopAwaitingTradeConfirmation - } - return MultiHopExecutionState.SecondHopAwaitingApprovalExecution - case MultiHopExecutionState.SecondHopAwaitingApprovalExecution: - if (!isMultiHopTrade) { - return MultiHopExecutionState.TradeComplete - } - return MultiHopExecutionState.SecondHopAwaitingTradeConfirmation - case MultiHopExecutionState.SecondHopAwaitingTradeConfirmation: - if (!isMultiHopTrade) { - return MultiHopExecutionState.TradeComplete - } - return MultiHopExecutionState.SeondHopAwaitingTradeExecution - case MultiHopExecutionState.SeondHopAwaitingTradeExecution: - return MultiHopExecutionState.TradeComplete - case MultiHopExecutionState.TradeComplete: - return MultiHopExecutionState.TradeComplete - default: - assertUnreachable(tradeExecutionState) - } -} - -export const getHopExecutionStates = (tradeExecutionState: MultiHopExecutionState) => { - switch (tradeExecutionState) { - case MultiHopExecutionState.Previewing: - return { firstHop: HopExecutionState.Pending, secondHop: HopExecutionState.Pending } - case MultiHopExecutionState.FirstHopAwaitingApprovalConfirmation: - return { - firstHop: HopExecutionState.AwaitingApprovalConfirmation, - secondHop: HopExecutionState.Pending, - } - case MultiHopExecutionState.FirstHopAwaitingApprovalExecution: - return { - firstHop: HopExecutionState.AwaitingApprovalExecution, - secondHop: HopExecutionState.Pending, - } - case MultiHopExecutionState.FirstHopAwaitingTradeConfirmation: - return { - firstHop: HopExecutionState.AwaitingTradeConfirmation, - secondHop: HopExecutionState.Pending, - } - case MultiHopExecutionState.FirstHopAwaitingTradeExecution: - return { - firstHop: HopExecutionState.AwaitingTradeExecution, - secondHop: HopExecutionState.Pending, - } - case MultiHopExecutionState.SecondHopAwaitingApprovalConfirmation: - return { - firstHop: HopExecutionState.Complete, - secondHop: HopExecutionState.AwaitingApprovalConfirmation, - } - case MultiHopExecutionState.SecondHopAwaitingApprovalExecution: - return { - firstHop: HopExecutionState.Complete, - secondHop: HopExecutionState.AwaitingApprovalExecution, - } - case MultiHopExecutionState.SecondHopAwaitingTradeConfirmation: - return { - firstHop: HopExecutionState.Complete, - secondHop: HopExecutionState.AwaitingTradeConfirmation, - } - case MultiHopExecutionState.SeondHopAwaitingTradeExecution: - return { - firstHop: HopExecutionState.Complete, - secondHop: HopExecutionState.AwaitingTradeExecution, - } - case MultiHopExecutionState.TradeComplete: - return { firstHop: HopExecutionState.Complete, secondHop: HopExecutionState.Complete } - default: - assertUnreachable(tradeExecutionState) - } -} diff --git a/src/test/mocks/store.ts b/src/test/mocks/store.ts index 5f0cbacfb26..19a8766f035 100644 --- a/src/test/mocks/store.ts +++ b/src/test/mocks/store.ts @@ -2,7 +2,7 @@ import { DEFAULT_HISTORY_TIMEFRAME } from 'constants/Config' import type { ReduxState } from 'state/reducer' import { defaultAsset } from 'state/slices/assetsSlice/assetsSlice' import { CurrencyFormats } from 'state/slices/preferencesSlice/preferencesSlice' -import { HopExecutionState, MultiHopExecutionState } from 'state/slices/tradeQuoteSlice/types' +import { initialTradeExecutionState } from 'state/slices/tradeQuoteSlice/constants' const mockApiFactory = (reducerPath: T) => ({ queries: {}, @@ -185,11 +185,7 @@ export const mockStore: ReduxState = { activeQuoteIndex: undefined, confirmedQuote: undefined, activeStep: undefined, - tradeExecution: { - state: MultiHopExecutionState.Previewing, - firstHop: { state: HopExecutionState.Pending }, - secondHop: { state: HopExecutionState.Pending }, - }, + tradeExecution: initialTradeExecutionState, }, snapshot: { votingPower: undefined, diff --git a/yarn.lock b/yarn.lock index ece81784a97..31c9eb1fe72 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9357,15 +9357,15 @@ __metadata: languageName: node linkType: hard -"@shapeshiftoss/hdwallet-coinbase@npm:1.52.12": - version: 1.52.12 - resolution: "@shapeshiftoss/hdwallet-coinbase@npm:1.52.12" +"@shapeshiftoss/hdwallet-coinbase@npm:1.52.13": + version: 1.52.13 + resolution: "@shapeshiftoss/hdwallet-coinbase@npm:1.52.13" dependencies: "@coinbase/wallet-sdk": ^3.6.6 - "@shapeshiftoss/hdwallet-core": 1.52.12 + "@shapeshiftoss/hdwallet-core": 1.52.13 eth-rpc-errors: ^4.0.3 lodash: ^4.17.21 - checksum: c8f97c67863c15ab4b420a6cf60fcf4071c85b9676c1cfe9b39daaa9039e7a6f722c2f2839e40295f7352c7d3d4a36ea823194f88147d18671a47330711f20d1 + checksum: fbb1859f4aeef6abd973b72064b7b027bf63372d36e4eae906f6281211e299c5e2b4be250408a123397e6f0e70288ee58372f67b78ad3721f7d41a48faafbfa9 languageName: node linkType: hard @@ -9395,9 +9395,9 @@ __metadata: languageName: node linkType: hard -"@shapeshiftoss/hdwallet-core@npm:1.52.12": - version: 1.52.12 - resolution: "@shapeshiftoss/hdwallet-core@npm:1.52.12" +"@shapeshiftoss/hdwallet-core@npm:1.52.13": + version: 1.52.13 + resolution: "@shapeshiftoss/hdwallet-core@npm:1.52.13" dependencies: "@shapeshiftoss/proto-tx-builder": ^0.8.0 eip-712: ^1.0.0 @@ -9405,30 +9405,30 @@ __metadata: lodash: ^4.17.21 rxjs: ^6.4.0 type-assertions: ^1.1.0 - checksum: 4dba9d430e31f9e606d668736c2f49f3aa2e936c6dc5594ad8c4b28a62c5a4ab189da797b45421d48411d9fcdcdf5da4976d698faa8896d817bb105d945a4dc1 + checksum: 244b42cff8ec51e4de86f1fe700c00aebce74ecb3de930fe31a1e168013550a3affff76ecf386b6cfdd631f55a82c8f03196898e0e15eb8a9f3860e2d4dc179a languageName: node linkType: hard -"@shapeshiftoss/hdwallet-keepkey-webusb@npm:1.52.12": - version: 1.52.12 - resolution: "@shapeshiftoss/hdwallet-keepkey-webusb@npm:1.52.12" +"@shapeshiftoss/hdwallet-keepkey-webusb@npm:1.52.13": + version: 1.52.13 + resolution: "@shapeshiftoss/hdwallet-keepkey-webusb@npm:1.52.13" dependencies: - "@shapeshiftoss/hdwallet-core": 1.52.12 - "@shapeshiftoss/hdwallet-keepkey": 1.52.12 - checksum: 8027b95ed7e871be6526d60cbbd8c14bbcab1a59dc3e80b0481a629c1456d8f43eddccd33739660f801b79452d05f2319abddaf776c66bfe071e117d137fe8ee + "@shapeshiftoss/hdwallet-core": 1.52.13 + "@shapeshiftoss/hdwallet-keepkey": 1.52.13 + checksum: e6b0b549ce22bedaa77550e22b1fc1b158c8a48be917b0dd6a20dcbfe8fe2340780e1823db69ab959a5c38aa5e5997395c0a32f45b612f8f96f6858c555a4d40 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-keepkey@npm:1.52.12": - version: 1.52.12 - resolution: "@shapeshiftoss/hdwallet-keepkey@npm:1.52.12" +"@shapeshiftoss/hdwallet-keepkey@npm:1.52.13": + version: 1.52.13 + resolution: "@shapeshiftoss/hdwallet-keepkey@npm:1.52.13" dependencies: "@ethereumjs/common": ^2.4.0 "@ethereumjs/tx": ^3.3.0 "@keepkey/device-protocol": ^7.12.2 "@metamask/eth-sig-util": ^7.0.0 "@shapeshiftoss/bitcoinjs-lib": 5.2.0-shapeshift.2 - "@shapeshiftoss/hdwallet-core": 1.52.12 + "@shapeshiftoss/hdwallet-core": 1.52.13 "@shapeshiftoss/proto-tx-builder": ^0.8.0 bignumber.js: ^9.0.1 bnb-javascript-sdk-nobroadcast: ^2.16.14 @@ -9440,43 +9440,43 @@ __metadata: p-lazy: ^3.1.0 semver: ^7.3.8 tiny-secp256k1: ^1.1.6 - checksum: cb94aa67b73a8ad7ea7780be7eba3545d108cae67e7dc8ec03bfd8d52c811cff8ab40a45e60cb13712396da6ba0c9aa1a526ec2763c04175d407c2225da01e98 + checksum: 67e09a14b850aa62f957d2c997a7de51dfce354714b1d79056652245ca10469cdc8e5e9858015c63439fdb9c7c85a5388c4d4366a4f2b8c3f0b2d5df68ea7a27 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-keplr@npm:1.52.12": - version: 1.52.12 - resolution: "@shapeshiftoss/hdwallet-keplr@npm:1.52.12" +"@shapeshiftoss/hdwallet-keplr@npm:1.52.13": + version: 1.52.13 + resolution: "@shapeshiftoss/hdwallet-keplr@npm:1.52.13" dependencies: "@shapeshiftoss/caip": 8.15.0 - "@shapeshiftoss/hdwallet-core": 1.52.12 + "@shapeshiftoss/hdwallet-core": 1.52.13 "@shapeshiftoss/proto-tx-builder": ^0.8.0 "@shapeshiftoss/types": 3.1.3 base64-js: ^1.5.1 lodash: ^4.17.21 - checksum: a8da90f587f464af73d17754ea3bc05f942eb089dfc94b4e9cd0539885611c661c296dcc448b693130fc6de26ec1166ce5a8765951fec105e2806eb4409b240d + checksum: 275e28db5ddef00a9709dab6a9c09dbe85cdbc4e17d0b7f4f16a0530ab565fa2471892625fee656bb51eb85f3b764524d4a1732c2555003a419039f7ca7e58f3 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-ledger-webhid@npm:1.52.12": - version: 1.52.12 - resolution: "@shapeshiftoss/hdwallet-ledger-webhid@npm:1.52.12" +"@shapeshiftoss/hdwallet-ledger-webhid@npm:1.52.13": + version: 1.52.13 + resolution: "@shapeshiftoss/hdwallet-ledger-webhid@npm:1.52.13" dependencies: "@ledgerhq/hw-app-btc": ^10.0.8 "@ledgerhq/hw-app-eth": ^6.9.0 "@ledgerhq/hw-transport": ^6.7.0 "@ledgerhq/hw-transport-webhid": ^6.7.0 "@ledgerhq/live-common": ^21.8.2 - "@shapeshiftoss/hdwallet-core": 1.52.12 - "@shapeshiftoss/hdwallet-ledger": 1.52.12 + "@shapeshiftoss/hdwallet-core": 1.52.13 + "@shapeshiftoss/hdwallet-ledger": 1.52.13 "@types/w3c-web-hid": ^1.0.2 - checksum: 308b91de8633cdcca5ce8fa38ef910717d0ced25edc0c6e07842dd3b75687f3a57eb91285b4c2a7c088b383a9185e3f0da0a9100d508345dbd0c0a5a0e2cc1cb + checksum: e168529954670e28403aa94c73a5f4f14346eee457e6530c442ab75fabd945c6cd42e64e3e805871b9dc5640c437ecfaeb82d91cec7bd3546c8ca8dcd4a396d9 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-ledger-webusb@npm:1.52.12": - version: 1.52.12 - resolution: "@shapeshiftoss/hdwallet-ledger-webusb@npm:1.52.12" +"@shapeshiftoss/hdwallet-ledger-webusb@npm:1.52.13": + version: 1.52.13 + resolution: "@shapeshiftoss/hdwallet-ledger-webusb@npm:1.52.13" dependencies: "@ledgerhq/hw-app-btc": ^10.0.8 "@ledgerhq/hw-app-eth": ^6.9.0 @@ -9484,22 +9484,22 @@ __metadata: "@ledgerhq/hw-transport-webusb": ^6.7.0 "@ledgerhq/live-common": ^21.8.2 "@ledgerhq/logs": ^6.10.1 - "@shapeshiftoss/hdwallet-core": 1.52.12 - "@shapeshiftoss/hdwallet-ledger": 1.52.12 + "@shapeshiftoss/hdwallet-core": 1.52.13 + "@shapeshiftoss/hdwallet-ledger": 1.52.13 "@types/w3c-web-usb": ^1.0.4 p-queue: ^7.4.1 - checksum: 7c03fb21ebe96f244c559fda36924c13894d8dc241af8a531510b02b9cbdb5702d8eee0f78dd250c72f5675a9aad7409193d4ecb3a8d0a6c472eb08a2b9a76f2 + checksum: c70425ec7a13f6e3d0dc42578cfad64c7da95b2fba9fd073de4f692c1026cbdc46c585a8a7c541bbac3c6c3ad8e3b505ca8255acb0b4220a37a7326d85308b4c languageName: node linkType: hard -"@shapeshiftoss/hdwallet-ledger@npm:1.52.12": - version: 1.52.12 - resolution: "@shapeshiftoss/hdwallet-ledger@npm:1.52.12" +"@shapeshiftoss/hdwallet-ledger@npm:1.52.13": + version: 1.52.13 + resolution: "@shapeshiftoss/hdwallet-ledger@npm:1.52.13" dependencies: "@ethereumjs/common": ^2.4.0 "@ethereumjs/tx": ^3.3.0 "@shapeshiftoss/bitcoinjs-lib": 5.2.0-shapeshift.2 - "@shapeshiftoss/hdwallet-core": 1.52.12 + "@shapeshiftoss/hdwallet-core": 1.52.13 base64-js: ^1.5.1 bchaddrjs: ^0.4.4 bitcoinjs-message: ^2.0.0 @@ -9507,28 +9507,28 @@ __metadata: ethereumjs-tx: 1.3.7 ethereumjs-util: ^6.1.0 lodash: ^4.17.21 - checksum: 73dbc9c12a564370dc6829d62d7edf115a412af9b46f169d852fcef473aff59b10ccc76d955d7c897a38d21c8593532f9bb001d945e4ca44ca24337ee42991b9 + checksum: 35e36638300ab02a219adefe41515e5b2a59e48dc5596fc7a213055a22615b7de8b127b502108fccacc6c3528017ecfa63a2d7f75eb3eb34999588fc17c6fdf4 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-metamask@npm:1.52.12": - version: 1.52.12 - resolution: "@shapeshiftoss/hdwallet-metamask@npm:1.52.12" +"@shapeshiftoss/hdwallet-metamask@npm:1.52.13": + version: 1.52.13 + resolution: "@shapeshiftoss/hdwallet-metamask@npm:1.52.13" dependencies: "@metamask/detect-provider": ^1.2.0 "@metamask/onboarding": ^1.0.1 - "@shapeshiftoss/hdwallet-core": 1.52.12 + "@shapeshiftoss/hdwallet-core": 1.52.13 eth-rpc-errors: ^4.0.3 lodash: ^4.17.21 - checksum: 55b816bebc0c8e6e4ad0c0b390bfa7b7f4c7f631a3be242641484a2d06f13f7c21baceb459e74b4b9d773e2ccb9e517c5c88944ba59d17ed40d8868116273db3 + checksum: 011780356274d9373f8fd1b03003e66ca45b06b51e8170e6c333fd33346f7158e379a65f62b76fbe3efaf27ad2823ce924783e6079a98004c8d1774472530733 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-native-vault@npm:1.52.12": - version: 1.52.12 - resolution: "@shapeshiftoss/hdwallet-native-vault@npm:1.52.12" +"@shapeshiftoss/hdwallet-native-vault@npm:1.52.13": + version: 1.52.13 + resolution: "@shapeshiftoss/hdwallet-native-vault@npm:1.52.13" dependencies: - "@shapeshiftoss/hdwallet-native": 1.52.12 + "@shapeshiftoss/hdwallet-native": 1.52.13 bip39: ^3.0.4 hash-wasm: ^4.9.0 idb-keyval: ^6.0.3 @@ -9537,17 +9537,17 @@ __metadata: type-assertions: ^1.1.0 uuid: ^8.3.2 web-encoding: ^1.1.0 - checksum: e0080c503c8fcedf9ccffaf87c8f81b35a5c01197e761618b99e6d602e2764e80d67cb8596cc66347beb32e2731878bebf5c95ca8be2f1208b9c16d381040520 + checksum: 73af4258d3c84b02e07ee739a5a56854c40d1e8f62b0ff6f5d1c8e38cd2ee65b8b6f5e7ccf8c17f687399d9a77f69ee4b9c24e71b0e4a81d72ee45cb555f21ca languageName: node linkType: hard -"@shapeshiftoss/hdwallet-native@npm:1.52.12": - version: 1.52.12 - resolution: "@shapeshiftoss/hdwallet-native@npm:1.52.12" +"@shapeshiftoss/hdwallet-native@npm:1.52.13": + version: 1.52.13 + resolution: "@shapeshiftoss/hdwallet-native@npm:1.52.13" dependencies: "@shapeshiftoss/bitcoinjs-lib": 5.2.0-shapeshift.2 "@shapeshiftoss/fiosdk": 1.2.1-shapeshift.6 - "@shapeshiftoss/hdwallet-core": 1.52.12 + "@shapeshiftoss/hdwallet-core": 1.52.13 "@shapeshiftoss/proto-tx-builder": ^0.8.0 "@zxing/text-encoding": ^0.9.0 bchaddrjs: ^0.4.9 @@ -9568,7 +9568,7 @@ __metadata: tendermint-tx-builder: ^1.0.9 tiny-secp256k1: ^1.1.6 web-encoding: ^1.1.0 - checksum: 2cb5668fe613f494251daf60e320dc149ebe2966db34103abb247b651a3073361151b53a8f864a7a0d377ed53331d84cbb859a04fc8467d4b898d72cd1d7bfad + checksum: 701bb4dff2c3fc10d832b0714d4e5b98b1fa090a161cc2d98ea26a62fe39bdee29dec7cb7557b909870e472a94696914172775eedc95687f81e29cfa7fd35f32 languageName: node linkType: hard @@ -9603,41 +9603,41 @@ __metadata: languageName: node linkType: hard -"@shapeshiftoss/hdwallet-shapeshift-multichain@npm:1.52.12": - version: 1.52.12 - resolution: "@shapeshiftoss/hdwallet-shapeshift-multichain@npm:1.52.12" +"@shapeshiftoss/hdwallet-shapeshift-multichain@npm:1.52.13": + version: 1.52.13 + resolution: "@shapeshiftoss/hdwallet-shapeshift-multichain@npm:1.52.13" dependencies: "@metamask/detect-provider": ^1.2.0 "@metamask/onboarding": ^1.0.1 "@shapeshiftoss/common-api": ^9.3.0 - "@shapeshiftoss/hdwallet-core": 1.52.12 + "@shapeshiftoss/hdwallet-core": 1.52.13 "@shapeshiftoss/metamask-snaps-adapter": ^1.0.8 "@shapeshiftoss/metamask-snaps-types": ^1.0.8 eth-rpc-errors: ^4.0.3 lodash: ^4.17.21 - checksum: 19b30cb3608734be1567fbafe79b5c6c24e989783f199b7de6f53acfcb88bfbda2af95cac748a65e5657339eba72a221699076ba2b564eb1b704be0c6fd0986b + checksum: 89061b57e4f9fb64428d6086f128b6157d927be43dcbe490e2d95cb47b530a320d8892b77fdb6ca5a5105e9ea8636ab9d336b78cf614e57ebb09d2c202f3d13a languageName: node linkType: hard -"@shapeshiftoss/hdwallet-walletconnectv2@npm:1.52.12": - version: 1.52.12 - resolution: "@shapeshiftoss/hdwallet-walletconnectv2@npm:1.52.12" +"@shapeshiftoss/hdwallet-walletconnectv2@npm:1.52.13": + version: 1.52.13 + resolution: "@shapeshiftoss/hdwallet-walletconnectv2@npm:1.52.13" dependencies: - "@shapeshiftoss/hdwallet-core": 1.52.12 + "@shapeshiftoss/hdwallet-core": 1.52.13 "@walletconnect/ethereum-provider": ^2.10.1 "@walletconnect/modal": ^2.6.2 ethers: ^5.6.5 - checksum: 3113e700cf5b67d95c2ec9b78fc2c35cae171266fb35c02a096c271bfff527e2d0fc85365224927205123643dfcb2de31775e886a4b46573341264d1b504cd11 + checksum: bbfab75c410064251a50581e8e192c42581fabbca68e6a6e624db75ed353cd9ec6fb092998cded9be5145446d55f29d94327a7110d4419349257e61b718c88bc languageName: node linkType: hard -"@shapeshiftoss/hdwallet-xdefi@npm:1.52.12": - version: 1.52.12 - resolution: "@shapeshiftoss/hdwallet-xdefi@npm:1.52.12" +"@shapeshiftoss/hdwallet-xdefi@npm:1.52.13": + version: 1.52.13 + resolution: "@shapeshiftoss/hdwallet-xdefi@npm:1.52.13" dependencies: - "@shapeshiftoss/hdwallet-core": 1.52.12 + "@shapeshiftoss/hdwallet-core": 1.52.13 lodash: ^4.17.21 - checksum: 75729b3677f240bbd4616fef400263d365d4974ba30cff3cc4147ea704ec85927f173e31b31719a7c5c1bbad870e81d5dfd28b9915c2321432861917dd894747 + checksum: ad5ce764cdeb02e3b999fac3b92043a4451ba1ec389fad98016fb20eb5adc4b7022f82237cfab97f2a02be3075698495294b4dc4bf9dee20dc1d07e8ee6f5835 languageName: node linkType: hard @@ -9807,20 +9807,20 @@ __metadata: "@shapeshiftoss/caip": "workspace:^" "@shapeshiftoss/chain-adapters": "workspace:^" "@shapeshiftoss/errors": "workspace:^" - "@shapeshiftoss/hdwallet-coinbase": 1.52.12 - "@shapeshiftoss/hdwallet-core": 1.52.12 - "@shapeshiftoss/hdwallet-keepkey": 1.52.12 - "@shapeshiftoss/hdwallet-keepkey-webusb": 1.52.12 - "@shapeshiftoss/hdwallet-keplr": 1.52.12 - "@shapeshiftoss/hdwallet-ledger": 1.52.12 - "@shapeshiftoss/hdwallet-ledger-webhid": 1.52.12 - "@shapeshiftoss/hdwallet-ledger-webusb": 1.52.12 - "@shapeshiftoss/hdwallet-metamask": 1.52.12 - "@shapeshiftoss/hdwallet-native": 1.52.12 - "@shapeshiftoss/hdwallet-native-vault": 1.52.12 - "@shapeshiftoss/hdwallet-shapeshift-multichain": 1.52.12 - "@shapeshiftoss/hdwallet-walletconnectv2": 1.52.12 - "@shapeshiftoss/hdwallet-xdefi": 1.52.12 + "@shapeshiftoss/hdwallet-coinbase": 1.52.13 + "@shapeshiftoss/hdwallet-core": 1.52.13 + "@shapeshiftoss/hdwallet-keepkey": 1.52.13 + "@shapeshiftoss/hdwallet-keepkey-webusb": 1.52.13 + "@shapeshiftoss/hdwallet-keplr": 1.52.13 + "@shapeshiftoss/hdwallet-ledger": 1.52.13 + "@shapeshiftoss/hdwallet-ledger-webhid": 1.52.13 + "@shapeshiftoss/hdwallet-ledger-webusb": 1.52.13 + "@shapeshiftoss/hdwallet-metamask": 1.52.13 + "@shapeshiftoss/hdwallet-native": 1.52.13 + "@shapeshiftoss/hdwallet-native-vault": 1.52.13 + "@shapeshiftoss/hdwallet-shapeshift-multichain": 1.52.13 + "@shapeshiftoss/hdwallet-walletconnectv2": 1.52.13 + "@shapeshiftoss/hdwallet-xdefi": 1.52.13 "@shapeshiftoss/types": "workspace:^" "@shapeshiftoss/unchained-client": "workspace:^" "@sniptt/monads": ^0.5.10