Skip to content

Commit ff635d2

Browse files
gambinishdarkwing
andauthored
fix: PortfolioView swap native token bug (#28639)
## **Description** When `PORTFOLIO_VIEW` feature flag is enabled, when swapping a native token from a different chain than the globally selected chain, the incorrect native token would be prepoulated in the `fromToken` in the swap UI. For instance, if user is on Ethereum mainnet, navigated to POL, then attempted to swap, the globally selected network would change from Ethereum mainnet to Polygon mainnet (expected), but the swaps `fromToken` would still be POL (unexpected) Changes in this PR fixes this, and prepoulates `fromToken` with the native token from the correct chain. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28639?quickstart=1) ## **Related issues** Fixes: #28534 ## **Manual testing steps** 1. `PORTFOLIO_VIEW=1 yarn webpack --watch` 2. Import wallet with at least two networks added 3. When "All Networks" is toggled, attempt to swap a native token from another network. Ensure that the token prepopulated in the swap UI is the native token from the correct chain 4. Ensure swap completes successfully. ## **Screenshots/Recordings** https://github.com/user-attachments/assets/016ffa54-9ed1-450c-9aa0-da27f0fd6caa ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: David Walsh <[email protected]>
1 parent 9c4d1b4 commit ff635d2

File tree

3 files changed

+223
-10
lines changed

3 files changed

+223
-10
lines changed

ui/pages/asset/components/asset-page.tsx

+13-4
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,21 @@ const AssetPage = ({
101101
const selectedAccount = useSelector(getSelectedAccount);
102102
const currency = useSelector(getCurrentCurrency);
103103
const conversionRate = useSelector(getConversionRate);
104-
const isBridgeChain = useSelector(getIsBridgeChain);
105104
const isBuyableChain = useSelector(getIsNativeTokenBuyable);
106-
const defaultSwapsToken = useSelector(getSwapsDefaultToken, isEqual);
105+
106+
const { chainId, type, symbol, name, image, decimals } = asset;
107+
108+
// These need to be specific to the asset and not the current chain
109+
const defaultSwapsToken = useSelector(
110+
(state) => getSwapsDefaultToken(state, chainId),
111+
isEqual,
112+
);
113+
const isSwapsChain = useSelector((state) => getIsSwapsChain(state, chainId));
114+
const isBridgeChain = useSelector((state) =>
115+
getIsBridgeChain(state, chainId),
116+
);
117+
107118
const account = useSelector(getSelectedInternalAccount, isEqual);
108-
const isSwapsChain = useSelector(getIsSwapsChain);
109119
const isSigningEnabled =
110120
account.methods.includes(EthMethod.SignTransaction) ||
111121
account.methods.includes(EthMethod.SignUserOperation);
@@ -132,7 +142,6 @@ const AssetPage = ({
132142
const selectedAccountTokenBalancesAcrossChains =
133143
tokenBalances[selectedAccount.address];
134144

135-
const { chainId, type, symbol, name, image, decimals } = asset;
136145
const isMetaMetricsEnabled = useSelector(getParticipateInMetaMetrics);
137146
const isMarketingEnabled = useSelector(getDataCollectionForMarketing);
138147
const metaMetricsId = useSelector(getMetaMetricsId);

ui/selectors/selectors.js

+11-6
Original file line numberDiff line numberDiff line change
@@ -1495,14 +1495,17 @@ export function getWeb3ShimUsageStateForOrigin(state, origin) {
14951495
* objects, per the above description.
14961496
*
14971497
* @param {object} state - the redux state object
1498+
* @param {string} overrideChainId - the chainId to override the current chainId
14981499
* @returns {SwapsEthToken} The token object representation of the currently
14991500
* selected account's ETH balance, as expected by the Swaps API.
15001501
*/
15011502

1502-
export function getSwapsDefaultToken(state) {
1503+
export function getSwapsDefaultToken(state, overrideChainId = null) {
15031504
const selectedAccount = getSelectedAccount(state);
15041505
const balance = selectedAccount?.balance;
1505-
const chainId = getCurrentChainId(state);
1506+
const currentChainId = getCurrentChainId(state);
1507+
1508+
const chainId = overrideChainId ?? currentChainId;
15061509
const defaultTokenObject = SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId];
15071510

15081511
return {
@@ -1516,8 +1519,9 @@ export function getSwapsDefaultToken(state) {
15161519
};
15171520
}
15181521

1519-
export function getIsSwapsChain(state) {
1520-
const chainId = getCurrentChainId(state);
1522+
export function getIsSwapsChain(state, overrideChainId) {
1523+
const currentChainId = getCurrentChainId(state);
1524+
const chainId = overrideChainId ?? currentChainId;
15211525
const isNotDevelopment =
15221526
process.env.METAMASK_ENVIRONMENT !== 'development' &&
15231527
process.env.METAMASK_ENVIRONMENT !== 'testing';
@@ -1526,8 +1530,9 @@ export function getIsSwapsChain(state) {
15261530
: ALLOWED_DEV_SWAPS_CHAIN_IDS.includes(chainId);
15271531
}
15281532

1529-
export function getIsBridgeChain(state) {
1530-
const chainId = getCurrentChainId(state);
1533+
export function getIsBridgeChain(state, overrideChainId) {
1534+
const currentChainId = getCurrentChainId(state);
1535+
const chainId = overrideChainId ?? currentChainId;
15311536
return ALLOWED_BRIDGE_CHAIN_IDS.includes(chainId);
15321537
}
15331538

ui/selectors/selectors.test.js

+199
Original file line numberDiff line numberDiff line change
@@ -1978,4 +1978,203 @@ describe('#getConnectedSitesList', () => {
19781978
expect(selectors.getSelectedEvmInternalAccount(state)).toBe(undefined);
19791979
});
19801980
});
1981+
1982+
describe('getSwapsDefaultToken', () => {
1983+
it('returns the token object for the current chainId when no overrideChainId is provided', () => {
1984+
const expectedToken = {
1985+
symbol: 'ETH',
1986+
name: 'Ether',
1987+
address: '0x0000000000000000000000000000000000000000',
1988+
decimals: 18,
1989+
balance: '966987986469506564059',
1990+
string: '966.988',
1991+
iconUrl: './images/black-eth-logo.svg',
1992+
};
1993+
1994+
const result = selectors.getSwapsDefaultToken(mockState);
1995+
1996+
expect(result).toStrictEqual(expectedToken);
1997+
});
1998+
1999+
it('returns the token object for the overridden chainId when overrideChainId is provided', () => {
2000+
const getCurrentChainIdSpy = jest.spyOn(selectors, 'getCurrentChainId');
2001+
const expectedToken = {
2002+
symbol: 'POL',
2003+
name: 'Polygon',
2004+
address: '0x0000000000000000000000000000000000000000',
2005+
decimals: 18,
2006+
balance: '966987986469506564059',
2007+
string: '966.988',
2008+
iconUrl: './images/pol-token.svg',
2009+
};
2010+
2011+
const result = selectors.getSwapsDefaultToken(
2012+
mockState,
2013+
CHAIN_IDS.POLYGON,
2014+
);
2015+
2016+
expect(result).toStrictEqual(expectedToken);
2017+
expect(getCurrentChainIdSpy).not.toHaveBeenCalled(); // Ensure overrideChainId is used
2018+
});
2019+
});
2020+
2021+
describe('getIsSwapsChain', () => {
2022+
it('returns true for an allowed chainId in production environment', () => {
2023+
process.env.METAMASK_ENVIRONMENT = 'production';
2024+
2025+
const state = {
2026+
...mockState,
2027+
metamask: {
2028+
...mockState.metamask,
2029+
selectedNetworkClientId: 'testNetworkConfigurationId', // corresponds to mainnet RPC in mockState
2030+
},
2031+
};
2032+
2033+
const result = selectors.getIsSwapsChain(state);
2034+
2035+
expect(result).toBe(true);
2036+
});
2037+
2038+
it('returns true for an allowed chainId in development environment', () => {
2039+
process.env.METAMASK_ENVIRONMENT = 'development';
2040+
2041+
const state = {
2042+
...mockState,
2043+
metamask: {
2044+
...mockState.metamask,
2045+
selectedNetworkClientId: 'goerli',
2046+
},
2047+
};
2048+
2049+
const result = selectors.getIsSwapsChain(state);
2050+
2051+
expect(result).toBe(true);
2052+
});
2053+
2054+
it('returns false for a disallowed chainId in production environment', () => {
2055+
process.env.METAMASK_ENVIRONMENT = 'production';
2056+
2057+
const state = {
2058+
...mockState,
2059+
metamask: {
2060+
...mockState.metamask,
2061+
selectedNetworkClientId: 'fooChain', // corresponds to mainnet RPC in mockState
2062+
networkConfigurationsByChainId: {
2063+
'0x8080': {
2064+
chainId: '0x8080',
2065+
name: 'Custom Mainnet RPC',
2066+
nativeCurrency: 'ETH',
2067+
defaultRpcEndpointIndex: 0,
2068+
rpcEndpoints: [
2069+
{
2070+
type: 'custom',
2071+
url: 'https://testrpc.com',
2072+
networkClientId: 'fooChain',
2073+
},
2074+
],
2075+
},
2076+
},
2077+
},
2078+
};
2079+
2080+
const result = selectors.getIsSwapsChain(state);
2081+
2082+
expect(result).toBe(false);
2083+
});
2084+
2085+
it('returns false for a disallowed chainId in development environment', () => {
2086+
process.env.METAMASK_ENVIRONMENT = 'development';
2087+
2088+
const state = {
2089+
...mockState,
2090+
metamask: {
2091+
...mockState.metamask,
2092+
selectedNetworkClientId: 'fooChain', // corresponds to mainnet RPC in mockState
2093+
networkConfigurationsByChainId: {
2094+
'0x8080': {
2095+
chainId: '0x8080',
2096+
name: 'Custom Mainnet RPC',
2097+
nativeCurrency: 'ETH',
2098+
defaultRpcEndpointIndex: 0,
2099+
rpcEndpoints: [
2100+
{
2101+
type: 'custom',
2102+
url: 'https://testrpc.com',
2103+
networkClientId: 'fooChain',
2104+
},
2105+
],
2106+
},
2107+
},
2108+
},
2109+
};
2110+
2111+
const result = selectors.getIsSwapsChain(state);
2112+
2113+
expect(result).toBe(false);
2114+
});
2115+
2116+
it('respects the overrideChainId parameter', () => {
2117+
process.env.METAMASK_ENVIRONMENT = 'production';
2118+
2119+
const getCurrentChainIdSpy = jest.spyOn(selectors, 'getCurrentChainId');
2120+
2121+
const result = selectors.getIsSwapsChain(mockState, '0x89');
2122+
expect(result).toBe(true);
2123+
expect(getCurrentChainIdSpy).not.toHaveBeenCalled(); // Ensure overrideChainId is used
2124+
});
2125+
});
2126+
2127+
describe('getIsBridgeChain', () => {
2128+
it('returns true for an allowed bridge chainId', () => {
2129+
const state = {
2130+
...mockState,
2131+
metamask: {
2132+
...mockState.metamask,
2133+
selectedNetworkClientId: 'testNetworkConfigurationId', // corresponds to mainnet RPC in mockState
2134+
},
2135+
};
2136+
2137+
const result = selectors.getIsBridgeChain(state);
2138+
2139+
expect(result).toBe(true);
2140+
});
2141+
2142+
it('returns false for a disallowed bridge chainId', () => {
2143+
const state = {
2144+
...mockState,
2145+
metamask: {
2146+
...mockState.metamask,
2147+
selectedNetworkClientId: 'fooChain', // corresponds to mainnet RPC in mockState
2148+
networkConfigurationsByChainId: {
2149+
'0x8080': {
2150+
chainId: '0x8080',
2151+
name: 'Custom Mainnet RPC',
2152+
nativeCurrency: 'ETH',
2153+
defaultRpcEndpointIndex: 0,
2154+
rpcEndpoints: [
2155+
{
2156+
type: 'custom',
2157+
url: 'https://testrpc.com',
2158+
networkClientId: 'fooChain',
2159+
},
2160+
],
2161+
},
2162+
},
2163+
},
2164+
};
2165+
2166+
const result = selectors.getIsBridgeChain(state);
2167+
2168+
expect(result).toBe(false);
2169+
});
2170+
2171+
it('respects the overrideChainId parameter', () => {
2172+
const getCurrentChainIdSpy = jest.spyOn(selectors, 'getCurrentChainId');
2173+
2174+
const result = selectors.getIsBridgeChain(mockState, '0x89');
2175+
2176+
expect(result).toBe(true);
2177+
expect(getCurrentChainIdSpy).not.toHaveBeenCalled(); // Ensure overrideChainId is used
2178+
});
2179+
});
19812180
});

0 commit comments

Comments
 (0)