diff --git a/components/Loader/Loader.tsx b/components/Loader/Loader.tsx index c6abeaf7ae..88cfa57aac 100644 --- a/components/Loader/Loader.tsx +++ b/components/Loader/Loader.tsx @@ -30,4 +30,8 @@ export const MiniLoader = () => { return ; }; +export const ButtonLoader = () => { + return ; +}; + export default Loader; diff --git a/components/TVChart/TVChart.tsx b/components/TVChart/TVChart.tsx index eeffcc20b4..5e6de8fb1b 100644 --- a/components/TVChart/TVChart.tsx +++ b/components/TVChart/TVChart.tsx @@ -6,7 +6,7 @@ import { ThemeContext } from 'styled-components'; import { chain } from 'wagmi'; import Connector from 'containers/Connector'; -import { FuturesOrder } from 'queries/futures/types'; +import { FuturesOrder } from 'sdk/types/futures'; import { ChartBody } from 'sections/exchange/TradeCard/Charts/common/styles'; import { currentThemeState } from 'store/ui'; import darkTheme from 'styles/theme/colors/dark'; @@ -88,9 +88,9 @@ export function TVChart({ }; const renderOrderLines = () => { - clearOrderLines(); _widget.current?.onChartReady(() => { _widget.current?.chart().dataReady(() => { + clearOrderLines(); _oderLineRefs.current = openOrders.reduce((acc, order) => { if (order.targetPrice) { const color = diff --git a/constants/queryKeys.ts b/constants/queryKeys.ts index b3da648a3c..8d2f932afd 100644 --- a/constants/queryKeys.ts +++ b/constants/queryKeys.ts @@ -167,7 +167,6 @@ export const QUERY_KEYS = { networkId, currencyKey, ], - Markets: (networkId: NetworkId) => ['futures', 'marketsSummaries', networkId], Market: (networkId: NetworkId, currencyKey: string | null) => [ 'futures', currencyKey, @@ -215,18 +214,7 @@ export const QUERY_KEYS = { networkId, currencyKey, ], - FundingRates: ( - networkId: NetworkId, - periodLength: number, - marketAssets: FuturesMarketAsset[] - ) => ['futures', 'fundingRates', networkId, periodLength, marketAssets], TradingVolumeForAll: (networkId: NetworkId) => ['futures', 'tradingVolumeForAll', networkId], - AllPositionHistory: (networkId: NetworkId, walletAddress: string) => [ - 'futures', - 'allPositionHistory', - networkId, - walletAddress, - ], Position: (networkId: NetworkId, market: string | null, walletAddress: string) => [ 'futures', 'position', @@ -234,27 +222,6 @@ export const QUERY_KEYS = { market, walletAddress, ], - MarketsPositions: ( - networkId: NetworkId, - markets: string[] | [], - walletAddress: string, - crossMarginAddress: string - ) => ['futures', 'marketsPositions', networkId, markets, walletAddress, crossMarginAddress], - Portfolio: ( - networkId: NetworkId, - markets: string[] | [], - walletAddress: string | null, - crossMarginAddress: string | null, - freeMargin: number - ) => [ - 'futures', - 'positions', - networkId, - markets, - walletAddress, - crossMarginAddress, - freeMargin, - ], PositionHistory: (walletAddress: string | null, networkId: NetworkId) => [ 'futures', 'accountPositions', @@ -318,13 +285,6 @@ export const QUERY_KEYS = { currencyKey: string | null ) => ['futures', 'currentRoundId', networkId, walletAddress, currencyKey], OverviewStats: (networkId: NetworkId) => ['futures', 'overview-stats', networkId], - CrossMarginAccountOverview: (networkId: NetworkId, wallet: string, retryCount: number) => [ - 'futures', - 'cross-margin-account-overview', - networkId, - wallet, - retryCount, - ], CrossMarginSettings: (networkId: NetworkId, settingsAddress: string) => [ 'futures', 'cross-margin-settings', diff --git a/containers/Connector/Connector.tsx b/containers/Connector/Connector.tsx index c85edfdc92..d7132ed1b5 100644 --- a/containers/Connector/Connector.tsx +++ b/containers/Connector/Connector.tsx @@ -63,7 +63,7 @@ const useConnector = () => { setProviderReady(true); }); transactionNotifier = new BaseTN(provider); - }, [provider, dispatch, handleNetworkChange]); + }, [provider, handleNetworkChange]); useEffect(() => { handleNetworkChange(network.id as NetworkId); diff --git a/contexts/RefetchContext.tsx b/contexts/RefetchContext.tsx index a675a6d268..18a0680c83 100644 --- a/contexts/RefetchContext.tsx +++ b/contexts/RefetchContext.tsx @@ -1,20 +1,19 @@ -import React, { useEffect } from 'react'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import React from 'react'; +import { useRecoilValue } from 'recoil'; -import useGetAverageFundingRateForMarkets from 'queries/futures/useGetAverageFundingRateForMarkets'; -import useGetCrossMarginAccountOverview from 'queries/futures/useGetCrossMarginAccountOverview'; -import useGetCrossMarginSettings from 'queries/futures/useGetCrossMarginSettings'; -import useGetFuturesOpenOrders from 'queries/futures/useGetFuturesOpenOrders'; -import useGetFuturesPositionForMarket from 'queries/futures/useGetFuturesPositionForMarket'; -import useGetFuturesPositionForMarkets from 'queries/futures/useGetFuturesPositionForMarkets'; import useGetFuturesPositionHistory from 'queries/futures/useGetFuturesPositionHistory'; -import useGetFuturesVolumes from 'queries/futures/useGetFuturesVolumes'; import useQueryCrossMarginAccount from 'queries/futures/useQueryCrossMarginAccount'; import useLaggedDailyPrice from 'queries/rates/useLaggedDailyPrice'; -import useSynthBalances from 'queries/synths/useSynthBalances'; -import { Period } from 'sdk/constants/period'; -import { futuresAccountState, futuresAccountTypeState, positionState } from 'store/futures'; -import logError from 'utils/logError'; +import { fetchBalances } from 'state/balances/actions'; +import { + fetchCrossMarginBalanceInfo, + fetchFuturesPositionsForType, + fetchOpenOrders, +} from 'state/futures/actions'; +import { selectFuturesType } from 'state/futures/selectors'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; +import { futuresAccountState } from 'store/futures'; +import { refetchWithComparator } from 'utils/queries'; type RefetchType = | 'modify-position' @@ -24,10 +23,7 @@ type RefetchType = | 'account-margin-change' | 'cross-margin-account-change'; -type RefetchUntilType = - | 'wallet-balance-change' - | 'cross-margin-account-change' - | 'account-margin-change'; +type RefetchUntilType = 'cross-margin-account-change'; type RefetchContextType = { handleRefetch: (refetchType: RefetchType, timeout?: number) => void; @@ -40,60 +36,42 @@ const RefetchContext = React.createContext({ }); export const RefetchProvider: React.FC = ({ children }) => { - const selectedAccountType = useRecoilValue(futuresAccountTypeState); + const selectedAccountType = useAppSelector(selectFuturesType); const { crossMarginAddress } = useRecoilValue(futuresAccountState); - const setPosition = useSetRecoilState(positionState); + const dispatch = useAppDispatch(); - const synthsBalancesQuery = useSynthBalances(); - const openOrdersQuery = useGetFuturesOpenOrders(); - const positionQuery = useGetFuturesPositionForMarket(); - const crossMarginAccountOverview = useGetCrossMarginAccountOverview(); - const positionsQuery = useGetFuturesPositionForMarkets(); const positionHistoryQuery = useGetFuturesPositionHistory(); const queryCrossMarginAccount = useQueryCrossMarginAccount(); - useGetAverageFundingRateForMarkets(Period.ONE_HOUR); useLaggedDailyPrice(); - useGetFuturesVolumes({ refetchInterval: 60000 }); - useGetCrossMarginSettings(); - - useEffect(() => { - if (positionQuery.error) { - setPosition(null); - } - }, [positionQuery.error, setPosition]); const handleRefetch = (refetchType: RefetchType, timeout?: number) => { setTimeout(() => { switch (refetchType) { case 'modify-position': - openOrdersQuery.refetch(); - positionsQuery.refetch(); + dispatch(fetchOpenOrders()); positionHistoryQuery.refetch(); + dispatch(fetchFuturesPositionsForType()); if (selectedAccountType === 'cross_margin') { - crossMarginAccountOverview.refetch(); + dispatch(fetchCrossMarginBalanceInfo()); } break; case 'new-order': - positionsQuery.refetch(); - openOrdersQuery.refetch(); + dispatch(fetchFuturesPositionsForType()); + dispatch(fetchOpenOrders()); break; case 'close-position': - positionQuery.refetch(); - positionsQuery.refetch(); + dispatch(fetchFuturesPositionsForType()); positionHistoryQuery.refetch(); - openOrdersQuery.refetch(); + dispatch(fetchOpenOrders()); break; case 'margin-change': - positionQuery.refetch(); - positionsQuery.refetch(); + dispatch(fetchFuturesPositionsForType()); positionHistoryQuery.refetch(); - openOrdersQuery.refetch(); - synthsBalancesQuery.refetch(); + dispatch(fetchBalances()); break; case 'account-margin-change': - crossMarginAccountOverview.refetch(); - synthsBalancesQuery.refetch(); + dispatch(fetchBalances()); break; case 'cross-margin-account-change': queryCrossMarginAccount(); @@ -104,23 +82,6 @@ export const RefetchProvider: React.FC = ({ children }) => { const refetchUntilUpdate = async (refetchType: RefetchUntilType) => { switch (refetchType) { - case 'account-margin-change': - return Promise.all([ - refetchWithComparator( - crossMarginAccountOverview.refetch, - crossMarginAccountOverview, - (prev, next) => - !next.data || - prev?.data?.freeMargin?.toString() === next?.data?.freeMargin?.toString() - ), - refetchWithComparator( - synthsBalancesQuery.refetch, - synthsBalancesQuery, - (prev, next) => - !next.data || - prev?.data.susdWalletBalance?.toString() === next?.data.susdWalletBalance?.toString() - ), - ]); case 'cross-margin-account-change': return refetchWithComparator( queryCrossMarginAccount, @@ -137,41 +98,6 @@ export const RefetchProvider: React.FC = ({ children }) => { ); }; -// Takes a comparitor which should return a bool condition to -// signal to continue retrying, comparing prev and new query result - -const refetchWithComparator = async ( - query: () => Promise, - existingResult: any, - comparator: (previous: any, current: any) => boolean, - interval = 1000, - max = 25 -) => { - return new Promise((res) => { - let count = 1; - - const refetch = async (existingResult: any) => { - const timeout = setTimeout(async () => { - if (count > max) { - clearTimeout(timeout); - logError('refetch timeout'); - res({ data: null, status: 'timeout' }); - } else { - const next = await query(); - count += 1; - if (!comparator(existingResult, next)) { - clearTimeout(timeout); - res({ data: next, status: 'complete' }); - } else { - refetch(next); - } - } - }, interval); - }; - refetch(existingResult); - }); -}; - export const useRefetchContext = () => { return React.useContext(RefetchContext); }; diff --git a/hooks/useAverageEntryPrice.ts b/hooks/useAverageEntryPrice.ts index c8bb2563bd..d1416eb376 100644 --- a/hooks/useAverageEntryPrice.ts +++ b/hooks/useAverageEntryPrice.ts @@ -1,12 +1,12 @@ import { useMemo } from 'react'; -import { useRecoilValue } from 'recoil'; import { PositionHistory, PositionSide } from 'queries/futures/types'; -import { potentialTradeDetailsState } from 'store/futures'; +import { selectTradePreview } from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; // Used to calculate the new average entry price of a modified position const useAverageEntryPrice = (positionHistory?: PositionHistory) => { - const { data: previewTrade } = useRecoilValue(potentialTradeDetailsState); + const previewTrade = useAppSelector(selectTradePreview); return useMemo(() => { if (positionHistory && previewTrade) { diff --git a/hooks/useFuturesData.ts b/hooks/useFuturesData.ts index 31c49754d3..ef616cc08e 100644 --- a/hooks/useFuturesData.ts +++ b/hooks/useFuturesData.ts @@ -2,119 +2,101 @@ import useSynthetixQueries from '@synthetixio/queries'; import Wei, { wei } from '@synthetixio/wei'; import { ethers } from 'ethers'; import { formatBytes32String } from 'ethers/lib/utils'; -import { debounce } from 'lodash'; import { useRouter } from 'next/router'; import { useState, useEffect, useMemo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; +import { CROSS_MARGIN_ENABLED, DEFAULT_FUTURES_MARGIN_TYPE } from 'constants/defaults'; +import { CROSS_MARGIN_ORDER_TYPES, ISOLATED_MARGIN_ORDER_TYPES } from 'constants/futures'; +import Connector from 'containers/Connector'; +import { FuturesAccountType } from 'queries/futures/types'; +import { serializeGasPrice } from 'state/app/helpers'; +import { setGasPrice } from 'state/app/reducer'; +import { selectGasSpeed } from 'state/app/selectors'; +import { clearTradeInputs, editTradeSizeInput } from 'state/futures/actions'; +import { usePollMarketFuturesData } from 'state/futures/hooks'; import { - CROSS_MARGIN_ENABLED, - DEFAULT_FUTURES_MARGIN_TYPE, - DEFAULT_LEVERAGE, -} from 'constants/defaults'; + setDynamicFeeRate as setDynamicFeeRateRedux, + setFuturesAccountType, + setOrderType, +} from 'state/futures/reducer'; import { - CROSS_MARGIN_ORDER_TYPES, - ISOLATED_MARGIN_ORDER_TYPES, - ORDER_KEEPER_ETH_DEPOSIT, -} from 'constants/futures'; -import Connector from 'containers/Connector'; -import { useRefetchContext } from 'contexts/RefetchContext'; -import { monitorTransaction } from 'contexts/RelayerContext'; -import { KWENTA_TRACKING_CODE, ORDER_PREVIEW_ERRORS } from 'queries/futures/constants'; -import { PositionSide, FuturesTradeInputs, FuturesAccountType } from 'queries/futures/types'; -import useGetFuturesPotentialTradeDetails from 'queries/futures/useGetFuturesPotentialTradeDetails'; -import { setFuturesAccountType, setOrderType as setReduxOrderType } from 'state/futures/reducer'; -import { selectMarketAssetRate, selectMaxLeverage } from 'state/futures/selectors'; + selectCrossMarginBalanceInfo, + selectCrossMarginAccount, + selectMarketAssetRate, + selectPosition, + selectMaxLeverage, + selectAboveMaxLeverage, + selectCrossMarginSettings, + selectTradeSizeInputs, + selectCrossMarginOrderPrice, + selectCrossMarginSelectedLeverage, + selectFuturesType, + selectLeverageSide, + selectOrderType, + selectIsAdvancedOrder, + selectCrossMarginTradeFees, + selectDynamicFeeRate, + selectCrossMarginMarginDelta, +} from 'state/futures/selectors'; import { selectMarketAsset, selectMarketInfo } from 'state/futures/selectors'; import { useAppSelector, useAppDispatch } from 'state/hooks'; -import { - crossMarginMarginDeltaState, - tradeFeesState, - futuresAccountState, - leverageSideState, - orderTypeState, - positionState, - futuresTradeInputsState, - crossMarginSettingsState, - futuresAccountTypeState, - preferredLeverageState, - simulatedTradeState, - potentialTradeDetailsState, - futuresOrderPriceState, - orderFeeCapState, - isAdvancedOrderState, - aboveMaxLeverageState, - crossMarginAccountOverviewState, - dynamicFeeRateState, -} from 'store/futures'; +import { futuresAccountState, orderFeeCapState } from 'store/futures'; import { computeMarketFee } from 'utils/costCalculations'; -import { zeroBN, floorNumber, weiToString } from 'utils/formatters/number'; -import { calculateMarginDelta, getDisplayAsset, MarketKeyByAsset } from 'utils/futures'; +import { zeroBN } from 'utils/formatters/number'; +import { MarketKeyByAsset } from 'utils/futures'; import logError from 'utils/logError'; import useCrossMarginAccountContracts from './useCrossMarginContracts'; -import usePersistedRecoilState from './usePersistedRecoilState'; - -const ZERO_TRADE_INPUTS = { - nativeSize: '', - susdSize: '', - nativeSizeDelta: zeroBN, - susdSizeDelta: zeroBN, - leverage: '', -}; - -const ZERO_FEES = { - staticFee: zeroBN, - crossMarginFee: zeroBN, - dynamicFeeRate: zeroBN, - keeperEthDeposit: zeroBN, - limitStopOrderFee: zeroBN, - total: zeroBN, -}; const useFuturesData = () => { const router = useRouter(); - const { t } = useTranslation(); - const { defaultSynthetixjs: synthetixjs, network, provider } = Connector.useContainer(); - const { useSynthetixTxn } = useSynthetixQueries(); + const { defaultSynthetixjs: synthetixjs, network } = Connector.useContainer(); + const { crossMarginAvailable } = useRecoilValue(futuresAccountState); + usePollMarketFuturesData(); + const dispatch = useAppDispatch(); + const crossMarginAddress = useAppSelector(selectCrossMarginAccount); - const getPotentialTrade = useGetFuturesPotentialTradeDetails(); - const crossMarginAccountOverview = useRecoilValue(crossMarginAccountOverviewState); + const crossMarginBalanceInfo = useAppSelector(selectCrossMarginBalanceInfo); const { crossMarginAccountContract } = useCrossMarginAccountContracts(); - const { handleRefetch, refetchUntilUpdate } = useRefetchContext(); + + const gasSpeed = useAppSelector(selectGasSpeed); + + // TODO: Move to sdk and redux + const { useEthGasPriceQuery } = useSynthetixQueries(); + const ethGasPriceQuery = useEthGasPriceQuery(); + + useEffect(() => { + const price = ethGasPriceQuery.data?.[gasSpeed]; + if (price) { + dispatch(setGasPrice(serializeGasPrice(price))); + } + }, [ethGasPriceQuery.data, gasSpeed, dispatch]); const marketAsset = useAppSelector(selectMarketAsset); - const [tradeInputs, setTradeInputs] = useRecoilState(futuresTradeInputsState); - const setSimulatedTrade = useSetRecoilState(simulatedTradeState); - const [crossMarginMarginDelta, setCrossMarginMarginDelta] = useRecoilState( - crossMarginMarginDeltaState - ); - const [tradeFees, setTradeFees] = useRecoilState(tradeFeesState); - const [dynamicFeeRate, setDynamicFeeRate] = useRecoilState(dynamicFeeRateState); - const leverageSide = useRecoilValue(leverageSideState); - const [orderType, setOrderType] = useRecoilState(orderTypeState); + const crossMarginMarginDelta = useAppSelector(selectCrossMarginMarginDelta); + const tradeFees = useAppSelector(selectCrossMarginTradeFees); + const dynamicFeeRate = useAppSelector(selectDynamicFeeRate); + const leverageSide = useAppSelector(selectLeverageSide); + const orderType = useAppSelector(selectOrderType); const feeCap = useRecoilValue(orderFeeCapState); - const position = useRecoilValue(positionState); - const aboveMaxLeverage = useRecoilValue(aboveMaxLeverageState); + const position = useAppSelector(selectPosition); + const aboveMaxLeverage = useAppSelector(selectAboveMaxLeverage); const maxLeverage = useAppSelector(selectMaxLeverage); - const { crossMarginAvailable, crossMarginAddress } = useRecoilValue(futuresAccountState); - const { tradeFee: crossMarginTradeFee, stopOrderFee, limitOrderFee } = useRecoilValue( - crossMarginSettingsState + const tradeSizeInputs = useAppSelector(selectTradeSizeInputs); + const selectedLeverage = useAppSelector(selectCrossMarginSelectedLeverage); + const selectedAccountType = useAppSelector(selectFuturesType); + + const { tradeFee: crossMarginTradeFee, stopOrderFee, limitOrderFee } = useAppSelector( + selectCrossMarginSettings ); - const isAdvancedOrder = useRecoilValue(isAdvancedOrderState); + const isAdvancedOrder = useAppSelector(selectIsAdvancedOrder); const marketAssetRate = useAppSelector(selectMarketAssetRate); - const orderPrice = useRecoilValue(futuresOrderPriceState); - const setPotentialTradeDetails = useSetRecoilState(potentialTradeDetailsState); - const [selectedAccountType, setSelectedAccountType] = useRecoilState(futuresAccountTypeState); - const [preferredLeverage] = usePersistedRecoilState(preferredLeverageState); - const dispatch = useAppDispatch(); - + const orderPrice = useAppSelector(selectCrossMarginOrderPrice); const market = useAppSelector(selectMarketInfo); const [maxFee, setMaxFee] = useState(zeroBN); - const [error, setError] = useState(null); const tradePrice = useMemo(() => wei(isAdvancedOrder ? orderPrice || zeroBN : marketAssetRate), [ orderPrice, @@ -123,18 +105,13 @@ const useFuturesData = () => { ]); const crossMarginAccount = useMemo(() => { - return crossMarginAvailable ? { freeMargin: crossMarginAccountOverview.freeMargin } : null; - }, [crossMarginAccountOverview.freeMargin, crossMarginAvailable]); + return crossMarginAvailable ? { freeMargin: crossMarginBalanceInfo.freeMargin } : null; + }, [crossMarginBalanceInfo.freeMargin, crossMarginAvailable]); const freeMargin = useMemo(() => crossMarginAccount?.freeMargin ?? zeroBN, [ crossMarginAccount?.freeMargin, ]); - const selectedLeverage = useMemo(() => { - const leverage = preferredLeverage[marketAsset] || DEFAULT_LEVERAGE; - return String(Math.min(maxLeverage.toNumber(), Number(leverage))); - }, [preferredLeverage, marketAsset, maxLeverage]); - const remainingMargin: Wei = useMemo(() => { if (selectedAccountType === 'isolated_margin') { return position?.remainingMargin || zeroBN; @@ -144,21 +121,9 @@ const useFuturesData = () => { return positionMargin.add(accountMargin); }, [position?.remainingMargin, crossMarginAccount?.freeMargin, selectedAccountType]); - const clearTradePreview = useCallback(() => { - setPotentialTradeDetails({ - data: null, - status: 'idle', - error: null, - }); - setTradeFees(ZERO_FEES); - }, [setPotentialTradeDetails, setTradeFees]); - const resetTradeState = useCallback(() => { - setSimulatedTrade(ZERO_TRADE_INPUTS); - setTradeInputs(ZERO_TRADE_INPUTS); - setCrossMarginMarginDelta(zeroBN); - clearTradePreview(); - }, [setSimulatedTrade, setTradeInputs, clearTradePreview, setCrossMarginMarginDelta]); + dispatch(clearTradeInputs()); + }, [dispatch]); const maxUsdInputAmount = useMemo(() => { if (selectedAccountType === 'isolated_margin') { @@ -209,21 +174,6 @@ const useFuturesData = () => { } }, [orderType, limitOrderFee, stopOrderFee]); - const getCrossMarginEthBal = useCallback(async () => { - if (!crossMarginAddress) return zeroBN; - const bal = await provider.getBalance(crossMarginAddress); - return wei(bal); - }, [crossMarginAddress, provider]); - - const calculateCrossMarginFee = useCallback( - (susdSizeDelta: Wei) => { - if (orderType !== 'limit' && orderType !== 'stop market') return zeroBN; - const advancedOrderFeeRate = orderType === 'limit' ? limitOrderFee : stopOrderFee; - return susdSizeDelta.abs().mul(advancedOrderFeeRate); - }, - [orderType, stopOrderFee, limitOrderFee] - ); - const totalFeeRate = useCallback( async (sizeDelta: Wei) => { const staticRate = computeMarketFee(market, sizeDelta); @@ -235,226 +185,6 @@ const useFuturesData = () => { [market, crossMarginTradeFee, dynamicFeeRate, advancedOrderFeeRate] ); - const calculateFees = useCallback( - async (susdSizeDelta: Wei, nativeSizeDelta: Wei) => { - if (!synthetixjs) return ZERO_FEES; - - const susdSize = susdSizeDelta.abs(); - const staticRate = computeMarketFee(market, nativeSizeDelta); - const tradeFee = susdSize.mul(staticRate).add(susdSize.mul(dynamicFeeRate)); - - const currentDeposit = - orderType === 'limit' || orderType === 'stop market' - ? await getCrossMarginEthBal() - : zeroBN; - const requiredDeposit = currentDeposit.lt(ORDER_KEEPER_ETH_DEPOSIT) - ? ORDER_KEEPER_ETH_DEPOSIT.sub(currentDeposit) - : zeroBN; - - const crossMarginFee = - selectedAccountType === 'cross_margin' ? susdSize.mul(crossMarginTradeFee) : zeroBN; - const limitStopOrderFee = calculateCrossMarginFee(susdSizeDelta); - const tradeFeeWei = wei(tradeFee); - - const fees = { - staticFee: tradeFeeWei, - crossMarginFee: crossMarginFee, - dynamicFeeRate, - keeperEthDeposit: requiredDeposit, - limitStopOrderFee: limitStopOrderFee, - total: tradeFeeWei.add(crossMarginFee).add(limitStopOrderFee), - }; - setTradeFees(fees); - return fees; - }, - [ - synthetixjs, - market, - dynamicFeeRate, - orderType, - getCrossMarginEthBal, - selectedAccountType, - crossMarginTradeFee, - calculateCrossMarginFee, - setTradeFees, - ] - ); - - // eslint-disable-next-line - const debounceFetchPreview = useCallback( - debounce(async (nextTrade: FuturesTradeInputs, fromLeverage = false) => { - setError(null); - try { - const fees = await calculateFees(nextTrade.susdSizeDelta, nextTrade.nativeSizeDelta); - let nextMarginDelta = zeroBN; - if (selectedAccountType === 'cross_margin') { - nextMarginDelta = - nextTrade.nativeSizeDelta.abs().gt(0) || fromLeverage - ? await calculateMarginDelta(nextTrade, fees, position) - : zeroBN; - setCrossMarginMarginDelta(nextMarginDelta); - } - getPotentialTrade( - nextTrade.nativeSizeDelta, - nextMarginDelta, - Number(nextTrade.leverage), - nextTrade.orderPrice - ); - } catch (err) { - if (Object.values(ORDER_PREVIEW_ERRORS).includes(err.message)) { - setError(err.message); - } else { - setError(t('futures.market.trade.preview.error')); - } - clearTradePreview(); - logError(err); - } - }, 500), - [ - setError, - calculateFees, - getPotentialTrade, - calculateMarginDelta, - position, - orderPrice, - orderType, - selectedAccountType, - logError, - setCrossMarginMarginDelta, - ] - ); - - const onStagePositionChange = useCallback( - (trade: FuturesTradeInputs) => { - setTradeInputs(trade); - setSimulatedTrade(null); - debounceFetchPreview(trade); - }, - [setTradeInputs, setSimulatedTrade, debounceFetchPreview] - ); - - const onTradeAmountChange = useCallback( - ( - value: string, - usdPrice: Wei, - currencyType: 'usd' | 'native', - options?: { simulateChange?: boolean; crossMarginLeverage?: Wei } - ) => { - if (!value || usdPrice.eq(0)) { - resetTradeState(); - return; - } - const positiveTrade = leverageSide === PositionSide.LONG; - const nativeSize = currencyType === 'native' ? wei(value) : wei(value).div(usdPrice); - const usdSize = currencyType === 'native' ? usdPrice.mul(value) : wei(value); - const changeEnabled = remainingMargin.gt(0) && value !== ''; - const isolatedMarginLeverage = changeEnabled ? usdSize.div(remainingMargin) : zeroBN; - - const inputLeverage = - selectedAccountType === 'cross_margin' - ? options?.crossMarginLeverage ?? wei(selectedLeverage) - : isolatedMarginLeverage; - let leverage = remainingMargin.eq(0) ? zeroBN : inputLeverage; - leverage = maxLeverage.gt(leverage) ? leverage : maxLeverage; - - const newTradeInputs = { - nativeSize: changeEnabled ? weiToString(nativeSize) : '', - susdSize: changeEnabled ? weiToString(usdSize) : '', - nativeSizeDelta: positiveTrade ? nativeSize : nativeSize.neg(), - susdSizeDelta: positiveTrade ? usdSize : usdSize.neg(), - orderPrice: usdPrice, - leverage: String(floorNumber(leverage)), - }; - - if (options?.simulateChange) { - // Allows us to keep it snappy updating the input values - setSimulatedTrade(newTradeInputs); - } else { - onStagePositionChange(newTradeInputs); - } - }, - [ - remainingMargin, - maxLeverage, - selectedLeverage, - selectedAccountType, - leverageSide, - resetTradeState, - setSimulatedTrade, - onStagePositionChange, - ] - ); - - const onChangeOpenPosLeverage = useCallback( - async (leverage: number) => { - debounceFetchPreview( - { - leverage: String(leverage), - nativeSize: '0', - susdSize: '0', - susdSizeDelta: zeroBN, - nativeSizeDelta: zeroBN, - }, - true - ); - }, - [debounceFetchPreview] - ); - - const onLeverageChange = useCallback( - (leverage: number) => { - if (selectedAccountType === 'cross_margin') { - onTradeAmountChange('', tradePrice, 'usd', { - crossMarginLeverage: wei(leverage), - }); - } else { - if (leverage <= 0) { - resetTradeState(); - } else { - const newTradeSize = - marketAssetRate.eq(0) || remainingMargin.eq(0) - ? '' - : wei(leverage).mul(remainingMargin).div(marketAssetRate).toString(); - onTradeAmountChange(newTradeSize, tradePrice, 'native'); - } - } - }, - [ - remainingMargin, - marketAssetRate, - selectedAccountType, - tradePrice, - resetTradeState, - onTradeAmountChange, - ] - ); - - const onTradeOrderPriceChange = useCallback( - (price: string) => { - if (price && tradeInputs.susdSize) { - // Recalc the trade - onTradeAmountChange(tradeInputs.susdSize, wei(price), 'usd'); - } - }, - [tradeInputs, onTradeAmountChange] - ); - - const orderTxn = useSynthetixTxn( - `FuturesMarket${getDisplayAsset(marketAsset)}`, - orderType === 'next price' ? 'submitNextPriceOrderWithTracking' : 'modifyPositionWithTracking', - [tradeInputs.nativeSizeDelta.toBN(), KWENTA_TRACKING_CODE], - {}, - { - enabled: - selectedAccountType === 'isolated_margin' && - !!marketAsset && - !!tradeInputs.leverage && - Number(tradeInputs.leverage) >= 0 && - maxLeverage.gte(tradeInputs.leverage) && - !tradeInputs.nativeSizeDelta.eq(zeroBN), - } - ); - const submitCrossMarginOrder = useCallback( async (fromEditLeverage?: boolean, gasLimit?: Wei | null) => { if (!crossMarginAccountContract) return; @@ -463,7 +193,7 @@ const useFuturesData = () => { { marketKey: formatBytes32String(MarketKeyByAsset[marketAsset]), marginDelta: crossMarginMarginDelta.toBN(), - sizeDelta: tradeInputs.nativeSizeDelta.toBN(), + sizeDelta: tradeSizeInputs.nativeSizeDelta.toBN(), }, ]; return await crossMarginAccountContract.distributeMargin(newPosition, { @@ -475,7 +205,7 @@ const useFuturesData = () => { return await crossMarginAccountContract.placeOrderWithFeeCap( formatBytes32String(MarketKeyByAsset[marketAsset]), crossMarginMarginDelta.toBN(), - tradeInputs.nativeSizeDelta.toBN(), + tradeSizeInputs.nativeSizeDelta.toBN(), wei(orderPrice).toBN(), enumType, feeCap.toBN(), @@ -489,29 +219,11 @@ const useFuturesData = () => { orderType, feeCap, crossMarginMarginDelta, - tradeInputs.nativeSizeDelta, + tradeSizeInputs.nativeSizeDelta, tradeFees.keeperEthDeposit, ] ); - const submitIsolatedMarginOrder = useCallback(() => { - orderTxn.mutate(); - }, [orderTxn]); - - useEffect(() => { - if (orderTxn.hash) { - monitorTransaction({ - txHash: orderTxn.hash, - onTxConfirmed: () => { - resetTradeState(); - handleRefetch('modify-position'); - refetchUntilUpdate('account-margin-change'); - }, - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [orderTxn.hash]); - useEffect(() => { const getMaxFee = async () => { if (remainingMargin.eq(0) || tradePrice.eq(0)) { @@ -550,16 +262,14 @@ const useFuturesData = () => { useEffect(() => { if (selectedAccountType === 'cross_margin' && !CROSS_MARGIN_ORDER_TYPES.includes(orderType)) { - setOrderType('market'); - dispatch(setReduxOrderType('market')); + dispatch(setOrderType('market')); } else if ( selectedAccountType === 'isolated_margin' && !ISOLATED_MARGIN_ORDER_TYPES.includes(orderType) ) { - setOrderType('market'); - dispatch(setReduxOrderType('market')); + dispatch(setOrderType('market')); } - onTradeAmountChange(tradeInputs.susdSize, tradePrice, 'usd'); + editTradeSizeInput(tradeSizeInputs.susdSizeString, 'usd'); // eslint-disable-next-line react-hooks/exhaustive-deps }, [dispatch, selectedAccountType, orderType, network.id]); @@ -575,8 +285,8 @@ const useFuturesData = () => { }, [router.events, resetTradeState]); useEffect(() => { - if (tradeInputs.susdSizeDelta.eq(0)) return; - onTradeAmountChange(tradeInputs.susdSize, tradePrice, 'usd'); + if (tradeSizeInputs.susdSizeDelta.eq(0)) return; + editTradeSizeInput(tradeSizeInputs.susdSizeString, 'usd'); // Only want to react to leverage side change // eslint-disable-next-line react-hooks/exhaustive-deps }, [leverageSide]); @@ -589,7 +299,6 @@ const useFuturesData = () => { useEffect(() => { if (!CROSS_MARGIN_ENABLED) { - setSelectedAccountType(DEFAULT_FUTURES_MARGIN_TYPE); dispatch(setFuturesAccountType(DEFAULT_FUTURES_MARGIN_TYPE)); return; } @@ -600,7 +309,6 @@ const useFuturesData = () => { const accountType = ['cross_margin', 'isolated_margin'].includes(routerType) ? routerType : DEFAULT_FUTURES_MARGIN_TYPE; - setSelectedAccountType(accountType); dispatch(setFuturesAccountType(accountType)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [dispatch, router.query.accountType]); @@ -608,32 +316,25 @@ const useFuturesData = () => { useEffect(() => { const getDynamicFee = async () => { if (!synthetixjs) return; + // TODO: Move to sdk const dynamicFeeRate = await synthetixjs.contracts.Exchanger.dynamicFeeRateForExchange( ethers.utils.formatBytes32String('sUSD'), ethers.utils.formatBytes32String(marketAsset) ); - setDynamicFeeRate(wei(dynamicFeeRate.feeRate)); + dispatch(setDynamicFeeRateRedux(wei(dynamicFeeRate.feeRate).toString())); }; getDynamicFee(); - }, [marketAsset, setDynamicFeeRate, synthetixjs]); + }, [marketAsset, synthetixjs, dispatch]); return { - onLeverageChange, - onTradeAmountChange, - submitIsolatedMarginOrder, submitCrossMarginOrder, resetTradeState, - onTradeOrderPriceChange, - onChangeOpenPosLeverage, marketAssetRate, position, market, - orderTxn, maxUsdInputAmount, tradeFees, selectedLeverage, - error, - debounceFetchPreview, tradePrice, }; }; diff --git a/hooks/useMonitorTransactions.ts b/hooks/useMonitorTransactions.ts new file mode 100644 index 0000000000..aad6f4bc53 --- /dev/null +++ b/hooks/useMonitorTransactions.ts @@ -0,0 +1,27 @@ +import { useEffect } from 'react'; +import { useDispatch } from 'react-redux'; + +import { monitorTransaction } from 'contexts/RelayerContext'; +import { TransactionStatus } from 'sdk/types/common'; +import { handleTransactionError, updateTransactionStatus } from 'state/futures/reducer'; +import { selectFuturesTransaction } from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; + +export default function useMonitorTransactions() { + const dispatch = useDispatch(); + const transaction = useAppSelector(selectFuturesTransaction); + + useEffect(() => { + if (transaction?.hash) { + monitorTransaction({ + txHash: transaction.hash, + onTxFailed: (err) => { + dispatch(handleTransactionError(err.failureReason ?? 'transaction_failed')); + }, + onTxConfirmed: () => { + dispatch(updateTransactionStatus(TransactionStatus.Confirmed)); + }, + }); + } + }, [transaction?.hash, dispatch]); +} diff --git a/pages/_app.tsx b/pages/_app.tsx index 9d1f466e52..01d66fc0c4 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -17,6 +17,7 @@ import { chain, WagmiConfig } from 'wagmi'; import Connector from 'containers/Connector'; import { chains, wagmiClient } from 'containers/Connector/config'; +import useMonitorTransactions from 'hooks/useMonitorTransactions'; import AcknowledgementModal from 'sections/app/AcknowledgementModal'; import Layout from 'sections/shared/Layout'; import SystemStatus from 'sections/shared/SystemStatus'; @@ -52,6 +53,7 @@ const InnerApp: FC = ({ Component, pageProps }: AppPropsWithLayout) => } = Connector.useContainer(); useAppData(providerReady); + useMonitorTransactions(); const getLayout = Component.getLayout || ((page) => page); diff --git a/pages/dashboard/index.tsx b/pages/dashboard/index.tsx index 6825a7f53e..f980e01361 100644 --- a/pages/dashboard/index.tsx +++ b/pages/dashboard/index.tsx @@ -4,14 +4,13 @@ import { useTranslation } from 'react-i18next'; import DashboardLayout from 'sections/dashboard/DashboardLayout'; import Overview from 'sections/dashboard/Overview'; import GitHashID from 'sections/shared/Layout/AppLayout/GitHashID'; -import { fetchMarkets } from 'state/futures/actions'; -import { usePollAction } from 'state/hooks'; +import { usePollDashboardFuturesData } from 'state/futures/hooks'; type DashboardComponent = React.FC & { getLayout: (page: HTMLElement) => JSX.Element }; const Dashboard: DashboardComponent = () => { const { t } = useTranslation(); - usePollAction(fetchMarkets); + usePollDashboardFuturesData(); return ( <> diff --git a/pages/dashboard/markets.tsx b/pages/dashboard/markets.tsx index ec44dcba74..8b77328469 100644 --- a/pages/dashboard/markets.tsx +++ b/pages/dashboard/markets.tsx @@ -1,6 +1,7 @@ import Head from 'next/head'; import { useTranslation } from 'react-i18next'; +import Connector from 'containers/Connector'; import DashboardLayout from 'sections/dashboard/DashboardLayout'; import Markets from 'sections/dashboard/Markets'; import GitHashID from 'sections/shared/Layout/AppLayout/GitHashID'; @@ -11,7 +12,8 @@ type MarketsProps = React.FC & { getLayout: (page: HTMLElement) => JSX.Element } const MarketsPage: MarketsProps = () => { const { t } = useTranslation(); - usePollAction(fetchMarkets); + const { network } = Connector.useContainer(); + usePollAction('fetchMarkets', fetchMarkets, { dependencies: [network.id] }); return ( <> diff --git a/pages/market.tsx b/pages/market.tsx index 60611de8d1..4c04fff15a 100644 --- a/pages/market.tsx +++ b/pages/market.tsx @@ -23,15 +23,10 @@ import TradeIsolatedMargin from 'sections/futures/Trade/TradeIsolatedMargin'; import TradeCrossMargin from 'sections/futures/TradeCrossMargin'; import AppLayout from 'sections/shared/Layout/AppLayout'; import GitHashID from 'sections/shared/Layout/AppLayout/GitHashID'; -import { fetchMarkets } from 'state/futures/actions'; import { setMarketAsset } from 'state/futures/reducer'; -import { selectMarketAsset } from 'state/futures/selectors'; -import { useAppDispatch, useAppSelector, usePollAction } from 'state/hooks'; -import { - futuresAccountState, - futuresAccountTypeState, - showCrossMarginOnboardState, -} from 'store/futures'; +import { selectFuturesType, selectMarketAsset } from 'state/futures/selectors'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; +import { futuresAccountState, showCrossMarginOnboardState } from 'store/futures'; import { PageContent, FullHeightContainer, RightSideContent } from 'styles/common'; import { FuturesMarketAsset, MarketKeyByAsset } from 'utils/futures'; @@ -43,7 +38,6 @@ const Market: MarketComponent = () => { const { walletAddress } = Connector.useContainer(); const futuresData = useFuturesData(); const dispatch = useAppDispatch(); - usePollAction(fetchMarkets); const routerMarketAsset = router.query.asset as FuturesMarketAsset; @@ -93,7 +87,7 @@ function TradePanelDesktop({ walletAddress, account }: TradePanelProps) { const { handleRefetch } = useRefetchContext(); const router = useRouter(); const isL2 = useIsL2(); - const accountType = useRecoilValue(futuresAccountTypeState); + const accountType = useAppSelector(selectFuturesType); if (walletAddress && !isL2) { return ; diff --git a/queries/futures/types.ts b/queries/futures/types.ts index c766d5d40e..463ab41801 100644 --- a/queries/futures/types.ts +++ b/queries/futures/types.ts @@ -2,54 +2,8 @@ import { Balances } from '@synthetixio/queries'; import Wei from '@synthetixio/wei'; import { BigNumber } from 'ethers'; -import { PotentialTradeStatus } from 'sections/futures/types'; -import { FuturesMarketAsset, FuturesMarketKey } from 'utils/futures'; - -export type PositionDetail = { - remainingMargin: Wei; - accessibleMargin: Wei; - orderPending: boolean; - order: { - pending: boolean; - fee: Wei; - leverage: Wei; - }; - position: { - fundingIndex: Wei; - lastPrice: Wei; - size: Wei; - margin: Wei; - }; - accruedFunding: Wei; - notionalValue: Wei; - liquidationPrice: Wei; - profitLoss: Wei; -}; - -export type FuturesFilledPosition = { - canLiquidatePosition: boolean; - side: PositionSide; - notionalValue: T; - accruedFunding: T; - initialMargin: T; - profitLoss: T; - fundingIndex: number; - lastPrice: T; - size: T; - liquidationPrice: T; - initialLeverage: T; - leverage: T; - pnl: T; - pnlPct: T; - marginRatio: T; -}; - -export type FuturesPosition = { - asset: FuturesMarketAsset; - remainingMargin: T; - accessibleMargin: T; - position: FuturesFilledPosition | null; -}; +import { FuturesOrderTypeDisplay, FuturesPotentialTradeDetails } from 'sdk/types/futures'; +import { FuturesMarketAsset } from 'utils/futures'; export type FuturesOpenInterest = { asset: string; @@ -135,14 +89,6 @@ export type FuturesTradeWithPrice = { price: string; }; -// This type exists to rename enum types from the subgraph to display-friendly types -export type FuturesOrderTypeDisplay = - | 'Next Price' - | 'Limit' - | 'Stop Market' - | 'Market' - | 'Liquidation'; - export type FuturesTrade = { size: Wei; asset: string; @@ -159,33 +105,6 @@ export type FuturesTrade = { accountType: FuturesAccountType; }; -export type FuturesOrder = { - id: string; - account: string; - asset: FuturesMarketAsset; - market: string; - marketKey: FuturesMarketKey; - size: Wei; - targetPrice: Wei | null; - marginDelta: Wei; - targetRoundId: Wei | null; - timestamp: Wei; - orderType: FuturesOrderTypeDisplay; - sizeTxt?: string; - targetPriceTxt?: string; - side?: PositionSide; - isStale?: boolean; - isExecutable?: boolean; - isCancelling?: boolean; -}; - -export type FuturesVolumes = { - [asset: string]: { - volume: Wei; - trades: Wei; - }; -}; - export type FuturesStat = { account: string; pnlWithFeesPaid: Wei; @@ -224,22 +143,6 @@ export type FundingRates = { [key in FuturesMarketAsset]: Wei; }; -export type FuturesPotentialTradeDetails = { - size: Wei; - sizeDelta: Wei; - liqPrice: Wei; - margin: Wei; - price: Wei; - fee: Wei; - leverage: Wei; - notionalValue: Wei; - minInitialMargin: Wei; - side: PositionSide; - status: PotentialTradeStatus; - showStatus: boolean; - statusMessage: string; -}; - export type FuturesPotentialTradeDetailsQuery = { data: FuturesPotentialTradeDetails | null; error: string | null; @@ -257,7 +160,6 @@ type CrossMarginAccount = string; type FactoryAddress = string; export type CrossMarginAccounts = Record>; -export type FuturesPositionsState = Record; export type PositionHistoryState = Record; export type Portfolio = { total: Wei; diff --git a/queries/futures/useGetAverageFundingRateForMarket.ts b/queries/futures/useGetAverageFundingRateForMarket.ts deleted file mode 100644 index 4859802fac..0000000000 --- a/queries/futures/useGetAverageFundingRateForMarket.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { NetworkId } from '@synthetixio/contracts-interface'; -import Wei, { wei } from '@synthetixio/wei'; -import request, { gql } from 'graphql-request'; -import { useQuery, UseQueryOptions } from 'react-query'; -import { useNetwork } from 'wagmi'; - -import QUERY_KEYS from 'constants/queryKeys'; -import useIsL2 from 'hooks/useIsL2'; -import { selectMarketInfo, selectMarketKey } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; -import logError from 'utils/logError'; - -import { FundingRateUpdate } from './types'; -import { getFuturesEndpoint, calculateFundingRate } from './utils'; - -const useGetAverageFundingRateForMarket = ( - periodLength: number, - options?: UseQueryOptions -) => { - const { chain: network } = useNetwork(); - const isL2 = useIsL2(); - const marketKey = useAppSelector(selectMarketKey); - const marketInfo = useAppSelector(selectMarketInfo); - const futuresEndpoint = getFuturesEndpoint(network?.id as NetworkId); - - const price = marketInfo?.price; - const currentFundingRate = marketInfo?.currentFundingRate; - const marketAddress = marketInfo?.market; - - return useQuery( - QUERY_KEYS.Futures.FundingRate(network?.id as NetworkId, marketKey || ''), - async () => { - if (!marketKey || !price || !marketInfo) return null; - const minTimestamp = Math.floor(Date.now() / 1000) - periodLength; - try { - const response: { string: FundingRateUpdate[] } = await request( - futuresEndpoint, - gql` - query fundingRateUpdates($market: String!, $minTimestamp: BigInt!) { - # last before timestamp - first: fundingRateUpdates( - first: 1 - where: { market: $market, timestamp_lt: $minTimestamp } - orderBy: sequenceLength - orderDirection: desc - ) { - timestamp - funding - } - - # first after timestamp - next: fundingRateUpdates( - first: 1 - where: { market: $market, timestamp_gt: $minTimestamp } - orderBy: sequenceLength - orderDirection: asc - ) { - timestamp - funding - } - - # latest update - latest: fundingRateUpdates( - first: 1 - where: { market: $market } - orderBy: sequenceLength - orderDirection: desc - ) { - timestamp - funding - } - } - `, - { market: marketAddress, minTimestamp: minTimestamp } - ); - const responseFilt = Object.values(response) - .filter((value: FundingRateUpdate[]) => value.length > 0) - .map((entry: FundingRateUpdate[]): FundingRateUpdate => entry[0]) - .sort((a: FundingRateUpdate, b: FundingRateUpdate) => a.timestamp - b.timestamp); - - return responseFilt && !!currentFundingRate - ? calculateFundingRate( - minTimestamp, - periodLength, - responseFilt, - price, - currentFundingRate - ) - : wei(0); - } catch (e) { - logError(e); - return null; - } - }, - { - enabled: isL2 && !!marketInfo && !!currentFundingRate, - ...options, - } - ); -}; - -export default useGetAverageFundingRateForMarket; diff --git a/queries/futures/useGetAverageFundingRateForMarkets.ts b/queries/futures/useGetAverageFundingRateForMarkets.ts deleted file mode 100644 index d7667b906e..0000000000 --- a/queries/futures/useGetAverageFundingRateForMarkets.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { NetworkId } from '@synthetixio/contracts-interface'; -import Wei from '@synthetixio/wei'; -import request, { gql } from 'graphql-request'; -import { useTranslation } from 'react-i18next'; -import { useQuery, UseQueryOptions } from 'react-query'; -import { useSetRecoilState } from 'recoil'; - -import QUERY_KEYS from 'constants/queryKeys'; -import Connector from 'containers/Connector'; -import { Period, PERIOD_IN_SECONDS } from 'sdk/constants/period'; -import { selectMarketAssets, selectMarkets } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; -import { fundingRatesState } from 'store/futures'; -import { FuturesMarketKey, MarketKeyByAsset } from 'utils/futures'; -import logError from 'utils/logError'; - -import { FundingRateUpdate } from './types'; -import { getFuturesEndpoint, calculateFundingRate } from './utils'; - -type FundingRateInput = { - marketAddress: string | undefined; - marketKey: FuturesMarketKey; - price: Wei | undefined; - currentFundingRate: Wei | undefined; -}; - -export type FundingRateResponse = { - asset: FuturesMarketKey; - fundingTitle: string; - fundingRate: Wei | null; -}; - -const useGetAverageFundingRateForMarkets = ( - period: Period, - options?: UseQueryOptions -) => { - const { t } = useTranslation(); - const { network } = Connector.useContainer(); - - const futuresMarkets = useAppSelector(selectMarkets); - const marketAssets = useAppSelector(selectMarketAssets); - const futuresEndpoint = getFuturesEndpoint(network?.id as NetworkId); - const setFundingRates = useSetRecoilState(fundingRatesState); - - const fundingRateInputs: FundingRateInput[] = futuresMarkets.map( - ({ asset, market, price, currentFundingRate }) => { - return { - marketAddress: market, - marketKey: MarketKeyByAsset[asset], - price: price, - currentFundingRate: currentFundingRate, - }; - } - ); - - const periodLength = PERIOD_IN_SECONDS[period]; - - const periodTitle = - period === Period.ONE_HOUR - ? t('futures.market.info.hourly-funding') - : t('futures.market.info.fallback-funding'); - - return useQuery( - QUERY_KEYS.Futures.FundingRates(network?.id as NetworkId, periodLength, marketAssets), - async () => { - const minTimestamp = Math.floor(Date.now() / 1000) - periodLength; - - const fundingRateQueries = fundingRateInputs.map(({ marketAddress, marketKey }) => { - return gql` - # last before timestamp - ${marketKey}_first: fundingRateUpdates( - first: 1 - where: { market: "${marketAddress}", timestamp_lt: $minTimestamp } - orderBy: sequenceLength - orderDirection: desc - ) { - timestamp - funding - } - - # first after timestamp - ${marketKey}_next: fundingRateUpdates( - first: 1 - where: { market: "${marketAddress}", timestamp_gt: $minTimestamp } - orderBy: sequenceLength - orderDirection: asc - ) { - timestamp - funding - } - - # latest update - ${marketKey}_latest: fundingRateUpdates( - first: 1 - where: { market: "${marketAddress}" } - orderBy: sequenceLength - orderDirection: desc - ) { - timestamp - funding - } - `; - }); - - try { - const marketFundingResponses: Record = await request( - futuresEndpoint, - gql` - query fundingRateUpdates($minTimestamp: BigInt!) { - ${fundingRateQueries.reduce((acc: string, curr: string) => { - return acc + curr; - })} - } - `, - { minTimestamp: minTimestamp } - ); - - const fundingRateResponses = fundingRateInputs.map( - ({ marketKey, currentFundingRate, price }) => { - if (!price) return null; - const marketResponses = [ - marketFundingResponses[`${marketKey}_first`], - marketFundingResponses[`${marketKey}_next`], - marketFundingResponses[`${marketKey}_latest`], - ]; - - const responseFilt = marketResponses - .filter((value: FundingRateUpdate[]) => value.length > 0) - .map((entry: FundingRateUpdate[]): FundingRateUpdate => entry[0]) - .sort((a: FundingRateUpdate, b: FundingRateUpdate) => a.timestamp - b.timestamp); - - const fundingRate = - responseFilt && !!currentFundingRate - ? calculateFundingRate( - minTimestamp, - periodLength, - responseFilt, - price, - currentFundingRate - ) - : currentFundingRate ?? null; - - const fundingPeriod = - responseFilt && !!currentFundingRate - ? periodTitle - : t('futures.markets.info.instant-funding'); - - const fundingRateResponse: FundingRateResponse = { - asset: marketKey, - fundingTitle: fundingPeriod, - fundingRate: fundingRate, - }; - return fundingRateResponse; - } - ); - - const fundingRates: FundingRateResponse[] = fundingRateResponses.filter( - (funding): funding is FundingRateResponse => !!funding - ); - - setFundingRates(fundingRates); - } catch (e) { - logError(e); - return null; - } - }, - { - enabled: futuresMarkets.length > 0 && futuresMarkets.length === marketAssets.length, - ...options, - } - ); -}; - -export default useGetAverageFundingRateForMarkets; diff --git a/queries/futures/useGetCrossMarginAccountOverview.ts b/queries/futures/useGetCrossMarginAccountOverview.ts deleted file mode 100644 index 9132a16521..0000000000 --- a/queries/futures/useGetCrossMarginAccountOverview.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { NetworkId } from '@synthetixio/contracts-interface'; -import { wei } from '@synthetixio/wei'; -import { useState } from 'react'; -import { useQuery } from 'react-query'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; - -import QUERY_KEYS from 'constants/queryKeys'; -import Connector from 'containers/Connector'; -import useCrossMarginAccountContracts from 'hooks/useCrossMarginContracts'; -import useSUSDContract from 'hooks/useSUSDContract'; -import { setCrossMarginAccountOverview } from 'state/futures/reducer'; -import { useAppDispatch } from 'state/hooks'; -import { crossMarginAccountOverviewState, futuresAccountState } from 'store/futures'; -import { zeroBN } from 'utils/formatters/number'; -import logError from 'utils/logError'; - -export default function useGetCrossMarginAccountOverview() { - const { walletAddress, network, provider } = Connector.useContainer(); - const { crossMarginAddress } = useRecoilValue(futuresAccountState); - const setAccountOverview = useSetRecoilState(crossMarginAccountOverviewState); - - const { crossMarginAccountContract } = useCrossMarginAccountContracts(); - const [retryCount, setRetryCount] = useState(0); - const susdContract = useSUSDContract(); - const dispatch = useAppDispatch(); - - return useQuery( - QUERY_KEYS.Futures.CrossMarginAccountOverview( - network.id as NetworkId, - crossMarginAddress || '', - retryCount - ), - async () => { - if (!crossMarginAddress || !crossMarginAccountContract || !susdContract || !walletAddress) { - const overview = { - freeMargin: zeroBN, - keeperEthBal: zeroBN, - allowance: zeroBN, - }; - setAccountOverview(overview); - dispatch( - setCrossMarginAccountOverview({ freeMargin: '0', keeperEthBal: '0', allowance: '0' }) - ); - return overview; - } - - try { - const [freeMargin, keeperEthBal, allowance] = await Promise.all([ - crossMarginAccountContract.freeMargin(), - provider.getBalance(crossMarginAddress), - susdContract.allowance(walletAddress, crossMarginAccountContract.address), - ]); - - const overview = { - freeMargin: wei(freeMargin), - keeperEthBal: wei(keeperEthBal), - allowance: wei(allowance), - }; - setRetryCount(0); - setAccountOverview(overview); - dispatch( - setCrossMarginAccountOverview({ - freeMargin: overview.freeMargin.toString(), - keeperEthBal: overview.keeperEthBal.toString(), - allowance: overview.allowance.toString(), - }) - ); - - return overview; - } catch (err) { - // This a hacky workaround to deal with the delayed Metamask error - // which causes the logs query to fail on network switching - // https://github.com/MetaMask/metamask-extension/issues/13375#issuecomment-1046125113 - if (retryCount < 2) { - setRetryCount(retryCount + 1); - } - logError(err); - } - } - ); -} diff --git a/queries/futures/useGetCrossMarginSettings.ts b/queries/futures/useGetCrossMarginSettings.ts deleted file mode 100644 index eda4d172dd..0000000000 --- a/queries/futures/useGetCrossMarginSettings.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { NetworkId } from '@synthetixio/contracts-interface'; -import { wei } from '@synthetixio/wei'; -import { useQuery } from 'react-query'; -import { useSetRecoilState } from 'recoil'; - -import QUERY_KEYS from 'constants/queryKeys'; -import Connector from 'containers/Connector'; -import useCrossMarginAccountContracts from 'hooks/useCrossMarginContracts'; -import { crossMarginSettingsState } from 'store/futures'; -import { zeroBN } from 'utils/formatters/number'; -import logError from 'utils/logError'; - -const BPS_CONVERSION = 10000; - -export default function useGetCrossMarginSettings() { - const { network } = Connector.useContainer(); - const setCrossMarginSettings = useSetRecoilState(crossMarginSettingsState); - - const { crossMarginBaseSettings } = useCrossMarginAccountContracts(); - - return useQuery( - QUERY_KEYS.Futures.CrossMarginSettings( - network.id as NetworkId, - crossMarginBaseSettings?.address ?? '' - ), - async () => { - if (!crossMarginBaseSettings) { - return; - } - - try { - const [tradeFee, limitOrderFee, stopOrderFee] = await Promise.all([ - crossMarginBaseSettings?.tradeFee(), - crossMarginBaseSettings?.limitOrderFee(), - crossMarginBaseSettings?.stopOrderFee(), - ]); - const settings = { - tradeFee: tradeFee ? wei(tradeFee.toNumber() / BPS_CONVERSION) : zeroBN, - limitOrderFee: limitOrderFee ? wei(limitOrderFee.toNumber() / BPS_CONVERSION) : zeroBN, - stopOrderFee: stopOrderFee ? wei(stopOrderFee.toNumber() / BPS_CONVERSION) : zeroBN, - }; - setCrossMarginSettings(settings); - - return; - } catch (err) { - logError(err); - } - } - ); -} diff --git a/queries/futures/useGetFuturesMarginTransfers.ts b/queries/futures/useGetFuturesMarginTransfers.ts index 4abc1d2ca7..d73751d025 100644 --- a/queries/futures/useGetFuturesMarginTransfers.ts +++ b/queries/futures/useGetFuturesMarginTransfers.ts @@ -1,12 +1,12 @@ import { NetworkId } from '@synthetixio/contracts-interface'; import request, { gql } from 'graphql-request'; import { useQuery, UseQueryOptions } from 'react-query'; -import { useRecoilValue } from 'recoil'; import QUERY_KEYS from 'constants/queryKeys'; import Connector from 'containers/Connector'; import useIsL2 from 'hooks/useIsL2'; -import { futuresAccountTypeState, selectedFuturesAddressState } from 'store/futures'; +import { selectFuturesAccount, selectFuturesType } from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; import { getDisplayAsset } from 'utils/futures'; import logError from 'utils/logError'; @@ -53,8 +53,8 @@ const useGetFuturesMarginTransfers = ( currencyKey: string | null, options?: UseQueryOptions ) => { - const selectedFuturesAddress = useRecoilValue(selectedFuturesAddressState); - const futuresAccountType = useRecoilValue(futuresAccountTypeState); + const selectedFuturesAddress = useAppSelector(selectFuturesAccount); + const futuresAccountType = useAppSelector(selectFuturesType); const { defaultSynthetixjs: synthetixjs, network, isWalletConnected } = Connector.useContainer(); const futuresEndpoint = getFuturesEndpoint(network?.id as NetworkId); const isL2 = useIsL2(); diff --git a/queries/futures/useGetFuturesMarkets.ts b/queries/futures/useGetFuturesMarkets.ts deleted file mode 100644 index 39ddb556f0..0000000000 --- a/queries/futures/useGetFuturesMarkets.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { NetworkId } from '@synthetixio/contracts-interface'; -import { wei } from '@synthetixio/wei'; -import { useQuery, UseQueryOptions } from 'react-query'; -import { useRecoilState } from 'recoil'; -import { chain } from 'wagmi'; - -import QUERY_KEYS from 'constants/queryKeys'; -import ROUTES from 'constants/routes'; -import Connector from 'containers/Connector'; -import { FuturesClosureReason } from 'hooks/useFuturesMarketClosed'; -import useIsL2 from 'hooks/useIsL2'; -import { FuturesMarket } from 'sdk/types/futures'; -import { setFuturesMarkets as setReduxFuturesMarkets } from 'state/futures/reducer'; -import { serializeWeiObject } from 'state/helpers'; -import { useAppDispatch } from 'state/hooks'; -import { futuresMarketsState } from 'store/futures'; -import { zeroBN } from 'utils/formatters/number'; -import { - FuturesMarketAsset, - getMarketName, - MarketKeyByAsset, - marketsForNetwork, -} from 'utils/futures'; - -import { getReasonFromCode } from './utils'; - -const useGetFuturesMarkets = (options?: UseQueryOptions) => { - const { network: activeChain, defaultSynthetixjs, l2Synthetixjs } = Connector.useContainer(); - - const homepage = window.location.pathname === ROUTES.Home.Root; - const isL2 = useIsL2(); - const network = homepage || !isL2 ? chain.optimism : activeChain; - const synthetixjs = homepage || !isL2 ? l2Synthetixjs : defaultSynthetixjs; - const [, setFuturesMarkets] = useRecoilState(futuresMarketsState); - const dispatch = useAppDispatch(); - - return useQuery( - QUERY_KEYS.Futures.Markets(network?.id as NetworkId), - async () => { - if (!synthetixjs) { - setFuturesMarkets([]); - return null; - } - const enabledMarkets = marketsForNetwork(network.id as NetworkId); - - const { - contracts: { FuturesMarketData, FuturesMarketSettings, SystemStatus, ExchangeRates }, - utils, - } = synthetixjs!; - - const [markets, globals] = await Promise.all([ - FuturesMarketData.allMarketSummaries(), - FuturesMarketData.globals(), - ]); - - const filteredMarkets = markets.filter((m: any) => { - const asset = utils.parseBytes32String(m.asset) as FuturesMarketAsset; - const market = enabledMarkets.find((market) => { - return asset === market.asset; - }); - return !!market; - }); - - const assetKeys = filteredMarkets.map((m: any) => { - const asset = utils.parseBytes32String(m.asset) as FuturesMarketAsset; - return utils.formatBytes32String(MarketKeyByAsset[asset]); - }); - - const currentRoundIdPromises = Promise.all( - assetKeys.map((key: string) => ExchangeRates.getCurrentRoundId(key)) - ); - - const marketLimitPromises = Promise.all( - assetKeys.map((key: string) => FuturesMarketSettings.maxMarketValueUSD(key)) - ); - - const systemStatusPromise = await SystemStatus.getFuturesMarketSuspensions(assetKeys); - - const [currentRoundIds, marketLimits, { suspensions, reasons }] = await Promise.all([ - currentRoundIdPromises, - marketLimitPromises, - systemStatusPromise, - ]); - - const futuresMarkets = filteredMarkets.map( - ( - { - market, - asset, - currentFundingRate, - feeRates, - marketDebt, - marketSkew, - maxLeverage, - marketSize, - price, - }: FuturesMarket, - i: number - ): FuturesMarket => ({ - market, - marketKey: MarketKeyByAsset[utils.parseBytes32String(asset) as FuturesMarketAsset], - marketName: getMarketName(utils.parseBytes32String(asset) as FuturesMarketAsset), - asset: utils.parseBytes32String(asset) as FuturesMarketAsset, - assetHex: asset, - currentFundingRate: wei(currentFundingRate).neg(), - currentRoundId: wei(currentRoundIds[i], 0), - feeRates: { - makerFee: wei(feeRates.makerFee), - takerFee: wei(feeRates.takerFee), - makerFeeNextPrice: wei(feeRates.makerFeeNextPrice), - takerFeeNextPrice: wei(feeRates.takerFeeNextPrice), - }, - openInterest: { - shortPct: wei(marketSize).eq(0) - ? 0 - : wei(marketSize).sub(marketSkew).div('2').div(marketSize).toNumber(), - longPct: wei(marketSize).eq(0) - ? 0 - : wei(marketSize).add(marketSkew).div('2').div(marketSize).toNumber(), - shortUSD: wei(marketSize).eq(0) - ? zeroBN - : wei(marketSize).sub(marketSkew).div('2').mul(price), - longUSD: wei(marketSize).eq(0) - ? zeroBN - : wei(marketSize).add(marketSkew).div('2').mul(price), - }, - marketDebt: wei(marketDebt), - marketSkew: wei(marketSkew), - maxLeverage: wei(maxLeverage), - marketSize: wei(marketSize), - marketLimit: wei(marketLimits[i]), - price: wei(price), - minInitialMargin: wei(globals.minInitialMargin), - keeperDeposit: wei(globals.minKeeperFee), - isSuspended: suspensions[i], - marketClosureReason: getReasonFromCode(reasons[i]) as FuturesClosureReason, - }) - ); - setFuturesMarkets(futuresMarkets); - dispatch(setReduxFuturesMarkets(futuresMarkets.map(serializeWeiObject))); - return futuresMarkets; - }, - { - enabled: !!synthetixjs, - refetchInterval: 15000, - ...options, - } - ); -}; - -export default useGetFuturesMarkets; diff --git a/queries/futures/useGetFuturesOpenOrders.ts b/queries/futures/useGetFuturesOpenOrders.ts deleted file mode 100644 index bfca36cfb7..0000000000 --- a/queries/futures/useGetFuturesOpenOrders.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { NetworkId } from '@synthetixio/contracts-interface'; -import request, { gql } from 'graphql-request'; -import { useQuery, UseQueryOptions } from 'react-query'; -import { useSetRecoilState, useRecoilValue } from 'recoil'; - -import QUERY_KEYS from 'constants/queryKeys'; -import Connector from 'containers/Connector'; -import { selectMarkets } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; -import { openOrdersState, selectedFuturesAddressState } from 'store/futures'; -import logError from 'utils/logError'; - -import { FuturesOrder } from './types'; -import { getFuturesEndpoint, mapFuturesOrders } from './utils'; - -const useGetFuturesOpenOrders = (options?: UseQueryOptions) => { - const selectedFuturesAddress = useRecoilValue(selectedFuturesAddressState); - const { network } = Connector.useContainer(); - const futuresEndpoint = getFuturesEndpoint(network?.id as NetworkId); - - const futuresMarkets = useAppSelector(selectMarkets); - const setOpenOrders = useSetRecoilState(openOrdersState); - - return useQuery( - QUERY_KEYS.Futures.OpenOrders(network?.id as NetworkId, selectedFuturesAddress), - async () => { - if (!selectedFuturesAddress) { - setOpenOrders([]); - return []; - } - try { - const response = await request( - futuresEndpoint, - gql` - query OpenOrders($account: String!) { - futuresOrders(where: { abstractAccount: $account, status: Pending }) { - id - account - size - market - asset - targetRoundId - marginDelta - targetPrice - timestamp - orderType - } - } - `, - { account: selectedFuturesAddress } - ); - - const openOrders: FuturesOrder[] = response - ? response.futuresOrders.map((o: any) => { - const marketInfo = futuresMarkets.find((m) => m.asset === o.asset); - return mapFuturesOrders(o, marketInfo); - }) - : []; - setOpenOrders(openOrders); - return openOrders; - } catch (e) { - logError(e); - return []; - } - }, - { - enabled: futuresMarkets.length > 0 && !!selectedFuturesAddress, - refetchInterval: 5000, - ...options, - } - ); -}; - -export default useGetFuturesOpenOrders; diff --git a/queries/futures/useGetFuturesPositionForMarket.ts b/queries/futures/useGetFuturesPositionForMarket.ts deleted file mode 100644 index 9bb573b523..0000000000 --- a/queries/futures/useGetFuturesPositionForMarket.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { NetworkId } from '@synthetixio/contracts-interface'; -import { utils as ethersUtils } from 'ethers'; -import { useQuery, UseQueryOptions } from 'react-query'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; - -import QUERY_KEYS from 'constants/queryKeys'; -import Connector from 'containers/Connector'; -import useIsL2 from 'hooks/useIsL2'; -import { setPosition as setReduxPosition } from 'state/futures/reducer'; -import { selectMarketKey } from 'state/futures/selectors'; -import { serializeWeiObject } from 'state/helpers'; -import { useAppSelector, useAppDispatch } from 'state/hooks'; -import { positionState, selectedFuturesAddressState } from 'store/futures'; -import { MarketAssetByKey } from 'utils/futures'; - -import { FuturesPosition } from './types'; -import { mapFuturesPosition, getFuturesMarketContract } from './utils'; - -const useGetFuturesPositionForMarket = (options?: UseQueryOptions) => { - const { defaultSynthetixjs: synthetixjs, network } = Connector.useContainer(); - const isL2 = useIsL2(); - const selectedFuturesAddress = useRecoilValue(selectedFuturesAddressState); - const market = useAppSelector(selectMarketKey); - const setPosition = useSetRecoilState(positionState); - const dispatch = useAppDispatch(); - - return useQuery( - QUERY_KEYS.Futures.Position( - network?.id as NetworkId, - market || null, - selectedFuturesAddress || '' - ), - async () => { - if (!isL2 || !market || !selectedFuturesAddress || !synthetixjs) { - setPosition(null); - return null; - } - const { - contracts: { FuturesMarketData }, - } = synthetixjs; - - const [futuresPosition, canLiquidatePosition] = await Promise.all([ - FuturesMarketData.positionDetailsForMarketKey( - ethersUtils.formatBytes32String(market), - selectedFuturesAddress - ), - getFuturesMarketContract(market, synthetixjs!.contracts).canLiquidate( - selectedFuturesAddress - ), - ]); - - const position = mapFuturesPosition( - futuresPosition, - canLiquidatePosition, - MarketAssetByKey[market] - ); - - setPosition(position); - dispatch(setReduxPosition(serializeWeiObject(position))); - - return position; - }, - { - refetchInterval: 5000, - ...options, - } - ); -}; - -export default useGetFuturesPositionForMarket; diff --git a/queries/futures/useGetFuturesPositionForMarkets.ts b/queries/futures/useGetFuturesPositionForMarkets.ts deleted file mode 100644 index f831c73bf8..0000000000 --- a/queries/futures/useGetFuturesPositionForMarkets.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { NetworkId } from '@synthetixio/contracts-interface'; -import { Provider, Contract } from 'ethcall'; -import { utils as ethersUtils } from 'ethers'; -import { useQuery, UseQueryOptions } from 'react-query'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; - -import QUERY_KEYS from 'constants/queryKeys'; -import Connector from 'containers/Connector'; -import useIsL2 from 'hooks/useIsL2'; -import FuturesMarketABI from 'sdk/contracts/abis/FuturesMarket.json'; -import FuturesMarketDataABI from 'sdk/contracts/abis/FuturesMarketData.json'; -import { selectMarkets } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; -import { positionsState, futuresAccountState } from 'store/futures'; -import { MarketKeyByAsset } from 'utils/futures'; - -import { FuturesAccountTypes, PositionDetail } from './types'; -import { mapFuturesPosition } from './utils'; - -const DEFAULT_POSITIONS = { - [FuturesAccountTypes.ISOLATED_MARGIN]: [], - [FuturesAccountTypes.CROSS_MARGIN]: [], -}; - -const useGetFuturesPositionForMarkets = (options?: UseQueryOptions) => { - const { defaultSynthetixjs: synthetixjs, provider, network } = Connector.useContainer(); - const isL2 = useIsL2(); - - const futuresMarkets = useAppSelector(selectMarkets); - const setPositions = useSetRecoilState(positionsState); - const { walletAddress, crossMarginAddress, crossMarginAvailable, status } = useRecoilValue( - futuresAccountState - ); - const assets = futuresMarkets.map(({ asset }) => asset); - - return useQuery( - QUERY_KEYS.Futures.MarketsPositions( - network?.id as NetworkId, - assets || [], - walletAddress ?? '', - crossMarginAddress ?? '' - ), - async () => { - if (!isL2 || !provider || status !== 'complete') { - setPositions(DEFAULT_POSITIONS); - return; - } - - const ethcallProvider = new Provider(); - await ethcallProvider.init(provider); - - const { - contracts: { FuturesMarketData }, - } = synthetixjs!; - - const FMD = new Contract(FuturesMarketData.address, FuturesMarketDataABI); - - const positionCalls = []; - const liquidationCalls = []; - - // isolated margin - for (const { market, asset } of futuresMarkets) { - positionCalls.push( - FMD.positionDetailsForMarketKey( - ethersUtils.formatBytes32String(MarketKeyByAsset[asset]), - walletAddress - ) - ); - const marketContract = new Contract(market, FuturesMarketABI); - liquidationCalls.push(marketContract.canLiquidate(walletAddress)); - } - - // cross margin - if (crossMarginAvailable && crossMarginAddress) { - for (const { market, asset } of futuresMarkets) { - positionCalls.push( - FMD.positionDetailsForMarketKey( - ethersUtils.formatBytes32String(MarketKeyByAsset[asset]), - crossMarginAddress - ) - ); - const marketContract = new Contract(market, FuturesMarketABI); - liquidationCalls.push(marketContract.canLiquidate(crossMarginAddress)); - } - } - - const positionDetails = (await ethcallProvider.all(positionCalls)) as PositionDetail[]; - const canLiquidateState = (await ethcallProvider.all(liquidationCalls)) as boolean[]; - - // split isolated and cross margin results - const positionDetailsIsolated = positionDetails.slice(0, futuresMarkets.length); - const positionDetailsCross = positionDetails.slice( - futuresMarkets.length, - positionDetails.length - ); - - const canLiquidateStateIsolated = canLiquidateState.slice(0, futuresMarkets.length); - const canLiquidateStateCross = canLiquidateState.slice( - futuresMarkets.length, - canLiquidateState.length - ); - - // map the positions using the results - const isolatedPositions = positionDetailsIsolated - .map((position, ind) => { - const canLiquidate = canLiquidateStateIsolated[ind]; - const asset = assets[ind]; - return mapFuturesPosition(position, canLiquidate, asset); - }) - .filter(({ remainingMargin }) => remainingMargin.gt(0)); - - const crossPositions = positionDetailsCross - .map((position, ind) => { - const canLiquidate = canLiquidateStateCross[ind]; - const asset = assets[ind]; - return mapFuturesPosition(position, canLiquidate, asset); - }) - .filter(({ remainingMargin }) => remainingMargin.gt(0)); - - setPositions({ - [FuturesAccountTypes.ISOLATED_MARGIN]: isolatedPositions, - [FuturesAccountTypes.CROSS_MARGIN]: crossPositions, - }); - }, - { - ...options, - } - ); -}; - -export default useGetFuturesPositionForMarkets; diff --git a/queries/futures/useGetFuturesPotentialTradeDetails.ts b/queries/futures/useGetFuturesPotentialTradeDetails.ts deleted file mode 100644 index e729b19947..0000000000 --- a/queries/futures/useGetFuturesPotentialTradeDetails.ts +++ /dev/null @@ -1,165 +0,0 @@ -import Wei, { wei } from '@synthetixio/wei'; -import { useCallback } from 'react'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; - -import Connector from 'containers/Connector'; -import useIsL2 from 'hooks/useIsL2'; -import { PotentialTradeStatus, POTENTIAL_TRADE_STATUS_TO_MESSAGE } from 'sections/futures/types'; -import { selectMarketAsset } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; -import { - leverageSideState, - potentialTradeDetailsState, - futuresAccountTypeState, - selectedFuturesAddressState, - orderTypeState, - crossMarginAccountOverviewState, -} from 'store/futures'; -import logError from 'utils/logError'; - -import { FuturesPotentialTradeDetails } from './types'; -import useGetCrossMarginPotentialTrade from './useGetCrossMarginTradePreview'; -import { getFuturesMarketContract } from './utils'; - -const SUCCESS = 'Success'; -const UNKNOWN = 'Unknown'; - -const useGetFuturesPotentialTradeDetails = () => { - const selectedAccountType = useRecoilValue(futuresAccountTypeState); - const selectedFuturesAddress = useRecoilValue(selectedFuturesAddressState); - const { defaultSynthetixjs: synthetixjs } = Connector.useContainer(); - const isL2 = useIsL2(); - - const leverageSide = useRecoilValue(leverageSideState); - const marketAsset = useAppSelector(selectMarketAsset); - - const orderType = useRecoilValue(orderTypeState); - const { freeMargin } = useRecoilValue(crossMarginAccountOverviewState); - const setPotentialTradeDetails = useSetRecoilState(potentialTradeDetailsState); - - const getPreview = useGetCrossMarginPotentialTrade(marketAsset, selectedFuturesAddress); - - const generatePreview = useCallback( - async ( - nativeSizeDelta: Wei, - positionMarginDelta: Wei, - leverage: number, - orderPrice?: Wei - ): Promise => { - if ( - !synthetixjs || - !marketAsset || - (!nativeSizeDelta && selectedAccountType === 'isolated_margin') || - (!nativeSizeDelta && (!positionMarginDelta || positionMarginDelta.eq(0))) || - ((orderType === 'limit' || orderType === 'stop market') && orderPrice?.eq(0)) || - !isL2 || - !selectedFuturesAddress - ) { - return null; - } - - if (positionMarginDelta.gt(freeMargin)) { - throw new Error('insufficient_margin'); - } - - const { - contracts: { FuturesMarketData }, - } = synthetixjs!; - - const FuturesMarketContract = getFuturesMarketContract(marketAsset, synthetixjs!.contracts); - - const globals = await FuturesMarketData.globals(); - const preview = - selectedAccountType === 'cross_margin' - ? await getPreview( - nativeSizeDelta.toBN(), - wei(positionMarginDelta).toBN(), - orderPrice ? wei(orderPrice).toBN() : undefined - ) - : await FuturesMarketContract.postTradeDetails( - wei(nativeSizeDelta).toBN(), - selectedFuturesAddress - ); - - if (!preview) { - return null; - } - - if (nativeSizeDelta.eq(0) && positionMarginDelta.eq(0)) { - // Size and margin changed to zero before query completed - return null; - } - - const { fee, liqPrice, margin, price, size, status } = preview; - - const potentialTradeDetails = { - fee: wei(fee), - liqPrice: wei(liqPrice), - margin: wei(margin), - price: wei(price), - size: wei(size), - sizeDelta: nativeSizeDelta, - side: leverageSide, - leverage: wei(leverage ? leverage : 1), - notionalValue: wei(size).mul(wei(price)), - minInitialMargin: wei(globals.minInitialMargin), - status, - showStatus: status > 0, // 0 is success - statusMessage: getStatusMessage(status), - }; - - return potentialTradeDetails; - }, - [ - selectedFuturesAddress, - marketAsset, - selectedAccountType, - isL2, - leverageSide, - synthetixjs, - orderType, - freeMargin, - getPreview, - ] - ); - - const getTradeDetails = useCallback( - async (nativeSize: Wei, positionMarginDelta: Wei, leverage: number, orderPrice?: Wei) => { - try { - setPotentialTradeDetails({ - data: null, - status: 'fetching', - error: null, - }); - const data = await generatePreview(nativeSize, positionMarginDelta, leverage, orderPrice); - setPotentialTradeDetails({ data, status: 'complete', error: null }); - } catch (err) { - logError(err); - setPotentialTradeDetails({ - data: null, - status: 'error', - error: err.message, - }); - } - }, - [setPotentialTradeDetails, generatePreview] - ); - - return getTradeDetails; -}; - -const getStatusMessage = (status: PotentialTradeStatus): string => { - if (typeof status !== 'number') { - return UNKNOWN; - } - - if (status === 0) { - return SUCCESS; - } else if (PotentialTradeStatus[status]) { - return POTENTIAL_TRADE_STATUS_TO_MESSAGE[PotentialTradeStatus[status]]; - } else { - return UNKNOWN; - } -}; - -export default useGetFuturesPotentialTradeDetails; diff --git a/queries/futures/useGetFuturesTradesForAccount.ts b/queries/futures/useGetFuturesTradesForAccount.ts index 82db0b38b6..4f65675c83 100644 --- a/queries/futures/useGetFuturesTradesForAccount.ts +++ b/queries/futures/useGetFuturesTradesForAccount.ts @@ -1,13 +1,13 @@ import { NetworkId } from '@synthetixio/contracts-interface'; import { utils as ethersUtils } from 'ethers'; import { useQuery, UseQueryOptions } from 'react-query'; -import { useRecoilValue } from 'recoil'; import { DEFAULT_NUMBER_OF_TRADES } from 'constants/defaults'; import QUERY_KEYS from 'constants/queryKeys'; import Connector from 'containers/Connector'; import useIsL2 from 'hooks/useIsL2'; -import { futuresAccountTypeState } from 'store/futures'; +import { selectFuturesType } from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; import logError from 'utils/logError'; import { FuturesAccountType, getFuturesTrades } from './subgraph'; @@ -19,7 +19,7 @@ const useGetFuturesTradesForAccount = ( account?: string | null, options?: UseQueryOptions & { forceAccount: boolean } ) => { - const selectedAccountType = useRecoilValue(futuresAccountTypeState); + const selectedAccountType = useAppSelector(selectFuturesType); const { network, isWalletConnected } = Connector.useContainer(); const isL2 = useIsL2(); const futuresEndpoint = getFuturesEndpoint(network?.id as NetworkId); diff --git a/queries/futures/useGetFuturesVolumes.ts b/queries/futures/useGetFuturesVolumes.ts deleted file mode 100644 index 59ff7f4e28..0000000000 --- a/queries/futures/useGetFuturesVolumes.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { NetworkId } from '@synthetixio/contracts-interface'; -import { useQuery, UseQueryOptions } from 'react-query'; -import { useSetRecoilState } from 'recoil'; -import { chain, useNetwork } from 'wagmi'; - -import QUERY_KEYS from 'constants/queryKeys'; -import ROUTES from 'constants/routes'; -import useIsL2 from 'hooks/useIsL2'; -import { PERIOD_IN_SECONDS } from 'sdk/constants/period'; -import { futuresVolumesState } from 'store/futures'; -import { calculateTimestampForPeriod } from 'utils/formatters/date'; -import logError from 'utils/logError'; - -import { DAY_PERIOD, FUTURES_ENDPOINT_OP_MAINNET } from './constants'; -import { getFuturesAggregateStats } from './subgraph'; -import { FuturesVolumes } from './types'; -import { calculateVolumes, getFuturesEndpoint } from './utils'; - -const useGetFuturesVolumes = (options?: UseQueryOptions) => { - const homepage = window.location.pathname === ROUTES.Home.Root; - const { chain: activeChain } = useNetwork(); - const isL2 = useIsL2(); - const network = homepage || !isL2 ? chain.optimism : activeChain; - const futuresEndpoint = homepage - ? FUTURES_ENDPOINT_OP_MAINNET - : getFuturesEndpoint(network?.id as NetworkId); - const setFuturesVolumes = useSetRecoilState(futuresVolumesState); - - return useQuery( - QUERY_KEYS.Futures.TradingVolumeForAll(network?.id as NetworkId), - async () => { - try { - const minTimestamp = Math.floor(calculateTimestampForPeriod(DAY_PERIOD) / 1000); - const response = await getFuturesAggregateStats( - futuresEndpoint, - { - first: 999999, - where: { - period: `${PERIOD_IN_SECONDS.ONE_HOUR}`, - timestamp_gte: `${minTimestamp}`, - }, - }, - { - id: true, - asset: true, - volume: true, - trades: true, - timestamp: true, - period: true, - feesKwenta: true, - feesSynthetix: true, - feesCrossMarginAccounts: true, - } - ); - const futuresVolumes = response ? calculateVolumes(response) : {}; - setFuturesVolumes(futuresVolumes); - return futuresVolumes; - } catch (e) { - logError(e); - return null; - } - }, - { ...options } - ); -}; - -export default useGetFuturesVolumes; diff --git a/queries/futures/useQueryCrossMarginAccount.ts b/queries/futures/useQueryCrossMarginAccount.ts index 32a7592e42..fa988eb6f4 100644 --- a/queries/futures/useQueryCrossMarginAccount.ts +++ b/queries/futures/useQueryCrossMarginAccount.ts @@ -6,6 +6,9 @@ import { useRecoilState } from 'recoil'; import { CROSS_MARGIN_ACCOUNT_FACTORY } from 'constants/address'; import Connector from 'containers/Connector'; import usePersistedRecoilState from 'hooks/usePersistedRecoilState'; +import { setCrossMarginAccount } from 'state/futures/reducer'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; +import { selectWallet } from 'state/wallet/selectors'; import { crossMarginAccountsState, futuresAccountState } from 'store/futures'; import logError from 'utils/logError'; @@ -41,12 +44,13 @@ const queryAccountsFromSubgraph = async ( export default function useQueryCrossMarginAccount() { const { crossMarginContractFactory } = useCrossMarginAccountContracts(); - const { network, walletAddress, signer } = Connector.useContainer(); - + const { network, signer } = Connector.useContainer(); const [futuresAccount, setFuturesAccount] = useRecoilState(futuresAccountState); const [storedCrossMarginAccounts, setStoredCrossMarginAccount] = usePersistedRecoilState( crossMarginAccountsState ); + const dispatch = useAppDispatch(); + const walletAddress = useAppSelector(selectWallet); const [retryCount, setRetryCount] = useState(0); const handleAccountQuery = async () => { @@ -106,6 +110,7 @@ export default function useQueryCrossMarginAccount() { crossMarginAvailable: true, walletAddress, }); + dispatch(setCrossMarginAccount(existing)); return existing; } @@ -141,6 +146,7 @@ export default function useQueryCrossMarginAccount() { walletAddress, }; setFuturesAccount(accountState); + dispatch(setCrossMarginAccount(crossMarginAccount)); return crossMarginAccount; }; diff --git a/queries/futures/utils.ts b/queries/futures/utils.ts index b9e5e73f79..4df0364285 100644 --- a/queries/futures/utils.ts +++ b/queries/futures/utils.ts @@ -8,8 +8,8 @@ import { chain } from 'wagmi'; import { ETH_UNIT } from 'constants/network'; import { MarketClosureReason } from 'hooks/useMarketClosed'; import { SynthsTrades, SynthsVolumes } from 'queries/synths/type'; -import { FuturesMarket } from 'sdk/types/futures'; -import { formatCurrency, formatDollars, weiFromWei, zeroBN } from 'utils/formatters/number'; +import { FuturesMarket, FuturesOrder, FuturesOrderTypeDisplay } from 'sdk/types/futures'; +import { formatCurrency, formatDollars, weiFromWei } from 'utils/formatters/number'; import { FuturesMarketAsset, getDisplayAsset, @@ -20,7 +20,6 @@ import { import { SECONDS_PER_DAY, FUTURES_ENDPOINTS } from './constants'; import { CrossMarginAccountTransferResult, - FuturesAggregateStatResult, FuturesMarginTransferResult, FuturesOrderResult, FuturesOrderType, @@ -28,18 +27,13 @@ import { FuturesTradeResult, } from './subgraph'; import { - FuturesPosition, FuturesOpenInterest, FuturesOneMinuteStat, - PositionDetail, PositionSide, - FuturesVolumes, PositionHistory, FundingRateUpdate, FuturesTrade, MarginTransfer, - FuturesOrder, - FuturesOrderTypeDisplay, } from './types'; export const getFuturesEndpoint = (networkId: NetworkId): string => { @@ -54,55 +48,6 @@ export const getFuturesMarketContract = (asset: string | null, contracts: Contra return contract; }; -export const mapFuturesPosition = ( - positionDetail: PositionDetail, - canLiquidatePosition: boolean, - asset: FuturesMarketAsset -): FuturesPosition => { - const { - remainingMargin, - accessibleMargin, - position: { fundingIndex, lastPrice, size, margin }, - accruedFunding, - notionalValue, - liquidationPrice, - profitLoss, - } = positionDetail; - const initialMargin = wei(margin); - const pnl = wei(profitLoss).add(wei(accruedFunding)); - const pnlPct = initialMargin.gt(0) ? pnl.div(wei(initialMargin)) : wei(0); - return { - asset, - remainingMargin: wei(remainingMargin), - accessibleMargin: wei(accessibleMargin), - position: wei(size).eq(zeroBN) - ? null - : { - canLiquidatePosition: !!canLiquidatePosition, - side: wei(size).gt(zeroBN) ? PositionSide.LONG : PositionSide.SHORT, - notionalValue: wei(notionalValue).abs(), - accruedFunding: wei(accruedFunding), - initialMargin, - profitLoss: wei(profitLoss), - fundingIndex: Number(fundingIndex), - lastPrice: wei(lastPrice), - size: wei(size).abs(), - liquidationPrice: wei(liquidationPrice), - initialLeverage: initialMargin.gt(0) - ? wei(size).mul(wei(lastPrice)).div(initialMargin).abs() - : wei(0), - pnl, - pnlPct, - marginRatio: wei(notionalValue).eq(zeroBN) - ? zeroBN - : wei(remainingMargin).div(wei(notionalValue).abs()), - leverage: wei(remainingMargin).eq(zeroBN) - ? zeroBN - : wei(notionalValue).div(wei(remainingMargin)).abs(), - }, - }; -}; - const mapOrderType = (orderType: Partial): FuturesOrderTypeDisplay => { return orderType === 'NextPrice' ? 'Next Price' @@ -194,24 +139,6 @@ export const mapOpenInterest = async ( return openInterest; }; -export const calculateVolumes = ( - futuresHourlyStats: FuturesAggregateStatResult[] -): FuturesVolumes => { - const volumes: FuturesVolumes = futuresHourlyStats.reduce( - (acc: FuturesVolumes, { asset, volume, trades }) => { - return { - ...acc, - [asset]: { - volume: volume.div(ETH_UNIT).add(acc[asset]?.volume ?? 0), - trades: trades.add(acc[asset]?.trades ?? 0), - }, - }; - }, - {} - ); - return volumes; -}; - export const calculateTradeVolumeForAllSynths = (SynthTrades: SynthsTrades): SynthsVolumes => { const result = SynthTrades.synthExchanges .filter((i) => i.fromSynth !== null) diff --git a/queries/synths/useSynthBalances.ts b/queries/synths/useSynthBalances.ts deleted file mode 100644 index 2bd67b7021..0000000000 --- a/queries/synths/useSynthBalances.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { CurrencyKey, NetworkId } from '@synthetixio/contracts-interface'; -import { SynthBalancesMap } from '@synthetixio/queries'; -import { wei } from '@synthetixio/wei'; -import { ethers } from 'ethers'; -import orderBy from 'lodash/orderBy'; -import { useQuery, UseQueryOptions } from 'react-query'; -import { useRecoilState } from 'recoil'; - -import QUERY_KEYS from 'constants/queryKeys'; -import Connector from 'containers/Connector'; -import { SynthBalances } from 'queries/futures/types'; -import { balancesState } from 'store/futures'; -import { zeroBN } from 'utils/formatters/number'; - -import { notNill } from './utils'; - -type SynthBalancesTuple = [string[], ethers.BigNumber[], ethers.BigNumber[]]; - -const useSynthBalances = (options?: UseQueryOptions) => { - const { network, defaultSynthetixjs: synthetixjs, walletAddress } = Connector.useContainer(); - - const [, setBalances] = useRecoilState(balancesState); - - return useQuery( - QUERY_KEYS.Synths.Balances(network?.id as NetworkId, walletAddress), - async () => { - if (!synthetixjs) { - // This should never happen since the query is not enabled when synthetixjs is undefined - throw Error('synthetixjs is undefined'); - } - const balancesMap: SynthBalancesMap = {}; - const [ - currencyKeys, - synthsBalances, - synthsUSDBalances, - ]: SynthBalancesTuple = await synthetixjs.contracts.SynthUtil.synthsBalances(walletAddress); - - let totalUSDBalance = wei(0); - - currencyKeys.forEach((currencyKeyBytes32, idx) => { - const balance = wei(synthsBalances[idx]); - - // discard empty balances - if (balance.gt(0)) { - const synthName = ethers.utils.parseBytes32String(currencyKeyBytes32) as CurrencyKey; - const usdBalance = wei(synthsUSDBalances[idx]); - - balancesMap[synthName] = { - currencyKey: synthName, - balance, - usdBalance, - }; - - totalUSDBalance = totalUSDBalance.add(usdBalance); - } - }); - - const balances = { - balancesMap: balancesMap, - balances: orderBy( - Object.values(balancesMap).filter(notNill), - (balance) => balance.usdBalance.toNumber(), - 'desc' - ), - totalUSDBalance, - susdWalletBalance: balancesMap?.['sUSD']?.balance ?? zeroBN, - }; - setBalances(balances); - - return balances; - }, - { - enabled: !!synthetixjs && !!walletAddress, - ...options, - } - ); -}; - -export default useSynthBalances; diff --git a/queries/walletBalances/types.ts b/queries/walletBalances/types.ts deleted file mode 100644 index 8992488a8b..0000000000 --- a/queries/walletBalances/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { NetworkId } from '@synthetixio/contracts-interface'; - -export type Token = { - address: string; - decimals: number; - logoURI: string; - name: string; - symbol: string; - chainId: NetworkId; - tags: string[]; -}; diff --git a/sdk/README.md b/sdk/README.md index eee31c9049..4003d47efa 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -39,9 +39,21 @@ The following tasks are expected to be completed before the SDK can be considere - [ ] Remove code that refers to `@synthetixio/*` packages. - [x] Add contract typings. - [x] Implement `Context` class. +- [ ] Cache redux state and hydrate on load - [ ] Ensure type correctness of all SDK methods. - [ ] Set up foundation for retries on select methods. - [ ] Set up service for interacting with our subgraphs. +- [ ] Remove Duplicated types and move most data types to sdk. +- [ ] Cache contract data where possible, especially settings, config etc which doesn't change very often +- [ ] Create a contracts class in sdk context where we can cache more dynamic contracts such as markets +- [ ] Ensure types are added to all redux actions and reducers +- [ ] Remove old unused code +- [ ] Ensure consistent logic patterns across various pages, sdk states and services +- [ ] Ensure all data is correctly refetched after some mutation e.g. polling for contract and subgraph changes after a transaction +- [ ] Consider experimenting with WebSockets for realtime data (again). +- [ ] Remove walletAddress from connector and change references to the redux state wallet, this means we're always taking the sdk as source of truth for the wallet and avoids race conditions where queries are attempted before the signer is set. +- [ ] Add query statuses for all key queries and create derived query statuses for components which rely on completion of multiple queries +- [ ] Make all sdk number params consistent, e.g. use Wei everywhere insreads of BigNumber or string - [ ] Remove Duplicated types. - [ ] Create a standard way of passing in numeric values (particularly amounts) to the SDK. Weigh pros and cons of (`Wei`, `ethers.BigNumber` and `string`). @@ -49,6 +61,7 @@ The following tasks are expected to be completed before the SDK can be considere - [ ] Refactor `handleExchange` method. - [ ] Rename quote/base to from/to. +- [ ] Update the write transactions to use the typed calls - [ ] Ensure all methods use from/to in correct order. - [ ] Experiment with exchange contexts (store an instance of from/to pairings, so that the client doesn't have to pass it every time). - [ ] Reduce number of queries, by storing more data in class instance. @@ -61,7 +74,7 @@ The following tasks are expected to be completed before the SDK can be considere - [ ] Separate methods for isolated and cross-margin accounts. - [ ] Implement methods for fetching orders, past trades and transfers from the subgraph. -- [ ] Consider experimenting with WebSockets for realtime data (again). +- [ ] Query cross margin accounts from sdk and cache them there. ## Kwenta Token diff --git a/sdk/constants/futures.ts b/sdk/constants/futures.ts index be666391f0..387f19184a 100644 --- a/sdk/constants/futures.ts +++ b/sdk/constants/futures.ts @@ -113,3 +113,5 @@ export const MAINNET_MARKETS = MARKETS_LIST.filter( export const TESTNET_MARKETS = MARKETS_LIST.filter( (m) => m.supports === 'testnet' || m.supports === 'both' ); + +export const BPS_CONVERSION = 10000; diff --git a/queries/futures/useGetCrossMarginTradePreview.ts b/sdk/contracts/FuturesMarketInternal.ts similarity index 84% rename from queries/futures/useGetCrossMarginTradePreview.ts rename to sdk/contracts/FuturesMarketInternal.ts index 0f1ef56c65..8f598725fe 100644 --- a/queries/futures/useGetCrossMarginTradePreview.ts +++ b/sdk/contracts/FuturesMarketInternal.ts @@ -1,15 +1,13 @@ -import { SynthetixJS } from '@synthetixio/contracts-interface'; -import { wei } from '@synthetixio/wei'; import BN from 'bn.js'; import { Provider, Contract as MultiCallContract } from 'ethcall'; import { BigNumber, ethers, Contract } from 'ethers'; import { formatBytes32String } from 'ethers/lib/utils'; -import { useCallback, useMemo } from 'react'; -import Connector from 'containers/Connector'; -import useIsL2 from 'hooks/useIsL2'; +import { KWENTA_TRACKING_CODE } from 'queries/futures/constants'; import FuturesMarket from 'sdk/contracts/abis/FuturesMarket.json'; -import { PotentialTradeStatus } from 'sections/futures/types'; +import { FuturesMarket__factory } from 'sdk/contracts/types'; +import { FuturesMarketKey, PotentialTradeStatus } from 'sdk/types/futures'; +import { sdk } from 'state/config'; import { zeroBN, ZERO_BIG_NUM, @@ -18,11 +16,6 @@ import { multiplyDecimal, divideDecimal, } from 'utils/formatters/number'; -import { FuturesMarketAsset, MarketKeyByAsset } from 'utils/futures'; -import logError from 'utils/logError'; - -import { KWENTA_TRACKING_CODE } from './constants'; -import { getFuturesMarketContract } from './utils'; // Need to recreate postTradeDetails from the contract here locally // so we can modify margin for use with cross margin @@ -51,45 +44,11 @@ type Position = { const ethcallProvider = new Provider(); -export default function useGetCrossMarginTradePreview( - marketAsset: FuturesMarketAsset, - address: string | null | undefined -) { - const { defaultSynthetixjs: synthetixjs, provider } = Connector.useContainer(); - const isL2 = useIsL2(); - - const contractInstance = useMemo(() => { - if (!synthetixjs || !provider || !address || !isL2) return null; - try { - return new FuturesMarketInternal(synthetixjs, provider, marketAsset, address); - } catch (err) { - logError(err); - return null; - } - }, [synthetixjs, provider, address, isL2, marketAsset]); - - const getPreview = useCallback( - async (sizeDelta: BigNumber, marginDelta: BigNumber, orderPrice?: BigNumber) => { - if (contractInstance) { - const sizeBN = wei(sizeDelta || 0).toBN(); - const marginBN = wei(marginDelta || 0).toBN(); - const res = await contractInstance.getTradePreview(sizeBN, marginBN, orderPrice); - return res; - } - }, - [contractInstance] - ); - - return getPreview; -} - class FuturesMarketInternal { - _synthetixjs: SynthetixJS; _provider: ethers.providers.Provider; _futuresMarketContract: Contract; - _futuresSettingsContract: Contract; + _futuresSettingsContract: Contract | undefined; _marketKeyBytes: string; - _account: string; _onChainData: { assetPrice: BigNumber; @@ -103,18 +62,15 @@ class FuturesMarketInternal { _cache: Record; constructor( - synthetixjs: SynthetixJS, provider: ethers.providers.Provider, - marketAsset: FuturesMarketAsset, - account: string + marketKey: FuturesMarketKey, + marketAddress: string ) { - this._synthetixjs = synthetixjs; this._provider = provider; - this._futuresMarketContract = getFuturesMarketContract(marketAsset, synthetixjs.contracts); - this._futuresSettingsContract = synthetixjs.contracts.FuturesMarketSettings; - this._marketKeyBytes = formatBytes32String(MarketKeyByAsset[marketAsset]); - this._account = account; + this._futuresMarketContract = FuturesMarket__factory.connect(marketAddress, provider); + this._futuresSettingsContract = sdk.context.contracts.FuturesMarketSettings; + this._marketKeyBytes = formatBytes32String(marketKey); this._cache = {}; this._onChainData = { assetPrice: BigNumber.from(0), @@ -127,6 +83,7 @@ class FuturesMarketInternal { } getTradePreview = async ( + account: string, sizeDelta: BigNumber, marginDelta: BigNumber, limitStopPrice?: BigNumber @@ -140,10 +97,10 @@ class FuturesMarketInternal { multiCallContract.assetPrice(), multiCallContract.marketSkew(), multiCallContract.marketSize(), - multiCallContract.accruedFunding(this._account), + multiCallContract.accruedFunding(account), multiCallContract.fundingSequenceLength(), multiCallContract.fundingLastRecomputed(), - multiCallContract.positions(this._account), + multiCallContract.positions(account), ]); this._onChainData = { @@ -186,7 +143,8 @@ class FuturesMarketInternal { tradeParams: TradeParams, marginDelta: BigNumber ) => { - const dynamicFee = await this._synthetixjs.contracts.Exchanger.dynamicFeeRateForExchange( + if (!sdk.context.contracts.Exchanger) throw new Error('Unsupported network'); + const dynamicFee = await sdk.context.contracts.Exchanger?.dynamicFeeRateForExchange( formatBytes32String('sUSD'), this._marketKeyBytes ); @@ -244,7 +202,11 @@ class FuturesMarketInternal { ); if (maxLeverage.add(UNIT_BIG_NUM.div(100)).lt(leverage.abs())) { - return { newPos: oldPos, fee: zeroBN, status: PotentialTradeStatus.MAX_LEVERAGE_EXCEEDED }; + return { + newPos: oldPos, + fee: ZERO_BIG_NUM, + status: PotentialTradeStatus.MAX_LEVERAGE_EXCEEDED, + }; } const maxMarketValueUSD = await this._getSetting('maxMarketValueUSD', [this._marketKeyBytes]); @@ -422,9 +384,12 @@ class FuturesMarketInternal { _getSetting = async (settingGetter: string, params: any[] = []) => { const cached = this._cache[settingGetter]; + if (!this._futuresSettingsContract) throw new Error('Unsupported network'); if (cached) return cached; const res = await this._futuresSettingsContract[settingGetter](...params); this._cache[settingGetter] = res; return res; }; } + +export default FuturesMarketInternal; diff --git a/sdk/contracts/constants.ts b/sdk/contracts/constants.ts index 8e68b29efb..2203b680c8 100644 --- a/sdk/contracts/constants.ts +++ b/sdk/contracts/constants.ts @@ -43,6 +43,11 @@ export const ADDRESSES: Record> = { 10: '0xaE55F163337A2A46733AA66dA9F35299f9A46e9e', 420: '0x0dde87714C3bdACB93bB1d38605aFff209a85998', }, + SUSD: { + 1: '0x57Ab1ec28D129707052df4dF418D58a2D46d5f51', + 10: '0x8c6f28f2F1A3C87F0f938b96d27520d9751ec8d9', + 420: '0xebaeaad9236615542844adc5c149f86c36ad1136', + }, Synthetix: { 1: '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F', 5: '0x51f44ca59b867E005e48FA573Cb8df83FC7f7597', diff --git a/sdk/contracts/index.ts b/sdk/contracts/index.ts index 6b914ee9fe..df632a6cbf 100644 --- a/sdk/contracts/index.ts +++ b/sdk/contracts/index.ts @@ -80,6 +80,9 @@ export const getContractsByNetwork = ( SynthSwap: ADDRESSES.SynthSwap[networkId] ? SynthSwap__factory.connect(ADDRESSES.SynthSwap[networkId], provider) : undefined, + SUSD: ADDRESSES.SUSD[networkId] + ? ERC20__factory.connect(ADDRESSES.SUSD[networkId], provider) + : undefined, CrossMarginAccountFactory: ADDRESSES.CrossMarginAccountFactory[networkId] ? CrossMarginAccountFactory__factory.connect( ADDRESSES.CrossMarginAccountFactory[networkId], diff --git a/sdk/services/exchange.ts b/sdk/services/exchange.ts index f330c9afb8..c32b203f7a 100644 --- a/sdk/services/exchange.ts +++ b/sdk/services/exchange.ts @@ -23,9 +23,9 @@ import { PriceResponse } from 'queries/coingecko/types'; import { KWENTA_TRACKING_CODE } from 'queries/futures/constants'; import { Rates } from 'queries/rates/types'; import { getProxySynthSymbol } from 'queries/synths/utils'; -import { Token } from 'queries/walletBalances/types'; import { getEthGasPrice } from 'sdk/common/gas'; import erc20Abi from 'sdk/contracts/abis/ERC20.json'; +import { Token } from 'sdk/types/tokens'; import { startInterval } from 'sdk/utils/interval'; import { newGetCoinGeckoPricesForCurrencies, @@ -1088,7 +1088,8 @@ export default class ExchangeService { // One idea is to create a "tokens" service that handles everything // related to 1inch tokens. - public async getTokenBalances(walletAddress: string) { + public async getTokenBalances(walletAddress: string): Promise { + if (!this.sdk.context.isMainnet) return {}; const filteredTokens = this.tokenList.filter( (t) => !FILTERED_TOKENS.includes(t.address.toLowerCase()) ); diff --git a/sdk/services/futures.ts b/sdk/services/futures.ts index 7642468065..a0e852ead4 100644 --- a/sdk/services/futures.ts +++ b/sdk/services/futures.ts @@ -1,11 +1,25 @@ -import { wei } from '@synthetixio/wei'; +import { NetworkId } from '@synthetixio/contracts-interface'; +import Wei, { wei } from '@synthetixio/wei'; +import { Contract as EthCallContract } from 'ethcall'; +import { BigNumber, ContractTransaction, ethers } from 'ethers'; import { formatBytes32String, parseBytes32String } from 'ethers/lib/utils'; import request, { gql } from 'graphql-request'; import KwentaSDK from 'sdk'; +import { DAY_PERIOD, KWENTA_TRACKING_CODE } from 'queries/futures/constants'; +import { getFuturesAggregateStats } from 'queries/futures/subgraph'; +import { mapFuturesOrders } from 'queries/futures/utils'; import { UNSUPPORTED_NETWORK } from 'sdk/common/errors'; +import { BPS_CONVERSION } from 'sdk/constants/futures'; import { Period, PERIOD_IN_SECONDS } from 'sdk/constants/period'; import { getContractsByNetwork } from 'sdk/contracts'; +import FuturesMarketABI from 'sdk/contracts/abis/FuturesMarket.json'; +import FuturesMarketInternal from 'sdk/contracts/FuturesMarketInternal'; +import { + CrossMarginBase__factory, + FuturesMarketData, + FuturesMarket__factory, +} from 'sdk/contracts/types'; import { NetworkOverrideOptions } from 'sdk/types/common'; import { FundingRateInput, @@ -13,80 +27,115 @@ import { FundingRateUpdate, FuturesMarket, FuturesMarketAsset, + FuturesMarketKey, + FuturesOrder, + FuturesVolumes, MarketClosureReason, + PositionDetail, + PositionSide, } from 'sdk/types/futures'; import { calculateFundingRate, + calculateVolumes, + formatPotentialTrade, getFuturesEndpoint, getMarketName, getReasonFromCode, + mapFuturesPosition, marketsForNetwork, } from 'sdk/utils/futures'; +import { calculateTimestampForPeriod } from 'utils/formatters/date'; import { MarketKeyByAsset } from 'utils/futures'; export default class FuturesService { private sdk: KwentaSDK; - private futuresGqlEndpoint: string; + public markets: FuturesMarket[] | undefined; + public internalFuturesMarkets: Partial< + Record + > = {}; constructor(sdk: KwentaSDK) { this.sdk = sdk; - this.futuresGqlEndpoint = getFuturesEndpoint(sdk.context.networkId); + } + + get futuresGqlEndpoint() { + return getFuturesEndpoint(this.sdk.context.networkId); + } + + private getInternalFuturesMarket(marketAddress: string, marketKey: FuturesMarketKey) { + let market = this.internalFuturesMarkets[this.sdk.context.networkId]?.[marketAddress]; + if (market) return market; + market = new FuturesMarketInternal(this.sdk.context.provider, marketKey, marketAddress); + this.internalFuturesMarkets = { + [this.sdk.context.networkId]: { + ...this.internalFuturesMarkets[this.sdk.context.networkId], + [marketAddress]: market, + }, + }; + + return market; } public async getMarkets(networkOverride?: NetworkOverrideOptions) { const enabledMarkets = marketsForNetwork( networkOverride?.networkId || this.sdk.context.networkId ); - const contracts = networkOverride && networkOverride?.networkId !== this.sdk.context.networkId ? getContractsByNetwork(networkOverride.networkId, networkOverride.provider) : this.sdk.context.contracts; - const { FuturesMarketSettings, SystemStatus, ExchangeRates, FuturesMarketData } = contracts; + const { SystemStatus } = contracts; + const { + FuturesMarketSettings, + ExchangeRates, + FuturesMarketData, + } = this.sdk.context.mutliCallContracts; if (!FuturesMarketData || !FuturesMarketSettings || !SystemStatus || !ExchangeRates) { throw new Error(UNSUPPORTED_NETWORK); } - const [markets, globals] = await Promise.all([ + const [markets, globals] = await this.sdk.context.multicallProvider.all([ FuturesMarketData.allMarketSummaries(), FuturesMarketData.globals(), ]); const filteredMarkets = markets.filter((m: any) => { - const asset = parseBytes32String(m.asset) as FuturesMarketAsset; + const marketKey = parseBytes32String(m.key) as FuturesMarketKey; const market = enabledMarkets.find((market) => { - return asset === market.asset; + return marketKey === market.key; }); return !!market; - }); + }) as FuturesMarketData.MarketSummaryStructOutput[]; - const assetKeys = filteredMarkets.map((m: any) => { - const asset = parseBytes32String(m.asset) as FuturesMarketAsset; - return formatBytes32String(MarketKeyByAsset[asset]); + const marketKeys = filteredMarkets.map((m: any) => { + return m.key; }); - const currentRoundIdPromises = Promise.all( - assetKeys.map((key: string) => ExchangeRates.getCurrentRoundId(key)) + const currentRoundIdCalls = marketKeys.map((key: string) => + ExchangeRates.getCurrentRoundId(key) ); - const marketLimitPromises = Promise.all( - assetKeys.map((key: string) => FuturesMarketSettings.maxMarketValueUSD(key)) + const marketLimitCalls = marketKeys.map((key: string) => + FuturesMarketSettings.maxMarketValueUSD(key) ); - const systemStatusPromise = await SystemStatus.getFuturesMarketSuspensions(assetKeys); - - const [currentRoundIds, marketLimits, { suspensions, reasons }] = await Promise.all([ - currentRoundIdPromises, - marketLimitPromises, - systemStatusPromise, + const responses = await this.sdk.context.multicallProvider.all([ + ...currentRoundIdCalls, + ...marketLimitCalls, ]); + const currentRoundIds = responses.slice(0, currentRoundIdCalls.length); + const marketLimits = responses.slice(currentRoundIdCalls.length); + + const { suspensions, reasons } = await SystemStatus.getFuturesMarketSuspensions(marketKeys); + const futuresMarkets = filteredMarkets.map( ( { market, + key, asset, currentFundingRate, feeRates, @@ -99,7 +148,7 @@ export default class FuturesService { i: number ): FuturesMarket => ({ market, - marketKey: MarketKeyByAsset[parseBytes32String(asset) as FuturesMarketAsset], + marketKey: parseBytes32String(key) as FuturesMarketKey, marketName: getMarketName(parseBytes32String(asset) as FuturesMarketAsset), asset: parseBytes32String(asset) as FuturesMarketAsset, assetHex: asset, @@ -140,6 +189,49 @@ export default class FuturesService { return futuresMarkets; } + // TODO: types + public async getFuturesPositions( + address: string, // Cross margin or EOA address + futuresMarkets: { asset: FuturesMarketAsset; marketKey: FuturesMarketKey; address: string }[] + ) { + const marketDataContract = this.sdk.context.mutliCallContracts.FuturesMarketData; + + if (!this.sdk.context.isL2 || !marketDataContract) { + throw new Error(UNSUPPORTED_NETWORK); + } + + const positionCalls = []; + const liquidationCalls = []; + + for (const { address: marketAddress, marketKey } of futuresMarkets) { + positionCalls.push( + marketDataContract.positionDetailsForMarketKey(formatBytes32String(marketKey), address) + ); + const marketContract = new EthCallContract(marketAddress, FuturesMarketABI); + liquidationCalls.push(marketContract.canLiquidate(address)); + } + + // TODO: Combine these two? + const positionDetails = (await this.sdk.context.multicallProvider.all( + positionCalls + )) as PositionDetail[]; + const canLiquidateState = (await this.sdk.context.multicallProvider.all( + liquidationCalls + )) as boolean[]; + + // map the positions using the results + const positions = positionDetails + .map((position, ind) => { + const canLiquidate = canLiquidateState[ind]; + const marketKey = futuresMarkets[ind].marketKey; + const asset = futuresMarkets[ind].asset; + return mapFuturesPosition(position, canLiquidate, asset, marketKey); + }) + .filter(({ remainingMargin }) => remainingMargin.gt(0)); + + return positions; + } + public async getAverageFundingRates(markets: FuturesMarket[], period: Period) { const fundingRateInputs: FundingRateInput[] = markets.map( ({ asset, market, price, currentFundingRate }) => { @@ -244,4 +336,200 @@ export default class FuturesService { return fundingRateResponses.filter((funding): funding is FundingRateResponse => !!funding); } + + public async getDailyVolumes(): Promise { + const minTimestamp = Math.floor(calculateTimestampForPeriod(DAY_PERIOD) / 1000); + const response = await getFuturesAggregateStats( + this.futuresGqlEndpoint, + { + first: 999999, + where: { + period: `${PERIOD_IN_SECONDS.ONE_HOUR}`, + timestamp_gte: `${minTimestamp}`, + }, + }, + { + id: true, + asset: true, + volume: true, + trades: true, + timestamp: true, + period: true, + feesCrossMarginAccounts: true, + feesKwenta: true, + feesSynthetix: true, + } + ); + return response ? calculateVolumes(response) : {}; + } + + public async getCrossMarginBalanceInfo(crossMarginAddress: string) { + const crossMarginAccountContract = CrossMarginBase__factory.connect( + crossMarginAddress, + this.sdk.context.provider + ); + const { SUSD } = this.sdk.context.contracts; + if (!SUSD) throw new Error(UNSUPPORTED_NETWORK); + + // TODO: EthCall + const [freeMargin, keeperEthBal, allowance] = await Promise.all([ + crossMarginAccountContract.freeMargin(), + this.sdk.context.provider.getBalance(crossMarginAddress), + SUSD.allowance(this.sdk.context.walletAddress, crossMarginAccountContract.address), + ]); + + return { + freeMargin: wei(freeMargin), + keeperEthBal: wei(keeperEthBal), + allowance: wei(allowance), + }; + } + + public async getOpenOrders(account: string, markets: FuturesMarket[]) { + const response = await request( + this.futuresGqlEndpoint, + gql` + query OpenOrders($account: String!) { + futuresOrders(where: { abstractAccount: $account, status: Pending }) { + id + account + size + market + asset + targetRoundId + marginDelta + targetPrice + timestamp + orderType + } + } + `, + { account: account } + ); + + const openOrders: FuturesOrder[] = response + ? response.futuresOrders.map((o: any) => { + const marketInfo = markets.find((m) => m.asset === o.asset); + return mapFuturesOrders(o, marketInfo); + }) + : []; + return openOrders; + } + + public async getCrossMarginSettings() { + const crossMarginBaseSettings = this.sdk.context.mutliCallContracts.CrossMarginBaseSettings; + if (!crossMarginBaseSettings) throw new Error(UNSUPPORTED_NETWORK); + + const [tradeFee, limitOrderFee, stopOrderFee] = await this.sdk.context.multicallProvider.all([ + crossMarginBaseSettings.tradeFee(), + crossMarginBaseSettings.limitOrderFee(), + crossMarginBaseSettings.stopOrderFee(), + ]); + return { + tradeFee: tradeFee ? wei(tradeFee.toNumber() / BPS_CONVERSION) : wei(0), + limitOrderFee: limitOrderFee ? wei(limitOrderFee.toNumber() / BPS_CONVERSION) : wei(0), + stopOrderFee: stopOrderFee ? wei(stopOrderFee.toNumber() / BPS_CONVERSION) : wei(0), + }; + } + + public async getIsolatedTradePreview( + marketAddress: string, + sizeDelta: Wei, + leverageSide: PositionSide + ) { + const market = FuturesMarket__factory.connect(marketAddress, this.sdk.context.signer); + const details = await market.postTradeDetails( + wei(sizeDelta).toBN(), + this.sdk.context.walletAddress + ); + return formatPotentialTrade(details, sizeDelta, leverageSide); + } + + public async getCrossMarginTradePreview( + crossMarginAccount: string, + marketKey: FuturesMarketKey, + marketAddress: string, + tradeParams: { + sizeDelta: Wei; + marginDelta: Wei; + orderPrice?: Wei; + leverageSide: PositionSide; + } + ) { + const marketInternal = this.getInternalFuturesMarket(marketAddress, marketKey); + + const preview = await marketInternal.getTradePreview( + crossMarginAccount, + tradeParams.sizeDelta.toBN(), + tradeParams.marginDelta.toBN(), + tradeParams.orderPrice?.toBN() + ); + + return formatPotentialTrade(preview, tradeParams.sizeDelta, tradeParams.leverageSide); + } + + public async getCrossMarginKeeperBalance(account: string) { + const bal = await this.sdk.context.provider.getBalance(account); + return wei(bal); + } + + // Contract mutations + + public async approveCrossMarginDeposit( + crossMarginAddress: string, + amount: BigNumber = ethers.constants.MaxUint256 + ) { + if (!this.sdk.context.contracts.SUSD) throw new Error(UNSUPPORTED_NETWORK); + return this.sdk.context.contracts.SUSD.approve(crossMarginAddress, amount); + } + + public async depositCrossMargin(crossMarginAddress: string, amount: Wei) { + // TODO: Store on instance + const crossMarginAccountContract = CrossMarginBase__factory.connect( + crossMarginAddress, + this.sdk.context.signer + ); + return crossMarginAccountContract.deposit(amount.toBN()); + } + + public async withdrawCrossMargin(crossMarginAddress: string, amount: Wei) { + // TODO: Store on instance + const crossMarginAccountContract = CrossMarginBase__factory.connect( + crossMarginAddress, + this.sdk.context.signer + ); + return crossMarginAccountContract.withdraw(amount.toBN()); + } + + public async depositIsolatedMargin(marketAddress: string, amount: Wei) { + const market = FuturesMarket__factory.connect(marketAddress, this.sdk.context.signer); + return market.transferMargin(amount.toBN()); + } + + public async withdrawIsolatedMargin(marketAddress: string, amount: Wei) { + const market = FuturesMarket__factory.connect(marketAddress, this.sdk.context.signer); + return market.transferMargin(amount.neg().toBN()); + } + + public async closeIsolatedPosition(marketAddress: string) { + const market = FuturesMarket__factory.connect(marketAddress, this.sdk.context.signer); + return market.closePositionWithTracking(KWENTA_TRACKING_CODE); + } + + public async modifyIsolatedMarginPosition( + marketAddress: string, + sizeDelta: Wei, + useNextPrice = false, + estimationOnly: T + ): TxReturn { + const market = FuturesMarket__factory.connect(marketAddress, this.sdk.context.signer); + const root = estimationOnly ? market.estimateGas : market; + return useNextPrice + ? (root.submitNextPriceOrderWithTracking(sizeDelta.toBN(), KWENTA_TRACKING_CODE) as any) + : (root.modifyPositionWithTracking(sizeDelta.toBN(), KWENTA_TRACKING_CODE) as any); + } } + +type TxReturn = Promise< + T extends true ? BigNumber : ContractTransaction +>; diff --git a/sdk/services/synths.ts b/sdk/services/synths.ts index b300f3a2ee..8d98f7816c 100644 --- a/sdk/services/synths.ts +++ b/sdk/services/synths.ts @@ -1,11 +1,11 @@ import { CurrencyKey } from '@synthetixio/contracts-interface'; -import { SynthBalancesMap } from '@synthetixio/queries'; import { wei } from '@synthetixio/wei'; import { ethers } from 'ethers'; import { orderBy } from 'lodash'; import KwentaSDK from 'sdk'; import { notNill } from 'queries/synths/utils'; +import { SynthBalance } from 'sdk/types/tokens'; import { zeroBN } from 'utils/formatters/number'; import * as sdkErrors from '../common/errors'; @@ -24,7 +24,7 @@ export default class SynthsService { throw new Error(sdkErrors.UNSUPPORTED_NETWORK); } - const balancesMap: SynthBalancesMap = {}; + const balancesMap: Record = {}; const [ currencyKeys, synthsBalances, diff --git a/sdk/types/common.ts b/sdk/types/common.ts index ef67ec1996..f3458b9bcf 100644 --- a/sdk/types/common.ts +++ b/sdk/types/common.ts @@ -5,3 +5,10 @@ export type NetworkOverrideOptions = { networkId: NetworkId; provider: providers.Provider; }; + +export enum TransactionStatus { + AwaitingExecution = 'AwaitingExecution', + Executed = 'Executed', + Confirmed = 'Confirmed', + Failed = 'Failed', +} diff --git a/sdk/types/futures.ts b/sdk/types/futures.ts index c05558c632..c82d2dac20 100644 --- a/sdk/types/futures.ts +++ b/sdk/types/futures.ts @@ -1,4 +1,5 @@ import Wei from '@synthetixio/wei'; +import { BigNumber } from 'ethers'; export type FundingRateInput = { marketAddress: string | undefined; @@ -17,7 +18,7 @@ export type MarketClosureReason = SynthSuspensionReason; export type FuturesMarket = { market: string; - marketKey?: FuturesMarketKey; + marketKey: FuturesMarketKey; marketName: string; asset: FuturesMarketAsset; assetHex: string; @@ -106,3 +107,130 @@ export interface FuturesMarketConfig { supports: 'mainnet' | 'testnet' | 'both'; disabled?: boolean; } + +export type FuturesVolumes = { + [asset: string]: { + volume: T; + trades: T; + }; +}; + +export type PositionDetail = { + remainingMargin: Wei; + accessibleMargin: Wei; + orderPending: boolean; + order: { + pending: boolean; + fee: Wei; + leverage: Wei; + }; + position: { + fundingIndex: Wei; + lastPrice: Wei; + size: Wei; + margin: Wei; + }; + accruedFunding: Wei; + notionalValue: Wei; + liquidationPrice: Wei; + profitLoss: Wei; +}; + +export enum PositionSide { + LONG = 'long', + SHORT = 'short', +} + +export type FuturesFilledPosition = { + canLiquidatePosition: boolean; + side: PositionSide; + notionalValue: T; + accruedFunding: T; + initialMargin: T; + profitLoss: T; + fundingIndex: number; + lastPrice: T; + size: T; + liquidationPrice: T; + initialLeverage: T; + leverage: T; + pnl: T; + pnlPct: T; + marginRatio: T; +}; + +export type FuturesPosition = { + asset: FuturesMarketAsset; + marketKey: FuturesMarketKey; + remainingMargin: T; + accessibleMargin: T; + position: FuturesFilledPosition | null; +}; + +// This type exists to rename enum types from the subgraph to display-friendly types +export type FuturesOrderTypeDisplay = + | 'Next Price' + | 'Limit' + | 'Stop Market' + | 'Market' + | 'Liquidation'; + +export type FuturesOrder = { + id: string; + account: string; + asset: FuturesMarketAsset; + market: string; + marketKey: FuturesMarketKey; + size: T; + targetPrice: T | null; + marginDelta: T; + targetRoundId: T | null; + timestamp: T; + orderType: FuturesOrderTypeDisplay; + sizeTxt?: string; + targetPriceTxt?: string; + side?: PositionSide; + isStale?: boolean; + isExecutable?: boolean; + isCancelling?: boolean; +}; + +export type FuturesPotentialTradeDetails = { + size: T; + sizeDelta: T; + liqPrice: T; + margin: T; + price: T; + fee: T; + leverage: T; + notionalValue: T; + side: PositionSide; + status: PotentialTradeStatus; + showStatus: boolean; + statusMessage: string; +}; + +// https://github.com/Synthetixio/synthetix/blob/4d2add4f74c68ac4f1106f6e7be4c31d4f1ccc76/contracts/interfaces/IFuturesMarketBaseTypes.sol#L6-L19 +export enum PotentialTradeStatus { + OK = 0, + INVALID_PRICE = 1, + PRICE_OUT_OF_BOUNDS = 2, + CAN_LIQUIDATE = 3, + CANNOT_LIQUIDATE = 4, + MAX_MARKET_SIZE_EXCEEDED = 5, + MAX_LEVERAGE_EXCEEDED = 6, + INSUFFICIENT_MARGIN = 7, + NOT_PERMITTED = 8, + NIL_ORDER = 9, + NO_POSITION_OPEN = 10, + PRICE_TOO_VOLATILE = 11, +} + +export type PostTradeDetailsResponse = { + margin: BigNumber; + size: BigNumber; + price: BigNumber; + liqPrice: BigNumber; + fee: BigNumber; + status: number; +}; diff --git a/sdk/types/tokens.ts b/sdk/types/tokens.ts new file mode 100644 index 0000000000..76d487c937 --- /dev/null +++ b/sdk/types/tokens.ts @@ -0,0 +1,28 @@ +import { NetworkId } from '@synthetixio/contracts-interface'; +import Wei from '@synthetixio/wei'; + +export type SynthBalance = { + currencyKey: string; + balance: T; + usdBalance: T; +}; + +export type TokenBalances = Partial< + Record< + string, + { + balance: T; + token: Token; + } + > +>; + +export type Token = { + address: string; + chainId: NetworkId; + decimals: number; + logoURI: string; + name: string; + symbol: string; + tags: string[]; +}; diff --git a/sdk/utils/futures.ts b/sdk/utils/futures.ts index 79d66c8970..221c4adcbc 100644 --- a/sdk/utils/futures.ts +++ b/sdk/utils/futures.ts @@ -1,9 +1,29 @@ import Wei, { wei } from '@synthetixio/wei'; import { BigNumber } from 'ethers'; +import { ETH_UNIT } from 'constants/network'; +import { FuturesAggregateStatResult } from 'queries/futures/subgraph'; import { FUTURES_ENDPOINTS, MAINNET_MARKETS, TESTNET_MARKETS } from 'sdk/constants/futures'; import { SECONDS_PER_DAY } from 'sdk/constants/period'; -import { FundingRateUpdate, FuturesMarketAsset, MarketClosureReason } from 'sdk/types/futures'; +import { + FundingRateUpdate, + FuturesMarketAsset, + FuturesMarketKey, + FuturesPosition, + FuturesPotentialTradeDetails, + FuturesVolumes, + MarketClosureReason, + PositionDetail, + PositionSide, + PostTradeDetailsResponse, + PotentialTradeStatus, +} from 'sdk/types/futures'; +import { + CrossMarginOrderType, + CrossMarginSettings, + IsolatedMarginOrderType, +} from 'state/futures/types'; +import { zeroBN } from 'utils/formatters/number'; import logError from 'utils/logError'; export const getFuturesEndpoint = (networkId: number): string => { @@ -100,3 +120,167 @@ export const getReasonFromCode = ( return 'unknown'; } }; + +export const calculateVolumes = ( + futuresHourlyStats: FuturesAggregateStatResult[] +): FuturesVolumes => { + const volumes: FuturesVolumes = futuresHourlyStats.reduce( + (acc: FuturesVolumes, { asset, volume, trades }) => { + return { + ...acc, + [asset]: { + volume: volume.div(ETH_UNIT).add(acc[asset]?.volume ?? 0), + trades: trades.add(acc[asset]?.trades ?? 0), + }, + }; + }, + {} + ); + return volumes; +}; + +export const mapFuturesPosition = ( + positionDetail: PositionDetail, + canLiquidatePosition: boolean, + asset: FuturesMarketAsset, + marketKey: FuturesMarketKey +): FuturesPosition => { + const { + remainingMargin, + accessibleMargin, + position: { fundingIndex, lastPrice, size, margin }, + accruedFunding, + notionalValue, + liquidationPrice, + profitLoss, + } = positionDetail; + const initialMargin = wei(margin); + const pnl = wei(profitLoss).add(wei(accruedFunding)); + const pnlPct = initialMargin.gt(0) ? pnl.div(wei(initialMargin)) : wei(0); + + return { + asset, + marketKey, + remainingMargin: wei(remainingMargin), + accessibleMargin: wei(accessibleMargin), + position: wei(size).eq(zeroBN) + ? null + : { + canLiquidatePosition: !!canLiquidatePosition, + side: wei(size).gt(zeroBN) ? PositionSide.LONG : PositionSide.SHORT, + notionalValue: wei(notionalValue).abs(), + accruedFunding: wei(accruedFunding), + initialMargin, + profitLoss: wei(profitLoss), + fundingIndex: Number(fundingIndex), + lastPrice: wei(lastPrice), + size: wei(size).abs(), + liquidationPrice: wei(liquidationPrice), + initialLeverage: initialMargin.gt(0) + ? wei(size).mul(wei(lastPrice)).div(initialMargin).abs() + : wei(0), + pnl, + pnlPct, + marginRatio: wei(notionalValue).eq(zeroBN) + ? zeroBN + : wei(remainingMargin).div(wei(notionalValue).abs()), + leverage: wei(remainingMargin).eq(zeroBN) + ? zeroBN + : wei(notionalValue).div(wei(remainingMargin)).abs(), + }, + }; +}; + +export const serializePotentialTrade = ( + preview: FuturesPotentialTradeDetails +): FuturesPotentialTradeDetails => ({ + ...preview, + size: preview.size.toString(), + sizeDelta: preview.sizeDelta.toString(), + liqPrice: preview.liqPrice.toString(), + margin: preview.margin.toString(), + price: preview.price.toString(), + fee: preview.fee.toString(), + leverage: preview.leverage.toString(), + notionalValue: preview.notionalValue.toString(), +}); + +export const unserializePotentialTrade = ( + preview: FuturesPotentialTradeDetails +): FuturesPotentialTradeDetails => ({ + ...preview, + size: wei(preview.size), + sizeDelta: wei(preview.sizeDelta), + liqPrice: wei(preview.liqPrice), + margin: wei(preview.margin), + price: wei(preview.price), + fee: wei(preview.fee), + leverage: wei(preview.leverage), + notionalValue: wei(preview.notionalValue), +}); + +export const formatPotentialTrade = ( + preview: PostTradeDetailsResponse, + nativeSizeDelta: Wei, + leverageSide: PositionSide +) => { + const { fee, liqPrice, margin, price, size, status } = preview; + + return { + fee: wei(fee), + liqPrice: wei(liqPrice), + margin: wei(margin), + price: wei(price), + size: wei(size), + sizeDelta: nativeSizeDelta, + side: leverageSide, + leverage: wei(margin).eq(0) ? wei(0) : wei(size).mul(wei(price)).div(wei(margin)).abs(), + notionalValue: wei(size).mul(wei(price)), + status, + showStatus: status > 0, // 0 is success + statusMessage: getTradeStatusMessage(status), + }; +}; + +const SUCCESS = 'Success'; +const UNKNOWN = 'Unknown'; + +export const getTradeStatusMessage = (status: PotentialTradeStatus): string => { + if (typeof status !== 'number') { + return UNKNOWN; + } + + if (status === 0) { + return SUCCESS; + } else if (PotentialTradeStatus[status]) { + return POTENTIAL_TRADE_STATUS_TO_MESSAGE[PotentialTradeStatus[status]]; + } else { + return UNKNOWN; + } +}; + +// https://github.com/Synthetixio/synthetix/blob/4d2add4f74c68ac4f1106f6e7be4c31d4f1ccc76/contracts/PerpsV2MarketBase.sol#L130-L141 +export const POTENTIAL_TRADE_STATUS_TO_MESSAGE: { [key: string]: string } = { + INVALID_PRICE: 'Invalid price', + PRICE_OUT_OF_BOUNDS: 'Price out of acceptable range', + CAN_LIQUIDATE: 'Position can be liquidated', + CANNOT_LIQUIDATE: 'Position cannot be liquidated', + MAX_MARKET_SIZE_EXCEEDED: 'Max market size exceeded', + MAX_LEVERAGE_EXCEEDED: 'Max leverage exceeded', + INSUFFICIENT_MARGIN: 'Insufficient margin', + NOT_PERMITTED: 'Not permitted by this address', + NIL_ORDER: 'Cannot submit empty order', + NO_POSITION_OPEN: 'No position open', + PRICE_TOO_VOLATILE: 'Price too volatile', +}; + +export const calculateCrossMarginFee = ( + orderType: CrossMarginOrderType | IsolatedMarginOrderType, + susdSize: Wei, + feeRates: CrossMarginSettings +) => { + if (orderType !== 'limit' && orderType !== 'stop market') return zeroBN; + const advancedOrderFeeRate = + orderType === 'limit' ? feeRates.limitOrderFee : feeRates.stopOrderFee; + return susdSize.mul(advancedOrderFeeRate); +}; diff --git a/sections/dashboard/FuturesHistoryTable/FuturesHistoryTable.tsx b/sections/dashboard/FuturesHistoryTable/FuturesHistoryTable.tsx index e0b1e3c080..89ee48a5c0 100644 --- a/sections/dashboard/FuturesHistoryTable/FuturesHistoryTable.tsx +++ b/sections/dashboard/FuturesHistoryTable/FuturesHistoryTable.tsx @@ -5,7 +5,6 @@ import Link from 'next/link'; import { FC, useMemo, ReactElement, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { CellProps } from 'react-table'; -import { useRecoilValue } from 'recoil'; import styled from 'styled-components'; import Currency from 'components/Currency'; @@ -25,7 +24,8 @@ import { FuturesTrade } from 'queries/futures/types'; import useGetAllFuturesTradesForAccount from 'queries/futures/useGetAllFuturesTradesForAccount'; import TradeDrawer from 'sections/futures/MobileTrade/drawers/TradeDrawer'; import { TradeStatus } from 'sections/futures/types'; -import { futuresAccountTypeState } from 'store/futures'; +import { selectFuturesType } from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; import { formatShortDateWithoutYear } from 'utils/formatters/date'; import { formatCryptoCurrency, formatDollars } from 'utils/formatters/number'; import { @@ -40,7 +40,7 @@ import TimeDisplay from '../../futures/Trades/TimeDisplay'; const FuturesHistoryTable: FC = () => { const [selectedTrade, setSelectedTrade] = useState(); - const accountType = useRecoilValue(futuresAccountTypeState); + const accountType = useAppSelector(selectFuturesType); const { walletAddress } = Connector.useContainer(); const { t } = useTranslation(); diff --git a/sections/dashboard/FuturesMarketsTable/FuturesMarketsTable.tsx b/sections/dashboard/FuturesMarketsTable/FuturesMarketsTable.tsx index 5decf25275..393ee36e83 100644 --- a/sections/dashboard/FuturesMarketsTable/FuturesMarketsTable.tsx +++ b/sections/dashboard/FuturesMarketsTable/FuturesMarketsTable.tsx @@ -14,15 +14,15 @@ import Table from 'components/Table'; import { DEFAULT_CRYPTO_DECIMALS } from 'constants/defaults'; import ROUTES from 'constants/routes'; import Connector from 'containers/Connector'; -import { FundingRateResponse } from 'queries/futures/useGetAverageFundingRateForMarkets'; -import { selectMarkets } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; +import { FundingRateResponse } from 'sdk/types/futures'; import { - pastRatesState, - fundingRatesState, - futuresVolumesState, - futuresAccountTypeState, -} from 'store/futures'; + selectAverageFundingRates, + selectFuturesType, + selectMarkets, + selectMarketVolumes, +} from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; +import { pastRatesState } from 'store/futures'; import { getSynthDescription, MarketKeyByAsset, FuturesMarketAsset } from 'utils/futures'; const FuturesMarketsTable: FC = () => { @@ -31,10 +31,10 @@ const FuturesMarketsTable: FC = () => { const { synthsMap } = Connector.useContainer(); const futuresMarkets = useAppSelector(selectMarkets); - const fundingRates = useRecoilValue(fundingRatesState); + const fundingRates = useAppSelector(selectAverageFundingRates); const pastRates = useRecoilValue(pastRatesState); - const futuresVolumes = useRecoilValue(futuresVolumesState); - const accountType = useRecoilValue(futuresAccountTypeState); + const futuresVolumes = useAppSelector(selectMarketVolumes); + const accountType = useAppSelector(selectFuturesType); let data = useMemo(() => { return futuresMarkets.map((market) => { diff --git a/sections/dashboard/FuturesPositionsTable/FuturesPositionsTable.tsx b/sections/dashboard/FuturesPositionsTable/FuturesPositionsTable.tsx index c1b897b3f8..912492590f 100644 --- a/sections/dashboard/FuturesPositionsTable/FuturesPositionsTable.tsx +++ b/sections/dashboard/FuturesPositionsTable/FuturesPositionsTable.tsx @@ -19,9 +19,14 @@ import Connector from 'containers/Connector'; import useIsL2 from 'hooks/useIsL2'; import useNetworkSwitcher from 'hooks/useNetworkSwitcher'; import { FuturesAccountType } from 'queries/futures/subgraph'; -import { selectMarketAsset, selectMarkets } from 'state/futures/selectors'; +import { + selectCrossMarginPositions, + selectIsolatedMarginPositions, + selectMarketAsset, + selectMarkets, +} from 'state/futures/selectors'; import { useAppSelector } from 'state/hooks'; -import { positionsState, positionHistoryState } from 'store/futures'; +import { positionHistoryState } from 'store/futures'; import { formatNumber } from 'utils/formatters/number'; import { getSynthDescription } from 'utils/futures'; @@ -43,13 +48,15 @@ const FuturesPositionsTable: FC = ({ const isL2 = useIsL2(); - const positions = useRecoilValue(positionsState); + const isolatedPositions = useAppSelector(selectIsolatedMarginPositions); + const crossMarginPositions = useAppSelector(selectCrossMarginPositions); const positionHistory = useRecoilValue(positionHistoryState); const currentMarket = useAppSelector(selectMarketAsset); const futuresMarkets = useAppSelector(selectMarkets); let data = useMemo(() => { - return positions[accountType] + const positions = accountType === 'cross_margin' ? crossMarginPositions : isolatedPositions; + return positions .map((position) => { const market = futuresMarkets.find((market) => market.asset === position.asset); const description = getSynthDescription(position.asset, synthsMap, t); @@ -69,7 +76,8 @@ const FuturesPositionsTable: FC = ({ position.position && (position?.market?.asset !== currentMarket || showCurrentMarket) ); }, [ - positions, + isolatedPositions, + crossMarginPositions, accountType, futuresMarkets, positionHistory, diff --git a/sections/dashboard/MobileDashboard/FuturesMarkets.tsx b/sections/dashboard/MobileDashboard/FuturesMarkets.tsx index 6583949d73..d63dceb22f 100644 --- a/sections/dashboard/MobileDashboard/FuturesMarkets.tsx +++ b/sections/dashboard/MobileDashboard/FuturesMarkets.tsx @@ -1,12 +1,10 @@ import { wei } from '@synthetixio/wei'; import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilValue } from 'recoil'; import { SectionHeader, SectionTitle } from 'sections/futures/MobileTrade/common'; -import { selectMarkets } from 'state/futures/selectors'; +import { selectMarkets, selectMarketVolumes } from 'state/futures/selectors'; import { useAppSelector } from 'state/hooks'; -import { futuresVolumesState } from 'store/futures'; import { formatDollars, formatNumber, zeroBN } from 'utils/formatters/number'; import FuturesMarketsTable from '../FuturesMarketsTable'; @@ -16,7 +14,7 @@ const FuturesMarkets = () => { const { t } = useTranslation(); const futuresMarkets = useAppSelector(selectMarkets); - const futuresVolumes = useRecoilValue(futuresVolumesState); + const futuresVolumes = useAppSelector(selectMarketVolumes); const openInterest = useMemo(() => { return ( diff --git a/sections/dashboard/MobileDashboard/OpenPositions.tsx b/sections/dashboard/MobileDashboard/OpenPositions.tsx index 81b6622a0f..1100176b95 100644 --- a/sections/dashboard/MobileDashboard/OpenPositions.tsx +++ b/sections/dashboard/MobileDashboard/OpenPositions.tsx @@ -1,14 +1,20 @@ import Wei from '@synthetixio/wei'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { SetterOrUpdater, useRecoilValue } from 'recoil'; +import { SetterOrUpdater } from 'recoil'; import styled from 'styled-components'; import TabButton from 'components/Button/TabButton'; import { TabPanel } from 'components/Tab'; import { FuturesAccountTypes } from 'queries/futures/types'; import { SectionHeader, SectionTitle } from 'sections/futures/MobileTrade/common'; -import { balancesState, portfolioState, positionsState } from 'store/futures'; +import { selectBalances } from 'state/balances/selectors'; +import { + selectCrossMarginPositions, + selectFuturesPortfolio, + selectIsolatedMarginPositions, +} from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; import { formatDollars } from 'utils/formatters/number'; import FuturesPositionsTable from '../FuturesPositionsTable'; @@ -37,16 +43,17 @@ const OpenPositions: React.FC = ({ exchangeTokenBalances, }) => { const { t } = useTranslation(); - const positions = useRecoilValue(positionsState); - const portfolio = useRecoilValue(portfolioState); - const balances = useRecoilValue(balancesState); + const crossPositions = useAppSelector(selectCrossMarginPositions); + const isolatedPositions = useAppSelector(selectIsolatedMarginPositions); + const portfolio = useAppSelector(selectFuturesPortfolio); + const balances = useAppSelector(selectBalances); const POSITIONS_TABS = useMemo( () => [ { name: PositionsTab.CROSS_MARGIN, label: t('dashboard.overview.positions-tabs.cross-margin'), - badge: positions[FuturesAccountTypes.CROSS_MARGIN].length, + badge: crossPositions.length, active: activePositionsTab === PositionsTab.CROSS_MARGIN, detail: formatDollars(portfolio.crossMarginFutures), disabled: false, @@ -55,7 +62,7 @@ const OpenPositions: React.FC = ({ { name: PositionsTab.ISOLATED_MARGIN, label: t('dashboard.overview.positions-tabs.isolated-margin'), - badge: positions[FuturesAccountTypes.ISOLATED_MARGIN].length, + badge: isolatedPositions.length, active: activePositionsTab === PositionsTab.ISOLATED_MARGIN, detail: formatDollars(portfolio.isolatedMarginFutures), disabled: false, @@ -72,7 +79,8 @@ const OpenPositions: React.FC = ({ ], [ t, - positions, + isolatedPositions, + crossPositions, activePositionsTab, portfolio.crossMarginFutures, portfolio.isolatedMarginFutures, diff --git a/sections/dashboard/Overview/Overview.tsx b/sections/dashboard/Overview/Overview.tsx index a321a65aba..07afc30ea9 100644 --- a/sections/dashboard/Overview/Overview.tsx +++ b/sections/dashboard/Overview/Overview.tsx @@ -1,7 +1,7 @@ import Wei from '@synthetixio/wei'; import { FC, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilState, useRecoilValue } from 'recoil'; +import { useRecoilState } from 'recoil'; import styled from 'styled-components'; import { erc20ABI, useContractRead } from 'wagmi'; @@ -15,9 +15,14 @@ import Connector from 'containers/Connector'; import useIsL2 from 'hooks/useIsL2'; import { FuturesAccountTypes } from 'queries/futures/types'; import { CompetitionBanner } from 'sections/shared/components/CompetitionBanner'; +import { selectBalances } from 'state/balances/selectors'; import { sdk } from 'state/config'; +import { + selectActiveCrossPositionsCount, + selectActiveIsolatedPositionsCount, + selectFuturesPortfolio, +} from 'state/futures/selectors'; import { useAppSelector } from 'state/hooks'; -import { balancesState, portfolioState, positionsState } from 'store/futures'; import { activePositionsTabState } from 'store/ui'; import { formatDollars, toWei, weiFromWei, zeroBN } from 'utils/formatters/number'; import logError from 'utils/logError'; @@ -41,9 +46,10 @@ export enum PositionsTab { const Overview: FC = () => { const { t } = useTranslation(); - const balances = useRecoilValue(balancesState); - const portfolio = useRecoilValue(portfolioState); - const positions = useRecoilValue(positionsState); + const balances = useAppSelector(selectBalances); + const portfolio = useAppSelector(selectFuturesPortfolio); + const isolatedPositionsCount = useAppSelector(selectActiveIsolatedPositionsCount); + const crossPositionsCount = useAppSelector(selectActiveCrossPositionsCount); const [activePositionsTab, setActivePositionsTab] = useRecoilState( activePositionsTabState @@ -163,8 +169,6 @@ const Overview: FC = () => { }, [kwentaBalance, noKwentaFound, oneInchEnabled, synthsMap, tokenBalances]); const POSITIONS_TABS = useMemo(() => { - const crossPositions = positions.cross_margin.filter(({ position }) => !!position).length; - const isolatedPositions = positions.isolated_margin.filter(({ position }) => !!position).length; const exchangeTokenBalances = exchangeTokens.reduce( (initial: Wei, { usdBalance }: { usdBalance: Wei }) => initial.add(usdBalance), zeroBN @@ -173,7 +177,7 @@ const Overview: FC = () => { { name: PositionsTab.CROSS_MARGIN, label: t('dashboard.overview.positions-tabs.cross-margin'), - badge: crossPositions, + badge: crossPositionsCount, titleIcon: , active: activePositionsTab === PositionsTab.CROSS_MARGIN, detail: formatDollars(portfolio.crossMarginFutures), @@ -183,7 +187,7 @@ const Overview: FC = () => { { name: PositionsTab.ISOLATED_MARGIN, label: t('dashboard.overview.positions-tabs.isolated-margin'), - badge: isolatedPositions, + badge: isolatedPositionsCount, active: activePositionsTab === PositionsTab.ISOLATED_MARGIN, titleIcon: , detail: formatDollars(portfolio.isolatedMarginFutures), @@ -200,8 +204,8 @@ const Overview: FC = () => { }, ]; }, [ - positions.cross_margin, - positions.isolated_margin, + crossPositionsCount, + isolatedPositionsCount, exchangeTokens, balances.totalUSDBalance, t, diff --git a/sections/dashboard/PortfolioChart/PortfolioChart.tsx b/sections/dashboard/PortfolioChart/PortfolioChart.tsx index 6c5553da3f..1e8f7fe847 100644 --- a/sections/dashboard/PortfolioChart/PortfolioChart.tsx +++ b/sections/dashboard/PortfolioChart/PortfolioChart.tsx @@ -1,19 +1,20 @@ import Wei from '@synthetixio/wei'; import { FC, useMemo } from 'react'; -import { useRecoilValue } from 'recoil'; import styled from 'styled-components'; import Currency from 'components/Currency'; import { MobileHiddenView, MobileOnlyView } from 'components/Media'; -import { balancesState, portfolioState } from 'store/futures'; +import { selectBalances } from 'state/balances/selectors'; +import { selectFuturesPortfolio } from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; type PortfolioChartProps = { exchangeTokenBalances: Wei; }; const PortfolioChart: FC = ({ exchangeTokenBalances }) => { - const portfolio = useRecoilValue(portfolioState); - const balances = useRecoilValue(balancesState); + const portfolio = useAppSelector(selectFuturesPortfolio); + const balances = useAppSelector(selectBalances); const total = useMemo( () => portfolio.total.add(balances.totalUSDBalance).add(exchangeTokenBalances), diff --git a/sections/dashboard/SynthBalancesTable/SynthBalancesTable.tsx b/sections/dashboard/SynthBalancesTable/SynthBalancesTable.tsx index cb53b6602c..c30ebbab40 100644 --- a/sections/dashboard/SynthBalancesTable/SynthBalancesTable.tsx +++ b/sections/dashboard/SynthBalancesTable/SynthBalancesTable.tsx @@ -15,9 +15,10 @@ import Table, { TableNoResults } from 'components/Table'; import { NO_VALUE } from 'constants/placeholder'; import Connector from 'containers/Connector'; import { Price } from 'queries/rates/types'; +import { selectBalances } from 'state/balances/selectors'; import { selectExchangeRates } from 'state/exchange/selectors'; import { useAppSelector } from 'state/hooks'; -import { balancesState, pastRatesState } from 'store/futures'; +import { pastRatesState } from 'store/futures'; import { sortWei } from 'utils/balances'; import { formatNumber, zeroBN } from 'utils/formatters/number'; import { isDecimalFour } from 'utils/futures'; @@ -60,10 +61,10 @@ const SynthBalancesTable: FC = ({ exchangeTokens }) => const { synthsMap } = Connector.useContainer(); const pastRates = useRecoilValue(pastRatesState); const exchangeRates = useAppSelector(selectExchangeRates); - const { balances } = useRecoilValue(balancesState); + const { synthBalances } = useAppSelector(selectBalances); const synthTokens = useMemo(() => { - return balances.map((synthBalance: SynthBalance) => { + return synthBalances.map((synthBalance: SynthBalance) => { const { currencyKey, balance, usdBalance } = synthBalance; const price = exchangeRates && exchangeRates[currencyKey]; @@ -79,7 +80,7 @@ const SynthBalancesTable: FC = ({ exchangeTokens }) => priceChange: calculatePriceChange(price, pastPrice), }; }); - }, [pastRates, exchangeRates, balances, synthsMap]); + }, [pastRates, exchangeRates, synthBalances, synthsMap]); const data = [...exchangeTokens, ...synthTokens].sort((a, b) => sortWei(a.usdBalance, b.usdBalance, 'descending') diff --git a/sections/futures/CrossMarginOnboard/CrossMarginOnboard.tsx b/sections/futures/CrossMarginOnboard/CrossMarginOnboard.tsx index 6350c923a0..1081c537da 100644 --- a/sections/futures/CrossMarginOnboard/CrossMarginOnboard.tsx +++ b/sections/futures/CrossMarginOnboard/CrossMarginOnboard.tsx @@ -4,7 +4,7 @@ import { constants } from 'ethers'; import { defaultAbiCoder } from 'ethers/lib/utils'; import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilState, useRecoilValue } from 'recoil'; +import { useRecoilState } from 'recoil'; import styled from 'styled-components'; import CompleteCheck from 'assets/svg/futures/onboard-complete-check.svg'; @@ -26,7 +26,9 @@ import { FuturesAccountState } from 'queries/futures/types'; import useQueryCrossMarginAccount, { useStoredCrossMarginAccounts, } from 'queries/futures/useQueryCrossMarginAccount'; -import { balancesState, futuresAccountState } from 'store/futures'; +import { selectBalances } from 'state/balances/selectors'; +import { useAppSelector } from 'state/hooks'; +import { futuresAccountState } from 'store/futures'; import { FlexDivRowCentered } from 'styles/common'; import { isUserDeniedError } from 'utils/formatters/error'; import { zeroBN } from 'utils/formatters/number'; @@ -59,7 +61,7 @@ export default function CrossMarginOnboard({ onClose, isOpen }: Props) { const queryCrossMarginAccount = useQueryCrossMarginAccount(); const { storeCrossMarginAccount } = useStoredCrossMarginAccounts(); const [futuresAccount, setFuturesAccount] = useRecoilState(futuresAccountState); - const balances = useRecoilValue(balancesState); + const balances = useAppSelector(selectBalances); const [depositAmount, setDepositAmount] = useState(''); const [depositComplete, setDepositComplete] = useState(false); diff --git a/sections/futures/FeeInfoBox/FeeInfoBox.tsx b/sections/futures/FeeInfoBox/FeeInfoBox.tsx index c2b05ba1eb..d1471ad12b 100644 --- a/sections/futures/FeeInfoBox/FeeInfoBox.tsx +++ b/sections/futures/FeeInfoBox/FeeInfoBox.tsx @@ -1,39 +1,40 @@ import React, { FC, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilValue } from 'recoil'; import styled from 'styled-components'; import TimerIcon from 'assets/svg/app/timer.svg'; import InfoBox, { DetailedInfo } from 'components/InfoBox/InfoBox'; import StyledTooltip from 'components/Tooltip/StyledTooltip'; import { NO_VALUE } from 'constants/placeholder'; -import { selectMarketInfo } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; import { - tradeFeesState, - orderTypeState, - sizeDeltaState, - futuresAccountTypeState, - crossMarginSettingsState, - dynamicFeeRateState, -} from 'store/futures'; + selectCrossMarginSettings, + selectCrossMarginTradeFees, + selectDynamicFeeRate, + selectFuturesType, + selectIsolatedMarginFee, + selectMarketInfo, + selectOrderType, + selectTradeSizeInputs, +} from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; import { computeNPFee, computeMarketFee } from 'utils/costCalculations'; import { formatCurrency, formatDollars, formatPercent, zeroBN } from 'utils/formatters/number'; const FeeInfoBox: React.FC = () => { - const orderType = useRecoilValue(orderTypeState); - const fees = useRecoilValue(tradeFeesState); - const dynamicFeeRate = useRecoilValue(dynamicFeeRateState); - const sizeDelta = useRecoilValue(sizeDeltaState); - const accountType = useRecoilValue(futuresAccountTypeState); - const { tradeFee: crossMarginTradeFee, limitOrderFee, stopOrderFee } = useRecoilValue( - crossMarginSettingsState + const orderType = useAppSelector(selectOrderType); + const crossMarginFees = useAppSelector(selectCrossMarginTradeFees); + const isolatedMarginFee = useAppSelector(selectIsolatedMarginFee); + const dynamicFeeRate = useAppSelector(selectDynamicFeeRate); + const { nativeSizeDelta } = useAppSelector(selectTradeSizeInputs); + const accountType = useAppSelector(selectFuturesType); + const { tradeFee: crossMarginTradeFeeRate, limitOrderFee, stopOrderFee } = useAppSelector( + selectCrossMarginSettings ); const marketInfo = useAppSelector(selectMarketInfo); - const { commitDeposit, nextPriceFee } = useMemo(() => computeNPFee(marketInfo, sizeDelta), [ + const { commitDeposit, nextPriceFee } = useMemo(() => computeNPFee(marketInfo, nativeSizeDelta), [ marketInfo, - sizeDelta, + nativeSizeDelta, ]); const totalDeposit = useMemo(() => { @@ -44,9 +45,9 @@ const FeeInfoBox: React.FC = () => { return (nextPriceFee ?? zeroBN).sub(commitDeposit ?? zeroBN); }, [commitDeposit, nextPriceFee]); - const staticRate = useMemo(() => computeMarketFee(marketInfo, sizeDelta), [ + const staticRate = useMemo(() => computeMarketFee(marketInfo, nativeSizeDelta), [ marketInfo, - sizeDelta, + nativeSizeDelta, ]); const orderFeeRate = useMemo( @@ -75,31 +76,31 @@ const FeeInfoBox: React.FC = () => { const feesInfo = useMemo>(() => { const crossMarginFeeInfo = { 'Protocol Fee': { - value: formatDollars(fees.staticFee, { - minDecimals: fees.staticFee.lt(0.01) ? 4 : 2, + value: formatDollars(crossMarginFees.staticFee, { + minDecimals: crossMarginFees.staticFee.lt(0.01) ? 4 : 2, }), keyNode: marketCostTooltip, }, 'Limit / Stop Fee': - fees.limitStopOrderFee.gt(0) && orderFeeRate + crossMarginFees.limitStopOrderFee.gt(0) && orderFeeRate ? { - value: formatDollars(fees.limitStopOrderFee, { - minDecimals: fees.limitStopOrderFee.lt(0.01) ? 4 : 2, + value: formatDollars(crossMarginFees.limitStopOrderFee, { + minDecimals: crossMarginFees.limitStopOrderFee.lt(0.01) ? 4 : 2, }), keyNode: formatPercent(orderFeeRate), } : null, 'Cross Margin Fee': { - value: formatDollars(fees.crossMarginFee, { - minDecimals: fees.crossMarginFee.lt(0.01) ? 4 : 2, + value: formatDollars(crossMarginFees.crossMarginFee, { + minDecimals: crossMarginFees.crossMarginFee.lt(0.01) ? 4 : 2, }), spaceBeneath: true, - keyNode: formatPercent(crossMarginTradeFee), + keyNode: formatPercent(crossMarginTradeFeeRate), }, 'Total Fee': { - value: formatDollars(fees.total, { - minDecimals: fees.total.lt(0.01) ? 4 : 2, + value: formatDollars(crossMarginFees.total, { + minDecimals: crossMarginFees.total.lt(0.01) ? 4 : 2, }), }, }; @@ -108,7 +109,7 @@ const FeeInfoBox: React.FC = () => { ...crossMarginFeeInfo, 'Keeper Deposit': { value: !!marketInfo?.keeperDeposit - ? formatCurrency('ETH', fees.keeperEthDeposit, { currencyKey: 'ETH' }) + ? formatCurrency('ETH', crossMarginFees.keeperEthDeposit, { currencyKey: 'ETH' }) : NO_VALUE, }, }; @@ -133,15 +134,15 @@ const FeeInfoBox: React.FC = () => { }, 'Estimated Fees': { value: formatDollars(totalDeposit.add(nextPriceDiscount ?? zeroBN)), - keyNode: fees.dynamicFeeRate?.gt(0) ? : null, + keyNode: dynamicFeeRate?.gt(0) ? : null, }, }; } return accountType === 'isolated_margin' ? { Fee: { - value: formatDollars(fees.total, { - minDecimals: fees.total.lt(0.01) ? 4 : 2, + value: formatDollars(isolatedMarginFee, { + minDecimals: isolatedMarginFee.lt(0.01) ? 4 : 2, }), keyNode: marketCostTooltip, }, @@ -149,9 +150,11 @@ const FeeInfoBox: React.FC = () => { : crossMarginFeeInfo; }, [ orderType, - crossMarginTradeFee, - fees, + crossMarginTradeFeeRate, + isolatedMarginFee, + crossMarginFees, orderFeeRate, + dynamicFeeRate, commitDeposit, accountType, marketInfo?.keeperDeposit, diff --git a/sections/futures/LeverageInput/LeverageInput.tsx b/sections/futures/LeverageInput/LeverageInput.tsx index 710f6b7def..1571771e26 100644 --- a/sections/futures/LeverageInput/LeverageInput.tsx +++ b/sections/futures/LeverageInput/LeverageInput.tsx @@ -1,39 +1,56 @@ -import { FC, useMemo, useState } from 'react'; +import { wei } from '@synthetixio/wei'; +import { FC, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useSetRecoilState, useRecoilValue } from 'recoil'; import styled from 'styled-components'; import Button from 'components/Button'; import CustomNumericInput from 'components/Input/CustomNumericInput'; import { DEFAULT_FIAT_DECIMALS } from 'constants/defaults'; -import { useFuturesContext } from 'contexts/FuturesContext'; -import { selectMarketInfo, selectMaxLeverage } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; +import { editIsolatedMarginSize } from 'state/futures/actions'; +import { setIsolatedMarginLeverageInput } from 'state/futures/reducer'; import { - leverageValueCommittedState, - nextPriceDisclaimerState, - orderTypeState, - positionState, - futuresTradeInputsState, -} from 'store/futures'; + selectIsolatedLeverageInput, + selectIsolatedMarginLeverage, + selectMarketAssetRate, + selectMarketInfo, + selectMaxLeverage, + selectNextPriceDisclaimer, + selectOrderType, + selectPosition, +} from 'state/futures/selectors'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; import { FlexDivCol, FlexDivRow } from 'styles/common'; -import { truncateNumbers } from 'utils/formatters/number'; +import { floorNumber, truncateNumbers, zeroBN } from 'utils/formatters/number'; import LeverageSlider from '../LeverageSlider'; const LeverageInput: FC = () => { const { t } = useTranslation(); + const dispatch = useAppDispatch(); const [mode, setMode] = useState<'slider' | 'input'>('input'); - const { leverage } = useRecoilValue(futuresTradeInputsState); - const orderType = useRecoilValue(orderTypeState); - const isDisclaimerDisplayed = useRecoilValue(nextPriceDisclaimerState); - const setIsLeverageValueCommitted = useSetRecoilState(leverageValueCommittedState); - const position = useRecoilValue(positionState); - + const leverage = useAppSelector(selectIsolatedMarginLeverage); + const orderType = useAppSelector(selectOrderType); + const isDisclaimerDisplayed = useAppSelector(selectNextPriceDisclaimer); + const position = useAppSelector(selectPosition); const marketInfo = useAppSelector(selectMarketInfo); const maxLeverage = useAppSelector(selectMaxLeverage); - - const { onLeverageChange } = useFuturesContext(); + const marketAssetRate = useAppSelector(selectMarketAssetRate); + const leverageInput = useAppSelector(selectIsolatedLeverageInput); + + const onLeverageChange = useCallback( + (newLeverage: number) => { + const remainingMargin = position?.remainingMargin ?? zeroBN; + const newTradeSize = + marketAssetRate.eq(0) || remainingMargin.eq(0) + ? '' + : wei(newLeverage).mul(remainingMargin).div(marketAssetRate).toString(); + const input = truncateNumbers(newLeverage, DEFAULT_FIAT_DECIMALS); + dispatch(setIsolatedMarginLeverageInput(input)); + const floored = floorNumber(Number(newTradeSize), 4); + dispatch(editIsolatedMarginSize(String(floored), 'native')); + }, + [position?.remainingMargin, marketAssetRate, dispatch] + ); const modeButton = useMemo(() => { return ( @@ -79,22 +96,19 @@ const LeverageInput: FC = () => { maxValue={Number(truncateMaxLeverage)} value={Number(truncateLeverage)} onChange={(_, newValue) => { - setIsLeverageValueCommitted(false); onLeverageChange(newValue as number); }} - onChangeCommitted={() => setIsLeverageValueCommitted(true)} /> ) : ( { - setIsLeverageValueCommitted(true); onLeverageChange(Number(newValue)); }} disabled={isDisabled} diff --git a/sections/futures/MarketDetails/useGetMarketData.ts b/sections/futures/MarketDetails/useGetMarketData.ts index f022b254fd..637984e29b 100644 --- a/sections/futures/MarketDetails/useGetMarketData.ts +++ b/sections/futures/MarketDetails/useGetMarketData.ts @@ -12,9 +12,10 @@ import { selectMarketAsset, selectMarketInfo, selectMarketKey, + selectMarketVolumes, } from 'state/futures/selectors'; import { useAppSelector } from 'state/hooks'; -import { futuresVolumesState, pastRatesState } from 'store/futures'; +import { pastRatesState } from 'store/futures'; import { isFiatCurrency } from 'utils/currencies'; import { formatCurrency, formatPercent, zeroBN } from 'utils/formatters/number'; import { isDecimalFour } from 'utils/futures'; @@ -32,7 +33,7 @@ const useGetMarketData = (mobile?: boolean) => { const marketInfo = useAppSelector(selectMarketInfo); const pastRates = useRecoilValue(pastRatesState); - const futuresVolumes = useRecoilValue(futuresVolumesState); + const futuresVolumes = useAppSelector(selectMarketVolumes); const { selectedPriceCurrency } = useSelectedPriceCurrency(); diff --git a/sections/futures/MarketInfoBox/MarketInfoBox.tsx b/sections/futures/MarketInfoBox/MarketInfoBox.tsx index 120dc11e1a..3ffba5630e 100644 --- a/sections/futures/MarketInfoBox/MarketInfoBox.tsx +++ b/sections/futures/MarketInfoBox/MarketInfoBox.tsx @@ -1,33 +1,31 @@ import Wei, { wei } from '@synthetixio/wei'; -import React, { useMemo } from 'react'; -import { useRecoilValue } from 'recoil'; +import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import InfoBox from 'components/InfoBox'; import PreviewArrow from 'components/PreviewArrow'; -import { FuturesPotentialTradeDetails } from 'queries/futures/types'; -import { selectMarketInfo, selectMaxLeverage } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; +import { FuturesPotentialTradeDetails } from 'sdk/types/futures'; import { - leverageSideState, - orderTypeState, - positionState, - potentialTradeDetailsState, - futuresTradeInputsState, -} from 'store/futures'; + selectLeverageSide, + selectMarketInfo, + selectMaxLeverage, + selectOrderType, + selectPosition, + selectTradePreview, + selectTradeSizeInputs, +} from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; import { computeNPFee } from 'utils/costCalculations'; import { formatDollars, formatPercent, zeroBN } from 'utils/formatters/number'; -import { PositionSide } from '../types'; - const MarketInfoBox: React.FC = () => { - const position = useRecoilValue(positionState); - const orderType = useRecoilValue(orderTypeState); - const leverageSide = useRecoilValue(leverageSideState); - const { nativeSize } = useRecoilValue(futuresTradeInputsState); - const potentialTrade = useRecoilValue(potentialTradeDetailsState); + const orderType = useAppSelector(selectOrderType); + const leverageSide = useAppSelector(selectLeverageSide); + const { nativeSize, nativeSizeDelta } = useAppSelector(selectTradeSizeInputs); + const potentialTrade = useAppSelector(selectTradePreview); const marketInfo = useAppSelector(selectMarketInfo); + const position = useAppSelector(selectPosition); const maxLeverage = useAppSelector(selectMaxLeverage); const totalMargin = position?.remainingMargin ?? zeroBN; @@ -39,13 +37,18 @@ const MarketInfoBox: React.FC = () => { ? totalMargin.sub(availableMargin).div(totalMargin) : zeroBN; + const minInitialMargin = useMemo(() => marketInfo?.minInitialMargin ?? zeroBN, [ + marketInfo?.minInitialMargin, + ]); + const isNextPriceOrder = orderType === 'next price'; const positionSize = position?.position?.size ? wei(position?.position?.size) : zeroBN; const orderDetails = useMemo(() => { - const newSize = - leverageSide === PositionSide.LONG ? wei(nativeSize || 0) : wei(nativeSize || 0).neg(); - return { newSize, size: (positionSize ?? zeroBN).add(newSize).abs() }; + return { + newSize: nativeSize, + size: (positionSize ?? zeroBN).add(nativeSizeDelta).abs(), + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [leverageSide, positionSize]); @@ -58,44 +61,50 @@ const MarketInfoBox: React.FC = () => { return (commitDeposit ?? zeroBN).add(marketInfo?.keeperDeposit ?? zeroBN); }, [commitDeposit, marketInfo?.keeperDeposit]); - const getPotentialAvailableMargin = ( - trade: FuturesPotentialTradeDetails | null, - marketMaxLeverage: Wei | undefined - ) => { - let inaccessible; + const getPotentialAvailableMargin = useCallback( + (trade: FuturesPotentialTradeDetails | null, marketMaxLeverage: Wei | undefined) => { + let inaccessible; - inaccessible = - (marketMaxLeverage && trade?.notionalValue.div(marketMaxLeverage).abs()) ?? zeroBN; + inaccessible = + (marketMaxLeverage && trade?.notionalValue.div(marketMaxLeverage).abs()) ?? zeroBN; - // If the user has a position open, we'll enforce a min initial margin requirement. - if (inaccessible.gt(0)) { - if (inaccessible.lt(trade?.minInitialMargin ?? zeroBN)) { - inaccessible = trade?.minInitialMargin ?? zeroBN; + // If the user has a position open, we'll enforce a min initial margin requirement. + if (inaccessible.gt(0)) { + if (inaccessible.lt(minInitialMargin)) { + inaccessible = minInitialMargin; + } } - } - // check if available margin will be less than 0 - return trade?.margin?.sub(inaccessible).gt(0) ? trade?.margin?.sub(inaccessible).abs() : zeroBN; - }; + // check if available margin will be less than 0 + return trade?.margin?.sub(inaccessible).gt(0) + ? trade?.margin?.sub(inaccessible).abs() + : zeroBN; + }, + [minInitialMargin] + ); const previewAvailableMargin = React.useMemo(() => { const potentialAvailableMargin = getPotentialAvailableMargin( - potentialTrade.data, + potentialTrade, marketInfo?.maxLeverage ); return isNextPriceOrder ? potentialAvailableMargin?.sub(totalDeposit) ?? zeroBN : potentialAvailableMargin; - }, [potentialTrade.data, marketInfo?.maxLeverage, isNextPriceOrder, totalDeposit]); + }, [ + potentialTrade, + marketInfo?.maxLeverage, + isNextPriceOrder, + totalDeposit, + getPotentialAvailableMargin, + ]); const previewTradeData = React.useMemo(() => { - const size = wei(nativeSize || zeroBN); + const size = nativeSizeDelta.abs(); - const potentialMarginUsage = potentialTrade.data?.margin.gt(0) - ? potentialTrade.data?.margin - ?.sub(previewAvailableMargin) - ?.div(potentialTrade.data?.margin) - ?.abs() ?? zeroBN + const potentialMarginUsage = potentialTrade?.margin.gt(0) + ? potentialTrade!.margin.sub(previewAvailableMargin).div(potentialTrade!.margin).abs() ?? + zeroBN : zeroBN; const potentialBuyingPower = @@ -103,12 +112,12 @@ const MarketInfoBox: React.FC = () => { return { showPreview: size && !size.eq(0), - totalMargin: potentialTrade.data?.margin || zeroBN, + totalMargin: potentialTrade?.margin || zeroBN, availableMargin: previewAvailableMargin.gt(0) ? previewAvailableMargin : zeroBN, buyingPower: potentialBuyingPower.gt(0) ? potentialBuyingPower : zeroBN, marginUsage: potentialMarginUsage.gt(1) ? wei(1) : potentialMarginUsage, }; - }, [nativeSize, potentialTrade.data?.margin, previewAvailableMargin, maxLeverage]); + }, [nativeSizeDelta, potentialTrade, previewAvailableMargin, maxLeverage]); return ( { currencyKey: undefined, })}`, valueNode: ( - + {formatDollars(previewTradeData?.availableMargin)} ), @@ -131,9 +138,7 @@ const MarketInfoBox: React.FC = () => { currencyKey: undefined, })}`, valueNode: previewTradeData?.buyingPower && ( - + {formatDollars(previewTradeData?.buyingPower)} ), @@ -141,9 +146,7 @@ const MarketInfoBox: React.FC = () => { 'Margin Usage': { value: `${formatPercent(marginUsage)}`, valueNode: ( - + {formatPercent(previewTradeData?.marginUsage)} ), diff --git a/sections/futures/MobileTrade/OverviewTabs/AccountTab.tsx b/sections/futures/MobileTrade/OverviewTabs/AccountTab.tsx index 9faec7eddd..5ccf78e6c9 100644 --- a/sections/futures/MobileTrade/OverviewTabs/AccountTab.tsx +++ b/sections/futures/MobileTrade/OverviewTabs/AccountTab.tsx @@ -1,15 +1,15 @@ import React from 'react'; -import { useRecoilValue } from 'recoil'; import MarketInfoBox from 'sections/futures/MarketInfoBox'; import MarketActions from 'sections/futures/Trade/MarketActions'; import MarginInfoBox from 'sections/futures/TradeCrossMargin/CrossMarginInfoBox'; -import { futuresAccountTypeState } from 'store/futures'; +import { selectFuturesType } from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; import { Pane, SectionHeader, SectionTitle } from '../common'; const AccountTab: React.FC = () => { - const accountType = useRecoilValue(futuresAccountTypeState); + const accountType = useAppSelector(selectFuturesType); return ( diff --git a/sections/futures/MobileTrade/PositionDetails.tsx b/sections/futures/MobileTrade/PositionDetails.tsx index e368d2b21a..b72f52f6fc 100644 --- a/sections/futures/MobileTrade/PositionDetails.tsx +++ b/sections/futures/MobileTrade/PositionDetails.tsx @@ -1,14 +1,14 @@ import React from 'react'; -import { useRecoilValue } from 'recoil'; import styled from 'styled-components'; -import { positionState } from 'store/futures'; +import { selectPosition } from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; import PositionCard from '../PositionCard'; import { SectionHeader, SectionSeparator, SectionTitle } from './common'; const PositionDetails = () => { - const position = useRecoilValue(positionState); + const position = useAppSelector(selectPosition); return position?.position ? ( diff --git a/sections/futures/MobileTrade/UserTabs/UserTabs.tsx b/sections/futures/MobileTrade/UserTabs/UserTabs.tsx index a7e7760f79..c0f13e61a0 100644 --- a/sections/futures/MobileTrade/UserTabs/UserTabs.tsx +++ b/sections/futures/MobileTrade/UserTabs/UserTabs.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import { useRecoilValue } from 'recoil'; import styled from 'styled-components'; import TabButton from 'components/Button/TabButton'; import { FuturesAccountType } from 'queries/futures/subgraph'; import TradeIsolatedMargin from 'sections/futures/Trade/TradeIsolatedMargin'; import TradeCrossMargin from 'sections/futures/TradeCrossMargin'; -import { futuresAccountTypeState } from 'store/futures'; +import { selectFuturesType } from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; import OrdersTab from './OrdersTab'; import TradesTab from './TradesTab'; @@ -38,7 +38,7 @@ const getTabs = (accountType: FuturesAccountType) => [ const UserTabs: React.FC = () => { const [activeTab, setActiveTab] = React.useState(0); - const accountType = useRecoilValue(futuresAccountTypeState); + const accountType = useAppSelector(selectFuturesType); const tabs = getTabs(accountType); diff --git a/sections/futures/MobileTrade/drawers/OrderDrawer.tsx b/sections/futures/MobileTrade/drawers/OrderDrawer.tsx index c0aa293275..2c40fd639e 100644 --- a/sections/futures/MobileTrade/drawers/OrderDrawer.tsx +++ b/sections/futures/MobileTrade/drawers/OrderDrawer.tsx @@ -3,7 +3,8 @@ import { useTranslation } from 'react-i18next'; import styled, { css } from 'styled-components'; import Button from 'components/Button'; -import { FuturesOrder, PositionSide } from 'queries/futures/types'; +import { PositionSide } from 'queries/futures/types'; +import { FuturesOrder } from 'sdk/types/futures'; import { getDisplayAsset } from 'utils/futures'; import BaseDrawer from './BaseDrawer'; diff --git a/sections/futures/MobileTrade/drawers/TradeConfirmationDrawer.tsx b/sections/futures/MobileTrade/drawers/TradeConfirmationDrawer.tsx deleted file mode 100644 index f9f2ccd67e..0000000000 --- a/sections/futures/MobileTrade/drawers/TradeConfirmationDrawer.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import React, { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useRecoilValue } from 'recoil'; -import styled from 'styled-components'; - -import Button from 'components/Button'; -import { CurrencyKey } from 'constants/currency'; -import Connector from 'containers/Connector'; -import { useFuturesContext } from 'contexts/FuturesContext'; -import useEstimateGasCost from 'hooks/useEstimateGasCost'; -import useSelectedPriceCurrency from 'hooks/useSelectedPriceCurrency'; -import { PositionSide } from 'sections/futures/types'; -import { selectMarketAsset } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; -import { potentialTradeDetailsState } from 'store/futures'; -import { zeroBN, formatDollars, formatCurrency, formatNumber } from 'utils/formatters/number'; - -import BaseDrawer from './BaseDrawer'; - -type TradeConfirmationDrawerProps = { - open: boolean; - closeDrawer(): void; -}; - -const TradeConfirmationDrawer: React.FC = ({ open, closeDrawer }) => { - const { t } = useTranslation(); - const { synthsMap } = Connector.useContainer(); - const marketAsset = useAppSelector(selectMarketAsset); - const { selectedPriceCurrency } = useSelectedPriceCurrency(); - const { data: potentialTradeDetails } = useRecoilValue(potentialTradeDetailsState); - const { estimateSnxTxGasCost } = useEstimateGasCost(); - - const { orderTxn } = useFuturesContext(); - - const transactionFee = estimateSnxTxGasCost(orderTxn); - - const positionDetails = useMemo(() => { - return potentialTradeDetails - ? { - ...potentialTradeDetails, - size: potentialTradeDetails.size.abs(), - side: potentialTradeDetails.size.gte(zeroBN) ? PositionSide.LONG : PositionSide.SHORT, - leverage: potentialTradeDetails.margin.eq(zeroBN) - ? zeroBN - : potentialTradeDetails.size - .mul(potentialTradeDetails.price) - .div(potentialTradeDetails.margin) - .abs(), - } - : null; - }, [potentialTradeDetails]); - - const dataRows = useMemo( - () => [ - { label: 'side', value: (positionDetails?.side ?? PositionSide.LONG).toUpperCase() }, - { - label: 'size', - value: formatCurrency(marketAsset || '', positionDetails?.size ?? zeroBN, { - sign: marketAsset ? synthsMap[marketAsset]?.sign : '', - }), - }, - { label: 'leverage', value: `${formatNumber(positionDetails?.leverage ?? zeroBN)}x` }, - { - label: 'current price', - value: formatDollars(positionDetails?.price ?? zeroBN), - }, - { - label: 'liquidation price', - value: formatDollars(positionDetails?.liqPrice ?? zeroBN), - }, - { - label: 'margin', - value: formatDollars(positionDetails?.margin ?? zeroBN), - }, - { - label: 'protocol fee', - value: formatDollars(positionDetails?.fee ?? zeroBN), - }, - { - label: 'network gas fee', - value: formatCurrency(selectedPriceCurrency.name as CurrencyKey, transactionFee ?? zeroBN, { - sign: '$', - minDecimals: 2, - }), - }, - ], - [positionDetails, marketAsset, synthsMap, transactionFee, selectedPriceCurrency] - ); - - return ( - { - orderTxn.mutate(); - closeDrawer(); - }} - disabled={!positionDetails} - > - {t('futures.market.trade.confirmation.modal.confirm-order')} - - } - /> - ); -}; - -const ConfirmTradeButton = styled(Button)` - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - height: 45px; - width: 100%; -`; - -export default TradeConfirmationDrawer; diff --git a/sections/futures/OrderPriceInput/OrderPriceInput.tsx b/sections/futures/OrderPriceInput/OrderPriceInput.tsx index 8c7ee751e6..19fbb93a50 100644 --- a/sections/futures/OrderPriceInput/OrderPriceInput.tsx +++ b/sections/futures/OrderPriceInput/OrderPriceInput.tsx @@ -2,7 +2,7 @@ import { wei } from '@synthetixio/wei'; import { debounce } from 'lodash'; import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilState, useRecoilValue } from 'recoil'; +import { useRecoilState } from 'recoil'; import styled from 'styled-components'; import CustomInput from 'components/Input/CustomInput'; @@ -10,9 +10,13 @@ import InputTitle from 'components/Input/InputTitle'; import SegmentedControl from 'components/SegmentedControl'; import StyledTooltip from 'components/Tooltip/StyledTooltip'; import { FuturesOrderType } from 'queries/futures/types'; -import { selectMarketAssetRate, selectMarketInfo } from 'state/futures/selectors'; +import { + selectMarketAssetRate, + selectMarketInfo, + selectLeverageSide, +} from 'state/futures/selectors'; import { useAppSelector } from 'state/hooks'; -import { leverageSideState, orderFeeCapState } from 'store/futures'; +import { orderFeeCapState } from 'store/futures'; import { weiToString, zeroBN } from 'utils/formatters/number'; import { orderPriceInvalidLabel } from 'utils/futures'; @@ -33,7 +37,7 @@ export default function OrderPriceInput({ }: Props) { const { t } = useTranslation(); const marketAssetRate = useAppSelector(selectMarketAssetRate); - const leverageSide = useRecoilValue(leverageSideState); + const leverageSide = useAppSelector(selectLeverageSide); const [selectedFeeCap, setSelectedFeeCap] = useRecoilState(orderFeeCapState); const marketInfo = useAppSelector(selectMarketInfo); diff --git a/sections/futures/OrderSizing/OrderSizeSlider.tsx b/sections/futures/OrderSizing/OrderSizeSlider.tsx index 0f59d7716a..859ba55845 100644 --- a/sections/futures/OrderSizing/OrderSizeSlider.tsx +++ b/sections/futures/OrderSizing/OrderSizeSlider.tsx @@ -1,38 +1,36 @@ import { wei } from '@synthetixio/wei'; import { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilValue } from 'recoil'; import styled from 'styled-components'; import ErrorView from 'components/Error'; import StyledSlider from 'components/Slider/StyledSlider'; import { useFuturesContext } from 'contexts/FuturesContext'; -import { selectMaxLeverage } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; +import { editCrossMarginSize } from 'state/futures/actions'; import { - aboveMaxLeverageState, - crossMarginAccountOverviewState, - futuresTradeInputsState, - leverageSideState, - positionState, -} from 'store/futures'; + selectAboveMaxLeverage, + selectCrossMarginBalanceInfo, + selectLeverageSide, + selectMaxLeverage, + selectPosition, + selectTradeSizeInputs, +} from 'state/futures/selectors'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; import { FlexDivRow } from 'styles/common'; export default function OrderSizeSlider() { const { t } = useTranslation(); - const { onTradeAmountChange, maxUsdInputAmount, tradePrice } = useFuturesContext(); - - const { freeMargin: freeCrossMargin } = useRecoilValue(crossMarginAccountOverviewState); - const { susdSize } = useRecoilValue(futuresTradeInputsState); - const aboveMaxLeverage = useRecoilValue(aboveMaxLeverageState); - - const position = useRecoilValue(positionState); - const leverageSide = useRecoilValue(leverageSideState); + const { maxUsdInputAmount } = useFuturesContext(); + const dispatch = useAppDispatch(); + const { freeMargin: freeCrossMargin } = useAppSelector(selectCrossMarginBalanceInfo); + const { susdSizeString } = useAppSelector(selectTradeSizeInputs); + const aboveMaxLeverage = useAppSelector(selectAboveMaxLeverage); + const maxLeverage = useAppSelector(selectMaxLeverage); + const leverageSide = useAppSelector(selectLeverageSide); + const position = useAppSelector(selectPosition); const [percent, setPercent] = useState(0); - const [usdValue, setUsdValue] = useState(susdSize); - - const maxLeverage = useAppSelector(selectMaxLeverage); + const [usdValue, setUsdValue] = useState(susdSizeString); // eslint-disable-next-line const onChangeMarginPercent = useCallback( @@ -42,23 +40,25 @@ export default function OrderSizeSlider() { const usdAmount = maxUsdInputAmount.mul(fraction).toString(); const usdValue = Number(usdAmount).toFixed(0); setUsdValue(usdValue); - onTradeAmountChange(usdValue, tradePrice, 'usd', { simulateChange: !commit }); + if (commit) { + dispatch(editCrossMarginSize(usdValue, 'usd')); + } }, - [onTradeAmountChange, maxUsdInputAmount, tradePrice] + [maxUsdInputAmount, dispatch] ); useEffect(() => { - if (susdSize !== usdValue) { - if (!susdSize || maxUsdInputAmount.eq(0)) { + if (susdSizeString !== usdValue) { + if (!susdSizeString || maxUsdInputAmount.eq(0)) { setPercent(0); return; } - const percent = wei(susdSize).div(maxUsdInputAmount).mul(100).toNumber(); + const percent = wei(susdSizeString).div(maxUsdInputAmount).mul(100).toNumber(); setPercent(Number(percent.toFixed(2))); } // eslint-disable-next-line - }, [susdSize]); + }, [susdSizeString]); if (aboveMaxLeverage && position?.position?.side === leverageSide) { return ( diff --git a/sections/futures/OrderSizing/OrderSizing.tsx b/sections/futures/OrderSizing/OrderSizing.tsx index dc124a6b58..5bc1d4f33c 100644 --- a/sections/futures/OrderSizing/OrderSizing.tsx +++ b/sections/futures/OrderSizing/OrderSizing.tsx @@ -1,25 +1,24 @@ import { wei } from '@synthetixio/wei'; -import { debounce } from 'lodash'; -import React, { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react'; -import { useRecoilValue } from 'recoil'; +import React, { ChangeEvent, useMemo, useState } from 'react'; import styled from 'styled-components'; import SwitchAssetArrows from 'assets/svg/futures/switch-arrows.svg'; import CustomInput from 'components/Input/CustomInput'; import InputTitle from 'components/Input/InputTitle'; import { useFuturesContext } from 'contexts/FuturesContext'; -import { selectMarketKey, selectMarketAssetRate } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; +import { editTradeSizeInput } from 'state/futures/actions'; import { - futuresAccountTypeState, - simulatedTradeState, - positionState, - futuresTradeInputsState, - orderTypeState, - futuresOrderPriceState, - crossMarginAccountOverviewState, - leverageSideState, -} from 'store/futures'; + selectMarketKey, + selectMarketAssetRate, + selectCrossMarginBalanceInfo, + selectPosition, + selectTradeSizeInputs, + selectCrossMarginOrderPrice, + selectOrderType, + selectLeverageSide, + selectFuturesType, +} from 'state/futures/selectors'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; import { FlexDivRow } from 'styles/common'; import { floorNumber, isZero, zeroBN } from 'utils/formatters/number'; import { getDisplayAsset } from 'utils/futures'; @@ -32,23 +31,21 @@ type OrderSizingProps = { }; const OrderSizing: React.FC = ({ disabled, isMobile }) => { - const { onTradeAmountChange, maxUsdInputAmount } = useFuturesContext(); + const { maxUsdInputAmount } = useFuturesContext(); + const dispatch = useAppDispatch(); - const { nativeSize, susdSize } = useRecoilValue(futuresTradeInputsState); - const simulatedTrade = useRecoilValue(simulatedTradeState); + const { susdSizeString, nativeSizeString } = useAppSelector(selectTradeSizeInputs); - const { freeMargin: freeCrossMargin } = useRecoilValue(crossMarginAccountOverviewState); - const position = useRecoilValue(positionState); - const selectedAccountType = useRecoilValue(futuresAccountTypeState); - const orderType = useRecoilValue(orderTypeState); + const { freeMargin: freeCrossMargin } = useAppSelector(selectCrossMarginBalanceInfo); + const position = useAppSelector(selectPosition); + const selectedAccountType = useAppSelector(selectFuturesType); + const orderType = useAppSelector(selectOrderType); const marketAssetRate = useAppSelector(selectMarketAssetRate); - const orderPrice = useRecoilValue(futuresOrderPriceState); - const selectedLeverageSide = useRecoilValue(leverageSideState); + const orderPrice = useAppSelector(selectCrossMarginOrderPrice); + const selectedLeverageSide = useAppSelector(selectLeverageSide); const marketKey = useAppSelector(selectMarketKey); - const [usdValue, setUsdValue] = useState(susdSize); - const [assetValue, setAssetValue] = useState(nativeSize); const [assetInputType, setAssetInputType] = useState<'usd' | 'native'>('usd'); const tradePrice = useMemo(() => (orderPrice ? wei(orderPrice) : marketAssetRate), [ @@ -60,59 +57,24 @@ const OrderSizing: React.FC = ({ disabled, isMobile }) => { [tradePrice, maxUsdInputAmount] ); - useEffect( - () => { - if (simulatedTrade && simulatedTrade.susdSize !== susdSize) { - setUsdValue(simulatedTrade.susdSize); - } else if (susdSize !== usdValue) { - setUsdValue(susdSize); - } - - if (simulatedTrade && simulatedTrade.nativeSize !== nativeSize) { - setAssetValue(simulatedTrade.nativeSize); - } else if (assetValue !== nativeSize) { - setAssetValue(nativeSize); - } - }, - // Don't want to react to internal value changes - // eslint-disable-next-line - [ - susdSize, - nativeSize, - simulatedTrade?.susdSize, - simulatedTrade?.nativeSize, - setUsdValue, - setAssetValue, - ] - ); + const onSizeChange = (value: string, assetType: 'native' | 'usd') => { + dispatch(editTradeSizeInput(value, assetType)); + }; const handleSetMax = () => { if (assetInputType === 'usd') { - onTradeAmountChange(String(floorNumber(maxUsdInputAmount)), tradePrice, 'usd'); + onSizeChange(String(floorNumber(maxUsdInputAmount)), 'usd'); } else { - onTradeAmountChange(String(floorNumber(maxNativeValue)), tradePrice, 'native'); + onSizeChange(String(floorNumber(maxNativeValue)), 'native'); } }; const handleSetPositionSize = () => { - onTradeAmountChange(position?.position?.size.toString() ?? '0', tradePrice, 'native'); + onSizeChange(position?.position?.size.toString() ?? '0', 'native'); }; - // eslint-disable-next-line - const debounceOnChangeValue = useCallback( - debounce((value, assetType) => { - onTradeAmountChange(value, tradePrice, assetType); - }, 500), - [debounce, onTradeAmountChange] - ); - - useEffect(() => { - return () => debounceOnChangeValue?.cancel(); - }, [debounceOnChangeValue]); - const onChangeValue = (_: ChangeEvent, v: string) => { - assetInputType === 'usd' ? setUsdValue(v) : setAssetValue(v); - debounceOnChangeValue(v, assetInputType); + dispatch(editTradeSizeInput(v, assetInputType)); }; const isDisabled = useMemo(() => { @@ -129,8 +91,12 @@ const OrderSizing: React.FC = ({ disabled, isMobile }) => { position?.position.side !== selectedLeverageSide; const invalid = - (assetInputType === 'usd' && usdValue !== '' && maxUsdInputAmount.lte(usdValue || 0)) || - (assetInputType === 'native' && assetValue !== '' && maxNativeValue.lte(assetValue || 0)); + (assetInputType === 'usd' && + susdSizeString !== '' && + maxUsdInputAmount.lte(susdSizeString || 0)) || + (assetInputType === 'native' && + nativeSizeString !== '' && + maxNativeValue.lte(nativeSizeString || 0)); return ( <> @@ -159,7 +125,7 @@ const OrderSizing: React.FC = ({ disabled, isMobile }) => { {} } - value={assetInputType === 'usd' ? usdValue : assetValue} + value={assetInputType === 'usd' ? susdSizeString : nativeSizeString} placeholder="0.00" onChange={onChangeValue} /> diff --git a/sections/futures/PositionCard/ClosePositionModal.tsx b/sections/futures/PositionCard/ClosePositionModal.tsx index 4e39ba36e9..1987934d5a 100644 --- a/sections/futures/PositionCard/ClosePositionModal.tsx +++ b/sections/futures/PositionCard/ClosePositionModal.tsx @@ -1,4 +1,3 @@ -import { CurrencyKey } from '@synthetixio/contracts-interface'; import Wei, { wei } from '@synthetixio/wei'; import { useMemo, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -7,11 +6,11 @@ import styled from 'styled-components'; import BaseModal from 'components/BaseModal'; import Button from 'components/Button'; import Error from 'components/Error'; +import { ButtonLoader } from 'components/Loader/Loader'; import Connector from 'containers/Connector'; -import useSelectedPriceCurrency from 'hooks/useSelectedPriceCurrency'; -import { FuturesFilledPosition } from 'queries/futures/types'; import { getFuturesMarketContract } from 'queries/futures/utils'; -import { selectMarketAsset } from 'state/futures/selectors'; +import { FuturesFilledPosition } from 'sdk/types/futures'; +import { selectIsClosingPosition, selectMarketAsset } from 'state/futures/selectors'; import { useAppSelector } from 'state/hooks'; import { FlexDivCentered, FlexDivCol } from 'styles/common'; import { formatCurrency, formatDollars, formatNumber, zeroBN } from 'utils/formatters/number'; @@ -19,7 +18,6 @@ import { formatCurrency, formatDollars, formatNumber, zeroBN } from 'utils/forma import { PositionSide } from '../types'; type ClosePositionModalProps = { - gasFee: Wei | null; positionDetails: FuturesFilledPosition | null | undefined; disabled?: boolean; errorMessage?: string | null | undefined; @@ -28,7 +26,6 @@ type ClosePositionModalProps = { }; function ClosePositionModal({ - gasFee, positionDetails, disabled, errorMessage, @@ -37,9 +34,9 @@ function ClosePositionModal({ }: ClosePositionModalProps) { const { t } = useTranslation(); const { defaultSynthetixjs: synthetixjs, synthsMap } = Connector.useContainer(); - const { selectedPriceCurrency } = useSelectedPriceCurrency(); const marketAsset = useAppSelector(selectMarketAsset); + const isClosing = useAppSelector(selectIsClosingPosition); const [orderFee, setOrderFee] = useState(wei(0)); const [error, setError] = useState(null); @@ -94,14 +91,8 @@ function ClosePositionModal({ label: t('futures.market.user.position.modal.fee'), value: formatDollars(orderFee), }, - { - label: t('futures.market.user.position.modal.gas-fee'), - value: formatCurrency(selectedPriceCurrency.name as CurrencyKey, gasFee ?? zeroBN, { - sign: '$', - }), - }, ]; - }, [positionDetails, marketAsset, t, orderFee, gasFee, selectedPriceCurrency, synthsMap]); + }, [positionDetails, marketAsset, t, orderFee, synthsMap]); return ( - {t('futures.market.user.position.modal.title')} + {isClosing ? : t('futures.market.user.position.modal.title')} {errorMessage && } @@ -166,7 +157,6 @@ const ValueColumn = styled(FlexDivCol)` const StyledButton = styled(Button)` margin-top: 24px; - margin-bottom: 16px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; diff --git a/sections/futures/PositionCard/ClosePositionModalCrossMargin.tsx b/sections/futures/PositionCard/ClosePositionModalCrossMargin.tsx index 654769f980..9cc9a43cf1 100644 --- a/sections/futures/PositionCard/ClosePositionModalCrossMargin.tsx +++ b/sections/futures/PositionCard/ClosePositionModalCrossMargin.tsx @@ -2,7 +2,6 @@ import Wei, { wei } from '@synthetixio/wei'; import { formatBytes32String } from 'ethers/lib/utils'; import { useMemo, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilValue } from 'recoil'; import { DEFAULT_CROSSMARGIN_GAS_BUFFER_PCT } from 'constants/defaults'; import { useFuturesContext } from 'contexts/FuturesContext'; @@ -10,9 +9,9 @@ import { useRefetchContext } from 'contexts/RefetchContext'; import { monitorTransaction } from 'contexts/RelayerContext'; import useCrossMarginAccountContracts from 'hooks/useCrossMarginContracts'; import useEstimateGasCost from 'hooks/useEstimateGasCost'; -import { selectMarketAsset, selectMarketKey } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; -import { positionState } from 'store/futures'; +import { fetchCrossMarginBalanceInfo } from 'state/futures/actions'; +import { selectMarketAsset, selectMarketKey, selectPosition } from 'state/futures/selectors'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; import { isUserDeniedError } from 'utils/formatters/error'; import { zeroBN } from 'utils/formatters/number'; import logError from 'utils/logError'; @@ -26,19 +25,19 @@ type Props = { export default function ClosePositionModalCrossMargin({ onDismiss }: Props) { const { t } = useTranslation(); - const { handleRefetch, refetchUntilUpdate } = useRefetchContext(); + const { handleRefetch } = useRefetchContext(); const { crossMarginAccountContract } = useCrossMarginAccountContracts(); const { resetTradeState } = useFuturesContext(); const { estimateEthersContractTxCost } = useEstimateGasCost(); + const dispatch = useAppDispatch(); - const [crossMarginGasFee, setCrossMarginGasFee] = useState(null); const [crossMarginGasLimit, setCrossMarginGasLimit] = useState(null); const [error, setError] = useState(null); const marketAsset = useAppSelector(selectMarketAsset); const marketKey = useAppSelector(selectMarketKey); - const position = useRecoilValue(positionState); + const position = useAppSelector(selectPosition); const positionDetails = position?.position; const positionSize = useMemo(() => positionDetails?.size ?? zeroBN, [positionDetails?.size]); @@ -73,13 +72,12 @@ export default function ClosePositionModalCrossMargin({ onDismiss }: Props) { useEffect(() => { if (!crossMarginAccountContract) return; const estimateGas = async () => { - const { gasPrice, gasLimit } = await estimateEthersContractTxCost( + const { gasLimit } = await estimateEthersContractTxCost( crossMarginAccountContract, 'distributeMargin', [crossMarginCloseParams], DEFAULT_CROSSMARGIN_GAS_BUFFER_PCT ); - setCrossMarginGasFee(gasPrice); setCrossMarginGasLimit(gasLimit); }; estimateGas(); @@ -93,7 +91,7 @@ export default function ClosePositionModalCrossMargin({ onDismiss }: Props) { onDismiss(); resetTradeState(); handleRefetch('close-position'); - refetchUntilUpdate('account-margin-change'); + dispatch(fetchCrossMarginBalanceInfo()); }, }); } @@ -118,7 +116,6 @@ export default function ClosePositionModalCrossMargin({ onDismiss }: Props) { return ( { - if (closeTxn?.hash) { - monitorTx(closeTxn.hash); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [closeTxn?.hash]); - - const monitorTx = (txHash: string) => { - if (txHash) { - monitorTransaction({ - txHash: txHash, - onTxConfirmed: () => { - onDismiss(); - resetTradeState(); - handleRefetch('close-position'); - refetchUntilUpdate('account-margin-change'); - }, - }); - } - }; return ( closeTxn?.mutate()} + onClosePosition={() => dispatch(closeIsolatedMarginPosition())} /> ); } diff --git a/sections/futures/PositionCard/PositionCard.tsx b/sections/futures/PositionCard/PositionCard.tsx index de8c32de8a..c443c74895 100644 --- a/sections/futures/PositionCard/PositionCard.tsx +++ b/sections/futures/PositionCard/PositionCard.tsx @@ -14,13 +14,15 @@ import useAverageEntryPrice from 'hooks/useAverageEntryPrice'; import useFuturesMarketClosed from 'hooks/useFuturesMarketClosed'; import useSelectedPriceCurrency from 'hooks/useSelectedPriceCurrency'; import { PositionSide } from 'queries/futures/types'; -import { selectMarketAsset, selectMarketKey, selectPosition } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; import { - futuresAccountTypeState, - positionHistoryState, - potentialTradeDetailsState, -} from 'store/futures'; + selectMarketAsset, + selectMarketKey, + selectPosition, + selectTradePreview, + selectFuturesType, +} from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; +import { positionHistoryState } from 'store/futures'; import { FlexDivCentered, FlexDivCol, PillButtonDiv } from 'styles/common'; import media from 'styles/media'; import { isFiatCurrency } from 'utils/currencies'; @@ -28,7 +30,7 @@ import { formatDollars, formatPercent, zeroBN } from 'utils/formatters/number'; import { formatNumber } from 'utils/formatters/number'; import { getMarketName, getSynthDescription, isDecimalFour, MarketKeyByAsset } from 'utils/futures'; -import EditLeverageModal from '../TradeCrossMargin/EditLeverageModal'; +import EditLeverageModal from '../TradeCrossMargin/EditCrossMarginLeverageModal'; type PositionCardProps = { dashboard?: boolean; @@ -65,22 +67,22 @@ type PositionPreviewData = { const PositionCard: React.FC = () => { const { t } = useTranslation(); - const futuresAccountType = useRecoilValue(futuresAccountTypeState); + const { synthsMap } = Connector.useContainer(); + const { marketAssetRate } = useFuturesContext(); + const { selectedPriceCurrency } = useSelectedPriceCurrency(); + const futuresAccountType = useAppSelector(selectFuturesType); const position = useAppSelector(selectPosition); const marketAsset = useAppSelector(selectMarketAsset); const marketKey = useAppSelector(selectMarketKey); - - const positionDetails = position?.position ?? null; + const positionHistory = useRecoilValue(positionHistoryState); + const previewTradeData = useAppSelector(selectTradePreview); const { isFuturesMarketClosed } = useFuturesMarketClosed(marketKey); - const positionHistory = useRecoilValue(positionHistoryState); + const positionDetails = position?.position ?? null; const [showEditLeverage, setShowEditLeverage] = useState(false); - const { synthsMap } = Connector.useContainer(); - - const { selectedPriceCurrency } = useSelectedPriceCurrency(); const minDecimals = isFiatCurrency(selectedPriceCurrency.name) && isDecimalFour(marketKey) ? DEFAULT_CRYPTO_DECIMALS @@ -92,10 +94,6 @@ const PositionCard: React.FC = () => { ); }, [positionHistory, marketAsset, futuresAccountType]); - const { marketAssetRate } = useFuturesContext(); - - const { data: previewTradeData } = useRecoilValue(potentialTradeDetailsState); - const modifiedAverage = useAverageEntryPrice(thisPositionHistory); const previewData: PositionPreviewData = React.useMemo(() => { diff --git a/sections/futures/PositionChart/PositionChart.tsx b/sections/futures/PositionChart/PositionChart.tsx index 87f1c95acb..94398c3f5d 100644 --- a/sections/futures/PositionChart/PositionChart.tsx +++ b/sections/futures/PositionChart/PositionChart.tsx @@ -4,27 +4,26 @@ import styled from 'styled-components'; import TVChart from 'components/TVChart'; import useAverageEntryPrice from 'hooks/useAverageEntryPrice'; -import { selectMarketAsset } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; import { - positionState, - potentialTradeDetailsState, - positionHistoryState, - futuresAccountTypeState, - openOrdersState, -} from 'store/futures'; + selectFuturesType, + selectMarketAsset, + selectOpenOrders, + selectPosition, + selectTradePreview, +} from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; +import { positionHistoryState } from 'store/futures'; export default function PositionChart() { - const [isChartReady, setIsChartReady] = useState(false); const marketAsset = useAppSelector(selectMarketAsset); - - const position = useRecoilValue(positionState); + const position = useAppSelector(selectPosition); const positionHistory = useRecoilValue(positionHistoryState); - const futuresAccountType = useRecoilValue(futuresAccountTypeState); - const openOrders = useRecoilValue(openOrdersState); - const { data: previewTrade } = useRecoilValue(potentialTradeDetailsState); + const futuresAccountType = useAppSelector(selectFuturesType); + const openOrders = useAppSelector(selectOpenOrders); + const previewTrade = useAppSelector(selectTradePreview); const [showOrderLines, setShowOrderLines] = useState(true); + const [isChartReady, setIsChartReady] = useState(false); const subgraphPosition = useMemo(() => { return positionHistory[futuresAccountType].find((p) => p.isOpen && p.asset === marketAsset); diff --git a/sections/futures/ShareModal/AmountContainer.tsx b/sections/futures/ShareModal/AmountContainer.tsx index 51a2a28d4f..c75a0d531b 100644 --- a/sections/futures/ShareModal/AmountContainer.tsx +++ b/sections/futures/ShareModal/AmountContainer.tsx @@ -2,7 +2,7 @@ import { FC } from 'react'; import styled from 'styled-components'; import CurrencyIcon from 'components/Currency/CurrencyIcon'; -import { FuturesPosition } from 'queries/futures/types'; +import { FuturesPosition } from 'sdk/types/futures'; import { selectMarketAsset } from 'state/futures/selectors'; import { useAppSelector } from 'state/hooks'; import { formatNumber, zeroBN } from 'utils/formatters/number'; diff --git a/sections/futures/ShareModal/PositionMetadata.tsx b/sections/futures/ShareModal/PositionMetadata.tsx index 958bc394ec..65ff868285 100644 --- a/sections/futures/ShareModal/PositionMetadata.tsx +++ b/sections/futures/ShareModal/PositionMetadata.tsx @@ -5,7 +5,9 @@ import { useRecoilValue } from 'recoil'; import styled from 'styled-components'; import { useFuturesContext } from 'contexts/FuturesContext'; -import { futuresAccountTypeState, positionHistoryState } from 'store/futures'; +import { selectFuturesType } from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; +import { positionHistoryState } from 'store/futures'; import getLocale from 'utils/formatters/getLocale'; type PositionMetadataProps = { @@ -72,7 +74,7 @@ const PositionMetadata: FC = ({ marketAsset }) => { const { marketAssetRate } = useFuturesContext(); const futuresPositionHistory = useRecoilValue(positionHistoryState); - const futuresAccountType = useRecoilValue(futuresAccountTypeState); + const futuresAccountType = useAppSelector(selectFuturesType); let avgEntryPrice = '', openAtDate = '', diff --git a/sections/futures/ShareModal/ShareModal.tsx b/sections/futures/ShareModal/ShareModal.tsx index 2b34baaf98..61c4b673b2 100644 --- a/sections/futures/ShareModal/ShareModal.tsx +++ b/sections/futures/ShareModal/ShareModal.tsx @@ -4,7 +4,7 @@ import styled from 'styled-components'; import PNLGraphicPNG from 'assets/png/pnl-graphic.png'; import BaseModal from 'components/BaseModal'; -import { FuturesPosition } from 'queries/futures/types'; +import { FuturesPosition } from 'sdk/types/futures'; import { FuturesMarketAsset } from 'utils/futures'; import AmountContainer from './AmountContainer'; diff --git a/sections/futures/Trade/ManagePosition.tsx b/sections/futures/Trade/ManagePosition.tsx index fb0e8f6af1..0633edf582 100644 --- a/sections/futures/Trade/ManagePosition.tsx +++ b/sections/futures/Trade/ManagePosition.tsx @@ -1,6 +1,5 @@ import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilValue, useRecoilState } from 'recoil'; import styled from 'styled-components'; import Button from 'components/Button'; @@ -9,28 +8,31 @@ import Loader from 'components/Loader'; import { useFuturesContext } from 'contexts/FuturesContext'; import { previewErrorI18n } from 'queries/futures/constants'; import { PositionSide } from 'queries/futures/types'; -import { setLeverageSide as setReduxLeverageSide } from 'state/futures/reducer'; +import { setOpenModal } from 'state/app/reducer'; +import { selectOpenModal } from 'state/app/selectors'; +import { changeLeverageSide, editTradeSizeInput } from 'state/futures/actions'; import { selectMarketInfo, selectIsMarketCapReached, selectMarketAssetRate, selectPlaceOrderTranslationKey, + selectPosition, selectMaxLeverage, + selectFuturesTransaction, + selectTradePreviewError, + selectTradePreview, + selectTradePreviewStatus, + selectTradeSizeInputs, + selectIsolatedMarginLeverage, + selectCrossMarginOrderPrice, + selectOrderType, + selectIsAdvancedOrder, + selectFuturesType, + selectCrossMarginMarginDelta, + selectLeverageSide, } from 'state/futures/selectors'; import { useAppDispatch, useAppSelector } from 'state/hooks'; -import { - confirmationModalOpenState, - leverageSideState, - orderTypeState, - positionState, - potentialTradeDetailsState, - sizeDeltaState, - futuresTradeInputsState, - futuresAccountTypeState, - crossMarginMarginDeltaState, - futuresOrderPriceState, - isAdvancedOrderState, -} from 'store/futures'; +import { FetchStatus } from 'state/types'; import { isZero } from 'utils/formatters/number'; import { orderPriceInvalidLabel } from 'utils/futures'; @@ -40,60 +42,48 @@ import NextPriceConfirmationModal from './NextPriceConfirmationModal'; import TradeConfirmationModalCrossMargin from './TradeConfirmationModalCrossMargin'; import TradeConfirmationModalIsolatedMargin from './TradeConfirmationModalIsolatedMargin'; -type OrderTxnError = { - reason: string; -}; - const ManagePosition: React.FC = () => { const { t } = useTranslation(); - const { - error, - orderTxn, - onTradeAmountChange, - maxUsdInputAmount, - tradePrice, - } = useFuturesContext(); + const dispatch = useAppDispatch(); - const sizeDelta = useRecoilValue(sizeDeltaState); - const marginDelta = useRecoilValue(crossMarginMarginDeltaState); - const position = useRecoilValue(positionState); - const selectedAccountType = useRecoilValue(futuresAccountTypeState); - const { data: previewTrade, error: previewError, status } = useRecoilValue( - potentialTradeDetailsState - ); - const orderType = useRecoilValue(orderTypeState); - const [leverageSide, setLeverageSide] = useRecoilState(leverageSideState); - const { leverage } = useRecoilValue(futuresTradeInputsState); - const [isConfirmationModalOpen, setConfirmationModalOpen] = useRecoilState( - confirmationModalOpenState - ); + const { maxUsdInputAmount } = useFuturesContext(); + + const { susdSize } = useAppSelector(selectTradeSizeInputs); + const marginDelta = useAppSelector(selectCrossMarginMarginDelta); + const position = useAppSelector(selectPosition); + const maxLeverageValue = useAppSelector(selectMaxLeverage); + const selectedAccountType = useAppSelector(selectFuturesType); + const previewTrade = useAppSelector(selectTradePreview); + + const previewError = useAppSelector(selectTradePreviewError); + const leverage = useAppSelector(selectIsolatedMarginLeverage); + const orderType = useAppSelector(selectOrderType); + const leverageSide = useAppSelector(selectLeverageSide); + + const futuresTransaction = useAppSelector(selectFuturesTransaction); const isMarketCapReached = useAppSelector(selectIsMarketCapReached); const placeOrderTranslationKey = useAppSelector(selectPlaceOrderTranslationKey); - const dispatch = useAppDispatch(); - const orderPrice = useRecoilValue(futuresOrderPriceState); + const orderPrice = useAppSelector(selectCrossMarginOrderPrice); const marketAssetRate = useAppSelector(selectMarketAssetRate); - const tradeInputs = useRecoilValue(futuresTradeInputsState); - const isAdvancedOrder = useRecoilValue(isAdvancedOrderState); - + const isAdvancedOrder = useAppSelector(selectIsAdvancedOrder); + const openModal = useAppSelector(selectOpenModal); const marketInfo = useAppSelector(selectMarketInfo); - const maxLeverageValue = useAppSelector(selectMaxLeverage); + const previewStatus = useAppSelector(selectTradePreviewStatus); - const [isCancelModalOpen, setCancelModalOpen] = React.useState(false); + const isCancelModalOpen = openModal === 'futures_close_position_confirm'; + const isConfirmationModalOpen = openModal === 'futures_modify_position_confirm'; const positionDetails = position?.position; const orderError = useMemo(() => { if (previewError) return t(previewErrorI18n(previewError)); - const orderTxnError = orderTxn.error as OrderTxnError; - if (orderTxnError?.reason) return orderTxnError.reason; - if (error) return error; + if (futuresTransaction?.error) return futuresTransaction.error; if (previewTrade?.showStatus) return previewTrade?.statusMessage; return null; }, [ - orderTxn.error, - error, previewTrade?.showStatus, previewTrade?.statusMessage, + futuresTransaction?.error, previewError, t, ]); @@ -102,7 +92,7 @@ const ManagePosition: React.FC = () => { if (selectedAccountType === 'cross_margin') return true; const leverageNum = Number(leverage || 0); return leverageNum > 0 && leverageNum < maxLeverageValue.toNumber(); - }, [leverage, selectedAccountType, maxLeverageValue]); + }, [selectedAccountType, maxLeverageValue, leverage]); const placeOrderDisabledReason = useMemo(() => { const invalidReason = orderPriceInvalidLabel( @@ -112,27 +102,25 @@ const ManagePosition: React.FC = () => { orderType ); if (!leverageValid) return 'invalid_leverage'; - if (!!error) return error; if (marketInfo?.isSuspended) return 'market_suspended'; if (isMarketCapReached) return 'market_cap_reached'; if ((orderType === 'limit' || orderType === 'stop market') && !!invalidReason) return invalidReason; - if (tradeInputs.susdSizeDelta.abs().gt(maxUsdInputAmount)) return 'max_size_exceeded'; + if (susdSize.gt(maxUsdInputAmount)) return 'max_size_exceeded'; if (placeOrderTranslationKey === 'futures.market.trade.button.deposit-margin-minimum') return 'min_margin_required'; if (selectedAccountType === 'cross_margin') { - if ((isZero(marginDelta) && isZero(sizeDelta)) || status !== 'complete') + if ((isZero(marginDelta) && isZero(susdSize)) || previewStatus.status !== FetchStatus.Success) return 'awaiting_preview'; - if (orderType !== 'market' && isZero(orderPrice)) return 'price_required'; - } else if (isZero(sizeDelta)) { + if (orderType !== 'market' && isZero(orderPrice)) return 'pricerequired'; + } else if (isZero(susdSize)) { return 'size_required'; } return null; }, [ leverageValid, - error, - sizeDelta, + susdSize, marginDelta, orderType, orderPrice, @@ -140,11 +128,10 @@ const ManagePosition: React.FC = () => { marketAssetRate, marketInfo?.isSuspended, placeOrderTranslationKey, - tradeInputs.susdSizeDelta, maxUsdInputAmount, selectedAccountType, isMarketCapReached, - status, + previewStatus, ]); // TODO: Better user feedback for disabled reasons @@ -162,9 +149,13 @@ const ManagePosition: React.FC = () => { noOutline fullWidth disabled={!!placeOrderDisabledReason} - onClick={() => setConfirmationModalOpen(true)} + onClick={() => dispatch(setOpenModal('futures_modify_position_confirm'))} > - {status === 'fetching' ? : t(placeOrderTranslationKey)} + {previewStatus.status === FetchStatus.Loading ? ( + + ) : ( + t(placeOrderTranslationKey) + )} { position.position.side === PositionSide.LONG ? PositionSide.SHORT : PositionSide.LONG; - setLeverageSide(newLeverageSide); - dispatch(setReduxLeverageSide(newLeverageSide)); - onTradeAmountChange(newTradeSize.toString(), tradePrice, 'native'); - setConfirmationModalOpen(true); + dispatch(changeLeverageSide(newLeverageSide)); + dispatch(editTradeSizeInput(newTradeSize.toString(), 'native')); + dispatch(setOpenModal('futures_modify_position_confirm')); } else { - setCancelModalOpen(true); + dispatch(setOpenModal('futures_close_position_confirm')); } }} disabled={!positionDetails || marketInfo?.isSuspended || isAdvancedOrder} @@ -195,24 +185,24 @@ const ManagePosition: React.FC = () => { {orderError && ( - + )} {isCancelModalOpen && (selectedAccountType === 'cross_margin' ? ( - setCancelModalOpen(false)} /> + dispatch(setOpenModal(null))} /> ) : ( - setCancelModalOpen(false)} /> + dispatch(setOpenModal(null))} /> ))} {isConfirmationModalOpen && (selectedAccountType === 'cross_margin' ? ( + ) : orderType === 'next price' ? ( + ) : ( ))} - - {isConfirmationModalOpen && orderType === 'next price' && } ); }; diff --git a/sections/futures/Trade/MarketActions.tsx b/sections/futures/Trade/MarketActions.tsx index 83eff95d90..6ae6f3cddd 100644 --- a/sections/futures/Trade/MarketActions.tsx +++ b/sections/futures/Trade/MarketActions.tsx @@ -1,14 +1,14 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilValue } from 'recoil'; import styled from 'styled-components'; import Button from 'components/Button'; import Connector from 'containers/Connector'; import useIsL2 from 'hooks/useIsL2'; -import { selectMarketInfo } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; -import { balancesState, positionState } from 'store/futures'; +import { setOpenModal } from 'state/app/reducer'; +import { selectOpenModal } from 'state/app/selectors'; +import { selectMarketInfo, selectPosition } from 'state/futures/selectors'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; import { zeroBN } from 'utils/formatters/number'; import TransferIsolatedMarginModal from './TransferIsolatedMarginModal'; @@ -16,12 +16,12 @@ import TransferIsolatedMarginModal from './TransferIsolatedMarginModal'; const MarketActions: React.FC = () => { const { t } = useTranslation(); const { walletAddress } = Connector.useContainer(); - const { susdWalletBalance } = useRecoilValue(balancesState); - - const position = useRecoilValue(positionState); + const dispatch = useAppDispatch(); + const position = useAppSelector(selectPosition); const marketInfo = useAppSelector(selectMarketInfo); + const openModal = useAppSelector(selectOpenModal); + const isL2 = useIsL2(); - const [openModal, setOpenModal] = React.useState<'deposit' | 'withdraw' | null>(null); return ( <> @@ -29,7 +29,7 @@ const MarketActions: React.FC = () => { setOpenModal('deposit')} + onClick={() => dispatch(setOpenModal('futures_isolated_transfer'))} noOutline > {t('futures.market.trade.button.deposit')} @@ -42,25 +42,23 @@ const MarketActions: React.FC = () => { !isL2 || !walletAddress } - onClick={() => setOpenModal('withdraw')} + onClick={() => dispatch(setOpenModal('futures_isolated_transfer'))} noOutline > {t('futures.market.trade.button.withdraw')} - {openModal === 'deposit' && ( + {openModal === 'futures_isolated_transfer' && ( setOpenModal(null)} + onDismiss={() => dispatch(setOpenModal(null))} /> )} - {openModal === 'withdraw' && ( + {openModal === 'futures_isolated_transfer' && ( setOpenModal(null)} + onDismiss={() => dispatch(setOpenModal(null))} /> )} diff --git a/sections/futures/Trade/MarketsDropdown.tsx b/sections/futures/Trade/MarketsDropdown.tsx index 0da5fed5eb..cbdee6ebc9 100644 --- a/sections/futures/Trade/MarketsDropdown.tsx +++ b/sections/futures/Trade/MarketsDropdown.tsx @@ -17,10 +17,11 @@ import { selectMarketAsset, selectMarkets, selectMarketsQueryStatus, + selectFuturesType, } from 'state/futures/selectors'; import { useAppSelector } from 'state/hooks'; import { FetchStatus } from 'state/types'; -import { futuresAccountTypeState, pastRatesState } from 'store/futures'; +import { pastRatesState } from 'store/futures'; import { assetToSynth, iStandardSynth } from 'utils/currencies'; import { formatCurrency, formatPercent, zeroBN } from 'utils/formatters/number'; import { @@ -68,7 +69,7 @@ type MarketsDropdownProps = { const MarketsDropdown: React.FC = ({ mobile }) => { const pastRates = useRecoilValue(pastRatesState); - const accountType = useRecoilValue(futuresAccountTypeState); + const accountType = useAppSelector(selectFuturesType); const marketAsset = useAppSelector(selectMarketAsset); const futuresMarkets = useAppSelector(selectMarkets); const marketsQueryStatus = useAppSelector(selectMarketsQueryStatus); @@ -151,7 +152,7 @@ const MarketsDropdown: React.FC = ({ mobile }) => { getMinDecimals, ]); - const isFetching = !futuresMarkets.length && marketsQueryStatus === FetchStatus.Loading; + const isFetching = !futuresMarkets.length && marketsQueryStatus.status === FetchStatus.Loading; return ( diff --git a/sections/futures/Trade/NextPriceConfirmationModal.tsx b/sections/futures/Trade/NextPriceConfirmationModal.tsx index 4ec8df12be..4ea7e9819b 100644 --- a/sections/futures/Trade/NextPriceConfirmationModal.tsx +++ b/sections/futures/Trade/NextPriceConfirmationModal.tsx @@ -1,28 +1,30 @@ import useSynthetixQueries from '@synthetixio/queries'; import { wei } from '@synthetixio/wei'; -import { FC, useMemo } from 'react'; +import { FC, useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; import styled from 'styled-components'; import BaseModal from 'components/BaseModal'; import Button from 'components/Button'; +import { ButtonLoader } from 'components/Loader/Loader'; import { DesktopOnlyView, MobileOrTabletView } from 'components/Media'; import { NO_VALUE } from 'constants/placeholder'; import Connector from 'containers/Connector'; -import { useFuturesContext } from 'contexts/FuturesContext'; -import useEstimateGasCost from 'hooks/useEstimateGasCost'; import useSelectedPriceCurrency from 'hooks/useSelectedPriceCurrency'; import GasPriceSelect from 'sections/shared/components/GasPriceSelect'; -import { selectMarketAsset, selectMarketInfo } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; +import { setOpenModal } from 'state/app/reducer'; +import { modifyIsolatedPosition, modifyIsolatedPositionEstimateGas } from 'state/futures/actions'; import { - confirmationModalOpenState, - leverageSideState, - nextPriceDisclaimerState, - positionState, - futuresTradeInputsState, -} from 'store/futures'; + selectIsModifyingIsolatedPosition, + selectLeverageSide, + selectMarketAsset, + selectMarketInfo, + selectModifyIsolatedGasEstimate, + selectNextPriceDisclaimer, + selectPosition, + selectTradeSizeInputs, +} from 'state/futures/selectors'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; import { FlexDivCol, FlexDivCentered } from 'styles/common'; import { computeNPFee } from 'utils/costCalculations'; import { zeroBN, formatCurrency, formatDollars } from 'utils/formatters/number'; @@ -34,28 +36,35 @@ import { MobileConfirmTradeButton } from './TradeConfirmationModal'; const NextPriceConfirmationModal: FC = () => { const { t } = useTranslation(); const { synthsMap } = Connector.useContainer(); - const isDisclaimerDisplayed = useRecoilValue(nextPriceDisclaimerState); + const isDisclaimerDisplayed = useAppSelector(selectNextPriceDisclaimer); const { useEthGasPriceQuery } = useSynthetixQueries(); const { selectedPriceCurrency } = useSelectedPriceCurrency(); const ethGasPriceQuery = useEthGasPriceQuery(); - const { estimateSnxTxGasCost } = useEstimateGasCost(); + const dispatch = useAppDispatch(); - const { nativeSize } = useRecoilValue(futuresTradeInputsState); - const leverageSide = useRecoilValue(leverageSideState); - const position = useRecoilValue(positionState); + const { nativeSize, nativeSizeDelta } = useAppSelector(selectTradeSizeInputs); + const leverageSide = useAppSelector(selectLeverageSide); + const position = useAppSelector(selectPosition); const marketInfo = useAppSelector(selectMarketInfo); const marketAsset = useAppSelector(selectMarketAsset); - - const setConfirmationModalOpen = useSetRecoilState(confirmationModalOpenState); - - const { orderTxn } = useFuturesContext(); + const submitting = useAppSelector(selectIsModifyingIsolatedPosition); + const gasEstimate = useAppSelector(selectModifyIsolatedGasEstimate); const gasPrices = useMemo( () => (ethGasPriceQuery.isSuccess ? ethGasPriceQuery?.data ?? undefined : undefined), [ethGasPriceQuery.isSuccess, ethGasPriceQuery.data] ); - const transactionFee = estimateSnxTxGasCost(orderTxn); + useEffect(() => { + dispatch( + modifyIsolatedPositionEstimateGas({ + sizeDelta: nativeSizeDelta, + useNextPrice: true, + }) + ); + }, [nativeSizeDelta, dispatch]); + + const transactionFee = useMemo(() => gasEstimate?.cost ?? zeroBN, [gasEstimate?.cost]); const positionSize = position?.position?.size ?? zeroBN; @@ -127,13 +136,17 @@ const NextPriceConfirmationModal: FC = () => { ] ); - const onDismiss = () => { - setConfirmationModalOpen(false); - }; + const onDismiss = useCallback(() => { + dispatch(setOpenModal(null)); + }, [dispatch]); const handleConfirmOrder = async () => { - orderTxn.mutate(); - onDismiss(); + dispatch( + modifyIsolatedPosition({ + sizeDelta: nativeSizeDelta, + useNextPrice: true, + }) + ); }; return ( @@ -158,8 +171,12 @@ const NextPriceConfirmationModal: FC = () => { {t('futures.market.trade.confirmation.modal.max-leverage-disclaimer')} )} - - {t('futures.market.trade.confirmation.modal.confirm-order')} + + {submitting ? ( + + ) : ( + t('futures.market.trade.confirmation.modal.confirm-order') + )} @@ -169,8 +186,16 @@ const NextPriceConfirmationModal: FC = () => { items={dataRows} closeDrawer={onDismiss} buttons={ - - {t('futures.market.trade.confirmation.modal.confirm-order')} + + {submitting ? ( + + ) : ( + t('futures.market.trade.confirmation.modal.confirm-order') + )} } /> diff --git a/sections/futures/Trade/TradeConfirmationModal.tsx b/sections/futures/Trade/TradeConfirmationModal.tsx index 7d2f671cdd..380c4acc51 100644 --- a/sections/futures/Trade/TradeConfirmationModal.tsx +++ b/sections/futures/Trade/TradeConfirmationModal.tsx @@ -2,23 +2,23 @@ import Wei from '@synthetixio/wei'; import { capitalize } from 'lodash'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilValue } from 'recoil'; import styled from 'styled-components'; import BaseModal from 'components/BaseModal'; import Button from 'components/Button'; import ErrorView from 'components/Error'; +import { ButtonLoader } from 'components/Loader/Loader'; import { DesktopOnlyView, MobileOrTabletView } from 'components/Media'; import { MIN_MARGIN_AMOUNT } from 'constants/futures'; -import { selectMarketAsset } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; import { - futuresOrderPriceState, - leverageSideState, - orderTypeState, - positionState, - potentialTradeDetailsState, -} from 'store/futures'; + selectCrossMarginOrderPrice, + selectLeverageSide, + selectMarketAsset, + selectOrderType, + selectPosition, + selectTradePreview, +} from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; import { FlexDivCentered } from 'styles/common'; import { zeroBN, formatCurrency, formatDollars, formatNumber } from 'utils/formatters/number'; import { getDisplayAsset } from 'utils/futures'; @@ -31,6 +31,7 @@ type Props = { tradeFee: Wei; keeperFee?: Wei | null; errorMessage?: string | null | undefined; + isSubmitting?: boolean; onConfirmOrder: () => any; onDismiss: () => void; }; @@ -40,17 +41,18 @@ export default function TradeConfirmationModal({ gasFee, keeperFee, errorMessage, + isSubmitting, onConfirmOrder, onDismiss, }: Props) { const { t } = useTranslation(); const marketAsset = useAppSelector(selectMarketAsset); - const { data: potentialTradeDetails } = useRecoilValue(potentialTradeDetailsState); - const orderType = useRecoilValue(orderTypeState); - const orderPrice = useRecoilValue(futuresOrderPriceState); - const position = useRecoilValue(positionState); - const leverageSide = useRecoilValue(leverageSideState); + const potentialTradeDetails = useAppSelector(selectTradePreview); + const orderType = useAppSelector(selectOrderType); + const orderPrice = useAppSelector(selectCrossMarginOrderPrice); + const position = useAppSelector(selectPosition); + const leverageSide = useAppSelector(selectLeverageSide); const positionSide = useMemo(() => { if (potentialTradeDetails?.size.eq(zeroBN)) { @@ -156,9 +158,13 @@ export default function TradeConfirmationModal({ data-testid="trade-open-position-confirm-order-button" variant="flat" onClick={onConfirmOrder} - disabled={!positionDetails || !!disabledReason || gasFee?.eq(0)} + disabled={!positionDetails || isSubmitting || !!disabledReason} > - {disabledReason || t('futures.market.trade.confirmation.modal.confirm-order')} + {isSubmitting ? ( + + ) : ( + disabledReason || t('futures.market.trade.confirmation.modal.confirm-order') + )} {errorMessage && ( @@ -176,9 +182,13 @@ export default function TradeConfirmationModal({ - {disabledReason || t('futures.market.trade.confirmation.modal.confirm-order')} + {isSubmitting ? ( + + ) : ( + disabledReason || t('futures.market.trade.confirmation.modal.confirm-order') + )} } /> diff --git a/sections/futures/Trade/TradeConfirmationModalCrossMargin.tsx b/sections/futures/Trade/TradeConfirmationModalCrossMargin.tsx index 13c9823f8b..d6eb5a8ebc 100644 --- a/sections/futures/Trade/TradeConfirmationModalCrossMargin.tsx +++ b/sections/futures/Trade/TradeConfirmationModalCrossMargin.tsx @@ -2,7 +2,6 @@ import Wei from '@synthetixio/wei'; import { formatBytes32String } from 'ethers/lib/utils'; import { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; import { DEFAULT_CROSSMARGIN_GAS_BUFFER_PCT } from 'constants/defaults'; import { useFuturesContext } from 'contexts/FuturesContext'; @@ -10,14 +9,14 @@ import { useRefetchContext } from 'contexts/RefetchContext'; import { monitorTransaction } from 'contexts/RelayerContext'; import useCrossMarginAccountContracts from 'hooks/useCrossMarginContracts'; import useEstimateGasCost from 'hooks/useEstimateGasCost'; -import { selectMarketKey } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; +import { setOpenModal } from 'state/app/reducer'; import { - confirmationModalOpenState, - crossMarginMarginDeltaState, - futuresTradeInputsState, - isAdvancedOrderState, -} from 'store/futures'; + selectCrossMarginMarginDelta, + selectIsAdvancedOrder, + selectMarketKey, + selectTradeSizeInputs, +} from 'state/futures/selectors'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; import { isUserDeniedError } from 'utils/formatters/error'; import { zeroBN } from 'utils/formatters/number'; import logError from 'utils/logError'; @@ -26,19 +25,18 @@ import TradeConfirmationModal from './TradeConfirmationModal'; export default function TradeConfirmationModalCrossMargin() { const { t } = useTranslation(); - const { handleRefetch, refetchUntilUpdate } = useRefetchContext(); + const { handleRefetch } = useRefetchContext(); const { crossMarginAccountContract } = useCrossMarginAccountContracts(); const { estimateEthersContractTxCost } = useEstimateGasCost(); + const dispatch = useAppDispatch(); const marketKey = useAppSelector(selectMarketKey); - const crossMarginMarginDelta = useRecoilValue(crossMarginMarginDeltaState); - const tradeInputs = useRecoilValue(futuresTradeInputsState); - const isAdvancedOrder = useRecoilValue(isAdvancedOrderState); + const crossMarginMarginDelta = useAppSelector(selectCrossMarginMarginDelta); + const { nativeSizeDelta } = useAppSelector(selectTradeSizeInputs); + const isAdvancedOrder = useAppSelector(selectIsAdvancedOrder); const { submitCrossMarginOrder, resetTradeState, tradeFees } = useFuturesContext(); - const setConfirmationModalOpen = useSetRecoilState(confirmationModalOpenState); - const [error, setError] = useState(null); const [gasFee, setGasFee] = useState(null); const [gasLimit, setGasLimit] = useState(null); @@ -50,7 +48,7 @@ export default function TradeConfirmationModalCrossMargin() { { marketKey: formatBytes32String(marketKey), marginDelta: crossMarginMarginDelta.toBN(), - sizeDelta: tradeInputs.nativeSizeDelta.toBN(), + sizeDelta: nativeSizeDelta.toBN(), }, ]; const { gasPrice, gasLimit } = await estimateEthersContractTxCost( @@ -68,13 +66,13 @@ export default function TradeConfirmationModalCrossMargin() { crossMarginAccountContract, marketKey, crossMarginMarginDelta, - tradeInputs.nativeSizeDelta, + nativeSizeDelta, estimateEthersContractTxCost, ]); const onDismiss = useCallback(() => { - setConfirmationModalOpen(false); - }, [setConfirmationModalOpen]); + dispatch(setOpenModal(null)); + }, [dispatch]); const handleConfirmOrder = useCallback(async () => { setError(null); @@ -91,7 +89,7 @@ export default function TradeConfirmationModalCrossMargin() { onTxConfirmed: () => { resetTradeState(); handleRefetch('modify-position'); - refetchUntilUpdate('account-margin-change'); + handleRefetch('account-margin-change'); }, }); onDismiss(); @@ -102,16 +100,7 @@ export default function TradeConfirmationModalCrossMargin() { setError(t('common.transaction.transaction-failed')); } } - }, [ - gasLimit, - setError, - handleRefetch, - refetchUntilUpdate, - resetTradeState, - onDismiss, - submitCrossMarginOrder, - t, - ]); + }, [gasLimit, setError, handleRefetch, resetTradeState, onDismiss, submitCrossMarginOrder, t]); return ( gasEstimate?.cost ?? zeroBN, [gasEstimate?.cost]); - const onDismiss = () => { - setConfirmationModalOpen(false); - }; + useEffect(() => { + dispatch( + modifyIsolatedPositionEstimateGas({ + sizeDelta: nativeSizeDelta, + useNextPrice: false, + }) + ); + }, [nativeSizeDelta, dispatch]); + + const onDismiss = useCallback(() => { + dispatch(setOpenModal(null)); + }, [dispatch]); const handleConfirmOrder = async () => { - submitIsolatedMarginOrder(); - onDismiss(); + dispatch( + modifyIsolatedPosition({ + sizeDelta: nativeSizeDelta, + useNextPrice: false, + }) + ); }; return ( @@ -30,6 +50,7 @@ export default function TradeConfirmationModalIsolatedMargin() { onDismiss={onDismiss} onConfirmOrder={handleConfirmOrder} gasFee={transactionFee} + isSubmitting={submitting} tradeFee={potentialTradeDetails?.fee || zeroBN} /> ); diff --git a/sections/futures/Trade/TradeIsolatedMargin.tsx b/sections/futures/Trade/TradeIsolatedMargin.tsx index 86ca127b6f..f23abe0a7e 100644 --- a/sections/futures/Trade/TradeIsolatedMargin.tsx +++ b/sections/futures/Trade/TradeIsolatedMargin.tsx @@ -1,15 +1,13 @@ -import { useState } from 'react'; -import { useRecoilState, useRecoilValue } from 'recoil'; import styled from 'styled-components'; import SegmentedControl from 'components/SegmentedControl'; import { ISOLATED_MARGIN_ORDER_TYPES } from 'constants/futures'; -import { - setLeverageSide as setReduxLeverageSide, - setOrderType as setReduxOrderType, -} from 'state/futures/reducer'; -import { useAppDispatch } from 'state/hooks'; -import { balancesState, leverageSideState, orderTypeState, positionState } from 'store/futures'; +import { setOpenModal } from 'state/app/reducer'; +import { selectOpenModal } from 'state/app/selectors'; +import { changeLeverageSide } from 'state/futures/actions'; +import { setOrderType } from 'state/futures/reducer'; +import { selectLeverageSide, selectOrderType, selectPosition } from 'state/futures/selectors'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; import { zeroBN } from 'utils/formatters/number'; import FeeInfoBox from '../FeeInfoBox'; @@ -27,19 +25,19 @@ type Props = { }; const TradeIsolatedMargin = ({ isMobile }: Props) => { - const [leverageSide, setLeverageSide] = useRecoilState(leverageSideState); - const { susdWalletBalance } = useRecoilValue(balancesState); - const position = useRecoilValue(positionState); + const dispatch = useAppDispatch(); + + const leverageSide = useAppSelector(selectLeverageSide); + const position = useAppSelector(selectPosition); + const openModal = useAppSelector(selectOpenModal); - const [orderType, setOrderType] = useRecoilState(orderTypeState); - const [openTransferModal, setOpenTransferModal] = useState(false); + const orderType = useAppSelector(selectOrderType); const totalMargin = position?.remainingMargin ?? zeroBN; - const dispatch = useAppDispatch(); return (
setOpenTransferModal(true)} + onManageBalance={() => dispatch(setOpenModal('futures_isolated_transfer'))} balance={totalMargin} accountType={'isolated_margin'} /> @@ -51,8 +49,7 @@ const TradeIsolatedMargin = ({ isMobile }: Props) => { values={ISOLATED_MARGIN_ORDER_TYPES} selectedIndex={ISOLATED_MARGIN_ORDER_TYPES.indexOf(orderType)} onChange={(oType: number) => { - setOrderType(oType === 0 ? 'market' : 'next price'); - dispatch(setReduxOrderType(oType === 0 ? 'market' : 'next price')); + dispatch(setOrderType(oType === 0 ? 'market' : 'next price')); }} /> @@ -61,8 +58,7 @@ const TradeIsolatedMargin = ({ isMobile }: Props) => { { - setLeverageSide(side); - dispatch(setReduxLeverageSide(side)); + dispatch(changeLeverageSide(side)); }} /> @@ -73,11 +69,10 @@ const TradeIsolatedMargin = ({ isMobile }: Props) => { - {openTransferModal && ( + {openModal === 'futures_isolated_transfer' && ( setOpenTransferModal(false)} + onDismiss={() => dispatch(setOpenModal(null))} /> )}
diff --git a/sections/futures/Trade/TransferIsolatedMarginModal.tsx b/sections/futures/Trade/TransferIsolatedMarginModal.tsx index 47ed30bdb6..c906ec84bc 100644 --- a/sections/futures/Trade/TransferIsolatedMarginModal.tsx +++ b/sections/futures/Trade/TransferIsolatedMarginModal.tsx @@ -1,8 +1,6 @@ -import useSynthetixQueries from '@synthetixio/queries'; -import Wei, { wei } from '@synthetixio/wei'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { wei } from '@synthetixio/wei'; +import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilValue } from 'recoil'; import styled from 'styled-components'; import BaseModal from 'components/BaseModal'; @@ -12,34 +10,32 @@ import CustomInput from 'components/Input/CustomInput'; import SegmentedControl from 'components/SegmentedControl'; import Spacer from 'components/Spacer'; import { MIN_MARGIN_AMOUNT } from 'constants/futures'; -import { NO_VALUE } from 'constants/placeholder'; -import { useRefetchContext } from 'contexts/RefetchContext'; -import { monitorTransaction } from 'contexts/RelayerContext'; -import useEstimateGasCost from 'hooks/useEstimateGasCost'; -import { selectMarketAsset } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; -import { positionState } from 'store/futures'; -import { gasSpeedState } from 'store/wallet'; +import { selectSusdBalance } from 'state/balances/selectors'; +import { depositIsolatedMargin, withdrawIsolatedMargin } from 'state/futures/actions'; +import { + selectIsolatedTransferError, + selectIsSubmittingIsolatedTransfer, + selectPosition, +} from 'state/futures/selectors'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; import { FlexDivRowCentered } from 'styles/common'; import { formatDollars, zeroBN } from 'utils/formatters/number'; -import { getDisplayAsset } from 'utils/futures'; type Props = { onDismiss(): void; defaultTab: 'deposit' | 'withdraw'; - sUSDBalance: Wei; }; const PLACEHOLDER = '$0.00'; -const TransferIsolatedMarginModal: React.FC = ({ onDismiss, sUSDBalance, defaultTab }) => { +const TransferIsolatedMarginModal: React.FC = ({ onDismiss, defaultTab }) => { const { t } = useTranslation(); - const { useEthGasPriceQuery, useSynthetixTxn } = useSynthetixQueries(); - const { estimateSnxTxGasCost } = useEstimateGasCost(); + const dispatch = useAppDispatch(); - const gasSpeed = useRecoilValue(gasSpeedState); - const position = useRecoilValue(positionState); - const marketAsset = useAppSelector(selectMarketAsset); + const position = useAppSelector(selectPosition); + const submitting = useAppSelector(selectIsSubmittingIsolatedTransfer); + const txError = useAppSelector(selectIsolatedTransferError); + const susdBalance = useAppSelector(selectSusdBalance); const minDeposit = useMemo(() => { const accessibleMargin = position?.accessibleMargin ?? zeroBN; @@ -50,17 +46,13 @@ const TransferIsolatedMarginModal: React.FC = ({ onDismiss, sUSDBalance, const [amount, setAmount] = useState(''); const [transferType, setTransferType] = useState(defaultTab === 'deposit' ? 0 : 1); - const ethGasPriceQuery = useEthGasPriceQuery(); - const { handleRefetch } = useRefetchContext(); - const gasPrice = ethGasPriceQuery.data != null ? ethGasPriceQuery.data[gasSpeed] : null; - - const susdBal = transferType === 0 ? sUSDBalance : position?.accessibleMargin || zeroBN; + const susdBal = transferType === 0 ? susdBalance : position?.accessibleMargin || zeroBN; const accessibleMargin = useMemo(() => position?.accessibleMargin ?? zeroBN, [ position?.accessibleMargin, ]); const isDisabled = useMemo(() => { - if (!amount) { + if (!amount || submitting) { return true; } const amtWei = wei(amount); @@ -68,49 +60,13 @@ const TransferIsolatedMarginModal: React.FC = ({ onDismiss, sUSDBalance, return true; } return false; - }, [amount, susdBal, minDeposit, transferType]); + }, [amount, susdBal, minDeposit, transferType, submitting]); const computedWithdrawAmount = useMemo( - () => - accessibleMargin.eq(wei(amount || 0)) - ? accessibleMargin.mul(wei(-1)).toBN() - : wei(-amount).toBN(), + () => (accessibleMargin.eq(wei(amount || 0)) ? accessibleMargin : wei(amount || 0)), [amount, accessibleMargin] ); - const depositTxn = useSynthetixTxn( - `FuturesMarket${getDisplayAsset(marketAsset)}`, - 'transferMargin', - [wei(amount || 0).toBN()], - gasPrice || undefined, - { enabled: !!marketAsset && !!amount && !isDisabled && transferType === 0 } - ); - - const withdrawTxn = useSynthetixTxn( - `FuturesMarket${getDisplayAsset(marketAsset)}`, - 'transferMargin', - [computedWithdrawAmount], - gasPrice || undefined, - { enabled: !!marketAsset && !!amount && transferType === 1 } - ); - - const transactionFee = estimateSnxTxGasCost(transferType === 0 ? depositTxn : withdrawTxn); - - useEffect(() => { - const hash = depositTxn.hash ?? withdrawTxn.hash; - if (hash) { - monitorTransaction({ - txHash: hash, - onTxConfirmed: () => { - handleRefetch('margin-change'); - onDismiss(); - }, - }); - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [depositTxn.hash, withdrawTxn.hash]); - const handleSetMax = useCallback(() => { if (transferType === 0) { setAmount(susdBal.toString()); @@ -124,6 +80,14 @@ const TransferIsolatedMarginModal: React.FC = ({ onDismiss, sUSDBalance, setAmount(''); }; + const onDeposit = () => { + dispatch(depositIsolatedMargin(wei(amount))); + }; + + const onWithdraw = () => { + dispatch(withdrawIsolatedMargin(computedWithdrawAmount)); + }; + return ( = ({ onDismiss, sUSDBalance, data-testid="futures-market-trade-deposit-margin-button" disabled={isDisabled} fullWidth - onClick={transferType === 0 ? () => depositTxn.mutate() : () => withdrawTxn.mutate()} + onClick={transferType === 0 ? onDeposit : onWithdraw} > {transferType === 0 ? t('futures.market.trade.margin.modal.deposit.button') : t('futures.market.trade.margin.modal.withdraw.button')} - - {t('futures.market.trade.margin.modal.gas-fee')}: - - - {transactionFee ? formatDollars(transactionFee, { maxDecimals: 1 }) : NO_VALUE} - - - - - {depositTxn.errorMessage && } + {txError && } ); }; diff --git a/sections/futures/TradeCrossMargin/CrossMarginInfoBox.tsx b/sections/futures/TradeCrossMargin/CrossMarginInfoBox.tsx index fe600ddfdc..1c16051741 100644 --- a/sections/futures/TradeCrossMargin/CrossMarginInfoBox.tsx +++ b/sections/futures/TradeCrossMargin/CrossMarginInfoBox.tsx @@ -1,6 +1,5 @@ import Wei, { wei } from '@synthetixio/wei'; import React, { useCallback, useMemo, useState } from 'react'; -import { useRecoilValue } from 'recoil'; import styled from 'styled-components'; import WithdrawArrow from 'assets/svg/futures/withdraw-arrow.svg'; @@ -8,19 +7,21 @@ import InfoBox from 'components/InfoBox'; import { MiniLoader } from 'components/Loader'; import PreviewArrow from 'components/PreviewArrow'; import { useFuturesContext } from 'contexts/FuturesContext'; -import { FuturesPotentialTradeDetails } from 'queries/futures/types'; -import { selectMarketInfo } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; +import { FuturesPotentialTradeDetails } from 'sdk/types/futures'; import { - crossMarginMarginDeltaState, - positionState, - potentialTradeDetailsState, - tradeFeesState, - futuresTradeInputsState, - orderTypeState, - futuresOrderPriceState, - crossMarginAccountOverviewState, -} from 'store/futures'; + selectCrossMarginBalanceInfo, + selectCrossMarginMarginDelta, + selectCrossMarginOrderPrice, + selectCrossMarginTradeFees, + selectMarketInfo, + selectOrderType, + selectPosition, + selectTradePreview, + selectTradePreviewStatus, + selectTradeSizeInputs, +} from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; +import { FetchStatus } from 'state/types'; import { PillButtonSpan } from 'styles/common'; import { formatCurrency, @@ -30,7 +31,7 @@ import { zeroBN, } from 'utils/formatters/number'; -import EditLeverageModal from './EditLeverageModal'; +import EditLeverageModal from './EditCrossMarginLeverageModal'; import ManageKeeperBalanceModal from './ManageKeeperBalanceModal'; type Props = { @@ -40,17 +41,18 @@ type Props = { function MarginInfoBox({ editingLeverage }: Props) { const { selectedLeverage } = useFuturesContext(); - const position = useRecoilValue(positionState); + const position = useAppSelector(selectPosition); const marketInfo = useAppSelector(selectMarketInfo); - const { nativeSize } = useRecoilValue(futuresTradeInputsState); - const potentialTrade = useRecoilValue(potentialTradeDetailsState); - const marginDelta = useRecoilValue(crossMarginMarginDeltaState); - const { freeMargin: crossMarginFreeMargin, keeperEthBal } = useRecoilValue( - crossMarginAccountOverviewState + const { nativeSize } = useAppSelector(selectTradeSizeInputs); + const potentialTrade = useAppSelector(selectTradePreview); + const marginDelta = useAppSelector(selectCrossMarginMarginDelta); + const { freeMargin: crossMarginFreeMargin, keeperEthBal } = useAppSelector( + selectCrossMarginBalanceInfo ); - const orderType = useRecoilValue(orderTypeState); - const orderPrice = useRecoilValue(futuresOrderPriceState); - const { crossMarginFee } = useRecoilValue(tradeFeesState); + const previewStatus = useAppSelector(selectTradePreviewStatus); + const orderType = useAppSelector(selectOrderType); + const orderPrice = useAppSelector(selectCrossMarginOrderPrice); + const { crossMarginFee } = useAppSelector(selectCrossMarginTradeFees); const [openModal, setOpenModal] = useState<'leverage' | 'keeper-deposit' | null>(null); @@ -58,11 +60,13 @@ function MarginInfoBox({ editingLeverage }: Props) { const remainingMargin = position?.remainingMargin ?? zeroBN; const marginUsage = totalMargin.gt(zeroBN) ? remainingMargin.div(totalMargin) : zeroBN; - + const minInitialMargin = useMemo(() => marketInfo?.minInitialMargin ?? zeroBN, [ + marketInfo?.minInitialMargin, + ]); const previewTotalMargin = useMemo(() => { const remainingMargin = crossMarginFreeMargin.sub(marginDelta); - return remainingMargin.add(potentialTrade.data?.margin || zeroBN); - }, [crossMarginFreeMargin, marginDelta, potentialTrade.data?.margin]); + return remainingMargin.add(potentialTrade?.margin || zeroBN); + }, [crossMarginFreeMargin, marginDelta, potentialTrade?.margin]); const getPotentialAvailableMargin = useCallback( (previewTrade: FuturesPotentialTradeDetails | null, marketMaxLeverage: Wei | undefined) => { @@ -74,8 +78,8 @@ function MarginInfoBox({ editingLeverage }: Props) { // If the user has a position open, we'll enforce a min initial margin requirement. if (inaccessible.gt(0)) { - if (inaccessible.lt(previewTrade?.minInitialMargin ?? zeroBN)) { - inaccessible = previewTrade?.minInitialMargin ?? zeroBN; + if (inaccessible.lt(minInitialMargin)) { + inaccessible = minInitialMargin; } } @@ -84,23 +88,23 @@ function MarginInfoBox({ editingLeverage }: Props) { ? previewTotalMargin.sub(inaccessible).abs() : zeroBN; }, - [previewTotalMargin] + [previewTotalMargin, minInitialMargin] ); const previewAvailableMargin = React.useMemo(() => { const potentialAvailableMargin = getPotentialAvailableMargin( - potentialTrade.data, + potentialTrade, marketInfo?.maxLeverage ); return potentialAvailableMargin; - }, [potentialTrade.data, marketInfo?.maxLeverage, getPotentialAvailableMargin]); + }, [potentialTrade, marketInfo?.maxLeverage, getPotentialAvailableMargin]); const potentialMarginUsage = useMemo(() => { - if (!potentialTrade.data) return zeroBN; - const notionalValue = potentialTrade.data.notionalValue.abs(); - const maxSize = totalMargin.mul(potentialTrade.data.leverage); + if (!potentialTrade) return zeroBN; + const notionalValue = potentialTrade.notionalValue.abs(); + const maxSize = totalMargin.mul(potentialTrade.leverage); return maxSize.gt(0) ? notionalValue.div(maxSize) : zeroBN; - }, [potentialTrade.data, totalMargin]); + }, [potentialTrade, totalMargin]); const previewTradeData = React.useMemo(() => { const size = wei(nativeSize || zeroBN); @@ -110,12 +114,12 @@ function MarginInfoBox({ editingLeverage }: Props) { ((orderType === 'market' || orderType === 'next price') && (!size.eq(0) || !marginDelta.eq(0))) || ((orderType === 'limit' || orderType === 'stop market') && !!orderPrice && !size.eq(0)), - totalMargin: potentialTrade.data?.margin.sub(crossMarginFee) || zeroBN, + totalMargin: potentialTrade?.margin.sub(crossMarginFee) || zeroBN, freeAccountMargin: crossMarginFreeMargin.sub(marginDelta), availableMargin: previewAvailableMargin.gt(0) ? previewAvailableMargin : zeroBN, - size: potentialTrade.data?.size || zeroBN, - leverage: potentialTrade.data?.margin.gt(0) - ? potentialTrade.data.notionalValue.div(potentialTrade.data.margin).abs() + size: potentialTrade?.size || zeroBN, + leverage: potentialTrade?.margin.gt(0) + ? potentialTrade.notionalValue.div(potentialTrade.margin).abs() : zeroBN, marginUsage: potentialMarginUsage.gt(1) ? wei(1) : potentialMarginUsage, }; @@ -125,16 +129,17 @@ function MarginInfoBox({ editingLeverage }: Props) { crossMarginFee, orderType, orderPrice, - potentialTrade.data?.margin, + potentialTrade?.margin, previewAvailableMargin, - potentialTrade.data?.notionalValue, - potentialTrade.data?.size, + potentialTrade?.notionalValue, + potentialTrade?.size, crossMarginFreeMargin, potentialMarginUsage, ]); - const showPreview = previewTradeData.showPreview && !potentialTrade.data?.showStatus; + const showPreview = previewTradeData.showPreview && !potentialTrade?.showStatus; + const isLoading = previewStatus.status === FetchStatus.Loading; return ( <> - {potentialTrade.status === 'fetching' ? ( - - ) : ( - formatDollars(previewTradeData.freeAccountMargin) - )} + {isLoading ? : formatDollars(previewTradeData.freeAccountMargin)}
), } @@ -161,11 +162,7 @@ function MarginInfoBox({ editingLeverage }: Props) { value: formatDollars(position?.remainingMargin || 0), valueNode: ( - {potentialTrade.status === 'fetching' ? ( - - ) : ( - formatDollars(previewTradeData.totalMargin) - )} + {isLoading ? : formatDollars(previewTradeData.totalMargin)} ), }, @@ -173,11 +170,7 @@ function MarginInfoBox({ editingLeverage }: Props) { value: formatPercent(marginUsage), valueNode: ( - {potentialTrade.status === 'fetching' ? ( - - ) : ( - formatPercent(previewTradeData?.marginUsage) - )} + {isLoading ? : formatPercent(previewTradeData?.marginUsage)} ), }, @@ -217,11 +210,7 @@ function MarginInfoBox({ editingLeverage }: Props) { ), valueNode: ( - {potentialTrade.status === 'fetching' ? ( - - ) : ( - formatNumber(previewTradeData.leverage || 0) + 'x' - )} + {isLoading ? : formatNumber(previewTradeData.leverage || 0) + 'x'} ), }, @@ -230,7 +219,7 @@ function MarginInfoBox({ editingLeverage }: Props) { /> {openModal === 'leverage' && ( - setOpenModal(null)} /> + setOpenModal(null)} /> )} {openModal === 'keeper-deposit' && ( setOpenModal(null)} /> diff --git a/sections/futures/TradeCrossMargin/DepositWithdrawCrossMargin.tsx b/sections/futures/TradeCrossMargin/DepositWithdrawCrossMargin.tsx index 265eb5ddb4..871b836193 100644 --- a/sections/futures/TradeCrossMargin/DepositWithdrawCrossMargin.tsx +++ b/sections/futures/TradeCrossMargin/DepositWithdrawCrossMargin.tsx @@ -1,8 +1,6 @@ import { wei } from '@synthetixio/wei'; -import { constants } from 'ethers'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilValue } from 'recoil'; import styled from 'styled-components'; import BaseModal from 'components/BaseModal'; @@ -12,12 +10,15 @@ import CustomInput from 'components/Input/CustomInput'; import Loader from 'components/Loader'; import SegmentedControl from 'components/SegmentedControl'; import { MIN_MARGIN_AMOUNT } from 'constants/futures'; -import Connector from 'containers/Connector'; -import { useRefetchContext } from 'contexts/RefetchContext'; -import { monitorTransaction } from 'contexts/RelayerContext'; -import useCrossMarginAccountContracts from 'hooks/useCrossMarginContracts'; -import useSUSDContract from 'hooks/useSUSDContract'; -import { balancesState, crossMarginAccountOverviewState } from 'store/futures'; +import { selectBalances } from 'state/balances/selectors'; +import { approveCrossMargin, depositCrossMargin, withdrawCrossMargin } from 'state/futures/actions'; +import { + selectCrossMarginBalanceInfo, + selectFuturesTransaction, + selectIsApprovingCrossDeposit, + selectIsSubmittingCrossTransfer, +} from 'state/futures/selectors'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; import { FlexDivRowCentered } from 'styles/common'; import { formatDollars, zeroBN } from 'utils/formatters/number'; import logError from 'utils/logError'; @@ -25,7 +26,6 @@ import logError from 'utils/logError'; type DepositMarginModalProps = { defaultTab: 'deposit' | 'withdraw'; onDismiss(): void; - onComplete?(): void; }; const PLACEHOLDER = '$0.00'; @@ -33,118 +33,64 @@ const PLACEHOLDER = '$0.00'; export default function DepositWithdrawCrossMargin({ defaultTab = 'deposit', onDismiss, - onComplete, }: DepositMarginModalProps) { const { t } = useTranslation(); - const { signer } = Connector.useContainer(); - const { crossMarginAccountContract } = useCrossMarginAccountContracts(); - const { refetchUntilUpdate } = useRefetchContext(); - const susdContract = useSUSDContract(); + const dispatch = useAppDispatch(); - const balances = useRecoilValue(balancesState); - const { freeMargin, allowance } = useRecoilValue(crossMarginAccountOverviewState); + const balances = useAppSelector(selectBalances); + const crossMarginBalanceInfo = useAppSelector(selectCrossMarginBalanceInfo); + const transactionState = useAppSelector(selectFuturesTransaction); + const isSubmitting = useAppSelector(selectIsSubmittingCrossTransfer); + const isApproving = useAppSelector(selectIsApprovingCrossDeposit); const [amount, setAmount] = useState(''); const [transferType, setTransferType] = useState(0); - const [txState, setTxState] = useState<'none' | 'approving' | 'submitting' | 'complete'>('none'); - const [error, setError] = useState(null); useEffect(() => { setTransferType(defaultTab === 'deposit' ? 0 : 1); }, [defaultTab]); - const susdBal = transferType === 0 ? balances?.susdWalletBalance || zeroBN : freeMargin; + const susdBal = + transferType === 0 ? balances?.susdWalletBalance || zeroBN : crossMarginBalanceInfo.freeMargin; const submitDeposit = useCallback(async () => { - try { - if (!crossMarginAccountContract) throw new Error('No cross-margin account'); - - setTxState('submitting'); - const tx = await crossMarginAccountContract.deposit(wei(amount || 0).toBN()); - monitorTransaction({ - txHash: tx.hash, - onTxConfirmed: async () => { - await refetchUntilUpdate('account-margin-change'); - setTxState('complete'); - onComplete?.(); - onDismiss(); - }, - }); - } catch (err) { - setError(err.message); - setTxState('none'); - logError(err); - } - }, [crossMarginAccountContract, amount, refetchUntilUpdate, onComplete, onDismiss]); + dispatch(depositCrossMargin(wei(amount))); + }, [amount, dispatch]); const depositMargin = useCallback(async () => { try { - const wallet = await signer?.getAddress(); - - if (!crossMarginAccountContract || !wallet) throw new Error('No cross margin account'); const weiAmount = wei(amount ?? 0, 18); - if (wei(allowance).lt(weiAmount)) { - setTxState('approving'); - const tx = await susdContract?.approve( - crossMarginAccountContract.address, - constants.MaxUint256 - ); - if (tx?.hash) { - monitorTransaction({ - txHash: tx.hash, - onTxConfirmed: () => { - submitDeposit(); - }, - }); - } + if (wei(crossMarginBalanceInfo.allowance).lt(weiAmount)) { + dispatch(approveCrossMargin()); } else { submitDeposit(); } } catch (err) { - setError(err.message); - setTxState('none'); logError(err); } - }, [crossMarginAccountContract, amount, signer, susdContract, allowance, submitDeposit]); + }, [amount, crossMarginBalanceInfo.allowance, dispatch, submitDeposit]); const withdrawMargin = useCallback(async () => { - try { - if (!crossMarginAccountContract) throw new Error('No cross-margin account'); - setTxState('submitting'); - const tx = await crossMarginAccountContract.withdraw(wei(amount).toBN()); - monitorTransaction({ - txHash: tx.hash, - onTxConfirmed: async () => { - await refetchUntilUpdate('account-margin-change'); - setTxState('complete'); - onComplete?.(); - onDismiss(); - }, - }); - } catch (err) { - setError(err.message); - setTxState('none'); - logError(err); - } - }, [crossMarginAccountContract, amount, refetchUntilUpdate, onComplete, onDismiss]); + dispatch(withdrawCrossMargin(wei(amount))); + }, [amount, dispatch]); const disabledReason = useMemo(() => { const amtWei = wei(amount || 0); if (transferType === 0) { - const total = wei(freeMargin).add(amtWei); + const total = wei(crossMarginBalanceInfo.freeMargin).add(amtWei); if (total.lt(MIN_MARGIN_AMOUNT)) return t('futures.market.trade.margin.modal.deposit.min-deposit'); if (amtWei.gt(susdBal)) return t('futures.market.trade.margin.modal.deposit.exceeds-balance'); } else { - if (amtWei.gt(freeMargin)) + if (amtWei.gt(crossMarginBalanceInfo.freeMargin)) return t('futures.market.trade.margin.modal.deposit.exceeds-balance'); } - }, [amount, freeMargin, transferType, susdBal, t]); + }, [amount, crossMarginBalanceInfo.freeMargin, transferType, susdBal, t]); const isApproved = useMemo(() => { - return allowance.gt(wei(amount || 0)); - }, [allowance, amount]); + return crossMarginBalanceInfo.allowance.gt(wei(amount || 0)); + }, [crossMarginBalanceInfo.allowance, amount]); const handleSetMax = React.useCallback(() => { setAmount(susdBal.toString()); @@ -155,6 +101,8 @@ export default function DepositWithdrawCrossMargin({ setAmount(''); }; + const isLoading = isSubmitting || isApproving; + return ( - {txState === 'approving' || txState === 'submitting' ? ( + {isLoading ? ( ) : ( disabledReason || @@ -206,7 +154,7 @@ export default function DepositWithdrawCrossMargin({ )} - {error && } + {transactionState?.error && } ); } diff --git a/sections/futures/TradeCrossMargin/EditLeverageModal.tsx b/sections/futures/TradeCrossMargin/EditCrossMarginLeverageModal.tsx similarity index 81% rename from sections/futures/TradeCrossMargin/EditLeverageModal.tsx rename to sections/futures/TradeCrossMargin/EditCrossMarginLeverageModal.tsx index 9f4311d6c5..03c3ba73bc 100644 --- a/sections/futures/TradeCrossMargin/EditLeverageModal.tsx +++ b/sections/futures/TradeCrossMargin/EditCrossMarginLeverageModal.tsx @@ -2,7 +2,6 @@ import { wei } from '@synthetixio/wei'; import { debounce } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilState, useRecoilValue } from 'recoil'; import styled from 'styled-components'; import BaseModal from 'components/BaseModal'; @@ -16,22 +15,23 @@ import { DEFAULT_LEVERAGE } from 'constants/defaults'; import { useFuturesContext } from 'contexts/FuturesContext'; import { useRefetchContext } from 'contexts/RefetchContext'; import { monitorTransaction } from 'contexts/RelayerContext'; -import usePersistedRecoilState from 'hooks/usePersistedRecoilState'; import { ORDER_PREVIEW_ERRORS_I18N, previewErrorI18n } from 'queries/futures/constants'; -import { setOrderType as setReduxOrderType } from 'state/futures/reducer'; -import { selectMarketAsset, selectMarketInfo } from 'state/futures/selectors'; -import { useAppSelector, useAppDispatch } from 'state/hooks'; +import { editExistingPositionLeverage, editCrossMarginSize } from 'state/futures/actions'; +import { setCrossMarginLeverage, setOrderType as setReduxOrderType } from 'state/futures/reducer'; import { - crossMarginTotalMarginState, - orderTypeState, - positionState, - potentialTradeDetailsState, - preferredLeverageState, - tradeFeesState, -} from 'store/futures'; + selectCrossMarginBalanceInfo, + selectCrossMarginSelectedLeverage, + selectCrossMarginTradeFees, + selectMarketInfo, + selectOrderType, + selectPosition, + selectTradePreview, + selectTradePreviewError, +} from 'state/futures/selectors'; +import { useAppSelector, useAppDispatch } from 'state/hooks'; import { FlexDivRow, FlexDivRowCentered } from 'styles/common'; import { isUserDeniedError } from 'utils/formatters/error'; -import { formatDollars } from 'utils/formatters/number'; +import { formatDollars, zeroBN } from 'utils/formatters/number'; import logError from 'utils/logError'; import FeeInfoBox from '../FeeInfoBox'; @@ -40,29 +40,31 @@ import MarginInfoBox from './CrossMarginInfoBox'; type DepositMarginModalProps = { onDismiss(): void; - editMode: 'existing_position' | 'next_trade'; + editMode: 'existing_position' | 'new_position'; }; export default function EditLeverageModal({ onDismiss, editMode }: DepositMarginModalProps) { const { t } = useTranslation(); - const { handleRefetch, refetchUntilUpdate } = useRefetchContext(); - const { - selectedLeverage, - onLeverageChange, - resetTradeState, - submitCrossMarginOrder, - onChangeOpenPosLeverage, - } = useFuturesContext(); + const { handleRefetch } = useRefetchContext(); + const dispatch = useAppDispatch(); + const { resetTradeState, submitCrossMarginOrder } = useFuturesContext(); - const marketAsset = useAppSelector(selectMarketAsset); - const market = useAppSelector(selectMarketInfo); - const position = useRecoilValue(positionState); - const totalMargin = useRecoilValue(crossMarginTotalMarginState); - const tradeFees = useRecoilValue(tradeFeesState); - const { error: previewError, data: previewData } = useRecoilValue(potentialTradeDetailsState); - const [orderType, setOrderType] = useRecoilState(orderTypeState); + const onLeverageChange = useCallback( + (leverage: number) => { + dispatch(setCrossMarginLeverage(String(leverage))); + dispatch(editCrossMarginSize('', 'usd')); + }, + [dispatch] + ); - const [preferredLeverage, setPreferredLeverage] = usePersistedRecoilState(preferredLeverageState); + const balanceInfo = useAppSelector(selectCrossMarginBalanceInfo); + const market = useAppSelector(selectMarketInfo); + const position = useAppSelector(selectPosition); + const tradeFees = useAppSelector(selectCrossMarginTradeFees); + const previewData = useAppSelector(selectTradePreview); + const previewError = useAppSelector(selectTradePreviewError); + const orderType = useAppSelector(selectOrderType); + const selectedLeverage = useAppSelector(selectCrossMarginSelectedLeverage); const [leverage, setLeverage] = useState( editMode === 'existing_position' && position?.position @@ -71,13 +73,15 @@ export default function EditLeverageModal({ onDismiss, editMode }: DepositMargin ); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); - const dispatch = useAppDispatch(); + + const totalMargin = useMemo(() => { + return position?.remainingMargin.add(balanceInfo.freeMargin) ?? zeroBN; + }, [position?.remainingMargin, balanceInfo.freeMargin]); const maxLeverage = Number((market?.maxLeverage || wei(DEFAULT_LEVERAGE)).toString(2)); useEffect(() => { if (editMode === 'existing_position' && orderType !== 'market') { - setOrderType('market'); dispatch(setReduxOrderType('market')); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -106,7 +110,7 @@ export default function EditLeverageModal({ onDismiss, editMode }: DepositMargin debounce((leverage: number) => { if (leverage >= 1) { editMode === 'existing_position' - ? onChangeOpenPosLeverage(leverage) + ? dispatch(editExistingPositionLeverage(String(leverage))) : onLeverageChange(leverage); } }, 200), @@ -129,7 +133,7 @@ export default function EditLeverageModal({ onDismiss, editMode }: DepositMargin try { resetTradeState(); handleRefetch('modify-position'); - refetchUntilUpdate('account-margin-change'); + handleRefetch('account-margin-change'); setSubmitting(false); onDismiss(); } catch (err) { @@ -145,29 +149,24 @@ export default function EditLeverageModal({ onDismiss, editMode }: DepositMargin } resetTradeState(); } else { + // TODO: consolidate leverage states onLeverageChange(leverage); - setPreferredLeverage({ - ...preferredLeverage, - [marketAsset]: String(leverage), - }); + dispatch(setCrossMarginLeverage(String(leverage))); onDismiss(); } }, [ - marketAsset, leverage, position?.position, - preferredLeverage, editMode, setSubmitting, resetTradeState, t, - setPreferredLeverage, onLeverageChange, submitCrossMarginOrder, setError, - refetchUntilUpdate, handleRefetch, onDismiss, + dispatch, ]); const onClose = () => { @@ -227,7 +226,7 @@ export default function EditLeverageModal({ onDismiss, editMode }: DepositMargin - {editMode === 'next_trade' && ( + {editMode === 'new_position' && (