Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: upgrade swappers to support returning multiple quotes #5095

Merged
merged 16 commits into from
Aug 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -157,21 +157,21 @@ export const TradeConfirm = () => {
const getSellTxLink = useCallback(
(sellTxHash: string) =>
getTxLink({
name: tradeQuoteStep?.sources[0]?.name,
name: tradeQuoteStep?.source,
woodenfurniture marked this conversation as resolved.
Show resolved Hide resolved
defaultExplorerBaseUrl: tradeQuoteStep?.sellAsset.explorerTxLink ?? '',
tradeId: sellTxHash,
}),
[tradeQuoteStep?.sellAsset.explorerTxLink, tradeQuoteStep?.sources],
[tradeQuoteStep?.sellAsset.explorerTxLink, tradeQuoteStep?.source],
)

const getBuyTxLink = useCallback(
(buyTxHash: string) =>
getTxLink({
name: lastStep?.sources[0]?.name,
name: lastStep?.source,
defaultExplorerBaseUrl: lastStep?.buyAsset.explorerTxLink ?? '',
txId: buyTxHash,
}),
[lastStep?.buyAsset.explorerTxLink, lastStep?.sources],
[lastStep?.buyAsset.explorerTxLink, lastStep?.source],
)

const txLink = useMemo(() => {
Expand Down Expand Up @@ -478,7 +478,12 @@ export const TradeConfirm = () => {
<RawText>{`1 ${sellAsset?.symbol ?? ''} = ${firstNonZeroDecimal(
bnOrZero(tradeQuoteStep?.rate),
)} ${buyAsset?.symbol}`}</RawText>
{!!swapperName && <RawText color='text.subtle'>@{swapperName}</RawText>}
{!!swapperName && (
// This works because we currently only support Li.Fi trades with a single hop,
// and Osmosis uses Osmosis as a source for its two hops
// TODO(woodenfurniture): ensure we show the swapper name, not the first hop source, when we support multi-hop trades
<RawText color='text.subtle'>@{tradeQuoteStep.source}</RawText>
woodenfurniture marked this conversation as resolved.
Show resolved Hide resolved
)}
</Box>
</Skeleton>
</Row>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ export const TradeQuoteLoaded: React.FC<TradeQuoteLoadedProps> = ({
)

const handleQuoteSelection = useCallback(() => {
dispatch(tradeQuoteSlice.actions.setSwapperName(quoteData.swapperName))
}, [dispatch, quoteData.swapperName])
dispatch(tradeQuoteSlice.actions.setActiveQuoteIndex(quoteData.index))
}, [dispatch, quoteData.index])

const feeAsset = useAppSelector(state => selectFeeAssetByChainId(state, sellAsset.chainId ?? ''))
if (!feeAsset)
Expand Down Expand Up @@ -236,7 +236,7 @@ export const TradeQuoteLoaded: React.FC<TradeQuoteLoadedProps> = ({
<Flex justifyContent='space-between' alignItems='center'>
<Flex gap={2} alignItems='center'>
<SwapperIcon swapperName={quoteData.swapperName} />
<RawText>{quoteData.swapperName}</RawText>
<RawText>{quote?.steps[0].source ?? quoteData.swapperName}</RawText>
woodenfurniture marked this conversation as resolved.
Show resolved Hide resolved
</Flex>
{quote && (
<Amount.Crypto
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Collapse, Flex } from '@chakra-ui/react'
import { memo, useMemo } from 'react'
import type { ApiQuote } from 'state/apis/swappers'
import { selectActiveSwapperName } from 'state/slices/tradeQuoteSlice/selectors'
import { selectActiveQuoteIndex } from 'state/slices/tradeQuoteSlice/selectors'
import { useAppSelector } from 'state/store'

import { TradeQuote } from './TradeQuote'
Expand All @@ -12,29 +12,29 @@ type TradeQuotesProps = {
}

export const TradeQuotes: React.FC<TradeQuotesProps> = memo(({ isOpen, sortedQuotes }) => {
const activeSwapperName = useAppSelector(selectActiveSwapperName)
const activeQuoteIndex = useAppSelector(selectActiveQuoteIndex)

const bestQuoteData = sortedQuotes[0]

const quotes = useMemo(
() =>
sortedQuotes.map((quoteData, i) => {
const { swapperName } = quoteData
const { index } = quoteData

// TODO(woodenfurniture): use quote ID when we want to support multiple quotes per swapper
const isActive = activeSwapperName === swapperName
const isActive = activeQuoteIndex === index

return (
<TradeQuote
isActive={isActive}
isBest={i === 0}
key={swapperName}
key={index}
quoteData={quoteData}
bestInputOutputRatio={bestQuoteData.inputOutputRatio}
/>
)
}),
[activeSwapperName, bestQuoteData, sortedQuotes],
[activeQuoteIndex, bestQuoteData, sortedQuotes],
)

return (
Expand Down
4 changes: 2 additions & 2 deletions src/components/MultiHopTrade/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getMaybeCompositeAssetSymbol } from 'lib/mixpanel/helpers'
import type { ReduxState } from 'state/reducer'
import { selectAssets, selectWillDonate } from 'state/slices/selectors'
import {
selectActiveSwapperName,
selectActiveQuoteIndex,
selectBuyAmountBeforeFeesCryptoPrecision,
selectFirstHopSellAsset,
selectLastHopBuyAsset,
Expand Down Expand Up @@ -32,7 +32,7 @@ export const getMixpanelEventData = () => {
const buyAmountBeforeFeesCryptoPrecision = selectBuyAmountBeforeFeesCryptoPrecision(state)
const sellAmountBeforeFeesCryptoPrecision = selectSellAmountBeforeFeesCryptoPrecision(state)
const willDonate = selectWillDonate(state)
const swapperName = selectActiveSwapperName(state)
const swapperName = selectActiveQuoteIndex(state)

const compositeBuyAsset = getMaybeCompositeAssetSymbol(buyAsset.assetId, assets)
const compositeSellAsset = getMaybeCompositeAssetSymbol(sellAsset.assetId, assets)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,15 +162,15 @@ export const useGetTradeQuotes = () => {
if (isEqualExceptAffiliateBpsAndSlippage(tradeQuoteInput, updatedTradeQuoteInput)) {
return
} else {
dispatch(tradeQuoteSlice.actions.resetSwapperName())
dispatch(tradeQuoteSlice.actions.resetActiveQuoteIndex())
}
}
})()
} else {
// if the quote input args changed, reset the selected swapper and update the trade quote args
if (tradeQuoteInput !== skipToken) {
setTradeQuoteInput(skipToken)
dispatch(tradeQuoteSlice.actions.resetSwapperName())
dispatch(tradeQuoteSlice.actions.resetActiveQuoteIndex())
}
}
}, [
Expand Down
2 changes: 1 addition & 1 deletion src/lib/getTxLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { SwapSource } from 'lib/swapper/api'
import { SwapperName } from 'lib/swapper/api'

type GetBaseUrl = {
name: SwapSource['name'] | Dex | undefined
name: SwapSource | Dex | undefined
defaultExplorerBaseUrl: string
isOrder?: boolean
}
Expand Down
9 changes: 3 additions & 6 deletions src/lib/swapper/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export type TradeBase<C extends ChainId> = {
sellAmountIncludingProtocolFeesCryptoBaseUnit: string
feeData: QuoteFeeData<C>
rate: string
sources: SwapSource[]
source: SwapSource
buyAsset: Asset
sellAsset: Asset
accountNumber: number
Expand All @@ -137,10 +137,7 @@ export type TradeQuote<C extends ChainId = ChainId> = {
rate: string // top-level rate for all steps (i.e. output amount / input amount)
}

export type SwapSource = {
name: SwapperName | string
proportion: string
}
export type SwapSource = SwapperName | `${SwapperName} • ${string}`

export enum SwapperName {
Thorchain = 'THORChain',
Expand Down Expand Up @@ -237,6 +234,6 @@ export type Swapper2Api = {
getTradeQuote: (
input: GetTradeQuoteInput,
deps: TradeQuoteDeps,
) => Promise<Result<TradeQuote2, SwapErrorRight>>
) => Promise<Result<TradeQuote2[], SwapErrorRight>>
getUnsignedTx(input: GetUnsignedTxArgs): Promise<UnsignedTx2>
}
2 changes: 1 addition & 1 deletion src/lib/swapper/swapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const getTradeQuotes = async (
swappers
.filter(({ swapperName }) => enabledSwappers.includes(swapperName))
.map(({ swapperName, swapper }) =>
timeout<TradeQuote2, SwapErrorRight>(
timeout<TradeQuote2[], SwapErrorRight>(
swapper.getTradeQuote(getTradeQuoteInput, deps),
QUOTE_TIMEOUT_MS,
QUOTE_TIMEOUT_ERROR,
Expand Down
4 changes: 2 additions & 2 deletions src/lib/swapper/swappers/CowSwapper/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const cowApi: Swapper2Api = {
getTradeQuote: async (
input: GetTradeQuoteInput,
{ sellAssetUsdRate, buyAssetUsdRate }: { sellAssetUsdRate: string; buyAssetUsdRate: string },
): Promise<Result<TradeQuote2, SwapErrorRight>> => {
): Promise<Result<TradeQuote2[], SwapErrorRight>> => {
const tradeQuoteResult = await getCowSwapTradeQuote(input as GetEvmTradeQuoteInput, {
sellAssetUsdRate,
buyAssetUsdRate,
Expand All @@ -57,7 +57,7 @@ export const cowApi: Swapper2Api = {
return tradeQuoteResult.map(tradeQuote => {
const id = uuid()
tradeQuoteMetadata.set(id, { chainId: tradeQuote.steps[0].sellAsset.chainId as EvmChainId })
return { id, receiveAddress, affiliateBps: undefined, ...tradeQuote }
return [{ id, receiveAddress, affiliateBps: undefined, ...tradeQuote }]
})
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ const expectedTradeQuoteWethToFox: TradeQuote<KnownChainIds.EthereumMainnet> = {
},
sellAmountIncludingProtocolFeesCryptoBaseUnit: '1000000000000000000',
buyAmountBeforeFeesCryptoBaseUnit: '14913256100953839475750', // 14913 FOX
sources: [{ name: SwapperName.CowSwap, proportion: '1' }],
source: SwapperName.CowSwap,
buyAsset: FOX_MAINNET,
sellAsset: WETH,
accountNumber: 0,
Expand All @@ -137,7 +137,7 @@ const expectedTradeQuoteFoxToEth: TradeQuote<KnownChainIds.EthereumMainnet> = {
},
sellAmountIncludingProtocolFeesCryptoBaseUnit: '1000000000000000000000',
buyAmountBeforeFeesCryptoBaseUnit: '51242479117266593',
sources: [{ name: SwapperName.CowSwap, proportion: '1' }],
source: SwapperName.CowSwap,
buyAsset: ETH,
sellAsset: FOX_MAINNET,
accountNumber: 0,
Expand All @@ -164,7 +164,7 @@ const expectedTradeQuoteUsdcToXdai: TradeQuote<KnownChainIds.GnosisMainnet> = {
},
sellAmountIncludingProtocolFeesCryptoBaseUnit: '20000000',
buyAmountBeforeFeesCryptoBaseUnit: '21006555357465608755',
sources: [{ name: SwapperName.CowSwap, proportion: '1' }],
source: SwapperName.CowSwap,
buyAsset: XDAI,
sellAsset: USDC_GNOSIS,
accountNumber: 0,
Expand All @@ -191,7 +191,7 @@ const expectedTradeQuoteSmallAmountWethToFox: TradeQuote<KnownChainIds.EthereumM
},
sellAmountIncludingProtocolFeesCryptoBaseUnit: '1000000000000',
buyAmountBeforeFeesCryptoBaseUnit: '165590332317788059940', // 165.59 FOX
sources: [{ name: SwapperName.CowSwap, proportion: '1' }],
source: SwapperName.CowSwap,
buyAsset: FOX_MAINNET,
sellAsset: WETH,
accountNumber: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import type { AxiosError } from 'axios'
import { getConfig } from 'config'
import { bn } from 'lib/bignumber/bignumber'
import type { GetTradeQuoteInput, SwapErrorRight, TradeQuote } from 'lib/swapper/api'
import { SwapperName } from 'lib/swapper/api'
import type { CowChainId, CowSwapQuoteResponse } from 'lib/swapper/swappers/CowSwapper/types'
import {
COW_SWAP_NATIVE_ASSET_MARKER_ADDRESS,
COW_SWAP_VAULT_RELAYER_ADDRESS,
DEFAULT_APP_DATA,
DEFAULT_SOURCE,
ORDER_KIND_SELL,
} from 'lib/swapper/swappers/CowSwapper/utils/constants'
import { cowService } from 'lib/swapper/swappers/CowSwapper/utils/cowService'
Expand Down Expand Up @@ -115,7 +115,7 @@ export async function getCowSwapTradeQuote(
},
sellAmountIncludingProtocolFeesCryptoBaseUnit: normalizedSellAmountCryptoBaseUnit,
buyAmountBeforeFeesCryptoBaseUnit,
sources: DEFAULT_SOURCE,
source: SwapperName.CowSwap,
buyAsset,
sellAsset,
accountNumber,
Expand Down
2 changes: 0 additions & 2 deletions src/lib/swapper/swappers/CowSwapper/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { AddressZero } from '@ethersproject/constants'
import { KnownChainIds } from '@shapeshiftoss/types'
import { SwapperName } from 'lib/swapper/api'

import type { CowChainId } from '../types'

Expand All @@ -9,7 +8,6 @@ export const MIN_COWSWAP_USD_TRADE_VALUES_BY_CHAIN_ID: Record<CowChainId, string
[KnownChainIds.GnosisMainnet]: '0.01',
}

export const DEFAULT_SOURCE = [{ name: SwapperName.CowSwap, proportion: '1' }]
export const DEFAULT_ADDRESS = AddressZero
export const DEFAULT_APP_DATA = '0x68a7b5781dfe48bd5d7aeb11261c17517f5c587da682e4fade9b6a00a59b8970'
export const COW_SWAP_VAULT_RELAYER_ADDRESS = '0xc92e8bdf79f0507f65a392b0ab4667716bfe0110'
Expand Down
40 changes: 25 additions & 15 deletions src/lib/swapper/swappers/LifiSwapper/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,21 @@ import { createDefaultStatusResponse } from 'lib/utils/evm'

import { getTradeQuote } from './getTradeQuote/getTradeQuote'
import { getLifiChainMap } from './utils/getLifiChainMap'
import { getLifiToolsMap } from './utils/getLifiToolsMap'
import { getUnsignedTx } from './utils/getUnsignedTx/getUnsignedTx'
import type { LifiTool } from './utils/types'

const tradeQuoteMetadata: Map<string, Route> = new Map()

let lifiChainMapPromise: Promise<Result<Map<ChainId, ChainKey>, SwapErrorRight>> | undefined
// cached metadata - would need persistent cache with expiry if moved server-side
let lifiChainMapPromise: Promise<Map<ChainId, ChainKey>> | undefined
let lifiToolsMapPromise: Promise<Map<string, LifiTool>> | undefined
gomesalexandre marked this conversation as resolved.
Show resolved Hide resolved

export const lifiApi: Swapper2Api = {
getTradeQuote: async (
input: GetTradeQuoteInput,
{ assets }: TradeQuoteDeps,
): Promise<Result<TradeQuote2, SwapErrorRight>> => {
): Promise<Result<TradeQuote2[], SwapErrorRight>> => {
if (input.sellAmountIncludingProtocolFeesCryptoBaseUnit === '0') {
return Err(
makeSwapErrorRight({
Expand All @@ -38,31 +42,37 @@ export const lifiApi: Swapper2Api = {
}),
)
}
if (lifiChainMapPromise === undefined) lifiChainMapPromise = getLifiChainMap()

const maybeLifiChainMap = await lifiChainMapPromise
if (lifiChainMapPromise === undefined) lifiChainMapPromise = getLifiChainMap()
if (lifiToolsMapPromise === undefined) lifiToolsMapPromise = getLifiToolsMap()

if (maybeLifiChainMap.isErr()) return Err(maybeLifiChainMap.unwrapErr())
const [lifiChainMap, lifiToolsMap] = await Promise.all([
lifiChainMapPromise,
lifiToolsMapPromise,
])

const tradeQuoteResult = await getTradeQuote(
input as GetEvmTradeQuoteInput,
maybeLifiChainMap.unwrap(),
lifiChainMap,
lifiToolsMap,
assets,
)
const { receiveAddress } = input

return tradeQuoteResult.map(({ selectedLifiRoute, ...tradeQuote }) => {
// TODO: quotes below the minimum arent valid and should not be processed as such
// selectedLifiRoute willbe missing for quotes below the minimum
if (!selectedLifiRoute) throw Error('missing selectedLifiRoute')
return tradeQuoteResult.map(quote =>
quote.map(({ selectedLifiRoute, ...tradeQuote }) => {
// TODO: quotes below the minimum aren't valid and should not be processed as such
// selectedLifiRoute will be missing for quotes below the minimum
if (!selectedLifiRoute) throw Error('missing selectedLifiRoute')

const id = selectedLifiRoute.id
const id = selectedLifiRoute.id

// store the lifi quote metadata for transaction building later
tradeQuoteMetadata.set(id, selectedLifiRoute)
// store the lifi quote metadata for transaction building later
tradeQuoteMetadata.set(id, selectedLifiRoute)

return { id, receiveAddress, affiliateBps: undefined, ...tradeQuote }
})
return { id, receiveAddress, affiliateBps: undefined, ...tradeQuote }
}),
)
},

getUnsignedTx: async ({ from, tradeQuote, stepIndex }: GetUnsignedTxArgs): Promise<ETHSignTx> => {
Expand Down
Loading