diff --git a/app/scripts/constants/sentry-state.ts b/app/scripts/constants/sentry-state.ts index 831dc6c539fb..76fb2386f1f6 100644 --- a/app/scripts/constants/sentry-state.ts +++ b/app/scripts/constants/sentry-state.ts @@ -104,6 +104,8 @@ export const SENTRY_BACKGROUND_STATE = { }, destTokens: {}, destTopAssets: [], + srcTokens: {}, + srcTopAssets: [], }, }, CronjobController: { diff --git a/app/scripts/controllers/bridge/bridge-controller.test.ts b/app/scripts/controllers/bridge/bridge-controller.test.ts index 221a1e1a2a00..25b6eae98c33 100644 --- a/app/scripts/controllers/bridge/bridge-controller.test.ts +++ b/app/scripts/controllers/bridge/bridge-controller.test.ts @@ -95,4 +95,28 @@ describe('BridgeController', function () { { address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', symbol: 'ABC' }, ]); }); + + it('selectSrcNetwork should set the bridge src tokens and top assets', async function () { + await bridgeController.selectSrcNetwork('0xa'); + expect(bridgeController.state.bridgeState.srcTokens).toStrictEqual({ + '0x0000000000000000000000000000000000000000': { + address: '0x0000000000000000000000000000000000000000', + decimals: 18, + iconUrl: './images/eth_logo.svg', + name: 'Ether', + symbol: 'ETH', + }, + '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984': { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + decimals: 16, + }, + }); + expect(bridgeController.state.bridgeState.srcTopAssets).toStrictEqual([ + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + }, + ]); + }); }); diff --git a/app/scripts/controllers/bridge/bridge-controller.ts b/app/scripts/controllers/bridge/bridge-controller.ts index 1bc673af43f8..841d735ac52c 100644 --- a/app/scripts/controllers/bridge/bridge-controller.ts +++ b/app/scripts/controllers/bridge/bridge-controller.ts @@ -39,6 +39,10 @@ export default class BridgeController extends BaseController< `${BRIDGE_CONTROLLER_NAME}:setBridgeFeatureFlags`, this.setBridgeFeatureFlags.bind(this), ); + this.messagingSystem.registerActionHandler( + `${BRIDGE_CONTROLLER_NAME}:selectSrcNetwork`, + this.selectSrcNetwork.bind(this), + ); this.messagingSystem.registerActionHandler( `${BRIDGE_CONTROLLER_NAME}:selectDestNetwork`, this.selectDestNetwork.bind(this), @@ -61,6 +65,11 @@ export default class BridgeController extends BaseController< }); }; + selectSrcNetwork = async (chainId: Hex) => { + await this.#setTopAssets(chainId, 'srcTopAssets'); + await this.#setTokens(chainId, 'srcTokens'); + }; + selectDestNetwork = async (chainId: Hex) => { await this.#setTopAssets(chainId, 'destTopAssets'); await this.#setTokens(chainId, 'destTokens'); diff --git a/app/scripts/controllers/bridge/constants.ts b/app/scripts/controllers/bridge/constants.ts index e21071d71c4d..58c7d015b7bb 100644 --- a/app/scripts/controllers/bridge/constants.ts +++ b/app/scripts/controllers/bridge/constants.ts @@ -8,6 +8,8 @@ export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState = { [BridgeFeatureFlagsKey.NETWORK_SRC_ALLOWLIST]: [], [BridgeFeatureFlagsKey.NETWORK_DEST_ALLOWLIST]: [], }, + srcTokens: {}, + srcTopAssets: [], destTokens: {}, destTopAssets: [], }; diff --git a/app/scripts/controllers/bridge/types.ts b/app/scripts/controllers/bridge/types.ts index ddc4668b3e53..2fb36e1e983e 100644 --- a/app/scripts/controllers/bridge/types.ts +++ b/app/scripts/controllers/bridge/types.ts @@ -21,11 +21,14 @@ export type BridgeFeatureFlags = { export type BridgeControllerState = { bridgeFeatureFlags: BridgeFeatureFlags; + srcTokens: Record; + srcTopAssets: { address: string }[]; destTokens: Record; destTopAssets: { address: string }[]; }; export enum BridgeUserAction { + SELECT_SRC_NETWORK = 'selectSrcNetwork', SELECT_DEST_NETWORK = 'selectDestNetwork', } export enum BridgeBackgroundAction { @@ -40,6 +43,7 @@ type BridgeControllerAction = { // Maps to BridgeController function names type BridgeControllerActions = | BridgeControllerAction + | BridgeControllerAction | BridgeControllerAction; type BridgeControllerEvents = ControllerStateChangeEvent< diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 584ae7e91ad2..45083c881e1f 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -3891,6 +3891,10 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger, `${BRIDGE_CONTROLLER_NAME}:${BridgeBackgroundAction.SET_FEATURE_FLAGS}`, ), + [BridgeUserAction.SELECT_SRC_NETWORK]: this.controllerMessenger.call.bind( + this.controllerMessenger, + `${BRIDGE_CONTROLLER_NAME}:${BridgeUserAction.SELECT_SRC_NETWORK}`, + ), [BridgeUserAction.SELECT_DEST_NETWORK]: this.controllerMessenger.call.bind( this.controllerMessenger, diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js index d56141572d81..83b8b29a5e83 100644 --- a/test/e2e/default-fixture.js +++ b/test/e2e/default-fixture.js @@ -129,6 +129,8 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { }, destTokens: {}, destTopAssets: [], + srcTokens: {}, + srcTopAssets: [], }, }, CurrencyController: { diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index a8c80e972346..415af23071e7 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -402,6 +402,8 @@ class FixtureBuilder { }, destTokens: {}, destTopAssets: [], + srcTokens: {}, + srcTopAssets: [], }, }; return this; diff --git a/test/e2e/tests/bridge/bridge-test-utils.ts b/test/e2e/tests/bridge/bridge-test-utils.ts index 596c7623208d..40bb8c6bd97f 100644 --- a/test/e2e/tests/bridge/bridge-test-utils.ts +++ b/test/e2e/tests/bridge/bridge-test-utils.ts @@ -120,7 +120,17 @@ const mockServer = }; }), ); - return Promise.all(featureFlagMocks); + const portfolioMock = async () => + await mockServer_ + .forGet('https://portfolio.metamask.io/bridge') + .always() + .thenCallback(() => { + return { + statusCode: 200, + json: {}, + }; + }); + return Promise.all([...featureFlagMocks, portfolioMock]); }; export const getBridgeFixtures = ( diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index bbd833c87656..11f74e9c7511 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -67,7 +67,9 @@ "destNetworkAllowlist": { "0": "string", "1": "string", "2": "string" } }, "destTokens": {}, - "destTopAssets": {} + "destTopAssets": {}, + "srcTokens": {}, + "srcTopAssets": {} } }, "CronjobController": { "jobs": "object" }, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index 9812df603e92..a6f5de2d24b6 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -254,7 +254,9 @@ "destNetworkAllowlist": { "0": "string", "1": "string", "2": "string" } }, "destTokens": {}, - "destTopAssets": {} + "destTopAssets": {}, + "srcTokens": {}, + "srcTopAssets": {} }, "ensEntries": "object", "ensResolutionsByAddress": "object", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index 14f0c27c5d80..f40b2687316b 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -155,7 +155,9 @@ } }, "destTokens": {}, - "destTopAssets": {} + "destTopAssets": {}, + "srcTokens": {}, + "srcTopAssets": {} } }, "SubjectMetadataController": { "subjectMetadata": "object" }, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index c899811aad0f..3c692fa59405 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -164,7 +164,9 @@ } }, "destTokens": {}, - "destTopAssets": {} + "destTopAssets": {}, + "srcTokens": {}, + "srcTopAssets": {} } }, "TransactionController": { "transactions": "object" }, diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts index 47912db8fd17..5bfbda1e23cf 100644 --- a/ui/ducks/bridge/actions.ts +++ b/ui/ducks/bridge/actions.ts @@ -42,6 +42,16 @@ export const setBridgeFeatureFlags = () => { }; // User actions +export const setFromChain = (chainId: Hex) => { + return async (dispatch: MetaMaskReduxDispatch) => { + dispatch( + callBridgeControllerMethod(BridgeUserAction.SELECT_SRC_NETWORK, [ + chainId, + ]), + ); + }; +}; + export const setToChain = (chainId: Hex) => { return async (dispatch: MetaMaskReduxDispatch) => { dispatch(setToChainId_(chainId)); diff --git a/ui/ducks/bridge/bridge.test.ts b/ui/ducks/bridge/bridge.test.ts index a9eddde18081..b8d2e09eb0ea 100644 --- a/ui/ducks/bridge/bridge.test.ts +++ b/ui/ducks/bridge/bridge.test.ts @@ -16,6 +16,7 @@ import { setFromTokenInputValue, setToChain, setToToken, + setFromChain, } from './actions'; const middleware = [thunk]; @@ -100,4 +101,21 @@ describe('Ducks - Bridge', () => { expect(mockSetBridgeFeatureFlags).toHaveBeenCalledTimes(1); }); }); + + describe('setFromChain', () => { + it('calls the selectSrcNetwork background action', async () => { + const mockSelectSrcNetwork = jest.fn().mockReturnValue({}); + setBackgroundConnection({ + [BridgeUserAction.SELECT_SRC_NETWORK]: mockSelectSrcNetwork, + } as never); + + await store.dispatch(setFromChain(CHAIN_IDS.MAINNET) as never); + + expect(mockSelectSrcNetwork).toHaveBeenCalledTimes(1); + expect(mockSelectSrcNetwork).toHaveBeenCalledWith( + '0x1', + expect.anything(), + ); + }); + }); }); diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts index 98c5264dd97d..cf27790aa943 100644 --- a/ui/ducks/bridge/selectors.test.ts +++ b/ui/ducks/bridge/selectors.test.ts @@ -12,6 +12,8 @@ import { getFromChain, getFromChains, getFromToken, + getFromTokens, + getFromTopAssets, getIsBridgeTx, getToAmount, getToChain, @@ -456,4 +458,37 @@ describe('Bridge selectors', () => { expect(result).toStrictEqual([]); }); }); + + describe('getFromTokens', () => { + it('returns src tokens from controller state', () => { + const state = createBridgeMockStore( + {}, + { toChainId: '0x1' }, + { + srcTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, + }, + ); + const result = getFromTokens(state as never); + + expect(result).toStrictEqual({ + '0x00': { address: '0x00', symbol: 'TEST' }, + }); + }); + }); + + describe('getFromTopAssets', () => { + it('returns src top assets from controller state', () => { + const state = createBridgeMockStore( + {}, + { toChainId: '0x1' }, + { + srcTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, + srcTopAssets: [{ address: '0x00', symbol: 'TEST' }], + }, + ); + const result = getFromTopAssets(state as never); + + expect(result).toStrictEqual([{ address: '0x00', symbol: 'TEST' }]); + }); + }); }); diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts index dd8dfa7a8999..8cd56928fc66 100644 --- a/ui/ducks/bridge/selectors.ts +++ b/ui/ducks/bridge/selectors.ts @@ -92,6 +92,14 @@ export const getToChain = createDeepEqualSelector( toChains.find(({ chainId }) => chainId === toChainId), ); +export const getFromTokens = (state: BridgeAppState) => { + return state.metamask.bridgeState.srcTokens ?? {}; +}; + +export const getFromTopAssets = (state: BridgeAppState) => { + return state.metamask.bridgeState.srcTopAssets ?? []; +}; + export const getToTopAssets = (state: BridgeAppState) => { return state.bridge.toChainId ? state.metamask.bridgeState.destTopAssets : []; }; diff --git a/ui/hooks/bridge/useBridging.test.ts b/ui/hooks/bridge/useBridging.test.ts index 9e2f205c28dc..df8bbb940f4e 100644 --- a/ui/hooks/bridge/useBridging.test.ts +++ b/ui/hooks/bridge/useBridging.test.ts @@ -15,9 +15,16 @@ jest.mock('react-router-dom', () => ({ }), })); +const mockDispatch = jest.fn().mockReturnValue(() => jest.fn()); jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), - useDispatch: jest.fn().mockReturnValue(() => jest.fn()), + useDispatch: () => mockDispatch, +})); + +const mockSetFromChain = jest.fn(); +jest.mock('../../ducks/bridge/actions', () => ({ + ...jest.requireActual('../../ducks/bridge/actions'), + setFromChain: () => mockSetFromChain(), })); const MOCK_METAMETRICS_ID = '0xtestMetaMetricsId'; @@ -94,6 +101,8 @@ describe('useBridging', () => { }, }); + expect(mockDispatch.mock.calls).toHaveLength(1); + expect(nock(BRIDGE_API_BASE_URL).isDone()).toBe(true); result.current.openBridgeExperience(location, token, urlSuffix); @@ -165,6 +174,7 @@ describe('useBridging', () => { result.current.openBridgeExperience(location, token, urlSuffix); + expect(mockDispatch.mock.calls).toHaveLength(3); expect(mockHistoryPush.mock.calls).toHaveLength(1); expect(mockHistoryPush).toHaveBeenCalledWith(expectedUrl); expect(openTabSpy).not.toHaveBeenCalled(); diff --git a/ui/hooks/bridge/useBridging.ts b/ui/hooks/bridge/useBridging.ts index d11aaeb821a9..ce4b8c48b89c 100644 --- a/ui/hooks/bridge/useBridging.ts +++ b/ui/hooks/bridge/useBridging.ts @@ -1,9 +1,11 @@ import { useCallback, useContext, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { setBridgeFeatureFlags } from '../../ducks/bridge/actions'; import { - getCurrentChainId, + setBridgeFeatureFlags, + setFromChain, +} from '../../ducks/bridge/actions'; +import { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) getCurrentKeyring, getDataCollectionForMarketing, @@ -31,6 +33,7 @@ import { isHardwareKeyring } from '../../helpers/utils/hardware'; import { getPortfolioUrl } from '../../helpers/utils/portfolio'; import { setSwapsFromToken } from '../../ducks/swaps/swaps'; import { SwapsTokenObject } from '../../../shared/constants/swaps'; +import { getProviderConfig } from '../../ducks/metamask/metamask'; ///: END:ONLY_INCLUDE_IF const useBridging = () => { @@ -41,7 +44,7 @@ const useBridging = () => { const metaMetricsId = useSelector(getMetaMetricsId); const isMetaMetricsEnabled = useSelector(getParticipateInMetaMetrics); const isMarketingEnabled = useSelector(getDataCollectionForMarketing); - const chainId = useSelector(getCurrentChainId); + const providerConfig = useSelector(getProviderConfig); const keyring = useSelector(getCurrentKeyring); const usingHardwareWallet = isHardwareKeyring(keyring.type); @@ -52,13 +55,20 @@ const useBridging = () => { dispatch(setBridgeFeatureFlags()); }, [dispatch, setBridgeFeatureFlags]); + useEffect(() => { + isBridgeChain && + isBridgeSupported && + providerConfig && + dispatch(setFromChain(providerConfig.chainId)); + }, []); + const openBridgeExperience = useCallback( ( location: string, token: SwapsTokenObject | SwapsEthToken, portfolioUrlSuffix?: string, ) => { - if (!isBridgeChain) { + if (!isBridgeChain || !providerConfig) { return; } @@ -70,7 +80,7 @@ const useBridging = () => { token_symbol: token.symbol, location, text: 'Bridge', - chain_id: chainId, + chain_id: providerConfig.chainId, }, }); dispatch( @@ -105,7 +115,7 @@ const useBridging = () => { location, text: 'Bridge', url: portfolioUrl, - chain_id: chainId, + chain_id: providerConfig.chainId, token_symbol: token.symbol, }, }); @@ -114,7 +124,6 @@ const useBridging = () => { [ isBridgeSupported, isBridgeChain, - chainId, setSwapsFromToken, dispatch, usingHardwareWallet, @@ -123,6 +132,7 @@ const useBridging = () => { trackEvent, isMetaMetricsEnabled, isMarketingEnabled, + providerConfig, ], ); diff --git a/ui/pages/bridge/index.test.tsx b/ui/pages/bridge/index.test.tsx index 4352ff359742..a73cfa370681 100644 --- a/ui/pages/bridge/index.test.tsx +++ b/ui/pages/bridge/index.test.tsx @@ -22,6 +22,8 @@ setBackgroundConnection({ getNetworkConfigurationByNetworkClientId: jest .fn() .mockResolvedValue({ chainId: '0x1' }), + setBridgeFeatureFlags: jest.fn(), + selectSrcNetwork: jest.fn(), // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); diff --git a/ui/pages/bridge/index.tsx b/ui/pages/bridge/index.tsx index 780a19bd71f4..e4b5c0b930d4 100644 --- a/ui/pages/bridge/index.tsx +++ b/ui/pages/bridge/index.tsx @@ -28,10 +28,14 @@ import { BlockSize, } from '../../helpers/constants/design-system'; import { getIsBridgeEnabled } from '../../selectors'; +import useBridging from '../../hooks/bridge/useBridging'; import { PrepareBridgePage } from './prepare/prepare-bridge-page'; const CrossChainSwap = () => { const t = useContext(I18nContext); + + useBridging(); + const history = useHistory(); const dispatch = useDispatch();