Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions app/components/Nav/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,8 @@ import Toast, {
} from '../../../component-library/components/Toast';
import AccountSelector from '../../../components/Views/AccountSelector';
import AddressSelector from '../../../components/Views/AddressSelector';
import { TokenSortBottomSheet } from '../../../components/UI/Tokens/TokensBottomSheet/TokenSortBottomSheet';
import { TokenSortBottomSheet } from '../../UI/Tokens/TokenSortBottomSheet/TokenSortBottomSheet';
import ProfilerManager from '../../../components/UI/ProfilerManager';
import { TokenFilterBottomSheet } from '../../../components/UI/Tokens/TokensBottomSheet/TokenFilterBottomSheet';
import NetworkManager from '../../../components/UI/NetworkManager';
import { AccountPermissionsScreens } from '../../../components/Views/AccountPermissions/AccountPermissions.types';
import AccountPermissionsConfirmRevokeAll from '../../../components/Views/AccountPermissions/AccountPermissionsConfirmRevokeAll';
Expand Down Expand Up @@ -485,10 +484,6 @@ const RootModalFlow = (props: RootModalFlowProps) => (
name={Routes.SHEET.TOKEN_SORT}
component={TokenSortBottomSheet}
/>
<Stack.Screen
name={Routes.SHEET.TOKEN_FILTER}
component={TokenFilterBottomSheet}
/>
<Stack.Screen
name={Routes.SHEET.NETWORK_MANAGER}
component={NetworkManager}
Expand Down
2 changes: 1 addition & 1 deletion app/components/UI/AssetOverview/Balance/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { MOCK_VAULT_APY_AVERAGES } from '../../Stake/components/PoolStakingLearn
import { TokenI } from '../../Tokens/types';
import { NetworkBadgeSource } from './Balance';
import renderWithProvider from '../../../../util/test/renderWithProvider';
import { ACCOUNT_TYPE_LABEL_TEST_ID } from '../../Tokens/TokenList/TokenListItem/TokenListItemBip44';
import { ACCOUNT_TYPE_LABEL_TEST_ID } from '../../Tokens/TokenList/TokenListItem/TokenListItem';
import { BtcAccountType } from '@metamask/keyring-api';

jest.mock('../../../../../locales/i18n', () => ({
Expand Down
54 changes: 29 additions & 25 deletions app/components/UI/Bridge/hooks/useRecipientInitialization.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useCallback } from 'react';
import { useEffect, useCallback, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
selectDestAddress,
Expand All @@ -7,11 +7,8 @@ import {
} from '../../../../core/redux/slices/bridge';
import { CaipAccountId, parseCaipAccountId } from '@metamask/utils';
import { selectSelectedAccountGroup } from '../../../../selectors/multichainAccounts/accountTreeController';
import {
isNonEvmAddress,
isNonEvmChainId,
} from '../../../../core/Multichain/utils';
import { useDestinationAccounts } from './useDestinationAccounts';
import { areAddressesEqual } from '../../../../util/address';

export const useRecipientInitialization = (
hasInitializedRecipient: React.MutableRefObject<boolean>,
Expand All @@ -33,32 +30,39 @@ export const useRecipientInitialization = (
[dispatch],
);

// Check if current destAddress is a valid destination account for the current destination chain
// This properly handles switching between different non-EVM chains (e.g., BTC → SOL)
// by checking if the address exists in the filtered destination accounts list
const isDestAddressValidForDestChain = useMemo(() => {
if (
!destAddress ||
!destToken?.chainId ||
destinationAccounts.length === 0
) {
return false;
}

// Check if the current destAddress matches any of the valid destination accounts
// destinationAccounts is already filtered by selectValidDestInternalAccountIds
// which uses account scopes to filter for the specific destination chain
return destinationAccounts.some((account) =>
areAddressesEqual(account.address, destAddress),
);
}, [destAddress, destToken?.chainId, destinationAccounts]);

// Initialize default recipient account
useEffect(() => {
// Only initialize if we haven't done so before, or if the current address doesn't match the network type
if (destinationAccounts.length === 0) {
return;
}

// Check if current destAddress matches the destination chain type
const isDestChainNonEvm =
destToken?.chainId && isNonEvmChainId(destToken.chainId);
const isDestAddressNonEvm = destAddress && isNonEvmAddress(destAddress);

// Address format should match the destination chain type:
// - If dest chain is non-EVM (e.g., Solana, Bitcoin), dest address should be non-EVM
// - If dest chain is EVM, dest address should be EVM
const doesDestAddrMatchNetworkType =
destAddress &&
destToken?.chainId &&
isDestChainNonEvm === isDestAddressNonEvm;

// Only initialize in these specific cases:
// 1. Never initialized AND no destAddress set
// 2. destAddress doesn't match the current network type (user switched networks)
const shouldInitialize =
(!hasInitializedRecipient.current && !destAddress) ||
!doesDestAddrMatchNetworkType;
// Initialize/reinitialize in these cases:
// 1. No destAddress is set (missing or cleared)
// 2. destAddress is not valid for the current destination chain (user switched networks)
// This handles switching between different non-EVM chains (e.g., BTC → SOL)
// Note: isDestAddressValidForDestChain returns false when destAddress is falsy,
const shouldInitialize = !isDestAddressValidForDestChain;

if (shouldInitialize) {
// Find an account from the currently selected account group that supports the destination network
Expand All @@ -78,10 +82,10 @@ export const useRecipientInitialization = (
}
}, [
destAddress,
destToken,
destinationAccounts,
handleSelectAccount,
currentlySelectedAccount,
hasInitializedRecipient,
isDestAddressValidForDestChain,
]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ import { TokenI } from '../../../Tokens/types';
// Mock dependencies
jest.mock('../../../../../util/networks');
jest.mock('../../../../../util/networks/customNetworks');
jest.mock(
'../../../Tokens/TokenList/TokenListItem/CustomNetworkNativeImgMapping',
);
jest.mock('../../../../Base/RemoteImage', () => 'RemoteImage');

import {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,7 @@ jest.mock('../../hooks/useNetworksByNamespace/useNetworksByNamespace', () => ({
},
}));

jest.mock('../Tokens/TokensBottomSheet', () => ({
createTokenBottomSheetFilterNavDetails: () => [
'RootModalFlow',
{ screen: 'TokenFilter' },
],
jest.mock('../Tokens/TokenSortBottomSheet/TokenSortBottomSheet', () => ({
createTokensBottomSheetNavDetails: () => [
'RootModalFlow',
{ screen: 'TokensBottomSheet' },
Expand Down
12 changes: 6 additions & 6 deletions app/components/UI/Perps/hooks/usePerpsHomeData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,17 @@ export const usePerpsHomeData = ({
// REST API fills state - WebSocket snapshot only contains recent fills,
// so we need to fetch complete history via REST API
const [restFills, setRestFills] = useState<OrderFill[]>([]);
const [isRestFillsLoading, setIsRestFillsLoading] = useState(true);

// Fetch historical fills via REST API on mount
// Fetch historical fills via REST API on mount (background, non-blocking)
// This ensures we have complete fill history, not just WebSocket snapshot
// Note: We don't track loading state - WebSocket data displays immediately,
// REST fills merge silently in the background via mergedFills
useEffect(() => {
const fetchFills = async () => {
try {
const controller = Engine.context.PerpsController;
const provider = controller?.getActiveProvider();
if (!provider) {
setIsRestFillsLoading(false);
return;
}

Expand All @@ -106,8 +106,6 @@ export const usePerpsHomeData = ({
} catch (error) {
// Log error but don't fail - WebSocket fills still work
console.error('[usePerpsHomeData] Failed to fetch REST fills:', error);
} finally {
setIsRestFillsLoading(false);
}
};
fetchFills();
Expand Down Expand Up @@ -372,7 +370,9 @@ export const usePerpsHomeData = ({
positions: isPositionsLoading,
orders: isOrdersLoading,
markets: isMarketsLoading,
activity: isFillsLoading || isRestFillsLoading,
// Only wait for WebSocket fills (fast ~100ms), not REST fills (slow 3s+)
// REST fills merge in background via mergedFills without blocking initial render
activity: isFillsLoading,
},
refresh,
};
Expand Down
38 changes: 38 additions & 0 deletions app/components/UI/Perps/providers/PerpsStreamManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,8 @@ class PositionStreamChannel extends StreamChannel<Position[]> {

// Specific channel for fills
class FillStreamChannel extends StreamChannel<OrderFill[]> {
private prewarmUnsubscribe?: () => void;

protected connect() {
if (this.wsSubscription) return;

Expand Down Expand Up @@ -716,6 +718,42 @@ class FillStreamChannel extends StreamChannel<OrderFill[]> {
protected getClearedData(): OrderFill[] {
return [];
}

/**
* Pre-warm the channel by creating a persistent subscription
* This keeps the WebSocket connection alive and caches fills data continuously
* @returns Cleanup function to call when leaving Perps environment
*/
public prewarm(): () => void {
if (this.prewarmUnsubscribe) {
DevLogger.log('FillStreamChannel: Already pre-warmed');
return this.prewarmUnsubscribe;
}

// Create a real subscription with no-op callback to keep connection alive
this.prewarmUnsubscribe = this.subscribe({
callback: () => {
// No-op callback - just keeps the connection alive for caching
},
throttleMs: 0, // No throttle for pre-warm
});

// Return cleanup function that clears internal state
return () => {
DevLogger.log('FillStreamChannel: Cleaning up prewarm subscription');
this.cleanupPrewarm();
};
}

/**
* Cleanup pre-warm subscription
*/
public cleanupPrewarm(): void {
if (this.prewarmUnsubscribe) {
this.prewarmUnsubscribe();
this.prewarmUnsubscribe = undefined;
}
}
}

// Specific channel for account state
Expand Down
2 changes: 2 additions & 0 deletions app/components/UI/Perps/services/PerpsConnectionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,7 @@ class PerpsConnectionManagerClass {
const accountCleanup = streamManager.account.prewarm();
const marketDataCleanup = streamManager.marketData.prewarm();
const oiCapCleanup = streamManager.oiCaps.prewarm();
const fillsCleanup = streamManager.fills.prewarm();

// Portfolio balance updates are now handled by usePerpsPortfolioBalance via usePerpsLiveAccount

Expand All @@ -882,6 +883,7 @@ class PerpsConnectionManagerClass {
accountCleanup,
marketDataCleanup,
oiCapCleanup,
fillsCleanup,
priceCleanup,
);

Expand Down
10 changes: 4 additions & 6 deletions app/components/UI/Ramp/Deposit/sdk/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
DepositSDKContext,
DepositSDKProvider,
useDepositSDK,
DEPOSIT_ENVIRONMENT,
} from '.';
import { backgroundState } from '../../../../../util/test/initial-root-state';
import {
Expand All @@ -15,11 +16,7 @@ import {
} from '../testUtils';
import renderWithProvider from '../../../../../util/test/renderWithProvider';

import {
NativeRampsSdk,
SdkEnvironment,
Context,
} from '@consensys/native-ramps-sdk';
import { NativeRampsSdk, Context } from '@consensys/native-ramps-sdk';

const mockDispatch = jest.fn();
jest.mock('react-redux', () => ({
Expand Down Expand Up @@ -157,8 +154,9 @@ describe('Deposit SDK Context', () => {
{
apiKey: 'test-provider-api-key',
context: Context.MobileIOS,
locale: 'en',
},
SdkEnvironment.Staging,
DEPOSIT_ENVIRONMENT,
);
});
});
Expand Down
23 changes: 21 additions & 2 deletions app/components/UI/Ramp/Deposit/sdk/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
setFiatOrdersPaymentMethodDeposit,
} from '../../../../../reducers/fiatOrders';
import Logger from '../../../../../util/Logger';
import { strings } from '../../../../../../locales/i18n';
import I18n, { I18nEvents, strings } from '../../../../../../locales/i18n';
import useRampAccountAddress from '../../hooks/useRampAccountAddress';
import { DepositNavigationParams } from '../types';

Expand Down Expand Up @@ -73,10 +73,15 @@ export const DEPOSIT_ENVIRONMENT = environment;
export const DepositSDKNoAuth = new NativeRampsSdk(
{
context,
locale: I18n.locale,
},
environment,
);

I18nEvents.addListener('localeChanged', (locale) => {
DepositSDKNoAuth.setLocale(locale);
});

export const DepositSDKContext = createContext<DepositSDK | undefined>(
undefined,
);
Expand Down Expand Up @@ -151,6 +156,7 @@ export const DepositSDKProvider = ({
{
apiKey: providerApiKey,
context,
locale: I18n.locale,
},
environment,
);
Expand All @@ -161,6 +167,19 @@ export const DepositSDKProvider = ({
}
}, [providerApiKey]);

// Listen for locale changes and update SDK locale
useEffect(() => {
if (!sdk) return;

const handleLocaleChange = (locale: string) => {
sdk.setLocale(locale);
};
I18nEvents.addListener('localeChanged', handleLocaleChange);
return () => {
I18nEvents.removeListener('localeChanged', handleLocaleChange);
};
}, [sdk]);

useEffect(() => {
if (sdk && authToken) {
sdk.setAccessToken(authToken);
Expand Down Expand Up @@ -216,7 +235,7 @@ export const DepositSDKProvider = ({
? await sdk.logout()
: await sdk
.logout()
.catch((error) =>
.catch((error: Error) =>
Logger.error(
error as Error,
'SDK logout failed but invalidation was not required. Error:',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const mockSelectAdditionalNetworksBlacklistFeatureFlag =
jest.mock('../../../../../../../../locales/i18n', () => ({
strings: jest.fn((key: string) => {
const mockStrings: Record<string, string> = {
'rewards.ways_to_earn.supported_networks': 'Supported Networks',
'rewards.ways_to_earn.supported_networks': 'Supported networks',
};
return mockStrings[key] || key;
}),
Expand Down Expand Up @@ -139,7 +139,7 @@ describe('SwapSupportedNetworksSection', () => {
const { getByText } = render(<SwapSupportedNetworksSection />);

// Assert
expect(getByText('Supported Networks')).toBeOnTheScreen();
expect(getByText('Supported networks')).toBeOnTheScreen();
});

it('renders supported networks', () => {
Expand Down
Loading
Loading