Skip to content

Commit 4956b5e

Browse files
fix: chains with only batchSell stablecoins in wallet should not appear
1 parent 10cc80f commit 4956b5e

5 files changed

Lines changed: 205 additions & 39 deletions

File tree

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

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ const mockDispatch = jest.fn();
2525
const mockNavigate = jest.fn();
2626
const mockUseTokensWithBalance = jest.fn();
2727
let mockDestinationStablecoins: BridgeToken[] = [];
28+
let mockDestinationStablecoinsByChain: Partial<
29+
Record<CaipChainId, BridgeToken[]>
30+
> = {};
2831
let mockWalletTokens: BridgeToken[] = [];
2932
let mockPricePercentChangesByAddress: Record<string, number | undefined> = {};
3033
let mockTokenMarketData: Record<
@@ -106,6 +109,9 @@ jest.mock('../../../../../selectors/networkController', () => ({
106109

107110
jest.mock('../../../../../core/redux/slices/bridge', () => ({
108111
selectBatchSellDestStablecoins: jest.fn(() => mockDestinationStablecoins),
112+
selectBatchSellDestStablecoinsByChain: jest.fn(
113+
() => mockDestinationStablecoinsByChain,
114+
),
109115
setBatchSellDestToken: jest.fn((token: BridgeToken | undefined) => ({
110116
type: 'bridge/setBatchSellDestToken',
111117
payload: token,
@@ -199,7 +205,9 @@ describe('filterBatchSellDestinationStablecoins', () => {
199205

200206
const result = removeStablecoinsFromSourceTokens({
201207
tokens: [lowBalanceToken, stablecoinToken, highBalanceToken],
202-
stablecoins: [BridgeTokenMetadata[usdcAssetId]],
208+
stablecoinsByChain: {
209+
['eip155:1' as CaipChainId]: [BridgeTokenMetadata[usdcAssetId]],
210+
},
203211
});
204212

205213
expect(result.map((token) => token.symbol)).toEqual(['LOW', 'HIGH']);
@@ -341,6 +349,7 @@ describe('BatchSellTokenSelect', () => {
341349
beforeEach(() => {
342350
jest.clearAllMocks();
343351
mockDestinationStablecoins = [];
352+
mockDestinationStablecoinsByChain = {};
344353
mockPricePercentChangesByAddress = {};
345354
mockTokenMarketData = {};
346355
mockCurrencyRates = {
@@ -372,6 +381,9 @@ describe('BatchSellTokenSelect', () => {
372381
it('renders only eligible wallet tokens', () => {
373382
const stablecoinAddress = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48';
374383
mockDestinationStablecoins = [BridgeTokenMetadata[usdcAssetId]];
384+
mockDestinationStablecoinsByChain = {
385+
['eip155:1' as CaipChainId]: mockDestinationStablecoins,
386+
};
375387
mockWalletTokens = [
376388
createToken({ symbol: 'SELL', name: 'Sell Token' }),
377389
createToken({
@@ -655,6 +667,74 @@ describe('BatchSellTokenSelect', () => {
655667
).not.toBeOnTheScreen();
656668
});
657669

670+
it('shows the no sellable tokens empty state when wallet only has destination stablecoins', () => {
671+
const stablecoin = BridgeTokenMetadata[usdcAssetId];
672+
mockDestinationStablecoins = [stablecoin];
673+
mockDestinationStablecoinsByChain = {
674+
['eip155:1' as CaipChainId]: [stablecoin],
675+
};
676+
mockWalletTokens = [
677+
createToken({
678+
symbol: 'USDC',
679+
name: 'USD Coin',
680+
address: stablecoin.address,
681+
chainId: stablecoin.chainId as Hex,
682+
tokenFiatAmount: 100,
683+
}),
684+
];
685+
686+
const { getByTestId, queryByTestId, queryByText } = render(
687+
<BatchSellTokenSelect />,
688+
);
689+
690+
expect(
691+
getByTestId(BatchSellTokenSelectSelectorsIDs.EMPTY_STATE),
692+
).toBeOnTheScreen();
693+
expect(
694+
queryByTestId(getNetworkPillTestId('eip155:1' as CaipChainId)),
695+
).not.toBeOnTheScreen();
696+
expect(queryByText('USDC')).not.toBeOnTheScreen();
697+
expect(
698+
queryByTestId(BatchSellTokenSelectSelectorsIDs.NEXT_BUTTON),
699+
).not.toBeOnTheScreen();
700+
});
701+
702+
it('omits stablecoin-only network pills when another chain has a sellable token', () => {
703+
const stablecoin = BridgeTokenMetadata[usdcAssetId];
704+
mockDestinationStablecoinsByChain = {
705+
['eip155:1' as CaipChainId]: [stablecoin],
706+
};
707+
mockWalletTokens = [
708+
createToken({
709+
symbol: 'USDC',
710+
name: 'USD Coin',
711+
address: stablecoin.address,
712+
chainId: stablecoin.chainId as Hex,
713+
tokenFiatAmount: 100,
714+
}),
715+
createToken({
716+
symbol: 'BASEA',
717+
name: 'Base A',
718+
address: '0x2222222222222222222222222222222222222222',
719+
chainId: '0x2105' as Hex,
720+
tokenFiatAmount: 1,
721+
}),
722+
];
723+
724+
const { getByTestId, getByText, queryByTestId, queryByText } = render(
725+
<BatchSellTokenSelect />,
726+
);
727+
728+
expect(
729+
getByTestId(getNetworkPillTestId('eip155:8453' as CaipChainId)),
730+
).toBeOnTheScreen();
731+
expect(
732+
queryByTestId(getNetworkPillTestId('eip155:1' as CaipChainId)),
733+
).not.toBeOnTheScreen();
734+
expect(getByText('BASEA')).toBeOnTheScreen();
735+
expect(queryByText('USDC')).not.toBeOnTheScreen();
736+
});
737+
658738
it('navigates to Explore Tokens from the empty state', () => {
659739
mockWalletTokens = [];
660740

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

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { strings } from '../../../../../../locales/i18n';
3232
import Routes from '../../../../../constants/navigation/Routes';
3333
import {
3434
selectBatchSellDestStablecoins,
35+
selectBatchSellDestStablecoinsByChain,
3536
setBatchSellDestToken,
3637
setBatchSellSourceTokens,
3738
} from '../../../../../core/redux/slices/bridge';
@@ -64,11 +65,20 @@ export function BatchSellTokenSelect() {
6465
const allWalletTokens = useTokensWithBalance({
6566
chainIds: SUPPORTED_BATCH_SELL_CHAIN_IDS,
6667
});
68+
const stablecoinsByChain = useSelector(selectBatchSellDestStablecoinsByChain);
6769
const [tokenSortDirection, setTokenSortDirection] =
6870
useState<BatchSellTokenSortDirection>('desc');
71+
const eligibleSourceTokens = useMemo(() => {
72+
const sourceTokens = removeStablecoinsFromSourceTokens({
73+
tokens: allWalletTokens,
74+
stablecoinsByChain,
75+
});
76+
77+
return sortBatchSellTokens(sourceTokens, tokenSortDirection);
78+
}, [allWalletTokens, stablecoinsByChain, tokenSortDirection]);
6979
const sortedEligibleChains = useMemo(
70-
() => buildBatchSellEligibleChains(allWalletTokens),
71-
[allWalletTokens],
80+
() => buildBatchSellEligibleChains(eligibleSourceTokens),
81+
[eligibleSourceTokens],
7282
);
7383
const [selectedChainId, setSelectedChainId] = useState<
7484
CaipChainId | undefined
@@ -106,24 +116,15 @@ export function BatchSellTokenSelect() {
106116
const destinationStablecoins = useSelector((state: RootState) =>
107117
selectBatchSellDestStablecoins(state, activeChainId),
108118
);
109-
const selectedChainTokens = useMemo(() => {
110-
const activeChainTokens = activeChainId
111-
? allWalletTokens.filter(
112-
(token) => formatChainIdToCaip(token.chainId) === activeChainId,
113-
)
114-
: allWalletTokens;
115-
const sourceTokens = removeStablecoinsFromSourceTokens({
116-
tokens: activeChainTokens,
117-
stablecoins: destinationStablecoins,
118-
});
119-
120-
return sortBatchSellTokens(sourceTokens, tokenSortDirection);
121-
}, [
122-
activeChainId,
123-
allWalletTokens,
124-
destinationStablecoins,
125-
tokenSortDirection,
126-
]);
119+
const selectedChainTokens = useMemo(
120+
() =>
121+
activeChainId
122+
? eligibleSourceTokens.filter(
123+
(token) => formatChainIdToCaip(token.chainId) === activeChainId,
124+
)
125+
: eligibleSourceTokens,
126+
[activeChainId, eligibleSourceTokens],
127+
);
127128
const selectedTokenKeys = useMemo(
128129
() => new Set(selectedTokens.map(getTokenKey)),
129130
[selectedTokens],

app/components/UI/Bridge/Views/BatchSellTokenSelect/BatchSellTokenSelect.utils.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,30 @@ export function getBatchSellDestinationToken(
4545

4646
export function removeStablecoinsFromSourceTokens({
4747
tokens,
48-
stablecoins,
48+
stablecoinsByChain,
4949
}: {
5050
tokens: BridgeToken[];
51-
stablecoins: BridgeToken[];
51+
stablecoinsByChain: Partial<Record<CaipChainId, BridgeToken[]>>;
5252
}): BridgeToken[] {
53-
const stablecoinAssetIds = new Set(
54-
stablecoins
55-
.map((stablecoin) => getBridgeTokenAssetId(stablecoin))
56-
.filter((assetId): assetId is CaipAssetType => Boolean(assetId)),
53+
const stablecoinAssetIdsByChain = new Map(
54+
Object.entries(stablecoinsByChain).map(([chainId, stablecoins]) => [
55+
chainId as CaipChainId,
56+
new Set(
57+
(stablecoins ?? [])
58+
.map((stablecoin) => getBridgeTokenAssetId(stablecoin))
59+
.filter((assetId): assetId is CaipAssetType => Boolean(assetId)),
60+
),
61+
]),
5762
);
5863

5964
return tokens.filter((token) => {
65+
const caipChainId = formatChainIdToCaip(token.chainId);
66+
const stablecoinAssetIds = stablecoinAssetIdsByChain.get(caipChainId);
67+
68+
if (!stablecoinAssetIds) {
69+
return true;
70+
}
71+
6072
const assetId = getBridgeTokenAssetId(token);
6173

6274
if (!assetId) {

app/core/redux/slices/bridge/index.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import reducer, {
2727
setBatchSellDestToken,
2828
selectBatchSellDestToken,
2929
selectBatchSellDestStablecoins,
30+
selectBatchSellDestStablecoinsByChain,
3031
} from '.';
3132
import { FEATURE_FLAG_NAME } from '../../../../selectors/featureFlagController/rwa';
3233
import {
@@ -710,6 +711,54 @@ describe('bridge slice', () => {
710711
]);
711712
});
712713

714+
it('returns configured stablecoins by chain with local metadata', () => {
715+
const mockState = cloneDeep(mockRootState);
716+
const ethUsdc =
717+
'eip155:1/erc20:0xA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48' as CaipAssetType;
718+
const baseUsdc =
719+
'eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913' as CaipAssetType;
720+
const unknownStablecoin =
721+
'eip155:1/erc20:0x0000000000000000000000000000000000000001' as CaipAssetType;
722+
723+
mockState.engine.backgroundState.RemoteFeatureFlagController.remoteFeatureFlags.bridgeConfigV2.chains[
724+
'eip155:1'
725+
] = {
726+
...mockState.engine.backgroundState.RemoteFeatureFlagController
727+
.remoteFeatureFlags.bridgeConfigV2.chains['eip155:1'],
728+
batchSellDestStablecoins: [unknownStablecoin, ethUsdc],
729+
} as unknown as any;
730+
mockState.engine.backgroundState.RemoteFeatureFlagController.remoteFeatureFlags.bridgeConfigV2.chains[
731+
'eip155:8453'
732+
] = {
733+
...mockState.engine.backgroundState.RemoteFeatureFlagController
734+
.remoteFeatureFlags.bridgeConfigV2.chains['eip155:8453'],
735+
isActiveSrc: true,
736+
isActiveDest: true,
737+
isGaslessSwapEnabled: false,
738+
batchSellDestStablecoins: [baseUsdc],
739+
} as unknown as any;
740+
741+
const expectedEthUsdc =
742+
BridgeTokenMetadata[
743+
'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' as CaipAssetType
744+
];
745+
const expectedBaseUsdc =
746+
BridgeTokenMetadata[
747+
'eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913' as CaipAssetType
748+
];
749+
750+
const result = selectBatchSellDestStablecoinsByChain(
751+
mockState as unknown as RootState,
752+
);
753+
754+
expect(result).toEqual(
755+
expect.objectContaining({
756+
'eip155:1': [expectedEthUsdc],
757+
'eip155:8453': [expectedBaseUsdc],
758+
}),
759+
);
760+
});
761+
713762
it('returns an empty array when no chain is selected', () => {
714763
const result = selectBatchSellDestStablecoins(
715764
mockRootState as unknown as RootState,

app/core/redux/slices/bridge/index.ts

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,41 @@ function getBridgeTokenMetadata(
367367
return metadataAssetId ? BridgeTokenMetadata[metadataAssetId] : undefined;
368368
}
369369

370+
function getBatchSellDestStablecoinMetadata(
371+
stablecoinAssetIds: CaipAssetType[],
372+
): BridgeToken[] {
373+
return stablecoinAssetIds.reduce<BridgeToken[]>(
374+
(stablecoins, stablecoinAssetId) => {
375+
const tokenMetadata = getBridgeTokenMetadata(stablecoinAssetId);
376+
377+
if (tokenMetadata) {
378+
stablecoins.push(tokenMetadata);
379+
}
380+
381+
return stablecoins;
382+
},
383+
[],
384+
);
385+
}
386+
387+
export const selectBatchSellDestStablecoinsByChain = createSelector(
388+
selectBridgeFeatureFlags,
389+
(bridgeFeatureFlags): Partial<Record<CaipChainId, BridgeToken[]>> =>
390+
Object.entries(bridgeFeatureFlags.chains ?? {}).reduce<
391+
Partial<Record<CaipChainId, BridgeToken[]>>
392+
>((stablecoinsByChain, [chainId, chainConfig]) => {
393+
const stablecoins = getBatchSellDestStablecoinMetadata(
394+
chainConfig.batchSellDestStablecoins ?? [],
395+
);
396+
397+
if (stablecoins.length > 0) {
398+
stablecoinsByChain[chainId as CaipChainId] = stablecoins;
399+
}
400+
401+
return stablecoinsByChain;
402+
}, {}),
403+
);
404+
370405
export const selectBatchSellDestStablecoins = createSelector(
371406
selectBridgeFeatureFlags,
372407
(_state: RootState, chainId?: BridgeToken['chainId']) =>
@@ -379,18 +414,7 @@ export const selectBatchSellDestStablecoins = createSelector(
379414
const batchSellDestStablecoins =
380415
bridgeFeatureFlags.chains?.[chainId]?.batchSellDestStablecoins ?? [];
381416

382-
return batchSellDestStablecoins.reduce<BridgeToken[]>(
383-
(stablecoins, stablecoinAssetId) => {
384-
const tokenMetadata = getBridgeTokenMetadata(stablecoinAssetId);
385-
386-
if (tokenMetadata) {
387-
stablecoins.push(tokenMetadata);
388-
}
389-
390-
return stablecoins;
391-
},
392-
[],
393-
);
417+
return getBatchSellDestStablecoinMetadata(batchSellDestStablecoins);
394418
},
395419
);
396420

0 commit comments

Comments
 (0)