Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/components/Synthetics/TradeBox/TradeBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ import { HighPriceImpactOrFeesWarningCard } from "../HighPriceImpactOrFeesWarnin
import TradeInfoIcon from "../TradeInfoIcon/TradeInfoIcon";
import TwapRows from "../TwapRows/TwapRows";
import { useDecreaseOrdersThatWillBeExecuted } from "./hooks/useDecreaseOrdersThatWillBeExecuted";
import { useShowHighLeverageWarning } from "./hooks/useShowHighLeverageWarning";
import { useExpressTradingWarnings } from "./hooks/useShowOneClickTradingInfo";
import { useTradeboxAcceptablePriceImpactValues } from "./hooks/useTradeboxAcceptablePriceImpactValues";
import { useTradeboxTPSLReset } from "./hooks/useTradeboxTPSLReset";
Expand Down Expand Up @@ -216,6 +217,8 @@ export function TradeBox({ isMobile }: { isMobile: boolean }) {
payUsd: fromUsd,
});

const { showHighLeverageWarning, dismissHighLeverageWarning } = useShowHighLeverageWarning();

const setIsDismissedRef = useLatest(priceImpactWarningState.setIsDismissed);

const setFromTokenInputValue = useCallback(
Expand Down Expand Up @@ -951,6 +954,11 @@ export function TradeBox({ isMobile }: { isMobile: boolean }) {
/>
</div>
)}
{showHighLeverageWarning && (
<AlertInfoCard type="info" onClose={dismissHighLeverageWarning}>
<Trans>Using high leverage increases the risk of liquidation.</Trans>
</AlertInfoCard>
)}
{isTrigger && (
<SyntheticsInfoRow
label={t`Market`}
Expand Down
106 changes: 106 additions & 0 deletions src/components/Synthetics/TradeBox/hooks/useShowHighLeverageWarning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { useCallback, useEffect, useState } from "react";

import {
AB_HIGH_LEVERAGE_WARNING_ALTCOIN_LEVERAGE,
AB_HIGH_LEVERAGE_WARNING_GROUP,
AB_HIGH_LEVERAGE_WARNING_MAJOR_TOKEN_LEVERAGE,
AB_HIGH_LEVERAGE_WARNING_PROBABILITY,
} from "config/ab";
import { ARBITRUM, AVALANCHE, BOTANIX } from "config/chains";
import { getHighLeverageWarningDismissedTimestampKey } from "config/localStorage";
import { selectAccount, selectChainId } from "context/SyntheticsStateContext/selectors/globalSelectors";
import { selectIsLeverageSliderEnabled } from "context/SyntheticsStateContext/selectors/settingsSelectors";
import { selectTradeboxToTokenAddress } from "context/SyntheticsStateContext/selectors/shared/baseSelectors";
import {
selectTradeboxIncreasePositionAmounts,
selectTradeboxLeverage,
} from "context/SyntheticsStateContext/selectors/tradeboxSelectors";
import { useSelector } from "context/SyntheticsStateContext/utils";
import { useIsFreshAccountForHighLeverageTrading } from "domain/synthetics/accountStats/useIsFreshAccountForHighLeverageTrading";
import { useIsAddressInGroup } from "lib/userAnalytics/getIsAddressInGroup";
import { getToken } from "sdk/configs/tokens";

const IS_MAJOR_TOKEN_MAP: Record<number, string[]> = {
[ARBITRUM]: ["BTC", "ETH", "SOL"],
[AVALANCHE]: ["AVAX", "ETH", "BTC"],
[BOTANIX]: ["BTC"],
};

const WAIVE_DISMISSAL_PERIOD_MS = 24 * 60 * 60 * 1000; // 24 hours
const DISMISSAL_POLL_INTERVAL_MS = 5000;

function getDismissedTimestamp(account: string) {
const value = localStorage.getItem(getHighLeverageWarningDismissedTimestampKey(account));
if (value === null) {
return 0;
}

const timestamp = Number(value);
return isNaN(timestamp) ? 0 : timestamp;
}

export function useShowHighLeverageWarning(): {
showHighLeverageWarning: boolean;
dismissHighLeverageWarning: () => void;
} {
const chainId = useSelector(selectChainId);
const account = useSelector(selectAccount);
const isFreshAccount = useIsFreshAccountForHighLeverageTrading();

const isInGroup = useIsAddressInGroup({
address: account,
experimentGroupProbability: AB_HIGH_LEVERAGE_WARNING_PROBABILITY,
grouping: AB_HIGH_LEVERAGE_WARNING_GROUP,
});

const toTokenAddress = useSelector(selectTradeboxToTokenAddress);
const toTokenSymbol = toTokenAddress ? getToken(chainId, toTokenAddress).symbol : undefined;
const isMajorToken = toTokenSymbol ? IS_MAJOR_TOKEN_MAP[chainId].includes(toTokenSymbol) : false;
const isLeverageSliderEnabled = useSelector(selectIsLeverageSliderEnabled);
const leverageSliderLeverage = useSelector(selectTradeboxLeverage);
const amounts = useSelector(selectTradeboxIncreasePositionAmounts);
const leverage = isLeverageSliderEnabled ? leverageSliderLeverage : amounts?.estimatedLeverage ?? 0n;

const isHighLeverage = isMajorToken
? leverage >= AB_HIGH_LEVERAGE_WARNING_MAJOR_TOKEN_LEVERAGE
: leverage >= AB_HIGH_LEVERAGE_WARNING_ALTCOIN_LEVERAGE;

const [dismissedTimestamp, setDismissedTimestamp] = useState(() => {
if (!account) {
return 0;
}

return getDismissedTimestamp(account);
});

useEffect(() => {
if (!account) {
return;
}

const timer = window.setInterval(() => {
const freshDismissedTimestamp = getDismissedTimestamp(account);
if (freshDismissedTimestamp !== dismissedTimestamp) {
setDismissedTimestamp(freshDismissedTimestamp);
}
}, DISMISSAL_POLL_INTERVAL_MS);

return () => {
window.clearInterval(timer);
};
}, [account, dismissedTimestamp]);

const isDismissed = (dismissedTimestamp ?? 0) > Date.now() - WAIVE_DISMISSAL_PERIOD_MS;

const dismissHighLeverageWarning = useCallback(() => {
setDismissedTimestamp(Date.now());
if (account) {
localStorage.setItem(getHighLeverageWarningDismissedTimestampKey(account), Date.now().toString());
}
}, [setDismissedTimestamp, account]);

return {
showHighLeverageWarning: isFreshAccount && isInGroup && isHighLeverage && !isDismissed,
dismissHighLeverageWarning,
};
}
8 changes: 8 additions & 0 deletions src/config/ab.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import mapValues from "lodash/mapValues";

import { BASIS_POINTS_DIVISOR_BIGINT } from "./factors";
import { AB_FLAG_STORAGE_KEY } from "./localStorage";

type AbFlagValue = {
Expand Down Expand Up @@ -98,3 +99,10 @@ export function getAbFlagUrlParams(): string {
.map(([flag, { enabled }]) => `${flag}=${enabled ? 1 : 0}`)
.join("&");
}

// Config for deterministic ab flags based on address

export const AB_HIGH_LEVERAGE_WARNING_GROUP = "alert-high-leverage";
export const AB_HIGH_LEVERAGE_WARNING_PROBABILITY = 0.5;
export const AB_HIGH_LEVERAGE_WARNING_MAJOR_TOKEN_LEVERAGE = 15n * BASIS_POINTS_DIVISOR_BIGINT;
export const AB_HIGH_LEVERAGE_WARNING_ALTCOIN_LEVERAGE = 10n * BASIS_POINTS_DIVISOR_BIGINT;
6 changes: 6 additions & 0 deletions src/config/localStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export const SUBACCOUNT_APPROVAL_KEY = "subaccount-approval";
export const TOKEN_PERMITS_KEY = "token-permits";
export const CLAIM_TERMS_ACCEPTED_KEY = "claim-terms-accepted";

export const HIGH_LEVERAGE_WARNING_DISMISSED_TIMESTAMP_KEY = "high-leverage-warning-dismissed-timestamp";

export const getSubgraphUrlKey = (chainId: number, subgraph: string) => `subgraphUrl:${chainId}:${subgraph}`;

export function getSubaccountApprovalKey(chainId: number, account: string | undefined) {
Expand Down Expand Up @@ -218,3 +220,7 @@ export function getClaimTermsAcceptedKey(
) {
return `${chainId}:${account}:${distributionId}:${claimTerms}-${CLAIM_TERMS_ACCEPTED_KEY}`;
}

export function getHighLeverageWarningDismissedTimestampKey(account: string) {
return `${account}-${HIGH_LEVERAGE_WARNING_DISMISSED_TIMESTAMP_KEY}`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ export const selectExternalSwapInputsByLeverageSize = createSelector((q) => {

const toTokenAmount = q(selectTradeboxToTokenAmount);
const marketInfo = q(selectTradeboxMarketInfo);
const leverage = q(selectTradeBoxLeverage);
const leverage = q(selectTradeboxLeverage);
const uiFeeFactor = q(selectUiFeeFactor);

const existingPosition = q(selectTradeboxExistingPosition);
Expand Down Expand Up @@ -447,7 +447,7 @@ export const selectTradeboxFromTokenAmount = createSelector((q) => {
return parsedValue;
});

export const selectTradeBoxLeverage = createSelector((q) => {
export const selectTradeboxLeverage = createSelector((q) => {
const leverageOption = q(selectTradeboxLeverageOption);
return BigInt(parseInt(String(Number(leverageOption!) * BASIS_POINTS_DIVISOR)));
});
Expand All @@ -473,7 +473,7 @@ export const selectTradeboxIncreasePositionAmounts = createSelector((q) => {
const toTokenAddress = q(selectTradeboxToTokenAddress);
const toTokenAmount = q(selectTradeboxToTokenAmount);
const marketAddress = q(selectTradeboxMarketAddress);
const leverage = q(selectTradeBoxLeverage);
const leverage = q(selectTradeboxLeverage);
const collateralTokenAddress = q(selectTradeboxCollateralTokenAddress);
const selectedTriggerAcceptablePriceImpactBps = q(selectTradeboxSelectedTriggerAcceptablePriceImpactBps);
const triggerPrice = q(selectTradeboxTriggerPrice);
Expand Down Expand Up @@ -609,10 +609,6 @@ export const selectTradeboxTradeFlags = createSelector((q) => {
return tradeFlags;
});

export const selectTradeboxLeverage = createSelectorDeprecated([selectTradeboxLeverageOption], (leverageOption) =>
BigInt(parseInt(String(Number(leverageOption!) * BASIS_POINTS_DIVISOR)))
);

export const selectTradeboxTradeFeesType = createSelector(
function selectTradeboxTradeFeesType(q): TradeFeesType | null {
const { isSwap, isIncrease, isTrigger } = q(selectTradeboxTradeFlags);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useMemo } from "react";
import useSWR from "swr";

import { getSubgraphUrl } from "config/subgraph";
import { selectAccount, selectChainId } from "context/SyntheticsStateContext/selectors/globalSelectors";
import { useSelector } from "context/SyntheticsStateContext/utils";
import graphqlFetcher from "sdk/utils/graphqlFetcher";

const MAX_TRADE_ACTIONS_COUNT = 30;

const QUERY = /* gql */ `
query($account: String!) {
tradeActionsConnection(orderBy: id_ASC, where: {AND: [{account_eq: $account}, {OR: [{eventName_eq: "OrderExecuted", orderType_eq: 4, twapGroupId_isNull: true, sizeDeltaUsd_not_eq: 0}, {eventName_eq: "OrderExecuted", orderType_eq: 5, twapGroupId_isNull: true}, {eventName_eq: "OrderExecuted", orderType_eq: 6, twapGroupId_isNull: true}, {eventName_eq: "OrderExecuted", orderType_eq: 7, twapGroupId_isNull: true}]}, {OR: [{orderType_not_eq: 7}, {eventName_not_eq: "OrderCreated"}]}, {OR: [{orderType_not_eq: 2}, {eventName_not_eq: "OrderCreated"}]}, {OR: [{orderType_not_eq: 4}, {eventName_not_eq: "OrderCreated"}]}, {OR: [{orderType_not_eq: 0}, {eventName_not_eq: "OrderCreated"}]}]}) {
totalCount
}
}
`;

export function useIsFreshAccountForHighLeverageTrading() {
const chainId = useSelector(selectChainId);
const account = useSelector(selectAccount);

const { data, isLoading } = useSWR<number>(account ? [chainId, account] : null, {
refreshInterval: undefined,
fetcher: () => {
const endpoint = getSubgraphUrl(chainId, "subsquid");

if (!endpoint) {
throw new Error("Subgraph endpoint not found");
}

return graphqlFetcher(endpoint, QUERY, { account }).then((res: any) => res?.tradeActionsConnection?.totalCount);
},
});

const isFreshAccount = useMemo(() => {
return !isLoading && data !== undefined && data < MAX_TRADE_ACTIONS_COUNT;
}, [isLoading, data]);

return isFreshAccount;
}
25 changes: 25 additions & 0 deletions src/lib/userAnalytics/getIsAddressInGroup.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, it } from "vitest";

import { getIsAddressInGroup } from "./getIsAddressInGroup";

describe("getIsAddressInGroup", () => {
it("it should be roughly in expected probability", () => {
const prefferedProbabilities = [0.01, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0];
for (const probability of prefferedProbabilities) {
const count = 50_000;
let positiveCount = 0;
for (let i = 0; i < count; i++) {
const isInGroup = getIsAddressInGroup({
address: i.toString(),
experimentGroupProbability: probability,
grouping: "test",
});
if (isInGroup) {
positiveCount++;
}
}

expect(positiveCount / count).toBeCloseTo(probability);
}
});
});
43 changes: 43 additions & 0 deletions src/lib/userAnalytics/getIsAddressInGroup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useMemo } from "react";
import { keccak256, stringToHex } from "viem/utils";

export function getIsAddressInGroup({
address,
experimentGroupProbability: probability,
grouping: salt,
}: {
address: string;
/**
* 0-1 meaning 0% - 100%
*/
experimentGroupProbability: number;
grouping: string;
}): boolean {
const hash = keccak256(stringToHex(address.toLowerCase() + (salt || "")));
const twoDigits = BigInt(hash) % 100n;
const isInGroup = twoDigits < BigInt(Math.trunc(probability * 100));
return isInGroup;
}

export function useIsAddressInGroup({
address,
experimentGroupProbability: probability,
grouping: salt,
}: {
address: string | undefined;
experimentGroupProbability: number;
grouping: string;
}) {
const isInGroup = useMemo(
() =>
address !== undefined &&
getIsAddressInGroup({
address,
experimentGroupProbability: probability,
grouping: salt,
}),
[address, probability, salt]
);

return isInGroup;
}
4 changes: 4 additions & 0 deletions src/locales/de/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -1757,6 +1757,10 @@ msgstr ""
msgid "Account"
msgstr "Konto"

#: src/components/Synthetics/TradeBox/TradeBox.tsx
msgid "Using high leverage increases the risk of liquidation."
msgstr ""

#: src/components/Synthetics/TradeBox/ExpressTradingWarningCard.tsx
msgid "Express Trading is not available for wrapping or unwrapping native token {0}."
msgstr ""
Expand Down
4 changes: 4 additions & 0 deletions src/locales/en/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -1757,6 +1757,10 @@ msgstr "Enter a price"
msgid "Account"
msgstr "Account"

#: src/components/Synthetics/TradeBox/TradeBox.tsx
msgid "Using high leverage increases the risk of liquidation."
msgstr "Using high leverage increases the risk of liquidation."

#: src/components/Synthetics/TradeBox/ExpressTradingWarningCard.tsx
msgid "Express Trading is not available for wrapping or unwrapping native token {0}."
msgstr "Express Trading is not available for wrapping or unwrapping native token {0}."
Expand Down
4 changes: 4 additions & 0 deletions src/locales/es/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -1757,6 +1757,10 @@ msgstr ""
msgid "Account"
msgstr "Cuenta"

#: src/components/Synthetics/TradeBox/TradeBox.tsx
msgid "Using high leverage increases the risk of liquidation."
msgstr ""

#: src/components/Synthetics/TradeBox/ExpressTradingWarningCard.tsx
msgid "Express Trading is not available for wrapping or unwrapping native token {0}."
msgstr ""
Expand Down
4 changes: 4 additions & 0 deletions src/locales/fr/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -1757,6 +1757,10 @@ msgstr ""
msgid "Account"
msgstr "Compte"

#: src/components/Synthetics/TradeBox/TradeBox.tsx
msgid "Using high leverage increases the risk of liquidation."
msgstr ""

#: src/components/Synthetics/TradeBox/ExpressTradingWarningCard.tsx
msgid "Express Trading is not available for wrapping or unwrapping native token {0}."
msgstr ""
Expand Down
4 changes: 4 additions & 0 deletions src/locales/ja/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -1757,6 +1757,10 @@ msgstr ""
msgid "Account"
msgstr "アカウント"

#: src/components/Synthetics/TradeBox/TradeBox.tsx
msgid "Using high leverage increases the risk of liquidation."
msgstr ""

#: src/components/Synthetics/TradeBox/ExpressTradingWarningCard.tsx
msgid "Express Trading is not available for wrapping or unwrapping native token {0}."
msgstr ""
Expand Down
4 changes: 4 additions & 0 deletions src/locales/ko/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -1757,6 +1757,10 @@ msgstr ""
msgid "Account"
msgstr "계정"

#: src/components/Synthetics/TradeBox/TradeBox.tsx
msgid "Using high leverage increases the risk of liquidation."
msgstr ""

#: src/components/Synthetics/TradeBox/ExpressTradingWarningCard.tsx
msgid "Express Trading is not available for wrapping or unwrapping native token {0}."
msgstr ""
Expand Down
4 changes: 4 additions & 0 deletions src/locales/pseudo/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -1757,6 +1757,10 @@ msgstr ""
msgid "Account"
msgstr ""

#: src/components/Synthetics/TradeBox/TradeBox.tsx
msgid "Using high leverage increases the risk of liquidation."
msgstr ""

#: src/components/Synthetics/TradeBox/ExpressTradingWarningCard.tsx
msgid "Express Trading is not available for wrapping or unwrapping native token {0}."
msgstr ""
Expand Down
Loading