Skip to content

Commit 94eb3cc

Browse files
dwjanusfionn223lukabuzKefancaoSam-dYdX
authored
chore: sync/upstream main 12 15 2025 (#15)
* feat: add Liquidation Rebates banner and update No Fee December string (dydxprotocol#2002) * fix: update liq competition to be monthly (dydxprotocol#2004) * feat: funding comparison link in menu (dydxprotocol#2005) * feat: use new fee rebate leaderboard (dydxprotocol#2006) * fix: initial state of leaderboard doesn't have price (dydxprotocol#2008) * fix: modify swap venues and add options (dydxprotocol#2003) Co-authored-by: Sam Newby <[email protected]> * feat: spot (dydxprotocol#1933) Co-authored-by: Kefan Cao <[email protected]> Co-authored-by: Kefan Cao <[email protected]> * fix: spot tv price formatting (dydxprotocol#2012) * feat: add dollar rewards (dydxprotocol#2010) * feat: spot feedback & missing features (dydxprotocol#2014) * fix: deposit dialog styling (dydxprotocol#2016) * fix: use instant deposit text when depositing from near instant chains (dydxprotocol#2015) * fix: update set leverage button styling (dydxprotocol#2013) * fix: Add cosmos wallet support to transaction supervisor (dydxprotocol#2017) * fix: remove gated spot route and handle spot index route to default to last viewed token (dydxprotocol#2018) * fix: make spot search input controlled and hide deposit button if not connected (dydxprotocol#2019) --------- Co-authored-by: fionn223 <[email protected]> Co-authored-by: Luka Buzaladze <[email protected]> Co-authored-by: Kefan Cao <[email protected]> Co-authored-by: Sam Newby <[email protected]> Co-authored-by: Paata Kardava <[email protected]> Co-authored-by: Kefan Cao <[email protected]> Co-authored-by: Jared Vu <[email protected]> Co-authored-by: pp346 <[email protected]>
1 parent b9f4dd1 commit 94eb3cc

File tree

16 files changed

+1324
-1212
lines changed

16 files changed

+1324
-1212
lines changed

public/configs/v1/env.json

Lines changed: 1142 additions & 1142 deletions
Large diffs are not rendered by default.

src/App.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ import { useAnalytics } from './hooks/useAnalytics';
5353
import { useBreakpoints } from './hooks/useBreakpoints';
5454
import { useCommandMenu } from './hooks/useCommandMenu';
5555
import { useComplianceState } from './hooks/useComplianceState';
56-
import { useEnableSpot } from './hooks/useEnableSpot';
5756
import { useInitializePage } from './hooks/useInitializePage';
5857
import { useLocalStorage } from './hooks/useLocalStorage';
5958
import { useReferralCode } from './hooks/useReferralCode';
@@ -112,7 +111,11 @@ const Content = () => {
112111
const isSimpleUi = useSimpleUiEnabled();
113112
const { showComplianceBanner } = useComplianceState();
114113
const isSimpleUiUserMenuOpen = useAppSelector(getIsUserMenuOpen);
115-
const isSpotEnabled = useEnableSpot();
114+
115+
// Track current path in Redux for conditional polling
116+
useEffect(() => {
117+
dispatch(setCurrentPath(location.pathname));
118+
}, [location.pathname, dispatch]);
116119

117120
// Track current path in Redux for conditional polling
118121
useEffect(() => {
@@ -213,9 +216,10 @@ const Content = () => {
213216
<Route path={AppRoute.Trade} element={<TradePage />} />
214217
</Route>
215218

216-
{isSpotEnabled && (
217-
<Route path={`${AppRoute.Spot}/:tokenMint`} element={<SpotPage />} />
218-
)}
219+
<Route path={AppRoute.Spot}>
220+
<Route path=":tokenMint" element={<SpotPage />} />
221+
<Route index element={<SpotPage />} />
222+
</Route>
219223

220224
<Route path={AppRoute.Markets}>
221225
<Route path={AppRoute.Markets} element={<MarketsPage />} />

src/bonsai/AccountTransactionSupervisor.ts

Lines changed: 79 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import {
4343
import type { RootStore } from '@/state/_store';
4444
import { store as reduxStore } from '@/state/_store';
4545
import { getSubaccountId, getUserWalletAddress } from '@/state/accountInfoSelectors';
46-
import { getSelectedNetwork } from '@/state/appSelectors';
46+
import { getSelectedDydxChainId, getSelectedNetwork } from '@/state/appSelectors';
4747
import { createAppSelector } from '@/state/appTypes';
4848
import {
4949
cancelAllSubmitted,
@@ -54,7 +54,7 @@ import {
5454
placeOrderSubmitted,
5555
placeOrderTimeout,
5656
} from '@/state/localOrders';
57-
import { getLocalWalletNonce } from '@/state/walletSelectors';
57+
import { getLocalWalletNonce, selectIsKeplrConnected } from '@/state/walletSelectors';
5858

5959
import { track } from '@/lib/analytics/analytics';
6060
import { calc } from '@/lib/do';
@@ -79,13 +79,15 @@ import { StateConditionNotifier, Tracker } from './StateConditionNotifier';
7979
import { getSimpleOrderStatus } from './calculators/orders';
8080
import { TradeFormPayload } from './forms/trade/types';
8181
import { PlaceOrderMarketInfo, PlaceOrderPayload } from './forms/triggers/types';
82+
import { getLazyLocalWallet } from './lib/lazyDynamicLibs';
8283
import { CompositeClientManager } from './rest/lib/compositeClientManager';
8384
import { estimateLiveValidatorHeight } from './selectors/apiStatus';
8485

8586
interface TransactionSupervisorShared {
8687
store: RootStore;
8788
compositeClientManager: typeof CompositeClientManager;
8889
stateNotifier: StateConditionNotifier;
90+
maybeDydxLocalWallet?: LocalWallet | null;
8991
}
9092

9193
const selectOrdersAndFills = createAppSelector(
@@ -111,10 +113,13 @@ export const SHORT_TERM_ORDER_DURATION_SAFETY_MARGIN = 5;
111113
export class AccountTransactionSupervisor {
112114
private store: RootStore;
113115

116+
private cachedDydxLocalWallet: LocalWallet | null;
117+
114118
private shared: TransactionSupervisorShared;
115119

116120
constructor(store: RootStore, compositeClientManager: typeof CompositeClientManager) {
117121
this.store = store;
122+
this.cachedDydxLocalWallet = null;
118123

119124
this.shared = {
120125
compositeClientManager,
@@ -130,9 +135,11 @@ export class AccountTransactionSupervisor {
130135
tracking?: Tracker<P, Q>
131136
) {
132137
return async () => {
138+
const maybeDydxLocalWallet = await this.getCosmosLocalWallet();
139+
133140
const result = await taskBuilder({ payload: basePayload })
134141
.with<AddSharedContextMiddlewareProps>(
135-
addSharedContextMiddleware(nameForLogging, this.shared)
142+
addSharedContextMiddleware(nameForLogging, { ...this.shared, maybeDydxLocalWallet })
136143
)
137144
.with<ValidateLocalWalletMiddlewareProps>(validateLocalWalletMiddleware())
138145
.with<StateTrackingProps<Q>>(stateTrackingMiddleware(tracking))
@@ -153,6 +160,28 @@ export class AccountTransactionSupervisor {
153160
};
154161
}
155162

163+
private async getCosmosLocalWallet() {
164+
const state = this.store.getState();
165+
const isKeplrConnected = selectIsKeplrConnected(state);
166+
167+
if (isKeplrConnected && window.keplr) {
168+
if (this.cachedDydxLocalWallet) {
169+
return this.cachedDydxLocalWallet;
170+
}
171+
172+
const chainId = getSelectedDydxChainId(state);
173+
const dydxOfflineSigner = await window.keplr.getOfflineSigner(chainId);
174+
const dydxLocalWallet = await (
175+
await getLazyLocalWallet()
176+
).fromOfflineSigner(dydxOfflineSigner);
177+
178+
this.cachedDydxLocalWallet = dydxLocalWallet;
179+
return dydxLocalWallet;
180+
}
181+
182+
return undefined;
183+
}
184+
156185
private createCancelOrderPayload(orderId: string): CancelOrderPayload | undefined {
157186
const state = this.store.getState();
158187
const orders = BonsaiCore.account.allOrders.data(state);
@@ -570,10 +599,15 @@ export class AccountTransactionSupervisor {
570599
payloadArg: PlaceOrderPayload,
571600
source: TradeMetadataSource
572601
): Promise<OperationResult<any>> {
602+
const maybeDydxLocalWallet = await this.getCosmosLocalWallet();
603+
573604
return (
574605
taskBuilder({ payload: payloadArg })
575606
.with<AddSharedContextMiddlewareProps>(
576-
addSharedContextMiddleware('AccountTransactionSupervisor/placeOrderWrapper', this.shared)
607+
addSharedContextMiddleware('AccountTransactionSupervisor/placeOrderWrapper', {
608+
...this.shared,
609+
maybeDydxLocalWallet,
610+
})
577611
)
578612
.with<ValidateLocalWalletMiddlewareProps>(validateLocalWalletMiddleware())
579613
// fully prepare/augment the trade payload
@@ -780,10 +814,14 @@ export class AccountTransactionSupervisor {
780814

781815
public async closeAllPositions() {
782816
track(AnalyticsEvents.TradeCloseAllPositionsClick({}));
817+
const maybeDydxLocalWallet = await this.getCosmosLocalWallet();
783818

784819
return taskBuilder({ payload: {} })
785820
.with<AddSharedContextMiddlewareProps>(
786-
addSharedContextMiddleware('AccountTransactionSupervisor/closeAllPositions', this.shared)
821+
addSharedContextMiddleware('AccountTransactionSupervisor/closeAllPositions', {
822+
...this.shared,
823+
maybeDydxLocalWallet,
824+
})
787825
)
788826
.with<ValidateLocalWalletMiddlewareProps>(validateLocalWalletMiddleware())
789827
.with<{ closePayloads: PlaceOrderPayload[] }>(async (context, next) => {
@@ -846,10 +884,15 @@ export class AccountTransactionSupervisor {
846884
orderId: string;
847885
withNotification?: boolean;
848886
}) {
887+
const maybeDydxLocalWallet = await this.getCosmosLocalWallet();
888+
849889
return (
850890
taskBuilder({ payload: { orderId, withNotification } })
851891
.with<AddSharedContextMiddlewareProps>(
852-
addSharedContextMiddleware('AccountTransactionSupervisor/cancelOrder', this.shared)
892+
addSharedContextMiddleware('AccountTransactionSupervisor/cancelOrder', {
893+
...this.shared,
894+
maybeDydxLocalWallet,
895+
})
853896
)
854897
.with<ValidateLocalWalletMiddlewareProps>(validateLocalWalletMiddleware())
855898
// populate order details
@@ -944,10 +987,14 @@ export class AccountTransactionSupervisor {
944987

945988
public async cancelAllOrders({ marketId }: { marketId?: string }) {
946989
track(AnalyticsEvents.TradeCancelAllOrdersClick({ marketId }));
990+
const maybeDydxLocalWallet = await this.getCosmosLocalWallet();
947991

948992
return taskBuilder({ payload: { marketId } })
949993
.with<AddSharedContextMiddlewareProps>(
950-
addSharedContextMiddleware('AccountTransactionSupervisor/cancelAllOrders', this.shared)
994+
addSharedContextMiddleware('AccountTransactionSupervisor/cancelAllOrders', {
995+
...this.shared,
996+
maybeDydxLocalWallet,
997+
})
951998
)
952999
.with<ValidateLocalWalletMiddlewareProps>(validateLocalWalletMiddleware())
9531000
.with<{ ordersWithUuids: Array<{ order: SubaccountOrder; uuid: string }> }>(
@@ -1038,6 +1085,8 @@ export class AccountTransactionSupervisor {
10381085
const hasStatefulOperations = isMainOrderStateful || (order.triggersPayloads?.length ?? 0) > 0;
10391086

10401087
if (hasStatefulOperations) {
1088+
const maybeDydxLocalWallet = await this.getCosmosLocalWallet();
1089+
10411090
return (
10421091
taskBuilder({
10431092
payload: {
@@ -1047,10 +1096,10 @@ export class AccountTransactionSupervisor {
10471096
},
10481097
})
10491098
.with<AddSharedContextMiddlewareProps>(
1050-
addSharedContextMiddleware(
1051-
'AccountTransactionSupervisor/executeBulkStatefulOrders',
1052-
this.shared
1053-
)
1099+
addSharedContextMiddleware('AccountTransactionSupervisor/executeBulkStatefulOrders', {
1100+
...this.shared,
1101+
maybeDydxLocalWallet,
1102+
})
10541103
)
10551104
.with<ValidateLocalWalletMiddlewareProps>(validateLocalWalletMiddleware())
10561105
// Prepare payloads with current height and collect cancel/place operations
@@ -1396,6 +1445,10 @@ function validateLocalWalletMiddleware() {
13961445
const state = context.shared.store.getState();
13971446
const localWalletNonce = getLocalWalletNonce(state);
13981447

1448+
if (context.shared.maybeDydxLocalWallet) {
1449+
return next(context);
1450+
}
1451+
13991452
if (localWalletNonce == null) {
14001453
const errorMsg = 'No valid local wallet available';
14011454
const errSource = context.fnName;
@@ -1420,14 +1473,16 @@ function addClientAndWalletMiddleware(store: RootStore) {
14201473

14211474
return createMiddleware<AddClientAndWalletMiddlewareProps, AddSharedContextMiddlewareProps>(
14221475
async (context, next) => {
1423-
const network = getSelectedNetwork(context.shared.store.getState());
1424-
const localWalletNonce = getLocalWalletNonce(context.shared.store.getState());
1476+
const state = context.shared.store.getState();
1477+
const network = getSelectedNetwork(state);
1478+
const localWalletNonce = getLocalWalletNonce(state);
14251479

14261480
const clientConfig = {
14271481
network,
14281482
dispatch: context.shared.store.dispatch,
14291483
};
14301484
const clientWrapper = context.shared.compositeClientManager.use(clientConfig);
1485+
const maybeDydxLocalWallet = context.shared.maybeDydxLocalWallet;
14311486

14321487
try {
14331488
if (network !== networkBefore) {
@@ -1436,11 +1491,18 @@ function addClientAndWalletMiddleware(store: RootStore) {
14361491
if (localWalletNonce !== nonceBefore) {
14371492
throw new Error('Local wallet changed before operation execution');
14381493
}
1439-
if (localWalletNonce == null) {
1440-
throw new Error('No valid local wallet nonce found');
1441-
}
14421494

1443-
const localWallet = localWalletManager.getLocalWallet(localWalletNonce);
1495+
const localWallet = calc(() => {
1496+
if (maybeDydxLocalWallet) {
1497+
return maybeDydxLocalWallet;
1498+
}
1499+
1500+
if (localWalletNonce == null) {
1501+
throw new Error('No valid local wallet nonce found');
1502+
}
1503+
1504+
return localWalletManager.getLocalWallet(localWalletNonce);
1505+
});
14441506

14451507
if (localWallet == null) {
14461508
throw new Error('Local wallet not initialized or nonce was incorrect.');

src/components/SearchInput.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,37 +10,43 @@ import { Icon, IconName } from '@/components/Icon';
1010
import { IconButton } from '@/components/IconButton';
1111
import { Input, InputType, type InputProps } from '@/components/Input';
1212

13+
import { isPresent } from '@/lib/typeUtils';
14+
1315
type ElementProps = {
1416
onTextChange?: (value: string) => void;
1517
className?: string;
1618
};
1719

1820
export type SearchInputProps = ElementProps & InputProps;
1921

20-
export const SearchInput = ({ placeholder, onTextChange, className }: SearchInputProps) => {
21-
const [value, setValue] = useState('');
22+
export const SearchInput = ({ value, placeholder, onTextChange, className }: SearchInputProps) => {
23+
const [internalValue, setInternalValue] = useState('');
2224
const inputRef = useRef<HTMLInputElement | null>(null);
2325

26+
const isControlled = isPresent(value);
27+
const displayValue = isControlled ? String(value) : internalValue;
28+
2429
return (
2530
<$Search className={className}>
2631
<$Icon iconName={IconName.Search} />
2732
<Input
2833
autoFocus
2934
ref={inputRef}
30-
value={value}
35+
value={displayValue}
3136
type={InputType.Search}
3237
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
33-
setValue(e.target.value);
34-
onTextChange?.(e.target.value);
38+
const newValue = e.target.value;
39+
setInternalValue(newValue);
40+
onTextChange?.(newValue);
3541
}}
3642
placeholder={placeholder}
3743
tw="max-w-full rounded-0"
3844
/>
39-
{value.length > 0 && (
45+
{displayValue.length > 0 && (
4046
<$IconButton
4147
iconName={IconName.Close}
4248
onClick={() => {
43-
setValue('');
49+
setInternalValue('');
4450
onTextChange?.('');
4551
}}
4652
buttonStyle={ButtonStyle.WithoutBackground}

src/constants/localStorage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export enum LocalStorageKey {
2323

2424
// UI State
2525
LastViewedMarket = 'dydx.LastViewedMarket',
26+
LastViewedSpotToken = 'dydx.LastViewedSpotToken',
2627
SelectedLocale = 'dydx.SelectedLocale',
2728
SelectedNetwork = 'dydx.SelectedNetwork',
2829
SelectedTradeLayout = 'dydx.SelectedTradeLayout',

src/hooks/useCurrentSpotToken.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,37 @@
11
import { useEffect } from 'react';
22

3-
import { useMatch } from 'react-router-dom';
3+
import { useMatch, useNavigate } from 'react-router-dom';
44

5+
import { LocalStorageKey } from '@/constants/localStorage';
56
import { AppRoute } from '@/constants/routes';
7+
import { SPOT_DEFAULT_TOKEN_MINT } from '@/constants/spot';
8+
9+
import { useLocalStorage } from '@/hooks/useLocalStorage';
610

711
import { useAppDispatch } from '@/state/appTypes';
812
import { setCurrentSpotToken } from '@/state/spot';
913

1014
export const useCurrentSpotToken = () => {
11-
const match = useMatch(`/${AppRoute.Spot}/:symbol`);
12-
const { symbol } = match?.params ?? {};
15+
const navigate = useNavigate();
16+
const match = useMatch(`/${AppRoute.Spot}/:tokenMint`);
17+
const { tokenMint } = match?.params ?? {};
1318
const dispatch = useAppDispatch();
1419

20+
const [lastViewedToken, setLastViewedToken] = useLocalStorage({
21+
key: LocalStorageKey.LastViewedSpotToken,
22+
defaultValue: SPOT_DEFAULT_TOKEN_MINT,
23+
});
24+
25+
const validTokenMint = tokenMint ?? lastViewedToken;
26+
1527
useEffect(() => {
16-
dispatch(setCurrentSpotToken(symbol));
17-
}, [symbol, dispatch]);
28+
if (!tokenMint) {
29+
navigate(`${AppRoute.Spot}/${validTokenMint}`, { replace: true });
30+
} else {
31+
setLastViewedToken(tokenMint);
32+
dispatch(setCurrentSpotToken(tokenMint));
33+
}
34+
}, [tokenMint, validTokenMint, navigate, dispatch, setLastViewedToken]);
1835

19-
return { currentSpotToken: symbol };
36+
return { currentSpotToken: validTokenMint };
2037
};

src/hooks/useEnableSpot.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
1-
import { StatsigFlags } from '@/constants/statsig';
2-
3-
import { testFlags } from '@/lib/testFlags';
4-
5-
import { useStatsigGateValue } from './useStatsig';
6-
71
export const useEnableSpot = () => {
8-
const forcedSpot = testFlags.spot;
9-
const spotFF = useStatsigGateValue(StatsigFlags.ffSpot);
10-
11-
return forcedSpot || spotFF;
2+
// const forcedSpot = testFlags.spot;
3+
// const spotFF = useStatsigGateValue(StatsigFlags.ffSpot);
4+
return true;
125
};

0 commit comments

Comments
 (0)