diff --git a/src/assets/svg/zap.svg b/src/assets/svg/zap.svg new file mode 100644 index 0000000000..5b34432ded --- /dev/null +++ b/src/assets/svg/zap.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Announcement/Popups/TransactionPopup.tsx b/src/components/Announcement/Popups/TransactionPopup.tsx index 72fc3bfccc..c0e0fac744 100644 --- a/src/components/Announcement/Popups/TransactionPopup.tsx +++ b/src/components/Announcement/Popups/TransactionPopup.tsx @@ -85,6 +85,19 @@ const summaryLiquidity = (txs: TransactionDetails) => { }` } +const zapInLiquidity = (txs: TransactionDetails) => { + const extraInfo = txs.extraInfo || {} + const { + tokenAmountIn = '', + tokenAmountOut = '', + tokenSymbolIn, + tokenSymbolOut, + zapAmountIn, + zapSymbolIn, + } = extraInfo as TransactionExtraInfo2Token + return t`You have zapped ${zapAmountIn} ${zapSymbolIn} into ${tokenAmountIn} ${tokenSymbolIn} and ${tokenAmountOut} ${tokenSymbolOut} of liquidity to the pool.` +} + const summaryCrossChain = (txs: TransactionDetails) => { const { tokenAmountIn, @@ -152,6 +165,7 @@ const SUMMARY: { [type in TRANSACTION_TYPE]: SummaryFunction } = { [TRANSACTION_TYPE.CLASSIC_REMOVE_LIQUIDITY]: summaryLiquidity, [TRANSACTION_TYPE.ELASTIC_REMOVE_LIQUIDITY]: summaryLiquidity, [TRANSACTION_TYPE.ELASTIC_INCREASE_LIQUIDITY]: summaryLiquidity, + [TRANSACTION_TYPE.ELASTIC_ZAP_IN_LIQUIDITY]: zapInLiquidity, [TRANSACTION_TYPE.ELASTIC_COLLECT_FEE]: summaryLiquidity, [TRANSACTION_TYPE.STAKE]: summaryStakeUnstakeFarm, diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 6f33128371..a20ba80a34 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -20,6 +20,7 @@ const disabledHoverBase = css` ` const Base = styled(RebassButton)<{ color?: string + backgroundColor?: string padding?: string margin?: string width?: string @@ -81,16 +82,16 @@ const disabledPrimary = css<{ opacity: ${({ altDisabledStyle }) => (altDisabledStyle ? '0.7' : '1')}; ` export const ButtonPrimary = styled(Base)` - background-color: ${({ theme }) => theme.primary}; - color: ${({ theme }) => theme.textReverse}; + background-color: ${({ theme, backgroundColor }) => backgroundColor || theme.primary}; + color: ${({ theme, color }) => color || theme.textReverse}; &:hover { - color: ${({ theme }) => theme.textReverse}; + color: ${({ theme, color }) => color || theme.textReverse}; filter: brightness(0.8); } &:active { - box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.1, theme.primary)}; - background-color: ${({ theme }) => darken(0.1, theme.primary)}; + box-shadow: 0 0 0 1pt ${({ theme, backgroundColor }) => darken(0.1, backgroundColor || theme.primary)}; + background-color: ${({ theme, backgroundColor }) => darken(0.1, backgroundColor || theme.primary)}; } &:disabled { ${disabledPrimary} @@ -263,6 +264,7 @@ const disabledError = css` export const ButtonErrorStyle = styled(Base)` background-color: ${({ theme }) => theme.red}; border: 1px solid ${({ theme }) => theme.red}; + color: ${({ theme }) => theme.text}; &:focus { box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.05, theme.red)}; diff --git a/src/components/ElasticZap/QuickZap.tsx b/src/components/ElasticZap/QuickZap.tsx new file mode 100644 index 0000000000..85958d91d5 --- /dev/null +++ b/src/components/ElasticZap/QuickZap.tsx @@ -0,0 +1,585 @@ +import { FeeAmount, Pool, Position } from '@kyberswap/ks-sdk-elastic' +import { Trans } from '@lingui/macro' +import { BigNumber } from 'ethers' +import { rgba } from 'polished' +import { ReactElement, useMemo, useState } from 'react' +import { Flex, Text } from 'rebass' +import styled from 'styled-components' + +import { ReactComponent as ZapIcon } from 'assets/svg/zap.svg' +import { ButtonLight, ButtonOutlined, ButtonPrimary } from 'components/Button' +import CurrencyInputPanel from 'components/CurrencyInputPanel' +import Dots from 'components/Dots' +import DoubleCurrencyLogo from 'components/DoubleLogo' +import LocalLoader from 'components/LocalLoader' +import Modal from 'components/Modal' +import PriceImpactNote, { PRICE_IMPACT_EXPLANATION_URL, TextUnderlineColor } from 'components/SwapForm/PriceImpactNote' +import SlippageSettingGroup from 'components/SwapForm/SlippageSettingGroup' +import useParsedAmount from 'components/SwapForm/hooks/useParsedAmount' +import { MouseoverTooltip } from 'components/Tooltip' +import { + ConfirmationPendingContent, + TransactionErrorContent, + TransactionSubmittedContent, +} from 'components/TransactionConfirmationModal' +import WarningNote from 'components/WarningNote' +import { abi } from 'constants/abis/v2/ProAmmPoolState.json' +import { APP_PATHS } from 'constants/index' +import { EVMNetworkInfo } from 'constants/networks/type' +import { useActiveWeb3React } from 'hooks' +import { useToken } from 'hooks/Tokens' +import { useZapInAction, useZapInPoolResult } from 'hooks/elasticZap' +import { ApprovalState, useApproveCallback } from 'hooks/useApproveCallback' +import { useProAmmTickReader, useReadingContract } from 'hooks/useContract' +import useDebounce from 'hooks/useDebounce' +import { useProAmmPositionsFromTokenId } from 'hooks/useProAmmPositions' +import useTheme from 'hooks/useTheme' +import { useChangeNetwork } from 'hooks/web3/useChangeNetwork' +import { useWalletModalToggle } from 'state/application/hooks' +import { useElasticFarmsV2 } from 'state/farms/elasticv2/hooks' +import { RANGE } from 'state/mint/proamm/type' +import { NEVER_RELOAD, useSingleCallResult, useSingleContractMultipleData } from 'state/multicall/hooks' +import { useTransactionAdder } from 'state/transactions/hooks' +import { TRANSACTION_TYPE } from 'state/transactions/type' +import { useCurrencyBalances } from 'state/wallet/hooks' +import { StyledInternalLink } from 'theme' +import { formattedNum } from 'utils' +import { maxAmountSpend } from 'utils/maxAmountSpend' +import { getTokenSymbolWithHardcode } from 'utils/tokenInfo' +import { unwrappedToken } from 'utils/wrappedCurrency' + +import RangeSelector, { FARMING_RANGE, useTicksFromRange } from './RangeSelector' +import ZapDetail, { useZapDetail } from './ZapDetail' + +const QuickZapButtonWrapper = styled(ButtonOutlined)<{ size: 'small' | 'medium' }>` + padding: 0; + width: ${({ size }) => (size === 'small' ? '28px' : '36px')}; + max-width: ${({ size }) => (size === 'small' ? '28px' : '36px')}; + height: ${({ size }) => (size === 'small' ? '28px' : '36px')}; + background: ${({ theme, disabled }) => rgba(disabled ? theme.subText : theme.warning, 0.2)}; + color: ${({ theme, disabled }) => (disabled ? theme.subText : '#FF9901')}; + &:hover { + cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; + border: 1px solid ${({ theme, disabled }) => rgba(disabled ? theme.subText : theme.warning, 0.2)}; + } + + border: 1px solid ${({ theme, disabled }) => rgba(disabled ? theme.subText : theme.warning, 0.2)}; + + &:active { + box-shadow: none; + } + + &:focus { + box-shadow: none; + } +` + +const Content = styled.div` + padding: 1rem; + width: 100%; +` + +const Overlay = styled.div<{ overlay: boolean }>` + opacity: ${({ overlay }) => (overlay ? 0.4 : 1)}; + pointer-events: ${({ overlay }) => (overlay ? 'none' : '')}; +` + +export const QuickZapButton = ({ + onClick, + size = 'medium', +}: { + onClick: (e: React.MouseEvent) => void + size?: 'small' | 'medium' +}) => { + const { networkInfo } = useActiveWeb3React() + const isZapAvailable = !!(networkInfo as EVMNetworkInfo).elastic.zap + const theme = useTheme() + + return ( + Quickly zap and add liquidity using only one token. + ) : ( + Zap will be available soon. + ) + } + > + + + + + ) +} + +type Props = { + poolAddress: string + tokenId?: string | number | BigNumber + isOpen: boolean + onDismiss: () => void + expectedChainId?: number +} + +export default function QuickZap(props: Props) { + if (!props.isOpen) return null + + return +} + +function QuickZapModal({ isOpen, onDismiss, poolAddress, tokenId, expectedChainId }: Props) { + const { chainId, networkInfo, account } = useActiveWeb3React() + const zapInContractAddress = (networkInfo as EVMNetworkInfo).elastic.zap?.router + const { changeNetwork } = useChangeNetwork() + const theme = useTheme() + const [selectedRange, setSelectedRange] = useState(null) + + const { farms: elasticFarmV2s } = useElasticFarmsV2() + const farmV2 = elasticFarmV2s + ?.filter(farm => farm.endTime > Date.now() / 1000 && !farm.isSettled) + .find(farm => farm.poolAddress.toLowerCase() === poolAddress.toLowerCase()) + + const toggleWalletModal = useWalletModalToggle() + const poolContract = useReadingContract(poolAddress, abi) + + const { loading: loadingPos, position: positionDetail } = useProAmmPositionsFromTokenId( + tokenId ? BigNumber.from(tokenId) : undefined, + ) + + const { loading: loadingPoolState, result: poolStateRes } = useSingleCallResult(poolContract, 'getPoolState') + const { loading: loadingToken0, result: token0Res } = useSingleCallResult( + poolContract, + 'token0', + undefined, + NEVER_RELOAD, + ) + const { loading: loadingToken1, result: token1Res } = useSingleCallResult( + poolContract, + 'token1', + undefined, + NEVER_RELOAD, + ) + const { loading: loadingFee, result: feeRes } = useSingleCallResult( + poolContract, + 'swapFeeUnits', + undefined, + NEVER_RELOAD, + ) + + const { loading: loadingLiqState, result: liqStateRes } = useSingleCallResult(poolContract, 'getLiquidityState') + + const token0 = useToken(token0Res?.[0]) + const token1 = useToken(token1Res?.[0]) + + const currency0 = token0 ? unwrappedToken(token0.wrapped) : undefined + const currency1 = token1 ? unwrappedToken(token1.wrapped) : undefined + + const pool = useMemo(() => { + const fee = feeRes?.[0] + if (!liqStateRes || !poolStateRes || !token0 || !token1 || !fee) return null + + return new Pool( + token0.wrapped, + token1.wrapped, + fee as FeeAmount, + poolStateRes.sqrtP, + liqStateRes.baseL, + liqStateRes.reinvestL, + poolStateRes.currentTick, + ) + }, [token0, token1, poolStateRes, liqStateRes, feeRes]) + + const position = useMemo(() => { + if (!positionDetail || !pool) return null + return new Position({ + pool, + tickLower: positionDetail.tickLower, + tickUpper: positionDetail.tickUpper, + liquidity: positionDetail.liquidity.toString(), + }) + }, [positionDetail, pool]) + + const loading = loadingPoolState || loadingLiqState || loadingToken0 || loadingToken1 || loadingFee || loadingPos + + const outOfRange = + !!position && (position.pool.tickCurrent < position.tickLower || position.pool.tickCurrent >= position.tickUpper) + + const currencies = useMemo( + () => [currency0, currency1, currency0?.wrapped, currency1?.wrapped], + [currency0, currency1], + ) + const balances = useCurrencyBalances(currencies) + + const [isReverse, setIsReverse] = useState(false) + const [useWrapped, setUseWrapped] = useState(false) + + const selectedCurrency = useMemo(() => { + const currency = isReverse ? currency1 : currency0 + if (useWrapped) return currency?.wrapped + return currency ? unwrappedToken(currency) : currency + }, [isReverse, currency0, currency1, useWrapped]) + + const quoteCurrency = useMemo(() => { + return isReverse ? currency0 : currency1 + }, [isReverse, currency0, currency1]) + + const [typedValue, setTypedValue] = useState('') + const debouncedValue = useDebounce(typedValue, 300) + + const amountIn = useParsedAmount(selectedCurrency, debouncedValue) + const equivalentQuoteAmount = + amountIn && pool && selectedCurrency && amountIn.multiply(pool.priceOf(selectedCurrency.wrapped)) + + const [tickLower, tickUpper] = useTicksFromRange(selectedRange, pool || undefined) + const tickReader = useProAmmTickReader() + + const vTickLower = position ? position.tickLower : tickLower + const vTickUpper = position ? position.tickUpper : tickUpper + const results = useSingleContractMultipleData(tickReader, 'getNearestInitializedTicks', [ + [poolAddress, vTickLower], + [poolAddress, vTickUpper], + ]) + + const tickPrevious = useMemo(() => { + return results.map(call => call.result?.previous) + }, [results]) + + const params = useMemo(() => { + return amountIn?.greaterThan('0') && selectedCurrency && quoteCurrency + ? { + poolAddress, + tokenIn: selectedCurrency.wrapped.address, + tokenOut: quoteCurrency.wrapped.address, + amountIn, + tickLower: vTickLower, + tickUpper: vTickUpper, + } + : undefined + }, [amountIn, poolAddress, selectedCurrency, vTickLower, vTickUpper, quoteCurrency]) + + const { loading: zapLoading, result, aggregatorData } = useZapInPoolResult(params) + const [approvalState, approve] = useApproveCallback(amountIn, zapInContractAddress) + const { zapIn } = useZapInAction() + + const balanceIndex = useWrapped ? (isReverse ? 3 : 2) : isReverse ? 1 : 0 + + let error: ReactElement | null = null + if (!typedValue) error = Enter an amount + else if (!amountIn) error = Invalid Input + else if (balances[balanceIndex] && amountIn?.greaterThan(balances[balanceIndex])) + error = Insufficient Balance + else if (!result) error = Insufficient Liquidity + + const renderActionName = () => { + if (error) return error + if (approvalState === ApprovalState.PENDING) + return ( + + Approving + + ) + if (approvalState !== ApprovalState.APPROVED) return Approve + + if (zapLoading) + return ( + + Loading + + ) + if (!!position) return Increase Liquidity + return Add Liquidity + } + + const symbol0 = getTokenSymbolWithHardcode( + chainId, + token0?.wrapped.address, + useWrapped ? currency0?.wrapped.symbol : currency0?.symbol, + ) + const symbol1 = getTokenSymbolWithHardcode( + chainId, + token1?.wrapped.address, + useWrapped ? currency1?.wrapped.symbol : currency1?.symbol, + ) + + const [attempingTx, setAttempingTx] = useState(false) + const [txHash, setTxHash] = useState('') + const [errorMsg, setError] = useState('') + const addTransactionWithType = useTransactionAdder() + + const zapDetail = useZapDetail({ + pool, + tokenIn: selectedCurrency?.wrapped.address, + tokenId: tokenId?.toString(), + position, + zapResult: result, + amountIn, + poolAddress, + tickLower: vTickLower, + tickUpper: vTickUpper, + previousTicks: tickPrevious, + aggregatorRoute: aggregatorData, + }) + + const { newPosDraft } = zapDetail + + const handleClick = async () => { + if (approvalState === ApprovalState.NOT_APPROVED) { + approve() + return + } + + if (selectedCurrency && (tokenId || tickPrevious.every(Boolean)) && result && amountIn?.quotient && pool) { + try { + setAttempingTx(true) + + const { hash: txHash } = await zapIn( + { + tokenId: tokenId ? tokenId.toString() : 0, + tokenIn: selectedCurrency.wrapped.address, + amountIn: amountIn.quotient.toString(), + equivalentQuoteAmount: equivalentQuoteAmount?.quotient.toString() || '0', + poolAddress, + tickLower: vTickLower, + tickUpper: vTickUpper, + tickPrevious: [tickPrevious[0], tickPrevious[1]], + poolInfo: { + token0: pool.token0.wrapped.address, + fee: pool.fee, + token1: pool.token1.wrapped.address, + }, + liquidity: result.liquidity.toString(), + aggregatorRoute: aggregatorData, + }, + { + zapWithNative: selectedCurrency.isNative, + }, + ) + + setTxHash(txHash) + addTransactionWithType({ + hash: txHash, + type: TRANSACTION_TYPE.ELASTIC_ZAP_IN_LIQUIDITY, + extraInfo: { + zapSymbolIn: selectedCurrency?.symbol ?? '', + tokenSymbolIn: symbol0 ?? '', + tokenSymbolOut: symbol1 ?? '', + zapAmountIn: amountIn.toSignificant(6), + tokenAmountIn: newPosDraft?.amount0?.toSignificant(6) || '0', + tokenAmountOut: newPosDraft?.amount1?.toSignificant(6) || '0', + tokenAddressIn: currency0?.wrapped.address || '', + tokenAddressOut: currency1?.wrapped.address || '', + }, + }) + + setAttempingTx(false) + } catch (e) { + setAttempingTx(false) + setError(e?.message || JSON.stringify(e)) + } + } + } + + const addliquidityLink = `/${networkInfo.route}${ + tokenId ? APP_PATHS.ELASTIC_INCREASE_LIQ : APP_PATHS.ELASTIC_CREATE_POOL + }/${currency0?.isNative ? currency0.symbol : currency0?.wrapped.address || ''}/${ + currency1?.isNative ? currency1.symbol : currency1?.wrapped.address || '' + }/${pool?.fee}${tokenId ? `/${tokenId}` : ''}` + + return ( + + {attempingTx ? ( + { + setAttempingTx(false) + }} + pendingText={ + + Zapping {amountIn?.toSignificant(6)} {selectedCurrency?.symbol} into{' '} + {newPosDraft?.amount0?.toSignificant(6)} {symbol0} and {newPosDraft?.amount1?.toSignificant(6)} {symbol1}{' '} + of liquidity to the pool + + } + /> + ) : errorMsg ? ( + { + setError('') + setAttempingTx(false) + }} + message={errorMsg} + /> + ) : txHash ? ( + + ) : ( + + {loading ? ( + + ) : ( + <> + + + + + + {symbol0} - {symbol1} + + + + {!!position && ( + + #{tokenId?.toString()} + + )} + + + + Add liquidity to the pool using a single token + + + {!!position ? ( + + Increase Liquidity + + ) : ( + + Step 1. Deposit Your Liquidity + + )} + + { + setTypedValue(v) + }} + onMax={() => { + setTypedValue(maxAmountSpend(balances[balanceIndex])?.toExact() || '') + }} + onHalf={() => { + setTypedValue(balances[balanceIndex].divide('2').toExact()) + }} + currency={selectedCurrency} + estimatedUsd={formattedNum(zapDetail.amountInUsd.toString(), true) || undefined} + positionMax="top" + showCommonBases + isSwitchMode + onSwitchCurrency={() => { + if (selectedCurrency?.isNative) { + setUseWrapped(true) + } else { + setUseWrapped(false) + setIsReverse(prev => !prev) + } + }} + /> + + {!position && pool && ( + <> + + + Step 2. Choose Price Range + + + + + Set a Custom Range ↗ + + + + + setSelectedRange(range)} + farmV2={farmV2} + /> + + )} + +
+ +
+ + + + {!!( + zapDetail.priceImpact?.isVeryHigh || + zapDetail.priceImpact?.isHigh || + zapDetail.priceImpact?.isInvalid + ) && + result && + !zapLoading && ( + <> + + {zapDetail.priceImpact.isVeryHigh ? ( + + + + Price Impact + {' '} + is very high. You will lose funds! Please turn on{' '} + + Degen Mode ↗ + + + + } + /> + ) : ( + + )} + + )} +
+ + + + Cancel + + + {!account ? ( + + Connect Wallet + + ) : expectedChainId && expectedChainId !== chainId ? ( + changeNetwork(expectedChainId)}>Switch Network + ) : ( + + {renderActionName()} + + )} + + + )} +
+ )} +
+ ) +} diff --git a/src/components/ElasticZap/RangeSelector.tsx b/src/components/ElasticZap/RangeSelector.tsx new file mode 100644 index 0000000000..ab8958aa12 --- /dev/null +++ b/src/components/ElasticZap/RangeSelector.tsx @@ -0,0 +1,191 @@ +import { FeeAmount, Pool, TICK_SPACINGS, TickMath, nearestUsableTick, tickToPrice } from '@kyberswap/ks-sdk-elastic' +import { Trans } from '@lingui/macro' +import { useEffect, useMemo, useRef, useState } from 'react' +import { Flex, Text } from 'rebass' +import styled from 'styled-components' + +import { ReactComponent as Down } from 'assets/svg/down.svg' +import { FeeSelectorWrapper, SelectWrapper, SelectWrapperOuter } from 'components/FeeSelector' +import { TwoWayArrow } from 'components/Icons' +import { useOnClickOutside } from 'hooks/useOnClickOutside' +import useTheme from 'hooks/useTheme' +import { RANGE_LIST, rangeData } from 'pages/AddLiquidityV2/constants' +import { ElasticFarmV2 } from 'state/farms/elasticv2/types' +import { Bound, RANGE } from 'state/mint/proamm/type' +import { getRecommendedRangeTicks } from 'state/mint/proamm/utils' +import { usePairFactor } from 'state/topTokens/hooks' +import { getTokenSymbolWithHardcode } from 'utils/tokenInfo' +import { unwrappedToken } from 'utils/wrappedCurrency' + +const StyledSelectWrapperOuter = styled(SelectWrapperOuter)` + background: ${({ theme }) => theme.buttonGray}; +` + +const Option = styled.div` + padding: 12px; + cursor: pointer; + + :hover { + background: ${({ theme }) => theme.buttonBlack}; + } +` +const defaultFee = FeeAmount.MOST_PAIR + +export interface FARMING_RANGE { + tickLower: number + tickUpper: number +} + +export const useTicksFromRange = (range: RANGE | FARMING_RANGE | null, pool?: Pool): [number, number] => { + const pairFactor = usePairFactor([pool?.token0, pool?.token1]) + + const tickSpaceLimits: { + [bound in Bound]: number + } = useMemo( + () => ({ + [Bound.LOWER]: nearestUsableTick(TickMath.MIN_TICK, TICK_SPACINGS[pool?.fee || defaultFee]), + [Bound.UPPER]: nearestUsableTick(TickMath.MAX_TICK, TICK_SPACINGS[pool?.fee || defaultFee]), + }), + [pool?.fee], + ) + + if (typeof range === 'object' && !!range) { + return [range.tickLower, range.tickUpper] + } + + const isFullRange = range === RANGE.FULL_RANGE + + return isFullRange + ? [tickSpaceLimits.LOWER, tickSpaceLimits.UPPER] + : pool && range + ? (getRecommendedRangeTicks(range, pool.token0, pool.token1, pool.tickCurrent, pairFactor).map(item => + nearestUsableTick(item, TICK_SPACINGS[pool?.fee || defaultFee]), + ) as unknown as [number, number]) + : [0, 0] +} +export default function RangeSelector({ + pool, + selectedRange, + onChange, + farmV2, +}: { + pool: Pool + selectedRange: RANGE | FARMING_RANGE | null + onChange: (range: RANGE | FARMING_RANGE) => void + farmV2?: ElasticFarmV2 +}) { + const [show, setShow] = useState(false) + + const theme = useTheme() + const ref = useRef(null) + useOnClickOutside(ref, () => { + setShow(false) + }) + + const symbol0 = getTokenSymbolWithHardcode( + pool.token0.chainId, + pool.token0.address, + unwrappedToken(pool.token0).symbol, + ) + const symbol1 = getTokenSymbolWithHardcode( + pool.token1.chainId, + pool.token1.address, + unwrappedToken(pool.token1).symbol, + ) + + const [tickLower, tickUpper] = useTicksFromRange(selectedRange, pool) + const isFullRange = selectedRange === RANGE.FULL_RANGE + const parsedLower = tickToPrice(pool.token0, pool.token1, tickLower) + const parsedUpper = tickToPrice(pool.token0, pool.token1, tickUpper) + + useEffect(() => { + if (!selectedRange) { + const range = farmV2?.ranges.filter(item => !item.isRemoved)?.[0] || RANGE_LIST[1] + onChange(range) + } + }, [selectedRange, farmV2, onChange]) + + const pairFactor = usePairFactor([pool.token0, pool.token1]) + const tickSpaceLimits: { + [bound in Bound]: number + } = useMemo( + () => ({ + [Bound.LOWER]: nearestUsableTick(TickMath.MIN_TICK, TICK_SPACINGS[pool.fee]), + [Bound.UPPER]: nearestUsableTick(TickMath.MAX_TICK, TICK_SPACINGS[pool.fee]), + }), + [pool.fee], + ) + + return ( + setShow(prev => !prev)} ref={ref}> +
+ + {typeof selectedRange === 'object' ? Farming Range : rangeData[selectedRange].title} + + + + Range ({symbol0}/{symbol1}): + + + {isFullRange ? '0' : parsedLower.toSignificant(6)} + + {isFullRange ? '∞' : parsedUpper.toSignificant(6)} + +
+ + + + + {farmV2?.ranges + .filter(item => !item.isRemoved) + .map(range => { + const parsedLower = tickToPrice(pool.token0, pool.token1, range.tickLower) + const parsedUpper = tickToPrice(pool.token0, pool.token1, range.tickUpper) + + return ( + + ) + })} + + {RANGE_LIST.map(item => { + const fullrange = item === RANGE.FULL_RANGE + const [tickLower, tickUpper] = fullrange + ? [tickSpaceLimits.LOWER, tickSpaceLimits.UPPER] + : getRecommendedRangeTicks(item, pool.token0, pool.token1, pool.tickCurrent, pairFactor) + + const parsedLower = tickToPrice(pool.token0, pool.token1, tickLower) + const parsedUpper = tickToPrice(pool.token0, pool.token1, tickUpper) + + return ( + + ) + })} + + +
+ ) +} diff --git a/src/components/ElasticZap/ZapDetail.tsx b/src/components/ElasticZap/ZapDetail.tsx new file mode 100644 index 0000000000..8c50a502e5 --- /dev/null +++ b/src/components/ElasticZap/ZapDetail.tsx @@ -0,0 +1,398 @@ +import { Currency, CurrencyAmount, NativeCurrency, Token, WETH } from '@kyberswap/ks-sdk-core' +import { Pool, Position } from '@kyberswap/ks-sdk-elastic' +import { ReactElement, useEffect, useState } from 'react' +import Skeleton from 'react-loading-skeleton' +import { Box, Flex, Text } from 'rebass' +import { RouteSummary } from 'services/route/types/getRoute' +import styled, { CSSProperties } from 'styled-components' + +import CurrencyLogo from 'components/CurrencyLogo' +import Divider from 'components/Divider' +import { GasStation } from 'components/Icons' +import { FeeTag } from 'components/YieldPools/ElasticFarmGroup/styleds' +import { ELASTIC_BASE_FEE_UNIT } from 'constants/index' +import { useActiveWeb3React } from 'hooks' +import { ZapResult, useZapInAction } from 'hooks/elasticZap' +import useTheme from 'hooks/useTheme' +import { useKyberSwapConfig } from 'state/application/hooks' +import { useTokenPrices } from 'state/tokenPrices/hooks' +import { formatDollarAmount } from 'utils/numbers' +import { checkPriceImpact } from 'utils/prices' +import { getTokenSymbolWithHardcode } from 'utils/tokenInfo' +import { unwrappedToken } from 'utils/wrappedCurrency' + +const Detail = styled(Box)` + display: flex; + flex-direction: column; + gap: 8px; + border: 1px solid ${({ theme }) => theme.border}; + border-radius: 1rem; + padding: 12px; +` + +export interface ZapDetail { + estimateGasUsd: number + priceImpact: { + value: number + isHigh: boolean + isInvalid: boolean + isVeryHigh: boolean + } + position: Position | null | undefined + pool: Pool | null | undefined + oldUsdValue: number + newUsdValue: number + amountInUsd: number + newPooledAmount0: CurrencyAmount | undefined + newPooledAmount1: CurrencyAmount | undefined + zapResult: ZapResult | undefined + skeleton: (w?: number) => ReactElement + newPosDraft: Position | undefined + token0IsNative: boolean + token1IsNative: boolean +} + +export const useZapDetail = ({ + pool, + zapResult: result, + position, + amountIn, + tokenIn, + tokenId, + poolAddress, + tickLower, + tickUpper, + previousTicks, + aggregatorRoute, +}: { + pool: Pool | null | undefined + tokenIn: string | undefined + zapResult: ZapResult | undefined + position: Position | null | undefined + amountIn: CurrencyAmount | undefined + aggregatorRoute: RouteSummary | null + poolAddress: string | undefined + tokenId?: string + tickLower?: number + tickUpper?: number + previousTicks?: number[] +}): ZapDetail => { + const { chainId } = useActiveWeb3React() + const { readProvider } = useKyberSwapConfig() + + const equivalentQuoteAmount = + (amountIn && + pool && + amountIn + .multiply( + pool.priceOf(pool.token0.address.toLowerCase() === tokenIn?.toLowerCase() ? pool.token0 : pool.token1), + ) + ?.quotient.toString()) || + '0' + + const currency0 = pool?.token0 && unwrappedToken(pool.token0) + const currency1 = pool?.token1 && unwrappedToken(pool.token1) + + const newPool = + pool && result + ? new Pool( + pool.token0, + pool.token1, + pool.fee, + result.sqrtP.toString(), + result.baseL.toString(), + result.reinvestL.toString(), + result.currentTick, + ) + : undefined + const newPosDraft = + pool && result && newPool && tickLower !== undefined && tickUpper !== undefined && tickLower < tickUpper + ? new Position({ + pool: newPool, + tickLower, + tickUpper, + liquidity: result.liquidity.toString(), + }) + : undefined + + let newPooledAmount0 = newPosDraft?.amount0 + let newPooledAmount1 = newPosDraft?.amount1 + + if (position && newPooledAmount0 && newPooledAmount1) { + newPooledAmount0 = newPooledAmount0.add(position.amount0) + newPooledAmount1 = newPooledAmount1.add(position.amount1) + } + + const prices = useTokenPrices( + [WETH[chainId].address, currency0?.wrapped.address, currency1?.wrapped.address].filter(Boolean) as string[], + ) + + const oldUsdValue = + +(position?.amount0.toExact() || '0') * (prices[currency0?.wrapped.address || ''] || 0) + + +(position?.amount1.toExact() || '0') * (prices[currency1?.wrapped.address || ''] || 0) + + const newUsdValue = + +(newPooledAmount0?.toExact() || '0') * (prices[currency0?.wrapped?.address || ''] || 0) + + +(newPooledAmount1?.toExact() || '0') * (prices[currency1?.wrapped?.address || ''] || 0) + + const amountInUsd = +(amountIn?.toExact() || '0') * (prices[amountIn?.currency?.wrapped.address || ''] || 0) + + const amountUSDAfterSwap = + currency0 && currency1 + ? +(newPooledAmount0?.toExact() || 0) * (prices[currency0.wrapped.address] || 0) + + +(newPooledAmount1?.toExact() || 0) * (prices[currency1.wrapped.address] || 0) + : 0 + + const priceImpact = + !prices[currency0?.wrapped?.address || ''] || !prices[currency1?.wrapped?.address || ''] + ? NaN + : ((amountInUsd - amountUSDAfterSwap) * 100) / amountInUsd + const priceImpactRes = checkPriceImpact(priceImpact) + + const [gas, setGas] = useState('') // GWei + const { zapIn } = useZapInAction() + + const amount = amountIn?.quotient.toString() + + const [gasPrice, setGasPrice] = useState('0') // wei + useEffect(() => { + readProvider?.getGasPrice().then(res => setGasPrice(res.toString())) + }, [readProvider]) + + useEffect(() => { + if ( + pool && + poolAddress && + tokenIn && + amount && + result && + tickLower !== undefined && + tickUpper !== undefined && + previousTicks?.length + ) { + zapIn( + { + tokenId: tokenId?.toString() || 0, + tokenIn, + amountIn: amount, + equivalentQuoteAmount, + poolAddress, + tickLower, + tickUpper, + tickPrevious: [previousTicks[0], previousTicks[1]], + poolInfo: { + token0: pool.token0.wrapped.address, + fee: pool.fee, + token1: pool.token1.wrapped.address, + }, + liquidity: result.liquidity.toString(), + aggregatorRoute, + }, + { + zapWithNative: !!amountIn?.currency.isNative, + estimateOnly: true, + }, + ) + .then(({ gasEstimated }) => { + setGas(gasEstimated.toString()) + }) + .catch(() => { + setGas('') + }) + } else { + setGas('') + } + }, [ + amount, + aggregatorRoute, + amountIn?.currency.isNative, + zapIn, + tokenIn, + poolAddress, + tokenId, + tickLower, + tickUpper, + previousTicks, + equivalentQuoteAmount, + readProvider, + result, + pool, + ]) + + const estimateGasUsd = + gas && prices[WETH[chainId].address] ? ((+gasPrice * +gas) / 1e18) * prices[WETH[chainId].address] : 0 + + const theme = useTheme() + const skeleton = (width?: number) => ( + + ) + + return { + estimateGasUsd, + priceImpact: { + value: priceImpact, + ...priceImpactRes, + }, + zapResult: result, + position, + pool, + oldUsdValue, + newUsdValue, + amountInUsd, + newPooledAmount0, + newPooledAmount1, + newPosDraft, + token0IsNative: !!(amountIn?.currency.isNative && amountIn?.currency.wrapped.address === pool?.token0.address), + token1IsNative: !!(amountIn?.currency.isNative && amountIn?.currency.wrapped.address === pool?.token1.address), + skeleton, + } +} + +export default function ZapDetail({ + zapLoading, + sx, + zapDetail, +}: { + sx?: CSSProperties + zapLoading: boolean + zapDetail: ZapDetail +}) { + const { + pool, + position, + estimateGasUsd, + zapResult: result, + newPooledAmount0, + newPooledAmount1, + oldUsdValue, + newUsdValue, + priceImpact, + skeleton, + token0IsNative, + token1IsNative, + } = zapDetail + + const theme = useTheme() + const currency0 = pool?.token0 && unwrappedToken(pool.token0) + const currency1 = pool?.token1 && unwrappedToken(pool.token1) + + const symbol0 = token0IsNative + ? currency0?.symbol + : getTokenSymbolWithHardcode(pool?.token0.chainId, pool?.token0?.wrapped.address, currency0?.wrapped.symbol || '') + const symbol1 = token1IsNative + ? currency1?.symbol + : getTokenSymbolWithHardcode(pool?.token0.chainId, pool?.token1?.wrapped.address, currency1?.wrapped.symbol || '') + + return ( + + + + + {symbol0} - {symbol1} + + FEE {((pool?.fee || 0) * 100) / ELASTIC_BASE_FEE_UNIT}% + + + {zapLoading ? ( + skeleton(40) + ) : ( + + + + {estimateGasUsd ? '$' + estimateGasUsd.toFixed(2) : '--'} + + + )} + + + + + + Est. Pooled {symbol0} + + {zapLoading ? ( + skeleton() + ) : !result ? ( + '--' + ) : ( + + {position && ( + <> + + {position.amount0.toSignificant(6)} → + + )} + + {newPooledAmount0?.toSignificant(6) || '--'} {symbol0} + + + )} + + + + Est. Pooled {symbol1} + + {zapLoading ? ( + skeleton() + ) : !result ? ( + '--' + ) : ( + + {position && ( + <> + + {position.amount1.toSignificant(6)} → + + )} + + {newPooledAmount1?.toSignificant(6) || '--'} {symbol1} + + + )} + + + + Est. Liquidity Value + {zapLoading ? ( + skeleton(120) + ) : !result ? ( + '--' + ) : ( + + {position && {formatDollarAmount(oldUsdValue)} →} + {formatDollarAmount(newUsdValue)} + + )} + + + + Price Impact + {zapLoading ? ( + skeleton(40) + ) : !result ? ( + '--' + ) : ( + + {priceImpact.isInvalid ? '--' : priceImpact.value < 0.01 ? '<0.01%' : priceImpact.value.toFixed(2) + '%'} + + )} + + + + Zap Fee + + Free + + + + ) +} diff --git a/src/components/FeeSelector/hook.ts b/src/components/FeeSelector/hook.ts index e3669a3a6d..7f5baec1e5 100644 --- a/src/components/FeeSelector/hook.ts +++ b/src/components/FeeSelector/hook.ts @@ -46,7 +46,7 @@ export const useFeeTierDistribution = ( .then(res => { const feeArray: { feeTier: string; activePositions: number }[] = res?.data?.pools?.map( (item: { positionCount: string; closedPositionCount: string; feeTier: string }) => { - const activePositions = Number(item.positionCount) - Number(item.closedPositionCount) + const activePositions = Number(item.positionCount) return { feeTier: item.feeTier, activePositions, diff --git a/src/components/FeeSelector/index.tsx b/src/components/FeeSelector/index.tsx index b8c6a7205b..4961bd2bcc 100644 --- a/src/components/FeeSelector/index.tsx +++ b/src/components/FeeSelector/index.tsx @@ -119,7 +119,7 @@ const FeeOption = ({ ) } -const FeeSelectorWrapper = styled.div` +export const FeeSelectorWrapper = styled.div` display: flex; padding: 8px 12px; justify-content: space-between; @@ -131,7 +131,7 @@ const FeeSelectorWrapper = styled.div` z-index: 3; ` -const SelectWrapperOuter = styled.div<{ show: boolean }>` +export const SelectWrapperOuter = styled.div<{ show: boolean }>` position: absolute; left: 0; right: 0; @@ -143,7 +143,7 @@ const SelectWrapperOuter = styled.div<{ show: boolean }>` transition: padding 0.15s ${({ show }) => (show ? 'ease-in' : 'ease-out')}; ` -const SelectWrapper = styled.div<{ show: boolean }>` +export const SelectWrapper = styled.div<{ show: boolean }>` border-radius: 20px; z-index: 2; box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.04); diff --git a/src/components/LiquidityProviderMode/index.tsx b/src/components/LiquidityProviderMode/index.tsx index cb2ebf8406..f4afc789de 100644 --- a/src/components/LiquidityProviderMode/index.tsx +++ b/src/components/LiquidityProviderMode/index.tsx @@ -13,10 +13,12 @@ const LiquidityProviderMode = ({ activeTab, setActiveTab, singleTokenInfo, + zapout, }: { activeTab: number setActiveTab: (activeTab: number) => void singleTokenInfo: string + zapout?: boolean }) => { const theme = useTheme() return ( @@ -25,7 +27,7 @@ const LiquidityProviderMode = ({ Token Pair setActiveTab(1)} role="button"> - Single Token + {zapout ? Zap Out : Zap In} { } else { fraction = parseInt(val.toExponential().match(/e([+-][0-9]+)/)?.[1] ?? '0') } - return formatDisplayNumber(val, { fractionDigits: 3 - fraction }) + return formatDisplayNumber(val, { fractionDigits: Math.max(3 - fraction, 3) }) } const CandleStickChart = ({ diff --git a/src/components/ProAmm/ProAmmPoolStat.tsx b/src/components/ProAmm/ProAmmPoolStat.tsx index 704ab594f5..91ce100d28 100644 --- a/src/components/ProAmm/ProAmmPoolStat.tsx +++ b/src/components/ProAmm/ProAmmPoolStat.tsx @@ -56,7 +56,6 @@ const getPrommAnalyticLink = (chainId: ChainId, poolAddress: string) => { const Wrapper = styled.div` padding: 16px; display: flex; - flex: 1; flex-direction: column; gap: 16px; background-image: url(${bgimg}); diff --git a/src/components/ProAmm/ProAmmPriceRangeConfirm.tsx b/src/components/ProAmm/ProAmmPriceRangeConfirm.tsx index 49d89653ee..7eefcdef64 100644 --- a/src/components/ProAmm/ProAmmPriceRangeConfirm.tsx +++ b/src/components/ProAmm/ProAmmPriceRangeConfirm.tsx @@ -8,6 +8,7 @@ import { ReactComponent as DoubleArrow } from 'assets/svg/double_arrow.svg' import { OutlineCard } from 'components/Card' import { AutoColumn } from 'components/Column' import Divider from 'components/Divider' +import { ZapDetail } from 'components/ElasticZap/ZapDetail' import InfoHelper from 'components/InfoHelper' import { RowBetween, RowFixed } from 'components/Row' import { MouseoverTooltip, TextDashed } from 'components/Tooltip' @@ -40,9 +41,11 @@ const Price = styled.div` export default function ProAmmPriceRangeConfirm({ position, ticksAtLimit, + zapDetail, }: { position: Position ticksAtLimit: { [bound: string]: boolean | undefined } + zapDetail?: ZapDetail }) { const theme = useTheme() @@ -113,6 +116,46 @@ export default function ProAmmPriceRangeConfirm({ + {zapDetail && ( + <> + + Price Impact + + {zapDetail.priceImpact.isInvalid + ? '--' + : zapDetail.priceImpact.value < 0.01 + ? '<0.01%' + : zapDetail.priceImpact.value.toFixed(2) + '%'} + + + + + + Est. Gas Fee + + + + {zapDetail.estimateGasUsd ? '$' + zapDetail.estimateGasUsd.toFixed(2) : '--'} + + + + + Zap Fee + + Free + + + + )} diff --git a/src/components/SwapForm/PriceImpactNote.tsx b/src/components/SwapForm/PriceImpactNote.tsx index 8b0b6d9097..a463441843 100644 --- a/src/components/SwapForm/PriceImpactNote.tsx +++ b/src/components/SwapForm/PriceImpactNote.tsx @@ -1,5 +1,6 @@ import { Trans } from '@lingui/macro' import { FC } from 'react' +import { useSearchParams } from 'react-router-dom' import { Text } from 'rebass' import styled from 'styled-components' @@ -9,9 +10,10 @@ import WarningNote from 'components/WarningNote' import useMixpanel, { MIXPANEL_TYPE } from 'hooks/useMixpanel' import useTheme from 'hooks/useTheme' import { useSwitchPairToLimitOrder } from 'state/swap/hooks' +import { StyledInternalLink } from 'theme' import { checkPriceImpact } from 'utils/prices' -const TextUnderlineColor = styled(Text)` +export const TextUnderlineColor = styled(Text)` border-bottom: 1px solid ${({ theme }) => theme.text}; width: fit-content; cursor: pointer; @@ -25,12 +27,12 @@ const TextUnderlineTransparent = styled(Text)` cursor: pointer; ` -const PRICE_IMPACT_EXPLANATION_URL = +export const PRICE_IMPACT_EXPLANATION_URL = 'https://docs.kyberswap.com/getting-started/foundational-topics/decentralized-finance/price-impact' type Props = { isDegenMode?: boolean - priceImpact: number | undefined + priceImpact: number | undefined | null showLimitOrderLink?: boolean } @@ -53,7 +55,7 @@ const PriceImpactNote: FC = ({ isDegenMode, priceImpact, showLimitOrderLi Unable to calculate - + Price Impact @@ -106,7 +108,7 @@ const PriceImpactNote: FC = ({ isDegenMode, priceImpact, showLimitOrderLi shortText={ - + Price Impact is very high. You will lose funds! @@ -154,4 +156,42 @@ const PriceImpactNote: FC = ({ isDegenMode, priceImpact, showLimitOrderLi return null } +export const ZapHighPriceImpact = ({ showInPopup }: { showInPopup?: boolean }) => { + const [searchParams, setSearchParams] = useSearchParams() + return ( + + + + Price Impact + {' '} + is very high. You will lose funds!{' '} + {showInPopup ? ( + + You have turned on Degen Mode from settings. Trades with very high price impact can be executed + + ) : ( + <> + Please turn on{' '} + { + e.preventDefault() + searchParams.set('showSetting', 'true') + setSearchParams(searchParams) + }} + > + Degen Mode ↗ + + + )} + + + } + /> + ) +} + export default PriceImpactNote diff --git a/src/components/Toggle/index.tsx b/src/components/Toggle/index.tsx index d51e518876..00fab85f54 100644 --- a/src/components/Toggle/index.tsx +++ b/src/components/Toggle/index.tsx @@ -2,6 +2,8 @@ import { rgba } from 'polished' import React, { CSSProperties, ReactNode } from 'react' import styled from 'styled-components' +import { highlight } from 'components/swapv2/styleds' + export interface ToggleProps { id?: string className?: string @@ -9,6 +11,7 @@ export interface ToggleProps { toggle: () => void style?: CSSProperties icon?: ReactNode + highlight?: boolean } const Dot = styled.div` @@ -30,9 +33,9 @@ const Dot = styled.div` justify-content: center; ` -const Toggle: React.FC = ({ id, isActive, toggle, style, className, icon }) => { +const Toggle: React.FC = ({ id, isActive, toggle, style, className, icon, highlight }) => { return ( -
+
{isActive && icon}
) @@ -55,4 +58,8 @@ export default styled(Toggle)<{ backgroundColor?: string }>` left: 32px; } } + + &[data-highlight='true'] { + animation: ${({ theme }) => highlight(theme)} 2s 2 alternate ease-in-out; + } ` diff --git a/src/components/TransactionConfirmationModal/index.tsx b/src/components/TransactionConfirmationModal/index.tsx index ff03ecc600..446ac03ae1 100644 --- a/src/components/TransactionConfirmationModal/index.tsx +++ b/src/components/TransactionConfirmationModal/index.tsx @@ -55,7 +55,7 @@ export function ConfirmationPendingContent({ }: { onDismiss: () => void pendingText: string | React.ReactNode - startedTime: number | undefined + startedTime?: number | undefined }) { const theme = useTheme() diff --git a/src/components/TransactionSettings/index.tsx b/src/components/TransactionSettings/index.tsx index 1ba2b6ccd0..05b27bee73 100644 --- a/src/components/TransactionSettings/index.tsx +++ b/src/components/TransactionSettings/index.tsx @@ -1,19 +1,21 @@ -import { t } from '@lingui/macro' +import { Trans, t } from '@lingui/macro' import { rgba } from 'polished' -import { useCallback, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' +import { useSearchParams } from 'react-router-dom' +import { Flex } from 'rebass' import styled, { css } from 'styled-components' import TransactionSettingsIcon from 'components/Icons/TransactionSettingsIcon' import MenuFlyout from 'components/MenuFlyout' import Toggle from 'components/Toggle' -import Tooltip from 'components/Tooltip' +import Tooltip, { MouseoverTooltip, TextDashed } from 'components/Tooltip' import SlippageSetting from 'components/swapv2/SwapSettingsPanel/SlippageSetting' import TransactionTimeLimitSetting from 'components/swapv2/SwapSettingsPanel/TransactionTimeLimitSetting' import { StyledActionButtonSwapForm } from 'components/swapv2/styleds' import useTheme from 'hooks/useTheme' import { ApplicationModal } from 'state/application/actions' import { useModalOpen, useToggleTransactionSettingsMenu } from 'state/application/hooks' -import { useDegenModeManager } from 'state/user/hooks' +import { useAggregatorForZapSetting, useDegenModeManager } from 'state/user/hooks' import AdvanceModeModal from './AdvanceModeModal' @@ -64,16 +66,43 @@ type Props = { export default function TransactionSettings({ hoverBg }: Props) { const theme = useTheme() - const [isDegenMode] = useDegenModeManager() + const [isDegenMode, toggleDegenMode] = useDegenModeManager() + const [isUseAggregatorForZap, toggleAggregatorForZap] = useAggregatorForZapSetting() const toggle = useToggleTransactionSettingsMenu() // show confirmation view before turning on const [showConfirmation, setShowConfirmation] = useState(false) const open = useModalOpen(ApplicationModal.TRANSACTION_SETTINGS) + const [searchParams, setSearchParams] = useSearchParams() + + const showSetting = searchParams.get('showSetting') + useEffect(() => { + if (showSetting === 'true') { + toggle() + } + // only toggle one + // eslint-disable-next-line + }, [showSetting]) + const [isShowTooltip, setIsShowTooltip] = useState(false) const showTooltip = useCallback(() => setIsShowTooltip(true), [setIsShowTooltip]) const hideTooltip = useCallback(() => setIsShowTooltip(false), [setIsShowTooltip]) + const handleToggleAdvancedMode = () => { + if (isDegenMode /* is already ON */) { + toggleDegenMode() + setShowConfirmation(false) + return + } + + toggle() + if (showSetting === 'true') { + searchParams.delete('showSetting') + setSearchParams(searchParams, { replace: true }) + } + + setShowConfirmation(true) + } return ( <> @@ -102,7 +131,13 @@ export default function TransactionSettings({ hoverBg }: Props) { } customStyle={MenuFlyoutBrowserStyle} isOpen={open} - toggle={toggle} + toggle={() => { + toggle() + if (showSetting === 'true') { + searchParams.delete('showSetting') + setSearchParams(searchParams, { replace: true }) + } + }} title={t`Advanced Settings`} mobileCustomStyle={{ paddingBottom: '40px' }} hasArrow @@ -110,6 +145,36 @@ export default function TransactionSettings({ hoverBg }: Props) { + + + + + + Degen Mode + + + + + + + + + + + Use Aggregator for Zaps + + + + + diff --git a/src/components/WalletPopup/Transactions/PoolFarmLink.tsx b/src/components/WalletPopup/Transactions/PoolFarmLink.tsx index 374f8db25f..d4d2a55752 100644 --- a/src/components/WalletPopup/Transactions/PoolFarmLink.tsx +++ b/src/components/WalletPopup/Transactions/PoolFarmLink.tsx @@ -21,6 +21,7 @@ const PoolFarmLink = ({ transaction }: { transaction: TransactionDetails }) => { TRANSACTION_TYPE.ELASTIC_REMOVE_LIQUIDITY, TRANSACTION_TYPE.ELASTIC_COLLECT_FEE, TRANSACTION_TYPE.ELASTIC_INCREASE_LIQUIDITY, + TRANSACTION_TYPE.ELASTIC_ZAP_IN_LIQUIDITY, TRANSACTION_TYPE.HARVEST, ].includes(type) diff --git a/src/components/WalletPopup/Transactions/TransactionItem.tsx b/src/components/WalletPopup/Transactions/TransactionItem.tsx index ad22aa1f7d..c7ea998c3d 100644 --- a/src/components/WalletPopup/Transactions/TransactionItem.tsx +++ b/src/components/WalletPopup/Transactions/TransactionItem.tsx @@ -87,6 +87,7 @@ const Description2Token = (transaction: TransactionDetails) => { TRANSACTION_TYPE.CLASSIC_CREATE_POOL, TRANSACTION_TYPE.ELASTIC_CREATE_POOL, TRANSACTION_TYPE.ELASTIC_INCREASE_LIQUIDITY, + TRANSACTION_TYPE.ELASTIC_ZAP_IN_LIQUIDITY, ].includes(type) const signTokenIn = [ @@ -371,6 +372,7 @@ const DESCRIPTION_MAP: { [TRANSACTION_TYPE.CLASSIC_REMOVE_LIQUIDITY]: DescriptionLiquidity, [TRANSACTION_TYPE.ELASTIC_REMOVE_LIQUIDITY]: DescriptionLiquidity, [TRANSACTION_TYPE.ELASTIC_INCREASE_LIQUIDITY]: DescriptionLiquidity, + [TRANSACTION_TYPE.ELASTIC_ZAP_IN_LIQUIDITY]: DescriptionLiquidity, [TRANSACTION_TYPE.ELASTIC_COLLECT_FEE]: DescriptionLiquidity, [TRANSACTION_TYPE.HARVEST]: DescriptionHarvestFarmReward, diff --git a/src/components/WarningNote/index.tsx b/src/components/WarningNote/index.tsx index 894681061c..e4b5a8ae03 100644 --- a/src/components/WarningNote/index.tsx +++ b/src/components/WarningNote/index.tsx @@ -29,6 +29,7 @@ const Wrapper = styled.div<{ $level: SeverityLevel }>` font-size: 12px; line-height: 16px; padding: 12px 16px; + width: 100%; ` type Props = { diff --git a/src/constants/abis/elastic-zap/router.json b/src/constants/abis/elastic-zap/router.json new file mode 100644 index 0000000000..897d083c04 --- /dev/null +++ b/src/constants/abis/elastic-zap/router.json @@ -0,0 +1,662 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_weth", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes", + "name": "_clientData", + "type": "bytes" + } + ], + "name": "ClientData", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "reason", + "type": "string" + } + ], + "name": "Error", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_executor", + "type": "address" + }, + { + "indexed": true, + "internalType": "bool", + "name": "_grantOrRevoke", + "type": "bool" + } + ], + "name": "ExecutorWhitelisted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "contract IERC20", + "name": "_token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bool", + "name": "_isNative", + "type": "bool" + }, + { + "indexed": false, + "internalType": "bool", + "name": "_isPermit", + "type": "bool" + } + ], + "name": "TokenCollected", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "grantOrRevoke", + "type": "bool" + } + ], + "name": "UpdateGuardian", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "grantOrRevoke", + "type": "bool" + } + ], + "name": "UpdateOperator", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_validator", + "type": "address" + }, + { + "indexed": true, + "internalType": "bool", + "name": "_grantOrRevoke", + "type": "bool" + } + ], + "name": "ValidatorWhitelisted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint8", + "name": "_dexType", + "type": "uint8" + }, + { + "indexed": true, + "internalType": "contract IERC20", + "name": "_srcToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "_srcAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "_validator", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "_executor", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "_zapInfo", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "_initialData", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "_zapResults", + "type": "bytes" + } + ], + "name": "ZapExecuted", + "type": "event" + }, + { + "inputs": [], + "name": "disableLogic", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "enableLogic", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "guardians", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "operators", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + } + ], + "name": "rescueFunds", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "bool", + "name": "grantOrRevoke", + "type": "bool" + } + ], + "name": "updateGuardian", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "bool", + "name": "grantOrRevoke", + "type": "bool" + } + ], + "name": "updateOperator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "weth", + "outputs": [ + { + "internalType": "contract IWETH", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_executors", + "type": "address[]" + }, + { + "internalType": "bool", + "name": "_grantOrRevoke", + "type": "bool" + } + ], + "name": "whitelistExecutors", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_validators", + "type": "address[]" + }, + { + "internalType": "bool", + "name": "_grantOrRevoke", + "type": "bool" + } + ], + "name": "whitelistValidators", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "whitelistedExecutor", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "whitelistedValidator", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint8", + "name": "dexType", + "type": "uint8" + }, + { + "internalType": "contract IERC20", + "name": "srcToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "srcAmount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "zapInfo", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "permitData", + "type": "bytes" + } + ], + "internalType": "struct IKSZapRouter.ZapDescription", + "name": "_desc", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "address", + "name": "validator", + "type": "address" + }, + { + "internalType": "address", + "name": "executor", + "type": "address" + }, + { + "internalType": "uint32", + "name": "deadline", + "type": "uint32" + }, + { + "internalType": "bytes", + "name": "executorData", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "clientData", + "type": "bytes" + } + ], + "internalType": "struct IKSZapRouter.ZapExecutionData", + "name": "_exe", + "type": "tuple" + } + ], + "name": "zapIn", + "outputs": [ + { + "internalType": "bytes", + "name": "zapResults", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint8", + "name": "dexType", + "type": "uint8" + }, + { + "internalType": "contract IERC20", + "name": "srcToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "srcAmount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "zapInfo", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "permitData", + "type": "bytes" + } + ], + "internalType": "struct IKSZapRouter.ZapDescription", + "name": "_desc", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "address", + "name": "validator", + "type": "address" + }, + { + "internalType": "address", + "name": "executor", + "type": "address" + }, + { + "internalType": "uint32", + "name": "deadline", + "type": "uint32" + }, + { + "internalType": "bytes", + "name": "executorData", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "clientData", + "type": "bytes" + } + ], + "internalType": "struct IKSZapRouter.ZapExecutionData", + "name": "_exe", + "type": "tuple" + } + ], + "name": "zapInWithNative", + "outputs": [ + { + "internalType": "bytes", + "name": "zapResults", + "type": "bytes" + } + ], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/constants/abis/elastic-zap/zap-helper.json b/src/constants/abis/elastic-zap/zap-helper.json new file mode 100644 index 0000000000..7435182913 --- /dev/null +++ b/src/constants/abis/elastic-zap/zap-helper.json @@ -0,0 +1,286 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "quoterAddress", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "InvalidZapParams", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "contract IPool", + "name": "pool", + "type": "address" + }, + { + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "internalType": "uint256", + "name": "amount0Desired", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1Desired", + "type": "uint256" + } + ], + "name": "getAddLiquidityAmounts", + "outputs": [ + { + "internalType": "uint256", + "name": "qty0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "qty1", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IPool", + "name": "pool", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "precision", + "type": "uint256" + }, + { + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + } + ], + "name": "getOptimalSwapAmountToZapIn", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "swapAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "returnedAmount", + "type": "uint256" + }, + { + "internalType": "uint160", + "name": "sqrtP", + "type": "uint160" + }, + { + "internalType": "uint256", + "name": "baseL", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reinvestL", + "type": "uint256" + } + ], + "internalType": "struct IKSElasticZapHelper.GetOptimalSwapAmountResults", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IPool", + "name": "pool", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "precision", + "type": "uint256" + }, + { + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + } + ], + "name": "getZapInPoolResults", + "outputs": [ + { + "components": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint256", + "name": "usedAmount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "usedAmount1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "remainingAmount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "remainingAmount1", + "type": "uint256" + }, + { + "internalType": "int24", + "name": "currentTick", + "type": "int24" + }, + { + "internalType": "uint160", + "name": "sqrtP", + "type": "uint160" + }, + { + "internalType": "uint256", + "name": "baseL", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reinvestL", + "type": "uint256" + } + ], + "internalType": "struct IKSElasticZapHelper.ZapInResults", + "name": "results", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "positionManager", + "type": "address" + }, + { + "internalType": "contract IPool", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint256", + "name": "positionId", + "type": "uint256" + }, + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "address", + "name": "receivedToken", + "type": "address" + } + ], + "name": "getZapOutPoolResults", + "outputs": [ + { + "internalType": "uint256", + "name": "returnAmount", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "quoter", + "outputs": [ + { + "internalType": "contract IQuoterV2", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/src/constants/abis/elastic-zap/zap-in.json b/src/constants/abis/elastic-zap/zap-in.json new file mode 100644 index 0000000000..9f4f029b5c --- /dev/null +++ b/src/constants/abis/elastic-zap/zap-in.json @@ -0,0 +1,846 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "helperAddress", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "AllowanceRemains", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidAmounts", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidFeeConfig", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidSwap", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidZapParams", + "type": "error" + }, + { + "inputs": [], + "name": "NotEnoughReturnedLiquidity", + "type": "error" + }, + { + "inputs": [], + "name": "NotEnoughReturnedToken", + "type": "error" + }, + { + "inputs": [], + "name": "NotWhitelistAggregator", + "type": "error" + }, + { + "inputs": [], + "name": "RemainTooMuch", + "type": "error" + }, + { + "inputs": [], + "name": "RemoveZeroLiquidity", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [], + "name": "ZapAgainFailed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "components": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint256", + "name": "usedAmount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "usedAmount1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "remainingAmount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "remainingAmount1", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct IZapHelper.ZapInResults", + "name": "ZapInResults", + "type": "tuple" + } + ], + "name": "ZapInToAddLiquidity", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint256", + "name": "usedAmount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "usedAmount1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "remainingAmount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "remainingAmount1", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct IZapHelper.ZapInResults", + "name": "ZapInResults", + "type": "tuple" + } + ], + "name": "ZapInToMint", + "type": "event" + }, + { + "inputs": [], + "name": "feeGlobalBps", + "outputs": [ + { + "internalType": "uint24", + "name": "", + "type": "uint24" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "feePerPoolBps", + "outputs": [ + { + "internalType": "uint24", + "name": "", + "type": "uint24" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "feeRecipient", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_feeRecipient", + "type": "address" + } + ], + "name": "setFeeRecipient", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + } + ], + "name": "setGlobalFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + } + ], + "name": "setfeePerPoolBps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int256", + "name": "deltaQty0", + "type": "int256" + }, + { + "internalType": "int256", + "name": "deltaQty1", + "type": "int256" + }, + { + "internalType": "bytes", + "name": "path", + "type": "bytes" + } + ], + "name": "swapCallback", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "aggregator", + "type": "address" + }, + { + "internalType": "bool", + "name": "value", + "type": "bool" + } + ], + "name": "updateWhitelistAggregator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "zapHelper", + "outputs": [ + { + "internalType": "contract IZapHelper", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "positionManager", + "type": "address" + }, + { + "internalType": "contract IPool", + "name": "pool", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "uint256", + "name": "positionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minLiquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxRemainingAmount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxRemainingAmount1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct IZapIn.ZapAddLiqParams", + "name": "params", + "type": "tuple" + }, + { + "internalType": "uint256", + "name": "precision", + "type": "uint256" + } + ], + "name": "zapInPoolToAddLiquidity", + "outputs": [ + { + "components": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint256", + "name": "usedAmount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "usedAmount1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "remainingAmount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "remainingAmount1", + "type": "uint256" + } + ], + "internalType": "struct IZapHelper.ZapInResults", + "name": "results", + "type": "tuple" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "positionManager", + "type": "address" + }, + { + "internalType": "contract IPool", + "name": "pool", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "internalType": "int24[2]", + "name": "ticksPrevious", + "type": "int24[2]" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minLiquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxRemainingAmount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxRemainingAmount1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct IZapIn.ZapMintParams", + "name": "params", + "type": "tuple" + }, + { + "internalType": "uint256", + "name": "precision", + "type": "uint256" + } + ], + "name": "zapInPoolToMint", + "outputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint256", + "name": "usedAmount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "usedAmount1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "remainingAmount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "remainingAmount1", + "type": "uint256" + } + ], + "internalType": "struct IZapHelper.ZapInResults", + "name": "results", + "type": "tuple" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "positionManager", + "type": "address" + }, + { + "internalType": "contract IPool", + "name": "pool", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "uint256", + "name": "positionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minLiquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxRemainingAmount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxRemainingAmount1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct IZapIn.ZapAddLiqParams", + "name": "params", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "address", + "name": "aggregator", + "type": "address" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "swapAmount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "swapData", + "type": "bytes" + } + ], + "internalType": "struct IZapIn.AggregationParams", + "name": "aggregationParams", + "type": "tuple" + }, + { + "internalType": "uint256", + "name": "zapAgainPrecision", + "type": "uint256" + } + ], + "name": "zapToAddLiqByAggregator", + "outputs": [ + { + "components": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint256", + "name": "usedAmount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "usedAmount1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "remainingAmount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "remainingAmount1", + "type": "uint256" + } + ], + "internalType": "struct IZapHelper.ZapInResults", + "name": "results", + "type": "tuple" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "positionManager", + "type": "address" + }, + { + "internalType": "contract IPool", + "name": "pool", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "internalType": "int24[2]", + "name": "ticksPrevious", + "type": "int24[2]" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minLiquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxRemainingAmount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxRemainingAmount1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct IZapIn.ZapMintParams", + "name": "params", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "address", + "name": "aggregator", + "type": "address" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "swapAmount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "swapData", + "type": "bytes" + } + ], + "internalType": "struct IZapIn.AggregationParams", + "name": "aggregationParams", + "type": "tuple" + }, + { + "internalType": "uint256", + "name": "zapAgainPrecision", + "type": "uint256" + } + ], + "name": "zapToMintByAggregator", + "outputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint256", + "name": "usedAmount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "usedAmount1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "remainingAmount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "remainingAmount1", + "type": "uint256" + } + ], + "internalType": "struct IZapHelper.ZapInResults", + "name": "results", + "type": "tuple" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/src/constants/abis/v2/ProAmmPoolState.json b/src/constants/abis/v2/ProAmmPoolState.json index a74f22d01d..4b6dc3ae9c 100644 --- a/src/constants/abis/v2/ProAmmPoolState.json +++ b/src/constants/abis/v2/ProAmmPoolState.json @@ -3,29 +3,841 @@ "contractName": "IUniswapV3PoolState", "sourceName": "contracts/interfaces/pool/IUniswapV3PoolState.sol", "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "indexed": true, + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "qty", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "qty0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "qty1", + "type": "uint256" + } + ], + "name": "Burn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "qty", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "qty0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "qty1", + "type": "uint256" + } + ], + "name": "BurnRTokens", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "qty0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "qty1", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "paid0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "paid1", + "type": "uint256" + } + ], + "name": "Flash", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint160", + "name": "sqrtP", + "type": "uint160" + }, + { + "indexed": false, + "internalType": "int24", + "name": "tick", + "type": "int24" + } + ], + "name": "Initialize", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "indexed": true, + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "qty", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "qty0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "qty1", + "type": "uint256" + } + ], + "name": "Mint", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "int256", + "name": "deltaQty0", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "deltaQty1", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint160", + "name": "sqrtP", + "type": "uint160" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "int24", + "name": "currentTick", + "type": "int24" + } + ], + "name": "Swap", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "internalType": "uint128", + "name": "qty", + "type": "uint128" + } + ], + "name": "burn", + "outputs": [ + { + "internalType": "uint256", + "name": "qty0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "qty1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "feeGrowthInsideLast", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_qty", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "isLogicalBurn", + "type": "bool" + } + ], + "name": "burnRTokens", + "outputs": [ + { + "internalType": "uint256", + "name": "qty0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "qty1", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "factory", + "outputs": [ + { + "internalType": "contract IFactory", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "qty0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "qty1", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "flash", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getFeeGrowthGlobal", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getLiquidityState", + "outputs": [ + { + "internalType": "uint128", + "name": "baseL", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "reinvestL", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "reinvestLLast", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "getPoolState", "outputs": [ { - "internalType": "uint160", - "name": "sqrtP", - "type": "uint160" + "internalType": "uint160", + "name": "sqrtP", + "type": "uint160" + }, + { + "internalType": "int24", + "name": "currentTick", + "type": "int24" + }, + { + "internalType": "int24", + "name": "nearestCurrentTick", + "type": "int24" + }, + { + "internalType": "bool", + "name": "locked", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + } + ], + "name": "getPositions", + "outputs": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint256", + "name": "feeGrowthInsideLast", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getSecondsPerLiquidityData", + "outputs": [ + { + "internalType": "uint128", + "name": "secondsPerLiquidityGlobal", + "type": "uint128" + }, + { + "internalType": "uint32", + "name": "lastUpdateTime", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + } + ], + "name": "getSecondsPerLiquidityInside", + "outputs": [ + { + "internalType": "uint128", + "name": "secondsPerLiquidityInside", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int24", + "name": "", + "type": "int24" + } + ], + "name": "initializedTicks", + "outputs": [ + { + "internalType": "int24", + "name": "previous", + "type": "int24" }, { "internalType": "int24", - "name": "currentTick", + "name": "next", + "type": "int24" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maxTickLiquidity", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "int24", + "name": "tickLower", "type": "int24" }, { "internalType": "int24", - "name": "nearestCurrentTick", + "name": "tickUpper", "type": "int24" }, + { + "internalType": "int24[2]", + "name": "ticksPrevious", + "type": "int24[2]" + }, + { + "internalType": "uint128", + "name": "qty", + "type": "uint128" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "mint", + "outputs": [ + { + "internalType": "uint256", + "name": "qty0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "qty1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "feeGrowthInsideLast", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "poolOracle", + "outputs": [ + { + "internalType": "contract IPoolOracle", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "int256", + "name": "swapQty", + "type": "int256" + }, { "internalType": "bool", - "name": "locked", + "name": "isToken0", "type": "bool" + }, + { + "internalType": "uint160", + "name": "limitSqrtP", + "type": "uint160" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "swap", + "outputs": [ + { + "internalType": "int256", + "name": "deltaQty0", + "type": "int256" + }, + { + "internalType": "int256", + "name": "deltaQty1", + "type": "int256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "swapFeeUnits", + "outputs": [ + { + "internalType": "uint24", + "name": "", + "type": "uint24" } ], "stateMutability": "view", @@ -33,49 +845,197 @@ }, { "inputs": [], - "name": "getLiquidityState", + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "tickDistance", + "outputs": [ + { + "internalType": "int24", + "name": "", + "type": "int24" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int24", + "name": "", + "type": "int24" + } + ], + "name": "ticks", "outputs": [ { "internalType": "uint128", - "name": "baseL", + "name": "liquidityGross", "type": "uint128" }, { - "internalType": "uint128", - "name": "reinvestL", - "type": "uint128" + "internalType": "int128", + "name": "liquidityNet", + "type": "int128" + }, + { + "internalType": "uint256", + "name": "feeGrowthOutside", + "type": "uint256" }, { "internalType": "uint128", - "name": "reinvestLLast", + "name": "secondsPerLiquidityOutside", "type": "uint128" } ], "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "token0", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "token1", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, { "internalType": "uint256", - "name": "_qty", + "name": "amount", "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ { "internalType": "bool", - "name": "isLogicalBurn", + "name": "", "type": "bool" } ], - "name": "burnRTokens", + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + } + ], + "name": "tweakPosZeroLiq", "outputs": [ { - "internalType": "uint128", + "internalType": "uint256", + "name": "feeGrowthInsideLast", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint160", + "name": "initialSqrtP", + "type": "uint160" + } + ], + "name": "unlockPool", + "outputs": [ + { + "internalType": "uint256", "name": "qty0", "type": "uint256" }, { - "internalType": "uint128", + "internalType": "uint256", "name": "qty1", "type": "uint256" } diff --git a/src/constants/networks/arbitrum.ts b/src/constants/networks/arbitrum.ts index c8b8c84025..9447f8dc53 100644 --- a/src/constants/networks/arbitrum.ts +++ b/src/constants/networks/arbitrum.ts @@ -65,6 +65,12 @@ const arbitrumInfo: EVMNetworkInfo = { '0x3D6AfE2fB73fFEd2E3dD00c501A174554e147a43', '0xf2BcDf38baA52F6b0C1Db5B025DfFf01Ae1d6dBd', ], + zap: { + router: '0x30C5322E4e08AD500c348007f92f120ab4E2b79e', + validator: '0xf0096e5B4AAfeEA1DF557264091569ba125c1172', + executor: '0x4f097F7074D52952006a0763312724929Ff95Cf0', + helper: '0x4E8419EFa0b0A149Dad77b689D37AF17f762f20A', + }, }, limitOrder: '*', averageBlockTimeInSeconds: 1, // TODO: check these info diff --git a/src/constants/networks/avax.ts b/src/constants/networks/avax.ts index 08b9ae26f3..d740a0fe44 100644 --- a/src/constants/networks/avax.ts +++ b/src/constants/networks/avax.ts @@ -74,6 +74,12 @@ const avaxInfo: EVMNetworkInfo = { '0x3d6afe2fb73ffed2e3dd00c501a174554e147a43', '0xf2BcDf38baA52F6b0C1Db5B025DfFf01Ae1d6dBd', ], + zap: { + router: '0x30C5322E4e08AD500c348007f92f120ab4E2b79e', + validator: '0xf0096e5B4AAfeEA1DF557264091569ba125c1172', + executor: '0x4f097F7074D52952006a0763312724929Ff95Cf0', + helper: '0x4E8419EFa0b0A149Dad77b689D37AF17f762f20A', + }, }, limitOrder: '*', averageBlockTimeInSeconds: 1.85, diff --git a/src/constants/networks/base.ts b/src/constants/networks/base.ts index 22ec8a9ce4..e664d90540 100644 --- a/src/constants/networks/base.ts +++ b/src/constants/networks/base.ts @@ -57,6 +57,12 @@ const base: EVMNetworkInfo = { farms: ['0x7D5ba536ab244aAA1EA42aB88428847F25E3E676'], farmv2Quoter: '0x6AFeb9EDd6Cf44fA8E89b1eee28284e6dD7705C8', farmV2S: ['0x3D6AfE2fB73fFEd2E3dD00c501A174554e147a43', '0xf2BcDf38baA52F6b0C1Db5B025DfFf01Ae1d6dBd'], + zap: { + router: '0x30C5322E4e08AD500c348007f92f120ab4E2b79e', + validator: '0xf0096e5B4AAfeEA1DF557264091569ba125c1172', + executor: '0x4f097F7074D52952006a0763312724929Ff95Cf0', + helper: '0x4E8419EFa0b0A149Dad77b689D37AF17f762f20A', + }, }, limitOrder: NOT_SUPPORT, averageBlockTimeInSeconds: 2, // dont use for base diff --git a/src/constants/networks/matic.ts b/src/constants/networks/matic.ts index 04d1a80533..5f5eb8c2f4 100644 --- a/src/constants/networks/matic.ts +++ b/src/constants/networks/matic.ts @@ -69,6 +69,12 @@ const maticInfo: EVMNetworkInfo = { '0x3D6AfE2fB73fFEd2E3dD00c501A174554e147a43', '0xf2BcDf38baA52F6b0C1Db5B025DfFf01Ae1d6dBd', ], + zap: { + router: '0x30C5322E4e08AD500c348007f92f120ab4E2b79e', + validator: '0xf0096e5B4AAfeEA1DF557264091569ba125c1172', + executor: '0x4f097F7074D52952006a0763312724929Ff95Cf0', + helper: '0x4E8419EFa0b0A149Dad77b689D37AF17f762f20A', + }, 'farmV2.1S': [], }, limitOrder: '*', diff --git a/src/constants/networks/optimism.ts b/src/constants/networks/optimism.ts index 8985db510e..514ed20974 100644 --- a/src/constants/networks/optimism.ts +++ b/src/constants/networks/optimism.ts @@ -60,6 +60,12 @@ const optimismInfo: EVMNetworkInfo = { '0x3D6AfE2fB73fFEd2E3dD00c501A174554e147a43', '0xf2BcDf38baA52F6b0C1Db5B025DfFf01Ae1d6dBd', ], + zap: { + router: '0x30C5322E4e08AD500c348007f92f120ab4E2b79e', + validator: '0xf0096e5B4AAfeEA1DF557264091569ba125c1172', + executor: '0x4f097F7074D52952006a0763312724929Ff95Cf0', + helper: '0x4E8419EFa0b0A149Dad77b689D37AF17f762f20A', + }, }, limitOrder: '*', averageBlockTimeInSeconds: 120, diff --git a/src/constants/networks/scroll.ts b/src/constants/networks/scroll.ts index d05ed1a6a8..289fd46cf4 100644 --- a/src/constants/networks/scroll.ts +++ b/src/constants/networks/scroll.ts @@ -59,6 +59,12 @@ const scroll: EVMNetworkInfo = { farmv2Quoter: '0x6AFeb9EDd6Cf44fA8E89b1eee28284e6dD7705C8', farmV2S: [], 'farmV2.1S': ['0x3D6AfE2fB73fFEd2E3dD00c501A174554e147a43', '0xf2BcDf38baA52F6b0C1Db5B025DfFf01Ae1d6dBd'], + zap: { + router: '0x30C5322E4e08AD500c348007f92f120ab4E2b79e', + validator: '0xf0096e5B4AAfeEA1DF557264091569ba125c1172', + executor: '0x4f097F7074D52952006a0763312724929Ff95Cf0', + helper: '0x4E8419EFa0b0A149Dad77b689D37AF17f762f20A', + }, }, limitOrder: NOT_SUPPORT, averageBlockTimeInSeconds: 8.4, // dont use for base diff --git a/src/constants/networks/type.ts b/src/constants/networks/type.ts index 1a5344c250..25370186b2 100644 --- a/src/constants/networks/type.ts +++ b/src/constants/networks/type.ts @@ -77,6 +77,12 @@ export interface EVMNetworkInfo extends NetworkInfo { readonly farms: string[] readonly farmv2Quoter?: string readonly farmV2S?: string[] + readonly zap?: { + helper: string + router: string + executor: string + validator: string + } readonly 'farmV2.1S'?: string[] } readonly limitOrder: null | '*' | EnvKeys[] diff --git a/src/hooks/elasticZap/index.ts b/src/hooks/elasticZap/index.ts new file mode 100644 index 0000000000..52638f6d2c --- /dev/null +++ b/src/hooks/elasticZap/index.ts @@ -0,0 +1,392 @@ +import { Currency, CurrencyAmount, Percent } from '@kyberswap/ks-sdk-core' +import { BigNumber } from 'ethers' +import { Interface, defaultAbiCoder as abiEncoder } from 'ethers/lib/utils' +import JSBI from 'jsbi' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { useBuildRouteMutation, useLazyGetRouteQuery } from 'services/route' +import { RouteSummary } from 'services/route/types/getRoute' + +import { BuildRouteResult } from 'components/SwapForm/hooks/useBuildRoute' +import ZAP_ROUTER_ABI from 'constants/abis/elastic-zap/router.json' +import ZAP_HELPER_ABI from 'constants/abis/elastic-zap/zap-helper.json' +import { AGGREGATOR_API_PATHS } from 'constants/index' +import { NETWORKS_INFO } from 'constants/networks' +import { EVMNetworkInfo } from 'constants/networks/type' +import { useActiveWeb3React, useWeb3React } from 'hooks' +import { useReadingContract, useSigningContract } from 'hooks/useContract' +import { useKyberswapGlobalConfig } from 'hooks/useKyberSwapConfig' +import useTransactionDeadline from 'hooks/useTransactionDeadline' +import { useSingleContractMultipleData } from 'state/multicall/hooks' +import { useAggregatorForZapSetting, useUserSlippageTolerance } from 'state/user/hooks' +import { calculateGasMargin } from 'utils' + +export interface ZapResult { + liquidity: BigNumber + usedAmount0: BigNumber + usedAmount1: BigNumber + remainingAmount0: BigNumber + remainingAmount1: BigNumber + currentTick: number + sqrtP: BigNumber + baseL: BigNumber + reinvestL: BigNumber +} + +const zapRouterInterface = new Interface(ZAP_ROUTER_ABI) + +export function useZapInPoolResult(params?: { + poolAddress: string + tokenIn: string + tokenOut: string + amountIn: CurrencyAmount + tickLower: number + tickUpper: number +}): { + loading: boolean + aggregatorData: RouteSummary | null + result: ZapResult | undefined +} { + const { networkInfo, chainId } = useActiveWeb3React() + const zapHelperContract = useReadingContract((networkInfo as EVMNetworkInfo).elastic.zap?.helper, ZAP_HELPER_ABI) + + const [useAggregatorForZap] = useAggregatorForZapSetting() + + const [loadingAggregator, setLoadingAggregator] = useState(false) + const [getRoute] = useLazyGetRouteQuery() + + const { aggregatorDomain } = useKyberswapGlobalConfig() + const url = `${aggregatorDomain}/${NETWORKS_INFO[chainId].aggregatorRoute}${AGGREGATOR_API_PATHS.GET_ROUTE}` + + const splitedAmount = useMemo(() => { + if (!params?.amountIn) return [] + const percent = [10, 20, 30, 40, 50, 60, 70, 80, 90] + const percents = percent.map(item => new Percent(item, 100)) + return percents.map(item => params.amountIn.multiply(item)) + }, [params?.amountIn]) + + const [aggregatorOutputs, setAggregatorOutputs] = useState>([]) + + const { tokenIn, tokenOut, poolAddress } = params || {} + useEffect(() => { + if (tokenIn && tokenOut && poolAddress) { + setAggregatorOutputs([]) + if (useAggregatorForZap) { + setLoadingAggregator(true) + + Promise.all( + splitedAmount.map(item => { + return getRoute({ + url, + authentication: false, + params: { + tokenIn, + tokenOut, + saveGas: '', + amountIn: item.quotient.toString(), + excludedPools: poolAddress, + }, + clientId: 'kyberswap-zap', + }) + }), + ) + .then(res => res?.map(item => item?.data?.data?.routeSummary) || []) + .then(res => setAggregatorOutputs(res.filter(Boolean) as Array)) + .finally(() => { + setTimeout(() => setLoadingAggregator(false), 100) + }) + } + } + }, [tokenIn, tokenOut, poolAddress, splitedAmount, getRoute, url, useAggregatorForZap]) + + const callParams = useMemo( + () => + params && !loadingAggregator && params.tickLower < params.tickUpper + ? [ + [ + params.poolAddress, + params.tokenIn, + params.amountIn.quotient.toString(), + '0', + 1, + params.tickLower, + params.tickUpper, + ], + ...aggregatorOutputs + .filter(item => JSBI.greaterThan(params.amountIn.quotient, JSBI.BigInt(item.amountIn))) + .map(item => [ + params.poolAddress, + params.tokenIn, + JSBI.subtract(params.amountIn.quotient, JSBI.BigInt(item.amountIn)).toString(), + item.amountOut, + 1, + params.tickLower, + params.tickUpper, + ]), + ] + : undefined, + [params, aggregatorOutputs, loadingAggregator], + ) + + const data = useSingleContractMultipleData( + params ? zapHelperContract : undefined, + 'getZapInPoolResults', + callParams || [], + ) + + const bestRes = useMemo(() => { + let index = -1 + let res = undefined + let maxLiq = BigNumber.from('0') + data.forEach((item, idx) => { + const l = BigNumber.from(item?.result?.results?.liquidity?.toString() || '0') + if (l.gt(maxLiq)) { + maxLiq = l + index = idx + res = item?.result?.results + } + }) + + return { + result: res, + loading: data?.some(item => item?.loading) || loadingAggregator, + // index = 0 => dont need to use aggregator + aggregatorData: + index === 0 || !params + ? null + : aggregatorOutputs?.filter(item => JSBI.greaterThan(params.amountIn.quotient, JSBI.BigInt(item.amountIn)))?.[ + index - 1 + ], + } + }, [data, loadingAggregator, aggregatorOutputs, params]) + + return bestRes +} + +export function useZapInAction() { + const { networkInfo, account, chainId } = useActiveWeb3React() + const { library } = useWeb3React() + const { router: zapRouterAddress, validator, executor } = (networkInfo as EVMNetworkInfo).elastic?.zap || {} + const zapRouterContract = useSigningContract(zapRouterAddress, ZAP_ROUTER_ABI) + + const posManagerAddress = (networkInfo as EVMNetworkInfo).elastic.nonfungiblePositionManager + const [slippage] = useUserSlippageTolerance() + const deadline = useTransactionDeadline() // custom from users settings + + const [useAggregatorForZap] = useAggregatorForZapSetting() + const [buildRoute] = useBuildRouteMutation() + + const { aggregatorDomain } = useKyberswapGlobalConfig() + const url = `${aggregatorDomain}/${NETWORKS_INFO[chainId].aggregatorRoute}${AGGREGATOR_API_PATHS.BUILD_ROUTE}` + + const zapIn = useCallback( + async ( + { + tokenId = 0, + tokenIn, + amountIn, + equivalentQuoteAmount, + poolAddress, + tickUpper, + tickLower, + poolInfo, + tickPrevious, + liquidity, + aggregatorRoute, + }: { + tokenId?: number | string + tokenIn: string + amountIn: string + equivalentQuoteAmount: string + poolAddress: string + tickLower: number + tickUpper: number + poolInfo: { + token0: string + token1: string + fee: number + } + tickPrevious: [number, number] + liquidity: string + aggregatorRoute: RouteSummary | null + }, + options: { + zapWithNative: boolean + estimateOnly?: boolean + }, + ) => { + if (zapRouterContract && account && library && executor) { + let aggregatorRes = null + if (aggregatorRoute && useAggregatorForZap) { + aggregatorRes = (await buildRoute({ + url, + payload: { + routeSummary: aggregatorRoute, + deadline: +(deadline?.toString() || (Math.floor(Date.now() / 1000) + 1200).toString()), + slippageTolerance: slippage, + sender: account, + recipient: executor, + source: 'kyberswap-zap', + skipSimulateTx: false, + }, + authentication: false, + })) as { data: BuildRouteResult } + } + + const minLiquidity = JSBI.divide( + JSBI.multiply(JSBI.BigInt(liquidity), JSBI.BigInt(10000 - slippage)), + JSBI.BigInt(10000), + ).toString() + + const zapInfo = abiEncoder.encode(['address', 'address', 'uint256'], [poolAddress, posManagerAddress, tokenId]) + const extraData = tokenId + ? abiEncoder.encode(['uint128'], [minLiquidity]) + : abiEncoder.encode(['address', 'int24', 'int24', 'uint128'], [account, tickLower, tickUpper, minLiquidity]) + + const zeros = '0'.repeat(128) + const minZapAmount0 = JSBI.divide( + JSBI.multiply(JSBI.BigInt(amountIn), JSBI.BigInt(slippage)), + JSBI.BigInt(10000), + ).toString(2) + + const minZapAmount1 = JSBI.divide( + JSBI.multiply(JSBI.BigInt(equivalentQuoteAmount), JSBI.BigInt(slippage)), + JSBI.BigInt(10000), + ).toString(2) + + const minZapAmount = JSBI.BigInt( + parseInt((zeros + minZapAmount0).slice(-128) + (zeros + minZapAmount1).slice(-128), 2), + ).toString() + + const zapExecutorData = abiEncoder.encode( + [ + 'address', + 'address', + 'tupple(address token0,int24 fee,address token1)', + 'uint256', + 'address', + 'uint256', + 'uint256', + 'int24', + 'int24', + 'int24[2]', + 'uint128', + 'bytes', + ], + [ + posManagerAddress, + poolAddress, + { token0: poolInfo.token0, fee: poolInfo.fee, token1: poolInfo.token1 }, + tokenId, + account, + 1, + minZapAmount, + tickLower, + tickUpper, + tickPrevious, + minLiquidity, + '0x', + ], + ) + + let aggregatorInfo = '0x' + if (aggregatorRes?.data?.data) { + aggregatorInfo = + '0x0000000000000000000000000000000000000000000000000000000000000020' + + abiEncoder + .encode( + ['address', 'uint256', 'bytes'], + [aggregatorRes.data.data.routerAddress, aggregatorRes.data.data.amountIn, aggregatorRes.data.data.data], + ) + .slice(2) + } + + const executorData = abiEncoder.encode( + ['uint8', 'address', 'uint256', 'bytes', 'bytes', 'bytes'], + [ + 0, + tokenIn, + amountIn, + '0x', // feeInfo + aggregatorInfo, + // hardcode for dynamic field (poolInfo) in contract + '0x0000000000000000000000000000000000000000000000000000000000000020' + zapExecutorData.slice(2), + ], + ) + + const params = [ + [ + 0, //dextype: elastic + tokenIn, + amountIn, + zapInfo, + extraData, + '0x', + ], + [validator, executor, deadline?.toString(), executorData, '0x'], + ] + + const callData = zapRouterInterface.encodeFunctionData( + options.zapWithNative ? 'zapInWithNative' : 'zapIn', + params, + ) + + console.debug('zap data', { + value: options.zapWithNative ? amountIn : undefined, + data: callData, + to: zapRouterAddress, + }) + + const gasEstimated = await zapRouterContract.estimateGas[options.zapWithNative ? 'zapInWithNative' : 'zapIn']( + ...params, + { + value: options.zapWithNative ? amountIn : undefined, + }, + ) + + if (options.estimateOnly) { + return { + gasEstimated, + hash: '', + } + } + + const txn = { + value: options.zapWithNative ? amountIn : undefined, + data: callData, + to: zapRouterAddress, + gasLimit: calculateGasMargin(gasEstimated), + } + + const { hash } = await library.getSigner().sendTransaction(txn) + + return { + gasEstimated, + hash, + } + } + + return { + gasEstimated: 0, + hash: '', + } + }, + [ + account, + deadline, + executor, + validator, + posManagerAddress, + zapRouterContract, + slippage, + buildRoute, + useAggregatorForZap, + url, + library, + zapRouterAddress, + ], + ) + + return { + zapIn, + } +} diff --git a/src/hooks/useApproveCallback.ts b/src/hooks/useApproveCallback.ts index 976cb0d799..d770f2f6b0 100644 --- a/src/hooks/useApproveCallback.ts +++ b/src/hooks/useApproveCallback.ts @@ -19,10 +19,10 @@ import { useActiveWeb3React } from './index' import { useTokenSigningContract } from './useContract' export enum ApprovalState { - UNKNOWN, - NOT_APPROVED, - PENDING, - APPROVED, + UNKNOWN = 'UNKNOWN', + NOT_APPROVED = 'NOT_APPROVED', + PENDING = 'PENDING', + APPROVED = 'APPROVED', } // returns a variable indicating the state of the approval and a function which approves if necessary or early returns diff --git a/src/hooks/useProAmmDerivedPositionInfo.ts b/src/hooks/useProAmmDerivedPositionInfo.ts index 011dd57c96..5875a2cae0 100644 --- a/src/hooks/useProAmmDerivedPositionInfo.ts +++ b/src/hooks/useProAmmDerivedPositionInfo.ts @@ -1,4 +1,5 @@ import { Pool, Position } from '@kyberswap/ks-sdk-elastic' +import { useMemo } from 'react' import { PositionDetails } from 'types/position' @@ -16,15 +17,21 @@ export function useProAmmDerivedPositionInfo(positionDetails: PositionDetails | // construct pool data const [poolState, pool] = usePool(currency0 ?? undefined, currency1 ?? undefined, positionDetails?.fee) - let position = undefined - if (pool && positionDetails) { - position = new Position({ - pool, - liquidity: positionDetails.liquidity.toString(), - tickLower: positionDetails.tickLower, - tickUpper: positionDetails.tickUpper, - }) - } + const liquidity = positionDetails?.liquidity.toString() + const tickLower = positionDetails?.tickLower + const tickUpper = positionDetails?.tickUpper + const position = useMemo(() => { + if (pool && liquidity && tickUpper !== undefined && tickLower !== undefined) { + return new Position({ + pool, + liquidity, + tickLower, + tickUpper, + }) + } + return undefined + }, [pool, liquidity, tickLower, tickUpper]) + return { position, pool: pool ?? undefined, diff --git a/src/hooks/useProAmmPreviousTicks.ts b/src/hooks/useProAmmPreviousTicks.ts index 46f7a14c3c..0dbef916b0 100644 --- a/src/hooks/useProAmmPreviousTicks.ts +++ b/src/hooks/useProAmmPreviousTicks.ts @@ -42,6 +42,7 @@ export default function useProAmmPreviousTicks( return undefined }, [results, loading, error, pool]) } + export function useProAmmMultiplePreviousTicks( pool: Pool | null | undefined, positions: (Position | undefined)[], diff --git a/src/pages/AddLiquidityV2/components/DisclaimerERC20.tsx b/src/pages/AddLiquidityV2/components/DisclaimerERC20.tsx index eab3e5f91f..68adfcc800 100644 --- a/src/pages/AddLiquidityV2/components/DisclaimerERC20.tsx +++ b/src/pages/AddLiquidityV2/components/DisclaimerERC20.tsx @@ -11,7 +11,7 @@ import { ExternalLink } from 'theme' export default function DisclaimerERC20({ href, token0, token1 }: { href?: string; token0: string; token1: string }) { const theme = useTheme() const { chainId } = useActiveWeb3React() - const { data } = useGetTokenListQuery( + const { data, isLoading } = useGetTokenListQuery( { chainId, addresses: `${token0},${token1}`, @@ -22,28 +22,29 @@ export default function DisclaimerERC20({ href, token0, token1 }: { href?: strin ) const hide = data?.data?.tokens?.[0]?.isStandardERC20 && data?.data?.tokens?.[1]?.isStandardERC20 - if (hide) return null - return ( - - - - - - Disclaimer: KyberSwap is a permissionless protocol optimized for the standard ERC20 implementation only. - Please do your own research before you provide liquidity using tokens with unique mechanics (e.g. FOT, - Rebase, LP tokens, contract deposits, etc.). More info{' '} - - here - - - - - - ) + if (!hide && !isLoading) + return ( + + + + + + Disclaimer: KyberSwap is a permissionless protocol optimized for the standard ERC20 implementation only. + Please do your own research before you provide liquidity using tokens with unique mechanics (e.g. FOT, + Rebase, LP tokens, contract deposits, etc.). More info{' '} + + here + + + + + + ) + return null } diff --git a/src/pages/AddLiquidityV2/index.tsx b/src/pages/AddLiquidityV2/index.tsx index 170fa347b1..2ce9bff4db 100644 --- a/src/pages/AddLiquidityV2/index.tsx +++ b/src/pages/AddLiquidityV2/index.tsx @@ -12,7 +12,7 @@ import { import { Trans, t } from '@lingui/macro' import { BigNumber } from 'ethers' import JSBI from 'jsbi' -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { AlertTriangle, Repeat } from 'react-feather' import { Navigate, useNavigate, useParams, useSearchParams } from 'react-router-dom' import { useMedia } from 'react-use' @@ -24,6 +24,8 @@ import { ButtonError, ButtonLight, ButtonPrimary } from 'components/Button' import { OutlineCard, SubTextCard, WarningCard } from 'components/Card' import { AutoColumn } from 'components/Column' import CurrencyInputPanel from 'components/CurrencyInputPanel' +import CurrencyLogo from 'components/CurrencyLogo' +import { useZapDetail } from 'components/ElasticZap/ZapDetail' import FeeSelector from 'components/FeeSelector' import HoverInlineText from 'components/HoverInlineText' import { Swap as SwapIcon, TwoWayArrow } from 'components/Icons' @@ -41,8 +43,13 @@ import Rating from 'components/Rating' import Row, { RowBetween, RowFixed } from 'components/Row' import ShareModal from 'components/ShareModal' import { SLIPPAGE_EXPLANATION_URL } from 'components/SlippageWarningNote' +import PriceImpactNote, { ZapHighPriceImpact } from 'components/SwapForm/PriceImpactNote' +import useParsedAmount from 'components/SwapForm/hooks/useParsedAmount' import Tooltip, { MouseoverTooltip } from 'components/Tooltip' -import TransactionConfirmationModal, { ConfirmationModalContent } from 'components/TransactionConfirmationModal' +import TransactionConfirmationModal, { + ConfirmationModalContent, + TransactionErrorContent, +} from 'components/TransactionConfirmationModal' import { TutorialType } from 'components/Tutorial' import { Dots } from 'components/swapv2/styleds' import { APP_PATHS, ETHER_ADDRESS } from 'constants/index' @@ -51,8 +58,10 @@ import { EVMNetworkInfo } from 'constants/networks/type' import { NativeCurrencies } from 'constants/tokens' import { useActiveWeb3React, useWeb3React } from 'hooks' import { useCurrency } from 'hooks/Tokens' +import { useZapInAction, useZapInPoolResult } from 'hooks/elasticZap' import { ApprovalState, useApproveCallback } from 'hooks/useApproveCallback' -import { useProAmmNFTPositionManagerReadingContract } from 'hooks/useContract' +import { useProAmmNFTPositionManagerReadingContract, useProAmmTickReader } from 'hooks/useContract' +import useDebounce from 'hooks/useDebounce' import useInterval from 'hooks/useInterval' import useMixpanel, { MIXPANEL_TYPE } from 'hooks/useMixpanel' import useProAmmPoolInfo from 'hooks/useProAmmPoolInfo' @@ -73,6 +82,7 @@ import { useRangeHopCallbacks, } from 'state/mint/proamm/hooks' import { Bound, Field, RANGE } from 'state/mint/proamm/type' +import { useSingleContractMultipleData } from 'state/multicall/hooks' import { useUserProMMPositions } from 'state/prommPools/hooks' import useGetElasticPools from 'state/prommPools/useGetElasticPools' import { useTokenPricesWithLoading } from 'state/tokenPrices/hooks' @@ -80,13 +90,16 @@ import { usePairFactor } from 'state/topTokens/hooks' import { useTransactionAdder } from 'state/transactions/hooks' import { TRANSACTION_TYPE } from 'state/transactions/type' import { useDegenModeManager, useUserSlippageTolerance } from 'state/user/hooks' +import { useCurrencyBalances } from 'state/wallet/hooks' import { ExternalLink, MEDIA_WIDTHS, StyledInternalLink, TYPE } from 'theme' import { basisPointsToPercent, calculateGasMargin, formattedNum } from 'utils' import { currencyId } from 'utils/currencyId' import { friendlyError } from 'utils/errorMessage' import { maxAmountSpend } from 'utils/maxAmountSpend' import { formatDisplayNumber, toString } from 'utils/numbers' -import { SLIPPAGE_STATUS, checkRangeSlippage } from 'utils/slippage' +import { SLIPPAGE_STATUS, checkRangeSlippage, formatSlippage } from 'utils/slippage' +import { getTokenSymbolWithHardcode } from 'utils/tokenInfo' +import { unwrappedToken } from 'utils/wrappedCurrency' import DisclaimerERC20 from './components/DisclaimerERC20' import NewPoolNote from './components/NewPoolNote' @@ -98,6 +111,7 @@ import { Container, DynamicSection, FlexLeft, + MethodSelector, PageWrapper, RangeBtn, RangeTab, @@ -239,11 +253,26 @@ export default function AddLiquidity() { const defaultFId = Number(searchParams.get('fId') || '0') const range = activeRanges.find(i => i.index === activeRangeIndex && i.farm.fId === defaultFId) + const isZapAvailable = !!(networkInfo as EVMNetworkInfo).elastic.zap + const [method, setMethod] = useState<'pair' | 'zap'>(() => (isZapAvailable ? 'zap' : 'pair')) + + useEffect(() => { + if (!isZapAvailable) { + setMethod('pair') + } + }, [isZapAvailable]) + const canJoinFarm = isFarmV2Available && positions.some(pos => activeRanges.some(r => pos && pos.tickLower <= r.tickLower && pos.tickUpper >= r.tickUpper)) - const farmPosWarning = positions.every(Boolean) && isFarmV2Available && !canJoinFarm + const farmPosWarning = + method === 'pair' + ? positions.every(Boolean) && isFarmV2Available && !canJoinFarm + : isFarmV2Available && + tickUpper !== undefined && + tickLower !== undefined && + activeRanges.every(r => r.tickLower < tickLower || r.tickUpper > tickUpper) const previousTicks: number[] | undefined = useProAmmPreviousTicks(pool, position) const mutiplePreviousTicks: number[][] | undefined = useProAmmMultiplePreviousTicks(pool, positions) @@ -606,7 +635,9 @@ export default function AddLiquidity() { ) const handleDismissConfirmation = useCallback(() => { - setShowConfirm(false) + if (method === 'zap') setShowZapConfirmation(false) + else setShowConfirm(false) + setZapError('') // if there was a tx hash, we want to clear the input if (txHash) { onFieldAInput('') @@ -614,7 +645,7 @@ export default function AddLiquidity() { navigate(`${APP_PATHS.MY_POOLS}/${networkInfo.route}?tab=elastic`) } setTxHash('') - }, [navigate, networkInfo.route, onFieldAInput, txHash]) + }, [navigate, networkInfo.route, onFieldAInput, txHash, method]) const handleDismissConfirmationRef = useRef(handleDismissConfirmation) @@ -816,9 +847,25 @@ export default function AddLiquidity() { const [allowedSlippage] = useUserSlippageTolerance() const slippageStatus = checkRangeSlippage(allowedSlippage, false) - const warnings = ( - - {noLiquidity && ( + useEffect(() => { + if (noLiquidity) { + setMethod('pair') + } + }, [noLiquidity]) + + const showWarning = + noLiquidity || + isPriceDeviated || + errorLabel || + invalidRange || + isFullRange || + outOfRange || + slippageStatus === SLIPPAGE_STATUS.HIGH || + farmPosWarning + + const warnings = !!showWarning && ( + + {!!noLiquidity && ( @@ -840,7 +887,7 @@ export default function AddLiquidity() { )} - {isPriceDeviated && ( + {!!isPriceDeviated && ( @@ -853,7 +900,7 @@ export default function AddLiquidity() { {formatDisplayNumber(usdPrices[tokenA.wrapped.address] / usdPrices[tokenB.wrapped.address], { significantDigits: 4, })}{' '} - {quoteCurrency.symbol}). You might have high impermanent loss after the pool is created + {quoteCurrency.symbol}). You might have high impermanent loss after the pool is created. ) : ( @@ -865,7 +912,7 @@ export default function AddLiquidity() { })}{' '} {quoteCurrency.symbol}) by {(priceDiff * 100).toFixed(2)}%. Please consider the{' '} - impermanent loss + impermanent loss. )} @@ -873,7 +920,7 @@ export default function AddLiquidity() { )} - {errorLabel && ( + {errorLabel && method === 'pair' && ( @@ -883,7 +930,7 @@ export default function AddLiquidity() { )} - {invalidRange ? ( + {!!invalidRange ? ( @@ -913,7 +960,7 @@ export default function AddLiquidity() { ) : null} - {farmPosWarning && ( + {!!farmPosWarning && ( @@ -960,282 +1007,651 @@ export default function AddLiquidity() { const isReverseWithFarm = baseCurrency?.wrapped.address !== farmV2S?.[0]?.token0.wrapped.address - const chart = ( - - {hasTab && ( - setPositionIndex(index)} - onAddTab={onAddPositionEvent} - onRemoveTab={onRemovePositionEvent} - showChart={showChart} - onToggleChart={(newShowChart: boolean | undefined) => { - const newValue = typeof newShowChart !== 'undefined' ? newShowChart : !showChart - if (newValue && tokenA?.symbol && tokenB?.symbol) { - mixpanelHandler(MIXPANEL_TYPE.ELASTIC_ADD_LIQUIDITY_CLICK_PRICE_CHART, { - token_1: tokenA?.symbol, - token_2: tokenB?.symbol, - }) - } - setShowChart(newValue) - }} - /> - )} - - {hasTab && ( - + const handleSwitch = (isQuote?: boolean) => { + const param1 = isQuote + ? currencyIdA + : baseCurrencyIsETHER + ? WETH[chainId].address + : NativeCurrencies[chainId].symbol + const param2 = isQuote + ? quoteCurrencyIsETHER + ? WETH[chainId].address + : NativeCurrencies[chainId].symbol + : currencyIdB + return ( + chainId && + navigate(`/${networkInfo.route}${APP_PATHS.ELASTIC_CREATE_POOL}/${param1}/${param2}/${feeAmount}`, { + replace: true, + }) + ) + } + + // ZAP state + const [zapValue, setZapValue] = useState('') + const [isReverse, setIsReverse] = useState(false) + const [useWrapped, setUseWrapped] = useState(false) + + const selectedCurrency = useMemo(() => { + const currency = isReverse ? currencies[Field.CURRENCY_B] : currencies[Field.CURRENCY_A] + if (useWrapped) return currency?.wrapped + return currency ? unwrappedToken(currency) : currency + }, [isReverse, currencies, useWrapped]) + + const quoteZapCurrency = useMemo(() => { + return isReverse ? currencies[Field.CURRENCY_A] : currencies[Field.CURRENCY_B] + }, [isReverse, currencies]) + + const debouncedValue = useDebounce(zapValue, 300) + const amountIn = useParsedAmount(selectedCurrency, debouncedValue) + + const equivalentQuoteAmount = + amountIn && pool && selectedCurrency && amountIn.multiply(pool.priceOf(selectedCurrency.wrapped)) + + const params = useMemo(() => { + return poolAddress && + amountIn?.greaterThan('0') && + selectedCurrency && + tickLower !== undefined && + tickUpper !== undefined && + quoteZapCurrency + ? { + poolAddress, + tokenIn: selectedCurrency.wrapped.address, + tokenOut: quoteZapCurrency.wrapped.address, + amountIn, + tickLower, + tickUpper, + } + : undefined + }, [amountIn, poolAddress, selectedCurrency, quoteZapCurrency, tickLower, tickUpper]) + + const { loading: zapLoading, result: zapResult, aggregatorData } = useZapInPoolResult(params) + const zapInContractAddress = (networkInfo as EVMNetworkInfo).elastic.zap?.router + const [zapApprovalState, zapApprove] = useApproveCallback(amountIn, zapInContractAddress) + const { zapIn } = useZapInAction() + const [showZapConfirmation, setShowZapConfirmation] = useState(false) + const [zapError, setZapError] = useState('') + + const zapBalances = useCurrencyBalances( + useMemo( + () => [ + currencies[Field.CURRENCY_A] + ? unwrappedToken(currencies[Field.CURRENCY_A] as Currency) + : currencies[Field.CURRENCY_A], + currencies[Field.CURRENCY_B] + ? unwrappedToken(currencies[Field.CURRENCY_B] as Currency) + : currencies[Field.CURRENCY_B], + currencies[Field.CURRENCY_A]?.wrapped, + currencies[Field.CURRENCY_B]?.wrapped, + ], + [currencies], + ), + ) + + const balanceIndex = useWrapped ? (isReverse ? 3 : 2) : isReverse ? 1 : 0 + const balance = zapBalances[balanceIndex] + let error: ReactElement | null = null + if (!zapValue) error = Enter an amount + else if (!amountIn) error = Invalid Input + else if (balance && amountIn?.greaterThan(balance)) error = Insufficient Balance + else if (!zapResult) error = Insufficient Liquidity + + const tickReader = useProAmmTickReader() + + const results = useSingleContractMultipleData( + poolAddress && tickLower !== undefined && tickUpper !== undefined ? tickReader : undefined, + 'getNearestInitializedTicks', + [ + [poolAddress, tickLower], + [poolAddress, tickUpper], + ], + ) + + const tickPreviousForZap = useMemo(() => { + return results.map(call => call.result?.previous) + }, [results]) + + const zapDetail = useZapDetail({ + pool, + tokenIn: selectedCurrency?.wrapped?.address, + position: undefined, + zapResult, + amountIn, + poolAddress, + tickLower, + tickUpper, + previousTicks: tickPreviousForZap, + aggregatorRoute: aggregatorData, + }) + + const { newPosDraft } = zapDetail + + const handleZap = async () => { + if (zapApprovalState === ApprovalState.NOT_APPROVED) { + zapApprove() + return + } + + if ( + tickUpper !== undefined && + tickLower !== undefined && + selectedCurrency && + zapResult && + amountIn?.quotient && + tickPreviousForZap.length == 2 && + pool + ) { + try { + setAttemptingTxn(true) + const { hash: txHash } = await zapIn( + { + tokenId: 0, + tokenIn: selectedCurrency.wrapped.address, + amountIn: amountIn.quotient.toString(), + equivalentQuoteAmount: equivalentQuoteAmount?.quotient.toString() || '0', + poolAddress, + tickLower, + tickUpper, + tickPrevious: [tickPreviousForZap[0], tickPreviousForZap[1]], + poolInfo: { + token0: pool.token0.wrapped.address, + fee: pool.fee, + token1: pool.token1.wrapped.address, + }, + liquidity: zapResult.liquidity.toString(), + aggregatorRoute: aggregatorData, + }, + { + zapWithNative: selectedCurrency.isNative, + }, + ) + setTxHash(txHash) + setAttemptingTxn(false) + const tokenSymbolIn = newPosDraft ? unwrappedToken(newPosDraft.amount0.currency).symbol : '' + const tokenSymbolOut = newPosDraft ? unwrappedToken(newPosDraft.amount1.currency).symbol : '' + addTransactionWithType({ + hash: txHash, + type: TRANSACTION_TYPE.ELASTIC_ZAP_IN_LIQUIDITY, + extraInfo: { + zapAmountIn: amountIn.toSignificant(6) || '0', + zapSymbolIn: selectedCurrency?.symbol || '', + tokenAmountIn: newPosDraft?.amount0.toSignificant(6) || '', + tokenAmountOut: newPosDraft?.amount1.toSignificant(6) || '', + tokenAddressIn: newPosDraft?.amount0.currency.wrapped.address || '', + tokenAddressOut: newPosDraft?.amount1.currency.wrapped.address || '', + tokenSymbolIn, + tokenSymbolOut, + arbitrary: { + token_1: tokenSymbolIn, + token_2: tokenSymbolOut, + }, + }, + }) + } catch (e) { + console.error('zap error', e) + setAttemptingTxn(false) + setZapError(e?.message || JSON.stringify(e)) + } + } + } + + const handleDissmissZap = () => { + setShowZapConfirmation(false) + setTxHash('') + setZapError('') + setAttemptingTxn(false) + } + + const zapPriceImpactNote = method === 'zap' && + !!(zapDetail.priceImpact?.isVeryHigh || zapDetail.priceImpact?.isHigh || zapDetail.priceImpact?.isInvalid) && + zapResult && + !zapLoading && ( + <> + {zapDetail.priceImpact.isVeryHigh ? ( + + ) : ( + )} - {hasTab && showChart ? null : ( - <> - - - - setShowFarmRangeSelect(false)}> - {isFarmV2Available ? Custom Ranges : Select a Range} - - - - {isFarmV2Available && ( - <> - - | - + + ) - - - Add your liquidity into one of the farming ranges to participate in Elastic Static Farm. - Only positions that cover the range of the farm will earn maximum rewards. Learn more{' '} - - here ↗ - - - - } - > - { - range && onFarmRangeSelected(range.tickLower, range.tickUpper) - setShowFarmRangeSelect(true) - }} + const ZapButton = ( + { + if (zapApprovalState === ApprovalState.NOT_APPROVED) { + zapApprove() + return + } + + setShowZapConfirmation(true) + }} + color={zapDetail.priceImpact?.isVeryHigh ? theme.text : undefined} + backgroundColor={ + zapApprovalState !== ApprovalState.APPROVED + ? undefined + : zapDetail.priceImpact.isVeryHigh + ? theme.red + : zapDetail.priceImpact.isHigh + ? theme.warning + : undefined + } + disabled={ + !!error || + zapApprovalState === ApprovalState.PENDING || + zapLoading || + (zapApprovalState === ApprovalState.APPROVED && !isDegenMode && zapDetail.priceImpact?.isVeryHigh) + } + style={{ width: upToMedium ? '100%' : 'fit-content', minWidth: '164px' }} + > + {(() => { + if (error) return error + if (zapApprovalState === ApprovalState.PENDING) + return ( + + Approving + + ) + if (zapApprovalState !== ApprovalState.APPROVED) return Approve + + if (zapLoading) + return ( + + Loading + + ) + return Preview + })()} + + ) + + const token0IsNative = + selectedCurrency?.isNative && selectedCurrency?.wrapped.address.toLowerCase() === pool?.token0.address.toLowerCase() + const zapSymbol0 = token0IsNative ? selectedCurrency.symbol : pool?.token0.symbol + const token1IsNative = + selectedCurrency?.isNative && selectedCurrency?.wrapped.address.toLowerCase() === pool?.token1.address.toLowerCase() + const zapSymbol1 = token1IsNative ? selectedCurrency.symbol : pool?.token1.symbol + + const chart = ( + <> + {!noLiquidity && } + + {hasTab && method !== 'zap' && ( + setPositionIndex(index)} + onAddTab={onAddPositionEvent} + onRemoveTab={onRemovePositionEvent} + showChart={showChart} + onToggleChart={(newShowChart: boolean | undefined) => { + const newValue = typeof newShowChart !== 'undefined' ? newShowChart : !showChart + if (newValue && tokenA?.symbol && tokenB?.symbol) { + mixpanelHandler(MIXPANEL_TYPE.ELASTIC_ADD_LIQUIDITY_CLICK_PRICE_CHART, { + token_1: tokenA?.symbol, + token_2: tokenB?.symbol, + }) + } + setShowChart(newValue) + }} + /> + )} + + {hasTab && ( + + )} + {hasTab && showChart ? null : ( + <> + + + + setShowFarmRangeSelect(false)}> + {isFarmV2Available ? Custom Ranges : Select a Range} + + + + {isFarmV2Available && ( + <> + + | + + + + + Add your liquidity into one of the farming ranges to participate in Elastic Static Farm. + Only positionsthat cover the range of the farm will earn maximum rewards. Learn more{' '} + + here ↗ + + + + } > - Farming Ranges - - - + { + range && onFarmRangeSelected(range.tickLower, range.tickUpper) + setShowFarmRangeSelect(true) + }} + > + Farming Ranges + + + + )} + + {showFarmRangeSelect && !!activeRanges.length && farmV2S?.[0] && ( + + {activeRanges.map(range => { + if (range.isRemoved) return null + return ( + { + searchParams.set('farmRange', range.index.toString()) + searchParams.set('fId', range.farm.fId.toString()) + setSearchParams(searchParams) + onFarmRangeSelected(+range.tickLower, +range.tickUpper) + }} + isSelected={activeRangeIndex === range.index && defaultFId === range.farm.fId} + > + + {convertTickToPrice( + isReverseWithFarm ? farmV2S[0].token1 : farmV2S[0].token0, + isReverseWithFarm ? farmV2S[0].token0 : farmV2S[0].token1, + isReverseWithFarm ? range.tickUpper : range.tickLower, + farmV2S[0].pool.fee, + )} + + {convertTickToPrice( + isReverseWithFarm ? farmV2S[0].token1 : farmV2S[0].token0, + isReverseWithFarm ? farmV2S[0].token0 : farmV2S[0].token1, + isReverseWithFarm ? range.tickLower : range.tickUpper, + farmV2S[0].pool.fee, + )} + + + ) + })} + )} - - {showFarmRangeSelect && !!activeRanges.length && farmV2S?.[0] && ( - - {activeRanges.map(range => { - if (range.isRemoved) return null + {!showFarmRangeSelect && + (() => { + const gap = '16px' + const buttonColumn = upToMedium ? 2 : 4 + const buttonWidth = `calc((100% - ${gap} * (${buttonColumn} - 1)) / ${buttonColumn})` return ( - { - searchParams.set('farmRange', range.index.toString()) - searchParams.set('fId', range.farm.fId.toString()) - setSearchParams(searchParams) - onFarmRangeSelected(+range.tickLower, +range.tickUpper) - }} - isSelected={activeRangeIndex === range.index && defaultFId === range.farm.fId} - > - - {convertTickToPrice( - isReverseWithFarm ? farmV2S[0].token1 : farmV2S[0].token0, - isReverseWithFarm ? farmV2S[0].token0 : farmV2S[0].token1, - isReverseWithFarm ? range.tickUpper : range.tickLower, - farmV2S[0].pool.fee, - )} - - {convertTickToPrice( - isReverseWithFarm ? farmV2S[0].token1 : farmV2S[0].token0, - isReverseWithFarm ? farmV2S[0].token0 : farmV2S[0].token1, - isReverseWithFarm ? range.tickLower : range.tickUpper, - farmV2S[0].pool.fee, - )} - - - ) - })} - - )} - {!showFarmRangeSelect && - (() => { - const gap = '16px' - const buttonColumn = upToMedium ? 2 : 4 - const buttonWidth = `calc((100% - ${gap} * (${buttonColumn} - 1)) / ${buttonColumn})` - return ( - - {RANGE_LIST.map(range => ( - - - setRange(range)} - isSelected={range === activeRange} - onMouseEnter={() => setShownTooltip(range)} - onMouseLeave={() => setShownTooltip(null)} + + {RANGE_LIST.map(range => ( + + - {rangeData[range].title} - - - - ))} - - ) - })()} - - - - Estimated Risk - - - - Estimated Profit - - - - {price && baseCurrency && quoteCurrency && !noLiquidity && ( - - - Current Price - - - + setRange(range)} + isSelected={range === activeRange} + onMouseEnter={() => setShownTooltip(range)} + onMouseLeave={() => setShownTooltip(null)} + > + {rangeData[range].title} + + + + ))} + + ) + })()} + + + + Estimated Risk - - {quoteCurrency?.symbol} per {baseCurrency.symbol} + + + Estimated Profit - - )} - - - - - - - + + + {price && baseCurrency && quoteCurrency && !noLiquidity && ( + + + Current Price + + + + + + {quoteCurrency?.symbol} per {baseCurrency.symbol} + + + )} + + + + + + + + - - - - Deposit Amounts - - - - { - onFieldAInput(maxAmounts[Field.CURRENCY_A]?.toExact() ?? '') - }} - onHalf={() => { - onFieldAInput(currencyBalanceA?.divide(2).toExact() ?? '') - }} - currency={currencies_A ?? null} - id="add-liquidity-input-tokena" - showCommonBases - positionMax="top" - locked={depositADisabled} - estimatedUsd={formattedNum(estimatedUsdCurrencyA.toString(), true) || undefined} - disableCurrencySelect={!baseCurrencyIsETHER && !baseCurrencyIsWETH} - isSwitchMode={baseCurrencyIsETHER || baseCurrencyIsWETH} - onSwitchCurrency={() => { - chainId && - navigate( - `/${networkInfo.route}${APP_PATHS.ELASTIC_CREATE_POOL}/${ - baseCurrencyIsETHER ? WETH[chainId].address : NativeCurrencies[chainId].symbol - }/${currencyIdB}/${feeAmount}`, - { replace: true }, - ) - }} - outline - /> - - - { - onFieldBInput(maxAmounts[Field.CURRENCY_B]?.toExact() ?? '') - }} - onHalf={() => { - onFieldBInput(currencyBalanceB?.divide(2).toExact() ?? '') - }} - currency={currencies_B ?? null} - id="add-liquidity-input-tokenb" - showCommonBases - positionMax="top" - locked={depositBDisabled} - estimatedUsd={formattedNum(estimatedUsdCurrencyB.toString(), true) || undefined} - disableCurrencySelect={!quoteCurrencyIsETHER && !quoteCurrencyIsWETH} - isSwitchMode={quoteCurrencyIsETHER || quoteCurrencyIsWETH} - onSwitchCurrency={() => { - chainId && - navigate( - `/${networkInfo.route}${APP_PATHS.ELASTIC_CREATE_POOL}/${currencyIdA}/${ - quoteCurrencyIsETHER ? WETH[chainId].address : NativeCurrencies[chainId].symbol - }/${feeAmount}`, - { replace: true }, - ) - }} - outline - /> - - - - - )} - - + + + Deposit Amounts + + + {method === 'pair' ? ( + + + { + onFieldAInput(maxAmounts[Field.CURRENCY_A]?.toExact() ?? '') + }} + onHalf={() => { + onFieldAInput(currencyBalanceA?.divide(2).toExact() ?? '') + }} + currency={currencies_A ?? null} + id="add-liquidity-input-tokena" + showCommonBases + positionMax="top" + locked={depositADisabled} + estimatedUsd={formattedNum(estimatedUsdCurrencyA.toString(), true) || undefined} + disableCurrencySelect={!baseCurrencyIsETHER && !baseCurrencyIsWETH} + isSwitchMode={baseCurrencyIsETHER || baseCurrencyIsWETH} + onSwitchCurrency={() => handleSwitch(false)} + outline + /> + + + { + onFieldBInput(maxAmounts[Field.CURRENCY_B]?.toExact() ?? '') + }} + onHalf={() => { + onFieldBInput(currencyBalanceB?.divide(2).toExact() ?? '') + }} + currency={currencies_B ?? null} + id="add-liquidity-input-tokenb" + showCommonBases + positionMax="top" + locked={depositBDisabled} + estimatedUsd={formattedNum(estimatedUsdCurrencyB.toString(), true) || undefined} + disableCurrencySelect={!quoteCurrencyIsETHER && !quoteCurrencyIsWETH} + isSwitchMode={quoteCurrencyIsETHER || quoteCurrencyIsWETH} + onSwitchCurrency={() => handleSwitch(true)} + outline + /> + + + ) : ( + +
+ { + setZapValue(v) + }} + onMax={() => { + const amount = zapBalances[balanceIndex] + if (amount) setZapValue(maxAmountSpend(amount)?.toExact() || '') + }} + onHalf={() => { + setZapValue(zapBalances[balanceIndex]?.divide('2').toExact() || '') + }} + currency={selectedCurrency} + positionMax="top" + showCommonBases + estimatedUsd={formattedNum(zapDetail.amountInUsd, true)} + isSwitchMode + onSwitchCurrency={() => { + if (selectedCurrency?.isNative) { + setUseWrapped(true) + } else { + setUseWrapped(false) + setIsReverse(prev => !prev) + } + }} + outline + /> +
+ + + Est. Pooled {zapSymbol0} + {zapLoading ? ( + zapDetail.skeleton() + ) : !zapResult || !pool ? ( + '--' + ) : ( + + + + {zapDetail.newPooledAmount0?.toSignificant(10)} {zapSymbol0} + + + )} + + + + Est. Pooled {zapSymbol1} + {zapLoading ? ( + zapDetail.skeleton() + ) : !zapResult || !pool ? ( + '--' + ) : ( + + + + {zapDetail.newPooledAmount1?.toSignificant(10)} {zapSymbol1} + + + )} + + + + Max Slippage + {formatSlippage(allowedSlippage)} + + + + Price Impact + {zapLoading ? ( + zapDetail.skeleton(40) + ) : !zapResult ? ( + '--' + ) : ( + + {zapDetail.priceImpact.isInvalid + ? '--' + : zapDetail.priceImpact.value < 0.01 + ? '<0.01%' + : zapDetail.priceImpact.value.toFixed(2) + '%'} + + )} + + +
+ )} +
+ + )} + + + + + {warnings} + {tokenA && tokenB && } + + {zapPriceImpactNote} + {method === 'pair' || !account ? : ZapButton} + + ) const [rotated, setRotated] = useState(false) @@ -1346,9 +1762,19 @@ export default function AddLiquidity() { onFarmRangeSelected(range.tickLower, range.tickUpper) } }, [isFarmV2Available, range?.tickUpper, range?.tickLower, onFarmRangeSelected, positionsState, pIndex]) - if (!isEVM) return + const symbol0 = getTokenSymbolWithHardcode( + chainId, + pool?.token0?.wrapped.address, + useWrapped ? pool?.token0?.wrapped.symbol : (pool?.token0 ? unwrappedToken(pool.token0) : pool?.token0)?.symbol, + ) + const symbol1 = getTokenSymbolWithHardcode( + chainId, + pool?.token1?.wrapped.address, + useWrapped ? pool?.token1?.wrapped.symbol : (pool?.token1 ? unwrappedToken(pool.token1) : pool?.token1)?.symbol, + ) + return ( <> {!upToMedium && {chart}}
- - {warnings && ( - - {warnings} - - )} - {tokenA && tokenB && ( - - - - )} - - - - - )} + + + Zapping {amountIn?.toSignificant(6)} {selectedCurrency?.symbol} into {newPosDraft?.amount0.toSignificant(6)}{' '} + {symbol0} and {newPosDraft?.amount1.toSignificant(6)} {symbol1} of liquidity to the pool + + } + content={() => ( + + {zapError ? ( + + ) : ( + ( +
+ {!!zapDetail.newPosDraft && } + + {!!zapDetail.newPosDraft && ( + + )} +
+ )} + showGridListOption={false} + bottomContent={() => ( + + {warnings} + {zapPriceImpactNote} + + + Supply + + + + )} + /> + )} +
+ )} + /> ) } diff --git a/src/pages/AddLiquidityV2/styled.tsx b/src/pages/AddLiquidityV2/styled.tsx index 95d90a6134..2a4672f85f 100644 --- a/src/pages/AddLiquidityV2/styled.tsx +++ b/src/pages/AddLiquidityV2/styled.tsx @@ -1,9 +1,16 @@ -import { Flex } from 'rebass' -import styled, { css, keyframes, useTheme } from 'styled-components' +import { Trans } from '@lingui/macro' +import { useMedia } from 'react-use' +import { Flex, Text } from 'rebass' +import styled, { CSSProperties, css, keyframes, useTheme } from 'styled-components' +import { ReactComponent as ZapIcon } from 'assets/svg/zap.svg' import { ButtonOutlined } from 'components/Button' import { AutoColumn } from 'components/Column' +import InfoHelper from 'components/InfoHelper' import Input from 'components/NumericalInput' +import { EVMNetworkInfo } from 'constants/networks/type' +import { useActiveWeb3React } from 'hooks' +import { MEDIA_WIDTHS } from 'theme' export const PageWrapper = styled(AutoColumn)` padding: 0 2rem 1rem; @@ -209,3 +216,104 @@ export const RangeTab = styled.div<{ active: boolean }>` color: ${({ theme, active }) => (active ? theme.primary : theme.subText)}; cursor: pointer; ` + +const MethodTabs = styled.div` + border-radius: 999px; + border: 1px solid ${({ theme }) => theme.border}; + background: ${({ theme }) => theme.buttonBlack}; + padding: 2px; + display: flex; +` + +const MethodTab = styled.div<{ active: boolean; disabled?: boolean }>` + background: ${({ theme, active }) => (active ? theme.tabActive : theme.buttonBlack)}; + opacity: ${({ active }) => (active ? 1 : 0.6)}; + cursor: pointer; + border-radius: 999px; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + font-weight: 500; + line-height: 20px; + padding: 2px; + min-width: 96px; + + ${({ disabled }) => + disabled + ? css` + opacity: 0.6; + cursor: not-allowed; + ` + : undefined} +` + +export const MethodSelector = ({ + method, + setMethod, + sx, +}: { + method: 'zap' | 'pair' + setMethod: (method: 'pair' | 'zap') => void + sx?: CSSProperties +}) => { + const theme = useTheme() + const upToExtraSmall = useMedia(`(max-width: ${MEDIA_WIDTHS.upToExtraSmall}px)`) + + const { networkInfo } = useActiveWeb3React() + const isZapAvailable = !!(networkInfo as EVMNetworkInfo).elastic.zap + + return ( + + {!upToExtraSmall && ( + + Your Position + + )} + + + Add liquidity by: + + + setMethod('pair')} active={method === 'pair'}> + Token Pair + + isZapAvailable && setMethod('zap')} + active={method === 'zap'} + disabled={!isZapAvailable} + > + + + + Zap In + + Add liquidity instantly using only one token! + ) : ( + Zap will be available soon. + ) + } + /> + + + + + + ) +} diff --git a/src/pages/IncreaseLiquidity/Chart.tsx b/src/pages/IncreaseLiquidity/Chart.tsx index 4bf050a9bf..7b970f18a3 100644 --- a/src/pages/IncreaseLiquidity/Chart.tsx +++ b/src/pages/IncreaseLiquidity/Chart.tsx @@ -1,6 +1,6 @@ import { Position } from '@kyberswap/ks-sdk-elastic' import { Trans, t } from '@lingui/macro' -import { useCallback, useState } from 'react' +import React, { useCallback, useState } from 'react' import { Flex, Text } from 'rebass' import { AutoColumn } from 'components/Column' @@ -13,7 +13,7 @@ import { Bound } from 'state/mint/proamm/type' import { formatTickPrice } from 'utils/formatTickPrice' import { unwrappedToken } from 'utils/wrappedCurrency' -export default function Chart({ +function Chart({ position, ticksAtLimit, }: { @@ -114,3 +114,5 @@ export default function Chart({ ) } + +export default React.memo(Chart) diff --git a/src/pages/IncreaseLiquidity/index.tsx b/src/pages/IncreaseLiquidity/index.tsx index eb309e0a55..231891ae24 100644 --- a/src/pages/IncreaseLiquidity/index.tsx +++ b/src/pages/IncreaseLiquidity/index.tsx @@ -4,7 +4,7 @@ import { FeeAmount, NonfungiblePositionManager } from '@kyberswap/ks-sdk-elastic import { Trans, t } from '@lingui/macro' import { BigNumber } from 'ethers' import JSBI from 'jsbi' -import { useCallback, useEffect, useState } from 'react' +import { ReactElement, useCallback, useEffect, useMemo, useState } from 'react' import { AlertTriangle } from 'react-feather' import { Link, Navigate, useNavigate, useParams } from 'react-router-dom' import { useMedia, usePrevious } from 'react-use' @@ -15,10 +15,13 @@ import RangeBadge from 'components/Badge/RangeBadge' import { ButtonError, ButtonLight, ButtonPrimary } from 'components/Button' import { BlackCard, WarningCard } from 'components/Card' import { AutoColumn } from 'components/Column' +import Copy from 'components/Copy' import CurrencyInputPanel from 'components/CurrencyInputPanel' import CurrencyLogo from 'components/CurrencyLogo' import Divider from 'components/Divider' import Dots from 'components/Dots' +import DoubleCurrencyLogo from 'components/DoubleLogo' +import { useZapDetail } from 'components/ElasticZap/ZapDetail' import FormattedCurrencyAmount from 'components/FormattedCurrencyAmount' import Loader from 'components/Loader' import { AddRemoveTabs, LiquidityAction } from 'components/NavigationTabs' @@ -28,24 +31,31 @@ import ProAmmPriceRangeConfirm from 'components/ProAmm/ProAmmPriceRangeConfirm' import Rating from 'components/Rating' import { RowBetween } from 'components/Row' import { SLIPPAGE_EXPLANATION_URL } from 'components/SlippageWarningNote' +import PriceImpactNote, { ZapHighPriceImpact } from 'components/SwapForm/PriceImpactNote' +import useParsedAmount from 'components/SwapForm/hooks/useParsedAmount' import TransactionConfirmationModal, { ConfirmationModalContent, TransactionErrorContent, } from 'components/TransactionConfirmationModal' import { TutorialType } from 'components/Tutorial' +import { FeeTag } from 'components/YieldPools/ElasticFarmGroup/styleds' import { didUserReject } from 'constants/connectors/utils' -import { APP_PATHS } from 'constants/index' +import { APP_PATHS, ELASTIC_BASE_FEE_UNIT } from 'constants/index' import { EVMNetworkInfo } from 'constants/networks/type' import { NativeCurrencies } from 'constants/tokens' import { useActiveWeb3React, useWeb3React } from 'hooks' import { useCurrency } from 'hooks/Tokens' +import { useZapInAction, useZapInPoolResult } from 'hooks/elasticZap' import { ApprovalState, useApproveCallback } from 'hooks/useApproveCallback' import { useProAmmNFTPositionManagerReadingContract } from 'hooks/useContract' +import useDebounce from 'hooks/useDebounce' import { useProAmmDerivedPositionInfo } from 'hooks/useProAmmDerivedPositionInfo' +import useProAmmPoolInfo from 'hooks/useProAmmPoolInfo' import { useProAmmPositionsFromTokenId } from 'hooks/useProAmmPositions' import useProAmmPreviousTicks from 'hooks/useProAmmPreviousTicks' import useTheme from 'hooks/useTheme' import useTransactionDeadline from 'hooks/useTransactionDeadline' +import { MethodSelector } from 'pages/AddLiquidityV2/styled' import { Container, Content, @@ -63,11 +73,13 @@ import { useTokenPrices } from 'state/tokenPrices/hooks' import { useTransactionAdder } from 'state/transactions/hooks' import { TRANSACTION_TYPE } from 'state/transactions/type' import { useDegenModeManager, useUserSlippageTolerance } from 'state/user/hooks' +import { useCurrencyBalances } from 'state/wallet/hooks' import { MEDIA_WIDTHS, TYPE } from 'theme' -import { calculateGasMargin, formattedNum, isAddressString } from 'utils' +import { calculateGasMargin, formattedNum, isAddressString, shortenAddress } from 'utils' import { maxAmountSpend } from 'utils/maxAmountSpend' import { formatDollarAmount } from 'utils/numbers' -import { SLIPPAGE_STATUS, checkRangeSlippage } from 'utils/slippage' +import { SLIPPAGE_STATUS, checkRangeSlippage, formatSlippage } from 'utils/slippage' +import { getTokenSymbolWithHardcode } from 'utils/tokenInfo' import { unwrappedToken } from 'utils/wrappedCurrency' import Chart from './Chart' @@ -88,8 +100,16 @@ const TextUnderlineTransparent = styled(Text)` ` export default function IncreaseLiquidity() { - const { currencyIdB, currencyIdA, feeAmount: feeAmountFromUrl, tokenId } = useParams() const { account, chainId, isEVM, networkInfo } = useActiveWeb3React() + const [method, setMethod] = useState<'pair' | 'zap'>('zap') + const isZapAvailable = !!(networkInfo as EVMNetworkInfo).elastic.zap + useEffect(() => { + if (!isZapAvailable) { + setMethod('pair') + } + }, [isZapAvailable]) + + const { currencyIdB, currencyIdA, feeAmount: feeAmountFromUrl, tokenId } = useParams() const { library } = useWeb3React() const navigate = useNavigate() const theme = useTheme() @@ -175,6 +195,12 @@ export default function IncreaseLiquidity() { const address1 = quoteCurrency?.wrapped.address || '' const usdPrices = useTokenPrices([address0, address1]) + const poolAddress = useProAmmPoolInfo( + existingPosition?.pool.token0, + existingPosition?.pool.token1, + existingPosition?.pool.fee as FeeAmount, + ) + const estimatedUsdCurrencyA = parsedAmounts[Field.CURRENCY_A] && usdPrices[address0] ? parseFloat(parsedAmounts[Field.CURRENCY_A]?.toExact() || '0') * usdPrices[address0] @@ -197,7 +223,7 @@ export default function IncreaseLiquidity() { const previousTicks = // : number[] = [] - useProAmmPreviousTicks(pool, position) + useProAmmPreviousTicks(pool, existingPosition) const { onFieldAInput, onFieldBInput, onResetMintState } = useProAmmMintActionHandlers(noLiquidity, 0) useEffect(() => { @@ -331,7 +357,9 @@ export default function IncreaseLiquidity() { } const handleDismissConfirmation = useCallback(() => { - setShowConfirm(false) + if (method === 'zap') setShowZapConfirmation(false) + else setShowConfirm(false) + setZapError('') // if there was a tx hash, we want to clear the input if (txHash) { onFieldAInput('') @@ -339,7 +367,7 @@ export default function IncreaseLiquidity() { navigate('/myPools') } setTxHash('') - }, [navigate, onFieldAInput, txHash]) + }, [navigate, onFieldAInput, txHash, method]) const addIsUnsupported = false @@ -427,10 +455,343 @@ export default function IncreaseLiquidity() { const slippageStatus = checkRangeSlippage(allowedSlippage, false) + // ZAP STATE + const [value, setValue] = useState('') + const [isReverse, setIsReverse] = useState(false) + const [useWrapped, setUseWrapped] = useState(false) + + const selectedCurrency = useMemo(() => { + const currency = isReverse ? currencies[Field.CURRENCY_B] : currencies[Field.CURRENCY_A] + if (useWrapped) return currency?.wrapped + return currency ? unwrappedToken(currency) : currency + }, [isReverse, currencies, useWrapped]) + + const quoteZapCurrency = useMemo(() => { + return isReverse ? currencies[Field.CURRENCY_A] : currencies[Field.CURRENCY_B] + }, [isReverse, currencies]) + + const debouncedValue = useDebounce(value, 300) + const amountIn = useParsedAmount(selectedCurrency, debouncedValue) + + const equivalentQuoteAmount = + amountIn && pool && selectedCurrency && amountIn.multiply(pool.priceOf(selectedCurrency.wrapped)) + + const tickLower = existingPosition?.tickLower + const tickUpper = existingPosition?.tickUpper + + const params = useMemo(() => { + return poolAddress && + amountIn?.greaterThan('0') && + selectedCurrency && + quoteZapCurrency && + tickLower !== undefined && + tickUpper !== undefined + ? { + poolAddress, + tokenIn: selectedCurrency.wrapped.address, + tokenOut: quoteZapCurrency.wrapped.address, + amountIn, + tickLower, + tickUpper, + } + : undefined + }, [amountIn, poolAddress, selectedCurrency, quoteZapCurrency, tickLower, tickUpper]) + + const { loading: zapLoading, result: zapResult, aggregatorData } = useZapInPoolResult(params) + const zapInContractAddress = (networkInfo as EVMNetworkInfo).elastic.zap?.router + const [zapApprovalState, zapApprove] = useApproveCallback(amountIn, zapInContractAddress) + const { zapIn } = useZapInAction() + const [showZapConfirmation, setShowZapConfirmation] = useState(false) + const [zapError, setZapError] = useState('') + + const zapBalances = useCurrencyBalances( + useMemo( + () => [ + currencies[Field.CURRENCY_A] + ? unwrappedToken(currencies[Field.CURRENCY_A] as Currency) + : currencies[Field.CURRENCY_A], + currencies[Field.CURRENCY_B] + ? unwrappedToken(currencies[Field.CURRENCY_B] as Currency) + : currencies[Field.CURRENCY_B], + currencies[Field.CURRENCY_A]?.wrapped, + currencies[Field.CURRENCY_B]?.wrapped, + ], + [currencies], + ), + ) + + const balanceIndex = useWrapped ? (isReverse ? 3 : 2) : isReverse ? 1 : 0 + const balance = zapBalances[balanceIndex] + let error: ReactElement | null = null + if (!value) error = Enter an amount + else if (!amountIn) error = Invalid Input + else if (balance && amountIn?.greaterThan(balance)) error = Insufficient Balance + + const zapDetail = useZapDetail({ + pool: existingPosition?.pool, + position: existingPosition, + tokenIn: selectedCurrency?.wrapped.address, + tokenId, + amountIn, + zapResult, + poolAddress, + tickLower: existingPosition?.tickLower, + tickUpper: existingPosition?.tickUpper, + previousTicks: previousTicks, + aggregatorRoute: aggregatorData, + }) + + const handleZap = async () => { + if (zapApprovalState === ApprovalState.NOT_APPROVED) { + zapApprove() + return + } + + if (selectedCurrency && tokenId && zapResult && amountIn?.quotient && existingPosition && previousTicks?.length) { + try { + setAttemptingTxn(true) + const { hash: txHash } = await zapIn( + { + tokenId: tokenId.toString(), + tokenIn: selectedCurrency.wrapped.address, + amountIn: amountIn.quotient.toString(), + equivalentQuoteAmount: equivalentQuoteAmount?.quotient.toString() || '0', + poolAddress, + tickLower: existingPosition.tickLower, + tickUpper: existingPosition.tickUpper, + tickPrevious: [previousTicks[0], previousTicks[1]], + poolInfo: { + token0: existingPosition.pool.token0.wrapped.address, + fee: existingPosition.pool.fee, + token1: existingPosition.pool.token1.wrapped.address, + }, + liquidity: zapResult.liquidity.toString(), + aggregatorRoute: aggregatorData, + }, + { + zapWithNative: selectedCurrency.isNative, + }, + ) + + setTxHash(txHash) + setAttemptingTxn(false) + const tokenSymbolIn = zapDetail.newPosDraft ? unwrappedToken(zapDetail.newPosDraft.amount0.currency).symbol : '' + const tokenSymbolOut = zapDetail.newPosDraft + ? unwrappedToken(zapDetail.newPosDraft?.amount1.currency).symbol + : '' + addTransactionWithType({ + hash: txHash, + type: TRANSACTION_TYPE.ELASTIC_ZAP_IN_LIQUIDITY, + extraInfo: { + zapAmountIn: amountIn.toSignificant(6) || '0', + zapSymbolIn: selectedCurrency?.symbol || '', + tokenAmountIn: zapDetail.newPosDraft?.amount0.toSignificant(6) || '', + tokenAmountOut: zapDetail.newPosDraft?.amount1.toSignificant(6) || '', + tokenAddressIn: zapDetail.newPosDraft?.amount0.currency.wrapped.address || '', + tokenAddressOut: zapDetail.newPosDraft?.amount1.currency.wrapped.address || '', + tokenSymbolIn, + tokenSymbolOut, + nftId: tokenId, + arbitrary: { + token_1: tokenSymbolIn, + token_2: tokenSymbolOut, + }, + }, + }) + } catch (e) { + console.error('zap error', e) + setAttemptingTxn(false) + setZapError(e?.message || JSON.stringify(e)) + } + } + } + + const ZapButton = ( + { + if (zapApprovalState === ApprovalState.NOT_APPROVED) { + zapApprove() + return + } + + setShowZapConfirmation(true) + }} + backgroundColor={ + zapApprovalState !== ApprovalState.APPROVED + ? undefined + : zapDetail.priceImpact.isVeryHigh + ? theme.red + : zapDetail.priceImpact.isHigh + ? theme.warning + : undefined + } + disabled={ + !!error || + zapApprovalState === ApprovalState.PENDING || + zapLoading || + (zapApprovalState === ApprovalState.APPROVED && !isDegenMode && zapDetail.priceImpact?.isVeryHigh) + } + style={{ width: upToMedium ? '100%' : 'fit-content', minWidth: '164px' }} + > + {(() => { + if (error) return error + if (zapApprovalState === ApprovalState.PENDING) + return ( + + Approving + + ) + if (zapApprovalState !== ApprovalState.APPROVED) return Approve + + if (zapLoading) + return ( + + Loading + + ) + return Preview + })()} + + ) + if (!isEVM) return + const inputAmountStyle = { + flex: 1, + border: `1px solid ${theme.border}`, + borderRadius: '1rem', + overflow: 'hidden', + } + + const handleSwitch = (isQuote?: boolean) => { + const param1 = isQuote + ? currencyIdA + : baseCurrencyIsETHER + ? WETH[chainId].address + : NativeCurrencies[chainId].symbol + const param2 = isQuote + ? quoteCurrencyIsETHER + ? WETH[chainId].address + : NativeCurrencies[chainId].symbol + : currencyIdB + return ( + chainId && + navigate(`/${networkInfo.route}${APP_PATHS.ELASTIC_INCREASE_LIQ}/${param1}/${param2}/${feeAmount}/${tokenId}`, { + replace: true, + }) + ) + } + + const handleDissmissZap = () => { + setShowZapConfirmation(false) + setTxHash('') + setZapError('') + setAttemptingTxn(false) + } + + const token0 = existingPosition?.pool.token0 + const token1 = existingPosition?.pool.token1 + + const symbol0 = getTokenSymbolWithHardcode( + chainId, + token0?.wrapped.address, + useWrapped ? token0?.wrapped.symbol : (token0 ? unwrappedToken(token0) : token0)?.symbol, + ) + const symbol1 = getTokenSymbolWithHardcode( + chainId, + token1?.wrapped.address, + useWrapped ? token1?.wrapped.symbol : (token1 ? unwrappedToken(token1) : token1)?.symbol, + ) + + const zapPriceImpactNote = method === 'zap' && + !!(zapDetail.priceImpact?.isVeryHigh || zapDetail.priceImpact?.isHigh || zapDetail.priceImpact?.isInvalid) && + zapResult && + !zapLoading && ( + <> + {zapDetail.priceImpact.isVeryHigh ? ( + + ) : ( + + )} +
+ + ) + + const token0IsNative = + selectedCurrency?.isNative && selectedCurrency?.wrapped.address.toLowerCase() === pool?.token0.address.toLowerCase() + const zapSymbol0 = token0IsNative ? selectedCurrency.symbol : pool?.token0.symbol + const token1IsNative = + selectedCurrency?.isNative && selectedCurrency?.wrapped.address.toLowerCase() === pool?.token1.address.toLowerCase() + const zapSymbol1 = token1IsNative ? selectedCurrency.symbol : pool?.token1.symbol + return ( <> + + Zapping {amountIn?.toSignificant(6)} {selectedCurrency?.symbol} into{' '} + {zapDetail.newPosDraft?.amount0.toSignificant(6)} {symbol0} and{' '} + {zapDetail.newPosDraft?.amount1.toSignificant(6)} {symbol1} of liquidity to the pool + + } + content={() => ( + + {zapError ? ( + + ) : ( + ( +
+ {!!zapDetail.newPosDraft && } + + {!!zapDetail.newPosDraft && ( + + )} +
+ )} + showGridListOption={false} + bottomContent={() => ( + + {zapPriceImpactNote} + + + Supply + + + + )} + /> + )} +
+ )} + /> + - + + + + + {unwrappedToken(existingPosition.pool.token0).symbol} -{' '} + {unwrappedToken(existingPosition.pool.token1).symbol} + + FEE {(existingPosition?.pool.fee * 100) / ELASTIC_BASE_FEE_UNIT}% + + + {shortenAddress(chainId, poolAddress)}{' '} + + } + /> + @@ -570,6 +959,7 @@ export default function IncreaseLiquidity() { + -
- { - onFieldAInput(maxAmounts[Field.CURRENCY_A]?.toExact() ?? '') - }} - onHalf={() => { - onFieldAInput(currencyBalances[Field.CURRENCY_A]?.divide(2)?.toExact() ?? '') - }} - currency={currencies[Field.CURRENCY_A] ?? null} - id="add-liquidity-input-tokena" - showCommonBases - positionMax="top" - locked={depositADisabled} - estimatedUsd={formattedNum(estimatedUsdCurrencyA.toString(), true) || undefined} - disableCurrencySelect={!baseCurrencyIsETHER && !baseCurrencyIsWETH} - isSwitchMode={baseCurrencyIsETHER || baseCurrencyIsWETH} - onSwitchCurrency={() => { - chainId && - navigate( - `/${networkInfo.route}${APP_PATHS.ELASTIC_INCREASE_LIQ}/${ - baseCurrencyIsETHER ? WETH[chainId].address : NativeCurrencies[chainId].symbol - }/${currencyIdB}/${feeAmount}/${tokenId}`, - { - replace: true, - }, - ) - }} - /> -
- -
- { - onFieldBInput(maxAmounts[Field.CURRENCY_B]?.toExact() ?? '') - }} - onHalf={() => { - onFieldBInput(currencyBalances[Field.CURRENCY_B]?.divide(2).toExact() ?? '') - }} - currency={currencies[Field.CURRENCY_B] ?? null} - id="add-liquidity-input-tokenb" - showCommonBases - positionMax="top" - locked={depositBDisabled} - estimatedUsd={formattedNum(estimatedUsdCurrencyB.toString(), true) || undefined} - disableCurrencySelect={!quoteCurrencyIsETHER && !quoteCurrencyIsWETH} - isSwitchMode={quoteCurrencyIsETHER || quoteCurrencyIsWETH} - onSwitchCurrency={() => { - chainId && - navigate( - `/${networkInfo.route}${APP_PATHS.ELASTIC_INCREASE_LIQ}/${currencyIdA}/${ - quoteCurrencyIsETHER ? WETH[chainId].address : NativeCurrencies[chainId].symbol - }/${feeAmount}/${tokenId}`, - { replace: true }, - ) - }} - /> -
+ {method === 'pair' ? ( + <> +
+ { + onFieldAInput(maxAmounts[Field.CURRENCY_A]?.toExact() ?? '') + }} + onHalf={() => { + onFieldAInput(currencyBalances[Field.CURRENCY_A]?.divide(2)?.toExact() ?? '') + }} + currency={currencies[Field.CURRENCY_A] ?? null} + id="add-liquidity-input-tokena" + showCommonBases + positionMax="top" + locked={depositADisabled} + estimatedUsd={formattedNum(estimatedUsdCurrencyA.toString(), true) || undefined} + disableCurrencySelect={!baseCurrencyIsETHER && !baseCurrencyIsWETH} + isSwitchMode={baseCurrencyIsETHER || baseCurrencyIsWETH} + onSwitchCurrency={() => handleSwitch(false)} + /> +
+ +
+ { + onFieldBInput(maxAmounts[Field.CURRENCY_B]?.toExact() ?? '') + }} + onHalf={() => { + onFieldBInput(currencyBalances[Field.CURRENCY_B]?.divide(2).toExact() ?? '') + }} + currency={currencies[Field.CURRENCY_B] ?? null} + id="add-liquidity-input-tokenb" + showCommonBases + positionMax="top" + locked={depositBDisabled} + estimatedUsd={formattedNum(estimatedUsdCurrencyB.toString(), true) || undefined} + disableCurrencySelect={!quoteCurrencyIsETHER && !quoteCurrencyIsWETH} + isSwitchMode={quoteCurrencyIsETHER || quoteCurrencyIsWETH} + onSwitchCurrency={() => handleSwitch(true)} + /> +
+ + ) : ( + +
+ { + setValue(v) + }} + onMax={() => { + const amount = zapBalances[balanceIndex] + if (amount) setValue(maxAmountSpend(amount)?.toExact() || '') + }} + onHalf={() => { + setValue(zapBalances[balanceIndex]?.divide('2').toExact() || '') + }} + currency={selectedCurrency} + positionMax="top" + showCommonBases + estimatedUsd={formattedNum(zapDetail.amountInUsd, true)} + isSwitchMode + onSwitchCurrency={() => { + if (selectedCurrency?.isNative) { + setUseWrapped(true) + } else { + setUseWrapped(false) + setIsReverse(prev => !prev) + } + }} + /> +
+ + + Est. Pooled {zapSymbol0} + {zapLoading ? ( + zapDetail.skeleton() + ) : !zapResult || !pool ? ( + '--' + ) : ( + + + + {zapDetail.newPooledAmount0?.toSignificant(10)} {zapSymbol0} + + + )} + + + + Est. Pooled {zapSymbol1} + {zapLoading ? ( + zapDetail.skeleton() + ) : !zapResult || !pool ? ( + '--' + ) : ( + + + + {zapDetail.newPooledAmount1?.toSignificant(10)} {zapSymbol1} + + + )} + + + + Max Slippage + {formatSlippage(allowedSlippage)} + + + + Price Impact + {zapLoading ? ( + zapDetail.skeleton(40) + ) : !zapResult ? ( + '--' + ) : ( + + {zapDetail.priceImpact.isInvalid + ? '--' + : zapDetail.priceImpact.value < 0.01 + ? '<0.01%' + : zapDetail.priceImpact.value.toFixed(2) + '%'} + + )} + + +
+ )}
@@ -692,9 +1163,9 @@ export default function IncreaseLiquidity() { )} - - - + {zapPriceImpactNote} + + {method === 'pair' || !account ? : ZapButton}
diff --git a/src/pages/MyEarnings/ElasticPools/SinglePool/index.tsx b/src/pages/MyEarnings/ElasticPools/SinglePool/index.tsx index 4a47727948..79ef78e71c 100644 --- a/src/pages/MyEarnings/ElasticPools/SinglePool/index.tsx +++ b/src/pages/MyEarnings/ElasticPools/SinglePool/index.tsx @@ -13,6 +13,7 @@ import { ButtonLight } from 'components/Button' import CopyHelper from 'components/Copy' import Divider from 'components/Divider' import DoubleCurrencyLogo from 'components/DoubleLogo' +import QuickZap, { QuickZapButton } from 'components/ElasticZap/QuickZap' import { FarmTag } from 'components/FarmTag' import Loader from 'components/Loader' import { MouseoverTooltip, TextDashed } from 'components/Tooltip' @@ -57,6 +58,7 @@ export const StatItem = ({ label, value }: { label: ReactNode | string; value: R const SinglePool: React.FC = ({ poolEarning, chainId, positionEarnings, pendingFees, tokenPrices }) => { const theme = useTheme() + const [showQuickZap, setShowQuickZap] = useState(false) const { mixpanelHandler } = useMixpanel() const [isExpanded, setExpanded] = useState(false) const tabletView = useMedia(`(max-width: ${WIDTHS[3]}px)`) @@ -238,6 +240,13 @@ const SinglePool: React.FC = ({ poolEarning, chainId, positionEarnings, p borderRadius: mobileView ? 0 : '1rem', }} > + setShowQuickZap(false)} + expectedChainId={chainId} + /> + = ({ poolEarning, chainId, positionEarnings, p + Add Liquidity ) : ( - { - e.stopPropagation() - }} - to={ - currency0Slug && currency1Slug - ? `/${NETWORKS_INFO[chainId].route}${APP_PATHS.ELASTIC_CREATE_POOL}/${currency0Slug}/${currency1Slug}/${feeAmount}` - : '#' - } - > - + Add Liquidity - + + { + e.stopPropagation() + }} + to={ + currency0Slug && currency1Slug + ? `/${NETWORKS_INFO[chainId].route}${APP_PATHS.ELASTIC_CREATE_POOL}/${currency0Slug}/${currency1Slug}/${feeAmount}` + : '#' + } + > + + Add Liquidity + + + { + e.stopPropagation() + setShowQuickZap(true) + }} + /> + )} @@ -493,26 +511,43 @@ const SinglePool: React.FC = ({ poolEarning, chainId, positionEarnings, p ) : ( - e.stopPropagation()} - > - - + <> + { + e.stopPropagation() + setShowQuickZap(true) + }} + size="small" + /> + + e.stopPropagation()} + > + + + )} {isExpanded && isExpandable && positions} + + setShowQuickZap(false)} + expectedChainId={chainId} + /> ) } diff --git a/src/pages/MyEarnings/ElasticPools/SinglePosition/ActionButtons.tsx b/src/pages/MyEarnings/ElasticPools/SinglePosition/ActionButtons.tsx index a967c727b0..f09a8e0889 100644 --- a/src/pages/MyEarnings/ElasticPools/SinglePosition/ActionButtons.tsx +++ b/src/pages/MyEarnings/ElasticPools/SinglePosition/ActionButtons.tsx @@ -1,25 +1,28 @@ import { ChainId, Currency } from '@kyberswap/ks-sdk-core' import { FeeAmount } from '@kyberswap/ks-sdk-elastic' import { Trans } from '@lingui/macro' +import { useState } from 'react' import { ChevronsUp, Minus } from 'react-feather' import { Link, useNavigate } from 'react-router-dom' import styled from 'styled-components' +import QuickZap, { QuickZapButton } from 'components/ElasticZap/QuickZap' import { APP_PATHS } from 'constants/index' import { NETWORKS_INFO } from 'constants/networks' import { useActiveWeb3React } from 'hooks' +import useProAmmPoolInfo from 'hooks/useProAmmPoolInfo' import { useChangeNetwork } from 'hooks/web3/useChangeNetwork' import { ActionButton } from 'pages/MyEarnings/ActionButton' const ActionButtonsWrapper = styled.div` display: flex; align-items: center; - justify-content: space-between; - gap: 16px; + gap: 8px; ${ActionButton} { flex: 1; gap: 4px; + height: 36px; } ${({ theme }) => theme.mediaWidth.upToExtraSmall` @@ -52,6 +55,7 @@ const ActionButtons: React.FC = ({ isLegacy, onRemoveLiquidityFromLegacyPosition, }) => { + const [showQuickZap, setShowQuickZap] = useState(false) const { chainId: currentChainId } = useActiveWeb3React() const chainRoute = NETWORKS_INFO[chainId].route @@ -61,6 +65,7 @@ const ActionButtons: React.FC = ({ const { changeNetwork } = useChangeNetwork() const navigate = useNavigate() + const poolAddress = useProAmmPoolInfo(currency0.wrapped, currency1.wrapped, feeAmount) const target = `/${chainRoute}${APP_PATHS.ELASTIC_INCREASE_LIQ}/${currency0Slug}/${currency1Slug}/${feeAmount}/${nftId}` const onIncreaseClick = (e: any) => { @@ -87,14 +92,14 @@ const ActionButtons: React.FC = ({ if (!liquidity || isLegacy) { return ( - Remove Liquidity + Remove ) } return ( - Remove Liquidity + Remove ) } @@ -103,14 +108,14 @@ const ActionButtons: React.FC = ({ if (isLegacy) { return ( - Increase Liquidity + Increase ) } return ( - Increase Liquidity + Increase ) } @@ -119,6 +124,21 @@ const ActionButtons: React.FC = ({ {renderRemoveButton()} {renderIncreaseButton()} + {!isLegacy && ( + { + e.stopPropagation() + setShowQuickZap(true) + }} + /> + )} + setShowQuickZap(false)} + /> ) } diff --git a/src/pages/ProAmmPool/PositionGrid.tsx b/src/pages/ProAmmPool/PositionGrid.tsx index cfb0438b39..e7465caf92 100644 --- a/src/pages/ProAmmPool/PositionGrid.tsx +++ b/src/pages/ProAmmPool/PositionGrid.tsx @@ -1,27 +1,16 @@ import { gql, useQuery } from '@apollo/client' import { useScroll } from '@use-gesture/react' -import { Interface } from 'ethers/lib/utils' import memoizeOne from 'memoize-one' -import React, { - CSSProperties, - ComponentType, - forwardRef, - memo, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react' +import React, { CSSProperties, ComponentType, forwardRef, memo, useMemo, useRef } from 'react' import { useMedia } from 'react-use' import { FixedSizeGrid as FixedSizeGridRW, GridChildComponentProps, areEqual } from 'react-window' import styled from 'styled-components' -import TickReaderABI from 'constants/abis/v2/ProAmmTickReader.json' import { EVMNetworkInfo } from 'constants/networks/type' import { useActiveWeb3React } from 'hooks' -import { useMulticallContract } from 'hooks/useContract' +import { useProAmmTickReader } from 'hooks/useContract' import { useKyberSwapConfig } from 'state/application/hooks' +import { useSingleContractMultipleData } from 'state/multicall/hooks' import { MEDIA_WIDTHS } from 'theme' import { PositionDetails } from 'types/position' @@ -65,8 +54,6 @@ export const PositionCardGrid = styled.div` `}; ` -const tickReaderInterface = new Interface(TickReaderABI.abi) - const queryPositionLastCollectedTimes = gql` query positions($ids: [String]!) { positions(where: { id_in: $ids }) { @@ -80,17 +67,11 @@ const queryPositionLastCollectedTimes = gql` function PositionGrid({ positions, refe }: { positions: PositionDetails[]; refe?: React.MutableRefObject }) { const { isEVM, networkInfo, chainId } = useActiveWeb3React() - const multicallContract = useMulticallContract() const { elasticClient } = useKyberSwapConfig(chainId) const upToSmall = useMedia(`(max-width: ${MEDIA_WIDTHS.upToSmall}px)`) const upToLarge = useMedia(`(max-width: ${MEDIA_WIDTHS.upToLarge}px)`) - // raw - const [feeRewards, setFeeRewards] = useState<{ - [tokenId: string]: [string, string] - }>(() => positions.reduce((acc, item) => ({ ...acc, [item.tokenId.toString()]: ['0', '0'] }), {})) - const positionIds = useMemo(() => positions.map(pos => pos.tokenId.toString()), [positions]) const { data } = useQuery(queryPositionLastCollectedTimes, { client: elasticClient, @@ -137,45 +118,30 @@ function PositionGrid({ positions, refe }: { positions: PositionDetails[]; refe? [data], ) - const getPositionFee = useCallback(async () => { - if (!multicallContract) return - const fragment = tickReaderInterface.getFunction('getTotalFeesOwedToPosition') - const callParams = positions.map(item => { + const tickReaderContract = useProAmmTickReader() + const rewardRes = useSingleContractMultipleData( + tickReaderContract, + 'getTotalFeesOwedToPosition', + positions.map(item => [ + (networkInfo as EVMNetworkInfo).elastic.nonfungiblePositionManager, + item.poolId, + item.tokenId, + ]), + ) + + const feeRewards = useMemo(() => { + return positions.reduce<{ [tokenId: string]: [string, string] }>((acc, item, index) => { return { - target: (networkInfo as EVMNetworkInfo).elastic.tickReader, - callData: tickReaderInterface.encodeFunctionData(fragment, [ - (networkInfo as EVMNetworkInfo).elastic.nonfungiblePositionManager, - item.poolId, - item.tokenId, - ]), + ...acc, + [item.tokenId.toString()]: rewardRes[index].result + ? [ + rewardRes[index].result?.token0Owed?.toString() || '0', + rewardRes[index].result?.token1Owed.toString() || '0', + ] + : ['0', '0'], } - }) - - const { returnData } = await multicallContract?.callStatic.tryBlockAndAggregate(false, callParams) - setFeeRewards( - returnData.reduce( - ( - acc: { [tokenId: string]: [string, string] }, - item: { success: boolean; returnData: string }, - index: number, - ) => { - if (item.success) { - const tmp = tickReaderInterface.decodeFunctionResult(fragment, item.returnData) - return { - ...acc, - [positions[index].tokenId.toString()]: [tmp.token0Owed.toString(), tmp.token1Owed.toString()], - } - } - return { ...acc, [positions[index].tokenId.toString()]: ['0', '0'] } - }, - {} as { [tokenId: string]: [string, string] }, - ), - ) - }, [multicallContract, positions, networkInfo]) - - useEffect(() => { - getPositionFee() - }, [getPositionFee]) + }, {}) + }, [positions, rewardRes]) const itemData = createItemData(positions, liquidityTimes, farmingTimes, feeRewards, createdAts, refe) diff --git a/src/pages/ProAmmPool/PositionListItem.tsx b/src/pages/ProAmmPool/PositionListItem.tsx index f57f8b8ae5..10366542d4 100644 --- a/src/pages/ProAmmPool/PositionListItem.tsx +++ b/src/pages/ProAmmPool/PositionListItem.tsx @@ -11,6 +11,7 @@ import styled from 'styled-components' import { ButtonEmpty, ButtonOutlined, ButtonPrimary } from 'components/Button' import { LightCard } from 'components/Card' import Divider from 'components/Divider' +import QuickZap, { QuickZapButton } from 'components/ElasticZap/QuickZap' import ProAmmFee from 'components/ProAmm/ProAmmFee' import ProAmmPoolInfo from 'components/ProAmm/ProAmmPoolInfo' import ProAmmPooledTokens from 'components/ProAmm/ProAmmPooledTokens' @@ -303,10 +304,18 @@ function PositionListItem({ return '' })() + const [showQuickZap, setShowQuickZap] = useState(false) + if (!position || !priceLower || !priceUpper) return return ( + setShowQuickZap(false)} + /> <> Increase Liquidity + + setShowQuickZap(true)} /> )} diff --git a/src/pages/ProAmmPools/CardItem.tsx b/src/pages/ProAmmPools/CardItem.tsx index 19b61aaddd..7c782d5ab9 100644 --- a/src/pages/ProAmmPools/CardItem.tsx +++ b/src/pages/ProAmmPools/CardItem.tsx @@ -1,6 +1,6 @@ import { ChainId, Token, WETH } from '@kyberswap/ks-sdk-core' import { Trans } from '@lingui/macro' -import { useMemo } from 'react' +import { useMemo, useState } from 'react' import { BarChart2, MoreHorizontal, Plus, Share2 } from 'react-feather' import { Link, useNavigate } from 'react-router-dom' import { Flex, Text } from 'rebass' @@ -12,6 +12,7 @@ import { ButtonLight, ButtonOutlined } from 'components/Button' import CopyHelper from 'components/Copy' import Divider from 'components/Divider' import DoubleCurrencyLogo from 'components/DoubleLogo' +import QuickZap, { QuickZapButton } from 'components/ElasticZap/QuickZap' import { FarmTag } from 'components/FarmTag' import { MouseoverTooltip } from 'components/Tooltip' import { FeeTag } from 'components/YieldPools/ElasticFarmGroup/styleds' @@ -61,6 +62,8 @@ export default function ProAmmPoolCardItem({ pool, onShared, userPositions }: Li const theme = useTheme() const navigate = useNavigate() + const [showQuickZap, setShowQuickZap] = useState(false) + const { farms: farmsV2 } = useElasticFarmsV2() const allTokens = useAllTokens() @@ -93,6 +96,7 @@ export default function ProAmmPoolCardItem({ pool, onShared, userPositions }: Li !item.isSettled && item.poolAddress.toLowerCase() === pool.address.toLowerCase(), ) + const isFarmV2 = !!farmV2 const maxFarmV2Apr = Math.max(...(farmV2?.ranges.map(item => item.apr || 0) || [])) @@ -120,6 +124,8 @@ export default function ProAmmPoolCardItem({ pool, onShared, userPositions }: Li return ( + setShowQuickZap(false)} /> + Add Liquidity + + setShowQuickZap(true)} /> ) diff --git a/src/pages/ProAmmPools/ListItem.tsx b/src/pages/ProAmmPools/ListItem.tsx index cd65570d58..57f44cb10b 100644 --- a/src/pages/ProAmmPools/ListItem.tsx +++ b/src/pages/ProAmmPools/ListItem.tsx @@ -1,6 +1,7 @@ import { ChainId, Token, WETH } from '@kyberswap/ks-sdk-core' import { Trans, t } from '@lingui/macro' import { rgba } from 'polished' +import { useState } from 'react' import { BarChart2, Plus, Share2 } from 'react-feather' import { Link, useNavigate } from 'react-router-dom' import { Flex, Text } from 'rebass' @@ -10,6 +11,7 @@ import { ReactComponent as ViewPositionIcon } from 'assets/svg/view_positions.sv import { ButtonEmpty } from 'components/Button' import CopyHelper from 'components/Copy' import DoubleCurrencyLogo from 'components/DoubleLogo' +import QuickZap, { QuickZapButton } from 'components/ElasticZap/QuickZap' import { FarmTag } from 'components/FarmTag' import { MouseoverTooltip } from 'components/Tooltip' import { FeeTag } from 'components/YieldPools/ElasticFarmGroup/styleds' @@ -79,6 +81,7 @@ export default function ProAmmPoolListItem({ pool, onShared, userPositions }: Li const { chainId, networkInfo } = useActiveWeb3React() const theme = useTheme() const navigate = useNavigate() + const [showQuickZap, setShowQuickZap] = useState(false) const allTokens = useAllTokens() @@ -150,6 +153,7 @@ export default function ProAmmPoolListItem({ pool, onShared, userPositions }: Li return ( + setShowQuickZap(false)} />
{myLiquidity ? formatDollarAmount(Number(myLiquidity)) : '-'} + setShowQuickZap(true)} size="small" /> Add liquidity } placement={'top'} width={'fit-content'}> ({ query: ({ url, payload, signal, authentication }) => ({ url, @@ -47,3 +47,5 @@ const routeApi = createApi({ }) export default routeApi + +export const { useLazyGetRouteQuery, useBuildRouteMutation } = routeApi diff --git a/src/services/route/types/getRoute.ts b/src/services/route/types/getRoute.ts index 9a4445852e..46b054b0d4 100644 --- a/src/services/route/types/getRoute.ts +++ b/src/services/route/types/getRoute.ts @@ -5,14 +5,15 @@ export type GetRouteParams = { tokenOut: string amountIn: string saveGas: string - includedSources: string + includedSources?: string excludedSources?: string - gasInclude: string - gasPrice: string - feeAmount: string - chargeFeeBy: ChargeFeeBy - isInBps: string - feeReceiver: string + excludedPools?: string + gasInclude?: string + gasPrice?: string + feeAmount?: string + chargeFeeBy?: ChargeFeeBy + isInBps?: string + feeReceiver?: string debug?: string } diff --git a/src/state/mint/proamm/utils.ts b/src/state/mint/proamm/utils.ts index 2472844f84..403ef0c8b4 100644 --- a/src/state/mint/proamm/utils.ts +++ b/src/state/mint/proamm/utils.ts @@ -83,13 +83,19 @@ export const getRecommendedRangeTicks = ( tokenB: Token, currentTick: number, pairFactor: PairFactor, -) => { +): [number, number] => { const rangeFactor = rangeData[range].factor const leftRange = 1 - (pairFactor * rangeFactor) / 10000 const rightRange = 1 + (pairFactor * rangeFactor) / 10000 - const result1 = [currentTick + Math.floor(log10001(leftRange)), currentTick + Math.ceil(log10001(rightRange))] - const result2 = [currentTick + Math.floor(log10001(1 / leftRange)), currentTick + Math.ceil(log10001(1 / rightRange))] + const result1: [number, number] = [ + currentTick + Math.floor(log10001(leftRange)), + currentTick + Math.ceil(log10001(rightRange)), + ] + const result2: [number, number] = [ + currentTick + Math.floor(log10001(1 / leftRange)), + currentTick + Math.ceil(log10001(1 / rightRange)), + ] const result = tokenA.sortsBefore(tokenB) ? result1 : result2 return result diff --git a/src/state/prommPools/useGetElasticPools/useGetElasticPoolsV1.ts b/src/state/prommPools/useGetElasticPools/useGetElasticPoolsV1.ts index a51893f346..71c7043993 100644 --- a/src/state/prommPools/useGetElasticPools/useGetElasticPoolsV1.ts +++ b/src/state/prommPools/useGetElasticPools/useGetElasticPoolsV1.ts @@ -1,6 +1,6 @@ import { useQuery } from '@apollo/client' import dayjs from 'dayjs' -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { PROMM_POOLS_BULK, ProMMPoolFields } from 'apollo/queries/promm' import { ELASTIC_BASE_FEE_UNIT } from 'constants/index' @@ -145,13 +145,19 @@ const useGetElasticPoolsV1 = (poolAddresses: string[]): CommonReturn => { skip: isEnableKNProtocol, }) + const data24Ref = useRef(data24) + + if (data24) data24Ref.current = data24 + const anyError = error24?.message.includes('Failed to decode `block.number`') ? Boolean(error) : Boolean(error || error24) const anyLoading = Boolean(loading || loading24) + const formatted = parsedPoolData(poolAddresses, data, data24Ref.current) + // return early if not all data yet - if (anyError || anyLoading) { + if ((anyError || anyLoading) && !formatted) { return { isLoading: anyLoading, isError: anyError, @@ -159,7 +165,6 @@ const useGetElasticPoolsV1 = (poolAddresses: string[]): CommonReturn => { } } - const formatted = parsedPoolData(poolAddresses, data, data24) return { isLoading: anyLoading, isError: anyError, diff --git a/src/state/transactions/type.ts b/src/state/transactions/type.ts index be0dc920f6..6c7be021e1 100644 --- a/src/state/transactions/type.ts +++ b/src/state/transactions/type.ts @@ -29,6 +29,8 @@ export type TransactionExtraInfo2Token = { chainIdIn?: ChainId chainIdOut?: ChainId nftId?: string + zapAmountIn?: string + zapSymbolIn?: string } export type TransactionExtraInfoHarvestFarm = { @@ -132,6 +134,7 @@ export enum TRANSACTION_TYPE { CLASSIC_REMOVE_LIQUIDITY = 'Classic Remove Liquidity', ELASTIC_REMOVE_LIQUIDITY = 'Elastic Remove Liquidity', ELASTIC_INCREASE_LIQUIDITY = 'Elastic Increase Liquidity', + ELASTIC_ZAP_IN_LIQUIDITY = 'Elastic Zap-in Liquidity', ELASTIC_COLLECT_FEE = 'Elastic Collect Fee', STAKE = 'Stake Into Farm', @@ -173,6 +176,7 @@ export const GROUP_TRANSACTION_BY_TYPE = { TRANSACTION_TYPE.CLASSIC_REMOVE_LIQUIDITY, TRANSACTION_TYPE.ELASTIC_REMOVE_LIQUIDITY, TRANSACTION_TYPE.ELASTIC_INCREASE_LIQUIDITY, + TRANSACTION_TYPE.ELASTIC_ZAP_IN_LIQUIDITY, TRANSACTION_TYPE.ELASTIC_DEPOSIT_LIQUIDITY, TRANSACTION_TYPE.ELASTIC_WITHDRAW_LIQUIDITY, TRANSACTION_TYPE.STAKE, diff --git a/src/state/user/actions.ts b/src/state/user/actions.ts index 9e3bdb54f5..dec2df2baa 100644 --- a/src/state/user/actions.ts +++ b/src/state/user/actions.ts @@ -22,6 +22,7 @@ export interface SerializedPair { export const updateUserDegenMode = createAction<{ userDegenMode: boolean; isStablePairSwap: boolean }>( 'user/updateUserDegenMode', ) +export const toggleUseAggregatorForZap = createAction('user/toggleUseAggregatorForZap') export const updateUserLocale = createAction<{ userLocale: SupportedLocale }>('user/updateUserLocale') export const updateUserSlippageTolerance = createAction<{ userSlippageTolerance: number }>( 'user/updateUserSlippageTolerance', diff --git a/src/state/user/hooks.tsx b/src/state/user/hooks.tsx index f231cdaa48..73f1d70937 100644 --- a/src/state/user/hooks.tsx +++ b/src/state/user/hooks.tsx @@ -41,6 +41,7 @@ import { toggleMyEarningChart, toggleTopTrendingTokens, toggleTradeRoutes, + toggleUseAggregatorForZap, updateAcceptedTermVersion, updateTokenAnalysisSettings, updateUserDeadline, @@ -131,6 +132,19 @@ export function useDegenModeManager(): [boolean, () => void] { return [degenMode, toggleSetDegenMode] } +export function useAggregatorForZapSetting(): [boolean, () => void] { + const dispatch = useDispatch() + const isUseAggregatorForZap = useSelector( + state => state.user.useAggregatorForZap, + ) + + const toggle = useCallback(() => { + dispatch(toggleUseAggregatorForZap()) + }, [dispatch]) + + return [isUseAggregatorForZap === undefined ? true : isUseAggregatorForZap, toggle] +} + export function useUserSlippageTolerance(): [number, (slippage: number) => void] { const dispatch = useDispatch() const userSlippageTolerance = useSelector(state => { diff --git a/src/state/user/reducer.ts b/src/state/user/reducer.ts index c24e88dda0..75e1d0ab09 100644 --- a/src/state/user/reducer.ts +++ b/src/state/user/reducer.ts @@ -32,6 +32,7 @@ import { toggleLiveChart, toggleMyEarningChart, toggleTradeRoutes, + toggleUseAggregatorForZap, updateAcceptedTermVersion, updateChainId, updateTokenAnalysisSettings, @@ -63,6 +64,7 @@ export interface UserState { userDegenMode: boolean userDegenModeAutoDisableTimestamp: number + useAggregatorForZap: boolean // user defined slippage tolerance in bips, used in all txns userSlippageTolerance: number @@ -148,6 +150,7 @@ export const CROSS_CHAIN_SETTING_DEFAULT = { const initialState: UserState = { userDegenMode: false, + useAggregatorForZap: true, userDegenModeAutoDisableTimestamp: 0, userLocale: null, userSlippageTolerance: INITIAL_ALLOWED_SLIPPAGE, @@ -356,5 +359,12 @@ export default createReducer(initialState, builder => }) .addCase(toggleMyEarningChart, state => { state.myEarningChart = !state.myEarningChart + }) + .addCase(toggleUseAggregatorForZap, state => { + if (state.useAggregatorForZap === undefined) { + state.useAggregatorForZap = false + } else { + state.useAggregatorForZap = !state.useAggregatorForZap + } }), ) diff --git a/src/utils/errorMessage.ts b/src/utils/errorMessage.ts index 30f41b3083..9fe781cd8e 100644 --- a/src/utils/errorMessage.ts +++ b/src/utils/errorMessage.ts @@ -21,6 +21,7 @@ function parseKnownPattern(text: string): string | undefined { 'return amount is not enough', 'code=call_exception', 'none of the calls threw an error', + 'failed to swap with aggr', ], error, )