diff --git a/app/scripts/controllers/bridge/bridge-controller.test.ts b/app/scripts/controllers/bridge/bridge-controller.test.ts index 3aa11d406bfa..86fa6b513dbd 100644 --- a/app/scripts/controllers/bridge/bridge-controller.test.ts +++ b/app/scripts/controllers/bridge/bridge-controller.test.ts @@ -59,6 +59,7 @@ describe('BridgeController', function () { symbol: 'ABC', }, ]); + bridgeController.resetState(); }); it('constructor should setup correctly', function () { @@ -102,6 +103,11 @@ describe('BridgeController', function () { expect(bridgeController.state.bridgeState.destTopAssets).toStrictEqual([ { address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', symbol: 'ABC' }, ]); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); }); it('selectSrcNetwork should set the bridge src tokens and top assets', async function () { @@ -126,5 +132,76 @@ describe('BridgeController', function () { symbol: 'ABC', }, ]); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + }); + + it('updateBridgeQuoteRequestParams should update the quoteRequest state', function () { + bridgeController.updateBridgeQuoteRequestParams({ srcChainId: 1 }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + srcChainId: 1, + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + + bridgeController.updateBridgeQuoteRequestParams({ destChainId: 10 }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + destChainId: 10, + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + + bridgeController.updateBridgeQuoteRequestParams({ destChainId: undefined }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + destChainId: undefined, + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + + bridgeController.updateBridgeQuoteRequestParams({ + srcTokenAddress: undefined, + }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + slippage: 0.5, + srcTokenAddress: undefined, + walletAddress: undefined, + }); + + bridgeController.updateBridgeQuoteRequestParams({ + srcTokenAmount: '100000', + destTokenAddress: '0x123', + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + srcTokenAmount: '100000', + destTokenAddress: '0x123', + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + + bridgeController.updateBridgeQuoteRequestParams({ + srcTokenAddress: '0x2ABC', + }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + slippage: 0.5, + srcTokenAddress: '0x2ABC', + walletAddress: undefined, + }); + + bridgeController.resetState(); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); }); }); diff --git a/app/scripts/controllers/bridge/bridge-controller.ts b/app/scripts/controllers/bridge/bridge-controller.ts index 841d735ac52c..0129dab0fac2 100644 --- a/app/scripts/controllers/bridge/bridge-controller.ts +++ b/app/scripts/controllers/bridge/bridge-controller.ts @@ -9,6 +9,9 @@ import { // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { fetchTopAssetsList } from '../../../../ui/pages/swaps/swaps.util'; +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { QuoteRequest } from '../../../../ui/pages/bridge/types'; import { BRIDGE_CONTROLLER_NAME, DEFAULT_BRIDGE_CONTROLLER_STATE, @@ -47,8 +50,29 @@ export default class BridgeController extends BaseController< `${BRIDGE_CONTROLLER_NAME}:selectDestNetwork`, this.selectDestNetwork.bind(this), ); + this.messagingSystem.registerActionHandler( + `${BRIDGE_CONTROLLER_NAME}:updateBridgeQuoteRequestParams`, + this.updateBridgeQuoteRequestParams.bind(this), + ); } + updateBridgeQuoteRequestParams = (paramsToUpdate: Partial) => { + const { bridgeState } = this.state; + const updatedQuoteRequest = { + ...DEFAULT_BRIDGE_CONTROLLER_STATE.quoteRequest, + ...paramsToUpdate, + }; + + this.update((_state) => { + _state.bridgeState = { + ...bridgeState, + quoteRequest: { + ...updatedQuoteRequest, + }, + }; + }); + }; + resetState = () => { this.update((_state) => { _state.bridgeState = { diff --git a/app/scripts/controllers/bridge/constants.ts b/app/scripts/controllers/bridge/constants.ts index d1062d3dc94a..9506a8cc5073 100644 --- a/app/scripts/controllers/bridge/constants.ts +++ b/app/scripts/controllers/bridge/constants.ts @@ -1,8 +1,10 @@ +import { zeroAddress } from 'ethereumjs-util'; import { BridgeControllerState, BridgeFeatureFlagsKey } from './types'; export const BRIDGE_CONTROLLER_NAME = 'BridgeController'; export const REFRESH_INTERVAL_MS = 30 * 1000; const DEFAULT_MAX_REFRESH_COUNT = 5; +const DEFAULT_SLIPPAGE = 0.5; export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState = { bridgeFeatureFlags: { @@ -18,4 +20,9 @@ export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState = { srcTopAssets: [], destTokens: {}, destTopAssets: [], + quoteRequest: { + walletAddress: undefined, + srcTokenAddress: zeroAddress(), + slippage: DEFAULT_SLIPPAGE, + }, }; diff --git a/app/scripts/controllers/bridge/types.ts b/app/scripts/controllers/bridge/types.ts index df6611c943d3..15257ff6ec4b 100644 --- a/app/scripts/controllers/bridge/types.ts +++ b/app/scripts/controllers/bridge/types.ts @@ -4,6 +4,9 @@ import { } from '@metamask/base-controller'; import { Hex } from '@metamask/utils'; import { SwapsTokenObject } from '../../../../shared/constants/swaps'; +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { QuoteRequest } from '../../../../ui/pages/bridge/types'; import BridgeController from './bridge-controller'; import { BRIDGE_CONTROLLER_NAME } from './constants'; @@ -30,11 +33,13 @@ export type BridgeControllerState = { srcTopAssets: { address: string }[]; destTokens: Record; destTopAssets: { address: string }[]; + quoteRequest: Partial; }; export enum BridgeUserAction { SELECT_SRC_NETWORK = 'selectSrcNetwork', SELECT_DEST_NETWORK = 'selectDestNetwork', + UPDATE_QUOTE_PARAMS = 'updateBridgeQuoteRequestParams', } export enum BridgeBackgroundAction { SET_FEATURE_FLAGS = 'setBridgeFeatureFlags', @@ -49,7 +54,8 @@ type BridgeControllerAction = { type BridgeControllerActions = | BridgeControllerAction | BridgeControllerAction - | BridgeControllerAction; + | BridgeControllerAction + | BridgeControllerAction; type BridgeControllerEvents = ControllerStateChangeEvent< typeof BRIDGE_CONTROLLER_NAME, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index c33485f665b7..f3b213ea3bf4 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -3942,6 +3942,11 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger, `${BRIDGE_CONTROLLER_NAME}:${BridgeUserAction.SELECT_DEST_NETWORK}`, ), + [BridgeUserAction.UPDATE_QUOTE_PARAMS]: + this.controllerMessenger.call.bind( + this.controllerMessenger, + `${BRIDGE_CONTROLLER_NAME}:${BridgeUserAction.UPDATE_QUOTE_PARAMS}`, + ), // Smart Transactions fetchSmartTransactionFees: smartTransactionsController.getFees.bind( diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts index 5e50b004b774..0cd3de70427a 100644 --- a/ui/ducks/bridge/actions.ts +++ b/ui/ducks/bridge/actions.ts @@ -11,26 +11,26 @@ import { import { forceUpdateMetamaskState } from '../../store/actions'; import { submitRequestToBackground } from '../../store/background-connection'; import { MetaMaskReduxDispatch } from '../../store/store'; +import { QuoteRequest } from '../../pages/bridge/types'; import { bridgeSlice } from './bridge'; const { - setToChainId: setToChainId_, + setToChainId, setFromToken, setToToken, setFromTokenInputValue, resetInputFields, - switchToAndFromTokens, } = bridgeSlice.actions; export { - setFromToken, + setToChainId, + resetInputFields, setToToken, + setFromToken, setFromTokenInputValue, - switchToAndFromTokens, - resetInputFields, }; -const callBridgeControllerMethod = ( +const callBridgeControllerMethod = >( bridgeAction: BridgeUserAction | BridgeBackgroundAction, args?: T[], ) => { @@ -62,7 +62,6 @@ export const setFromChain = (chainId: Hex) => { export const setToChain = (chainId: Hex) => { return async (dispatch: MetaMaskReduxDispatch) => { - dispatch(setToChainId_(chainId)); dispatch( callBridgeControllerMethod(BridgeUserAction.SELECT_DEST_NETWORK, [ chainId, @@ -70,3 +69,16 @@ export const setToChain = (chainId: Hex) => { ); }; }; + +export const updateQuoteRequestParams = >( + params: T, +) => { + return async (dispatch: MetaMaskReduxDispatch) => { + await dispatch( + callBridgeControllerMethod>( + BridgeUserAction.UPDATE_QUOTE_PARAMS, + [params], + ), + ); + }; +}; diff --git a/ui/ducks/bridge/bridge.test.ts b/ui/ducks/bridge/bridge.test.ts index f4a566c233b5..6b85565c6143 100644 --- a/ui/ducks/bridge/bridge.test.ts +++ b/ui/ducks/bridge/bridge.test.ts @@ -1,5 +1,6 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import { zeroAddress } from 'ethereumjs-util'; import { createBridgeMockStore } from '../../../test/jest/mock-store'; import { CHAIN_IDS } from '../../../shared/constants/network'; import { setBackgroundConnection } from '../../store/background-connection'; @@ -18,7 +19,8 @@ import { setToToken, setFromChain, resetInputFields, - switchToAndFromTokens, + setToChainId, + updateQuoteRequestParams, } from './actions'; const middleware = [thunk]; @@ -31,11 +33,25 @@ describe('Ducks - Bridge', () => { store.clearActions(); }); - describe('setToChain', () => { - it('calls the "bridge/setToChainId" action and the selectDestNetwork background action', () => { + describe('setToChainId', () => { + it('calls the "bridge/setToChainId" action', () => { const state = store.getState().bridge; const actionPayload = CHAIN_IDS.OPTIMISM; + store.dispatch(setToChainId(actionPayload as never) as never); + + // Check redux state + const actions = store.getActions(); + expect(actions[0].type).toStrictEqual('bridge/setToChainId'); + const newState = bridgeReducer(state, actions[0]); + expect(newState.toChainId).toStrictEqual(actionPayload); + }); + }); + + describe('setToChain', () => { + it('calls the selectDestNetwork background action', () => { + const actionPayload = CHAIN_IDS.OPTIMISM; + const mockSelectDestNetwork = jest.fn().mockReturnValue({}); setBackgroundConnection({ [BridgeUserAction.SELECT_DEST_NETWORK]: mockSelectDestNetwork, @@ -43,11 +59,6 @@ describe('Ducks - Bridge', () => { store.dispatch(setToChain(actionPayload as never) as never); - // Check redux state - const actions = store.getActions(); - expect(actions[0].type).toStrictEqual('bridge/setToChainId'); - const newState = bridgeReducer(state, actions[0]); - expect(newState.toChainId).toStrictEqual(actionPayload); // Check background state expect(mockSelectDestNetwork).toHaveBeenCalledTimes(1); expect(mockSelectDestNetwork).toHaveBeenCalledWith( @@ -61,7 +72,7 @@ describe('Ducks - Bridge', () => { it('calls the "bridge/setFromToken" action', () => { const state = store.getState().bridge; const actionPayload = { symbol: 'SYMBOL', address: '0x13341432' }; - store.dispatch(setFromToken(actionPayload)); + store.dispatch(setFromToken(actionPayload as never) as never); const actions = store.getActions(); expect(actions[0].type).toStrictEqual('bridge/setFromToken'); const newState = bridgeReducer(state, actions[0]); @@ -73,7 +84,8 @@ describe('Ducks - Bridge', () => { it('calls the "bridge/setToToken" action', () => { const state = store.getState().bridge; const actionPayload = { symbol: 'SYMBOL', address: '0x13341431' }; - store.dispatch(setToToken(actionPayload)); + + store.dispatch(setToToken(actionPayload as never) as never); const actions = store.getActions(); expect(actions[0].type).toStrictEqual('bridge/setToToken'); const newState = bridgeReducer(state, actions[0]); @@ -85,7 +97,8 @@ describe('Ducks - Bridge', () => { it('calls the "bridge/setFromTokenInputValue" action', () => { const state = store.getState().bridge; const actionPayload = '10'; - store.dispatch(setFromTokenInputValue(actionPayload)); + + store.dispatch(setFromTokenInputValue(actionPayload as never) as never); const actions = store.getActions(); expect(actions[0].type).toStrictEqual('bridge/setFromTokenInputValue'); const newState = bridgeReducer(state, actions[0]); @@ -137,31 +150,30 @@ describe('Ducks - Bridge', () => { }); }); - describe('switchToAndFromTokens', () => { - it('switches to and from input values', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const bridgeStore = configureMockStore(middleware)( - createBridgeMockStore( - {}, - { - toChainId: CHAIN_IDS.MAINNET, - fromToken: { symbol: 'WETH', address: '0x13341432' }, - toToken: { symbol: 'USDC', address: '0x13341431' }, - fromTokenInputValue: '10', - }, - ), + describe('updateQuoteRequestParams', () => { + it('dispatches quote params to the bridge controller', () => { + const mockUpdateParams = jest.fn(); + setBackgroundConnection({ + [BridgeUserAction.UPDATE_QUOTE_PARAMS]: mockUpdateParams, + } as never); + + store.dispatch( + updateQuoteRequestParams({ + srcChainId: 1, + srcTokenAddress: zeroAddress(), + destTokenAddress: undefined, + }) as never, + ); + + expect(mockUpdateParams).toHaveBeenCalledTimes(1); + expect(mockUpdateParams).toHaveBeenCalledWith( + { + srcChainId: 1, + srcTokenAddress: zeroAddress(), + destTokenAddress: undefined, + }, + expect.anything(), ); - const state = bridgeStore.getState().bridge; - bridgeStore.dispatch(switchToAndFromTokens(CHAIN_IDS.POLYGON)); - const actions = bridgeStore.getActions(); - expect(actions[0].type).toStrictEqual('bridge/switchToAndFromTokens'); - const newState = bridgeReducer(state, actions[0]); - expect(newState).toStrictEqual({ - toChainId: CHAIN_IDS.POLYGON, - fromToken: { symbol: 'USDC', address: '0x13341431' }, - toToken: { symbol: 'WETH', address: '0x13341432' }, - fromTokenInputValue: null, - }); }); }); }); diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts index 9ec744d9e953..c75030c7591d 100644 --- a/ui/ducks/bridge/bridge.ts +++ b/ui/ducks/bridge/bridge.ts @@ -39,12 +39,6 @@ const bridgeSlice = createSlice({ resetInputFields: () => ({ ...initialState, }), - switchToAndFromTokens: (state, { payload }) => ({ - toChainId: payload, - fromToken: state.toToken, - toToken: state.fromToken, - fromTokenInputValue: null, - }), }, }); diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts index 8cd56928fc66..390a768e49a0 100644 --- a/ui/ducks/bridge/selectors.ts +++ b/ui/ducks/bridge/selectors.ts @@ -7,7 +7,6 @@ import { getNetworkConfigurationsByChainId, getIsBridgeEnabled, getSwapsDefaultToken, - SwapsEthToken, } from '../../selectors'; import { ALLOWED_BRIDGE_CHAIN_IDS } from '../../../shared/constants/bridge'; import { @@ -18,7 +17,6 @@ import { } from '../../../app/scripts/controllers/bridge/types'; import { createDeepEqualSelector } from '../../selectors/util'; import { getProviderConfig } from '../metamask/metamask'; -import { SwapsTokenObject } from '../../../shared/constants/swaps'; import { BridgeState } from './bridge'; type BridgeAppState = { @@ -108,17 +106,13 @@ export const getToTokens = (state: BridgeAppState) => { return state.bridge.toChainId ? state.metamask.bridgeState.destTokens : {}; }; -export const getFromToken = ( - state: BridgeAppState, -): SwapsTokenObject | SwapsEthToken => { +export const getFromToken = (state: BridgeAppState) => { return state.bridge.fromToken?.address ? state.bridge.fromToken : getSwapsDefaultToken(state); }; -export const getToToken = ( - state: BridgeAppState, -): SwapsTokenObject | SwapsEthToken | null => { +export const getToToken = (state: BridgeAppState) => { return state.bridge.toToken; }; diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx index 2fdb11289c5b..4d2bb4423c58 100644 --- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx +++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx @@ -1,13 +1,15 @@ -import React, { useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import classnames from 'classnames'; +import { debounce } from 'lodash'; import { setFromChain, setFromToken, setFromTokenInputValue, setToChain, + setToChainId, setToToken, - switchToAndFromTokens, + updateQuoteRequestParams, } from '../../../ducks/bridge/actions'; import { getFromAmount, @@ -28,11 +30,13 @@ import { ButtonIcon, IconName, } from '../../../components/component-library'; +import { BlockSize } from '../../../helpers/constants/design-system'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { TokenBucketPriority } from '../../../../shared/constants/swaps'; import { useTokensWithFiltering } from '../../../hooks/useTokensWithFiltering'; import { setActiveNetwork } from '../../../store/actions'; -import { BlockSize } from '../../../helpers/constants/design-system'; +import { Numeric } from '../../../../shared/modules/Numeric'; +import { hexToDecimal } from '../../../../shared/modules/conversion.utils'; import { BridgeInputGroup } from './bridge-input-group'; const PrepareBridgePage = () => { @@ -71,6 +75,35 @@ const PrepareBridgePage = () => { const [rotateSwitchTokens, setRotateSwitchTokens] = useState(false); + const quoteParams = useMemo( + () => ({ + srcTokenAddress: fromToken?.address, + destTokenAddress: toToken?.address || undefined, + srcTokenAmount: + fromAmount && fromAmount !== '' && fromToken?.decimals + ? Numeric.from(fromAmount, 10) + .shiftedBy(-1 * Number(fromToken.decimals)) + .toString() + : undefined, + srcChainId: fromChain?.chainId + ? Number(hexToDecimal(fromChain.chainId)) + : undefined, + destChainId: toChain?.chainId + ? Number(hexToDecimal(toChain.chainId)) + : undefined, + }), + [fromToken, toToken, fromChain?.chainId, toChain?.chainId, fromAmount], + ); + + const debouncedUpdateQuoteRequestInController = useCallback( + debounce((p) => dispatch(updateQuoteRequestParams(p)), 300), + [], + ); + + useEffect(() => { + debouncedUpdateQuoteRequestInController(quoteParams); + }, Object.values(quoteParams)); + return (
@@ -81,7 +114,10 @@ const PrepareBridgePage = () => { onAmountChange={(e) => { dispatch(setFromTokenInputValue(e)); }} - onAssetChange={(token) => dispatch(setFromToken(token))} + onAssetChange={(token) => { + dispatch(setFromToken(token)); + dispatch(setFromTokenInputValue(null)); + }} networkProps={{ network: fromChain, networks: fromChains, @@ -94,6 +130,8 @@ const PrepareBridgePage = () => { ), ); dispatch(setFromChain(networkConfig.chainId)); + dispatch(setFromToken(null)); + dispatch(setFromTokenInputValue(null)); }, }} customTokenListGenerator={ @@ -121,12 +159,18 @@ const PrepareBridgePage = () => { onClick={() => { setRotateSwitchTokens(!rotateSwitchTokens); const toChainClientId = - toChain?.defaultRpcEndpointIndex && toChain?.rpcEndpoints - ? toChain.rpcEndpoints?.[toChain.defaultRpcEndpointIndex] + toChain?.defaultRpcEndpointIndex !== undefined && + toChain?.rpcEndpoints + ? toChain.rpcEndpoints[toChain.defaultRpcEndpointIndex] .networkClientId : undefined; toChainClientId && dispatch(setActiveNetwork(toChainClientId)); - dispatch(switchToAndFromTokens({ fromChain })); + toChain && dispatch(setFromChain(toChain.chainId)); + dispatch(setFromToken(toToken)); + dispatch(setFromTokenInputValue(null)); + fromChain?.chainId && dispatch(setToChain(fromChain.chainId)); + fromChain?.chainId && dispatch(setToChainId(fromChain.chainId)); + dispatch(setToToken(fromToken)); }} /> @@ -140,6 +184,7 @@ const PrepareBridgePage = () => { network: toChain, networks: toChains, onNetworkChange: (networkConfig) => { + dispatch(setToChainId(networkConfig.chainId)); dispatch(setToChain(networkConfig.chainId)); }, }}