From 5b18c2998f81c08887a463ddce7cfd57fe9d8116 Mon Sep 17 00:00:00 2001 From: David Uhlmann Date: Wed, 13 Dec 2023 11:24:10 +1000 Subject: [PATCH] #496 --- package-lock.json | 14 +- package.json | 2 +- src/components/cards/LimitOrderCard.tsx | 2 +- src/components/cards/LimitOrderContext.tsx | 2 +- src/components/cards/PoolsTableCard.tsx | 73 +-- src/lib/web3/hooks/useChains.ts | 2 +- src/lib/web3/hooks/useIndexer.ts | 2 +- src/lib/web3/hooks/useTickLiquidity.ts | 28 +- src/lib/web3/hooks/useUserDeposits.ts | 131 +++++ src/lib/web3/hooks/useUserReserves.ts | 542 ++++++++++++++++++ src/lib/web3/hooks/useUserShareValues.ts | 112 ---- src/lib/web3/hooks/useUserShares.ts | 398 ------------- src/lib/web3/indexerProvider.tsx | 108 +--- src/lib/web3/lcdClient.ts | 8 +- src/lib/web3/rpcMsgClient.ts | 6 +- src/lib/web3/utils/events.ts | 28 +- src/lib/web3/utils/pairs.ts | 6 +- src/lib/web3/utils/shares.ts | 13 +- src/lib/web3/utils/tokens.ts | 15 +- src/pages/MyLiquidity/MyLiquidity.tsx | 4 +- src/pages/MyLiquidity/useEditLiquidity.ts | 93 ++- src/pages/Pool/MyPositionTableCard.tsx | 90 ++- src/pages/Pool/PoolManagement.tsx | 100 ++-- src/pages/Pool/PoolOverview.tsx | 29 +- src/pages/Pool/Pools.tsx | 8 +- .../Pool/hooks/useTransactionTableData.ts | 14 +- src/pages/Pool/useDeposit.ts | 16 +- src/pages/Swap/Swap.tsx | 6 +- src/pages/Swap/hooks/useSwap.tsx | 18 +- 29 files changed, 983 insertions(+), 887 deletions(-) create mode 100644 src/lib/web3/hooks/useUserDeposits.ts create mode 100644 src/lib/web3/hooks/useUserReserves.ts delete mode 100644 src/lib/web3/hooks/useUserShareValues.ts delete mode 100644 src/lib/web3/hooks/useUserShares.ts diff --git a/package-lock.json b/package-lock.json index 73962d74f..b003734af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@cosmjs/crypto": "0.31.1", "@cosmjs/proto-signing": "0.31.1", "@cosmjs/stargate": "0.31.1", - "@duality-labs/dualityjs": "0.3.3-json-types", + "@duality-labs/dualityjs": "0.4.0", "@floating-ui/react": "^0.24.5", "@fortawesome/fontawesome-svg-core": "^6.1.2", "@fortawesome/free-solid-svg-icons": "^6.1.2", @@ -2487,9 +2487,9 @@ } }, "node_modules/@duality-labs/dualityjs": { - "version": "0.3.3-json-types", - "resolved": "https://registry.npmjs.org/@duality-labs/dualityjs/-/dualityjs-0.3.3-json-types.tgz", - "integrity": "sha512-y60Fzn6dbLkF3LkiVYeZ48zFNVUTwuzu+mtiVeBCW5Fe1YN4dXkShNjocedjXXCJ1TNc02gzCVw1CEt/lip4wg==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@duality-labs/dualityjs/-/dualityjs-0.4.0.tgz", + "integrity": "sha512-5sqS2tX4rGVn7Bo0OUQw4TDEmPLAILzbsMSNDcuDcRXxcvHr9+Q+JI0gQdnc/lXiGCkv1yxPjJvyLXChDwF3Vw==", "dependencies": { "@babel/runtime": "^7.22.15", "@cosmjs/amino": "0.31.1", @@ -28076,9 +28076,9 @@ } }, "@duality-labs/dualityjs": { - "version": "0.3.3-json-types", - "resolved": "https://registry.npmjs.org/@duality-labs/dualityjs/-/dualityjs-0.3.3-json-types.tgz", - "integrity": "sha512-y60Fzn6dbLkF3LkiVYeZ48zFNVUTwuzu+mtiVeBCW5Fe1YN4dXkShNjocedjXXCJ1TNc02gzCVw1CEt/lip4wg==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@duality-labs/dualityjs/-/dualityjs-0.4.0.tgz", + "integrity": "sha512-5sqS2tX4rGVn7Bo0OUQw4TDEmPLAILzbsMSNDcuDcRXxcvHr9+Q+JI0gQdnc/lXiGCkv1yxPjJvyLXChDwF3Vw==", "requires": { "@babel/runtime": "^7.22.15", "@cosmjs/amino": "0.31.1", diff --git a/package.json b/package.json index a81b7818f..35c676727 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@cosmjs/crypto": "0.31.1", "@cosmjs/proto-signing": "0.31.1", "@cosmjs/stargate": "0.31.1", - "@duality-labs/dualityjs": "0.3.3-json-types", + "@duality-labs/dualityjs": "0.4.0", "@floating-ui/react": "^0.24.5", "@fortawesome/fontawesome-svg-core": "^6.1.2", "@fortawesome/free-solid-svg-icons": "^6.1.2", diff --git a/src/components/cards/LimitOrderCard.tsx b/src/components/cards/LimitOrderCard.tsx index bef3256a4..8fc923fca 100644 --- a/src/components/cards/LimitOrderCard.tsx +++ b/src/components/cards/LimitOrderCard.tsx @@ -285,7 +285,7 @@ function LimitOrder({ orderType: orderTypeEnum[execution], // todo: set tickIndex to allow for a tolerance: // the below function is a tolerance of 0 - tickIndex: Long.fromNumber( + tickIndexInToOut: Long.fromNumber( showLimitPrice ? // set given limit price displayPriceToTickIndex( diff --git a/src/components/cards/LimitOrderContext.tsx b/src/components/cards/LimitOrderContext.tsx index 62f5ba0d2..ee13e6c35 100644 --- a/src/components/cards/LimitOrderContext.tsx +++ b/src/components/cards/LimitOrderContext.tsx @@ -6,7 +6,7 @@ import { useMemo, useState, } from 'react'; -import { LimitOrderType } from '@duality-labs/dualityjs/types/codegen/dualitylabs/duality/dex/tx'; +import { LimitOrderType } from '@duality-labs/dualityjs/types/codegen/duality/dex/tx'; export type LimitOrderTypeKeys = keyof Omit< typeof LimitOrderType, diff --git a/src/components/cards/PoolsTableCard.tsx b/src/components/cards/PoolsTableCard.tsx index 19caa25c3..0116dee8e 100644 --- a/src/components/cards/PoolsTableCard.tsx +++ b/src/components/cards/PoolsTableCard.tsx @@ -17,12 +17,10 @@ import { formatAmount, formatCurrency } from '../../lib/utils/number'; import { Token, getDisplayDenomAmount } from '../../lib/web3/utils/tokens'; import useTokenPairs from '../../lib/web3/hooks/useTokenPairs'; import { getPairID } from '../../lib/web3/utils/pairs'; - -import { UserPositionDepositContext } from '../../lib/web3/hooks/useUserShares'; import { - ValuedUserPositionDepositContext, - useUserPositionsShareValues, -} from '../../lib/web3/hooks/useUserShareValues'; + UserValuedReserves, + useEstimatedUserReserves, +} from '../../lib/web3/hooks/useUserReserves'; import './PoolsTableCard.scss'; @@ -192,7 +190,7 @@ export function MyPoolsTableCard({ const tokenList = useTokensWithIbcInfo(useTokens()); - const userPositionsShareValues = useUserPositionsShareValues(); + const { data: userValuedReserves } = useEstimatedUserReserves(); const myPoolsList = useMemo< Array< @@ -200,16 +198,16 @@ export function MyPoolsTableCard({ pairID: string, token0: Token, token1: Token, - userPositions: UserPositionDepositContext[] + userValuedReserves: UserValuedReserves[] ] > >(() => { // collect positions into token pair groups - const userPositionsShareValueMap = userPositionsShareValues.reduce<{ + const userValuedReservesMap = (userValuedReserves || []).reduce<{ [pairID: string]: { token0: Token; token1: Token; - userPositions: UserPositionDepositContext[]; + userValuedReserves: UserValuedReserves[]; }; }>((map, userPosition) => { const { token0: token0Address, token1: token1Address } = @@ -218,25 +216,29 @@ export function MyPoolsTableCard({ const token0 = tokenList.find(matchTokenByDenom(token0Address)); const token1 = tokenList.find(matchTokenByDenom(token1Address)); if (pairID && token0 && token1) { - map[pairID] = map[pairID] || { token0, token1, userPositions: [] }; - map[pairID].userPositions.push(userPosition); + map[pairID] = map[pairID] || { + token0, + token1, + userValuedReserves: [], + }; + map[pairID].userValuedReserves.push(userPosition); } return map; }, {}); - return userPositionsShareValueMap - ? Object.entries(userPositionsShareValueMap).map< - [string, Token, Token, UserPositionDepositContext[]] - >(([pairId, { token0, token1, userPositions }]) => { - return [pairId, token0, token1, userPositions]; + return userValuedReservesMap + ? Object.entries(userValuedReservesMap).map< + [string, Token, Token, UserValuedReserves[]] + >(([pairId, { token0, token1, userValuedReserves }]) => { + return [pairId, token0, token1, userValuedReserves]; }) : []; - }, [userPositionsShareValues, tokenList]); + }, [userValuedReserves, tokenList]); const filteredPoolTokenList = useFilteredTokenList(tokenList, searchValue); const filteredPoolsList = useMemo< - Array<[string, Token, Token, UserPositionDepositContext[]]> + Array<[string, Token, Token, UserValuedReserves[]]> >(() => { const tokenList = filteredPoolTokenList.map(({ token }) => token); return myPoolsList.filter(([, token0, token1]) => { @@ -266,7 +268,7 @@ export function MyPoolsTableCard({ {filteredPoolsList.map( - ([pairId, token0, token1, userPositions]) => { + ([pairId, token0, token1, userValuedReserves]) => { const onRowClick: | MouseEventHandler | undefined = onTokenPairClick @@ -278,7 +280,7 @@ export function MyPoolsTableCard({ key={pairId} token0={token0} token1={token1} - userPositions={userPositions} + userValuedReserves={userValuedReserves} onClick={onRowClick} actions={userPositionActions} /> @@ -314,7 +316,7 @@ export interface Actions { action: (action: { token0: Token; token1: Token; - userPositions: Array; + userValuedReserves: Array; navigate: NavigateFunction; }) => void; }; @@ -335,30 +337,26 @@ const defaultActions: Actions = { function PositionRow({ token0, token1, - userPositions, + userValuedReserves, onClick, actions = {}, }: { token0: Token; token1: Token; - userPositions: Array; + userValuedReserves: Array; onClick?: MouseEventHandler; actions?: Actions; }) { const navigate = useNavigate(); - const total0 = userPositions.reduce((acc, { token0Context }) => { - return acc.plus(token0Context?.userReserves || 0); - }, new BigNumber(0)); - const total1 = userPositions.reduce((acc, { token1Context }) => { - return acc.plus(token1Context?.userReserves || 0); + const total0 = userValuedReserves.reduce((acc, estimate) => { + return acc.plus(estimate.reserves.reserves0 || 0); }, new BigNumber(0)); - - const value0 = userPositions.reduce((acc, { token0Value }) => { - return acc.plus(token0Value || 0); + const total1 = userValuedReserves.reduce((acc, estimate) => { + return acc.plus(estimate.reserves.reserves1 || 0); }, new BigNumber(0)); - const value1 = userPositions.reduce((acc, { token1Value }) => { - return acc.plus(token1Value || 0); + const value = userValuedReserves.reduce((acc, { value }) => { + return acc.plus(value || 0); }, new BigNumber(0)); if (total0 && total1) { @@ -367,7 +365,7 @@ function PositionRow({ - {formatCurrency(value0.plus(value1).toNumber())} + {formatCurrency(value.toNumber())} {formatAmount(getDisplayDenomAmount(token0, total0) || 0)} @@ -387,7 +385,12 @@ function PositionRow({ key={actionKey} type="button" onClick={() => { - action({ token0, token1, userPositions, navigate }); + action({ + token0, + token1, + userValuedReserves, + navigate, + }); }} className={['button nowrap', className] .filter(Boolean) diff --git a/src/lib/web3/hooks/useChains.ts b/src/lib/web3/hooks/useChains.ts index 009d3e0a5..1928e273f 100644 --- a/src/lib/web3/hooks/useChains.ts +++ b/src/lib/web3/hooks/useChains.ts @@ -39,7 +39,7 @@ export const dualityChain: Chain = { pretty_name: REACT_APP__CHAIN_NAME, status: 'upcoming', network_type: 'testnet', - bech32_prefix: 'cosmos', + bech32_prefix: 'dual', slip44: 118, logo_URIs: { svg: dualityLogo, diff --git a/src/lib/web3/hooks/useIndexer.ts b/src/lib/web3/hooks/useIndexer.ts index bbf77590e..9def702a6 100644 --- a/src/lib/web3/hooks/useIndexer.ts +++ b/src/lib/web3/hooks/useIndexer.ts @@ -489,7 +489,7 @@ export function useIndexerStreamOfDualDataSet< url, IndexerStreamAccumulateDualDataSet, opts - ); + ) as StaleWhileRevalidateState<[DataSet, DataSet]>; } // add higher-level function to fetch multiple pages of data as "one request" diff --git a/src/lib/web3/hooks/useTickLiquidity.ts b/src/lib/web3/hooks/useTickLiquidity.ts index cd36781b5..8b6241987 100644 --- a/src/lib/web3/hooks/useTickLiquidity.ts +++ b/src/lib/web3/hooks/useTickLiquidity.ts @@ -10,25 +10,39 @@ import { useIndexerStreamOfDualDataSet } from './useIndexer'; import { Token, TokenID } from '../utils/tokens'; type ReserveDataRow = [tickIndex: number, reserves: number]; +type ReserveDataSet = Map; -// add convenience method to fetch ticks in a pair -export function useTokenPairTickLiquidity([tokenIdA, tokenIdB]: [ +// add convenience method to fetch liquidity maps of a pair +export function useTokenPairMapLiquidity([tokenIdA, tokenIdB]: [ TokenID?, TokenID? ]): { - data: [TickInfo[] | undefined, TickInfo[] | undefined]; - isValidating: boolean; - error: unknown; + data?: [ReserveDataSet, ReserveDataSet]; + error?: unknown; } { - const [tokenId0, tokenId1] = useOrderedTokenPair([tokenIdA, tokenIdB]) || []; // stream data from indexer - const { data, error } = useIndexerStreamOfDualDataSet( + return useIndexerStreamOfDualDataSet( tokenIdA && tokenIdB && `/liquidity/pair/${tokenIdA}/${tokenIdB}`, { // remove entries of value 0 from the accumulated map, they are not used mapEntryRemovalValue: 0, } ); +} + +// add convenience method to fetch ticks in a pair +export function useTokenPairTickLiquidity([tokenIdA, tokenIdB]: [ + TokenID?, + TokenID? +]): { + data: [TickInfo[] | undefined, TickInfo[] | undefined]; + isValidating: boolean; + error: unknown; +} { + const [tokenId0, tokenId1] = useOrderedTokenPair([tokenIdA, tokenIdB]) || []; + + // use stream data from indexer + const { data, error } = useTokenPairMapLiquidity([tokenIdA, tokenIdB]); // add token context into pool reserves const token0 = useToken(tokenId0); diff --git a/src/lib/web3/hooks/useUserDeposits.ts b/src/lib/web3/hooks/useUserDeposits.ts new file mode 100644 index 000000000..82f5a71a2 --- /dev/null +++ b/src/lib/web3/hooks/useUserDeposits.ts @@ -0,0 +1,131 @@ +import { useEffect, useMemo } from 'react'; +import { UseQueryResult, useQuery } from '@tanstack/react-query'; +import { duality } from '@duality-labs/dualityjs'; +import { DepositRecord } from '@duality-labs/dualityjs/types/codegen/duality/dex/deposit_record'; +import { useDeepCompareMemoize } from 'use-deep-compare-effect'; + +import subscriber from '../subscriptionManager'; +import { useWeb3 } from '../useWeb3'; +import { MessageActionEvent, TendermintTxData } from '../events'; +import { CoinTransferEvent, mapEventAttributes } from '../utils/events'; +import { TokenIdPair, TokenPair, resolveTokenIdPair } from '../utils/tokens'; +import { minutes } from '../../utils/time'; + +const { REACT_APP__REST_API = '' } = process.env; + +function useAllUserDeposits(): UseQueryResult { + const { address } = useWeb3(); + + const result = useQuery({ + queryKey: ['user-deposits', address], + enabled: !!address, + queryFn: async (): Promise => { + if (address) { + // get LCD client + const lcd = await duality.ClientFactory.createLCDClient({ + restEndpoint: REACT_APP__REST_API, + }); + // get all user's deposits + const response = await lcd.duality.dex.userDepositsAll({ + address, + }); + // return unwrapped result + return response.Deposits?.sort( + (a, b) => + a.centerTickIndex.sub(b.centerTickIndex).toNumber() || + b.fee.sub(a.fee).toNumber() + ); + } + }, + refetchInterval: 5 * minutes, + }); + + const refetch = result.refetch; + + // on update to user's bank balance, we should update the user's sharesOwned + useEffect(() => { + if (address) { + const onTxBalanceUpdate = ( + event: MessageActionEvent, + tx: TendermintTxData + ) => { + const events = tx.value.TxResult.result.events.map(mapEventAttributes); + const transferBalanceEvents = events + .filter( + (event): event is CoinTransferEvent => event.type === 'transfer' + ) + .filter( + (event) => + event.attributes.recipient === address || + event.attributes.sender === address + ); + + // todo: use partial updates to avoid querying all of user's deposits + // on any balance update, but decide first whether to requests + // an update to all the user's deposits, or just partial updates + if (transferBalanceEvents.length >= 3) { + // update all known users shares + refetch({ cancelRefetch: false }); + } else { + // todo: add partial update logic here + refetch({ cancelRefetch: false }); + } + }; + // subscribe to changes in the user's bank balance + subscriber.subscribeMessage(onTxBalanceUpdate, { + transfer: { recipient: address }, + }); + subscriber.subscribeMessage(onTxBalanceUpdate, { + transfer: { sender: address }, + }); + return () => { + subscriber.unsubscribeMessage(onTxBalanceUpdate); + }; + } + }, [refetch, address]); + + return result; +} + +export function useUserDeposits( + tokenPair?: TokenPair | TokenIdPair +): UseQueryResult { + const tokenPairIDs = useDeepCompareMemoize(resolveTokenIdPair(tokenPair)); + + const result = useAllUserDeposits(); + const userDeposits = useDeepCompareMemoize(result.data); + const userDepositsOfTokenPair = useMemo(() => { + // if a pair ID request was given then filter the response + if (tokenPairIDs) { + const [tokenIdA, tokenIdB] = tokenPairIDs || []; + if (tokenIdA && tokenIdB) { + const tokenIDs = [tokenIdA, tokenIdB]; + return userDeposits?.filter((deposit) => { + return ( + tokenIDs.includes(deposit.pairID.token0) && + tokenIDs.includes(deposit.pairID.token1) + ); + }); + } + // return filtered deposits with no match + return []; + } + // return unfiltered deposits with all matches + return userDeposits; + }, [tokenPairIDs, userDeposits]); + + return { + ...result, + data: userDepositsOfTokenPair, + } as UseQueryResult; +} + +export function useUserHasDeposits( + tokenPair?: TokenPair | TokenIdPair +): UseQueryResult { + const result = useUserDeposits(tokenPair); + return { + ...result, + data: result.data && result.data.length > 0, + } as UseQueryResult; +} diff --git a/src/lib/web3/hooks/useUserReserves.ts b/src/lib/web3/hooks/useUserReserves.ts new file mode 100644 index 000000000..25d83313e --- /dev/null +++ b/src/lib/web3/hooks/useUserReserves.ts @@ -0,0 +1,542 @@ +import BigNumber from 'bignumber.js'; +import { useMemo, useRef } from 'react'; +import { useQueries } from '@tanstack/react-query'; +import { DepositRecord } from '@duality-labs/dualityjs/types/codegen/duality/dex/deposit_record'; +import { QuerySupplyOfRequest } from '@duality-labs/dualityjs/types/codegen/cosmos/bank/v1beta1/query'; +import { QueryGetPoolReservesRequest } from '@duality-labs/dualityjs/types/codegen/duality/dex/query'; +import { useDeepCompareMemoize } from 'use-deep-compare-effect'; + +import { useLcdClientPromise } from '../lcdClient'; +import { useUserDeposits } from './useUserDeposits'; +import { useSimplePrice } from '../../tokenPrices'; + +import { getPairID } from '../utils/pairs'; +import { priceToTickIndex, tickIndexToPrice } from '../utils/ticks'; +import { + Token, + TokenIdPair, + TokenPair, + getBaseDenomAmount, + getTokenId, + resolveTokenIdPair, +} from '../utils/tokens'; +import { useRpcPromise } from '../rpcQueryClient'; +import { duality } from '@duality-labs/dualityjs'; +import useTokens, { useTokensWithIbcInfo } from './useTokens'; +import { useTokenPairMapLiquidity } from '../../web3/hooks/useTickLiquidity'; +import { useOrderedTokenPair } from './useTokenPairs'; + +interface PairReserves { + reserves0: string; + reserves1: string; +} +interface UserReservesTotalShares { + deposit: DepositRecord; + totalShares: string; +} +interface UserReservesTotalReserves { + deposit: DepositRecord; + totalReserves: PairReserves; +} +interface IndicativeUserReserves { + deposit: DepositRecord; + indicativeReserves: PairReserves; +} +export interface UserReserves { + deposit: DepositRecord; + reserves: PairReserves; +} +export interface UserValuedReserves extends UserReserves { + value: number; +} + +interface CombinedUseQueries { + data: T | undefined; + isFetching: boolean; + error: Error | null; +} + +function useUserDepositsTotalShares( + tokenPair?: TokenPair | TokenIdPair +): CombinedUseQueries { + const lcdClientPromise = useLcdClientPromise(); + const { data: userPairDeposits } = useUserDeposits(tokenPair); + + // for each specific amount of userDeposits, fetch the totalShares that match + const memoizedData = useRef(); + const result = useQueries({ + queries: useMemo(() => { + return (userPairDeposits || []).flatMap((deposit) => { + const { pairID, centerTickIndex, fee, sharesOwned } = deposit; + if (Number(sharesOwned) > 0) { + const params: QuerySupplyOfRequest = { + denom: `DualityPoolShares-${pairID.token0}-${pairID.token1}-t${centerTickIndex}-f${fee}`, + }; + return { + // include sharesOwned in query key so that share updates trigger data update + queryKey: ['cosmos.bank.v1beta1.supplyOf', params, sharesOwned], + queryFn: async (): Promise => { + const lcdClient = await lcdClientPromise; + if (lcdClient) { + return lcdClient.cosmos.bank.v1beta1 + .supplyOf(params) + .then((response) => { + return { + deposit, + totalShares: response.amount.amount, + }; + }); + } + return null; + }, + }; + } + return []; + }); + }, [lcdClientPromise, userPairDeposits]), + combine(results) { + // only process data from successfully resolved queries + const data = results + .map((result) => result.data) + .filter((data): data is UserReservesTotalShares => !!data); + + // if array length or any item of the array has changed, then update data + if (data.length === memoizedData.current?.length) { + for (let i = 0; i < data.length; i++) { + const a = memoizedData.current[i]; + const b = data[i]; + if ( + a.totalShares !== b.totalShares || + !isEqualDeposit(a.deposit, b.deposit) + ) { + // an item has changed, update data + memoizedData.current = data; + break; + } + } + } else { + // the data array has changed, update data + memoizedData.current = data; + } + return { + data: memoizedData.current, + isFetching: results.some((result) => result.isFetching), + error: results.find((result) => result.error)?.error ?? null, + }; + }, + }); + + return result; +} + +function useUserDepositsTotalReserves( + tokenPair?: TokenPair | TokenIdPair +): CombinedUseQueries { + const rpcPromise = useRpcPromise(); + const { data: userPairDeposits } = useUserDeposits(tokenPair); + + // for each specific amount of userDeposits, fetch the totalShares that match + const memoizedData = useRef(); + const result = useQueries({ + queries: useMemo(() => { + return (userPairDeposits || []).flatMap((deposit) => { + const { pairID, fee, sharesOwned } = deposit; + const { token0, token1 } = pairID; + if (Number(sharesOwned) > 0) { + // return both upper and lower tick pools + return [ + // note: need to flip token0 tick index to align with query: + // param.tickIndex now means tickIndexTakerToMaker + { tokenIn: token0, tickIndex: deposit.lowerTickIndex.negate() }, + { tokenIn: token1, tickIndex: deposit.upperTickIndex }, + ].map(({ tokenIn, tickIndex }) => { + const params: QueryGetPoolReservesRequest = { + pairID: getPairID(pairID.token0, pairID.token1), + tokenIn, + tickIndex, + fee, + }; + return { + queryKey: ['duality.dex.poolReserves', params, sharesOwned], + queryFn: async () => { + // we use an RPC call here because the LCD endpoint always 404s + const rpc = await rpcPromise; + const client = new duality.dex.QueryClientImpl(rpc); + return client + .poolReserves(params) + .then((response) => { + return { + deposit, + params, + totalReserves: + response.poolReserves?.reservesMakerDenom || '0', + }; + }) + .catch(() => { + // assume the result was a 404: there is 0 liquidity + return { + deposit, + params, + totalReserves: '0', + }; + }); + }, + retry: false, + }; + }); + } + return []; + }); + }, [rpcPromise, userPairDeposits]), + combine(results) { + // only process data from successfully resolved queries + const data = results + .map((result) => result.data) + // combine reserves for unique deposits + .reduce((acc, data) => { + if (data) { + const foundReserves = acc.find(({ deposit }) => + isEqualDeposit(deposit, data.deposit) + ); + const reserves: UserReservesTotalReserves = foundReserves || { + deposit: data.deposit, + totalReserves: { reserves0: '', reserves1: '' }, + }; + if (data.params.tokenIn === data.deposit.pairID.token0) { + reserves.totalReserves.reserves0 = data.totalReserves; + } else if (data.params.tokenIn === data.deposit.pairID.token1) { + reserves.totalReserves.reserves1 = data.totalReserves; + } + return foundReserves ? acc : acc.concat(reserves); + } + return acc; + }, []) + // ensure these aren't empty strings + .filter( + (data) => data.totalReserves.reserves0 && data.totalReserves.reserves1 + ); + + // if array length or any item of the array has changed, then update data + if (data.length === memoizedData.current?.length) { + for (let i = 0; i < data.length; i++) { + const a = memoizedData.current[i]; + const b = data[i]; + if ( + a.totalReserves !== b.totalReserves || + !isEqualDeposit(a.deposit, b.deposit) + ) { + // an item has changed, update data + memoizedData.current = data; + break; + } + } + } else { + // the data array has changed, update data + memoizedData.current = data; + } + return { + data: memoizedData.current, + isFetching: results.some((result) => result.isFetching), + error: results.find((result) => result.error)?.error ?? null, + }; + }, + }); + + return result; +} + +function useUserIndicativeReserves( + tokenPair?: TokenPair | TokenIdPair +): CombinedUseQueries { + const userDepositsResults = useUserDeposits(tokenPair); + const userTotalSharesResults = useUserDepositsTotalShares(tokenPair); + const userTotalReservesResults = useUserDepositsTotalReserves(tokenPair); + + const data = useMemo((): IndicativeUserReserves[] | undefined => { + return userDepositsResults.data?.flatMap( + (deposit) => { + const foundTotalShares = userTotalSharesResults.data?.find((data) => + isEqualDeposit(data.deposit, deposit) + ); + const foundTotalReserves = userTotalReservesResults.data?.find((data) => + isEqualDeposit(data.deposit, deposit) + ); + if (foundTotalShares && foundTotalReserves) { + const sharesOwned = new BigNumber(deposit.sharesOwned); + const totalShares = new BigNumber(foundTotalShares.totalShares); + const totalReserves = foundTotalReserves.totalReserves; + const reserves0 = new BigNumber(totalReserves.reserves0); + const reserves1 = new BigNumber(totalReserves.reserves1); + const userPercentageOfShares = sharesOwned.dividedBy(totalShares); + const lowerTickIndex = new BigNumber(deposit.lowerTickIndex.toInt()); + const upperTickIndex = new BigNumber(deposit.upperTickIndex.toInt()); + const allReservesAsToken0 = reserves0.plus( + reserves1.multipliedBy(tickIndexToPrice(lowerTickIndex)) + ); + const allReservesAsToken1 = reserves1.plus( + reserves0.dividedBy(tickIndexToPrice(upperTickIndex)) + ); + return { + deposit, + indicativeReserves: { + reserves0: userPercentageOfShares + .multipliedBy(allReservesAsToken0) + .toFixed(0), + reserves1: userPercentageOfShares + .multipliedBy(allReservesAsToken1) + .toFixed(0), + }, + }; + } + return []; + } + ); + }, [ + userDepositsResults.data, + userTotalSharesResults.data, + userTotalReservesResults.data, + ]); + + return { + data, + isFetching: + userDepositsResults.isFetching || + userTotalSharesResults.isFetching || + userTotalReservesResults.isFetching, + error: + userDepositsResults.error || + userTotalSharesResults.error || + userTotalReservesResults.error, + }; +} + +export function useEstimatedUserReserves( + tokenPair?: TokenPair | TokenIdPair +): CombinedUseQueries { + const { + data: userIndicatveReserves, + isFetching, + error, + } = useUserIndicativeReserves(tokenPair); + + const allTokens = useTokensWithIbcInfo(useTokens()); + const allTokensByIdMap = useMemo>(() => { + return allTokens.reduce>((acc, token) => { + const id = getTokenId(token); + if (id && !acc.has(id)) { + acc.set(id, token); + } + return acc; + }, new Map()); + }, [allTokens]); + + const tokenByIdMap = useMemo>(() => { + const searchedTokenStrings: string[] = []; + return (userIndicatveReserves || []).reduce>( + (acc, indicateReserves) => { + for (const tokenId of Object.values(indicateReserves.deposit.pairID)) { + if (!searchedTokenStrings.includes(tokenId)) { + const foundToken = allTokensByIdMap.get(tokenId); + if (foundToken) { + acc.set(tokenId, foundToken); + } + searchedTokenStrings.push(tokenId); + } + } + return acc; + }, + new Map() + ); + }, [userIndicatveReserves, allTokensByIdMap]); + + const tokenList = useMemo( + () => Array.from(tokenByIdMap.values()), + [tokenByIdMap] + ); + + const { data: tokenPrices } = useSimplePrice(tokenList); + + const tokenPriceByIdMap = useMemo(() => { + return tokenList.reduce>( + (acc, token, index) => { + const tokenId = getTokenId(token); + if (tokenId) { + acc.set(tokenId, tokenPrices[index]); + } + return acc; + }, + new Map() + ); + }, [tokenList, tokenPrices]); + + // using the current price, make assumptions about the current reserves + return useMemo(() => { + if (Object.values(tokenPriceByIdMap).every((price = 0) => price > 0)) { + const userReserves = userIndicatveReserves?.flatMap( + ({ deposit, indicativeReserves: { reserves0, reserves1 } }) => { + const token0 = tokenByIdMap.get(deposit.pairID.token0); + const token1 = tokenByIdMap.get(deposit.pairID.token1); + const tokenPrice0 = tokenPriceByIdMap.get(deposit.pairID.token0); + const tokenPrice1 = tokenPriceByIdMap.get(deposit.pairID.token1); + if (token0 && token1 && tokenPrice0 && tokenPrice1) { + const display0 = getBaseDenomAmount(token0, 1) || 1; + const display1 = getBaseDenomAmount(token1, 1) || 1; + const basePrice0 = new BigNumber(tokenPrice0).dividedBy(display0); + const basePrice1 = new BigNumber(tokenPrice1).dividedBy(display1); + const centerTickIndex = priceToTickIndex( + basePrice1.div(basePrice0) + ); + // decide if the reserves are of token0 or token1 + const depositIsLeftOfPrice = centerTickIndex.isLessThan( + deposit.centerTickIndex.toInt() + ); + return depositIsLeftOfPrice + ? { + deposit, + reserves: { + reserves0, + reserves1: '0', + }, + value: basePrice0.multipliedBy(reserves0).toNumber(), + } + : { + deposit, + reserves: { + reserves0: '0', + reserves1, + }, + value: basePrice1.multipliedBy(reserves1).toNumber(), + }; + } + return []; + } + ); + return { + data: userReserves, + isFetching, + error, + }; + } + return { + data: undefined, + isFetching, + error, + }; + }, [ + tokenByIdMap, + tokenPriceByIdMap, + userIndicatveReserves, + isFetching, + error, + ]); +} + +// calculate total +export function useEstimatedUserReservesValue( + tokenPair?: TokenPair | TokenIdPair +): BigNumber { + const { data: estimatedUserReserves } = useEstimatedUserReserves(tokenPair); + return useMemo(() => { + return (estimatedUserReserves || []).reduce( + (acc, estimatedUserReserve) => acc.plus(estimatedUserReserve.value), + new BigNumber(0) + ); + }, [estimatedUserReserves]); +} + +function isEqualDeposit(a: DepositRecord, b: DepositRecord) { + // compare by reference or compare by properties + return ( + a === b || + (a.pairID.token0 === b.pairID.token0 && + a.pairID.token1 === b.pairID.token1 && + a.centerTickIndex.equals(b.centerTickIndex) && + a.fee.equals(b.fee)) + ); +} + +const emptyDataSet: never[] = []; +export function useAccurateUserReserves( + tokenPair?: TokenPair | TokenIdPair +): CombinedUseQueries { + const tokenIdPair = resolveTokenIdPair(tokenPair) || []; + const [tokenId0, tokenId1] = useOrderedTokenPair(tokenIdPair) || []; + const [tokenIdA, tokenIdB] = tokenIdPair; + const forward = tokenId0 === tokenIdA && tokenId1 === tokenIdB; + const reverse = tokenId0 === tokenIdB && tokenId1 === tokenIdA; + + // combine data from user reserves and tick liquidity + const { + data: userIndicativeReserves, + isFetching, + error, + } = useUserIndicativeReserves(tokenPair); + const { data: [liquidityMapA, liquidityMapB] = [] } = + useTokenPairMapLiquidity(tokenIdPair); + + const liquidityMap0 = forward ? liquidityMapA : liquidityMapB; + const liquidityMap1 = reverse ? liquidityMapA : liquidityMapB; + + // note: memoize this middle state as an optimization + // - liquidity maps update frequently + // - user indicative reserves update infrequently + // - the relevant liquidity pools to the user don't update that often + const userSpecificLiquidityKeyValues = useDeepCompareMemoize( + useMemo(() => { + return userIndicativeReserves?.map<[number, [number, number]]>( + ({ deposit }) => { + // find state from tick liquidity + // note: reserve of token is in tickIndexTakerToMaker and needs to be + // converted into tickIndex1to0 to align with token0/token1 math + const reserves0 = + liquidityMap0?.get(deposit.lowerTickIndex.toNumber()) || 0; + const reserves1 = + liquidityMap1?.get(deposit.upperTickIndex.negate().toNumber()) || 0; + // return in key, value format ready to create an array or map + // use this format because it is easy to memoize and deep-compare + return [deposit.centerTickIndex.toNumber(), [reserves0, reserves1]]; + } + ); + }, [liquidityMap0, liquidityMap1, userIndicativeReserves]) + ); + + const userReserves = useMemo(() => { + if ( + userIndicativeReserves?.length && + userSpecificLiquidityKeyValues?.length + ) { + const liquidityMap = new Map(userSpecificLiquidityKeyValues); + return (userIndicativeReserves || []).map( + ({ deposit, indicativeReserves }) => { + // find state from tick liquidity + const [reserves0, reserves1] = liquidityMap?.get( + deposit.centerTickIndex.toNumber() + ) || [0, 0]; + // compute user reserves from state to + const reserves1As0 = + reserves1 * + tickIndexToPrice( + new BigNumber(deposit.centerTickIndex.toNumber()) + ).toNumber(); + const totalAs0 = reserves0 + reserves1As0; + const percentage0 = totalAs0 > 0 ? reserves0 / totalAs0 : 0; + const percentage1 = totalAs0 > 0 ? reserves1As0 / totalAs0 : 0; + return { + deposit, + reserves: { + reserves0: new BigNumber(indicativeReserves.reserves0) + .multipliedBy(percentage0) + .toFixed(0), + reserves1: new BigNumber(indicativeReserves.reserves1) + .multipliedBy(percentage1) + .toFixed(0), + }, + }; + } + ); + } + return emptyDataSet; + }, [userIndicativeReserves, userSpecificLiquidityKeyValues]); + + return { data: userReserves, isFetching, error }; +} diff --git a/src/lib/web3/hooks/useUserShareValues.ts b/src/lib/web3/hooks/useUserShareValues.ts deleted file mode 100644 index 5b583c4b8..000000000 --- a/src/lib/web3/hooks/useUserShareValues.ts +++ /dev/null @@ -1,112 +0,0 @@ -import BigNumber from 'bignumber.js'; -import { useMemo } from 'react'; -import useTokens, { matchTokenByDenom } from './useTokens'; -import { useSimplePrice } from '../../tokenPrices'; -import { Token, getDisplayDenomAmount, getTokenId } from '../utils/tokens'; -import { - ShareValueContext, - UserDepositFilter, - UserPositionDepositContext, - useUserDeposits, - useUserPositionsContext, -} from './useUserShares'; - -export interface ValuedUserPositionDepositContext - extends UserPositionDepositContext { - token0Value?: BigNumber; - token1Value?: BigNumber; -} - -export function useUserPositionsShareValues( - poolDepositFilter?: UserDepositFilter -): ValuedUserPositionDepositContext[] { - const selectedPoolDeposits = useUserDeposits(poolDepositFilter); - const userPositionDepositContext = useUserPositionsContext(poolDepositFilter); - - const allTokens = useTokens(); - const selectedTokens = useMemo(() => { - const tokenMap = (selectedPoolDeposits || []).reduce< - Set - >((result, deposit) => { - const { token0, token1 } = deposit.pairID || {}; - if (token0) { - result.add(allTokens.find(matchTokenDenom(token0))); - } - if (token1) { - result.add(allTokens.find(matchTokenDenom(token1))); - } - return result; - }, new Set()); - - // return tokens - return Array.from(tokenMap.values()).filter( - (token): token is Token => !!token - ); - - function matchTokenDenom(denom: string) { - return (token: Token) => - !!token.denom_units.find((unit) => unit.denom === denom); - } - }, [allTokens, selectedPoolDeposits]); - - const { data: selectedTokensPrices } = useSimplePrice(selectedTokens); - - const selectedTokensPriceMap = useMemo(() => { - return selectedTokens.reduce<{ - [tokenId: string]: number | undefined; - }>((acc, token, index) => { - const tokenId = getTokenId(token); - if (tokenId) { - acc[tokenId] = selectedTokensPrices[index]; - } - return acc; - }, {}); - }, [selectedTokens, selectedTokensPrices]); - - return useMemo(() => { - return userPositionDepositContext.map( - ({ token0Context, token1Context, ...rest }) => { - return { - ...rest, - token0Context, - token1Context, - token0Value: token0Context && getValueOfContext(token0Context), - token1Value: token1Context && getValueOfContext(token1Context), - }; - } - ); - - function getValueOfContext( - context: ShareValueContext - ): BigNumber | undefined { - const { token: tokenDenom, userReserves } = context; - // what is the price per token? - const token = selectedTokens.find(matchTokenByDenom(tokenDenom)); - const price = selectedTokensPriceMap[getTokenId(token) || '']; - if (token && price && !isNaN(price)) { - // how many tokens does the user have? - const amount = getDisplayDenomAmount(token, userReserves); - // how much are those tokens worth? - const value = new BigNumber(amount || 0).multipliedBy(price); - return value; - } - } - }, [selectedTokens, selectedTokensPriceMap, userPositionDepositContext]); -} - -// calculate total -export function useUserPositionsShareValue( - poolDepositFilter?: UserDepositFilter -): BigNumber { - const userPositionsShareValues = - useUserPositionsShareValues(poolDepositFilter); - - return useMemo(() => { - return userPositionsShareValues.reduce( - (acc, { token0Value, token1Value }) => { - return acc.plus(token0Value || 0).plus(token1Value || 0); - }, - new BigNumber(0) - ); - }, [userPositionsShareValues]); -} diff --git a/src/lib/web3/hooks/useUserShares.ts b/src/lib/web3/hooks/useUserShares.ts deleted file mode 100644 index ba1892c9d..000000000 --- a/src/lib/web3/hooks/useUserShares.ts +++ /dev/null @@ -1,398 +0,0 @@ -import BigNumber from 'bignumber.js'; -import Long from 'long'; -import { useCallback, useMemo, useRef } from 'react'; -import { useQueries } from '@tanstack/react-query'; -import { - QuerySupplyOfRequest, - QuerySupplyOfResponse, -} from '@duality-labs/dualityjs/types/codegen/cosmos/bank/v1beta1/query'; -import { DepositRecord } from '@duality-labs/dualityjs/types/codegen/dualitylabs/duality/dex/deposit_record'; -import { - QueryGetPoolReservesRequest, - QueryGetPoolReservesResponse, -} from '@duality-labs/dualityjs/types/codegen/dualitylabs/duality/dex/query'; -import { dualitylabs } from '@duality-labs/dualityjs'; - -import { useLcdClientPromise } from '../lcdClient'; -import { useRpcPromise } from '../rpcQueryClient'; -import { getPairID } from '../utils/pairs'; -import { - Token, - TokenID, - TokenIdPair, - TokenPair, - resolveTokenIdPair, -} from '../utils/tokens'; -import useTokens, { - matchTokenByDenom, - useTokensWithIbcInfo, -} from './useTokens'; -import { useShares } from '../indexerProvider'; -import { getShareInfo } from '../utils/shares'; - -interface DirectionalDepositRecord - extends Omit< - DepositRecord, - 'centerTickIndex' | 'lowerTickIndex' | 'upperTickIndex' - > { - centerTickIndex1To0: DepositRecord['centerTickIndex']; - lowerTickIndex1To0: DepositRecord['lowerTickIndex']; - upperTickIndex1To0: DepositRecord['upperTickIndex']; -} - -// default useUserPositionsTotalShares filter to all user's deposits -export type UserDepositFilter = ( - poolDeposit: DirectionalDepositRecord -) => boolean; -const defaultFilter: UserDepositFilter = () => true; - -export function usePoolDepositFilterForPair( - tokenPair: TokenPair | TokenIdPair | undefined -): (poolDeposit: DirectionalDepositRecord) => boolean { - const [tokenIdA, tokenIdB] = resolveTokenIdPair(tokenPair); - const poolDepositFilter = useCallback( - (poolDeposit: DirectionalDepositRecord) => { - return ( - !!tokenIdA && - !!tokenIdB && - !!poolDeposit.pairID && - [tokenIdA, tokenIdB].includes(poolDeposit.pairID.token0) && - [tokenIdA, tokenIdB].includes(poolDeposit.pairID.token1) - ); - }, - [tokenIdA, tokenIdB] - ); - return poolDepositFilter; -} - -// select all (or optional token pair list of) user shares -export function useUserDeposits( - poolDepositFilter: UserDepositFilter = defaultFilter -): Required[] | undefined { - const { data: shares } = useShares(); - return useMemo(() => { - const deposits = shares?.map((share) => { - const { fee, pairId, sharesOwned, tickIndex1To0 } = share; - const [token0Address, token1Address] = pairId.split('<>'); - return { - pairID: { token0: token0Address, token1: token1Address }, - sharesOwned, - centerTickIndex1To0: Long.fromString(tickIndex1To0), - lowerTickIndex1To0: Long.fromString(tickIndex1To0).sub(fee), - upperTickIndex1To0: Long.fromString(tickIndex1To0).add(fee), - fee: Long.fromString(fee), - }; - }); - // return filtered list of deposits - const filteredDeposits = deposits?.filter(poolDepositFilter); - // only accept deposits with pairID properties attached (should be always) - return filteredDeposits?.filter( - (deposit): deposit is Required => - !!deposit.pairID - ); - }, [shares, poolDepositFilter]); -} - -// select all (or optional token pair list of) user shares -export function useUserPositionsTotalShares( - poolDepositFilter: UserDepositFilter = defaultFilter -) { - const lcdClientPromise = useLcdClientPromise(); - const selectedPoolDeposits = useUserDeposits(poolDepositFilter); - - const memoizedData = useRef([]); - const { data } = useQueries({ - queries: useMemo(() => { - return (selectedPoolDeposits || []).flatMap( - ({ pairID: { token0, token1 } = {}, centerTickIndex1To0, fee }) => { - if (token0 && token1) { - const params: QuerySupplyOfRequest = { - denom: `DualityPoolShares-${token0}-${token1}-t${centerTickIndex1To0}-f${fee}`, - }; - return { - queryKey: ['cosmos.bank.v1beta1.supplyOf', params], - queryFn: async () => { - const lcdClient = await lcdClientPromise; - return lcdClient - ? lcdClient.cosmos.bank.v1beta1.supplyOf(params) - : null; - }, - staleTime: 10e3, - }; - } - return []; - } - ); - }, [lcdClientPromise, selectedPoolDeposits]), - combine(results) { - // only process data from successfully resolved queries - const data = results - .map((result) => result.data) - .filter((data): data is QuerySupplyOfResponse => !!data); - - if (data.length === memoizedData.current.length) { - // let isSame: boolean = true; - for (let i = 0; i < data.length; i++) { - const supply1 = data[i]; - const supply2 = memoizedData.current[i]; - if (!(supply1.amount === supply2.amount)) { - // an item has changed, update data - memoizedData.current = data; - break; - } - } - } else { - // the data array has changed, update data - memoizedData.current = data; - } - return { - data: memoizedData.current, - pending: results.some((result) => result.isPending), - }; - }, - }); - - return data; -} - -// select all (or optional token pair list of) user reserves -export function useUserPositionsTotalReserves( - poolDepositFilter?: UserDepositFilter -) { - const rpcPromise = useRpcPromise(); - const selectedPoolDeposits = useUserDeposits(poolDepositFilter); - - const memoizedData = useRef([]); - const { data } = useQueries({ - queries: useMemo(() => { - return (selectedPoolDeposits || []).flatMap( - ({ - pairID: { token0, token1 } = {}, - lowerTickIndex1To0, - upperTickIndex1To0, - fee, - }) => { - const pairID = getPairID(token0, token1); - if (token0 && token1 && pairID && fee !== undefined) { - // return both upper and lower tick pools - return [ - { tokenIn: token0, tickIndex1To0: lowerTickIndex1To0 }, - { tokenIn: token1, tickIndex1To0: upperTickIndex1To0 }, - ].map(({ tokenIn, tickIndex1To0 }) => { - const params: QueryGetPoolReservesRequest = { - pairID, - tokenIn, - tickIndex: tickIndex1To0, - fee, - }; - return { - queryKey: ['dualitylabs.duality.dex.poolReserves', params], - queryFn: async () => { - const rpc = await rpcPromise; - const client = new dualitylabs.duality.dex.QueryClientImpl( - rpc - ); - // todo: when switching to RPC pool reserves with pagination - // remember that all pagination fields are required - const result = await client.poolReserves(params); - return result ?? null; - }, - // don't retry, a 404 means there is 0 liquidity there - retry: false, - // refetch not that often - staleTime: 60 * 1e3, - }; - }); - } - return []; - } - ); - }, [rpcPromise, selectedPoolDeposits]), - combine(results) { - // only process data from successfully resolved queries - const data = results - .map((result) => result.data) - .filter((data): data is QueryGetPoolReservesResponse => !!data); - - if (data.length === memoizedData.current.length) { - // let isSame: boolean = true; - for (let i = 0; i < data.length; i++) { - const poolReserves1 = data[i].poolReserves; - const poolReserves2 = memoizedData.current[i].poolReserves; - if ( - !( - poolReserves1?.tickIndex === poolReserves2?.tickIndex && - poolReserves1?.fee === poolReserves2?.fee && - poolReserves1?.reserves === poolReserves2?.reserves && - poolReserves1?.pairID?.token0 === poolReserves2?.pairID?.token0 && - poolReserves1?.pairID?.token1 === poolReserves2?.pairID?.token1 && - poolReserves1?.tokenIn === poolReserves2?.tokenIn - ) - ) { - // an item has changed, update data - memoizedData.current = data; - break; - } - } - } else { - // the data array has changed, update data - memoizedData.current = data; - } - return { - data: memoizedData.current, - pending: results.some((result) => result.isPending), - }; - }, - }); - - return data; -} - -export interface ShareValueContext { - userShares: BigNumber; - dexTotalShares: BigNumber; - token: TokenID; - tickIndex1To0: BigNumber; - dexTotalReserves: BigNumber; - userReserves: BigNumber; -} -export interface UserPositionDepositContext { - deposit: Required; - token0: Token; - token0Context?: ShareValueContext; - token1: Token; - token1Context?: ShareValueContext; -} - -// collect all the context about the user's positions together -export function useUserPositionsContext( - poolDepositFilter?: UserDepositFilter -): UserPositionDepositContext[] { - const selectedPoolDeposits = useUserDeposits(poolDepositFilter); - const userPositionsTotalShares = - useUserPositionsTotalShares(poolDepositFilter); - const userPositionsTotalReserves = - useUserPositionsTotalReserves(poolDepositFilter); - - const allTokens = useTokensWithIbcInfo(useTokens()); - - return useMemo(() => { - return (selectedPoolDeposits || []).flatMap( - (deposit) => { - const totalSharesResponse = - userPositionsTotalShares?.find((data) => { - const shareInfo = data?.amount && getShareInfo(data.amount); - if (shareInfo) { - return ( - shareInfo.token0Address === deposit.pairID?.token0 && - shareInfo.token1Address === deposit.pairID?.token1 && - shareInfo.tickIndex1To0String === - deposit.centerTickIndex1To0.toString() && - shareInfo.feeString === deposit.fee.toString() - ); - } - return false; - }) ?? undefined; - - // find the upper and lower reserves that match this position - const lowerReserveResponse = - userPositionsTotalReserves?.find((data) => { - return ( - data?.poolReserves?.tokenIn === deposit.pairID?.token0 && - data?.poolReserves?.pairID?.token0 === deposit.pairID?.token0 && - data?.poolReserves?.pairID?.token1 === deposit.pairID?.token1 && - data?.poolReserves?.tickIndex.toString() === - deposit.lowerTickIndex1To0.toString() && - data?.poolReserves?.fee.toString() === deposit.fee.toString() - ); - }) ?? undefined; - const upperReserveResponse = - userPositionsTotalReserves?.find((data) => { - return ( - data?.poolReserves?.tokenIn === deposit.pairID?.token1 && - data?.poolReserves?.pairID?.token0 === deposit.pairID?.token0 && - data?.poolReserves?.pairID?.token1 === deposit.pairID?.token1 && - data?.poolReserves?.tickIndex.toString() === - deposit.upperTickIndex1To0.toString() && - data?.poolReserves?.fee.toString() === deposit.fee.toString() - ); - }) ?? undefined; - const token0 = allTokens.find(matchTokenByDenom(deposit.pairID.token0)); - const token1 = allTokens.find(matchTokenByDenom(deposit.pairID.token1)); - - if (token0 && token1) { - // collect context of both side of the liquidity - const token0Context: ShareValueContext | undefined = deposit && - totalSharesResponse && - lowerReserveResponse && { - token: lowerReserveResponse.poolReserves?.tokenIn ?? '', - tickIndex1To0: new BigNumber( - lowerReserveResponse.poolReserves?.tickIndex.toString() ?? 0 - ), - userShares: new BigNumber(deposit.sharesOwned), - dexTotalShares: new BigNumber( - totalSharesResponse.amount?.amount ?? 0 - ), - // start with empty value, will be filled in next step - userReserves: new BigNumber(0), - dexTotalReserves: new BigNumber( - lowerReserveResponse.poolReserves?.reserves ?? 0 - ), - }; - - const token1Context: ShareValueContext | undefined = deposit && - totalSharesResponse && - upperReserveResponse && { - token: upperReserveResponse.poolReserves?.tokenIn ?? '', - tickIndex1To0: new BigNumber( - upperReserveResponse.poolReserves?.tickIndex.toString() ?? 0 - ), - userShares: new BigNumber(deposit.sharesOwned), - dexTotalShares: new BigNumber( - totalSharesResponse?.amount?.amount ?? 0 - ), - // start with empty value, will be filled in next step - userReserves: new BigNumber(0), - dexTotalReserves: new BigNumber( - upperReserveResponse.poolReserves?.reserves ?? 0 - ), - }; - return [ - { - deposit, - token0, - token1, - // calculate the user's reserves - token0Context: token0Context && { - ...token0Context, - userReserves: getReservesFromShareValueContext(token0Context), - }, - token1Context: token1Context && { - ...token1Context, - userReserves: getReservesFromShareValueContext(token1Context), - }, - }, - ]; - } - // skip - return []; - } - ); - - // calculate the user's reserves - function getReservesFromShareValueContext({ - userShares, - dexTotalShares, - dexTotalReserves, - }: ShareValueContext): BigNumber { - return dexTotalReserves - .multipliedBy(userShares) - .dividedBy(dexTotalShares); - } - }, [ - allTokens, - selectedPoolDeposits, - userPositionsTotalShares, - userPositionsTotalReserves, - ]); -} diff --git a/src/lib/web3/indexerProvider.tsx b/src/lib/web3/indexerProvider.tsx index 04723c4fd..c5b546cc5 100644 --- a/src/lib/web3/indexerProvider.tsx +++ b/src/lib/web3/indexerProvider.tsx @@ -21,10 +21,8 @@ import useTokens, { } from '../../lib/web3/hooks/useTokens'; import useTokenPairs, { TokenPairReserves } from './hooks/useTokenPairs'; -import { feeTypes } from './utils/fees'; - -import { Token, getTokenId } from './utils/tokens'; -import { IndexedShare, getShareInfo } from './utils/shares'; +import { Token } from './utils/tokens'; +import { isDexShare } from './utils/shares'; import { PairIdString, getPairID } from './utils/pairs'; import { Coin } from '@duality-labs/dualityjs/types/codegen/cosmos/base/v1beta1/coin'; @@ -37,9 +35,6 @@ interface UserBankBalance { balances: Array; } -interface UserShares { - shares: Array; -} interface PairUpdateHeightData { [pairID: string]: number; // block height } @@ -49,10 +44,6 @@ interface IndexerContextType { data?: UserBankBalance; isValidating: boolean; }; - shares: { - data?: UserShares; - isValidating: boolean; - }; tokens: { data?: Token[]; isValidating: boolean; @@ -74,9 +65,6 @@ const IndexerContext = createContext({ bank: { isValidating: true, }, - shares: { - isValidating: true, - }, tokens: { isValidating: true, }, @@ -93,7 +81,6 @@ const defaultFetchParams: Partial = { export function IndexerProvider({ children }: { children: React.ReactNode }) { const [bankData, setBankData] = useState(); - const [shareData, setShareData] = useState(); const [poolUpdateHeightData, setPoolUpdateHeightData] = useState({}); const tokensData = useTokens(); @@ -116,51 +103,8 @@ export function IndexerProvider({ children }: { children: React.ReactNode }) { fetchBankData() .then((coins = []) => { // separate out 'normal' and 'share' tokens from the bank balance - const [tokens, tokenizedShares] = coins.reduce< - [Array, Array] - >( - ([tokens, tokenizedShares], coin) => { - const { - token0Address: token0, - token1Address: token1, - tickIndex1To0String: tickIndex1To0, - feeString: fee, - } = getShareInfo(coin) || {}; - // transform tokenized shares into shares - if (token0 && token1 && tickIndex1To0 && fee) { - // add tokenized share if everything is fine - if (address) { - const tokenizedShare: IndexedShare = { - // todo: remove address from here - address, - pairId: getPairID(token0, token1), - tickIndex1To0, - fee, - sharesOwned: coin.amount, - }; - return [tokens, [...tokenizedShares, tokenizedShare]]; - } - // drop unknown (to front end) share - else { - // eslint-disable-next-line no-console - console.warn( - `Received unknown denomination in tokenized shares: ${coin.denom}`, - { - feeTypes, - fee, - address, - } - ); - return [tokens, tokenizedShares]; - } - } else { - return [[...tokens, coin], tokenizedShares]; - } - }, - [[], []] - ); - setBankData({ balances: tokens }); - setShareData({ shares: tokenizedShares }); + const nonDexCoins = coins.filter((coin) => !isDexShare(coin)); + setBankData({ balances: nonDexCoins }); }) .catch((e) => { setFetchBankDataState((state) => ({ @@ -263,8 +207,8 @@ export function IndexerProvider({ children }: { children: React.ReactNode }) { return tickUpdateEvents.reduce( (poolUpdateHeightData, event) => { const pairID = getPairID( - event.attributes.Token0, - event.attributes.Token1 + event.attributes.TokenZero, + event.attributes.TokenOne ); poolUpdateHeightData[pairID] = height; return poolUpdateHeightData; @@ -286,13 +230,9 @@ export function IndexerProvider({ children }: { children: React.ReactNode }) { data: bankData, isValidating: !bankData, }, - shares: { - data: shareData, - isValidating: !shareData, - }, tokens: { data: tokensData, - isValidating: !shareData, + isValidating: !bankData, }, tokenPairs: { data: tokenPairsData, @@ -302,7 +242,7 @@ export function IndexerProvider({ children }: { children: React.ReactNode }) { }, pairUpdateHeight: poolUpdateHeightData, }; - }, [bankData, shareData, tokensData, tokenPairsData, poolUpdateHeightData]); + }, [bankData, tokensData, tokenPairsData, poolUpdateHeightData]); return ( {children} @@ -339,38 +279,6 @@ export function useBankBalances() { return { data: balances, ...rest }; } -export function useShareData() { - return useContext(IndexerContext).shares; -} - -export function useShares({ - tokens, -}: { tokens?: [tokenA: Token, tokenB: Token] } = {}) { - const { data, isValidating } = useShareData(); - const shares = useMemo((): IndexedShare[] | undefined => { - // filter to specific tokens if asked for - const shares = data?.shares.filter( - (share) => Number(share.sharesOwned) > 0 - ); - if (tokens) { - return shares?.filter(tokensFilter(tokens)); - } - return shares; - - function tokensFilter(tokens: [tokenA: Token, tokenB: Token]) { - const [denomA, denomB] = tokens.map((token) => getTokenId(token)); - return function tokenFilter({ pairId = '' }: IndexedShare): boolean { - const [denom0, denom1] = pairId.split('/'); - return ( - (denomA === denom0 && denomB === denom1) || - (denomA === denom1 && denomB === denom0) - ); - }; - } - }, [data?.shares, tokens]); - return { data: shares, isValidating }; -} - export function useTokensList() { return useContext(IndexerContext).tokens; } diff --git a/src/lib/web3/lcdClient.ts b/src/lib/web3/lcdClient.ts index a728b37e4..dc058af3e 100644 --- a/src/lib/web3/lcdClient.ts +++ b/src/lib/web3/lcdClient.ts @@ -1,16 +1,16 @@ import { HttpEndpoint } from '@cosmjs/tendermint-rpc'; -import { dualitylabs } from '@duality-labs/dualityjs'; +import { duality } from '@duality-labs/dualityjs'; import { useMemo } from 'react'; const { REACT_APP__REST_API = '' } = process.env; export function lcdClient(rpcURL = REACT_APP__REST_API) { - return dualitylabs.ClientFactory.createLCDClient({ restEndpoint: rpcURL }); + return duality.ClientFactory.createLCDClient({ restEndpoint: rpcURL }); } type LcdClient = Awaited< - ReturnType + ReturnType >; const _lcdClients: Record = {}; @@ -33,7 +33,7 @@ const getLcdClient = async ( if (_lcdClients.hasOwnProperty(key)) { return _lcdClients[key]; } - const lcd = await dualitylabs.ClientFactory.createLCDClient({ + const lcd = await duality.ClientFactory.createLCDClient({ restEndpoint: key, }); _lcdClients[key] = lcd; diff --git a/src/lib/web3/rpcMsgClient.ts b/src/lib/web3/rpcMsgClient.ts index 6a734abaa..3e39d4b80 100644 --- a/src/lib/web3/rpcMsgClient.ts +++ b/src/lib/web3/rpcMsgClient.ts @@ -3,7 +3,7 @@ import { OfflineAminoSigner } from '@cosmjs/amino'; import { getSigningCosmosClient, - getSigningDualitylabsClient, + getSigningDualityClient, getSigningIbcClient, } from '@duality-labs/dualityjs'; @@ -13,7 +13,7 @@ export default function rpcClient( wallet?: OfflineSigner, rpcURL = REACT_APP__RPC_API ) { - return getSigningDualitylabsClient({ + return getSigningDualityClient({ rpcEndpoint: rpcURL, signer: wallet as OfflineAminoSigner, }); @@ -22,7 +22,7 @@ export default function rpcClient( export function signingRpcClient( getSigningClientFunction: | typeof getSigningCosmosClient - | typeof getSigningDualitylabsClient, + | typeof getSigningDualityClient, wallet?: OfflineSigner, rpcURL = REACT_APP__RPC_API ) { diff --git a/src/lib/web3/utils/events.ts b/src/lib/web3/utils/events.ts index b2be51baa..33d11dfa7 100644 --- a/src/lib/web3/utils/events.ts +++ b/src/lib/web3/utils/events.ts @@ -71,13 +71,13 @@ export interface DexDepositEvent { action: 'Deposit'; Creator: WalletAddress; Receiver: WalletAddress; - Token0: string; - Token1: string; + TokenZero: string; + TokenOne: string; TickIndex: string; Fee: string; SharesMinted: string; - Reserves0Deposited: string; - Reserves1Deposited: string; + ReservesZeroDeposited: string; + ReservesOneDeposited: string; }; } @@ -88,12 +88,12 @@ export interface DexWithdrawalEvent { action: 'Withdraw'; Creator: WalletAddress; Receiver: WalletAddress; - Token0: string; - Token1: string; + TokenZero: string; + TokenOne: string; TickIndex: string; Fee: string; - Reserves0Withdrawn: string; - Reserves1Withdrawn: string; + ReservesZeroWithdrawn: string; + ReservesOneWithdrawn: string; SharesRemoved: string; }; } @@ -105,8 +105,8 @@ export interface DexPlaceLimitOrderEvent { action: 'PlaceLimitOrder'; Creator: WalletAddress; Receiver: WalletAddress; - Token0: string; - Token1: string; + TokenZero: string; + TokenOne: string; TokenIn: string; AmountIn: string; LimitTick: string; @@ -126,8 +126,8 @@ export interface DexTickUpdateEvent { attributes: { module: 'dex'; action: 'TickUpdate'; - Token0: string; - Token1: string; + TokenZero: string; + TokenOne: string; TokenIn: string; TickIndex: string; Fee: string; @@ -211,8 +211,8 @@ export function getLastPrice( const tickIndex = lastTickUpdate ? new BigNumber(lastTickUpdate.attributes.TickIndex) : undefined; - const forward = lastTickUpdate?.attributes.Token0 === getTokenId(tokenA); - const reverse = lastTickUpdate?.attributes.Token0 === getTokenId(tokenB); + const forward = lastTickUpdate?.attributes.TokenZero === getTokenId(tokenA); + const reverse = lastTickUpdate?.attributes.TokenZero === getTokenId(tokenB); return tickIndex && (forward || reverse) ? tickIndexToPrice(forward ? tickIndex : tickIndex.negated()) : undefined; diff --git a/src/lib/web3/utils/pairs.ts b/src/lib/web3/utils/pairs.ts index 75ee1ac0e..d73411c10 100644 --- a/src/lib/web3/utils/pairs.ts +++ b/src/lib/web3/utils/pairs.ts @@ -35,12 +35,12 @@ export function getPairID( export function getTokenPairID( tokenPair: TokenPair | TokenIdPair ): PairIdString { - const tokenIdPair = resolveTokenIdPair(tokenPair); + const tokenIdPair = resolveTokenIdPair(tokenPair) || []; return getPairID(...tokenIdPair); } /** - * Check if the current TokenA/TokenB pair is in the same order as Token0/1 + * Check if the current TokenA/TokenB pair is in the same order as TokenZero/One * @param pairID pair id for tokens * @param tokenA ID of token A * @param tokenB ID of token B @@ -66,7 +66,7 @@ export function guessInvertedOrder( /** * Checks given token pair against stored data to determine - * if the current TokenA/TokenB pair exists and is in the same order as Token0/1 + * if the current TokenA/TokenB pair exists and is in the same order as TokenZero/One * @param pairMap pair map of stored tokens * @param tokenA ID of token A * @param tokenB ID of token B diff --git a/src/lib/web3/utils/shares.ts b/src/lib/web3/utils/shares.ts index fcd9663bf..3ceabd4f2 100644 --- a/src/lib/web3/utils/shares.ts +++ b/src/lib/web3/utils/shares.ts @@ -10,10 +10,15 @@ export interface IndexedShare { sharesOwned: string; } +const DexShareRegex = /^DualityPoolShares-([^-]+)-([^-]+)-t(-?\d+)-f(\d+)$/; + +// Duality denoms may be tokenized Dex shares or regular tokens on the chain +export function isDexShare(coin: Coin) { + return DexShareRegex.test(coin.denom); +} + export function getShareInfo(coin: Coin) { - const match = coin.denom.match( - /^DualityPoolShares-([^-]+)-([^-]+)-t(-?\d+)-f(\d+)$/ - ); + const match = coin.denom.match(DexShareRegex); if (match) { const [, token0Address, token1Address, tickIndexString, feeString] = match; return { @@ -32,7 +37,7 @@ export function getShareDenom( tickIndex1To0: number, fee: number ): string | undefined { - const tokenIds = resolveTokenIdPair(tokens); + const tokenIds = resolveTokenIdPair(tokens) || []; const [tokenId0, tokenId1] = guessInvertedOrder(tokens) ? [tokenIds[1], tokenIds[0]] : tokenIds; diff --git a/src/lib/web3/utils/tokens.ts b/src/lib/web3/utils/tokens.ts index 56cfceb7d..85ab14351 100644 --- a/src/lib/web3/utils/tokens.ts +++ b/src/lib/web3/utils/tokens.ts @@ -18,13 +18,16 @@ export function resolveTokenId( ): TokenID | undefined { return typeof token === 'string' ? token : getTokenId(token); } + +// treat input of [] as a tokenPair requirement, output: [undefined, undefined] +// treat input of undefined as no requirement, output: undefined export function resolveTokenIdPair( - [token0, token1]: TokenPair | TokenIdPair | [undefined, undefined] = [ - undefined, - undefined, - ] -): [TokenID | undefined, TokenID | undefined] { - return [resolveTokenId(token0), resolveTokenId(token1)]; + tokenPair: TokenPair | TokenIdPair | [undefined, undefined] | undefined +): [TokenID | undefined, TokenID | undefined] | undefined { + const [token0, token1] = tokenPair || [undefined, undefined]; + return tokenPair + ? [resolveTokenId(token0), resolveTokenId(token1)] + : undefined; } export const ibcDenomRegex = /^ibc\/[0-9A-Fa-f]+$/; diff --git a/src/pages/MyLiquidity/MyLiquidity.tsx b/src/pages/MyLiquidity/MyLiquidity.tsx index 0adf5c97e..37c52c33b 100644 --- a/src/pages/MyLiquidity/MyLiquidity.tsx +++ b/src/pages/MyLiquidity/MyLiquidity.tsx @@ -7,7 +7,7 @@ import { import AssetsTableCard from '../../components/cards/AssetsTableCard'; import { useWeb3 } from '../../lib/web3/useWeb3'; -import { useUserPositionsShareValue } from '../../lib/web3/hooks/useUserShareValues'; +import { useEstimatedUserReservesValue } from '../../lib/web3/hooks/useUserReserves'; import { useUserBankValue } from '../../lib/web3/hooks/useUserBankValues'; import './MyLiquidity.scss'; @@ -48,7 +48,7 @@ function Heading() { function HeroCard() { const { wallet } = useWeb3(); - const allUserSharesValue = useUserPositionsShareValue(); + const allUserSharesValue = useEstimatedUserReservesValue(); const allUserBankValue = useUserBankValue(); return (
diff --git a/src/pages/MyLiquidity/useEditLiquidity.ts b/src/pages/MyLiquidity/useEditLiquidity.ts index 9b4f0ce5f..155952c45 100644 --- a/src/pages/MyLiquidity/useEditLiquidity.ts +++ b/src/pages/MyLiquidity/useEditLiquidity.ts @@ -10,13 +10,15 @@ import { checkMsgSuccessToast, createLoadingToast, } from '../../components/Notifications/common'; -import { getBaseDenomAmount } from '../../lib/web3/utils/tokens'; +import { Token, getBaseDenomAmount } from '../../lib/web3/utils/tokens'; -import { UserPositionDepositContext } from '../../lib/web3/hooks/useUserShares'; +import { UserReserves } from '../../lib/web3/hooks/useUserReserves'; import rpcClient from '../../lib/web3/rpcMsgClient'; -import { dualitylabs } from '@duality-labs/dualityjs'; +import { duality } from '@duality-labs/dualityjs'; -export interface EditedPosition extends UserPositionDepositContext { +export interface EditedPosition extends UserReserves { + token0: Token; + token1: Token; tickDiff0: BigNumber; tickDiff1: BigNumber; } @@ -88,26 +90,25 @@ export function useEditLiquidity(): [ ({ deposit: { pairID: { token0: token0Address, token1: token1Address }, - centerTickIndex1To0, + centerTickIndex, fee, sharesOwned: userShares, }, + reserves: { reserves0, reserves1 }, token0, token1, - token0Context, - token1Context, tickDiff0, tickDiff1, }) => { const userTotalReserves = BigNumber.sum( - token0Context?.userReserves || 0, - token1Context?.userReserves || 0 + reserves0 || 0, + reserves1 || 0 ); - return centerTickIndex1To0 !== undefined && + return centerTickIndex !== undefined && fee !== undefined && !isNaN(Number(fee)) && - token0Address && - token1Address && + token0 && + token1 && userTotalReserves.isGreaterThan(0) ? // for situations where withdrawing both side of liquidity // then add both together @@ -116,39 +117,37 @@ export function useEditLiquidity(): [ // I'm not certain that non-100% withdrawals work in all cases. tickDiff0.isLessThan(0) && tickDiff1.isLessThan(0) ? [ - dualitylabs.duality.dex.MessageComposer.withTypeUrl.withdrawal( - { - creator: web3Address, - tokenA: token0Address, - tokenB: token1Address, - receiver: web3Address, - tickIndexesAToB: [centerTickIndex1To0], - fees: [fee], - // approximate removal using percentages - // todo: this probably has a bug when withdrawing from a tick - // that has both token0 and token1 as this only takes into account one side - sharesToRemove: [ - tickDiff0 - .plus(tickDiff1) - .negated() - .dividedBy(userTotalReserves) - .multipliedBy(userShares) - .toFixed(0), - ], - } - ), + duality.dex.MessageComposer.withTypeUrl.withdrawal({ + creator: web3Address, + tokenA: token0Address, + tokenB: token1Address, + receiver: web3Address, + tickIndexesAToB: [centerTickIndex], + fees: [fee], + // approximate removal using percentages + // todo: this probably has a bug when withdrawing from a tick + // that has both token0 and token1 as this only takes into account one side + sharesToRemove: [ + tickDiff0 + .plus(tickDiff1) + .negated() + .dividedBy(userTotalReserves) + .multipliedBy(userShares) + .toFixed(0), + ], + }), ] : [ ...(!tickDiff0.isZero() ? [ tickDiff0.isGreaterThan(0) - ? dualitylabs.duality.dex.MessageComposer.withTypeUrl.deposit( + ? duality.dex.MessageComposer.withTypeUrl.deposit( { creator: web3Address, tokenA: token0Address, tokenB: token1Address, receiver: web3Address, - tickIndexesAToB: [centerTickIndex1To0], + tickIndexesAToB: [centerTickIndex], fees: [fee], amountsA: [ getBaseDenomAmount(token0, tickDiff0) || @@ -159,24 +158,22 @@ export function useEditLiquidity(): [ Options: [{ disable_autoswap: false }], } ) - : dualitylabs.duality.dex.MessageComposer.withTypeUrl.withdrawal( + : duality.dex.MessageComposer.withTypeUrl.withdrawal( { creator: web3Address, tokenA: token0Address, tokenB: token1Address, receiver: web3Address, - tickIndexesAToB: [centerTickIndex1To0], + tickIndexesAToB: [centerTickIndex], fees: [fee], // approximate removal using percentages // todo: this probably has a bug when withdrawing from a tick // that has both token0 and token1 as this only takes into account one side - sharesToRemove: token0Context + sharesToRemove: reserves0 ? [ tickDiff0 .negated() - .dividedBy( - token0Context.userReserves - ) + .dividedBy(reserves0) .multipliedBy(userShares) .toFixed(0), ] @@ -188,13 +185,13 @@ export function useEditLiquidity(): [ ...(!tickDiff1.isZero() ? [ tickDiff1.isGreaterThan(0) - ? dualitylabs.duality.dex.MessageComposer.withTypeUrl.deposit( + ? duality.dex.MessageComposer.withTypeUrl.deposit( { creator: web3Address, tokenA: token0Address, tokenB: token1Address, receiver: web3Address, - tickIndexesAToB: [centerTickIndex1To0], + tickIndexesAToB: [centerTickIndex], fees: [fee], amountsA: ['0'], amountsB: [ @@ -205,24 +202,22 @@ export function useEditLiquidity(): [ Options: [{ disable_autoswap: false }], } ) - : dualitylabs.duality.dex.MessageComposer.withTypeUrl.withdrawal( + : duality.dex.MessageComposer.withTypeUrl.withdrawal( { creator: web3Address, tokenA: token0Address, tokenB: token1Address, receiver: web3Address, - tickIndexesAToB: [centerTickIndex1To0], + tickIndexesAToB: [centerTickIndex], fees: [fee], // approximate removal using percentages // todo: this probably has a bug when withdrawing from a tick // that has both token0 and token1 as this only takes into account one side - sharesToRemove: token1Context + sharesToRemove: reserves1 ? [ tickDiff1 .negated() - .dividedBy( - token1Context.userReserves - ) + .dividedBy(reserves1) .multipliedBy(userShares) .toFixed(0), ] diff --git a/src/pages/Pool/MyPositionTableCard.tsx b/src/pages/Pool/MyPositionTableCard.tsx index 64446507b..f85ee3a02 100644 --- a/src/pages/Pool/MyPositionTableCard.tsx +++ b/src/pages/Pool/MyPositionTableCard.tsx @@ -249,10 +249,10 @@ export function MyEditedPositionTableCard({ // sort by price .sort((a, b) => { return !!invertedTokenOrder - ? b.deposit.centerTickIndex1To0.toNumber() - - a.deposit.centerTickIndex1To0.toNumber() - : a.deposit.centerTickIndex1To0.toNumber() - - b.deposit.centerTickIndex1To0.toNumber(); + ? b.deposit.centerTickIndex.toNumber() - + a.deposit.centerTickIndex.toNumber() + : a.deposit.centerTickIndex.toNumber() - + b.deposit.centerTickIndex.toNumber(); }) ); }, [editedUserPosition, invertedTokenOrder]); @@ -263,17 +263,16 @@ export function MyEditedPositionTableCard({ const maxPoolEquivalentReservesA = useMemo(() => { return sortedPosition.reduce( - (acc, { token0, token0Context, token1Context }) => { - const tokenAContext = matchTokens(tokenA, token0) - ? token0Context - : token1Context; - const tokenBContext = matchTokens(tokenB, token0) - ? token0Context - : token1Context; - const equivalentReserveA = tokenAContext?.userReserves.toNumber() ?? 0; + (acc, { token0, token1, reserves: { reserves0, reserves1 } }) => { + const [tokenReservesA, tokenReservesB] = + (matchTokens(tokenA, token0) && + matchTokens(tokenB, token1) && [reserves0, reserves1]) || + (matchTokens(tokenA, token1) && + matchTokens(tokenB, token0) && [reserves1, reserves0]) || + []; + const equivalentReserveA = Number(tokenReservesA) ?? 0; const equivalentReserveB = - tokenBContext?.userReserves.multipliedBy(edgpePriceBToA).toNumber() ?? - 0; + edgpePriceBToA.multipliedBy(tokenReservesB ?? 0).toNumber() ?? 0; return Math.max(acc, equivalentReserveA, equivalentReserveB); }, 0 @@ -282,31 +281,25 @@ export function MyEditedPositionTableCard({ const poolValues = useMemo(() => { return sortedPosition.map<[number, number]>( - ({ token0, token0Context, token1Context }) => { - const tokenAContext = matchTokens(tokenA, token0) - ? token0Context - : token1Context; - const tokenBContext = matchTokens(tokenB, token0) - ? token0Context - : token1Context; + ({ token0, token1, reserves: { reserves0, reserves1 } }) => { + const [tokenReservesA, tokenReservesB] = + (matchTokens(tokenA, token0) && + matchTokens(tokenB, token1) && [reserves0, reserves1]) || + (matchTokens(tokenA, token1) && + matchTokens(tokenB, token0) && [reserves1, reserves0]) || + []; const valueA = (priceA || 0) * new BigNumber( - (tokenAContext?.userReserves && - getDisplayDenomAmount( - tokenA, - tokenAContext?.userReserves || 0 - )) || + (tokenReservesA && + getDisplayDenomAmount(tokenA, tokenReservesA || 0)) || 0 ).toNumber(); const valueB = (priceB || 0) * new BigNumber( - (tokenBContext?.userReserves && - getDisplayDenomAmount( - tokenB, - tokenBContext?.userReserves || 0 - )) || + (tokenReservesB && + getDisplayDenomAmount(tokenB, tokenReservesB || 0)) || 0 ).toNumber(); return [valueA, valueB]; @@ -328,20 +321,19 @@ export function MyEditedPositionTableCard({ tickDiff0, tickDiff1, deposit, - token0Context, - token1Context, + reserves: { reserves0, reserves1 }, } = userPosition; const reserveA = !invertedTokenOrder - ? tickDiff0.plus(token0Context?.userReserves || 0) - : tickDiff1.plus(token1Context?.userReserves || 0); + ? tickDiff0.plus(reserves0 || 0) + : tickDiff1.plus(reserves1 || 0); const reserveB = !invertedTokenOrder - ? tickDiff1.plus(token1Context?.userReserves || 0) - : tickDiff0.plus(token0Context?.userReserves || 0); + ? tickDiff1.plus(reserves1 || 0) + : tickDiff0.plus(reserves0 || 0); const [valueA, valueB] = poolValues[index]; const tickIndexBToA = !invertedTokenOrder - ? new BigNumber(deposit.centerTickIndex1To0.toNumber()) - : new BigNumber(deposit.centerTickIndex1To0.toNumber()).negated(); + ? new BigNumber(deposit.centerTickIndex.toNumber()) + : new BigNumber(deposit.centerTickIndex.toNumber()).negated(); const displayPriceBToA = tickIndexToDisplayPrice( tickIndexBToA, @@ -413,18 +405,18 @@ export function MyEditedPositionTableCard({ onClick={() => { setEditedUserPosition((ticks) => { return ticks.map((tick) => { - return tick.deposit.centerTickIndex1To0.toNumber() === - deposit.centerTickIndex1To0.toNumber() && + return tick.deposit.centerTickIndex.toNumber() === + deposit.centerTickIndex.toNumber() && tick.deposit.fee.toNumber() === deposit.fee.toNumber() ? { ...tick, - tickDiff0: - tick.token0Context?.userReserves.negated() || - new BigNumber(0), - tickDiff1: - tick.token1Context?.userReserves.negated() || - new BigNumber(0), + tickDiff0: new BigNumber( + tick.reserves.reserves0 ?? 0 + ).negated(), + tickDiff1: new BigNumber( + tick.reserves.reserves1 ?? 0 + ).negated(), } : tick; }); @@ -441,8 +433,8 @@ export function MyEditedPositionTableCard({ onClick={() => { setEditedUserPosition((ticks) => { return ticks.map((tick) => { - return tick.deposit.centerTickIndex1To0.toNumber() === - deposit.centerTickIndex1To0.toNumber() && + return tick.deposit.centerTickIndex.toNumber() === + deposit.centerTickIndex.toNumber() && tick.deposit.fee.toNumber() === deposit.fee.toNumber() ? { diff --git a/src/pages/Pool/PoolManagement.tsx b/src/pages/Pool/PoolManagement.tsx index 1c3ccc76d..21f2de26d 100644 --- a/src/pages/Pool/PoolManagement.tsx +++ b/src/pages/Pool/PoolManagement.tsx @@ -38,7 +38,11 @@ import { MyNewPositionTableCard, } from './MyPositionTableCard'; -import { useTokenPathPart } from '../../lib/web3/hooks/useTokens'; +import { useAccurateUserReserves } from '../../lib/web3/hooks/useUserReserves'; +import { + matchTokenByDenom, + useTokenPathPart, +} from '../../lib/web3/hooks/useTokens'; import { useDeposit } from './useDeposit'; import useFeeLiquidityMap from './useFeeLiquidityMap'; @@ -57,6 +61,7 @@ import { FeeType, feeTypes } from '../../lib/web3/utils/fees'; import { LiquidityShape, liquidityShapes } from '../../lib/web3/utils/shape'; import { Token, + TokenPair, getBaseDenomAmount, getDisplayDenomAmount, getTokenId, @@ -71,10 +76,6 @@ import { useEditLiquidity, } from '../MyLiquidity/useEditLiquidity'; import { guessInvertedOrder } from '../../lib/web3/utils/pairs'; -import { - usePoolDepositFilterForPair, - useUserPositionsContext, -} from '../../lib/web3/hooks/useUserShares'; import { usePairPrice } from '../../lib/tokenPrices'; import PoolLayout from './PoolLayout'; @@ -133,8 +134,7 @@ function getEditedPositionTick( token0, token1, deposit, - token0Context, - token1Context, + reserves, tickDiff0, tickDiff1, }: EditedPosition): Tick => { @@ -142,17 +142,17 @@ function getEditedPositionTick( const maybeTickDiff1 = includeTickDiff ? tickDiff1 : new BigNumber(0); return { reserveA: !invertedTokenOrder - ? maybeTickDiff0.plus(token0Context?.userReserves || 0) - : maybeTickDiff1.plus(token1Context?.userReserves || 0), + ? maybeTickDiff0.plus(reserves.reserves0 || 0) + : maybeTickDiff1.plus(reserves.reserves1 || 0), reserveB: !invertedTokenOrder - ? maybeTickDiff1.plus(token1Context?.userReserves || 0) - : maybeTickDiff0.plus(token0Context?.userReserves || 0), + ? maybeTickDiff1.plus(reserves.reserves1 || 0) + : maybeTickDiff0.plus(reserves.reserves0 || 0), tickIndexBToA: - (!invertedTokenOrder ? 1 : -1) * deposit.centerTickIndex1To0.toNumber(), + (!invertedTokenOrder ? 1 : -1) * deposit.centerTickIndex.toNumber(), priceBToA: tickIndexToPrice( !invertedTokenOrder - ? new BigNumber(deposit.centerTickIndex1To0.toNumber()) - : new BigNumber(deposit.centerTickIndex1To0.toNumber()).negated() + ? new BigNumber(deposit.centerTickIndex.toNumber()) + : new BigNumber(deposit.centerTickIndex.toNumber()).negated() ), fee: deposit.fee.toNumber(), tokenA: !invertedTokenOrder ? token0 : token1, @@ -797,10 +797,11 @@ export default function PoolManagement({ getTokenId(tokenB) ); - const pairPoolDepositFilter = usePoolDepositFilterForPair( - tokenA && tokenB ? [tokenA, tokenB] : ['', ''] - ); - const userPositionsContext = useUserPositionsContext(pairPoolDepositFilter); + const tokenPair = useMemo(() => { + if (tokenA && tokenB) { + return [tokenA, tokenB]; + } + }, [tokenA, tokenB]); const [{ isValidating: isValidatingEdit }, sendEditRequest] = useEditLiquidity(); @@ -811,35 +812,45 @@ export default function PoolManagement({ const [[viewableMinIndex, viewableMaxIndex] = [], setViewableIndexes] = useState<[number, number] | undefined>(); + const { data: userReserves } = useAccurateUserReserves(tokenPair); + + // add token information to user reserves + // note: this could be refactored away if we didn't need token information + // for converting denom amounts. but the conversion ability is helpful + const userTokenReserves = useMemo(() => { + const token0Address = userReserves?.at(0)?.deposit.pairID.token0 ?? ''; + const token1Address = userReserves?.at(0)?.deposit.pairID.token1 ?? ''; + const token0 = tokenPair?.find(matchTokenByDenom(token0Address)); + const token1 = tokenPair?.find(matchTokenByDenom(token1Address)); + return ( + token0 && + token1 && + userReserves?.map((userReserve) => ({ ...userReserve, token0, token1 })) + ); + }, [userReserves, tokenPair]); + const [editedUserPosition, setEditedUserPosition] = useState< Array >([]); useEffect(() => { setEditedUserPosition((editedUserPosition) => { - let isEqual = editedUserPosition.length === userPositionsContext.length; - if (isEqual) { + let isEqual = editedUserPosition.length === userTokenReserves?.length; + if (userTokenReserves && isEqual) { for (let i = 0; i < editedUserPosition.length; i++) { const deposit = editedUserPosition[i].deposit; - const updatedDeposit = userPositionsContext[i].deposit; + const updatedDeposit = userTokenReserves[i].deposit; if ( // check if the user's deposits have changed at all !( deposit.pairID.token0 === updatedDeposit.pairID.token0 && deposit.pairID.token1 === updatedDeposit.pairID.token1 && deposit.sharesOwned === updatedDeposit.sharesOwned && - deposit.lowerTickIndex1To0.equals( - updatedDeposit.lowerTickIndex1To0 - ) && - deposit.centerTickIndex1To0.equals( - updatedDeposit.centerTickIndex1To0 - ) && - deposit.upperTickIndex1To0.equals( - updatedDeposit.upperTickIndex1To0 - ) && + deposit.lowerTickIndex.equals(updatedDeposit.lowerTickIndex) && + deposit.centerTickIndex.equals(updatedDeposit.centerTickIndex) && + deposit.upperTickIndex.equals(updatedDeposit.upperTickIndex) && deposit.fee.equals(updatedDeposit.fee) ) - // todo: check if the reserves have changed side ) { isEqual = false; break; @@ -847,36 +858,37 @@ export default function PoolManagement({ } } - if (isEqual) { + if (userTokenReserves && isEqual) { // merge context updates into the edited position return editedUserPosition.map((editedUserPosition, i) => { return { ...editedUserPosition, - token0Context: userPositionsContext[i].token0Context, - token1Context: userPositionsContext[i].token1Context, + // update position with new data + deposit: userTokenReserves[i].deposit, + reserves: userTokenReserves[i].reserves, }; }); } else { // reset the position as it has changed - return userPositionsContext.map((userPosition) => ({ - ...userPosition, + return (userTokenReserves || []).map((userReserve) => ({ + ...userReserve, tickDiff0: new BigNumber(0), tickDiff1: new BigNumber(0), })); } }); - }, [userPositionsContext]); + }, [userTokenReserves]); const editedUserTicksBase = useMemo(() => { - return userPositionsContext - .map((userPosition) => ({ - ...userPosition, + return (userTokenReserves || []) + .map((userReserve) => ({ + ...userReserve, tickDiff0: new BigNumber(0), tickDiff1: new BigNumber(0), })) .map(getEditedPositionTick(true, invertedTokenOrder)) .sort((a, b) => a.tickIndexBToA - b.tickIndexBToA); - }, [userPositionsContext, invertedTokenOrder]); + }, [userTokenReserves, invertedTokenOrder]); const editedUserTicks = useMemo(() => { return editedUserPosition @@ -1113,9 +1125,7 @@ export default function PoolManagement({ : [userPosition.token0, userPosition.token1]; const price = formatPrice( tickIndexToPrice( - new BigNumber( - userPosition.deposit.centerTickIndex1To0.toNumber() - ) + new BigNumber(userPosition.deposit.centerTickIndex.toNumber()) ).toNumber() ); const withdrawA = diffA.isLessThan(0) && ( @@ -1174,7 +1184,7 @@ export default function PoolManagement({ ); return ( {depositA} {depositB} diff --git a/src/pages/Pool/PoolOverview.tsx b/src/pages/Pool/PoolOverview.tsx index 33f7ed776..05da8a65a 100644 --- a/src/pages/Pool/PoolOverview.tsx +++ b/src/pages/Pool/PoolOverview.tsx @@ -14,6 +14,7 @@ import StatCardTVL from '../../components/stats/StatCardTVL'; import { formatAddress } from '../../lib/web3/utils/address'; import { Token, + TokenPair, getDisplayDenomAmount, getTokenId, } from '../../lib/web3/utils/tokens'; @@ -31,6 +32,7 @@ import { } from '../../lib/web3/utils/events'; import useTransactionTableData, { Tx } from './hooks/useTransactionTableData'; +import { useUserHasDeposits } from '../../lib/web3/hooks/useUserDeposits'; import { useSimplePrice } from '../../lib/tokenPrices'; import { formatAmount, formatCurrency } from '../../lib/utils/number'; import { formatRelativeTime } from '../../lib/utils/time'; @@ -40,10 +42,6 @@ import { useTokenPathPart, useTokenValue, } from '../../lib/web3/hooks/useTokens'; -import { - usePoolDepositFilterForPair, - useUserDeposits, -} from '../../lib/web3/hooks/useUserShares'; import StatCardVolume from '../../components/stats/StatCardVolume'; import StatCardFees from '../../components/stats/StatCardFees'; import StatCardVolatility from '../../components/stats/StatCardVolatility'; @@ -62,8 +60,11 @@ export default function PoolOverview({ setTokens([tokenB, tokenA]); }, [tokenA, tokenB, setTokens]); - const pairPoolDepositFilter = usePoolDepositFilterForPair([tokenA, tokenB]); - const userPairDeposits = useUserDeposits(pairPoolDepositFilter); + const tokenPair = useMemo( + () => [tokenA, tokenB], + [tokenA, tokenB] + ); + const { data: userHasDeposits } = useUserHasDeposits(tokenPair); const tokenAPath = useTokenPathPart(tokenA); const tokenBPath = useTokenPathPart(tokenB); @@ -82,7 +83,7 @@ export default function PoolOverview({
- {userPairDeposits && userPairDeposits.length > 0 && ( + {userHasDeposits && (