diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index 06a783bf428..9ca31f33d38 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **BREAKING:** Add `migrationPhase` to selector params, which determines how metadata is resolved ([#9744](https://github.com/MetaMask/core/pull/9744)) + - Export `QuoteMetadataMigrationPhase` constant + - Phase 1 omits V2 quote metadata provided by the QuoteResponse. Legacy metadata is served to the clients + - Phase 1.5 includes V2 quote metadata but falls back to legacy metadata + - Phase 2 omits legacy metadata from the quotes and enables removal of deprecated metadata utils + - Remove V1 support from mergeQuoteMetadata and add migrationPhase parameter - Bump `@metamask/transaction-controller` from `^69.5.0` to `^69.5.1` ([#9798](https://github.com/MetaMask/core/pull/9798)) - Bump `@metamask/accounts-controller` from `^39.0.6` to `^39.0.7` ([#9791](https://github.com/MetaMask/core/pull/9791)) - Bump `@metamask/multichain-network-controller` from `^3.2.2` to `^3.2.3` ([#9791](https://github.com/MetaMask/core/pull/9791)) diff --git a/packages/bridge-controller/src/__snapshots__/selectors.test.ts.snap b/packages/bridge-controller/src/__snapshots__/selectors.test.ts.snap new file mode 100644 index 00000000000..aed0f15f23e --- /dev/null +++ b/packages/bridge-controller/src/__snapshots__/selectors.test.ts.snap @@ -0,0 +1,403 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Bridge Selectors selectBridgeQuotes should return sorted quotes with metadata (Phase 1.5) 1`] = ` +{ + "adjustedReturn": { + "usd": "2.099927", + "valueInCurrency": "419.985546", + }, + "approval": { + "chainId": 1, + "data": "0x0", + "effectiveGas": 46000, + "from": "0x0000000000000000000000000000000000000000", + "gasLimit": 49000, + "to": "0x0000000000000000000000000000000000000000", + "value": "0x0", + }, + "chainId": "eip155:1", + "cost": { + "usd": "8.900073", + "valueInCurrency": "1758.014454", + }, + "estimatedProcessingTimeInSeconds": 300, + "gasFee": { + "total": { + "amount": "0.0000073", + "usd": "0.000073", + "valueInCurrency": "0.014454", + }, + }, + "minToTokenAmount": { + "amount": "1.8", + "usd": "1.8", + "valueInCurrency": "360", + }, + "namespace": "eip155", + "priceImpact": { + "usd": "8.9", + "valueInCurrency": "1758", + }, + "quote": { + "aggregator": "bridge1", + "dest": { + "amount": "2100000000000000000", + "asset": { + "assetId": "eip155:137/slip44:966", + "decimals": 18, + "name": "Polygon", + "symbol": "POL", + }, + "minAmount": "1800000000000000000", + "minAmountNormalized": "1.8", + "minAmountUsd": "1.8", + "minAmountValueInCurrency": "360", + "normalizedAmount": "2.1", + "usd": "2.1", + "valueInCurrency": "420", + }, + "feeData": { + "metabridge": [ + { + "amount": "100000000000000000", + "asset": { + "assetId": "eip155:1/slip44:60", + "decimals": 18, + "name": "Ethereum", + "symbol": "ETH", + }, + }, + ], + "network": [ + { + "amount": "7500000000000", + "asset": { + "assetId": "eip155:1/slip44:60", + "decimals": 18, + "name": "Ether", + "symbol": "ETH", + }, + "normalizedAmount": "0.0000075", + "usd": "0.01514", + "valueInCurrency": "2.99772", + }, + ], + "relayer": [ + { + "amount": "100000000000", + "asset": { + "assetId": "eip155:1/slip44:60", + "decimals": 18, + "name": "Ether", + "symbol": "ETH", + }, + "normalizedAmount": "0.0000001", + }, + ], + "txFee": undefined, + }, + "priceData": { + "adjustedReturn": { + "usd": "2.099927", + "valueInCurrency": "419.985546", + }, + "priceImpact": { + "usd": "7.9", + "valueInCurrency": "1564.2", + }, + "swapRate": "1.90909090909090909091", + }, + "protocols": [ + "bridge1", + ], + "requestId": "456", + "src": { + "amount": "1100000000000000000", + "asset": { + "assetId": "eip155:1/slip44:60", + "decimals": 18, + "name": "Ethereum", + "symbol": "ETH", + }, + "normalizedAmount": "1.1", + "usd": "1", + "valueInCurrency": "198", + }, + "steps": [], + }, + "sentAmount": { + "amount": "1.1", + "usd": "11", + "valueInCurrency": "2178", + }, + "swapRate": "1.90909090909090909091", + "toTokenAmount": { + "amount": "2.1", + "usd": "2.1", + "valueInCurrency": "420", + }, + "totalNetworkFee": { + "amount": "0.0000073", + "usd": "0.000073", + "valueInCurrency": "0.014454", + }, + "trade": { + "chainId": 1, + "data": "0x0", + "effectiveGas": 21000, + "from": "0x0000000000000000000000000000000000000000", + "gasLimit": 24000, + "to": "0x0000000000000000000000000000000000000000", + "value": "0x0", + }, +} +`; + +exports[`Bridge Selectors selectBridgeQuotes should return sorted quotes with metadata (Phase 2) 1`] = ` +{ + "approval": { + "chainId": 1, + "data": "0x0", + "effectiveGas": 46000, + "from": "0x0000000000000000000000000000000000000000", + "gasLimit": 49000, + "to": "0x0000000000000000000000000000000000000000", + "value": "0x0", + }, + "chainId": "eip155:1", + "estimatedProcessingTimeInSeconds": 300, + "namespace": "eip155", + "quote": { + "aggregator": "bridge1", + "dest": { + "amount": "2100000000000000000", + "asset": { + "assetId": "eip155:137/slip44:966", + "decimals": 18, + "name": "Polygon", + "symbol": "POL", + }, + "minAmount": "1800000000000000000", + "minAmountNormalized": "1.8", + "normalizedAmount": "2.1", + "usd": "2", + "valueInCurrency": "396", + }, + "feeData": { + "metabridge": [ + { + "amount": "100000000000000000", + "asset": { + "assetId": "eip155:1/slip44:60", + "decimals": 18, + "name": "Ethereum", + "symbol": "ETH", + }, + }, + ], + "network": [ + { + "amount": "7500000000000", + "asset": { + "assetId": "eip155:1/slip44:60", + "decimals": 18, + "name": "Ether", + "symbol": "ETH", + }, + "normalizedAmount": "0.0000075", + "usd": undefined, + }, + ], + "relayer": [ + { + "amount": "100000000000", + "asset": { + "assetId": "eip155:1/slip44:60", + "decimals": 18, + "name": "Ether", + "symbol": "ETH", + }, + "normalizedAmount": "0.0000001", + "usd": "0.0001", + "valueInCurrency": "0.0198", + }, + ], + "txFee": undefined, + }, + "priceData": { + "priceImpact": { + "usd": "7.9", + "valueInCurrency": "1564.2", + }, + }, + "protocols": [ + "bridge1", + ], + "requestId": "456", + "src": { + "amount": "1100000000000000000", + "asset": { + "assetId": "eip155:1/slip44:60", + "decimals": 18, + "name": "Ethereum", + "symbol": "ETH", + }, + "normalizedAmount": "1.1", + "usd": "1", + "valueInCurrency": "198", + }, + "steps": [], + }, + "trade": { + "chainId": 1, + "data": "0x0", + "effectiveGas": 21000, + "from": "0x0000000000000000000000000000000000000000", + "gasLimit": 24000, + "to": "0x0000000000000000000000000000000000000000", + "value": "0x0", + }, +} +`; + +exports[`Bridge Selectors selectBridgeQuotes should return sorted quotes with metadata 1`] = ` +{ + "adjustedReturn": { + "usd": "2.099927", + "valueInCurrency": "419.985546", + }, + "approval": { + "chainId": 1, + "data": "0x0", + "effectiveGas": 46000, + "from": "0x0000000000000000000000000000000000000000", + "gasLimit": 49000, + "to": "0x0000000000000000000000000000000000000000", + "value": "0x0", + }, + "chainId": "eip155:1", + "cost": { + "usd": "8.900073", + "valueInCurrency": "1758.014454", + }, + "estimatedProcessingTimeInSeconds": 300, + "gasFee": { + "total": { + "amount": "0.0000073", + "usd": "0.000073", + "valueInCurrency": "0.014454", + }, + }, + "minToTokenAmount": { + "amount": "1.8", + "usd": "1.8", + "valueInCurrency": "360", + }, + "namespace": "eip155", + "priceImpact": { + "usd": "8.9", + "valueInCurrency": "1758", + }, + "quote": { + "aggregator": "bridge1", + "dest": { + "amount": "2100000000000000000", + "asset": { + "assetId": "eip155:137/slip44:966", + "decimals": 18, + "name": "Polygon", + "symbol": "POL", + }, + "minAmount": "1800000000000000000", + "minAmountNormalized": "1.8", + "minAmountUsd": "1.8", + "minAmountValueInCurrency": "360", + "normalizedAmount": "2.1", + "usd": "2.1", + "valueInCurrency": "420", + }, + "feeData": { + "metabridge": [ + { + "amount": "100000000000000000", + "asset": { + "assetId": "eip155:1/slip44:60", + "decimals": 18, + "name": "Ethereum", + "symbol": "ETH", + }, + }, + ], + "network": [ + { + "amount": "7300000000000", + "asset": { + "assetId": "eip155:1/slip44:60", + "decimals": 18, + "name": "Ether", + "symbol": "ETH", + }, + "normalizedAmount": "0.0000073", + "usd": "0.000073", + "valueInCurrency": "0.014454", + }, + ], + "relayer": undefined, + "txFee": undefined, + }, + "priceData": { + "adjustedReturn": { + "usd": "2.099927", + "valueInCurrency": "419.985546", + }, + "priceImpact": { + "usd": "8.9", + "valueInCurrency": "1758", + }, + "swapRate": "1.90909090909090909091", + }, + "protocols": [ + "bridge1", + ], + "requestId": "456", + "src": { + "amount": "1100000000000000000", + "asset": { + "assetId": "eip155:1/slip44:60", + "decimals": 18, + "name": "Ethereum", + "symbol": "ETH", + }, + "normalizedAmount": "1.1", + "usd": "11", + "valueInCurrency": "2178", + }, + "steps": [], + }, + "sentAmount": { + "amount": "1.1", + "usd": "11", + "valueInCurrency": "2178", + }, + "swapRate": "1.90909090909090909091", + "toTokenAmount": { + "amount": "2.1", + "usd": "2.1", + "valueInCurrency": "420", + }, + "totalNetworkFee": { + "amount": "0.0000073", + "usd": "0.000073", + "valueInCurrency": "0.014454", + }, + "trade": { + "chainId": 1, + "data": "0x0", + "effectiveGas": 21000, + "from": "0x0000000000000000000000000000000000000000", + "gasLimit": 24000, + "to": "0x0000000000000000000000000000000000000000", + "value": "0x0", + }, +} +`; diff --git a/packages/bridge-controller/src/coercers/quote-response-v2-to-v1.test.ts b/packages/bridge-controller/src/coercers/quote-response-v2-to-v1.test.ts index be6db9f6029..d59dd70abf1 100644 --- a/packages/bridge-controller/src/coercers/quote-response-v2-to-v1.test.ts +++ b/packages/bridge-controller/src/coercers/quote-response-v2-to-v1.test.ts @@ -216,10 +216,10 @@ describe('quote-response-v1 compatibility', () => { }); it('should return a valid QuoteResponseV1 with V2 input (remove metadata)', () => { - const quoteResponseV1WithMetadata = mergeQuoteMetadata( - mockBridgeQuotesErc20Erc20V1[0], - MOCK_QUOTE_METADATA, - ); + const quoteResponseV1WithMetadata = { + ...mockBridgeQuotesErc20Erc20V1[0], + ...MOCK_QUOTE_METADATA, + }; // Build input data by converting V1 to V2 const quoteResponseV2 = mergeQuoteMetadata( @@ -271,10 +271,10 @@ describe('quote-response-v1 compatibility', () => { }); it('should return a valid QuoteResponseV1 with V1 input and metadata', () => { - const quoteResponse = mergeQuoteMetadata( - mockBridgeQuotesErc20Erc20V1[0], - MOCK_QUOTE_METADATA, - ); + const quoteResponse = { + ...mockBridgeQuotesErc20Erc20V1[0], + ...MOCK_QUOTE_METADATA, + }; // Convert to V1 const quoteResponseV1 = toQuoteResponseV1(quoteResponse); diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index 943a079c055..19b0eee762b 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -67,10 +67,11 @@ export type { DeepPartial, } from './types.js'; -export { - type QuoteMetadata, - type TokenAmountValues, +export type { + QuoteMetadata, + TokenAmountValues, } from './utils/quote-metadata/types.js'; +export { QuoteMetadataMigrationPhase } from './utils/quote-metadata/types.js'; export { validateQuoteResponseV1, QuoteResponseSchemaV1, diff --git a/packages/bridge-controller/src/selectors.test.ts b/packages/bridge-controller/src/selectors.test.ts index 3bc70a617d0..fad55f58092 100644 --- a/packages/bridge-controller/src/selectors.test.ts +++ b/packages/bridge-controller/src/selectors.test.ts @@ -44,6 +44,7 @@ import { import { calcQuoteMetadata } from './utils/quote-metadata/calculators.js'; import { mergeQuoteMetadata } from './utils/quote-metadata/merge.js'; import { toQuoteMetadataV1 } from './utils/quote-metadata/to-quote-metadata-v1.js'; +import { QuoteMetadataMigrationPhase } from './utils/quote-metadata/types.js'; import { BatchSellTransactionType } from './validators/batch-sell.js'; import type { BridgeAssetV2 } from './validators/bridge-asset.js'; import { validateQuoteResponseV1 } from './validators/quote-response-v1.js'; @@ -703,6 +704,8 @@ describe('Bridge Selectors', () => { ...quote, quote: { ...quote.quote, + src: { ...quote.quote.src, usd: '1' }, + dest: { ...quote.quote.dest, usd: '2' }, feeData: { ...quote.quote.feeData, network: [ @@ -736,8 +739,8 @@ describe('Bridge Selectors', () => { }, mockClientParams, ); - const quote = mockState.quotes[1]; - const expectedQuoteMetadata = calcQuoteMetadata(quote, { + + const expectedQuoteMetadata = calcQuoteMetadata(mockState.quotes[1], { srcTokenExchangeRate: { exchangeRate: '1980', usdExchangeRate: '10' }, bridgeFeesPerGas: { estimatedBaseFeeInDecGwei: '0', @@ -746,36 +749,145 @@ describe('Bridge Selectors', () => { destTokenExchangeRate: { exchangeRate: '200', usdExchangeRate: '1' }, nativeExchangeRate: { exchangeRate: '1980', usdExchangeRate: '10' }, }); - const expectedQuoteV2 = mergeQuoteMetadata(quote, expectedQuoteMetadata); - expect( - expectedQuoteV2?.quote?.priceData?.priceImpact?.usd, - ).toMatchInlineSnapshot(`"8.9"`); - - expect(result.sortedQuotes[0]).toStrictEqual(expectedQuoteV2); - - expect(result.recommendedQuote?.priceImpact?.valueInCurrency).toBe( - expectedQuoteV2.priceImpact?.valueInCurrency, - ); - expect(result.recommendedQuote?.quote.priceData?.priceImpact) - .toMatchInlineSnapshot(` - { - "usd": "8.9", - "valueInCurrency": "1758", - } - `); - expect(result.recommendedQuote?.priceImpact).toMatchInlineSnapshot(` - { - "usd": "8.9", - "valueInCurrency": "1758", - } - `); - expect(toQuoteMetadataV1(result.recommendedQuote)).toStrictEqual( expectedQuoteMetadata, ); - expect(result.sortedQuotes[0]).toStrictEqual(expectedQuoteV2); expect(result.sortedQuotes[0].cost?.valueInCurrency).toBe('1758.014454'); + // eslint-disable-next-line jest/no-restricted-matchers + expect(result.recommendedQuote).toMatchSnapshot(); + }); + + it('should return sorted quotes with metadata (Phase 1.5)', () => { + const migrationPhase = QuoteMetadataMigrationPhase.V2WithV1Fallback; + const mockState = getMockState(1); + const mockQuote = mockState.quotes[0]; + const quotes = mockState.quotes.map((quote) => ({ + ...quote, + quote: { + ...quote.quote, + src: { ...quote.quote.src, usd: '1' }, + feeData: { + ...quote.quote.feeData, + network: [ + { + amount: '7500000000000', + usd: '0.01514', + asset: toBridgeAssetV2(getNativeAssetForChainId(1)), + }, + ], + relayer: [ + { + amount: '100000000000', + asset: toBridgeAssetV2(getNativeAssetForChainId(1)), + }, + ], + }, + priceData: { + ...quote.quote.priceData, + ...(quote.quote.requestId === '456' && { + priceImpact: { + usd: '7.9', + }, + }), + }, + }, + })); + const { quotesInitialLoadTimeMs, quotesLastFetchedMs, ...result } = + selectBridgeQuotes( + { + ...mockState, + quotes, + assetExchangeRates: { + [mockQuote.quote.src.asset.assetId]: { + exchangeRate: '1980', + usdExchangeRate: '10', + }, + [mockQuote.quote.dest.asset.assetId]: { + exchangeRate: '200', + usdExchangeRate: '1', + }, + }, + }, + { ...mockClientParams, migrationPhase }, + ); + + const expectedQuoteMetadata = calcQuoteMetadata(quotes[1], { + srcTokenExchangeRate: { exchangeRate: '1980', usdExchangeRate: '10' }, + bridgeFeesPerGas: { + estimatedBaseFeeInDecGwei: '0', + feePerGasInDecGwei: '.1', + }, + destTokenExchangeRate: { exchangeRate: '200', usdExchangeRate: '1' }, + nativeExchangeRate: { exchangeRate: '1980', usdExchangeRate: '10' }, + }); + + // eslint-disable-next-line jest/no-restricted-matchers + expect(result.sortedQuotes[0]).toMatchSnapshot(); + expect(result.recommendedQuote).toMatchObject(expectedQuoteMetadata); + expect(result.recommendedQuote).not.toMatchObject( + toQuoteMetadataV1(result.recommendedQuote, migrationPhase), + ); + }); + + it('should return sorted quotes with metadata (Phase 2)', () => { + const migrationPhase = QuoteMetadataMigrationPhase.V2Only; + const mockState = getMockState(1); + const mockQuote = mockState.quotes[0]; + const quotes = mockState.quotes.map((quote) => ({ + ...quote, + quote: { + ...quote.quote, + src: { ...quote.quote.src, usd: '1' }, + dest: { ...quote.quote.dest, usd: '2' }, + feeData: { + ...quote.quote.feeData, + network: [ + { + amount: '7500000000000', + asset: toBridgeAssetV2(getNativeAssetForChainId(1)), + usd: undefined, + }, + ], + relayer: [ + { + amount: '100000000000', + usd: '0.0001', + asset: toBridgeAssetV2(getNativeAssetForChainId(1)), + }, + ], + }, + priceData: { + ...quote.quote.priceData, + ...(quote.quote.requestId === '456' && { + priceImpact: { + usd: '7.9', + }, + }), + }, + }, + })); + const { quotesInitialLoadTimeMs, quotesLastFetchedMs, ...result } = + selectBridgeQuotes( + { + ...mockState, + quotes, + assetExchangeRates: { + [mockQuote.quote.src.asset.assetId]: { + exchangeRate: '1980', + usdExchangeRate: '10', + }, + [mockQuote.quote.dest.asset.assetId]: { + exchangeRate: '200', + usdExchangeRate: '1', + }, + }, + }, + { ...mockClientParams, migrationPhase }, + ); + + // eslint-disable-next-line jest/no-restricted-matchers + expect(result.recommendedQuote).toMatchSnapshot(); }); it('should return metadata when quotes are empty', () => { @@ -1958,7 +2070,11 @@ describe('Bridge Selectors', () => { }, }, }, - { ...mockClientParams, requestCount: 2 }, + { + ...mockClientParams, + requestCount: 2, + migrationPhase: QuoteMetadataMigrationPhase.V1Data, + }, ); const { totalReceived, minimumReceived, recommendedQuotes, ...rest } = @@ -2063,7 +2179,11 @@ describe('Bridge Selectors', () => { }, }, }, - { ...mockClientParams, requestCount: 2 }, + { + ...mockClientParams, + requestCount: 2, + migrationPhase: QuoteMetadataMigrationPhase.V1Data, + }, ); const { totalReceived, minimumReceived, recommendedQuotes, ...rest } = @@ -2102,7 +2222,11 @@ describe('Bridge Selectors', () => { }, }, }, - { ...mockClientParams, requestCount: 1 }, + { + ...mockClientParams, + requestCount: 1, + migrationPhase: QuoteMetadataMigrationPhase.V1Data, + }, ); expect(recommendedQuotes).toHaveLength(1); diff --git a/packages/bridge-controller/src/selectors.ts b/packages/bridge-controller/src/selectors.ts index 55f4a07f5f4..89018b46ad6 100644 --- a/packages/bridge-controller/src/selectors.ts +++ b/packages/bridge-controller/src/selectors.ts @@ -38,10 +38,14 @@ import { } from './utils/caip-formatters.js'; import { processFeatureFlags } from './utils/feature-flags.js'; import { sumAmounts } from './utils/number-formatters.js'; -import { calcBatchFees } from './utils/quote-metadata/calculators.js'; -import { calcQuoteMetadata } from './utils/quote-metadata/calculators.js'; +import { + calcBatchFees, + calcQuoteMetadata, +} from './utils/quote-metadata/calculators.js'; import { mergeQuoteMetadata } from './utils/quote-metadata/merge.js'; +import { toCurrencyValues } from './utils/quote-metadata/to-currency-values.js'; import type { QuoteMetadata } from './utils/quote-metadata/types.js'; +import { QuoteMetadataMigrationPhase } from './utils/quote-metadata/types.js'; import { getDefaultSlippagePercentage } from './utils/slippage.js'; import type { QuoteResponse } from './validators/quote-response.js'; @@ -98,6 +102,7 @@ const createBridgeSelector = createSelector_.withTypes(); type BridgeQuotesClientParams = { sortOrder: SortOrder; selectedQuote: (QuoteResponse & QuoteMetadata) | null; + migrationPhase?: QuoteMetadataMigrationPhase; }; type EvmTokenExchangeRate = { price?: number; currency?: string }; @@ -334,8 +339,18 @@ const selectMetadata = createBridgeSelector( selectBridgeFeesPerGas, selectExchangeRateSources, ({ quoteRequest }) => quoteRequest, + (_, { migrationPhase }: BridgeQuotesClientParams) => migrationPhase, ], - (quotes, bridgeFeesPerGas, exchangeRateSources, quoteRequest) => { + ( + quotes, + bridgeFeesPerGas, + exchangeRateSources, + quoteRequest, + migrationPhase, + ) => { + if (migrationPhase === QuoteMetadataMigrationPhase.V2Only) { + return []; + } const { destTokenAddress, srcChainId, destChainId } = quoteRequest[0] ?? {}; return quotes.map((quote) => @@ -362,13 +377,61 @@ const selectMetadata = createBridgeSelector( }, ); +const selectUsdToFiatExchangeRate = createBridgeSelector( + [ + selectExchangeRateSources, + ({ quoteRequest }) => + getNativeAssetForChainId(quoteRequest[0]?.srcChainId ?? 1)?.assetId, + ], + (exchangeRateSources, nativeAssetId) => { + const exchangeRate = selectExchangeRateByAssetId( + exchangeRateSources, + nativeAssetId, + ); + return exchangeRate?.exchangeRate && exchangeRate?.usdExchangeRate + ? new BigNumber(exchangeRate.exchangeRate) + .div(exchangeRate.usdExchangeRate) + .toFixed() + : undefined; + }, +); + +const selectCurrencyValues = createBridgeSelector( + [ + ({ quotes }) => quotes, + selectUsdToFiatExchangeRate, + (_, { migrationPhase }: BridgeQuotesClientParams) => migrationPhase, + ], + (quotes, usdToFiatExchangeRateString, migrationPhase) => { + if (migrationPhase === QuoteMetadataMigrationPhase.V1Data) { + return []; + } + const usdToFiatExchangeRate = usdToFiatExchangeRateString + ? new BigNumber(usdToFiatExchangeRateString) + : undefined; + return quotes.map((quote) => + toCurrencyValues(quote, usdToFiatExchangeRate), + ); + }, +); + // Selects cross-chain swap quotes including their metadata const selectBridgeQuotesWithMetadata = createBridgeSelector( - [selectMetadata, ({ quotes }) => quotes], - (quoteMetadata, quotes) => - quotes.map((quote, index) => - mergeQuoteMetadata(quote, quoteMetadata[index]), - ), + [ + selectMetadata, + selectCurrencyValues, + ({ quotes }) => quotes, + (_, { migrationPhase }: BridgeQuotesClientParams) => migrationPhase, + ], + (legacyQuoteMetadata, quoteMetadataV2, quotes, migrationPhase) => + quotes.map((quote, index) => { + return mergeQuoteMetadata( + quote, + legacyQuoteMetadata[index], + migrationPhase, + quoteMetadataV2[index], + ); + }), ); const selectSortedBridgeQuotes = createBridgeSelector( diff --git a/packages/bridge-controller/src/utils/quote-metadata/calculators.ts b/packages/bridge-controller/src/utils/quote-metadata/calculators.ts index d21cdafec30..5bf4092f738 100644 --- a/packages/bridge-controller/src/utils/quote-metadata/calculators.ts +++ b/packages/bridge-controller/src/utils/quote-metadata/calculators.ts @@ -22,6 +22,7 @@ import type { QuoteResponse } from '../../validators/quote-response.js'; import type { TxData } from '../../validators/trade.js'; import { isEvmQuoteResponse, isNativeAddress } from '../bridge.js'; import { calcNormalizedTokenAmount } from '../number-formatters.js'; +import { includeIfTruthy } from './include-if-truthy.js'; import type { QuoteMetadata, TokenAmountValues } from './types.js'; export const calcNonEvmTotalNetworkFee = ( @@ -523,16 +524,10 @@ export const calcQuoteMetadata = ( Should only be used for display purposes. */ gasFee, - ...(adjustedReturn && - Object.values(adjustedReturn).some(Boolean) && { adjustedReturn }), - ...(cost && Object.values(cost).some(Boolean) && { cost }), - ...(includedTxFees && - Object.values(includedTxFees).some(Boolean) && { includedTxFees }), - ...(relayerFee && - Object.values(relayerFee).some(Boolean) && { relayerFee }), - ...(priceImpact && - Object.values(priceImpact).some(Boolean) && { - priceImpact, - }), + ...includeIfTruthy(adjustedReturn, { adjustedReturn }), + ...includeIfTruthy(cost, { cost }), + ...includeIfTruthy(includedTxFees, { includedTxFees }), + ...includeIfTruthy(relayerFee, { relayerFee }), + ...includeIfTruthy(priceImpact, { priceImpact }), }; }; diff --git a/packages/bridge-controller/src/utils/quote-metadata/include-if-truthy.ts b/packages/bridge-controller/src/utils/quote-metadata/include-if-truthy.ts new file mode 100644 index 00000000000..3adb92a0dc8 --- /dev/null +++ b/packages/bridge-controller/src/utils/quote-metadata/include-if-truthy.ts @@ -0,0 +1,14 @@ +export const includeIfTruthy = >( + value: Record | undefined, + result: ResultType, +): ResultType | undefined => { + if (!value) { + return undefined; + } + + if (Object.values(value).some(Boolean)) { + return result; + } + + return undefined; +}; diff --git a/packages/bridge-controller/src/utils/quote-metadata/merge.ts b/packages/bridge-controller/src/utils/quote-metadata/merge.ts index 438f55b495b..b9f541a8141 100644 --- a/packages/bridge-controller/src/utils/quote-metadata/merge.ts +++ b/packages/bridge-controller/src/utils/quote-metadata/merge.ts @@ -1,28 +1,31 @@ -import { is } from '@metamask/superstruct'; import { merge } from 'lodash'; -import { QuoteResponseSchemaV1 } from '../../validators/quote-response-v1.js'; -import type { QuoteResponseV1 } from '../../validators/quote-response-v1.js'; +import type { DeepPartial } from '../../types.js'; import type { QuoteResponse } from '../../validators/quote-response.js'; import { toNormalizedAmounts } from './to-normalized-amounts.js'; import { toQuoteMetadataV2 } from './to-quote-metadata-v2.js'; import type { QuoteMetadata } from './types.js'; +import { QuoteMetadataMigrationPhase } from './types.js'; /** * Merges legacy {@link QuoteMetadata} values into the {@link QuoteResponse} * - * @param quoteResponse - The {@link QuoteResponse} or {@link QuoteResponseV1} to merge the metadata into + * @param quoteResponse - The {@link QuoteResponse} to merge the metadata into * @param legacyQuoteMetadata - The {@link QuoteMetadata} values to merge + * @param migrationPhase - The active {@link QuoteMetadataMigrationPhase} + * @param currencyValues - The amounts in the user's currency, derived from the backend's `usd` values * @returns The {@link QuoteResponse} with the metadata merged in */ -export function mergeQuoteMetadata< - QuoteType extends QuoteResponse | QuoteResponseV1 = QuoteResponse, ->( - quoteResponse: QuoteType, - legacyQuoteMetadata: QuoteMetadata, -): QuoteType & QuoteMetadata { - if (is(quoteResponse, QuoteResponseSchemaV1)) { - return merge({}, quoteResponse, legacyQuoteMetadata); +export function mergeQuoteMetadata( + quoteResponse: QuoteResponse, + legacyQuoteMetadata: QuoteMetadata = {}, + migrationPhase: QuoteMetadataMigrationPhase = QuoteMetadataMigrationPhase.V1Data, + currencyValues?: DeepPartial, +): QuoteResponse & QuoteMetadata { + const normalizedAmounts = toNormalizedAmounts(quoteResponse); + + if (migrationPhase === QuoteMetadataMigrationPhase.V2Only) { + return merge({}, quoteResponse, normalizedAmounts, currencyValues); } const legacyQuoteMetadataV2 = toQuoteMetadataV2( @@ -30,14 +33,22 @@ export function mergeQuoteMetadata< quoteResponse, ); - const normalizedAmounts = toNormalizedAmounts(quoteResponse); + if (migrationPhase === QuoteMetadataMigrationPhase.V2WithV1Fallback) { + return merge( + {}, + legacyQuoteMetadataV2, + legacyQuoteMetadata, + quoteResponse, + normalizedAmounts, + currencyValues, + ); + } - // Phase 1 of migration uses calcQuoteMetadata's results return merge( {}, quoteResponse, normalizedAmounts, - legacyQuoteMetadataV2, // legacy metadata in v2 format - legacyQuoteMetadata, // return legacy metadata for client testing + legacyQuoteMetadataV2, + legacyQuoteMetadata, ); } 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 new file mode 100644 index 00000000000..548707a38bf --- /dev/null +++ b/packages/bridge-controller/src/utils/quote-metadata/to-currency-values.ts @@ -0,0 +1,76 @@ +import { BigNumber } from 'bignumber.js'; + +import type { DeepPartial } from '../../types.js'; +import type { AmountsAndAsset } from '../../validators/amount-and-asset.js'; +import type { QuoteResponse } from '../../validators/quote-response.js'; +import { FeeType } from '../../validators/quote.js'; + +/** + * Builds a partial {@link QuoteResponse} object with fiat values derived from the usd values provided by the bridge-api + * + * @param quote - The quote response to calculate the metadata for + * @param usdToFiatExchangeRate - The usd to fiat exchange rate + * @returns The partial {@link QuoteResponse} object with fiat values + */ +export const toCurrencyValues = ( + quote: QuoteResponse, + usdToFiatExchangeRate?: BigNumber, +): DeepPartial => { + const { + quote: { src, dest, feeData, priceData }, + } = quote; + + const { adjustedReturn, priceImpact } = priceData ?? {}; + + const toFiat = ({ + usd, + valueInCurrency, + }: Pick): + | Pick + | undefined => { + if (usd && usdToFiatExchangeRate) { + return { valueInCurrency: usdToFiatExchangeRate.times(usd).toFixed() }; + } + if (valueInCurrency) { + return { valueInCurrency }; + } + return undefined; + }; + + const priceImpactFiat = priceImpact ? toFiat(priceImpact) : undefined; + const adjustedReturnFiat = adjustedReturn + ? toFiat(adjustedReturn) + : undefined; + + const minAmountValueInCurrency = toFiat({ + usd: dest.minAmountUsd, + valueInCurrency: dest.minAmountValueInCurrency, + })?.valueInCurrency; + + return { + quote: { + src: toFiat(src), + dest: { + ...toFiat(dest), + ...(minAmountValueInCurrency && { + minAmountValueInCurrency, + }), + }, + feeData: + feeData && + Object.fromEntries( + Object.values(FeeType) + .filter((feeType) => feeData[feeType]) + .map((feeType) => [feeType, feeData[feeType]?.map(toFiat)]), + ), + priceData: { + ...(priceImpactFiat && { + priceImpact: priceImpactFiat, + }), + ...(adjustedReturnFiat && { + adjustedReturn: adjustedReturnFiat, + }), + }, + }, + }; +}; 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 bf5bd21798d..a1ec544dc13 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 @@ -1,21 +1,29 @@ +import { is } from '@metamask/superstruct'; import { merge } from 'lodash'; import type { DeepPartial } from '../../types.js'; +import type { AmountsAndAsset } from '../../validators/amount-and-asset.js'; import type { QuoteResponseV1 } from '../../validators/quote-response-v1.js'; +import { QuoteResponseSchemaV2 } from '../../validators/quote-response.js'; import type { QuoteResponse } from '../../validators/quote-response.js'; -import type { QuoteMetadata } from './types.js'; +import { sumAmounts } from '../number-formatters.js'; +import { includeIfTruthy } from './include-if-truthy.js'; +import type { QuoteMetadata, TokenAmountValues } from './types.js'; +import { QuoteMetadataMigrationPhase } from './types.js'; /** * Extracts legacy {@link QuoteMetadata} values from a {@link QuoteResponse} or {@link QuoteResponseV1}. * If a QuoteResponse is provided, this assumes that its `valueInCurrency` properties are set. * * @param quoteResponse - The quote to extract the metadata from + * @param migrationPhase - The migration phase to use * @returns A partial {@link QuoteMetadata} object */ export const toQuoteMetadataV1 = ( quoteResponse: | (DeepPartial & QuoteMetadata) | null, + migrationPhase: QuoteMetadataMigrationPhase = QuoteMetadataMigrationPhase.V1Data, ): QuoteMetadata => { /* istanbul ignore if */ if (!quoteResponse) { @@ -43,21 +51,85 @@ export const toQuoteMetadataV1 = ( swapRate, gasFee, totalNetworkFee, - ...(adjustedReturn && Object.values(adjustedReturn).some(Boolean) - ? { adjustedReturn } - : {}), - ...(cost && Object.values(cost).some(Boolean) ? { cost } : {}), - ...(priceImpact && Object.values(priceImpact).some(Boolean) - ? { priceImpact } - : {}), - ...(relayerFee && Object.values(relayerFee).some(Boolean) - ? { relayerFee } - : {}), - ...(includedTxFees && Object.values(includedTxFees).some(Boolean) - ? { includedTxFees } - : {}), + ...includeIfTruthy(adjustedReturn, { adjustedReturn }), + ...includeIfTruthy(cost, { cost }), + ...includeIfTruthy(priceImpact, { priceImpact }), + ...includeIfTruthy(relayerFee, { relayerFee }), + ...includeIfTruthy(includedTxFees, { includedTxFees }), }; - // Phase 1 only uses legacyMetadata - return merge({}, legacyMetadata); + if ( + migrationPhase === QuoteMetadataMigrationPhase.V1Data || + !is(quoteResponse, QuoteResponseSchemaV2) + ) { + return legacyMetadata; + } + + const { + quote: { + src, + dest, + priceData, + feeData: { network, relayer, txFee }, + }, + } = quoteResponse; + + const totalNetworkFeeV2 = sumAmounts(network, relayer); + + const toTokenAmountValues = ( + data?: Pick< + AmountsAndAsset, + 'normalizedAmount' | 'usd' | 'valueInCurrency' + >, + ): Partial => { + return { + amount: data?.normalizedAmount, + usd: data?.usd, + valueInCurrency: data?.valueInCurrency, + }; + }; + + // Build V1 from V2 quote + const v2Metadata: QuoteMetadata = { + ...includeIfTruthy(src, { + sentAmount: toTokenAmountValues(src), + }), + ...includeIfTruthy(dest, { + toTokenAmount: toTokenAmountValues(dest), + minToTokenAmount: { + amount: dest.minAmountNormalized, + valueInCurrency: dest.minAmountValueInCurrency, + usd: dest.minAmountUsd, + }, + }), + ...(priceData?.swapRate && { swapRate: priceData.swapRate }), + ...includeIfTruthy(priceData?.adjustedReturn, { + adjustedReturn: toTokenAmountValues(priceData?.adjustedReturn), + }), + ...includeIfTruthy(network?.[0], { + gasFee: { + total: toTokenAmountValues(network?.[0]), + }, + }), + ...includeIfTruthy(totalNetworkFeeV2, { + totalNetworkFee: toTokenAmountValues(totalNetworkFeeV2), + }), + ...(priceData?.priceImpact && { + priceImpact: toTokenAmountValues(priceData?.priceImpact), + // Use priceImpact as cost + cost: toTokenAmountValues(priceData?.priceImpact), + }), + ...includeIfTruthy(relayer?.[0], { + relayerFee: toTokenAmountValues(relayer?.[0]), + }), + ...includeIfTruthy(txFee?.[0], { + includedTxFees: toTokenAmountValues(txFee?.[0]), + }), + }; + + if (migrationPhase === QuoteMetadataMigrationPhase.V2WithV1Fallback) { + return merge({}, legacyMetadata, v2Metadata); + } + + return v2Metadata; }; 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 8b177a55d5d..94f76721a89 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 @@ -2,10 +2,13 @@ import { parseCaipAssetType } from '@metamask/utils'; import { toBridgeAssetV2 } from '../../coercers/quote-response-v1-to-v2.js'; import type { DeepPartial } from '../../types.js'; +import type { AmountsAndAsset } from '../../validators/amount-and-asset.js'; +import type { BridgeAssetV2 } from '../../validators/bridge-asset.js'; import type { QuoteResponse } from '../../validators/quote-response.js'; import { getNativeAssetForChainId } from '../bridge.js'; import { calcAtomicTokenAmount } from '../number-formatters.js'; -import type { QuoteMetadata } from './types.js'; +import { includeIfTruthy } from './include-if-truthy.js'; +import type { QuoteMetadata, TokenAmountValues } from './types.js'; /** * Converts a {@link QuoteMetadata} to a partial {@link QuoteResponse} containing only metadata @@ -50,23 +53,26 @@ export const toQuoteMetadataV2 = ( }; const networkFeeToUse = gasFee?.total ?? totalNetworkFee; + const toAmountAndAsset = ( + asset?: DeepPartial, + metadata?: Partial, + extraFields?: DeepPartial, + ): DeepPartial => { + return { + amount: calcAtomicTokenAmount(metadata?.amount, asset?.decimals), + normalizedAmount: metadata?.amount, + valueInCurrency: metadata?.valueInCurrency, + usd: metadata?.usd, + ...extraFields, + }; + }; + return { ...rest, quote: { - src: { - amount: calcAtomicTokenAmount(sentAmount?.amount, srcAsset?.decimals), - normalizedAmount: sentAmount?.amount, - valueInCurrency: sentAmount?.valueInCurrency, - usd: sentAmount?.usd, - }, + src: toAmountAndAsset(srcAsset, sentAmount), dest: { - amount: calcAtomicTokenAmount( - toTokenAmount?.amount, - destAsset?.decimals, - ), - normalizedAmount: toTokenAmount?.amount, - valueInCurrency: toTokenAmount?.valueInCurrency, - usd: toTokenAmount?.usd, + ...toAmountAndAsset(destAsset, toTokenAmount), minAmount: calcAtomicTokenAmount( minToTokenAmount?.amount, destAsset?.decimals, @@ -77,60 +83,28 @@ export const toQuoteMetadataV2 = ( }, feeData: { network: [ - { - amount: calcAtomicTokenAmount( - networkFeeToUse?.amount, - nativeAsset?.decimals, - ), - normalizedAmount: networkFeeToUse?.amount, - valueInCurrency: networkFeeToUse?.valueInCurrency, - usd: networkFeeToUse?.usd, + toAmountAndAsset(nativeAsset, networkFeeToUse, { asset: nativeAsset, - }, - ], - ...(relayerFee && - Object.values(relayerFee).some(Boolean) && { - relayer: [ - { - amount: calcAtomicTokenAmount( - relayerFee?.amount, - nativeAsset?.decimals, - ), - normalizedAmount: relayerFee.amount, - valueInCurrency: relayerFee.valueInCurrency, - usd: relayerFee.usd, - asset: nativeAsset, - }, - ], - }), - ...(includedTxFees && - Object.values(includedTxFees).some(Boolean) && { - txFee: [ - { - amount: calcAtomicTokenAmount( - includedTxFees?.amount, - txFeeAsset?.decimals, - ), - normalizedAmount: includedTxFees?.amount, - valueInCurrency: includedTxFees?.valueInCurrency, - usd: includedTxFees?.usd, - asset: txFeeAsset, - }, - ], }), + ], + ...includeIfTruthy(relayerFee, { + relayer: [ + toAmountAndAsset(nativeAsset, relayerFee, { asset: nativeAsset }), + ], + }), + ...includeIfTruthy(includedTxFees, { + txFee: [ + toAmountAndAsset(txFeeAsset, includedTxFees, { asset: txFeeAsset }), + ], + }), }, priceData: { - ...(priceImpactToUse && - Object.values(priceImpactToUse).some(Boolean) && { - priceImpact: priceImpactToUse, - }), - ...(adjustedReturn && - Object.values(adjustedReturn).some(Boolean) && { - adjustedReturn: { - valueInCurrency: adjustedReturn?.valueInCurrency, - usd: adjustedReturn?.usd, - }, - }), + ...includeIfTruthy(priceImpactToUse, { + priceImpact: priceImpactToUse, + }), + ...includeIfTruthy(adjustedReturn, { + adjustedReturn, + }), swapRate, }, }, diff --git a/packages/bridge-controller/src/utils/quote-metadata/types.ts b/packages/bridge-controller/src/utils/quote-metadata/types.ts index c70c4f14eb1..98daf9c6593 100644 --- a/packages/bridge-controller/src/utils/quote-metadata/types.ts +++ b/packages/bridge-controller/src/utils/quote-metadata/types.ts @@ -86,4 +86,28 @@ type QuoteMetadataV1 = { priceImpact: Omit; // abs(sentAmount - toTokenAmount); }; +/** + * The partial legacy quote metadata + * + * @deprecated Avoid introducing new usages and use the nested QuoteResponse metadata instead + */ export type QuoteMetadata = DeepPartial; + +export const QuoteMetadataMigrationPhase = { + /** + * Phase 1: omit API V2 currency metadata; serve legacy calcQuoteMetadata + * into V2 nested shape + */ + V1Data: '1', + /** + * Phase 1.5: prefer API V2 metadata (+ fiat from usd); fall back to legacy. + */ + V2WithV1Fallback: '1.5', + /** + * Phase 2: API V2 metadata only; legacy metadata utils can be removed. + */ + V2Only: '2', +} as const; + +export type QuoteMetadataMigrationPhase = + (typeof QuoteMetadataMigrationPhase)[keyof typeof QuoteMetadataMigrationPhase]; diff --git a/packages/bridge-status-controller/CHANGELOG.md b/packages/bridge-status-controller/CHANGELOG.md index 91de48401b4..ced19b7f44c 100644 --- a/packages/bridge-status-controller/CHANGELOG.md +++ b/packages/bridge-status-controller/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Add migrationPhase to submit handlers, which determines how quote metadata is resolved ([#9744](https://github.com/MetaMask/core/pull/9744)) - Bump `@metamask/transaction-controller` from `^69.5.0` to `^69.5.1` ([#9798](https://github.com/MetaMask/core/pull/9798)) - Bump `@metamask/bridge-controller` from `^79.0.0` to `^79.0.1` ([#9788](https://github.com/MetaMask/core/pull/9788)) - Bump `@metamask/accounts-controller` from `^39.0.6` to `^39.0.7` ([#9791](https://github.com/MetaMask/core/pull/9791)) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts index 382773c6d59..02217bbd85e 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts @@ -7,7 +7,6 @@ import type { import { BatchSellTransactionType, FeatureId, - mergeQuoteMetadata, } from '@metamask/bridge-controller'; import { toHex } from '@metamask/controller-utils'; import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger'; @@ -165,8 +164,9 @@ const mockQuotes = mockBatchSellErc20Erc20 gasSponsored: undefined, }, })) - .map((quote) => - mergeQuoteMetadata(quote, { + .map((quote) => ({ + ...quote, + ...{ sentAmount: { usd: '100', valueInCurrency: '200', @@ -175,8 +175,8 @@ const mockQuotes = mockBatchSellErc20Erc20 usd: '101', valueInCurrency: '201', }, - }), - ); + }, + })); const mockTransferTx: BatchSellTradesResponse['transactions'][number] = { chainId: 10, from: '0xaccount1', diff --git a/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts index c1932b5d288..aa782fb2a18 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts @@ -4,7 +4,6 @@ import { BridgeClientId, UnifiedSwapBridgeEventName, - mergeQuoteMetadata, StatusTypes, QuoteResponse as QuoteResponseV1, getNativeAssetForChainId, @@ -110,13 +109,15 @@ const minimalIntentQuoteResponse = ( }, ...overrides, }; - return mergeQuoteMetadata(quote as never, { - sentAmount: { amount: '1', usd: '1' }, - gasFee: { effective: { amount: '0', usd: '0' } }, - toTokenAmount: { usd: '1' }, - }); + return { + ...quote, + ...{ + sentAmount: { amount: '1', usd: '1' }, + gasFee: { effective: { amount: '0', usd: '0' } }, + toTokenAmount: { usd: '1' }, + }, + }; }; -validateQuoteResponseV1(minimalIntentQuoteResponse()); const minimalBridgeQuoteResponse = ( accountAddress: string, @@ -174,11 +175,14 @@ const minimalBridgeQuoteResponse = ( }, ...overrides, }; - return mergeQuoteMetadata(quote as never, { - sentAmount: { amount: '1', usd: '1' }, - gasFee: { effective: { amount: '0', usd: '0' } }, - toTokenAmount: { usd: '1' }, - }); + return { + ...quote, + ...{ + sentAmount: { amount: '1', usd: '1' }, + gasFee: { effective: { amount: '0', usd: '0' } }, + toTokenAmount: { usd: '1' }, + }, + }; }; validateQuoteResponseV1(minimalBridgeQuoteResponse('0xAccount1')); @@ -972,8 +976,8 @@ describe('BridgeStatusController (target uncovered branches)', () => { // make startPolling return different tokens for the same tx startPollingSpy.mockReturnValueOnce('tok1').mockReturnValueOnce('tok2'); - const quoteResponse = mergeQuoteMetadata( - { + const quoteResponse = { + ...{ quote: { srcChainId: 1, destChainId: 10, @@ -981,12 +985,12 @@ describe('BridgeStatusController (target uncovered branches)', () => { }, estimatedProcessingTimeInSeconds: 1, }, - { + ...{ sentAmount: { amount: '0' }, gasFee: { effective: { amount: '0' } }, toTokenAmount: { usd: '0' }, }, - ); + }; // first time => starts polling tok1 controller.startPollingForBridgeTxStatus({ @@ -1039,8 +1043,8 @@ describe('BridgeStatusController (target uncovered branches)', () => { mockTxHistory, }); - const quoteResponse = mergeQuoteMetadata( - { + const quoteResponse = { + ...{ quote: { srcChainId: 1, destChainId: 10, @@ -1049,12 +1053,12 @@ describe('BridgeStatusController (target uncovered branches)', () => { }, estimatedProcessingTimeInSeconds: 1, }, - { + ...{ sentAmount: { amount: '0' }, gasFee: { effective: { amount: '0' } }, toTokenAmount: { usd: '0' }, }, - ); + }; const statusResponse = { status: { diff --git a/packages/bridge-status-controller/src/bridge-status-controller.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.test.ts index a337b40e997..746429479f0 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -313,8 +313,8 @@ const getMockStartPollingForBridgeTxStatusArgs = ({ id: txMetaId, hash: srcTxHash === 'undefined' ? undefined : srcTxHash, } as TransactionMeta, - quoteResponse: mergeQuoteMetadata( - { + quoteResponse: { + ...{ quote: getMockQuote({ srcChainId, destChainId }), trade: { chainId: srcChainId, @@ -327,7 +327,7 @@ const getMockStartPollingForBridgeTxStatusArgs = ({ approval: undefined, estimatedProcessingTimeInSeconds: 15, }, - { + ...{ sentAmount: { amount: '1.234', valueInCurrency: undefined, @@ -355,7 +355,7 @@ const getMockStartPollingForBridgeTxStatusArgs = ({ swapRate: '1.234', cost: { valueInCurrency: undefined, usd: undefined }, }, - ), + }, accountAddress: account, startTime: 1729964825189, slippagePercentage: 0, @@ -2260,40 +2260,43 @@ describe('BridgeStatusController', () => { trade: 'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAHDXLY8oVRIwA8ZdRSGjM5RIZJW8Wv+Twyw3NqU4Hov+OHoHp/dmeDvstKbICW3ezeGR69t3/PTAvdXgZVdJFJXaxkoKXUTWfEAyQyCCG9nwVoDsd10OFdnM9ldSi+9SLqHpqWVDV+zzkmftkF//DpbXxqeH8obNXHFR7pUlxG9uNVOn64oNsFdeUvD139j1M51iRmUY839Y25ET4jDRscT081oGb+rLnywLjLSrIQx6MkqNBhCFbxqY1YmoGZVORW/QMGRm/lIRcy/+ytunLDm+e8jOW7xfcSayxDmzpAAAAAjJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+FkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpBHnVW/IxwG7udMVuzmgVB/2xst6j9I5RArHNola8E4+0P/on9df2SnTAmx8pWHneSwmrNt/J3VFLMhqns4zl6JmXkZ+niuxMhAGrmKBaBo94uMv2Sl+Xh3i+VOO0m5BdNZ1ElenbwQylHQY+VW1ydG1MaUEeNpG+EVgswzPMwPoLBgAFAsBcFQAGAAkDQA0DAAAAAAAHBgABAhMICQAHBgADABYICQEBCAIAAwwCAAAAUEYVOwAAAAAJAQMBEQoUCQADBAETCgsKFw0ODxARAwQACRQj5RfLl3rjrSoBAAAAQ2QAAVBGFTsAAAAAyYZnBwAAAABkAAAJAwMAAAEJDAkAAAIBBBMVCQjGASBMKQwnooTbKNxdBwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUHTKomh4KXvNgA0ovYKS5F8GIOBgAAAAAAAAAAAAAAAAAQgAAAAAAAAAAAAAAAAAAAAAAAEIF7RFOAwAAAAAAAAAAAAAAaAIAAAAAAAC4CwAAAAAAAOAA2mcAAAAAAAAAAAAAAAAAAAAApapuIXG0FuHSfsU8qME9s/kaic0AAwGCsZdSuxV5eCm+Ria4LEQPgTg4bg65gNrTAefEzpAfPQgCABIMAgAAAAAAAAAAAAAACAIABQwCAAAAsIOFAAAAAAADWk6DVOZO8lMFQg2r0dgfltD6tRL/B1hH3u00UzZdgqkAAxEqIPdq2eRt/F6mHNmFe7iwZpdrtGmHNJMFlK7c6Bc6k6kjBezr6u/tAgvu3OGsJSwSElmcOHZ21imqH/rhJ2KgqDJdBPFH4SYIM1kBAAA=', }; - const mockQuoteResponse = mergeQuoteMetadata(mockQuote, { - sentAmount: { - amount: '1', - valueInCurrency: '100', - usd: '100', - }, - toTokenAmount: { - amount: '0.5', - valueInCurrency: '1000', - usd: '1000', - }, - minToTokenAmount: { - amount: '0.475', - valueInCurrency: '950', - usd: '950', - }, - totalNetworkFee: { - amount: '0.1', - valueInCurrency: '10', - usd: '10', - }, - gasFee: { - total: { amount: '0.05', valueInCurrency: '5', usd: '5' }, - }, - adjustedReturn: { - valueInCurrency: '985', - usd: '985', - }, - cost: { - valueInCurrency: '15', - usd: '15', + const mockQuoteResponse = { + ...mockQuote, + ...{ + sentAmount: { + amount: '1', + valueInCurrency: '100', + usd: '100', + }, + toTokenAmount: { + amount: '0.5', + valueInCurrency: '1000', + usd: '1000', + }, + minToTokenAmount: { + amount: '0.475', + valueInCurrency: '950', + usd: '950', + }, + totalNetworkFee: { + amount: '0.1', + valueInCurrency: '10', + usd: '10', + }, + gasFee: { + total: { amount: '0.05', valueInCurrency: '5', usd: '5' }, + }, + adjustedReturn: { + valueInCurrency: '985', + usd: '985', + }, + cost: { + valueInCurrency: '15', + usd: '15', + }, + swapRate: '0.5', }, - swapRate: '0.5', - }); + }; const mockSolanaAccount = { id: 'solana-account-1', diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index c2540b43e76..e6e3cebf991 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -9,9 +9,9 @@ import { InputPrimaryDenomination, QuoteResponse, toQuoteMetadataV1, - mergeQuoteMetadata, toQuoteResponseV1, } from '@metamask/bridge-controller'; +import type { QuoteMetadataMigrationPhase } from '@metamask/bridge-controller'; import { isNonEvmChainId, StatusTypes, @@ -1372,6 +1372,7 @@ export class BridgeStatusController extends StaticIntervalPollingController => { /** * If there are multiple quote responses, we assume that they all originate from the same src chain @@ -1404,8 +1406,14 @@ export class BridgeStatusController extends StaticIntervalPollingController => { const { quoteResponse, accountAddress, location, abTests, + migrationPhase, activeAbTests, tokenSecurityTypeDestination, inputPrimaryDenomination, @@ -1569,6 +1580,7 @@ export class BridgeStatusController extends StaticIntervalPollingController => { /** * Retrieve the batch sell trades from the BridgeController's state to ensure we submit @@ -1600,6 +1613,8 @@ export class BridgeStatusController extends StaticIntervalPollingController { describe('getInitialHistoryItem', () => { const baseArgs = { bridgeTxMeta: { id: 'tx1', hash: '0xhash' }, - quoteResponse: mergeQuoteMetadata( - { + quoteResponse: { + ...{ quote: { srcChainId: 1, destChainId: 10 }, estimatedProcessingTimeInSeconds: 60, }, - { + ...{ sentAmount: { amount: '1', usd: '2' }, gasFee: { effective: { amount: '0.001', usd: '3' } }, toTokenAmount: { amount: '1', usd: '4' }, }, - ), + }, startTime: 1, slippagePercentage: 0, accountAddress: '0xaccount', diff --git a/packages/bridge-status-controller/src/utils/metrics.test.ts b/packages/bridge-status-controller/src/utils/metrics.test.ts index 5e0b7fcd828..3531c1a6db2 100644 --- a/packages/bridge-status-controller/src/utils/metrics.test.ts +++ b/packages/bridge-status-controller/src/utils/metrics.test.ts @@ -3,7 +3,6 @@ import { FeeType, ActionTypes, MetaMetricsSwapsEventSource, - mergeQuoteMetadata, } from '@metamask/bridge-controller'; import { MetricsSwapType, @@ -1014,17 +1013,17 @@ describe('metrics utils', () => { { key: 'bridge_quote_sorting', value: 'variant_b' }, ]; const result = getPreConfirmationPropertiesFromQuote( - mergeQuoteMetadata( - { + { + ...{ quote: mockHistoryItem.quote, estimatedProcessingTimeInSeconds: 900, }, - { + ...{ adjustedReturn: { usd: '1980' }, sentAmount: { usd: '2000' }, gasFee: { effective: { usd: '2.54739' } }, }, - ) as never, + } as never, false, null, MetaMetricsSwapsEventSource.MainView, diff --git a/packages/bridge-status-controller/src/utils/snaps.test.ts b/packages/bridge-status-controller/src/utils/snaps.test.ts index 827d93963f6..56a0a5d1a8a 100644 --- a/packages/bridge-status-controller/src/utils/snaps.test.ts +++ b/packages/bridge-status-controller/src/utils/snaps.test.ts @@ -1,4 +1,4 @@ -import { ChainId, mergeQuoteMetadata } from '@metamask/bridge-controller'; +import { ChainId } from '@metamask/bridge-controller'; /* eslint-disable consistent-return */ import { v4 as uuid } from 'uuid'; @@ -74,20 +74,20 @@ describe('Snaps Utils', () => { const { time, ...result } = await handleNonEvmTx( messenger, transaction, - mergeQuoteMetadata( - { + { + ...{ quote: { srcChainId: ChainId.SOLANA, srcAsset: { symbol: 'SOL' }, destAsset: { symbol: 'MATIC' }, }, }, - { + ...{ sentAmount: { amount: '1000000000', }, }, - ) as never, + } as never, { id: accountId, metadata: { snap: { id: snapId } } } as never, ); @@ -161,20 +161,20 @@ describe('Snaps Utils', () => { const { time, ...result } = await handleNonEvmTx( messenger, transaction, - mergeQuoteMetadata( - { + { + ...{ quote: { srcChainId: ChainId.SOLANA, srcAsset: { symbol: 'SOL' }, destAsset: { symbol: 'MATIC' }, }, }, - { + ...{ sentAmount: { amount: '1000000000', }, }, - ) as never, + } as never, { id: accountId, metadata: { snap: { id: snapId } } } as never, );