@@ -87,11 +76,11 @@ 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');
+ setOrderType(oType === 0 ? 'market' : 'next price');
}}
/>
- {orderType === 'next-price' &&
}
+ {orderType === 'next price' &&
}
diff --git a/sections/futures/Trade/TradePanelHeader.tsx b/sections/futures/Trade/TradePanelHeader.tsx
index 53d590b484..5f7188b0e6 100644
--- a/sections/futures/Trade/TradePanelHeader.tsx
+++ b/sections/futures/Trade/TradePanelHeader.tsx
@@ -1,12 +1,14 @@
-import { FunctionComponent } from 'react';
+import { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
+import HelpIcon from 'assets/svg/app/question-mark.svg';
import Button from 'components/Button';
import { ButtonVariant } from 'components/Button/Button';
+import FuturesIcon from 'components/Nav/FuturesIcon';
+import { EXTERNAL_LINKS } from 'constants/links';
import { FuturesAccountType } from 'queries/futures/subgraph';
import { BorderedPanel } from 'styles/common';
-import media from 'styles/media';
type Props = {
accountType: FuturesAccountType;
@@ -14,24 +16,31 @@ type Props = {
onClick: (() => void) | undefined;
variant?: ButtonVariant;
i18nTitle: string;
- Icon?: FunctionComponent
;
+ icon?: ReactNode;
}[];
};
export default function TradePanelHeader({ accountType, buttons }: Props) {
const { t } = useTranslation();
+
return (
+
{t(
accountType === 'cross_margin'
? 'futures.market.trade.cross-margin.title'
: 'futures.market.trade.isolated-margin.title'
)}
+ {accountType === 'cross_margin' && (
+ window.open(EXTERNAL_LINKS.Docs.CrossMarginFaq)}>
+
+
+ )}
{buttons &&
- buttons.map(({ Icon, i18nTitle, variant, onClick }) => (
+ buttons.map(({ icon, i18nTitle, variant, onClick }) => (
{t(i18nTitle)}
- {Icon && (
-
-
-
- )}
+ {icon && {icon} }
))}
@@ -53,16 +58,31 @@ export default function TradePanelHeader({ accountType, buttons }: Props) {
);
}
+const StyledFuturesIcon = styled(FuturesIcon)`
+ margin-right: 6px;
+`;
+
const Container = styled(BorderedPanel)`
display: flex;
justify-content: space-between;
- padding: 10px 14px;
+ padding: 10px 10px;
margin-bottom: 16px;
`;
const Title = styled.div`
font-family: ${(props) => props.theme.fonts.bold};
font-size: 16px;
+ display: flex;
+ align-items: center;
+ cursor: default;
+`;
+
+const FAQLink = styled.div`
+ &:hover {
+ opacity: 0.5;
+ }
+ cursor: pointer;
+ margin-left: 5px;
`;
const Buttons = styled.div`
@@ -71,17 +91,19 @@ const Buttons = styled.div`
const HeaderButton = styled(Button)`
margin-left: 10px;
+ font-size: 11px;
`;
const Label = styled.span`
- ${media.lessThan('xl')`
- display: none;
- `}
+ @media (max-width: 1550px) {
+ display: none;
+ }
`;
const IconContainer = styled.span`
margin-left: 5px;
- ${media.lessThan('xl')`
- margin-left: 0;
- `}
+
+ @media (max-width: 1550px) {
+ margin-left: 0;
+ }
`;
diff --git a/sections/futures/TradeCrossMargin/CreateAccount.tsx b/sections/futures/TradeCrossMargin/CreateAccount.tsx
index 36ecf4357c..b03864cd95 100644
--- a/sections/futures/TradeCrossMargin/CreateAccount.tsx
+++ b/sections/futures/TradeCrossMargin/CreateAccount.tsx
@@ -22,7 +22,7 @@ export default function CreateAccount({ onShowOnboard }: Props) {
return (
<>
-
+
{t('futures.market.trade.cross-margin.title')}
{t('futures.market.trade.cross-margin.create-account')}
@@ -30,7 +30,7 @@ export default function CreateAccount({ onShowOnboard }: Props) {
- {t('futures.market.trade.cross-margin.faq-title')}
+ {t('futures.market.trade.cross-margin.faq-title')}
@@ -51,7 +51,13 @@ const FAQContainer = styled(BorderedPanel)`
margin-top: 20px;
`;
-const Title = styled.div<{ yellow?: boolean }>`
+const Title = styled.div`
+ font-family: ${(props) => props.theme.fonts.monoBold};
+ font-size: 23px;
+ color: ${(props) => props.theme.colors.selectedTheme.button.text.primary};
+`;
+
+const FaqTitle = styled.span<{ yellow?: boolean }>`
font-family: ${(props) => props.theme.fonts.monoBold};
font-size: 23px;
color: ${(props) =>
diff --git a/sections/futures/TradeCrossMargin/CrossMarginInfoBox.tsx b/sections/futures/TradeCrossMargin/CrossMarginInfoBox.tsx
index 51eb053c07..5f44555d6d 100644
--- a/sections/futures/TradeCrossMargin/CrossMarginInfoBox.tsx
+++ b/sections/futures/TradeCrossMargin/CrossMarginInfoBox.tsx
@@ -1,7 +1,7 @@
import Wei, { wei } from '@synthetixio/wei';
import React, { useCallback, useMemo, useState } from 'react';
import { useRecoilValue } from 'recoil';
-import styled from 'styled-components';
+import styled, { useTheme } from 'styled-components';
import WithdrawArrow from 'assets/svg/futures/withdraw-arrow.svg';
import InfoBox from 'components/InfoBox';
@@ -37,6 +37,7 @@ type Props = {
function MarginInfoBox({ editingLeverage }: Props) {
const { selectedLeverage } = useFuturesContext();
+ const { colors } = useTheme();
const position = useRecoilValue(positionState);
const marketInfo = useRecoilValue(marketInfoState);
@@ -106,9 +107,9 @@ function MarginInfoBox({ editingLeverage }: Props) {
return {
showPreview:
- ((orderType === 'market' || orderType === 'next-price') &&
+ ((orderType === 'market' || orderType === 'next price') &&
(!size.eq(0) || !marginDelta.eq(0))) ||
- ((orderType === 'limit' || orderType === 'stop') && !!orderPrice && !size.eq(0)),
+ ((orderType === 'limit' || orderType === 'stop market') && !!orderPrice && !size.eq(0)),
totalMargin: potentialTrade.data?.margin.sub(crossMarginFee) || zeroBN,
freeAccountMargin: crossMarginFreeMargin.sub(marginDelta),
availableMargin: previewAvailableMargin.gt(0) ? previewAvailableMargin : zeroBN,
@@ -142,7 +143,10 @@ function MarginInfoBox({ editingLeverage }: Props) {
'Free Account Margin': {
value: formatDollars(crossMarginFreeMargin),
valueNode: (
-
+
{potentialTrade.status === 'fetching' ? (
) : (
@@ -177,21 +181,24 @@ function MarginInfoBox({ editingLeverage }: Props) {
),
},
- 'Keeper ETH Balance':
- orderType === 'limit' || orderType === 'stop'
- ? {
- value: formatCurrency('ETH', keeperEthBal, { currencyKey: 'ETH' }),
- valueNode: (
- <>
- {keeperEthBal.gt(0) && (
- setOpenModal('keeper-deposit')}>
-
-
- )}
- >
- ),
- }
- : null,
+ 'Account ETH Balance': !editingLeverage
+ ? {
+ value: formatCurrency('ETH', keeperEthBal, { currencyKey: 'ETH' }),
+ valueNode: (
+ <>
+ {keeperEthBal.gt(0) && (
+ setOpenModal('keeper-deposit')}>
+
+
+ )}
+ >
+ ),
+ }
+ : null,
Leverage: {
value: (
<>
@@ -260,12 +267,18 @@ const ActionButton = styled(Button)<{ hideBorder?: boolean }>`
margin-left: 8px;
cursor: pointer;
font-size: 10px;
- font-family: ${(props) => props.theme.fonts.bold};
+ font-family: ${(props) => props.theme.fonts.black};
+ font-variant: all-small-caps;
border: 1px solid
${(props) => (!props.hideBorder ? props.theme.colors.selectedTheme.yellow : 'none')};
- color: ${(props) => props.theme.colors.selectedTheme.yellow};
+ color: ${(props) => props.theme.colors.selectedTheme.button.pill.background};
border-radius: 10px;
padding: ${(props) => (props.hideBorder ? '3px 2px 3px 0px' : '3px 5px')};
+ &:hover {
+ background-color: ${(props) => props.theme.colors.selectedTheme.button.pill.background};
+ color: ${(props) => props.theme.colors.selectedTheme.button.pill.hover};
+ opacity: unset;
+ }
`;
export default React.memo(MarginInfoBox);
diff --git a/sections/futures/TradeCrossMargin/CrossMarginUnsupported.tsx b/sections/futures/TradeCrossMargin/CrossMarginUnsupported.tsx
index d1031ca406..9fb3f16016 100644
--- a/sections/futures/TradeCrossMargin/CrossMarginUnsupported.tsx
+++ b/sections/futures/TradeCrossMargin/CrossMarginUnsupported.tsx
@@ -10,7 +10,9 @@ export default function CrossMarginUnsupported() {
return (
- {t('futures.market.trade.cross-margin.title')}
+
+ {t('futures.market.trade.cross-margin.title')}
+
{t('futures.market.trade.cross-margin.unsupported')}
diff --git a/sections/futures/TradeCrossMargin/DepositWithdrawCrossMargin.tsx b/sections/futures/TradeCrossMargin/DepositWithdrawCrossMargin.tsx
index 225cb8a20f..cd321490ab 100644
--- a/sections/futures/TradeCrossMargin/DepositWithdrawCrossMargin.tsx
+++ b/sections/futures/TradeCrossMargin/DepositWithdrawCrossMargin.tsx
@@ -39,11 +39,11 @@ export default function DepositWithdrawCrossMargin({
const { signer } = Connector.useContainer();
const { monitorTransaction } = TransactionNotifier.useContainer();
const { crossMarginAccountContract } = useCrossMarginAccountContracts();
- const { handleRefetch, refetchUntilUpdate } = useRefetchContext();
+ const { refetchUntilUpdate } = useRefetchContext();
const susdContract = useSUSDContract();
const balances = useRecoilValue(balancesState);
- const { freeMargin } = useRecoilValue(crossMarginAccountOverviewState);
+ const { freeMargin, allowance } = useRecoilValue(crossMarginAccountOverviewState);
const [amount, setAmount] = useState('');
const [transferType, setTransferType] = useState(0);
@@ -65,16 +65,10 @@ export default function DepositWithdrawCrossMargin({
monitorTransaction({
txHash: tx.hash,
onTxConfirmed: async () => {
- try {
- await refetchUntilUpdate('wallet-balance-change');
- handleRefetch('account-margin-change');
- } catch (err) {
- logError(err);
- } finally {
- setTxState('complete');
- onComplete?.();
- onDismiss();
- }
+ await refetchUntilUpdate('account-margin-change');
+ setTxState('complete');
+ onComplete?.();
+ onDismiss();
},
});
} catch (err) {
@@ -87,7 +81,6 @@ export default function DepositWithdrawCrossMargin({
amount,
refetchUntilUpdate,
monitorTransaction,
- handleRefetch,
onComplete,
onDismiss,
]);
@@ -98,7 +91,6 @@ export default function DepositWithdrawCrossMargin({
if (!crossMarginAccountContract || !wallet) throw new Error('No cross margin account');
const weiAmount = wei(amount ?? 0, 18);
- const allowance = await susdContract?.allowance(wallet, crossMarginAccountContract.address);
if (wei(allowance).lt(weiAmount)) {
setTxState('approving');
@@ -122,7 +114,15 @@ export default function DepositWithdrawCrossMargin({
setTxState('none');
logError(err);
}
- }, [crossMarginAccountContract, amount, signer, susdContract, monitorTransaction, submitDeposit]);
+ }, [
+ crossMarginAccountContract,
+ amount,
+ signer,
+ susdContract,
+ allowance,
+ monitorTransaction,
+ submitDeposit,
+ ]);
const withdrawMargin = useCallback(async () => {
try {
@@ -131,9 +131,9 @@ export default function DepositWithdrawCrossMargin({
const tx = await crossMarginAccountContract.withdraw(wei(amount).toBN());
monitorTransaction({
txHash: tx.hash,
- onTxConfirmed: () => {
+ onTxConfirmed: async () => {
+ await refetchUntilUpdate('account-margin-change');
setTxState('complete');
- handleRefetch('account-margin-change');
onComplete?.();
onDismiss();
},
@@ -147,7 +147,7 @@ export default function DepositWithdrawCrossMargin({
crossMarginAccountContract,
amount,
monitorTransaction,
- handleRefetch,
+ refetchUntilUpdate,
onComplete,
onDismiss,
]);
@@ -165,6 +165,10 @@ export default function DepositWithdrawCrossMargin({
}
}, [amount, freeMargin, transferType, susdBal, t]);
+ const isApproved = useMemo(() => {
+ return allowance.gt(wei(amount || 0));
+ }, [allowance, amount]);
+
const handleSetMax = React.useCallback(() => {
setAmount(susdBal.toString());
}, [susdBal]);
@@ -215,11 +219,13 @@ export default function DepositWithdrawCrossMargin({
) : (
disabledReason ||
- t(
- `futures.market.trade.margin.modal.${
- transferType === 0 ? 'deposit' : 'withdraw'
- }.button`
- )
+ (transferType === 0
+ ? t(
+ `futures.market.trade.margin.modal.deposit.${
+ isApproved ? 'button' : 'approve-button'
+ }`
+ )
+ : t(`futures.market.trade.margin.modal.withdraw.button`))
)}
@@ -253,6 +259,7 @@ export const MarginActionButton = styled(Button)`
margin-top: 16px;
height: 55px;
font-size: 15px;
+ text-transform: initial;
`;
export const MaxButton = styled.button`
diff --git a/sections/futures/TradeCrossMargin/EditLeverageModal.tsx b/sections/futures/TradeCrossMargin/EditLeverageModal.tsx
index 565514ca58..39a9aaa367 100644
--- a/sections/futures/TradeCrossMargin/EditLeverageModal.tsx
+++ b/sections/futures/TradeCrossMargin/EditLeverageModal.tsx
@@ -2,7 +2,7 @@ import { wei } from '@synthetixio/wei';
import { debounce } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { useRecoilValue } from 'recoil';
+import { useRecoilState, useRecoilValue } from 'recoil';
import styled from 'styled-components';
import BaseModal from 'components/BaseModal';
@@ -17,15 +17,19 @@ import TransactionNotifier from 'containers/TransactionNotifier';
import { useFuturesContext } from 'contexts/FuturesContext';
import { useRefetchContext } from 'contexts/RefetchContext';
import usePersistedRecoilState from 'hooks/usePersistedRecoilState';
+import { ORDER_PREVIEW_ERRORS_I18N, previewErrorI18n } from 'queries/futures/constants';
import {
crossMarginTotalMarginState,
currentMarketState,
marketInfoState,
+ orderTypeState,
positionState,
+ potentialTradeDetailsState,
preferredLeverageState,
tradeFeesState,
} from 'store/futures';
import { FlexDivRow, FlexDivRowCentered } from 'styles/common';
+import { isUserDeniedError } from 'utils/formatters/error';
import { formatDollars } from 'utils/formatters/number';
import logError from 'utils/logError';
@@ -40,7 +44,7 @@ type DepositMarginModalProps = {
export default function EditLeverageModal({ onDismiss }: DepositMarginModalProps) {
const { t } = useTranslation();
const { monitorTransaction } = TransactionNotifier.useContainer();
- const { handleRefetch } = useRefetchContext();
+ const { handleRefetch, refetchUntilUpdate } = useRefetchContext();
const {
selectedLeverage,
onLeverageChange,
@@ -53,6 +57,8 @@ export default function EditLeverageModal({ onDismiss }: DepositMarginModalProps
const marketAsset = useRecoilValue(currentMarketState);
const totalMargin = useRecoilValue(crossMarginTotalMarginState);
const tradeFees = useRecoilValue(tradeFeesState);
+ const { error: previewError, data: previewData } = useRecoilValue(potentialTradeDetailsState);
+ const [orderType, setOrderType] = useRecoilState(orderTypeState);
const [preferredLeverage, setPreferredLeverage] = usePersistedRecoilState(preferredLeverageState);
@@ -62,18 +68,27 @@ export default function EditLeverageModal({ onDismiss }: DepositMarginModalProps
const maxLeverage = Number((market?.maxLeverage || wei(DEFAULT_LEVERAGE)).toString(2));
+ useEffect(() => {
+ if (orderType !== 'market') {
+ setOrderType('market');
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
const maxPositionUsd = useMemo(() => {
return totalMargin.mul(leverage);
}, [totalMargin, leverage]);
const handleIncrease = () => {
- const newLeverage = Math.max(leverage + 1, 1);
+ let newLeverage = wei(leverage).add(1).toNumber();
+ newLeverage = Math.max(newLeverage, 1);
setLeverage(Math.min(newLeverage, maxLeverage));
previewPositionChange(newLeverage);
};
const handleDecrease = () => {
- const newLeverage = Math.max(leverage - 1, 1);
+ let newLeverage = wei(leverage).sub(1).toNumber();
+ newLeverage = Math.max(newLeverage, 1);
setLeverage(newLeverage);
previewPositionChange(newLeverage);
};
@@ -81,7 +96,9 @@ export default function EditLeverageModal({ onDismiss }: DepositMarginModalProps
// eslint-disable-next-line react-hooks/exhaustive-deps
const previewPositionChange = useCallback(
debounce((leverage: number) => {
- onLeverageChange(leverage);
+ if (leverage >= 1) {
+ onLeverageChange(leverage);
+ }
}, 200),
[onLeverageChange]
);
@@ -99,17 +116,22 @@ export default function EditLeverageModal({ onDismiss }: DepositMarginModalProps
setError(failureMessage?.failureReason || t('common.transaction.transaction-failed'));
},
onTxConfirmed: () => {
- resetTradeState();
- handleRefetch('modify-position');
- onDismiss();
+ try {
+ resetTradeState();
+ handleRefetch('modify-position');
+ refetchUntilUpdate('account-margin-change');
+ setSubmitting(false);
+ onDismiss();
+ } catch (err) {
+ logError(err);
+ }
},
});
}
} catch (err) {
+ setSubmitting(false);
setError(t('common.transaction.transaction-failed'));
logError(err);
- } finally {
- setSubmitting(false);
}
resetTradeState();
} else {
@@ -133,6 +155,7 @@ export default function EditLeverageModal({ onDismiss }: DepositMarginModalProps
onLeverageChange,
submitCrossMarginOrder,
setError,
+ refetchUntilUpdate,
handleRefetch,
onDismiss,
]);
@@ -152,6 +175,8 @@ export default function EditLeverageModal({ onDismiss }: DepositMarginModalProps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
+ const errorMessage = error || previewError;
+
return (
{t('futures.market.trade.leverage.modal.input-label')}:
{
setLeverage(Number(v));
previewPositionChange(Number(v));
@@ -193,12 +218,14 @@ export default function EditLeverageModal({ onDismiss }: DepositMarginModalProps
{position?.position && (
<>
+
{tradeFees.total.gt(0) && }
>
)}
: t('futures.market.trade.leverage.modal.confirm')}
- {error && (
+ {errorMessage && !isUserDeniedError(errorMessage) && (
<>
-
+
>
)}
@@ -231,6 +267,7 @@ const MaxPosContainer = styled(FlexDivRowCentered)`
`;
const Label = styled.p`
+ font-size: 13px;
color: ${(props) => props.theme.colors.selectedTheme.gray};
`;
diff --git a/sections/futures/TradeCrossMargin/ManageKeeperBalanceModal.tsx b/sections/futures/TradeCrossMargin/ManageKeeperBalanceModal.tsx
index d20f0c7c4e..ddd025fd96 100644
--- a/sections/futures/TradeCrossMargin/ManageKeeperBalanceModal.tsx
+++ b/sections/futures/TradeCrossMargin/ManageKeeperBalanceModal.tsx
@@ -1,9 +1,10 @@
import { wei } from '@synthetixio/wei';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
+import { useRecoilValue } from 'recoil';
import styled from 'styled-components';
-import Error from 'components/Error';
+import ErrorView from 'components/Error';
import CustomInput from 'components/Input/CustomInput';
import Loader from 'components/Loader';
import SegmentedControl from 'components/SegmentedControl';
@@ -12,7 +13,7 @@ import Connector from 'containers/Connector';
import TransactionNotifier from 'containers/TransactionNotifier';
import { useRefetchContext } from 'contexts/RefetchContext';
import useCrossMarginAccountContracts from 'hooks/useCrossMarginContracts';
-import useCrossMarginKeeperDeposit from 'hooks/useCrossMarginKeeperEthBal';
+import { crossMarginAccountOverviewState, openOrdersState } from 'store/futures';
import { isUserDeniedError } from 'utils/formatters/error';
import { formatCurrency, zeroBN } from 'utils/formatters/number';
import logError from 'utils/logError';
@@ -36,11 +37,13 @@ const DEPOSIT_ENABLED = false;
export default function ManageKeeperBalanceModal({ onDismiss, defaultType }: Props) {
const { t } = useTranslation();
- const { keeperEthBal, getKeeperEthBal } = useCrossMarginKeeperDeposit();
const { crossMarginAccountContract } = useCrossMarginAccountContracts();
const { monitorTransaction } = TransactionNotifier.useContainer();
const { provider, walletAddress } = Connector.useContainer();
- const { handleRefetch } = useRefetchContext();
+ const { refetchUntilUpdate } = useRefetchContext();
+
+ const { keeperEthBal } = useRecoilValue(crossMarginAccountOverviewState);
+ const openOrders = useRecoilValue(openOrdersState);
const [amount, setAmount] = useState('');
const [isMax, setMax] = useState(false);
@@ -73,13 +76,10 @@ export default function ManageKeeperBalanceModal({ onDismiss, defaultType }: Pro
if (tx?.hash) {
monitorTransaction({
txHash: tx.hash,
- onTxConfirmed: () => {
- setTimeout(() => {
- handleRefetch('account-margin-change');
- setTransacting(false);
- getKeeperEthBal();
- onDismiss();
- }, 2000);
+ onTxConfirmed: async () => {
+ refetchUntilUpdate('account-margin-change');
+ setTransacting(false);
+ onDismiss();
},
});
}
@@ -95,9 +95,8 @@ export default function ManageKeeperBalanceModal({ onDismiss, defaultType }: Pro
crossMarginAccountContract,
amount,
t,
- handleRefetch,
+ refetchUntilUpdate,
onDismiss,
- getKeeperEthBal,
monitorTransaction,
]);
@@ -202,8 +201,15 @@ export default function ManageKeeperBalanceModal({ onDismiss, defaultType }: Pro
)
)}
+ {openOrders.length && transferType === 1 && (
+
+ )}
- {error && }
+ {error && }
);
}
diff --git a/sections/futures/TradeCrossMargin/TradeCrossMargin.tsx b/sections/futures/TradeCrossMargin/TradeCrossMargin.tsx
index c33d621620..dd6c05d7de 100644
--- a/sections/futures/TradeCrossMargin/TradeCrossMargin.tsx
+++ b/sections/futures/TradeCrossMargin/TradeCrossMargin.tsx
@@ -1,14 +1,11 @@
-import { useConnectModal } from '@rainbow-me/rainbowkit';
-import { wei } from '@synthetixio/wei';
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
-import styled from 'styled-components';
+import { useTheme } from 'styled-components';
import DepositArrow from 'assets/svg/futures/deposit-arrow.svg';
import WithdrawArrow from 'assets/svg/futures/withdraw-arrow.svg';
import Loader from 'components/Loader';
import SegmentedControl from 'components/SegmentedControl';
-import StyledSlider from 'components/Slider/StyledSlider';
import Spacer from 'components/Spacer';
import { CROSS_MARGIN_ORDER_TYPES } from 'constants/futures';
import Connector from 'containers/Connector';
@@ -18,14 +15,12 @@ import {
futuresAccountState,
futuresAccountTypeState,
leverageSideState,
- futuresTradeInputsState,
orderTypeState,
futuresOrderPriceState,
marketAssetRateState,
showCrossMarginOnboardState,
crossMarginAccountOverviewState,
} from 'store/futures';
-import { FlexDivRow } from 'styles/common';
import { ceilNumber, floorNumber } from 'utils/formatters/number';
import { orderPriceInvalidLabel } from 'utils/futures';
@@ -47,38 +42,21 @@ type Props = {
export default function TradeCrossMargin({ isMobile }: Props) {
const { walletAddress } = Connector.useContainer();
+ const { colors } = useTheme();
const [leverageSide, setLeverageSide] = useRecoilState(leverageSideState);
const { crossMarginAddress, crossMarginAvailable, status } = useRecoilValue(futuresAccountState);
const selectedAccountType = useRecoilValue(futuresAccountTypeState);
const { freeMargin } = useRecoilValue(crossMarginAccountOverviewState);
-
- const { susdSize } = useRecoilValue(futuresTradeInputsState);
const marketAssetRate = useRecoilValue(marketAssetRateState);
const [orderType, setOrderType] = useRecoilState(orderTypeState);
const [orderPrice, setOrderPrice] = useRecoilState(futuresOrderPriceState);
- const { onTradeAmountChange, maxUsdInputAmount, onTradeOrderPriceChange } = useFuturesContext();
- const { openConnectModal: connectWallet } = useConnectModal();
+ const { onTradeOrderPriceChange } = useFuturesContext();
- const [percent, setPercent] = useState(0);
- const [usdAmount, setUsdAmount] = useState(susdSize);
const [showOnboard, setShowOnboard] = useRecoilState(showCrossMarginOnboardState);
const [openTransferModal, setOpenTransferModal] = useState<'deposit' | 'withdraw' | null>(null);
- // eslint-disable-next-line
- const onChangeMarginPercent = useCallback(
- (value, commit = false) => {
- setPercent(value);
- const fraction = value / 100;
- const usdAmount = maxUsdInputAmount.mul(fraction).toString();
- const usdValue = Number(usdAmount).toFixed(0);
- setUsdAmount(usdValue);
- onTradeAmountChange(usdValue, 'usd', { simulateChange: !commit });
- },
- [onTradeAmountChange, maxUsdInputAmount]
- );
-
const onChangeOrderPrice = useCallback(
(price: string) => {
const invalidLabel = orderPriceInvalidLabel(price, leverageSide, marketAssetRate, orderType);
@@ -90,38 +68,20 @@ export default function TradeCrossMargin({ isMobile }: Props) {
[onTradeOrderPriceChange, setOrderPrice, leverageSide, marketAssetRate, orderType]
);
- useEffect(() => {
- if (susdSize !== usdAmount) {
- if (!susdSize || maxUsdInputAmount.eq(0)) {
- setPercent(0);
- return;
- }
-
- const percent = wei(susdSize).div(maxUsdInputAmount).mul(100).toNumber();
- setPercent(Number(percent.toFixed(2)));
- }
- // eslint-disable-next-line
- }, [susdSize]);
-
const headerButtons = walletAddress
? [
{
i18nTitle: 'futures.market.trade.button.deposit',
- Icon: DepositArrow,
+ icon: ,
onClick: () => setOpenTransferModal('deposit'),
},
{
i18nTitle: 'futures.market.trade.button.withdraw',
- Icon: WithdrawArrow,
+ icon: ,
onClick: () => setOpenTransferModal('withdraw'),
},
]
- : [
- {
- i18nTitle: 'futures.market.trade.button.connect-wallet',
- onClick: connectWallet,
- },
- ];
+ : [];
if (!showOnboard && (status === 'refetching' || status === 'initial-fetch')) return ;
@@ -136,7 +96,7 @@ export default function TradeCrossMargin({ isMobile }: Props) {
{!isMobile && }
- {}
+
-
-
- onChangeMarginPercent(value, false)}
- onChangeCommitted={(_, value) => onChangeMarginPercent(value, true)}
- marks={[
- { value: 0, label: `0%` },
- { value: 100, label: `100%` },
- ]}
- valueLabelDisplay="on"
- valueLabelFormat={(v) => `${v}%`}
- $currentMark={percent}
- />
-
+
{orderType !== 'market' && (
<>
);
}
-
-const SliderRow = styled(FlexDivRow)`
- margin-top: 8px;
- margin-bottom: 32px;
- position: relative;
-`;
diff --git a/sections/futures/Trades/Trades.tsx b/sections/futures/Trades/Trades.tsx
index ba34143430..70dea783f7 100644
--- a/sections/futures/Trades/Trades.tsx
+++ b/sections/futures/Trades/Trades.tsx
@@ -45,7 +45,7 @@ const Trades: React.FC = ({ history, isLoading, isLoaded, marketAss
feesPaid: trade?.feesPaid.div(ETH_UNIT),
id: trade?.txnHash,
asset: marketAsset,
- type: trade?.orderType === 'NextPrice' ? 'Next Price' : trade?.orderType,
+ type: trade?.orderType,
status: trade?.positionClosed ? TradeStatus.CLOSED : TradeStatus.OPEN,
};
});
diff --git a/sections/futures/UserInfo/OpenOrdersTable.tsx b/sections/futures/UserInfo/OpenOrdersTable.tsx
index 166dfe2d9f..56ed236783 100644
--- a/sections/futures/UserInfo/OpenOrdersTable.tsx
+++ b/sections/futures/UserInfo/OpenOrdersTable.tsx
@@ -18,6 +18,7 @@ import useNetworkSwitcher from 'hooks/useNetworkSwitcher';
import { FuturesOrder, PositionSide } from 'queries/futures/types';
import { currentMarketState, openOrdersState, selectedFuturesAddressState } from 'store/futures';
import { gasSpeedState } from 'store/wallet';
+import { formatDollars } from 'utils/formatters/number';
import { getDisplayAsset } from 'utils/futures';
import logError from 'utils/logError';
@@ -85,7 +86,7 @@ const OpenOrdersTable: React.FC = () => {
async (order: FuturesOrder | undefined) => {
if (!order) return;
setCancelling(order.id);
- if (order.orderType === 'Limit' || order.orderType === 'Stop') {
+ if (order.orderType === 'Limit' || order.orderType === 'Stop Market') {
try {
const id = order.id.split('-')[2];
const tx = await crossMarginAccountContract?.cancelOrder(id);
@@ -112,12 +113,22 @@ const OpenOrdersTable: React.FC = () => {
}, [cancelNextPriceOrder.hash, executeNextPriceOrder.hash]);
const rowsData = useMemo(() => {
- if (!cancelling) return openOrders;
- const copyOrders = [...openOrders];
- const cancellingIndex = copyOrders.findIndex((o) => o.id === cancelling);
- copyOrders[cancellingIndex] = { ...copyOrders[cancellingIndex], isCancelling: true };
- return copyOrders;
- }, [openOrders, cancelling]);
+ const ordersWithCancel = openOrders
+ .map((o) => ({ ...o, cancel: () => onCancel(o) }))
+ .sort((a, b) => {
+ return b.asset === currencyKey && a.asset !== currencyKey
+ ? 1
+ : b.asset === currencyKey && a.asset === currencyKey
+ ? 0
+ : -1;
+ });
+ const cancellingIndex = ordersWithCancel.findIndex((o) => o.id === cancelling);
+ ordersWithCancel[cancellingIndex] = {
+ ...ordersWithCancel[cancellingIndex],
+ isCancelling: true,
+ };
+ return ordersWithCancel;
+ }, [openOrders, cancelling, currencyKey, onCancel]);
return (
<>
@@ -155,7 +166,7 @@ const OpenOrdersTable: React.FC = () => {
{cellProps.row.original.market}
{cellProps.row.original.isStale && (
-
+
{t('futures.market.user.open-orders.badges.expired')}
)}
@@ -209,6 +220,20 @@ const OpenOrdersTable: React.FC = () => {
sortable: true,
width: 50,
},
+ {
+ Header: (
+
+ {t('futures.market.user.open-orders.table.reserved-margin')}
+
+ ),
+ accessor: 'marginDelta',
+ Cell: (cellProps: CellProps) => {
+ const { marginDelta } = cellProps.row.original;
+ return {formatDollars(marginDelta?.gt(0) ? marginDelta : '0')}
;
+ },
+ sortable: true,
+ width: 50,
+ },
{
Header: (
@@ -220,10 +245,7 @@ const OpenOrdersTable: React.FC = () => {
const cancellingRow = cellProps.row.original.isCancelling;
return (
-
onCancel(cellProps.row.original)}
- >
+
{t('futures.market.user.open-orders.actions.cancel')}
{cellProps.row.original.isExecutable && (
diff --git a/sections/futures/UserInfo/UserInfo.tsx b/sections/futures/UserInfo/UserInfo.tsx
index 102f7019b9..56dcc34129 100644
--- a/sections/futures/UserInfo/UserInfo.tsx
+++ b/sections/futures/UserInfo/UserInfo.tsx
@@ -13,10 +13,8 @@ import TabButton from 'components/Button/TabButton';
import { TabPanel } from 'components/Tab';
import ROUTES from 'constants/routes';
import Connector from 'containers/Connector';
-import { PositionHistory } from 'queries/futures/types';
import { FuturesTrade } from 'queries/futures/types';
import useGetFuturesMarginTransfers from 'queries/futures/useGetFuturesMarginTransfers';
-import useGetFuturesPositionForAccount from 'queries/futures/useGetFuturesPositionForAccount';
import useGetFuturesTradesForAccount from 'queries/futures/useGetFuturesTradesForAccount';
import FuturesPositionsTable from 'sections/dashboard/FuturesPositionsTable';
import {
@@ -53,10 +51,6 @@ const UserInfo: React.FC = () => {
const openOrders = useRecoilValue(openOrdersState);
const accountType = useRecoilValue(futuresAccountTypeState);
- const futuresPositionQuery = useGetFuturesPositionForAccount();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- const futuresPositionHistory = futuresPositionQuery?.data ?? [];
-
const [showShareModal, setShowShareModal] = useState(false);
const [hasOpenPosition, setHasOpenPosition] = useState(false);
const [openProfitCalcModal, setOpenProfitCalcModal] = useState(false);
@@ -156,16 +150,8 @@ const UserInfo: React.FC = () => {
);
useEffect(() => {
- let currentPosition: PositionHistory[] = [];
-
- if (futuresPositionHistory.length > 0) {
- currentPosition = futuresPositionHistory.filter(
- (obj: PositionHistory) => obj.asset === marketAsset
- );
-
- setHasOpenPosition(currentPosition.length === 0 ? false : true);
- }
- }, [futuresPositionHistory, marketAsset]);
+ setHasOpenPosition(!!position && !!position.position);
+ }, [position]);
return (
<>
@@ -203,10 +189,7 @@ const UserInfo: React.FC = () => {
-
+
@@ -238,7 +221,6 @@ const UserInfo: React.FC = () => {
position={position}
marketAsset={marketAsset}
setShowShareModal={setShowShareModal}
- futuresPositionHistory={futuresPositionHistory}
/>
)}
>
diff --git a/sections/homepage/Hero/Hero.tsx b/sections/homepage/Hero/Hero.tsx
index f0e878a700..f132f87c0f 100644
--- a/sections/homepage/Hero/Hero.tsx
+++ b/sections/homepage/Hero/Hero.tsx
@@ -8,9 +8,8 @@ import LogoNoTextSVG from 'assets/svg/brand/logo-no-text.svg';
import Button from 'components/Button';
import PoweredBySynthetix from 'components/PoweredBySynthetix';
import Webp from 'components/Webp';
+import { DEFAULT_FUTURES_MARGIN_TYPE } from 'constants/defaults';
import ROUTES from 'constants/routes';
-import usePersistedRecoilState from 'hooks/usePersistedRecoilState';
-import { futuresAccountTypeState } from 'store/futures';
import { FlexDivColCentered, GridDiv, Paragraph } from 'styles/common';
import media from 'styles/media';
@@ -18,7 +17,6 @@ import { StackSection } from '../common';
const Hero = () => {
const { t } = useTranslation();
- const [accountType] = usePersistedRecoilState(futuresAccountTypeState);
return (
@@ -34,7 +32,7 @@ const Hero = () => {
-
+
{t('homepage.nav.trade-now')}
diff --git a/sections/homepage/ShortList/ShortList.tsx b/sections/homepage/ShortList/ShortList.tsx
index 5203fc90f0..2aef2bd7ab 100644
--- a/sections/homepage/ShortList/ShortList.tsx
+++ b/sections/homepage/ShortList/ShortList.tsx
@@ -1,4 +1,3 @@
-import Wei, { wei } from '@synthetixio/wei';
import { useRouter } from 'next/router';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -12,41 +11,19 @@ import Loader from 'components/Loader';
import Table from 'components/Table';
import ROUTES from 'constants/routes';
import useENS from 'hooks/useENS';
-import { FuturesStat } from 'queries/futures/types';
import useGetFuturesCumulativeStats from 'queries/futures/useGetFuturesCumulativeStats';
import useGetStats from 'queries/futures/useGetStats';
import { FlexDivColCentered, FlexDivRow, SmallGoldenHeader, WhiteHeader } from 'styles/common';
import media, { Media } from 'styles/media';
import { formatDollars, formatNumber, zeroBN } from 'utils/formatters/number';
-import { truncateAddress } from 'utils/formatters/string';
-import { Copy, StackSection, Title } from '../common';
-
-type Stat = {
- pnl: Wei;
- liquidations: Wei;
- totalTrades: Wei;
- totalVolume: Wei;
-};
+import { StackSection, Title } from '../common';
const ShortList = () => {
const { t } = useTranslation();
const statsQuery = useGetStats(true);
const stats = useMemo(() => statsQuery.data ?? [], [statsQuery]);
- const pnlMap = useMemo(
- () =>
- stats.reduce((acc: Record, stat: FuturesStat) => {
- acc[stat.account] = {
- pnl: wei(stat.pnlWithFeesPaid ?? 0, 18, true),
- liquidations: new Wei(stat.liquidations ?? 0),
- totalTrades: new Wei(stat.totalTrades ?? 0),
- totalVolume: wei(stat.totalVolume ?? 0, 18, true),
- };
- return acc;
- }, {}),
- [stats]
- );
const router = useRouter();
const onClickTrader = (trader: string) => {
@@ -66,26 +43,6 @@ const ShortList = () => {
}
};
- let data = useMemo(
- () =>
- stats
- .sort(
- (a: FuturesStat, b: FuturesStat) =>
- (pnlMap[b.account]?.pnl || 0) - (pnlMap[a.account]?.pnl || 0)
- )
- .map((stat: FuturesStat, i: number) => ({
- rank: i + 1,
- trader: stat.account,
- traderShort: truncateAddress(stat.account),
- totalTrades: (pnlMap[stat.account]?.totalTrades ?? wei(0)).toNumber(),
- totalVolume: (pnlMap[stat.account]?.totalVolume ?? wei(0)).toNumber(),
- liquidations: (pnlMap[stat.account]?.liquidations ?? wei(0)).toNumber(),
- '24h': 80000,
- pnl: (pnlMap[stat.account]?.pnl ?? wei(0)).toNumber(),
- })),
- [stats, pnlMap]
- );
-
const title = (
<>
{t('homepage.shortlist.title')}
@@ -111,7 +68,7 @@ const ShortList = () => {
isLoading={statsQuery.isLoading}
showShortList
onTableRowClick={(row) => onClickTrader(row.original.trader)}
- data={data}
+ data={stats}
pageSize={5}
hideHeaders={false}
columns={[
@@ -193,7 +150,7 @@ const ShortList = () => {
isLoading={statsQuery.isLoading}
showShortList
onTableRowClick={(row) => onClickTrader(row.original.trader)}
- data={data}
+ data={stats}
pageSize={5}
hideHeaders={false}
columns={[
@@ -415,14 +372,6 @@ const StyledTrader = styled.a`
font-size: 15px;
`;
-const FeatureCopy = styled(Copy)`
- font-size: 15px;
- line-height: 150%;
- letter-spacing: -0.03em;
- color: ${(props) => props.theme.colors.common.secondaryGray};
- width: 183px;
-`;
-
const FeatureTitle = styled(Title)`
font-size: 24px;
line-height: 100%;
@@ -442,14 +391,4 @@ const SectionFeatureTitle = styled(FeatureTitle)`
`}
`;
-const SectionFeatureCopy = styled(FeatureCopy)`
- margin-top: 16px;
- text-align: center;
- width: 500px;
- font-size: 18px;
- ${media.lessThan('sm')`
- width: 100vw;
- `}
-`;
-
export default ShortList;
diff --git a/sections/homepage/TradeNow/TradeNow.tsx b/sections/homepage/TradeNow/TradeNow.tsx
index 3b901249d1..d007b6a5ae 100644
--- a/sections/homepage/TradeNow/TradeNow.tsx
+++ b/sections/homepage/TradeNow/TradeNow.tsx
@@ -4,15 +4,13 @@ import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
import Button from 'components/Button';
+import { DEFAULT_FUTURES_MARGIN_TYPE } from 'constants/defaults';
import ROUTES from 'constants/routes';
-import usePersistedRecoilState from 'hooks/usePersistedRecoilState';
-import { futuresAccountTypeState } from 'store/futures';
import { FlexDivColCentered, Paragraph, SmallGoldenHeader, WhiteHeader } from 'styles/common';
import media from 'styles/media';
const TradeNow = () => {
const { t } = useTranslation();
- const [accountType] = usePersistedRecoilState(futuresAccountTypeState);
const title = (
@@ -20,7 +18,7 @@ const TradeNow = () => {
{t('homepage.tradenow.description')}
{t('homepage.tradenow.categories')}
-
+
{t('homepage.nav.trade-now')}
diff --git a/sections/leaderboard/AllTime/AllTime.tsx b/sections/leaderboard/AllTime/AllTime.tsx
index 8843ccb061..c213395e06 100644
--- a/sections/leaderboard/AllTime/AllTime.tsx
+++ b/sections/leaderboard/AllTime/AllTime.tsx
@@ -9,18 +9,27 @@ import Table from 'components/Table';
import { DEFAULT_LEADERBOARD_ROWS } from 'constants/defaults';
import Connector from 'containers/Connector';
import useENSAvatar from 'hooks/useENSAvatar';
+import { AccountStat } from 'queries/futures/types';
-import { AccountStat, getMedal, PIN, StyledTrader } from '../common';
+import { getMedal, StyledTrader } from '../common';
type AllTimeProps = {
stats: AccountStat[];
isLoading: boolean;
- searchTerm: string;
+ pinRow: AccountStat[];
onClickTrader: (trader: string) => void;
compact?: boolean;
+ activeTab?: string;
};
-const AllTime: FC = ({ stats, isLoading, searchTerm, onClickTrader, compact }) => {
+const AllTime: FC = ({
+ stats,
+ isLoading,
+ pinRow,
+ onClickTrader,
+ compact,
+ activeTab,
+}) => {
const { t } = useTranslation();
const { staticMainnetProvider, walletAddress } = Connector.useContainer();
@@ -39,30 +48,8 @@ const AllTime: FC = ({ stats, isLoading, searchTerm, onClickTrader
}
const data = useMemo(() => {
- const statsData = stats
- .sort((a: AccountStat, b: AccountStat) => a.rank - b.rank)
- .map((trader: any) => {
- return {
- ...trader,
- rankText: trader.rank.toString(),
- };
- })
- .filter((i: { account: string; traderEns: string }) =>
- searchTerm?.length
- ? i.account.toLowerCase().includes(searchTerm) ||
- i.traderEns?.toLowerCase().includes(searchTerm)
- : true
- );
-
- const pinRow = statsData
- .filter((trader) => trader.account.toLowerCase() === walletAddress?.toLowerCase())
- .map((trader) => ({
- ...trader,
- rankText: `${trader.rank}${PIN}`,
- }));
-
- return [...pinRow, ...statsData];
- }, [stats, searchTerm, walletAddress]);
+ return [...pinRow, ...stats];
+ }, [stats, pinRow]);
return (
<>
@@ -77,11 +64,14 @@ const AllTime: FC = ({ stats, isLoading, searchTerm, onClickTrader
hiddenColumns={
compact ? ['rank', 'totalTrades', 'liquidations', 'totalVolume', 'pnl'] : undefined
}
+ columnsDeps={[activeTab]}
columns={[
{
Header: (
- {t('leaderboard.leaderboard.table.title')}
+
+ {activeTab} {t('leaderboard.leaderboard.table.title')}
+
),
accessor: 'title',
@@ -140,25 +130,20 @@ const AllTime: FC = ({ stats, isLoading, searchTerm, onClickTrader
{t('leaderboard.leaderboard.table.total-trades')}
),
accessor: 'totalTrades',
- sortType: 'basic',
width: 80,
- sortable: true,
},
{
Header: (
{t('leaderboard.leaderboard.table.liquidations')}
),
accessor: 'liquidations',
- sortType: 'basic',
width: 80,
- sortable: true,
},
{
Header: (
{t('leaderboard.leaderboard.table.total-volume')}
),
accessor: 'totalVolume',
- sortType: 'basic',
Cell: (cellProps: CellProps) => (
= ({ stats, isLoading, searchTerm, onClickTrader
/>
),
width: compact ? 'auto' : 100,
- sortable: true,
},
{
Header: {t('leaderboard.leaderboard.table.pnl')} ,
accessor: 'pnl',
- sortType: 'basic',
Cell: (cellProps: CellProps) => (
= ({ stats, isLoading, searchTerm, onClickTrader
/>
),
width: compact ? 'auto' : 100,
- sortable: true,
},
],
},
@@ -282,6 +264,7 @@ const TableTitle = styled.div`
const TitleText = styled.div`
font-family: ${(props) => props.theme.fonts.regular};
color: ${(props) => props.theme.colors.selectedTheme.gray};
+ text-transform: capitalize;
`;
const TableHeader = styled.div`
diff --git a/sections/leaderboard/Competition/Competition.tsx b/sections/leaderboard/Competition/Competition.tsx
index 7c4bd6c333..a82a62903c 100644
--- a/sections/leaderboard/Competition/Competition.tsx
+++ b/sections/leaderboard/Competition/Competition.tsx
@@ -9,10 +9,11 @@ import { DesktopOnlyView, MobileOrTabletView } from 'components/Media';
import Table, { TableNoResults } from 'components/Table';
import Connector from 'containers/Connector';
import useGetFile from 'queries/files/useGetFile';
+import { AccountStat } from 'queries/futures/types';
import { formatPercent } from 'utils/formatters/number';
import { truncateAddress } from 'utils/formatters/string';
-import { AccountStat, getMedal, PIN, StyledTrader, Tier } from '../common';
+import { getMedal, PIN, StyledTrader, Tier } from '../common';
import { COMPETITION_DATA_LOCATION, MOBILE_COMPETITION_START } from './constants';
type CompetitionProps = {
diff --git a/sections/leaderboard/Leaderboard/Leaderboard.tsx b/sections/leaderboard/Leaderboard/Leaderboard.tsx
index c051e52b52..fcaf1741b7 100644
--- a/sections/leaderboard/Leaderboard/Leaderboard.tsx
+++ b/sections/leaderboard/Leaderboard/Leaderboard.tsx
@@ -1,24 +1,21 @@
-import { wei } from '@synthetixio/wei';
+import { getAddress, isAddress } from 'ethers/lib/utils';
import { useRouter } from 'next/router';
-import { FC, useMemo, useState } from 'react';
-import { useRecoilValue } from 'recoil';
+import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import TabButton from 'components/Button/TabButton';
import Search from 'components/Table/Search';
import ROUTES from 'constants/routes';
+import useENS from 'hooks/useENS';
import useENSs from 'hooks/useENSs';
-import { FuturesStat } from 'queries/futures/types';
-import useGetStats from 'queries/futures/useGetStats';
+import { AccountStat } from 'queries/futures/types';
+import useLeaderboard, { DEFAULT_LEADERBOARD_DATA } from 'queries/futures/useLeaderboard';
import { CompetitionBanner } from 'sections/shared/components/CompetitionBanner';
-import { isCompetitionActive } from 'store/ui';
import { FlexDivCol } from 'styles/common';
import media from 'styles/media';
-import { truncateAddress } from 'utils/formatters/string';
import AllTime from '../AllTime';
-import { AccountStat, COMPETITION_TIERS, Tier } from '../common';
-import Competition from '../Competition';
+import { PIN } from '../common';
import TraderHistory from '../TraderHistory';
type LeaderboardProps = {
@@ -26,104 +23,132 @@ type LeaderboardProps = {
mobile?: boolean;
};
+enum LeaderboardTab {
+ Top = 'top',
+ Bottom = 'bottom',
+}
+
+const LEADERBOARD_TABS = [LeaderboardTab.Top, LeaderboardTab.Bottom];
+
const Leaderboard: FC = ({ compact, mobile }: LeaderboardProps) => {
- const competitionActive = useRecoilValue(isCompetitionActive);
+ const [activeTab, setActiveTab] = useState(LeaderboardTab.Top);
+ const [searchInput, setSearchInput] = useState('');
const [searchTerm, setSearchTerm] = useState('');
- const [activeTier, setActiveTier] = useState(competitionActive ? 'bronze' : null);
+ const [searchAddress, setSearchAddress] = useState('');
const [selectedTrader, setSelectedTrader] = useState('');
+ const searchEns = useENS(searchTerm);
const router = useRouter();
- const statsQuery = useGetStats();
- const statsData = useMemo(() => statsQuery.data ?? [], [statsQuery]);
+ const leaderboardQuery = useLeaderboard(searchAddress);
+ const leaderboardData = useMemo(() => leaderboardQuery.data ?? DEFAULT_LEADERBOARD_DATA, [
+ leaderboardQuery,
+ ]);
const traders = useMemo(
() =>
- statsData.map((stat: FuturesStat) => {
+ leaderboardData.all?.map((stat: AccountStat) => {
return stat.account;
}) ?? [],
- [statsData]
+ [leaderboardData]
);
+
const ensInfoQuery = useENSs(traders);
const ensInfo = useMemo(() => ensInfoQuery.data ?? {}, [ensInfoQuery]);
- let stats: AccountStat[] = useMemo(() => {
- return statsData
- .map((stat: FuturesStat) => ({
- account: stat.account,
- trader: stat.account,
- traderShort: truncateAddress(stat.account),
- traderEns: ensInfo[stat.account] ?? null,
- totalTrades: stat.totalTrades,
- totalVolume: wei(stat.totalVolume, 18, true).toNumber(),
- liquidations: stat.liquidations,
- pnl: wei(stat.pnlWithFeesPaid, 18, true).toNumber(),
- }))
- .filter((stat: FuturesStat) => stat.totalVolume > 0)
- .sort((a: FuturesStat, b: FuturesStat) => (b?.pnl || 0) - (a?.pnl || 0))
- .map((stat: FuturesStat, i: number) => ({
- rank: i + 1,
- ...stat,
- }));
- }, [statsData, ensInfo]);
+ const pinRow: AccountStat[] = useMemo(() => {
+ return leaderboardData.wallet
+ ? leaderboardData.wallet.map((trader) => ({
+ ...trader,
+ rankText: PIN,
+ }))
+ : [];
+ }, [leaderboardData.wallet]);
useMemo(() => {
if (router.asPath.startsWith(ROUTES.Leaderboard.Home) && router.query.trader) {
const trader = router.query.trader as string;
setSelectedTrader(trader);
} else {
+ setSearchInput('');
setSearchTerm('');
+ setSearchAddress('');
setSelectedTrader('');
}
return null;
}, [router.query, router.asPath]);
- const onChangeSearch = (text: string) => {
- setSearchTerm(text?.toLowerCase());
+ const onChangeSearch = async (text: string) => {
+ setSearchInput(text?.toLowerCase());
+
+ if (isAddress(text)) {
+ setSearchTerm(getAddress(text));
+ } else if (text.endsWith('.eth')) {
+ setSearchTerm(text);
+ } else {
+ setSearchTerm('');
+ }
};
const onClickTrader = (trader: string) => {
+ setSearchInput('');
setSearchTerm('');
+ setSearchAddress('');
setSelectedTrader(trader);
router.push(ROUTES.Leaderboard.Trader(trader));
};
const resetSelection = () => {
+ setSearchInput('');
setSearchTerm('');
+ setSearchAddress('');
setSelectedTrader('');
router.push(ROUTES.Leaderboard.Home);
};
+ useEffect(() => {
+ setSearchAddress(
+ searchEns.ensAddress ? searchEns.ensAddress : isAddress(searchTerm) ? searchTerm : ''
+ );
+ }, [searchTerm, searchEns]);
+
+ const mapEnsName = useCallback(
+ (stat: AccountStat) => ({
+ ...stat,
+ traderEns: ensInfo[stat.account] ?? null,
+ }),
+ [ensInfo]
+ );
+
+ const stats = useMemo(() => {
+ return {
+ top: leaderboardData.top.map(mapEnsName),
+ bottom: leaderboardData.bottom.map(mapEnsName),
+ wallet: leaderboardData.wallet.map(mapEnsName),
+ search: leaderboardData.search.map(mapEnsName),
+ all: leaderboardData.all.map(mapEnsName),
+ };
+ }, [leaderboardData, mapEnsName]);
+
return (
<>
- {competitionActive && (
-
- {COMPETITION_TIERS.map((tier) => (
- {
- setActiveTier(tier);
- setSelectedTrader('');
- }}
- />
- ))}
+
+ {LEADERBOARD_TABS.map((tab) => (
{
- setActiveTier(null);
+ setActiveTab(tab);
setSelectedTrader('');
}}
/>
-
- )}
+ ))}
+
-
+
@@ -135,21 +160,22 @@ const Leaderboard: FC = ({ compact, mobile }: LeaderboardProps
compact={compact}
searchTerm={searchTerm}
/>
- ) : activeTier ? (
-
) : (
)}
@@ -173,7 +199,7 @@ const StyledTabButton = styled(TabButton)`
const TabButtonContainer = styled.div<{ mobile?: boolean }>`
display: grid;
- grid-template-columns: repeat(4, 1fr);
+ grid-template-columns: repeat(2, 1fr);
margin-bottom: ${({ mobile }) => (mobile ? '16px' : '0px')};
`;
diff --git a/sections/leaderboard/TraderHistory/TraderHistory.tsx b/sections/leaderboard/TraderHistory/TraderHistory.tsx
index d9f0697980..61c3ad37ef 100644
--- a/sections/leaderboard/TraderHistory/TraderHistory.tsx
+++ b/sections/leaderboard/TraderHistory/TraderHistory.tsx
@@ -7,10 +7,11 @@ import styled from 'styled-components';
import Currency from 'components/Currency';
import CurrencyIcon from 'components/Currency/CurrencyIcon';
import { DesktopOnlyView, MobileOrTabletView } from 'components/Media';
+import FuturesIcon from 'components/Nav/FuturesIcon';
import Table from 'components/Table';
import ROUTES from 'constants/routes';
-import { PositionHistory } from 'queries/futures/types';
-import useGetFuturesAccountPositionHistory from 'queries/futures/useGetFuturesAccountPositionHistory';
+import { FuturesAccountTypes, PositionHistory } from 'queries/futures/types';
+import useGetFuturesPositionHistoryForAccount from 'queries/futures/useGetFuturesPositionHistoryForAccount';
import TimeDisplay from 'sections/futures/Trades/TimeDisplay';
import { FlexDiv } from 'styles/common';
import { getMarketName } from 'utils/futures';
@@ -31,8 +32,16 @@ const TraderHistory: FC = ({
searchTerm,
}: TraderHistoryProps) => {
const { t } = useTranslation();
- const positionsQuery = useGetFuturesAccountPositionHistory(trader);
- const positions = useMemo(() => positionsQuery.data ?? [], [positionsQuery]);
+ const positionsQuery = useGetFuturesPositionHistoryForAccount(trader);
+ const positions = useMemo(() => {
+ const positionData = positionsQuery.data;
+ return positionData
+ ? [
+ ...positionData[FuturesAccountTypes.ISOLATED_MARGIN],
+ ...positionData[FuturesAccountTypes.CROSS_MARGIN],
+ ]
+ : [];
+ }, [positionsQuery]);
const traderENSName = useMemo(() => ensInfo[trader] ?? null, [trader, ensInfo]);
let data = useMemo(() => {
@@ -40,23 +49,17 @@ const TraderHistory: FC = ({
.sort((a: PositionHistory, b: PositionHistory) => b.timestamp - a.timestamp)
.map((stat: PositionHistory, i: number) => {
return {
+ ...stat,
rank: i + 1,
currencyIconKey: stat.asset ? (stat.asset[0] !== 's' ? 's' : '') + stat.asset : '',
marketShortName: getMarketName(stat.asset),
- openTimestamp: stat.openTimestamp,
- asset: stat.asset,
status: stat.isOpen ? 'Open' : stat.isLiquidated ? 'Liquidated' : 'Closed',
- feesPaid: stat.feesPaid,
- netFunding: stat.netFunding,
pnl: stat.pnlWithFeesPaid,
pnlPct: `(${stat.pnlWithFeesPaid
.div(stat.initialMargin.add(stat.totalDeposits))
.mul(100)
.toNumber()
.toFixed(2)}%)`,
- totalVolume: stat.totalVolume,
- trades: stat.trades,
- side: stat.side,
};
})
.filter((i: { marketShortName: string; status: string }) =>
@@ -123,6 +126,7 @@ const TraderHistory: FC = ({
{cellProps.row.original.marketShortName}
+
),
width: compact ? 40 : 100,
@@ -235,9 +239,10 @@ const TraderHistory: FC = ({
{cellProps.row.original.marketShortName}
+
),
- width: 40,
+ width: 50,
},
{
Header: {t('leaderboard.trader-history.table.status')} ,
@@ -245,7 +250,7 @@ const TraderHistory: FC = ({
Cell: (cellProps: CellProps) => {
return {cellProps.row.original.status} ;
},
- width: 40,
+ width: 30,
},
{
Header: (
@@ -378,4 +383,8 @@ const StyledValue = styled.div`
text-align: end;
`;
+const StyledFuturesIcon = styled(FuturesIcon)`
+ margin-left: 5px;
+`;
+
export default TraderHistory;
diff --git a/sections/leaderboard/common.tsx b/sections/leaderboard/common.tsx
index 40e93e0c0e..dc41bb6389 100644
--- a/sections/leaderboard/common.tsx
+++ b/sections/leaderboard/common.tsx
@@ -1,4 +1,3 @@
-import Wei from '@synthetixio/wei';
import styled from 'styled-components';
export type Tier = 'gold' | 'silver' | 'bronze' | null;
@@ -16,18 +15,6 @@ export const getMedal = (position: number) => {
export const COMPETITION_TIERS: Tier[] = ['bronze', 'silver', 'gold'];
-export type AccountStat = {
- rank: number;
- account: string;
- trader: string;
- traderShort: string;
- traderEns: string | null;
- totalTrades: Wei;
- totalVolume: Wei;
- liquidation: Wei;
- pnl: Wei;
-};
-
export const PIN = ' ๐';
const Medal = styled.span`
diff --git a/sections/shared/Layout/AppLayout/Header/BalanceActions.tsx b/sections/shared/Layout/AppLayout/Header/BalanceActions.tsx
index 2c5e2e635a..e4639ab003 100644
--- a/sections/shared/Layout/AppLayout/Header/BalanceActions.tsx
+++ b/sections/shared/Layout/AppLayout/Header/BalanceActions.tsx
@@ -2,16 +2,22 @@ import { useRouter } from 'next/router';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { components } from 'react-select';
-import { useRecoilValue } from 'recoil';
+import { useRecoilValue, useSetRecoilState } from 'recoil';
import styled, { useTheme } from 'styled-components';
import Button from 'components/Button';
import CurrencyIcon from 'components/Currency/CurrencyIcon';
import Select from 'components/Select';
-import { balancesState, positionsState } from 'store/futures';
+import { FuturesAccountTypes, FuturesPosition } from 'queries/futures/types';
+import {
+ positionsState,
+ balancesState,
+ portfolioState,
+ futuresAccountTypeState,
+} from 'store/futures';
import { FlexDivRow, FlexDivRowCentered } from 'styles/common';
import { zeroBN, formatDollars } from 'utils/formatters/number';
-import { FuturesMarketAsset, getMarketName, MarketKeyByAsset } from 'utils/futures';
+import { getMarketName, MarketKeyByAsset } from 'utils/futures';
type ReactSelectOptionProps = {
label: string;
@@ -26,44 +32,41 @@ const BalanceActions: FC = () => {
const theme = useTheme();
const router = useRouter();
- const futuresPositions = useRecoilValue(positionsState);
+ const positions = useRecoilValue(positionsState);
+ const setFuturesAccountType = useSetRecoilState(futuresAccountTypeState);
+ const portfolio = useRecoilValue(portfolioState);
const { susdWalletBalance } = useRecoilValue(balancesState);
- const accessiblePositions = useMemo(
- () => futuresPositions?.filter((position) => position.remainingMargin.gt(zeroBN)) ?? [],
- [futuresPositions]
- );
-
- const totalRemainingMargin = accessiblePositions.reduce(
- (prev, position) => prev.add(position.remainingMargin),
- zeroBN
- );
-
const setMarketConfig = useCallback(
- (asset: FuturesMarketAsset): ReactSelectOptionProps => {
- const remainingMargin =
- accessiblePositions.find((position) => position.asset === asset)?.remainingMargin ?? zeroBN;
-
+ (position: FuturesPosition, accountType: FuturesAccountTypes): ReactSelectOptionProps => {
return {
- label: getMarketName(asset),
- synthIcon: MarketKeyByAsset[asset],
- marketRemainingMargin: formatDollars(remainingMargin),
- onClick: () => router.push(`/market/?asset=${asset}`),
+ label: getMarketName(position.asset),
+ synthIcon: MarketKeyByAsset[position.asset],
+ marketRemainingMargin: formatDollars(position.remainingMargin),
+ onClick: () => {
+ setFuturesAccountType(accountType);
+ return router.push(`/market/?asset=${position.asset}&accountType=${accountType}`);
+ },
};
},
- [accessiblePositions, router]
+ [router, setFuturesAccountType]
);
- const OPTIONS = useMemo(
- () => [
+ const OPTIONS = useMemo(() => {
+ const isolatedPositions = positions.isolated_margin
+ .filter((position) => position.remainingMargin.gt(zeroBN))
+ .map((position) => setMarketConfig(position, FuturesAccountTypes.ISOLATED_MARGIN));
+ const crossPositions = positions.cross_margin
+ .filter((position) => position.remainingMargin.gt(zeroBN))
+ .map((position) => setMarketConfig(position, FuturesAccountTypes.CROSS_MARGIN));
+ return [
{
label: 'header.balance.total-margin-label',
- totalAvailableMargin: formatDollars(totalRemainingMargin),
- options: accessiblePositions.map((market) => setMarketConfig(market.asset)),
+ totalAvailableMargin: formatDollars(portfolio.total),
+ options: [...isolatedPositions, ...crossPositions],
},
- ],
- [accessiblePositions, setMarketConfig, totalRemainingMargin]
- );
+ ];
+ }, [positions, setMarketConfig, portfolio]);
const OptionsGroupLabel: FC<{ label: string; totalAvailableMargin?: string }> = ({
label,
@@ -124,7 +127,7 @@ const BalanceActions: FC = () => {
return (
- {susdWalletBalance.eq(zeroBN) && futuresPositions?.length === 0 ? (
+ {susdWalletBalance.eq(zeroBN) && OPTIONS.length === 0 ? (
router.push(`/exchange/?quote=sUSD`)}
@@ -211,6 +214,7 @@ const StyledLabel = styled.div<{ noPadding: boolean }>`
const LabelContainer = styled(FlexDivRowCentered)`
color: ${(props) => props.theme.colors.selectedTheme.button.text.primary};
font-size: 13px;
+ line-height: 13px;
padding: 10px;
> div {
align-items: center;
diff --git a/sections/shared/Layout/AppLayout/Header/MobileUserMenu/MobileSettingsModal.tsx b/sections/shared/Layout/AppLayout/Header/MobileUserMenu/MobileSettingsModal.tsx
index 0aa21df743..570ec6b5cd 100644
--- a/sections/shared/Layout/AppLayout/Header/MobileUserMenu/MobileSettingsModal.tsx
+++ b/sections/shared/Layout/AppLayout/Header/MobileUserMenu/MobileSettingsModal.tsx
@@ -21,7 +21,7 @@ import { languageState } from 'store/app';
import { currentThemeState } from 'store/ui';
import colors from 'styles/theme/colors';
-import { lanugageIcons } from './common';
+import { languageIcon } from './common';
import MobileSubMenu from './MobileSubMenu';
type MobileSettingsModalProps = {
@@ -126,7 +126,7 @@ export const MobileSettingsModal: FC = ({ onDismiss })
onToggle={handleToggle('language')}
options={languageOptions.map((option) => ({
label: option.label,
- icon: {lanugageIcons[option.value as Language]}
,
+ icon: {languageIcon[option.value as Language]}
,
selected: languages[language] === option.value,
onClick: () => setLanguage(option.value as Language),
}))}
diff --git a/sections/shared/Layout/AppLayout/Header/MobileUserMenu/MobileSubMenu.tsx b/sections/shared/Layout/AppLayout/Header/MobileUserMenu/MobileSubMenu.tsx
index bffb61aba3..a4e43e6d71 100644
--- a/sections/shared/Layout/AppLayout/Header/MobileUserMenu/MobileSubMenu.tsx
+++ b/sections/shared/Layout/AppLayout/Header/MobileUserMenu/MobileSubMenu.tsx
@@ -6,8 +6,10 @@ import styled, { css } from 'styled-components';
import ChevronDown from 'assets/svg/app/chevron-down.svg';
import ChevronUp from 'assets/svg/app/chevron-up.svg';
+import Badge from 'components/Badge';
import ROUTES from 'constants/routes';
import { currentThemeState } from 'store/ui';
+import { FlexDivRow } from 'styles/common';
import { ThemeName } from 'styles/theme';
import { SubMenuLink } from '../constants';
@@ -53,12 +55,15 @@ const MobileSubMenu: React.FC = ({
{active && (
{links
- ? links.map(({ i18nLabel, link: subLink }) => (
+ ? links.map(({ i18nLabel, link: subLink, badge }) => (
ยท
- {t(i18nLabel)}
+
+ {t(i18nLabel)}{' '}
+ {badge && {t(badge.i18nLabel)} }
+
@@ -99,6 +104,17 @@ const SubMenuButton = styled(MenuButton)`
`}
`;
+const SubMenuRow = styled(FlexDivRow)`
+ justify-content: flex-start;
+ align-items: center;
+`;
+
+const StyledBadge = styled(Badge)`
+ font-size: 12px;
+ padding: 2.5px 5px 2px 5px;
+ margin-left: 8px;
+`;
+
const SubMenuContainer = styled.div`
box-sizing: border-box;
`;
@@ -137,13 +153,13 @@ const SubMenuItem = styled.div<{ currentTheme: ThemeName; active?: boolean; sele
${(props) =>
props.active &&
css`
- color: ${(props) => props.theme.colors.common.primaryWhite};
+ color: ${(props) => props.theme.colors.selectedTheme.button.active};
`}
${(props) =>
props.selected &&
css`
- color: ${(props) => props.theme.colors.common.secondaryGold};
+ color: ${(props) => props.theme.colors.selectedTheme.gold};
`}
`;
diff --git a/sections/shared/Layout/AppLayout/Header/MobileUserMenu/MobileUserMenu.tsx b/sections/shared/Layout/AppLayout/Header/MobileUserMenu/MobileUserMenu.tsx
index fba9edef89..f16c9b0c53 100644
--- a/sections/shared/Layout/AppLayout/Header/MobileUserMenu/MobileUserMenu.tsx
+++ b/sections/shared/Layout/AppLayout/Header/MobileUserMenu/MobileUserMenu.tsx
@@ -49,6 +49,9 @@ const MobileUserMenu: FC = () => {
{!!isOpen ? : }
+ {!(window.location.pathname === ROUTES.Home.Root) && isOpen === 'menu' && (
+ {t('modals.menu.title')}
+ )}
{!(window.location.pathname === ROUTES.Home.Root) && isOpen === 'settings' && (
{t('modals.settings.title')}
diff --git a/sections/shared/Layout/AppLayout/Header/MobileUserMenu/common.ts b/sections/shared/Layout/AppLayout/Header/MobileUserMenu/common.ts
index 79b7e14f6a..8173177554 100644
--- a/sections/shared/Layout/AppLayout/Header/MobileUserMenu/common.ts
+++ b/sections/shared/Layout/AppLayout/Header/MobileUserMenu/common.ts
@@ -47,6 +47,6 @@ export const SUB_MENUS = {
],
};
-export const lanugageIcons = {
+export const languageIcon = {
en: '๐',
};
diff --git a/sections/shared/Layout/AppLayout/Header/Nav/Nav.tsx b/sections/shared/Layout/AppLayout/Header/Nav/Nav.tsx
index 256715d3a0..d74f5661dc 100644
--- a/sections/shared/Layout/AppLayout/Header/Nav/Nav.tsx
+++ b/sections/shared/Layout/AppLayout/Header/Nav/Nav.tsx
@@ -5,21 +5,22 @@ import { useTranslation } from 'react-i18next';
import { useRecoilValue } from 'recoil';
import styled from 'styled-components';
+import Badge from 'components/Badge';
import LabelContainer from 'components/Nav/DropDownLabel';
import Select from 'components/Select';
import { DropdownIndicator, IndicatorSeparator } from 'components/Select/Select';
import { currentMarketState } from 'store/futures';
import { linkCSS } from 'styles/common';
-import { DESKTOP_NAV_LINKS } from '../constants';
+import { DESKTOP_NAV_LINKS, Badge as BadgeType } from '../constants';
type ReactSelectOptionProps = {
- label: string;
+ i18nLabel: string;
postfixIcon?: string;
isActive: boolean;
link: string;
+ badge: BadgeType;
Icon: FunctionComponent;
- onClick?: () => {};
};
const Nav: FC = () => {
@@ -31,17 +32,26 @@ const Nav: FC = () => {
return link.slice(0, 7) === '/market' ? `/market/?asset=${currentMarket}` : link;
}
- const formatOptionLabel = ({ label, Icon, link, isActive, onClick }: ReactSelectOptionProps) => {
- if (label === 'header.nav.markets')
+ const formatOptionLabel = ({
+ i18nLabel,
+ Icon,
+ badge,
+ link,
+ isActive,
+ }: ReactSelectOptionProps) => {
+ if (i18nLabel === 'header.nav.markets')
return (
-
- {t(label)}
+
+ {t(i18nLabel)}
);
return (
-
+
- {label !== 'Futures' ? t(label) : {t(label)} }
+
+ {t(i18nLabel)}
+ {badge && {t(badge.i18nLabel)} }
+
{Icon && }
@@ -53,8 +63,9 @@ const Nav: FC = () => {
{DESKTOP_NAV_LINKS.map(({ i18nLabel, link, links }) => {
const routeBase = asPath.split('/')[1];
- const linkBase = link.split('/')[1];
+ const linkBase = link.split('/')[1]?.split('?')[0];
const isActive = routeBase === linkBase;
+
const url = getLink(link);
if (!links) {
return (
@@ -66,18 +77,14 @@ const Nav: FC = () => {
);
}
- const options = links.map((l) => {
- return { label: l.i18nLabel, Icon: l.Icon, link: l.link, onClick: () => {} };
- });
-
return (
props.theme.fonts.bold};
font-size: 15px;
+ line-height: 15px;
`;
const MenuInside = styled.div<{ isActive: boolean; isDropDown?: boolean }>`
diff --git a/sections/shared/Layout/AppLayout/Header/constants.ts b/sections/shared/Layout/AppLayout/Header/constants.tsx
similarity index 91%
rename from sections/shared/Layout/AppLayout/Header/constants.ts
rename to sections/shared/Layout/AppLayout/Header/constants.tsx
index 9e938b6a38..cee981ccf5 100644
--- a/sections/shared/Layout/AppLayout/Header/constants.ts
+++ b/sections/shared/Layout/AppLayout/Header/constants.tsx
@@ -5,9 +5,15 @@ import { CROSS_MARGIN_ENABLED, DEFAULT_FUTURES_MARGIN_TYPE } from 'constants/def
import { EXTERNAL_LINKS } from 'constants/links';
import ROUTES from 'constants/routes';
+export type Badge = {
+ i18nLabel: string;
+ color: 'yellow' | 'red';
+};
+
export type SubMenuLink = {
i18nLabel: string;
link: string;
+ badge?: Badge;
Icon?: FunctionComponent;
};
@@ -68,6 +74,10 @@ export const getMenuLinks = (isMobile: boolean): MenuLinks => [
{
link: ROUTES.Markets.Home('cross_margin'),
i18nLabel: 'header.nav.cross-margin',
+ badge: {
+ i18nLabel: 'header.nav.beta-badge',
+ color: 'yellow',
+ },
Icon: CrossMarginIcon,
},
]
diff --git a/store/futures/index.ts b/store/futures/index.ts
index 3ee0beea7a..70ee323812 100644
--- a/store/futures/index.ts
+++ b/store/futures/index.ts
@@ -15,6 +15,10 @@ import {
FuturesOrderType,
FuturesVolumes,
CrossMarginAccounts,
+ FuturesPositionsState,
+ PositionHistoryState,
+ FuturesAccountTypes,
+ FuturesOrder,
} from 'queries/futures/types';
import { FundingRateResponse } from 'queries/futures/useGetAverageFundingRateForMarkets';
import { Price, Rates } from 'queries/rates/types';
@@ -68,6 +72,32 @@ export const balancesState = atom({
},
});
+export const portfolioState = selector({
+ key: getFuturesKey('portfolio'),
+ get: ({ get }) => {
+ const positions = get(positionsState);
+ const { freeMargin } = get(crossMarginAccountOverviewState);
+
+ const isolatedValue =
+ positions.isolated_margin.reduce(
+ (sum, { remainingMargin }) => sum.add(remainingMargin),
+ wei(0)
+ ) ?? wei(0);
+ const crossValue =
+ positions.cross_margin.reduce(
+ (sum, { remainingMargin }) => sum.add(remainingMargin),
+ wei(0)
+ ) ?? wei(0);
+ const totalValue = isolatedValue.add(crossValue).add(freeMargin);
+
+ return {
+ total: totalValue,
+ crossMarginFutures: crossValue.add(freeMargin),
+ isolatedMarginFutures: isolatedValue,
+ };
+ },
+});
+
export const activeTabState = atom({
key: getFuturesKey('activeTab'),
default: 0,
@@ -78,9 +108,20 @@ export const positionState = atom({
default: null,
});
-export const positionsState = atom({
+export const positionHistoryState = atom({
+ key: getFuturesKey('positionHistory'),
+ default: {
+ [FuturesAccountTypes.CROSS_MARGIN]: [],
+ [FuturesAccountTypes.ISOLATED_MARGIN]: [],
+ },
+});
+
+export const positionsState = atom({
key: getFuturesKey('positions'),
- default: null,
+ default: {
+ cross_margin: [],
+ isolated_margin: [],
+ },
});
export const futuresMarketsState = atom({
@@ -146,6 +187,7 @@ export const crossMarginAccountOverviewState = atom({
default: {
freeMargin: zeroBN,
keeperEthBal: zeroBN,
+ allowance: zeroBN,
},
});
@@ -190,7 +232,7 @@ export const isAdvancedOrderState = selector({
key: getFuturesKey('isAdvancedOrder'),
get: ({ get }) => {
const orderType = get(orderTypeState);
- return orderType === 'limit' || orderType === 'stop';
+ return orderType === 'limit' || orderType === 'stop market';
},
});
@@ -226,7 +268,7 @@ export const leverageValueCommittedState = atom({
default: true,
});
-export const openOrdersState = atom({
+export const openOrdersState = atom({
key: getFuturesKey('openOrders'),
default: [],
});
@@ -258,16 +300,18 @@ export const maxLeverageState = selector({
const orderType = get(orderTypeState);
const market = get(marketInfoState);
const leverageSide = get(leverageSideState);
+ const accountType = get(futuresAccountTypeState);
const positionLeverage = position?.position?.leverage ?? wei(0);
const positionSide = position?.position?.side;
const marketMaxLeverage = market?.maxLeverage ?? DEFAULT_MAX_LEVERAGE;
const adjustedMaxLeverage =
- orderType === 'next-price'
+ orderType === 'next price'
? marketMaxLeverage.mul(DEFAULT_NP_LEVERAGE_ADJUSTMENT)
: marketMaxLeverage;
if (!positionLeverage || positionLeverage.eq(wei(0))) return adjustedMaxLeverage;
+ if (accountType === 'cross_margin') return adjustedMaxLeverage;
if (positionSide === leverageSide) {
return adjustedMaxLeverage?.sub(positionLeverage);
} else {
@@ -314,6 +358,15 @@ export const selectedFuturesAddressState = selector({
},
});
+export const aboveMaxLeverageState = selector({
+ key: getFuturesKey('aboveMaxLeverage'),
+ get: ({ get }) => {
+ const position = get(positionState);
+ const maxLeverage = get(maxLeverageState);
+ return position?.position?.leverage && maxLeverage.lt(position.position.leverage);
+ },
+});
+
export const crossMarginAccountsState = atom({
key: getFuturesKey('crossMarginAccounts'),
default: {},
@@ -387,9 +440,9 @@ export const placeOrderTranslationKeyState = selector({
remainingMargin = positionMargin.add(freeMargin);
}
- if (orderType === 'next-price') return 'futures.market.trade.button.place-next-price-order';
+ if (orderType === 'next price') return 'futures.market.trade.button.place-next-price-order';
if (orderType === 'limit') return 'futures.market.trade.button.place-limit-order';
- if (orderType === 'stop') return 'futures.market.trade.button.place-stop-order';
+ if (orderType === 'stop market') return 'futures.market.trade.button.place-stop-order';
if (!!position?.position) return 'futures.market.trade.button.modify-position';
return remainingMargin.lt('50')
? 'futures.market.trade.button.deposit-margin-minimum'
diff --git a/store/ui/index.ts b/store/ui/index.ts
index 0da1648c27..a9837dfb21 100644
--- a/store/ui/index.ts
+++ b/store/ui/index.ts
@@ -1,7 +1,7 @@
import { atom } from 'recoil';
import { DEFAULT_SLIPPAGE } from 'constants/defaults';
-import { MarketsTab } from 'sections/dashboard/Markets/Markets';
+import { PositionsTab } from 'sections/dashboard/Overview/Overview';
import { localStorageEffect } from 'store/effects';
import { ThemeName } from 'styles/theme';
@@ -28,7 +28,7 @@ export const isCompetitionActive = atom({
default: process.env.NEXT_PUBLIC_COMPETITION_ACTIVE === 'true',
});
-export const activePositionsTabState = atom({
+export const activePositionsTabState = atom({
key: getUIKey('activePositionsTabState'),
- default: MarketsTab.FUTURES,
+ default: PositionsTab.CROSS_MARGIN,
});
diff --git a/styles/common.tsx b/styles/common.tsx
index 5de0d70e38..6aa6d97d98 100644
--- a/styles/common.tsx
+++ b/styles/common.tsx
@@ -300,6 +300,9 @@ export const SwapCurrenciesButton = styled.button`
transform: rotate(180deg);
transition-duration: 0.12s;
transition-timing-function: ease-in-out;
+ path {
+ fill: ${(props) => props.theme.colors.selectedTheme.white};
+ }
}
.arrow {
diff --git a/styles/theme/colors/common.ts b/styles/theme/colors/common.ts
index dbc7f801d9..41604ba779 100644
--- a/styles/theme/colors/common.ts
+++ b/styles/theme/colors/common.ts
@@ -5,10 +5,13 @@ const common = {
primaryGold: '#C9975B',
primaryRed: '#EF6868',
primaryGreen: '#7FD482',
- secondaryGray: '#787878',
+ secondaryGray: '#515151',
tertiaryGray: '#999999',
secondaryGold: '#E4B378',
primaryYellow: '#FFB800',
+ black: '#171002',
+ dark: { white: '#ECE8E3', yellow: '#FFB800' },
+ light: { white: '#F2F2F2', yellow: '#6A3300' },
};
export default common;
diff --git a/styles/theme/colors/dark.ts b/styles/theme/colors/dark.ts
index 649df9ded2..ec724ccd56 100644
--- a/styles/theme/colors/dark.ts
+++ b/styles/theme/colors/dark.ts
@@ -6,6 +6,7 @@ const darkTheme = {
outlineBorder: '1px solid rgba(255, 255, 255, 0.12)',
red: '#EF6868',
green: '#7FD482',
+ orange: '#DA8332',
black: '#171002',
white: '#ECE8E3',
gray: '#787878',
@@ -13,7 +14,10 @@ const darkTheme = {
yellow: common.primaryYellow,
table: { fill: 'rgba(255, 255, 255, 0.01)', hover: 'rgba(255, 255, 255, 0.05)' },
gold: '#E4B378',
- badge: { background: '#EF6868', text: 'black' },
+ badge: {
+ red: { background: '#EF6868', text: 'black' },
+ yellow: { background: common.primaryYellow, text: 'black' },
+ },
tab: { background: { active: '#252525', inactive: 'transparent' } },
button: {
border: 'rgb(255 255 255 / 10%)',
@@ -55,6 +59,7 @@ const darkTheme = {
},
disabled: { border: '1px solid #353333', text: '#353333' },
},
+ pill: { background: common.dark.yellow, text: common.dark.yellow, hover: common.black },
},
input: {
background: '#151515',
@@ -63,6 +68,7 @@ const darkTheme = {
},
placeholder: '#787878',
shadow: '0px 0.5px 0px rgba(255, 255, 255, 0.08)',
+ hover: common.dark.white,
},
segmented: {
background: '#0b0b0b',
diff --git a/styles/theme/colors/light.ts b/styles/theme/colors/light.ts
index 1bfebb7aca..5682a5836a 100644
--- a/styles/theme/colors/light.ts
+++ b/styles/theme/colors/light.ts
@@ -6,14 +6,18 @@ const lightTheme = {
outlineBorder: '1px solid rgba(0,0,0,0.17)',
red: '#A80300',
green: '#1D5D1F',
+ orange: '#DA8332',
black: '#171002',
white: '#F2F2F2',
gray: '#515151',
gray2: '#D2D2D2', // TODO: Update once added to designs
- yellow: common.primaryYellow,
+ yellow: '#6A3300',
table: { fill: '#EEE', hover: '#E6E6E6' },
gold: '#724713',
- badge: { background: '#A80300', text: 'white' },
+ badge: {
+ red: { background: '#A80300', text: 'white' },
+ yellow: { background: '#6A3300', text: 'white' },
+ },
tab: { background: { active: 'transparent', inactive: '#e8e8e8' } },
button: {
border: 'rgb(0 0 0 / 10%)',
@@ -26,7 +30,8 @@ const lightTheme = {
'0px 2px 2px rgb(0 0 0 / 5%), inset 0px 1px 0px rgb(255 255 255 / 8%), inset 0px 0px 20px rgb(255 255 255 / 3%)',
text: {
primary: '#171002',
- yellow: common.primaryYellow,
+ yellow: '#6A3300',
+ white: '#FFFFFF',
},
primary: {
background: 'linear-gradient(180deg, #BE9461 0%, #9C6C3C 100%)',
@@ -56,6 +61,7 @@ const lightTheme = {
},
disabled: { border: '1px solid #353333', text: '#B3B3B3' },
},
+ pill: { background: common.light.yellow, text: common.light.yellow, hover: common.light.white },
},
input: {
background: '#dbdbdb',
@@ -64,6 +70,7 @@ const lightTheme = {
},
placeholder: '#686868',
shadow: '0px 0.5px 0px rgba(255, 255, 255, 0.08)',
+ hover: common.black,
},
segmented: {
background: '#eaeaea',
diff --git a/test-utils/ContextProvider.tsx b/test-utils/ContextProvider.tsx
deleted file mode 100644
index 71396e0f59..0000000000
--- a/test-utils/ContextProvider.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { FC } from 'react';
-import { RecoilRoot } from 'recoil';
-import { ThemeProvider } from 'styled-components';
-
-import { themes } from 'styles/theme';
-
-const ContextProvider: FC = ({ children }) => {
- return (
-
- {children}
-
- );
-};
-
-export default ContextProvider;
diff --git a/tests/e2e/.eslintrc.js b/testing/e2e/.eslintrc.js
similarity index 100%
rename from tests/e2e/.eslintrc.js
rename to testing/e2e/.eslintrc.js
diff --git a/tests/e2e/pages/exchange/exchange-page.js b/testing/e2e/pages/exchange/exchange-page.js
similarity index 100%
rename from tests/e2e/pages/exchange/exchange-page.js
rename to testing/e2e/pages/exchange/exchange-page.js
diff --git a/tests/e2e/pages/exchange/header.js b/testing/e2e/pages/exchange/header.js
similarity index 100%
rename from tests/e2e/pages/exchange/header.js
rename to testing/e2e/pages/exchange/header.js
diff --git a/tests/e2e/pages/exchange/notifications.js b/testing/e2e/pages/exchange/notifications.js
similarity index 100%
rename from tests/e2e/pages/exchange/notifications.js
rename to testing/e2e/pages/exchange/notifications.js
diff --git a/tests/e2e/pages/exchange/onboard.js b/testing/e2e/pages/exchange/onboard.js
similarity index 100%
rename from tests/e2e/pages/exchange/onboard.js
rename to testing/e2e/pages/exchange/onboard.js
diff --git a/tests/e2e/pages/markets/futures-page.js b/testing/e2e/pages/markets/futures-page.js
similarity index 100%
rename from tests/e2e/pages/markets/futures-page.js
rename to testing/e2e/pages/markets/futures-page.js
diff --git a/tests/e2e/pages/markets/header.js b/testing/e2e/pages/markets/header.js
similarity index 100%
rename from tests/e2e/pages/markets/header.js
rename to testing/e2e/pages/markets/header.js
diff --git a/tests/e2e/pages/markets/notifications.js b/testing/e2e/pages/markets/notifications.js
similarity index 100%
rename from tests/e2e/pages/markets/notifications.js
rename to testing/e2e/pages/markets/notifications.js
diff --git a/tests/e2e/pages/markets/onboard.js b/testing/e2e/pages/markets/onboard.js
similarity index 100%
rename from tests/e2e/pages/markets/onboard.js
rename to testing/e2e/pages/markets/onboard.js
diff --git a/tests/e2e/pages/page.js b/testing/e2e/pages/page.js
similarity index 100%
rename from tests/e2e/pages/page.js
rename to testing/e2e/pages/page.js
diff --git a/tests/e2e/specs/futures-spec.js b/testing/e2e/specs/futures-spec.js
similarity index 100%
rename from tests/e2e/specs/futures-spec.js
rename to testing/e2e/specs/futures-spec.js
diff --git a/tests/e2e/specs/trade-spec.js b/testing/e2e/specs/trade-spec.js
similarity index 100%
rename from tests/e2e/specs/trade-spec.js
rename to testing/e2e/specs/trade-spec.js
diff --git a/tests/e2e/tsconfig.json b/testing/e2e/tsconfig.json
similarity index 100%
rename from tests/e2e/tsconfig.json
rename to testing/e2e/tsconfig.json
diff --git a/testing/e2e/videos/futures-spec.js.mp4 b/testing/e2e/videos/futures-spec.js.mp4
new file mode 100644
index 0000000000..f5cb147c39
Binary files /dev/null and b/testing/e2e/videos/futures-spec.js.mp4 differ
diff --git a/testing/e2e/videos/trade-spec.js.mp4 b/testing/e2e/videos/trade-spec.js.mp4
new file mode 100644
index 0000000000..96549650ce
Binary files /dev/null and b/testing/e2e/videos/trade-spec.js.mp4 differ
diff --git a/tests/lighthouse/desktop.conf.js b/testing/lighthouse/desktop.conf.js
similarity index 100%
rename from tests/lighthouse/desktop.conf.js
rename to testing/lighthouse/desktop.conf.js
diff --git a/tests/lighthouse/lhci-desktop.conf.js b/testing/lighthouse/lhci-desktop.conf.js
similarity index 100%
rename from tests/lighthouse/lhci-desktop.conf.js
rename to testing/lighthouse/lhci-desktop.conf.js
diff --git a/tests/lighthouse/lhci-mobile.conf.js b/testing/lighthouse/lhci-mobile.conf.js
similarity index 100%
rename from tests/lighthouse/lhci-mobile.conf.js
rename to testing/lighthouse/lhci-mobile.conf.js
diff --git a/tests/lighthouse/mobile.conf.js b/testing/lighthouse/mobile.conf.js
similarity index 100%
rename from tests/lighthouse/mobile.conf.js
rename to testing/lighthouse/mobile.conf.js
diff --git a/tests/lint.js b/testing/lint.js
similarity index 100%
rename from tests/lint.js
rename to testing/lint.js
diff --git a/testing/unit/__mocks__/MockProviders.tsx b/testing/unit/__mocks__/MockProviders.tsx
new file mode 100644
index 0000000000..eec6624699
--- /dev/null
+++ b/testing/unit/__mocks__/MockProviders.tsx
@@ -0,0 +1,85 @@
+import { NetworkId } from '@synthetixio/contracts-interface';
+import { createQueryContext, SynthetixQueryContextProvider } from '@synthetixio/queries';
+import WithAppContainers from 'containers';
+import mockRouter from 'next-router-mock';
+import { ReactNode } from 'react';
+import { QueryClient, QueryClientProvider } from 'react-query';
+import { RecoilRoot } from 'recoil';
+import { ThemeProvider } from 'styled-components';
+import { WagmiConfig } from 'wagmi';
+
+import { initRainbowkit } from 'containers/Connector/config';
+import { RefetchProvider } from 'contexts/RefetchContext';
+import { themes } from 'styles/theme';
+
+import { DEFAULT_NETWORK } from '../constants';
+import { mockProvider, MockEthProvider } from './mockEthersProvider';
+
+jest.mock('@rainbow-me/rainbowkit', () => ({
+ wallet: {
+ metaMask: () => {},
+ rainbow: () => {},
+ coinbase: () => {},
+ walletConnect: () => {},
+ ledger: () => {},
+ brave: () => {},
+ trust: () => {},
+ },
+ connectorsForWallets: () => {},
+ useConnectModal: () => ({
+ openConnectModal: () => {},
+ }),
+}));
+
+jest.mock('next/router', () => require('next-router-mock'));
+// This is needed for mocking 'next/link':
+jest.mock('next/dist/client/router', () => require('next-router-mock'));
+
+const { wagmiClient } = initRainbowkit();
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+});
+
+type Props = {
+ children: ReactNode;
+ ethProviderOverrides?: MockEthProvider;
+ route?: string;
+};
+
+process.env.GIT_HASH_ID = '12345';
+
+const MockProviders = ({ children, ethProviderOverrides, route }: Props) => {
+ const mockedProvider = mockProvider(ethProviderOverrides);
+
+ mockRouter.setCurrentUrl(route || '/');
+
+ return (
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+ );
+};
+
+export default MockProviders;
diff --git a/testing/unit/__mocks__/mockConnector.ts b/testing/unit/__mocks__/mockConnector.ts
new file mode 100644
index 0000000000..19bf55c8e4
--- /dev/null
+++ b/testing/unit/__mocks__/mockConnector.ts
@@ -0,0 +1,41 @@
+import Connector from 'containers/Connector/Connector';
+import { DEFAULT_NETWORK, MOCK_SIGNER, TEST_ADDR } from 'testing/unit/constants';
+
+import { mockProvider } from './mockEthersProvider';
+
+const DEFAULT_CONNECTOR = {
+ activeChain: {
+ id: DEFAULT_NETWORK.id,
+ name: DEFAULT_NETWORK.name,
+ network: DEFAULT_NETWORK.name,
+ rpcUrls: {
+ infura: '',
+ default: '',
+ },
+ },
+ unsupportedNetwork: false,
+ isWalletConnected: true,
+ walletAddress: TEST_ADDR,
+ provider: mockProvider(),
+ l2Provider: null,
+ signer: MOCK_SIGNER,
+ network: DEFAULT_NETWORK,
+ synthsMap: {},
+ tokensMap: {},
+ staticMainnetProvider: null,
+ defaultSynthetixjs: null,
+ l2Synthetixjs: null,
+ l2SynthsMap: {},
+};
+
+const mockConnector = (overrides?: Record) => {
+ // @ts-ignore
+ jest.spyOn(Connector, 'useContainer').mockImplementation(() => {
+ return {
+ ...DEFAULT_CONNECTOR,
+ ...(overrides ?? {}),
+ };
+ });
+};
+
+export default mockConnector;
diff --git a/testing/unit/__mocks__/mockEthersProvider.ts b/testing/unit/__mocks__/mockEthersProvider.ts
new file mode 100644
index 0000000000..6dd393ead0
--- /dev/null
+++ b/testing/unit/__mocks__/mockEthersProvider.ts
@@ -0,0 +1,30 @@
+import { BigNumber } from '@ethersproject/bignumber';
+
+import { DEFAULT_NETWORK } from 'testing/unit/constants';
+import { weiFromEth } from 'utils/formatters/number';
+
+export type MockEthProvider = Record;
+
+const DEFAULT_PROVIDER = {
+ network: DEFAULT_NETWORK,
+ blockNumber: 5000,
+ getNetwork: () => DEFAULT_NETWORK,
+ waitForTransaction: () => {},
+ getBlockNumber: () => 5000,
+ getGasPrice: () => BigNumber.from('2000000000'),
+ getBalance: () => BigNumber.from(weiFromEth('10')),
+ estimateGas: () => BigNumber.from('100000'),
+ sendTransaction: () => {},
+ getLogs: () => [],
+ getEtherPrice: () => 1000,
+ getTransactionCount: () => 1,
+ getAvatar: () => '',
+ lookupAddress: () => 'name',
+};
+
+export const mockProvider = (overrides: MockEthProvider = {}) => {
+ return {
+ ...DEFAULT_PROVIDER,
+ ...overrides,
+ };
+};
diff --git a/testing/unit/__mocks__/mockQueries.ts b/testing/unit/__mocks__/mockQueries.ts
new file mode 100644
index 0000000000..5bb9bdde80
--- /dev/null
+++ b/testing/unit/__mocks__/mockQueries.ts
@@ -0,0 +1,9 @@
+export const mockGrapqhlRequest = (returnData: any) => {
+ jest.spyOn(require('graphql-request'), 'default').mockImplementation(() => returnData);
+};
+
+export const mockReactQuery = (returnValue?: any) => {
+ jest.mock('react-query', () => ({
+ useQuery: jest.fn().mockReturnValue(returnValue ?? { data: {}, isLoading: false, error: {} }),
+ }));
+};
diff --git a/testing/unit/constants.ts b/testing/unit/constants.ts
new file mode 100644
index 0000000000..929ce494f5
--- /dev/null
+++ b/testing/unit/constants.ts
@@ -0,0 +1,138 @@
+import { Wallet } from 'ethers';
+
+export const TEST_ADDR = '0x1c099210997E5C5689189A256C6145ca743B2610';
+export const TEST_PK = 'b826c22c04853f6ba05575275494a3a5c9eff298c36cb58d11370f83c87574e4';
+
+export const MOCK_SIGNER = new Wallet(TEST_PK);
+
+export const DEFAULT_NETWORK = {
+ id: 420,
+ name: 'optimism-goerli',
+};
+
+export const CHAINS = [
+ {
+ id: 10,
+ name: 'Optimism',
+ network: 'optimism',
+ nativeCurrency: {
+ name: 'Ether',
+ symbol: 'ETH',
+ decimals: 18,
+ },
+ rpcUrls: {
+ alchemy: 'https://opt-mainnet.g.alchemy.com/v2',
+ default: 'https://optimism-mainnet.infura.io/v3',
+ infura: 'https://optimism-mainnet.infura.io/v3',
+ public: 'https://mainnet.optimism.io',
+ },
+ blockExplorers: {
+ etherscan: {
+ name: 'Etherscan',
+ url: 'https://optimistic.etherscan.io',
+ },
+ default: {
+ name: 'Etherscan',
+ url: 'https://optimistic.etherscan.io',
+ },
+ },
+ multicall: {
+ address: '0xca11bde05977b3631167028862be2a173976ca11',
+ blockCreated: 4286263,
+ },
+ },
+ {
+ id: 1,
+ name: 'Ethereum',
+ network: 'homestead',
+ nativeCurrency: {
+ name: 'Ether',
+ symbol: 'ETH',
+ decimals: 18,
+ },
+ rpcUrls: {
+ alchemy: 'https://eth-mainnet.alchemyapi.io/v2',
+ default: 'https://mainnet.infura.io/v3',
+ infura: 'https://mainnet.infura.io/v3',
+ public: 'https://eth-mainnet.alchemyapi.io/v2/_gg7wSSi0KMBsdKnGVfHDueq6xMB9EkC',
+ },
+ blockExplorers: {
+ etherscan: {
+ name: 'Etherscan',
+ url: 'https://etherscan.io',
+ },
+ default: {
+ name: 'Etherscan',
+ url: 'https://etherscan.io',
+ },
+ },
+ ens: {
+ address: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
+ },
+ multicall: {
+ address: '0xca11bde05977b3631167028862be2a173976ca11',
+ blockCreated: 14353601,
+ },
+ },
+ {
+ id: 420,
+ name: 'Optimism Goerli',
+ network: 'optimism-goerli',
+ nativeCurrency: {
+ name: 'Goerli Ether',
+ symbol: 'ETH',
+ decimals: 18,
+ },
+ rpcUrls: {
+ alchemy: 'https://opt-goerli.g.alchemy.com/v2',
+ default: 'https://optimism-goerli.infura.io/v3',
+ infura: 'https://optimism-goerli.infura.io/v3',
+ public: 'https://goerli.optimism.io',
+ },
+ blockExplorers: {
+ default: {
+ name: 'Blockscout',
+ url: 'https://blockscout.com/optimism/goerli',
+ },
+ },
+ multicall: {
+ address: '0xca11bde05977b3631167028862be2a173976ca11',
+ blockCreated: 49461,
+ },
+ testnet: true,
+ },
+ {
+ id: 5,
+ name: 'Goerli',
+ network: 'goerli',
+ nativeCurrency: {
+ name: 'Goerli Ether',
+ symbol: 'ETH',
+ decimals: 18,
+ },
+ rpcUrls: {
+ alchemy: 'https://eth-goerli.alchemyapi.io/v2',
+ default: 'https://goerli.infura.io/v3',
+ infura: 'https://goerli.infura.io/v3',
+ public: 'https://eth-goerli.alchemyapi.io/v2/_gg7wSSi0KMBsdKnGVfHDueq6xMB9EkC',
+ },
+ blockExplorers: {
+ etherscan: {
+ name: 'Etherscan',
+ url: 'https://goerli.etherscan.io',
+ },
+ default: {
+ name: 'Etherscan',
+ url: 'https://goerli.etherscan.io',
+ },
+ },
+ ens: {
+ address: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
+ },
+ multicall: {
+ address: '0xca11bde05977b3631167028862be2a173976ca11',
+ blockCreated: 6507670,
+ },
+ testnet: true,
+ },
+];
diff --git a/test-setup/global.js b/testing/unit/setup/global.js
similarity index 100%
rename from test-setup/global.js
rename to testing/unit/setup/global.js
diff --git a/test-setup/setup.js b/testing/unit/setup/setup.js
similarity index 100%
rename from test-setup/setup.js
rename to testing/unit/setup/setup.js
diff --git a/translations/en.json b/translations/en.json
index 9a41b84f67..c22d11e482 100644
--- a/translations/en.json
+++ b/translations/en.json
@@ -17,7 +17,8 @@
"isolated-margin": "Isolated Margin",
"cross-margin": "Cross Margin",
"leaderboard": "leaderboard",
- "earn": "earn"
+ "earn": "earn",
+ "beta-badge": "Beta"
},
"networks-switcher": {
"chains": "Switch Networks",
@@ -417,7 +418,7 @@
"overview": {
"futures-positions-table": {
"market": "Market",
- "position": "Side",
+ "side": "Side",
"avg-entry": "Avg. Entry Price",
"last-entry": "Last Entry Price",
"leverage": "Leverage",
@@ -458,6 +459,8 @@
"total-trades": "Total Trades"
},
"positions-tabs": {
+ "isolated-margin": "Isolated Margin",
+ "cross-margin": "Cross Margin",
"futures": "Futures Positions",
"shorts": "Shorts",
"spot": "Spot Balances"
@@ -486,6 +489,7 @@
"no-trade-history-link": "Visit the Kwenta exchange to swap synths"
},
"futures-history-table": {
+ "asset": "Asset",
"size": "Size",
"pnl": "P&L",
"date-time": "Date/Time",
@@ -493,7 +497,7 @@
"price": "Price",
"side": "Side",
"market": "Market",
- "order-type": "Type",
+ "type": "Type",
"no-result": "You have no futures trading history"
}
},
@@ -618,6 +622,7 @@
"sell": "sell",
"remaining-margin": "Remaining margin",
"max": "max",
+ "max-leverage-error": "Your current position exceeds the max leverage of ({{maxLeverage}}x). You need to reduce your position or adjust leverage.",
"leverage": {
"title": "leverage",
"buy": "buy",
@@ -642,9 +647,9 @@
"modify-position": "modify",
"deposit-margin-minimum": "Deposit Margin",
"oi-caps-reached": "Open Interest Cap Reached",
- "place-next-price-order": "Place Next-Price Order",
+ "place-next-price-order": "Place Next Price Order",
"place-limit-order": "Place Limit Order",
- "place-stop-order": "Place Stop Order",
+ "place-stop-order": "Place Stop Market Order",
"connect-wallet": "Connect Wallet"
},
"margin": {
@@ -661,8 +666,10 @@
"deposit": {
"title": "Deposit Margin",
"button": "Deposit Margin",
+ "approve-button": "Approve sUSD",
"disclaimer": "A $50 margin minimum is required to open a position.",
"min-deposit": "$50 minimum margin to open a position",
+ "min-margin-error": "Position size too small, minimum market margin of $50 required",
"exceeds-balance": "Amount exceeds balance"
},
"withdraw": {
@@ -677,7 +684,7 @@
}
},
"next-price": {
- "description": "Next-Price orders are subject to volatility and execute at the very next on-chain price.",
+ "description": "Next Price orders are subject to volatility and execute at the very next on-chain price.",
"learn-more": "Learn more"
},
"cost-basis": {
@@ -687,12 +694,12 @@
"modal": {
"confirm-order": "Confirm order",
"max-leverage-disclaimer": "Note: A next price update could cause your order to exceed maximum leverage resulting in the order being dropped by the keeper.",
- "disabled-min-margin": "Minimum $50 margin required"
+ "disabled-min-margin": "Minimum market margin of $50 required"
}
},
"leverage": {
"modal": {
- "max-pos": "Maximum position at current leverage:",
+ "max-pos": "Max position at current leverage:",
"title": "Adjust Leverage",
"input-label": "Leverage",
"confirm": "Confirm"
@@ -704,22 +711,30 @@
"cross-margin": {
"connect-wallet": "Connect your wallet to start trading",
"title": "Cross Margin",
- "faq-title": "Cross Margin FAQ's",
+ "faq-title": " Cross Margin FAQ",
"create-account": "Create Account",
"unsupported": "Cross margin is not supported on this network",
"switch-type": "Switch to isolated margin",
"account-query-failed": "Failed to fetch account"
},
"preview": {
+ "insufficient-margin": "Insufficient free margin",
"error": "Error generating preview"
},
"orders": {
"manage-keeper-deposit": {
- "title": "Manage Keeper Balance",
+ "title": "Manage Account ETH Balance",
"withdraw": "Withdraw",
- "deposit": "Deposit"
+ "deposit": "Deposit",
+
+ "withdraw-warning": "Withdrawing ETH while you still have open orders pending risks leaving insufficient balance for orders to execute."
},
- "fee-rejection-label": "Fee Rejection"
+ "fee-rejection-label": "Fee Rejection",
+ "fee-rejection-tooltip": "This allows you to set a maximum fee on your order. If the dynamic fee exceeds your selected fee limit at the time of execution, the order will not execute."
+ },
+ "edit-leverage": {
+ "failed": "Transaction failed",
+ "insufficient-margin": "Leverage cannot be decreased, please deposit more margin or reduce open position sizes"
}
},
"history": {
@@ -760,9 +775,9 @@
"gas-fee": "Network Gas Fee",
"order-type": "Order Type",
"market-order": "Market",
- "next-price-order": "Next-Price",
+ "next-price-order": "Next Price",
"deposit": "Total Deposit",
- "np-discount": "Next-Price Discount"
+ "np-discount": "Next Price Discount"
}
},
"open-orders": {
@@ -775,6 +790,7 @@
"type": "Type",
"size": "Size",
"size-price": "Size/Price",
+ "reserved-margin": "Reserved margin",
"price": "Price",
"status": "Status",
"parameters": "Parameters",
@@ -792,7 +808,7 @@
}
},
"trades": {
- "tab": "History",
+ "tab": "Trades",
"table": {
"id": "ID",
"trade-size": "trade size",
@@ -802,6 +818,7 @@
"fees": "fees",
"price": "price",
"side": "side",
+ "side-type": "Side/Type",
"no-results": "You have no trade history",
"order-type": "order type",
"trade-types": {
@@ -855,13 +872,13 @@
},
"onboard": {
"title": "Cross Margin",
- "step1-intro": "To Get started you must first create your cross margin account",
+ "step1-intro": "Start by creating your cross margin account",
"step2-intro": "Now, approve the contract to spend sUSD",
- "step3-intro": "Lastly, deposit your sUSD into the contact",
+ "step3-intro": "Lastly, deposit sUSD to begin trading",
"step3-complete": "Your cross margin account has been successfully created!",
"faq1": "What is cross margin?",
- "faq2": "Is cross margin better than isolated margin",
- "faq3": "How will a cross margin account affect my trading",
+ "faq2": "Is cross margin better than isolated margin?",
+ "faq3": "How will a cross margin account affect my trading?",
"unsupported-network": "Cross margin is not supported on this network"
}
}
@@ -870,7 +887,7 @@
"page-title": "Leaderboard | Kwenta",
"leaderboard": {
"table": {
- "title": "All Traders",
+ "title": "{{ activeTab }} Traders",
"rank": "Rank",
"trader": "Trader",
"total-trades": "Total Trades",
@@ -958,6 +975,9 @@
"title": "approve transaction",
"confirm-with-provider": "Confirm your transaction through your wallet."
},
+ "menu": {
+ "title": "menu"
+ },
"settings": {
"title": "settings",
"options": {
diff --git a/tsconfig.json b/tsconfig.json
index c2b5404312..602edd6144 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -23,7 +23,7 @@
},
"exclude": [
"node_modules",
- "tests/e2e/helper/*.ts"
+ "testing/e2e/helper/*.ts"
],
"include": [
"next-env.d.ts",
diff --git a/utils/__tests__/number.test.ts b/utils/__tests__/number.test.ts
new file mode 100644
index 0000000000..3b8006d41d
--- /dev/null
+++ b/utils/__tests__/number.test.ts
@@ -0,0 +1,21 @@
+import { BigNumber } from 'ethers';
+
+import { gweiToWei, formatDollars, weiFromWei } from 'utils/formatters/number';
+
+describe('number utils', () => {
+ test('ether to gwei', () => {
+ const gweiVal = gweiToWei('10');
+ expect(gweiVal).toEqual('10000000000');
+ });
+ test('formats dollars', () => {
+ let formatted = formatDollars('1000000000.764552');
+ expect(formatted).toEqual('$1,000,000,000.76');
+
+ formatted = formatDollars('0.004552');
+ expect(formatted).toEqual('$0.00');
+ });
+ test('weiFromWei', () => {
+ let weiVal = weiFromWei(BigNumber.from('100000000000000000'));
+ expect(weiVal.toNumber()).toEqual(0.1);
+ });
+});
diff --git a/utils/formatters/date.ts b/utils/formatters/date.ts
index e7bc6b5dc5..f7c5795d35 100644
--- a/utils/formatters/date.ts
+++ b/utils/formatters/date.ts
@@ -65,6 +65,8 @@ export const timePresentation = (timestamp: string, t: TFunction) => {
export const formatDateWithoutYear = (date: Date) => formatDate(date, 'MMMM dd');
+export const formatShortDateWithoutYear = (date: Date) => formatDate(date, 'M/dd');
+
export const calculatedTimeDifference = (dateLeft: Date, dateRight: Date) =>
differenceInSeconds(dateLeft, dateRight);
diff --git a/utils/formatters/number.ts b/utils/formatters/number.ts
index 7415199023..e476ebb3eb 100644
--- a/utils/formatters/number.ts
+++ b/utils/formatters/number.ts
@@ -1,6 +1,7 @@
import Wei, { wei } from '@synthetixio/wei';
import BN from 'bn.js';
import { BigNumber, ethers, utils } from 'ethers';
+import { parseUnits } from 'ethers/lib/utils';
import { CurrencyKey } from 'constants/currency';
import {
@@ -203,7 +204,12 @@ export const multiplyDecimal = (x: BigNumber, y: BigNumber) => {
};
export const weiFromWei = (weiAmount: WeiSource) => {
- return wei(weiAmount, 18, true);
+ if (weiAmount instanceof Wei) {
+ const precisionDiff = 18 - weiAmount.p;
+ return wei(weiAmount, 18, true).div(10 ** precisionDiff);
+ } else {
+ return wei(weiAmount, 18, true);
+ }
};
export const suggestedDecimals = (value: WeiSource) => {
@@ -233,3 +239,9 @@ export const weiToString = (weiVal: Wei) => {
export const isZero = (num: WeiSource) => {
return wei(num || 0).eq(0);
};
+
+export const weiFromEth = (num: WeiSource) => wei(num).toBN().toString();
+
+export const gweiToWei = (val: WeiSource) => {
+ return parseUnits(wei(val).toString(), 9).toString();
+};
diff --git a/utils/futures.ts b/utils/futures.ts
index e537048418..ba0e3777a8 100644
--- a/utils/futures.ts
+++ b/utils/futures.ts
@@ -282,12 +282,12 @@ export const orderPriceInvalidLabel = (
if (!orderPrice || Number(orderPrice) <= 0) return null;
const isLong = leverageSide === 'long';
if (
- ((isLong && orderType === 'limit') || (!isLong && orderType === 'stop')) &&
+ ((isLong && orderType === 'limit') || (!isLong && orderType === 'stop market')) &&
wei(orderPrice).gt(currentPrice)
)
return 'max ' + formatNumber(currentPrice);
if (
- ((!isLong && orderType === 'limit') || (isLong && orderType === 'stop')) &&
+ ((!isLong && orderType === 'limit') || (isLong && orderType === 'stop market')) &&
wei(orderPrice).lt(currentPrice)
)
return 'min ' + formatNumber(currentPrice);