diff --git a/public/configs/v1/env.json b/public/configs/v1/env.json index 27e491af99..67cb4fc02a 100644 --- a/public/configs/v1/env.json +++ b/public/configs/v1/env.json @@ -462,7 +462,8 @@ "stakingAPR": "https://apybara-proxy-web-testnet.infrastructure-34d.workers.dev/v0/protocols/dydx", "faucet": "https://faucet.v4dev.dydx.exchange", "affiliates": "https://dydx.stg.fuul.xyz", - "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com" + "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com", + "pnlImageApi": "https://image-generator.dydx.trade/generate-trade-card-web" }, "stakingValidators": [], "featureFlags": { @@ -504,7 +505,8 @@ "neutronValidator": "https://neutron-testnet-rpc.polkachu.com/", "geoV2": "https://geo-whitelist-web-mainnet-preview.infrastructure-34d.workers.dev/", "geo": "https://api.dydx.exchange/v4/geo", - "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com" + "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com", + "pnlImageApi": "https://image-generator.dydx.trade/generate-trade-card-web" }, "stakingValidators": [], "featureFlags": { @@ -549,7 +551,8 @@ "stakingAPR": "https://apybara-proxy-web-testnet.infrastructure-34d.workers.dev/v0/protocols/dydx", "faucet": "http://dev3-faucet-lb-public-1644791410.us-east-2.elb.amazonaws.com", "affiliates": "https://dydx.stg.fuul.xyz", - "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com" + "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com", + "pnlImageApi": "https://image-generator.dydx.trade/generate-trade-card-web" }, "stakingValidators": [], "featureFlags": { @@ -594,7 +597,8 @@ "stakingAPR": "https://apybara-proxy-web-testnet.infrastructure-34d.workers.dev/v0/protocols/dydx", "faucet": "https://faucet.v4dev4.dydx.exchange", "affiliates": "https://dydx.stg.fuul.xyz", - "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com" + "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com", + "pnlImageApi": "https://image-generator.dydx.trade/generate-trade-card-web" }, "stakingValidators": [], "featureFlags": { @@ -636,7 +640,8 @@ "neutronValidator": "https://neutron-testnet-rpc.polkachu.com/", "geoV2": "https://geo-whitelist-web-mainnet-preview.infrastructure-34d.workers.dev/", "geo": "https://api.dydx.exchange/v4/geo", - "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com" + "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com", + "pnlImageApi": "https://image-generator.dydx.trade/generate-trade-card-web" }, "stakingValidators": [], "featureFlags": { @@ -680,7 +685,8 @@ "neutronValidator": "https://neutron-testnet-rpc.polkachu.com/", "geo": "https://api.dydx.exchange/v4/geo", "geoV2": "https://geo-whitelist-web-mainnet-preview.infrastructure-34d.workers.dev/", - "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com" + "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com", + "pnlImageApi": "https://image-generator.dydx.trade/generate-trade-card-web" }, "stakingValidators": [], "featureFlags": { @@ -724,7 +730,8 @@ "neutronValidator": "https://neutron-testnet-rpc.polkachu.com/", "geoV2": "https://geo-whitelist-web-mainnet-preview.infrastructure-34d.workers.dev/", "geo": "https://api.dydx.exchange/v4/geo", - "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com" + "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com", + "pnlImageApi": "https://image-generator.dydx.trade/generate-trade-card-web" }, "apps": { "ios": { @@ -780,7 +787,8 @@ "neutronValidator": "https://neutron-testnet-rpc.polkachu.com/", "geoV2": "https://geo-whitelist-web-mainnet-preview.infrastructure-34d.workers.dev/", "geo": "https://api.dydx.exchange/v4/geo", - "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com" + "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com", + "pnlImageApi": "https://image-generator.dydx.trade/generate-trade-card-web" }, "stakingValidators": [], "featureFlags": { @@ -826,7 +834,8 @@ "stakingAPR": "https://apybara-proxy-web-testnet.infrastructure-34d.workers.dev/v0/protocols/dydx", "faucet": "https://faucet.v4testnet.dydx.exchange", "affiliates": "https://dydx.stg.fuul.xyz", - "spotApi": "https://dydx-solana-api-staging-e2fb353831a4.herokuapp.com" + "spotApi": "https://dydx-solana-api-staging-e2fb353831a4.herokuapp.com", + "pnlImageApi": "https://image-generator.dydx.trade/generate-trade-card-web" }, "stakingValidators": [ "dydxvaloper1vvc9vl6z9pu0vt2y79d0ln8zp6qmpmrhxx99h4", @@ -874,7 +883,8 @@ "stakingAPR": "https://apybara-proxy-web-testnet.infrastructure-34d.workers.dev/v0/protocols/dydx", "faucet": "https://faucet.v4testnet.dydx.exchange", "affiliates": "https://dydx.stg.fuul.xyz", - "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com" + "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com", + "pnlImageApi": "https://image-generator.dydx.trade/generate-trade-card-web" }, "stakingValidators": [], "featureFlags": { @@ -919,7 +929,8 @@ "stakingAPR": "https://apybara-proxy-web-testnet.infrastructure-34d.workers.dev/v0/protocols/dydx", "faucet": "https://faucet.v4testnet.dydx.exchange", "affiliates": "https://dydx.stg.fuul.xyz", - "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com" + "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com", + "pnlImageApi": "https://image-generator.dydx.trade/generate-trade-card-web" }, "stakingValidators": [], "featureFlags": { @@ -964,7 +975,8 @@ "stakingAPR": "https://apybara-proxy-web-testnet.infrastructure-34d.workers.dev/v0/protocols/dydx", "faucet": "https://faucet.v4testnet.dydx.exchange", "affiliates": "https://dydx.stg.fuul.xyz", - "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com" + "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com", + "pnlImageApi": "https://image-generator.dydx.trade/generate-trade-card-web" }, "stakingValidators": [], "featureFlags": { @@ -1009,7 +1021,8 @@ "stakingAPR": "https://apybara-proxy-web-testnet.infrastructure-34d.workers.dev/v0/protocols/dydx", "faucet": "https://faucet.v4testnet.dydx.exchange", "affiliates": "https://dydx.stg.fuul.xyz", - "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com" + "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com", + "pnlImageApi": "https://image-generator.dydx.trade/generate-trade-card-web" }, "stakingValidators": [], "featureFlags": { @@ -1054,7 +1067,8 @@ "stakingAPR": "https://apybara-proxy-web-testnet.infrastructure-34d.workers.dev/v0/protocols/dydx", "faucet": "https://faucet.v4testnet.dydx.exchange", "affiliates": "https://dydx.stg.fuul.xyz", - "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com" + "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com", + "pnlImageApi": "https://image-generator.dydx.trade/generate-trade-card-web" }, "stakingValidators": [], "featureFlags": { @@ -1099,7 +1113,8 @@ "stakingAPR": "https://apybara-proxy-web-testnet.infrastructure-34d.workers.dev/v0/protocols/dydx", "faucet": "https://faucet.v4testnet.dydx.exchange", "affiliates": "https://dydx.stg.fuul.xyz", - "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com" + "spotApi": "https://dydx-solana-api-prod-89bf4c933ba0.herokuapp.com", + "pnlImageApi": "https://image-generator.dydx.trade/generate-trade-card-web" }, "stakingValidators": [], "featureFlags": { @@ -1144,7 +1159,8 @@ "geoV2": "[geo v2 endpoint for mainnet]", "stakingAPR": "[staking APR endpoint for mainnet]", "affiliates": "[affiliates endpoint for mainnet]", - "spotApi": "[spot api endpoint for mainnet]" + "spotApi": "[spot api endpoint for mainnet]", + "pnlImageApi": "[pnl image api endpoint for mainnet]" }, "stakingValidators": [], "featureFlags": { diff --git a/src/App.tsx b/src/App.tsx index 48a5cf5c3a..c18feaf9f5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -52,9 +52,9 @@ import { SkipProvider } from './hooks/transfers/skipClient'; import { useAnalytics } from './hooks/useAnalytics'; import { useBreakpoints } from './hooks/useBreakpoints'; import { useCommandMenu } from './hooks/useCommandMenu'; +import { useComplianceState } from './hooks/useComplianceState'; import { useInitializePage } from './hooks/useInitializePage'; import { useLocalStorage } from './hooks/useLocalStorage'; -import { usePerpetualsComplianceState } from './hooks/usePerpetualsComplianceState'; import { useReferralCode } from './hooks/useReferralCode'; import { useShouldShowFooter } from './hooks/useShouldShowFooter'; import { useSimpleUiEnabled } from './hooks/useSimpleUiEnabled'; @@ -109,7 +109,7 @@ const Content = () => { const isShowingFooter = useShouldShowFooter(); const abDefaultToMarkets = useCustomFlagValue(CustomFlags.abDefaultToMarkets); const isSimpleUi = useSimpleUiEnabled(); - const { showComplianceBanner } = usePerpetualsComplianceState(); + const { showComplianceBanner } = useComplianceState(); const isSimpleUiUserMenuOpen = useAppSelector(getIsUserMenuOpen); // Track current path in Redux for conditional polling diff --git a/src/bonsai/calculators/compliance.ts b/src/bonsai/calculators/compliance.ts index ad906d5c51..048a32534c 100644 --- a/src/bonsai/calculators/compliance.ts +++ b/src/bonsai/calculators/compliance.ts @@ -7,9 +7,10 @@ export function calculateCompliance({ geo: geoBase, localAddressScreenV2, sourceAddressScreenV2, + solanaAddressScreen, }: ComplianceState): Compliance { const geo = { - currentlyGeoBlocked: geoBase.data?.whitelisted + isPerpetualsGeoBlocked: geoBase.data?.whitelisted ? false : geoHeaders.data?.status === 'restricted', currentCountry: geoHeaders.data?.country, @@ -23,6 +24,14 @@ export function calculateCompliance({ }; } + if (solanaAddressScreen.data?.status === ComplianceStatus.BLOCKED) { + return { + geo, + status: ComplianceStatus.BLOCKED, + updatedAt: solanaAddressScreen.data.updatedAt, + }; + } + if (localAddressScreenV2.data?.errors != null) { return { geo, diff --git a/src/bonsai/lifecycles/cancelTriggerOrdersLifecycle.ts b/src/bonsai/lifecycles/cancelTriggerOrdersLifecycle.ts index f5a98926fa..4b49485c0f 100644 --- a/src/bonsai/lifecycles/cancelTriggerOrdersLifecycle.ts +++ b/src/bonsai/lifecycles/cancelTriggerOrdersLifecycle.ts @@ -1,3 +1,6 @@ +// eslint-disable-next-line import/no-internal-modules +import { selectRawParentSubaccountData } from '@/bonsai/selectors/base'; + import { timeUnits } from '@/constants/time'; import { WalletNetworkType } from '@/constants/wallets'; @@ -25,7 +28,7 @@ export function setUpCancelOrphanedTriggerOrdersLifecycle(store: RootStore) { const selector = createAppSelector( [selectTxAuthorizedCloseOnlyAccount, selectOrphanedTriggerOrders], (txAuthorizedAccount, orphanedTriggerOrders) => { - if (!txAuthorizedAccount || orphanedTriggerOrders == null) { + if (!txAuthorizedAccount || orphanedTriggerOrders?.ordersToCancel == null) { return undefined; } @@ -35,7 +38,8 @@ export function setUpCancelOrphanedTriggerOrdersLifecycle(store: RootStore) { localDydxWallet, sourceAccount, parentSubaccountInfo, - ordersToCancel: orphanedTriggerOrders, + ordersToCancel: orphanedTriggerOrders.ordersToCancel, + groupedPositions: orphanedTriggerOrders.groupedPositions, }; } ); @@ -51,7 +55,7 @@ export function setUpCancelOrphanedTriggerOrdersLifecycle(store: RootStore) { runFn(async () => { try { - const { ordersToCancel: ordersToCancelRaw } = data; + const { ordersToCancel: ordersToCancelRaw, groupedPositions } = data; const ordersToCancel = ordersToCancelRaw.filter((o) => !cancelingOrderIds.has(o.id)); // context: Cosmos wallets do not support our lifecycle methods and are instead handled within useNotificationTypes @@ -67,6 +71,8 @@ export function setUpCancelOrphanedTriggerOrdersLifecycle(store: RootStore) { `Cancelling ${ordersToCancel.length} trigger orders`, { ordersToCancel, + groupedPositions, + bonsaiParentSubaccountData: selectRawParentSubaccountData(store.getState()), } ); diff --git a/src/bonsai/rest/compliance.ts b/src/bonsai/rest/compliance.ts index 38d2d06776..6dd423dcf8 100644 --- a/src/bonsai/rest/compliance.ts +++ b/src/bonsai/rest/compliance.ts @@ -1,10 +1,18 @@ import { timeUnits } from '@/constants/time'; import { type RootStore } from '@/state/_store'; -import { getUserSourceWalletAddress, getUserWalletAddress } from '@/state/accountInfoSelectors'; +import { + getUserSolanaWalletAddress, + getUserSourceWalletAddress, + getUserWalletAddress, +} from '@/state/accountInfoSelectors'; import { getSelectedDydxChainId, getSelectedNetwork } from '@/state/appSelectors'; import { createAppSelector } from '@/state/appTypes'; -import { setLocalAddressScreenV2Raw, setSourceAddressScreenV2Raw } from '@/state/raw'; +import { + setLocalAddressScreenV2Raw, + setSolanaAddressScreenRaw, + setSourceAddressScreenV2Raw, +} from '@/state/raw'; import { getHdKeyNonce } from '@/state/walletSelectors'; import { loadableIdle } from '../lib/loadable'; @@ -103,3 +111,26 @@ export function setUpIndexerLocalAddressScreenV2Query(store: RootStore) { store.dispatch(setLocalAddressScreenV2Raw(loadableIdle())); }; } + +export function setUpIndexerSolanaAddressScreenQuery(store: RootStore) { + const cleanupEffect = createIndexerQueryStoreEffect(store, { + name: 'solanaAddressScreen', + selector: getUserSolanaWalletAddress, + getQueryKey: (address) => ['screenSolanaWallet', address], + getQueryFn: (indexerClient, address) => { + if (address == null) { + return null; + } + return () => indexerClient.utility.complianceScreen(address); + }, + onResult: (screen) => { + store.dispatch(setSolanaAddressScreenRaw(queryResultToLoadable(screen))); + }, + onNoQuery: () => store.dispatch(setSolanaAddressScreenRaw(loadableIdle())), + ...pollingOptions, + }); + return () => { + cleanupEffect(); + store.dispatch(setSolanaAddressScreenRaw(loadableIdle())); + }; +} diff --git a/src/bonsai/rest/fills.ts b/src/bonsai/rest/fills.ts index 1e70e7bad0..0cf4f1fb0c 100644 --- a/src/bonsai/rest/fills.ts +++ b/src/bonsai/rest/fills.ts @@ -19,7 +19,7 @@ export function setUpFillsQuery(store: RootStore) { selector: selectParentSubaccountInfo, getQueryKey: (data) => ['account', 'fills', data.wallet, data.subaccount], getQueryFn: (indexerClient, data) => { - if (!isTruthy(data.wallet) || data.subaccount == null || data.isGeoRestricted) { + if (!isTruthy(data.wallet) || data.subaccount == null || data.isPerpsGeoRestricted) { return null; } return () => diff --git a/src/bonsai/rest/lib/nobleTransactionStoreEffect.ts b/src/bonsai/rest/lib/nobleTransactionStoreEffect.ts index 94a7618631..05623752b9 100644 --- a/src/bonsai/rest/lib/nobleTransactionStoreEffect.ts +++ b/src/bonsai/rest/lib/nobleTransactionStoreEffect.ts @@ -58,7 +58,7 @@ const selectNobleTxAuthorizedAccount = createAppSelector( ComplianceStatus.CLOSE_ONLY, ComplianceStatus.FIRST_STRIKE_CLOSE_ONLY, ].includes(complianceData.status) && - !(complianceData.geo.currentlyGeoBlocked && enableGeoCheck); + !(complianceData.geo.isPerpetualsGeoBlocked && enableGeoCheck); if (!parentSubaccountInfo.wallet || !isAccountRestrictionFree || localWalletNonce == null) { return undefined; diff --git a/src/bonsai/rest/orders.ts b/src/bonsai/rest/orders.ts index bfabe11caf..9cb34ff94b 100644 --- a/src/bonsai/rest/orders.ts +++ b/src/bonsai/rest/orders.ts @@ -21,7 +21,7 @@ export function setUpOrdersQuery(store: RootStore) { selector: selectParentSubaccountInfo, getQueryKey: (data) => ['account', 'orders', data.wallet, data.subaccount], getQueryFn: (indexerClient, data) => { - if (!isTruthy(data.wallet) || data.subaccount == null || data.isGeoRestricted) { + if (!isTruthy(data.wallet) || data.subaccount == null || data.isPerpsGeoRestricted) { return null; } return () => diff --git a/src/bonsai/rest/transfers.ts b/src/bonsai/rest/transfers.ts index 0e5b5db00b..4ce85d3dbf 100644 --- a/src/bonsai/rest/transfers.ts +++ b/src/bonsai/rest/transfers.ts @@ -21,7 +21,7 @@ export function setUpTransfersQuery(store: RootStore) { selector: selectParentSubaccountInfo, getQueryKey: (data) => ['account', 'transfers', data], getQueryFn: (indexerClient, data) => { - if (!isTruthy(data.wallet) || data.subaccount == null || data.isGeoRestricted) { + if (!isTruthy(data.wallet) || data.subaccount == null || data.isPerpsGeoRestricted) { return null; } return async () => { diff --git a/src/bonsai/selectors/accountTransaction.ts b/src/bonsai/selectors/accountTransaction.ts index 967eb1937d..dd67098eeb 100644 --- a/src/bonsai/selectors/accountTransaction.ts +++ b/src/bonsai/selectors/accountTransaction.ts @@ -39,7 +39,7 @@ export const selectTxAuthorizedAccount = createAppSelector( ComplianceStatus.CLOSE_ONLY, ComplianceStatus.FIRST_STRIKE_CLOSE_ONLY, ].includes(complianceData.status) && - !(complianceData.geo.currentlyGeoBlocked && geoCheckEnabled); + !(complianceData.geo.isPerpetualsGeoBlocked && geoCheckEnabled); if (!parentSubaccountInfo.wallet || !isAccountRestrictionFree || localWalletNonce == null) { return undefined; diff --git a/src/bonsai/selectors/base.ts b/src/bonsai/selectors/base.ts index ee0daa0a12..cce4a2692c 100644 --- a/src/bonsai/selectors/base.ts +++ b/src/bonsai/selectors/base.ts @@ -71,6 +71,8 @@ export const selectRawLocalAddressScreenV2 = (state: RootState) => state.raw.compliance.localAddressScreenV2; export const selectRawSourceAddressScreenV2 = (state: RootState) => state.raw.compliance.sourceAddressScreenV2; +export const selectRawSolanaAddressScreen = (state: RootState) => + state.raw.compliance.solanaAddressScreen; export const selectRawGeo = (state: RootState) => state.raw.compliance.geo; export const selectRawGeoHeaders = (state: RootState) => state.raw.compliance.geoHeaders; export const selectRawRewardParams = (state: RootState) => state.raw.rewards.data.data; diff --git a/src/bonsai/selectors/compliance.ts b/src/bonsai/selectors/compliance.ts index 335f6a142f..5dcf41d5b6 100644 --- a/src/bonsai/selectors/compliance.ts +++ b/src/bonsai/selectors/compliance.ts @@ -6,6 +6,7 @@ import { selectRawGeo, selectRawGeoHeaders, selectRawLocalAddressScreenV2, + selectRawSolanaAddressScreen, selectRawSourceAddressScreenV2, } from './base'; @@ -15,9 +16,16 @@ export const selectCompliance = createAppSelector( selectRawGeoHeaders, selectRawSourceAddressScreenV2, selectRawLocalAddressScreenV2, + selectRawSolanaAddressScreen, ], - (geo, geoHeaders, sourceAddressScreenV2, localAddressScreenV2) => - calculateCompliance({ geoHeaders, geo, localAddressScreenV2, sourceAddressScreenV2 }) + (geo, geoHeaders, sourceAddressScreenV2, localAddressScreenV2, solanaAddressScreen) => + calculateCompliance({ + geoHeaders, + geo, + localAddressScreenV2, + sourceAddressScreenV2, + solanaAddressScreen, + }) ); export const selectComplianceLoading = createAppSelector( @@ -26,12 +34,19 @@ export const selectComplianceLoading = createAppSelector( selectRawGeoHeaders, selectRawSourceAddressScreenV2, selectRawLocalAddressScreenV2, + selectRawSolanaAddressScreen, ], - (geo, geoHeaders, sourceAddressScreenV2, localAddressScreenV2) => - mergeLoadableStatus(geo, geoHeaders, localAddressScreenV2, sourceAddressScreenV2) + (geo, geoHeaders, sourceAddressScreenV2, localAddressScreenV2, solanaAddressScreen) => + mergeLoadableStatus( + geo, + geoHeaders, + localAddressScreenV2, + sourceAddressScreenV2, + solanaAddressScreen + ) ); -export const selectIsGeoRestricted = createAppSelector( +export const selectIsPerpsGeoRestricted = createAppSelector( [selectRawGeoHeaders], (geoHeaders) => geoHeaders.data?.status === 'restricted' ); diff --git a/src/bonsai/socketSelectors.ts b/src/bonsai/socketSelectors.ts index 160bd1607d..645829b57d 100644 --- a/src/bonsai/socketSelectors.ts +++ b/src/bonsai/socketSelectors.ts @@ -6,7 +6,7 @@ import { type RootState } from '@/state/_store'; import { getSelectedNetwork } from '@/state/appSelectors'; import { createAppSelector } from '@/state/appTypes'; -import { selectIsGeoRestricted } from './selectors/compliance'; +import { selectIsPerpsGeoRestricted } from './selectors/compliance'; const suffix = '/v4/ws'; export function getWebsocketUrlForNetwork(network: DydxNetwork) { @@ -25,8 +25,8 @@ export const selectIndexerUrl = createAppSelector([getSelectedNetwork], (network // TODO allow configurable parent subaccount number export const selectParentSubaccountInfo = createAppSelector( - [(state) => state.wallet.localWallet?.address, selectIsGeoRestricted], - (wallet, isGeoRestricted) => ({ wallet, subaccount: 0, isGeoRestricted }) + [(state) => state.wallet.localWallet?.address, selectIsPerpsGeoRestricted], + (wallet, isPerpsGeoRestricted) => ({ wallet, subaccount: 0, isPerpsGeoRestricted }) ); export const selectIndexerReady = createAppSelector( diff --git a/src/bonsai/storeLifecycles.ts b/src/bonsai/storeLifecycles.ts index 2238516869..311fa76e33 100644 --- a/src/bonsai/storeLifecycles.ts +++ b/src/bonsai/storeLifecycles.ts @@ -7,6 +7,7 @@ import { setUpAssetsQuery } from './rest/assets'; import { setUpBlockTradingRewardsQuery } from './rest/blockTradingRewards'; import { setUpIndexerLocalAddressScreenV2Query, + setUpIndexerSolanaAddressScreenQuery, setUpIndexerSourceAddressScreenV2Query, } from './rest/compliance'; import { setUpConfigTiersQuery } from './rest/configTiers'; @@ -70,6 +71,7 @@ export const storeLifecycles = [ setUpGeoQuery, setUpIndexerSourceAddressScreenV2Query, setUpIndexerLocalAddressScreenV2Query, + setUpIndexerSolanaAddressScreenQuery, setUpUsdcRebalanceLifecycle, setUpNobleBalanceQuery, setUpNobleBalanceSweepLifecycle, diff --git a/src/bonsai/types/summaryTypes.ts b/src/bonsai/types/summaryTypes.ts index eb0affb627..a844d20f36 100644 --- a/src/bonsai/types/summaryTypes.ts +++ b/src/bonsai/types/summaryTypes.ts @@ -396,7 +396,7 @@ export type GeoState = { }; export type Compliance = ComplianceResponse & { - geo: { currentlyGeoBlocked: boolean; currentCountry?: string }; + geo: { isPerpetualsGeoBlocked: boolean; currentCountry?: string }; }; export type SubaccountPnlEntry = SubaccountPnlTick; diff --git a/src/components/ComplianceBanner.tsx b/src/components/ComplianceBanner.tsx index 55b821fd68..a107590d7a 100644 --- a/src/components/ComplianceBanner.tsx +++ b/src/components/ComplianceBanner.tsx @@ -7,7 +7,7 @@ import { ButtonStyle, ButtonType } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; import { useBreakpoints } from '@/hooks/useBreakpoints'; -import { usePerpetualsComplianceState } from '@/hooks/usePerpetualsComplianceState'; +import { useComplianceState } from '@/hooks/useComplianceState'; import { useResizeObserver } from '@/hooks/useResizeObserver'; import { useSimpleUiEnabled } from '@/hooks/useSimpleUiEnabled'; import { useStringGetter } from '@/hooks/useStringGetter'; @@ -17,14 +17,12 @@ import breakpoints from '@/styles/breakpoints'; import { AlertMessage } from './AlertMessage'; import { IconName } from './Icon'; import { IconButton } from './IconButton'; -import { TermsOfUseLink } from './TermsOfUseLink'; export const ComplianceBanner = ({ className }: { className?: string }) => { const [showLess, setShowLess] = useState(false); const complianceBannerRef = useRef(null); const stringGetter = useStringGetter(); - const { complianceMessage, showComplianceBanner, showRestrictionWarning } = - usePerpetualsComplianceState(); + const { complianceMessage, showComplianceBanner } = useComplianceState(); const { isTablet } = useBreakpoints(); const isSimpleUi = useSimpleUiEnabled(); @@ -42,19 +40,6 @@ export const ComplianceBanner = ({ className }: { className?: string }) => { return null; } - const complianceContent = showRestrictionWarning ? ( - - {stringGetter({ - key: STRING_KEYS.PERPETUALS_UNAVAILABLE_MESSAGE, - params: { - TERMS_OF_USE_LINK: , - }, - })} - - ) : ( - {complianceMessage} - ); - const toggleShowLess = () => { setShowLess((prev) => !prev); }; @@ -70,9 +55,10 @@ export const ComplianceBanner = ({ className }: { className?: string }) => { {showLess ? ( stringGetter({ key: STRING_KEYS.COMPLIANCE_WARNING }) ) : ( -
{complianceContent}
+
+ {complianceMessage} +
)} - { buttonStyle={ButtonStyle.WithoutBackground} /> )} - - {showLess && isTablet - ? stringGetter({ key: STRING_KEYS.COMPLIANCE_WARNING }) - : complianceContent} + {showLess && isTablet ? ( + stringGetter({ key: STRING_KEYS.COMPLIANCE_WARNING }) + ) : ( + {complianceMessage} + )} ); }; diff --git a/src/constants/clc.ts b/src/constants/clc.ts new file mode 100644 index 0000000000..732b9e37e4 --- /dev/null +++ b/src/constants/clc.ts @@ -0,0 +1,324 @@ +export const TRADING_LEAGUE_REWARDS_DETAILS_ROUND_2 = { + claimDeadline: '2026-01-31T23:59:59.000Z', + claimStartTime: '2026-01-01T00:00:00.000Z', + assumedPrice: 0.33, + estimatedWalletRewards: { + dydx19d870eyk3qh46y2h7kg9kzssa4gwhuktna3uql: 40000, + dydx15pnlwr3mkaph2u3kxvvdprzncmuw9l7h525n5k: 40000, + dydx1qe9qjrmedx3pjqg98acn3tfxhxrteduskfctn0: 35000, + dydx1hsest59cpjhzlpslwdsjmvwapjyd0cfthmn3ja: 32000, + dydx172rtxnesdvgx89nr9umykly7wtwvsk8z2rteme: 29500, + dydx1jxe3l4h2ychf66q5f37qxfpx2jh6l3zjum39rz: 25000, + dydx1klkt566wcz4hel793gah0t43lqlpqwg04h8uhf: 25000, + dydx197azmzp9hhntrff92lvjhks294v3eazdfcal83: 22500, + dydx1xpc9hhel8pq62z8dv2c87davmk03g4cf3297fh: 19500, + dydx1y7m4kmlgj5my4g5l2hk0rujy0n8y8hqws0fa2y: 17000, + dydx1m9hg73dtn5ku8ulmj8rjmdqh0hk7uuhawc69cn: 15500, + dydx1rgmc9rjk9d5q2x9h4c6l9y77r9ul0n6dpg5n8f: 15000, + dydx1a3ggqr30aduu4nhf92xxv8zqceczqdsg078lk4: 15000, + dydx16y908xy4pg2q07a8ydfzxwcryy8uzwt044uvu0: 15000, + dydx1vh0w3zk8frkwpru40dk7h9c6tegfm9rcjj3ghk: 14500, + dydx1snmymvxr6hvhl5gqv8aq8fvyyxjvk6dkhwqez3: 13500, + dydx13aeza8l3mrcr8d34g9z8tcunncl74gprf7t98g: 13500, + dydx1pj9ldud50jrrxv30vp5pssh0p6jj7sk6hlufag: 10500, + dydx102d67xwnxufuy0egj6kw3x9994wzxwzscmdcy6: 10500, + dydx1wdm2vfjzqjn2k37n86aqz7ccf5qpdpwj5serjr: 9000, + dydx1mlf3afr8k6fh6k9sk9ktauylwh6ckwg8t70zhg: 9000, + dydx12v3g68sjcn0s4v3jlc5jtuklv5arps2y6syjuz: 8500, + dydx1fhkd3hzwwk9jrrsnuuq78uf5asjra4l8fhnsma: 8500, + dydx18cc9ykg9jhmhphqnzevt9hvx0924gkzgvhkg3f: 8500, + dydx1tr4k95ttntc4852pldhjaqwp5magt3m6j620tt: 8000, + dydx1avw959lk9lg4u88atedw7988aluyanhmu26mtx: 7500, + dydx1uyyz3nsm4rpgjja9zhhvrvd63wcmyhuphtk2qk: 7500, + dydx1afskx2knwm4fp6s5ydkaf0txnlxux7qavr8xws: 7000, + dydx1l8p5yw52ka6c09lwnw8q6k8fych5gp25ah9c8k: 7000, + dydx1wvg5l490ce2wckt74cvxlskkwsrdje8sz20292: 7000, + dydx179gwlz4fx20dntagmyzgu2uem5f95ej05qzpse: 7000, + dydx13z3sg996n7mu0pvy0yf26fsmmnn9uladre4w2r: 7000, + dydx1l8t5jpsc0r94ljqrjlqxk4z0z6qx7cxp7hp3uw: 7000, + dydx1n76jumjapfqkpnyqgvvqcv8762m06k4yl6cr5y: 7000, + dydx17tch34sk5gzmn92zc5srwjngqgfqxcfex9339j: 6500, + dydx1aw5qvzhjgtyukhx4g8hxxsh0e8pfn28gdp0ley: 6000, + dydx1sdx3cdcvkvygkq24tcsa9z2z4lkszfu44q0lgs: 5500, + dydx12r9zw52gcejn4smmcvvjc5d24cr2drkknv4kkh: 5000, + dydx130h5uc3ejwjtdhaacp5jydnccte86d3s09pfw6: 5000, + dydx1uz7qvyf59qx8x57gfd9a7uedk42m4vpadsu6wg: 5000, + dydx1p8e28y4msnlqrdsp352l35x7myevu54m60jznd: 4000, + dydx1eue0sumfls5hu59ka0ut49unxdn40q2lu2c7hp: 4000, + dydx1xa8vpn0e9gjfhaqhnl06rjhjngg54mywk0aax8: 4000, + dydx13esn47l9dsd9hzkzmra9q3xwdt5dt34apm2877: 3500, + dydx1xdwvhke36ta6g9ma58hnrqe8m5wkla7qy097cn: 3500, + dydx1rmt5tm8eesmp06nwsy0ulcy8x3d65wz3x2al25: 3500, + dydx1nr4swq0m7xd0duuccwfgl045x2zt6gmtp8dlnw: 3500, + dydx1f3l724ezg3m9eykkyem6xamguqyf434ptzpkda: 3500, + dydx10ur40x9pcn8pdz5h6t9tjt0smuyuazvv0vvldf: 3500, + dydx1m9js03rpj88rta3c5gcx7w6h0h2ulu40wqryhv: 3000, + dydx1qmx0way5kd9umj9vgsyeg4e2sh9m3s293mpgq9: 3000, + dydx1x89rwrgsd287j8nyrk084c2rqwxv9zxypv7nht: 3000, + dydx1de4c2mlgkw6g8mfyq7pnkfaeje0h4rcdpfptap: 3000, + dydx1njex4w6u3f8em7rdqraevyscfyev5s4gka06yw: 3000, + dydx1eds2v66jspluc26rztn07qd0m8548096qk06tw: 3000, + dydx137knwu32kuvshpanxsz6plfnsjymxun89u6lwz: 3000, + dydx10wv0ur56cq3wyk5v58cjnux4745xc85fe7uurj: 3000, + dydx1tz6xaeuwu8edptyq82aleveduymuemf4lvhttn: 3000, + dydx1ehcctnrgr9uq65z4dgkxfz9jhkf9x4ylld6aef: 3000, + dydx10a2avqqa6dmgp0gx83pdyz4dp7pllhu0x5d3xx: 3000, + dydx14mlqzx6v5ajmerfqwmeu2asr32a0qs2dfc5970: 3000, + dydx18qu7qjy674dmzmq4qd5qj6nvc3kfhhfslmp5u5: 3000, + dydx1erkqndnmndy5fmgny7wh0v4a72qy3q6vdhhvn0: 3000, + dydx1eqszeg4snqjp7qw7tgkl2mchucalkf46jefw27: 3000, + dydx18023tv2kw8c03us8nl77ma00466zqqzgzh7l6y: 3000, + dydx13j3xankqf8xjv84a6gmlr25u2nq4w2cy774kjz: 3000, + dydx1gd5k000vvuhm6aexjemktfaevzy53wm3cyyp20: 3000, + dydx163jxd5fpspu96dyn4et0364thlgtzsumk8ruvf: 3000, + dydx1afr2uvufcw4qwr4k9le2g8yxughnztncm3j5v9: 3000, + dydx187dq90qnnentgfxwynf8cmjjahrn6jy07m4k5g: 3000, + dydx1cuasd5stzaahvmtxnd9mj0rutf92q5a4lzl2vd: 3000, + dydx188q90vue8dhggrpvsk40drwv7h446ev9dtvdjg: 3000, + dydx14wl379fsahkev7xj8k4jaya2vp2fayexs35mzk: 3000, + dydx13gfqeuw48waxl9m50seajkgq6h37pz80w7l04g: 3000, + dydx1q8pg5tldgfv6kfzdcp8g5y3c6vvr7n7yauq8zk: 2500, + dydx1h94vyztjrp4t6m6vymnfaq9zt9ww5snfqqxhzn: 2500, + dydx1hmsgjf9qkaud966z63nwcn9p9l36jl5r3z9gqq: 2500, + dydx13yr5xfrf0r72f9l8zmyu9khmzlt0lj6e2ps7ql: 2500, + dydx190769ssnqlk89zsplrh6aegjly30k6e5vqn2g6: 2500, + dydx109dq6uwrkg9px936zh7w66dvyjswthhmg32jjx: 2000, + dydx1pa4dlv44a5wpya29t8trjs44w7s9r96y0j0580: 2000, + dydx1wyhxdz7aej4ze80f3szmdveufz56zanhv6ej7t: 2000, + dydx1k47wywwmxwu90jrj5dxlcw43jl92rra3qfv409: 2000, + dydx1qcmtnyl3t7xtc6hewc05u35cxx38up6thlqnra: 2000, + dydx1muahvk8j3lqpdymsx3s05rxr0sc0yy72k5yvdx: 2000, + dydx1rgvj3snmdsxersda2zyfre2km0de2hxyydynlp: 2000, + dydx1r0jdema35fkl4k4q0808955n56rjeq92rhz7jh: 2000, + dydx1vd27pygl6y4qndpmd4hc86fqvdj7tj4hhf6hee: 1500, + dydx1tdmpay2z3q4zpzefq4pp65t4als8ujzrrfud9r: 1500, + dydx1py5jha7vg3lm2xknefn33zyv0sj40fy2fhyjvt: 1500, + dydx1fs456vxznh5ajpqwa59x4etzv0tq6eg6asq0ls: 1500, + dydx184pvt88rcnsfkps38k69nwem26fsefcx2krrej: 1500, + dydx1fapz906ddzt8glslkrd36cctnqzcwwawppvlv0: 1500, + dydx13erzx7unukhpe2vplet2z0kpdqj30wzh5muhem: 1500, + dydx1u4up78lpppuua56vlsvnhqctcw2emgsxqc0ty6: 1500, + dydx14r34ern0duvaerpypl6lh0lyeesa5454hr5ctr: 1500, + dydx1xr46hn6jgm3j6akwfgpmfzlpm50cnpptj8d6jr: 1500, + dydx1lacuh4rh5vzkv00ctrsstevyh35s3ql77g2tcu: 1500, + dydx15zcrkaq3w2k0gqdha3r4c5pqlz4nrjtfn67vww: 1500, + dydx1jfl3c74shcqpq7kvaa6ac7g3pmg2warlrfa2mk: 1500, + dydx1keelhggvqu9qley30qx9s8evu4f9jznxugh5pn: 1500, + dydx1607xpmlwf6ehf2gnracmzrstf9zflqs5hhl2l5: 1500, + dydx1mrkn5wkt8xy6ajlvjkq5fm8j9lq6er48xk84tr: 1500, + dydx17c2u8cj7jswzkj5ev8cakx285tw7d5q4mwrt27: 1500, + dydx1y8vy456kawy2gt2d8zxwqjn4ttdp76zmc023pp: 1500, + dydx164d2zx5tsuaup94t49lcmhkazc4zsdae8elrwc: 1500, + dydx1864p4pym4hnddvfeq32zlej9y89m2yc7h8jn22: 1500, + dydx17qvwdpdljavs8ksg89pdjuqf3nmzdu6x92wst9: 1500, + dydx1svafrk74j7gy3yk6594ln47l5ejeuah6hzv00t: 1500, + dydx15jan3n0g57f7wd9wfq45t8trga52yhj2zu6ky2: 1500, + dydx1edu3jw2pakcq3r8fdde93ej3eclzx8utp3c355: 1500, + dydx1wfxtn2m4j7f2tfjfl9tfaw4ffysc2mzh39zjlr: 1500, + dydx15jldkg89x34u6wmy2qw2rxmgjtxj0zdyu34n9p: 1500, + dydx1c5sprm6rz2usnj2lrs90tpczy4n88hlz6clau8: 1500, + dydx1jnsey8ggez76uug9xvmmeutdjz7y2ass0ecf78: 1500, + dydx1sfmzn7vrek3x4zmk8jgx5s2nqgq0tw2hu3ajhf: 1500, + dydx1kahh3jcyh5x34z73xzv8lrek6ywq3zqq5k78w0: 1500, + dydx1l727655ld94sd4mgrvstyhpnknzva6x949j5km: 1500, + dydx1plcdfmmez7xa8wevq3ssmv3v4kekhvr2z5e9qr: 1500, + dydx1au6ek6rnxre3ra393u3g588mhzzj9nzjvxatq4: 1500, + dydx14gkzm985h58vmjm9ckhfnlcvhzcyfhqkhr9dy4: 1500, + dydx1cteyp74m3ym9ps8nr4ktm9gltvmfj3glmws44q: 1000, + dydx1csrt84ze7pxkhr3jdyjlk057y30walygzwldrf: 1000, + dydx1uwyn5tzq9n3ud4ezm37lfqrcfetgxapsu0ach2: 1000, + dydx1kn4e686xfcuwvntsy74rh9ktf72fhhfxzlc85y: 1000, + dydx1kf3h0u4cmjsen54zk2lr4f2j88m59a50v0r0kj: 1000, + dydx1um23reecq7p9yfve92jmg4w06sptjt3vw2vm2h: 1000, + dydx18c5f8qsxcxqusg45ka7d6n3n8qeerqk2fauchu: 1000, + dydx19nguaevz6768uyk3z4jrge4utwms93a0y77jxf: 1000, + dydx12fdr5ppm4vyhd54x84n8878h2wcshqgkejkuxl: 1000, + dydx1t73wulju7kzgzq5hljp6uwyd8ufenejj80nlwh: 1000, + dydx1gqjjglznj0u3v36hpj0nzss39ccprav3hehfgn: 1000, + dydx1v5g96ajdl652jrtvgdz65c3f0aus5g0uyrfk7l: 1000, + dydx1lxc8dcw0v4sram0287kmv0v7g73yehl644jvtc: 1000, + dydx1e7nngz28j92c6hmjh57n0q4tqul8utcrzfax0m: 1000, + dydx19z6mfaw668eecnrsdyxnfz22rn4rclq4thj4p5: 1000, + dydx1uktr3yqtjky04ddkdsdvgsnefa5v0a2dapgz9g: 1000, + dydx1x5ap3q26sfh9nqu6knxd6748p2s7nthqf66msz: 1000, + dydx14flnc25tppft4lv6gpwq9kwcsxprvxf0cr8pwm: 1000, + dydx1x6lt3each6yswn68rjkwl8j40wys6386wumpx9: 1000, + dydx1af63pr5p8pyfu2nakwyjs5hjx5jj8kuu0d2980: 1000, + dydx1jx6lmnanuhczyntpcjlwyfwwn8vjk34e0yuqvv: 1000, + dydx1lqv8k0f8h9kvm0g74urd3330r93j8f8ef6watp: 1000, + dydx1rlzzqaz08v2t0r64fppuacr9rvmw0zxt98tgcp: 500, + dydx1q8cednyr7sdy5s0u4jgvyh3de4nzt8078de3nu: 500, + dydx13r7n4klw0tv2sj8hzjrtdkwuws6ra7wxuual6t: 500, + dydx1dz6frxlgpph46mj8c9782w6ag4qy7pn89usp2q: 500, + dydx1652u6kkczjvxmjz8rg7tgqcjw0shexayxgcnjs: 500, + dydx1c5w07e9n23amjqvv46rva2zanw7lelxse5kp0z: 500, + dydx1drr5ydek02a5kezncmnl66mlqafgaxvymuly98: 500, + dydx1fh3763g2s0qus7ws82l6naufj4sww46cnw49pz: 500, + dydx1mt44hpstxml2swhjara3hudl5wtnuq73ugy4yv: 500, + dydx1wpxg935c52j236wmq7rzy94yn3pjdd220r23xy: 500, + dydx1x4nxgjy2emk2ysfafch0yj8a6s8ttxwrur8fgt: 500, + dydx1nqdhw7c25mdmswyhxjgtpnpf87pfq8m0fhal32: 500, + dydx13e3zh3vn87qyyqefmamemg0k2vdqer3v0nmh69: 500, + dydx1te024wtlhclslwf4vg4gvvupmgzrvj3egmuntk: 500, + dydx147fq0dfk625h325xjd9wcvx55am8578aj8eul3: 500, + dydx1squ2wfcsysec2x0xk374jaqqt6gf3w9tn73dmf: 500, + dydx1xtcq873fs2a7mkyqcwpfftul8ernpfklgmmtrt: 500, + } as Record, +}; + +export const LOSS_REBATE_DETAILS_DECEMBER = { + claimDeadline: '2026-01-31T23:59:59.000Z', + claimStartTime: '2026-01-01T00:00:00.000Z', + estimatedWalletRebates: { + dydx1mlf3afr8k6fh6k9sk9ktauylwh6ckwg8t70zhg: 21214.59, + dydx1t7e8322wxcrdnjs4lks0x4epzql00rkwqxnt33: 12569.39, + dydx1hyfm837du0zzn2pw0r328q7nl07c0um7u207ge: 5_144.26, + dydx17vt04dtfau0hvm0v6ahx708lflpgg4gedfqsyh: 5_132.79, + dydx1ufs97x7psym2735x7sfksdrsux63wzjfzfvpwd: 4_816.17, + dydx1qkw0s4vzn8qmq7kx8cusdmqexsgrnx73ue7xpf: 4_738.85, + dydx10yeumv4mdfre68g3tcf03nlr9y4rlsn4uss4uf: 4_486.49, + dydx17tch34sk5gzmn92zc5srwjngqgfqxcfex9339j: 4_478.6, + dydx1xg8lddfdvvlht55ddvaljupsh05n2t5m6v590z: 4_375.48, + dydx13aeza8l3mrcr8d34g9z8tcunncl74gprf7t98g: 3_666.87, + dydx172rtxnesdvgx89nr9umykly7wtwvsk8z2rteme: 3_062.1, + dydx1jxe3l4h2ychf66q5f37qxfpx2jh6l3zjum39rz: 2_849.81, + dydx1a3ggqr30aduu4nhf92xxv8zqceczqdsg078lk4: 2_667.3, + dydx1vz42lw99mnk99ln7qurfg4rwrld7e96cm76xe5: 2_579.8, + dydx1j9z9uma04xrr52fcmw9q56qjmtmax53ne6c53m: 2_532.1, + dydx19z6vfsndsjsm9cadum24vft9tvp23cmfe8twj2: 2_417.43, + dydx1fq6yr280mge9ke9rncwzfdsf8fq8pxvxj643h0: 2_068.35, + dydx1hsest59cpjhzlpslwdsjmvwapjyd0cfthmn3ja: 1_893.51, + dydx1kh9rhnyjmh6h4dpzpxqfcsc3dhmzwpz9myu7ev: 1_880.28, + dydx1e7hkcyyskgdgcc4j4pk06j99ylcfj0y8mf5s83: 1_879.3, + dydx1za0zs4stp65vll2nmh26qualq0uj6thh7ryyxc: 1_791.37, + dydx17c3qn6jxe2c3ea7ck960h0q20fma7gz65f8gkr: 1_750.99, + dydx1t9hd44tz6rh5ah7ljcyp99053rpymwm5fw4f53: 1_715.4, + dydx1zug353eq708ycmx5m8wp2ldj5nwfusl3kz2svk: 1_646.83, + dydx146keskm02u7hkqc6v6nvq3f8slavrytwpks6zz: 1_625.57, + dydx1n76jumjapfqkpnyqgvvqcv8762m06k4yl6cr5y: 1_491.74, + dydx1r0jdema35fkl4k4q0808955n56rjeq92rhz7jh: 1_297.48, + dydx1qnulemnmyrm2rv2u3y6nfq379kldjza8qxgcga: 1_279.73, + dydx1p8e28y4msnlqrdsp352l35x7myevu54m60jznd: 1_232.31, + dydx142v4mv2ng6ejxd8anx7a93p5tx8md5mqgz6nqj: 1_214.43, + dydx19vs0ys95hauyu6yejdkt59rvungn9z8zj6l8kt: 1_171.12, + dydx1afr2uvufcw4qwr4k9le2g8yxughnztncm3j5v9: 1_168.91, + dydx1y92j8l9nmnx8htjx3f9jec4eg4xzlepreg5xa5: 1_156.29, + dydx10gtrjmf0qsutgrlsc32qwv6dq4v8xwqdcameyl: 1_121.41, + dydx18023tv2kw8c03us8nl77ma00466zqqzgzh7l6y: 1_075.65, + dydx1f3l724ezg3m9eykkyem6xamguqyf434ptzpkda: 975.78, + dydx10wv0ur56cq3wyk5v58cjnux4745xc85fe7uurj: 935.56, + dydx1m4mc4dsvdf3q7jpa5kgpz2w86wykfqergfmc5v: 926.01, + dydx1hn02fr6lt0pjnmz4cax69uhfrhsdxanha5yjya: 887.64, + dydx19v3y45yas9k0k9qzv3pqmwdzusgrlxev6t45fj: 867.28, + dydx16y908xy4pg2q07a8ydfzxwcryy8uzwt044uvu0: 853.66, + dydx1yrne3cadvch3hju8at5k4tn9xp98z4hgkzyhj5: 849.7, + dydx1270ufzkufkkp4l922zltjf5g9rlufupl748lr2: 846.13, + dydx1nvuqg7755yzeun2vkulltlnx854uxm0gur6m0p: 819.33, + dydx1ekhpltj546m0ket08s32r6ggzsj60zjelhtyqq: 812.27, + dydx1878e4s67ura0l5mp4zesz6ghkcr4u232zhfcy4: 811.61, + dydx1qlnpjgqhdfca54gjrute88estqwz7mchffyawk: 806.79, + dydx1lh75k0str8nxgwgrd2xwx440l38ds9ur0ek67e: 801.82, + dydx176txwnwps6shutxl0jwlv9p4vhdasmp3fm6s4e: 795.8, + dydx1rr2q3dm02dpagddyj2fklgn78094sdpt9hldh4: 732.98, + dydx1c5sprm6rz2usnj2lrs90tpczy4n88hlz6clau8: 696.99, + dydx1h0xansqzvhs2th9m7cdn5r6e52tkwvxrxp9yuf: 679.59, + dydx1vstyyprk2xtrfe8ealw3wrqt9rgfs9rx9ztfvp: 660.93, + dydx1pwnxr3cwvyc8xlqf8rxzs7ahamkc2g52y7p4yl: 650.76, + dydx1460wq5jknd83vmqgxnrm0jfjjr2tda28ysxc2j: 643.54, + dydx1keelhggvqu9qley30qx9s8evu4f9jznxugh5pn: 635.65, + dydx1zqgese9vvr338c2c63f27gwcg9h8npsstnvsda: 611.51, + dydx1vgl5r7shzlx4s026jvsyyld4v5lz98alwe5xtd: 611.29, + dydx1mf5myafs9vtt0erqpnfrk447g5fd5aa4sqx9t8: 605.22, + dydx1rmt5tm8eesmp06nwsy0ulcy8x3d65wz3x2al25: 601.18, + dydx1yrs7kz3l6rmy97tm7a5lxm4smat2caf8tfnhwa: 597.31, + dydx1rng56c8lg2ggdpav004as9mh5aft9v9fnw73j5: 593.91, + dydx1x7xnr38gpvmk30zk9ggqg6smky2060lj0dme94: 590.43, + dydx14jk68rhpjrlquupvdystfzw2snf7v43qscanqt: 580.64, + dydx12p4z697emcxra05tu0t27ayj33y5tcpx2zq68n: 579.98, + dydx1mefa0n6j58zvxpva0c4na8rgm5rf6sj9wr6xdl: 573.68, + dydx1cdh7lg5awsxdgc2gvm26k8ut8f5nflrw55dzcg: 570.6, + dydx1fq359sn2y7waawamk28t0k3wgkucz25njzm3tc: 567.4, + dydx163s0g5a0f4x8264w5nx2qear06ffe3w45dcv5u: 550.08, + dydx1mc04vfdgq5wqmndlcg3w8ec78sj8mqk6qjfeq2: 548.61, + dydx1psmqc6srxwzqva6s7xccyqr44hmvjsc9hgm0qg: 541.04, + dydx1xg9c2jj7z07krwsfz3njc5gnwcmwfqxcjwl6tw: 537.98, + dydx1r8qx6zsytaah63d93xzd700ls2aw4nr2ew64ht: 532.67, + dydx1x89rwrgsd287j8nyrk084c2rqwxv9zxypv7nht: 496.3, + dydx1r9dnrhek4gneng3kkd2uh8nek3kth34yxmew9k: 496.05, + dydx19x88qpljqxsem2kg54529u8zyvlan4hn3dwjrd: 494.65, + dydx1u98rum2e3plmvwugegcnge6st4pvxpu2ccsvwe: 489.22, + dydx1wdm2vfjzqjn2k37n86aqz7ccf5qpdpwj5serjr: 487.1, + dydx1jfueucq6qg4avnjev0vd2nvtg0rdcl4lvgsr5g: 449.09, + dydx13rysc2grgj8kt8yys6ylm03n9chpwktvx9rcsd: 443.57, + dydx1eds2v66jspluc26rztn07qd0m8548096qk06tw: 441.32, + dydx1uzzht3xa8k6vnwkyq670d9kg8trzajfnceqcrw: 441.16, + dydx1243dmezzarsf6s864jed8qhchkxncn3unl6rez: 439.94, + dydx1tn8k4dkf9zyqqxs5uyrcupuvzh6nz48qrh0ukk: 432.85, + dydx1nr4swq0m7xd0duuccwfgl045x2zt6gmtp8dlnw: 432.79, + dydx1qcslanrp92v7r37w9qx867l5lghk5r5yfc5ajy: 430.0, + dydx19hpc0z4e47w3uyz8xjjs2yv6q74dry79jeamr7: 425.37, + dydx1lszly2ec0aalr8hdmqdcd5afz8raelmstg3whl: 419.85, + dydx1w26nye5g2psq8edhj3ch99tc0p4qkv3kvcdnkx: 407.12, + dydx1qf7flvprrgsdxcd4gmj62gzn7n06lxxgh037eq: 405.73, + dydx1eqszeg4snqjp7qw7tgkl2mchucalkf46jefw27: 404.51, + dydx1jhrcf80p504wxy7lkfd5yh9xf3q0kp90gmx0md: 402.76, + dydx19gvq2407chnwy0qz7ra83cktfymnwy0nn08puz: 378.6, + dydx1sdx3cdcvkvygkq24tcsa9z2z4lkszfu44q0lgs: 372.29, + dydx15uh9tkcht0nvx0n8rqnqd2ky05xkrnmd7un972: 369.69, + dydx1vm2yw07sptmmgum60rvp7ku202j55gkh86k4vv: 365.52, + dydx1mx8selmafedqj7dwljmpttt5m6z5lgufqes6cm: 338.22, + dydx10pacmn48mkmsx22kfn7xqzva99xzqysw8uyu9j: 332.95, + dydx1vk0h00qqzfvw9kdjsywxlatk985a497krmh5hj: 332.01, + dydx17fvqfps00zjvw4adwyfruk03vfw4t662455w6s: 326.91, + dydx15zcrkaq3w2k0gqdha3r4c5pqlz4nrjtfn67vww: 325.4, + dydx13x8welmr77vhhx2369pauhqaay9l7e2z24kjz2: 323.9, + dydx16ca2dg7y477zyrw0demqsse3stesud7val6dk9: 323.42, + dydx1vd27pygl6y4qndpmd4hc86fqvdj7tj4hhf6hee: 322.14, + dydx1gch3w7yu2cp97gsukx4ludfn8stwdk50gcf65d: 319.44, + dydx1rdh0tkmnc24uzx7ek94g9qzg02jnpjp8w3wwm2: 317.11, + dydx1rjswanm2hr2m5xlr7da8zsdv8nmldgvrg7x5eq: 300.9, + dydx1gmmyuv4ttzgduj6zpxtsnemvfa5gr6ca4cmcj2: 298.93, + dydx1cks57memrkcnc87taxddvmphlkm2ypja5u7g3x: 297.47, + dydx1st9u6puqvv38hf3jyvce8f9j0gvlprze9jqa8f: 294.29, + dydx1ga7aqlv9ll765v0pqtzcspmvg9xgxgj3kjvpss: 294.07, + dydx1pvp6acnlve8yl4gu7uvgvhnlzt8tayc73kzyur: 289.03, + dydx1k9a76t62p6dv0wrkc4fg8vkrjtkv6ssfjg5l5e: 287.96, + dydx19rpqpy7hf3zs7z0c0urd7es0qk3rq8dlsplptl: 270.88, + dydx1ckjdu8xntrwhdeepmpp7r6l6uv3gduez0fgh6s: 270.47, + dydx177r48xzlzys8r3nz0kyelurul65xhw09p2cmp9: 267.89, + dydx130unqxxlzy96wf7jd5dzw0espgfg7c9ytmcw9a: 267.43, + dydx1mz3hmy6tw7d2kljgqxcyt5l7j43h4jnye5tksd: 263.95, + dydx17qvwdpdljavs8ksg89pdjuqf3nmzdu6x92wst9: 262.57, + dydx18zswqnds6ctwhm05vf2nfv3edw0kdkwf8u4fdh: 256.9, + dydx1k36d0u3s9670yuphwcaem9f3k8zkhxa50xhhz8: 256.15, + dydx1xr46hn6jgm3j6akwfgpmfzlpm50cnpptj8d6jr: 255.51, + dydx1erkqndnmndy5fmgny7wh0v4a72qy3q6vdhhvn0: 248.26, + dydx1s6e0aej3g4nax2gmw9dcd8tnpwse8tfvpp729k: 239.96, + dydx182glr4x8d72nxuq0nj8f68ucy206uhrtw02m0k: 229.32, + dydx19qg6vutphyqkrajks0rmxpapan5xkzkadg0jek: 226.7, + dydx1kvuc2mcjy49jv9tzfdt25alkzhxlwh83v9c5yv: 223.64, + dydx1sglhvdmt28t39u5lyn36v9qwpjfgqk8u8usyc2: 220.32, + dydx1cc4zg3genhyca4447l56g9w03gvjj7hm5rnpn4: 205.17, + dydx1uprnd4j0twwgfrqgcqnv0jeq60z28ar2c6dz5h: 203.73, + dydx1gjhj2y43kvk973vr2wwmlkg8w0f5vgvduxn9ra: 200.44, + dydx1dr2vkdjkxxv7k8quaece8eu8977x09hn0ahjmz: 196.64, + dydx139r5pzr0cjyw4826kcjv7cstf62zkc8w0hkzhd: 193.89, + dydx1f8w6v90nvppq9yl04ajt4522c46jvju2cwac00: 184.56, + dydx1urprj9t75a3tfhh3vyupz3vqccgqua74m800ze: 180.42, + dydx1xq7hh2yw8jpfmynhe4kdkdvjk37zftt4mwddsr: 177.89, + dydx1e9rdljrlqzjm9g42fe3h5pekgnx6xphgsrhnnt: 175.53, + dydx19cgkf9vdvmvdcg27d0zn7e9z2klttf0us6wp09: 167.98, + dydx1axv8uda4c8ryz9e5gyx7hc3vz4ptmr6gmn67jw: 167.87, + dydx1sxx63srcn4mjm56k44wqa3ataslm3zasv6m6nt: 164.0, + dydx14y778ruqy6v75pkw3cwatgyq5mlppwnd9sxl06: 161.63, + dydx1a3z36lmvaga6c2jc0p0gpk99f3w6vv5qu8a58f: 157.12, + dydx1c8m7ew5ezvsp9xswwr73gamd2tcnd8n9v6lpx9: 153.6, + dydx1zgkgpa6s4rj9q6ljqlpkav5e0lvmtp4xn5zvma: 149.63, + dydx10dfv4eh8vcln5rlgewfnd2xakq8d9q3qumw9h4: 149.45, + dydx10cw0va8wpdkfl64kjs56evqnczm9nkwhzszplf: 146.62, + dydx1h6npszpvkhpuljfa2lg2ay5lsehsdd4wa2ws52: 144.69, + dydx17zhdx920qly7483k2var03a8ljy8mwkqq4gmx4: 143.65, + dydx155yjamlk2csdudgz99s4th2d3jfqgwufaeky86: 143.38, + dydx1jnsey8ggez76uug9xvmmeutdjz7y2ass0ecf78: 143.38, + } as Record, +}; diff --git a/src/constants/compliance.ts b/src/constants/compliance.ts index 21caa439c5..642b15fc54 100644 --- a/src/constants/compliance.ts +++ b/src/constants/compliance.ts @@ -2,6 +2,5 @@ export enum ComplianceStates { FULL_ACCESS = 'FUll_ACCESS', READ_ONLY = 'READ_ONLY', CLOSE_ONLY = 'CLOSE_ONLY', + SPOT_ONLY = 'SPOT_ONLY', } - -export const CLOSE_ONLY_GRACE_PERIOD = 7; diff --git a/src/constants/statsig.ts b/src/constants/statsig.ts index 600c4e912c..c2d4e0d5a8 100644 --- a/src/constants/statsig.ts +++ b/src/constants/statsig.ts @@ -22,6 +22,7 @@ export enum StatsigFlags { abPopupDeposit = 'ab_popup_deposit', ffSpotBonk = 'ff_spot_bonk', ffBonkPnlLeaderboard = 'ff_bonk_pnl_leaderboard', + ffOnlyShowLiquidationRebates = 'ff_only_show_liquidation_rebates', } export enum CustomFlags { diff --git a/src/hooks/Onboarding/useOnboardingFlow.ts b/src/hooks/Onboarding/useOnboardingFlow.ts index 593315a163..1433ec20ca 100644 --- a/src/hooks/Onboarding/useOnboardingFlow.ts +++ b/src/hooks/Onboarding/useOnboardingFlow.ts @@ -9,7 +9,7 @@ import { forceOpenDialog } from '@/state/dialogs'; import { track } from '@/lib/analytics/analytics'; -import { usePerpetualsComplianceState } from '../usePerpetualsComplianceState'; +import { useComplianceState } from '../useComplianceState'; import { useAutoconnectMobileWalletBrowser } from './useAutoconnectMobileWalletBrowser'; const useOnboardingFlow = ({ onClick }: { onClick?: () => void } = {}) => { @@ -35,7 +35,7 @@ const useOnboardingFlow = ({ onClick }: { onClick?: () => void } = {}) => { } }; - const { disableConnectButton } = usePerpetualsComplianceState(); + const { disableConnectButton } = useComplianceState(); const onboardingState = useAppSelector(getOnboardingState); const isAccountViewOnly = useAppSelector(calculateIsAccountViewOnly); diff --git a/src/hooks/TradingForm/useTradeForm.ts b/src/hooks/TradingForm/useTradeForm.ts index 67ca1def43..0a7d7c195c 100644 --- a/src/hooks/TradingForm/useTradeForm.ts +++ b/src/hooks/TradingForm/useTradeForm.ts @@ -30,8 +30,8 @@ import { purgeBigNumbers } from '@/lib/purgeBigNumber'; import { useAccounts } from '../useAccounts'; import { ConnectionErrorType, useApiState } from '../useApiState'; +import { useComplianceState } from '../useComplianceState'; import { useOnOrderIndexed } from '../useOnOrderIndexed'; -import { usePerpetualsComplianceState } from '../usePerpetualsComplianceState'; import { useStringGetter } from '../useStringGetter'; import { useSubaccount } from '../useSubaccount'; @@ -67,7 +67,7 @@ export const useTradeForm = ({ const stringGetter = useStringGetter(); const { connectionError } = useApiState(); - const { complianceState } = usePerpetualsComplianceState(); + const { complianceState } = useComplianceState(); const { updateLeverage } = useSubaccount(); const { dydxAddress } = useAccounts(); @@ -104,6 +104,7 @@ export const useTradeForm = ({ const tradingUnavailable = closeOnlyTradingUnavailable || complianceState === ComplianceStates.READ_ONLY || + complianceState === ComplianceStates.SPOT_ONLY || connectionError === ConnectionErrorType.CHAIN_DISRUPTION; const shouldEnableTrade = diff --git a/src/hooks/rewards/hooks.ts b/src/hooks/rewards/hooks.ts index 0667dd1d3d..a7fd4dce01 100644 --- a/src/hooks/rewards/hooks.ts +++ b/src/hooks/rewards/hooks.ts @@ -145,3 +145,40 @@ export function useBonkPnlDistribution() { data: bonkPnlItems, }; } + +export type LiquidationLeaderboardItem = { + address: string; + total_liquidation_losses: string; + rank: number; +}; + +type LiquidationLeaderboardResponse = { + success: boolean; + data: LiquidationLeaderboardItem[]; + pagination?: { + total: number; + totalPages: number; + page: number; + perPage: number; + }; +}; + +async function getLiquidationLeaderboard() { + const res = await fetch( + `https://pp-external-api-ffb2ad95ef03.herokuapp.com/api/dydx-liquidation-leaderboard?perPage=1000` + ); + + const data = (await res.json()) as LiquidationLeaderboardResponse; + return data.data; +} + +export function useLiquidationLeaderboard() { + return useQuery({ + queryKey: ['dydx-liquidation-leaderboard'], + queryFn: wrapAndLogError( + () => getLiquidationLeaderboard(), + 'LaunchIncentives/fetchLiquidationLeaderboard', + true + ), + }); +} diff --git a/src/hooks/rewards/util.ts b/src/hooks/rewards/util.ts index 02eec961ee..ff2f9a0f25 100644 --- a/src/hooks/rewards/util.ts +++ b/src/hooks/rewards/util.ts @@ -44,6 +44,11 @@ export const CURRENT_SURGE_REWARDS_DETAILS = { endTime: '2026-01-31T23:59:59.000Z', // end of jan 2026 }; +export const LIQUIDATION_REBATES_DETAILS = { + rebateAmount: '$1M', + rebateAmountUsd: 1_000_000, +}; + export const CURRENT_BONK_REWARDS_DETAILS = { startTime: '2026-02-01T00:00:00.000Z', // start of february 2026 endTime: '2026-02-28T23:59:59.000Z', // end of february 2026 diff --git a/src/hooks/usePerpetualsComplianceState.tsx b/src/hooks/useComplianceState.tsx similarity index 67% rename from src/hooks/usePerpetualsComplianceState.tsx rename to src/hooks/useComplianceState.tsx index f4ee03993c..b7bb1dfb01 100644 --- a/src/hooks/usePerpetualsComplianceState.tsx +++ b/src/hooks/useComplianceState.tsx @@ -14,12 +14,13 @@ import { TermsOfUseLink } from '@/components/TermsOfUseLink'; import { getComplianceStatus, getGeo, getOnboardingState } from '@/state/accountSelectors'; import { useAppSelector } from '@/state/appTypes'; -import { useEnableSpot } from './useEnableSpot'; +import { isPresent } from '@/lib/typeUtils'; + import { useEnvFeatures } from './useEnvFeatures'; import { useStringGetter } from './useStringGetter'; import { useURLConfigs } from './useURLConfigs'; -export const usePerpetualsComplianceState = () => { +export const useComplianceState = () => { const stringGetter = useStringGetter(); const { help } = useURLConfigs(); const complianceStatus = useAppSelector(getComplianceStatus); @@ -27,9 +28,12 @@ export const usePerpetualsComplianceState = () => { const onboardingState = useAppSelector(getOnboardingState); const { checkForGeo } = useEnvFeatures(); const isSpotPage = useMatch(`${AppRoute.Spot}/*`) != null; - const isSpotEnabled = useEnableSpot(); const complianceState = useMemo(() => { + if (complianceStatus === ComplianceStatus.BLOCKED) { + return ComplianceStates.READ_ONLY; + } + if ( complianceStatus === ComplianceStatus.FIRST_STRIKE_CLOSE_ONLY || complianceStatus === ComplianceStatus.CLOSE_ONLY @@ -37,32 +41,17 @@ export const usePerpetualsComplianceState = () => { return ComplianceStates.CLOSE_ONLY; } - if (complianceStatus === ComplianceStatus.BLOCKED || (geo.currentlyGeoBlocked && checkForGeo)) { - return ComplianceStates.READ_ONLY; + if (geo.isPerpetualsGeoBlocked && checkForGeo) { + return ComplianceStates.SPOT_ONLY; } return ComplianceStates.FULL_ACCESS; }, [checkForGeo, complianceStatus, geo]); const complianceMessage = useMemo(() => { - let message; - - const firstStrikeStatuses = [ - ComplianceStatus.FIRST_STRIKE_CLOSE_ONLY, - ComplianceStatus.CLOSE_ONLY, - ]; - - const isGeoBlocked = geo.currentlyGeoBlocked && checkForGeo; - - if (firstStrikeStatuses.includes(complianceStatus) || isGeoBlocked) { - message = stringGetter({ - key: STRING_KEYS.PERPETUALS_UNAVAILABLE_MESSAGE, - params: { - TERMS_OF_USE_LINK: , - }, - }); - } else if (complianceStatus === ComplianceStatus.BLOCKED) { - message = stringGetter({ + // Applies to both perps & spot + if (complianceStatus === ComplianceStatus.BLOCKED) { + return stringGetter({ key: STRING_KEYS.PERMANENTLY_BLOCKED_MESSAGE_WITH_HELP, params: { HELP_LINK: ( @@ -74,21 +63,33 @@ export const usePerpetualsComplianceState = () => { }); } - return message; - }, [checkForGeo, complianceStatus, geo, help, stringGetter]); + // Rest of the states are not relevant to spot + if (isSpotPage) return null; + + if ( + complianceState === ComplianceStates.CLOSE_ONLY || + complianceState === ComplianceStates.SPOT_ONLY + ) { + return stringGetter({ + key: STRING_KEYS.PERPETUALS_UNAVAILABLE_MESSAGE, + params: { + TERMS_OF_USE_LINK: , + }, + }); + } + + return null; + }, [complianceState, complianceStatus, help, isSpotPage, stringGetter]); const disableConnectButton = complianceState === ComplianceStates.READ_ONLY && - onboardingState === OnboardingState.Disconnected && - !isSpotEnabled; + onboardingState === OnboardingState.Disconnected; return { complianceStatus, complianceState, complianceMessage, disableConnectButton, - showRestrictionWarning: complianceState === ComplianceStates.READ_ONLY && !isSpotPage, - showComplianceBanner: - (complianceMessage != null || complianceState === ComplianceStates.READ_ONLY) && !isSpotPage, + showComplianceBanner: isPresent(complianceMessage), }; }; diff --git a/src/hooks/useEnableLiquidationRebates.ts b/src/hooks/useEnableLiquidationRebates.ts new file mode 100644 index 0000000000..6565669423 --- /dev/null +++ b/src/hooks/useEnableLiquidationRebates.ts @@ -0,0 +1,10 @@ +import { StatsigFlags } from '@/constants/statsig'; + +import { useStatsigGateValue } from './useStatsig'; + +export const useEnableLiquidationRebates = () => { + const onlyShowLiquidationRebatesFF = useStatsigGateValue( + StatsigFlags.ffOnlyShowLiquidationRebates + ); + return onlyShowLiquidationRebatesFF; +}; diff --git a/src/hooks/useEndpointsConfig.ts b/src/hooks/useEndpointsConfig.ts index 2f724102f8..49f71b356c 100644 --- a/src/hooks/useEndpointsConfig.ts +++ b/src/hooks/useEndpointsConfig.ts @@ -19,6 +19,7 @@ export interface EndpointsConfig { affiliates?: string; spotApi: string; geoV2: string; + pnlImageApi: string; } export const useEndpointsConfig = () => { @@ -38,5 +39,6 @@ export const useEndpointsConfig = () => { affiliatesBaseUrl: endpointsConfig.affiliates, spotApi: endpointsConfig.spotApi, geoV2: endpointsConfig.geoV2, + pnlImageApi: endpointsConfig.pnlImageApi, }; }; diff --git a/src/hooks/useNotificationTypes.tsx b/src/hooks/useNotificationTypes.tsx index 0ffbcdec6a..97b839a5a6 100644 --- a/src/hooks/useNotificationTypes.tsx +++ b/src/hooks/useNotificationTypes.tsx @@ -8,6 +8,10 @@ import tw from 'twin.macro'; import { AMOUNT_RESERVED_FOR_GAS_USDC, AMOUNT_USDC_BEFORE_REBALANCE } from '@/constants/account'; import { CHAIN_INFO } from '@/constants/chains'; +import { + LOSS_REBATE_DETAILS_DECEMBER, + TRADING_LEAGUE_REWARDS_DETAILS_ROUND_2, +} from '@/constants/clc'; import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; import { @@ -72,7 +76,7 @@ import { } from '@/lib/enumToStringKeyHelpers'; import { BIG_NUMBERS, MaybeBigNumber, MustNumber } from '@/lib/numbers'; import { getAverageFillPrice } from '@/lib/orders'; -import { isPresent, orEmptyRecord } from '@/lib/typeUtils'; +import { isPresent, orEmptyObj, orEmptyRecord } from '@/lib/typeUtils'; import { DEC_2025_COMPETITION_DETAILS } from './rewards/util'; import { useAccounts } from './useAccounts'; @@ -611,6 +615,157 @@ export const notificationTypes: NotificationTypeConfig[] = [ }); } }, [decimalSeparator, dydxAddress, groupSeparator, selectedLocale, stringGetter, trigger]); + + const qualifiedForRound2 = useMemo(() => { + return ( + Date.now() < new Date(TRADING_LEAGUE_REWARDS_DETAILS_ROUND_2.claimDeadline).getTime() && + Date.now() > new Date(TRADING_LEAGUE_REWARDS_DETAILS_ROUND_2.claimStartTime).getTime() && + TRADING_LEAGUE_REWARDS_DETAILS_ROUND_2.estimatedWalletRewards[ + dydxAddress?.toLowerCase() ?? '' + ] != null + ); + }, [dydxAddress]); + + const tokenRewardPrice = useAppSelector(BonsaiCore.rewardParams.data).tokenPrice; + + useEffect(() => { + if (qualifiedForRound2 && dydxAddress != null && tokenRewardPrice != null) { + const estimatedUsdRewardAmount = + TRADING_LEAGUE_REWARDS_DETAILS_ROUND_2.estimatedWalletRewards[ + dydxAddress.toLowerCase() + ] ?? 0; + + const adjustedUsdRewardAmount = + (estimatedUsdRewardAmount / TRADING_LEAGUE_REWARDS_DETAILS_ROUND_2.assumedPrice) * + tokenRewardPrice; + + const formattedRewardAmount = formatNumberOutput( + adjustedUsdRewardAmount, + OutputType.Number, + { + decimalSeparator, + groupSeparator, + selectedLocale, + fractionDigits: USD_DECIMALS, + minimumFractionDigits: USD_DECIMALS, + } + ); + + trigger({ + id: `jan-2026-trading-league-rewards-round-2`, + displayData: { + icon: , + title: stringGetter({ + key: STRING_KEYS.TRADING_LEAGUE_REWARD_CLAIM_TITLE, + }), + body: stringGetter({ + key: STRING_KEYS.TRADING_LEAGUE_REWARD_CLAIM_BODY, + params: { + REWARD_AMOUNT: formattedRewardAmount, + CLAIM_DEADLINE: new Date( + TRADING_LEAGUE_REWARDS_DETAILS_ROUND_2.claimDeadline + ).toLocaleDateString(selectedLocale, { month: 'short', day: 'numeric' }), + LEARN_MORE_LINK: ( + + {stringGetter({ key: STRING_KEYS.HERE })} + + ), + }, + }), + toastSensitivity: 'foreground', + groupKey: NotificationType.RewardsProgramUpdates, + actionAltText: stringGetter({ key: STRING_KEYS.CLAIM }), + renderActionSlot: () => ( + + {stringGetter({ key: STRING_KEYS.CLAIM })} → + + ), + }, + updateKey: [`jan-2026-trading-league-rewards-round-2`, dydxAddress], + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + Boolean(tokenRewardPrice), + qualifiedForRound2, + dydxAddress, + stringGetter, + trigger, + decimalSeparator, + groupSeparator, + selectedLocale, + ]); + + const qualifyForDecLossRebate = useMemo(() => { + return ( + LOSS_REBATE_DETAILS_DECEMBER.estimatedWalletRebates[dydxAddress?.toLowerCase() ?? ''] != + null && + Date.now() < new Date(LOSS_REBATE_DETAILS_DECEMBER.claimDeadline).getTime() && + Date.now() > new Date(LOSS_REBATE_DETAILS_DECEMBER.claimStartTime).getTime() + ); + }, [dydxAddress]); + + useEffect(() => { + if (!qualifyForDecLossRebate) { + return; + } + + const usdRebateAmount = + LOSS_REBATE_DETAILS_DECEMBER.estimatedWalletRebates[dydxAddress?.toLowerCase() ?? ''] ?? + 0; + const formattedRebateAmount = formatNumberOutput(usdRebateAmount, OutputType.Fiat, { + decimalSeparator, + groupSeparator, + selectedLocale, + fractionDigits: USD_DECIMALS, + minimumFractionDigits: USD_DECIMALS, + }); + + trigger({ + id: `dec-2025-loss-rebate-claim`, + displayData: { + icon: , + title: stringGetter({ + key: STRING_KEYS.TRADING_LOSS_REBATE_CLAIM_TITLE, + }), + body: stringGetter({ + key: STRING_KEYS.TRADING_LOSS_REBATE_CLAIM_BODY, + params: { + REBATE_AMOUNT: formattedRebateAmount, + CLAIM_DEADLINE: new Date( + LOSS_REBATE_DETAILS_DECEMBER.claimDeadline + ).toLocaleDateString(selectedLocale, { month: 'short', day: 'numeric' }), + HERE_LINK: ( + + {stringGetter({ key: STRING_KEYS.HERE })} + + ), + }, + }), + toastSensitivity: 'foreground', + groupKey: NotificationType.RewardsProgramUpdates, + actionAltText: stringGetter({ key: STRING_KEYS.CLAIM }), + renderActionSlot: () => ( + + {stringGetter({ key: STRING_KEYS.CLAIM })} → + + ), + }, + updateKey: [`jan-2026-trading-league-rewards-round-2`, dydxAddress], + }); + }, [ + qualifyForDecLossRebate, + trigger, + stringGetter, + dydxAddress, + decimalSeparator, + groupSeparator, + selectedLocale, + ]); }, }, { @@ -1077,7 +1232,7 @@ export const notificationTypes: NotificationTypeConfig[] = [ const stringGetter = useStringGetter(); const isKeplr = useAppSelector(selectIsKeplrConnected); const reclaimableChildSubaccountFunds = useAppSelector(selectReclaimableChildSubaccountFunds); - const ordersToCancel = useAppSelector(selectOrphanedTriggerOrders); + const { ordersToCancel } = orEmptyObj(useAppSelector(selectOrphanedTriggerOrders)); const maybeRebalanceAction = useAppSelector(selectShouldAccountRebalanceUsdc); useEffect(() => { diff --git a/src/hooks/useSharePnlImage.ts b/src/hooks/useSharePnlImage.ts new file mode 100644 index 0000000000..82b78a901d --- /dev/null +++ b/src/hooks/useSharePnlImage.ts @@ -0,0 +1,118 @@ +import { logBonsaiError } from '@/bonsai/logs'; +import { useQuery } from '@tanstack/react-query'; + +import { timeUnits } from '@/constants/time'; +import { IndexerPerpetualPositionStatus, IndexerPositionSide } from '@/types/indexer/indexerApiGen'; + +import { useAccounts } from '@/hooks/useAccounts'; + +import { getOpenPositions } from '@/state/accountSelectors'; +import { useAppSelector } from '@/state/appTypes'; + +import { Nullable } from '@/lib/typeUtils'; +import { truncateAddress } from '@/lib/wallet'; + +import { useEndpointsConfig } from './useEndpointsConfig'; + +export type SharePnlImageParams = { + assetId: string; + marketId: string; + side: Nullable; + leverage: Nullable; + oraclePrice: Nullable; + entryPrice: Nullable; + unrealizedPnl: Nullable; + type?: 'open' | 'close' | 'liquidated' | undefined; +}; + +export const useSharePnlImage = ({ + assetId, + marketId, + side, + leverage, + oraclePrice, + entryPrice, + unrealizedPnl, + type = 'open', +}: SharePnlImageParams) => { + const { pnlImageApi } = useEndpointsConfig(); + const { dydxAddress } = useAccounts(); + const openPositions = useAppSelector(getOpenPositions); + + const position = openPositions?.find((p) => p.market === marketId); + + const positionType = + position?.status === IndexerPerpetualPositionStatus.CLOSED + ? 'close' + : position?.status === IndexerPerpetualPositionStatus.LIQUIDATED + ? 'liquidated' + : 'open'; + + const pnl = (position?.realizedPnl.toNumber() ?? 0) + (unrealizedPnl ?? 0); + + const queryFn = async (): Promise => { + if (!dydxAddress) { + return undefined; + } + + const requestBody = { + brand: 'bonk', + ticker: assetId, + type: positionType, + leverage: leverage ?? 0, + username: truncateAddress(dydxAddress), + isLong: side === IndexerPositionSide.LONG, + isCross: position?.marginMode === 'CROSS', + // Optional fields - include if available + size: position?.value.toNumber(), + pnl, + uPnl: unrealizedPnl ?? undefined, + pnlPercentage: position?.updatedUnrealizedPnlPercent?.toNumber(), + entryPx: entryPrice ?? undefined, + exitPx: position?.exitPrice?.toNumber(), + liquidationPx: position?.liquidationPrice?.toNumber(), + markPx: oraclePrice ?? undefined, + height: 1000, + }; + + const response = await fetch(pnlImageApi, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + logBonsaiError('useSharePnlImage', 'Failed to fetch share image', { response }); + throw new Error(`Failed to fetch share image: ${response.status}`); + } + + return response.blob(); + }; + + return useQuery({ + queryKey: [ + 'sharePnlImage', + marketId, + dydxAddress, + side, + leverage, + oraclePrice, + entryPrice, + unrealizedPnl, + type, + position?.marginMode, + position?.unsignedSize.toString(), + position?.liquidationPrice?.toString(), + ], + queryFn, + enabled: Boolean(dydxAddress), + refetchOnWindowFocus: false, + refetchOnReconnect: false, + staleTime: 2 * timeUnits.minute, // 2 minutes + retry: 2, + retryDelay: 1 * timeUnits.second, // 1 second + retryOnMount: true, + }); +}; diff --git a/src/hooks/useShouldShowTriggers.ts b/src/hooks/useShouldShowTriggers.ts index c666904d40..8465d16074 100644 --- a/src/hooks/useShouldShowTriggers.ts +++ b/src/hooks/useShouldShowTriggers.ts @@ -3,11 +3,11 @@ import { ComplianceStates } from '@/constants/compliance'; import { calculateShouldRenderTriggersInPositionsTable } from '@/state/accountCalculators'; import { useAppSelector } from '@/state/appTypes'; -import { usePerpetualsComplianceState } from './usePerpetualsComplianceState'; +import { useComplianceState } from './useComplianceState'; export const useShouldShowTriggers = () => { const shouldRenderTriggers = useAppSelector(calculateShouldRenderTriggersInPositionsTable); - const { complianceState } = usePerpetualsComplianceState(); + const { complianceState } = useComplianceState(); return shouldRenderTriggers && complianceState === ComplianceStates.FULL_ACCESS; }; diff --git a/src/hooks/useSpotForm.tsx b/src/hooks/useSpotForm.tsx index 4b076f6151..1061b45a52 100644 --- a/src/hooks/useSpotForm.tsx +++ b/src/hooks/useSpotForm.tsx @@ -4,6 +4,8 @@ import { SpotBuyInputType, SpotSellInputType, SpotSide } from '@/bonsai/forms/sp import { ErrorType, getHighestPriorityAlert } from '@/bonsai/lib/validationErrors'; import { BonsaiCore } from '@/bonsai/ontology'; +import { ComplianceStates } from '@/constants/compliance'; + import { useAccounts } from '@/hooks/useAccounts'; import { useCustomNotification } from '@/hooks/useCustomNotification'; import { useLocaleSeparators } from '@/hooks/useLocaleSeparators'; @@ -20,6 +22,8 @@ import { getSpotFormSummary } from '@/state/spotFormSelectors'; import { SpotApiSide } from '@/clients/spotApi'; +import { useComplianceState } from './useComplianceState'; + // TODO: spot localization export function useSpotForm() { @@ -31,6 +35,7 @@ export function useSpotForm() { const tokenMetadata = useAppSelector(BonsaiCore.spot.tokenMetadata.data); const { decimal: decimalSeparator, group: groupSeparator } = useLocaleSeparators(); const selectedLocale = useAppSelector(getSelectedLocale); + const { complianceState } = useComplianceState(); const hasErrors = useMemo( () => formSummary.errors.some((error) => error.type === ErrorType.error), @@ -43,8 +48,13 @@ export function useSpotForm() { ); const canSubmit = useMemo( - () => canDeriveSolanaWallet && !hasErrors && formSummary.summary.payload != null && !isPending, - [canDeriveSolanaWallet, formSummary.summary.payload, hasErrors, isPending] + () => + canDeriveSolanaWallet && + !hasErrors && + formSummary.summary.payload != null && + !isPending && + complianceState !== ComplianceStates.READ_ONLY, + [canDeriveSolanaWallet, complianceState, formSummary.summary.payload, hasErrors, isPending] ); const actions = useMemo( diff --git a/src/layout/Footer/FooterMobile.tsx b/src/layout/Footer/FooterMobile.tsx index 0e626dcc03..68cd8866b9 100644 --- a/src/layout/Footer/FooterMobile.tsx +++ b/src/layout/Footer/FooterMobile.tsx @@ -6,8 +6,8 @@ import { STRING_KEYS } from '@/constants/localization'; import { DEFAULT_MARKETID } from '@/constants/markets'; import { AppRoute } from '@/constants/routes'; +import { useComplianceState } from '@/hooks/useComplianceState'; import { useNotifications } from '@/hooks/useNotifications'; -import { usePerpetualsComplianceState } from '@/hooks/usePerpetualsComplianceState'; import { useShouldShowFooter } from '@/hooks/useShouldShowFooter'; import { useStringGetter } from '@/hooks/useStringGetter'; @@ -31,7 +31,7 @@ export const FooterMobile = () => { const marketId = useAppSelector(getCurrentMarketId); - const { disableConnectButton } = usePerpetualsComplianceState(); + const { disableConnectButton } = useComplianceState(); const { hasUnreadNotifications } = useNotifications(); if (!useShouldShowFooter()) return null; diff --git a/src/layout/Header/HeaderDesktop.tsx b/src/layout/Header/HeaderDesktop.tsx index f66cd219c1..307864ffce 100644 --- a/src/layout/Header/HeaderDesktop.tsx +++ b/src/layout/Header/HeaderDesktop.tsx @@ -9,8 +9,8 @@ import { STRING_KEYS } from '@/constants/localization'; import { AppRoute } from '@/constants/routes'; import { useAccounts } from '@/hooks/useAccounts'; +import { useComplianceState } from '@/hooks/useComplianceState'; import { useEnableSpot } from '@/hooks/useEnableSpot'; -import { usePerpetualsComplianceState } from '@/hooks/usePerpetualsComplianceState'; import { useStringGetter } from '@/hooks/useStringGetter'; import { useURLConfigs } from '@/hooks/useURLConfigs'; @@ -43,7 +43,7 @@ export const HeaderDesktop = () => { const dispatch = useAppDispatch(); const { dydxAccounts } = useAccounts(); const onboardingState = useAppSelector(getOnboardingState); - const { complianceState } = usePerpetualsComplianceState(); + const { complianceState } = useComplianceState(); const isSpotEnabled = useEnableSpot(); const currentTheme = useAppSelector(getAppTheme); @@ -153,7 +153,7 @@ export const HeaderDesktop = () => { <$NavAfter> {onboardingState === OnboardingState.AccountConnected && - complianceState === ComplianceStates.FULL_ACCESS && ( + complianceState !== ComplianceStates.READ_ONLY && ( <$DepositButton tw="mr-[0.5em]" shape={ButtonShape.Pill} diff --git a/src/lib/assetUtils.ts b/src/lib/assetUtils.ts index eff83b0972..d2b0765ae4 100644 --- a/src/lib/assetUtils.ts +++ b/src/lib/assetUtils.ts @@ -1,3 +1,4 @@ +import { ASSET_ICON_MAP } from '@/constants/assets'; import { DEFAULT_QUOTE_ASSET } from '@/constants/markets'; import { Nullable } from '@/lib/typeUtils'; @@ -81,3 +82,13 @@ export const getAssetDescriptionStringKeys = (assetId: string) => ({ primary: `APP.__ASSETS.${assetId}.PRIMARY`, secondary: `APP.__ASSETS.${assetId}.SECONDARY`, }); + +type AssetIconMapKey = keyof typeof ASSET_ICON_MAP; + +/** + * @param assetId + * @returns checks if the assetId is a valid key in the ASSET_ICON_MAP + */ +export const isAssetIconMapKey = (key: string): key is AssetIconMapKey => { + return key in ASSET_ICON_MAP; +}; diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index 720db1f42f..4be185cab6 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -11,8 +11,8 @@ import { AppRoute, HistoryRoute, PortfolioRoute } from '@/constants/routes'; import { ConnectorType, EvmAddress, WalletNetworkType, wallets } from '@/constants/wallets'; import { useAccounts } from '@/hooks/useAccounts'; +import { useComplianceState } from '@/hooks/useComplianceState'; import { useEnvConfig } from '@/hooks/useEnvConfig'; -import { usePerpetualsComplianceState } from '@/hooks/usePerpetualsComplianceState'; import { useStringGetter } from '@/hooks/useStringGetter'; import breakpoints from '@/styles/breakpoints'; @@ -59,7 +59,7 @@ const Profile = () => { const isConnected = onboardingState !== OnboardingState.Disconnected; const { sourceAccount, dydxAddress } = useAccounts(); - const { disableConnectButton } = usePerpetualsComplianceState(); + const { disableConnectButton } = useComplianceState(); const { data: ensName } = useEnsName({ address: diff --git a/src/pages/portfolio/AccountDetailsAndHistory.tsx b/src/pages/portfolio/AccountDetailsAndHistory.tsx index b0d40ec9d2..2c31b8be3a 100644 --- a/src/pages/portfolio/AccountDetailsAndHistory.tsx +++ b/src/pages/portfolio/AccountDetailsAndHistory.tsx @@ -10,7 +10,7 @@ import { STRING_KEYS } from '@/constants/localization'; import { NumberSign } from '@/constants/numbers'; import { usePortfolioValues } from '@/hooks/PortfolioValues/usePortfolioValues'; -import { usePerpetualsComplianceState } from '@/hooks/usePerpetualsComplianceState'; +import { useComplianceState } from '@/hooks/useComplianceState'; import { useStringGetter } from '@/hooks/useStringGetter'; import breakpoints from '@/styles/breakpoints'; @@ -31,7 +31,7 @@ import { orEmptyObj } from '@/lib/typeUtils'; export const AccountDetailsAndHistory = () => { const stringGetter = useStringGetter(); - const { complianceState } = usePerpetualsComplianceState(); + const { complianceState } = useComplianceState(); const selectedLocale = useAppSelector(getSelectedLocale); const onboardingState = useAppSelector(getOnboardingState); diff --git a/src/pages/portfolio/Portfolio.tsx b/src/pages/portfolio/Portfolio.tsx index 63c594bd19..723c802e6c 100644 --- a/src/pages/portfolio/Portfolio.tsx +++ b/src/pages/portfolio/Portfolio.tsx @@ -14,8 +14,8 @@ import { HistoryRoute, PortfolioRoute } from '@/constants/routes'; import { useAccountBalance } from '@/hooks/useAccountBalance'; import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useComplianceState } from '@/hooks/useComplianceState'; import { useDocumentTitle } from '@/hooks/useDocumentTitle'; -import { usePerpetualsComplianceState } from '@/hooks/usePerpetualsComplianceState'; import { useSimpleUiEnabled } from '@/hooks/useSimpleUiEnabled'; import { useStringGetter } from '@/hooks/useStringGetter'; @@ -63,7 +63,7 @@ const PortfolioPage = () => { const dispatch = useAppDispatch(); const stringGetter = useStringGetter(); const { isTablet, isNotTablet } = useBreakpoints(); - const { complianceState } = usePerpetualsComplianceState(); + const { complianceState } = useComplianceState(); const initialPageSize = 20; const isSimpleUi = useSimpleUiEnabled(); diff --git a/src/pages/token/CompetitionLeaderboardPanel.tsx b/src/pages/token/CompetitionLeaderboardPanel.tsx index 154e12869b..dfd5acdfc3 100644 --- a/src/pages/token/CompetitionLeaderboardPanel.tsx +++ b/src/pages/token/CompetitionLeaderboardPanel.tsx @@ -5,7 +5,6 @@ import styled from 'styled-components'; import { STRING_KEYS, StringGetterFunction } from '@/constants/localization'; import { IncentiveCompetitionItem, useClcPnlDistribution } from '@/hooks/rewards/hooks'; -import { CURRENT_SURGE_REWARDS_DETAILS } from '@/hooks/rewards/util'; import { useAccounts } from '@/hooks/useAccounts'; import { useStringGetter } from '@/hooks/useStringGetter'; @@ -18,6 +17,9 @@ import { Output, OutputType } from '@/components/Output'; import { Panel } from '@/components/Panel'; import { ColumnDef, Table } from '@/components/Table'; +import { useAppSelector } from '@/state/appTypes'; +import { getSelectedLocale } from '@/state/localizationSelectors'; + import { exportCSV } from '@/lib/csv'; import { truncateAddress } from '@/lib/wallet'; @@ -32,6 +34,7 @@ export const CompetitionLeaderboardPanel = () => { const stringGetter = useStringGetter(); const { data: topPnls, isLoading } = useClcPnlDistribution(); const { dydxAddress } = useAccounts(); + const selectedLocale = useAppSelector(getSelectedLocale); const getRowKey = useCallback((row: IncentiveCompetitionItem) => row.rank, []); @@ -67,7 +70,7 @@ export const CompetitionLeaderboardPanel = () => { })); exportCSV(csvRows, { - filename: `rewards-leaderboard-season-${CURRENT_SURGE_REWARDS_DETAILS.season}`, + filename: `loss-rebates-leaderboard-${new Date().toLocaleDateString(selectedLocale, { month: 'short', year: 'numeric' })}`, columnHeaders: [ { key: 'rank', diff --git a/src/pages/token/LiquidationRebatesHeader.tsx b/src/pages/token/LiquidationRebatesHeader.tsx new file mode 100644 index 0000000000..c8e270d8d9 --- /dev/null +++ b/src/pages/token/LiquidationRebatesHeader.tsx @@ -0,0 +1,118 @@ +import { useEffect, useMemo, useState } from 'react'; + +import { Duration } from 'luxon'; +import tw from 'twin.macro'; + +import { STRING_KEYS } from '@/constants/localization'; + +import { LIQUIDATION_REBATES_DETAILS } from '@/hooks/rewards/util'; +import { useNow } from '@/hooks/useNow'; +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { Icon, IconName } from '@/components/Icon'; +import { Link } from '@/components/Link'; +import { Panel } from '@/components/Panel'; +import { SuccessTag, TagSize } from '@/components/Tag'; + +export const LiquidationRebatesHeader = () => { + const stringGetter = useStringGetter(); + + // Calculate the last millisecond of the current UTC month + const now = new Date(); + const endOfCurrentMonth = (() => { + const date = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1, 0, 0, 0, 0)); // first ms of next month + date.setTime(date.getTime() - 1); // last ms of this month + return date.toISOString(); + })(); + + return ( + <$Panel> +
+
+
+
+
+ + {stringGetter({ + key: STRING_KEYS.LIQUIDATION_REBATES_HEADLINE, + params: { + REBATE_AMOUNT: LIQUIDATION_REBATES_DETAILS.rebateAmount, + }, + })} + +
+ + {stringGetter({ key: STRING_KEYS.ACTIVE })} + +
+
+

+ {stringGetter({ + key: STRING_KEYS.LIQUIDATION_REBATES_BODY, + })} +

+

+ {stringGetter({ + key: STRING_KEYS.LIQUIDATION_REBATES_SUB_BODY, + params: { + LOSS_REBATES_LINK: ( + + {stringGetter({ key: STRING_KEYS.LOSS_REBATES })} + + ), + CHECK_ELIGIBILITY_LINK: ( + + {stringGetter({ key: STRING_KEYS.HERE })} + + ), + }, + })} +

+
+
+
+ +
+
+ {stringGetter({ + key: STRING_KEYS.MONTH_COUNTDOWN, + })} + : +
+ {/* Countdown to end of current month */} + +
+
+
+
+ + ); +}; + +const MinutesCountdown = ({ endTime }: { endTime: string }) => { + const targetMs = Date.parse(endTime); + const now = useNow(); + const [msLeft, setMsLeft] = useState(Math.max(0, Math.floor(targetMs - Date.now()))); + + useEffect(() => { + if (now > targetMs) { + return; + } + + const newMsLeft = Math.max(0, Math.floor(targetMs - now)); + setMsLeft(newMsLeft); + }, [now, targetMs]); + + const formattedMsLeft = useMemo(() => { + return Duration.fromMillis(msLeft) + .shiftTo('days', 'hours', 'minutes', 'seconds') + .toFormat("d'd' h'h' m'm' s's'", { floor: true }); + }, [msLeft]); + + return
{formattedMsLeft}
; +}; + +const $Panel = tw(Panel)`bg-color-layer-3 w-full`; diff --git a/src/pages/token/LiquidationRebatesPanel.tsx b/src/pages/token/LiquidationRebatesPanel.tsx new file mode 100644 index 0000000000..2ca904273b --- /dev/null +++ b/src/pages/token/LiquidationRebatesPanel.tsx @@ -0,0 +1,265 @@ +import { useCallback } from 'react'; + +import styled from 'styled-components'; + +import { STRING_KEYS, StringGetterFunction } from '@/constants/localization'; + +import { useLiquidationLeaderboard } from '@/hooks/rewards/hooks'; +import { useAccounts } from '@/hooks/useAccounts'; +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { TrophyIcon } from '@/icons'; + +import { CopyButton } from '@/components/CopyButton'; +import { Icon, IconName } from '@/components/Icon'; +import { LoadingSpace } from '@/components/Loading/LoadingSpinner'; +import { Output, OutputType } from '@/components/Output'; +import { Panel } from '@/components/Panel'; +import { ColumnDef, Table } from '@/components/Table'; + +import { useAppSelector } from '@/state/appTypes'; +import { getSelectedLocale } from '@/state/localizationSelectors'; + +import { exportCSV } from '@/lib/csv'; +import { truncateAddress } from '@/lib/wallet'; + +export enum LiquidationLeaderboardTableColumns { + Rank = 'Rank', + Trader = 'Trader', + TotalLosses = 'TotalLosses', +} + +type LiquidationLeaderboardItem = { + rank: number; + account: string; + totalLosses: number; +}; + +export const LiquidationRebatesPanel = () => { + const stringGetter = useStringGetter(); + const { data: leaderboardData, isLoading } = useLiquidationLeaderboard(); + const { dydxAddress } = useAccounts(); + const selectedLocale = useAppSelector(getSelectedLocale); + + const getRowKey = useCallback((row: LiquidationLeaderboardItem) => row.rank, []); + + const columns = Object.values(LiquidationLeaderboardTableColumns).map( + (key: LiquidationLeaderboardTableColumns) => + getLiquidationLeaderboardTableColumnDef({ + key, + stringGetter, + dydxAddress, + }) + ); + + const data = (leaderboardData ?? []).map((entry) => ({ + rank: entry.rank, + account: entry.address, + totalLosses: parseFloat(entry.total_liquidation_losses), + })); + + const onDownload = () => { + if (data.length === 0) return; + + const csvRows = data.map((item) => ({ + rank: item.rank, + address: item.account, + totalLiquidationLosses: item.totalLosses, + })); + + exportCSV(csvRows, { + filename: `liquidation-leaderboard-${new Date().toLocaleDateString(selectedLocale, { month: 'short', year: 'numeric' })}`, + columnHeaders: [ + { + key: 'rank', + displayLabel: stringGetter({ key: STRING_KEYS.RANK }), + }, + { + key: 'address', + displayLabel: stringGetter({ key: STRING_KEYS.TRADER }), + }, + { + key: 'totalLiquidationLosses', + displayLabel: stringGetter({ key: STRING_KEYS.LIQUIDATION_LOSSES }), + }, + ], + }); + }; + + return ( + <$Panel> +
+
+
+ {/* {stringGetter({ key: STRING_KEYS.COMPETITION_LEADERBOARD_TITLE })} */} + Liquidation Rebates +
+ +
+ +
+ <$Table + label={stringGetter({ key: STRING_KEYS.LEADERBOARD })} + data={data} + tableId="trading-rewards" + getRowKey={getRowKey} + columns={columns} + defaultSortDescriptor={{ + column: LiquidationLeaderboardTableColumns.Rank, + direction: 'ascending', + }} + getIsRowPinned={(row) => { + return row.account === dydxAddress; + }} + slotEmpty={ + isLoading ? ( + + ) : ( +
+ + {stringGetter({ key: STRING_KEYS.TRADING_REWARD_TABLE_EMPTY_STATE })} +
+ ) + } + getRowAttributes={({ account }) => ({ + style: { + backgroundColor: account === dydxAddress ? 'var(--color-accent-faded)' : undefined, + }, + })} + selectionBehavior="replace" + withInnerBorders + initialPageSize={10} + withScrollSnapColumns + withScrollSnapRows + /> +
+
+ + ); +}; + +const $Panel = styled(Panel)` + --panel-content-paddingY: 1.5rem; + --panel-content-paddingX: 1.5rem; +`; + +const $Table = styled(Table)` + --tableCell-padding: 0.25rem; + font: var(--font-mini-book); + --stickyArea-background: transparent; + + table { + --stickyArea-background: transparent; + } + + thead, + tbody { + --stickyArea-background: transparent; + tr { + td:first-of-type, + th:first-of-type { + --tableCell-padding: 0.5rem 0.25rem 0.5rem 1rem; + } + td:last-of-type, + th:last-of-type { + --tableCell-padding: 0.5rem 1rem 0.5rem 0.25rem; + } + } + } + + tbody { + font: var(--font-small-book); + } + + tfoot { + --stickyArea-background: transparent; + --tableCell-padding: 0.5rem 1rem 0.5rem 1rem; + } + + min-width: 1px; + tbody { + font: var(--font-small-book); + } +` as typeof Table; + +const getLiquidationLeaderboardTableColumnDef = ({ + key, + stringGetter, + dydxAddress, +}: { + key: LiquidationLeaderboardTableColumns; + stringGetter: StringGetterFunction; + dydxAddress?: string; +}): ColumnDef => ({ + ...( + { + [LiquidationLeaderboardTableColumns.Rank]: { + columnKey: LiquidationLeaderboardTableColumns.Rank, + getCellValue: (row) => row.rank, + label: ( +
+ {stringGetter({ key: STRING_KEYS.RANK })} +
+ ), + renderCell: ({ rank, account }) => ( +
+
+
+ {rank} +
+
+ {rank === 1 && } + {rank === 2 && } + {rank === 3 && } + {account === dydxAddress && ( +
+ + {stringGetter({ key: STRING_KEYS.YOU })} + +
+ )} +
+ ), + }, + [LiquidationLeaderboardTableColumns.Trader]: { + columnKey: LiquidationLeaderboardTableColumns.Trader, + getCellValue: (row) => row.account, + label: ( +
+ {stringGetter({ key: STRING_KEYS.TRADER })} +
+ ), + renderCell: ({ account }) => ( + + {truncateAddress(account)} + + ), + }, + [LiquidationLeaderboardTableColumns.TotalLosses]: { + columnKey: LiquidationLeaderboardTableColumns.TotalLosses, + getCellValue: (row) => row.totalLosses, + label: ( +
Total Liquidation Losses
+ ), + renderCell: ({ totalLosses }) => ( + + ), + }, + } satisfies Record> + )[key], +}); diff --git a/src/pages/token/CompetitionIncentivesPanel.tsx b/src/pages/token/RebatesIncetivesPanel.tsx similarity index 92% rename from src/pages/token/CompetitionIncentivesPanel.tsx rename to src/pages/token/RebatesIncetivesPanel.tsx index 8bd0139dfc..4d89ca5080 100644 --- a/src/pages/token/CompetitionIncentivesPanel.tsx +++ b/src/pages/token/RebatesIncetivesPanel.tsx @@ -21,11 +21,11 @@ import { Panel } from '@/components/Panel'; import { SuccessTag, TagSize } from '@/components/Tag'; import { WithTooltip } from '@/components/WithTooltip'; -export const CompetitionIncentivesPanel = () => { - return ; +export const RebatesIncetivesPanel = () => { + return ; }; -const September2025RewardsPanel = () => { +const LossRebatesPanel = () => { const stringGetter = useStringGetter(); const now = new Date(); // December 2025 is the first month (Month 1) @@ -101,13 +101,13 @@ const September2025RewardsPanel = () => { - + ); }; -const Sept2025RewardsPanel = () => { +const EstimatedRewards = () => { const stringGetter = useStringGetter(); const { dydxAddress } = useAccounts(); const { data: topPnls, isLoading } = useClcPnlDistribution(); @@ -146,6 +146,18 @@ const Sept2025RewardsPanel = () => { reward-stars + +
+ {stringGetter({ key: STRING_KEYS.POWERED_BY_ALL_CAPS })}{' '} + + CLC + +
); }; diff --git a/src/pages/token/RewardsPage.tsx b/src/pages/token/RewardsPage.tsx index 5575034d7d..3ae07199d1 100644 --- a/src/pages/token/RewardsPage.tsx +++ b/src/pages/token/RewardsPage.tsx @@ -12,8 +12,9 @@ import { EMPTY_ARR } from '@/constants/objects'; import { AppRoute } from '@/constants/routes'; import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useComplianceState } from '@/hooks/useComplianceState'; import { useEnableBonkPnlLeaderboard } from '@/hooks/useEnableBonkPnlLeaderboard'; -import { usePerpetualsComplianceState } from '@/hooks/usePerpetualsComplianceState'; +import { useEnableLiquidationRebates } from '@/hooks/useEnableLiquidationRebates'; import { useStringGetter } from '@/hooks/useStringGetter'; import { useTokenConfigs } from '@/hooks/useTokenConfigs'; @@ -29,10 +30,12 @@ import { orEmptyObj } from '@/lib/typeUtils'; import { BonkIncentivesPanel } from './BonkIncentivesPanel'; import { BonkPnlPanel } from './BonkPnlPanel'; -import { CompetitionIncentivesPanel } from './CompetitionIncentivesPanel'; import { CompetitionLeaderboardPanel } from './CompetitionLeaderboardPanel'; import { GeoblockedPanel } from './GeoblockedPanel'; import { LaunchIncentivesPanel } from './LaunchIncentivesPanel'; +import { LiquidationRebatesHeader } from './LiquidationRebatesHeader'; +import { LiquidationRebatesPanel } from './LiquidationRebatesPanel'; +import { RebatesIncetivesPanel } from './RebatesIncetivesPanel'; import { RewardsHelpPanel } from './RewardsHelpPanel'; import { StakingRewardPanel } from './StakingRewardPanel'; import { SwapAndStakingPanel } from './SwapAndStakingPanel'; @@ -41,6 +44,7 @@ import { UnbondingPanels } from './UnbondingPanels'; enum Tab { BonkPnl = 'BonkPnl', Rewards = 'Rewards', + LiquidationRebates = 'LiquidationRebates', Competition = 'Competition', } @@ -49,12 +53,19 @@ const RewardsPage = () => { const navigate = useNavigate(); const enableBonkPnlLeaderboard = useEnableBonkPnlLeaderboard(); - const { complianceState } = usePerpetualsComplianceState(); + const { complianceState } = useComplianceState(); const { isTablet } = useBreakpoints(); + const enableLiquidationRebates = useEnableLiquidationRebates(); const { usdcDenom } = useTokenConfigs(); - const [value, setValue] = useState(enableBonkPnlLeaderboard ? Tab.BonkPnl : Tab.Competition); + const [value, setValue] = useState( + enableBonkPnlLeaderboard + ? Tab.BonkPnl + : enableLiquidationRebates + ? Tab.LiquidationRebates + : Tab.Competition + ); const { totalRewards } = orEmptyObj(BonsaiHooks.useStakingRewards().data); @@ -93,16 +104,31 @@ const RewardsPage = () => { }, ] : []), - { - content: ( -
- - -
- ), - label: stringGetter({ key: STRING_KEYS.REBATES }), - value: Tab.Competition, - }, + ...(enableLiquidationRebates + ? [ + { + content: ( +
+ + +
+ ), + label: stringGetter({ key: STRING_KEYS.LIQUIDATION_REBATES }), + value: Tab.LiquidationRebates, + }, + ] + : [ + { + content: ( +
+ + +
+ ), + label: stringGetter({ key: STRING_KEYS.REBATES }), + value: Tab.Competition, + }, + ]), ]; return ( diff --git a/src/pages/token/Stake.tsx b/src/pages/token/Stake.tsx index 9602c29eb3..3336992c1c 100644 --- a/src/pages/token/Stake.tsx +++ b/src/pages/token/Stake.tsx @@ -7,7 +7,7 @@ import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; import { useAccountBalance } from '@/hooks/useAccountBalance'; -import { usePerpetualsComplianceState } from '@/hooks/usePerpetualsComplianceState'; +import { useComplianceState } from '@/hooks/useComplianceState'; import { useStakingAPR } from '@/hooks/useStakingAPR'; import { useStringGetter } from '@/hooks/useStringGetter'; import { useTokenConfigs } from '@/hooks/useTokenConfigs'; @@ -33,7 +33,7 @@ export const Stake = () => { const canAccountTrade = useAppSelector(calculateCanAccountTrade); const stakingApr = useStakingAPR(); - const { complianceState } = usePerpetualsComplianceState(); + const { complianceState } = useComplianceState(); const { nativeTokenBalance, nativeStakingBalance } = useAccountBalance(); const { chainTokenLabel } = useTokenConfigs(); const { protocolStaking } = useURLConfigs(); diff --git a/src/pages/trade/simple-ui/AssetPage.tsx b/src/pages/trade/simple-ui/AssetPage.tsx index f1dad90077..bfa9ddb2ab 100644 --- a/src/pages/trade/simple-ui/AssetPage.tsx +++ b/src/pages/trade/simple-ui/AssetPage.tsx @@ -6,8 +6,8 @@ import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; import { DEFAULT_VAULT_DEPOSIT_FOR_LAUNCH } from '@/constants/numbers'; +import { useComplianceState } from '@/hooks/useComplianceState'; import { useCurrentMarketId } from '@/hooks/useCurrentMarketId'; -import { usePerpetualsComplianceState } from '@/hooks/usePerpetualsComplianceState'; import { useStringGetter } from '@/hooks/useStringGetter'; import { Button } from '@/components/Button'; @@ -35,7 +35,7 @@ const AssetPage = () => { const currentMarketId = useAppSelector(getCurrentMarketId); const { isViewingUnlaunchedMarket } = useCurrentMarketId(); const canAccountTrade = useAppSelector(calculateCanAccountTrade); - const { complianceState } = usePerpetualsComplianceState(); + const { complianceState } = useComplianceState(); if (currentMarketId == null) { return ; diff --git a/src/state/accountSelectors.ts b/src/state/accountSelectors.ts index a78f163688..5e556ea6cd 100644 --- a/src/state/accountSelectors.ts +++ b/src/state/accountSelectors.ts @@ -484,11 +484,12 @@ export const selectOrphanedTriggerOrders = createAppSelector( return undefined; } + const groupedPositions = keyBy(openPositions, (o) => o.uniqueId); + const ordersToCancel = calc(() => { if (ordersLoading !== 'success' || positionsLoading !== 'success') { return []; } - const groupedPositions = keyBy(openPositions, (o) => o.uniqueId); const filteredOrders = openOrders.filter((o) => { const isConditionalOrder = o.orderFlags === OrderFlags.CONDITIONAL; @@ -511,7 +512,7 @@ export const selectOrphanedTriggerOrders = createAppSelector( return cancelOrders; }); - return ordersToCancel; + return { ordersToCancel, groupedPositions }; } ); diff --git a/src/state/raw.ts b/src/state/raw.ts index d93d8ca118..6eae88db6e 100644 --- a/src/state/raw.ts +++ b/src/state/raw.ts @@ -81,6 +81,7 @@ export type ComplianceState = { geo: Loadable; sourceAddressScreenV2: Loadable; localAddressScreenV2: Loadable; + solanaAddressScreen: Loadable; }; export interface RawDataState { @@ -161,6 +162,7 @@ const initialState: RawDataState = { geo: loadableIdle(), localAddressScreenV2: loadableIdle(), sourceAddressScreenV2: loadableIdle(), + solanaAddressScreen: loadableIdle(), }, rewards: { data: loadableIdle(), @@ -249,13 +251,16 @@ export const rawSlice = createSlice({ }, setComplianceGeoHeadersRaw: ( state, - action: PayloadAction | undefined>> + action: PayloadAction< + Loadable | undefined> & { force?: boolean } + > ) => { const now = Date.now(); const lastUpdated = state.compliance.geoHeaders.data?.lastUpdated; - // Update if no data exists or if it's been more than an hour since last update + // Update if forced, no data exists, or if it's been more than an hour since last update const shouldUpdate = + action.payload.force === true || !state.compliance.geoHeaders.data || !lastUpdated || now - new Date(lastUpdated).getTime() > timeUnits.hour; @@ -284,6 +289,12 @@ export const rawSlice = createSlice({ ) => { state.compliance.sourceAddressScreenV2 = action.payload; }, + setSolanaAddressScreenRaw: ( + state, + action: PayloadAction> + ) => { + state.compliance.solanaAddressScreen = action.payload; + }, setRewardsParams: (state, action: PayloadAction>) => { state.rewards.data = action.payload; }, @@ -422,6 +433,7 @@ export const { setComplianceGeoHeadersRaw, setLocalAddressScreenV2Raw, setSourceAddressScreenV2Raw, + setSolanaAddressScreenRaw, setRewardsParams, setRewardsTokenPrice, setSelectedMarketLeverage, diff --git a/src/views/AccountInfo/AccountInfoSection.tsx b/src/views/AccountInfo/AccountInfoSection.tsx index 011b082171..a4271f60fa 100644 --- a/src/views/AccountInfo/AccountInfoSection.tsx +++ b/src/views/AccountInfo/AccountInfoSection.tsx @@ -9,7 +9,7 @@ import { STRING_KEYS } from '@/constants/localization'; import { useAccounts } from '@/hooks/useAccounts'; import { useBreakpoints } from '@/hooks/useBreakpoints'; -import { usePerpetualsComplianceState } from '@/hooks/usePerpetualsComplianceState'; +import { useComplianceState } from '@/hooks/useComplianceState'; import { useStringGetter } from '@/hooks/useStringGetter'; import breakpoints from '@/styles/breakpoints'; @@ -50,7 +50,7 @@ export const AccountInfoSection = () => { const dispatch = useAppDispatch(); const { isTablet } = useBreakpoints(); - const { complianceState } = usePerpetualsComplianceState(); + const { complianceState } = useComplianceState(); const { dydxAccounts } = useAccounts(); const subAccount = orEmptyObj(useAppSelector(BonsaiCore.account.parentSubaccountSummary.data)); diff --git a/src/views/TradeFormMessages/TradeFormMessages.tsx b/src/views/TradeFormMessages/TradeFormMessages.tsx index c31fbb2679..7d783f1720 100644 --- a/src/views/TradeFormMessages/TradeFormMessages.tsx +++ b/src/views/TradeFormMessages/TradeFormMessages.tsx @@ -6,7 +6,7 @@ import styled from 'styled-components'; import { AlertType } from '@/constants/alerts'; import { ButtonAction, ButtonShape, ButtonSize } from '@/constants/buttons'; -import { usePerpetualsComplianceState } from '@/hooks/usePerpetualsComplianceState'; +import { useComplianceState } from '@/hooks/useComplianceState'; import { AlertMessage } from '@/components/AlertMessage'; import { IconName } from '@/components/Icon'; @@ -29,7 +29,7 @@ export const TradeFormMessages = ({ shouldPromptUserToPlaceLimitOrder: boolean; }) => { const dispatch = useAppDispatch(); - const { complianceMessage, complianceStatus } = usePerpetualsComplianceState(); + const { complianceMessage, complianceStatus } = useComplianceState(); return ( <> diff --git a/src/views/dialogs/CancelOrphanedTriggerOrdersDialog.tsx b/src/views/dialogs/CancelOrphanedTriggerOrdersDialog.tsx index b2e2940b84..6e5d355344 100644 --- a/src/views/dialogs/CancelOrphanedTriggerOrdersDialog.tsx +++ b/src/views/dialogs/CancelOrphanedTriggerOrdersDialog.tsx @@ -17,11 +17,13 @@ import { Dialog } from '@/components/Dialog'; import { selectOrphanedTriggerOrders } from '@/state/accountSelectors'; import { useAppSelector } from '@/state/appTypes'; +import { orEmptyObj } from '@/lib/typeUtils'; + export const CancelOrphanedTriggerOrdersDialog = ({ setIsOpen, }: DialogProps) => { const stringGetter = useStringGetter(); - const ordersToCancel = useAppSelector(selectOrphanedTriggerOrders); + const { ordersToCancel } = orEmptyObj(useAppSelector(selectOrphanedTriggerOrders)); const [isLoading, setIsLoading] = useState(false); const markets: Record = useMemo(() => { diff --git a/src/views/dialogs/CoinbaseDepositDialog.tsx b/src/views/dialogs/CoinbaseDepositDialog.tsx index 46cd70a079..998e136489 100644 --- a/src/views/dialogs/CoinbaseDepositDialog.tsx +++ b/src/views/dialogs/CoinbaseDepositDialog.tsx @@ -1,13 +1,15 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useLayoutEffect, useState } from 'react'; import styled from 'styled-components'; import { AnalyticsEvents } from '@/constants/analytics'; import { ButtonAction, ButtonType } from '@/constants/buttons'; +import { ComplianceStates } from '@/constants/compliance'; import { CoinbaseDepositDialogProps, DialogProps } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; import { useAccounts } from '@/hooks/useAccounts'; +import { useComplianceState } from '@/hooks/useComplianceState'; import { useEnableSpot } from '@/hooks/useEnableSpot'; import { useStringGetter } from '@/hooks/useStringGetter'; @@ -33,6 +35,7 @@ export const CoinbaseDepositDialog = ({ const [selectedTab, setSelectedTab] = useState<'perps' | 'spot'>('perps'); const { nobleAddress, solanaAddress } = useAccounts(); const isSpotEnabled = useEnableSpot(); + const { complianceState } = useComplianceState(); useEffect(() => { if (selectedTab === 'spot') { @@ -40,6 +43,14 @@ export const CoinbaseDepositDialog = ({ } }, [selectedTab]); + useLayoutEffect(() => { + if (complianceState === ComplianceStates.READ_ONLY) { + setIsOpen(false); + } else if (complianceState !== ComplianceStates.FULL_ACCESS) { + setSelectedTab('spot'); + } + }, [complianceState, setIsOpen]); + const onCopy = () => { if (!nobleAddress) return; @@ -88,6 +99,7 @@ export const CoinbaseDepositDialog = ({ value: 'perps', label: stringGetter({ key: STRING_KEYS.PERPETUALS }), content: perpetualsContent, + disabled: complianceState !== ComplianceStates.FULL_ACCESS, }, { value: 'spot', diff --git a/src/views/dialogs/ComplianceConfigDialog.tsx b/src/views/dialogs/ComplianceConfigDialog.tsx index b9dea38216..bbe287cb12 100644 --- a/src/views/dialogs/ComplianceConfigDialog.tsx +++ b/src/views/dialogs/ComplianceConfigDialog.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { loadableLoaded } from '@/bonsai/lib/loadable'; -import { ComplianceResponse, ComplianceStatus, GeoState } from '@/bonsai/types/summaryTypes'; +import { ComplianceResponse, ComplianceStatus } from '@/bonsai/types/summaryTypes'; import styled from 'styled-components'; import { ButtonAction } from '@/constants/buttons'; @@ -17,7 +17,7 @@ import { Switch } from '@/components/Switch'; import { getComplianceStatus, getGeo } from '@/state/accountSelectors'; import { useAppDispatch, useAppSelector } from '@/state/appTypes'; -import { setComplianceGeoRaw, setLocalAddressScreenV2Raw } from '@/state/raw'; +import { setComplianceGeoHeadersRaw, setLocalAddressScreenV2Raw } from '@/state/raw'; const complianceStatusOptions = [ { status: ComplianceStatus.COMPLIANT, label: 'Compliant' }, @@ -29,14 +29,12 @@ const complianceStatusOptions = [ const setCompliance = (payload: ComplianceResponse) => setLocalAddressScreenV2Raw(loadableLoaded(payload)); -const setGeo = (payload: GeoState) => setComplianceGeoRaw(loadableLoaded(payload)); const usePreferenceMenu = () => { const dispatch = useAppDispatch(); - const complianceStatus = useAppSelector(getComplianceStatus); const geo = useAppSelector(getGeo); - const geoRestricted = geo.currentlyGeoBlocked; + const { isPerpetualsGeoBlocked } = geo; const notificationSection = useMemo( (): MenuGroup => ({ group: 'status', @@ -63,39 +61,30 @@ const usePreferenceMenu = () => { items: [ { value: 'RestrictGeo', - label: 'Simulate Restricted Geo', + label: 'Simulate Restricted Perps Geo', slotAfter: ( - null} /> + null} + /> ), onSelect: () => { dispatch( - geoRestricted - ? setGeo({ - country: 'JP', - region: 'Tokyo', - regionCode: '13', - city: 'Tokyo', - timezone: 'Asia/Tokyo', - ll: [35.6762, 139.6503], - blocked: false, - whitelisted: false, - }) - : setGeo({ - country: 'US', - region: 'California', - regionCode: 'CA', - city: 'Los Angeles', - timezone: 'America/Los_Angeles', - ll: [34.0522, -118.2437], - blocked: true, - whitelisted: false, - }) + setComplianceGeoHeadersRaw({ + ...loadableLoaded( + isPerpetualsGeoBlocked + ? { status: undefined, country: 'JP', region: 'Tokyo' } + : { status: 'restricted', country: 'US', region: 'California' } + ), + force: true, + }) ); }, }, ], }), - [dispatch, geoRestricted] + [dispatch, isPerpetualsGeoBlocked] ); return [otherSection, notificationSection]; @@ -108,16 +97,17 @@ export const ComplianceConfigDialog = ({ setIsOpen }: DialogProps { + const submit = () => { const endpoint = `${compositeClient?.indexerClient.config.restEndpoint}/v4/compliance/setStatus`; if (dydxAddress) { - await fetch(endpoint, { + fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ address: dydxAddress, status: complianceStatus }), }); + setIsOpen(false); } }; diff --git a/src/views/dialogs/SharePNLAnalyticsDialog.tsx b/src/views/dialogs/SharePNLAnalyticsDialog.tsx index 94ae1f72eb..9a2e4410dd 100644 --- a/src/views/dialogs/SharePNLAnalyticsDialog.tsx +++ b/src/views/dialogs/SharePNLAnalyticsDialog.tsx @@ -1,38 +1,29 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; -import { BonsaiHelpers } from '@/bonsai/ontology'; -import { useToBlob } from '@hugocxl/react-to-image'; +import { logBonsaiError } from '@/bonsai/logs'; import styled from 'styled-components'; import tw from 'twin.macro'; import { AnalyticsEvents } from '@/constants/analytics'; -import { ASSET_ICON_MAP } from '@/constants/assets'; import { ButtonAction } from '@/constants/buttons'; import { DialogProps, SharePNLAnalyticsDialogProps } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; -import { IndexerPositionSide } from '@/types/indexer/indexerApiGen'; -import { useCustomNotification } from '@/hooks/useCustomNotification'; -import { useAppSelectorWithArgs } from '@/hooks/useParameterizedSelector'; +import { useSharePnlImage } from '@/hooks/useSharePnlImage'; import { useStringGetter } from '@/hooks/useStringGetter'; -import { LogoShortIcon } from '@/icons/logo-short'; import { layoutMixins } from '@/styles/layoutMixins'; -import { AssetIcon } from '@/components/AssetIcon'; import { Button } from '@/components/Button'; import { Dialog } from '@/components/Dialog'; import { Icon, IconName } from '@/components/Icon'; -import { Output, OutputType, ShowSign } from '@/components/Output'; -import { QrCode } from '@/components/QrCode'; -import { Tag, TagSign } from '@/components/Tag'; +import { LoadingSpinner } from '@/components/Loading/LoadingSpinner'; import { useAppDispatch } from '@/state/appTypes'; import { closeDialog } from '@/state/dialogs'; import { track } from '@/lib/analytics/analytics'; import { getDisplayableAssetFromBaseAsset } from '@/lib/assetUtils'; -import { MustBigNumber } from '@/lib/numbers'; import { triggerTwitterIntent } from '@/lib/twitter'; const copyBlobToClipboard = async (blob: Blob | null) => { @@ -54,7 +45,6 @@ export const SharePNLAnalyticsDialog = ({ marketId, assetId, side, - sideLabel, leverage, oraclePrice, entryPrice, @@ -63,35 +53,44 @@ export const SharePNLAnalyticsDialog = ({ }: DialogProps) => { const stringGetter = useStringGetter(); const dispatch = useAppDispatch(); - const logoUrl = useAppSelectorWithArgs(BonsaiHelpers.assets.selectAssetLogo, assetId); const symbol = getDisplayableAssetFromBaseAsset(assetId); - const notify = useCustomNotification(); + const [isCopying, setIsCopying] = useState(false); + const [isSharing, setIsSharing] = useState(false); const [isCopied, setIsCopied] = useState(false); - const [{ isLoading: isCopying }, convert, ref] = useToBlob({ - quality: 1.0, - onSuccess: async (blob) => { - await copyBlobToClipboard(blob); - setIsCopied(true); - setTimeout(() => setIsCopied(false), 2000); - }, - onError: (error) => { - // eslint-disable-next-line no-console - console.error('Failed to copy blob. ', error); - notify({ - title: stringGetter({ key: STRING_KEYS.ERROR }), - body: stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG }), - slotTitleLeft: , - toastDuration: 5000, - }); - }, + const getPnlImage = useSharePnlImage({ + assetId, + marketId, + side, + leverage, + oraclePrice, + entryPrice, + unrealizedPnl, }); - const [{ isLoading: isSharing }, convertShare, refShare] = useToBlob({ - quality: 1.0, - onSuccess: async (blob) => { - await copyBlobToClipboard(blob); + const pnlImage = useMemo(() => getPnlImage.data ?? undefined, [getPnlImage.data]); + + const copyPnlImage = async () => { + if (isCopying || isCopied || !pnlImage) return; + setIsCopying(true); + try { + await copyBlobToClipboard(pnlImage); + track(AnalyticsEvents.SharePnlCopied({ asset: assetId })); + setIsCopying(false); + setIsCopied(true); + setTimeout(() => setIsCopied(false), 2000); + } catch (error) { + logBonsaiError('SharePNLAnalyticsDialog/copyPnlImage', 'Failed to copy PNL image', { error }); + } finally { + setIsCopying(false); + } + }; + const sharePnlImage = async () => { + if (isSharing || !pnlImage) return; + setIsSharing(true); + try { + await copyBlobToClipboard(pnlImage); triggerTwitterIntent({ text: `${stringGetter({ key: STRING_KEYS.TWEET_MARKET_POSITION, @@ -101,204 +100,67 @@ export const SharePNLAnalyticsDialog = ({ })}\n\n#bonk_trade #${symbol}\n[${stringGetter({ key: STRING_KEYS.TWEET_PASTE_IMAGE_AND_DELETE_THIS })}]`, related: 'bonk_inu', }); - - dispatch(closeDialog()); - }, - }); - - const sideSign = useMemo(() => { - switch (side) { - case IndexerPositionSide.LONG: - return TagSign.Positive; - case IndexerPositionSide.SHORT: - return TagSign.Negative; - default: - return TagSign.Neutral; - } - }, [side]); - - const unrealizedPnlIsNegative = MustBigNumber(unrealizedPnl).isNegative(); - - const [assetLeft, assetRight] = marketId.split('-'); - - const [logoBase64, setLogoBase64] = useState(null); - - const localLogoUrl = useMemo(() => { - if (assetId && Object.prototype.hasOwnProperty.call(ASSET_ICON_MAP, assetId)) { - return ASSET_ICON_MAP[assetId as keyof typeof ASSET_ICON_MAP]; + track(AnalyticsEvents.SharePnlShared({ asset: assetId })); + setIsSharing(false); + } catch (error) { + logBonsaiError('SharePNLAnalyticsDialog/sharePnlImage', 'Failed to share PNL image', { + error, + }); + } finally { + setIsSharing(false); } - return logoUrl; - }, [logoUrl, assetId]); - - useEffect(() => { - if (!logoUrl) return; - const img = new Image(); - img.crossOrigin = 'anonymous'; - img.src = logoUrl; - img.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = img.width || 26; - canvas.height = img.height || 26; - const ctx = canvas.getContext('2d'); - ctx?.drawImage(img, 0, 0, canvas.width, canvas.height); - setLogoBase64(canvas.toDataURL('image/png')); - }; - img.onerror = () => { - // eslint-disable-next-line no-console - console.error('Failed to load asset image. ', logoUrl); - setLogoBase64(null); - }; - }, [logoUrl]); + dispatch(closeDialog()); + }; return ( - <$ShareableCard - ref={(domNode) => { - if (domNode) { - ref(domNode); - refShare(domNode); - } - }} - > -
-
- - - - {assetLeft}/{assetRight} - - - {sideLabel} + <$ShareableCard> + {!pnlImage ? ( +
+
- - <$HighlightOutput - isNegative={unrealizedPnlIsNegative} - type={OutputType.CompactFiat} - value={unrealizedPnl} - showSign={ShowSign.Both} + ) : ( + Shareable PNL Card -
- -
-
- -
-
- <$ShareableCardStatLabel> - {stringGetter({ key: STRING_KEYS.ENTRY })} - - <$ShareableCardStatOutput type={OutputType.Fiat} value={entryPrice} withSubscript /> - - <$ShareableCardStatLabel> - {stringGetter({ key: STRING_KEYS.INDEX })} - - <$ShareableCardStatOutput type={OutputType.Fiat} value={oraclePrice} withSubscript /> - - <$ShareableCardStatLabel> - {stringGetter({ key: STRING_KEYS.LEVERAGE })} - - - <$ShareableCardStatOutput - type={OutputType.Multiple} - value={leverage} - showSign={ShowSign.None} - /> -
- -
- {import.meta.env.VITE_SHARE_PNL_ANALYTICS_URL ? ( - <$QrCode - tw="rounded-0.25 bg-color-layer-3" - size={68} - value={import.meta.env.VITE_SHARE_PNL_ANALYTICS_URL} - options={{ - cells: { - fill: 'var(--color-text-2)', - }, - finder: { - fill: 'var(--color-text-2)', - }, - }} - /> - ) : ( -
- )} -
+ )} + +
+ <$Action + action={ButtonAction.Secondary} + slotLeft={} + onClick={copyPnlImage} + state={{ + isLoading: isCopying, + }} + > + {stringGetter({ key: isCopied ? STRING_KEYS.COPIED : STRING_KEYS.COPY })} + + <$Action + action={ButtonAction.Primary} + slotLeft={} + onClick={sharePnlImage} + state={{ + isLoading: isSharing, + }} + > + {stringGetter({ key: STRING_KEYS.SHARE })} +
- -
- <$Action - action={ButtonAction.Secondary} - slotLeft={} - onClick={() => { - track(AnalyticsEvents.SharePnlCopied({ asset: assetId })); - convert(); - }} - state={{ - isLoading: isCopying, - }} - > - {stringGetter({ key: isCopied ? STRING_KEYS.COPIED : STRING_KEYS.COPY })} - - <$Action - action={ButtonAction.Primary} - slotLeft={} - onClick={() => { - track(AnalyticsEvents.SharePnlShared({ asset: assetId })); - convertShare(); - }} - state={{ - isLoading: isSharing, - }} - > - {stringGetter({ key: STRING_KEYS.SHARE })} - -
); }; + const $Action = tw(Button)`flex-1`; const $ShareableCard = styled.div` - ${layoutMixins.row} + ${layoutMixins.column} gap: 0.5rem; justify-content: space-between; align-items: flex-start; - margin-bottom: 1.25rem; - background-color: var(--color-layer-4); - padding: 1.75rem 1.25rem 1.25rem 1.25rem; border-radius: 0.5rem; `; -const $ShareableCardStatLabel = tw.div`text-right text-color-text-0 font-base-bold`; - -const $ShareableCardStatOutput = tw(Output)`font-base-bold text-color-text-2`; -const $QrCode = styled(QrCode)` - width: 5.25rem; - height: 5.25rem; - margin-top: 1rem; - margin-left: auto; - - svg { - border: none; - } -`; - -const $HighlightOutput = styled(Output)<{ isNegative?: boolean }>` - font-size: 2.25rem; - font-weight: var(--fontWeight-bold); - - color: var(--output-sign-color); - --secondary-item-color: currentColor; - --output-sign-color: ${({ isNegative }) => - isNegative !== undefined - ? isNegative - ? `var(--color-negative)` - : `var(--color-positive)` - : `var(--color-text-1)`}; -`; diff --git a/src/views/dialogs/SimpleUiTradeDialog/SimpleTradeForm.tsx b/src/views/dialogs/SimpleUiTradeDialog/SimpleTradeForm.tsx index 66bc037ba7..d41034e207 100644 --- a/src/views/dialogs/SimpleUiTradeDialog/SimpleTradeForm.tsx +++ b/src/views/dialogs/SimpleUiTradeDialog/SimpleTradeForm.tsx @@ -22,7 +22,7 @@ import { import { useTradeErrors } from '@/hooks/TradingForm/useTradeErrors'; import { TradeFormSource, useTradeForm } from '@/hooks/TradingForm/useTradeForm'; -import { usePerpetualsComplianceState } from '@/hooks/usePerpetualsComplianceState'; +import { useComplianceState } from '@/hooks/useComplianceState'; import { useStringGetter } from '@/hooks/useStringGetter'; import { Button } from '@/components/Button'; @@ -68,7 +68,7 @@ export const SimpleTradeForm = ({ const displayUnit = useAppSelector(getSelectedDisplayUnit); const tradeValues = useAppSelector(getTradeFormValues); const fullFormSummary = useAppSelector(getTradeFormSummary); - const { complianceState } = usePerpetualsComplianceState(); + const { complianceState } = useComplianceState(); const { summary } = fullFormSummary; const midPrice = useAppSelector(BonsaiHelpers.currentMarket.midPrice.data); const buyingPower = useAppSelector(BonsaiHelpers.currentMarket.account.buyingPower); diff --git a/src/views/dialogs/TransferDialogs/DepositAddressDialog.tsx b/src/views/dialogs/TransferDialogs/DepositAddressDialog.tsx index d4bfa3fb21..93b7e85259 100644 --- a/src/views/dialogs/TransferDialogs/DepositAddressDialog.tsx +++ b/src/views/dialogs/TransferDialogs/DepositAddressDialog.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import { avalanche, mainnet, polygon } from 'viem/chains'; @@ -6,12 +6,14 @@ import { avalanche, mainnet, polygon } from 'viem/chains'; import { AlertType } from '@/constants/alerts'; import { AnalyticsEvents } from '@/constants/analytics'; import { CHAIN_INFO, EVM_DEPOSIT_CHAINS } from '@/constants/chains'; +import { ComplianceStates } from '@/constants/compliance'; import { DepositDialog2Props, DialogProps } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; import { SOLANA_MAINNET_ID } from '@/constants/solana'; import { useAccounts } from '@/hooks/useAccounts'; import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useComplianceState } from '@/hooks/useComplianceState'; import { useDepositAddress } from '@/hooks/useDepositAddress'; import { useEnableSpot } from '@/hooks/useEnableSpot'; import { useLocaleSeparators } from '@/hooks/useLocaleSeparators'; @@ -54,6 +56,7 @@ export const DepositAddressDialog = ({ setIsOpen }: DialogProps { - setSelectedTab(newTab === 'perps' ? 'perpetuals' : 'spot'); - }; + const handleTabChange = useCallback( + (newTab: 'perps' | 'spot') => { + if (newTab === selectedTab) return; + setSelectedTab(newTab === 'perps' ? 'perpetuals' : 'spot'); + }, + [selectedTab] + ); + + useLayoutEffect(() => { + if (complianceState === ComplianceStates.READ_ONLY) { + setIsOpen(false); + } else if (complianceState !== ComplianceStates.FULL_ACCESS) { + handleTabChange('spot'); + } + }, [complianceState, handleTabChange, setIsOpen]); const perpetualsContent = (
@@ -255,6 +270,7 @@ export const DepositAddressDialog = ({ setIsOpen }: DialogProps) => { const dispatch = useAppDispatch(); const { sourceAccount, solanaAddress } = useAccounts(); + const { complianceState } = useComplianceState(); const { isLoading: isLoadingBalances, withBalances } = useDepositTokenBalances(); const highestBalance = withBalances.at(0); @@ -86,10 +89,14 @@ export const DepositDialog2 = ({ setIsOpen }: DialogProps) }>(); const tokenSelectRef = useRef(null); - const handleTabChange = (newTab: 'perps' | 'spot') => { - setCurrentDepositType(newTab); - setFormState('form'); - }; + const handleTabChange = useCallback( + (newTab: 'perps' | 'spot') => { + if (newTab === currentDepositType) return; + setCurrentDepositType(newTab); + setFormState('form'); + }, + [currentDepositType] + ); const dialogTitle = ( { @@ -126,6 +133,14 @@ export const DepositDialog2 = ({ setIsOpen }: DialogProps) } }, [sourceAccount, dispatch, setIsOpen]); + useLayoutEffect(() => { + if (complianceState === ComplianceStates.READ_ONLY) { + setIsOpen(false); + } else if (complianceState !== ComplianceStates.FULL_ACCESS) { + handleTabChange('spot'); + } + }, [complianceState, handleTabChange, setIsOpen]); + const tabs: SpotTabItem[] = [ { value: 'perps', @@ -144,6 +159,7 @@ export const DepositDialog2 = ({ setIsOpen }: DialogProps) onShowForm={onShowForm} /> ), + disabled: complianceState !== ComplianceStates.FULL_ACCESS, }, { value: 'spot', diff --git a/src/views/forms/TradeForm/AdvancedTradeOptions.tsx b/src/views/forms/TradeForm/AdvancedTradeOptions.tsx index dd7d1b2041..e5896cc4b9 100644 --- a/src/views/forms/TradeForm/AdvancedTradeOptions.tsx +++ b/src/views/forms/TradeForm/AdvancedTradeOptions.tsx @@ -13,7 +13,7 @@ import { TimeUnitShort } from '@/constants/time'; import { GOOD_TIL_TIME_TIMESCALE_STRINGS } from '@/constants/trade'; import { useBreakpoints } from '@/hooks/useBreakpoints'; -import { usePerpetualsComplianceState } from '@/hooks/usePerpetualsComplianceState'; +import { useComplianceState } from '@/hooks/useComplianceState'; import { useStringGetter } from '@/hooks/useStringGetter'; import { formMixins } from '@/styles/formMixins'; @@ -38,7 +38,7 @@ export const AdvancedTradeOptions = () => { const stringGetter = useStringGetter(); const dispatch = useAppDispatch(); const { isTablet } = useBreakpoints(); - const { complianceState } = usePerpetualsComplianceState(); + const { complianceState } = useComplianceState(); const currentTradeFormSummary = useAppSelector(getTradeFormSummary).summary; const currentTradeFormConfig = currentTradeFormSummary.options; diff --git a/src/views/forms/TradeForm/PlaceOrderButtonAndReceipt.tsx b/src/views/forms/TradeForm/PlaceOrderButtonAndReceipt.tsx index eeaff73384..6c26044b41 100644 --- a/src/views/forms/TradeForm/PlaceOrderButtonAndReceipt.tsx +++ b/src/views/forms/TradeForm/PlaceOrderButtonAndReceipt.tsx @@ -12,7 +12,7 @@ import { StatsigFlags } from '@/constants/statsig'; import { MobilePlaceOrderSteps } from '@/constants/trade'; import { IndexerPositionSide } from '@/types/indexer/indexerApiGen'; -import { usePerpetualsComplianceState } from '@/hooks/usePerpetualsComplianceState'; +import { useComplianceState } from '@/hooks/useComplianceState'; import { useStatsigGateValue } from '@/hooks/useStatsig'; import { useStringGetter } from '@/hooks/useStringGetter'; import { useTokenConfigs } from '@/hooks/useTokenConfigs'; @@ -77,7 +77,7 @@ export const PlaceOrderButtonAndReceipt = ({ const stringGetter = useStringGetter(); const dispatch = useAppDispatch(); const { chainTokenImage, chainTokenLabel } = useTokenConfigs(); - const { complianceState } = usePerpetualsComplianceState(); + const { complianceState } = useComplianceState(); const canAccountTrade = useAppSelector(calculateCanAccountTrade); const subaccountNumber = useAppSelector(getSubaccountId); diff --git a/src/views/menus/AccountMenu/AccountMenu.tsx b/src/views/menus/AccountMenu/AccountMenu.tsx index 930f75ddc5..f75fc2f35c 100644 --- a/src/views/menus/AccountMenu/AccountMenu.tsx +++ b/src/views/menus/AccountMenu/AccountMenu.tsx @@ -18,10 +18,10 @@ import { ConnectorType, DydxChainAsset, wallets, WalletType } from '@/constants/ import { useAccountBalance } from '@/hooks/useAccountBalance'; import { useAccounts } from '@/hooks/useAccounts'; import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useComplianceState } from '@/hooks/useComplianceState'; import { useEnableSpot } from '@/hooks/useEnableSpot'; import { useEnvFeatures } from '@/hooks/useEnvFeatures'; // import { useMobileAppUrl } from '@/hooks/useMobileAppUrl'; -import { usePerpetualsComplianceState } from '@/hooks/usePerpetualsComplianceState'; import { useStatsigGateValue } from '@/hooks/useStatsig'; import { useStringGetter } from '@/hooks/useStringGetter'; import { useSubaccount } from '@/hooks/useSubaccount'; @@ -63,7 +63,7 @@ import { WalletActions } from './WalletActions'; export const AccountMenu = () => { const stringGetter = useStringGetter(); const { isTablet } = useBreakpoints(); - const { complianceState } = usePerpetualsComplianceState(); + const { complianceState } = useComplianceState(); const affiliatesEnabled = useStatsigGateValue(StatsigFlags.ffEnableAffiliates); const spotEnabled = useEnableSpot(); const dispatch = useAppDispatch(); diff --git a/src/views/menus/AccountMenu/SpotActions.tsx b/src/views/menus/AccountMenu/SpotActions.tsx index dae0e9e7d7..df0de57f19 100644 --- a/src/views/menus/AccountMenu/SpotActions.tsx +++ b/src/views/menus/AccountMenu/SpotActions.tsx @@ -4,9 +4,11 @@ import { Item } from '@radix-ui/react-dropdown-menu'; import styled, { css } from 'styled-components'; import { ButtonAction, ButtonShape } from '@/constants/buttons'; +import { ComplianceStates } from '@/constants/compliance'; import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; +import { useComplianceState } from '@/hooks/useComplianceState'; import { useStringGetter } from '@/hooks/useStringGetter'; import { IconName } from '@/components/Icon'; @@ -24,11 +26,12 @@ export const SpotActions = memo(() => { const stringGetter = useStringGetter(); const maxWithdrawable = useMaxWithdrawableSol(); const hasBalance = maxWithdrawable > 0; + const { complianceState } = useComplianceState(); return (
{[ - { + complianceState !== ComplianceStates.READ_ONLY && { dialog: DialogTypes.Deposit2({}), iconName: IconName.Deposit, tooltipStringKey: STRING_KEYS.DEPOSIT, diff --git a/src/views/menus/UserMenuContent.tsx b/src/views/menus/UserMenuContent.tsx index d0d6915a58..e74db1fdfd 100644 --- a/src/views/menus/UserMenuContent.tsx +++ b/src/views/menus/UserMenuContent.tsx @@ -13,7 +13,7 @@ import { STRING_KEYS } from '@/constants/localization'; import { AppRoute } from '@/constants/routes'; import { useAccounts } from '@/hooks/useAccounts'; -import { usePerpetualsComplianceState } from '@/hooks/usePerpetualsComplianceState'; +import { useComplianceState } from '@/hooks/useComplianceState'; import { useStringGetter } from '@/hooks/useStringGetter'; import { Button } from '@/components/Button'; @@ -37,7 +37,7 @@ export const UserMenuContent = () => { const dispatch = useAppDispatch(); const navigate = useNavigate(); const onboardingState = useAppSelector(getOnboardingState); - const { complianceState } = usePerpetualsComplianceState(); + const { complianceState } = useComplianceState(); const canAccountTrade = useAppSelector(calculateCanAccountTrade); const isTurnkeyConnected = useAppSelector(selectIsTurnkeyConnected); const { equity, freeCollateral } = orEmptyObj( diff --git a/src/views/tables/PositionsTable/PositionsTriggersCell.tsx b/src/views/tables/PositionsTable/PositionsTriggersCell.tsx index 2356361901..2aec375258 100644 --- a/src/views/tables/PositionsTable/PositionsTriggersCell.tsx +++ b/src/views/tables/PositionsTable/PositionsTriggersCell.tsx @@ -9,8 +9,8 @@ import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; import { IndexerPositionSide } from '@/types/indexer/indexerApiGen'; +import { useComplianceState } from '@/hooks/useComplianceState'; import { useEnvFeatures } from '@/hooks/useEnvFeatures'; -import { usePerpetualsComplianceState } from '@/hooks/usePerpetualsComplianceState'; import { useStringGetter } from '@/hooks/useStringGetter'; import { layoutMixins } from '@/styles/layoutMixins'; @@ -60,7 +60,7 @@ export const PositionsTriggersCell = ({ const dispatch = useAppDispatch(); const { isSlTpLimitOrdersEnabled } = useEnvFeatures(); - const { complianceState } = usePerpetualsComplianceState(); + const { complianceState } = useComplianceState(); const onViewOrders = () => onViewOrdersClick(marketId);