From 88113db168fecc55197ba6d9d858b688928f4cf4 Mon Sep 17 00:00:00 2001 From: micaelae Date: Mon, 31 Aug 2026 11:14:50 -0700 Subject: [PATCH 1/5] fix: sort by cost --- .../bridge-controller/src/selectors.test.ts | 158 ++++++++++++++++-- packages/bridge-controller/src/selectors.ts | 12 +- 2 files changed, 155 insertions(+), 15 deletions(-) diff --git a/packages/bridge-controller/src/selectors.test.ts b/packages/bridge-controller/src/selectors.test.ts index 39983e7caa..3975d10072 100644 --- a/packages/bridge-controller/src/selectors.test.ts +++ b/packages/bridge-controller/src/selectors.test.ts @@ -285,6 +285,30 @@ describe('Bridge Selectors', () => { ), ).toStrictEqual({}); }); + + it('should treat missing conversionRate and usdConversionRate as 0 for EVM tokens', () => { + const result = selectExchangeRateByAssetId( + { + ...mockExchangeRateSources, + currencyRates: { + ETH: {}, + }, + marketData: { + '0x1': { + [MOCK_MUSD_ADDRESS]: { + price: 2, + currency: 'ETH', + }, + }, + }, + } as unknown as BridgeAppState, + formatAddressToAssetId(MOCK_MUSD_ADDRESS.toLowerCase(), '1'), + ); + expect(result).toStrictEqual({ + exchangeRate: '0', + usdExchangeRate: '0', + }); + }); }); describe('selectIsAssetExchangeRateInState', () => { @@ -1052,6 +1076,106 @@ describe('Bridge Selectors', () => { ).toBe('-0.02'); }); + it('should use priceImpact.valueInCurrency to sort quotes if cost and amount are unavailable (Phase 1.5)', () => { + const mockState = getMockState(1); + const quotes = [ + { + ...mockState.quotes[0], + quote: { + ...mockState.quotes[0].quote, + priceData: { priceImpact: { usd: '10' } }, + }, + }, + { + ...mockState.quotes[1], + quote: { + ...mockState.quotes[1].quote, + priceData: { priceImpact: { usd: '1' } }, + }, + }, + ]; + const { recommendedQuote, sortedQuotes } = selectBridgeQuotes( + { + ...mockState, + assetExchangeRates: {}, + marketData: {}, + quotes, + currencyRates: { + ETH: { + conversionRate: 1980, + usdConversionRate: 10, + }, + }, + }, + { + ...mockClientParams, + migrationPhase: QuoteMetadataMigrationPhase.V2WithV1Fallback, + }, + ); + + expect( + sortedQuotes.every( + (quote) => + !quote.cost?.valueInCurrency && + !quote.quote.priceData?.priceImpact?.amount, + ), + ).toBe(true); + expect( + sortedQuotes.map(({ quote }) => [ + quote.requestId, + quote.priceData?.priceImpact?.usd, + quote.priceData?.priceImpact?.valueInCurrency, + ]), + ).toStrictEqual([ + ['456', '1', '198'], + ['123', '10', '1980'], + ]); + expect(recommendedQuote?.quote.requestId).toBe('456'); + }); + + it('does not derive fiat from usd when the src native rate pair is missing (Phase 1.5)', () => { + const mockState = getMockState(1); + const quotes = mockState.quotes.map((quote) => ({ + ...quote, + quote: { + ...quote.quote, + priceData: { priceImpact: { usd: '10' } }, + }, + })); + const { sortedQuotes } = selectBridgeQuotes( + { + ...mockState, + quotes, + assetExchangeRates: {}, + marketData: {}, + currencyRates: {}, + }, + { + ...mockClientParams, + migrationPhase: QuoteMetadataMigrationPhase.V2WithV1Fallback, + }, + ); + + expect( + sortedQuotes.map( + (quote) => quote.quote.priceData?.priceImpact?.valueInCurrency, + ), + ).toStrictEqual([undefined, undefined]); + }); + + it('calculates metadata when quoteRequest is empty', () => { + const mockState = getMockState(1); + const { sortedQuotes } = selectBridgeQuotes( + { + ...mockState, + quoteRequest: [], + }, + mockClientParams, + ); + + expect(sortedQuotes).toHaveLength(mockState.quotes.length); + }); + describe('returns swap metadata', () => { const getMockSwapState = ( srcAsset: Omit, @@ -2083,7 +2207,7 @@ describe('Bridge Selectors', () => { expect(totalReceived).toMatchInlineSnapshot(` { - "amount": "38240503", + "amount": "38423182", "asset": { "assetId": "eip155:137/erc20:0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", "decimals": 6, @@ -2091,18 +2215,18 @@ describe('Bridge Selectors', () => { "name": "Native USD Coin (POS)", "symbol": "USDC", }, - "minAmount": "37460000", - "minAmountNormalized": "37.46", - "minAmountUsd": "37.46", - "minAmountValueInCurrency": "7492", - "normalizedAmount": "38.240503", - "usd": "38.240503", - "valueInCurrency": "7648.1006", + "minAmount": "37600000", + "minAmountNormalized": "37.6", + "minAmountUsd": "37.6", + "minAmountValueInCurrency": "7520", + "normalizedAmount": "38.423182", + "usd": "38.423182", + "valueInCurrency": "7684.6364", } `); expect(minimumReceived).toMatchInlineSnapshot(` { - "amount": "37460000", + "amount": "37600000", "asset": { "assetId": "eip155:137/erc20:0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", "decimals": 6, @@ -2110,9 +2234,9 @@ describe('Bridge Selectors', () => { "name": "Native USD Coin (POS)", "symbol": "USDC", }, - "normalizedAmount": "37.46", - "usd": "37.46", - "valueInCurrency": "7492", + "normalizedAmount": "37.6", + "usd": "37.6", + "valueInCurrency": "7520", } `); expect(rest).toMatchInlineSnapshot(` @@ -2123,10 +2247,18 @@ describe('Bridge Selectors', () => { "quotesRefreshCount": 0, } `); + expect(recommendedQuotes.map((quote) => quote?.cost?.valueInCurrency)) + .toMatchInlineSnapshot(` + [ + "-4867.87052548", + "24925.00683656", + ] + `); + expect(recommendedQuotes.map((quote) => quote?.quote.requestId)) .toMatchInlineSnapshot(` [ - "4277a368-40d7-4e82-aa67-74f29dc5f98a", + "381c23bc-e3e4-48fe-bc53-257471e388ad", "90ae8e69-f03a-4cf6-bab7-ed4e3431eb37", ] `); diff --git a/packages/bridge-controller/src/selectors.ts b/packages/bridge-controller/src/selectors.ts index 39677f3c21..7c74222ff9 100644 --- a/packages/bridge-controller/src/selectors.ts +++ b/packages/bridge-controller/src/selectors.ts @@ -458,6 +458,13 @@ const selectSortedBridgeQuotes = createBridgeSelector( 'asc', ); default: + if (quotesWithMetadata.every((quote) => quote.cost?.valueInCurrency)) { + return orderBy( + quotesWithMetadata, + ({ cost }) => Number(cost?.valueInCurrency), + 'asc', + ); + } if ( quotesWithMetadata.every( (quote) => quote.quote.priceData?.priceImpact?.amount, @@ -469,7 +476,8 @@ const selectSortedBridgeQuotes = createBridgeSelector( Number(priceData?.priceImpact?.amount), 'asc', ); - } else if ( + } + if ( quotesWithMetadata.every( (quote) => quote.quote.priceData?.priceImpact?.valueInCurrency, ) @@ -585,7 +593,7 @@ const selectRecommendedQuotes = createBridgeSelector( const requestIndex = quote.quoteRequestIndex ?? 0; acc[requestIndex] ??= quote; return acc; - }, Array(requestCount).fill(null)), + }, Array<(QuoteResponse & QuoteMetadata) | null>(requestCount).fill(null)), ); const selectDestAmountSum = createBridgeSelector( From afff84b665e89b89aab0689968c4962e87f82d17 Mon Sep 17 00:00:00 2001 From: micaelae Date: Fri, 28 Aug 2026 15:25:12 -0700 Subject: [PATCH 2/5] feat: read cost from backend --- packages/bridge-controller/src/validators/quote.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/bridge-controller/src/validators/quote.ts b/packages/bridge-controller/src/validators/quote.ts index ca318670c9..7514529689 100644 --- a/packages/bridge-controller/src/validators/quote.ts +++ b/packages/bridge-controller/src/validators/quote.ts @@ -156,6 +156,7 @@ export const QuoteSchemaV2 = intersection([ 'usd', 'valueInCurrency', ]), + cost: pick(AmountsAndAssetSchema, ['usd', 'valueInCurrency']), }), ), ), From abc8c12049b593f449536d65af39caae5c958b18 Mon Sep 17 00:00:00 2001 From: micaelae Date: Fri, 28 Aug 2026 15:25:52 -0700 Subject: [PATCH 3/5] fix: stop using priceImpact as cost --- .../src/__snapshots__/selectors.test.ts.snap | 8 ++++++++ packages/bridge-controller/src/selectors.test.ts | 11 ++++++++++- packages/bridge-controller/src/selectors.ts | 11 ++++++++--- .../src/utils/quote-metadata/to-quote-metadata-v1.ts | 5 +++-- .../src/utils/quote-metadata/to-quote-metadata-v2.ts | 11 +++++------ 5 files changed, 34 insertions(+), 12 deletions(-) diff --git a/packages/bridge-controller/src/__snapshots__/selectors.test.ts.snap b/packages/bridge-controller/src/__snapshots__/selectors.test.ts.snap index aed0f15f23..8ec4aae912 100644 --- a/packages/bridge-controller/src/__snapshots__/selectors.test.ts.snap +++ b/packages/bridge-controller/src/__snapshots__/selectors.test.ts.snap @@ -101,6 +101,10 @@ exports[`Bridge Selectors selectBridgeQuotes should return sorted quotes with me "usd": "2.099927", "valueInCurrency": "419.985546", }, + "cost": { + "usd": "8.900073", + "valueInCurrency": "1758.014454", + }, "priceImpact": { "usd": "7.9", "valueInCurrency": "1564.2", @@ -350,6 +354,10 @@ exports[`Bridge Selectors selectBridgeQuotes should return sorted quotes with me "usd": "2.099927", "valueInCurrency": "419.985546", }, + "cost": { + "usd": "8.900073", + "valueInCurrency": "1758.014454", + }, "priceImpact": { "usd": "8.9", "valueInCurrency": "1758", diff --git a/packages/bridge-controller/src/selectors.test.ts b/packages/bridge-controller/src/selectors.test.ts index 3975d10072..7e1efb17d9 100644 --- a/packages/bridge-controller/src/selectors.test.ts +++ b/packages/bridge-controller/src/selectors.test.ts @@ -2254,7 +2254,16 @@ describe('Bridge Selectors', () => { "24925.00683656", ] `); - + expect( + recommendedQuotes.map( + (quote) => quote?.quote.priceData?.cost?.valueInCurrency, + ), + ).toMatchInlineSnapshot(` + [ + "-4867.87052548", + "24925.00683656", + ] + `); expect(recommendedQuotes.map((quote) => quote?.quote.requestId)) .toMatchInlineSnapshot(` [ diff --git a/packages/bridge-controller/src/selectors.ts b/packages/bridge-controller/src/selectors.ts index 7c74222ff9..aa3bd9bce9 100644 --- a/packages/bridge-controller/src/selectors.ts +++ b/packages/bridge-controller/src/selectors.ts @@ -449,7 +449,7 @@ const selectSortedBridgeQuotes = createBridgeSelector( selectBridgeQuotesWithMetadata, (_, { sortOrder }: BridgeQuotesClientParams) => sortOrder, ], - (quotesWithMetadata, sortOrder): (QuoteResponse & QuoteMetadata)[] => { + (quotesWithMetadata, sortOrder): QuoteResponse[] => { switch (sortOrder) { case SortOrder.ETA_ASC: return orderBy( @@ -458,10 +458,15 @@ const selectSortedBridgeQuotes = createBridgeSelector( 'asc', ); default: - if (quotesWithMetadata.every((quote) => quote.cost?.valueInCurrency)) { + if ( + quotesWithMetadata.every( + (quote) => quote.quote.priceData?.cost?.valueInCurrency, + ) + ) { return orderBy( quotesWithMetadata, - ({ cost }) => Number(cost?.valueInCurrency), + ({ quote: { priceData } }) => + Number(priceData?.cost?.valueInCurrency), 'asc', ); } diff --git a/packages/bridge-controller/src/utils/quote-metadata/to-quote-metadata-v1.ts b/packages/bridge-controller/src/utils/quote-metadata/to-quote-metadata-v1.ts index 9f37cb4e11..64d78455c5 100644 --- a/packages/bridge-controller/src/utils/quote-metadata/to-quote-metadata-v1.ts +++ b/packages/bridge-controller/src/utils/quote-metadata/to-quote-metadata-v1.ts @@ -106,8 +106,9 @@ export const toQuoteMetadataV1 = ( }), ...includeIfTruthy(priceData?.priceImpact, { priceImpact: toTokenAmountValues(priceData?.priceImpact), - // Use priceImpact as cost - cost: toTokenAmountValues(priceData?.priceImpact), + }), + ...includeIfTruthy(priceData?.cost, { + cost: toTokenAmountValues(priceData?.cost), }), ...includeIfTruthy(relayer?.[0], { relayerFee: toTokenAmountValues(relayer?.[0]), diff --git a/packages/bridge-controller/src/utils/quote-metadata/to-quote-metadata-v2.ts b/packages/bridge-controller/src/utils/quote-metadata/to-quote-metadata-v2.ts index 889cb124c5..b79c617e8c 100644 --- a/packages/bridge-controller/src/utils/quote-metadata/to-quote-metadata-v2.ts +++ b/packages/bridge-controller/src/utils/quote-metadata/to-quote-metadata-v2.ts @@ -61,10 +61,6 @@ export const toQuoteMetadataV2 = ( : undefined; const txFeeAsset = quoteResponseV2?.quote?.feeData?.txFee?.[0]?.asset; - const priceImpactToUse = { - usd: priceImpact?.usd ?? cost?.usd, - valueInCurrency: priceImpact?.valueInCurrency ?? cost?.valueInCurrency, - }; const networkFeeToUse = gasFee?.total ?? totalNetworkFee; return { @@ -101,12 +97,15 @@ export const toQuoteMetadataV2 = ( }), }, priceData: { - ...includeIfTruthy(priceImpactToUse, { - priceImpact: priceImpactToUse, + ...includeIfTruthy(priceImpact, { + priceImpact, }), ...includeIfTruthy(adjustedReturn, { adjustedReturn, }), + ...includeIfTruthy(cost, { + cost, + }), swapRate, }, }, From f8fe28a8fb885abba7498e00a71733b2cc2d7399 Mon Sep 17 00:00:00 2001 From: micaelae Date: Mon, 31 Aug 2026 11:50:39 -0700 Subject: [PATCH 4/5] changelog --- packages/bridge-controller/CHANGELOG.md | 4 ++++ packages/bridge-controller/src/selectors.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index fc3584c880..39ae78a600 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Read `priceData.cost` from backend-provided quote metadata and use it to sort quotes ([#10012](https://github.com/MetaMask/core/pull/10012)) + ## [80.1.0] ### Added diff --git a/packages/bridge-controller/src/selectors.ts b/packages/bridge-controller/src/selectors.ts index aa3bd9bce9..7d71624adf 100644 --- a/packages/bridge-controller/src/selectors.ts +++ b/packages/bridge-controller/src/selectors.ts @@ -598,7 +598,7 @@ const selectRecommendedQuotes = createBridgeSelector( const requestIndex = quote.quoteRequestIndex ?? 0; acc[requestIndex] ??= quote; return acc; - }, Array<(QuoteResponse & QuoteMetadata) | null>(requestCount).fill(null)), + }, Array(requestCount).fill(null)), ); const selectDestAmountSum = createBridgeSelector( From d6091c64835097639632f5881d638f2298d5595b Mon Sep 17 00:00:00 2001 From: micaelae Date: Mon, 31 Aug 2026 11:59:34 -0700 Subject: [PATCH 5/5] chore: cost --- .../src/utils/quote-metadata/to-currency-values.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/bridge-controller/src/utils/quote-metadata/to-currency-values.ts b/packages/bridge-controller/src/utils/quote-metadata/to-currency-values.ts index d5fe176506..c5b511c3fe 100644 --- a/packages/bridge-controller/src/utils/quote-metadata/to-currency-values.ts +++ b/packages/bridge-controller/src/utils/quote-metadata/to-currency-values.ts @@ -34,10 +34,11 @@ export const toCurrencyValues = ( quote: { src, dest, feeData, priceData }, } = quote; - const { adjustedReturn, priceImpact } = priceData ?? {}; + const { adjustedReturn, priceImpact, cost } = priceData ?? {}; const priceImpactFiat = toCurrency(priceImpact, usdToFiatExchangeRate); const adjustedReturnFiat = toCurrency(adjustedReturn, usdToFiatExchangeRate); + const costFiat = toCurrency(cost, usdToFiatExchangeRate); const minAmountValueInCurrency = toCurrency( { @@ -68,7 +69,7 @@ export const toCurrencyValues = ( ), ]), ), - ...((priceImpactFiat ?? adjustedReturnFiat) && { + ...((priceImpactFiat ?? adjustedReturnFiat ?? costFiat) && { priceData: { ...(priceImpactFiat && { priceImpact: priceImpactFiat, @@ -76,6 +77,9 @@ export const toCurrencyValues = ( ...(adjustedReturnFiat && { adjustedReturn: adjustedReturnFiat, }), + ...(costFiat && { + cost: costFiat, + }), }, }), },