Skip to content

Commit 80bb590

Browse files
authored
Merge pull request #18 from dydxprotocol/sync/upstream-main-12-16-2025
chore: sync/upstream main 12 16 2025
2 parents 8394537 + 6f8d3cd commit 80bb590

File tree

20 files changed

+535
-208
lines changed

20 files changed

+535
-208
lines changed

src/bonsai/forms/spot.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { LAMPORTS_PER_SOL } from '@solana/web3.js';
22

33
import { SpotWalletStatus } from '@/constants/account';
4-
import { MIN_SOL_RESERVE } from '@/constants/spot';
4+
import {
5+
DEFAULT_PRIORITY_FEE_LAMPORTS,
6+
DEFAULT_SLIPPAGE_BPS,
7+
MIN_SOL_RESERVE,
8+
} from '@/constants/spot';
59

610
import { SpotApiCreateTransactionRequest, SpotApiSide, SpotApiTradeRoute } from '@/clients/spotApi';
711
import { calc, mapIfPresent } from '@/lib/do';
@@ -151,6 +155,8 @@ function calculateSummary(state: SpotFormState, inputData: SpotFormInputData): S
151155
inAmount,
152156
pool: pairAddress,
153157
tradeRoute,
158+
maxSlippageBps: DEFAULT_SLIPPAGE_BPS,
159+
priorityFeeLamports: DEFAULT_PRIORITY_FEE_LAMPORTS,
154160
};
155161
}
156162
);
@@ -172,6 +178,8 @@ function calculateSummary(state: SpotFormState, inputData: SpotFormInputData): S
172178
inAmount,
173179
pool: pairAddress,
174180
tradeRoute,
181+
maxSlippageBps: DEFAULT_SLIPPAGE_BPS,
182+
priorityFeeLamports: DEFAULT_PRIORITY_FEE_LAMPORTS,
175183
};
176184
}
177185
);

src/bonsai/rest/spot.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export function setUpTokenMetadataQuery(store: RootStore) {
5252
queryKey: ['spot', 'tokenMetadata', currentSpotToken],
5353
queryFn: wrapAndLogBonsaiError(
5454
() => getSpotTokenMetadata(endpoint, currentSpotToken),
55-
'tokenMetadata'
55+
'spot/tokenMetadata'
5656
),
5757
refetchInterval: timeUnits.minute * 5,
5858
staleTime: timeUnits.minute * 5,
@@ -64,8 +64,8 @@ export function setUpTokenMetadataQuery(store: RootStore) {
6464
store.dispatch(setSpotTokenMetadata(queryResultToLoadable(result)));
6565
} catch (e) {
6666
logBonsaiError(
67-
'setUpTokenMetadataQuery',
68-
'Error handling result from react query',
67+
'spot/setUpTokenMetadataQuery',
68+
'Error handling result from react query store effect',
6969
e,
7070
result
7171
);
@@ -90,7 +90,7 @@ export function setUpSolPriceQuery(store: RootStore) {
9090
queryKey: ['spotTokenPrice', SOL_MINT_ADDRESS],
9191
queryFn: wrapAndLogBonsaiError(
9292
() => getSpotTokenUsdPrice(params.endpoint, SOL_MINT_ADDRESS),
93-
'solPrice'
93+
'spot/solPrice'
9494
),
9595
refetchInterval: timeUnits.second * 10,
9696
staleTime: timeUnits.second * 10,
@@ -101,7 +101,12 @@ export function setUpSolPriceQuery(store: RootStore) {
101101
try {
102102
store.dispatch(setSpotSolPrice(queryResultToLoadable(result)));
103103
} catch (e) {
104-
logBonsaiError('setUpSolPriceQuery', 'Error handling result from react query', e, result);
104+
logBonsaiError(
105+
'spot/setUpSolPriceQuery',
106+
'Error handling result from react query store effect',
107+
e,
108+
result
109+
);
105110
}
106111
});
107112

@@ -123,7 +128,7 @@ export function setUpSpotTokenPriceQuery(store: RootStore) {
123128
queryKey: ['spotTokenPrice', params.currentSpotToken],
124129
queryFn: wrapAndLogBonsaiError(
125130
() => getSpotTokenUsdPrice(params.endpoint, params.currentSpotToken),
126-
'tokenPrice'
131+
'spot/tokenPrice'
127132
),
128133
refetchInterval: timeUnits.second * 10,
129134
staleTime: timeUnits.second * 10,
@@ -135,8 +140,8 @@ export function setUpSpotTokenPriceQuery(store: RootStore) {
135140
store.dispatch(setSpotTokenPrice(queryResultToLoadable(result)));
136141
} catch (e) {
137142
logBonsaiError(
138-
'setUpSpotTokenPriceQuery',
139-
'Error handling result from react query',
143+
'spot/setUpSpotTokenPriceQuery',
144+
'Error handling result from react query store effect',
140145
e,
141146
result
142147
);
@@ -173,7 +178,7 @@ export function setUpPortfolioTradesQuery(store: RootStore) {
173178
queryKey: ['spot', 'portfolioTrades', walletAddress],
174179
queryFn: wrapAndLogBonsaiError(
175180
() => getSpotPortfolioTrades(endpoint, walletAddress),
176-
'portfolioTrades'
181+
'spot/portfolioTrades'
177182
),
178183
refetchInterval: timeUnits.minute * 2,
179184
staleTime: timeUnits.minute * 2,
@@ -185,8 +190,8 @@ export function setUpPortfolioTradesQuery(store: RootStore) {
185190
store.dispatch(setSpotPortfolioTrades(queryResultToLoadable(result)));
186191
} catch (e) {
187192
logBonsaiError(
188-
'setUpPortfolioTradesQuery',
189-
'Error handling result from react query',
193+
'spot/setUpPortfolioTradesQuery',
194+
'Error handling result from react query store effect',
190195
e,
191196
result
192197
);

src/clients/spotApi.ts

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { logBonsaiError } from '@/bonsai/logs';
22

3-
import { DEFAULT_PRIORITY_FEE_LAMPORTS, DEFAULT_SLIPPAGE_BPS } from '@/constants/spot';
4-
53
import { simpleFetch } from '@/lib/simpleFetch';
4+
import { isPresent } from '@/lib/typeUtils';
65

76
export enum SpotApiLandingMethod {
87
JITO = 'jito',
@@ -34,10 +33,10 @@ export type SpotApiCreateTransactionRequest = {
3433
tokenMint: string;
3534
side: SpotApiSide;
3635
inAmount: string;
37-
maxSlippageBps?: number;
36+
maxSlippageBps: number;
3837
pool: string;
3938
tradeRoute: SpotApiTradeRoute;
40-
priorityFeeLamports?: number;
39+
priorityFeeLamports: number;
4140
};
4241

4342
export type SpotApiTransactionMetadataObject = {
@@ -191,13 +190,27 @@ export type SpotApiBarsResolution =
191190
| '1D'
192191
| '7D';
193192

193+
export enum SpotApiTokenPairStatisticsType {
194+
Filtered = 'FILTERED',
195+
Unfiltered = 'UNFILTERED',
196+
}
197+
194198
export type SpotApiGetBarsQuery = {
195199
from: number; // unix timestamp
196200
tokenMint: string;
197201
to?: number; // unix timestamp, defaults to current time
198202
resolution?: SpotApiBarsResolution;
203+
statsType?: SpotApiTokenPairStatisticsType;
204+
removeEmptyBars?: boolean;
205+
removeLeadingNullValues?: boolean;
206+
countback?: number;
199207
};
200208

209+
export enum SpotApiHistoricalBarsStatus {
210+
OK = 'ok',
211+
NO_DATA = 'no_data',
212+
}
213+
201214
export interface SpotApiGetBarsResponseData {
202215
buyVolume: string[];
203216
buyers: number[];
@@ -215,7 +228,7 @@ export interface SpotApiGetBarsResponseData {
215228
transactions: number[];
216229
volume: string[];
217230
volumeNativeToken: string[];
218-
s: string;
231+
s: SpotApiHistoricalBarsStatus;
219232
pair: Record<string, any>;
220233
}
221234

@@ -291,20 +304,12 @@ export class SpotApiClient {
291304
}
292305

293306
async getBars(query: SpotApiGetBarsQuery) {
294-
const params = new URLSearchParams({
295-
from: query.from.toString(),
296-
tokenMint: query.tokenMint,
297-
});
298-
299-
if (query.to) {
300-
params.append('to', query.to.toString());
301-
}
302-
303-
if (query.resolution) {
304-
params.append('resolution', query.resolution);
305-
}
306-
307-
return this._get<SpotApiGetBarsResponse>(`tokens/bars?${params.toString()}`);
307+
const params = new URLSearchParams(
308+
Object.entries(query)
309+
.filter(([_, v]) => isPresent(v))
310+
.map(([k, v]) => [k, String(v)])
311+
);
312+
return this._get<SpotApiGetBarsResponse>(`tokens/bars?${params}`);
308313
}
309314

310315
async createTransaction(request: SpotApiCreateTransactionRequest) {
@@ -353,14 +358,7 @@ export const createSpotTransaction = async (
353358
request: SpotApiCreateTransactionRequest
354359
) => {
355360
const client = getOrCreateSpotApiClient(apiUrl);
356-
357-
const requestWithDefaults: Required<SpotApiCreateTransactionRequest> = {
358-
...request,
359-
maxSlippageBps: request.maxSlippageBps ?? DEFAULT_SLIPPAGE_BPS,
360-
priorityFeeLamports: request.priorityFeeLamports ?? DEFAULT_PRIORITY_FEE_LAMPORTS,
361-
};
362-
363-
return client.createTransaction(requestWithDefaults);
361+
return client.createTransaction(request);
364362
};
365363

366364
export const landSpotTransaction = async (
@@ -387,8 +385,7 @@ export const transformBarsResponseToBars = (
387385

388386
export const getSpotBars = async (apiUrl: string, query: SpotApiGetBarsQuery) => {
389387
const client = getOrCreateSpotApiClient(apiUrl);
390-
const response = await client.getBars(query);
391-
return transformBarsResponseToBars(response);
388+
return client.getBars(query);
392389
};
393390

394391
export const getSpotPortfolioTrades = async (apiUrl: string, address: string) => {

src/constants/analytics.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { type SpotBuyInputType, type SpotSellInputType } from '@/bonsai/forms/spot';
12
import { OrderSide, TradeFormType } from '@/bonsai/forms/trade/types';
23
import { PlaceOrderPayload } from '@/bonsai/forms/triggers/types';
34
import { ApiStatus, SubaccountFill } from '@/bonsai/types/summaryTypes';
@@ -11,6 +12,8 @@ import { type ConnectorType, type DydxAddress, type WalletType } from '@/constan
1112

1213
import type { Deposit, Withdraw } from '@/state/transfers';
1314

15+
import { type SpotApiSide, type SpotApiTradeRoute } from '@/clients/spotApi';
16+
1417
import type { OnboardingState, OnboardingSteps } from './account';
1518
import { type DialogTypesTypes } from './dialogs';
1619
import type { SupportedLocales } from './localization';
@@ -80,6 +83,7 @@ export const AnalyticsUserProperties = unionize(
8083

8184
// Account
8285
DydxAddress: ofType<DydxAddress | null>(),
86+
SolanaAddress: ofType<string | null>(),
8387
SubaccountNumber: ofType<number | null>(),
8488

8589
// Affiliate
@@ -109,6 +113,7 @@ export const AnalyticsUserPropertyLoggableTypes = {
109113
WalletAddress: 'walletAddress',
110114
IsRememberMe: 'isRememberMe',
111115
DydxAddress: 'dydxAddress',
116+
SolanaAddress: 'solanaAddress',
112117
SubaccountNumber: 'subaccountNumber',
113118
AffiliateAddress: 'affiliateAddress',
114119
BonsaiValidatorUrl: 'bonsaiValidator',
@@ -596,6 +601,53 @@ export const AnalyticsEvents = unionize(
596601
campaign: string;
597602
timestamp: number;
598603
}>(),
604+
605+
// Spot Trading
606+
SpotTransactionSubmitStarted: ofType<{
607+
side: SpotApiSide;
608+
tokenMint: string;
609+
tokenSymbol?: string;
610+
tradeRoute: SpotApiTradeRoute;
611+
estimatedUsdAmount?: number;
612+
inputType: SpotBuyInputType | SpotSellInputType;
613+
}>(),
614+
SpotTransactionSubmitSuccess: ofType<{
615+
side: SpotApiSide;
616+
tokenMint: string;
617+
tokenSymbol?: string;
618+
tradeRoute: SpotApiTradeRoute;
619+
usdAmount: number;
620+
solAmount: number;
621+
timingMs: Record<string, number>;
622+
}>(),
623+
SpotTransactionSubmitError: ofType<{
624+
side: SpotApiSide;
625+
tokenMint: string;
626+
tokenSymbol?: string;
627+
tradeRoute: SpotApiTradeRoute;
628+
estimatedUsdAmount?: number;
629+
step: string;
630+
errorName: string;
631+
errorMessage: string;
632+
}>(),
633+
634+
// Spot Withdrawal
635+
SpotSolWithdrawalStarted: ofType<{
636+
solAmount: number;
637+
}>(),
638+
SpotSolWithdrawalSuccess: ofType<{
639+
solAmount: number;
640+
timingMs: Record<string, number>;
641+
}>(),
642+
SpotSolWithdrawalError: ofType<{
643+
solAmount: number;
644+
step: string;
645+
errorName: string;
646+
errorMessage: string;
647+
}>(),
648+
649+
// Spot Deposit
650+
SpotDepositInitiated: ofType<{}>(),
599651
},
600652
{ tag: 'type' as const, value: 'payload' as const }
601653
);

src/constants/spot.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export const SOLANA_BASE_TRANSACTION_FEE = 0.000005;
77
export const SOL_WITHDRAWAL_POLL_INTERVAL_MS = 1000;
88
export const SOL_WITHDRAWAL_TIMEOUT_MS = 30000;
99
export const SPOT_DEFAULT_TOKEN_MINT = 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263';
10+
export const SPOT_TV_DEFAULT_VISIBLE_BAR_COUNT = 250;

src/hooks/tradingView/useSpotChartMarketAndResolution.ts

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { useEffect, useRef } from 'react';
22

33
import type { ResolutionString } from 'public/tradingview/charting_library';
44

5-
import { DEFAULT_RESOLUTION, SPOT_RESOLUTION_CHART_CONFIGS } from '@/constants/candles';
5+
import { DEFAULT_RESOLUTION } from '@/constants/candles';
6+
import { SPOT_TV_DEFAULT_VISIBLE_BAR_COUNT } from '@/constants/spot';
67
import type { TvWidget } from '@/constants/tvchart';
78

89
import { useAppSelector } from '@/state/appTypes';
@@ -42,29 +43,43 @@ export const useSpotChartMarketAndResolution = ({
4243
tvWidget
4344
.activeChart()
4445
.onIntervalChanged()
45-
.subscribe(null, (newResolution) => {
46-
setVisibleRangeForResolution(tvWidget, newResolution);
46+
.subscribe(null, () => {
47+
setVisibleRange(tvWidget);
4748
});
4849

49-
// Set visible range on initial render
50-
setVisibleRangeForResolution(tvWidget, resolution);
50+
setVisibleRange(tvWidget);
5151
});
5252
}, [tokenMint, tvWidget]);
5353
};
5454

55-
const setVisibleRangeForResolution = (tvWidget: TvWidget, resolution: ResolutionString) => {
56-
const defaultRange: number | undefined = SPOT_RESOLUTION_CHART_CONFIGS[resolution]?.defaultRange;
57-
58-
if (defaultRange) {
59-
const to = Date.now() / 1000;
60-
const from = (Date.now() - defaultRange) / 1000;
61-
62-
tvWidget.activeChart().setVisibleRange(
63-
{
64-
from,
65-
to,
66-
},
67-
{ percentRightMargin: 10 }
68-
);
69-
}
55+
const setVisibleRange = (tvWidget: TvWidget) => {
56+
const defaultBarCount = SPOT_TV_DEFAULT_VISIBLE_BAR_COUNT;
57+
const chart = tvWidget.activeChart();
58+
59+
const handleDataLoaded = () => {
60+
chart
61+
.exportData({ includeTime: true, includeSeries: true, includedStudies: [] })
62+
.then((exportedData) => {
63+
const bars = exportedData.data;
64+
if (bars.length === 0) return;
65+
66+
const startIndex = Math.max(0, bars.length - defaultBarCount);
67+
const fromBar = bars[startIndex];
68+
const toBar = bars[bars.length - 1];
69+
const from = fromBar?.[0];
70+
const to = toBar?.[0];
71+
72+
if (from && to) {
73+
chart.setVisibleRange({ from, to }, { percentRightMargin: 10 });
74+
}
75+
})
76+
.catch(() => {
77+
// Fallback: do nothing, let TradingView use default zoom
78+
});
79+
80+
// Unsubscribe after handling
81+
chart.onDataLoaded().unsubscribe(null, handleDataLoaded);
82+
};
83+
84+
chart.onDataLoaded().subscribe(null, handleDataLoaded);
7085
};

0 commit comments

Comments
 (0)