Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"js/ts.tsdk.path": "node_modules/typescript/lib"
}
2 changes: 2 additions & 0 deletions packages/bridge-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- **BREAKING:** `appendFeesToQuotes` interface now requires a chainId parameter, but still accepts both V1 and V2 quotes
- Pass migrationPhase to selector, determines how metadata is resolved
- **BREAKING:** Use QuoteResponse V2 within the BridgeController; this affects the batch-sell, unified swap/bridge and quickBuy experiences ([#9085](https://github.com/MetaMask/core/pull/9085))

- convert quotes to QuoteResponse v2 in `fetchBridgeQuoteStream`
- store quotes as QuoteResponse v2 in the BridgeController
- `QuoteResponse` export now means v2; v1 is still exported as `QuoteResponseV1`
Expand Down
1 change: 1 addition & 0 deletions packages/bridge-controller/src/bridge-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
getDefaultBridgeControllerState,
isCrossChain,
isEthUsdt,
isNativeAddress,

Check failure on line 42 in packages/bridge-controller/src/bridge-controller.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (lint:eslint) (24.x)

'isNativeAddress' is defined but never used
isNonEvmChainId,
isSolanaChainId,
} from './utils/bridge.js';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ describe('quote-response-v2 migration', () => {
"valueInCurrency": undefined,
},
],
"txFee": undefined,
}
`);

Expand Down
3 changes: 3 additions & 0 deletions packages/bridge-controller/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ export {
export {
calcSlippagePercentage,
calcQuoteMetadata,
calcQuoteMetadataV2,
} from './utils/quote-metadata/calculators.js';

export { calcLatestSrcBalance } from './utils/balance.js';
Expand Down Expand Up @@ -247,6 +248,8 @@ export {
selectBridgeFeatureFlags,
selectMinimumBalanceForRentExemptionInSOL,
selectTokenWarnings,
selectMetadataV2,
selectUsdToFiatExchangeRate,
} from './selectors.js';

export { DEFAULT_FEATURE_FLAG_CONFIG } from './constants/bridge.js';
Expand Down
63 changes: 54 additions & 9 deletions packages/bridge-controller/src/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ 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,
calcQuoteMetadataV2,
calcQuoteMetadata,
} from './utils/quote-metadata/calculators.js';
import { mergeQuoteMetadata } from './utils/quote-metadata/merge.js';
import type { QuoteMetadata } from './utils/quote-metadata/types.js';
import { getDefaultSlippagePercentage } from './utils/slippage.js';
Expand Down Expand Up @@ -98,6 +101,7 @@ const createBridgeSelector = createSelector_.withTypes<BridgeAppState>();
type BridgeQuotesClientParams = {
sortOrder: SortOrder;
selectedQuote: (QuoteResponse & QuoteMetadata) | null;
migrationPhase: '1' | '1.5' | '2';
};

type EvmTokenExchangeRate = { price?: number; currency?: string };
Expand Down Expand Up @@ -362,13 +366,54 @@ const selectMetadata = createBridgeSelector(
},
);

export 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;
},
);

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(
Expand Down Expand Up @@ -465,8 +510,8 @@ export const selectIsQuoteExpired = createBridgeSelector(
(isQuoteGoingToRefresh, quotesLastFetched, refreshRate, currentTimeInMs) =>
Boolean(
!isQuoteGoingToRefresh &&
quotesLastFetched &&
currentTimeInMs - quotesLastFetched > refreshRate,
quotesLastFetched &&
currentTimeInMs - quotesLastFetched > refreshRate,
),
);

Expand Down
1 change: 1 addition & 0 deletions packages/bridge-controller/src/utils/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type { QuoteResponse } from '../validators/quote-response.js';
import { validateQuoteStreamComplete } from '../validators/quote-stream-complete.js';
import { validateTokenFeature } from '../validators/token-feature.js';
import { isEvmTxData } from '../validators/trade.js';
import type { TxData } from '../validators/trade.js';
import { getEthUsdtResetData } from './bridge.js';
import {
formatAddressToAssetId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type { TxData } from '../../validators/trade.js';
import { isEvmQuoteResponse, isNativeAddress } from '../bridge.js';
import { calcTokenAmount } from '../number-formatters.js';
import type { QuoteMetadata, TokenAmountValues } from './types.js';
import { FeeType } from '../../index.js';

export const calcNonEvmTotalNetworkFee = (
bridgeQuote: QuoteResponseV1 & NonEvmFees,
Expand Down Expand Up @@ -447,6 +448,7 @@ export const calcQuoteMetadata = (

const isQuoteV2 = is(quote, QuoteResponseSchemaV2);
const quoteV1 = isQuoteV2 ? toQuoteResponseV1(quote) : quote;
// const quote = toQuoteResponseV2(toQuoteResponseV1(baseQuote));

const sentAmount = calcSentAmount(
quoteV1.quote,
Expand Down Expand Up @@ -533,3 +535,59 @@ export const calcQuoteMetadata = (
}),
};
};

export const calcQuoteMetadataV2 = (
quote: QuoteResponse,
usdToFiatExchangeRateString?: string,
): DeepPartial<QuoteResponse> => {
if (!usdToFiatExchangeRateString) {
return {};
}
const usdToFiatExchangeRate = new BigNumber(usdToFiatExchangeRateString);
// Calculate fiat based on usd value
return {
quote: {
src: {
valueInCurrency:
quote.quote.src.usd &&
usdToFiatExchangeRate.times(quote.quote.src.usd).toFixed(),
},
dest: {
valueInCurrency:
quote.quote.dest.usd &&
usdToFiatExchangeRate.times(quote.quote.dest.usd).toFixed(),
minAmountValueInCurrency:
quote.quote.dest.minAmountUsd &&
usdToFiatExchangeRate.times(quote.quote.dest.minAmountUsd).toFixed(),
},
feeData: Object.fromEntries(
Object.values(FeeType).map((feeType) => [
feeType,
quote.quote.feeData[feeType]
?.filter((fee) => fee.usd)
.map((fee) => ({
valueInCurrency: usdToFiatExchangeRate
.times(fee.usd as string)
.toFixed(),
})),
]),
),
priceData: {
priceImpact: {
valueInCurrency: quote.quote.priceData?.priceImpact?.usd
? usdToFiatExchangeRate
.times(quote.quote.priceData?.priceImpact?.usd)
.toFixed()
: undefined,
},
adjustedReturn: {
valueInCurrency: quote.quote.priceData?.adjustedReturn?.usd
? usdToFiatExchangeRate
.times(quote.quote.priceData.adjustedReturn.usd)
.toFixed()
: undefined,
},
},
},
};
};
29 changes: 29 additions & 0 deletions packages/bridge-controller/src/utils/quote-metadata/merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,24 @@ import type { QuoteResponse } from '../../validators/quote-response.js';
import { toQuoteMetadataV2 } from './to-quote-metadata-v2.js';
import type { QuoteMetadata } from './types.js';
import { toNormalizedAmounts } from './to-normalized-amounts.js';
import type { DeepPartial } 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 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<
QuoteType extends QuoteResponse | QuoteResponseV1 = QuoteResponse,
>(
quoteResponse: QuoteType,
legacyQuoteMetadata: QuoteMetadata,
migrationPhase: '1' | '1.5' | '2' = '1',
fiatQuoteMetadata?: DeepPartial<QuoteResponse>,
): QuoteType & QuoteMetadata {
if (is(quoteResponse, QuoteResponseSchemaV1)) {
return merge({}, quoteResponse, legacyQuoteMetadata);
Expand All @@ -31,6 +36,30 @@ export function mergeQuoteMetadata<
);
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') {
// console.log('====', {
// legacyQuoteMetadataV2: legacyQuoteMetadataV2.quote?.priceData,
// quoteResponse: quoteResponse.quote?.priceData,
// normalizedAmountsV2: normalizedAmountsV2.quote?.priceData,
// fiatQuoteMetadata: fiatQuoteMetadata?.quote?.priceData,
// legacyQuoteMetadata: legacyQuoteMetadata?.priceImpact,
// });
return merge(
{},
legacyQuoteMetadataV2,
quoteResponse,
normalizedAmountsV2,
fiatQuoteMetadata,
legacyQuoteMetadata, // return for client testing
);
}

// Phase 1 of migration uses calcQuoteMetadata's results
return merge(
{},
Expand Down
Loading
Loading