From 0e386482a56cbf803173f347ba04485fd36a6ec4 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 17 Jan 2024 23:28:56 +0100 Subject: [PATCH] feat: standalone "Add Liquidity" (#6028) * fix: default * fix: selection * feat: set asymSide * fix: don't hide deposit type options when standalone * fix: hide radio options on pool page * fix: types * feat: cleanup * feat: more cleanup * feat: more more cleanup * fix: page refresh * fix: selectPair should be rendered conditionally as well * fix: trade asset inputs on pool page * fix: use buyAssetSerach --------- Co-authored-by: Apotheosis <0xapotheosis@gmail.com> Co-authored-by: Apotheosis <97164662+0xApotheosis@users.noreply.github.com> --- .../AddLiquitity/AddLiquidityInput.tsx | 149 ++++++++++++++---- .../AddLiquitity/components/DepositType.tsx | 19 +-- 2 files changed, 132 insertions(+), 36 deletions(-) diff --git a/src/pages/ThorChainLP/components/AddLiquitity/AddLiquidityInput.tsx b/src/pages/ThorChainLP/components/AddLiquitity/AddLiquidityInput.tsx index 55b806ce386..8a2c2910284 100644 --- a/src/pages/ThorChainLP/components/AddLiquitity/AddLiquidityInput.tsx +++ b/src/pages/ThorChainLP/components/AddLiquitity/AddLiquidityInput.tsx @@ -19,7 +19,7 @@ import { import { thorchainAssetId } from '@shapeshiftoss/caip' import type { Asset, MarketData } from '@shapeshiftoss/types' import prettyMilliseconds from 'pretty-ms' -import React, { useCallback, useEffect, useMemo } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import { BiSolidBoltCircle } from 'react-icons/bi' import { FaPlus } from 'react-icons/fa6' import { useTranslate } from 'react-polyglot' @@ -32,12 +32,14 @@ import { Row } from 'components/Row/Row' import { SlideTransition } from 'components/SlideTransition' import { RawText } from 'components/Text' import { useBrowserRouter } from 'hooks/useBrowserRouter/useBrowserRouter' +import { useModal } from 'hooks/useModal/useModal' import { bn, bnOrZero, convertPrecision } from 'lib/bignumber/bignumber' +import { isSome } from 'lib/utils' import { THOR_PRECISION } from 'lib/utils/thorchain/constants' import { estimateAddThorchainLiquidityPosition } from 'lib/utils/thorchain/lp' import { usePools } from 'pages/ThorChainLP/hooks/usePools' import { AsymSide } from 'pages/ThorChainLP/hooks/useUserLpData' -import { selectAssetById, selectMarketDataById } from 'state/slices/selectors' +import { selectAssetById, selectAssets, selectMarketDataById } from 'state/slices/selectors' import { useAppSelector } from 'state/store' import type { AddLiquidityProps } from './AddLiquidity' @@ -72,12 +74,70 @@ export const AddLiquidityInput: React.FC = ({ const { data: parsedPools } = usePools() - const foundPool = useMemo(() => { + const assets = useAppSelector(selectAssets) + const poolAssets = useMemo(() => { + if (!parsedPools) return [] + + return [...new Set(parsedPools.map(pool => assets[pool.assetId]).filter(isSome))] + }, [assets, parsedPools]) + + // TODO(gomes): Even though that's an edge case for users, and a bad practice, handling sym and asymm positions simultaneously + // *is* possible and *is* something that both we and TS do. We can do one better than TS here however: + // - When a user deposits symetrically, they can then deposit asymetrically, but only on the asset side + // - When a user deposits asymetrically, no matter the side, they *can* deposit symetrically on the other side + // - They can also deposit asymetrically after that, but with one caveat: they can do so only if they deposited asym on the *asset* side only + // In other words, if they have an active asym. RUNE position, they can't deposit symetrically after that unless they withdraw + // The reason for that is that the RUNE side memo performs a nameservice operation, registering the asset address (or a placeholder) + // + // We should handle this in the UI and block users from deposits that *will* fail, by detecting their current position(s) + // and not allowing them to select the sure-to-fail deposit types + const defaultOpportunityId = useMemo(() => { if (!parsedPools) return undefined + if (opportunityId) return undefined + + const firstAsymOpportunityId = parsedPools.find(pool => pool.asymSide === null)?.opportunityId - return parsedPools.find(pool => pool.opportunityId === opportunityId) + return firstAsymOpportunityId }, [opportunityId, parsedPools]) + const [activeOpportunityId, setActiveOpportunityId] = useState( + opportunityId ?? defaultOpportunityId, + ) + + useEffect(() => { + if (!(opportunityId || defaultOpportunityId)) return + + setActiveOpportunityId(opportunityId ?? defaultOpportunityId) + }, [defaultOpportunityId, opportunityId]) + + const foundPool = useMemo(() => { + if (!parsedPools) return undefined + + return parsedPools.find(pool => pool.opportunityId === activeOpportunityId) + }, [activeOpportunityId, parsedPools]) + + const _asset = useAppSelector(state => selectAssetById(state, foundPool?.assetId ?? '')) + useEffect(() => { + if (!_asset) return + setAsset(_asset) + }, [_asset]) + + const rune = useAppSelector(state => selectAssetById(state, thorchainAssetId)) + + const [asset, setAsset] = useState(_asset) + + useEffect(() => { + if (!(asset && parsedPools)) return + // We only want to run this effect in the standalone AddLiquidity page + if (!defaultOpportunityId) return + + const foundOpportunityId = (parsedPools ?? []).find( + pool => pool.assetId === asset.assetId && pool.asymSide === null, + )?.opportunityId + if (!foundOpportunityId) return + setActiveOpportunityId(foundOpportunityId) + }, [asset, defaultOpportunityId, parsedPools]) + const handleAssetChange = useCallback((asset: Asset) => { console.info(asset) }, []) @@ -134,9 +194,7 @@ export const AddLiquidityInput: React.FC = ({ ) }, [backIcon, handleBackClick, headerComponent, translate]) - const asset = useAppSelector(state => selectAssetById(state, foundPool?.assetId ?? '')) const assetMarketData = useAppSelector(state => selectMarketDataById(state, asset?.assetId ?? '')) - const rune = useAppSelector(state => selectAssetById(state, thorchainAssetId)) const runeMarketData = useAppSelector(state => selectMarketDataById(state, rune?.assetId ?? '')) const [assetCryptoLiquidityAmount, setAssetCryptoLiquidityAmount] = React.useState< @@ -284,43 +342,80 @@ export const AddLiquidityInput: React.FC = ({ ) }, [asset, foundPool, rune, translate]) + const buyAssetSearch = useModal('buyAssetSearch') + const handlePoolAssetClick = useCallback(() => { + buyAssetSearch.open({ + onClick: setAsset, + title: 'pools.pool', + assets: poolAssets, + }) + }, [buyAssetSearch, poolAssets]) + + const pairSelect = useMemo(() => { + // We only want to show the pair select on standalone "Add Liquidity" - not on the pool page + if (!defaultOpportunityId) return null + return ( + <> + + {translate('pools.selectPair')} + + + + + ) + }, [asset?.assetId, defaultOpportunityId, handleAssetChange, handlePoolAssetClick, translate]) + + const handleAsymSideChange = useCallback( + (asymSide: string | null) => { + if (!(parsedPools && asset)) return + + // The null option gets casted as an empty string by the radio component so we cast it back to null + const parsedAsymSide = (asymSide as AsymSide | '') || null + const assetPools = parsedPools.filter(pool => pool.assetId === asset.assetId) + const foundPool = assetPools.find(pool => pool.asymSide === parsedAsymSide) + if (!foundPool) return + + setActiveOpportunityId(foundPool.opportunityId) + }, + [asset, parsedPools], + ) + if (!foundPool || !asset || !rune) return null return ( {renderHeader} - - - {translate('pools.selectPair')} - - - - + {pairSelect} {translate('pools.depositAmounts')} - + {tradeAssetInputs} diff --git a/src/pages/ThorChainLP/components/AddLiquitity/components/DepositType.tsx b/src/pages/ThorChainLP/components/AddLiquitity/components/DepositType.tsx index 3f681a60283..d3b71acc407 100644 --- a/src/pages/ThorChainLP/components/AddLiquitity/components/DepositType.tsx +++ b/src/pages/ThorChainLP/components/AddLiquitity/components/DepositType.tsx @@ -75,12 +75,14 @@ const options = [ type DepositTypeProps = { assetId: AssetId - // If undefined/not passed, we're not locking the user in any kind of symmetrical/asymmetrical deposit type, i.e they can choose any of the three - // If null, user can only deposit symmetrical - // If AsymSide, user can only deposit asymmetrical on said Asymside - asymSide?: AsymSide | null + onAsymSideChange: (asymSide: string | null) => void + defaultOpportunityId?: string } -export const DepositType = ({ assetId, asymSide }: DepositTypeProps) => { +export const DepositType = ({ + assetId, + defaultOpportunityId, + onAsymSideChange, +}: DepositTypeProps) => { const assetIds = useMemo(() => { return [assetId, thorchainAssetId] }, [assetId]) @@ -99,12 +101,11 @@ export const DepositType = ({ assetId, asymSide }: DepositTypeProps) => { const { getRootProps, getRadioProps } = useRadioGroup({ name: 'depositType', defaultValue: 'one', - onChange: console.log, + onChange: onAsymSideChange, }) const radioOptions = useMemo(() => { - const _options = asymSide ? [{ value: asymSide }] : options - if (_options.length === 1) return null + const _options = defaultOpportunityId ? options : [] return _options.map((option, index) => { const radio = getRadioProps({ value: option.value }) @@ -124,7 +125,7 @@ export const DepositType = ({ assetId, asymSide }: DepositTypeProps) => { ) }) - }, [asymSide, getRadioProps, makeAssetIdsOption]) + }, [defaultOpportunityId, getRadioProps, makeAssetIdsOption]) const group = getRootProps() return (