From e93a235d65d26f61f0407254eaea7b9b58021632 Mon Sep 17 00:00:00 2001 From: micaelae Date: Thu, 23 Jul 2026 09:19:16 -0700 Subject: [PATCH 1/2] wip --- .vscode/settings.json | 3 + packages/bridge-controller/src/index.ts | 2 + packages/bridge-controller/src/selectors.ts | 59 ++++++++++++++++--- .../src/utils/quote-metadata/calculators.ts | 46 +++++++++++++++ .../src/utils/quote-metadata/merge.ts | 23 ++++++++ 5 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..512ff90b13 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "js/ts.tsdk.path": "node_modules/typescript/lib" +} diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index 18d28378be..28031fbb6c 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -240,6 +240,8 @@ export { selectBridgeFeatureFlags, selectMinimumBalanceForRentExemptionInSOL, selectTokenWarnings, + selectMetadataV2, + selectUsdToFiatExchangeRate, } from './selectors'; export { DEFAULT_FEATURE_FLAG_CONFIG } from './constants/bridge'; diff --git a/packages/bridge-controller/src/selectors.ts b/packages/bridge-controller/src/selectors.ts index afbf0a543d..20aa891811 100644 --- a/packages/bridge-controller/src/selectors.ts +++ b/packages/bridge-controller/src/selectors.ts @@ -34,7 +34,10 @@ import { } from './utils/caip-formatters'; import { processFeatureFlags } from './utils/feature-flags'; import { sumAmounts } from './utils/number-formatters'; -import { calcBatchFees } from './utils/quote-metadata/calculators'; +import { + calcBatchFees, + calcQuoteMetadataV2, +} from './utils/quote-metadata/calculators'; import { calcQuoteMetadata } from './utils/quote-metadata/calculators'; import { mergeQuoteMetadata } from './utils/quote-metadata/merge'; import type { QuoteMetadata } from './utils/quote-metadata/types'; @@ -94,6 +97,7 @@ const createBridgeSelector = createSelector_.withTypes(); type BridgeQuotesClientParams = { sortOrder: SortOrder; selectedQuote: (QuoteResponse & QuoteMetadata) | null; + migrationPhase: '1' | '1.5' | '2'; }; type EvmTokenExchangeRate = { price?: number; currency?: string }; @@ -355,13 +359,52 @@ const selectMetadata = createBridgeSelector( }, ); +export const selectUsdToFiatExchangeRate = createBridgeSelector( + [ + selectExchangeRateSources, + ({ quoteRequest }) => + getNativeAssetForChainId(quoteRequest[0]?.srcChainId ?? 1)?.assetId, + ], + (exchangeRateSources, nativeAssetId) => { + const exchangeRate = selectExchangeRateByAssetId( + exchangeRateSources, + nativeAssetId, + ); + return new BigNumber(exchangeRate?.exchangeRate ?? 0).div( + exchangeRate?.usdExchangeRate ?? 0, + ); + }, +); + +export const selectMetadataV2 = createBridgeSelector( + [({ quotes }) => quotes, selectUsdToFiatExchangeRate], + (quotes, usdToFiatExchangeRate) => { + return quotes.map((quote) => + calcQuoteMetadataV2(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, + selectMetadataV2, + ({ quotes }) => quotes, + (_, { migrationPhase }: BridgeQuotesClientParams) => migrationPhase, + ], + (quoteMetadata, quoteMetadataV2, quotes, migrationPhase) => + quotes.map((quote, index) => { + // if phase 1, skip toQuoteMetadataV2(quoteMetadataV2) so new metadata keys have old data + // if phase 1.5, add quoteMetadataV2, so new metadata keys can have both old and new data + // if phase 2, skip quoteMetadata, so new metadata keys have new data and old data can be removed + return mergeQuoteMetadata( + quote, + quoteMetadata[index], + migrationPhase, + quoteMetadataV2?.[index], + ); + }), ); const selectSortedBridgeQuotes = createBridgeSelector( @@ -458,8 +501,8 @@ export const selectIsQuoteExpired = createBridgeSelector( (isQuoteGoingToRefresh, quotesLastFetched, refreshRate, currentTimeInMs) => Boolean( !isQuoteGoingToRefresh && - quotesLastFetched && - currentTimeInMs - quotesLastFetched > refreshRate, + quotesLastFetched && + currentTimeInMs - quotesLastFetched > refreshRate, ), ); diff --git a/packages/bridge-controller/src/utils/quote-metadata/calculators.ts b/packages/bridge-controller/src/utils/quote-metadata/calculators.ts index 2986d9acad..631542aef0 100644 --- a/packages/bridge-controller/src/utils/quote-metadata/calculators.ts +++ b/packages/bridge-controller/src/utils/quote-metadata/calculators.ts @@ -20,6 +20,7 @@ import type { QuoteResponse } from '../../validators/quote-response'; import { isNativeAddress } from '../bridge'; import { calcTokenAmount } from '../number-formatters'; import type { QuoteMetadata, TokenAmountValues } from './types'; +import { FeeType, toQuoteResponseV1, toQuoteResponseV2 } from '../..'; export const calcNonEvmTotalNetworkFee = ( bridgeQuote: QuoteResponse & NonEvmFees, @@ -421,6 +422,7 @@ export const calcQuoteMetadata = ( nativeExchangeRate = {}, } = options; + // const quote = toQuoteResponseV2(toQuoteResponseV1(baseQuote)); const sentAmount = calcSentAmount(quote.quote, srcTokenExchangeRate); const toTokenAmount = calcToAmount( quote.quote.dest.amount, @@ -507,3 +509,47 @@ export const calcQuoteMetadata = ( }), }; }; + +// TODO Phase 1.5 of migration uses calcQuoteMetadata's results as fallback +export const calcQuoteMetadataV2 = ( + quote: QuoteResponse, + usdToFiatExchangeRate: BigNumber, +): DeepPartial => { + // Calculate fiat based on usd value + return { + quote: { + src: { + valueInCurrency: usdToFiatExchangeRate + .times(quote.quote.src.usd ?? '0') + .toFixed(), + }, + dest: { + valueInCurrency: usdToFiatExchangeRate + .times(quote.quote.dest.usd ?? '0') + .toFixed(), + minAmountValueInCurrency: usdToFiatExchangeRate + .times(quote.quote.dest.minAmountUsd ?? '0') + .toFixed(), + }, + feeData: Object.fromEntries( + Object.values(FeeType).map((feeType) => [ + feeType, + quote.quote.feeData[feeType]?.map((fee) => ({ + valueInCurrency: usdToFiatExchangeRate + .times(fee.usd ?? '0') + .toFixed(), + })), + ]), + ), + priceData: { + priceImpact: { + valueInCurrency: usdToFiatExchangeRate + .times(quote.quote.priceData?.priceImpact?.usd ?? '0') + .toFixed(), + }, + }, + }, + }; +}; + +// TODO Phase 2 of migration only uses metadata from the API response diff --git a/packages/bridge-controller/src/utils/quote-metadata/merge.ts b/packages/bridge-controller/src/utils/quote-metadata/merge.ts index 264cbdfb1c..6a66523c93 100644 --- a/packages/bridge-controller/src/utils/quote-metadata/merge.ts +++ b/packages/bridge-controller/src/utils/quote-metadata/merge.ts @@ -7,12 +7,15 @@ import type { QuoteResponseV1 } from '../../validators/quote-response-v1'; import { toNormalizedAmounts } from './to-normalized-amounts'; import { toQuoteMetadataV2 } from './to-quote-metadata-v2'; import type { QuoteMetadata } from './types'; +import type { DeepPartial } from '../../types'; /** * Merges legacy {@link QuoteMetadata} values into the {@link QuoteResponse} * * @param quoteResponse - The {@link QuoteResponse} or {@link QuoteResponseV1} to merge the metadata into * @param legacyQuoteMetadata - The {@link QuoteMetadata} values to merge + * @param migrationPhase - The migration phase + * @param fiatQuoteMetadata - The {@link QuoteMetadataV2} values to merge * @returns The {@link QuoteResponse} with the metadata merged in */ export function mergeQuoteMetadata< @@ -20,6 +23,8 @@ export function mergeQuoteMetadata< >( quoteResponse: QuoteType, legacyQuoteMetadata: QuoteMetadata, + migrationPhase: '1' | '1.5' | '2' = '1', + fiatQuoteMetadata?: DeepPartial, ): QuoteType & QuoteMetadata { if (is(quoteResponse, QuoteResponseSchemaV1)) { return merge({}, quoteResponse, legacyQuoteMetadata); @@ -30,6 +35,24 @@ export function mergeQuoteMetadata< quoteResponse, ); const normalizedAmountsV2 = toNormalizedAmounts(quoteResponse); + + if (migrationPhase === '2') { + // TODO Phase 2 of migration only uses metadata from the API response + // @ts-expect-error - TODO: fix this + return merge({}, quoteResponse, normalizedAmountsV2, fiatQuoteMetadata); + } + + if (migrationPhase === '1.5') { + return merge( + {}, + legacyQuoteMetadatV2, + quoteResponse, + normalizedAmountsV2, + fiatQuoteMetadata, + legacyQuoteMetadata, // return for client testing + ); + } + // Phase 1 of migration uses calcQuoteMetadata's results return merge( {}, From e146900583462bb689f44bb80686f440776e979b Mon Sep 17 00:00:00 2001 From: micaelae Date: Thu, 23 Jul 2026 17:30:07 -0700 Subject: [PATCH 2/2] chore: always fetch native price --- packages/bridge-controller/src/bridge-controller.ts | 5 ++++- .../src/utils/quote-metadata/calculators.ts | 3 --- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 855e58edad..fa05caeb43 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -43,6 +43,7 @@ import { getDefaultBridgeControllerState, isCrossChain, isEthUsdt, + isNativeAddress, isNonEvmChainId, isSolanaChainId, } from './utils/bridge'; @@ -579,7 +580,9 @@ export class BridgeController extends StaticIntervalPollingController - !selectIsAssetExchangeRateInState(exchangeRateSources, assetId), + !selectIsAssetExchangeRateInState(exchangeRateSources, assetId) || + // Always fetch native asset exchange rates + isNativeAddress(assetId), ), ); diff --git a/packages/bridge-controller/src/utils/quote-metadata/calculators.ts b/packages/bridge-controller/src/utils/quote-metadata/calculators.ts index 631542aef0..9f6b62db2f 100644 --- a/packages/bridge-controller/src/utils/quote-metadata/calculators.ts +++ b/packages/bridge-controller/src/utils/quote-metadata/calculators.ts @@ -510,7 +510,6 @@ export const calcQuoteMetadata = ( }; }; -// TODO Phase 1.5 of migration uses calcQuoteMetadata's results as fallback export const calcQuoteMetadataV2 = ( quote: QuoteResponse, usdToFiatExchangeRate: BigNumber, @@ -551,5 +550,3 @@ export const calcQuoteMetadataV2 = ( }, }; }; - -// TODO Phase 2 of migration only uses metadata from the API response