From 9fd8bd9325f11e94150c77a9b3cb9a143feffe58 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 5 Nov 2024 07:42:58 +0900 Subject: [PATCH 01/62] feat: let's get this chunky boi started --- .env.dev | 2 +- .env.develop | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.dev b/.env.dev index dde868728b1..bd1c54c4273 100644 --- a/.env.dev +++ b/.env.dev @@ -1,5 +1,5 @@ # feature flags -REACT_APP_FEATURE_PUBLIC_TRADE_ROUTE=false +REACT_APP_FEATURE_PUBLIC_TRADE_ROUTE=true # logging REACT_APP_REDUX_WINDOW=false diff --git a/.env.develop b/.env.develop index d3d6011e285..2c691e13604 100644 --- a/.env.develop +++ b/.env.develop @@ -1,6 +1,6 @@ # feature flags REACT_APP_FEATURE_LIMIT_ORDERS=true -REACT_APP_FEATURE_PUBLIC_TRADE_ROUTE=false +REACT_APP_FEATURE_PUBLIC_TRADE_ROUTE=true # mixpanel REACT_APP_MIXPANEL_TOKEN=1c1369f6ea23a6404bac41b42817cc4b From d0c65db0c2750e06603fa18bd68aed850727a8a0 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 5 Nov 2024 12:25:28 +0900 Subject: [PATCH 02/62] [skip ci] wip: meaty rates vs. quotes final wire-up --- .../src/swappers/ZrxSwapper/endpoints.ts | 4 +- .../MultiHopTrade/MultiHopTrade.tsx | 18 +- .../MultiHopTradeConfirm.tsx | 1 + .../hooks.tsx/useGetSwapperTradeQuote.tsx | 2 + .../useGetTradeQuotes/useGetTradeQuotes.tsx | 59 ++- .../useGetTradeQuotes/useGetTradeRates.tsx | 338 ++++++++++++++++++ src/state/slices/tradeQuoteSlice/selectors.ts | 10 +- 7 files changed, 405 insertions(+), 27 deletions(-) create mode 100644 src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx diff --git a/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts b/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts index bc3771007cf..5b17bacf0ed 100644 --- a/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts @@ -23,7 +23,7 @@ import { type TradeQuote, } from '../../types' import { checkEvmSwapStatus, isExecutableTradeQuote } from '../../utils' -import { getZrxPseudoTradeQuote, getZrxTradeRate } from './getZrxTradeQuote/getZrxTradeQuote' +import { getZrxTradeQuote, getZrxTradeRate } from './getZrxTradeQuote/getZrxTradeQuote' import { fetchZrxQuote } from './utils/fetchFromZrx' export const zrxApi: SwapperApi = { @@ -34,7 +34,7 @@ export const zrxApi: SwapperApi = { // TODO(gomes): when we wire this up, this should consume getZrTradeQuote and we should ditch this guy // getTradeQuote() is currently consumed at input time (for all swappers, not just ZRX) with weird Frankenstein "quote endpoint fetching ZRX rate endpoint // but actually expecting quote input/output" logic. This is a temporary method to get the ZRX swapper working with the new swapper architecture. - const tradeQuoteResult = await getZrxPseudoTradeQuote( + const tradeQuoteResult = await getZrxTradeQuote( input as GetEvmTradeQuoteInputBase, assertGetEvmChainAdapter, config.REACT_APP_FEATURE_ZRX_PERMIT2, diff --git a/src/components/MultiHopTrade/MultiHopTrade.tsx b/src/components/MultiHopTrade/MultiHopTrade.tsx index 1b606e660b7..edb074b325f 100644 --- a/src/components/MultiHopTrade/MultiHopTrade.tsx +++ b/src/components/MultiHopTrade/MultiHopTrade.tsx @@ -17,6 +17,7 @@ import { Claim } from './components/TradeInput/components/Claim/Claim' import { TradeInput } from './components/TradeInput/TradeInput' import { VerifyAddresses } from './components/VerifyAddresses/VerifyAddresses' import { useGetTradeQuotes } from './hooks/useGetTradeQuotes/useGetTradeQuotes' +import { useGetTradeRates } from './hooks/useGetTradeQuotes/useGetTradeRates' import { TradeInputTab, TradeRoutePaths } from './types' const TradeRouteEntries = [ @@ -38,7 +39,12 @@ type MatchParams = { assetSubId?: string } -// dummy component to allow us to mount or unmount the `useGetTradeQuotes` hook conditionally +// dummy component to allow us to mount or unmount the `useGetTradeRates` hook conditionally +const GetTradeRates = () => { + useGetTradeRates() + return <> +} +// dummy component to allow us to mount or unmount the `useGetTradeRates` hook conditionally const GetTradeQuotes = () => { useGetTradeQuotes() return <> @@ -111,13 +117,18 @@ const TradeRoutes = memo(({ isCompact }: TradeRoutesProps) => { const tradeInputRef = useRef(null) - const shouldUseTradeQuotes = useMemo(() => { - // We only want to fetch quotes when the user is on the trade input or quote list route + const shouldUseTradeRates = useMemo(() => { + // We only want to fetch rates when the user is on the trade input or quote list route return [TradeRoutePaths.Input, TradeRoutePaths.QuoteList].includes( location.pathname as TradeRoutePaths, ) }, [location.pathname]) + const shouldUseTradeQuotes = useMemo(() => { + // We only want to fetch rates when the user is on the trade input or quote list route + return [TradeRoutePaths.Confirm].includes(location.pathname as TradeRoutePaths) + }, [location.pathname]) + const handleChangeTab = useCallback( (newTab: TradeInputTab) => { switch (newTab) { @@ -175,6 +186,7 @@ const TradeRoutes = memo(({ isCompact }: TradeRoutesProps) => { {/* Stop polling for quotes by unmounting the hook. This prevents trade execution getting */} {/* corrupted from state being mutated during trade execution. */} {/* TODO: move the hook into a react-query or similar and pass a flag */} + {shouldUseTradeRates ? : null} {shouldUseTradeQuotes ? : null} ) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx index 97a19174b75..168cf2c7fd1 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx @@ -98,6 +98,7 @@ export const MultiHopTradeConfirm = memo(() => { } }, [handleTradeConfirm, isModeratePriceImpact]) + console.log({ confirmedTradeExecutionState }) if (!confirmedTradeExecutionState) return null return ( diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks.tsx/useGetSwapperTradeQuote.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks.tsx/useGetSwapperTradeQuote.tsx index b636053e06f..2de9e1a1eb8 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks.tsx/useGetSwapperTradeQuote.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks.tsx/useGetSwapperTradeQuote.tsx @@ -51,6 +51,8 @@ export const useGetSwapperTradeQuote = ({ const queryStateMeta = swapperApi.endpoints.getTradeQuote.useQueryState(queryStateRequest) useEffect(() => { + // Ensures we don't rug the state by upserting undefined data - this is *not* the place to do so and will rug the switch between quotes and rates + if (!queryStateMeta.data) return dispatch( tradeQuoteSlice.actions.upsertTradeQuotes({ swapperName, diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index 5f92d0e54f3..a8e1e1beea7 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -11,7 +11,6 @@ import { isThorTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/Thorchain import { useCallback, useEffect, useMemo, useState } from 'react' import { getTradeQuoteInput } from 'components/MultiHopTrade/hooks/useGetTradeQuotes/getTradeQuoteInput' import { useReceiveAddress } from 'components/MultiHopTrade/hooks/useReceiveAddress' -import { useFeatureFlag } from 'hooks/useFeatureFlag/useFeatureFlag' import { useHasFocus } from 'hooks/useHasFocus' import { useWallet } from 'hooks/useWallet/useWallet' import { useWalletSupportsChain } from 'hooks/useWalletSupportsChain/useWalletSupportsChain' @@ -36,11 +35,14 @@ import { selectUserSlippagePercentageDecimal, } from 'state/slices/selectors' import { + selectActiveQuote, selectActiveQuoteMetaOrDefault, + selectHopExecutionMetadata, selectIsAnyTradeQuoteLoading, selectSortedTradeQuotes, } from 'state/slices/tradeQuoteSlice/selectors' import { tradeQuoteSlice } from 'state/slices/tradeQuoteSlice/tradeQuoteSlice' +import { HopExecutionState } from 'state/slices/tradeQuoteSlice/types' import { store, useAppDispatch, useAppSelector } from 'state/store' import type { UseGetSwapperTradeQuoteArgs } from './hooks.tsx/useGetSwapperTradeQuote' @@ -117,9 +119,34 @@ export const useGetTradeQuotes = () => { const { state: { wallet }, } = useWallet() - const isPublicTradeRouteEnabled = useFeatureFlag('PublicTradeRoute') - // TODO(gomes): This is temporary, and we will want to do one better when wiring this up - const quoteOrRate = isPublicTradeRouteEnabled ? 'rate' : 'quote' + + const sortedTradeQuotes = useAppSelector(selectSortedTradeQuotes) + const activeQuote = useAppSelector(selectActiveQuote) + const activeTradeId = activeQuote?.id + const activeQuoteMeta = useAppSelector(selectActiveQuoteMetaOrDefault) + + const hopExecutionMetadataFilter = useMemo(() => { + if (!activeTradeId) return undefined + + return { + tradeId: activeTradeId, + // TODO(gomes): multi-hop here + hopIndex: 0, + } + }, [activeTradeId]) + + const hopExecutionMetadata = useAppSelector(state => + hopExecutionMetadataFilter + ? selectHopExecutionMetadata(state, hopExecutionMetadataFilter) + : undefined, + ) + + const quoteOrRate = useMemo(() => { + return 'quote' as const + }, []) + + console.log({ hopExecutionMetadata }) + const [tradeQuoteInput, setTradeQuoteInput] = useState( skipToken, ) @@ -177,38 +204,34 @@ export const useGetTradeQuotes = () => { const walletSupportsBuyAssetChain = useWalletSupportsChain(buyAsset.chainId, wallet) const isBuyAssetChainSupported = walletSupportsBuyAssetChain + // TODO(gomes): this should *not* refetch, this should refetch the *correct* swapper/quote once and call cache it forever until unmount const shouldRefetchTradeQuotes = useMemo( () => Boolean( - (hasFocus && + hasFocus && + hopExecutionMetadata?.state === HopExecutionState.AwaitingSwap && wallet && sellAccountId && sellAccountMetadata && receiveAddress && - !isVotingPowerLoading) || - quoteOrRate === 'rate', + !isVotingPowerLoading, ), [ hasFocus, + hopExecutionMetadata?.state, wallet, sellAccountId, sellAccountMetadata, receiveAddress, isVotingPowerLoading, - quoteOrRate, ], ) useEffect(() => { - // Always invalidate tags when this effect runs - args have changed, and whether we want to fetch an actual quote - // or a "skipToken" no-op, we always want to ensure that the tags are invalidated before a new query is ran - // That effectively means we'll unsubscribe to queries, considering them stale - dispatch(swapperApi.util.invalidateTags(['TradeQuote'])) + // Only run this effect when we're actually ready + if (hopExecutionMetadata?.state !== HopExecutionState.AwaitingSwap) return - // Clear the slice before asynchronously generating the input and running the request. - // This is to ensure the initial state change is done synchronously to prevent race conditions - // and losing sync on loading state etc. - dispatch(tradeQuoteSlice.actions.clear()) + dispatch(swapperApi.util.invalidateTags(['TradeQuote'])) // Early exit on any invalid state if ( @@ -281,6 +304,7 @@ export const useGetTradeQuotes = () => { isVotingPowerLoading, isBuyAssetChainSupported, quoteOrRate, + hopExecutionMetadata?.state, ]) const getTradeQuoteArgs = useCallback( @@ -306,9 +330,6 @@ export const useGetTradeQuotes = () => { // true if any debounce, input or swapper is fetching const isAnyTradeQuoteLoading = useAppSelector(selectIsAnyTradeQuoteLoading) - const sortedTradeQuotes = useAppSelector(selectSortedTradeQuotes) - const activeQuoteMeta = useAppSelector(selectActiveQuoteMetaOrDefault) - // auto-select the best quote once all quotes have arrived useEffect(() => { // don't override user selection, don't rug users by auto-selecting while results are incoming diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx new file mode 100644 index 00000000000..9a0a79250db --- /dev/null +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx @@ -0,0 +1,338 @@ +import { skipToken } from '@reduxjs/toolkit/dist/query' +import { fromAccountId } from '@shapeshiftoss/caip' +import { isLedger } from '@shapeshiftoss/hdwallet-ledger' +import type { GetTradeQuoteInput } from '@shapeshiftoss/swapper' +import { + DEFAULT_GET_TRADE_QUOTE_POLLING_INTERVAL, + SwapperName, + swappers, +} from '@shapeshiftoss/swapper' +import { isThorTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/getThorTradeQuoteOrRate/getTradeQuoteOrRate' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { getTradeQuoteInput } from 'components/MultiHopTrade/hooks/useGetTradeQuotes/getTradeQuoteInput' +import { useReceiveAddress } from 'components/MultiHopTrade/hooks/useReceiveAddress' +import { useFeatureFlag } from 'hooks/useFeatureFlag/useFeatureFlag' +import { useHasFocus } from 'hooks/useHasFocus' +import { useWallet } from 'hooks/useWallet/useWallet' +import { useWalletSupportsChain } from 'hooks/useWalletSupportsChain/useWalletSupportsChain' +import { bnOrZero } from 'lib/bignumber/bignumber' +import { calculateFees } from 'lib/fees/model' +import type { ParameterModel } from 'lib/fees/parameters/types' +import { getMixPanel } from 'lib/mixpanel/mixPanelSingleton' +import { MixPanelEvent } from 'lib/mixpanel/types' +import { isSome } from 'lib/utils' +import { selectIsSnapshotApiQueriesPending, selectVotingPower } from 'state/apis/snapshot/selectors' +import { swapperApi } from 'state/apis/swapper/swapperApi' +import type { ApiQuote, TradeQuoteError } from 'state/apis/swapper/types' +import { + selectFirstHopSellAccountId, + selectInputBuyAsset, + selectInputSellAmountCryptoPrecision, + selectInputSellAmountUsd, + selectInputSellAsset, + selectLastHopBuyAccountId, + selectPortfolioAccountMetadataByAccountId, + selectUsdRateByAssetId, + selectUserSlippagePercentageDecimal, +} from 'state/slices/selectors' +import { + selectActiveQuoteMetaOrDefault, + selectIsAnyTradeQuoteLoading, + selectSortedTradeQuotes, +} from 'state/slices/tradeQuoteSlice/selectors' +import { tradeQuoteSlice } from 'state/slices/tradeQuoteSlice/tradeQuoteSlice' +import { store, useAppDispatch, useAppSelector } from 'state/store' + +import type { UseGetSwapperTradeQuoteArgs } from './hooks.tsx/useGetSwapperTradeQuote' +import { useGetSwapperTradeQuote } from './hooks.tsx/useGetSwapperTradeQuote' + +type MixPanelQuoteMeta = { + swapperName: SwapperName + differenceFromBestQuoteDecimalPercentage: number + quoteReceived: boolean + isStreaming: boolean + isLongtail: boolean + errors: TradeQuoteError[] + isActionable: boolean // is the individual quote actionable +} + +type GetMixPanelDataFromApiQuotesReturn = { + quoteMeta: MixPanelQuoteMeta[] + sellAssetId: string + buyAssetId: string + sellAssetChainId: string + buyAssetChainId: string + sellAmountUsd: string | undefined + version: string // ISO 8601 standard basic format date + isActionable: boolean // is any quote in the request actionable +} + +const votingPowerParams: { feeModel: ParameterModel } = { feeModel: 'SWAPPER' } +const thorVotingPowerParams: { feeModel: ParameterModel } = { feeModel: 'THORSWAP' } + +const getMixPanelDataFromApiQuotes = ( + quotes: Pick[], +): GetMixPanelDataFromApiQuotesReturn => { + const bestInputOutputRatio = quotes[0]?.inputOutputRatio + const state = store.getState() + const { assetId: sellAssetId, chainId: sellAssetChainId } = selectInputSellAsset(state) + const { assetId: buyAssetId, chainId: buyAssetChainId } = selectInputBuyAsset(state) + const sellAmountUsd = selectInputSellAmountUsd(state) + const quoteMeta: MixPanelQuoteMeta[] = quotes + .map(({ quote, errors, swapperName, inputOutputRatio }) => { + const differenceFromBestQuoteDecimalPercentage = + (inputOutputRatio / bestInputOutputRatio - 1) * -1 + return { + swapperName, + differenceFromBestQuoteDecimalPercentage, + quoteReceived: !!quote, + isStreaming: quote?.isStreaming ?? false, + isLongtail: quote?.isLongtail ?? false, + tradeType: isThorTradeQuote(quote) ? quote?.tradeType : null, + errors: errors.map(({ error }) => error), + isActionable: !!quote && !errors.length, + } + }) + .filter(isSome) + + const isActionable = quoteMeta.some(({ isActionable }) => isActionable) + + // Add a version string, in the form of an ISO 8601 standard basic format date, to the JSON blob to help with reporting + const version = '20240115' + + return { + quoteMeta, + sellAssetId, + buyAssetId, + sellAmountUsd, + sellAssetChainId, + buyAssetChainId, + version, + isActionable, + } +} + +export const useGetTradeRates = () => { + const dispatch = useAppDispatch() + const { + state: { wallet }, + } = useWallet() + const isPublicTradeRouteEnabled = useFeatureFlag('PublicTradeRoute') + + const sortedTradeQuotes = useAppSelector(selectSortedTradeQuotes) + const activeQuoteMeta = useAppSelector(selectActiveQuoteMetaOrDefault) + + const quoteOrRate = useMemo(() => { + return isPublicTradeRouteEnabled ? 'rate' : 'quote' + }, [isPublicTradeRouteEnabled]) + + const [tradeQuoteInput, setTradeQuoteInput] = useState( + skipToken, + ) + const hasFocus = useHasFocus() + const sellAsset = useAppSelector(selectInputSellAsset) + const buyAsset = useAppSelector(selectInputBuyAsset) + const useReceiveAddressArgs = useMemo( + () => ({ + fetchUnchainedAddress: Boolean(wallet && isLedger(wallet)), + }), + [wallet], + ) + const { manualReceiveAddress, walletReceiveAddress } = useReceiveAddress(useReceiveAddressArgs) + const receiveAddress = manualReceiveAddress ?? walletReceiveAddress + const sellAmountCryptoPrecision = useAppSelector(selectInputSellAmountCryptoPrecision) + + const sellAccountId = useAppSelector(selectFirstHopSellAccountId) + const buyAccountId = useAppSelector(selectLastHopBuyAccountId) + + const userSlippageTolerancePercentageDecimal = useAppSelector(selectUserSlippagePercentageDecimal) + + const sellAccountMetadataFilter = useMemo( + () => ({ + accountId: sellAccountId, + }), + [sellAccountId], + ) + + const buyAccountMetadataFilter = useMemo( + () => ({ + accountId: buyAccountId, + }), + [buyAccountId], + ) + + const sellAccountMetadata = useAppSelector(state => + selectPortfolioAccountMetadataByAccountId(state, sellAccountMetadataFilter), + ) + const receiveAccountMetadata = useAppSelector(state => + selectPortfolioAccountMetadataByAccountId(state, buyAccountMetadataFilter), + ) + + const mixpanel = getMixPanel() + + const sellAssetUsdRate = useAppSelector(state => selectUsdRateByAssetId(state, sellAsset.assetId)) + + const isSnapshotApiQueriesPending = useAppSelector(selectIsSnapshotApiQueriesPending) + const votingPower = useAppSelector(state => selectVotingPower(state, votingPowerParams)) + const thorVotingPower = useAppSelector(state => selectVotingPower(state, thorVotingPowerParams)) + const isVotingPowerLoading = useMemo( + () => isSnapshotApiQueriesPending && votingPower === undefined, + [isSnapshotApiQueriesPending, votingPower], + ) + + const walletSupportsBuyAssetChain = useWalletSupportsChain(buyAsset.chainId, wallet) + const isBuyAssetChainSupported = walletSupportsBuyAssetChain + + const shouldRefetchTradeQuotes = useMemo( + () => + Boolean( + (hasFocus && + wallet && + sellAccountId && + sellAccountMetadata && + receiveAddress && + !isVotingPowerLoading) || + quoteOrRate === 'rate', + ), + [ + hasFocus, + wallet, + sellAccountId, + sellAccountMetadata, + receiveAddress, + isVotingPowerLoading, + quoteOrRate, + ], + ) + + useEffect(() => { + // Always invalidate tags when this effect runs - args have changed, and whether we want to fetch an actual quote + // or a "skipToken" no-op, we always want to ensure that the tags are invalidated before a new query is ran + // That effectively means we'll unsubscribe to queries, considering them stale + dispatch(swapperApi.util.invalidateTags(['TradeQuote'])) + + // Clear the slice before asynchronously generating the input and running the request. + // This is to ensure the initial state change is done synchronously to prevent race conditions + // and losing sync on loading state etc. + dispatch(tradeQuoteSlice.actions.clear()) + + // Early exit on any invalid state + if ( + bnOrZero(sellAmountCryptoPrecision).isZero() || + (quoteOrRate === 'quote' && + (!sellAccountId || !sellAccountMetadata || !receiveAddress || isVotingPowerLoading)) + ) { + setTradeQuoteInput(skipToken) + dispatch(tradeQuoteSlice.actions.setIsTradeQuoteRequestAborted(true)) + return + } + ;(async () => { + const sellAccountNumber = sellAccountMetadata?.bip44Params?.accountNumber + const receiveAssetBip44Params = receiveAccountMetadata?.bip44Params + const receiveAccountNumber = receiveAssetBip44Params?.accountNumber + + const tradeAmountUsd = bnOrZero(sellAssetUsdRate).times(sellAmountCryptoPrecision) + + const { feeBps, feeBpsBeforeDiscount } = calculateFees({ + tradeAmountUsd, + foxHeld: bnOrZero(votingPower), + thorHeld: bnOrZero(thorVotingPower), + feeModel: 'SWAPPER', + }) + + const potentialAffiliateBps = feeBpsBeforeDiscount.toFixed(0) + const affiliateBps = feeBps.toFixed(0) + + if (quoteOrRate === 'quote' && sellAccountNumber === undefined) + throw new Error('sellAccountNumber is required') + if (quoteOrRate === 'quote' && !receiveAddress) throw new Error('receiveAddress is required') + + const updatedTradeQuoteInput: GetTradeQuoteInput | undefined = await getTradeQuoteInput({ + sellAsset, + sellAccountNumber, + receiveAccountNumber, + sellAccountType: sellAccountMetadata?.accountType, + buyAsset, + wallet: wallet ?? undefined, + quoteOrRate, + receiveAddress, + sellAmountBeforeFeesCryptoPrecision: sellAmountCryptoPrecision, + allowMultiHop: true, + affiliateBps, + potentialAffiliateBps, + // Pass in the user's slippage preference if it's set, else let the swapper use its default + slippageTolerancePercentageDecimal: userSlippageTolerancePercentageDecimal, + pubKey: + wallet && isLedger(wallet) && sellAccountId + ? fromAccountId(sellAccountId).account + : undefined, + }) + + setTradeQuoteInput(updatedTradeQuoteInput) + })() + }, [ + buyAsset, + dispatch, + receiveAddress, + sellAccountMetadata, + sellAmountCryptoPrecision, + sellAsset, + votingPower, + thorVotingPower, + wallet, + receiveAccountMetadata?.bip44Params, + userSlippageTolerancePercentageDecimal, + sellAssetUsdRate, + sellAccountId, + isVotingPowerLoading, + isBuyAssetChainSupported, + quoteOrRate, + ]) + + const getTradeQuoteArgs = useCallback( + (swapperName: SwapperName): UseGetSwapperTradeQuoteArgs => { + return { + swapperName, + tradeQuoteInput, + skip: !shouldRefetchTradeQuotes, + pollingInterval: + swappers[swapperName]?.pollingInterval ?? DEFAULT_GET_TRADE_QUOTE_POLLING_INTERVAL, + } + }, + [shouldRefetchTradeQuotes, tradeQuoteInput], + ) + + useGetSwapperTradeQuote(getTradeQuoteArgs(SwapperName.CowSwap)) + useGetSwapperTradeQuote(getTradeQuoteArgs(SwapperName.ArbitrumBridge)) + useGetSwapperTradeQuote(getTradeQuoteArgs(SwapperName.Portals)) + useGetSwapperTradeQuote(getTradeQuoteArgs(SwapperName.LIFI)) + useGetSwapperTradeQuote(getTradeQuoteArgs(SwapperName.Thorchain)) + useGetSwapperTradeQuote(getTradeQuoteArgs(SwapperName.Zrx)) + + // true if any debounce, input or swapper is fetching + const isAnyTradeQuoteLoading = useAppSelector(selectIsAnyTradeQuoteLoading) + + // auto-select the best quote once all quotes have arrived + useEffect(() => { + // don't override user selection, don't rug users by auto-selecting while results are incoming + if (activeQuoteMeta || isAnyTradeQuoteLoading) return + + const bestQuote: ApiQuote | undefined = selectSortedTradeQuotes(store.getState())[0] + + // don't auto-select nothing, don't auto-select errored quotes + if (bestQuote?.quote === undefined || bestQuote.errors.length > 0) { + return + } + + dispatch(tradeQuoteSlice.actions.setActiveQuote(bestQuote)) + }, [activeQuoteMeta, isAnyTradeQuoteLoading, dispatch]) + + // TODO: move to separate hook so we don't need to pull quote data into here + useEffect(() => { + if (isAnyTradeQuoteLoading) return + if (mixpanel) { + const quoteData = getMixPanelDataFromApiQuotes(sortedTradeQuotes) + mixpanel.track(MixPanelEvent.QuotesReceived, quoteData) + } + }, [sortedTradeQuotes, mixpanel, isAnyTradeQuoteLoading]) +} diff --git a/src/state/slices/tradeQuoteSlice/selectors.ts b/src/state/slices/tradeQuoteSlice/selectors.ts index 916c925124e..49a5b639729 100644 --- a/src/state/slices/tradeQuoteSlice/selectors.ts +++ b/src/state/slices/tradeQuoteSlice/selectors.ts @@ -200,7 +200,10 @@ export const selectActiveStepOrDefault: Selector = createSel ) const selectConfirmedQuote: Selector = - createDeepEqualOutputSelector(selectTradeQuoteSlice, tradeQuote => tradeQuote.confirmedQuote) + createDeepEqualOutputSelector(selectTradeQuoteSlice, tradeQuoteState => { + console.log({ tradeQuoteState }) + return tradeQuoteState.confirmedQuote + }) export const selectActiveQuoteMetaOrDefault: Selector< ReduxState, @@ -619,9 +622,10 @@ export const selectHopExecutionMetadata = createDeepEqualOutputSelector( selectTradeIdParamFromRequiredFilter, selectHopIndexParamFromRequiredFilter, (swappers, tradeId, hopIndex) => { + console.log({ tradeExecution: swappers.tradeExecution }) return hopIndex === 0 - ? swappers.tradeExecution[tradeId].firstHop - : swappers.tradeExecution[tradeId].secondHop + ? swappers.tradeExecution[tradeId]?.firstHop + : swappers.tradeExecution[tradeId]?.secondHop }, ) From 5806875ad005450b41770f598f99dd8c8b7e0a1f Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:20:13 +0900 Subject: [PATCH 03/62] feat: progression --- .../src/swappers/ZrxSwapper/endpoints.ts | 2 + .../getZrxTradeQuote/getZrxTradeQuote.ts | 4 +- .../components/TradeInput/TradeInput.tsx | 2 - .../hooks.tsx/useGetSwapperTradeQuote.tsx | 7 +- .../useGetTradeQuotes/useGetTradeQuotes.tsx | 70 ++++++++++++------- 5 files changed, 53 insertions(+), 32 deletions(-) diff --git a/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts b/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts index 5b17bacf0ed..c74e6ec5c6a 100644 --- a/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts @@ -81,6 +81,8 @@ export const zrxApi: SwapperApi = { transactionMetadata, } = steps[0] + console.log({ tradeQuote }) + const { value, to, data, estimatedGas } = await (async () => { // If this is a quote from the 0x V2 API, i.e. has `transactionMetadata`, the comment below RE // re-fetching does not apply. We must use the original transaction returned in the quote diff --git a/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts b/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts index 51766206964..05788f69d3b 100644 --- a/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts +++ b/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts @@ -170,7 +170,7 @@ async function _getZrxTradePseudoQuote( try { return Ok({ id: uuid(), - receiveAddress, + receiveAddress: undefined, potentialAffiliateBps, affiliateBps, // Slippage protection is only provided for specific pairs. @@ -752,7 +752,7 @@ async function _getZrxPermit2TradeRate( return Ok({ id: uuid(), accountNumber: undefined, - receiveAddress, + receiveAddress: undefined, potentialAffiliateBps, affiliateBps, // Slippage protection is only provided for specific pairs. diff --git a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx index e3b0443d613..2233fcc3d38 100644 --- a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx @@ -232,8 +232,6 @@ export const TradeInput = ({ isCompact, tradeInputRef, onChangeTab }: TradeInput if (!tradeQuoteStep) throw Error('missing tradeQuoteStep') if (!activeQuote) throw Error('missing activeQuote') - if (!isExecutableTradeQuote(activeQuote)) throw new Error('Unable to execute trade') - dispatch(tradeQuoteSlice.actions.setConfirmedQuote(activeQuote)) if (isLedger(wallet)) { diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks.tsx/useGetSwapperTradeQuote.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks.tsx/useGetSwapperTradeQuote.tsx index 2de9e1a1eb8..59924742b6e 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks.tsx/useGetSwapperTradeQuote.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks.tsx/useGetSwapperTradeQuote.tsx @@ -6,7 +6,7 @@ import { tradeQuoteSlice } from 'state/slices/tradeQuoteSlice/tradeQuoteSlice' import { useAppDispatch } from 'state/store' export type UseGetSwapperTradeQuoteArgs = { - swapperName: SwapperName + swapperName: SwapperName | undefined tradeQuoteInput: GetTradeQuoteInput | typeof skipToken skip: boolean pollingInterval: number | undefined @@ -20,7 +20,7 @@ export const useGetSwapperTradeQuote = ({ }: UseGetSwapperTradeQuoteArgs) => { const dispatch = useAppDispatch() const tradeQuoteRequest = useMemo(() => { - return skip || tradeQuoteInput === skipToken + return skip || tradeQuoteInput === skipToken || !swapperName ? skipToken : Object.assign({}, tradeQuoteInput, { swapperName }) }, [skip, swapperName, tradeQuoteInput]) @@ -37,7 +37,7 @@ export const useGetSwapperTradeQuote = ({ ) const queryStateRequest = useMemo(() => { - return tradeQuoteInput === skipToken + return tradeQuoteInput === skipToken || !swapperName ? skipToken : Object.assign({}, tradeQuoteInput, { swapperName }) }, [swapperName, tradeQuoteInput]) @@ -51,6 +51,7 @@ export const useGetSwapperTradeQuote = ({ const queryStateMeta = swapperApi.endpoints.getTradeQuote.useQueryState(queryStateRequest) useEffect(() => { + if (!swapperName) return // Ensures we don't rug the state by upserting undefined data - this is *not* the place to do so and will rug the switch between quotes and rates if (!queryStateMeta.data) return dispatch( diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index a8e1e1beea7..ac89b1ebe52 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -1,14 +1,14 @@ import { skipToken } from '@reduxjs/toolkit/dist/query' import { fromAccountId } from '@shapeshiftoss/caip' import { isLedger } from '@shapeshiftoss/hdwallet-ledger' -import type { GetTradeQuoteInput } from '@shapeshiftoss/swapper' +import type { GetTradeQuoteInput, SwapperName, TradeQuote } from '@shapeshiftoss/swapper' import { DEFAULT_GET_TRADE_QUOTE_POLLING_INTERVAL, - SwapperName, + isExecutableTradeQuote, swappers, } from '@shapeshiftoss/swapper' import { isThorTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/getThorTradeQuoteOrRate/getTradeQuoteOrRate' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { getTradeQuoteInput } from 'components/MultiHopTrade/hooks/useGetTradeQuotes/getTradeQuoteInput' import { useReceiveAddress } from 'components/MultiHopTrade/hooks/useReceiveAddress' import { useHasFocus } from 'hooks/useHasFocus' @@ -121,9 +121,25 @@ export const useGetTradeQuotes = () => { } = useWallet() const sortedTradeQuotes = useAppSelector(selectSortedTradeQuotes) - const activeQuote = useAppSelector(selectActiveQuote) - const activeTradeId = activeQuote?.id + const activeTrade = useAppSelector(selectActiveQuote) + const activeTradeId = activeTrade?.id + const activeRateRef = useRef() + const activeTradeIdRef = useRef() const activeQuoteMeta = useAppSelector(selectActiveQuoteMetaOrDefault) + const activeQuoteMetaRef = useRef<{ swapperName: SwapperName; identifier: string } | undefined>() + + useEffect( + () => { + activeRateRef.current = activeTrade + activeTradeIdRef.current = activeTradeId + activeQuoteMetaRef.current = activeQuoteMeta + }, + // WARNING: DO NOT SET ANY DEP HERE. + // We're using this to keep the ref of the rate and matching tradeId for it on mount. + // This should never update afterwards + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ) const hopExecutionMetadataFilter = useMemo(() => { if (!activeTradeId) return undefined @@ -209,6 +225,10 @@ export const useGetTradeQuotes = () => { () => Boolean( hasFocus && + // Only fetch quote if the current "quote" is a rate (which we have gotten from input step) + activeTrade && + !isExecutableTradeQuote(activeTrade) && + // and if we're actually at pre-execution time hopExecutionMetadata?.state === HopExecutionState.AwaitingSwap && wallet && sellAccountId && @@ -218,6 +238,7 @@ export const useGetTradeQuotes = () => { ), [ hasFocus, + activeTrade, hopExecutionMetadata?.state, wallet, sellAccountId, @@ -230,6 +251,8 @@ export const useGetTradeQuotes = () => { useEffect(() => { // Only run this effect when we're actually ready if (hopExecutionMetadata?.state !== HopExecutionState.AwaitingSwap) return + // And only run it once + if (activeTrade && isExecutableTradeQuote(activeTrade)) return dispatch(swapperApi.util.invalidateTags(['TradeQuote'])) @@ -305,14 +328,16 @@ export const useGetTradeQuotes = () => { isBuyAssetChainSupported, quoteOrRate, hopExecutionMetadata?.state, + activeTrade, ]) const getTradeQuoteArgs = useCallback( - (swapperName: SwapperName): UseGetSwapperTradeQuoteArgs => { + (swapperName: SwapperName | undefined): UseGetSwapperTradeQuoteArgs => { return { swapperName, tradeQuoteInput, - skip: !shouldRefetchTradeQuotes, + // Skip trade quotes fetching which aren't for the swapper we have a rate for + skip: !swapperName || !shouldRefetchTradeQuotes, pollingInterval: swappers[swapperName]?.pollingInterval ?? DEFAULT_GET_TRADE_QUOTE_POLLING_INTERVAL, } @@ -320,30 +345,25 @@ export const useGetTradeQuotes = () => { [shouldRefetchTradeQuotes, tradeQuoteInput], ) - useGetSwapperTradeQuote(getTradeQuoteArgs(SwapperName.CowSwap)) - useGetSwapperTradeQuote(getTradeQuoteArgs(SwapperName.ArbitrumBridge)) - useGetSwapperTradeQuote(getTradeQuoteArgs(SwapperName.Portals)) - useGetSwapperTradeQuote(getTradeQuoteArgs(SwapperName.LIFI)) - useGetSwapperTradeQuote(getTradeQuoteArgs(SwapperName.Thorchain)) - useGetSwapperTradeQuote(getTradeQuoteArgs(SwapperName.Zrx)) + const queryStateMeta = useGetSwapperTradeQuote( + getTradeQuoteArgs(activeQuoteMetaRef.current?.swapperName), + ) // true if any debounce, input or swapper is fetching const isAnyTradeQuoteLoading = useAppSelector(selectIsAnyTradeQuoteLoading) // auto-select the best quote once all quotes have arrived useEffect(() => { - // don't override user selection, don't rug users by auto-selecting while results are incoming - if (activeQuoteMeta || isAnyTradeQuoteLoading) return - - const bestQuote: ApiQuote | undefined = selectSortedTradeQuotes(store.getState())[0] - - // don't auto-select nothing, don't auto-select errored quotes - if (bestQuote?.quote === undefined || bestQuote.errors.length > 0) { - return - } - - dispatch(tradeQuoteSlice.actions.setActiveQuote(bestQuote)) - }, [activeQuoteMeta, isAnyTradeQuoteLoading, dispatch]) + const swapperName = activeQuoteMetaRef.current?.swapperName + if (!swapperName) return + if (!queryStateMeta?.data) return + const quoteData = queryStateMeta.data[swapperName] + if (!quoteData?.quote) return + + // Set as both confirmed *and* active + dispatch(tradeQuoteSlice.actions.setConfirmedQuote(quoteData?.quote)) + dispatch(tradeQuoteSlice.actions.setActiveQuote(quoteData)) + }, [activeTrade, activeQuoteMeta, dispatch, queryStateMeta.data]) // TODO: move to separate hook so we don't need to pull quote data into here useEffect(() => { From 0ea575302306127b446fb77224861ab16dfacb09 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:37:34 +0900 Subject: [PATCH 04/62] feat: getting there --- .../components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx index 168cf2c7fd1..f1347851e61 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx @@ -73,7 +73,7 @@ export const MultiHopTradeConfirm = memo(() => { previousTradeExecutionState !== confirmedTradeExecutionState && previousTradeExecutionState === TradeExecutionState.FirstHop ) { - if (isFirstHopOpen) onToggleFirstHop() + if (!isFirstHopOpen) onToggleFirstHop() if (!isSecondHopOpen) onToggleSecondHop() } }, [ From f8132008e154858bad3f17a87d28d135fff6e46d Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:41:59 +0900 Subject: [PATCH 05/62] feat: progression --- .../MultiHopTrade/components/TradeInput/TradeInput.tsx | 1 - .../hooks/useGetTradeQuotes/useGetTradeQuotes.tsx | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx index 2233fcc3d38..613099c2c25 100644 --- a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx @@ -1,5 +1,4 @@ import { isLedger } from '@shapeshiftoss/hdwallet-ledger' -import { isExecutableTradeQuote } from '@shapeshiftoss/swapper' import { isArbitrumBridgeTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ArbitrumBridgeSwapper/getTradeQuote/getTradeQuote' import type { ThorTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/getThorTradeQuoteOrRate/getTradeQuoteOrRate' import type { Asset } from '@shapeshiftoss/types' diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index ac89b1ebe52..586d6a83197 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -339,7 +339,8 @@ export const useGetTradeQuotes = () => { // Skip trade quotes fetching which aren't for the swapper we have a rate for skip: !swapperName || !shouldRefetchTradeQuotes, pollingInterval: - swappers[swapperName]?.pollingInterval ?? DEFAULT_GET_TRADE_QUOTE_POLLING_INTERVAL, + swappers[swapperName as SwapperName]?.pollingInterval ?? + DEFAULT_GET_TRADE_QUOTE_POLLING_INTERVAL, } }, [shouldRefetchTradeQuotes, tradeQuoteInput], From 3f226ad2c3f0afb3de77eb7406ce7bfa9330fc5f Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 5 Nov 2024 17:25:57 +0900 Subject: [PATCH 06/62] feat: classic zrx too --- .../swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts | 4 ++-- .../hooks/useGetTradeQuotes/useGetTradeQuotes.tsx | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts b/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts index 05788f69d3b..2544fafae2f 100644 --- a/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts +++ b/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts @@ -434,7 +434,7 @@ async function _getZrxTradeRate( return Ok({ id: uuid(), accountNumber: undefined, - receiveAddress, + receiveAddress: undefined, potentialAffiliateBps, affiliateBps, // Slippage protection is only provided for specific pairs. @@ -631,7 +631,7 @@ async function _getZrxPermit2TradeQuote( return Ok({ id: uuid(), - receiveAddress, + receiveAddress: undefined, potentialAffiliateBps, affiliateBps, // Slippage protection is always enabled for 0x api v2 unlike api v1 which was only supported on specific pairs. diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index 586d6a83197..a6ca2a27818 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -362,8 +362,10 @@ export const useGetTradeQuotes = () => { if (!quoteData?.quote) return // Set as both confirmed *and* active - dispatch(tradeQuoteSlice.actions.setConfirmedQuote(quoteData?.quote)) + dispatch(tradeQuoteSlice.actions.setConfirmedQuote(quoteData.quote)) dispatch(tradeQuoteSlice.actions.setActiveQuote(quoteData)) + // And re-confirm the trade since we're effectively resetting the state machine here + dispatch(tradeQuoteSlice.actions.confirmTrade(quoteData.quote.id)) }, [activeTrade, activeQuoteMeta, dispatch, queryStateMeta.data]) // TODO: move to separate hook so we don't need to pull quote data into here From 17d75dfd07ca0b59ea114e83a9479d73f29a49ac Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 5 Nov 2024 17:40:50 +0900 Subject: [PATCH 07/62] feat: stopping point --- .../hooks/useGetTradeQuotes/useGetTradeQuotes.tsx | 7 +++++++ src/state/slices/tradeQuoteSlice/selectors.ts | 9 +++++++++ src/state/slices/tradeQuoteSlice/tradeQuoteSlice.ts | 7 +++++++ 3 files changed, 23 insertions(+) diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index a6ca2a27818..849ff2093a3 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -37,11 +37,13 @@ import { import { selectActiveQuote, selectActiveQuoteMetaOrDefault, + selectConfirmedTradeExecution, selectHopExecutionMetadata, selectIsAnyTradeQuoteLoading, selectSortedTradeQuotes, } from 'state/slices/tradeQuoteSlice/selectors' import { tradeQuoteSlice } from 'state/slices/tradeQuoteSlice/tradeQuoteSlice' +import type { TradeExecutionMetadata } from 'state/slices/tradeQuoteSlice/types' import { HopExecutionState } from 'state/slices/tradeQuoteSlice/types' import { store, useAppDispatch, useAppSelector } from 'state/store' @@ -127,6 +129,9 @@ export const useGetTradeQuotes = () => { const activeTradeIdRef = useRef() const activeQuoteMeta = useAppSelector(selectActiveQuoteMetaOrDefault) const activeQuoteMetaRef = useRef<{ swapperName: SwapperName; identifier: string } | undefined>() + // TODO(gomes): set trade execution of quote to the same as we stopped in rate to reconciliate things + const confirmedTradeExecution = useAppSelector(selectConfirmedTradeExecution) + const confirmedTradeExecutionRef = useRef() useEffect( () => { @@ -355,6 +360,8 @@ export const useGetTradeQuotes = () => { // auto-select the best quote once all quotes have arrived useEffect(() => { + // We already have an executable active trade, don't rerun this or this will run forever + if (activeTrade && isExecutableTradeQuote(activeTrade)) return const swapperName = activeQuoteMetaRef.current?.swapperName if (!swapperName) return if (!queryStateMeta?.data) return diff --git a/src/state/slices/tradeQuoteSlice/selectors.ts b/src/state/slices/tradeQuoteSlice/selectors.ts index 49a5b639729..4518421f7e3 100644 --- a/src/state/slices/tradeQuoteSlice/selectors.ts +++ b/src/state/slices/tradeQuoteSlice/selectors.ts @@ -598,6 +598,15 @@ export const selectTradeQuoteAffiliateFeeAfterDiscountUserCurrency = createSelec }, ) +export const selectConfirmedTradeExecution = createSelector( + selectTradeQuoteSlice, + selectConfirmedQuoteTradeId, + (swappers, confirmedTradeId) => { + if (!confirmedTradeId) return + return swappers.tradeExecution[confirmedTradeId] + }, +) + export const selectConfirmedTradeExecutionState = createSelector( selectTradeQuoteSlice, selectConfirmedQuoteTradeId, diff --git a/src/state/slices/tradeQuoteSlice/tradeQuoteSlice.ts b/src/state/slices/tradeQuoteSlice/tradeQuoteSlice.ts index e537d28a0ca..a5bfa3cc54c 100644 --- a/src/state/slices/tradeQuoteSlice/tradeQuoteSlice.ts +++ b/src/state/slices/tradeQuoteSlice/tradeQuoteSlice.ts @@ -6,6 +6,7 @@ import type { InterpolationOptions } from 'node-polyglot' import type { ApiQuote } from 'state/apis/swapper/types' import { initialState, initialTradeExecutionState } from './constants' +import type { TradeExecutionMetadata } from './types' import { AllowanceKey, HopExecutionState, @@ -51,6 +52,12 @@ export const tradeQuoteSlice = createSlice({ state.confirmedQuote = action.payload state.tradeExecution[action.payload.id] = initialTradeExecutionState }, + setTradeExecutionMetadata: ( + state, + action: PayloadAction<{ id: TradeQuote['id']; executionMetadata: TradeExecutionMetadata }>, + ) => { + state.tradeExecution[action.payload.id] = action.payload.executionMetadata + }, setTradeInitialized: (state, action: PayloadAction) => { state.tradeExecution[action.payload].state = TradeExecutionState.Previewing }, From c0feac6b8526a68397a11933bf7036dfd7dbadf6 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 6 Nov 2024 08:48:32 +0900 Subject: [PATCH 08/62] feat: she's disgusting but she's working, fuck yeah --- .../getZrxTradeQuote/getZrxTradeQuote.ts | 154 ++---------------- .../swapper/src/swappers/ZrxSwapper/types.ts | 2 +- .../MultiHopTradeConfirm.tsx | 4 +- .../components/TradeInput/TradeInput.tsx | 1 + .../useGetTradeQuotes/useGetTradeQuotes.tsx | 14 +- .../slices/tradeQuoteSlice/tradeQuoteSlice.ts | 4 +- 6 files changed, 31 insertions(+), 148 deletions(-) diff --git a/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts b/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts index 2544fafae2f..74d895ac60b 100644 --- a/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts +++ b/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts @@ -59,7 +59,7 @@ export function getZrxTradeQuote( assetsById: AssetsByIdPartial, zrxBaseUrl: string, ): Promise> { - if (!isPermit2Enabled) return _getZrxTradePseudoQuote(input, assertGetEvmChainAdapter, zrxBaseUrl) + if (!isPermit2Enabled) return _getZrxTradeQuote(input, assertGetEvmChainAdapter, zrxBaseUrl) return _getZrxPermit2TradeQuote(input, assertGetEvmChainAdapter, assetsById, zrxBaseUrl) } @@ -74,143 +74,6 @@ export function getZrxTradeRate( return _getZrxPermit2TradeRate(input, assertGetEvmChainAdapter, assetsById, zrxBaseUrl) } -// TODO(gomes): temporary concept for the purpose of being non-breaking, remove me, this really gets a rate -async function _getZrxTradePseudoQuote( - input: GetEvmTradeQuoteInput, - assertGetEvmChainAdapter: (chainId: ChainId) => EvmChainAdapter, - zrxBaseUrl: string, -): Promise> { - const { - sellAsset, - buyAsset, - accountNumber, - receiveAddress, - affiliateBps, - potentialAffiliateBps, - sellAmountIncludingProtocolFeesCryptoBaseUnit, - supportsEIP1559, - chainId, - } = input - - const slippageTolerancePercentageDecimal = - input.slippageTolerancePercentageDecimal ?? - getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Zrx) - - const sellAssetChainId = sellAsset.chainId - const buyAssetChainId = buyAsset.chainId - - if (!isSupportedChainId(sellAssetChainId)) { - return Err( - makeSwapErrorRight({ - message: `unsupported chainId`, - code: TradeQuoteError.UnsupportedChain, - details: { chainId: sellAsset.chainId }, - }), - ) - } - - if (!isSupportedChainId(buyAssetChainId)) { - return Err( - makeSwapErrorRight({ - message: `unsupported chainId`, - code: TradeQuoteError.UnsupportedChain, - details: { chainId: sellAsset.chainId }, - }), - ) - } - - if (sellAssetChainId !== buyAssetChainId) { - return Err( - makeSwapErrorRight({ - message: `cross-chain not supported - both assets must be on chainId ${sellAsset.chainId}`, - code: TradeQuoteError.CrossChainNotSupported, - details: { buyAsset, sellAsset }, - }), - ) - } - - const maybeZrxPriceResponse = await fetchZrxPrice({ - buyAsset, - sellAsset, - sellAmountIncludingProtocolFeesCryptoBaseUnit, - receiveAddress, - affiliateBps, - slippageTolerancePercentageDecimal, - zrxBaseUrl, - }) - - if (maybeZrxPriceResponse.isErr()) return Err(maybeZrxPriceResponse.unwrapErr()) - const zrxPriceResponse = maybeZrxPriceResponse.unwrap() - - const { - buyAmount: buyAmountAfterFeesCryptoBaseUnit, - grossBuyAmount: buyAmountBeforeFeesCryptoBaseUnit, - price, - allowanceTarget, - gas, - expectedSlippage, - } = zrxPriceResponse - - const useSellAmount = !!sellAmountIncludingProtocolFeesCryptoBaseUnit - const rate = useSellAmount ? price : bn(1).div(price).toString() - - const adapter = assertGetEvmChainAdapter(chainId) - const { average } = await adapter.getGasFeeData() - - const networkFeeCryptoBaseUnit = evm.calcNetworkFeeCryptoBaseUnit({ - ...average, - supportsEIP1559: Boolean(supportsEIP1559), - // add gas limit buffer to account for the fact we perform all of our validation on the trade quote estimations - // which are inaccurate and not what we use for the tx to broadcast - gasLimit: bnOrZero(gas).times(1.2).toFixed(), - }) - - // 0x approvals are cheaper than trades, but we don't have dynamic quote data for them. - // Instead, we use a hardcoded gasLimit estimate in place of the estimatedGas in the 0x quote response. - try { - return Ok({ - id: uuid(), - receiveAddress: undefined, - potentialAffiliateBps, - affiliateBps, - // Slippage protection is only provided for specific pairs. - // If slippage protection is not provided, assume a no slippage limit. - // If slippage protection is provided, return the limit instead of the estimated slippage. - // https://0x.org/docs/0x-swap-api/api-references/get-swap-v1-quote - slippageTolerancePercentageDecimal: expectedSlippage - ? slippageTolerancePercentageDecimal - : undefined, - rate, - steps: [ - { - estimatedExecutionTimeMs: undefined, - allowanceContract: allowanceTarget, - buyAsset, - sellAsset, - accountNumber, - rate, - feeData: { - protocolFees: {}, - networkFeeCryptoBaseUnit, // L1 fee added inside of evm.calcNetworkFeeCryptoBaseUnit - }, - buyAmountBeforeFeesCryptoBaseUnit, - buyAmountAfterFeesCryptoBaseUnit, - sellAmountIncludingProtocolFeesCryptoBaseUnit, - source: SwapperName.Zrx, - }, - ] as SingleHopTradeQuoteSteps, - }) - } catch (err) { - return Err( - makeSwapErrorRight({ - message: 'failed to get fee data', - cause: err, - code: TradeQuoteError.NetworkFeeEstimationFailed, - }), - ) - } -} - async function _getZrxTradeQuote( input: GetEvmTradeQuoteInput, _assertGetEvmChainAdapter: (chainId: ChainId) => EvmChainAdapter, @@ -278,7 +141,15 @@ async function _getZrxTradeQuote( }) if (maybeZrxQuoteResponse.isErr()) return Err(maybeZrxQuoteResponse.unwrapErr()) - const zrxPriceResponse = maybeZrxQuoteResponse.unwrap() + const zrxQuoteResponse = maybeZrxQuoteResponse.unwrap() + + const transactionMetadata: TradeQuoteStep['transactionMetadata'] = { + to: zrxQuoteResponse.to, + data: zrxQuoteResponse.data as `0x${string}`, + gasPrice: zrxQuoteResponse.gasPrice ? BigInt(zrxQuoteResponse.gasPrice) : undefined, + gas: zrxQuoteResponse.gas ? BigInt(zrxQuoteResponse.gas) : undefined, + value: BigInt(zrxQuoteResponse.value), + } const { buyAmount: buyAmountAfterFeesCryptoBaseUnit, @@ -288,7 +159,7 @@ async function _getZrxTradeQuote( estimatedGas, gasPrice, expectedSlippage, - } = zrxPriceResponse + } = zrxQuoteResponse const useSellAmount = !!sellAmountIncludingProtocolFeesCryptoBaseUnit const rate = useSellAmount ? price : bn(1).div(price).toString() @@ -311,6 +182,7 @@ async function _getZrxTradeQuote( rate, steps: [ { + transactionMetadata, estimatedExecutionTimeMs: undefined, allowanceContract: allowanceTarget, buyAsset, @@ -631,7 +503,7 @@ async function _getZrxPermit2TradeQuote( return Ok({ id: uuid(), - receiveAddress: undefined, + receiveAddress, potentialAffiliateBps, affiliateBps, // Slippage protection is always enabled for 0x api v2 unlike api v1 which was only supported on specific pairs. diff --git a/packages/swapper/src/swappers/ZrxSwapper/types.ts b/packages/swapper/src/swappers/ZrxSwapper/types.ts index 592db4e6482..fd747539131 100644 --- a/packages/swapper/src/swappers/ZrxSwapper/types.ts +++ b/packages/swapper/src/swappers/ZrxSwapper/types.ts @@ -40,7 +40,7 @@ export type ZrxPriceResponse = { } export type ZrxQuoteResponse = ZrxPriceResponse & { - to: string + to: Address data: string value: string } diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx index f1347851e61..b632cb1660f 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx @@ -47,9 +47,11 @@ export const MultiHopTradeConfirm = memo(() => { useEffect(() => { if (isLoading || !activeQuote) return + // Only set the trade to initialized if it was actually initializing previously. Now that we shove quotes in at confirm time, we can't rely on this effect only running once. + if (confirmedTradeExecutionState !== TradeExecutionState.Initializing) return dispatch(tradeQuoteSlice.actions.setTradeInitialized(activeQuote.id)) - }, [dispatch, isLoading, activeQuote]) + }, [dispatch, isLoading, activeQuote, confirmedTradeExecutionState]) const isTradeComplete = useMemo( () => confirmedTradeExecutionState === TradeExecutionState.TradeComplete, diff --git a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx index 613099c2c25..75448879b54 100644 --- a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx @@ -232,6 +232,7 @@ export const TradeInput = ({ isCompact, tradeInputRef, onChangeTab }: TradeInput if (!activeQuote) throw Error('missing activeQuote') dispatch(tradeQuoteSlice.actions.setConfirmedQuote(activeQuote)) + dispatch(tradeQuoteSlice.actions.clearQuoteExecutionState(activeQuote.id)) if (isLedger(wallet)) { history.push({ pathname: TradeRoutePaths.VerifyAddresses }) diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index 849ff2093a3..2f7ac701fc1 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -131,7 +131,6 @@ export const useGetTradeQuotes = () => { const activeQuoteMetaRef = useRef<{ swapperName: SwapperName; identifier: string } | undefined>() // TODO(gomes): set trade execution of quote to the same as we stopped in rate to reconciliate things const confirmedTradeExecution = useAppSelector(selectConfirmedTradeExecution) - const confirmedTradeExecutionRef = useRef() useEffect( () => { @@ -360,6 +359,7 @@ export const useGetTradeQuotes = () => { // auto-select the best quote once all quotes have arrived useEffect(() => { + if (!confirmedTradeExecution) return // We already have an executable active trade, don't rerun this or this will run forever if (activeTrade && isExecutableTradeQuote(activeTrade)) return const swapperName = activeQuoteMetaRef.current?.swapperName @@ -368,12 +368,18 @@ export const useGetTradeQuotes = () => { const quoteData = queryStateMeta.data[swapperName] if (!quoteData?.quote) return + // Set the execution metadata to that of the previous rate so we can take over + dispatch( + tradeQuoteSlice.actions.setTradeExecutionMetadata({ + id: quoteData.quote.id, + executionMetadata: confirmedTradeExecution, + }), + ) // Set as both confirmed *and* active dispatch(tradeQuoteSlice.actions.setConfirmedQuote(quoteData.quote)) - dispatch(tradeQuoteSlice.actions.setActiveQuote(quoteData)) // And re-confirm the trade since we're effectively resetting the state machine here - dispatch(tradeQuoteSlice.actions.confirmTrade(quoteData.quote.id)) - }, [activeTrade, activeQuoteMeta, dispatch, queryStateMeta.data]) + // dispatch(tradeQuoteSlice.actions.confirmTrade(quoteData.quote.id)) + }, [activeTrade, activeQuoteMeta, dispatch, queryStateMeta.data, confirmedTradeExecution]) // TODO: move to separate hook so we don't need to pull quote data into here useEffect(() => { diff --git a/src/state/slices/tradeQuoteSlice/tradeQuoteSlice.ts b/src/state/slices/tradeQuoteSlice/tradeQuoteSlice.ts index a5bfa3cc54c..d2632e6d2fa 100644 --- a/src/state/slices/tradeQuoteSlice/tradeQuoteSlice.ts +++ b/src/state/slices/tradeQuoteSlice/tradeQuoteSlice.ts @@ -50,7 +50,9 @@ export const tradeQuoteSlice = createSlice({ }, setConfirmedQuote: (state, action: PayloadAction) => { state.confirmedQuote = action.payload - state.tradeExecution[action.payload.id] = initialTradeExecutionState + }, + clearQuoteExecutionState: (state, action: PayloadAction) => { + state.tradeExecution[action.payload] = initialTradeExecutionState }, setTradeExecutionMetadata: ( state, From e0c4124150fdc352387946d6cb19a4730a671753 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 6 Nov 2024 08:51:27 +0900 Subject: [PATCH 09/62] feat: feelsgoodman.jpg --- .../src/swappers/ZrxSwapper/endpoints.ts | 52 ++++--------------- 1 file changed, 9 insertions(+), 43 deletions(-) diff --git a/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts b/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts index c74e6ec5c6a..686ee22be0c 100644 --- a/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts @@ -5,7 +5,6 @@ import BigNumber from 'bignumber.js' import type { Hex } from 'viem' import { concat, numberToHex, size } from 'viem' -import { getDefaultSlippageDecimalPercentageForSwapper } from '../../constants' import type { CommonTradeQuoteInput, GetEvmTradeQuoteInputBase, @@ -19,12 +18,10 @@ import { type SwapErrorRight, type SwapperApi, type SwapperDeps, - SwapperName, type TradeQuote, } from '../../types' import { checkEvmSwapStatus, isExecutableTradeQuote } from '../../utils' import { getZrxTradeQuote, getZrxTradeRate } from './getZrxTradeQuote/getZrxTradeQuote' -import { fetchZrxQuote } from './utils/fetchFromZrx' export const zrxApi: SwapperApi = { getTradeQuote: async ( @@ -69,52 +66,21 @@ export const zrxApi: SwapperApi = { permit2Signature, supportsEIP1559, assertGetEvmChainAdapter, - config, }: GetUnsignedEvmTransactionArgs): Promise => { if (!isExecutableTradeQuote(tradeQuote)) throw new Error('Unable to execute trade') - const { affiliateBps, receiveAddress, slippageTolerancePercentageDecimal, steps } = tradeQuote - const { - buyAsset, - sellAsset, - sellAmountIncludingProtocolFeesCryptoBaseUnit, - transactionMetadata, - } = steps[0] + const { steps } = tradeQuote + const { transactionMetadata } = steps[0] - console.log({ tradeQuote }) + if (!transactionMetadata) throw new Error('Transaction metadata is required') - const { value, to, data, estimatedGas } = await (async () => { - // If this is a quote from the 0x V2 API, i.e. has `transactionMetadata`, the comment below RE - // re-fetching does not apply. We must use the original transaction returned in the quote - // because the Permit2 signature is coupled to it. - if (transactionMetadata) { - return { - value: transactionMetadata.value?.toString() ?? '0', - to: transactionMetadata.to ?? '0x', - data: transactionMetadata.data ?? '0x', - estimatedGas: transactionMetadata.gas?.toString() ?? '0', - } + const { value, to, data, estimatedGas } = (() => { + return { + value: transactionMetadata.value?.toString() ?? '0', + to: transactionMetadata.to ?? '0x', + data: transactionMetadata.data ?? '0x', + estimatedGas: transactionMetadata.gas?.toString() ?? '0', } - - // We need to re-fetch the quote from 0x here because actual quote fetches include validation of - // approvals, which prevent quotes during trade input from succeeding if the user hasn't already - // approved the token they are getting a quote for. - // TODO: we'll want to let users know if the quoted amounts change much after re-fetching - const zrxQuoteResponse = await fetchZrxQuote({ - buyAsset, - sellAsset, - sellAmountIncludingProtocolFeesCryptoBaseUnit, - receiveAddress, - affiliateBps: affiliateBps ?? '0', - slippageTolerancePercentageDecimal: - slippageTolerancePercentageDecimal ?? - getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Zrx), - zrxBaseUrl: config.REACT_APP_ZRX_BASE_URL, - }) - - if (zrxQuoteResponse.isErr()) throw zrxQuoteResponse.unwrapErr() - - return zrxQuoteResponse.unwrap() })() const calldataWithSignature = (() => { From 5192de70886985923836a8a1bc77a474ce824ed8 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:00:56 +0900 Subject: [PATCH 10/62] feat: cleanup --- .../swapper/src/swappers/ZrxSwapper/endpoints.ts | 11 ++--------- .../ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts | 12 ++++++------ packages/swapper/src/types.ts | 10 ++++++++-- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts b/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts index 686ee22be0c..c0c8c1fda22 100644 --- a/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts @@ -74,14 +74,7 @@ export const zrxApi: SwapperApi = { if (!transactionMetadata) throw new Error('Transaction metadata is required') - const { value, to, data, estimatedGas } = (() => { - return { - value: transactionMetadata.value?.toString() ?? '0', - to: transactionMetadata.to ?? '0x', - data: transactionMetadata.data ?? '0x', - estimatedGas: transactionMetadata.gas?.toString() ?? '0', - } - })() + const { value, to, data, gas: estimatedGas } = transactionMetadata const calldataWithSignature = (() => { if (!permit2Signature) return data @@ -114,7 +107,7 @@ export const zrxApi: SwapperApi = { chainId: Number(fromChainId(chainId).chainReference), // Use the higher amount of the node or the API, as the node doesn't always provide enough gas padding for // total gas used. - gasLimit: BigNumber.max(gasLimit, estimatedGas).toFixed(), + gasLimit: BigNumber.max(gasLimit, estimatedGas ?? '0').toFixed(), ...feeData, } }, diff --git a/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts b/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts index 74d895ac60b..a60af7033fc 100644 --- a/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts +++ b/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts @@ -146,9 +146,9 @@ async function _getZrxTradeQuote( const transactionMetadata: TradeQuoteStep['transactionMetadata'] = { to: zrxQuoteResponse.to, data: zrxQuoteResponse.data as `0x${string}`, - gasPrice: zrxQuoteResponse.gasPrice ? BigInt(zrxQuoteResponse.gasPrice) : undefined, - gas: zrxQuoteResponse.gas ? BigInt(zrxQuoteResponse.gas) : undefined, - value: BigInt(zrxQuoteResponse.value), + gasPrice: zrxQuoteResponse.gasPrice ? zrxQuoteResponse.gasPrice : undefined, + gas: zrxQuoteResponse.gas ? zrxQuoteResponse.gas : undefined, + value: zrxQuoteResponse.value, } const { @@ -442,9 +442,9 @@ async function _getZrxPermit2TradeQuote( const transactionMetadata: TradeQuoteStep['transactionMetadata'] = { to: transaction.to, data: transaction.data as `0x${string}`, - gasPrice: transaction.gasPrice ? BigInt(transaction.gasPrice) : undefined, - gas: transaction.gas ? BigInt(transaction.gas) : undefined, - value: BigInt(transaction.value), + gasPrice: transaction.gasPrice ? transaction.gasPrice : undefined, + gas: transaction.gas ? transaction.gas : undefined, + value: transaction.value, } // for the rate to be valid, both amounts must be converted to the same precision diff --git a/packages/swapper/src/types.ts b/packages/swapper/src/types.ts index ca9595785ef..10cceccb647 100644 --- a/packages/swapper/src/types.ts +++ b/packages/swapper/src/types.ts @@ -22,7 +22,7 @@ import type { evm, TxStatus } from '@shapeshiftoss/unchained-client' import type { Result } from '@sniptt/monads' import type { TypedData } from 'eip-712' import type { InterpolationOptions } from 'node-polyglot' -import type { TransactionRequest } from 'viem' +import type { Address, TransactionRequest } from 'viem' import type { CowMessageToSign } from './swappers/CowSwapper/types' import type { makeSwapperAxiosServiceMonadic } from './utils' @@ -243,7 +243,13 @@ export type TradeQuoteStep = { allowanceContract: string estimatedExecutionTimeMs: number | undefined permit2Eip712?: TypedData - transactionMetadata?: Pick + transactionMetadata?: { + to: Address + data: Address + gasPrice: string | undefined + gas: string | undefined + value: string + } } export type TradeRateStep = Omit & { accountNumber: undefined } From 2e6d33729e708f7086a00bd090ffb212bf3ac0ce Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:03:58 +0900 Subject: [PATCH 11/62] feat: more cleanup --- .../useGetTradeQuotes/useGetTradeRates.tsx | 41 ++----------------- 1 file changed, 3 insertions(+), 38 deletions(-) diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx index 9a0a79250db..b725e7a5bc0 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx @@ -11,7 +11,6 @@ import { isThorTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/Thorchain import { useCallback, useEffect, useMemo, useState } from 'react' import { getTradeQuoteInput } from 'components/MultiHopTrade/hooks/useGetTradeQuotes/getTradeQuoteInput' import { useReceiveAddress } from 'components/MultiHopTrade/hooks/useReceiveAddress' -import { useFeatureFlag } from 'hooks/useFeatureFlag/useFeatureFlag' import { useHasFocus } from 'hooks/useHasFocus' import { useWallet } from 'hooks/useWallet/useWallet' import { useWalletSupportsChain } from 'hooks/useWalletSupportsChain/useWalletSupportsChain' @@ -117,15 +116,10 @@ export const useGetTradeRates = () => { const { state: { wallet }, } = useWallet() - const isPublicTradeRouteEnabled = useFeatureFlag('PublicTradeRoute') const sortedTradeQuotes = useAppSelector(selectSortedTradeQuotes) const activeQuoteMeta = useAppSelector(selectActiveQuoteMetaOrDefault) - const quoteOrRate = useMemo(() => { - return isPublicTradeRouteEnabled ? 'rate' : 'quote' - }, [isPublicTradeRouteEnabled]) - const [tradeQuoteInput, setTradeQuoteInput] = useState( skipToken, ) @@ -183,27 +177,7 @@ export const useGetTradeRates = () => { const walletSupportsBuyAssetChain = useWalletSupportsChain(buyAsset.chainId, wallet) const isBuyAssetChainSupported = walletSupportsBuyAssetChain - const shouldRefetchTradeQuotes = useMemo( - () => - Boolean( - (hasFocus && - wallet && - sellAccountId && - sellAccountMetadata && - receiveAddress && - !isVotingPowerLoading) || - quoteOrRate === 'rate', - ), - [ - hasFocus, - wallet, - sellAccountId, - sellAccountMetadata, - receiveAddress, - isVotingPowerLoading, - quoteOrRate, - ], - ) + const shouldRefetchTradeQuotes = useMemo(() => hasFocus, [hasFocus]) useEffect(() => { // Always invalidate tags when this effect runs - args have changed, and whether we want to fetch an actual quote @@ -217,11 +191,7 @@ export const useGetTradeRates = () => { dispatch(tradeQuoteSlice.actions.clear()) // Early exit on any invalid state - if ( - bnOrZero(sellAmountCryptoPrecision).isZero() || - (quoteOrRate === 'quote' && - (!sellAccountId || !sellAccountMetadata || !receiveAddress || isVotingPowerLoading)) - ) { + if (bnOrZero(sellAmountCryptoPrecision).isZero()) { setTradeQuoteInput(skipToken) dispatch(tradeQuoteSlice.actions.setIsTradeQuoteRequestAborted(true)) return @@ -243,10 +213,6 @@ export const useGetTradeRates = () => { const potentialAffiliateBps = feeBpsBeforeDiscount.toFixed(0) const affiliateBps = feeBps.toFixed(0) - if (quoteOrRate === 'quote' && sellAccountNumber === undefined) - throw new Error('sellAccountNumber is required') - if (quoteOrRate === 'quote' && !receiveAddress) throw new Error('receiveAddress is required') - const updatedTradeQuoteInput: GetTradeQuoteInput | undefined = await getTradeQuoteInput({ sellAsset, sellAccountNumber, @@ -254,7 +220,7 @@ export const useGetTradeRates = () => { sellAccountType: sellAccountMetadata?.accountType, buyAsset, wallet: wallet ?? undefined, - quoteOrRate, + quoteOrRate: 'rate', receiveAddress, sellAmountBeforeFeesCryptoPrecision: sellAmountCryptoPrecision, allowMultiHop: true, @@ -286,7 +252,6 @@ export const useGetTradeRates = () => { sellAccountId, isVotingPowerLoading, isBuyAssetChainSupported, - quoteOrRate, ]) const getTradeQuoteArgs = useCallback( From c737ce06d09a70e5012162ed1338000dda2fa2ac Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:05:15 +0900 Subject: [PATCH 12/62] feat: more more cleanup --- .../hooks/useGetTradeQuotes/useGetTradeQuotes.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index 2f7ac701fc1..a3285515e55 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -43,7 +43,6 @@ import { selectSortedTradeQuotes, } from 'state/slices/tradeQuoteSlice/selectors' import { tradeQuoteSlice } from 'state/slices/tradeQuoteSlice/tradeQuoteSlice' -import type { TradeExecutionMetadata } from 'state/slices/tradeQuoteSlice/types' import { HopExecutionState } from 'state/slices/tradeQuoteSlice/types' import { store, useAppDispatch, useAppSelector } from 'state/store' @@ -224,8 +223,7 @@ export const useGetTradeQuotes = () => { const walletSupportsBuyAssetChain = useWalletSupportsChain(buyAsset.chainId, wallet) const isBuyAssetChainSupported = walletSupportsBuyAssetChain - // TODO(gomes): this should *not* refetch, this should refetch the *correct* swapper/quote once and call cache it forever until unmount - const shouldRefetchTradeQuotes = useMemo( + const shouldFetchTradeQuotes = useMemo( () => Boolean( hasFocus && @@ -341,13 +339,13 @@ export const useGetTradeQuotes = () => { swapperName, tradeQuoteInput, // Skip trade quotes fetching which aren't for the swapper we have a rate for - skip: !swapperName || !shouldRefetchTradeQuotes, + skip: !swapperName || !shouldFetchTradeQuotes, pollingInterval: swappers[swapperName as SwapperName]?.pollingInterval ?? DEFAULT_GET_TRADE_QUOTE_POLLING_INTERVAL, } }, - [shouldRefetchTradeQuotes, tradeQuoteInput], + [shouldFetchTradeQuotes, tradeQuoteInput], ) const queryStateMeta = useGetSwapperTradeQuote( From 939309b7b00eef97b8e4b8270b69f8a8354486e7 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:06:03 +0900 Subject: [PATCH 13/62] feat: more more more cleanup --- .../useGetTradeQuotes/useGetTradeQuotes.tsx | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index a3285515e55..c6a9a1947b0 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -160,12 +160,6 @@ export const useGetTradeQuotes = () => { : undefined, ) - const quoteOrRate = useMemo(() => { - return 'quote' as const - }, []) - - console.log({ hopExecutionMetadata }) - const [tradeQuoteInput, setTradeQuoteInput] = useState( skipToken, ) @@ -261,8 +255,10 @@ export const useGetTradeQuotes = () => { // Early exit on any invalid state if ( bnOrZero(sellAmountCryptoPrecision).isZero() || - (quoteOrRate === 'quote' && - (!sellAccountId || !sellAccountMetadata || !receiveAddress || isVotingPowerLoading)) + !sellAccountId || + !sellAccountMetadata || + !receiveAddress || + isVotingPowerLoading ) { setTradeQuoteInput(skipToken) dispatch(tradeQuoteSlice.actions.setIsTradeQuoteRequestAborted(true)) @@ -285,9 +281,8 @@ export const useGetTradeQuotes = () => { const potentialAffiliateBps = feeBpsBeforeDiscount.toFixed(0) const affiliateBps = feeBps.toFixed(0) - if (quoteOrRate === 'quote' && sellAccountNumber === undefined) - throw new Error('sellAccountNumber is required') - if (quoteOrRate === 'quote' && !receiveAddress) throw new Error('receiveAddress is required') + if (sellAccountNumber === undefined) throw new Error('sellAccountNumber is required') + if (!receiveAddress) throw new Error('receiveAddress is required') const updatedTradeQuoteInput: GetTradeQuoteInput | undefined = await getTradeQuoteInput({ sellAsset, @@ -296,7 +291,7 @@ export const useGetTradeQuotes = () => { sellAccountType: sellAccountMetadata?.accountType, buyAsset, wallet: wallet ?? undefined, - quoteOrRate, + quoteOrRate: 'quote', receiveAddress, sellAmountBeforeFeesCryptoPrecision: sellAmountCryptoPrecision, allowMultiHop: true, @@ -328,7 +323,6 @@ export const useGetTradeQuotes = () => { sellAccountId, isVotingPowerLoading, isBuyAssetChainSupported, - quoteOrRate, hopExecutionMetadata?.state, activeTrade, ]) From ee990ec36ce253bdf472ec319a5bc0c3272578c2 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 6 Nov 2024 13:48:55 +0900 Subject: [PATCH 14/62] feat: permit2 progression --- .../getZrxTradeQuote/getZrxTradeQuote.ts | 24 ++++------ .../swappers/ZrxSwapper/utils/fetchFromZrx.ts | 14 +++--- packages/swapper/src/types.ts | 2 +- .../MultiHopTrade/MultiHopTrade.tsx | 11 ----- .../ApprovalStep/hooks/usePermit2Content.tsx | 20 ++++---- .../components/HopTransactionStep.tsx | 3 ++ .../hooks/useSignPermit2.tsx | 46 +++++++++++++++++-- 7 files changed, 74 insertions(+), 46 deletions(-) diff --git a/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts b/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts index a60af7033fc..cbcb95b4dbe 100644 --- a/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts +++ b/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts @@ -32,15 +32,7 @@ import { } from '../utils/fetchFromZrx' import { assetIdToZrxToken, isSupportedChainId, zrxTokenToAssetId } from '../utils/helpers/helpers' -// We can' just split between quotes and rates just yet, but need a temporary notion of a "quote which is actually a rate which is acting like a quote". -// This is because of (classic, not permit2 ZRX) being v. weird -// I've lost too many brain cells with the current flow, and we *have* to have some intermediary notion of a "pseudo-quote", or however we want to call it. -// The reason why we need this: -// - Current getTradeQuote endpoint actually calls the `/price` endpoint, which seems relatively simple to tackle and seemingly just a rename needed. -// - However, we then use the `gas` (gasLimit) property and factor supportsEIP1559 to do pseudo-fees calculation -// This means that the current implementation isn't really a rate (requires quote input), but not really a quote either (we don't fetch the quote endpoint), -// and to avoid breaking changes in the interim, we've renamed the current `_getTradeQuote` implementation to this -// TODO(gomes): ditch this when wiring things up, and bring sanity back to the world +// TODO(gomes): rm me and update tests back to getZrxTradeQuote export function getZrxPseudoTradeQuote( input: GetEvmTradeQuoteInputBase, assertGetEvmChainAdapter: (chainId: ChainId) => EvmChainAdapter, @@ -134,7 +126,8 @@ async function _getZrxTradeQuote( buyAsset, sellAsset, sellAmountIncludingProtocolFeesCryptoBaseUnit, - receiveAddress, + // Cross-account not supported for ZRX + sellAddress: receiveAddress, affiliateBps, slippageTolerancePercentageDecimal, zrxBaseUrl, @@ -269,7 +262,8 @@ async function _getZrxTradeRate( buyAsset, sellAsset, sellAmountIncludingProtocolFeesCryptoBaseUnit, - receiveAddress, + // Cross-account not supported for ZRX + sellAddress: receiveAddress, affiliateBps, slippageTolerancePercentageDecimal, zrxBaseUrl, @@ -406,7 +400,8 @@ async function _getZrxPermit2TradeQuote( buyAsset, sellAsset, sellAmountIncludingProtocolFeesCryptoBaseUnit, - receiveAddress, + // Cross-account not supported for ZRX + sellAddress: receiveAddress, affiliateBps, slippageTolerancePercentageDecimal, zrxBaseUrl, @@ -599,7 +594,8 @@ async function _getZrxPermit2TradeRate( buyAsset, sellAsset, sellAmountIncludingProtocolFeesCryptoBaseUnit, - receiveAddress, + // Cross-account not supported for ZRX + sellAddress: receiveAddress, affiliateBps, slippageTolerancePercentageDecimal, zrxBaseUrl, @@ -637,7 +633,7 @@ async function _getZrxPermit2TradeRate( { estimatedExecutionTimeMs: undefined, // We don't care about this - this is a rate, and if we really wanted to, we know the permit2 allowance target - allowanceContract: undefined, + allowanceContract: isNativeEvmAsset(sellAsset.assetId) ? undefined : PERMIT2_CONTRACT, buyAsset, sellAsset, accountNumber, diff --git a/packages/swapper/src/swappers/ZrxSwapper/utils/fetchFromZrx.ts b/packages/swapper/src/swappers/ZrxSwapper/utils/fetchFromZrx.ts index 958d97f8c98..32557e929e3 100644 --- a/packages/swapper/src/swappers/ZrxSwapper/utils/fetchFromZrx.ts +++ b/packages/swapper/src/swappers/ZrxSwapper/utils/fetchFromZrx.ts @@ -27,7 +27,7 @@ type FetchFromZrxInput = { buyAsset: Asset sellAsset: Asset sellAmountIncludingProtocolFeesCryptoBaseUnit: string - receiveAddress: string | undefined + sellAddress: string | undefined affiliateBps: string slippageTolerancePercentageDecimal: string zrxBaseUrl: string @@ -37,7 +37,7 @@ type FetchZrxQuoteInput = { buyAsset: Asset sellAsset: Asset sellAmountIncludingProtocolFeesCryptoBaseUnit: string - receiveAddress: string + sellAddress: string affiliateBps: string slippageTolerancePercentageDecimal: string zrxBaseUrl: string @@ -47,7 +47,7 @@ type FetchZrxPriceInput = { buyAsset: Asset sellAsset: Asset sellAmountIncludingProtocolFeesCryptoBaseUnit: string - receiveAddress: string | undefined + sellAddress: string | undefined affiliateBps: string slippageTolerancePercentageDecimal: string zrxBaseUrl: string @@ -58,7 +58,7 @@ const fetchFromZrx = async ({ buyAsset, sellAsset, sellAmountIncludingProtocolFeesCryptoBaseUnit, - receiveAddress, + sellAddress, affiliateBps, slippageTolerancePercentageDecimal, zrxBaseUrl, @@ -86,7 +86,7 @@ const fetchFromZrx = async ({ buyToken: assetIdToZrxToken(buyAsset.assetId), sellToken: assetIdToZrxToken(sellAsset.assetId), sellAmount: sellAmountIncludingProtocolFeesCryptoBaseUnit, - takerAddress: receiveAddress, + takerAddress: sellAddress, affiliateAddress: AFFILIATE_ADDRESS, // Used for 0x analytics // Always skip validation, so that we can get a quote even if no wallet is connected skipValidation: true, @@ -125,7 +125,7 @@ const fetchFromZrxPermit2 = async ({ buyAsset, sellAsset, sellAmountIncludingProtocolFeesCryptoBaseUnit, - receiveAddress, + sellAddress, affiliateBps, slippageTolerancePercentageDecimal, zrxBaseUrl, @@ -153,7 +153,7 @@ const fetchFromZrxPermit2 = async ({ buyToken: assetIdToZrxToken(buyAsset.assetId), sellToken: assetIdToZrxToken(sellAsset.assetId), sellAmount: sellAmountIncludingProtocolFeesCryptoBaseUnit, - taker: receiveAddress, + taker: sellAddress, swapFeeBps: parseInt(affiliateBps), swapFeeToken: assetIdToZrxToken(buyAsset.assetId), // must be set to the buy asset to simplify fee calcs slippageBps: convertDecimalPercentageToBasisPoints( diff --git a/packages/swapper/src/types.ts b/packages/swapper/src/types.ts index 10cceccb647..cfa6e9f11fe 100644 --- a/packages/swapper/src/types.ts +++ b/packages/swapper/src/types.ts @@ -22,7 +22,7 @@ import type { evm, TxStatus } from '@shapeshiftoss/unchained-client' import type { Result } from '@sniptt/monads' import type { TypedData } from 'eip-712' import type { InterpolationOptions } from 'node-polyglot' -import type { Address, TransactionRequest } from 'viem' +import type { Address } from 'viem' import type { CowMessageToSign } from './swappers/CowSwapper/types' import type { makeSwapperAxiosServiceMonadic } from './utils' diff --git a/src/components/MultiHopTrade/MultiHopTrade.tsx b/src/components/MultiHopTrade/MultiHopTrade.tsx index edb074b325f..e28c6baca0a 100644 --- a/src/components/MultiHopTrade/MultiHopTrade.tsx +++ b/src/components/MultiHopTrade/MultiHopTrade.tsx @@ -44,11 +44,6 @@ const GetTradeRates = () => { useGetTradeRates() return <> } -// dummy component to allow us to mount or unmount the `useGetTradeRates` hook conditionally -const GetTradeQuotes = () => { - useGetTradeQuotes() - return <> -} export const MultiHopTrade = memo(({ defaultBuyAssetId, isCompact }: TradeCardProps) => { const location = useLocation() @@ -124,11 +119,6 @@ const TradeRoutes = memo(({ isCompact }: TradeRoutesProps) => { ) }, [location.pathname]) - const shouldUseTradeQuotes = useMemo(() => { - // We only want to fetch rates when the user is on the trade input or quote list route - return [TradeRoutePaths.Confirm].includes(location.pathname as TradeRoutePaths) - }, [location.pathname]) - const handleChangeTab = useCallback( (newTab: TradeInputTab) => { switch (newTab) { @@ -187,7 +177,6 @@ const TradeRoutes = memo(({ isCompact }: TradeRoutesProps) => { {/* corrupted from state being mutated during trade execution. */} {/* TODO: move the hook into a react-query or similar and pass a flag */} {shouldUseTradeRates ? : null} - {shouldUseTradeQuotes ? : null} ) }) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep/hooks/usePermit2Content.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep/hooks/usePermit2Content.tsx index bd7e0738bce..87c6ad62ed7 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep/hooks/usePermit2Content.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep/hooks/usePermit2Content.tsx @@ -38,16 +38,16 @@ export const usePermit2Content = ({ allowanceApproval, } = useAppSelector(state => selectHopExecutionMetadata(state, hopExecutionMetadataFilter)) - const { signPermit2 } = useSignPermit2(tradeQuoteStep, hopIndex, activeTradeId) + const { isLoading, signPermit2 } = useSignPermit2(tradeQuoteStep, hopIndex, activeTradeId) const isButtonDisabled = useMemo(() => { const isAwaitingPermit2 = hopExecutionState === HopExecutionState.AwaitingPermit2 const isError = permit2.state === TransactionExecutionState.Failed const isAwaitingConfirmation = permit2.state === TransactionExecutionState.AwaitingConfirmation - const isDisabled = !isAwaitingPermit2 || !(isError || isAwaitingConfirmation) + const isDisabled = isLoading || !isAwaitingPermit2 || !(isError || isAwaitingConfirmation) return isDisabled - }, [permit2.state, hopExecutionState]) + }, [hopExecutionState, permit2.state, isLoading]) const subHeadingTranslation: [string, InterpolationOptions] = useMemo(() => { return ['trade.permit2.description', { symbol: tradeQuoteStep.sellAsset.symbol }] @@ -59,10 +59,7 @@ export const usePermit2Content = ({ ) - }, [hopExecutionState, isButtonDisabled, permit2.state, signPermit2, subHeadingTranslation]) + }, [ + hopExecutionState, + isButtonDisabled, + isLoading, + permit2.state, + signPermit2, + subHeadingTranslation, + ]) const description = useMemo(() => { const txLines = [ diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx index 780ed925725..9edb8dc0beb 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx @@ -13,6 +13,7 @@ import type { KnownChainIds } from '@shapeshiftoss/types' import { useCallback, useMemo } from 'react' import { useTranslate } from 'react-polyglot' import { MiddleEllipsis } from 'components/MiddleEllipsis/MiddleEllipsis' +import { useGetTradeQuotes } from 'components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes' import { RawText, Text } from 'components/Text' import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingleton' import { useSafeTxQuery } from 'hooks/queries/useSafeTx' @@ -152,6 +153,8 @@ export const HopTransactionStep = ({ return }, [swapTxState, swapperName]) + useGetTradeQuotes() + const content = useMemo(() => { if (isActive && swapTxState === TransactionExecutionState.AwaitingConfirmation) { return ( diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useSignPermit2.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useSignPermit2.tsx index 3fc29269783..0afdaf67aaf 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useSignPermit2.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useSignPermit2.tsx @@ -1,6 +1,10 @@ +import { fromAccountId } from '@shapeshiftoss/caip' import { toAddressNList } from '@shapeshiftoss/chain-adapters' import type { TradeQuote, TradeQuoteStep } from '@shapeshiftoss/swapper' -import assert from 'assert' +import { fetchZrxPermit2Quote } from '@shapeshiftoss/swapper/dist/swappers/ZrxSwapper/utils/fetchFromZrx' +import { skipToken, useQuery } from '@tanstack/react-query' +import { getConfig } from 'config' +import type { TypedData } from 'eip-712' import { useCallback, useMemo } from 'react' import { useErrorHandler } from 'hooks/useErrorToast/useErrorToast' import { useWallet } from 'hooks/useWallet/useWallet' @@ -30,6 +34,38 @@ export const useSignPermit2 = ( selectHopSellAccountId(state, hopSellAccountIdFilter), ) + const sellAssetAccountAddress = useMemo( + () => (sellAssetAccountId ? fromAccountId(sellAssetAccountId).account : undefined), + [sellAssetAccountId], + ) + + // Fetch permit2 in-place + const { isFetching, data: permit2Eip712Data } = useQuery({ + queryKey: ['zrxPermit2', tradeQuoteStep], + queryFn: sellAssetAccountAddress + ? () => + fetchZrxPermit2Quote({ + buyAsset: tradeQuoteStep.buyAsset, + sellAsset: tradeQuoteStep.sellAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit: + tradeQuoteStep.sellAmountIncludingProtocolFeesCryptoBaseUnit, + sellAddress: sellAssetAccountAddress, + // irrelevant, we're only concerned about this query for the sole purpose of getting eip712 typed data + affiliateBps: '0', + // irrelevant for the same reason as above + slippageTolerancePercentageDecimal: '0.020', + zrxBaseUrl: getConfig().REACT_APP_ZRX_BASE_URL, + }) + : skipToken, + select: data => { + if (data.isErr()) throw data.unwrapErr() + + const { permit2 } = data.unwrap() + + return permit2?.eip712 as TypedData | undefined + }, + }) + const accountMetadataFilter = useMemo( () => ({ accountId: sellAssetAccountId }), [sellAssetAccountId], @@ -39,7 +75,7 @@ export const useSignPermit2 = ( ) const signPermit2 = useCallback(async () => { - if (!wallet || !accountMetadata) return + if (!wallet || !accountMetadata || !permit2Eip712Data) return dispatch( tradeQuoteSlice.actions.setPermit2SignaturePending({ @@ -49,10 +85,9 @@ export const useSignPermit2 = ( ) try { - assert(tradeQuoteStep.permit2Eip712, 'Trade quote is missing permit2 eip712 metadata') const typedDataToSign = { addressNList: toAddressNList(accountMetadata.bip44Params), - typedData: tradeQuoteStep?.permit2Eip712, + typedData: permit2Eip712Data, } const adapter = assertGetEvmChainAdapter(tradeQuoteStep.sellAsset.chainId) @@ -80,13 +115,14 @@ export const useSignPermit2 = ( confirmedTradeId, dispatch, hopIndex, + permit2Eip712Data, showErrorToast, - tradeQuoteStep.permit2Eip712, tradeQuoteStep.sellAsset.chainId, wallet, ]) return { signPermit2, + isLoading: isFetching, } } From 10bec0333b26d90c23575cd2e330fa544df1145c Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 6 Nov 2024 18:50:35 +0900 Subject: [PATCH 15/62] feat: the bloodbath continues --- .../src/swappers/CowSwapper/endpoints.ts | 41 +++---------------- .../getCowSwapTradeQuote.ts | 11 ++--- .../src/swappers/ZrxSwapper/endpoints.ts | 2 +- .../getZrxTradeQuote/getZrxTradeQuote.ts | 8 ++-- packages/swapper/src/types.ts | 5 ++- .../MultiHopTrade/MultiHopTrade.tsx | 1 - 6 files changed, 19 insertions(+), 49 deletions(-) diff --git a/packages/swapper/src/swappers/CowSwapper/endpoints.ts b/packages/swapper/src/swappers/CowSwapper/endpoints.ts index 4667aaf6704..fbff3ca17b7 100644 --- a/packages/swapper/src/swappers/CowSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/CowSwapper/endpoints.ts @@ -1,4 +1,3 @@ -import { fromAssetId } from '@shapeshiftoss/caip' import type { EvmChainId } from '@shapeshiftoss/types' import { TxStatus } from '@shapeshiftoss/unchained-client' import { bn } from '@shapeshiftoss/utils' @@ -26,7 +25,6 @@ import { getHopByIndex, isExecutableTradeQuote, } from '../../utils' -import { isNativeEvmAsset } from '../utils/helpers/helpers' import { getCowSwapTradeQuote, getCowSwapTradeRate, @@ -36,12 +34,9 @@ import { CoWSwapBuyTokenDestination, type CowSwapGetTradesResponse, type CowSwapGetTransactionsResponse, - CoWSwapOrderKind, - type CowSwapQuoteResponse, CoWSwapSellTokenSource, CoWSwapSigningScheme, } from './types' -import { COW_SWAP_NATIVE_ASSET_MARKER_ADDRESS } from './utils/constants' import { cowService } from './utils/cowService' import { deductAffiliateFeesFromAmount, @@ -49,7 +44,6 @@ import { getAffiliateAppDataFragmentByChainId, getCowswapNetwork, getFullAppData, - getNowPlusThirtyMinutesTimestamp, } from './utils/helpers/helpers' const tradeQuoteMetadata: Map = new Map() @@ -85,19 +79,16 @@ export const cowApi: SwapperApi = { }, getUnsignedEvmMessage: async ({ - from, tradeQuote, stepIndex, chainId, - config, }: GetUnsignedEvmMessageArgs): Promise => { const hop = getHopByIndex(tradeQuote, stepIndex) if (!hop) throw new Error(`No hop found for stepIndex ${stepIndex}`) - const { buyAsset, sellAsset, sellAmountIncludingProtocolFeesCryptoBaseUnit } = hop + const { sellAsset } = hop const { - receiveAddress, slippageTolerancePercentageDecimal = getDefaultSlippageDecimalPercentageForSwapper( SwapperName.CowSwap, ), @@ -105,46 +96,24 @@ export const cowApi: SwapperApi = { if (!isExecutableTradeQuote(tradeQuote)) throw new Error('Unable to execute trade') - const buyTokenAddress = !isNativeEvmAsset(buyAsset.assetId) - ? fromAssetId(buyAsset.assetId).assetReference - : COW_SWAP_NATIVE_ASSET_MARKER_ADDRESS - const maybeNetwork = getCowswapNetwork(sellAsset.chainId) if (maybeNetwork.isErr()) throw maybeNetwork.unwrapErr() - const network = maybeNetwork.unwrap() - const affiliateAppDataFragment = getAffiliateAppDataFragmentByChainId({ affiliateBps: tradeQuote.affiliateBps, chainId: sellAsset.chainId, }) - const { appData, appDataHash } = await getFullAppData( + const { appDataHash } = await getFullAppData( slippageTolerancePercentageDecimal, affiliateAppDataFragment, 'market', ) - // https://api.cow.fi/docs/#/default/post_api_v1_quote - const maybeQuoteResponse = await cowService.post( - `${config.REACT_APP_COWSWAP_BASE_URL}/${network}/api/v1/quote/`, - { - sellToken: fromAssetId(sellAsset.assetId).assetReference, - buyToken: buyTokenAddress, - receiver: receiveAddress, - validTo: getNowPlusThirtyMinutesTimestamp(), - appData, - appDataHash, - partiallyFillable: false, - from, - kind: CoWSwapOrderKind.Sell, - sellAmountBeforeFee: sellAmountIncludingProtocolFeesCryptoBaseUnit, - }, - ) - if (maybeQuoteResponse.isErr()) throw maybeQuoteResponse.unwrapErr() + const { cowswapQuoteResponse } = hop - const { data: cowSwapQuoteResponse } = maybeQuoteResponse.unwrap() + if (!cowswapQuoteResponse) throw new Error('CowSwap quote data is required') - const { id, quote } = cowSwapQuoteResponse + const { id, quote } = cowswapQuoteResponse // Note: While CowSwap returns us a quote, and we have slippageBips in the appData, this isn't enough. // For the slippage actually to be enforced, the final message to be signed needs to have slippage deducted. // Failure to do so means orders may take forever to be filled, or never be filled at all. diff --git a/packages/swapper/src/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.ts b/packages/swapper/src/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.ts index c7d516f4c30..e0859869e27 100644 --- a/packages/swapper/src/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.ts +++ b/packages/swapper/src/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.ts @@ -113,20 +113,20 @@ async function _getCowSwapTradeQuote( return Err(maybeQuoteResponse.unwrapErr()) } - const { data } = maybeQuoteResponse.unwrap() + const { data: cowswapQuoteResponse } = maybeQuoteResponse.unwrap() - const { feeAmount: feeAmountInSellTokenCryptoBaseUnit } = data.quote + const { feeAmount: feeAmountInSellTokenCryptoBaseUnit } = cowswapQuoteResponse.quote const { rate, buyAmountAfterFeesCryptoBaseUnit, buyAmountBeforeFeesCryptoBaseUnit } = getValuesFromQuoteResponse({ buyAsset, sellAsset, - response: data, + response: cowswapQuoteResponse, affiliateBps, }) const quote: TradeQuote = { - id: data.id.toString(), + id: cowswapQuoteResponse.id.toString(), receiveAddress, affiliateBps, potentialAffiliateBps, @@ -155,6 +155,7 @@ async function _getCowSwapTradeQuote( buyAsset, sellAsset, accountNumber, + cowswapQuoteResponse, }, ], } @@ -257,7 +258,7 @@ async function _getCowSwapTradeRate( const quote: TradeRate = { id: data.id.toString(), accountNumber, - receiveAddress, + receiveAddress: undefined, affiliateBps, potentialAffiliateBps, rate, diff --git a/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts b/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts index c0c8c1fda22..3b160c5ce26 100644 --- a/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts @@ -70,7 +70,7 @@ export const zrxApi: SwapperApi = { if (!isExecutableTradeQuote(tradeQuote)) throw new Error('Unable to execute trade') const { steps } = tradeQuote - const { transactionMetadata } = steps[0] + const { zrxTransactionMetadata: transactionMetadata } = steps[0] if (!transactionMetadata) throw new Error('Transaction metadata is required') diff --git a/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts b/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts index cbcb95b4dbe..ba29d8dee3e 100644 --- a/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts +++ b/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts @@ -136,7 +136,7 @@ async function _getZrxTradeQuote( if (maybeZrxQuoteResponse.isErr()) return Err(maybeZrxQuoteResponse.unwrapErr()) const zrxQuoteResponse = maybeZrxQuoteResponse.unwrap() - const transactionMetadata: TradeQuoteStep['transactionMetadata'] = { + const transactionMetadata: TradeQuoteStep['zrxTransactionMetadata'] = { to: zrxQuoteResponse.to, data: zrxQuoteResponse.data as `0x${string}`, gasPrice: zrxQuoteResponse.gasPrice ? zrxQuoteResponse.gasPrice : undefined, @@ -175,7 +175,7 @@ async function _getZrxTradeQuote( rate, steps: [ { - transactionMetadata, + zrxTransactionMetadata: transactionMetadata, estimatedExecutionTimeMs: undefined, allowanceContract: allowanceTarget, buyAsset, @@ -434,7 +434,7 @@ async function _getZrxPermit2TradeQuote( ) } - const transactionMetadata: TradeQuoteStep['transactionMetadata'] = { + const transactionMetadata: TradeQuoteStep['zrxTransactionMetadata'] = { to: transaction.to, data: transaction.data as `0x${string}`, gasPrice: transaction.gasPrice ? transaction.gasPrice : undefined, @@ -521,7 +521,7 @@ async function _getZrxPermit2TradeQuote( sellAmountIncludingProtocolFeesCryptoBaseUnit, source: SwapperName.Zrx, permit2Eip712: permit2Eip712 as unknown as TypedData | undefined, - transactionMetadata, + zrxTransactionMetadata: transactionMetadata, }, ] as SingleHopTradeQuoteSteps, }) diff --git a/packages/swapper/src/types.ts b/packages/swapper/src/types.ts index cfa6e9f11fe..8b46014bb67 100644 --- a/packages/swapper/src/types.ts +++ b/packages/swapper/src/types.ts @@ -24,7 +24,7 @@ import type { TypedData } from 'eip-712' import type { InterpolationOptions } from 'node-polyglot' import type { Address } from 'viem' -import type { CowMessageToSign } from './swappers/CowSwapper/types' +import type { CowMessageToSign, CowSwapQuoteResponse } from './swappers/CowSwapper/types' import type { makeSwapperAxiosServiceMonadic } from './utils' // TODO: Rename all properties in this type to be camel case and not react specific @@ -243,13 +243,14 @@ export type TradeQuoteStep = { allowanceContract: string estimatedExecutionTimeMs: number | undefined permit2Eip712?: TypedData - transactionMetadata?: { + zrxTransactionMetadata?: { to: Address data: Address gasPrice: string | undefined gas: string | undefined value: string } + cowswapQuoteResponse?: CowSwapQuoteResponse } export type TradeRateStep = Omit & { accountNumber: undefined } diff --git a/src/components/MultiHopTrade/MultiHopTrade.tsx b/src/components/MultiHopTrade/MultiHopTrade.tsx index e28c6baca0a..3869182f5ba 100644 --- a/src/components/MultiHopTrade/MultiHopTrade.tsx +++ b/src/components/MultiHopTrade/MultiHopTrade.tsx @@ -16,7 +16,6 @@ import { QuoteListRoute } from './components/QuoteList/QuoteListRoute' import { Claim } from './components/TradeInput/components/Claim/Claim' import { TradeInput } from './components/TradeInput/TradeInput' import { VerifyAddresses } from './components/VerifyAddresses/VerifyAddresses' -import { useGetTradeQuotes } from './hooks/useGetTradeQuotes/useGetTradeQuotes' import { useGetTradeRates } from './hooks/useGetTradeQuotes/useGetTradeRates' import { TradeInputTab, TradeRoutePaths } from './types' From 318c1a9fe6d86cb33ef66bcd458675c25d3d5e7d Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 6 Nov 2024 19:09:26 +0900 Subject: [PATCH 16/62] fix: lifi receiveAddress should be undefined on rate --- .../src/swappers/LifiSwapper/getTradeQuote/getTradeQuote.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/swapper/src/swappers/LifiSwapper/getTradeQuote/getTradeQuote.ts b/packages/swapper/src/swappers/LifiSwapper/getTradeQuote/getTradeQuote.ts index 4e5f175a730..bff178fe8ed 100644 --- a/packages/swapper/src/swappers/LifiSwapper/getTradeQuote/getTradeQuote.ts +++ b/packages/swapper/src/swappers/LifiSwapper/getTradeQuote/getTradeQuote.ts @@ -53,6 +53,7 @@ async function getTrade( supportsEIP1559, affiliateBps, potentialAffiliateBps, + quoteOrRate, } = input const slippageTolerancePercentageDecimal = @@ -246,7 +247,7 @@ async function getTrade( return { id: selectedLifiRoute.id, - receiveAddress, + receiveAddress: quoteOrRate === 'quote' ? receiveAddress : undefined, affiliateBps, potentialAffiliateBps, steps, From 9297644afed39abbf93b6001bc0c7dd5482340c6 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Thu, 7 Nov 2024 08:52:34 +0900 Subject: [PATCH 17/62] feat: portals trade rates --- .../getPortalsTradeQuote.ts | 28 ++++++++++++------- .../useGetTradeQuotes/useGetTradeQuotes.tsx | 1 + 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts b/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts index f5db8c4f9c6..d870afa6ea3 100644 --- a/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts +++ b/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts @@ -38,7 +38,6 @@ export async function getPortalsTradeRate( const { sellAsset, buyAsset, - sendAddress, accountNumber, affiliateBps, potentialAffiliateBps, @@ -106,17 +105,25 @@ export async function getPortalsTradeRate( ? Number(input.slippageTolerancePercentageDecimal) : undefined // Use auto slippage if no user preference is provided - // Use the quote estimate endpoint to get a quote without a wallet - const quoteEstimateResponse = await fetchPortalsTradeEstimate({ - sender: sendAddress, + // TODO(gomes): we should use the fetchPortalsTradeEstimate method here (leveraing /v2/portal/estimate) but we can't do it yet + // because of upstream not returning us the allowance target + const quoteEstimateResponse = await fetchPortalsTradeOrder({ + // Portals needs a (any) valid address for the `/v2/portal` endpoint. Once we switch to the `/v2/portal/estimate` endpoint here, we can remove this madness + sender: zeroAddress, inputToken, outputToken, inputAmount: sellAmountIncludingProtocolFeesCryptoBaseUnit, slippageTolerancePercentage: userSlippageTolerancePercentageDecimalOrDefault ? userSlippageTolerancePercentageDecimalOrDefault * 100 - : undefined, + : bnOrZero(getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Portals)) + .times(100) + .toNumber(), + partner: getTreasuryAddressFromChainId(sellAsset.chainId), + // Effectively emulating the estimate endpoint here by skipping validation + validate: false, swapperConfig, }) + // Use the quote estimate endpoint to get a quote without a wallet const rate = getRate({ sellAmountCryptoBaseUnit: input.sellAmountIncludingProtocolFeesCryptoBaseUnit, @@ -128,23 +135,24 @@ export async function getPortalsTradeRate( const tradeRate = { id: uuid(), accountNumber, - receiveAddress: input.receiveAddress, + receiveAddress: undefined, affiliateBps, potentialAffiliateBps, rate, - slippageTolerancePercentageDecimal: quoteEstimateResponse?.context.slippageTolerancePercentage + slippageTolerancePercentageDecimal: quoteEstimateResponse.context.slippageTolerancePercentage ? bn(quoteEstimateResponse.context.slippageTolerancePercentage).div(100).toString() : undefined, steps: [ { estimatedExecutionTimeMs: undefined, // Portals doesn't provide this info - allowanceContract: undefined, + // TODO(gomes): we will most likely need this for allowance checks. How do? + allowanceContract: quoteEstimateResponse.context.target, accountNumber, rate, buyAsset, sellAsset, - buyAmountBeforeFeesCryptoBaseUnit: quoteEstimateResponse.minOutputAmount, - buyAmountAfterFeesCryptoBaseUnit: quoteEstimateResponse.outputAmount, + buyAmountBeforeFeesCryptoBaseUnit: quoteEstimateResponse.context.minOutputAmount, + buyAmountAfterFeesCryptoBaseUnit: quoteEstimateResponse.context.outputAmount, sellAmountIncludingProtocolFeesCryptoBaseUnit: input.sellAmountIncludingProtocolFeesCryptoBaseUnit, feeData: { diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index c6a9a1947b0..85b08c3f6c4 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -368,6 +368,7 @@ export const useGetTradeQuotes = () => { }), ) // Set as both confirmed *and* active + dispatch(tradeQuoteSlice.actions.setActiveQuote(quoteData)) dispatch(tradeQuoteSlice.actions.setConfirmedQuote(quoteData.quote)) // And re-confirm the trade since we're effectively resetting the state machine here // dispatch(tradeQuoteSlice.actions.confirmTrade(quoteData.quote.id)) From acb603fc7fd2008bddefd8027e088c1de93fab32 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Thu, 7 Nov 2024 08:54:30 +0900 Subject: [PATCH 18/62] feat: the bloodbath continues --- .../getPortalsTradeQuote.ts | 77 +----------------- .../swappers/PortalsSwapper/utils/helpers.ts | 78 +------------------ 2 files changed, 3 insertions(+), 152 deletions(-) diff --git a/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts b/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts index d870afa6ea3..f53d0098c64 100644 --- a/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts +++ b/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts @@ -27,8 +27,8 @@ import { import { getRate, makeSwapErrorRight } from '../../../utils' import { getTreasuryAddressFromChainId, isNativeEvmAsset } from '../../utils/helpers/helpers' import { chainIdToPortalsNetwork } from '../constants' -import { fetchPortalsTradeEstimate, fetchPortalsTradeOrder } from '../utils/fetchPortalsTradeOrder' -import { getDummyQuoteParams, isSupportedChainId } from '../utils/helpers' +import { fetchPortalsTradeOrder } from '../utils/fetchPortalsTradeOrder' +import { isSupportedChainId } from '../utils/helpers' export async function getPortalsTradeRate( input: GetEvmTradeRateInput, @@ -261,7 +261,6 @@ export async function getPortalsTradeQuote( const inputToken = `${portalsNetwork}:${sellAssetAddress}` const outputToken = `${portalsNetwork}:${buyAssetAddress}` - // Attempt fetching a quote with validation enabled to leverage upstream gasLimit estimate const portalsTradeOrderResponse = await fetchPortalsTradeOrder({ sender: sendAddress, inputToken, @@ -274,78 +273,6 @@ export async function getPortalsTradeQuote( feePercentage: affiliateBpsPercentage, validate: true, swapperConfig, - }).catch(async e => { - // If validation fails, fire 3 more quotes: - // 1. a quote estimate (does not require approval) to get the optimal slippage tolerance - // 2. a quote with validation enabled, but using a well-funded address to get a rough gasLimit estimate - // 3. another quote with validation disabled, to get an actual quote (using the user slippage, or the optimal from the estimate) - console.info('failed to get Portals quote with validation enabled', e) - - // Use the quote estimate endpoint to get the optimal slippage tolerance - const quoteEstimateResponse = await fetchPortalsTradeEstimate({ - sender: sendAddress, - inputToken, - outputToken, - inputAmount: sellAmountIncludingProtocolFeesCryptoBaseUnit, - swapperConfig, - }).catch(e => { - console.info('failed to get Portals quote estimate', e) - return undefined - }) - - const dummyQuoteParams = getDummyQuoteParams(sellAsset.chainId) - - const dummySellAssetAddress = fromAssetId(dummyQuoteParams.sellAssetId).assetReference - const dummyBuyAssetAddress = fromAssetId(dummyQuoteParams.buyAssetId).assetReference - - const dummyInputToken = `${portalsNetwork}:${dummySellAssetAddress}` - const dummyOutputToken = `${portalsNetwork}:${dummyBuyAssetAddress}` - - const userSlippageTolerancePercentageOrDefault = - userSlippageTolerancePercentageDecimalOrDefault - ? userSlippageTolerancePercentageDecimalOrDefault * 100 - : undefined - - // Use a dummy request to the portal endpoint to get a rough gasLimit estimate - const dummyOrderResponse = await fetchPortalsTradeOrder({ - sender: dummyQuoteParams.accountAddress, - inputToken: dummyInputToken, - outputToken: dummyOutputToken, - inputAmount: dummyQuoteParams.sellAmountCryptoBaseUnit, - slippageTolerancePercentage: userSlippageTolerancePercentageOrDefault, - partner: getTreasuryAddressFromChainId(sellAsset.chainId), - feePercentage: affiliateBpsPercentage, - validate: true, - swapperConfig, - }) - .then(({ context }) => ({ - maybeGasLimit: context.gasLimit, - })) - .catch(e => { - console.info('failed to get Portals quote with validation enabled using dummy address', e) - return undefined - }) - - const order = await fetchPortalsTradeOrder({ - sender: sendAddress, - inputToken, - outputToken, - inputAmount: sellAmountIncludingProtocolFeesCryptoBaseUnit, - slippageTolerancePercentage: - userSlippageTolerancePercentageOrDefault ?? - quoteEstimateResponse?.context.slippageTolerancePercentage ?? - bnOrZero(getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Portals)) - .times(100) - .toNumber(), - partner: getTreasuryAddressFromChainId(sellAsset.chainId), - feePercentage: affiliateBpsPercentage, - validate: false, - swapperConfig, - }) - - if (dummyOrderResponse?.maybeGasLimit) - order.context.gasLimit = dummyOrderResponse.maybeGasLimit - return order }) const { diff --git a/packages/swapper/src/swappers/PortalsSwapper/utils/helpers.ts b/packages/swapper/src/swappers/PortalsSwapper/utils/helpers.ts index c7879d2a315..e4eea5099f2 100644 --- a/packages/swapper/src/swappers/PortalsSwapper/utils/helpers.ts +++ b/packages/swapper/src/swappers/PortalsSwapper/utils/helpers.ts @@ -1,84 +1,8 @@ -import { type AssetId, type ChainId, foxAssetId, toAccountId } from '@shapeshiftoss/caip' -import type { EvmChainId } from '@shapeshiftoss/types' -import { KnownChainIds } from '@shapeshiftoss/types' +import { type ChainId } from '@shapeshiftoss/caip' import type { PortalsSupportedChainId } from '../types' import { PortalsSupportedChainIds } from '../types' -const WELL_FUNDED_ADDRESS = '0x267586F48043e159624c4FE24300c8ad2f352fc7' - export const isSupportedChainId = (chainId: ChainId): chainId is PortalsSupportedChainId => { return PortalsSupportedChainIds.includes(chainId as PortalsSupportedChainId) } - -export const getDummyQuoteParams = (chainId: ChainId) => { - // Assume a token sell/buy - inherently slightly more expensive than having a native asset either on the buy or sell side, which works in our favor as a buffer - const DUMMY_QUOTE_PARAMS_BY_CHAIN_ID: Record< - Exclude, - { - sellAssetId: AssetId - sellAmountCryptoBaseUnit: string - buyAssetId: AssetId - } - > = { - [KnownChainIds.EthereumMainnet]: { - sellAssetId: foxAssetId, - sellAmountCryptoBaseUnit: '100000000000000000000', // 100 FOX - buyAssetId: 'eip155:1/erc20:0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // WETH - }, - [KnownChainIds.AvalancheMainnet]: { - sellAssetId: 'eip155:43114/erc20:0x49d5c2bdffac6ce2bfdb6640f4f80f226bc10bab', // WETH - sellAmountCryptoBaseUnit: '1000000000000000', // 0.001 WETH - buyAssetId: 'eip155:43114/erc20:0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7', // USDT - }, - [KnownChainIds.ArbitrumMainnet]: { - sellAssetId: 'eip155:42161/erc20:0xaf88d065e77c8cc2239327c5edb3a432268e5831', // USDC - sellAmountCryptoBaseUnit: '3000000', // 3 USDC - buyAssetId: 'eip155:42161/erc20:0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9', // USDT - }, - [KnownChainIds.PolygonMainnet]: { - sellAssetId: 'eip155:137/erc20:0x3c499c542cef5e3811e1192ce70d8cc03d5c3359', // 5 USDC - sellAmountCryptoBaseUnit: '5000000', // 5 USDC - buyAssetId: 'eip155:137/erc20:0xc2132d05d31c914a87c6611c10748aeb04b58e8f', // USDT - }, - [KnownChainIds.OptimismMainnet]: { - sellAssetId: 'eip155:10/erc20:0x0b2c639c533813f4aa9d7837caf62653d097ff85', // USDC - sellAmountCryptoBaseUnit: '1000000', // 1 USDC - buyAssetId: 'eip155:10/erc20:0x4200000000000000000000000000000000000006', // WETH - }, - [KnownChainIds.BnbSmartChainMainnet]: { - sellAssetId: 'eip155:56/bep20:0xc5f0f7b66764f6ec8c8dff7ba683102295e16409', // FDUSD - sellAmountCryptoBaseUnit: '1000000000000000000', // 1 FDUSD - buyAssetId: 'eip155:56/bep20:0x2170ed0880ac9a755fd29b2688956bd959f933f8', // WETH - }, - [KnownChainIds.BaseMainnet]: { - sellAssetId: 'eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', // USDC - sellAmountCryptoBaseUnit: '2000000', // 2 USDC - buyAssetId: 'eip155:8453/erc20:0x4200000000000000000000000000000000000006', // WETH - }, - [KnownChainIds.GnosisMainnet]: { - sellAssetId: 'eip155:250/erc20:0x6b175474e89094c44da98b954eedeac495271d0f', // WETH - sellAmountCryptoBaseUnit: '1000000000000000', // 0.001 WETH - buyAssetId: 'eip155:100/erc20:0x8e5bbbb09ed1ebde8674cda39a0c169401db4252', // WBTC - }, - } - const params = - DUMMY_QUOTE_PARAMS_BY_CHAIN_ID[ - chainId as Exclude - ] - const dummySellAssetId = params.sellAssetId - const dummyBuyAssetId = params.buyAssetId - const dummyAmountCryptoBaseUnit = params.sellAmountCryptoBaseUnit - const dummyAccountId = toAccountId({ - chainId, - account: WELL_FUNDED_ADDRESS, // well-enough funded addy with approvals granted for the assets above - }) - - return { - accountId: dummyAccountId, - accountAddress: WELL_FUNDED_ADDRESS, - sellAssetId: dummySellAssetId, - buyAssetId: dummyBuyAssetId, - sellAmountCryptoBaseUnit: dummyAmountCryptoBaseUnit, - } -} From 9d5598c0aeb73f9e2dfa178b6648026d03e53f0e Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Thu, 7 Nov 2024 12:05:47 +0900 Subject: [PATCH 19/62] fix: thor getRate undefined receiveAddress --- .../src/swappers/ThorchainSwapper/utils/getL1quote.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/swapper/src/swappers/ThorchainSwapper/utils/getL1quote.ts b/packages/swapper/src/swappers/ThorchainSwapper/utils/getL1quote.ts index 9a4185c55b1..b7570b24518 100644 --- a/packages/swapper/src/swappers/ThorchainSwapper/utils/getL1quote.ts +++ b/packages/swapper/src/swappers/ThorchainSwapper/utils/getL1quote.ts @@ -737,7 +737,7 @@ export const getL1Rate = async ( id: uuid(), accountNumber: undefined, memo, - receiveAddress, + receiveAddress: undefined, affiliateBps, potentialAffiliateBps, isStreaming, @@ -825,7 +825,7 @@ export const getL1Rate = async ( id: uuid(), accountNumber: undefined, memo, - receiveAddress, + receiveAddress: undefined, affiliateBps, potentialAffiliateBps, isStreaming, @@ -905,7 +905,7 @@ export const getL1Rate = async ( id: uuid(), accountNumber: undefined, memo, - receiveAddress, + receiveAddress: undefined, affiliateBps, potentialAffiliateBps, isStreaming, From 47420ca3f06c6b42f8461709657eb3b227c835db Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Thu, 7 Nov 2024 12:20:46 +0900 Subject: [PATCH 20/62] feat: another one bites the dust --- .../swapper/src/swappers/ThorchainSwapper/utils/getL1quote.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/swapper/src/swappers/ThorchainSwapper/utils/getL1quote.ts b/packages/swapper/src/swappers/ThorchainSwapper/utils/getL1quote.ts index b7570b24518..d61e3a4d0c1 100644 --- a/packages/swapper/src/swappers/ThorchainSwapper/utils/getL1quote.ts +++ b/packages/swapper/src/swappers/ThorchainSwapper/utils/getL1quote.ts @@ -583,7 +583,7 @@ export const getL1Rate = async ( sellAsset, buyAssetId: buyAsset.assetId, sellAmountCryptoBaseUnit, - receiveAddress, + receiveAddress: undefined, streaming: true, affiliateBps: requestedAffiliateBps, streamingInterval, From 2b5cdcd6e2d1eb43ab7b2768537961765a8be76a Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Thu, 7 Nov 2024 17:02:08 +0900 Subject: [PATCH 21/62] feat: revert portals to estimate endpoint --- .../getPortalsTradeQuote.ts | 22 +++++------- .../utils/fetchPortalsTradeOrder.ts | 4 +-- .../swappers/PortalsSwapper/utils/helpers.ts | 35 ++++++++++++++++++- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts b/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts index f53d0098c64..e11ff417db9 100644 --- a/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts +++ b/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts @@ -27,8 +27,8 @@ import { import { getRate, makeSwapErrorRight } from '../../../utils' import { getTreasuryAddressFromChainId, isNativeEvmAsset } from '../../utils/helpers/helpers' import { chainIdToPortalsNetwork } from '../constants' -import { fetchPortalsTradeOrder } from '../utils/fetchPortalsTradeOrder' -import { isSupportedChainId } from '../utils/helpers' +import { fetchPortalsTradeEstimate, fetchPortalsTradeOrder } from '../utils/fetchPortalsTradeOrder' +import { getPortalsRouterAddressByChainId, isSupportedChainId } from '../utils/helpers' export async function getPortalsTradeRate( input: GetEvmTradeRateInput, @@ -79,6 +79,8 @@ export async function getPortalsTradeRate( } try { + if (!isSupportedChainId(chainId)) throw new Error(`Unsupported chainId ${sellAsset.chainId}`) + const portalsNetwork = chainIdToPortalsNetwork[chainId as KnownChainIds] if (!portalsNetwork) { @@ -105,11 +107,7 @@ export async function getPortalsTradeRate( ? Number(input.slippageTolerancePercentageDecimal) : undefined // Use auto slippage if no user preference is provided - // TODO(gomes): we should use the fetchPortalsTradeEstimate method here (leveraing /v2/portal/estimate) but we can't do it yet - // because of upstream not returning us the allowance target - const quoteEstimateResponse = await fetchPortalsTradeOrder({ - // Portals needs a (any) valid address for the `/v2/portal` endpoint. Once we switch to the `/v2/portal/estimate` endpoint here, we can remove this madness - sender: zeroAddress, + const quoteEstimateResponse = await fetchPortalsTradeEstimate({ inputToken, outputToken, inputAmount: sellAmountIncludingProtocolFeesCryptoBaseUnit, @@ -118,9 +116,6 @@ export async function getPortalsTradeRate( : bnOrZero(getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Portals)) .times(100) .toNumber(), - partner: getTreasuryAddressFromChainId(sellAsset.chainId), - // Effectively emulating the estimate endpoint here by skipping validation - validate: false, swapperConfig, }) // Use the quote estimate endpoint to get a quote without a wallet @@ -132,6 +127,8 @@ export async function getPortalsTradeRate( buyAsset, }) + const allowanceContract = getPortalsRouterAddressByChainId(chainId) + const tradeRate = { id: uuid(), accountNumber, @@ -145,13 +142,12 @@ export async function getPortalsTradeRate( steps: [ { estimatedExecutionTimeMs: undefined, // Portals doesn't provide this info - // TODO(gomes): we will most likely need this for allowance checks. How do? - allowanceContract: quoteEstimateResponse.context.target, + allowanceContract, accountNumber, rate, buyAsset, sellAsset, - buyAmountBeforeFeesCryptoBaseUnit: quoteEstimateResponse.context.minOutputAmount, + buyAmountBeforeFeesCryptoBaseUnit: quoteEstimateResponse.minOutputAmount, buyAmountAfterFeesCryptoBaseUnit: quoteEstimateResponse.context.outputAmount, sellAmountIncludingProtocolFeesCryptoBaseUnit: input.sellAmountIncludingProtocolFeesCryptoBaseUnit, diff --git a/packages/swapper/src/swappers/PortalsSwapper/utils/fetchPortalsTradeOrder.ts b/packages/swapper/src/swappers/PortalsSwapper/utils/fetchPortalsTradeOrder.ts index 28d57ea710c..a7f2172828d 100644 --- a/packages/swapper/src/swappers/PortalsSwapper/utils/fetchPortalsTradeOrder.ts +++ b/packages/swapper/src/swappers/PortalsSwapper/utils/fetchPortalsTradeOrder.ts @@ -20,7 +20,7 @@ type PortalsTradeOrderParams = { type PortalsTradeOrderEstimateParams = Omit< PortalsTradeOrderParams, 'partner' | 'validate' | 'sender' -> & { sender: string | undefined } +> type PortalsTradeOrderResponse = { context: { @@ -116,7 +116,6 @@ export const fetchPortalsTradeOrder = async ({ } export const fetchPortalsTradeEstimate = async ({ - sender, inputToken, inputAmount, outputToken, @@ -126,7 +125,6 @@ export const fetchPortalsTradeEstimate = async ({ const url = `${swapperConfig.REACT_APP_PORTALS_BASE_URL}/v2/portal/estimate` const params = new URLSearchParams({ - ...(sender ? { sender } : {}), inputToken, inputAmount, outputToken, diff --git a/packages/swapper/src/swappers/PortalsSwapper/utils/helpers.ts b/packages/swapper/src/swappers/PortalsSwapper/utils/helpers.ts index e4eea5099f2..b2d6c61149d 100644 --- a/packages/swapper/src/swappers/PortalsSwapper/utils/helpers.ts +++ b/packages/swapper/src/swappers/PortalsSwapper/utils/helpers.ts @@ -1,4 +1,14 @@ -import { type ChainId } from '@shapeshiftoss/caip' +import { + arbitrumChainId, + avalancheChainId, + baseChainId, + binanceChainId, + type ChainId, + ethChainId, + gnosisChainId, + optimismChainId, + polygonChainId, +} from '@shapeshiftoss/caip' import type { PortalsSupportedChainId } from '../types' import { PortalsSupportedChainIds } from '../types' @@ -6,3 +16,26 @@ import { PortalsSupportedChainIds } from '../types' export const isSupportedChainId = (chainId: ChainId): chainId is PortalsSupportedChainId => { return PortalsSupportedChainIds.includes(chainId as PortalsSupportedChainId) } + +export const getPortalsRouterAddressByChainId = (chainId: PortalsSupportedChainId): string => { + switch (chainId) { + case polygonChainId: + return '0xC74063fdb47fe6dCE6d029A489BAb37b167Da57f' + case ethChainId: + return '0xbf5a7f3629fb325e2a8453d595ab103465f75e62' + case avalancheChainId: + return '0xbf5A7F3629fB325E2a8453D595AB103465F75E62' + case binanceChainId: + return '0x34b6a821d2f26c6b7cdb01cd91895170c6574a0d' + case optimismChainId: + return '0x43838f0c0d499f5c3101589f0f452b1fc7515178' + case arbitrumChainId: + return '0x34b6a821d2f26c6b7cdb01cd91895170c6574a0d' + case baseChainId: + return '0xb0324286b3ef7dddc93fb2ff7c8b7b8a3524803c' + case gnosisChainId: + return '0x8e74454b2cf2f6cc2a06083ef122187551cf391c' + default: + throw new Error(`Router address not found for chainId: ${chainId}`) + } +} From 662c7d552949fa69e6810c95578b9c7a98e88b0f Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Thu, 7 Nov 2024 17:29:41 +0900 Subject: [PATCH 22/62] feat: ihavenoideawhatimdoingdog.jpg --- .../components/HopTransactionStep.tsx | 21 +++++++++++++------ .../useGetTradeQuotes/useGetTradeQuotes.tsx | 2 ++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx index 9edb8dc0beb..0165b96ee32 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx @@ -153,14 +153,21 @@ export const HopTransactionStep = ({ return }, [swapTxState, swapperName]) - useGetTradeQuotes() + const { isFetching, data } = useGetTradeQuotes() const content = useMemo(() => { if (isActive && swapTxState === TransactionExecutionState.AwaitingConfirmation) { return ( - @@ -183,14 +190,16 @@ export const HopTransactionStep = ({ ) } }, [ - handleSignTx, - hopIndex, isActive, - sellTxHash, swapTxState, - activeTradeId, tradeQuoteStep.source, + sellTxHash, + handleSignTx, + isFetching, + data, translate, + hopIndex, + activeTradeId, ]) const description = useMemo(() => { diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index 85b08c3f6c4..523e1b97718 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -382,4 +382,6 @@ export const useGetTradeQuotes = () => { mixpanel.track(MixPanelEvent.QuotesReceived, quoteData) } }, [sortedTradeQuotes, mixpanel, isAnyTradeQuoteLoading]) + + return queryStateMeta } From 5fa3d0c4f2c434b4a11645b8181420c4c3bd02a0 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Thu, 7 Nov 2024 17:43:59 +0900 Subject: [PATCH 23/62] fix: receiveAddress should be undefined for arb trade rate --- .../ArbitrumBridgeSwapper/getTradeQuote/getTradeQuote.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/swapper/src/swappers/ArbitrumBridgeSwapper/getTradeQuote/getTradeQuote.ts b/packages/swapper/src/swappers/ArbitrumBridgeSwapper/getTradeQuote/getTradeQuote.ts index a2ef0b83b8c..608a1e24ae0 100644 --- a/packages/swapper/src/swappers/ArbitrumBridgeSwapper/getTradeQuote/getTradeQuote.ts +++ b/packages/swapper/src/swappers/ArbitrumBridgeSwapper/getTradeQuote/getTradeQuote.ts @@ -184,7 +184,7 @@ export async function getTradeRate( return Ok({ id: uuid(), accountNumber: undefined, - receiveAddress, + receiveAddress: undefined, affiliateBps: '0', potentialAffiliateBps: '0', rate, From 7faf6380c0d879dcd41db239e7289e887581254a Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Fri, 8 Nov 2024 11:14:00 +0900 Subject: [PATCH 24/62] feat: move getTradeQuoteInput to react-query --- .../useGetTradeQuotes/useGetTradeQuotes.tsx | 97 ++++++++++--------- 1 file changed, 49 insertions(+), 48 deletions(-) diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index 523e1b97718..fa660f0ffe7 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -1,4 +1,4 @@ -import { skipToken } from '@reduxjs/toolkit/dist/query' +import { skipToken as reduxSkipToken } from '@reduxjs/toolkit/query' import { fromAccountId } from '@shapeshiftoss/caip' import { isLedger } from '@shapeshiftoss/hdwallet-ledger' import type { GetTradeQuoteInput, SwapperName, TradeQuote } from '@shapeshiftoss/swapper' @@ -8,7 +8,8 @@ import { swappers, } from '@shapeshiftoss/swapper' import { isThorTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/getThorTradeQuoteOrRate/getTradeQuoteOrRate' -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { skipToken as reactQuerySkipToken, useQuery } from '@tanstack/react-query' +import { useCallback, useEffect, useMemo, useRef } from 'react' import { getTradeQuoteInput } from 'components/MultiHopTrade/hooks/useGetTradeQuotes/getTradeQuoteInput' import { useReceiveAddress } from 'components/MultiHopTrade/hooks/useReceiveAddress' import { useHasFocus } from 'hooks/useHasFocus' @@ -20,7 +21,7 @@ import type { ParameterModel } from 'lib/fees/parameters/types' import { getMixPanel } from 'lib/mixpanel/mixPanelSingleton' import { MixPanelEvent } from 'lib/mixpanel/types' import { isSome } from 'lib/utils' -import { selectIsSnapshotApiQueriesPending, selectVotingPower } from 'state/apis/snapshot/selectors' +import { selectVotingPower } from 'state/apis/snapshot/selectors' import { swapperApi } from 'state/apis/swapper/swapperApi' import type { ApiQuote, TradeQuoteError } from 'state/apis/swapper/types' import { @@ -160,9 +161,6 @@ export const useGetTradeQuotes = () => { : undefined, ) - const [tradeQuoteInput, setTradeQuoteInput] = useState( - skipToken, - ) const hasFocus = useHasFocus() const sellAsset = useAppSelector(selectInputSellAsset) const buyAsset = useAppSelector(selectInputBuyAsset) @@ -206,13 +204,8 @@ export const useGetTradeQuotes = () => { const sellAssetUsdRate = useAppSelector(state => selectUsdRateByAssetId(state, sellAsset.assetId)) - const isSnapshotApiQueriesPending = useAppSelector(selectIsSnapshotApiQueriesPending) const votingPower = useAppSelector(state => selectVotingPower(state, votingPowerParams)) const thorVotingPower = useAppSelector(state => selectVotingPower(state, thorVotingPowerParams)) - const isVotingPowerLoading = useMemo( - () => isSnapshotApiQueriesPending && votingPower === undefined, - [isSnapshotApiQueriesPending, votingPower], - ) const walletSupportsBuyAssetChain = useWalletSupportsChain(buyAsset.chainId, wallet) const isBuyAssetChainSupported = walletSupportsBuyAssetChain @@ -226,45 +219,30 @@ export const useGetTradeQuotes = () => { !isExecutableTradeQuote(activeTrade) && // and if we're actually at pre-execution time hopExecutionMetadata?.state === HopExecutionState.AwaitingSwap && - wallet && sellAccountId && sellAccountMetadata && - receiveAddress && - !isVotingPowerLoading, + receiveAddress, ), + // eslint-disable-next-line react-hooks/exhaustive-deps [ hasFocus, activeTrade, hopExecutionMetadata?.state, - wallet, sellAccountId, sellAccountMetadata, receiveAddress, - isVotingPowerLoading, ], ) - useEffect(() => { - // Only run this effect when we're actually ready - if (hopExecutionMetadata?.state !== HopExecutionState.AwaitingSwap) return + const queryFnOrSkip = useMemo(() => { + // Only run this query when we're actually ready + if (hopExecutionMetadata?.state !== HopExecutionState.AwaitingSwap) return reactQuerySkipToken // And only run it once - if (activeTrade && isExecutableTradeQuote(activeTrade)) return + if (activeTrade && isExecutableTradeQuote(activeTrade)) return reactQuerySkipToken + + return async () => { + dispatch(swapperApi.util.invalidateTags(['TradeQuote'])) - dispatch(swapperApi.util.invalidateTags(['TradeQuote'])) - - // Early exit on any invalid state - if ( - bnOrZero(sellAmountCryptoPrecision).isZero() || - !sellAccountId || - !sellAccountMetadata || - !receiveAddress || - isVotingPowerLoading - ) { - setTradeQuoteInput(skipToken) - dispatch(tradeQuoteSlice.actions.setIsTradeQuoteRequestAborted(true)) - return - } - ;(async () => { const sellAccountNumber = sellAccountMetadata?.bip44Params?.accountNumber const receiveAssetBip44Params = receiveAccountMetadata?.bip44Params const receiveAccountNumber = receiveAssetBip44Params?.accountNumber @@ -305,33 +283,56 @@ export const useGetTradeQuotes = () => { : undefined, }) - setTradeQuoteInput(updatedTradeQuoteInput) - })() + return updatedTradeQuoteInput + } }, [ + activeTrade, buyAsset, dispatch, + hopExecutionMetadata?.state, + receiveAccountMetadata?.bip44Params, receiveAddress, - sellAccountMetadata, + sellAccountId, + sellAccountMetadata?.accountType, + sellAccountMetadata?.bip44Params?.accountNumber, sellAmountCryptoPrecision, sellAsset, - votingPower, + sellAssetUsdRate, thorVotingPower, - wallet, - receiveAccountMetadata?.bip44Params, userSlippageTolerancePercentageDecimal, - sellAssetUsdRate, - sellAccountId, - isVotingPowerLoading, - isBuyAssetChainSupported, - hopExecutionMetadata?.state, - activeTrade, + votingPower, + wallet, ]) + const { data: tradeQuoteInput } = useQuery({ + queryKey: [ + 'getTradeQuoteInput', + { + buyAsset, + dispatch, + receiveAddress, + sellAccountMetadata, + sellAmountCryptoPrecision, + sellAsset, + votingPower, + thorVotingPower, + receiveAccountMetadata, + userSlippageTolerancePercentageDecimal, + sellAssetUsdRate, + sellAccountId, + isBuyAssetChainSupported, + hopExecutionMetadata, + activeTrade, + }, + ], + queryFn: queryFnOrSkip, + }) + const getTradeQuoteArgs = useCallback( (swapperName: SwapperName | undefined): UseGetSwapperTradeQuoteArgs => { return { swapperName, - tradeQuoteInput, + tradeQuoteInput: tradeQuoteInput ?? reduxSkipToken, // Skip trade quotes fetching which aren't for the swapper we have a rate for skip: !swapperName || !shouldFetchTradeQuotes, pollingInterval: From 6634b9ee3fa95a0c500fc466d70fc29eef5547f8 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Fri, 8 Nov 2024 11:23:11 +0900 Subject: [PATCH 25/62] feat: feelsgoodman.jpg --- .../src/swappers/PortalsSwapper/endpoints.ts | 47 ++----------------- .../getPortalsTradeQuote.ts | 4 ++ .../utils/fetchPortalsTradeOrder.ts | 5 +- packages/swapper/src/types.ts | 7 +++ 4 files changed, 19 insertions(+), 44 deletions(-) diff --git a/packages/swapper/src/swappers/PortalsSwapper/endpoints.ts b/packages/swapper/src/swappers/PortalsSwapper/endpoints.ts index e8a69517914..b046bfc197b 100644 --- a/packages/swapper/src/swappers/PortalsSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/PortalsSwapper/endpoints.ts @@ -1,10 +1,7 @@ -import { fromAssetId, fromChainId } from '@shapeshiftoss/caip' +import { fromChainId } from '@shapeshiftoss/caip' import { evm } from '@shapeshiftoss/chain-adapters' -import type { KnownChainIds } from '@shapeshiftoss/types' -import { convertBasisPointsToDecimalPercentage } from '@shapeshiftoss/utils' import type { Result } from '@sniptt/monads/build' import BigNumber from 'bignumber.js' -import { zeroAddress } from 'viem' import type { CommonTradeQuoteInput, @@ -20,13 +17,10 @@ import type { TradeRate, } from '../../types' import { checkEvmSwapStatus, isExecutableTradeQuote } from '../../utils' -import { getTreasuryAddressFromChainId, isNativeEvmAsset } from '../utils/helpers/helpers' -import { chainIdToPortalsNetwork } from './constants' import { getPortalsTradeQuote, getPortalsTradeRate, } from './getPortalsTradeQuote/getPortalsTradeQuote' -import { fetchPortalsTradeOrder } from './utils/fetchPortalsTradeOrder' export const portalsApi: SwapperApi = { getTradeQuote: async ( @@ -63,44 +57,13 @@ export const portalsApi: SwapperApi = { tradeQuote, supportsEIP1559, assertGetEvmChainAdapter, - config: swapperConfig, }: GetUnsignedEvmTransactionArgs): Promise => { if (!isExecutableTradeQuote(tradeQuote)) throw new Error('Unable to execute trade') - const { affiliateBps, slippageTolerancePercentageDecimal, steps } = tradeQuote - const { buyAsset, sellAsset, sellAmountIncludingProtocolFeesCryptoBaseUnit } = steps[0] + const { steps } = tradeQuote + const { portalsTransactionMetadata } = steps[0] - const portalsNetwork = chainIdToPortalsNetwork[chainId as KnownChainIds] - const sellAssetAddress = isNativeEvmAsset(sellAsset.assetId) - ? zeroAddress - : fromAssetId(sellAsset.assetId).assetReference - const buyAssetAddress = isNativeEvmAsset(buyAsset.assetId) - ? zeroAddress - : fromAssetId(buyAsset.assetId).assetReference - const inputToken = `${portalsNetwork}:${sellAssetAddress}` - const outputToken = `${portalsNetwork}:${buyAssetAddress}` - - // Not a decimal percentage, just a good ol' percentage e.g 1 for 1% - const affiliateBpsPercentage = convertBasisPointsToDecimalPercentage(affiliateBps) - .times(100) - .toNumber() - // We need to re-fetch the quote from Portals here with validation - // approvals, which prevent quotes during trade input from succeeding if the user hasn't already - // approved the token they are getting a quote for. - // TODO: we'll want to let users know if the quoted amounts change much after re-fetching - const portalsTradeOrderResponse = await fetchPortalsTradeOrder({ - sender: from, - inputToken, - outputToken, - inputAmount: sellAmountIncludingProtocolFeesCryptoBaseUnit, - slippageTolerancePercentage: Number(slippageTolerancePercentageDecimal) * 100, - partner: getTreasuryAddressFromChainId(sellAsset.chainId), - feePercentage: affiliateBpsPercentage, - validate: true, - swapperConfig, - }) - - if (!portalsTradeOrderResponse.tx) throw new Error('Portals Tx simulation failed upstream') + if (!portalsTransactionMetadata) throw new Error('Transaction metadata is required') const { value, @@ -108,7 +71,7 @@ export const portalsApi: SwapperApi = { data, // Portals has a 15% buffer on gas estimations, which may or may not turn out to be more reliable than our "pure" simulations gasLimit: estimatedGas, - } = portalsTradeOrderResponse.tx + } = portalsTransactionMetadata const { gasLimit, ...feeData } = await evm.getFees({ adapter: assertGetEvmChainAdapter(chainId), diff --git a/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts b/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts index e11ff417db9..b5a5713aa55 100644 --- a/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts +++ b/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts @@ -281,8 +281,11 @@ export async function getPortalsTradeQuote( feeAmount, gasLimit, }, + tx, } = portalsTradeOrderResponse + if (!tx) throw new Error('Portals Tx simulation failed upstream') + const rate = getRate({ sellAmountCryptoBaseUnit: input.sellAmountIncludingProtocolFeesCryptoBaseUnit, buyAmountCryptoBaseUnit: buyAmountAfterFeesCryptoBaseUnit, @@ -336,6 +339,7 @@ export async function getPortalsTradeQuote( }, source: SwapperName.Portals, estimatedExecutionTimeMs: undefined, // Portals doesn't provide this info + portalsTransactionMetadata: tx, }, ] as SingleHopTradeQuoteSteps, } diff --git a/packages/swapper/src/swappers/PortalsSwapper/utils/fetchPortalsTradeOrder.ts b/packages/swapper/src/swappers/PortalsSwapper/utils/fetchPortalsTradeOrder.ts index a7f2172828d..6adef58ac6c 100644 --- a/packages/swapper/src/swappers/PortalsSwapper/utils/fetchPortalsTradeOrder.ts +++ b/packages/swapper/src/swappers/PortalsSwapper/utils/fetchPortalsTradeOrder.ts @@ -1,4 +1,5 @@ import axios from 'axios' +import type { Address } from 'viem' import type { SwapperConfig } from '../../../types' @@ -48,8 +49,8 @@ type PortalsTradeOrderResponse = { feeAmountUsd?: number } tx?: { - to: string - from: string + to: Address + from: Address data: string value: string gasLimit: string diff --git a/packages/swapper/src/types.ts b/packages/swapper/src/types.ts index 8b46014bb67..6f8584f83f3 100644 --- a/packages/swapper/src/types.ts +++ b/packages/swapper/src/types.ts @@ -250,6 +250,13 @@ export type TradeQuoteStep = { gas: string | undefined value: string } + portalsTransactionMetadata?: { + to: Address + from: Address + data: string + value: string + gasLimit: string + } cowswapQuoteResponse?: CowSwapQuoteResponse } From 64d409c63858c2a018d199a7e8f008686968ee69 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:01:38 +0900 Subject: [PATCH 26/62] feat: wip portals gas things pending product feedback --- src/assets/translations/en/main.json | 4 +++- .../MultiHopTrade/components/RateGasRow.tsx | 10 ++++++++-- .../SharedTradeInputFooter.tsx | 4 ++-- .../components/TradeQuotes/TradeQuote.tsx | 2 +- src/state/slices/tradeQuoteSlice/helpers.ts | 8 ++++++-- src/state/slices/tradeQuoteSlice/selectors.ts | 15 +++++++++++---- 6 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index 2ee4b89fd8c..2b26a16ee61 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -182,7 +182,8 @@ "yes": "Yes", "activeAccount": "Active Account", "update": "Update", - "apy": "APY" + "apy": "APY", + "tbd": "TBD" }, "consentBanner": { "body": { @@ -894,6 +895,7 @@ "enterCustomRecipientAddress": "Enter custom recipient address", "receiveAtLeast": "Receive at least", "tooltip": { + "tbdGas": "This swapper does not support gas estimations yet. Stay tuned!", "changeQuote": "Change quote", "rate": "This is the expected rate for this trade pair.", "noRateAvailable": "This is often due to very limited or no liquidity on the trade pair.", diff --git a/src/components/MultiHopTrade/components/RateGasRow.tsx b/src/components/MultiHopTrade/components/RateGasRow.tsx index 60af29ec9a9..0b5adf92883 100644 --- a/src/components/MultiHopTrade/components/RateGasRow.tsx +++ b/src/components/MultiHopTrade/components/RateGasRow.tsx @@ -35,7 +35,7 @@ type RateGasRowProps = { sellSymbol?: string buySymbol?: string rate?: string - gasFee: string + gasFee: string | undefined isLoading?: boolean allowSelectQuote: boolean swapperName?: SwapperName @@ -188,7 +188,13 @@ export const RateGasRow: FC = memo( - + {!gasFee ? ( + + + + ) : ( + + )} {isOpen ? ( diff --git a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputFooter/SharedTradeInputFooter.tsx b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputFooter/SharedTradeInputFooter.tsx index f4227022076..1e28eaf66a0 100644 --- a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputFooter/SharedTradeInputFooter.tsx +++ b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputFooter/SharedTradeInputFooter.tsx @@ -39,7 +39,7 @@ type SharedTradeInputFooterProps = { shouldForceManualAddressEntry: boolean swapperName: SwapperName | undefined swapSource: SwapSource | undefined - totalNetworkFeeFiatPrecision: string + totalNetworkFeeFiatPrecision: string | undefined receiveSummaryDetails?: JSX.Element | null onRateClick: () => void } @@ -135,7 +135,7 @@ export const SharedTradeInputFooter = ({ = memo( quote, getFeeAsset, getFeeAssetUserCurrencyRate, - ).toString() + )?.toString() }, [quote]) // NOTE: don't pull this from the slice - we're not displaying the active quote here diff --git a/src/state/slices/tradeQuoteSlice/helpers.ts b/src/state/slices/tradeQuoteSlice/helpers.ts index c5e29308696..6b3a70e07ca 100644 --- a/src/state/slices/tradeQuoteSlice/helpers.ts +++ b/src/state/slices/tradeQuoteSlice/helpers.ts @@ -39,8 +39,11 @@ export const getTotalNetworkFeeUserCurrencyPrecision = ( quote: TradeQuote, getFeeAsset: (assetId: AssetId) => Asset, getFeeAssetRate: (feeAssetId: AssetId) => string, -): BigNumber => - quote.steps.reduce((acc, step) => { +): BigNumber | undefined => { + // network fee is unknown, which is different than it being akschual 0 + if (quote.steps.every(step => !step.feeData.networkFeeCryptoBaseUnit)) return + + return quote.steps.reduce((acc, step) => { const feeAsset = getFeeAsset(step.sellAsset.assetId) const networkFeeFiatPrecision = getHopTotalNetworkFeeUserCurrencyPrecision( step.feeData.networkFeeCryptoBaseUnit, @@ -49,6 +52,7 @@ export const getTotalNetworkFeeUserCurrencyPrecision = ( ) return acc.plus(networkFeeFiatPrecision ?? '0') }, bn(0)) +} export const getHopTotalProtocolFeesFiatPrecision = ( tradeQuoteStep: TradeQuote['steps'][number], diff --git a/src/state/slices/tradeQuoteSlice/selectors.ts b/src/state/slices/tradeQuoteSlice/selectors.ts index 4518421f7e3..fd331dce7a2 100644 --- a/src/state/slices/tradeQuoteSlice/selectors.ts +++ b/src/state/slices/tradeQuoteSlice/selectors.ts @@ -461,14 +461,21 @@ export const selectHopNetworkFeeUserCurrencyPrecision = createDeepEqualOutputSel }, ) -export const selectTotalNetworkFeeUserCurrencyPrecision: Selector = +export const selectTotalNetworkFeeUserCurrencyPrecision: Selector = createSelector( selectFirstHopNetworkFeeUserCurrencyPrecision, selectSecondHopNetworkFeeUserCurrencyPrecision, - (firstHopNetworkFeeUserCurrencyPrecision, secondHopNetworkFeeUserCurrencyPrecision) => - bnOrZero(firstHopNetworkFeeUserCurrencyPrecision) + (firstHopNetworkFeeUserCurrencyPrecision, secondHopNetworkFeeUserCurrencyPrecision) => { + if ( + firstHopNetworkFeeUserCurrencyPrecision === undefined && + secondHopNetworkFeeUserCurrencyPrecision === undefined + ) + return + + return bnOrZero(firstHopNetworkFeeUserCurrencyPrecision) .plus(secondHopNetworkFeeUserCurrencyPrecision ?? 0) - .toString(), + .toString() + }, ) export const selectDefaultSlippagePercentage: Selector = createSelector( From 556df270de7dda1b63505664568a9b98a2480d78 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:12:23 +0900 Subject: [PATCH 27/62] feat: conkschitschtency --- src/components/MultiHopTrade/components/RateGasRow.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/MultiHopTrade/components/RateGasRow.tsx b/src/components/MultiHopTrade/components/RateGasRow.tsx index 0b5adf92883..eec35bdd825 100644 --- a/src/components/MultiHopTrade/components/RateGasRow.tsx +++ b/src/components/MultiHopTrade/components/RateGasRow.tsx @@ -189,9 +189,7 @@ export const RateGasRow: FC = memo( {!gasFee ? ( - - - + ) : ( )} From 2367bf2620a446b1c1e8dabcf24c487634be6677 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:26:52 +0900 Subject: [PATCH 28/62] feat: two hard things in software --- .../MultiHopTradeConfirm/components/HopTransactionStep.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx index 0165b96ee32..5b1e192c0e9 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx @@ -153,7 +153,7 @@ export const HopTransactionStep = ({ return }, [swapTxState, swapperName]) - const { isFetching, data } = useGetTradeQuotes() + const { isFetching, data: tradeQuoteQueryData } = useGetTradeQuotes() const content = useMemo(() => { if (isActive && swapTxState === TransactionExecutionState.AwaitingConfirmation) { @@ -165,7 +165,7 @@ export const HopTransactionStep = ({ size='sm' onClick={handleSignTx} isLoading={isFetching} - isDisabled={!data} + isDisabled={!tradeQuoteQueryData} width='100%' > {translate('common.signTransaction')} @@ -196,7 +196,7 @@ export const HopTransactionStep = ({ sellTxHash, handleSignTx, isFetching, - data, + tradeQuoteQueryData, translate, hopIndex, activeTradeId, From e6d625ac18abd6cbf5c886c51e11bfc68ffbd59b Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:45:26 +0900 Subject: [PATCH 29/62] Revert "feat: permit2 progression" This reverts commit ee990ec36ce253bdf472ec319a5bc0c3272578c2. --- .../ApprovalStep/hooks/usePermit2Content.tsx | 20 ++++---- .../hooks/useSignPermit2.tsx | 46 ++----------------- 2 files changed, 13 insertions(+), 53 deletions(-) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep/hooks/usePermit2Content.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep/hooks/usePermit2Content.tsx index 87c6ad62ed7..bd7e0738bce 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep/hooks/usePermit2Content.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep/hooks/usePermit2Content.tsx @@ -38,16 +38,16 @@ export const usePermit2Content = ({ allowanceApproval, } = useAppSelector(state => selectHopExecutionMetadata(state, hopExecutionMetadataFilter)) - const { isLoading, signPermit2 } = useSignPermit2(tradeQuoteStep, hopIndex, activeTradeId) + const { signPermit2 } = useSignPermit2(tradeQuoteStep, hopIndex, activeTradeId) const isButtonDisabled = useMemo(() => { const isAwaitingPermit2 = hopExecutionState === HopExecutionState.AwaitingPermit2 const isError = permit2.state === TransactionExecutionState.Failed const isAwaitingConfirmation = permit2.state === TransactionExecutionState.AwaitingConfirmation - const isDisabled = isLoading || !isAwaitingPermit2 || !(isError || isAwaitingConfirmation) + const isDisabled = !isAwaitingPermit2 || !(isError || isAwaitingConfirmation) return isDisabled - }, [hopExecutionState, permit2.state, isLoading]) + }, [permit2.state, hopExecutionState]) const subHeadingTranslation: [string, InterpolationOptions] = useMemo(() => { return ['trade.permit2.description', { symbol: tradeQuoteStep.sellAsset.symbol }] @@ -59,7 +59,10 @@ export const usePermit2Content = ({ ) - }, [ - hopExecutionState, - isButtonDisabled, - isLoading, - permit2.state, - signPermit2, - subHeadingTranslation, - ]) + }, [hopExecutionState, isButtonDisabled, permit2.state, signPermit2, subHeadingTranslation]) const description = useMemo(() => { const txLines = [ diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useSignPermit2.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useSignPermit2.tsx index 0afdaf67aaf..3fc29269783 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useSignPermit2.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useSignPermit2.tsx @@ -1,10 +1,6 @@ -import { fromAccountId } from '@shapeshiftoss/caip' import { toAddressNList } from '@shapeshiftoss/chain-adapters' import type { TradeQuote, TradeQuoteStep } from '@shapeshiftoss/swapper' -import { fetchZrxPermit2Quote } from '@shapeshiftoss/swapper/dist/swappers/ZrxSwapper/utils/fetchFromZrx' -import { skipToken, useQuery } from '@tanstack/react-query' -import { getConfig } from 'config' -import type { TypedData } from 'eip-712' +import assert from 'assert' import { useCallback, useMemo } from 'react' import { useErrorHandler } from 'hooks/useErrorToast/useErrorToast' import { useWallet } from 'hooks/useWallet/useWallet' @@ -34,38 +30,6 @@ export const useSignPermit2 = ( selectHopSellAccountId(state, hopSellAccountIdFilter), ) - const sellAssetAccountAddress = useMemo( - () => (sellAssetAccountId ? fromAccountId(sellAssetAccountId).account : undefined), - [sellAssetAccountId], - ) - - // Fetch permit2 in-place - const { isFetching, data: permit2Eip712Data } = useQuery({ - queryKey: ['zrxPermit2', tradeQuoteStep], - queryFn: sellAssetAccountAddress - ? () => - fetchZrxPermit2Quote({ - buyAsset: tradeQuoteStep.buyAsset, - sellAsset: tradeQuoteStep.sellAsset, - sellAmountIncludingProtocolFeesCryptoBaseUnit: - tradeQuoteStep.sellAmountIncludingProtocolFeesCryptoBaseUnit, - sellAddress: sellAssetAccountAddress, - // irrelevant, we're only concerned about this query for the sole purpose of getting eip712 typed data - affiliateBps: '0', - // irrelevant for the same reason as above - slippageTolerancePercentageDecimal: '0.020', - zrxBaseUrl: getConfig().REACT_APP_ZRX_BASE_URL, - }) - : skipToken, - select: data => { - if (data.isErr()) throw data.unwrapErr() - - const { permit2 } = data.unwrap() - - return permit2?.eip712 as TypedData | undefined - }, - }) - const accountMetadataFilter = useMemo( () => ({ accountId: sellAssetAccountId }), [sellAssetAccountId], @@ -75,7 +39,7 @@ export const useSignPermit2 = ( ) const signPermit2 = useCallback(async () => { - if (!wallet || !accountMetadata || !permit2Eip712Data) return + if (!wallet || !accountMetadata) return dispatch( tradeQuoteSlice.actions.setPermit2SignaturePending({ @@ -85,9 +49,10 @@ export const useSignPermit2 = ( ) try { + assert(tradeQuoteStep.permit2Eip712, 'Trade quote is missing permit2 eip712 metadata') const typedDataToSign = { addressNList: toAddressNList(accountMetadata.bip44Params), - typedData: permit2Eip712Data, + typedData: tradeQuoteStep?.permit2Eip712, } const adapter = assertGetEvmChainAdapter(tradeQuoteStep.sellAsset.chainId) @@ -115,14 +80,13 @@ export const useSignPermit2 = ( confirmedTradeId, dispatch, hopIndex, - permit2Eip712Data, showErrorToast, + tradeQuoteStep.permit2Eip712, tradeQuoteStep.sellAsset.chainId, wallet, ]) return { signPermit2, - isLoading: isFetching, } } From 18d05420e3c1c8f70aea9d8e3794dc62db9e8c12 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Fri, 8 Nov 2024 13:08:01 +0900 Subject: [PATCH 30/62] feat: gm zrx permit2 --- .../components/ApprovalStep/ApprovalStep.tsx | 5 ++++ .../useGetTradeQuotes/useGetTradeQuotes.tsx | 24 +++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep/ApprovalStep.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep/ApprovalStep.tsx index 19b8657efca..08245fd2234 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep/ApprovalStep.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep/ApprovalStep.tsx @@ -4,6 +4,7 @@ import { AnimatePresence } from 'framer-motion' import { useMemo } from 'react' import { FaThumbsUp } from 'react-icons/fa6' import { useTranslate } from 'react-polyglot' +import { useGetTradeQuotes } from 'components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes' import { SlideTransitionX } from 'components/SlideTransitionX' import { selectHopExecutionMetadata } from 'state/slices/tradeQuoteSlice/selectors' import { HopExecutionState, TransactionExecutionState } from 'state/slices/tradeQuoteSlice/types' @@ -61,6 +62,10 @@ export const ApprovalStep = ({ activeTradeId, }) + // TODO: permit2 quotes at pre-signing time + const test = useGetTradeQuotes() + console.log({ test }) + const { content: permit2Content, description: permit2Description } = usePermit2Content({ tradeQuoteStep, hopIndex, diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index fa660f0ffe7..8947198f50d 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -1,10 +1,11 @@ import { skipToken as reduxSkipToken } from '@reduxjs/toolkit/query' import { fromAccountId } from '@shapeshiftoss/caip' import { isLedger } from '@shapeshiftoss/hdwallet-ledger' -import type { GetTradeQuoteInput, SwapperName, TradeQuote } from '@shapeshiftoss/swapper' +import type { GetTradeQuoteInput, TradeQuote } from '@shapeshiftoss/swapper' import { DEFAULT_GET_TRADE_QUOTE_POLLING_INTERVAL, isExecutableTradeQuote, + SwapperName, swappers, } from '@shapeshiftoss/swapper' import { isThorTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/getThorTradeQuoteOrRate/getTradeQuoteOrRate' @@ -44,7 +45,7 @@ import { selectSortedTradeQuotes, } from 'state/slices/tradeQuoteSlice/selectors' import { tradeQuoteSlice } from 'state/slices/tradeQuoteSlice/tradeQuoteSlice' -import { HopExecutionState } from 'state/slices/tradeQuoteSlice/types' +import { HopExecutionState, TransactionExecutionState } from 'state/slices/tradeQuoteSlice/types' import { store, useAppDispatch, useAppSelector } from 'state/store' import type { UseGetSwapperTradeQuoteArgs } from './hooks.tsx/useGetSwapperTradeQuote' @@ -210,6 +211,19 @@ export const useGetTradeQuotes = () => { const walletSupportsBuyAssetChain = useWalletSupportsChain(buyAsset.chainId, wallet) const isBuyAssetChainSupported = walletSupportsBuyAssetChain + // Is the step we're in a step which requires final quote fetching? + const isFetchStep = useMemo(() => { + const swapperName = activeQuoteMetaRef.current?.swapperName + if (!swapperName) return + const permit2 = hopExecutionMetadata?.permit2 + if (swapperName === SwapperName.Zrx) + return ( + permit2?.isRequired && permit2?.state === TransactionExecutionState.AwaitingConfirmation + ) + + return hopExecutionMetadata?.state === HopExecutionState.AwaitingSwap + }, [hopExecutionMetadata?.permit2, hopExecutionMetadata?.state]) + const shouldFetchTradeQuotes = useMemo( () => Boolean( @@ -218,7 +232,7 @@ export const useGetTradeQuotes = () => { activeTrade && !isExecutableTradeQuote(activeTrade) && // and if we're actually at pre-execution time - hopExecutionMetadata?.state === HopExecutionState.AwaitingSwap && + isFetchStep && sellAccountId && sellAccountMetadata && receiveAddress, @@ -236,7 +250,7 @@ export const useGetTradeQuotes = () => { const queryFnOrSkip = useMemo(() => { // Only run this query when we're actually ready - if (hopExecutionMetadata?.state !== HopExecutionState.AwaitingSwap) return reactQuerySkipToken + if (!isFetchStep) return reactQuerySkipToken // And only run it once if (activeTrade && isExecutableTradeQuote(activeTrade)) return reactQuerySkipToken @@ -289,7 +303,7 @@ export const useGetTradeQuotes = () => { activeTrade, buyAsset, dispatch, - hopExecutionMetadata?.state, + isFetchStep, receiveAccountMetadata?.bip44Params, receiveAddress, sellAccountId, From 10fd05143ce5bff45f60a8bf8a5556fc94aff4f1 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Fri, 8 Nov 2024 15:34:32 +0900 Subject: [PATCH 31/62] feat: renamy and bring back unknown tooltip --- src/assets/translations/en/main.json | 2 +- .../LimitOrder/components/LimitOrderInput.tsx | 2 +- .../MultiHopTrade/components/RateGasRow.tsx | 15 ++++++++++----- .../SharedTradeInputFooter.tsx | 6 +++--- .../TradeInput/components/ConfirmSummary.tsx | 2 +- .../components/TradeQuotes/TradeQuote.tsx | 4 ++-- .../components/TradeQuoteContent.tsx | 17 ++++++++--------- 7 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index 2b26a16ee61..d6f7f48601b 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -895,7 +895,7 @@ "enterCustomRecipientAddress": "Enter custom recipient address", "receiveAtLeast": "Receive at least", "tooltip": { - "tbdGas": "This swapper does not support gas estimations yet. Stay tuned!", + "continueSwapping": "Continue swapping for final details", "changeQuote": "Change quote", "rate": "This is the expected rate for this trade pair.", "noRateAvailable": "This is often due to very limited or no liquidity on the trade pair.", diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx index 7fe56d287cf..c483013083d 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx @@ -336,7 +336,7 @@ export const LimitOrderInput = ({ shouldForceManualAddressEntry={false} swapperName={SwapperName.CowSwap} swapSource={SwapperName.CowSwap} - totalNetworkFeeFiatPrecision={'1.1234'} + networkFeeFiatUserCurrency={'1.1234'} /> ) }, [ diff --git a/src/components/MultiHopTrade/components/RateGasRow.tsx b/src/components/MultiHopTrade/components/RateGasRow.tsx index eec35bdd825..219c4e3b8b6 100644 --- a/src/components/MultiHopTrade/components/RateGasRow.tsx +++ b/src/components/MultiHopTrade/components/RateGasRow.tsx @@ -35,7 +35,7 @@ type RateGasRowProps = { sellSymbol?: string buySymbol?: string rate?: string - gasFee: string | undefined + networkFeeFiatUserCurrency: string | undefined isLoading?: boolean allowSelectQuote: boolean swapperName?: SwapperName @@ -55,7 +55,7 @@ export const RateGasRow: FC = memo( sellSymbol, buySymbol, rate, - gasFee, + networkFeeFiatUserCurrency, isLoading, allowSelectQuote, swapperName, @@ -188,10 +188,15 @@ export const RateGasRow: FC = memo( - {!gasFee ? ( - + {!networkFeeFiatUserCurrency ? ( + + + ) : ( - + )} diff --git a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputFooter/SharedTradeInputFooter.tsx b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputFooter/SharedTradeInputFooter.tsx index 1e28eaf66a0..56d2b66154a 100644 --- a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputFooter/SharedTradeInputFooter.tsx +++ b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputFooter/SharedTradeInputFooter.tsx @@ -39,7 +39,7 @@ type SharedTradeInputFooterProps = { shouldForceManualAddressEntry: boolean swapperName: SwapperName | undefined swapSource: SwapSource | undefined - totalNetworkFeeFiatPrecision: string | undefined + networkFeeFiatUserCurrency: string | undefined receiveSummaryDetails?: JSX.Element | null onRateClick: () => void } @@ -65,7 +65,7 @@ export const SharedTradeInputFooter = ({ shouldForceManualAddressEntry, swapperName, swapSource, - totalNetworkFeeFiatPrecision, + networkFeeFiatUserCurrency, receiveSummaryDetails, onRateClick, }: SharedTradeInputFooterProps) => { @@ -135,7 +135,7 @@ export const SharedTradeInputFooter = ({ {nativeAssetBridgeWarning ? ( diff --git a/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/TradeQuote.tsx b/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/TradeQuote.tsx index 7ed7358f539..0ef5aeca8f2 100644 --- a/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/TradeQuote.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/TradeQuote.tsx @@ -298,11 +298,11 @@ export const TradeQuote: FC = memo( buyAsset={buyAsset} isBest={isBest} numHops={quote?.steps.length} - totalReceiveAmountFiatPrecision={totalReceiveAmountFiatPrecision} + totalReceiveAmountFiatUserCurrency={totalReceiveAmountFiatPrecision} hasAmountWithPositiveReceive={hasAmountWithPositiveReceive} totalReceiveAmountCryptoPrecision={totalReceiveAmountCryptoPrecision} quoteDifferenceDecimalPercentage={quoteAmountDifferenceDecimalPercentage} - networkFeeUserCurrencyPrecision={networkFeeUserCurrencyPrecision} + networkFeeFiatUserCurrency={networkFeeUserCurrencyPrecision} totalEstimatedExecutionTimeMs={totalEstimatedExecutionTimeMs} slippage={slippage} tradeQuote={quote} diff --git a/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/components/TradeQuoteContent.tsx b/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/components/TradeQuoteContent.tsx index fb5d6025174..383a688bddb 100644 --- a/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/components/TradeQuoteContent.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/components/TradeQuoteContent.tsx @@ -17,11 +17,11 @@ export type TradeQuoteContentProps = { buyAsset: Asset isBest: boolean numHops: number - totalReceiveAmountFiatPrecision: string | undefined + totalReceiveAmountFiatUserCurrency: string | undefined hasAmountWithPositiveReceive: boolean totalReceiveAmountCryptoPrecision: string quoteDifferenceDecimalPercentage: number | undefined - networkFeeUserCurrencyPrecision: string | undefined + networkFeeFiatUserCurrency: string | undefined totalEstimatedExecutionTimeMs: number | undefined slippage: JSX.Element | undefined tradeQuote: TradeQuote | undefined @@ -32,11 +32,11 @@ export const TradeQuoteContent = ({ buyAsset, isBest, numHops, - totalReceiveAmountFiatPrecision, + totalReceiveAmountFiatUserCurrency, hasAmountWithPositiveReceive, totalReceiveAmountCryptoPrecision, quoteDifferenceDecimalPercentage: maybeQuoteDifferenceDecimalPercentage, - networkFeeUserCurrencyPrecision, + networkFeeFiatUserCurrency, totalEstimatedExecutionTimeMs, slippage, tradeQuote, @@ -128,10 +128,10 @@ export const TradeQuoteContent = ({ )} - {totalReceiveAmountFiatPrecision ? ( + {totalReceiveAmountFiatUserCurrency ? ( @@ -147,13 +147,12 @@ export const TradeQuoteContent = ({ - { // We cannot infer gas fees in specific scenarios, so if the fee is undefined we must render is as such - !networkFeeUserCurrencyPrecision ? ( + !networkFeeFiatUserCurrency ? ( translate('trade.unknownGas') ) : ( - + ) } From 0534047d2e3f4e93e9827ca5e24cdcf4250e8b81 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Fri, 8 Nov 2024 15:35:33 +0900 Subject: [PATCH 32/62] feat: tooltip in quote too --- .../TradeQuotes/components/TradeQuoteContent.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/components/TradeQuoteContent.tsx b/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/components/TradeQuoteContent.tsx index 383a688bddb..03fbd90a36c 100644 --- a/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/components/TradeQuoteContent.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/components/TradeQuoteContent.tsx @@ -9,7 +9,7 @@ import { MdOfflineBolt } from 'react-icons/md' import { useTranslate } from 'react-polyglot' import { Amount } from 'components/Amount/Amount' import { usePriceImpact } from 'components/MultiHopTrade/hooks/quoteValidation/usePriceImpact' -import { RawText } from 'components/Text' +import { RawText, Text } from 'components/Text' import { useLocaleFormatter } from 'hooks/useLocaleFormatter/useLocaleFormatter' export type TradeQuoteContentProps = { @@ -150,7 +150,12 @@ export const TradeQuoteContent = ({ { // We cannot infer gas fees in specific scenarios, so if the fee is undefined we must render is as such !networkFeeFiatUserCurrency ? ( - translate('trade.unknownGas') + + + ) : ( ) From 0a9df4f1c70c56b013bc64a34063a4b4a1b0efdd Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:05:45 +0900 Subject: [PATCH 33/62] feat: stop passing receiveAddress in getTradeQuoteInput --- .../useGetTradeQuotes/useGetTradeRates.tsx | 28 ++++++------------- src/state/apis/swapper/swapperApi.ts | 3 +- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx index b725e7a5bc0..528cae3e9d7 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx @@ -1,7 +1,7 @@ import { skipToken } from '@reduxjs/toolkit/dist/query' import { fromAccountId } from '@shapeshiftoss/caip' import { isLedger } from '@shapeshiftoss/hdwallet-ledger' -import type { GetTradeQuoteInput } from '@shapeshiftoss/swapper' +import type { GetTradeRateInput } from '@shapeshiftoss/swapper' import { DEFAULT_GET_TRADE_QUOTE_POLLING_INTERVAL, SwapperName, @@ -10,7 +10,6 @@ import { import { isThorTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/getThorTradeQuoteOrRate/getTradeQuoteOrRate' import { useCallback, useEffect, useMemo, useState } from 'react' import { getTradeQuoteInput } from 'components/MultiHopTrade/hooks/useGetTradeQuotes/getTradeQuoteInput' -import { useReceiveAddress } from 'components/MultiHopTrade/hooks/useReceiveAddress' import { useHasFocus } from 'hooks/useHasFocus' import { useWallet } from 'hooks/useWallet/useWallet' import { useWalletSupportsChain } from 'hooks/useWalletSupportsChain/useWalletSupportsChain' @@ -120,20 +119,12 @@ export const useGetTradeRates = () => { const sortedTradeQuotes = useAppSelector(selectSortedTradeQuotes) const activeQuoteMeta = useAppSelector(selectActiveQuoteMetaOrDefault) - const [tradeQuoteInput, setTradeQuoteInput] = useState( + const [tradeRateInput, setTradeRateInput] = useState( skipToken, ) const hasFocus = useHasFocus() const sellAsset = useAppSelector(selectInputSellAsset) const buyAsset = useAppSelector(selectInputBuyAsset) - const useReceiveAddressArgs = useMemo( - () => ({ - fetchUnchainedAddress: Boolean(wallet && isLedger(wallet)), - }), - [wallet], - ) - const { manualReceiveAddress, walletReceiveAddress } = useReceiveAddress(useReceiveAddressArgs) - const receiveAddress = manualReceiveAddress ?? walletReceiveAddress const sellAmountCryptoPrecision = useAppSelector(selectInputSellAmountCryptoPrecision) const sellAccountId = useAppSelector(selectFirstHopSellAccountId) @@ -192,7 +183,7 @@ export const useGetTradeRates = () => { // Early exit on any invalid state if (bnOrZero(sellAmountCryptoPrecision).isZero()) { - setTradeQuoteInput(skipToken) + setTradeRateInput(skipToken) dispatch(tradeQuoteSlice.actions.setIsTradeQuoteRequestAborted(true)) return } @@ -213,7 +204,7 @@ export const useGetTradeRates = () => { const potentialAffiliateBps = feeBpsBeforeDiscount.toFixed(0) const affiliateBps = feeBps.toFixed(0) - const updatedTradeQuoteInput: GetTradeQuoteInput | undefined = await getTradeQuoteInput({ + const updatedTradeRateInput = (await getTradeQuoteInput({ sellAsset, sellAccountNumber, receiveAccountNumber, @@ -221,7 +212,7 @@ export const useGetTradeRates = () => { buyAsset, wallet: wallet ?? undefined, quoteOrRate: 'rate', - receiveAddress, + receiveAddress: undefined, sellAmountBeforeFeesCryptoPrecision: sellAmountCryptoPrecision, allowMultiHop: true, affiliateBps, @@ -232,14 +223,13 @@ export const useGetTradeRates = () => { wallet && isLedger(wallet) && sellAccountId ? fromAccountId(sellAccountId).account : undefined, - }) + })) as GetTradeRateInput - setTradeQuoteInput(updatedTradeQuoteInput) + setTradeRateInput(updatedTradeRateInput) })() }, [ buyAsset, dispatch, - receiveAddress, sellAccountMetadata, sellAmountCryptoPrecision, sellAsset, @@ -258,13 +248,13 @@ export const useGetTradeRates = () => { (swapperName: SwapperName): UseGetSwapperTradeQuoteArgs => { return { swapperName, - tradeQuoteInput, + tradeQuoteInput: tradeRateInput, skip: !shouldRefetchTradeQuotes, pollingInterval: swappers[swapperName]?.pollingInterval ?? DEFAULT_GET_TRADE_QUOTE_POLLING_INTERVAL, } }, - [shouldRefetchTradeQuotes, tradeQuoteInput], + [shouldRefetchTradeQuotes, tradeRateInput], ) useGetSwapperTradeQuote(getTradeQuoteArgs(SwapperName.CowSwap)) diff --git a/src/state/apis/swapper/swapperApi.ts b/src/state/apis/swapper/swapperApi.ts index 417720dd6ed..90fe05cffe6 100644 --- a/src/state/apis/swapper/swapperApi.ts +++ b/src/state/apis/swapper/swapperApi.ts @@ -55,7 +55,8 @@ export const swapperApi = createApi({ quoteOrRate, } = tradeQuoteInput - const isCrossAccountTrade = sendAddress !== receiveAddress + const isCrossAccountTrade = + Boolean(sendAddress && receiveAddress) && sendAddress !== receiveAddress const featureFlags: FeatureFlags = selectFeatureFlags(state) const isSwapperEnabled = getEnabledSwappers(featureFlags, isCrossAccountTrade)[swapperName] From 21e2306a7936551fa23c75ee374202fe32ed4e2b Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Fri, 8 Nov 2024 22:55:37 +0900 Subject: [PATCH 34/62] fix: disconnected state --- src/state/apis/swapper/helpers/validateTradeQuote.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/state/apis/swapper/helpers/validateTradeQuote.ts b/src/state/apis/swapper/helpers/validateTradeQuote.ts index 49095fb1539..668739c534d 100644 --- a/src/state/apis/swapper/helpers/validateTradeQuote.ts +++ b/src/state/apis/swapper/helpers/validateTradeQuote.ts @@ -17,6 +17,7 @@ import { selectPortfolioAccountBalancesBaseUnit, selectPortfolioCryptoPrecisionBalanceByFilter, selectWalletConnectedChainIds, + selectWalletId, } from 'state/slices/common-selectors' import { selectAssets, @@ -114,6 +115,9 @@ export const validateTradeQuote = ( // This should really never happen in case the wallet *is* connected but in case it does: if (quoteOrRate === 'quote' && !sendAddress) throw new Error('sendAddress is required') + // If we have a walletId at the time we hit this, we have a wallet. Else, none is connected, meaning we shouldn't surface balance errors + const walletId = selectWalletId(state) + // A quote always consists of at least one hop const firstHop = getHopByIndex(quote, 0)! const secondHop = getHopByIndex(quote, 1) @@ -265,7 +269,8 @@ export const validateTradeQuote = ( chainSymbol: getChainShortName(secondHop.sellAsset.chainId as KnownChainIds), }, }, - !firstHopHasSufficientBalanceForGas && + walletId !== undefined && + !firstHopHasSufficientBalanceForGas && quoteOrRate === 'rate' && { error: TradeQuoteValidationError.InsufficientFirstHopFeeAssetBalance, meta: { @@ -275,7 +280,8 @@ export const validateTradeQuote = ( : '', }, }, - !secondHopHasSufficientBalanceForGas && + walletId !== undefined && + !secondHopHasSufficientBalanceForGas && quoteOrRate === 'rate' && { error: TradeQuoteValidationError.InsufficientSecondHopFeeAssetBalance, meta: { From 1a119eb66d510e32fd0b8ca57e126ceaabf45be3 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 11 Nov 2024 17:45:58 +0700 Subject: [PATCH 35/62] fix: ci --- .../hooks/useGetTradeQuotes/useGetTradeQuotes.tsx | 2 +- .../MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index 2dce4d6ff76..cacbc0ed8df 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -10,7 +10,7 @@ import { } from '@shapeshiftoss/swapper' import { isThorTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/getThorTradeQuoteOrRate/getTradeQuoteOrRate' import { skipToken as reactQuerySkipToken, useQuery } from '@tanstack/react-query' -import { useCallback, useEffect, useRef } from 'react' +import { useCallback, useEffect, useMemo, useRef } from 'react' import { useTradeReceiveAddress } from 'components/MultiHopTrade/components/TradeInput/hooks/useTradeReceiveAddress' import { getTradeQuoteInput } from 'components/MultiHopTrade/hooks/useGetTradeQuotes/getTradeQuoteInput' import { useHasFocus } from 'hooks/useHasFocus' diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx index 528cae3e9d7..d68579339e8 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx @@ -189,8 +189,6 @@ export const useGetTradeRates = () => { } ;(async () => { const sellAccountNumber = sellAccountMetadata?.bip44Params?.accountNumber - const receiveAssetBip44Params = receiveAccountMetadata?.bip44Params - const receiveAccountNumber = receiveAssetBip44Params?.accountNumber const tradeAmountUsd = bnOrZero(sellAssetUsdRate).times(sellAmountCryptoPrecision) @@ -207,7 +205,6 @@ export const useGetTradeRates = () => { const updatedTradeRateInput = (await getTradeQuoteInput({ sellAsset, sellAccountNumber, - receiveAccountNumber, sellAccountType: sellAccountMetadata?.accountType, buyAsset, wallet: wallet ?? undefined, From 6843340f663c49a3499824f788f630a649da0571 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 11 Nov 2024 17:55:41 +0700 Subject: [PATCH 36/62] feat: cleanup log --- src/state/slices/tradeQuoteSlice/selectors.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/state/slices/tradeQuoteSlice/selectors.ts b/src/state/slices/tradeQuoteSlice/selectors.ts index 34db2e929a1..66b9a5b66ea 100644 --- a/src/state/slices/tradeQuoteSlice/selectors.ts +++ b/src/state/slices/tradeQuoteSlice/selectors.ts @@ -202,7 +202,6 @@ export const selectActiveStepOrDefault: Selector = createSel const selectConfirmedQuote: Selector = createDeepEqualOutputSelector(selectTradeQuoteSlice, tradeQuoteState => { - console.log({ tradeQuoteState }) return tradeQuoteState.confirmedQuote }) From 880866b53d8d4de7f529d5f24c121074e9818948 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 11 Nov 2024 18:17:41 +0700 Subject: [PATCH 37/62] fix: flashy flash --- .../MultiHopTrade/components/TradeInput/TradeInput.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx index 59417fde208..217987cc9c6 100644 --- a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx @@ -36,6 +36,7 @@ import { selectInputSellAsset, selectIsAnyAccountMetadataLoadedForChainId, selectIsInputtingFiatSellAmount, + selectWalletId, } from 'state/slices/selectors' import { tradeInput } from 'state/slices/tradeInputSlice/tradeInputSlice' import { @@ -120,6 +121,7 @@ export const TradeInput = ({ isCompact, tradeInputRef, onChangeTab }: TradeInput const isAnyAccountMetadataLoadedForChainId = useAppSelector(state => selectIsAnyAccountMetadataLoadedForChainId(state, isAnyAccountMetadataLoadedForChainIdFilter), ) + const walletId = useAppSelector(selectWalletId) const inputOutputDifferenceDecimalPercentage = useInputOutputDifferenceDecimalPercentage(activeQuote) @@ -140,15 +142,16 @@ export const TradeInput = ({ isCompact, tradeInputRef, onChangeTab }: TradeInput const isLoading = useMemo( () => // No account meta loaded for that chain - !isAnyAccountMetadataLoadedForChainId || + (walletId && !isAnyAccountMetadataLoadedForChainId) || (!shouldShowTradeQuoteOrAwaitInput && !isTradeQuoteRequestAborted) || isConfirmationLoading || // Only consider snapshot API queries as pending if we don't have voting power yet // if we do, it means we have persisted or cached (both stale) data, which is enough to let the user continue // as we are optimistic and don't want to be waiting for a potentially very long time for the snapshot API to respond isVotingPowerLoading || - isWalletReceiveAddressLoading, + (walletId && isWalletReceiveAddressLoading), [ + walletId, isAnyAccountMetadataLoadedForChainId, shouldShowTradeQuoteOrAwaitInput, isTradeQuoteRequestAborted, From a441116094c4b1e0e57ef6b3570f8779ac007061 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 11 Nov 2024 19:45:03 +0700 Subject: [PATCH 38/62] feat: progression --- .../Modals/RateChanged/RateChanged.tsx | 14 +++++++++++ .../components/ApprovalStep/ApprovalStep.tsx | 7 +++--- .../ApprovalStep/hooks/usePermit2Content.tsx | 19 +++++++++++---- .../components/AssetSummaryStep.tsx | 23 ++++++++++++++++++- .../components/TradeInput/TradeInput.tsx | 4 ++-- src/context/ModalProvider/ModalContainer.tsx | 9 ++++++++ src/context/ModalProvider/types.ts | 1 + 7 files changed, 66 insertions(+), 11 deletions(-) create mode 100644 src/components/Modals/RateChanged/RateChanged.tsx diff --git a/src/components/Modals/RateChanged/RateChanged.tsx b/src/components/Modals/RateChanged/RateChanged.tsx new file mode 100644 index 00000000000..82cfb726aa3 --- /dev/null +++ b/src/components/Modals/RateChanged/RateChanged.tsx @@ -0,0 +1,14 @@ +import { Modal, ModalContent, ModalOverlay } from '@chakra-ui/react' +import { useModal } from 'hooks/useModal/useModal' + +export const RateChangedModal = () => { + const rateChanged = useModal('rateChanged') + const { close, isOpen } = rateChanged + + return ( + + + Rate changed - TODO @reallybeard oil + + ) +} diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep/ApprovalStep.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep/ApprovalStep.tsx index 08245fd2234..e075653efdd 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep/ApprovalStep.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep/ApprovalStep.tsx @@ -33,6 +33,9 @@ export const ApprovalStep = ({ isLoading, activeTradeId, }: ApprovalStepProps) => { + // DO NOT REMOVE ME. Fetches and upserts permit2 quotes at pre-permit2-signing time + useGetTradeQuotes() + const translate = useTranslate() const hopExecutionMetadataFilter = useMemo(() => { @@ -62,10 +65,6 @@ export const ApprovalStep = ({ activeTradeId, }) - // TODO: permit2 quotes at pre-signing time - const test = useGetTradeQuotes() - console.log({ test }) - const { content: permit2Content, description: permit2Description } = usePermit2Content({ tradeQuoteStep, hopIndex, diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep/hooks/usePermit2Content.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep/hooks/usePermit2Content.tsx index bd7e0738bce..bb85519eae0 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep/hooks/usePermit2Content.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep/hooks/usePermit2Content.tsx @@ -2,6 +2,7 @@ import type { TradeQuote, TradeQuoteStep } from '@shapeshiftoss/swapper' import { isSome } from '@shapeshiftoss/utils' import type { InterpolationOptions } from 'node-polyglot' import { useMemo } from 'react' +import { useGetTradeQuotes } from 'components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes' import { selectHopExecutionMetadata } from 'state/slices/tradeQuoteSlice/selectors' import { HopExecutionState, TransactionExecutionState } from 'state/slices/tradeQuoteSlice/types' import { useAppSelector } from 'state/store' @@ -40,14 +41,17 @@ export const usePermit2Content = ({ const { signPermit2 } = useSignPermit2(tradeQuoteStep, hopIndex, activeTradeId) + const { isLoading: isTradeQuotesLoading } = useGetTradeQuotes() + const isButtonDisabled = useMemo(() => { const isAwaitingPermit2 = hopExecutionState === HopExecutionState.AwaitingPermit2 const isError = permit2.state === TransactionExecutionState.Failed const isAwaitingConfirmation = permit2.state === TransactionExecutionState.AwaitingConfirmation - const isDisabled = !isAwaitingPermit2 || !(isError || isAwaitingConfirmation) + const isDisabled = + !isAwaitingPermit2 || !(isError || isAwaitingConfirmation) || isTradeQuotesLoading return isDisabled - }, [permit2.state, hopExecutionState]) + }, [hopExecutionState, permit2.state, isTradeQuotesLoading]) const subHeadingTranslation: [string, InterpolationOptions] = useMemo(() => { return ['trade.permit2.description', { symbol: tradeQuoteStep.sellAsset.symbol }] @@ -61,7 +65,7 @@ export const usePermit2Content = ({ isDisabled={isButtonDisabled} isLoading={ /* NOTE: No loading state when signature in progress because it's instant */ - false + isTradeQuotesLoading } subHeadingTranslation={subHeadingTranslation} titleTranslation='trade.permit2.title' @@ -70,7 +74,14 @@ export const usePermit2Content = ({ onSubmit={signPermit2} /> ) - }, [hopExecutionState, isButtonDisabled, permit2.state, signPermit2, subHeadingTranslation]) + }, [ + hopExecutionState, + isButtonDisabled, + isTradeQuotesLoading, + permit2.state, + signPermit2, + subHeadingTranslation, + ]) const description = useMemo(() => { const txLines = [ diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/AssetSummaryStep.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/AssetSummaryStep.tsx index f1742bf09f7..9f4ac9deccc 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/AssetSummaryStep.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/AssetSummaryStep.tsx @@ -1,9 +1,11 @@ +import { usePrevious } from '@chakra-ui/react' import type { Asset } from '@shapeshiftoss/types' -import { useMemo } from 'react' +import { useEffect, useMemo } from 'react' import { useTranslate } from 'react-polyglot' import { AssetIcon } from 'components/AssetIcon' import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingleton' import { useLocaleFormatter } from 'hooks/useLocaleFormatter/useLocaleFormatter' +import { useModal } from 'hooks/useModal/useModal' import { bn } from 'lib/bignumber/bignumber' import { fromBaseUnit } from 'lib/math' import { @@ -27,6 +29,7 @@ export const AssetSummaryStep = ({ isLastStep, button, }: AssetSummaryStepProps) => { + const rateChanged = useModal('rateChanged') const translate = useTranslate() const { number: { toCrypto, toFiat }, @@ -57,6 +60,24 @@ export const AssetSummaryStep = ({ return chainName }, [asset.chainId]) + const prevAmountCryptoBaseUnit = usePrevious(amountCryptoBaseUnit) + + useEffect(() => { + if (!isLastStep) return + if ( + !( + amountCryptoBaseUnit && + prevAmountCryptoBaseUnit && + amountCryptoBaseUnit !== '0' && + prevAmountCryptoBaseUnit !== '0' + ) + ) + return + if (amountCryptoBaseUnit === prevAmountCryptoBaseUnit) return + + rateChanged.open({}) + }, [amountCryptoBaseUnit, isLastStep, prevAmountCryptoBaseUnit, rateChanged]) + const assetIcon = useMemo(() => { return }, [asset.assetId]) diff --git a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx index 217987cc9c6..f6dde8641e6 100644 --- a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx @@ -142,14 +142,14 @@ export const TradeInput = ({ isCompact, tradeInputRef, onChangeTab }: TradeInput const isLoading = useMemo( () => // No account meta loaded for that chain - (walletId && !isAnyAccountMetadataLoadedForChainId) || + Boolean(walletId && !isAnyAccountMetadataLoadedForChainId) || (!shouldShowTradeQuoteOrAwaitInput && !isTradeQuoteRequestAborted) || isConfirmationLoading || // Only consider snapshot API queries as pending if we don't have voting power yet // if we do, it means we have persisted or cached (both stale) data, which is enough to let the user continue // as we are optimistic and don't want to be waiting for a potentially very long time for the snapshot API to respond isVotingPowerLoading || - (walletId && isWalletReceiveAddressLoading), + Boolean(walletId && isWalletReceiveAddressLoading), [ walletId, isAnyAccountMetadataLoadedForChainId, diff --git a/src/context/ModalProvider/ModalContainer.tsx b/src/context/ModalProvider/ModalContainer.tsx index 614354dd2d7..f85541bc1c9 100644 --- a/src/context/ModalProvider/ModalContainer.tsx +++ b/src/context/ModalProvider/ModalContainer.tsx @@ -146,6 +146,14 @@ const NftModal = makeSuspenseful( ), ) +const RateChangedModal = makeSuspenseful( + lazy(() => + import('components/Modals/RateChanged/RateChanged').then(({ RateChangedModal }) => ({ + default: RateChangedModal, + })), + ), +) + const FeedbackAndSupport = makeSuspenseful( lazy(() => import('components/Modals/FeedbackSupport/FeedbackSupport').then(({ FeedbackAndSupport }) => ({ @@ -203,6 +211,7 @@ export const MODALS: Modals = { // Important: Order matters here -This modal must be mounted before the ManageAccountsModal so it can be opened ledgerOpenApp: LedgerOpenAppModal, manageAccounts: ManageAccountsModal, + rateChanged: RateChangedModal, } as const export const modalReducer = (state: ModalState, action: ModalActions): ModalState => { diff --git a/src/context/ModalProvider/types.ts b/src/context/ModalProvider/types.ts index c8f6c784dbe..406751d5849 100644 --- a/src/context/ModalProvider/types.ts +++ b/src/context/ModalProvider/types.ts @@ -35,6 +35,7 @@ export type Modals = { snaps: FC manageAccounts: FC ledgerOpenApp: FC + rateChanged: FC } export type ModalActions = OpenModalType | CloseModalType From 847119d4ec3a6ff56ee78462ab17a8e2368f20a7 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 13 Nov 2024 23:06:18 +0700 Subject: [PATCH 39/62] fix: tests --- .../swappers/CowSwapper/CowSwapper.test.ts | 51 ++++++++-------- .../getCowSwapTradeQuote.test.ts | 60 +++++++++++++++++++ 2 files changed, 87 insertions(+), 24 deletions(-) diff --git a/packages/swapper/src/swappers/CowSwapper/CowSwapper.test.ts b/packages/swapper/src/swappers/CowSwapper/CowSwapper.test.ts index c0b99b061d3..3b0cb277a65 100644 --- a/packages/swapper/src/swappers/CowSwapper/CowSwapper.test.ts +++ b/packages/swapper/src/swappers/CowSwapper/CowSwapper.test.ts @@ -187,6 +187,31 @@ describe('cowApi', () => { const stepIndex = 0 const chainId = ethChainId const slippageTolerancePercentageDecimal = '0.005' // 0.5% + + const cowswapQuoteResponse = { + quote: { + sellToken: '0xc770eefad204b5180df6a14ee197d99d808ee52d', + buyToken: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + receiver: '0x90a48d5cf7343b08da12e067680b4c6dbfe551be', + sellAmount: '9755648144619063874259', + buyAmount: '289305614806369753', + validTo: 1712259433, + appData: + '{"appCode":"shapeshift","metadata":{"orderClass":{"orderClass":"market"},"quote":{"slippageBips":"50"}},"version":"0.9.0"}', + appDataHash: '0x9b3c15b566e3b432f1ba3533bb0b071553fd03cec359caf3e6559b29fec1e62e', + feeAmount: '184116879335769833472', + kind: 'sell', + partiallyFillable: false, + sellTokenBalance: 'erc20', + buyTokenBalance: 'erc20', + signingScheme: 'eip712', + }, + from: '0x90a48d5cf7343b08da12e067680b4c6dbfe551be', + expiration: '2024-04-04T19:09:12.792412370Z', + id: 474006349, + verified: false, + } + const tradeQuote = { id: '474004127', receiveAddress: '0x90a48d5cf7343b08da12e067680b4c6dbfe551be', @@ -252,36 +277,14 @@ describe('cowApi', () => { relatedAssetKey: 'eip155:1/erc20:0xc770eefad204b5180df6a14ee197d99d808ee52d', }, accountNumber: 0, + cowswapQuoteResponse, }, ], } as unknown as TradeQuote - const cowSwapQuoteResponse = { - quote: { - sellToken: '0xc770eefad204b5180df6a14ee197d99d808ee52d', - buyToken: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - receiver: '0x90a48d5cf7343b08da12e067680b4c6dbfe551be', - sellAmount: '9755648144619063874259', - buyAmount: '289305614806369753', - validTo: 1712259433, - appData: - '{"appCode":"shapeshift","metadata":{"orderClass":{"orderClass":"market"},"quote":{"slippageBips":"50"}},"version":"0.9.0"}', - appDataHash: '0x9b3c15b566e3b432f1ba3533bb0b071553fd03cec359caf3e6559b29fec1e62e', - feeAmount: '184116879335769833472', - kind: 'sell', - partiallyFillable: false, - sellTokenBalance: 'erc20', - buyTokenBalance: 'erc20', - signingScheme: 'eip712', - }, - from: '0x90a48d5cf7343b08da12e067680b4c6dbfe551be', - expiration: '2024-04-04T19:09:12.792412370Z', - id: 474006349, - verified: false, - } mockedCowService.post.mockReturnValue( Promise.resolve( - Ok({ data: cowSwapQuoteResponse } as unknown as AxiosResponse), + Ok({ data: cowswapQuoteResponse } as unknown as AxiosResponse), ), ) const actual = await cowApi.getUnsignedEvmMessage!({ diff --git a/packages/swapper/src/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.test.ts b/packages/swapper/src/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.test.ts index 316221c3ee5..8e4dc7ef4d6 100644 --- a/packages/swapper/src/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.test.ts +++ b/packages/swapper/src/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.test.ts @@ -167,6 +167,18 @@ const expectedTradeQuoteWethToFox: TradeQuote = { buyAsset: FOX_MAINNET, sellAsset: WETH, accountNumber: 0, + cowswapQuoteResponse: { + id: 123, + quote: { + ...expectedApiInputWethToFox, + sellAmountBeforeFee: undefined, + sellAmount: '985442057341242012', + buyAmount: '14707533959600717283163', + feeAmount: '14557942658757988', + sellTokenBalance: CoWSwapSellTokenSource.ERC20, + buyTokenBalance: CoWSwapBuyTokenDestination.ERC20, + }, + } as unknown as CowSwapQuoteResponse, }, ], } @@ -200,6 +212,18 @@ const expectedTradeQuoteFoxToEth: TradeQuote = { buyAsset: ETH, sellAsset: FOX_MAINNET, accountNumber: 0, + cowswapQuoteResponse: { + id: 123, + quote: { + ...expectedApiInputFoxToEth, + sellAmountBeforeFee: undefined, + sellAmount: '938195228120306016256', + buyAmount: '46868859830863283', + feeAmount: '61804771879693983744', + sellTokenBalance: CoWSwapSellTokenSource.ERC20, + buyTokenBalance: CoWSwapBuyTokenDestination.ERC20, + }, + } as unknown as CowSwapQuoteResponse, }, ], } @@ -233,6 +257,18 @@ const expectedTradeQuoteUsdcToXdai: TradeQuote = { buyAsset: XDAI, sellAsset: USDC_GNOSIS, accountNumber: 0, + cowswapQuoteResponse: { + id: 123, + quote: { + ...expectedApiInputUsdcGnosisToXdai, + sellAmountBeforeFee: undefined, + sellAmount: '20998812', + buyAmount: '21005367357465608755', + feeAmount: '1188', + sellTokenBalance: CoWSwapSellTokenSource.ERC20, + buyTokenBalance: CoWSwapBuyTokenDestination.ERC20, + }, + } as unknown as CowSwapQuoteResponse, }, ], } @@ -266,6 +302,18 @@ const expectedTradeQuoteUsdcToEthArbitrum: TradeQuote = { buyAsset: ETH_ARBITRUM, sellAsset: USDC_ARBITRUM, accountNumber: 0, + cowswapQuoteResponse: { + id: 123, + quote: { + ...expectedApiInputUsdcToEthArbitrum, + sellAmountBeforeFee: undefined, + sellAmount: '492056', + buyAmount: '141649103137616', + feeAmount: '7944', + sellTokenBalance: CoWSwapSellTokenSource.ERC20, + buyTokenBalance: CoWSwapBuyTokenDestination.ERC20, + }, + } as unknown as CowSwapQuoteResponse, }, ], } @@ -299,6 +347,18 @@ const expectedTradeQuoteSmallAmountWethToFox: TradeQuote = { buyAsset: FOX_MAINNET, sellAsset: WETH, accountNumber: 0, + cowswapQuoteResponse: { + id: 123, + quote: { + ...expectedApiInputSmallAmountWethToFox, + sellAmountBeforeFee: undefined, + sellAmount: '9854420573412420', + buyAmount: '145018118182475950905', + feeAmount: '1455794265875791', + sellTokenBalance: CoWSwapSellTokenSource.ERC20, + buyTokenBalance: CoWSwapBuyTokenDestination.ERC20, + }, + } as unknown as CowSwapQuoteResponse, }, ], } From c940ce2d4fc355119ce9c232f1558fbe2abf0a81 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Thu, 14 Nov 2024 11:14:48 +0700 Subject: [PATCH 40/62] fix: lifi --- .../src/swappers/LifiSwapper/getTradeQuote/getTradeQuote.ts | 6 ++++-- .../hooks/useGetTradeQuotes/useGetTradeQuotes.tsx | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/swapper/src/swappers/LifiSwapper/getTradeQuote/getTradeQuote.ts b/packages/swapper/src/swappers/LifiSwapper/getTradeQuote/getTradeQuote.ts index bff178fe8ed..086855089f2 100644 --- a/packages/swapper/src/swappers/LifiSwapper/getTradeQuote/getTradeQuote.ts +++ b/packages/swapper/src/swappers/LifiSwapper/getTradeQuote/getTradeQuote.ts @@ -53,7 +53,6 @@ async function getTrade( supportsEIP1559, affiliateBps, potentialAffiliateBps, - quoteOrRate, } = input const slippageTolerancePercentageDecimal = @@ -247,7 +246,10 @@ async function getTrade( return { id: selectedLifiRoute.id, - receiveAddress: quoteOrRate === 'quote' ? receiveAddress : undefined, + // This isn't a mistake - with Li.Fi, we can never go with our full-on intent of rate vs. quotes. As soon as a wallet is connected, we get a *quote* + // even though we're lying and saying this is a rate. With the "rate" containing a receiveAddress, a quote will *not* be fired at pre-sign time, which + // ensures users aren't rugged with routes that aren't available anymore when going from input to confirm + receiveAddress, affiliateBps, potentialAffiliateBps, steps, diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index cacbc0ed8df..f9af5513ec3 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -359,10 +359,10 @@ export const useGetTradeQuotes = () => { if (!confirmedTradeExecution) return // We already have an executable active trade, don't rerun this or this will run forever if (activeTrade && isExecutableTradeQuote(activeTrade)) return - const swapperName = activeQuoteMetaRef.current?.swapperName - if (!swapperName) return + const identifier = activeQuoteMetaRef.current?.identifier + if (!identifier) return if (!queryStateMeta?.data) return - const quoteData = queryStateMeta.data[swapperName] + const quoteData = queryStateMeta.data[identifier] if (!quoteData?.quote) return // Set the execution metadata to that of the previous rate so we can take over From bbf004b29ad5bd76787c8999045be29bf89dd872 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Thu, 14 Nov 2024 11:18:26 +0700 Subject: [PATCH 41/62] feat: flag --- .env.base | 2 +- .env.dev | 1 - .env.develop | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.env.base b/.env.base index 1bf65f2c9bc..32f9ccbb2fa 100644 --- a/.env.base +++ b/.env.base @@ -48,7 +48,7 @@ REACT_APP_FEATURE_FOX_PAGE_FOX_FARMING_SECTION=true REACT_APP_FEATURE_FOX_PAGE_GOVERNANCE=true REACT_APP_FEATURE_PHANTOM_WALLET=true REACT_APP_FEATURE_ZRX_PERMIT2=true -REACT_APP_FEATURE_PUBLIC_TRADE_ROUTE=false +REACT_APP_FEATURE_PUBLIC_TRADE_ROUTE=true REACT_APP_FEATURE_THOR_FREE_FEES=true REACT_APP_FEATURE_LIMIT_ORDERS=false diff --git a/.env.dev b/.env.dev index 8864a4a8316..4137b189c7b 100644 --- a/.env.dev +++ b/.env.dev @@ -1,5 +1,4 @@ # feature flags -REACT_APP_FEATURE_PUBLIC_TRADE_ROUTE=true REACT_APP_FEATURE_LIMIT_ORDERS=true # logging diff --git a/.env.develop b/.env.develop index 2c691e13604..9a628aa6a56 100644 --- a/.env.develop +++ b/.env.develop @@ -1,6 +1,5 @@ # feature flags REACT_APP_FEATURE_LIMIT_ORDERS=true -REACT_APP_FEATURE_PUBLIC_TRADE_ROUTE=true # mixpanel REACT_APP_MIXPANEL_TOKEN=1c1369f6ea23a6404bac41b42817cc4b From 3d260272482e9060fc9b5ef3172da9b9d3940d26 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 18 Nov 2024 14:29:51 +0700 Subject: [PATCH 42/62] feat: rm todo --- packages/swapper/src/swappers/ZrxSwapper/endpoints.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts b/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts index 3b160c5ce26..aae058dd898 100644 --- a/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts @@ -28,9 +28,6 @@ export const zrxApi: SwapperApi = { input: CommonTradeQuoteInput, { assertGetEvmChainAdapter, assetsById, config }: SwapperDeps, ): Promise> => { - // TODO(gomes): when we wire this up, this should consume getZrTradeQuote and we should ditch this guy - // getTradeQuote() is currently consumed at input time (for all swappers, not just ZRX) with weird Frankenstein "quote endpoint fetching ZRX rate endpoint - // but actually expecting quote input/output" logic. This is a temporary method to get the ZRX swapper working with the new swapper architecture. const tradeQuoteResult = await getZrxTradeQuote( input as GetEvmTradeQuoteInputBase, assertGetEvmChainAdapter, From 21502945042a372c202c5e715b8b824abf4bf353 Mon Sep 17 00:00:00 2001 From: reallybeard <89934888+reallybeard@users.noreply.github.com> Date: Mon, 18 Nov 2024 01:56:04 -0600 Subject: [PATCH 43/62] rate expired modal --- src/assets/translations/en/main.json | 5 ++++ .../Modals/RateChanged/RateChanged.tsx | 30 +++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index 29ec1f5fa89..02e96f939e6 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -922,6 +922,11 @@ "rates": { "tags": { "negativeRatio": "Insufficient sell amount" + }, + "rateExpired": { + "title": "Rate expired", + "body": "Your previous swap rate has expired. We've fetched a new rate and updated your quote amounts. Please review the latest details before confirming your swap.", + "cta": "Ok, got it" } }, "permit2": { diff --git a/src/components/Modals/RateChanged/RateChanged.tsx b/src/components/Modals/RateChanged/RateChanged.tsx index 82cfb726aa3..902422568b9 100644 --- a/src/components/Modals/RateChanged/RateChanged.tsx +++ b/src/components/Modals/RateChanged/RateChanged.tsx @@ -1,14 +1,40 @@ -import { Modal, ModalContent, ModalOverlay } from '@chakra-ui/react' +import { WarningIcon } from '@chakra-ui/icons' +import { + Button, + Heading, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalOverlay, + Stack, +} from '@chakra-ui/react' +import { useTranslate } from 'react-polyglot' +import { Text } from 'components/Text' import { useModal } from 'hooks/useModal/useModal' export const RateChangedModal = () => { const rateChanged = useModal('rateChanged') const { close, isOpen } = rateChanged + const translate = useTranslate() return ( - Rate changed - TODO @reallybeard oil + + + + + {translate('trade.rates.rateExpired.title')} + + + + + + + ) } From 5b28687d79156da690d5601388adde84502646a5 Mon Sep 17 00:00:00 2001 From: reallybeard <89934888+reallybeard@users.noreply.github.com> Date: Mon, 18 Nov 2024 01:56:51 -0600 Subject: [PATCH 44/62] Update RateChanged.tsx --- src/components/Modals/RateChanged/RateChanged.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Modals/RateChanged/RateChanged.tsx b/src/components/Modals/RateChanged/RateChanged.tsx index 902422568b9..811feae6d26 100644 --- a/src/components/Modals/RateChanged/RateChanged.tsx +++ b/src/components/Modals/RateChanged/RateChanged.tsx @@ -29,7 +29,7 @@ export const RateChangedModal = () => { - + From f8f2155e34bceddcb0890152b7560b6168a994ae Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:21:10 +0700 Subject: [PATCH 45/62] fix: raceish condition --- .../components/TradeInput/TradeInput.tsx | 18 ++++++++++++++++-- src/state/slices/tradeQuoteSlice/selectors.ts | 6 ++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx index f6dde8641e6..017a31679f7 100644 --- a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx @@ -1,4 +1,5 @@ import { isLedger } from '@shapeshiftoss/hdwallet-ledger' +import { isExecutableTradeQuote } from '@shapeshiftoss/swapper' import { isArbitrumBridgeTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ArbitrumBridgeSwapper/getTradeQuote/getTradeQuote' import type { ThorTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/getThorTradeQuoteOrRate/getTradeQuoteOrRate' import type { Asset } from '@shapeshiftoss/types' @@ -28,6 +29,7 @@ import { getMixPanel } from 'lib/mixpanel/mixPanelSingleton' import { MixPanelEvent } from 'lib/mixpanel/types' import { isKeplrHDWallet } from 'lib/utils' import { selectIsSnapshotApiQueriesPending, selectVotingPower } from 'state/apis/snapshot/selectors' +import type { ApiQuote } from 'state/apis/swapper/types' import { selectHasUserEnteredAmount, selectInputBuyAsset, @@ -41,15 +43,18 @@ import { import { tradeInput } from 'state/slices/tradeInputSlice/tradeInputSlice' import { selectActiveQuote, + selectActiveQuoteMeta, + selectActiveQuoteMetaOrDefault, selectBuyAmountAfterFeesCryptoPrecision, selectBuyAmountAfterFeesUserCurrency, selectFirstHop, selectIsTradeQuoteRequestAborted, selectIsUnsafeActiveQuote, selectShouldShowTradeQuoteOrAwaitInput, + selectSortedTradeQuotes, } from 'state/slices/tradeQuoteSlice/selectors' import { tradeQuoteSlice } from 'state/slices/tradeQuoteSlice/tradeQuoteSlice' -import { useAppDispatch, useAppSelector } from 'state/store' +import { store, useAppDispatch, useAppSelector } from 'state/store' import { useAccountIds } from '../../hooks/useAccountIds' import { SharedTradeInput } from '../SharedTradeInput/SharedTradeInput' @@ -99,6 +104,7 @@ export const TradeInput = ({ isCompact, tradeInputRef, onChangeTab }: TradeInput const [shouldShowArbitrumBridgeAcknowledgement, setShouldShowArbitrumBridgeAcknowledgement] = useState(false) + const activeQuoteMeta = useAppSelector(selectActiveQuoteMeta) const sellAmountCryptoPrecision = useAppSelector(selectInputSellAmountCryptoPrecision) const sellAmountUserCurrency = useAppSelector(selectInputSellAmountUserCurrency) const buyAmountAfterFeesCryptoPrecision = useAppSelector(selectBuyAmountAfterFeesCryptoPrecision) @@ -149,7 +155,7 @@ export const TradeInput = ({ isCompact, tradeInputRef, onChangeTab }: TradeInput // if we do, it means we have persisted or cached (both stale) data, which is enough to let the user continue // as we are optimistic and don't want to be waiting for a potentially very long time for the snapshot API to respond isVotingPowerLoading || - Boolean(walletId && isWalletReceiveAddressLoading), + isWalletReceiveAddressLoading, [ walletId, isAnyAccountMetadataLoadedForChainId, @@ -239,6 +245,13 @@ export const TradeInput = ({ isCompact, tradeInputRef, onChangeTab }: TradeInput if (!tradeQuoteStep) throw Error('missing tradeQuoteStep') if (!activeQuote) throw Error('missing activeQuote') + const bestQuote: ApiQuote | undefined = selectSortedTradeQuotes(store.getState())[0] + + // Set the best quote as activeQuoteMeta, unless user has already a custom quote selected, in which case don't override it + if (!activeQuoteMeta && bestQuote?.quote !== undefined && !bestQuote.errors.length) { + dispatch(tradeQuoteSlice.actions.setActiveQuote(bestQuote)) + } + dispatch(tradeQuoteSlice.actions.setConfirmedQuote(activeQuote)) dispatch(tradeQuoteSlice.actions.clearQuoteExecutionState(activeQuote.id)) @@ -256,6 +269,7 @@ export const TradeInput = ({ isCompact, tradeInputRef, onChangeTab }: TradeInput setIsConfirmationLoading(false) }, [ activeQuote, + activeQuoteMeta, dispatch, handleConnect, history, diff --git a/src/state/slices/tradeQuoteSlice/selectors.ts b/src/state/slices/tradeQuoteSlice/selectors.ts index 91a49fac49d..60e011543b2 100644 --- a/src/state/slices/tradeQuoteSlice/selectors.ts +++ b/src/state/slices/tradeQuoteSlice/selectors.ts @@ -62,10 +62,8 @@ import { SWAPPER_USER_ERRORS } from './constants' import type { ActiveQuoteMeta } from './types' const selectTradeQuoteSlice = (state: ReduxState) => state.tradeQuoteSlice -const selectActiveQuoteMeta: Selector = createSelector( - selectTradeQuoteSlice, - tradeQuoteSlice => tradeQuoteSlice.activeQuoteMeta, -) +export const selectActiveQuoteMeta: Selector = + createSelector(selectTradeQuoteSlice, tradeQuoteSlice => tradeQuoteSlice.activeQuoteMeta) const selectTradeQuotes = createDeepEqualOutputSelector( selectTradeQuoteSlice, From 1982311b89b9f34f15bdca667b053867cc32b4a4 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:26:35 +0700 Subject: [PATCH 46/62] feat: rm dead comment --- .../MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index f9af5513ec3..630016fea5a 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -375,8 +375,6 @@ export const useGetTradeQuotes = () => { // Set as both confirmed *and* active dispatch(tradeQuoteSlice.actions.setActiveQuote(quoteData)) dispatch(tradeQuoteSlice.actions.setConfirmedQuote(quoteData.quote)) - // And re-confirm the trade since we're effectively resetting the state machine here - // dispatch(tradeQuoteSlice.actions.confirmTrade(quoteData.quote.id)) }, [activeTrade, activeQuoteMeta, dispatch, queryStateMeta.data, confirmedTradeExecution]) // TODO: move to separate hook so we don't need to pull quote data into here From cc766f90784b6d03ff1d1eccf56514004ee3df86 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:27:16 +0700 Subject: [PATCH 47/62] fix: lint --- .../MultiHopTrade/components/TradeInput/TradeInput.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx index 017a31679f7..049adcec13e 100644 --- a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx @@ -1,5 +1,4 @@ import { isLedger } from '@shapeshiftoss/hdwallet-ledger' -import { isExecutableTradeQuote } from '@shapeshiftoss/swapper' import { isArbitrumBridgeTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ArbitrumBridgeSwapper/getTradeQuote/getTradeQuote' import type { ThorTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/getThorTradeQuoteOrRate/getTradeQuoteOrRate' import type { Asset } from '@shapeshiftoss/types' @@ -44,7 +43,6 @@ import { tradeInput } from 'state/slices/tradeInputSlice/tradeInputSlice' import { selectActiveQuote, selectActiveQuoteMeta, - selectActiveQuoteMetaOrDefault, selectBuyAmountAfterFeesCryptoPrecision, selectBuyAmountAfterFeesUserCurrency, selectFirstHop, From 8e8b80e0f787447210421395293c903c9b91fc69 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 18 Nov 2024 22:00:35 +0700 Subject: [PATCH 48/62] feat: cleanup --- .../getZrxTradeQuote/getZrxTradeQuote.test.ts | 14 +++++++------- .../getZrxTradeQuote/getZrxTradeQuote.ts | 12 ------------ 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.test.ts b/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.test.ts index d865349db8f..44c5c3418a5 100644 --- a/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.test.ts +++ b/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.test.ts @@ -12,7 +12,7 @@ import { BTC } from '../../utils/test-data/assets' import { gasFeeData } from '../../utils/test-data/fees' import { setupQuote } from '../../utils/test-data/setupSwapQuote' import { zrxServiceFactory } from '../utils/zrxService' -import { getZrxPseudoTradeQuote } from './getZrxTradeQuote' +import { getZrxTradeQuote } from './getZrxTradeQuote' const mocks = vi.hoisted(() => ({ get: vi.fn(), @@ -77,7 +77,7 @@ describe('getZrxTradeQuote', () => { } as AxiosResponse), ), ) - const maybeQuote = await getZrxPseudoTradeQuote( + const maybeQuote = await getZrxTradeQuote( quoteInput, assertGetChainAdapter, false, @@ -104,7 +104,7 @@ describe('getZrxTradeQuote', () => { >, ), ) - const maybeTradeQuote = await getZrxPseudoTradeQuote( + const maybeTradeQuote = await getZrxTradeQuote( quoteInput, assertGetChainAdapter, false, @@ -126,7 +126,7 @@ describe('getZrxTradeQuote', () => { }) as unknown as never, ) - const maybeTradeQuote = await getZrxPseudoTradeQuote( + const maybeTradeQuote = await getZrxTradeQuote( quoteInput, assertGetChainAdapter, false, @@ -149,7 +149,7 @@ describe('getZrxTradeQuote', () => { } as AxiosResponse), ), ) - const maybeQuote = await getZrxPseudoTradeQuote( + const maybeQuote = await getZrxTradeQuote( quoteInput, assertGetChainAdapter, false, @@ -169,7 +169,7 @@ describe('getZrxTradeQuote', () => { const { quoteInput } = setupQuote() vi.mocked(zrxService.get).mockReturnValue(Promise.resolve(Ok({} as AxiosResponse))) - const maybeTradeQuote = await getZrxPseudoTradeQuote( + const maybeTradeQuote = await getZrxTradeQuote( { ...quoteInput, buyAsset: BTC, @@ -195,7 +195,7 @@ describe('getZrxTradeQuote', () => { Promise.resolve(Ok({} as AxiosResponse)), ) - const maybeTradeQuote = await getZrxPseudoTradeQuote( + const maybeTradeQuote = await getZrxTradeQuote( { ...quoteInput, sellAsset: { ...sellAsset, chainId: btcChainId }, diff --git a/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts b/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts index ba29d8dee3e..cfa0b950bbc 100644 --- a/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts +++ b/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts @@ -32,18 +32,6 @@ import { } from '../utils/fetchFromZrx' import { assetIdToZrxToken, isSupportedChainId, zrxTokenToAssetId } from '../utils/helpers/helpers' -// TODO(gomes): rm me and update tests back to getZrxTradeQuote -export function getZrxPseudoTradeQuote( - input: GetEvmTradeQuoteInputBase, - assertGetEvmChainAdapter: (chainId: ChainId) => EvmChainAdapter, - isPermit2Enabled: boolean, - assetsById: AssetsByIdPartial, - zrxBaseUrl: string, -): Promise> { - if (!isPermit2Enabled) return _getZrxTradeQuote(input, assertGetEvmChainAdapter, zrxBaseUrl) - return _getZrxPermit2TradeQuote(input, assertGetEvmChainAdapter, assetsById, zrxBaseUrl) -} - export function getZrxTradeQuote( input: GetEvmTradeQuoteInputBase, assertGetEvmChainAdapter: (chainId: ChainId) => EvmChainAdapter, From ee06a4d7390be4578eaac4b1c4c45307a89a6f5a Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 18 Nov 2024 22:04:29 +0700 Subject: [PATCH 49/62] feat: leverage viem --- packages/contracts/src/contractManager.ts | 4 ++-- .../swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/contracts/src/contractManager.ts b/packages/contracts/src/contractManager.ts index 283b3058199..16861645fcb 100644 --- a/packages/contracts/src/contractManager.ts +++ b/packages/contracts/src/contractManager.ts @@ -49,7 +49,7 @@ export const getOrCreateContractByType: KnownContractByType = ({ @@ -57,7 +57,7 @@ export const getOrCreateContractByType: => { diff --git a/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts b/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts index cfa0b950bbc..c7e37ed73c0 100644 --- a/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts +++ b/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts @@ -8,6 +8,7 @@ import type { Result } from '@sniptt/monads' import { Err, Ok } from '@sniptt/monads' import type { TypedData } from 'eip-712' import { v4 as uuid } from 'uuid' +import type { Address } from 'viem' import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants' import type { @@ -126,7 +127,7 @@ async function _getZrxTradeQuote( const transactionMetadata: TradeQuoteStep['zrxTransactionMetadata'] = { to: zrxQuoteResponse.to, - data: zrxQuoteResponse.data as `0x${string}`, + data: zrxQuoteResponse.data as Address, gasPrice: zrxQuoteResponse.gasPrice ? zrxQuoteResponse.gasPrice : undefined, gas: zrxQuoteResponse.gas ? zrxQuoteResponse.gas : undefined, value: zrxQuoteResponse.value, @@ -424,7 +425,7 @@ async function _getZrxPermit2TradeQuote( const transactionMetadata: TradeQuoteStep['zrxTransactionMetadata'] = { to: transaction.to, - data: transaction.data as `0x${string}`, + data: transaction.data as Address, gasPrice: transaction.gasPrice ? transaction.gasPrice : undefined, gas: transaction.gas ? transaction.gas : undefined, value: transaction.value, From f80dff5ae7f41cd9c99fee984c8b88d1e5a159e8 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 18 Nov 2024 22:06:07 +0700 Subject: [PATCH 50/62] feat: rm --- .../components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx index b632cb1660f..220116655b4 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx @@ -100,7 +100,6 @@ export const MultiHopTradeConfirm = memo(() => { } }, [handleTradeConfirm, isModeratePriceImpact]) - console.log({ confirmedTradeExecutionState }) if (!confirmedTradeExecutionState) return null return ( From 2bacb52cada9bb2cef1193c2f8f77d48ae3328b9 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 18 Nov 2024 22:07:55 +0700 Subject: [PATCH 51/62] feat: cleanup --- src/components/MultiHopTrade/components/RateGasRow.tsx | 5 +---- .../components/TradeQuotes/components/TradeQuoteContent.tsx | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/components/MultiHopTrade/components/RateGasRow.tsx b/src/components/MultiHopTrade/components/RateGasRow.tsx index f94fe91b4d1..84160b7f50a 100644 --- a/src/components/MultiHopTrade/components/RateGasRow.tsx +++ b/src/components/MultiHopTrade/components/RateGasRow.tsx @@ -128,10 +128,7 @@ export const RateGasRow: FC = memo( {!networkFeeFiatUserCurrency ? ( - + ) : ( diff --git a/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/components/TradeQuoteContent.tsx b/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/components/TradeQuoteContent.tsx index 03fbd90a36c..b0f46836595 100644 --- a/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/components/TradeQuoteContent.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/components/TradeQuoteContent.tsx @@ -150,10 +150,7 @@ export const TradeQuoteContent = ({ { // We cannot infer gas fees in specific scenarios, so if the fee is undefined we must render is as such !networkFeeFiatUserCurrency ? ( - + ) : ( From aca6ed52f6e1db90d262f901ae1e65726eebbe0e Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 18 Nov 2024 22:09:40 +0700 Subject: [PATCH 52/62] feat: remove eslint-ignore --- .../hooks/useGetTradeQuotes/useGetTradeQuotes.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index 630016fea5a..ad149cb9fbd 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -231,15 +231,7 @@ export const useGetTradeQuotes = () => { sellAccountMetadata && receiveAddress, ), - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - hasFocus, - activeTrade, - hopExecutionMetadata?.state, - sellAccountId, - sellAccountMetadata, - receiveAddress, - ], + [hasFocus, activeTrade, isFetchStep, sellAccountId, sellAccountMetadata, receiveAddress], ) const queryFnOrSkip = useMemo(() => { From 3622358529c46f46810bc91b5ad6c7edaebcaa2b Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 18 Nov 2024 23:16:14 +0700 Subject: [PATCH 53/62] feat: jfc --- .../ArbitrumBridgeSwapper/endpoints.ts | 3 +- .../getTradeQuote/getTradeQuote.ts | 106 +--- .../getTradeRate/getTradeRate.ts | 106 ++++ .../swappers/ArbitrumBridgeSwapper/types.ts | 15 + .../swappers/ChainflipSwapper/endpoints.ts | 3 +- .../swapperApi/getTradeQuote.ts | 14 +- .../swapperApi/getTradeRate.ts | 20 + .../src/swappers/CowSwapper/endpoints.ts | 6 +- .../getCowSwapTradeQuote.ts | 137 ------ .../getCowSwapTradeRate.ts | 162 ++++++ .../src/swappers/LifiSwapper/endpoints.ts | 3 +- .../getTradeQuote/getTradeQuote.ts | 17 +- .../LifiSwapper/getTradeRate/getTradeRate.ts | 19 + .../src/swappers/PortalsSwapper/endpoints.ts | 6 +- .../getPortalsTradeQuote.ts | 159 +----- .../getPortalsTradeRate.tsx | 166 +++++++ .../swappers/ThorchainSwapper/endpoints.ts | 9 +- .../getTradeQuote.test.ts} | 4 +- .../getThorTradeQuote/getTradeQuote.ts | 109 +++++ .../getTradeQuoteOrRate.ts | 243 --------- .../getThorTradeRate/getTradeRate.ts | 106 ++++ .../src/swappers/ThorchainSwapper/types.ts | 41 ++ .../ThorchainSwapper/utils/getL1Rate.ts | 462 ++++++++++++++++++ .../utils/getL1ToLongtailQuote.ts | 149 +----- .../utils/getL1ToLongtailRate.ts | 161 ++++++ .../ThorchainSwapper/utils/getL1quote.ts | 437 +---------------- .../utils/getLongtailQuote.ts | 94 +--- .../ThorchainSwapper/utils/getLongtailRate.ts | 110 +++++ .../utils/getQuote/getQuote.ts | 2 +- .../src/swappers/ZrxSwapper/endpoints.ts | 3 +- .../getZrxTradeQuote/getZrxTradeQuote.ts | 273 +---------- .../getZrxTradeRate/getZrxTradeRate.ts | 284 +++++++++++ .../components/TradeInput/TradeInput.tsx | 2 +- src/components/MultiHopTrade/helpers.ts | 2 +- .../useGetSwapperTradeQuote.tsx | 0 .../useGetTradeQuotes/useGetTradeQuotes.tsx | 6 +- .../useGetTradeQuotes/useGetTradeRates.tsx | 6 +- .../Stake/Bridge/hooks/useRfoxBridge.ts | 2 +- .../swapper/helpers/validateTradeQuote.ts | 2 +- src/state/apis/swapper/swapperApi.ts | 2 +- 40 files changed, 1809 insertions(+), 1642 deletions(-) create mode 100644 packages/swapper/src/swappers/ArbitrumBridgeSwapper/getTradeRate/getTradeRate.ts create mode 100644 packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts create mode 100644 packages/swapper/src/swappers/CowSwapper/getCowSwapTradeRate/getCowSwapTradeRate.ts create mode 100644 packages/swapper/src/swappers/LifiSwapper/getTradeRate/getTradeRate.ts create mode 100644 packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeRate/getPortalsTradeRate.tsx rename packages/swapper/src/swappers/ThorchainSwapper/{getThorTradeQuoteOrRate/getTradeQuoteOrRate.test.ts => getThorTradeQuote/getTradeQuote.test.ts} (98%) create mode 100644 packages/swapper/src/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.ts delete mode 100644 packages/swapper/src/swappers/ThorchainSwapper/getThorTradeQuoteOrRate/getTradeQuoteOrRate.ts create mode 100644 packages/swapper/src/swappers/ThorchainSwapper/getThorTradeRate/getTradeRate.ts create mode 100644 packages/swapper/src/swappers/ThorchainSwapper/utils/getL1Rate.ts create mode 100644 packages/swapper/src/swappers/ThorchainSwapper/utils/getL1ToLongtailRate.ts create mode 100644 packages/swapper/src/swappers/ThorchainSwapper/utils/getLongtailRate.ts create mode 100644 packages/swapper/src/swappers/ZrxSwapper/getZrxTradeRate/getZrxTradeRate.ts rename src/components/MultiHopTrade/hooks/useGetTradeQuotes/{hooks.tsx => hooks}/useGetSwapperTradeQuote.tsx (100%) diff --git a/packages/swapper/src/swappers/ArbitrumBridgeSwapper/endpoints.ts b/packages/swapper/src/swappers/ArbitrumBridgeSwapper/endpoints.ts index 0e0a5646c35..14e2ca48467 100644 --- a/packages/swapper/src/swappers/ArbitrumBridgeSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/ArbitrumBridgeSwapper/endpoints.ts @@ -25,7 +25,8 @@ import type { TradeRate, } from '../../types' import { checkEvmSwapStatus, getHopByIndex, isExecutableTradeQuote } from '../../utils' -import { getTradeQuote, getTradeRate } from './getTradeQuote/getTradeQuote' +import { getTradeQuote } from './getTradeQuote/getTradeQuote' +import { getTradeRate } from './getTradeRate/getTradeRate' import { fetchArbitrumBridgeQuote } from './utils/fetchArbitrumBridgeSwap' import { assertValidTrade } from './utils/helpers' diff --git a/packages/swapper/src/swappers/ArbitrumBridgeSwapper/getTradeQuote/getTradeQuote.ts b/packages/swapper/src/swappers/ArbitrumBridgeSwapper/getTradeQuote/getTradeQuote.ts index 608a1e24ae0..309c464695a 100644 --- a/packages/swapper/src/swappers/ArbitrumBridgeSwapper/getTradeQuote/getTradeQuote.ts +++ b/packages/swapper/src/swappers/ArbitrumBridgeSwapper/getTradeQuote/getTradeQuote.ts @@ -1,5 +1,5 @@ import { ethChainId } from '@shapeshiftoss/caip' -import { type HDWallet, supportsETH } from '@shapeshiftoss/hdwallet-core' +import { supportsETH } from '@shapeshiftoss/hdwallet-core' import type { Result } from '@sniptt/monads' import { Err, Ok } from '@sniptt/monads' import { v4 as uuid } from 'uuid' @@ -7,35 +7,18 @@ import { v4 as uuid } from 'uuid' import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants' import type { GetEvmTradeQuoteInput, - GetEvmTradeQuoteInputBase, - GetEvmTradeRateInput, SingleHopTradeQuoteSteps, - SingleHopTradeRateSteps, SwapErrorRight, SwapperDeps, TradeQuote, - TradeRate, } from '../../../types' import { SwapperName, TradeQuoteError } from '../../../types' import { makeSwapErrorRight } from '../../../utils' +import type { ArbitrumBridgeTradeQuote, GetEvmTradeQuoteInputWithWallet } from '../types' import type { FetchArbitrumBridgeQuoteInput } from '../utils/fetchArbitrumBridgeSwap' -import { - fetchArbitrumBridgePrice, - fetchArbitrumBridgeQuote, -} from '../utils/fetchArbitrumBridgeSwap' +import { fetchArbitrumBridgeQuote } from '../utils/fetchArbitrumBridgeSwap' import { assertValidTrade } from '../utils/helpers' -export type GetEvmTradeQuoteInputWithWallet = Omit & { - wallet: HDWallet -} - -type ArbitrumBridgeSpecificMetadata = { - direction: 'deposit' | 'withdrawal' -} - -export type ArbitrumBridgeTradeQuote = TradeQuote & ArbitrumBridgeSpecificMetadata -export type ArbitrumBridgeTradeRate = TradeRate & ArbitrumBridgeSpecificMetadata - export const isArbitrumBridgeTradeQuote = ( quote: TradeQuote | undefined, ): quote is ArbitrumBridgeTradeQuote => !!quote && 'direction' in quote @@ -138,86 +121,3 @@ export async function getTradeQuote( ) } } - -export async function getTradeRate( - input: GetEvmTradeRateInput, - { assertGetEvmChainAdapter }: SwapperDeps, -): Promise> { - const { - chainId, - sellAsset, - buyAsset, - supportsEIP1559, - receiveAddress, - sellAmountIncludingProtocolFeesCryptoBaseUnit, - sendAddress, - } = input - - const assertion = await assertValidTrade({ buyAsset, sellAsset }) - if (assertion.isErr()) return Err(assertion.unwrapErr()) - - const isDeposit = sellAsset.chainId === ethChainId - - // 15 minutes for deposits, 7 days for withdrawals - const estimatedExecutionTimeMs = isDeposit ? 15 * 60 * 1000 : 7 * 24 * 60 * 60 * 1000 - - // 1/1 when bridging on Arbitrum bridge - const rate = '1' - - try { - const args = { - supportsEIP1559, - chainId, - buyAsset, - sellAmountIncludingProtocolFeesCryptoBaseUnit, - sellAsset, - sendAddress, - receiveAddress, - assertGetEvmChainAdapter, - quoteOrRate: 'rate', - } - const swap = await fetchArbitrumBridgePrice(args) - - const buyAmountBeforeFeesCryptoBaseUnit = sellAmountIncludingProtocolFeesCryptoBaseUnit - const buyAmountAfterFeesCryptoBaseUnit = sellAmountIncludingProtocolFeesCryptoBaseUnit - - return Ok({ - id: uuid(), - accountNumber: undefined, - receiveAddress: undefined, - affiliateBps: '0', - potentialAffiliateBps: '0', - rate, - slippageTolerancePercentageDecimal: getDefaultSlippageDecimalPercentageForSwapper( - SwapperName.ArbitrumBridge, - ), - steps: [ - { - estimatedExecutionTimeMs, - allowanceContract: swap.allowanceContract, - rate, - buyAsset, - sellAsset, - accountNumber: undefined, - buyAmountBeforeFeesCryptoBaseUnit, - buyAmountAfterFeesCryptoBaseUnit, - sellAmountIncludingProtocolFeesCryptoBaseUnit, - feeData: { - protocolFees: {}, - networkFeeCryptoBaseUnit: swap.networkFeeCryptoBaseUnit, - }, - source: SwapperName.ArbitrumBridge, - }, - ] as SingleHopTradeRateSteps, - direction: isDeposit ? ('deposit' as const) : ('withdrawal' as const), - }) - } catch (err) { - return Err( - makeSwapErrorRight({ - message: '[ArbitrumBridge: tradeQuote] - failed to get fee data', - cause: err, - code: TradeQuoteError.NetworkFeeEstimationFailed, - }), - ) - } -} diff --git a/packages/swapper/src/swappers/ArbitrumBridgeSwapper/getTradeRate/getTradeRate.ts b/packages/swapper/src/swappers/ArbitrumBridgeSwapper/getTradeRate/getTradeRate.ts new file mode 100644 index 00000000000..0a8a0d9632e --- /dev/null +++ b/packages/swapper/src/swappers/ArbitrumBridgeSwapper/getTradeRate/getTradeRate.ts @@ -0,0 +1,106 @@ +import { ethChainId } from '@shapeshiftoss/caip' +import { type HDWallet } from '@shapeshiftoss/hdwallet-core' +import type { Result } from '@sniptt/monads' +import { Err, Ok } from '@sniptt/monads' +import { v4 as uuid } from 'uuid' + +import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants' +import type { + GetEvmTradeQuoteInputBase, + GetEvmTradeRateInput, + SingleHopTradeRateSteps, + SwapErrorRight, + SwapperDeps, +} from '../../../types' +import { SwapperName, TradeQuoteError } from '../../../types' +import { makeSwapErrorRight } from '../../../utils' +import type { ArbitrumBridgeTradeRate } from '../types' +import { fetchArbitrumBridgePrice } from '../utils/fetchArbitrumBridgeSwap' +import { assertValidTrade } from '../utils/helpers' + +export type GetEvmTradeQuoteInputWithWallet = Omit & { + wallet: HDWallet +} + +export async function getTradeRate( + input: GetEvmTradeRateInput, + { assertGetEvmChainAdapter }: SwapperDeps, +): Promise> { + const { + chainId, + sellAsset, + buyAsset, + supportsEIP1559, + receiveAddress, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + sendAddress, + } = input + + const assertion = await assertValidTrade({ buyAsset, sellAsset }) + if (assertion.isErr()) return Err(assertion.unwrapErr()) + + const isDeposit = sellAsset.chainId === ethChainId + + // 15 minutes for deposits, 7 days for withdrawals + const estimatedExecutionTimeMs = isDeposit ? 15 * 60 * 1000 : 7 * 24 * 60 * 60 * 1000 + + // 1/1 when bridging on Arbitrum bridge + const rate = '1' + + try { + const args = { + supportsEIP1559, + chainId, + buyAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + sellAsset, + sendAddress, + receiveAddress, + assertGetEvmChainAdapter, + quoteOrRate: 'rate', + } + const swap = await fetchArbitrumBridgePrice(args) + + const buyAmountBeforeFeesCryptoBaseUnit = sellAmountIncludingProtocolFeesCryptoBaseUnit + const buyAmountAfterFeesCryptoBaseUnit = sellAmountIncludingProtocolFeesCryptoBaseUnit + + return Ok({ + id: uuid(), + accountNumber: undefined, + receiveAddress: undefined, + affiliateBps: '0', + potentialAffiliateBps: '0', + rate, + slippageTolerancePercentageDecimal: getDefaultSlippageDecimalPercentageForSwapper( + SwapperName.ArbitrumBridge, + ), + steps: [ + { + estimatedExecutionTimeMs, + allowanceContract: swap.allowanceContract, + rate, + buyAsset, + sellAsset, + accountNumber: undefined, + buyAmountBeforeFeesCryptoBaseUnit, + buyAmountAfterFeesCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + feeData: { + protocolFees: {}, + networkFeeCryptoBaseUnit: swap.networkFeeCryptoBaseUnit, + }, + source: SwapperName.ArbitrumBridge, + }, + ] as SingleHopTradeRateSteps, + direction: isDeposit ? ('deposit' as const) : ('withdrawal' as const), + }) + } catch (err) { + return Err( + makeSwapErrorRight({ + message: '[ArbitrumBridge: tradeQuote] - failed to get fee data', + cause: err, + code: TradeQuoteError.NetworkFeeEstimationFailed, + }), + ) + } +} diff --git a/packages/swapper/src/swappers/ArbitrumBridgeSwapper/types.ts b/packages/swapper/src/swappers/ArbitrumBridgeSwapper/types.ts index 9968c2544fe..fcf6fcbd9d2 100644 --- a/packages/swapper/src/swappers/ArbitrumBridgeSwapper/types.ts +++ b/packages/swapper/src/swappers/ArbitrumBridgeSwapper/types.ts @@ -1,6 +1,21 @@ +import type { HDWallet } from '@shapeshiftoss/hdwallet-core' + +import type { GetEvmTradeQuoteInputBase, TradeQuote, TradeRate } from '../../types' + export enum BRIDGE_TYPE { ETH_DEPOSIT = 'ETH Deposit', ERC20_DEPOSIT = 'ERC20 Deposit', ETH_WITHDRAWAL = 'ETH Withdrawal', ERC20_WITHDRAWAL = 'ERC20 Withdrawal', } + +export type GetEvmTradeQuoteInputWithWallet = Omit & { + wallet: HDWallet +} + +type ArbitrumBridgeSpecificMetadata = { + direction: 'deposit' | 'withdrawal' +} + +export type ArbitrumBridgeTradeQuote = TradeQuote & ArbitrumBridgeSpecificMetadata +export type ArbitrumBridgeTradeRate = TradeRate & ArbitrumBridgeSpecificMetadata diff --git a/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts b/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts index 25350ee68df..b68e0924ce8 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts @@ -18,7 +18,8 @@ import type { import { isExecutableTradeQuote, isExecutableTradeStep, isToken } from '../../utils' import { CHAINFLIP_BAAS_COMMISSION } from './constants' import type { ChainflipBaasSwapDepositAddress } from './models/ChainflipBaasSwapDepositAddress' -import { getTradeQuote, getTradeRate } from './swapperApi/getTradeQuote' +import { getTradeQuote } from './swapperApi/getTradeQuote' +import { getTradeRate } from './swapperApi/getTradeRate' import type { ChainFlipStatus } from './types' import { chainflipService } from './utils/chainflipService' import { getLatestChainflipStatusMessage } from './utils/getLatestChainflipStatusMessage' diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts index 9b2235f6cce..28f3cfecbdf 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts @@ -10,12 +10,10 @@ import { v4 as uuid } from 'uuid' import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants' import type { CommonTradeQuoteInput, - GetTradeRateInput, GetUtxoTradeQuoteInput, SwapErrorRight, SwapperDeps, TradeQuote, - TradeRate, } from '../../../types' import { type GetEvmTradeQuoteInput, @@ -40,7 +38,7 @@ import { getEvmTxFees } from '../utils/getEvmTxFees' import { getUtxoTxFees } from '../utils/getUtxoTxFees' import { getChainFlipIdFromAssetId, isSupportedAssetId, isSupportedChainId } from '../utils/helpers' -const _getTradeQuote = async ( +export const _getTradeQuote = async ( input: CommonTradeQuoteInput, deps: SwapperDeps, ): Promise> => { @@ -343,16 +341,6 @@ const _getTradeQuote = async ( return Ok(quotes) } -// This isn't a mistake. A trade rate *is* a trade quote. Chainflip doesn't really have a notion of a trade quote, -// they do have a notion of a "swap" (which we effectively only use to get the deposit address), which is irrelevant to the notion of quote vs. rate -export const getTradeRate = async ( - input: GetTradeRateInput, - deps: SwapperDeps, -): Promise> => { - const rates = await _getTradeQuote(input as unknown as CommonTradeQuoteInput, deps) - return rates as Result -} - export const getTradeQuote = async ( input: CommonTradeQuoteInput, deps: SwapperDeps, diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts new file mode 100644 index 00000000000..0add3599c5a --- /dev/null +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts @@ -0,0 +1,20 @@ +import type { Result } from '@sniptt/monads' + +import type { + CommonTradeQuoteInput, + GetTradeRateInput, + SwapErrorRight, + SwapperDeps, + TradeRate, +} from '../../../types' +import { _getTradeQuote } from './getTradeQuote' + +// This isn't a mistake. A trade rate *is* a trade quote. Chainflip doesn't really have a notion of a trade quote, +// they do have a notion of a "swap" (which we effectively only use to get the deposit address), which is irrelevant to the notion of quote vs. rate +export const getTradeRate = async ( + input: GetTradeRateInput, + deps: SwapperDeps, +): Promise> => { + const rates = await _getTradeQuote(input as unknown as CommonTradeQuoteInput, deps) + return rates as Result +} diff --git a/packages/swapper/src/swappers/CowSwapper/endpoints.ts b/packages/swapper/src/swappers/CowSwapper/endpoints.ts index fbff3ca17b7..a0b47d858c7 100644 --- a/packages/swapper/src/swappers/CowSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/CowSwapper/endpoints.ts @@ -25,10 +25,8 @@ import { getHopByIndex, isExecutableTradeQuote, } from '../../utils' -import { - getCowSwapTradeQuote, - getCowSwapTradeRate, -} from './getCowSwapTradeQuote/getCowSwapTradeQuote' +import { getCowSwapTradeQuote } from './getCowSwapTradeQuote/getCowSwapTradeQuote' +import { getCowSwapTradeRate } from './getCowSwapTradeRate/getCowSwapTradeRate' import type { CowSwapOrder } from './types' import { CoWSwapBuyTokenDestination, diff --git a/packages/swapper/src/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.ts b/packages/swapper/src/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.ts index e0859869e27..d4be6416d93 100644 --- a/packages/swapper/src/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.ts +++ b/packages/swapper/src/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.ts @@ -8,11 +8,9 @@ import { zeroAddress } from 'viem' import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants' import type { GetEvmTradeQuoteInputBase, - GetEvmTradeRateInput, SwapErrorRight, SwapperConfig, TradeQuote, - TradeRate, } from '../../../types' import { SwapperName } from '../../../types' import { createTradeAmountTooSmallErr } from '../../../utils' @@ -163,142 +161,7 @@ async function _getCowSwapTradeQuote( return Ok(quote) } -async function _getCowSwapTradeRate( - input: GetEvmTradeRateInput, - config: SwapperConfig, -): Promise> { - const { - sellAsset, - buyAsset, - accountNumber, - chainId, - receiveAddress, - sellAmountIncludingProtocolFeesCryptoBaseUnit, - potentialAffiliateBps, - affiliateBps, - } = input - - const slippageTolerancePercentageDecimal = - input.slippageTolerancePercentageDecimal ?? - getDefaultSlippageDecimalPercentageForSwapper(SwapperName.CowSwap) - - const assertion = assertValidTrade({ - buyAsset, - sellAsset, - supportedChainIds: SUPPORTED_CHAIN_IDS, - }) - if (assertion.isErr()) return Err(assertion.unwrapErr()) - - const buyToken = !isNativeEvmAsset(buyAsset.assetId) - ? fromAssetId(buyAsset.assetId).assetReference - : COW_SWAP_NATIVE_ASSET_MARKER_ADDRESS - - const maybeNetwork = getCowswapNetwork(chainId) - if (maybeNetwork.isErr()) return Err(maybeNetwork.unwrapErr()) - - const network = maybeNetwork.unwrap() - - const affiliateAppDataFragment = getAffiliateAppDataFragmentByChainId({ - affiliateBps, - chainId: sellAsset.chainId, - }) - - const { appData, appDataHash } = await getFullAppData( - slippageTolerancePercentageDecimal, - affiliateAppDataFragment, - 'market', - ) - - // https://api.cow.fi/docs/#/default/post_api_v1_quote - const maybeQuoteResponse = await cowService.post( - `${config.REACT_APP_COWSWAP_BASE_URL}/${network}/api/v1/quote/`, - { - sellToken: fromAssetId(sellAsset.assetId).assetReference, - buyToken, - receiver: receiveAddress, - validTo: getNowPlusThirtyMinutesTimestamp(), - appData, - appDataHash, - partiallyFillable: false, - from: zeroAddress, - kind: CoWSwapOrderKind.Sell, - sellAmountBeforeFee: sellAmountIncludingProtocolFeesCryptoBaseUnit, - }, - ) - - if (maybeQuoteResponse.isErr()) { - const err = maybeQuoteResponse.unwrapErr() - const errData = (err.cause as AxiosError)?.response?.data - if ( - (err.cause as AxiosError)?.isAxiosError && - errData?.errorType === 'SellAmountDoesNotCoverFee' - ) { - return Err( - createTradeAmountTooSmallErr({ - assetId: sellAsset.assetId, - minAmountCryptoBaseUnit: bn(errData?.data.fee_amount ?? '0x0', 16).toFixed(), - }), - ) - } - return Err(maybeQuoteResponse.unwrapErr()) - } - - const { data } = maybeQuoteResponse.unwrap() - - const { feeAmount: feeAmountInSellTokenCryptoBaseUnit } = data.quote - - const { rate, buyAmountAfterFeesCryptoBaseUnit, buyAmountBeforeFeesCryptoBaseUnit } = - getValuesFromQuoteResponse({ - buyAsset, - sellAsset, - response: data, - affiliateBps, - }) - - const quote: TradeRate = { - id: data.id.toString(), - accountNumber, - receiveAddress: undefined, - affiliateBps, - potentialAffiliateBps, - rate, - slippageTolerancePercentageDecimal, - steps: [ - { - estimatedExecutionTimeMs: undefined, - allowanceContract: COW_SWAP_VAULT_RELAYER_ADDRESS, - rate, - feeData: { - networkFeeCryptoBaseUnit: '0', // no miner fee for CowSwap - protocolFees: { - [sellAsset.assetId]: { - amountCryptoBaseUnit: feeAmountInSellTokenCryptoBaseUnit, - // Technically does, but we deduct it off the sell amount - requiresBalance: false, - asset: sellAsset, - }, - }, - }, - sellAmountIncludingProtocolFeesCryptoBaseUnit, - buyAmountBeforeFeesCryptoBaseUnit, - buyAmountAfterFeesCryptoBaseUnit, - source: SwapperName.CowSwap, - buyAsset, - sellAsset, - accountNumber, - }, - ], - } - - return Ok(quote) -} - export const getCowSwapTradeQuote = ( input: GetEvmTradeQuoteInputBase, config: SwapperConfig, ): Promise> => _getCowSwapTradeQuote(input, config) - -export const getCowSwapTradeRate = ( - input: GetEvmTradeRateInput, - config: SwapperConfig, -): Promise> => _getCowSwapTradeRate(input, config) diff --git a/packages/swapper/src/swappers/CowSwapper/getCowSwapTradeRate/getCowSwapTradeRate.ts b/packages/swapper/src/swappers/CowSwapper/getCowSwapTradeRate/getCowSwapTradeRate.ts new file mode 100644 index 00000000000..442ddc9dcee --- /dev/null +++ b/packages/swapper/src/swappers/CowSwapper/getCowSwapTradeRate/getCowSwapTradeRate.ts @@ -0,0 +1,162 @@ +import { fromAssetId } from '@shapeshiftoss/caip' +import { bn } from '@shapeshiftoss/utils' +import type { Result } from '@sniptt/monads' +import { Err, Ok } from '@sniptt/monads' +import type { AxiosError } from 'axios' +import { zeroAddress } from 'viem' + +import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants' +import type { GetEvmTradeRateInput, SwapErrorRight, SwapperConfig, TradeRate } from '../../../types' +import { SwapperName } from '../../../types' +import { createTradeAmountTooSmallErr } from '../../../utils' +import { isNativeEvmAsset } from '../../utils/helpers/helpers' +import { CoWSwapOrderKind, type CowSwapQuoteError, type CowSwapQuoteResponse } from '../types' +import { + COW_SWAP_NATIVE_ASSET_MARKER_ADDRESS, + COW_SWAP_VAULT_RELAYER_ADDRESS, + SUPPORTED_CHAIN_IDS, +} from '../utils/constants' +import { cowService } from '../utils/cowService' +import { + assertValidTrade, + getAffiliateAppDataFragmentByChainId, + getCowswapNetwork, + getFullAppData, + getNowPlusThirtyMinutesTimestamp, + getValuesFromQuoteResponse, +} from '../utils/helpers/helpers' + +async function _getCowSwapTradeRate( + input: GetEvmTradeRateInput, + config: SwapperConfig, +): Promise> { + const { + sellAsset, + buyAsset, + accountNumber, + chainId, + receiveAddress, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + potentialAffiliateBps, + affiliateBps, + } = input + + const slippageTolerancePercentageDecimal = + input.slippageTolerancePercentageDecimal ?? + getDefaultSlippageDecimalPercentageForSwapper(SwapperName.CowSwap) + + const assertion = assertValidTrade({ + buyAsset, + sellAsset, + supportedChainIds: SUPPORTED_CHAIN_IDS, + }) + if (assertion.isErr()) return Err(assertion.unwrapErr()) + + const buyToken = !isNativeEvmAsset(buyAsset.assetId) + ? fromAssetId(buyAsset.assetId).assetReference + : COW_SWAP_NATIVE_ASSET_MARKER_ADDRESS + + const maybeNetwork = getCowswapNetwork(chainId) + if (maybeNetwork.isErr()) return Err(maybeNetwork.unwrapErr()) + + const network = maybeNetwork.unwrap() + + const affiliateAppDataFragment = getAffiliateAppDataFragmentByChainId({ + affiliateBps, + chainId: sellAsset.chainId, + }) + + const { appData, appDataHash } = await getFullAppData( + slippageTolerancePercentageDecimal, + affiliateAppDataFragment, + 'market', + ) + + // https://api.cow.fi/docs/#/default/post_api_v1_quote + const maybeQuoteResponse = await cowService.post( + `${config.REACT_APP_COWSWAP_BASE_URL}/${network}/api/v1/quote/`, + { + sellToken: fromAssetId(sellAsset.assetId).assetReference, + buyToken, + receiver: receiveAddress, + validTo: getNowPlusThirtyMinutesTimestamp(), + appData, + appDataHash, + partiallyFillable: false, + from: zeroAddress, + kind: CoWSwapOrderKind.Sell, + sellAmountBeforeFee: sellAmountIncludingProtocolFeesCryptoBaseUnit, + }, + ) + + if (maybeQuoteResponse.isErr()) { + const err = maybeQuoteResponse.unwrapErr() + const errData = (err.cause as AxiosError)?.response?.data + if ( + (err.cause as AxiosError)?.isAxiosError && + errData?.errorType === 'SellAmountDoesNotCoverFee' + ) { + return Err( + createTradeAmountTooSmallErr({ + assetId: sellAsset.assetId, + minAmountCryptoBaseUnit: bn(errData?.data.fee_amount ?? '0x0', 16).toFixed(), + }), + ) + } + return Err(maybeQuoteResponse.unwrapErr()) + } + + const { data } = maybeQuoteResponse.unwrap() + + const { feeAmount: feeAmountInSellTokenCryptoBaseUnit } = data.quote + + const { rate, buyAmountAfterFeesCryptoBaseUnit, buyAmountBeforeFeesCryptoBaseUnit } = + getValuesFromQuoteResponse({ + buyAsset, + sellAsset, + response: data, + affiliateBps, + }) + + const quote: TradeRate = { + id: data.id.toString(), + accountNumber, + receiveAddress: undefined, + affiliateBps, + potentialAffiliateBps, + rate, + slippageTolerancePercentageDecimal, + steps: [ + { + estimatedExecutionTimeMs: undefined, + allowanceContract: COW_SWAP_VAULT_RELAYER_ADDRESS, + rate, + feeData: { + networkFeeCryptoBaseUnit: '0', // no miner fee for CowSwap + protocolFees: { + [sellAsset.assetId]: { + amountCryptoBaseUnit: feeAmountInSellTokenCryptoBaseUnit, + // Technically does, but we deduct it off the sell amount + requiresBalance: false, + asset: sellAsset, + }, + }, + }, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + buyAmountBeforeFeesCryptoBaseUnit, + buyAmountAfterFeesCryptoBaseUnit, + source: SwapperName.CowSwap, + buyAsset, + sellAsset, + accountNumber, + }, + ], + } + + return Ok(quote) +} + +export const getCowSwapTradeRate = ( + input: GetEvmTradeRateInput, + config: SwapperConfig, +): Promise> => _getCowSwapTradeRate(input, config) diff --git a/packages/swapper/src/swappers/LifiSwapper/endpoints.ts b/packages/swapper/src/swappers/LifiSwapper/endpoints.ts index a4c920e5c63..be6ff384edd 100644 --- a/packages/swapper/src/swappers/LifiSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/LifiSwapper/endpoints.ts @@ -25,7 +25,8 @@ import { isExecutableTradeQuote, makeSwapErrorRight, } from '../../utils' -import { getTradeQuote, getTradeRate } from './getTradeQuote/getTradeQuote' +import { getTradeQuote } from './getTradeQuote/getTradeQuote' +import { getTradeRate } from './getTradeRate/getTradeRate' import { configureLiFi } from './utils/configureLiFi' import { getLifiChainMap } from './utils/getLifiChainMap' diff --git a/packages/swapper/src/swappers/LifiSwapper/getTradeQuote/getTradeQuote.ts b/packages/swapper/src/swappers/LifiSwapper/getTradeQuote/getTradeQuote.ts index 086855089f2..cc9258d7e56 100644 --- a/packages/swapper/src/swappers/LifiSwapper/getTradeQuote/getTradeQuote.ts +++ b/packages/swapper/src/swappers/LifiSwapper/getTradeQuote/getTradeQuote.ts @@ -16,7 +16,6 @@ import { Err, Ok } from '@sniptt/monads' import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants' import type { GetEvmTradeQuoteInputBase, - GetEvmTradeRateInput, MultiHopTradeQuoteSteps, SingleHopTradeQuoteSteps, SwapperDeps, @@ -36,9 +35,9 @@ import { getLifiEvmAssetAddress } from '../utils/getLifiEvmAssetAddress/getLifiE import { getNetworkFeeCryptoBaseUnit } from '../utils/getNetworkFeeCryptoBaseUnit/getNetworkFeeCryptoBaseUnit' import { lifiTokenToAsset } from '../utils/lifiTokenToAsset/lifiTokenToAsset' import { transformLifiStepFeeData } from '../utils/transformLifiFeeData/transformLifiFeeData' -import type { LifiTradeQuote, LifiTradeRate } from '../utils/types' +import type { LifiTradeQuote } from '../utils/types' -async function getTrade( +export async function getTrade( input: GetEvmTradeQuoteInput, deps: SwapperDeps, lifiChainMap: Map, @@ -299,15 +298,3 @@ export const getTradeQuote = ( deps: SwapperDeps, lifiChainMap: Map, ): Promise> => getTrade(input, deps, lifiChainMap) - -export const getTradeRate = async ( - input: GetEvmTradeRateInput, - deps: SwapperDeps, - lifiChainMap: Map, -): Promise> => { - const rate = (await getTrade(input, deps, lifiChainMap)) as Result< - LifiTradeRate[], - SwapErrorRight - > - return rate -} diff --git a/packages/swapper/src/swappers/LifiSwapper/getTradeRate/getTradeRate.ts b/packages/swapper/src/swappers/LifiSwapper/getTradeRate/getTradeRate.ts new file mode 100644 index 00000000000..99bcdd2b592 --- /dev/null +++ b/packages/swapper/src/swappers/LifiSwapper/getTradeRate/getTradeRate.ts @@ -0,0 +1,19 @@ +import type { ChainKey } from '@lifi/sdk' +import type { ChainId } from '@shapeshiftoss/caip' +import type { Result } from '@sniptt/monads' + +import type { GetEvmTradeRateInput, SwapErrorRight, SwapperDeps } from '../../../types' +import { getTrade } from '../getTradeQuote/getTradeQuote' +import type { LifiTradeRate } from '../utils/types' + +export const getTradeRate = async ( + input: GetEvmTradeRateInput, + deps: SwapperDeps, + lifiChainMap: Map, +): Promise> => { + const rate = (await getTrade(input, deps, lifiChainMap)) as Result< + LifiTradeRate[], + SwapErrorRight + > + return rate +} diff --git a/packages/swapper/src/swappers/PortalsSwapper/endpoints.ts b/packages/swapper/src/swappers/PortalsSwapper/endpoints.ts index b046bfc197b..a77ee0a7a97 100644 --- a/packages/swapper/src/swappers/PortalsSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/PortalsSwapper/endpoints.ts @@ -17,10 +17,8 @@ import type { TradeRate, } from '../../types' import { checkEvmSwapStatus, isExecutableTradeQuote } from '../../utils' -import { - getPortalsTradeQuote, - getPortalsTradeRate, -} from './getPortalsTradeQuote/getPortalsTradeQuote' +import { getPortalsTradeQuote } from './getPortalsTradeQuote/getPortalsTradeQuote' +import { getPortalsTradeRate } from './getPortalsTradeRate/getPortalsTradeRate' export const portalsApi: SwapperApi = { getTradeQuote: async ( diff --git a/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts b/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts index b5a5713aa55..2495ab5e36c 100644 --- a/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts +++ b/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts @@ -3,20 +3,12 @@ import { fromAssetId } from '@shapeshiftoss/caip' import type { EvmChainAdapter } from '@shapeshiftoss/chain-adapters' import { evm } from '@shapeshiftoss/chain-adapters' import type { KnownChainIds } from '@shapeshiftoss/types' -import { bn, bnOrZero, convertBasisPointsToDecimalPercentage } from '@shapeshiftoss/utils' +import { bnOrZero, convertBasisPointsToDecimalPercentage } from '@shapeshiftoss/utils' import type { Result } from '@sniptt/monads' import { Err, Ok } from '@sniptt/monads' -import { v4 as uuid } from 'uuid' import { zeroAddress } from 'viem' -import { getDefaultSlippageDecimalPercentageForSwapper } from '../../..' -import type { - GetEvmTradeQuoteInputBase, - GetEvmTradeRateInput, - SingleHopTradeRateSteps, - SwapperConfig, - TradeRate, -} from '../../../types' +import type { GetEvmTradeQuoteInputBase, SwapperConfig } from '../../../types' import { type SingleHopTradeQuoteSteps, type SwapErrorRight, @@ -27,151 +19,8 @@ import { import { getRate, makeSwapErrorRight } from '../../../utils' import { getTreasuryAddressFromChainId, isNativeEvmAsset } from '../../utils/helpers/helpers' import { chainIdToPortalsNetwork } from '../constants' -import { fetchPortalsTradeEstimate, fetchPortalsTradeOrder } from '../utils/fetchPortalsTradeOrder' -import { getPortalsRouterAddressByChainId, isSupportedChainId } from '../utils/helpers' - -export async function getPortalsTradeRate( - input: GetEvmTradeRateInput, - _assertGetEvmChainAdapter: (chainId: ChainId) => EvmChainAdapter, - swapperConfig: SwapperConfig, -): Promise> { - const { - sellAsset, - buyAsset, - accountNumber, - affiliateBps, - potentialAffiliateBps, - chainId, - sellAmountIncludingProtocolFeesCryptoBaseUnit, - } = input - - const sellAssetChainId = sellAsset.chainId - const buyAssetChainId = buyAsset.chainId - - if (!isSupportedChainId(sellAssetChainId)) { - return Err( - makeSwapErrorRight({ - message: `unsupported chainId`, - code: TradeQuoteError.UnsupportedChain, - details: { chainId: sellAsset.chainId }, - }), - ) - } - - if (!isSupportedChainId(buyAssetChainId)) { - return Err( - makeSwapErrorRight({ - message: `unsupported chainId`, - code: TradeQuoteError.UnsupportedChain, - details: { chainId: sellAsset.chainId }, - }), - ) - } - - if (sellAssetChainId !== buyAssetChainId) { - return Err( - makeSwapErrorRight({ - message: `cross-chain not supported - both assets must be on chainId ${sellAsset.chainId}`, - code: TradeQuoteError.CrossChainNotSupported, - details: { buyAsset, sellAsset }, - }), - ) - } - - try { - if (!isSupportedChainId(chainId)) throw new Error(`Unsupported chainId ${sellAsset.chainId}`) - - const portalsNetwork = chainIdToPortalsNetwork[chainId as KnownChainIds] - - if (!portalsNetwork) { - return Err( - makeSwapErrorRight({ - message: `unsupported ChainId`, - code: TradeQuoteError.UnsupportedChain, - details: { chainId: input.chainId }, - }), - ) - } - - const sellAssetAddress = isNativeEvmAsset(sellAsset.assetId) - ? zeroAddress - : fromAssetId(sellAsset.assetId).assetReference - const buyAssetAddress = isNativeEvmAsset(buyAsset.assetId) - ? zeroAddress - : fromAssetId(buyAsset.assetId).assetReference - - const inputToken = `${portalsNetwork}:${sellAssetAddress}` - const outputToken = `${portalsNetwork}:${buyAssetAddress}` - - const userSlippageTolerancePercentageDecimalOrDefault = input.slippageTolerancePercentageDecimal - ? Number(input.slippageTolerancePercentageDecimal) - : undefined // Use auto slippage if no user preference is provided - - const quoteEstimateResponse = await fetchPortalsTradeEstimate({ - inputToken, - outputToken, - inputAmount: sellAmountIncludingProtocolFeesCryptoBaseUnit, - slippageTolerancePercentage: userSlippageTolerancePercentageDecimalOrDefault - ? userSlippageTolerancePercentageDecimalOrDefault * 100 - : bnOrZero(getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Portals)) - .times(100) - .toNumber(), - swapperConfig, - }) - // Use the quote estimate endpoint to get a quote without a wallet - - const rate = getRate({ - sellAmountCryptoBaseUnit: input.sellAmountIncludingProtocolFeesCryptoBaseUnit, - buyAmountCryptoBaseUnit: quoteEstimateResponse?.context.outputAmount, - sellAsset, - buyAsset, - }) - - const allowanceContract = getPortalsRouterAddressByChainId(chainId) - - const tradeRate = { - id: uuid(), - accountNumber, - receiveAddress: undefined, - affiliateBps, - potentialAffiliateBps, - rate, - slippageTolerancePercentageDecimal: quoteEstimateResponse.context.slippageTolerancePercentage - ? bn(quoteEstimateResponse.context.slippageTolerancePercentage).div(100).toString() - : undefined, - steps: [ - { - estimatedExecutionTimeMs: undefined, // Portals doesn't provide this info - allowanceContract, - accountNumber, - rate, - buyAsset, - sellAsset, - buyAmountBeforeFeesCryptoBaseUnit: quoteEstimateResponse.minOutputAmount, - buyAmountAfterFeesCryptoBaseUnit: quoteEstimateResponse.context.outputAmount, - sellAmountIncludingProtocolFeesCryptoBaseUnit: - input.sellAmountIncludingProtocolFeesCryptoBaseUnit, - feeData: { - networkFeeCryptoBaseUnit: undefined, - // Protocol fees are always denominated in sell asset here - protocolFees: {}, - }, - source: SwapperName.Portals, - }, - ] as unknown as SingleHopTradeRateSteps, - } - - return Ok(tradeRate) - } catch (err) { - return Err( - makeSwapErrorRight({ - message: 'failed to get Portals quote', - cause: err, - code: TradeQuoteError.NetworkFeeEstimationFailed, - }), - ) - } -} +import { fetchPortalsTradeOrder } from '../utils/fetchPortalsTradeOrder' +import { isSupportedChainId } from '../utils/helpers' export async function getPortalsTradeQuote( input: GetEvmTradeQuoteInputBase, diff --git a/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeRate/getPortalsTradeRate.tsx b/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeRate/getPortalsTradeRate.tsx new file mode 100644 index 00000000000..1b88e6bdcab --- /dev/null +++ b/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeRate/getPortalsTradeRate.tsx @@ -0,0 +1,166 @@ +import type { ChainId } from '@shapeshiftoss/caip' +import { fromAssetId } from '@shapeshiftoss/caip' +import type { EvmChainAdapter } from '@shapeshiftoss/chain-adapters' +import type { KnownChainIds } from '@shapeshiftoss/types' +import { bn, bnOrZero } from '@shapeshiftoss/utils' +import type { Result } from '@sniptt/monads' +import { Err, Ok } from '@sniptt/monads' +import { v4 as uuid } from 'uuid' +import { zeroAddress } from 'viem' + +import { getDefaultSlippageDecimalPercentageForSwapper } from '../../..' +import type { + GetEvmTradeRateInput, + SingleHopTradeRateSteps, + SwapperConfig, + TradeRate, +} from '../../../types' +import { type SwapErrorRight, SwapperName, TradeQuoteError } from '../../../types' +import { getRate, makeSwapErrorRight } from '../../../utils' +import { isNativeEvmAsset } from '../../utils/helpers/helpers' +import { chainIdToPortalsNetwork } from '../constants' +import { fetchPortalsTradeEstimate } from '../utils/fetchPortalsTradeOrder' +import { getPortalsRouterAddressByChainId, isSupportedChainId } from '../utils/helpers' + +export async function getPortalsTradeRate( + input: GetEvmTradeRateInput, + _assertGetEvmChainAdapter: (chainId: ChainId) => EvmChainAdapter, + swapperConfig: SwapperConfig, +): Promise> { + const { + sellAsset, + buyAsset, + accountNumber, + affiliateBps, + potentialAffiliateBps, + chainId, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + } = input + + const sellAssetChainId = sellAsset.chainId + const buyAssetChainId = buyAsset.chainId + + if (!isSupportedChainId(sellAssetChainId)) { + return Err( + makeSwapErrorRight({ + message: `unsupported chainId`, + code: TradeQuoteError.UnsupportedChain, + details: { chainId: sellAsset.chainId }, + }), + ) + } + + if (!isSupportedChainId(buyAssetChainId)) { + return Err( + makeSwapErrorRight({ + message: `unsupported chainId`, + code: TradeQuoteError.UnsupportedChain, + details: { chainId: sellAsset.chainId }, + }), + ) + } + + if (sellAssetChainId !== buyAssetChainId) { + return Err( + makeSwapErrorRight({ + message: `cross-chain not supported - both assets must be on chainId ${sellAsset.chainId}`, + code: TradeQuoteError.CrossChainNotSupported, + details: { buyAsset, sellAsset }, + }), + ) + } + + try { + if (!isSupportedChainId(chainId)) throw new Error(`Unsupported chainId ${sellAsset.chainId}`) + + const portalsNetwork = chainIdToPortalsNetwork[chainId as KnownChainIds] + + if (!portalsNetwork) { + return Err( + makeSwapErrorRight({ + message: `unsupported ChainId`, + code: TradeQuoteError.UnsupportedChain, + details: { chainId: input.chainId }, + }), + ) + } + + const sellAssetAddress = isNativeEvmAsset(sellAsset.assetId) + ? zeroAddress + : fromAssetId(sellAsset.assetId).assetReference + const buyAssetAddress = isNativeEvmAsset(buyAsset.assetId) + ? zeroAddress + : fromAssetId(buyAsset.assetId).assetReference + + const inputToken = `${portalsNetwork}:${sellAssetAddress}` + const outputToken = `${portalsNetwork}:${buyAssetAddress}` + + const userSlippageTolerancePercentageDecimalOrDefault = input.slippageTolerancePercentageDecimal + ? Number(input.slippageTolerancePercentageDecimal) + : undefined // Use auto slippage if no user preference is provided + + const quoteEstimateResponse = await fetchPortalsTradeEstimate({ + inputToken, + outputToken, + inputAmount: sellAmountIncludingProtocolFeesCryptoBaseUnit, + slippageTolerancePercentage: userSlippageTolerancePercentageDecimalOrDefault + ? userSlippageTolerancePercentageDecimalOrDefault * 100 + : bnOrZero(getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Portals)) + .times(100) + .toNumber(), + swapperConfig, + }) + // Use the quote estimate endpoint to get a quote without a wallet + + const rate = getRate({ + sellAmountCryptoBaseUnit: input.sellAmountIncludingProtocolFeesCryptoBaseUnit, + buyAmountCryptoBaseUnit: quoteEstimateResponse?.context.outputAmount, + sellAsset, + buyAsset, + }) + + const allowanceContract = getPortalsRouterAddressByChainId(chainId) + + const tradeRate = { + id: uuid(), + accountNumber, + receiveAddress: undefined, + affiliateBps, + potentialAffiliateBps, + rate, + slippageTolerancePercentageDecimal: quoteEstimateResponse.context.slippageTolerancePercentage + ? bn(quoteEstimateResponse.context.slippageTolerancePercentage).div(100).toString() + : undefined, + steps: [ + { + estimatedExecutionTimeMs: undefined, // Portals doesn't provide this info + allowanceContract, + accountNumber, + rate, + buyAsset, + sellAsset, + buyAmountBeforeFeesCryptoBaseUnit: quoteEstimateResponse.minOutputAmount, + buyAmountAfterFeesCryptoBaseUnit: quoteEstimateResponse.context.outputAmount, + sellAmountIncludingProtocolFeesCryptoBaseUnit: + input.sellAmountIncludingProtocolFeesCryptoBaseUnit, + feeData: { + networkFeeCryptoBaseUnit: undefined, + // Protocol fees are always denominated in sell asset here + protocolFees: {}, + }, + source: SwapperName.Portals, + }, + ] as unknown as SingleHopTradeRateSteps, + } + + return Ok(tradeRate) + } catch (err) { + return Err( + makeSwapErrorRight({ + message: 'failed to get Portals quote', + cause: err, + code: TradeQuoteError.NetworkFeeEstimationFailed, + }), + ) + } +} diff --git a/packages/swapper/src/swappers/ThorchainSwapper/endpoints.ts b/packages/swapper/src/swappers/ThorchainSwapper/endpoints.ts index 80ebd0d2b25..5558562aa8f 100644 --- a/packages/swapper/src/swappers/ThorchainSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/ThorchainSwapper/endpoints.ts @@ -36,12 +36,9 @@ import { import { isNativeEvmAsset } from '../utils/helpers/helpers' import { THORCHAIN_OUTBOUND_FEE_RUNE_THOR_UNIT } from './constants' import { getThorTxInfo as getEvmThorTxInfo } from './evm/utils/getThorTxData' -import { - getThorTradeQuote, - getThorTradeRate, - type ThorEvmTradeQuote, -} from './getThorTradeQuoteOrRate/getTradeQuoteOrRate' -import type { ThornodeStatusResponse, ThornodeTxResponse } from './types' +import { getThorTradeQuote } from './getThorTradeQuote/getTradeQuote' +import { getThorTradeRate } from './getThorTradeRate/getTradeRate' +import type { ThorEvmTradeQuote, ThornodeStatusResponse, ThornodeTxResponse } from './types' import { getLatestThorTxStatusMessage } from './utils/getLatestThorTxStatusMessage' import { TradeType } from './utils/longTailHelpers' import { parseThorBuyTxHash } from './utils/parseThorBuyTxHash' diff --git a/packages/swapper/src/swappers/ThorchainSwapper/getThorTradeQuoteOrRate/getTradeQuoteOrRate.test.ts b/packages/swapper/src/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts similarity index 98% rename from packages/swapper/src/swappers/ThorchainSwapper/getThorTradeQuoteOrRate/getTradeQuoteOrRate.test.ts rename to packages/swapper/src/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts index d7c86a62138..f9cb768c61a 100644 --- a/packages/swapper/src/swappers/ThorchainSwapper/getThorTradeQuoteOrRate/getTradeQuoteOrRate.test.ts +++ b/packages/swapper/src/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts @@ -11,6 +11,7 @@ import { setupQuote } from '../../utils/test-data/setupSwapQuote' import { getThorTxInfo } from '../evm/utils/getThorTxData' import type { InboundAddressResponse, + ThorEvmTradeQuote, ThornodePoolResponse, ThornodeQuoteResponseSuccess, } from '../types' @@ -18,8 +19,7 @@ import { TradeType } from '../utils/longTailHelpers' import { mockInboundAddresses, thornodePools } from '../utils/test-data/responses' import { mockEvmChainAdapter } from '../utils/test-data/setupThorswapDeps' import { thorService } from '../utils/thorService' -import type { ThorEvmTradeQuote } from './getTradeQuoteOrRate' -import { getThorTradeQuote } from './getTradeQuoteOrRate' +import { getThorTradeQuote } from './getTradeQuote' const mockedGetThorTxInfo = vi.mocked(getThorTxInfo) const mockedThorService = vi.mocked(thorService) diff --git a/packages/swapper/src/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.ts b/packages/swapper/src/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.ts new file mode 100644 index 00000000000..eb821692a9a --- /dev/null +++ b/packages/swapper/src/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.ts @@ -0,0 +1,109 @@ +import { assertUnreachable, bn } from '@shapeshiftoss/utils' +import type { Result } from '@sniptt/monads' +import { Err } from '@sniptt/monads' + +import type { CommonTradeQuoteInput, SwapErrorRight, SwapperDeps, TradeQuote } from '../../../types' +import { TradeQuoteError } from '../../../types' +import { makeSwapErrorRight } from '../../../utils' +import { buySupportedChainIds, sellSupportedChainIds } from '../constants' +import type { ThornodePoolResponse, ThorTradeQuote } from '../types' +import { getL1Quote } from '../utils/getL1quote' +import { getL1ToLongtailQuote } from '../utils/getL1ToLongtailQuote' +import { getLongtailToL1Quote } from '../utils/getLongtailQuote' +import { getTradeType, TradeType } from '../utils/longTailHelpers' +import { assetIdToPoolAssetId } from '../utils/poolAssetHelpers/poolAssetHelpers' +import { thorService } from '../utils/thorService' + +export const isThorTradeQuote = (quote: TradeQuote | undefined): quote is ThorTradeQuote => + !!quote && 'tradeType' in quote + +export const getThorTradeQuote = async ( + input: CommonTradeQuoteInput, + deps: SwapperDeps, +): Promise> => { + const thorchainSwapLongtailEnabled = deps.config.REACT_APP_FEATURE_THORCHAINSWAP_LONGTAIL + const thorchainSwapL1ToLongtailEnabled = + deps.config.REACT_APP_FEATURE_THORCHAINSWAP_L1_TO_LONGTAIL + const { sellAsset, buyAsset } = input + + if (!sellSupportedChainIds[sellAsset.chainId] || !buySupportedChainIds[buyAsset.chainId]) { + return Err( + makeSwapErrorRight({ + message: 'Unsupported chain', + code: TradeQuoteError.UnsupportedChain, + }), + ) + } + + const daemonUrl = deps.config.REACT_APP_THORCHAIN_NODE_URL + const maybePoolsResponse = await thorService.get( + `${daemonUrl}/lcd/thorchain/pools`, + ) + + if (maybePoolsResponse.isErr()) return Err(maybePoolsResponse.unwrapErr()) + + const { data: poolsResponse } = maybePoolsResponse.unwrap() + + const buyPoolId = assetIdToPoolAssetId({ assetId: buyAsset.assetId }) + const sellPoolId = assetIdToPoolAssetId({ assetId: sellAsset.assetId }) + + // If one or both of these are undefined it means we are tradeing one or more long-tail ERC20 tokens + const sellAssetPool = poolsResponse.find(pool => pool.asset === sellPoolId) + const buyAssetPool = poolsResponse.find(pool => pool.asset === buyPoolId) + + const tradeType = thorchainSwapLongtailEnabled + ? getTradeType(sellAssetPool, buyAssetPool, sellPoolId, buyPoolId) + : TradeType.L1ToL1 + if (tradeType === undefined) { + return Err( + makeSwapErrorRight({ + message: 'Unknown trade type', + code: TradeQuoteError.UnsupportedTradePair, + }), + ) + } + + if ( + (!buyPoolId && tradeType !== TradeType.L1ToLongTail) || + (!sellPoolId && tradeType !== TradeType.LongTailToL1) + ) { + return Err( + makeSwapErrorRight({ + message: 'Unsupported trade pair', + code: TradeQuoteError.UnsupportedTradePair, + }), + ) + } + + const streamingInterval = + sellAssetPool && buyAssetPool + ? (() => { + const sellAssetDepthBps = sellAssetPool.derived_depth_bps + const buyAssetDepthBps = buyAssetPool.derived_depth_bps + const swapDepthBps = bn(sellAssetDepthBps).plus(buyAssetDepthBps).div(2) + // Low health for the pools of this swap - use a longer streaming interval + if (swapDepthBps.lt(5000)) return 10 + // Moderate health for the pools of this swap - use a moderate streaming interval + if (swapDepthBps.lt(9000) && swapDepthBps.gte(5000)) return 5 + // Pool is at 90%+ health - use a 1 block streaming interval + return 1 + })() + : // TODO: One of the pools is RUNE - use the as-is 10 until we work out how best to handle this + 10 + + switch (tradeType) { + case TradeType.L1ToL1: + return getL1Quote(input, deps, streamingInterval, tradeType) + case TradeType.LongTailToL1: + return getLongtailToL1Quote(input, deps, streamingInterval) + case TradeType.L1ToLongTail: + if (!thorchainSwapL1ToLongtailEnabled) + return Err(makeSwapErrorRight({ message: 'Not implemented yet' })) + + return getL1ToLongtailQuote(input, deps, streamingInterval) + case TradeType.LongTailToLongTail: + return Err(makeSwapErrorRight({ message: 'Not implemented yet' })) + default: + assertUnreachable(tradeType) + } +} diff --git a/packages/swapper/src/swappers/ThorchainSwapper/getThorTradeQuoteOrRate/getTradeQuoteOrRate.ts b/packages/swapper/src/swappers/ThorchainSwapper/getThorTradeQuoteOrRate/getTradeQuoteOrRate.ts deleted file mode 100644 index 7a6e9af8a1c..00000000000 --- a/packages/swapper/src/swappers/ThorchainSwapper/getThorTradeQuoteOrRate/getTradeQuoteOrRate.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { assertUnreachable, bn } from '@shapeshiftoss/utils' -import type { Result } from '@sniptt/monads' -import { Err } from '@sniptt/monads' - -import type { - CommonTradeQuoteInput, - GetTradeRateInput, - SwapErrorRight, - SwapperDeps, - TradeQuote, - TradeRate, -} from '../../../types' -import { TradeQuoteError } from '../../../types' -import { makeSwapErrorRight } from '../../../utils' -import { buySupportedChainIds, sellSupportedChainIds } from '../constants' -import type { ThornodePoolResponse } from '../types' -import { getL1Quote, getL1Rate } from '../utils/getL1quote' -import { getL1ToLongtailQuote, getL1ToLongtailRate } from '../utils/getL1ToLongtailQuote' -import { getLongtailToL1Quote, getLongtailToL1Rate } from '../utils/getLongtailQuote' -import { getTradeType, TradeType } from '../utils/longTailHelpers' -import { assetIdToPoolAssetId } from '../utils/poolAssetHelpers/poolAssetHelpers' -import { thorService } from '../utils/thorService' - -type ThorTradeQuoteSpecificMetadata = { - isStreaming: boolean - memo: string - recommendedMinimumCryptoBaseUnit: string - tradeType: TradeType - expiry: number - longtailData?: { - longtailToL1ExpectedAmountOut?: bigint - L1ToLongtailExpectedAmountOut?: bigint - } -} -export type ThorEvmTradeQuote = TradeQuote & - ThorTradeQuoteSpecificMetadata & { - router: string - vault: string - aggregator?: string - data: string - tradeType: TradeType - } & { - receiveAddress: string - } - -export type ThorEvmTradeRate = TradeRate & - ThorTradeQuoteSpecificMetadata & { - router: string - vault: string - aggregator?: string - data: string - tradeType: TradeType - } - -export type ThorTradeUtxoOrCosmosQuote = TradeQuote & ThorTradeQuoteSpecificMetadata -export type ThorTradeUtxoOrCosmosRate = TradeRate & ThorTradeQuoteSpecificMetadata -export type ThorTradeQuote = ThorEvmTradeQuote | ThorTradeUtxoOrCosmosQuote -export type ThorTradeRate = ThorEvmTradeRate | ThorTradeUtxoOrCosmosRate - -export const isThorTradeQuote = (quote: TradeQuote | undefined): quote is ThorTradeQuote => - !!quote && 'tradeType' in quote - -export const getThorTradeQuote = async ( - input: CommonTradeQuoteInput, - deps: SwapperDeps, -): Promise> => { - const thorchainSwapLongtailEnabled = deps.config.REACT_APP_FEATURE_THORCHAINSWAP_LONGTAIL - const thorchainSwapL1ToLongtailEnabled = - deps.config.REACT_APP_FEATURE_THORCHAINSWAP_L1_TO_LONGTAIL - const { sellAsset, buyAsset } = input - - if (!sellSupportedChainIds[sellAsset.chainId] || !buySupportedChainIds[buyAsset.chainId]) { - return Err( - makeSwapErrorRight({ - message: 'Unsupported chain', - code: TradeQuoteError.UnsupportedChain, - }), - ) - } - - const daemonUrl = deps.config.REACT_APP_THORCHAIN_NODE_URL - const maybePoolsResponse = await thorService.get( - `${daemonUrl}/lcd/thorchain/pools`, - ) - - if (maybePoolsResponse.isErr()) return Err(maybePoolsResponse.unwrapErr()) - - const { data: poolsResponse } = maybePoolsResponse.unwrap() - - const buyPoolId = assetIdToPoolAssetId({ assetId: buyAsset.assetId }) - const sellPoolId = assetIdToPoolAssetId({ assetId: sellAsset.assetId }) - - // If one or both of these are undefined it means we are tradeing one or more long-tail ERC20 tokens - const sellAssetPool = poolsResponse.find(pool => pool.asset === sellPoolId) - const buyAssetPool = poolsResponse.find(pool => pool.asset === buyPoolId) - - const tradeType = thorchainSwapLongtailEnabled - ? getTradeType(sellAssetPool, buyAssetPool, sellPoolId, buyPoolId) - : TradeType.L1ToL1 - if (tradeType === undefined) { - return Err( - makeSwapErrorRight({ - message: 'Unknown trade type', - code: TradeQuoteError.UnsupportedTradePair, - }), - ) - } - - if ( - (!buyPoolId && tradeType !== TradeType.L1ToLongTail) || - (!sellPoolId && tradeType !== TradeType.LongTailToL1) - ) { - return Err( - makeSwapErrorRight({ - message: 'Unsupported trade pair', - code: TradeQuoteError.UnsupportedTradePair, - }), - ) - } - - const streamingInterval = - sellAssetPool && buyAssetPool - ? (() => { - const sellAssetDepthBps = sellAssetPool.derived_depth_bps - const buyAssetDepthBps = buyAssetPool.derived_depth_bps - const swapDepthBps = bn(sellAssetDepthBps).plus(buyAssetDepthBps).div(2) - // Low health for the pools of this swap - use a longer streaming interval - if (swapDepthBps.lt(5000)) return 10 - // Moderate health for the pools of this swap - use a moderate streaming interval - if (swapDepthBps.lt(9000) && swapDepthBps.gte(5000)) return 5 - // Pool is at 90%+ health - use a 1 block streaming interval - return 1 - })() - : // TODO: One of the pools is RUNE - use the as-is 10 until we work out how best to handle this - 10 - - switch (tradeType) { - case TradeType.L1ToL1: - return getL1Quote(input, deps, streamingInterval, tradeType) - case TradeType.LongTailToL1: - return getLongtailToL1Quote(input, deps, streamingInterval) - case TradeType.L1ToLongTail: - if (!thorchainSwapL1ToLongtailEnabled) - return Err(makeSwapErrorRight({ message: 'Not implemented yet' })) - - return getL1ToLongtailQuote(input, deps, streamingInterval) - case TradeType.LongTailToLongTail: - return Err(makeSwapErrorRight({ message: 'Not implemented yet' })) - default: - assertUnreachable(tradeType) - } -} - -export const getThorTradeRate = async ( - input: GetTradeRateInput, - deps: SwapperDeps, -): Promise> => { - const thorchainSwapLongtailEnabled = deps.config.REACT_APP_FEATURE_THORCHAINSWAP_LONGTAIL - const thorchainSwapL1ToLongtailEnabled = - deps.config.REACT_APP_FEATURE_THORCHAINSWAP_L1_TO_LONGTAIL - const { sellAsset, buyAsset } = input - - if (!sellSupportedChainIds[sellAsset.chainId] || !buySupportedChainIds[buyAsset.chainId]) { - return Err( - makeSwapErrorRight({ - message: 'Unsupported chain', - code: TradeQuoteError.UnsupportedChain, - }), - ) - } - - const daemonUrl = deps.config.REACT_APP_THORCHAIN_NODE_URL - const maybePoolsResponse = await thorService.get( - `${daemonUrl}/lcd/thorchain/pools`, - ) - - if (maybePoolsResponse.isErr()) return Err(maybePoolsResponse.unwrapErr()) - - const { data: poolsResponse } = maybePoolsResponse.unwrap() - - const buyPoolId = assetIdToPoolAssetId({ assetId: buyAsset.assetId }) - const sellPoolId = assetIdToPoolAssetId({ assetId: sellAsset.assetId }) - - // If one or both of these are undefined it means we are tradeing one or more long-tail ERC20 tokens - const sellAssetPool = poolsResponse.find(pool => pool.asset === sellPoolId) - const buyAssetPool = poolsResponse.find(pool => pool.asset === buyPoolId) - - const tradeType = thorchainSwapLongtailEnabled - ? getTradeType(sellAssetPool, buyAssetPool, sellPoolId, buyPoolId) - : TradeType.L1ToL1 - if (tradeType === undefined) { - return Err( - makeSwapErrorRight({ - message: 'Unknown trade type', - code: TradeQuoteError.UnsupportedTradePair, - }), - ) - } - - if ( - (!buyPoolId && tradeType !== TradeType.L1ToLongTail) || - (!sellPoolId && tradeType !== TradeType.LongTailToL1) - ) { - return Err( - makeSwapErrorRight({ - message: 'Unsupported trade pair', - code: TradeQuoteError.UnsupportedTradePair, - }), - ) - } - - const streamingInterval = - sellAssetPool && buyAssetPool - ? (() => { - const sellAssetDepthBps = sellAssetPool.derived_depth_bps - const buyAssetDepthBps = buyAssetPool.derived_depth_bps - const swapDepthBps = bn(sellAssetDepthBps).plus(buyAssetDepthBps).div(2) - // Low health for the pools of this swap - use a longer streaming interval - if (swapDepthBps.lt(5000)) return 10 - // Moderate health for the pools of this swap - use a moderate streaming interval - if (swapDepthBps.lt(9000) && swapDepthBps.gte(5000)) return 5 - // Pool is at 90%+ health - use a 1 block streaming interval - return 1 - })() - : // TODO: One of the pools is RUNE - use the as-is 10 until we work out how best to handle this - 10 - - switch (tradeType) { - case TradeType.L1ToL1: - return getL1Rate(input, deps, streamingInterval, tradeType) - case TradeType.LongTailToL1: - return getLongtailToL1Rate(input, deps, streamingInterval) - case TradeType.L1ToLongTail: - if (!thorchainSwapL1ToLongtailEnabled) - return Err(makeSwapErrorRight({ message: 'Not implemented yet' })) - - return getL1ToLongtailRate(input, deps, streamingInterval) - case TradeType.LongTailToLongTail: - return Err(makeSwapErrorRight({ message: 'Not implemented yet' })) - default: - assertUnreachable(tradeType) - } -} diff --git a/packages/swapper/src/swappers/ThorchainSwapper/getThorTradeRate/getTradeRate.ts b/packages/swapper/src/swappers/ThorchainSwapper/getThorTradeRate/getTradeRate.ts new file mode 100644 index 00000000000..4f2faa3ad8e --- /dev/null +++ b/packages/swapper/src/swappers/ThorchainSwapper/getThorTradeRate/getTradeRate.ts @@ -0,0 +1,106 @@ +import { assertUnreachable, bn } from '@shapeshiftoss/utils' +import type { Result } from '@sniptt/monads' +import { Err } from '@sniptt/monads' + +import type { GetTradeRateInput, SwapErrorRight, SwapperDeps } from '../../../types' +import { TradeQuoteError } from '../../../types' +import { makeSwapErrorRight } from '../../../utils' +import { buySupportedChainIds, sellSupportedChainIds } from '../constants' +import type { ThornodePoolResponse, ThorTradeRate } from '../types' +import { getL1Rate } from '../utils/getL1Rate' +import { getL1ToLongtailRate } from '../utils/getL1ToLongtailRate' +import { getLongtailToL1Rate } from '../utils/getLongtailRate' +import { getTradeType, TradeType } from '../utils/longTailHelpers' +import { assetIdToPoolAssetId } from '../utils/poolAssetHelpers/poolAssetHelpers' +import { thorService } from '../utils/thorService' + +export const getThorTradeRate = async ( + input: GetTradeRateInput, + deps: SwapperDeps, +): Promise> => { + const thorchainSwapLongtailEnabled = deps.config.REACT_APP_FEATURE_THORCHAINSWAP_LONGTAIL + const thorchainSwapL1ToLongtailEnabled = + deps.config.REACT_APP_FEATURE_THORCHAINSWAP_L1_TO_LONGTAIL + const { sellAsset, buyAsset } = input + + if (!sellSupportedChainIds[sellAsset.chainId] || !buySupportedChainIds[buyAsset.chainId]) { + return Err( + makeSwapErrorRight({ + message: 'Unsupported chain', + code: TradeQuoteError.UnsupportedChain, + }), + ) + } + + const daemonUrl = deps.config.REACT_APP_THORCHAIN_NODE_URL + const maybePoolsResponse = await thorService.get( + `${daemonUrl}/lcd/thorchain/pools`, + ) + + if (maybePoolsResponse.isErr()) return Err(maybePoolsResponse.unwrapErr()) + + const { data: poolsResponse } = maybePoolsResponse.unwrap() + + const buyPoolId = assetIdToPoolAssetId({ assetId: buyAsset.assetId }) + const sellPoolId = assetIdToPoolAssetId({ assetId: sellAsset.assetId }) + + // If one or both of these are undefined it means we are tradeing one or more long-tail ERC20 tokens + const sellAssetPool = poolsResponse.find(pool => pool.asset === sellPoolId) + const buyAssetPool = poolsResponse.find(pool => pool.asset === buyPoolId) + + const tradeType = thorchainSwapLongtailEnabled + ? getTradeType(sellAssetPool, buyAssetPool, sellPoolId, buyPoolId) + : TradeType.L1ToL1 + if (tradeType === undefined) { + return Err( + makeSwapErrorRight({ + message: 'Unknown trade type', + code: TradeQuoteError.UnsupportedTradePair, + }), + ) + } + + if ( + (!buyPoolId && tradeType !== TradeType.L1ToLongTail) || + (!sellPoolId && tradeType !== TradeType.LongTailToL1) + ) { + return Err( + makeSwapErrorRight({ + message: 'Unsupported trade pair', + code: TradeQuoteError.UnsupportedTradePair, + }), + ) + } + + const streamingInterval = + sellAssetPool && buyAssetPool + ? (() => { + const sellAssetDepthBps = sellAssetPool.derived_depth_bps + const buyAssetDepthBps = buyAssetPool.derived_depth_bps + const swapDepthBps = bn(sellAssetDepthBps).plus(buyAssetDepthBps).div(2) + // Low health for the pools of this swap - use a longer streaming interval + if (swapDepthBps.lt(5000)) return 10 + // Moderate health for the pools of this swap - use a moderate streaming interval + if (swapDepthBps.lt(9000) && swapDepthBps.gte(5000)) return 5 + // Pool is at 90%+ health - use a 1 block streaming interval + return 1 + })() + : // TODO: One of the pools is RUNE - use the as-is 10 until we work out how best to handle this + 10 + + switch (tradeType) { + case TradeType.L1ToL1: + return getL1Rate(input, deps, streamingInterval, tradeType) + case TradeType.LongTailToL1: + return getLongtailToL1Rate(input, deps, streamingInterval) + case TradeType.L1ToLongTail: + if (!thorchainSwapL1ToLongtailEnabled) + return Err(makeSwapErrorRight({ message: 'Not implemented yet' })) + + return getL1ToLongtailRate(input, deps, streamingInterval) + case TradeType.LongTailToLongTail: + return Err(makeSwapErrorRight({ message: 'Not implemented yet' })) + default: + assertUnreachable(tradeType) + } +} diff --git a/packages/swapper/src/swappers/ThorchainSwapper/types.ts b/packages/swapper/src/swappers/ThorchainSwapper/types.ts index 5ae1ae261fe..59170e164c0 100644 --- a/packages/swapper/src/swappers/ThorchainSwapper/types.ts +++ b/packages/swapper/src/swappers/ThorchainSwapper/types.ts @@ -1,5 +1,8 @@ import type { KnownChainIds } from '@shapeshiftoss/types' +import type { TradeQuote, TradeRate } from '../../types' +import type { TradeType } from './utils/longTailHelpers' + export type ThornodePoolStatuses = 'Available' | 'Staged' | 'Suspended' export type MidgardPoolResponse = { @@ -252,3 +255,41 @@ export enum ThorchainChain { THOR = 'THOR', BSC = 'BSC', } + +export type ThorEvmTradeQuote = TradeQuote & + ThorTradeQuoteSpecificMetadata & { + router: string + vault: string + aggregator?: string + data: string + tradeType: TradeType + } & { + receiveAddress: string + } + +export type ThorTradeUtxoOrCosmosQuote = TradeQuote & ThorTradeQuoteSpecificMetadata +export type ThorTradeQuote = ThorEvmTradeQuote | ThorTradeUtxoOrCosmosQuote + +type ThorTradeQuoteSpecificMetadata = { + isStreaming: boolean + memo: string + recommendedMinimumCryptoBaseUnit: string + tradeType: TradeType + expiry: number + longtailData?: { + longtailToL1ExpectedAmountOut?: bigint + L1ToLongtailExpectedAmountOut?: bigint + } +} + +export type ThorEvmTradeRate = TradeRate & + ThorTradeQuoteSpecificMetadata & { + router: string + vault: string + aggregator?: string + data: string + tradeType: TradeType + } + +export type ThorTradeUtxoOrCosmosRate = TradeRate & ThorTradeQuoteSpecificMetadata +export type ThorTradeRate = ThorEvmTradeRate | ThorTradeUtxoOrCosmosRate diff --git a/packages/swapper/src/swappers/ThorchainSwapper/utils/getL1Rate.ts b/packages/swapper/src/swappers/ThorchainSwapper/utils/getL1Rate.ts new file mode 100644 index 00000000000..703159c8b3e --- /dev/null +++ b/packages/swapper/src/swappers/ThorchainSwapper/utils/getL1Rate.ts @@ -0,0 +1,462 @@ +import type { AssetId } from '@shapeshiftoss/caip' +import { CHAIN_NAMESPACE, fromAssetId } from '@shapeshiftoss/caip' +import { + assertUnreachable, + bn, + bnOrZero, + convertDecimalPercentageToBasisPoints, + convertPrecision, + fromBaseUnit, + isFulfilled, + isRejected, + toBaseUnit, +} from '@shapeshiftoss/utils' +import type { Result } from '@sniptt/monads' +import { Err, Ok } from '@sniptt/monads' +import { v4 as uuid } from 'uuid' + +import { getDefaultSlippageDecimalPercentageForSwapper } from '../../..' +import type { GetEvmTradeQuoteInput, ProtocolFee, SwapperDeps } from '../../../types' +import { + type GetTradeRateInput, + type SwapErrorRight, + SwapperName, + TradeQuoteError, +} from '../../../types' +import { makeSwapErrorRight } from '../../../utils' +import { + THOR_PRECISION, + THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE, + THORCHAIN_LONGTAIL_SWAP_SOURCE, + THORCHAIN_STREAM_SWAP_SOURCE, +} from '../constants' +import { getThorTxInfo as getEvmThorTxInfo } from '../evm/utils/getThorTxData' +import type { + ThorEvmTradeRate, + ThornodeQuoteResponseSuccess, + ThorTradeRate, + ThorTradeUtxoOrCosmosRate, +} from '../types' +import { THORCHAIN_FIXED_PRECISION } from './constants' +import { getQuote } from './getQuote/getQuote' +import { TradeType } from './longTailHelpers' +import { getEvmTxFees } from './txFeeHelpers/evmTxFees/getEvmTxFees' + +export const getL1Rate = async ( + input: GetTradeRateInput, + deps: SwapperDeps, + streamingInterval: number, + tradeType: TradeType, +): Promise> => { + const { + sellAsset, + buyAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, + accountNumber, + receiveAddress, + affiliateBps: requestedAffiliateBps, + potentialAffiliateBps, + } = input + + const { chainNamespace } = fromAssetId(sellAsset.assetId) + + if (chainNamespace === CHAIN_NAMESPACE.Solana) { + return Err( + makeSwapErrorRight({ + message: 'Solana is not supported', + code: TradeQuoteError.UnsupportedTradePair, + }), + ) + } + + const slippageTolerancePercentageDecimal = + input.slippageTolerancePercentageDecimal ?? + getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Thorchain) + + const inputSlippageBps = convertDecimalPercentageToBasisPoints(slippageTolerancePercentageDecimal) + + const maybeSwapQuote = await getQuote( + { + sellAsset, + buyAssetId: buyAsset.assetId, + sellAmountCryptoBaseUnit, + receiveAddress, + streaming: false, + affiliateBps: requestedAffiliateBps, + }, + deps, + ) + + if (maybeSwapQuote.isErr()) return Err(maybeSwapQuote.unwrapErr()) + const swapQuote = maybeSwapQuote.unwrap() + + const maybeStreamingSwapQuote = deps.config.REACT_APP_FEATURE_THOR_SWAP_STREAMING_SWAPS + ? await getQuote( + { + sellAsset, + buyAssetId: buyAsset.assetId, + sellAmountCryptoBaseUnit, + receiveAddress: undefined, + streaming: true, + affiliateBps: requestedAffiliateBps, + streamingInterval, + }, + deps, + ) + : undefined + + if (maybeStreamingSwapQuote?.isErr()) return Err(maybeStreamingSwapQuote.unwrapErr()) + const streamingSwapQuote = maybeStreamingSwapQuote?.unwrap() + + // recommended_min_amount_in should be the same value for both types of swaps + const recommendedMinimumCryptoBaseUnit = swapQuote.recommended_min_amount_in + ? convertPrecision({ + value: swapQuote.recommended_min_amount_in, + inputExponent: THORCHAIN_FIXED_PRECISION, + outputExponent: sellAsset.precision, + }).toFixed() + : '0' + + const getRouteValues = (quote: ThornodeQuoteResponseSuccess, isStreaming: boolean) => { + const source = (() => { + if (isStreaming && tradeType === TradeType.L1ToL1) return THORCHAIN_STREAM_SWAP_SOURCE + if ( + isStreaming && + [TradeType.L1ToLongTail, TradeType.LongTailToL1, TradeType.LongTailToLongTail].includes( + tradeType, + ) + ) + return THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE + if ( + !isStreaming && + [TradeType.L1ToLongTail, TradeType.LongTailToL1, TradeType.LongTailToLongTail].includes( + tradeType, + ) + ) + return THORCHAIN_LONGTAIL_SWAP_SOURCE + return SwapperName.Thorchain + })() + + return { + source, + quote, + expectedAmountOutThorBaseUnit: bnOrZero(quote.expected_amount_out).toFixed(), + isStreaming, + affiliateBps: quote.fees.affiliate === '0' ? '0' : requestedAffiliateBps, + // 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, + } + } + + const perRouteValues = [getRouteValues(swapQuote, false)] + + if ( + streamingSwapQuote && + swapQuote.expected_amount_out !== streamingSwapQuote.expected_amount_out + ) { + perRouteValues.push(getRouteValues(streamingSwapQuote, true)) + } + + const getRouteRate = (expectedAmountOutThorBaseUnit: string) => { + const sellAmountCryptoPrecision = fromBaseUnit(sellAmountCryptoBaseUnit, sellAsset.precision) + // All thorchain pool amounts are base 8 regardless of token precision + const sellAmountCryptoThorBaseUnit = bn(toBaseUnit(sellAmountCryptoPrecision, THOR_PRECISION)) + + return bnOrZero(expectedAmountOutThorBaseUnit).div(sellAmountCryptoThorBaseUnit).toFixed() + } + + const getRouteBuyAmountBeforeFeesCryptoBaseUnit = (quote: ThornodeQuoteResponseSuccess) => { + const buyAmountBeforeFeesCryptoThorPrecision = bn(quote.expected_amount_out).plus( + quote.fees.total, + ) + return toBaseUnit( + fromBaseUnit(buyAmountBeforeFeesCryptoThorPrecision, THORCHAIN_FIXED_PRECISION), + buyAsset.precision, + ) + } + + const getProtocolFees = (quote: ThornodeQuoteResponseSuccess) => { + // THORChain fees consist of liquidity, outbound, and affiliate fees + // For the purpose of displaying protocol fees to the user, we don't need the latter + // The reason for that is the affiliate fee is shown as its own "ShapeShift fee" section + // Including the affiliate fee here would result in the protocol fee being wrong, as affiliate fees would be + // double accounted for both in protocol fees, and affiliate fee + const buyAssetTradeFeeBuyAssetCryptoThorPrecision = bnOrZero(quote.fees.total).minus( + quote.fees.affiliate, + ) + const buyAssetTradeFeeBuyAssetCryptoBaseUnit = convertPrecision({ + value: buyAssetTradeFeeBuyAssetCryptoThorPrecision, + inputExponent: THORCHAIN_FIXED_PRECISION, + outputExponent: buyAsset.precision, + }) + + const protocolFees: Record = {} + + if (!buyAssetTradeFeeBuyAssetCryptoBaseUnit.isZero()) { + protocolFees[buyAsset.assetId] = { + amountCryptoBaseUnit: buyAssetTradeFeeBuyAssetCryptoBaseUnit.toString(), + requiresBalance: false, + asset: buyAsset, + } + } + + return protocolFees + } + + switch (chainNamespace) { + case CHAIN_NAMESPACE.Evm: { + const sellAdapter = deps.assertGetEvmChainAdapter(sellAsset.chainId) + const { networkFeeCryptoBaseUnit } = await getEvmTxFees({ + adapter: sellAdapter, + supportsEIP1559: Boolean((input as GetEvmTradeQuoteInput).supportsEIP1559), + }) + + const maybeRoutes = await Promise.allSettled( + perRouteValues.map( + async ({ + source, + quote, + expectedAmountOutThorBaseUnit, + isStreaming, + estimatedExecutionTimeMs, + affiliateBps, + }): Promise => { + const rate = getRouteRate(expectedAmountOutThorBaseUnit) + const buyAmountBeforeFeesCryptoBaseUnit = + getRouteBuyAmountBeforeFeesCryptoBaseUnit(quote) + + // No memo returned for rates + const memo = '' + + const { data, router, vault } = await getEvmThorTxInfo({ + sellAsset, + sellAmountCryptoBaseUnit, + memo, + expiry: quote.expiry, + config: deps.config, + }) + + const buyAmountAfterFeesCryptoBaseUnit = convertPrecision({ + value: expectedAmountOutThorBaseUnit, + inputExponent: THORCHAIN_FIXED_PRECISION, + outputExponent: buyAsset.precision, + }).toFixed() + + return { + id: uuid(), + accountNumber: undefined, + memo, + receiveAddress: undefined, + affiliateBps, + potentialAffiliateBps, + isStreaming, + recommendedMinimumCryptoBaseUnit, + slippageTolerancePercentageDecimal: isStreaming + ? undefined + : slippageTolerancePercentageDecimal, + rate, + data, + router, + vault, + expiry: quote.expiry, + tradeType: tradeType ?? TradeType.L1ToL1, + steps: [ + { + estimatedExecutionTimeMs, + rate, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, + buyAmountBeforeFeesCryptoBaseUnit, + buyAmountAfterFeesCryptoBaseUnit, + source, + buyAsset, + sellAsset, + accountNumber: accountNumber!, + allowanceContract: router, + feeData: { + networkFeeCryptoBaseUnit, + protocolFees: getProtocolFees(quote), + }, + }, + ], + } + }, + ), + ) + + const routes = maybeRoutes.filter(isFulfilled).map(maybeRoute => maybeRoute.value) + + // if no routes succeeded, return failure from swapper + if (!routes.length) + return Err( + makeSwapErrorRight({ + message: 'Unable to create any routes', + code: TradeQuoteError.UnsupportedTradePair, + cause: maybeRoutes.filter(isRejected).map(maybeRoute => maybeRoute.reason), + }), + ) + + // otherwise, return all that succeeded + return Ok(routes) + } + + case CHAIN_NAMESPACE.Utxo: { + const maybeRoutes = await Promise.allSettled( + perRouteValues.map( + ({ + source, + quote, + expectedAmountOutThorBaseUnit, + isStreaming, + estimatedExecutionTimeMs, + affiliateBps, + }): ThorTradeUtxoOrCosmosRate => { + const rate = getRouteRate(expectedAmountOutThorBaseUnit) + const buyAmountBeforeFeesCryptoBaseUnit = + getRouteBuyAmountBeforeFeesCryptoBaseUnit(quote) + + // No memo for trade rates + const memo = '' + + // TODO(gomes): for UTXOs, we should be able to get a very rough estimation (not taking users' UTXOs into account) + // using sats per byte and byte size from memo. Yes, we don't have a memo returned, but can build it in-house for this purpose easily. + const feeData = { + networkFeeCryptoBaseUnit: undefined, + protocolFees: getProtocolFees(quote), + } + + const buyAmountAfterFeesCryptoBaseUnit = convertPrecision({ + value: expectedAmountOutThorBaseUnit, + inputExponent: THORCHAIN_FIXED_PRECISION, + outputExponent: buyAsset.precision, + }).toFixed() + + return { + id: uuid(), + accountNumber: undefined, + memo, + receiveAddress: undefined, + affiliateBps, + potentialAffiliateBps, + isStreaming, + recommendedMinimumCryptoBaseUnit, + tradeType: tradeType ?? TradeType.L1ToL1, + expiry: quote.expiry, + slippageTolerancePercentageDecimal: isStreaming + ? undefined + : slippageTolerancePercentageDecimal, + rate, + steps: [ + { + estimatedExecutionTimeMs, + rate, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, + buyAmountBeforeFeesCryptoBaseUnit, + buyAmountAfterFeesCryptoBaseUnit, + source, + buyAsset, + sellAsset, + // TODO(gomes): when we actually split between TradeQuote and TradeRate in https://github.com/shapeshift/web/issues/7941, + // this won't be an issue anymore - for now this is tackled at runtime with the isConnected check above + accountNumber: accountNumber!, + allowanceContract: '0x0', // not applicable to UTXOs + feeData, + }, + ], + } + }, + ), + ) + + const routes = maybeRoutes.filter(isFulfilled).map(maybeRoute => maybeRoute.value) + + // if no routes succeeded, return failure from swapper + if (!routes.length) + return Err( + makeSwapErrorRight({ + message: 'Unable to create any routes', + code: TradeQuoteError.UnsupportedTradePair, + cause: maybeRoutes.filter(isRejected).map(maybeRoute => maybeRoute.reason), + }), + ) + + // otherwise, return all that succeeded + return Ok(routes) + } + + case CHAIN_NAMESPACE.CosmosSdk: { + const cosmosChainAdapter = deps.assertGetCosmosSdkChainAdapter(sellAsset.chainId) + const feeData = await cosmosChainAdapter.getFeeData({}) + + return Ok( + perRouteValues.map( + ({ + source, + quote, + expectedAmountOutThorBaseUnit, + isStreaming, + estimatedExecutionTimeMs, + affiliateBps, + }): ThorTradeUtxoOrCosmosRate => { + const rate = getRouteRate(expectedAmountOutThorBaseUnit) + const buyAmountBeforeFeesCryptoBaseUnit = + getRouteBuyAmountBeforeFeesCryptoBaseUnit(quote) + + const buyAmountAfterFeesCryptoBaseUnit = convertPrecision({ + value: expectedAmountOutThorBaseUnit, + inputExponent: THORCHAIN_FIXED_PRECISION, + outputExponent: buyAsset.precision, + }).toFixed() + + // No memo returned for rates + const memo = '' + + return { + id: uuid(), + accountNumber: undefined, + memo, + receiveAddress: undefined, + affiliateBps, + potentialAffiliateBps, + isStreaming, + recommendedMinimumCryptoBaseUnit, + expiry: quote.expiry, + slippageTolerancePercentageDecimal: isStreaming + ? undefined + : slippageTolerancePercentageDecimal, + rate, + tradeType: tradeType ?? TradeType.L1ToL1, + steps: [ + { + estimatedExecutionTimeMs, + rate, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, + buyAmountBeforeFeesCryptoBaseUnit, + buyAmountAfterFeesCryptoBaseUnit, + source, + buyAsset, + sellAsset, + accountNumber, + allowanceContract: '0x0', // not applicable to cosmos + feeData: { + networkFeeCryptoBaseUnit: feeData.fast.txFee, + protocolFees: getProtocolFees(quote), + chainSpecific: { + estimatedGasCryptoBaseUnit: feeData.fast.chainSpecific.gasLimit, + }, + }, + }, + ], + } + }, + ), + ) + } + + default: + assertUnreachable(chainNamespace) + } +} diff --git a/packages/swapper/src/swappers/ThorchainSwapper/utils/getL1ToLongtailQuote.ts b/packages/swapper/src/swappers/ThorchainSwapper/utils/getL1ToLongtailQuote.ts index 64ce0fe5216..6391234edf1 100644 --- a/packages/swapper/src/swappers/ThorchainSwapper/utils/getL1ToLongtailQuote.ts +++ b/packages/swapper/src/swappers/ThorchainSwapper/utils/getL1ToLongtailQuote.ts @@ -13,18 +13,16 @@ import { Err, Ok } from '@sniptt/monads' import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants' import type { CommonTradeQuoteInput, - GetTradeRateInput, MultiHopTradeQuoteSteps, - MultiHopTradeRateSteps, SwapErrorRight, SwapperDeps, } from '../../../types' import { SwapperName, TradeQuoteError } from '../../../types' import { getHopByIndex, makeSwapErrorRight } from '../../../utils' -import type { ThorTradeQuote, ThorTradeRate } from '../getThorTradeQuoteOrRate/getTradeQuoteOrRate' +import type { ThorTradeQuote } from '../types' import { addL1ToLongtailPartsToMemo } from './addL1ToLongtailPartsToMemo' import { getBestAggregator } from './getBestAggregator' -import { getL1Quote, getL1Rate } from './getL1quote' +import { getL1Quote } from './getL1quote' import type { AggregatorContract } from './longTailHelpers' import { getTokenFromAsset, getWrappedToken, TradeType } from './longTailHelpers' @@ -182,146 +180,3 @@ export const getL1ToLongtailQuote = async ( return Ok(updatedQuotes) } - -export const getL1ToLongtailRate = async ( - input: GetTradeRateInput, - deps: SwapperDeps, - streamingInterval: number, -): Promise> => { - const { - buyAsset, - sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, - sellAsset, - } = input - - const longtailTokensJson = await import('../generated/generatedThorLongtailTokens.json') - const longtailTokens: AssetId[] = longtailTokensJson.default - - if (!longtailTokens.includes(buyAsset.assetId)) { - return Err( - makeSwapErrorRight({ - message: `[getThorTradeQuote] - Unsupported buyAssetId ${buyAsset.assetId}.`, - code: TradeQuoteError.UnsupportedTradePair, - details: { buyAsset, sellAsset }, - }), - ) - } - - /* - We only support L1 -> ethereum longtail swaps for now. - */ - if (buyAsset.chainId !== ethChainId) { - return Err( - makeSwapErrorRight({ - message: `[getThorTradeQuote] - Unsupported chainId ${buyAsset.chainId}.`, - code: TradeQuoteError.UnsupportedChain, - details: { buyAssetChainId: buyAsset.chainId }, - }), - ) - } - - const sellAssetChainId = sellAsset.chainId - const buyAssetChainId = buyAsset.chainId - - const sellAssetFeeAssetId = deps.assertGetChainAdapter(sellAssetChainId).getFeeAssetId() - const sellAssetFeeAsset = sellAssetFeeAssetId ? deps.assetsById[sellAssetFeeAssetId] : undefined - - const buyAssetFeeAssetId = deps.assertGetChainAdapter(buyAssetChainId).getFeeAssetId() - const buyAssetFeeAsset = buyAssetFeeAssetId ? deps.assetsById[buyAssetFeeAssetId] : undefined - - if (!buyAssetFeeAsset) { - return Err( - makeSwapErrorRight({ - message: `[getThorTradeQuote] - No native buy asset found for ${buyAssetChainId}.`, - code: TradeQuoteError.InternalError, - details: { buyAssetChainId }, - }), - ) - } - - if (!sellAssetFeeAsset) { - return Err( - makeSwapErrorRight({ - message: `[getThorTradeQuote] - No native buy asset found for ${sellAssetChainId}.`, - code: TradeQuoteError.InternalError, - details: { sellAssetChainId }, - }), - ) - } - - const l1Tol1RateInput: GetTradeRateInput = { - ...input, - buyAsset: buyAssetFeeAsset, - sellAsset, - sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, - } - - const maybeThorchainRates = await getL1Rate( - l1Tol1RateInput, - deps, - streamingInterval, - TradeType.L1ToLongTail, - ) - - if (maybeThorchainRates.isErr()) return Err(maybeThorchainRates.unwrapErr()) - - const thorchainRates = maybeThorchainRates.unwrap() - - let bestAggregator: AggregatorContract - let quotedAmountOut: bigint - - const promises = await Promise.allSettled( - thorchainRates.map(async quote => { - // A quote always has a first step - const onlyStep = getHopByIndex(quote, 0)! - - const maybeBestAggregator = await getBestAggregator( - buyAssetFeeAsset, - getWrappedToken(buyAssetFeeAsset), - getTokenFromAsset(buyAsset), - onlyStep.buyAmountAfterFeesCryptoBaseUnit, - ) - - if (maybeBestAggregator.isErr()) return Err(maybeBestAggregator.unwrapErr()) - - const unwrappedResult = maybeBestAggregator.unwrap() - - bestAggregator = unwrappedResult.bestAggregator - quotedAmountOut = unwrappedResult.quotedAmountOut - - // No memo is returned upstream for rates - const updatedMemo = '' - - return Ok({ - ...quote, - memo: updatedMemo, - aggregator: bestAggregator, - steps: quote.steps.map(s => ({ - ...s, - buyAsset, - buyAmountAfterFeesCryptoBaseUnit: quotedAmountOut.toString(), - // This is wrong, we should get the get the value before fees or display ETH value received after the thorchain bridge - buyAmountBeforeFeesCryptoBaseUnit: quotedAmountOut.toString(), - allowanceContract: TS_AGGREGATOR_TOKEN_TRANSFER_PROXY_CONTRACT_MAINNET, - })) as MultiHopTradeRateSteps, // assuming multi-hop rate steps here since we're mapping over quote steps, - isLongtail: true, - longtailData: { - L1ToLongtailExpectedAmountOut: quotedAmountOut, - }, - }) - }), - ) - - if (promises.every(promise => isRejected(promise) || isResolvedErr(promise))) { - return Err( - makeSwapErrorRight({ - message: '[getThorTradeQuote] - failed to get best aggregator', - code: TradeQuoteError.InternalError, - }), - ) - } - - const updatedQuotes = promises.filter(isFulfilled).map(element => element.value.unwrap()) - - return Ok(updatedQuotes) -} diff --git a/packages/swapper/src/swappers/ThorchainSwapper/utils/getL1ToLongtailRate.ts b/packages/swapper/src/swappers/ThorchainSwapper/utils/getL1ToLongtailRate.ts new file mode 100644 index 00000000000..b427b999cc4 --- /dev/null +++ b/packages/swapper/src/swappers/ThorchainSwapper/utils/getL1ToLongtailRate.ts @@ -0,0 +1,161 @@ +import { type AssetId, ethChainId } from '@shapeshiftoss/caip' +import { TS_AGGREGATOR_TOKEN_TRANSFER_PROXY_CONTRACT_MAINNET } from '@shapeshiftoss/contracts' +import { isFulfilled, isRejected, isResolvedErr } from '@shapeshiftoss/utils' +import { Err, Ok, type Result } from '@sniptt/monads' + +import type { MultiHopTradeRateSteps } from '../../../types' +import { + type GetTradeRateInput, + type SwapErrorRight, + type SwapperDeps, + TradeQuoteError, +} from '../../../types' +import { getHopByIndex, makeSwapErrorRight } from '../../../utils' +import type { ThorTradeRate } from '../types' +import { getBestAggregator } from './getBestAggregator' +import { getL1Rate } from './getL1Rate' +import type { AggregatorContract } from './longTailHelpers' +import { getTokenFromAsset, getWrappedToken, TradeType } from './longTailHelpers' + +export const getL1ToLongtailRate = async ( + input: GetTradeRateInput, + deps: SwapperDeps, + streamingInterval: number, +): Promise> => { + const { + buyAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, + sellAsset, + } = input + + const longtailTokensJson = await import('../generated/generatedThorLongtailTokens.json') + const longtailTokens: AssetId[] = longtailTokensJson.default + + if (!longtailTokens.includes(buyAsset.assetId)) { + return Err( + makeSwapErrorRight({ + message: `[getThorTradeQuote] - Unsupported buyAssetId ${buyAsset.assetId}.`, + code: TradeQuoteError.UnsupportedTradePair, + details: { buyAsset, sellAsset }, + }), + ) + } + + /* + We only support L1 -> ethereum longtail swaps for now. + */ + if (buyAsset.chainId !== ethChainId) { + return Err( + makeSwapErrorRight({ + message: `[getThorTradeQuote] - Unsupported chainId ${buyAsset.chainId}.`, + code: TradeQuoteError.UnsupportedChain, + details: { buyAssetChainId: buyAsset.chainId }, + }), + ) + } + + const sellAssetChainId = sellAsset.chainId + const buyAssetChainId = buyAsset.chainId + + const sellAssetFeeAssetId = deps.assertGetChainAdapter(sellAssetChainId).getFeeAssetId() + const sellAssetFeeAsset = sellAssetFeeAssetId ? deps.assetsById[sellAssetFeeAssetId] : undefined + + const buyAssetFeeAssetId = deps.assertGetChainAdapter(buyAssetChainId).getFeeAssetId() + const buyAssetFeeAsset = buyAssetFeeAssetId ? deps.assetsById[buyAssetFeeAssetId] : undefined + + if (!buyAssetFeeAsset) { + return Err( + makeSwapErrorRight({ + message: `[getThorTradeQuote] - No native buy asset found for ${buyAssetChainId}.`, + code: TradeQuoteError.InternalError, + details: { buyAssetChainId }, + }), + ) + } + + if (!sellAssetFeeAsset) { + return Err( + makeSwapErrorRight({ + message: `[getThorTradeQuote] - No native buy asset found for ${sellAssetChainId}.`, + code: TradeQuoteError.InternalError, + details: { sellAssetChainId }, + }), + ) + } + + const l1Tol1RateInput: GetTradeRateInput = { + ...input, + buyAsset: buyAssetFeeAsset, + sellAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, + } + + const maybeThorchainRates = await getL1Rate( + l1Tol1RateInput, + deps, + streamingInterval, + TradeType.L1ToLongTail, + ) + + if (maybeThorchainRates.isErr()) return Err(maybeThorchainRates.unwrapErr()) + + const thorchainRates = maybeThorchainRates.unwrap() + + let bestAggregator: AggregatorContract + let quotedAmountOut: bigint + + const promises = await Promise.allSettled( + thorchainRates.map(async quote => { + // A quote always has a first step + const onlyStep = getHopByIndex(quote, 0)! + + const maybeBestAggregator = await getBestAggregator( + buyAssetFeeAsset, + getWrappedToken(buyAssetFeeAsset), + getTokenFromAsset(buyAsset), + onlyStep.buyAmountAfterFeesCryptoBaseUnit, + ) + + if (maybeBestAggregator.isErr()) return Err(maybeBestAggregator.unwrapErr()) + + const unwrappedResult = maybeBestAggregator.unwrap() + + bestAggregator = unwrappedResult.bestAggregator + quotedAmountOut = unwrappedResult.quotedAmountOut + + // No memo is returned upstream for rates + const updatedMemo = '' + + return Ok({ + ...quote, + memo: updatedMemo, + aggregator: bestAggregator, + steps: quote.steps.map(s => ({ + ...s, + buyAsset, + buyAmountAfterFeesCryptoBaseUnit: quotedAmountOut.toString(), + // This is wrong, we should get the get the value before fees or display ETH value received after the thorchain bridge + buyAmountBeforeFeesCryptoBaseUnit: quotedAmountOut.toString(), + allowanceContract: TS_AGGREGATOR_TOKEN_TRANSFER_PROXY_CONTRACT_MAINNET, + })) as MultiHopTradeRateSteps, // assuming multi-hop rate steps here since we're mapping over quote steps, + isLongtail: true, + longtailData: { + L1ToLongtailExpectedAmountOut: quotedAmountOut, + }, + }) + }), + ) + + if (promises.every(promise => isRejected(promise) || isResolvedErr(promise))) { + return Err( + makeSwapErrorRight({ + message: '[getThorTradeQuote] - failed to get best aggregator', + code: TradeQuoteError.InternalError, + }), + ) + } + + const updatedQuotes = promises.filter(isFulfilled).map(element => element.value.unwrap()) + + return Ok(updatedQuotes) +} diff --git a/packages/swapper/src/swappers/ThorchainSwapper/utils/getL1quote.ts b/packages/swapper/src/swappers/ThorchainSwapper/utils/getL1quote.ts index d61e3a4d0c1..2fe9922f815 100644 --- a/packages/swapper/src/swappers/ThorchainSwapper/utils/getL1quote.ts +++ b/packages/swapper/src/swappers/ThorchainSwapper/utils/getL1quote.ts @@ -18,7 +18,6 @@ import { addLimitToMemo } from '../../../thorchain-utils/memo/addLimitToMemo' import type { CommonTradeQuoteInput, GetEvmTradeQuoteInput, - GetTradeRateInput, GetUtxoTradeQuoteInput, ProtocolFee, SwapErrorRight, @@ -35,13 +34,10 @@ import { import { getThorTxInfo as getEvmThorTxInfo } from '../evm/utils/getThorTxData' import type { ThorEvmTradeQuote, - ThorEvmTradeRate, + ThornodeQuoteResponseSuccess, ThorTradeQuote, - ThorTradeRate, ThorTradeUtxoOrCosmosQuote, - ThorTradeUtxoOrCosmosRate, -} from '../getThorTradeQuoteOrRate/getTradeQuoteOrRate' -import type { ThornodeQuoteResponseSuccess } from '../types' +} from '../types' import { getThorTxInfo as getUtxoThorTxInfo } from '../utxo/utils/getThorTxData' import { THORCHAIN_FIXED_PRECISION } from './constants' import { getLimitWithManualSlippage } from './getLimitWithManualSlippage' @@ -409,9 +405,7 @@ export const getL1Quote = async ( source, buyAsset, sellAsset, - // TODO(gomes): when we actually split between TradeQuote and TradeRate in https://github.com/shapeshift/web/issues/7941, - // this won't be an issue anymore - for now this is tackled at runtime with the isConnected check above - accountNumber: accountNumber!, + accountNumber, allowanceContract: '0x0', // not applicable to UTXOs feeData, }, @@ -505,430 +499,7 @@ export const getL1Quote = async ( source, buyAsset, sellAsset, - // TODO(gomes): when we actually split between TradeQuote and TradeRate in https://github.com/shapeshift/web/issues/7941, - // this won't be an issue anymore - for now this is tackled at runtime with the isConnected check above - accountNumber: accountNumber!, - allowanceContract: '0x0', // not applicable to cosmos - feeData: { - networkFeeCryptoBaseUnit: feeData.fast.txFee, - protocolFees: getProtocolFees(quote), - chainSpecific: { - estimatedGasCryptoBaseUnit: feeData.fast.chainSpecific.gasLimit, - }, - }, - }, - ], - } - }, - ), - ) - } - - default: - assertUnreachable(chainNamespace) - } -} - -export const getL1Rate = async ( - input: GetTradeRateInput, - deps: SwapperDeps, - streamingInterval: number, - tradeType: TradeType, -): Promise> => { - const { - sellAsset, - buyAsset, - sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, - accountNumber, - receiveAddress, - affiliateBps: requestedAffiliateBps, - potentialAffiliateBps, - } = input - - const { chainNamespace } = fromAssetId(sellAsset.assetId) - - if (chainNamespace === CHAIN_NAMESPACE.Solana) { - return Err( - makeSwapErrorRight({ - message: 'Solana is not supported', - code: TradeQuoteError.UnsupportedTradePair, - }), - ) - } - - const slippageTolerancePercentageDecimal = - input.slippageTolerancePercentageDecimal ?? - getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Thorchain) - - const inputSlippageBps = convertDecimalPercentageToBasisPoints(slippageTolerancePercentageDecimal) - - const maybeSwapQuote = await getQuote( - { - sellAsset, - buyAssetId: buyAsset.assetId, - sellAmountCryptoBaseUnit, - receiveAddress, - streaming: false, - affiliateBps: requestedAffiliateBps, - }, - deps, - ) - - if (maybeSwapQuote.isErr()) return Err(maybeSwapQuote.unwrapErr()) - const swapQuote = maybeSwapQuote.unwrap() - - const maybeStreamingSwapQuote = deps.config.REACT_APP_FEATURE_THOR_SWAP_STREAMING_SWAPS - ? await getQuote( - { - sellAsset, - buyAssetId: buyAsset.assetId, - sellAmountCryptoBaseUnit, - receiveAddress: undefined, - streaming: true, - affiliateBps: requestedAffiliateBps, - streamingInterval, - }, - deps, - ) - : undefined - - if (maybeStreamingSwapQuote?.isErr()) return Err(maybeStreamingSwapQuote.unwrapErr()) - const streamingSwapQuote = maybeStreamingSwapQuote?.unwrap() - - // recommended_min_amount_in should be the same value for both types of swaps - const recommendedMinimumCryptoBaseUnit = swapQuote.recommended_min_amount_in - ? convertPrecision({ - value: swapQuote.recommended_min_amount_in, - inputExponent: THORCHAIN_FIXED_PRECISION, - outputExponent: sellAsset.precision, - }).toFixed() - : '0' - - const getRouteValues = (quote: ThornodeQuoteResponseSuccess, isStreaming: boolean) => { - const source = (() => { - if (isStreaming && tradeType === TradeType.L1ToL1) return THORCHAIN_STREAM_SWAP_SOURCE - if ( - isStreaming && - [TradeType.L1ToLongTail, TradeType.LongTailToL1, TradeType.LongTailToLongTail].includes( - tradeType, - ) - ) - return THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE - if ( - !isStreaming && - [TradeType.L1ToLongTail, TradeType.LongTailToL1, TradeType.LongTailToLongTail].includes( - tradeType, - ) - ) - return THORCHAIN_LONGTAIL_SWAP_SOURCE - return SwapperName.Thorchain - })() - - return { - source, - quote, - expectedAmountOutThorBaseUnit: bnOrZero(quote.expected_amount_out).toFixed(), - isStreaming, - affiliateBps: quote.fees.affiliate === '0' ? '0' : requestedAffiliateBps, - // 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, - } - } - - const perRouteValues = [getRouteValues(swapQuote, false)] - - if ( - streamingSwapQuote && - swapQuote.expected_amount_out !== streamingSwapQuote.expected_amount_out - ) { - perRouteValues.push(getRouteValues(streamingSwapQuote, true)) - } - - const getRouteRate = (expectedAmountOutThorBaseUnit: string) => { - const sellAmountCryptoPrecision = fromBaseUnit(sellAmountCryptoBaseUnit, sellAsset.precision) - // All thorchain pool amounts are base 8 regardless of token precision - const sellAmountCryptoThorBaseUnit = bn(toBaseUnit(sellAmountCryptoPrecision, THOR_PRECISION)) - - return bnOrZero(expectedAmountOutThorBaseUnit).div(sellAmountCryptoThorBaseUnit).toFixed() - } - - const getRouteBuyAmountBeforeFeesCryptoBaseUnit = (quote: ThornodeQuoteResponseSuccess) => { - const buyAmountBeforeFeesCryptoThorPrecision = bn(quote.expected_amount_out).plus( - quote.fees.total, - ) - return toBaseUnit( - fromBaseUnit(buyAmountBeforeFeesCryptoThorPrecision, THORCHAIN_FIXED_PRECISION), - buyAsset.precision, - ) - } - - const getProtocolFees = (quote: ThornodeQuoteResponseSuccess) => { - // THORChain fees consist of liquidity, outbound, and affiliate fees - // For the purpose of displaying protocol fees to the user, we don't need the latter - // The reason for that is the affiliate fee is shown as its own "ShapeShift fee" section - // Including the affiliate fee here would result in the protocol fee being wrong, as affiliate fees would be - // double accounted for both in protocol fees, and affiliate fee - const buyAssetTradeFeeBuyAssetCryptoThorPrecision = bnOrZero(quote.fees.total).minus( - quote.fees.affiliate, - ) - const buyAssetTradeFeeBuyAssetCryptoBaseUnit = convertPrecision({ - value: buyAssetTradeFeeBuyAssetCryptoThorPrecision, - inputExponent: THORCHAIN_FIXED_PRECISION, - outputExponent: buyAsset.precision, - }) - - const protocolFees: Record = {} - - if (!buyAssetTradeFeeBuyAssetCryptoBaseUnit.isZero()) { - protocolFees[buyAsset.assetId] = { - amountCryptoBaseUnit: buyAssetTradeFeeBuyAssetCryptoBaseUnit.toString(), - requiresBalance: false, - asset: buyAsset, - } - } - - return protocolFees - } - - switch (chainNamespace) { - case CHAIN_NAMESPACE.Evm: { - const sellAdapter = deps.assertGetEvmChainAdapter(sellAsset.chainId) - const { networkFeeCryptoBaseUnit } = await getEvmTxFees({ - adapter: sellAdapter, - supportsEIP1559: Boolean((input as GetEvmTradeQuoteInput).supportsEIP1559), - }) - - const maybeRoutes = await Promise.allSettled( - perRouteValues.map( - async ({ - source, - quote, - expectedAmountOutThorBaseUnit, - isStreaming, - estimatedExecutionTimeMs, - affiliateBps, - }): Promise => { - const rate = getRouteRate(expectedAmountOutThorBaseUnit) - const buyAmountBeforeFeesCryptoBaseUnit = - getRouteBuyAmountBeforeFeesCryptoBaseUnit(quote) - - // No memo returned for rates - const memo = '' - - const { data, router, vault } = await getEvmThorTxInfo({ - sellAsset, - sellAmountCryptoBaseUnit, - memo, - expiry: quote.expiry, - config: deps.config, - }) - - const buyAmountAfterFeesCryptoBaseUnit = convertPrecision({ - value: expectedAmountOutThorBaseUnit, - inputExponent: THORCHAIN_FIXED_PRECISION, - outputExponent: buyAsset.precision, - }).toFixed() - - return { - id: uuid(), - accountNumber: undefined, - memo, - receiveAddress: undefined, - affiliateBps, - potentialAffiliateBps, - isStreaming, - recommendedMinimumCryptoBaseUnit, - slippageTolerancePercentageDecimal: isStreaming - ? undefined - : slippageTolerancePercentageDecimal, - rate, - data, - router, - vault, - expiry: quote.expiry, - tradeType: tradeType ?? TradeType.L1ToL1, - steps: [ - { - estimatedExecutionTimeMs, - rate, - sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, - buyAmountBeforeFeesCryptoBaseUnit, - buyAmountAfterFeesCryptoBaseUnit, - source, - buyAsset, - sellAsset, - accountNumber: accountNumber!, - allowanceContract: router, - feeData: { - networkFeeCryptoBaseUnit, - protocolFees: getProtocolFees(quote), - }, - }, - ], - } - }, - ), - ) - - const routes = maybeRoutes.filter(isFulfilled).map(maybeRoute => maybeRoute.value) - - // if no routes succeeded, return failure from swapper - if (!routes.length) - return Err( - makeSwapErrorRight({ - message: 'Unable to create any routes', - code: TradeQuoteError.UnsupportedTradePair, - cause: maybeRoutes.filter(isRejected).map(maybeRoute => maybeRoute.reason), - }), - ) - - // otherwise, return all that succeeded - return Ok(routes) - } - - case CHAIN_NAMESPACE.Utxo: { - const maybeRoutes = await Promise.allSettled( - perRouteValues.map( - ({ - source, - quote, - expectedAmountOutThorBaseUnit, - isStreaming, - estimatedExecutionTimeMs, - affiliateBps, - }): ThorTradeUtxoOrCosmosRate => { - const rate = getRouteRate(expectedAmountOutThorBaseUnit) - const buyAmountBeforeFeesCryptoBaseUnit = - getRouteBuyAmountBeforeFeesCryptoBaseUnit(quote) - - // No memo for trade rates - const memo = '' - - // TODO(gomes): for UTXOs, we should be able to get a very rough estimation (not taking users' UTXOs into account) - // using sats per byte and byte size from memo. Yes, we don't have a memo returned, but can build it in-house for this purpose easily. - const feeData = { - networkFeeCryptoBaseUnit: undefined, - protocolFees: getProtocolFees(quote), - } - - const buyAmountAfterFeesCryptoBaseUnit = convertPrecision({ - value: expectedAmountOutThorBaseUnit, - inputExponent: THORCHAIN_FIXED_PRECISION, - outputExponent: buyAsset.precision, - }).toFixed() - - return { - id: uuid(), - accountNumber: undefined, - memo, - receiveAddress: undefined, - affiliateBps, - potentialAffiliateBps, - isStreaming, - recommendedMinimumCryptoBaseUnit, - tradeType: tradeType ?? TradeType.L1ToL1, - expiry: quote.expiry, - slippageTolerancePercentageDecimal: isStreaming - ? undefined - : slippageTolerancePercentageDecimal, - rate, - steps: [ - { - estimatedExecutionTimeMs, - rate, - sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, - buyAmountBeforeFeesCryptoBaseUnit, - buyAmountAfterFeesCryptoBaseUnit, - source, - buyAsset, - sellAsset, - // TODO(gomes): when we actually split between TradeQuote and TradeRate in https://github.com/shapeshift/web/issues/7941, - // this won't be an issue anymore - for now this is tackled at runtime with the isConnected check above - accountNumber: accountNumber!, - allowanceContract: '0x0', // not applicable to UTXOs - feeData, - }, - ], - } - }, - ), - ) - - const routes = maybeRoutes.filter(isFulfilled).map(maybeRoute => maybeRoute.value) - - // if no routes succeeded, return failure from swapper - if (!routes.length) - return Err( - makeSwapErrorRight({ - message: 'Unable to create any routes', - code: TradeQuoteError.UnsupportedTradePair, - cause: maybeRoutes.filter(isRejected).map(maybeRoute => maybeRoute.reason), - }), - ) - - // otherwise, return all that succeeded - return Ok(routes) - } - - case CHAIN_NAMESPACE.CosmosSdk: { - const cosmosChainAdapter = deps.assertGetCosmosSdkChainAdapter(sellAsset.chainId) - const feeData = await cosmosChainAdapter.getFeeData({}) - - return Ok( - perRouteValues.map( - ({ - source, - quote, - expectedAmountOutThorBaseUnit, - isStreaming, - estimatedExecutionTimeMs, - affiliateBps, - }): ThorTradeUtxoOrCosmosRate => { - const rate = getRouteRate(expectedAmountOutThorBaseUnit) - const buyAmountBeforeFeesCryptoBaseUnit = - getRouteBuyAmountBeforeFeesCryptoBaseUnit(quote) - - const buyAmountAfterFeesCryptoBaseUnit = convertPrecision({ - value: expectedAmountOutThorBaseUnit, - inputExponent: THORCHAIN_FIXED_PRECISION, - outputExponent: buyAsset.precision, - }).toFixed() - - // No memo returned for rates - const memo = '' - - return { - id: uuid(), - accountNumber: undefined, - memo, - receiveAddress: undefined, - affiliateBps, - potentialAffiliateBps, - isStreaming, - recommendedMinimumCryptoBaseUnit, - expiry: quote.expiry, - slippageTolerancePercentageDecimal: isStreaming - ? undefined - : slippageTolerancePercentageDecimal, - rate, - tradeType: tradeType ?? TradeType.L1ToL1, - steps: [ - { - estimatedExecutionTimeMs, - rate, - sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, - buyAmountBeforeFeesCryptoBaseUnit, - buyAmountAfterFeesCryptoBaseUnit, - source, - buyAsset, - sellAsset, - // TODO(gomes): when we actually split between TradeQuote and TradeRate in https://github.com/shapeshift/web/issues/7941, - // this won't be an issue anymore - for now this is tackled at runtime with the isConnected check above - accountNumber: accountNumber!, + accountNumber, allowanceContract: '0x0', // not applicable to cosmos feeData: { networkFeeCryptoBaseUnit: feeData.fast.txFee, diff --git a/packages/swapper/src/swappers/ThorchainSwapper/utils/getLongtailQuote.ts b/packages/swapper/src/swappers/ThorchainSwapper/utils/getLongtailQuote.ts index 6f5aff8da37..52e75d0b436 100644 --- a/packages/swapper/src/swappers/ThorchainSwapper/utils/getLongtailQuote.ts +++ b/packages/swapper/src/swappers/ThorchainSwapper/utils/getLongtailQuote.ts @@ -10,17 +10,15 @@ import assert from 'assert' import type { CommonTradeQuoteInput, - GetTradeRateInput, MultiHopTradeQuoteSteps, - MultiHopTradeRateSteps, SwapErrorRight, SwapperDeps, } from '../../../types' import { TradeQuoteError } from '../../../types' import { makeSwapErrorRight } from '../../../utils' -import type { ThorTradeQuote, ThorTradeRate } from '../getThorTradeQuoteOrRate/getTradeQuoteOrRate' +import type { ThorTradeQuote } from '../types' import { getBestAggregator } from './getBestAggregator' -import { getL1Quote, getL1Rate } from './getL1quote' +import { getL1Quote } from './getL1quote' import { getTokenFromAsset, getWrappedToken, TradeType } from './longTailHelpers' // This just uses UniswapV3 to get the longtail quote for now. @@ -108,91 +106,3 @@ export const getLongtailToL1Quote = async ( return Ok(updatedQuotes) }) } - -// This just uses UniswapV3 to get the longtail quote for now. -export const getLongtailToL1Rate = async ( - input: GetTradeRateInput, - deps: SwapperDeps, - streamingInterval: number, -): Promise> => { - const { sellAsset, sellAmountIncludingProtocolFeesCryptoBaseUnit } = input - - /* - We only support ethereum longtail -> L1 swaps for now. - We can later add BSC via UniV3, or Avalanche (e.g. via PancakeSwap) - */ - if (sellAsset.chainId !== ethChainId) { - return Err( - makeSwapErrorRight({ - message: `[getThorTradeQuote] - Unsupported chainId ${sellAsset.chainId}.`, - code: TradeQuoteError.UnsupportedChain, - details: { sellAssetChainId: sellAsset.chainId }, - }), - ) - } - - const sellChainId = sellAsset.chainId - const buyAssetFeeAssetId = deps.assertGetChainAdapter(sellChainId)?.getFeeAssetId() - const buyAssetFeeAsset = buyAssetFeeAssetId ? deps.assetsById[buyAssetFeeAssetId] : undefined - if (!buyAssetFeeAsset) { - return Err( - makeSwapErrorRight({ - message: `[getThorTradeQuote] - No native buy asset found for ${sellChainId}.`, - code: TradeQuoteError.InternalError, - details: { sellAssetChainId: sellChainId }, - }), - ) - } - - // TODO: use more than just UniswapV3, and also consider trianglar routes. - const publicClient = viemClientByChainId[sellChainId as EvmChainId] - assert(publicClient !== undefined, `no public client found for chainId '${sellChainId}'`) - - const maybeBestAggregator = await getBestAggregator( - buyAssetFeeAsset, - getTokenFromAsset(sellAsset), - getWrappedToken(buyAssetFeeAsset), - sellAmountIncludingProtocolFeesCryptoBaseUnit, - ) - - if (maybeBestAggregator.isErr()) { - return Err(maybeBestAggregator.unwrapErr()) - } - - const { bestAggregator, quotedAmountOut } = maybeBestAggregator.unwrap() - - const l1Tol1QuoteInput: GetTradeRateInput = { - ...input, - sellAsset: buyAssetFeeAsset, - sellAmountIncludingProtocolFeesCryptoBaseUnit: quotedAmountOut.toString(), - } - - const thorchainRates = await getL1Rate( - l1Tol1QuoteInput, - deps, - streamingInterval, - TradeType.LongTailToL1, - ) - - return thorchainRates.andThen(rates => { - const updatedRates: ThorTradeRate[] = rates.map(q => ({ - ...q, - accountNumber: undefined, - aggregator: bestAggregator, - // This logic will need to be updated to support multi-hop, if that's ever implemented for THORChain - steps: q.steps.map(s => ({ - ...s, - accountNumber: undefined, - sellAmountIncludingProtocolFeesCryptoBaseUnit, - sellAsset, - allowanceContract: TS_AGGREGATOR_TOKEN_TRANSFER_PROXY_CONTRACT_MAINNET, - })) as MultiHopTradeRateSteps, // assuming multi-hop quote steps here since we're mapping over quote steps - isLongtail: true, - longtailData: { - longtailToL1ExpectedAmountOut: quotedAmountOut, - }, - })) - - return Ok(updatedRates) - }) -} diff --git a/packages/swapper/src/swappers/ThorchainSwapper/utils/getLongtailRate.ts b/packages/swapper/src/swappers/ThorchainSwapper/utils/getLongtailRate.ts new file mode 100644 index 00000000000..f03b5b5a4e5 --- /dev/null +++ b/packages/swapper/src/swappers/ThorchainSwapper/utils/getLongtailRate.ts @@ -0,0 +1,110 @@ +import { ethChainId } from '@shapeshiftoss/caip' +import { + TS_AGGREGATOR_TOKEN_TRANSFER_PROXY_CONTRACT_MAINNET, + viemClientByChainId, +} from '@shapeshiftoss/contracts' +import type { EvmChainId } from '@shapeshiftoss/types' +import type { Result } from '@sniptt/monads' +import { Err, Ok } from '@sniptt/monads' +import assert from 'assert' + +import type { + GetTradeRateInput, + MultiHopTradeRateSteps, + SwapErrorRight, + SwapperDeps, +} from '../../../types' +import { TradeQuoteError } from '../../../types' +import { makeSwapErrorRight } from '../../../utils' +import type { ThorTradeRate } from '../types' +import { getBestAggregator } from './getBestAggregator' +import { getL1Rate } from './getL1Rate' +import { getTokenFromAsset, getWrappedToken, TradeType } from './longTailHelpers' + +// This just uses UniswapV3 to get the longtail quote for now. +export const getLongtailToL1Rate = async ( + input: GetTradeRateInput, + deps: SwapperDeps, + streamingInterval: number, +): Promise> => { + const { sellAsset, sellAmountIncludingProtocolFeesCryptoBaseUnit } = input + + /* + We only support ethereum longtail -> L1 swaps for now. + We can later add BSC via UniV3, or Avalanche (e.g. via PancakeSwap) + */ + if (sellAsset.chainId !== ethChainId) { + return Err( + makeSwapErrorRight({ + message: `[getThorTradeQuote] - Unsupported chainId ${sellAsset.chainId}.`, + code: TradeQuoteError.UnsupportedChain, + details: { sellAssetChainId: sellAsset.chainId }, + }), + ) + } + + const sellChainId = sellAsset.chainId + const buyAssetFeeAssetId = deps.assertGetChainAdapter(sellChainId)?.getFeeAssetId() + const buyAssetFeeAsset = buyAssetFeeAssetId ? deps.assetsById[buyAssetFeeAssetId] : undefined + if (!buyAssetFeeAsset) { + return Err( + makeSwapErrorRight({ + message: `[getThorTradeQuote] - No native buy asset found for ${sellChainId}.`, + code: TradeQuoteError.InternalError, + details: { sellAssetChainId: sellChainId }, + }), + ) + } + + // TODO: use more than just UniswapV3, and also consider trianglar routes. + const publicClient = viemClientByChainId[sellChainId as EvmChainId] + assert(publicClient !== undefined, `no public client found for chainId '${sellChainId}'`) + + const maybeBestAggregator = await getBestAggregator( + buyAssetFeeAsset, + getTokenFromAsset(sellAsset), + getWrappedToken(buyAssetFeeAsset), + sellAmountIncludingProtocolFeesCryptoBaseUnit, + ) + + if (maybeBestAggregator.isErr()) { + return Err(maybeBestAggregator.unwrapErr()) + } + + const { bestAggregator, quotedAmountOut } = maybeBestAggregator.unwrap() + + const l1Tol1QuoteInput: GetTradeRateInput = { + ...input, + sellAsset: buyAssetFeeAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit: quotedAmountOut.toString(), + } + + const thorchainRates = await getL1Rate( + l1Tol1QuoteInput, + deps, + streamingInterval, + TradeType.LongTailToL1, + ) + + return thorchainRates.andThen(rates => { + const updatedRates: ThorTradeRate[] = rates.map(q => ({ + ...q, + accountNumber: undefined, + aggregator: bestAggregator, + // This logic will need to be updated to support multi-hop, if that's ever implemented for THORChain + steps: q.steps.map(s => ({ + ...s, + accountNumber: undefined, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + sellAsset, + allowanceContract: TS_AGGREGATOR_TOKEN_TRANSFER_PROXY_CONTRACT_MAINNET, + })) as MultiHopTradeRateSteps, // assuming multi-hop quote steps here since we're mapping over quote steps + isLongtail: true, + longtailData: { + longtailToL1ExpectedAmountOut: quotedAmountOut, + }, + })) + + return Ok(updatedRates) + }) +} diff --git a/packages/swapper/src/swappers/ThorchainSwapper/utils/getQuote/getQuote.ts b/packages/swapper/src/swappers/ThorchainSwapper/utils/getQuote/getQuote.ts index e0a99ff8162..e003b40d513 100644 --- a/packages/swapper/src/swappers/ThorchainSwapper/utils/getQuote/getQuote.ts +++ b/packages/swapper/src/swappers/ThorchainSwapper/utils/getQuote/getQuote.ts @@ -89,7 +89,7 @@ const _getQuote = async ( } else if (isError && /trading is halted/.test(data.error)) { return Err( makeSwapErrorRight({ - message: `[getTradeRate]: Trading is halted, cannot process swap`, + message: `[_getQuote]: Trading is halted, cannot process swap`, code: TradeQuoteError.TradingHalted, details: { sellAssetId: sellAsset.assetId, buyAssetId }, }), diff --git a/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts b/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts index aae058dd898..30f6d8e900f 100644 --- a/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/ZrxSwapper/endpoints.ts @@ -21,7 +21,8 @@ import { type TradeQuote, } from '../../types' import { checkEvmSwapStatus, isExecutableTradeQuote } from '../../utils' -import { getZrxTradeQuote, getZrxTradeRate } from './getZrxTradeQuote/getZrxTradeQuote' +import { getZrxTradeQuote } from './getZrxTradeQuote/getZrxTradeQuote' +import { getZrxTradeRate } from './getZrxTradeRate/getZrxTradeRate' export const zrxApi: SwapperApi = { getTradeQuote: async ( diff --git a/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts b/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts index c7e37ed73c0..d0e0c966e8a 100644 --- a/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts +++ b/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts @@ -14,23 +14,15 @@ import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constant import type { GetEvmTradeQuoteInput, GetEvmTradeQuoteInputBase, - GetEvmTradeRateInput, SingleHopTradeQuoteSteps, - SingleHopTradeRateSteps, SwapErrorRight, TradeQuote, TradeQuoteStep, - TradeRate, } from '../../../types' import { SwapperName, TradeQuoteError } from '../../../types' import { makeSwapErrorRight } from '../../../utils' import { isNativeEvmAsset } from '../../utils/helpers/helpers' -import { - fetchZrxPermit2Price, - fetchZrxPermit2Quote, - fetchZrxPrice, - fetchZrxQuote, -} from '../utils/fetchFromZrx' +import { fetchZrxPermit2Quote, fetchZrxQuote } from '../utils/fetchFromZrx' import { assetIdToZrxToken, isSupportedChainId, zrxTokenToAssetId } from '../utils/helpers/helpers' export function getZrxTradeQuote( @@ -44,17 +36,6 @@ export function getZrxTradeQuote( return _getZrxPermit2TradeQuote(input, assertGetEvmChainAdapter, assetsById, zrxBaseUrl) } -export function getZrxTradeRate( - input: GetEvmTradeRateInput, - assertGetEvmChainAdapter: (chainId: ChainId) => EvmChainAdapter, - isPermit2Enabled: boolean, - assetsById: AssetsByIdPartial, - zrxBaseUrl: string, -): Promise> { - if (!isPermit2Enabled) return _getZrxTradeRate(input, assertGetEvmChainAdapter, zrxBaseUrl) - return _getZrxPermit2TradeRate(input, assertGetEvmChainAdapter, assetsById, zrxBaseUrl) -} - async function _getZrxTradeQuote( input: GetEvmTradeQuoteInput, _assertGetEvmChainAdapter: (chainId: ChainId) => EvmChainAdapter, @@ -193,143 +174,6 @@ async function _getZrxTradeQuote( } } -async function _getZrxTradeRate( - input: GetEvmTradeRateInput, - assertGetEvmChainAdapter: (chainId: ChainId) => EvmChainAdapter, - zrxBaseUrl: string, -): Promise> { - const { - sellAsset, - buyAsset, - accountNumber, - receiveAddress, - affiliateBps, - potentialAffiliateBps, - chainId, - supportsEIP1559, - sellAmountIncludingProtocolFeesCryptoBaseUnit, - } = input - - const slippageTolerancePercentageDecimal = - input.slippageTolerancePercentageDecimal ?? - getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Zrx) - - const sellAssetChainId = sellAsset.chainId - const buyAssetChainId = buyAsset.chainId - - if (!isSupportedChainId(sellAssetChainId)) { - return Err( - makeSwapErrorRight({ - message: `unsupported chainId`, - code: TradeQuoteError.UnsupportedChain, - details: { chainId: sellAsset.chainId }, - }), - ) - } - - if (!isSupportedChainId(buyAssetChainId)) { - return Err( - makeSwapErrorRight({ - message: `unsupported chainId`, - code: TradeQuoteError.UnsupportedChain, - details: { chainId: sellAsset.chainId }, - }), - ) - } - - if (sellAssetChainId !== buyAssetChainId) { - return Err( - makeSwapErrorRight({ - message: `cross-chain not supported - both assets must be on chainId ${sellAsset.chainId}`, - code: TradeQuoteError.CrossChainNotSupported, - details: { buyAsset, sellAsset }, - }), - ) - } - - const maybeZrxPriceResponse = await fetchZrxPrice({ - buyAsset, - sellAsset, - sellAmountIncludingProtocolFeesCryptoBaseUnit, - // Cross-account not supported for ZRX - sellAddress: receiveAddress, - affiliateBps, - slippageTolerancePercentageDecimal, - zrxBaseUrl, - }) - - if (maybeZrxPriceResponse.isErr()) return Err(maybeZrxPriceResponse.unwrapErr()) - const zrxPriceResponse = maybeZrxPriceResponse.unwrap() - - const { - buyAmount: buyAmountAfterFeesCryptoBaseUnit, - grossBuyAmount: buyAmountBeforeFeesCryptoBaseUnit, - price, - allowanceTarget, - gas, - expectedSlippage, - } = zrxPriceResponse - - const useSellAmount = !!sellAmountIncludingProtocolFeesCryptoBaseUnit - const rate = useSellAmount ? price : bn(1).div(price).toString() - - // 0x approvals are cheaper than trades, but we don't have dynamic quote data for them. - // Instead, we use a hardcoded gasLimit estimate in place of the estimatedGas in the 0x quote response. - try { - const adapter = assertGetEvmChainAdapter(chainId) - const { average } = await adapter.getGasFeeData() - const networkFeeCryptoBaseUnit = evm.calcNetworkFeeCryptoBaseUnit({ - ...average, - supportsEIP1559: Boolean(supportsEIP1559), - // add gas limit buffer to account for the fact we perform all of our validation on the trade quote estimations - // which are inaccurate and not what we use for the tx to broadcast - gasLimit: bnOrZero(gas).times(1.2).toFixed(), - }) - - return Ok({ - id: uuid(), - accountNumber: undefined, - receiveAddress: undefined, - potentialAffiliateBps, - affiliateBps, - // Slippage protection is only provided for specific pairs. - // If slippage protection is not provided, assume a no slippage limit. - // If slippage protection is provided, return the limit instead of the estimated slippage. - // https://0x.org/docs/0x-swap-api/api-references/get-swap-v1-quote - slippageTolerancePercentageDecimal: expectedSlippage - ? slippageTolerancePercentageDecimal - : undefined, - rate, - steps: [ - { - estimatedExecutionTimeMs: undefined, - allowanceContract: allowanceTarget, - buyAsset, - sellAsset, - accountNumber, - rate, - feeData: { - protocolFees: {}, - networkFeeCryptoBaseUnit, // TODO(gomes): do we still want to handle L1 fee here for rates, since we leverage upstream fees calcs? - }, - buyAmountBeforeFeesCryptoBaseUnit, - buyAmountAfterFeesCryptoBaseUnit, - sellAmountIncludingProtocolFeesCryptoBaseUnit, - source: SwapperName.Zrx, - }, - ] as SingleHopTradeRateSteps, - }) - } catch (err) { - return Err( - makeSwapErrorRight({ - message: 'failed to get fee data', - cause: err, - code: TradeQuoteError.NetworkFeeEstimationFailed, - }), - ) - } -} - async function _getZrxPermit2TradeQuote( input: GetEvmTradeQuoteInputBase, assertGetEvmChainAdapter: (chainId: ChainId) => EvmChainAdapter, @@ -524,118 +368,3 @@ async function _getZrxPermit2TradeQuote( ) } } - -// TODO(gomes): consume me -async function _getZrxPermit2TradeRate( - input: GetEvmTradeRateInput, - _assertGetEvmChainAdapter: (chainId: ChainId) => EvmChainAdapter, - _assetsById: AssetsByIdPartial, - zrxBaseUrl: string, -): Promise> { - const { - sellAsset, - buyAsset, - accountNumber, - receiveAddress, - affiliateBps, - potentialAffiliateBps, - sellAmountIncludingProtocolFeesCryptoBaseUnit, - } = input - - const slippageTolerancePercentageDecimal = - input.slippageTolerancePercentageDecimal ?? - getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Zrx) - - const sellAssetChainId = sellAsset.chainId - const buyAssetChainId = buyAsset.chainId - - if (!isSupportedChainId(sellAssetChainId)) { - return Err( - makeSwapErrorRight({ - message: `unsupported chainId`, - code: TradeQuoteError.UnsupportedChain, - details: { chainId: sellAsset.chainId }, - }), - ) - } - - if (!isSupportedChainId(buyAssetChainId)) { - return Err( - makeSwapErrorRight({ - message: `unsupported chainId`, - code: TradeQuoteError.UnsupportedChain, - details: { chainId: sellAsset.chainId }, - }), - ) - } - - if (sellAssetChainId !== buyAssetChainId) { - return Err( - makeSwapErrorRight({ - message: `cross-chain not supported - both assets must be on chainId ${sellAsset.chainId}`, - code: TradeQuoteError.CrossChainNotSupported, - details: { buyAsset, sellAsset }, - }), - ) - } - - const maybeZrxPriceResponse = await fetchZrxPermit2Price({ - buyAsset, - sellAsset, - sellAmountIncludingProtocolFeesCryptoBaseUnit, - // Cross-account not supported for ZRX - sellAddress: receiveAddress, - affiliateBps, - slippageTolerancePercentageDecimal, - zrxBaseUrl, - }) - - if (maybeZrxPriceResponse.isErr()) return Err(maybeZrxPriceResponse.unwrapErr()) - const zrxPriceResponse = maybeZrxPriceResponse.unwrap() - - const { - buyAmount: buyAmountAfterFeesCryptoBaseUnit, - minBuyAmount: buyAmountBeforeFeesCryptoBaseUnit, - totalNetworkFee, - } = zrxPriceResponse - - const rate = bnOrZero(buyAmountAfterFeesCryptoBaseUnit) - .div(sellAmountIncludingProtocolFeesCryptoBaseUnit) - .toString() - - // 0x approvals are cheaper than trades, but we don't have dynamic quote data for them. - // Instead, we use a hardcoded gasLimit estimate in place of the estimatedGas in the 0x quote response. - const networkFeeCryptoBaseUnit = totalNetworkFee - return Ok({ - id: uuid(), - accountNumber: undefined, - receiveAddress: undefined, - potentialAffiliateBps, - affiliateBps, - // Slippage protection is only provided for specific pairs. - // If slippage protection is not provided, assume a no slippage limit. - // If slippage protection is provided, return the limit instead of the estimated slippage. - // https://0x.org/docs/0x-swap-api/api-references/get-swap-v1-quote - slippageTolerancePercentageDecimal, - rate, - steps: [ - { - estimatedExecutionTimeMs: undefined, - // We don't care about this - this is a rate, and if we really wanted to, we know the permit2 allowance target - allowanceContract: isNativeEvmAsset(sellAsset.assetId) ? undefined : PERMIT2_CONTRACT, - buyAsset, - sellAsset, - accountNumber, - rate, - feeData: { - protocolFees: {}, - networkFeeCryptoBaseUnit, // L1 fee added inside of evm.calcNetworkFeeCryptoBaseUnit - }, - buyAmountBeforeFeesCryptoBaseUnit, - buyAmountAfterFeesCryptoBaseUnit, - sellAmountIncludingProtocolFeesCryptoBaseUnit, - source: SwapperName.Zrx, - }, - ] as unknown as SingleHopTradeRateSteps, - }) -} diff --git a/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeRate/getZrxTradeRate.ts b/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeRate/getZrxTradeRate.ts new file mode 100644 index 00000000000..9926cd477c2 --- /dev/null +++ b/packages/swapper/src/swappers/ZrxSwapper/getZrxTradeRate/getZrxTradeRate.ts @@ -0,0 +1,284 @@ +import type { ChainId } from '@shapeshiftoss/caip' +import type { EvmChainAdapter } from '@shapeshiftoss/chain-adapters' +import { evm } from '@shapeshiftoss/chain-adapters' +import { PERMIT2_CONTRACT } from '@shapeshiftoss/contracts' +import type { AssetsByIdPartial } from '@shapeshiftoss/types' +import { bn, bnOrZero } from '@shapeshiftoss/utils' +import type { Result } from '@sniptt/monads' +import { Err, Ok } from '@sniptt/monads' +import { v4 as uuid } from 'uuid' + +import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants' +import type { + GetEvmTradeRateInput, + SingleHopTradeRateSteps, + SwapErrorRight, + TradeRate, +} from '../../../types' +import { SwapperName, TradeQuoteError } from '../../../types' +import { makeSwapErrorRight } from '../../../utils' +import { isNativeEvmAsset } from '../../utils/helpers/helpers' +import { fetchZrxPermit2Price, fetchZrxPrice } from '../utils/fetchFromZrx' +import { isSupportedChainId } from '../utils/helpers/helpers' + +export function getZrxTradeRate( + input: GetEvmTradeRateInput, + assertGetEvmChainAdapter: (chainId: ChainId) => EvmChainAdapter, + isPermit2Enabled: boolean, + assetsById: AssetsByIdPartial, + zrxBaseUrl: string, +): Promise> { + if (!isPermit2Enabled) return _getZrxTradeRate(input, assertGetEvmChainAdapter, zrxBaseUrl) + return _getZrxPermit2TradeRate(input, assertGetEvmChainAdapter, assetsById, zrxBaseUrl) +} + +async function _getZrxTradeRate( + input: GetEvmTradeRateInput, + assertGetEvmChainAdapter: (chainId: ChainId) => EvmChainAdapter, + zrxBaseUrl: string, +): Promise> { + const { + sellAsset, + buyAsset, + accountNumber, + receiveAddress, + affiliateBps, + potentialAffiliateBps, + chainId, + supportsEIP1559, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + } = input + + const slippageTolerancePercentageDecimal = + input.slippageTolerancePercentageDecimal ?? + getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Zrx) + + const sellAssetChainId = sellAsset.chainId + const buyAssetChainId = buyAsset.chainId + + if (!isSupportedChainId(sellAssetChainId)) { + return Err( + makeSwapErrorRight({ + message: `unsupported chainId`, + code: TradeQuoteError.UnsupportedChain, + details: { chainId: sellAsset.chainId }, + }), + ) + } + + if (!isSupportedChainId(buyAssetChainId)) { + return Err( + makeSwapErrorRight({ + message: `unsupported chainId`, + code: TradeQuoteError.UnsupportedChain, + details: { chainId: sellAsset.chainId }, + }), + ) + } + + if (sellAssetChainId !== buyAssetChainId) { + return Err( + makeSwapErrorRight({ + message: `cross-chain not supported - both assets must be on chainId ${sellAsset.chainId}`, + code: TradeQuoteError.CrossChainNotSupported, + details: { buyAsset, sellAsset }, + }), + ) + } + + const maybeZrxPriceResponse = await fetchZrxPrice({ + buyAsset, + sellAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + // Cross-account not supported for ZRX + sellAddress: receiveAddress, + affiliateBps, + slippageTolerancePercentageDecimal, + zrxBaseUrl, + }) + + if (maybeZrxPriceResponse.isErr()) return Err(maybeZrxPriceResponse.unwrapErr()) + const zrxPriceResponse = maybeZrxPriceResponse.unwrap() + + const { + buyAmount: buyAmountAfterFeesCryptoBaseUnit, + grossBuyAmount: buyAmountBeforeFeesCryptoBaseUnit, + price, + allowanceTarget, + gas, + expectedSlippage, + } = zrxPriceResponse + + const useSellAmount = !!sellAmountIncludingProtocolFeesCryptoBaseUnit + const rate = useSellAmount ? price : bn(1).div(price).toString() + + // 0x approvals are cheaper than trades, but we don't have dynamic quote data for them. + // Instead, we use a hardcoded gasLimit estimate in place of the estimatedGas in the 0x quote response. + try { + const adapter = assertGetEvmChainAdapter(chainId) + const { average } = await adapter.getGasFeeData() + const networkFeeCryptoBaseUnit = evm.calcNetworkFeeCryptoBaseUnit({ + ...average, + supportsEIP1559: Boolean(supportsEIP1559), + // add gas limit buffer to account for the fact we perform all of our validation on the trade quote estimations + // which are inaccurate and not what we use for the tx to broadcast + gasLimit: bnOrZero(gas).times(1.2).toFixed(), + }) + + return Ok({ + id: uuid(), + accountNumber: undefined, + receiveAddress: undefined, + potentialAffiliateBps, + affiliateBps, + // Slippage protection is only provided for specific pairs. + // If slippage protection is not provided, assume a no slippage limit. + // If slippage protection is provided, return the limit instead of the estimated slippage. + // https://0x.org/docs/0x-swap-api/api-references/get-swap-v1-quote + slippageTolerancePercentageDecimal: expectedSlippage + ? slippageTolerancePercentageDecimal + : undefined, + rate, + steps: [ + { + estimatedExecutionTimeMs: undefined, + allowanceContract: allowanceTarget, + buyAsset, + sellAsset, + accountNumber, + rate, + feeData: { + protocolFees: {}, + networkFeeCryptoBaseUnit, // TODO(gomes): do we still want to handle L1 fee here for rates, since we leverage upstream fees calcs? + }, + buyAmountBeforeFeesCryptoBaseUnit, + buyAmountAfterFeesCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + source: SwapperName.Zrx, + }, + ] as SingleHopTradeRateSteps, + }) + } catch (err) { + return Err( + makeSwapErrorRight({ + message: 'failed to get fee data', + cause: err, + code: TradeQuoteError.NetworkFeeEstimationFailed, + }), + ) + } +} + +async function _getZrxPermit2TradeRate( + input: GetEvmTradeRateInput, + _assertGetEvmChainAdapter: (chainId: ChainId) => EvmChainAdapter, + _assetsById: AssetsByIdPartial, + zrxBaseUrl: string, +): Promise> { + const { + sellAsset, + buyAsset, + accountNumber, + receiveAddress, + affiliateBps, + potentialAffiliateBps, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + } = input + + const slippageTolerancePercentageDecimal = + input.slippageTolerancePercentageDecimal ?? + getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Zrx) + + const sellAssetChainId = sellAsset.chainId + const buyAssetChainId = buyAsset.chainId + + if (!isSupportedChainId(sellAssetChainId)) { + return Err( + makeSwapErrorRight({ + message: `unsupported chainId`, + code: TradeQuoteError.UnsupportedChain, + details: { chainId: sellAsset.chainId }, + }), + ) + } + + if (!isSupportedChainId(buyAssetChainId)) { + return Err( + makeSwapErrorRight({ + message: `unsupported chainId`, + code: TradeQuoteError.UnsupportedChain, + details: { chainId: sellAsset.chainId }, + }), + ) + } + + if (sellAssetChainId !== buyAssetChainId) { + return Err( + makeSwapErrorRight({ + message: `cross-chain not supported - both assets must be on chainId ${sellAsset.chainId}`, + code: TradeQuoteError.CrossChainNotSupported, + details: { buyAsset, sellAsset }, + }), + ) + } + + const maybeZrxPriceResponse = await fetchZrxPermit2Price({ + buyAsset, + sellAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + // Cross-account not supported for ZRX + sellAddress: receiveAddress, + affiliateBps, + slippageTolerancePercentageDecimal, + zrxBaseUrl, + }) + + if (maybeZrxPriceResponse.isErr()) return Err(maybeZrxPriceResponse.unwrapErr()) + const zrxPriceResponse = maybeZrxPriceResponse.unwrap() + + const { + buyAmount: buyAmountAfterFeesCryptoBaseUnit, + minBuyAmount: buyAmountBeforeFeesCryptoBaseUnit, + totalNetworkFee, + } = zrxPriceResponse + + const rate = bnOrZero(buyAmountAfterFeesCryptoBaseUnit) + .div(sellAmountIncludingProtocolFeesCryptoBaseUnit) + .toString() + + // 0x approvals are cheaper than trades, but we don't have dynamic quote data for them. + // Instead, we use a hardcoded gasLimit estimate in place of the estimatedGas in the 0x quote response. + const networkFeeCryptoBaseUnit = totalNetworkFee + return Ok({ + id: uuid(), + accountNumber: undefined, + receiveAddress: undefined, + potentialAffiliateBps, + affiliateBps, + // Slippage protection is only provided for specific pairs. + // If slippage protection is not provided, assume a no slippage limit. + // If slippage protection is provided, return the limit instead of the estimated slippage. + // https://0x.org/docs/0x-swap-api/api-references/get-swap-v1-quote + slippageTolerancePercentageDecimal, + rate, + steps: [ + { + estimatedExecutionTimeMs: undefined, + // We don't care about this - this is a rate, and if we really wanted to, we know the permit2 allowance target + allowanceContract: isNativeEvmAsset(sellAsset.assetId) ? undefined : PERMIT2_CONTRACT, + buyAsset, + sellAsset, + accountNumber, + rate, + feeData: { + protocolFees: {}, + networkFeeCryptoBaseUnit, // L1 fee added inside of evm.calcNetworkFeeCryptoBaseUnit + }, + buyAmountBeforeFeesCryptoBaseUnit, + buyAmountAfterFeesCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + source: SwapperName.Zrx, + }, + ] as unknown as SingleHopTradeRateSteps, + }) +} diff --git a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx index 049adcec13e..f985184b7f8 100644 --- a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx @@ -1,6 +1,6 @@ import { isLedger } from '@shapeshiftoss/hdwallet-ledger' import { isArbitrumBridgeTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ArbitrumBridgeSwapper/getTradeQuote/getTradeQuote' -import type { ThorTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/getThorTradeQuoteOrRate/getTradeQuoteOrRate' +import type { ThorTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/types' import type { Asset } from '@shapeshiftoss/types' import { positiveOrZero } from '@shapeshiftoss/utils' import type { FormEvent } from 'react' diff --git a/src/components/MultiHopTrade/helpers.ts b/src/components/MultiHopTrade/helpers.ts index 425669f4b6a..090a15c5aff 100644 --- a/src/components/MultiHopTrade/helpers.ts +++ b/src/components/MultiHopTrade/helpers.ts @@ -1,4 +1,4 @@ -import { isThorTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/getThorTradeQuoteOrRate/getTradeQuoteOrRate' +import { isThorTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote' import { getMaybeCompositeAssetSymbol } from 'lib/mixpanel/helpers' import type { ReduxState } from 'state/reducer' import { selectAssets, selectFeeAssetById } from 'state/slices/selectors' diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks.tsx/useGetSwapperTradeQuote.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks/useGetSwapperTradeQuote.tsx similarity index 100% rename from src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks.tsx/useGetSwapperTradeQuote.tsx rename to src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks/useGetSwapperTradeQuote.tsx diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index ad149cb9fbd..20e1db46a00 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -8,7 +8,7 @@ import { SwapperName, swappers, } from '@shapeshiftoss/swapper' -import { isThorTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/getThorTradeQuoteOrRate/getTradeQuoteOrRate' +import { isThorTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote' import { skipToken as reactQuerySkipToken, useQuery } from '@tanstack/react-query' import { useCallback, useEffect, useMemo, useRef } from 'react' import { useTradeReceiveAddress } from 'components/MultiHopTrade/components/TradeInput/hooks/useTradeReceiveAddress' @@ -48,8 +48,8 @@ import { tradeQuoteSlice } from 'state/slices/tradeQuoteSlice/tradeQuoteSlice' import { HopExecutionState, TransactionExecutionState } from 'state/slices/tradeQuoteSlice/types' import { store, useAppDispatch, useAppSelector } from 'state/store' -import type { UseGetSwapperTradeQuoteArgs } from './hooks.tsx/useGetSwapperTradeQuote' -import { useGetSwapperTradeQuote } from './hooks.tsx/useGetSwapperTradeQuote' +import type { UseGetSwapperTradeQuoteArgs } from './hooks/useGetSwapperTradeQuote' +import { useGetSwapperTradeQuote } from './hooks/useGetSwapperTradeQuote' type MixPanelQuoteMeta = { swapperName: SwapperName diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx index d68579339e8..c11a851a8e3 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx @@ -7,7 +7,7 @@ import { SwapperName, swappers, } from '@shapeshiftoss/swapper' -import { isThorTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/getThorTradeQuoteOrRate/getTradeQuoteOrRate' +import { isThorTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote' import { useCallback, useEffect, useMemo, useState } from 'react' import { getTradeQuoteInput } from 'components/MultiHopTrade/hooks/useGetTradeQuotes/getTradeQuoteInput' import { useHasFocus } from 'hooks/useHasFocus' @@ -41,8 +41,8 @@ import { import { tradeQuoteSlice } from 'state/slices/tradeQuoteSlice/tradeQuoteSlice' import { store, useAppDispatch, useAppSelector } from 'state/store' -import type { UseGetSwapperTradeQuoteArgs } from './hooks.tsx/useGetSwapperTradeQuote' -import { useGetSwapperTradeQuote } from './hooks.tsx/useGetSwapperTradeQuote' +import type { UseGetSwapperTradeQuoteArgs } from './hooks/useGetSwapperTradeQuote' +import { useGetSwapperTradeQuote } from './hooks/useGetSwapperTradeQuote' type MixPanelQuoteMeta = { swapperName: SwapperName diff --git a/src/pages/RFOX/components/Stake/Bridge/hooks/useRfoxBridge.ts b/src/pages/RFOX/components/Stake/Bridge/hooks/useRfoxBridge.ts index 9d34d29d2cd..19e25b07f04 100644 --- a/src/pages/RFOX/components/Stake/Bridge/hooks/useRfoxBridge.ts +++ b/src/pages/RFOX/components/Stake/Bridge/hooks/useRfoxBridge.ts @@ -5,8 +5,8 @@ import { getEthersV5Provider, viemClientByChainId } from '@shapeshiftoss/contrac import { supportsETH } from '@shapeshiftoss/hdwallet-core' import type { SwapErrorRight, TradeQuote } from '@shapeshiftoss/swapper' import { arbitrumBridgeApi } from '@shapeshiftoss/swapper/dist/swappers/ArbitrumBridgeSwapper/endpoints' -import type { GetEvmTradeQuoteInputWithWallet } from '@shapeshiftoss/swapper/dist/swappers/ArbitrumBridgeSwapper/getTradeQuote/getTradeQuote' import { getTradeQuoteWithWallet } from '@shapeshiftoss/swapper/dist/swappers/ArbitrumBridgeSwapper/getTradeQuote/getTradeQuote' +import type { GetEvmTradeQuoteInputWithWallet } from '@shapeshiftoss/swapper/dist/swappers/ArbitrumBridgeSwapper/types' import type { Asset, MarketData } from '@shapeshiftoss/types' import { TxStatus } from '@shapeshiftoss/unchained-client' import { bnOrZero } from '@shapeshiftoss/utils' diff --git a/src/state/apis/swapper/helpers/validateTradeQuote.ts b/src/state/apis/swapper/helpers/validateTradeQuote.ts index 97ee25ed73b..75abafae958 100644 --- a/src/state/apis/swapper/helpers/validateTradeQuote.ts +++ b/src/state/apis/swapper/helpers/validateTradeQuote.ts @@ -5,7 +5,7 @@ import { SwapperName, TradeQuoteError as SwapperTradeQuoteError, } from '@shapeshiftoss/swapper' -import type { ThorTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/getThorTradeQuoteOrRate/getTradeQuoteOrRate' +import type { ThorTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/types' import type { KnownChainIds } from '@shapeshiftoss/types' import { getChainShortName } from 'components/MultiHopTrade/components/MultiHopTradeConfirm/utils/getChainShortName' import { isMultiHopTradeQuote } from 'components/MultiHopTrade/utils' diff --git a/src/state/apis/swapper/swapperApi.ts b/src/state/apis/swapper/swapperApi.ts index 572fa6b623b..9d86332c090 100644 --- a/src/state/apis/swapper/swapperApi.ts +++ b/src/state/apis/swapper/swapperApi.ts @@ -9,7 +9,7 @@ import { getTradeRates, SwapperName, } from '@shapeshiftoss/swapper' -import type { ThorEvmTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/getThorTradeQuoteOrRate/getTradeQuoteOrRate' +import type { ThorEvmTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/types' import { TradeType } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/utils/longTailHelpers' import { getConfig } from 'config' import { reactQueries } from 'react-queries' From 1cc6c8aa9893b06f4a8e696bceb28d1c76443dc5 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 18 Nov 2024 23:28:06 +0700 Subject: [PATCH 54/62] feat: rm todo --- .../MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index 20e1db46a00..966dcaf4fe6 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -151,7 +151,6 @@ export const useGetTradeQuotes = () => { return { tradeId: activeTradeId, - // TODO(gomes): multi-hop here hopIndex: 0, } }, [activeTradeId]) From add4215c627ce847ae6a2ba53e7a4f0a2e731f34 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 18 Nov 2024 23:33:14 +0700 Subject: [PATCH 55/62] feat: unbork chainflip --- .../MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx index c11a851a8e3..0652bd1fad9 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx @@ -260,6 +260,7 @@ export const useGetTradeRates = () => { useGetSwapperTradeQuote(getTradeQuoteArgs(SwapperName.LIFI)) useGetSwapperTradeQuote(getTradeQuoteArgs(SwapperName.Thorchain)) useGetSwapperTradeQuote(getTradeQuoteArgs(SwapperName.Zrx)) + useGetSwapperTradeQuote(getTradeQuoteArgs(SwapperName.Chainflip)) // true if any debounce, input or swapper is fetching const isAnyTradeQuoteLoading = useAppSelector(selectIsAnyTradeQuoteLoading) From 00c45623d1dec57b1c01143d500f28616527df31 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 18 Nov 2024 23:37:17 +0700 Subject: [PATCH 56/62] feat: cleanup --- .../MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index 966dcaf4fe6..66f792b4d22 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -130,7 +130,6 @@ export const useGetTradeQuotes = () => { const activeTradeIdRef = useRef() const activeQuoteMeta = useAppSelector(selectActiveQuoteMetaOrDefault) const activeQuoteMetaRef = useRef<{ swapperName: SwapperName; identifier: string } | undefined>() - // TODO(gomes): set trade execution of quote to the same as we stopped in rate to reconciliate things const confirmedTradeExecution = useAppSelector(selectConfirmedTradeExecution) useEffect( From 1d5fc11a0461e2309ceddd909113c76a35ad4e1a Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 19 Nov 2024 05:18:08 +0700 Subject: [PATCH 57/62] feat: better naming --- ....tsx => useGetSwapperTradeQuoteOrRate.tsx} | 6 +++--- .../useGetTradeQuotes/useGetTradeQuotes.tsx | 8 ++++---- .../useGetTradeQuotes/useGetTradeRates.tsx | 20 +++++++++---------- 3 files changed, 17 insertions(+), 17 deletions(-) rename src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks/{useGetSwapperTradeQuote.tsx => useGetSwapperTradeQuoteOrRate.tsx} (94%) diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks/useGetSwapperTradeQuote.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks/useGetSwapperTradeQuoteOrRate.tsx similarity index 94% rename from src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks/useGetSwapperTradeQuote.tsx rename to src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks/useGetSwapperTradeQuoteOrRate.tsx index 59924742b6e..7deeb68d1a5 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks/useGetSwapperTradeQuote.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks/useGetSwapperTradeQuoteOrRate.tsx @@ -5,19 +5,19 @@ import { swapperApi, useGetTradeQuoteQuery } from 'state/apis/swapper/swapperApi import { tradeQuoteSlice } from 'state/slices/tradeQuoteSlice/tradeQuoteSlice' import { useAppDispatch } from 'state/store' -export type UseGetSwapperTradeQuoteArgs = { +export type UseGetSwapperTradeQuoteOrRateArgs = { swapperName: SwapperName | undefined tradeQuoteInput: GetTradeQuoteInput | typeof skipToken skip: boolean pollingInterval: number | undefined } -export const useGetSwapperTradeQuote = ({ +export const useGetSwapperTradeQuoteOrRate = ({ swapperName, tradeQuoteInput, pollingInterval, skip, -}: UseGetSwapperTradeQuoteArgs) => { +}: UseGetSwapperTradeQuoteOrRateArgs) => { const dispatch = useAppDispatch() const tradeQuoteRequest = useMemo(() => { return skip || tradeQuoteInput === skipToken || !swapperName diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index 66f792b4d22..d4aa07450c6 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -48,8 +48,8 @@ import { tradeQuoteSlice } from 'state/slices/tradeQuoteSlice/tradeQuoteSlice' import { HopExecutionState, TransactionExecutionState } from 'state/slices/tradeQuoteSlice/types' import { store, useAppDispatch, useAppSelector } from 'state/store' -import type { UseGetSwapperTradeQuoteArgs } from './hooks/useGetSwapperTradeQuote' -import { useGetSwapperTradeQuote } from './hooks/useGetSwapperTradeQuote' +import type { UseGetSwapperTradeQuoteOrRateArgs } from './hooks/useGetSwapperTradeQuoteOrRate' +import { useGetSwapperTradeQuoteOrRate } from './hooks/useGetSwapperTradeQuoteOrRate' type MixPanelQuoteMeta = { swapperName: SwapperName @@ -323,7 +323,7 @@ export const useGetTradeQuotes = () => { }) const getTradeQuoteArgs = useCallback( - (swapperName: SwapperName | undefined): UseGetSwapperTradeQuoteArgs => { + (swapperName: SwapperName | undefined): UseGetSwapperTradeQuoteOrRateArgs => { return { swapperName, tradeQuoteInput: tradeQuoteInput ?? reduxSkipToken, @@ -337,7 +337,7 @@ export const useGetTradeQuotes = () => { [shouldFetchTradeQuotes, tradeQuoteInput], ) - const queryStateMeta = useGetSwapperTradeQuote( + const queryStateMeta = useGetSwapperTradeQuoteOrRate( getTradeQuoteArgs(activeQuoteMetaRef.current?.swapperName), ) diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx index 0652bd1fad9..51b6b596296 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx @@ -41,8 +41,8 @@ import { import { tradeQuoteSlice } from 'state/slices/tradeQuoteSlice/tradeQuoteSlice' import { store, useAppDispatch, useAppSelector } from 'state/store' -import type { UseGetSwapperTradeQuoteArgs } from './hooks/useGetSwapperTradeQuote' -import { useGetSwapperTradeQuote } from './hooks/useGetSwapperTradeQuote' +import type { UseGetSwapperTradeQuoteOrRateArgs } from './hooks/useGetSwapperTradeQuoteOrRate' +import { useGetSwapperTradeQuoteOrRate } from './hooks/useGetSwapperTradeQuoteOrRate' type MixPanelQuoteMeta = { swapperName: SwapperName @@ -242,7 +242,7 @@ export const useGetTradeRates = () => { ]) const getTradeQuoteArgs = useCallback( - (swapperName: SwapperName): UseGetSwapperTradeQuoteArgs => { + (swapperName: SwapperName): UseGetSwapperTradeQuoteOrRateArgs => { return { swapperName, tradeQuoteInput: tradeRateInput, @@ -254,13 +254,13 @@ export const useGetTradeRates = () => { [shouldRefetchTradeQuotes, tradeRateInput], ) - useGetSwapperTradeQuote(getTradeQuoteArgs(SwapperName.CowSwap)) - useGetSwapperTradeQuote(getTradeQuoteArgs(SwapperName.ArbitrumBridge)) - useGetSwapperTradeQuote(getTradeQuoteArgs(SwapperName.Portals)) - useGetSwapperTradeQuote(getTradeQuoteArgs(SwapperName.LIFI)) - useGetSwapperTradeQuote(getTradeQuoteArgs(SwapperName.Thorchain)) - useGetSwapperTradeQuote(getTradeQuoteArgs(SwapperName.Zrx)) - useGetSwapperTradeQuote(getTradeQuoteArgs(SwapperName.Chainflip)) + useGetSwapperTradeQuoteOrRate(getTradeQuoteArgs(SwapperName.CowSwap)) + useGetSwapperTradeQuoteOrRate(getTradeQuoteArgs(SwapperName.ArbitrumBridge)) + useGetSwapperTradeQuoteOrRate(getTradeQuoteArgs(SwapperName.Portals)) + useGetSwapperTradeQuoteOrRate(getTradeQuoteArgs(SwapperName.LIFI)) + useGetSwapperTradeQuoteOrRate(getTradeQuoteArgs(SwapperName.Thorchain)) + useGetSwapperTradeQuoteOrRate(getTradeQuoteArgs(SwapperName.Zrx)) + useGetSwapperTradeQuoteOrRate(getTradeQuoteArgs(SwapperName.Chainflip)) // true if any debounce, input or swapper is fetching const isAnyTradeQuoteLoading = useAppSelector(selectIsAnyTradeQuoteLoading) From f77e9ac741af72d8e6434631dda40092dbe14af7 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 19 Nov 2024 14:18:31 +0700 Subject: [PATCH 58/62] fix: emptiness checks --- .../useGetTradeQuotes/hooks/useGetSwapperTradeQuoteOrRate.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks/useGetSwapperTradeQuoteOrRate.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks/useGetSwapperTradeQuoteOrRate.tsx index 7deeb68d1a5..41bc63e02ad 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks/useGetSwapperTradeQuoteOrRate.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks/useGetSwapperTradeQuoteOrRate.tsx @@ -1,5 +1,6 @@ import { skipToken } from '@reduxjs/toolkit/dist/query' import type { GetTradeQuoteInput, SwapperName } from '@shapeshiftoss/swapper' +import isEmpty from 'lodash/isEmpty' import { useEffect, useMemo } from 'react' import { swapperApi, useGetTradeQuoteQuery } from 'state/apis/swapper/swapperApi' import { tradeQuoteSlice } from 'state/slices/tradeQuoteSlice/tradeQuoteSlice' @@ -53,7 +54,7 @@ export const useGetSwapperTradeQuoteOrRate = ({ useEffect(() => { if (!swapperName) return // Ensures we don't rug the state by upserting undefined data - this is *not* the place to do so and will rug the switch between quotes and rates - if (!queryStateMeta.data) return + if (isEmpty(queryStateMeta.data ?? {})) return dispatch( tradeQuoteSlice.actions.upsertTradeQuotes({ swapperName, From f8e2b7d0536343c636957209a2a5bcbbbfec58be Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 19 Nov 2024 14:19:01 +0700 Subject: [PATCH 59/62] fix: equality checks --- src/state/apis/swapper/swapperApi.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/state/apis/swapper/swapperApi.ts b/src/state/apis/swapper/swapperApi.ts index 9d86332c090..707c47f91d6 100644 --- a/src/state/apis/swapper/swapperApi.ts +++ b/src/state/apis/swapper/swapperApi.ts @@ -56,8 +56,7 @@ export const swapperApi = createApi({ quoteOrRate, } = tradeQuoteInput - const isCrossAccountTrade = - Boolean(sendAddress && receiveAddress) && sendAddress !== receiveAddress + const isCrossAccountTrade = sendAddress?.toLowerCase() !== receiveAddress?.toLowerCase() const featureFlags: FeatureFlags = selectFeatureFlags(state) const isSwapperEnabled = getEnabledSwappers(featureFlags, isCrossAccountTrade)[swapperName] From 358c8a7061452afb0526299254ab5cc815747f73 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 19 Nov 2024 17:37:53 +0700 Subject: [PATCH 60/62] fix: jfc 2 --- .../hooks/useGetSwapperTradeQuoteOrRate.tsx | 2 +- .../hooks/useGetTradeQuotes/useGetTradeQuotes.tsx | 5 ++++- .../hooks/useGetTradeQuotes/useGetTradeRates.tsx | 7 ++++++- src/state/apis/swapper/swapperApi.ts | 7 ++++++- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks/useGetSwapperTradeQuoteOrRate.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks/useGetSwapperTradeQuoteOrRate.tsx index 41bc63e02ad..a29ab80da01 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks/useGetSwapperTradeQuoteOrRate.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks/useGetSwapperTradeQuoteOrRate.tsx @@ -54,7 +54,7 @@ export const useGetSwapperTradeQuoteOrRate = ({ useEffect(() => { if (!swapperName) return // Ensures we don't rug the state by upserting undefined data - this is *not* the place to do so and will rug the switch between quotes and rates - if (isEmpty(queryStateMeta.data ?? {})) return + if (!queryStateMeta.data) return dispatch( tradeQuoteSlice.actions.upsertTradeQuotes({ swapperName, diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index d4aa07450c6..76770bc1e5f 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -208,9 +208,12 @@ export const useGetTradeQuotes = () => { const swapperName = activeQuoteMetaRef.current?.swapperName if (!swapperName) return const permit2 = hopExecutionMetadata?.permit2 + // ZRX is the odd one - we either want to fetch the final quote at pre-permit, or pre-swap input, depending on whether permit2 is required or not if (swapperName === SwapperName.Zrx) return ( - permit2?.isRequired && permit2?.state === TransactionExecutionState.AwaitingConfirmation + (permit2?.isRequired && + permit2?.state === TransactionExecutionState.AwaitingConfirmation) || + (!permit2?.isRequired && hopExecutionMetadata?.state === HopExecutionState.AwaitingSwap) ) return hopExecutionMetadata?.state === HopExecutionState.AwaitingSwap diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx index 51b6b596296..c0dc8e232d2 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeRates.tsx @@ -9,6 +9,7 @@ import { } from '@shapeshiftoss/swapper' import { isThorTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote' import { useCallback, useEffect, useMemo, useState } from 'react' +import { useTradeReceiveAddress } from 'components/MultiHopTrade/components/TradeInput/hooks/useTradeReceiveAddress' import { getTradeQuoteInput } from 'components/MultiHopTrade/hooks/useGetTradeQuotes/getTradeQuoteInput' import { useHasFocus } from 'hooks/useHasFocus' import { useWallet } from 'hooks/useWallet/useWallet' @@ -170,6 +171,9 @@ export const useGetTradeRates = () => { const shouldRefetchTradeQuotes = useMemo(() => hasFocus, [hasFocus]) + const { manualReceiveAddress, walletReceiveAddress } = useTradeReceiveAddress() + const receiveAddress = manualReceiveAddress ?? walletReceiveAddress + useEffect(() => { // Always invalidate tags when this effect runs - args have changed, and whether we want to fetch an actual quote // or a "skipToken" no-op, we always want to ensure that the tags are invalidated before a new query is ran @@ -209,7 +213,7 @@ export const useGetTradeRates = () => { buyAsset, wallet: wallet ?? undefined, quoteOrRate: 'rate', - receiveAddress: undefined, + receiveAddress, sellAmountBeforeFeesCryptoPrecision: sellAmountCryptoPrecision, allowMultiHop: true, affiliateBps, @@ -239,6 +243,7 @@ export const useGetTradeRates = () => { sellAccountId, isVotingPowerLoading, isBuyAssetChainSupported, + receiveAddress, ]) const getTradeQuoteArgs = useCallback( diff --git a/src/state/apis/swapper/swapperApi.ts b/src/state/apis/swapper/swapperApi.ts index 707c47f91d6..cbdc12ad855 100644 --- a/src/state/apis/swapper/swapperApi.ts +++ b/src/state/apis/swapper/swapperApi.ts @@ -56,7 +56,9 @@ export const swapperApi = createApi({ quoteOrRate, } = tradeQuoteInput - const isCrossAccountTrade = sendAddress?.toLowerCase() !== receiveAddress?.toLowerCase() + const isCrossAccountTrade = + Boolean(sendAddress && receiveAddress) && + sendAddress?.toLowerCase() !== receiveAddress?.toLowerCase() const featureFlags: FeatureFlags = selectFeatureFlags(state) const isSwapperEnabled = getEnabledSwappers(featureFlags, isCrossAccountTrade)[swapperName] @@ -87,6 +89,9 @@ export const swapperApi = createApi({ return getTradeRates( { ...tradeQuoteInput, + // Receive address should always be undefined for trade *rates*, however, we *do* pass it to check for cross-account support + // so we have to ensure it is gone by the time we call getTradeRates + receiveAddress: undefined, affiliateBps, } as GetTradeRateInput, swapperName, From 7dbf87e9911d76d0122a73a088fef1b148c6b9e2 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 19 Nov 2024 17:49:09 +0700 Subject: [PATCH 61/62] fix: ci --- .../useGetTradeQuotes/hooks/useGetSwapperTradeQuoteOrRate.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks/useGetSwapperTradeQuoteOrRate.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks/useGetSwapperTradeQuoteOrRate.tsx index a29ab80da01..7deeb68d1a5 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks/useGetSwapperTradeQuoteOrRate.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks/useGetSwapperTradeQuoteOrRate.tsx @@ -1,6 +1,5 @@ import { skipToken } from '@reduxjs/toolkit/dist/query' import type { GetTradeQuoteInput, SwapperName } from '@shapeshiftoss/swapper' -import isEmpty from 'lodash/isEmpty' import { useEffect, useMemo } from 'react' import { swapperApi, useGetTradeQuoteQuery } from 'state/apis/swapper/swapperApi' import { tradeQuoteSlice } from 'state/slices/tradeQuoteSlice/tradeQuoteSlice' From f50ed9d8eb27739cabf833421d3584414963c844 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 20 Nov 2024 19:35:54 +0700 Subject: [PATCH 62/62] feat: rm log --- src/state/slices/tradeQuoteSlice/selectors.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/state/slices/tradeQuoteSlice/selectors.ts b/src/state/slices/tradeQuoteSlice/selectors.ts index 1fe467a5fc9..c47c8abb983 100644 --- a/src/state/slices/tradeQuoteSlice/selectors.ts +++ b/src/state/slices/tradeQuoteSlice/selectors.ts @@ -608,7 +608,6 @@ export const selectHopExecutionMetadata = createDeepEqualOutputSelector( selectTradeIdParamFromRequiredFilter, selectHopIndexParamFromRequiredFilter, (swappers, tradeId, hopIndex) => { - console.log({ tradeExecution: swappers.tradeExecution }) return hopIndex === 0 ? swappers.tradeExecution[tradeId]?.firstHop : swappers.tradeExecution[tradeId]?.secondHop