Skip to content
Closed
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
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,8 @@ android {
applicationId "io.metamask"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.64.0"
versionCode 3646
versionName "7.64.2"
versionCode 3667
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST"
Expand Down
200 changes: 175 additions & 25 deletions app/core/redux/slices/bridge/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import reducer, {
selectIsBridgeEnabledDest,
selectIsSwapsLive,
selectDestChainRanking,
selectSourceChainRanking,
} from '.';
import {
BridgeToken,
Expand Down Expand Up @@ -531,25 +532,28 @@ describe('bridge slice', () => {
});

it('returns false when bridge is not enabled as source for the chain', () => {
const mockState = cloneDeep(mockRootState) as unknown as RootState;
// @ts-expect-error - Mock state has correct structure at runtime
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
mockState.engine.backgroundState.RemoteFeatureFlagController.remoteFeatureFlags.bridgeConfigV2!.chains[
'eip155:1'
].isActiveSrc = false;
const mockState = cloneDeep(mockRootState);
// Remove chain from chainRanking to disable it (chainRanking presence = enabled)
mockState.engine.backgroundState.RemoteFeatureFlagController.remoteFeatureFlags.bridgeConfigV2.chainRanking =
mockState.engine.backgroundState.RemoteFeatureFlagController.remoteFeatureFlags.bridgeConfigV2.chainRanking.filter(
(chain) => chain.chainId !== 'eip155:1',
);

const result = selectIsBridgeEnabledSource(mockState, '0x1');
const result = selectIsBridgeEnabledSource(
mockState as unknown as RootState,
'0x1',
);

expect(result).toBe(false);
});

it('returns undefined when chain is not in bridge config', () => {
it('returns false when chain is not in bridge config', () => {
const result = selectIsBridgeEnabledSource(
mockRootState as unknown as RootState,
'0x999' as Hex,
);

expect(result).toBeUndefined();
expect(result).toBe(false);
});
});

Expand Down Expand Up @@ -597,6 +601,77 @@ describe('bridge slice', () => {
});
});

describe('selectSourceChainRanking', () => {
it('returns only supported and user-configured chains', () => {
const result = selectSourceChainRanking(
mockRootState as unknown as RootState,
);

// Should return chains that are both in ALLOWED_BRIDGE_CHAIN_IDS
// and in the user's configured networks
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBeGreaterThan(0);

// Ethereum (0x1) is allowed and configured in the mock state
expect(result.some((chain) => chain.chainId === 'eip155:1')).toBe(true);
});

it('filters out unsupported EVM chains from chainRanking', () => {
const mockState = cloneDeep(mockRootState);
mockState.engine.backgroundState.RemoteFeatureFlagController.remoteFeatureFlags.bridgeConfigV2.chainRanking =
[
...mockState.engine.backgroundState.RemoteFeatureFlagController
.remoteFeatureFlags.bridgeConfigV2.chainRanking,
{ chainId: 'eip155:99999', name: 'Unsupported EVM Chain' },
];

const result = selectSourceChainRanking(
mockState as unknown as RootState,
);

expect(result.some((chain) => chain.chainId === 'eip155:99999')).toBe(
false,
);
});

it('filters out unsupported non-EVM chains from chainRanking', () => {
const mockState = cloneDeep(mockRootState);
mockState.engine.backgroundState.RemoteFeatureFlagController.remoteFeatureFlags.bridgeConfigV2.chainRanking =
[
...mockState.engine.backgroundState.RemoteFeatureFlagController
.remoteFeatureFlags.bridgeConfigV2.chainRanking,
{ chainId: 'cosmos:cosmoshub-4', name: 'Unsupported Cosmos Chain' },
];

const result = selectSourceChainRanking(
mockState as unknown as RootState,
);

expect(
result.some((chain) => chain.chainId === 'cosmos:cosmoshub-4'),
).toBe(false);
});

it('filters out chains not in user-configured networks', () => {
const result = selectSourceChainRanking(
mockRootState as unknown as RootState,
);

// Optimism (0xa) is in chainRanking and ALLOWED_BRIDGE_CHAIN_IDS
// AND in the mock user's configured networks
const hasOptimism = result.some((chain) => chain.chainId === 'eip155:10');
expect(hasOptimism).toBe(true);

// Verify no chains appear that aren't in the user's configured networks
// The user only has Ethereum (0x1) and Optimism (0xa) configured as EVM networks
result.forEach((chain) => {
if (chain.chainId.startsWith('eip155:')) {
expect(['eip155:1', 'eip155:10']).toContain(chain.chainId);
}
});
});
});

describe('selectDestChainRanking', () => {
it('returns chainRanking from feature flags', () => {
const result = selectDestChainRanking(
Expand All @@ -622,16 +697,75 @@ describe('bridge slice', () => {
expect(hasEthereum).toBe(true);
});

it('returns all chains without filtering (unlike selectSourceChainRanking)', () => {
it('returns all supported chains without filtering by user-configured networks', () => {
const result = selectDestChainRanking(
mockRootState as unknown as RootState,
);

// selectDestChainRanking should return all chains from feature flags
// selectDestChainRanking should return all supported chains from feature flags
// This is the key difference from selectSourceChainRanking which filters
// by user-configured networks
expect(result.length).toBeGreaterThan(0);
});

it('filters out unsupported EVM chains not in ALLOWED_BRIDGE_CHAIN_IDS', () => {
const mockState = cloneDeep(mockRootState);
mockState.engine.backgroundState.RemoteFeatureFlagController.remoteFeatureFlags.bridgeConfigV2.chainRanking =
[
...mockState.engine.backgroundState.RemoteFeatureFlagController
.remoteFeatureFlags.bridgeConfigV2.chainRanking,
{ chainId: 'eip155:99999', name: 'Unsupported Future Chain' },
];

const result = selectDestChainRanking(mockState as unknown as RootState);

expect(result.some((chain) => chain.chainId === 'eip155:99999')).toBe(
false,
);
expect(result.some((chain) => chain.chainId === 'eip155:1')).toBe(true);
});

it('filters out unsupported non-EVM chains not in ALLOWED_BRIDGE_CHAIN_IDS', () => {
const mockState = cloneDeep(mockRootState);
mockState.engine.backgroundState.RemoteFeatureFlagController.remoteFeatureFlags.bridgeConfigV2.chainRanking =
[
...mockState.engine.backgroundState.RemoteFeatureFlagController
.remoteFeatureFlags.bridgeConfigV2.chainRanking,
{ chainId: 'cosmos:cosmoshub-4', name: 'Unsupported Cosmos Chain' },
];

const result = selectDestChainRanking(mockState as unknown as RootState);

expect(
result.some((chain) => chain.chainId === 'cosmos:cosmoshub-4'),
).toBe(false);
expect(
result.some(
(chain) =>
chain.chainId === 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
),
).toBe(true);
});
});

describe('selectIsBridgeEnabledSource - ALLOWED_BRIDGE_CHAIN_IDS filtering', () => {
it('returns false for a chain in chainRanking but not in ALLOWED_BRIDGE_CHAIN_IDS', () => {
const mockState = cloneDeep(mockRootState);
// Add an unsupported chain to chainRanking
mockState.engine.backgroundState.RemoteFeatureFlagController.remoteFeatureFlags.bridgeConfigV2.chainRanking =
[
...mockState.engine.backgroundState.RemoteFeatureFlagController
.remoteFeatureFlags.bridgeConfigV2.chainRanking,
{ chainId: 'eip155:99999', name: 'Unsupported Future Chain' },
];

const result = selectIsBridgeEnabledSource(
mockState as unknown as RootState,
'0x1869F' as Hex, // hex for 99999
);

expect(result).toBe(false);
});
});

describe('selectIsSwapsLive', () => {
Expand Down Expand Up @@ -671,19 +805,24 @@ describe('bridge slice', () => {
});

it('returns false when bridge is disabled for both source and destination', () => {
const mockState = cloneDeep(mockRootState) as unknown as RootState;
// @ts-expect-error - Mock state has correct structure at runtime
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
mockState.engine.backgroundState.RemoteFeatureFlagController.remoteFeatureFlags.bridgeConfigV2!.chains[
'eip155:1'
].isActiveSrc = false;
// @ts-expect-error - Mock state has correct structure at runtime
const mockState = cloneDeep(mockRootState);
// Remove chain from chainRanking to disable source (chainRanking presence = enabled)
mockState.engine.backgroundState.RemoteFeatureFlagController.remoteFeatureFlags.bridgeConfigV2.chainRanking =
mockState.engine.backgroundState.RemoteFeatureFlagController.remoteFeatureFlags.bridgeConfigV2.chainRanking.filter(
(chain) => chain.chainId !== 'eip155:1',
);
// Disable destination via chains config
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
mockState.engine.backgroundState.RemoteFeatureFlagController.remoteFeatureFlags.bridgeConfigV2!.chains[
(
mockState as any
).engine.backgroundState.RemoteFeatureFlagController.remoteFeatureFlags.bridgeConfigV2!.chains[
'eip155:1'
].isActiveDest = false;

const result = selectIsSwapsLive(mockState, '0x1');
const result = selectIsSwapsLive(
mockState as unknown as RootState,
'0x1',
);

expect(result).toBe(false);
});
Expand All @@ -697,13 +836,24 @@ describe('bridge slice', () => {
expect(result).toBeUndefined();
});

it('returns false when support flag is disabled', () => {
const mockState = cloneDeep(mockRootState) as unknown as RootState;
// @ts-expect-error - Mock state has correct structure at runtime
it('returns false when support flag is disabled and source is not in chainRanking', () => {
const mockState = cloneDeep(mockRootState);
// Remove chain from chainRanking to disable source
mockState.engine.backgroundState.RemoteFeatureFlagController.remoteFeatureFlags.bridgeConfigV2.chainRanking =
mockState.engine.backgroundState.RemoteFeatureFlagController.remoteFeatureFlags.bridgeConfigV2.chainRanking.filter(
(chain) => chain.chainId !== 'eip155:1',
);
// Disable destination via support flag
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
mockState.engine.backgroundState.RemoteFeatureFlagController.remoteFeatureFlags.bridgeConfigV2!.support = false;
(
mockState as any
).engine.backgroundState.RemoteFeatureFlagController.remoteFeatureFlags.bridgeConfigV2!.support =
false;

const result = selectIsSwapsLive(mockState, '0x1');
const result = selectIsSwapsLive(
mockState as unknown as RootState,
'0x1',
);

expect(result).toBe(false);
});
Expand Down
39 changes: 33 additions & 6 deletions app/core/redux/slices/bridge/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,26 @@ export const selectBridgeFeatureFlags = createSelector(
);

/**
* Selector that returns the chainRanking from feature flags filtered by user-configured networks.
* Checks whether a CAIP chain ID from chainRanking is supported by this version of the client.
* This ensures that chains added to the remote chainRanking flag in the future
* won't be surfaced by older app versions that lack support for them.
*/
const isAllowedBridgeChainId = (caipChainId: string): boolean => {
if (caipChainId.startsWith('eip155:')) {
const hexChainId = formatChainIdToHex(caipChainId);
return ALLOWED_BRIDGE_CHAIN_IDS.includes(
hexChainId as AllowedBridgeChainIds,
);
}
return ALLOWED_BRIDGE_CHAIN_IDS.includes(
caipChainId as AllowedBridgeChainIds,
);
};

/**
* Selector that returns the chainRanking from feature flags filtered by:
* 1. Chains supported by this version of the client
* 2. User-configured networks
* Used by NetworkPills in SOURCE mode to show all networks the user has added.
*/
export const selectSourceChainRanking = createSelector(
Expand All @@ -297,6 +316,11 @@ export const selectSourceChainRanking = createSelector(
return chainRanking.filter((chain) => {
const { chainId } = chain;

// First, ensure this chain is supported by the current client version
if (!isAllowedBridgeChainId(chainId)) {
return false;
}

// For EVM chains (eip155:*), extract the hex chain ID and check if enabled
if (chainId.startsWith('eip155:')) {
const hexChainId = formatChainIdToHex(chainId);
Expand All @@ -310,14 +334,17 @@ export const selectSourceChainRanking = createSelector(
);

/**
* Selector that returns all chains from chainRanking (all bridge-supported networks).
* Selector that returns all chains from chainRanking that are supported by this
* version of the client.
* Used by NetworkPills in DEST mode to show all available destination networks.
*/
export const selectDestChainRanking = createSelector(
selectBridgeFeatureFlags,
(bridgeFeatureFlags) => {
const { chainRanking } = bridgeFeatureFlags;
return chainRanking ?? [];
return (chainRanking ?? []).filter((chain) =>
isAllowedBridgeChainId(chain.chainId),
);
},
);

Expand All @@ -333,9 +360,9 @@ export const selectIsBridgeEnabledSourceFactory = createSelector(
(bridgeFeatureFlags) => (chainId: Hex | CaipChainId) => {
const caipChainId = formatChainIdToCaip(chainId);

return (
bridgeFeatureFlags.support &&
bridgeFeatureFlags.chains[caipChainId]?.isActiveSrc
return bridgeFeatureFlags.chainRanking?.some(
(chain) =>
chain.chainId === caipChainId && isAllowedBridgeChainId(chain.chainId),
);
},
);
Expand Down
8 changes: 4 additions & 4 deletions bitrise.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3469,16 +3469,16 @@ app:
PROJECT_LOCATION_IOS: ios
- opts:
is_expand: false
VERSION_NAME: 7.64.0
VERSION_NAME: 7.64.2
- opts:
is_expand: false
VERSION_NUMBER: 3646
VERSION_NUMBER: 3667
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.64.0
FLASK_VERSION_NAME: 7.64.2
- opts:
is_expand: false
FLASK_VERSION_NUMBER: 3646
FLASK_VERSION_NUMBER: 3667
- opts:
is_expand: false
ANDROID_APK_LINK: ''
Expand Down
Loading
Loading