diff --git a/src/components/Aggregator/ConnectButton.tsx b/src/components/Aggregator/ConnectButton.tsx index 9a849857..ed82b3a7 100644 --- a/src/components/Aggregator/ConnectButton.tsx +++ b/src/components/Aggregator/ConnectButton.tsx @@ -7,6 +7,7 @@ const Wrapper = styled.div` right: 0px; z-index: 100; display: flex; + align-items: center; gap: 8px; `; diff --git a/src/components/Aggregator/index.tsx b/src/components/Aggregator/index.tsx index 435ce950..1e68e731 100644 --- a/src/components/Aggregator/index.tsx +++ b/src/components/Aggregator/index.tsx @@ -70,6 +70,7 @@ import { ArrowBackIcon, ArrowForwardIcon, RepeatIcon, SettingsIcon } from '@chak import { Settings } from './Settings'; import { formatAmount } from '~/utils/formatAmount'; import { RefreshIcon } from '../RefreshIcon'; +import { useGetAutoSlippage } from '~/queries/useGetAutoSlippage'; /* Integrated: @@ -320,10 +321,10 @@ export function AggregatorContainer({ tokenList, sandwichList }) { const [isPrivacyEnabled, setIsPrivacyEnabled] = useLocalStorage('llamaswap-isprivacyenabled', false); const [[amount, amountOut], setAmount] = useState<[number | string, number | string]>(['10', '']); - const [slippage, setSlippage] = useLocalStorage('llamaswap-slippage', '0.5'); + const [slippage, setSlippage] = useLocalStorage('llamaswap-slippage', 'auto'); const [lastOutputValue, setLastOutputValue] = useState(null); const [disabledAdapters, setDisabledAdapters] = useLocalStorage('llamaswap-disabledadapters', []); - const [isDegenModeEnabled, _] = useLocalStorage('llamaswap-degenmode', false); + const [isDegenModeEnabled] = useLocalStorage('llamaswap-degenmode', false); const [isSettingsModalOpen, setSettingsModalOpen] = useState(false); // mobile states @@ -409,6 +410,25 @@ export function AggregatorContainer({ tokenList, sandwichList }) { return { finalSelectedFromToken, finalSelectedToToken }; }, [fromToken2, selectedChain?.id, toToken2, selectedFromToken, selectedToToken]); + const { data: autoSlippage, isLoading: fetchingAutoSlippage } = useGetAutoSlippage({ + chainId: selectedChain?.chainId, + fromToken: finalSelectedFromToken?.value, + toToken: finalSelectedToToken?.value, + disabled: slippage !== 'auto', + onError: () => { + setSlippage('0.5'); + } + }); + + useEffect(() => { + // auto slippage is only supported on ethereum + if (isConnected && chainOnWallet?.id !== 1 && slippage === 'auto') { + setSlippage('0.5'); + } + }, [isConnected, chainOnWallet, slippage]); + + const finalSlippage = autoSlippage ?? slippage; + // format input amount of selected from token const amountWithDecimals = BigNumber(debouncedAmount && debouncedAmount !== '' ? debouncedAmount : '0') .times(BigNumber(10).pow(finalSelectedFromToken?.decimals || 18)) @@ -475,10 +495,11 @@ export function AggregatorContainer({ tokenList, sandwichList }) { amount: debouncedAmount, fromToken: finalSelectedFromToken, toToken: finalSelectedToToken, - slippage, + slippage: finalSlippage, isPrivacyEnabled, amountOut: amountOutWithDecimals - } + }, + disabled: fetchingAutoSlippage || finalSlippage === 'auto' }); const { data: gasData, isLoading: isGasDataLoading } = useEstimateGas({ @@ -678,7 +699,7 @@ export function AggregatorContainer({ tokenList, sandwichList }) { ? +selectedRoute?.fromAmount > +balance.data.value : false; - const slippageIsWorng = Number.isNaN(Number(slippage)) || slippage === ''; + const slippageIsWrong = finalSlippage === '' || Number.isNaN(Number(finalSlippage)); const forceRefreshTokenBalance = () => { if (chainOnWallet && address) { @@ -690,9 +711,9 @@ export function AggregatorContainer({ tokenList, sandwichList }) { // approve/swap tokens const amountToApprove = - amountOut && amountOut !== '' + !!finalSlippage && amountOut && amountOut !== '' ? BigNumber(selectedRoute?.fromAmount) - .times(100 + Number(slippage) * 2) + .times(100 + Number(finalSlippage) * 2) .div(100) .toFixed(0) : selectedRoute?.fromAmount; @@ -800,7 +821,7 @@ export function AggregatorContainer({ tokenList, sandwichList }) { amount: String(variables.amountIn), amountUsd: +fromTokenPrice * +variables.amountIn || 0, errorData: data, - slippage, + slippage: finalSlippage, routePlace: String(variables?.index), route: variables.route }); @@ -838,7 +859,7 @@ export function AggregatorContainer({ tokenList, sandwichList }) { amount: String(variables.amountIn), amountUsd: +fromTokenPrice * +variables.amountIn || 0, errorData: {}, - slippage, + slippage: finalSlippage, routePlace: String(variables?.index), route: variables.route }); @@ -890,7 +911,7 @@ export function AggregatorContainer({ tokenList, sandwichList }) { amount: String(variables.amountIn), amountUsd: +fromTokenPrice * +variables.amountIn || 0, errorData: {}, - slippage, + slippage: finalSlippage, routePlace: String(variables?.index), route: variables.route, reportedOutput: Number(variables.amount) || 0, @@ -915,7 +936,7 @@ export function AggregatorContainer({ tokenList, sandwichList }) { amount: String(variables.amountIn), amountUsd: +fromTokenPrice * +variables.amountIn || 0, errorData: err, - slippage, + slippage: finalSlippage, routePlace: String(variables?.index), route: variables.route }); @@ -924,7 +945,7 @@ export function AggregatorContainer({ tokenList, sandwichList }) { }); const handleSwap = () => { - if (selectedRoute && selectedRoute.price && !slippageIsWorng) { + if (selectedRoute && selectedRoute.price && !slippageIsWrong) { if (hasMaxPriceImpact && !isDegenModeEnabled) { toast({ title: 'Price impact is too high!', @@ -939,7 +960,7 @@ export function AggregatorContainer({ tokenList, sandwichList }) { to: finalSelectedToToken.value, signer, signTypedDataAsync, - slippage, + slippage: finalSlippage, adapter: selectedRoute.name, rawQuote: selectedRoute.price.rawQuote, tokens: { fromToken: finalSelectedFromToken, toToken: finalSelectedToToken }, @@ -979,7 +1000,9 @@ export function AggregatorContainer({ tokenList, sandwichList }) { const warnings = [ aggregator === 'CowSwap' ? ( <> - {finalSelectedFromToken.value === ethers.constants.AddressZero && Number(slippage) < 2 ? ( + {!!finalSlippage && + finalSelectedFromToken.value === ethers.constants.AddressZero && + Number(finalSlippage) < 2 ? ( Swaps from {finalSelectedFromToken.symbol} on CowSwap need to have slippage higher than 2%. @@ -1141,10 +1164,11 @@ export function AggregatorContainer({ tokenList, sandwichList }) { setSlippage={setSlippage} fromToken={finalSelectedFromToken?.symbol} toToken={finalSelectedToToken?.symbol} + finalSlippage={finalSlippage} /> @@ -1263,7 +1287,7 @@ export function AggregatorContainer({ tokenList, sandwichList }) { !(finalSelectedFromToken && finalSelectedToToken) || insufficientBalance || !selectedRoute || - slippageIsWorng || + slippageIsWrong || !isAmountSynced || isApproveInfiniteLoading } @@ -1272,7 +1296,7 @@ export function AggregatorContainer({ tokenList, sandwichList }) { ? 'Select Aggregator' : isApproved ? `Swap via ${selectedRoute.name}` - : slippageIsWorng + : slippageIsWrong ? 'Set Slippage' : 'Approve'} @@ -1396,6 +1420,8 @@ export function AggregatorContainer({ tokenList, sandwichList }) { finalSelectedToToken && !(disabledAdapters.length === adaptersNames.length) ? ( + ) : fetchingAutoSlippage ? ( + ) : (!debouncedAmount && !debouncedAmountOut) || !finalSelectedFromToken || !finalSelectedToToken || @@ -1522,7 +1548,7 @@ export function AggregatorContainer({ tokenList, sandwichList }) { isApproveLoading || isApproveResetLoading || !selectedRoute || - slippageIsWorng || + slippageIsWrong || !isAmountSynced } > @@ -1530,7 +1556,7 @@ export function AggregatorContainer({ tokenList, sandwichList }) { ? 'Select Aggregator' : isApproved ? `Swap via ${selectedRoute?.name}` - : slippageIsWorng + : slippageIsWrong ? 'Set Slippage' : 'Approve'} diff --git a/src/components/HistoryModal/index.tsx b/src/components/HistoryModal/index.tsx index 415abbf2..59e78134 100644 --- a/src/components/HistoryModal/index.tsx +++ b/src/components/HistoryModal/index.tsx @@ -98,8 +98,7 @@ function HistoryModal({ tokensUrlMap, tokensSymbolsMap }) { ))} { diff --git a/src/queries/useGetAutoSlippage.tsx b/src/queries/useGetAutoSlippage.tsx new file mode 100644 index 00000000..76fa6719 --- /dev/null +++ b/src/queries/useGetAutoSlippage.tsx @@ -0,0 +1,43 @@ +import { useQuery } from '@tanstack/react-query'; +import { getAddress } from 'ethers/lib/utils.js'; + +interface IGetAutoSlippage { + fromToken: string; + toToken: string; + chainId: number; + disabled: boolean; + onError?: () => void; +} + +export async function getAutoSlippage({ fromToken, toToken, chainId, disabled }: IGetAutoSlippage) { + if (chainId !== 1 || disabled || !fromToken || !toToken) { + return null; + } + + const data = await fetch(`https://slippage.llama.fi/inference`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ from_address: getAddress(fromToken), to_address: getAddress(toToken) }) + }).then((res) => res.json()); + + if (data.error) { + throw new Error(data.error); + } + + return data.predictedSlippage; +} + +export function useGetAutoSlippage({ fromToken, toToken, chainId, disabled, onError }: IGetAutoSlippage) { + return useQuery( + ['auto-slippage', fromToken, toToken, chainId, disabled], + () => getAutoSlippage({ fromToken, toToken, chainId, disabled }), + { + refetchOnWindowFocus: false, + refetchIntervalInBackground: false, + retry: false, + onError + } + ); +} diff --git a/src/queries/useGetRoutes.tsx b/src/queries/useGetRoutes.tsx index 81e990db..9a590412 100644 --- a/src/queries/useGetRoutes.tsx +++ b/src/queries/useGetRoutes.tsx @@ -13,6 +13,7 @@ interface IGetListRoutesProps { extra?: any; disabledAdapters?: Array; customRefetchInterval?: number; + disabled?: boolean; } export interface IRoute { @@ -133,20 +134,23 @@ export function useGetRoutes({ amount, extra = {}, disabledAdapters = [], - customRefetchInterval + customRefetchInterval, + disabled }: IGetListRoutesProps) { const res = useQueries({ - queries: adapters - .filter((adap) => adap.chainToId[chain] !== undefined && !disabledAdapters.includes(adap.name)) - .map>((adapter) => { - return { - queryKey: ['routes', adapter.name, chain, from, to, amount, JSON.stringify(omit(extra, 'amount'))], - queryFn: () => getAdapterRoutes({ adapter, chain, from, to, amount, extra }), - refetchInterval: customRefetchInterval || REFETCH_INTERVAL, - refetchOnWindowFocus: false, - refetchIntervalInBackground: false - }; - }) + queries: disabled + ? [] + : adapters + .filter((adap) => adap.chainToId[chain] !== undefined && !disabledAdapters.includes(adap.name)) + .map>((adapter) => { + return { + queryKey: ['routes', adapter.name, chain, from, to, amount, JSON.stringify(omit(extra, 'amount'))], + queryFn: () => getAdapterRoutes({ adapter, chain, from, to, amount, extra }), + refetchInterval: customRefetchInterval || REFETCH_INTERVAL, + refetchOnWindowFocus: false, + refetchIntervalInBackground: false + }; + }) }); const data = res?.filter((r) => r.status === 'success') ?? []; const resData = res?.filter((r) => r.status === 'success' && !!r.data && r.data.price) ?? [];