diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index 5df1e17952c..b8fc09d3dfb 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add optional `active_ab_tests` property in Unified SwapBridge metrics event context and payload types, alongside existing `ab_tests`. ([#8152](https://github.com/MetaMask/core/pull/8152)) +### Fixed + +- Check whether `selectedQuote` exists in `selectBridgeQuotes.sortedQuotes` before returning it as the `activeQuote`. Fall back on the `recommendedQuote` if selectedQuote is stale ([#8154](https://github.com/MetaMask/core/pull/8154)) + ## [69.0.1] ### Changed diff --git a/packages/bridge-controller/src/selectors.test.ts b/packages/bridge-controller/src/selectors.test.ts index 2083a4b0f32..f05d1d2615a 100644 --- a/packages/bridge-controller/src/selectors.test.ts +++ b/packages/bridge-controller/src/selectors.test.ts @@ -1256,12 +1256,37 @@ describe('Bridge Selectors', () => { }); it('should handle selected quote', () => { + const selectedQuote = { + ...mockState.quotes[0], + quote: { ...mockState.quotes[0].quote, requestId: '123' }, + } as never; const result = selectBridgeQuotes(mockState, { ...mockClientParams, - selectedQuote: mockQuote as never, + selectedQuote, }); - expect(result.activeQuote).toStrictEqual(mockQuote); + expect(result.recommendedQuote).toStrictEqual( + expect.objectContaining(mockState.quotes[1]), + ); + expect(result.activeQuote).toStrictEqual( + expect.objectContaining(selectedQuote), + ); + }); + + it('should set recommendedQuote as activeQuote when selected quote is not found', () => { + const selectedQuote = { + ...mockState.quotes[0], + quote: { ...mockState.quotes[0].quote, requestId: 'abc' }, + } as never; + const result = selectBridgeQuotes(mockState, { + ...mockClientParams, + selectedQuote, + }); + + expect(result.recommendedQuote).toStrictEqual( + expect.objectContaining(mockState.quotes[1]), + ); + expect(result.activeQuote).toStrictEqual(result.recommendedQuote); }); it('should handle quote refresh state', () => { diff --git a/packages/bridge-controller/src/selectors.ts b/packages/bridge-controller/src/selectors.ts index 2a969663c2c..8597bcb526a 100644 --- a/packages/bridge-controller/src/selectors.ts +++ b/packages/bridge-controller/src/selectors.ts @@ -414,15 +414,15 @@ const selectBridgeQuotesWithMetadata = createBridgeSelector( minToTokenAmount, swapRate: calcSwapRate(sentAmount.amount, toTokenAmount.amount), /** - This is the amount required to submit the transactions - Includes the relayer fee or other native fees + This is the amount required to submit all the transactions. + Includes the relayer fee or other native fees. Should be used for balance checks and tx submission. */ totalNetworkFee: totalEstimatedNetworkFee, totalMaxNetworkFee, /** This contains gas fee estimates for the bridge transaction - Does not include the relayer fee (if needed), just the gasLimit and effectiveGas returned by the bridge API + Does not include the relayer fee (if needed), just the gasLimit and effectiveGas returned by the bridge API. Should only be used for display purposes. */ gasFee, @@ -485,9 +485,13 @@ const selectRecommendedQuote = createBridgeSelector( const selectActiveQuote = createBridgeSelector( [ selectRecommendedQuote, - (_, { selectedQuote }: BridgeQuotesClientParams) => selectedQuote, + selectSortedBridgeQuotes, + (_, { selectedQuote }) => selectedQuote, ], - (recommendedQuote, selectedQuote) => selectedQuote ?? recommendedQuote, + (recommendedQuote, sortedQuotes, selectedQuote) => + sortedQuotes.find( + (quote) => quote.quote.requestId === selectedQuote?.quote.requestId, + ) ?? recommendedQuote, ); const selectIsQuoteGoingToRefresh = createBridgeSelector( diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index 12d265eb5b5..7ee7cc18997 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -114,7 +114,8 @@ export type ExchangeRate = { exchangeRate?: string; usdExchangeRate?: string }; */ export type QuoteMetadata = { /** - * If gas is included, this is the value of the src or dest token that was used to pay for the gas + * If gas is included, this is the value of the src or dest token that was used to pay for the gas. + * Show this value to indicate transaction fees for gasless quotes. */ includedTxFees?: TokenAmountValues | null; /** @@ -125,6 +126,12 @@ export type QuoteMetadata = { * max is the max gas fee that will be used by the transaction. */ gasFee: Record<'effective' | 'total' | 'max', TokenAmountValues>; + /** + * The total network fee required to submit the trade and any approvals. This includes + * the relayer fee or other native fees. Should be used for balance checks and tx submission. + * Note: This is only accurate for non-gasless transactions. Use {@link QuoteMetadata.includedTxFees} to + * get the total network fee for gasless transactions. + */ totalNetworkFee: TokenAmountValues; // estimatedGasFees + relayerFees totalMaxNetworkFee: TokenAmountValues; // maxGasFees + relayerFees /** @@ -136,16 +143,24 @@ export type QuoteMetadata = { */ minToTokenAmount: TokenAmountValues; /** - * If gas is included: toTokenAmount - * Otherwise: toTokenAmount - totalNetworkFee + * If gas is included: {@link QuoteMetadata.toTokenAmount} - {@link QuoteMetadata.includedTxFees}. + * Otherwise: {@link QuoteMetadata.toTokenAmount} - {@link QuoteMetadata.totalNetworkFee}. */ adjustedReturn: Omit; /** - * The amount that the user will send, including fees - * srcTokenAmount + metabridgeFee + txFee + * The amount that the user will send, including fees that are paid in the src token + * {@link Quote.srcTokenAmount} + {@link Quote.feeData[FeeType.METABRIDGE].amount} + {@link Quote.feeData[FeeType.TX_FEE].amount} */ sentAmount: TokenAmountValues; - swapRate: string; // destTokenAmount / sentAmount + /** + * The swap rate is the amount that the user will receive per amount sent. Accounts for fees paid in the src or dest token. + * This is calculated as {@link QuoteMetadata.toTokenAmount} / {@link QuoteMetadata.sentAmount}. + */ + swapRate: string; + /** + * The cost of the trade, which is the difference between the amount sent and the adjusted return. + * This is calculated as {@link QuoteMetadata.sentAmount} - {@link QuoteMetadata.adjustedReturn}. + */ cost: Omit; // sentAmount - adjustedReturn };