Skip to content

Commit d6f70fe

Browse files
feat: (batchSell-quotes-4 first commit) setup useBatchSellQuoteRequest
1 parent 326a6bf commit d6f70fe

10 files changed

Lines changed: 891 additions & 29 deletions

File tree

app/components/UI/Bridge/Views/BatchSellReview/BatchSellReview.test.tsx

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import Routes from '../../../../../constants/navigation/Routes';
99

1010
const mockNavigate = jest.fn();
1111
const mockDispatch = jest.fn();
12+
const mockCancelBatchSellQuoteParams = jest.fn();
13+
const mockUpdateBatchSellQuoteParams = Object.assign(jest.fn(), {
14+
cancel: mockCancelBatchSellQuoteParams,
15+
});
1216
const defaultSelectedTokens: BridgeToken[] = [
1317
{
1418
address: '0x1111111111111111111111111111111111111111',
@@ -51,6 +55,9 @@ let mockSelectedDestinationToken: BridgeToken | undefined = usdcToken;
5155
let mockDestinationTokens: BridgeToken[] = [usdcToken];
5256
let mockBatchSellSlippages: Partial<Record<CaipAssetType, string | undefined>> =
5357
{};
58+
let mockBatchSellSourceTokenAmounts: Partial<
59+
Record<CaipAssetType, string | undefined>
60+
> = {};
5461

5562
jest.mock('@react-navigation/native', () => ({
5663
useNavigation: () => ({
@@ -68,10 +75,31 @@ jest.mock('../../../../../core/redux/slices/bridge', () => ({
6875
selectBatchSellDestStablecoins: jest.fn(() => mockDestinationTokens),
6976
selectBatchSellDestToken: jest.fn(() => mockSelectedDestinationToken),
7077
selectBatchSellSlippages: jest.fn(() => mockBatchSellSlippages),
78+
selectBatchSellSourceTokenAmounts: jest.fn(
79+
() => mockBatchSellSourceTokenAmounts,
80+
),
7181
setBatchSellDestToken: jest.fn((token: BridgeToken) => ({
7282
type: 'bridge/setBatchSellDestToken',
7383
payload: token,
7484
})),
85+
setBatchSellSourceTokenAmount: jest.fn(
86+
({
87+
assetId,
88+
amount,
89+
}: {
90+
assetId: CaipAssetType;
91+
amount: string | undefined;
92+
}) => ({
93+
type: 'bridge/setBatchSellSourceTokenAmount',
94+
payload: { assetId, amount },
95+
}),
96+
),
97+
setBatchSellSourceTokenAmounts: jest.fn(
98+
(amounts: Partial<Record<CaipAssetType, string | undefined>>) => ({
99+
type: 'bridge/setBatchSellSourceTokenAmounts',
100+
payload: amounts,
101+
}),
102+
),
75103
setBatchSellSourceTokens: jest.fn((tokens: BridgeToken[]) => ({
76104
type: 'bridge/setBatchSellSourceTokens',
77105
payload: tokens,
@@ -89,6 +117,18 @@ jest.mock('react-redux', () => ({
89117
useSelector: (selector: (state: unknown) => unknown) => selector({}),
90118
}));
91119

120+
jest.mock('../../hooks/useBatchSellQuoteRequest', () => ({
121+
getBatchSellAtomicSourceAmount: jest.fn(
122+
(token: { balance?: string }, amount?: string) =>
123+
token.balance && amount && Number(amount) > 0 ? '1' : undefined,
124+
),
125+
getBatchSellSourceTokenAmount: jest.fn(
126+
(token: { balance?: string }, percent: number) =>
127+
token.balance && percent > 0 ? token.balance : '0',
128+
),
129+
useBatchSellQuoteRequest: jest.fn(() => mockUpdateBatchSellQuoteParams),
130+
}));
131+
92132
jest.mock('./BatchSellReviewTokenRow', () => {
93133
const ReactActual = jest.requireActual('react');
94134
const { Pressable, Text, View } = jest.requireActual('react-native');
@@ -137,6 +177,10 @@ describe('BatchSellReview', () => {
137177
mockSelectedDestinationToken = usdcToken;
138178
mockDestinationTokens = [usdcToken];
139179
mockBatchSellSlippages = {};
180+
mockBatchSellSourceTokenAmounts = {
181+
'eip155:1/erc20:0x1111111111111111111111111111111111111111': '1.498',
182+
'eip155:1/erc20:0x2222222222222222222222222222222222222222': '154.297',
183+
};
140184
});
141185

142186
it('renders the quote loading screen', () => {
@@ -310,13 +354,27 @@ describe('BatchSellReview', () => {
310354
});
311355
});
312356

313-
it('resets bridge state on unmount', () => {
357+
it('updates Batch Sell quote params when Redux inputs are present', () => {
358+
render(<BatchSellReview />);
359+
360+
expect(mockUpdateBatchSellQuoteParams).toHaveBeenCalled();
361+
});
362+
363+
it('cancels the Batch Sell quote params update on unmount', () => {
364+
const { unmount } = render(<BatchSellReview />);
365+
366+
unmount();
367+
368+
expect(mockCancelBatchSellQuoteParams).toHaveBeenCalled();
369+
});
370+
371+
it('leaves bridge state intact on unmount', () => {
314372
const { unmount } = render(<BatchSellReview />);
315373

316374
mockDispatch.mockClear();
317375
unmount();
318376

319-
expect(mockDispatch).toHaveBeenCalledWith({
377+
expect(mockDispatch).not.toHaveBeenCalledWith({
320378
type: 'bridge/resetBridgeState',
321379
});
322380
});

app/components/UI/Bridge/Views/BatchSellReview/BatchSellReview.tsx

Lines changed: 86 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useNavigation } from '@react-navigation/native';
2-
import React, { useCallback, useEffect, useState } from 'react';
2+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
33
import { ScrollView } from 'react-native-gesture-handler';
44
import { SafeAreaView } from 'react-native-safe-area-context';
55
import { useDispatch, useSelector } from 'react-redux';
@@ -29,12 +29,14 @@ import { strings } from '../../../../../../locales/i18n';
2929
import Routes from '../../../../../constants/navigation/Routes';
3030
import { Skeleton } from '../../../../../component-library/components-temp/Skeleton';
3131
import {
32-
resetBridgeState,
3332
selectBatchSellSlippages,
3433
selectBatchSellDestToken,
3534
selectBatchSellDestStablecoins,
35+
selectBatchSellSourceTokenAmounts,
3636
selectBatchSellSourceTokens,
3737
setBatchSellDestToken,
38+
setBatchSellSourceTokenAmount,
39+
setBatchSellSourceTokenAmounts,
3840
setBatchSellSourceTokens,
3941
setBatchSellTokenSlippages,
4042
} from '../../../../../core/redux/slices/bridge';
@@ -49,11 +51,14 @@ import {
4951
import { BatchSellFinalReviewSourceTokenData } from '../../components/BatchSellFinalReviewModal/BatchSellFinalReviewModal.types';
5052
import { BatchSellReviewSelectorsIDs } from './BatchSellReview.testIds';
5153
import { BatchSellReviewTokenRow } from './BatchSellReviewTokenRow';
54+
import {
55+
getBatchSellAtomicSourceAmount,
56+
getBatchSellSourceTokenAmount,
57+
useBatchSellQuoteRequest,
58+
} from '../../hooks/useBatchSellQuoteRequest';
5259

5360
const DEFAULT_PERCENT = 100;
5461
const UNKNOWN_DESTINATION_TOKEN_SYMBOL = 'UNKNOWN';
55-
// TODO(SWAPS-4439): When Batch Sell quote fetching is wired, pass
56-
// batchSellSlippages[assetId] into each token's BridgeController quote request.
5762
const HAS_QUOTES = true;
5863
const QUOTE_DETAILS_PLACEHOLDER_AMOUNT = '--';
5964
const NETWORK_FEE_PLACEHOLDER = '1.20 USDC';
@@ -75,7 +80,7 @@ function getSourceTokenData(
7580
return sourceTokenData;
7681
}
7782

78-
function areBatchSellSlippageMapsEqual(
83+
function areBatchSellValueMapsEqual(
7984
first: Record<string, string | undefined>,
8085
second: Record<string, string | undefined>,
8186
) {
@@ -103,10 +108,29 @@ export function BatchSellReview() {
103108
);
104109
const selectedDestinationToken = useSelector(selectBatchSellDestToken);
105110
const batchSellSlippages = useSelector(selectBatchSellSlippages);
111+
const batchSellSourceTokenAmounts = useSelector(
112+
selectBatchSellSourceTokenAmounts,
113+
);
106114
const isRemoveTokenDisabled = selectedTokens.length <= 2;
107115
const [percentsByTokenKey, setPercentsByTokenKey] = useState<
108116
Record<string, number>
109117
>({});
118+
const updateBatchSellQuoteParams = useBatchSellQuoteRequest();
119+
const hasValidBatchSellInputs = useMemo(
120+
() =>
121+
Boolean(selectedDestinationToken) &&
122+
selectedTokens.some((token) => {
123+
const assetId = getBridgeTokenAssetId(token);
124+
return (
125+
assetId &&
126+
getBatchSellAtomicSourceAmount(
127+
token,
128+
batchSellSourceTokenAmounts[assetId],
129+
)
130+
);
131+
}),
132+
[batchSellSourceTokenAmounts, selectedDestinationToken, selectedTokens],
133+
);
110134

111135
// Seed the selected destination token on entry so the pill always reads from Redux.
112136
useEffect(() => {
@@ -126,13 +150,47 @@ export function BatchSellReview() {
126150
);
127151
}, [selectedTokens]);
128152

129-
// Reset bridge state when component unmounts.
130-
useEffect(
131-
() => () => {
132-
dispatch(resetBridgeState());
133-
},
134-
[dispatch],
135-
);
153+
useEffect(() => {
154+
if (hasValidBatchSellInputs) {
155+
updateBatchSellQuoteParams();
156+
}
157+
158+
return () => {
159+
updateBatchSellQuoteParams.cancel();
160+
};
161+
}, [hasValidBatchSellInputs, updateBatchSellQuoteParams]);
162+
163+
useEffect(() => {
164+
const nextSourceTokenAmounts = selectedTokens.reduce<
165+
Record<string, string | undefined>
166+
>((sourceAmountsByAssetId, token) => {
167+
const assetId = getBridgeTokenAssetId(token);
168+
169+
if (!assetId) return sourceAmountsByAssetId;
170+
171+
sourceAmountsByAssetId[assetId] =
172+
batchSellSourceTokenAmounts[assetId] ??
173+
getBatchSellSourceTokenAmount(
174+
token,
175+
percentsByTokenKey[getTokenKey(token)] ?? DEFAULT_PERCENT,
176+
);
177+
return sourceAmountsByAssetId;
178+
}, {});
179+
180+
if (
181+
!areBatchSellValueMapsEqual(
182+
batchSellSourceTokenAmounts,
183+
nextSourceTokenAmounts,
184+
)
185+
) {
186+
dispatch(setBatchSellSourceTokenAmounts(nextSourceTokenAmounts));
187+
}
188+
}, [
189+
batchSellSourceTokenAmounts,
190+
dispatch,
191+
percentsByTokenKey,
192+
selectedTokens,
193+
]);
136194

137195
useEffect(() => {
138196
// Keep Redux slippages aligned with selected tokens when the user removes tokens.
@@ -150,7 +208,7 @@ export function BatchSellReview() {
150208
return slippageByAssetId;
151209
}, {});
152210

153-
if (!areBatchSellSlippageMapsEqual(batchSellSlippages, nextSlippage)) {
211+
if (!areBatchSellValueMapsEqual(batchSellSlippages, nextSlippage)) {
154212
dispatch(setBatchSellTokenSlippages(nextSlippage));
155213
}
156214
}, [batchSellSlippages, dispatch, selectedTokens]);
@@ -161,8 +219,22 @@ export function BatchSellReview() {
161219
...currentPercents,
162220
[tokenKey]: percent,
163221
}));
222+
223+
const token = selectedTokens.find(
224+
(selectedToken) => getTokenKey(selectedToken) === tokenKey,
225+
);
226+
const assetId = token ? getBridgeTokenAssetId(token) : undefined;
227+
228+
if (!token || !assetId) return;
229+
230+
dispatch(
231+
setBatchSellSourceTokenAmount({
232+
assetId,
233+
amount: getBatchSellSourceTokenAmount(token, percent),
234+
}),
235+
);
164236
},
165-
[],
237+
[dispatch, selectedTokens],
166238
);
167239

168240
const handleBackPress = useCallback(() => {

app/components/UI/Bridge/Views/BatchSellTokenSelect/BatchSellTokenSelect.test.tsx

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
} from './BatchSellTokenSelect.utils';
1717
import Routes from '../../../../../constants/navigation/Routes';
1818
import { BridgeTokenMetadata } from '../../constants/tokens';
19+
import { DEFAULT_BATCH_SELL_SLIPPAGE } from '../../components/SlippageModal/utils';
1920
import {
2021
TextColor as ComponentLibraryTextColor,
2122
TextVariant as ComponentLibraryTextVariant,
@@ -119,6 +120,22 @@ jest.mock('../../../../../core/redux/slices/bridge', () => ({
119120
type: 'bridge/setBatchSellSourceTokens',
120121
payload: tokens,
121122
})),
123+
setBatchSellSourceTokenAmounts: jest.fn(
124+
(amounts: Partial<Record<CaipAssetType, string | undefined>>) => ({
125+
type: 'bridge/setBatchSellSourceTokenAmounts',
126+
payload: amounts,
127+
}),
128+
),
129+
setBatchSellDestToken: jest.fn((token: BridgeToken | undefined) => ({
130+
type: 'bridge/setBatchSellDestToken',
131+
payload: token,
132+
})),
133+
setBatchSellTokenSlippages: jest.fn(
134+
(slippages: Partial<Record<CaipAssetType, string | undefined>>) => ({
135+
type: 'bridge/setBatchSellTokenSlippages',
136+
payload: slippages,
137+
}),
138+
),
122139
}));
123140

124141
jest.mock('../../components/TokenSelectorItem', () => {
@@ -402,16 +419,17 @@ describe('BatchSellTokenSelect', () => {
402419
expect(queryByText('USDC')).not.toBeOnTheScreen();
403420
});
404421

405-
it('resets bridge state on unmount', () => {
422+
it('resets bridge state on mount', () => {
406423
const { unmount } = render(<BatchSellTokenSelect />);
407424

408-
expect(mockDispatch).not.toHaveBeenCalledWith({
425+
expect(mockDispatch).toHaveBeenCalledWith({
409426
type: 'bridge/resetBridgeState',
410427
});
411428

429+
mockDispatch.mockClear();
412430
unmount();
413431

414-
expect(mockDispatch).toHaveBeenCalledWith({
432+
expect(mockDispatch).not.toHaveBeenCalledWith({
415433
type: 'bridge/resetBridgeState',
416434
});
417435
});
@@ -841,12 +859,15 @@ describe('BatchSellTokenSelect', () => {
841859
});
842860
});
843861

844-
it('dispatches selected source tokens for multi-token handoff', () => {
862+
it('dispatches Batch Sell Redux handoff data for multi-token Continue', () => {
863+
const stablecoinAssetId =
864+
'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' as CaipAssetType;
845865
const firstToken = createToken({ symbol: 'ONE' });
846866
const secondToken = createToken({
847867
symbol: 'TWO',
848868
address: '0x2222222222222222222222222222222222222222',
849869
});
870+
mockDestinationStablecoins = [BridgeTokenMetadata[stablecoinAssetId]];
850871
mockWalletTokens = [firstToken, secondToken];
851872

852873
const { getByTestId, getByText } = render(<BatchSellTokenSelect />);
@@ -855,12 +876,33 @@ describe('BatchSellTokenSelect', () => {
855876
fireEvent.press(getByText('TWO'));
856877
expect(getByText('Continue with (2) tokens')).toBeOnTheScreen();
857878

879+
mockDispatch.mockClear();
858880
fireEvent.press(getByTestId(BatchSellTokenSelectSelectorsIDs.NEXT_BUTTON));
859881

860-
expect(mockDispatch).toHaveBeenCalledWith({
882+
expect(mockDispatch).toHaveBeenNthCalledWith(1, {
861883
type: 'bridge/setBatchSellSourceTokens',
862884
payload: [firstToken, secondToken],
863885
});
886+
expect(mockDispatch).toHaveBeenNthCalledWith(2, {
887+
type: 'bridge/setBatchSellSourceTokenAmounts',
888+
payload: {
889+
'eip155:1/erc20:0x1111111111111111111111111111111111111111': '1',
890+
'eip155:1/erc20:0x2222222222222222222222222222222222222222': '1',
891+
},
892+
});
893+
expect(mockDispatch).toHaveBeenNthCalledWith(3, {
894+
type: 'bridge/setBatchSellDestToken',
895+
payload: BridgeTokenMetadata[stablecoinAssetId],
896+
});
897+
expect(mockDispatch).toHaveBeenNthCalledWith(4, {
898+
type: 'bridge/setBatchSellTokenSlippages',
899+
payload: {
900+
'eip155:1/erc20:0x1111111111111111111111111111111111111111':
901+
DEFAULT_BATCH_SELL_SLIPPAGE,
902+
'eip155:1/erc20:0x2222222222222222222222222222222222222222':
903+
DEFAULT_BATCH_SELL_SLIPPAGE,
904+
},
905+
});
864906
expect(mockNavigate).toHaveBeenCalledWith(Routes.BRIDGE.BATCH_SELL_REVIEW);
865907
});
866908
});

0 commit comments

Comments
 (0)