diff --git a/ui/ducks/bridge/actions.test.ts b/ui/ducks/bridge/actions.test.ts index af1e13379533..0ac66337b26e 100644 --- a/ui/ducks/bridge/actions.test.ts +++ b/ui/ducks/bridge/actions.test.ts @@ -6,7 +6,6 @@ import * as actions from '../../store/actions'; import * as selectors from '../../selectors'; import { submitBridgeTransaction } from './actions'; import { DummyQuotesNoApproval, DummyQuotesWithApproval } from './dummy-quotes'; -import * as bridgeSelectors from './selectors'; jest.mock('../../store/actions', () => { const original = jest.requireActual('../../store/actions'); @@ -18,13 +17,6 @@ jest.mock('../../store/actions', () => { }; }); -jest.mock('./selectors', () => { - const original = jest.requireActual('./selectors'); - return { - ...original, - getQuotes: jest.fn(), - }; -}); jest.mock('../../selectors', () => { const original = jest.requireActual('../../selectors'); return { @@ -119,10 +111,6 @@ describe('bridge/actions', () => { }; }); - (bridgeSelectors.getQuotes as jest.Mock).mockImplementation( - () => DummyQuotesWithApproval.ETH_11_USDC_TO_ARB, - ); - // For some reason, setBackgroundConnection does not work, gets hung up on the promise, so mock this way instead (actions.addTransactionAndWaitForPublish as jest.Mock).mockImplementation( mockAddTransactionAndWaitForPublish, @@ -131,7 +119,12 @@ describe('bridge/actions', () => { const history = makeMockHistory(); // Execute - await store.dispatch(submitBridgeTransaction(history as any) as any); + await store.dispatch( + submitBridgeTransaction( + DummyQuotesWithApproval.ETH_11_USDC_TO_ARB[0] as any, + history as any, + ) as any, + ); // Assert expect(mockAddTransactionAndWaitForPublish).toHaveBeenLastCalledWith( @@ -173,10 +166,6 @@ describe('bridge/actions', () => { }; }); - (bridgeSelectors.getQuotes as jest.Mock).mockImplementation( - () => DummyQuotesWithApproval.ETH_11_USDC_TO_ARB, - ); - // For some reason, setBackgroundConnection does not work, gets hung up on the promise, so mock this way instead (actions.addTransactionAndWaitForPublish as jest.Mock).mockImplementation( mockAddTransactionAndWaitForPublish, @@ -185,7 +174,12 @@ describe('bridge/actions', () => { const history = makeMockHistory(); // Execute - await store.dispatch(submitBridgeTransaction(history as any) as any); + await store.dispatch( + submitBridgeTransaction( + DummyQuotesWithApproval.ETH_11_USDC_TO_ARB[0] as any, + history as any, + ) as any, + ); // Assert expect(mockAddTransactionAndWaitForPublish).toHaveBeenNthCalledWith( @@ -247,15 +241,17 @@ describe('bridge/actions', () => { const store = makeMockStore(); const history = makeMockHistory(); - (bridgeSelectors.getQuotes as jest.Mock).mockImplementation( - () => DummyQuotesWithApproval.ETH_11_USDC_TO_ARB, - ); (actions.addToken as jest.Mock).mockImplementation( () => async () => ({}), ); // Execute - await store.dispatch(submitBridgeTransaction(history as any) as any); + await store.dispatch( + submitBridgeTransaction( + DummyQuotesWithApproval.ETH_11_USDC_TO_ARB[0] as any, + history as any, + ) as any, + ); // Assert expect(actions.addToken).toHaveBeenCalledWith({ @@ -287,15 +283,17 @@ describe('bridge/actions', () => { (actions.addTransactionAndWaitForPublish as jest.Mock).mockImplementation( mockAddTransactionAndWaitForPublish, ); - (bridgeSelectors.getQuotes as jest.Mock).mockImplementation( - () => DummyQuotesNoApproval.OP_0_005_ETH_TO_ARB, - ); (actions.addToken as jest.Mock).mockImplementation( () => async () => ({}), ); // Execute - await store.dispatch(submitBridgeTransaction(history as any) as any); + await store.dispatch( + submitBridgeTransaction( + DummyQuotesNoApproval.OP_0_005_ETH_TO_ARB[0] as any, + history as any, + ) as any, + ); // Assert expect(actions.addToken).not.toHaveBeenCalled(); @@ -311,15 +309,17 @@ describe('bridge/actions', () => { const store = makeMockStore(); const history = makeMockHistory(); - (bridgeSelectors.getQuotes as jest.Mock).mockImplementation( - () => DummyQuotesWithApproval.ETH_11_USDC_TO_ARB, - ); (actions.addToken as jest.Mock).mockImplementation( () => async () => ({}), ); // Execute - await store.dispatch(submitBridgeTransaction(history as any) as any); + await store.dispatch( + submitBridgeTransaction( + DummyQuotesWithApproval.ETH_11_USDC_TO_ARB[0] as any, + history as any, + ) as any, + ); // Assert expect(actions.addToken).toHaveBeenCalledWith({ @@ -351,15 +351,17 @@ describe('bridge/actions', () => { (actions.addTransactionAndWaitForPublish as jest.Mock).mockImplementation( mockAddTransactionAndWaitForPublish, ); - (bridgeSelectors.getQuotes as jest.Mock).mockImplementation( - () => DummyQuotesNoApproval.OP_0_005_ETH_TO_ARB, - ); (actions.addToken as jest.Mock).mockImplementation( () => async () => ({}), ); // Execute - await store.dispatch(submitBridgeTransaction(history as any) as any); + await store.dispatch( + submitBridgeTransaction( + DummyQuotesNoApproval.OP_0_005_ETH_TO_ARB[0] as any, + history as any, + ) as any, + ); // Assert expect(actions.addToken).not.toHaveBeenCalled(); @@ -385,12 +387,11 @@ describe('bridge/actions', () => { mockAddTransactionAndWaitForPublish, ); - (bridgeSelectors.getQuotes as jest.Mock).mockImplementation( - () => DummyQuotesWithApproval.ETH_11_USDC_TO_ARB, - ); - ( - selectors.getNetworkConfigurationsByChainId as jest.Mock - ).mockImplementation(() => ({ + const mockedGetNetworkConfigurationsByChainId = + // @ts-expect-error this is a jest mock + selectors.getNetworkConfigurationsByChainId as jest.Mock; + + mockedGetNetworkConfigurationsByChainId.mockImplementation(() => ({ '0x1': { blockExplorerUrls: ['https://etherscan.io'], chainId: '0x1', @@ -424,7 +425,12 @@ describe('bridge/actions', () => { })); // Execute - await store.dispatch(submitBridgeTransaction(history as any) as any); + await store.dispatch( + submitBridgeTransaction( + DummyQuotesWithApproval.ETH_11_USDC_TO_ARB[0] as any, + history as any, + ) as any, + ); // Assert expect(actions.addNetwork).toHaveBeenCalledWith({ @@ -447,7 +453,12 @@ describe('bridge/actions', () => { const history = makeMockHistory(); // Execute - await store.dispatch(submitBridgeTransaction(history as any) as any); + await store.dispatch( + submitBridgeTransaction( + DummyQuotesWithApproval.ETH_11_USDC_TO_ARB[0] as any, + history as any, + ) as any, + ); // Assert expect(history.push).toHaveBeenCalledWith('/'); diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts index 259233054044..44e7fde92b9c 100644 --- a/ui/ducks/bridge/actions.ts +++ b/ui/ducks/bridge/actions.ts @@ -21,9 +21,12 @@ import { } from '../../store/actions'; import { submitRequestToBackground } from '../../store/background-connection'; import { + ChainId, FeeType, GasMultiplierByChainId, QuoteRequest, + QuoteResponse, + TxData, } from '../../pages/bridge/types'; import { AssetWithDisplayData, @@ -49,7 +52,6 @@ import { BridgeAppState, getApprovalGasMultipliers, getBridgeGasMultipliers, - getQuotes, } from './selectors'; const { @@ -186,6 +188,7 @@ export const getBridgeERC20Allowance = async ( }; export const submitBridgeTransaction = ( + quoteResponse: QuoteResponse, history: ReturnType, ) => { return async ( @@ -193,9 +196,6 @@ export const submitBridgeTransaction = ( getState: () => MetaMaskReduxState & BridgeAppState, ) => { const state = getState(); - const quoteMetas = getQuotes(state); - const quoteMeta = quoteMetas[0]; - // Track event TODO const calcFeePerGas = () => { @@ -238,12 +238,12 @@ export const submitBridgeTransaction = ( }: { txType: 'bridgeApproval' | 'bridge'; txParams: { - chainId: number; + chainId: ChainId; to: string; from: string; value: string; data: string; - gasLimit: number; + gasLimit: number | null; }; gasMultipliers: GasMultiplierByChainId; maxFeePerGas: string | undefined; @@ -259,7 +259,7 @@ export const submitBridgeTransaction = ( } const maxGasLimit = calcMaxGasLimit( - txParams.gasLimit, + txParams.gasLimit ?? 0, gasMultipliers[hexChainId], ); @@ -278,7 +278,7 @@ export const submitBridgeTransaction = ( type: txType, swaps: { hasApproveTx: - txType === 'bridge' ? Boolean(quoteMeta?.approval) : true, + txType === 'bridge' ? Boolean(quoteResponse?.approval) : true, meta, }, }); @@ -289,10 +289,12 @@ export const submitBridgeTransaction = ( }; const handleEthUsdtAllowanceReset = async ({ + approval, hexChainId, maxFeePerGas, maxPriorityFeePerGas, }: { + approval: TxData; hexChainId: Hex; maxFeePerGas: string | undefined; maxPriorityFeePerGas: string | undefined; @@ -303,8 +305,8 @@ export const submitBridgeTransaction = ( // quote.srcTokenAmount is actually after the fees // so we need to add fees back in for total allowance to give - const sentAmount = new BigNumber(quoteMeta.quote.srcTokenAmount) - .plus(quoteMeta.quote.feeData[FeeType.METABRIDGE].amount) + const sentAmount = new BigNumber(quoteResponse.quote.srcTokenAmount) + .plus(quoteResponse.quote.feeData[FeeType.METABRIDGE].amount) .toString(); const shouldResetApproval = allowance.lt(sentAmount) && allowance.gt(0); @@ -312,7 +314,7 @@ export const submitBridgeTransaction = ( if (shouldResetApproval) { const resetData = getEthUsdtResetData(); const txParams = { - ...quoteMeta.approval, + ...approval, data: resetData, }; const gasMultipliers = getApprovalGasMultipliers(state); @@ -331,21 +333,24 @@ export const submitBridgeTransaction = ( }; const handleApprovalTx = async ({ + approval, maxFeePerGas, maxPriorityFeePerGas, }: { + approval: TxData; maxFeePerGas: string | undefined; maxPriorityFeePerGas: string | undefined; }) => { const hexChainId = new Numeric( - quoteMeta.approval.chainId, + approval.chainId, 10, ).toPrefixedHexString() as `0x${string}`; // On Ethereum, we need to reset the allowance to 0 for USDT first if we need to set a new allowance // https://www.google.com/url?q=https://docs.unizen.io/trade-api/before-you-get-started/token-allowance-management-for-non-updatable-allowance-tokens&sa=D&source=docs&ust=1727386175513609&usg=AOvVaw3Opm6BSJeu7qO0Ve5iLTOh - if (isEthUsdt(hexChainId, quoteMeta.quote.srcAsset.address)) { + if (isEthUsdt(hexChainId, quoteResponse.quote.srcAsset.address)) { await handleEthUsdtAllowanceReset({ + approval, hexChainId, maxFeePerGas, maxPriorityFeePerGas, @@ -355,13 +360,13 @@ export const submitBridgeTransaction = ( const gasMultipliers = getApprovalGasMultipliers(state); const txMeta = await handleTx({ txType: 'bridgeApproval', - txParams: quoteMeta.approval, + txParams: approval, gasMultipliers, maxFeePerGas, maxPriorityFeePerGas, meta: { type: 'bridgeApproval', // TransactionType.bridgeApproval, // TODO - sourceTokenSymbol: quoteMeta.quote.srcAsset.symbol, + sourceTokenSymbol: quoteResponse.quote.srcAsset.symbol, }, }); @@ -380,7 +385,7 @@ export const submitBridgeTransaction = ( const gasMultipliers = getBridgeGasMultipliers(state); const txMeta = await handleTx({ txType: 'bridge', - txParams: quoteMeta.trade, + txParams: quoteResponse.trade, gasMultipliers, maxFeePerGas, maxPriorityFeePerGas, @@ -388,14 +393,14 @@ export const submitBridgeTransaction = ( // estimatedBaseFee: decEstimatedBaseFee, // swapMetaData, type: 'bridge', // TransactionType.bridge, // TODO add this type - sourceTokenSymbol: quoteMeta.quote.srcAsset.symbol, - destinationTokenSymbol: quoteMeta.quote.destAsset.symbol, - destinationTokenDecimals: quoteMeta.quote.destAsset.decimals, - destinationTokenAddress: quoteMeta.quote.destAsset.address, + sourceTokenSymbol: quoteResponse.quote.srcAsset.symbol, + destinationTokenSymbol: quoteResponse.quote.destAsset.symbol, + destinationTokenDecimals: quoteResponse.quote.destAsset.decimals, + destinationTokenAddress: quoteResponse.quote.destAsset.address, approvalTxId, // this is the decimal (non atomic) amount (not USD value) of source token to swap - swapTokenValue: new Numeric(quoteMeta.quote.srcTokenAmount, 10) - .shiftedBy(quoteMeta.quote.srcAsset.decimals) + swapTokenValue: new Numeric(quoteResponse.quote.srcTokenAmount, 10) + .shiftedBy(quoteResponse.quote.srcAsset.decimals) .toString(), }, }); @@ -411,10 +416,10 @@ export const submitBridgeTransaction = ( symbol, icon: image, chainId, - } = quoteMeta.quote.srcAsset; + } = quoteResponse.quote.srcAsset; const srcHexChainId = new Numeric( - quoteMeta.quote.srcChainId, + quoteResponse.quote.srcChainId, 10, ).toPrefixedHexString() as `0x${string}`; const tokenHexChainId = new Numeric( @@ -439,7 +444,7 @@ export const submitBridgeTransaction = ( const addDestToken = async () => { // Look up the destination chain - const hexDestChainId = new Numeric(quoteMeta.quote.destChainId, 10) + const hexDestChainId = new Numeric(quoteResponse.quote.destChainId, 10) .toPrefixedHexString() .toLowerCase() as `0x${string}`; const networkConfigurations = getNetworkConfigurationsByChainId(state); @@ -473,7 +478,7 @@ export const submitBridgeTransaction = ( decimals, symbol, icon: image, - } = quoteMeta.quote.destAsset; + } = quoteResponse.quote.destAsset; await dispatch( addToken({ address, @@ -490,8 +495,9 @@ export const submitBridgeTransaction = ( // Execute transaction(s) let approvalTxId: string | undefined; - if (quoteMeta?.approval) { + if (quoteResponse?.approval) { approvalTxId = await handleApprovalTx({ + approval: quoteResponse.approval, maxFeePerGas, maxPriorityFeePerGas, }); @@ -504,10 +510,10 @@ export const submitBridgeTransaction = ( }); // Add tokens if not the native gas token - if (quoteMeta.quote.srcAsset.address !== zeroAddress()) { + if (quoteResponse.quote.srcAsset.address !== zeroAddress()) { addSourceToken(); } - if (quoteMeta.quote.destAsset.address !== zeroAddress()) { + if (quoteResponse.quote.destAsset.address !== zeroAddress()) { await addDestToken(); } diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts index a80a8baeca2d..93e89e334ebc 100644 --- a/ui/ducks/bridge/selectors.ts +++ b/ui/ducks/bridge/selectors.ts @@ -26,7 +26,6 @@ import { calcTokenAmount } from '../../../shared/lib/transactions-controller-uti import { RequestStatus } from '../../../app/scripts/controllers/bridge/constants'; import { isValidQuoteRequest } from '../../pages/bridge/types'; import { BridgeState } from './bridge'; -import { DummyQuotesWithApproval } from './dummy-quotes'; export type BridgeAppState = { metamask: NetworkState & { bridgeState: BridgeControllerState } & { @@ -185,7 +184,3 @@ export const getIsBridgeTx = createDeepEqualSelector( ? fromChain.chainId !== toChain.chainId : false, ); - -export const getQuotes = (state: BridgeAppState) => { - return DummyQuotesWithApproval.ETH_11_USDC_TO_ARB; -}; diff --git a/ui/pages/bridge/prepare/bridge-cta-button.tsx b/ui/pages/bridge/prepare/bridge-cta-button.tsx index 0222e1bc9495..230af28a06b6 100644 --- a/ui/pages/bridge/prepare/bridge-cta-button.tsx +++ b/ui/pages/bridge/prepare/bridge-cta-button.tsx @@ -6,6 +6,7 @@ import { getFromAmount, getFromChain, getFromToken, + getRecommendedQuote, getToAmount, getToChain, getToToken, @@ -26,6 +27,8 @@ export const BridgeCTAButton = () => { const fromAmount = useSelector(getFromAmount); const toAmount = useSelector(getToAmount); + const quoteResponse = useSelector(getRecommendedQuote); + const isTxSubmittable = fromToken && toToken && fromChain && toChain && fromAmount && toAmount; @@ -42,7 +45,7 @@ export const BridgeCTAButton = () => { data-testid="bridge-cta-button" onClick={() => { if (isTxSubmittable) { - dispatch(submitBridgeTransaction(history)); + dispatch(submitBridgeTransaction(quoteResponse, history)); } }} disabled={!isTxSubmittable} diff --git a/ui/pages/bridge/types.ts b/ui/pages/bridge/types.ts index c55221a3b1f5..9ced8345caa6 100644 --- a/ui/pages/bridge/types.ts +++ b/ui/pages/bridge/types.ts @@ -128,7 +128,7 @@ export type QuoteResponse = { estimatedProcessingTimeInSeconds: number; }; -enum ChainId { +export enum ChainId { ETH = 1, OPTIMISM = 10, BSC = 56,