From b9ba43af081542840e1598672b9614d0a4b2acc8 Mon Sep 17 00:00:00 2001 From: micaelae Date: Wed, 3 Jun 2026 18:32:42 -0700 Subject: [PATCH 1/9] refactor: split up validators file --- .../src/bridge-controller.sse.batch.test.ts | 3 +- .../src/bridge-controller.sse.test.ts | 9 +- .../src/bridge-controller.test.ts | 9 +- .../src/bridge-controller.ts | 3 +- packages/bridge-controller/src/index.ts | 12 +- .../bridge-controller/src/selectors.test.ts | 4 +- packages/bridge-controller/src/types.ts | 30 +- .../src/utils/feature-flags.ts | 2 +- .../bridge-controller/src/utils/fetch.test.ts | 31 +- packages/bridge-controller/src/utils/fetch.ts | 16 +- .../src/utils/metrics/properties.ts | 2 +- packages/bridge-controller/src/utils/quote.ts | 6 +- .../src/validators/batch-sell.ts | 55 ++++ .../src/validators/bridge-asset.ts | 50 ++++ .../src/validators/feature-flags.ts | 137 +++++++++ .../quote-response.ts} | 275 ++---------------- .../src/validators/quote-stream-complete.ts | 39 +++ .../src/validators/token-feature.ts | 22 ++ .../{utils => validators}/validators.test.ts | 12 +- .../tests/mock-quotes-erc20-erc20.ts | 5 +- .../tests/mock-quotes-erc20-native.ts | 5 +- .../tests/mock-quotes-native-erc20-eth.ts | 5 +- .../tests/mock-quotes-native-erc20.ts | 5 +- .../tests/mock-quotes-sol-erc20.ts | 5 +- 24 files changed, 417 insertions(+), 325 deletions(-) create mode 100644 packages/bridge-controller/src/validators/batch-sell.ts create mode 100644 packages/bridge-controller/src/validators/bridge-asset.ts create mode 100644 packages/bridge-controller/src/validators/feature-flags.ts rename packages/bridge-controller/src/{utils/validators.ts => validators/quote-response.ts} (51%) create mode 100644 packages/bridge-controller/src/validators/quote-stream-complete.ts create mode 100644 packages/bridge-controller/src/validators/token-feature.ts rename packages/bridge-controller/src/{utils => validators}/validators.test.ts (99%) diff --git a/packages/bridge-controller/src/bridge-controller.sse.batch.test.ts b/packages/bridge-controller/src/bridge-controller.sse.batch.test.ts index 1f42e404c8..23baf3f531 100644 --- a/packages/bridge-controller/src/bridge-controller.sse.batch.test.ts +++ b/packages/bridge-controller/src/bridge-controller.sse.batch.test.ts @@ -20,11 +20,12 @@ import { DEFAULT_BRIDGE_CONTROLLER_STATE, } from './constants/bridge'; import * as selectors from './selectors'; -import { ChainId, RequestStatus, FeatureId } from './types'; +import { ChainId, RequestStatus } from './types'; import type { BridgeControllerMessenger } from './types'; import * as balanceUtils from './utils/balance'; import * as featureFlagUtils from './utils/feature-flags'; import * as fetchUtils from './utils/fetch'; +import { FeatureId } from './validators/feature-flags'; type RootMessenger = Messenger< MockAnyNamespace, diff --git a/packages/bridge-controller/src/bridge-controller.sse.test.ts b/packages/bridge-controller/src/bridge-controller.sse.test.ts index cf4d62a102..9f4db46703 100644 --- a/packages/bridge-controller/src/bridge-controller.sse.test.ts +++ b/packages/bridge-controller/src/bridge-controller.sse.test.ts @@ -29,16 +29,15 @@ import { DEFAULT_BRIDGE_CONTROLLER_STATE, ETH_USDT_ADDRESS, } from './constants/bridge'; -import { ChainId, RequestStatus, FeatureId } from './types'; +import { ChainId, RequestStatus } from './types'; import type { BridgeControllerMessenger, TxData } from './types'; import * as balanceUtils from './utils/balance'; import { formatChainIdToDec } from './utils/caip-formatters'; import * as featureFlagUtils from './utils/feature-flags'; import * as fetchUtils from './utils/fetch'; -import { - TokenFeatureType, - QuoteStreamCompleteReason, -} from './utils/validators'; +import { QuoteStreamCompleteReason } from './validators/quote-stream-complete'; +import { TokenFeatureType } from './validators/token-feature'; +import { FeatureId } from './validators/feature-flags'; type RootMessenger = Messenger< MockAnyNamespace, diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 81565834b8..dbecaf1f8f 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -32,13 +32,7 @@ import { } from './constants/bridge'; import { SWAPS_API_V2_BASE_URL } from './constants/swaps'; import * as selectors from './selectors'; -import { - ChainId, - RequestStatus, - SortOrder, - StatusTypes, - FeatureId, -} from './types'; +import { ChainId, RequestStatus, SortOrder, StatusTypes } from './types'; import type { BridgeControllerMessenger, QuoteResponseV1, @@ -61,6 +55,7 @@ import { MetricsSwapType, UnifiedSwapBridgeEventName, } from './utils/metrics/constants'; +import { FeatureId } from './validators/feature-flags'; const EMPTY_INIT_STATE = DEFAULT_BRIDGE_CONTROLLER_STATE; diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 0b37efac21..f0e3805ca1 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -25,7 +25,7 @@ import { ExchangeRateSourcesForLookup, selectIsAssetExchangeRateInState, } from './selectors'; -import { FeatureId, RequestStatus } from './types'; +import { RequestStatus } from './types'; import type { L1GasFees, GenericQuoteRequest, @@ -94,6 +94,7 @@ import { } from './utils/quote'; import { appendFeesToQuotes } from './utils/quote-fees'; import { getMinimumBalanceForRentExemptionInLamports } from './utils/snaps'; +import type { FeatureId } from './validators/feature-flags'; const metadata: StateMetadata = { quoteRequest: { diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index 35d92f39cf..0b166aff4a 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -94,7 +94,6 @@ export { SortOrder, ChainId, RequestStatus, - FeatureId, type TokenFeature, type QuoteStreamCompleteData, type BridgeControllerGetStateAction, @@ -105,12 +104,15 @@ export { DiscountType, FeeType, ActionTypes, - BridgeAssetSchema, - TokenFeatureType, +} from './validators/quote-response'; +export { validateQuoteStreamComplete, QuoteStreamCompleteReason, - BatchSellTransactionType, -} from './utils/validators'; +} from './validators/quote-stream-complete'; +export { BatchSellTransactionType } from './validators/batch-sell'; +export { TokenFeatureType } from './validators/token-feature'; +export { BridgeAssetSchema } from './validators/bridge-asset'; +export { FeatureId } from './validators/feature-flags'; export { ALLOWED_BRIDGE_CHAIN_IDS, diff --git a/packages/bridge-controller/src/selectors.test.ts b/packages/bridge-controller/src/selectors.test.ts index 23d95fc601..6bd770fd16 100644 --- a/packages/bridge-controller/src/selectors.test.ts +++ b/packages/bridge-controller/src/selectors.test.ts @@ -38,8 +38,8 @@ import { formatChainIdToDec, formatChainIdToHex, } from './utils/caip-formatters'; -import { validateQuoteResponseV1 } from './utils/validators'; -import { BatchSellTransactionType } from './utils/validators'; +import { validateQuoteResponseV1 } from './validators/quote-response'; +import { BatchSellTransactionType } from './validators/batch-sell'; const MOCK_USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; const MOCK_MUSD_ADDRESS = '0x12345A7890123456789012345678901234567890'; diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index c83478fbb1..5aa2d54c5c 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -30,28 +30,31 @@ import type { import type { BridgeController } from './bridge-controller'; import type { BridgeControllerMethodActions } from './bridge-controller-method-action-types'; import type { BRIDGE_CONTROLLER_NAME } from './constants/bridge'; +import type { SimulatedGasFeeLimitsSchema } from './validators/batch-sell'; +import type { BatchSellTradesResponseSchema } from './validators/batch-sell'; +import type { BridgeAssetSchema } from './validators/bridge-asset'; +import type { FeatureId } from './validators/feature-flags'; import type { - BitcoinTradeDataSchema, - BridgeAssetSchema, ChainConfigurationSchema, ChainRankingSchema, + PlatformConfigSchema, +} from './validators/feature-flags'; +import type { + BitcoinTradeDataSchema, FeeDataSchema, IntentSchema, - PlatformConfigSchema, ProtocolSchema, QuoteResponseSchema, QuoteSchema, StepSchema, - TokenFeatureSchema, - QuoteStreamCompleteSchema, StellarTradeDataSchema, TronTradeDataSchema, TxDataSchema, - BatchSellTradesResponseSchema, GaslessPropertiesSchema, - SimulatedGasFeeLimitsSchema, TxFeeGasLimitsSchema, -} from './utils/validators'; +} from './validators/quote-response'; +import type { QuoteStreamCompleteSchema } from './validators/quote-stream-complete'; +import type { TokenFeatureSchema } from './validators/token-feature'; export type FetchFunction = ( input: RequestInfo | URL | string, @@ -261,17 +264,6 @@ export enum StatusTypes { COMPLETE = 'COMPLETE', } -export enum FeatureId { - UNKNOWN = 'unknown', - PERPS = 'perps', - QUICK_BUY_FOLLOW_TRADING = 'quick_buy_follow_trading', - QUICK_BUY_TOKEN_DETAILS = 'quick_buy_token_details', - QUICK_BUY_EXPLORE = 'quick_buy_explore', - DAPP_SWAP = 'dapp_swap', - BATCH_SELL = 'batch_sell', - UNIFIED_SWAP_BRIDGE = 'unified_swap_bridge', -} - /** * These are types that components pass in. Since data is a mix of types when coming from the redux store, we need to use a generic type that can cover all the types. * Payloads with this type are transformed into QuoteRequest by fetchBridgeQuotes right before fetching quotes diff --git a/packages/bridge-controller/src/utils/feature-flags.ts b/packages/bridge-controller/src/utils/feature-flags.ts index 4060f302b0..b0aedd5197 100644 --- a/packages/bridge-controller/src/utils/feature-flags.ts +++ b/packages/bridge-controller/src/utils/feature-flags.ts @@ -5,8 +5,8 @@ import { DEFAULT_FEATURE_FLAG_CONFIG, } from '../constants/bridge'; import type { FeatureFlagsPlatformConfig, ChainConfiguration } from '../types'; +import { validateFeatureFlagsResponse } from '../validators/feature-flags'; import { formatChainIdToCaip } from './caip-formatters'; -import { validateFeatureFlagsResponse } from './validators'; export const formatFeatureFlags = ( bridgeFeatureFlags: FeatureFlagsPlatformConfig, diff --git a/packages/bridge-controller/src/utils/fetch.test.ts b/packages/bridge-controller/src/utils/fetch.test.ts index e0fb5d06c1..82fd3f3f51 100644 --- a/packages/bridge-controller/src/utils/fetch.test.ts +++ b/packages/bridge-controller/src/utils/fetch.test.ts @@ -4,7 +4,8 @@ import type { CaipAssetType } from '@metamask/utils'; import { mockBridgeQuotesErc20Erc20V1 } from '../../tests/mock-quotes-erc20-erc20'; import { mockBridgeQuotesNativeErc20V1 } from '../../tests/mock-quotes-native-erc20'; import { BridgeClientId, BRIDGE_PROD_API_BASE_URL } from '../constants/bridge'; -import { FeatureId } from '../types'; +import { BatchSellTransactionType } from '../validators/batch-sell'; +import { FeatureId } from '../validators/feature-flags'; import { fetchBridgeQuotes, fetchBridgeTokens, @@ -12,7 +13,6 @@ import { fetchBatchSellTrades, formatBatchSellTradesRequest, } from './fetch'; -import { BatchSellTransactionType } from './validators'; const mockFetchFn = jest.fn(); @@ -887,6 +887,27 @@ describe('fetch', () => { ), ), ); + expect( + // @ts-expect-error - reason is not in type + result.map((error) => ({ ...error, reason: error.reason?.message })), + ).toMatchInlineSnapshot(` + [ + { + "reason": "Invalid batch simulation response. StructError: At path: transactions.0.maxFeePerGas -- Expected a string, but received: 1000", + "status": "rejected", + }, + { + "reason": "Invalid batch simulation response. StructError: At path: transactions.0.maxFeePerGas -- Expected a string matching \`/^0x[0-9a-f]+$/\` but received "1000"", + "status": "rejected", + }, + { + "reason": "Invalid batch simulation response. StructError: At path: transactions.0.maxFeePerGas -- Expected a string, but received: 291", + "status": "rejected", + }, + ] + `); + expect(mockConsoleWarn).not.toHaveBeenCalled(); + mockConsoleWarn.mockRestore(); expect(mockFetchFn).toHaveBeenCalledTimes(4); expect(mockFetchFn).toHaveBeenCalledWith( @@ -900,15 +921,15 @@ describe('fetch', () => { ).toMatchInlineSnapshot(` [ { - "reason": "Invalid batch simulation response. StructError: At path: transactions.0.maxFeePerGas -- Expected a value of type \`HexString\`, but received: \`1000\`", + "reason": "Invalid batch simulation response. StructError: At path: transactions.0.maxFeePerGas -- Expected a string, but received: 1000", "status": "rejected", }, { - "reason": "Invalid batch simulation response. StructError: At path: transactions.0.maxFeePerGas -- Expected a value of type \`HexString\`, but received: \`"1000"\`", + "reason": "Invalid batch simulation response. StructError: At path: transactions.0.maxFeePerGas -- Expected a string matching \`/^0x[0-9a-f]+$/\` but received "1000"", "status": "rejected", }, { - "reason": "Invalid batch simulation response. StructError: At path: transactions.0.maxFeePerGas -- Expected a value of type \`HexString\`, but received: \`291\`", + "reason": "Invalid batch simulation response. StructError: At path: transactions.0.maxFeePerGas -- Expected a string, but received: 291", "status": "rejected", }, ] diff --git a/packages/bridge-controller/src/utils/fetch.ts b/packages/bridge-controller/src/utils/fetch.ts index 07f9795afe..3672468ec4 100644 --- a/packages/bridge-controller/src/utils/fetch.ts +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -12,8 +12,13 @@ import type { QuoteStreamCompleteData, BatchSellTradesRequest, BatchSellTradesResponse, - FeatureId, } from '../types'; +import { validateBatchSellTradesResponse } from '../validators/batch-sell'; +import { validateBridgeAsset } from '../validators/bridge-asset'; +import type { FeatureId } from '../validators/feature-flags'; +import { validateQuoteResponseV1 } from '../validators/quote-response'; +import { validateQuoteStreamComplete } from '../validators/quote-stream-complete'; +import { validateTokenFeature } from '../validators/token-feature'; import { getEthUsdtResetData } from './bridge'; import { formatAddressToAssetId, @@ -22,13 +27,6 @@ import { } from './caip-formatters'; import { fetchServerEvents } from './fetch-server-events'; import { isEvmTxData } from './trade-utils'; -import { - validateQuoteResponseV1, - validateSwapsTokenObject, - validateTokenFeature, - validateQuoteStreamComplete, - validateBatchSellTradesResponse, -} from './validators'; export const getClientHeaders = ({ clientId, @@ -76,7 +74,7 @@ export async function fetchBridgeTokens( const transformedTokens: Record = {}; tokens.forEach((token: unknown) => { - if (validateSwapsTokenObject(token)) { + if (validateBridgeAsset(token)) { transformedTokens[token.address] = token; } }); diff --git a/packages/bridge-controller/src/utils/metrics/properties.ts b/packages/bridge-controller/src/utils/metrics/properties.ts index 89b65fcf26..af3b18b6ac 100644 --- a/packages/bridge-controller/src/utils/metrics/properties.ts +++ b/packages/bridge-controller/src/utils/metrics/properties.ts @@ -9,7 +9,6 @@ import type { QuoteResponseV1, TxData, } from '../../types'; -import { FeatureId } from '../../types'; import { getNativeAssetForChainId, isCrossChain } from '../bridge'; import { formatAddressToAssetId, @@ -23,6 +22,7 @@ import type { QuoteWarning, RequestParams, } from './types'; +import { FeatureId } from '../../validators/feature-flags'; export const toInputChangedPropertyKey: Partial< Record diff --git a/packages/bridge-controller/src/utils/quote.ts b/packages/bridge-controller/src/utils/quote.ts index 00acc26507..f6112dce9e 100644 --- a/packages/bridge-controller/src/utils/quote.ts +++ b/packages/bridge-controller/src/utils/quote.ts @@ -17,7 +17,7 @@ import type { NonEvmFees, TxData, } from '../types'; -import { FeatureId } from '../types'; +import { FeatureId } from '../validators/feature-flags'; import { isNativeAddress, isNonEvmChainId } from './bridge'; export const isValidQuoteRequest = ( @@ -152,9 +152,7 @@ export const calcSentAmount = ( const sentAmount = intent ? new BigNumber(srcTokenAmount) : Object.values(feeData) - .filter( - (fee) => fee && fee.amount && fee.asset?.assetId === srcAsset.assetId, - ) + .filter((fee) => fee?.amount && fee.asset?.assetId === srcAsset.assetId) .reduce( (acc, { amount }) => acc.plus(amount), new BigNumber(srcTokenAmount), diff --git a/packages/bridge-controller/src/validators/batch-sell.ts b/packages/bridge-controller/src/validators/batch-sell.ts new file mode 100644 index 0000000000..35acf98d1d --- /dev/null +++ b/packages/bridge-controller/src/validators/batch-sell.ts @@ -0,0 +1,55 @@ +import { + intersection, + type, + array, + enums, + optional, + assert, + union, +} from '@metamask/superstruct'; +import type { Infer } from '@metamask/superstruct'; +import { StrictHexStruct } from '@metamask/utils'; + +import { BridgeAssetSchema } from './bridge-asset'; +import { + TxDataSchema, + NumberStringSchema, + GaslessPropertiesSchema, +} from './quote-response'; + +export enum BatchSellTransactionType { + TRADE = 'trade', + APPROVAL = 'approval', + TRANSFER = 'transfer', +} + +export const SimulatedGasFeeLimitsSchema = type({ + maxFeePerGas: StrictHexStruct, + maxPriorityFeePerGas: StrictHexStruct, +}); + +export const BatchSellTradesResponseSchema = intersection([ + type({ + transactions: array( + intersection([ + TxDataSchema, + SimulatedGasFeeLimitsSchema, + type({ type: enums(Object.values(BatchSellTransactionType)) }), + ]), + ), + fee: optional( + type({ + asset: BridgeAssetSchema, + amount: NumberStringSchema, + }), + ), + }), + GaslessPropertiesSchema, +]); + +export const validateBatchSellTradesResponse = ( + data: unknown, +): data is Infer => { + assert(data, BatchSellTradesResponseSchema); + return true; +}; diff --git a/packages/bridge-controller/src/validators/bridge-asset.ts b/packages/bridge-controller/src/validators/bridge-asset.ts new file mode 100644 index 0000000000..0e8092a691 --- /dev/null +++ b/packages/bridge-controller/src/validators/bridge-asset.ts @@ -0,0 +1,50 @@ +import { + number, + type, + string, + optional, + nullable, + is, +} from '@metamask/superstruct'; +import type { Infer } from '@metamask/superstruct'; +import { CaipAssetTypeStruct } from '@metamask/utils'; + +export const ChainIdSchema = number(); + +export const BridgeAssetSchema = type({ + /** + * The chainId of the token + */ + chainId: ChainIdSchema, + /** + * An address that the metaswap-api recognizes as the default token + */ + address: string(), + /** + * The assetId of the token + */ + assetId: CaipAssetTypeStruct, + /** + * The symbol of token object + */ + symbol: string(), + /** + * The name for the network + */ + name: string(), + decimals: number(), + /** + * URL for token icon + */ + icon: optional(nullable(string())), + /** + * URL for token icon + */ + iconUrl: optional(nullable(string())), +}); + +export const validateBridgeAsset = ( + data: unknown, +): data is Infer => { + return is(data, BridgeAssetSchema); +}; diff --git a/packages/bridge-controller/src/validators/feature-flags.ts b/packages/bridge-controller/src/validators/feature-flags.ts new file mode 100644 index 0000000000..1aeb84965c --- /dev/null +++ b/packages/bridge-controller/src/validators/feature-flags.ts @@ -0,0 +1,137 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { + type, + record, + string, + optional, + array, + boolean, + number, + enums, + is, + define, +} from '@metamask/superstruct'; +import type { Infer } from '@metamask/superstruct'; +import { CaipChainIdStruct, CaipAssetTypeStruct } from '@metamask/utils'; + +export enum FeatureId { + UNKNOWN = 'unknown', + PERPS = 'perps', + QUICK_BUY_FOLLOW_TRADING = 'quick_buy_follow_trading', + QUICK_BUY_TOKEN_DETAILS = 'quick_buy_token_details', + QUICK_BUY_EXPLORE = 'quick_buy_explore', + DAPP_SWAP = 'dapp_swap', + BATCH_SELL = 'batch_sell', + UNIFIED_SWAP_BRIDGE = 'unified_swap_bridge', +} + +export const VersionStringSchema = define( + 'VersionString', + (value: unknown) => + typeof value === 'string' && + /^(\d+\.*){2}\d+$/u.test(value) && + value.split('.').length === 3, +); + +const DefaultPairSchema = type({ + /** + * The standard default pairs. Use this if the pair is only set once. + * The key is the CAIP asset type of the src token and the value is the CAIP asset type of the dest token. + */ + standard: record(string(), string()), + /** + * The other default pairs. Use this if the dest token depends on the src token and can be set multiple times. + * The key is the CAIP asset type of the src token and the value is the CAIP asset type of the dest token. + */ + other: record(string(), string()), +}); + +export const ChainRankingItemSchema = type({ + /** + * The CAIP-2 chain identifier (e.g., "eip155:1" for Ethereum mainnet) + */ + chainId: CaipChainIdStruct, + /** + * The display name of the chain (e.g., "Ethereum") + */ + name: string(), +}); + +export const ChainRankingSchema = optional(array(ChainRankingItemSchema)); + +export const ChainConfigurationSchema = type({ + isActiveSrc: boolean(), + isActiveDest: boolean(), + refreshRate: optional(number()), + topAssets: optional(array(string())), + stablecoins: optional(array(string())), + batchSellDestStablecoins: optional(array(CaipAssetTypeStruct)), + isUnifiedUIEnabled: optional(boolean()), + isSingleSwapBridgeButtonEnabled: optional(boolean()), + isGaslessSwapEnabled: optional(boolean()), + noFeeAssets: optional(array(string())), + defaultPairs: optional(DefaultPairSchema), +}); + +export const PriceImpactThresholdSchema = type({ + // TODO: + // We are moving into a unified approach where + // price impact thresholds will be segmented by + // importance rather than transaction type. + // The introduction of warning/danger will first be handled + // by mobile, followed by extension and then removal of gasless/normal + // from LD configs. + // To make the migration easier, we define all fields as optional for now. + // After the migration takes place, gasless/normal will be removed + // and warning/danger will be set as required fields. + gasless: number(), // Percentage value in decimal format (eg 0.02 is 2%) + normal: number(), // Percentage value in decimal format + warning: optional(number()), // Percentage value in decimal format + error: optional(number()), // Percentage value in decimal format +}); +const GenericQuoteRequestSchema = type({ + aggIds: optional(array(string())), + bridgeIds: optional(array(string())), + fee: optional(number()), +}); +const FeatureIdSchema = enums(Object.values(FeatureId)); +/** + * This is the schema for the feature flags response from the RemoteFeatureFlagController + */ + +export const PlatformConfigSchema = type({ + priceImpactThreshold: optional(PriceImpactThresholdSchema), + quoteRequestOverrides: optional( + record(FeatureIdSchema, optional(GenericQuoteRequestSchema)), + ), + minimumVersion: string(), + refreshRate: number(), + maxRefreshCount: number(), + support: boolean(), + chains: record(string(), ChainConfigurationSchema), + /** + * The bip44 default pairs for the chains + * Key is the CAIP chainId namespace + */ + bip44DefaultPairs: optional(record(string(), optional(DefaultPairSchema))), + sse: optional( + type({ + enabled: boolean(), + /** + * The minimum version of the client required to enable SSE, for example 13.8.0 + */ + minimumVersion: VersionStringSchema, + }), + ), + /** + * Array of chain objects ordered by preference/ranking + */ + chainRanking: ChainRankingSchema, + maxPendingHistoryItemAgeMs: optional(number()), +}); + +export const validateFeatureFlagsResponse = ( + data: unknown, +): data is Infer => { + return is(data, PlatformConfigSchema); +}; diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/validators/quote-response.ts similarity index 51% rename from packages/bridge-controller/src/utils/validators.ts rename to packages/bridge-controller/src/validators/quote-response.ts index c1ae574b74..0111e2ab84 100644 --- a/packages/bridge-controller/src/utils/validators.ts +++ b/packages/bridge-controller/src/validators/quote-response.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { isValidHexAddress } from '@metamask/controller-utils'; import type { Infer } from '@metamask/superstruct'; import { any, @@ -7,7 +6,6 @@ import { boolean, number, type, - is, record, array, nullable, @@ -20,11 +18,13 @@ import { intersection, } from '@metamask/superstruct'; import { - CaipAssetTypeStruct, - CaipChainIdStruct, - isStrictHexString, + HexAddressStruct, + HexChecksumAddressStruct, + StrictHexStruct, } from '@metamask/utils'; +import { BridgeAssetSchema, ChainIdSchema } from './bridge-asset'; + export enum FeeType { METABRIDGE = 'metabridge', REFUEL = 'refuel', @@ -43,171 +43,19 @@ export enum ActionTypes { REFUEL = 'refuel', } -const HexAddressSchema = define<`0x${string}`>('HexAddress', (value: unknown) => - isValidHexAddress(value as string, { allowNonPrefixed: false }), -); - -const HexStringSchema = define<`0x${string}`>('HexString', isStrictHexString); +const HexAddressOrChecksumAddressSchema = union([ + HexAddressStruct, + HexChecksumAddressStruct, +]); -const NumberStringSchema = define( +export const NumberStringSchema = define( 'NumberString', (value: unknown) => typeof value === 'string' && /^\d+$/u.test(value), ); -const VersionStringSchema = define( - 'VersionString', - (value: unknown) => - typeof value === 'string' && - /^(\d+\.*){2}\d+$/u.test(value) && - value.split('.').length === 3, -); - export const truthyString = (value: string): boolean => Boolean(value?.length); const TruthyDigitStringSchema = pattern(string(), /^\d+$/u); -const ChainIdSchema = number(); - -export const BridgeAssetSchema = type({ - /** - * The chainId of the token - */ - chainId: ChainIdSchema, - /** - * An address that the metaswap-api recognizes as the default token - */ - address: string(), - /** - * The assetId of the token - */ - assetId: CaipAssetTypeStruct, - /** - * The symbol of token object - */ - symbol: string(), - /** - * The name for the network - */ - name: string(), - decimals: number(), - /** - * URL for token icon - */ - icon: optional(nullable(string())), - /** - * URL for token icon - */ - iconUrl: optional(nullable(string())), -}); - -const DefaultPairSchema = type({ - /** - * The standard default pairs. Use this if the pair is only set once. - * The key is the CAIP asset type of the src token and the value is the CAIP asset type of the dest token. - */ - standard: record(string(), string()), - /** - * The other default pairs. Use this if the dest token depends on the src token and can be set multiple times. - * The key is the CAIP asset type of the src token and the value is the CAIP asset type of the dest token. - */ - other: record(string(), string()), -}); - -export const ChainRankingItemSchema = type({ - /** - * The CAIP-2 chain identifier (e.g., "eip155:1" for Ethereum mainnet) - */ - chainId: CaipChainIdStruct, - /** - * The display name of the chain (e.g., "Ethereum") - */ - name: string(), -}); - -export const ChainRankingSchema = optional(array(ChainRankingItemSchema)); - -export const ChainConfigurationSchema = type({ - isActiveSrc: boolean(), - isActiveDest: boolean(), - refreshRate: optional(number()), - topAssets: optional(array(string())), - stablecoins: optional(array(string())), - batchSellDestStablecoins: optional(array(CaipAssetTypeStruct)), - isUnifiedUIEnabled: optional(boolean()), - isSingleSwapBridgeButtonEnabled: optional(boolean()), - isGaslessSwapEnabled: optional(boolean()), - noFeeAssets: optional(array(string())), - defaultPairs: optional(DefaultPairSchema), -}); - -export const PriceImpactThresholdSchema = type({ - // TODO: - // We are moving into a unified approach where - // price impact thresholds will be segmented by - // importance rather than transaction type. - // The introduction of warning/danger will first be handled - // by mobile, followed by extension and then removal of gasless/normal - // from LD configs. - // To make the migration easier, we define all fields as optional for now. - // After the migration takes place, gasless/normal will be removed - // and warning/danger will be set as required fields. - gasless: number(), // Percentage value in decimal format (eg 0.02 is 2%) - normal: number(), // Percentage value in decimal format - warning: optional(number()), // Percentage value in decimal format - error: optional(number()), // Percentage value in decimal format -}); - -const GenericQuoteRequestSchema = type({ - aggIds: optional(array(string())), - bridgeIds: optional(array(string())), - fee: optional(number()), -}); - -/** - * This is the schema for the feature flags response from the RemoteFeatureFlagController - */ -export const PlatformConfigSchema = type({ - priceImpactThreshold: optional(PriceImpactThresholdSchema), - quoteRequestOverrides: optional( - record(string(), optional(GenericQuoteRequestSchema)), - ), - minimumVersion: string(), - refreshRate: number(), - maxRefreshCount: number(), - support: boolean(), - chains: record(string(), ChainConfigurationSchema), - /** - * The bip44 default pairs for the chains - * Key is the CAIP chainId namespace - */ - bip44DefaultPairs: optional(record(string(), optional(DefaultPairSchema))), - sse: optional( - type({ - enabled: boolean(), - /** - * The minimum version of the client required to enable SSE, for example 13.8.0 - */ - minimumVersion: VersionStringSchema, - }), - ), - /** - * Array of chain objects ordered by preference/ranking - */ - chainRanking: ChainRankingSchema, - maxPendingHistoryItemAgeMs: optional(number()), -}); - -export const validateFeatureFlagsResponse = ( - data: unknown, -): data is Infer => { - return is(data, PlatformConfigSchema); -}; - -export const validateSwapsTokenObject = ( - data: unknown, -): data is Infer => { - return is(data, BridgeAssetSchema); -}; - export const FeeDataSchema = type({ amount: TruthyDigitStringSchema, asset: BridgeAssetSchema, @@ -255,18 +103,18 @@ export const IntentOrderSchema = type({ /** * Address of the token being sold. */ - sellToken: HexAddressSchema, + sellToken: HexAddressOrChecksumAddressSchema, /** * Address of the token being bought. */ - buyToken: HexAddressSchema, + buyToken: HexAddressOrChecksumAddressSchema, /** * Optional receiver of the bought tokens. * If omitted, defaults to the signer / order owner. */ - receiver: optional(HexAddressSchema), + receiver: optional(HexAddressOrChecksumAddressSchema), /** * Order expiration time. @@ -284,7 +132,7 @@ export const IntentOrderSchema = type({ /** * Hash of the `appData` field, used for EIP-712 signing. */ - appDataHash: HexStringSchema, + appDataHash: StrictHexStruct, /** * Fee amount paid for order execution, expressed as a digit string. @@ -323,7 +171,7 @@ export const IntentOrderSchema = type({ * * Provided for convenience when building the EIP-712 domain and message. */ - from: optional(HexAddressSchema), + from: optional(HexAddressOrChecksumAddressSchema), }); /** @@ -346,7 +194,7 @@ export const IntentSchema = type({ /** * Optional settlement contract address used for execution. */ - settlementContract: optional(HexAddressSchema), + settlementContract: optional(HexAddressOrChecksumAddressSchema), /** * Optional EIP-712 typed data payload for signing. @@ -437,10 +285,10 @@ export const QuoteSchema = intersection([ export const TxDataSchema = type({ chainId: number(), - to: HexAddressSchema, - from: HexAddressSchema, - value: HexStringSchema, - data: HexStringSchema, + to: HexAddressOrChecksumAddressSchema, + from: HexAddressOrChecksumAddressSchema, + value: StrictHexStruct, + data: StrictHexStruct, gasLimit: nullable(number()), effectiveGas: optional(number()), }); @@ -497,86 +345,3 @@ export const validateQuoteResponseV1 = ( assert(data, QuoteResponseSchema); return true; }; - -export enum TokenFeatureType { - MALICIOUS = 'Malicious', - WARNING = 'Warning', - INFO = 'Info', - BENIGN = 'Benign', -} - -export const TokenFeatureSchema = type({ - feature_id: string(), - type: enums(Object.values(TokenFeatureType)), - description: string(), -}); - -export const validateTokenFeature = ( - data: unknown, -): data is Infer => { - assert(data, TokenFeatureSchema); - return true; -}; - -export enum QuoteStreamCompleteReason { - RETRY = 'RETRY', - AMOUNT_TOO_HIGH = 'AMOUNT_TOO_HIGH', - AMOUNT_TOO_LOW = 'AMOUNT_TOO_LOW', - SLIPPAGE_TOO_HIGH = 'SLIPPAGE_TOO_HIGH', - SLIPPAGE_TOO_LOW = 'SLIPPAGE_TOO_LOW', - TOKEN_NOT_SUPPORTED = 'TOKEN_NOT_SUPPORTED', - RWA_GEO_RESTRICTED = 'RWA_GEO_RESTRICTED', - RWA_NATIVE_TOKEN_UNSUPPORTED = 'RWA_NATIVE_TOKEN_UNSUPPORTED', - RWA_MARKET_UNAVAILABLE = 'RWA_MARKET_UNAVAILABLE', -} - -export const QuoteStreamCompleteSchema = type({ - quoteCount: number(), - hasQuotes: boolean(), - reason: optional(enums(Object.values(QuoteStreamCompleteReason))), - context: optional(record(string(), any())), -}); - -export const validateQuoteStreamComplete = ( - data: unknown, -): data is Infer => { - assert(data, QuoteStreamCompleteSchema); - return true; -}; - -export enum BatchSellTransactionType { - TRADE = 'trade', - APPROVAL = 'approval', - TRANSFER = 'transfer', -} - -export const SimulatedGasFeeLimitsSchema = type({ - maxFeePerGas: HexStringSchema, - maxPriorityFeePerGas: HexStringSchema, -}); - -export const BatchSellTradesResponseSchema = intersection([ - type({ - transactions: array( - intersection([ - TxDataSchema, - SimulatedGasFeeLimitsSchema, - type({ type: enums(Object.values(BatchSellTransactionType)) }), - ]), - ), - fee: optional( - type({ - asset: BridgeAssetSchema, - amount: NumberStringSchema, - }), - ), - }), - GaslessPropertiesSchema, -]); - -export const validateBatchSellTradesResponse = ( - data: unknown, -): data is Infer => { - assert(data, BatchSellTradesResponseSchema); - return true; -}; diff --git a/packages/bridge-controller/src/validators/quote-stream-complete.ts b/packages/bridge-controller/src/validators/quote-stream-complete.ts new file mode 100644 index 0000000000..4e4dc790b9 --- /dev/null +++ b/packages/bridge-controller/src/validators/quote-stream-complete.ts @@ -0,0 +1,39 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { + type, + number, + boolean, + optional, + enums, + record, + string, + any, + assert, +} from '@metamask/superstruct'; +import type { Infer } from '@metamask/superstruct'; + +export enum QuoteStreamCompleteReason { + RETRY = 'RETRY', + AMOUNT_TOO_HIGH = 'AMOUNT_TOO_HIGH', + AMOUNT_TOO_LOW = 'AMOUNT_TOO_LOW', + SLIPPAGE_TOO_HIGH = 'SLIPPAGE_TOO_HIGH', + SLIPPAGE_TOO_LOW = 'SLIPPAGE_TOO_LOW', + TOKEN_NOT_SUPPORTED = 'TOKEN_NOT_SUPPORTED', + RWA_GEO_RESTRICTED = 'RWA_GEO_RESTRICTED', + RWA_NATIVE_TOKEN_UNSUPPORTED = 'RWA_NATIVE_TOKEN_UNSUPPORTED', + RWA_MARKET_UNAVAILABLE = 'RWA_MARKET_UNAVAILABLE', +} + +export const QuoteStreamCompleteSchema = type({ + quoteCount: number(), + hasQuotes: boolean(), + reason: optional(enums(Object.values(QuoteStreamCompleteReason))), + context: optional(record(string(), any())), +}); + +export const validateQuoteStreamComplete = ( + data: unknown, +): data is Infer => { + assert(data, QuoteStreamCompleteSchema); + return true; +}; diff --git a/packages/bridge-controller/src/validators/token-feature.ts b/packages/bridge-controller/src/validators/token-feature.ts new file mode 100644 index 0000000000..e9e5fcaf84 --- /dev/null +++ b/packages/bridge-controller/src/validators/token-feature.ts @@ -0,0 +1,22 @@ +import { type, string, enums, assert } from '@metamask/superstruct'; +import type { Infer } from '@metamask/superstruct'; + +export enum TokenFeatureType { + MALICIOUS = 'Malicious', + WARNING = 'Warning', + INFO = 'Info', + BENIGN = 'Benign', +} + +export const TokenFeatureSchema = type({ + feature_id: string(), + type: enums(Object.values(TokenFeatureType)), + description: string(), +}); + +export const validateTokenFeature = ( + data: unknown, +): data is Infer => { + assert(data, TokenFeatureSchema); + return true; +}; diff --git a/packages/bridge-controller/src/utils/validators.test.ts b/packages/bridge-controller/src/validators/validators.test.ts similarity index 99% rename from packages/bridge-controller/src/utils/validators.test.ts rename to packages/bridge-controller/src/validators/validators.test.ts index 2a935284c3..20193d63e9 100644 --- a/packages/bridge-controller/src/utils/validators.test.ts +++ b/packages/bridge-controller/src/validators/validators.test.ts @@ -1,15 +1,17 @@ import { is } from '@metamask/superstruct'; import { mockBridgeQuotesNativeErc20EthV1 } from '../../tests/mock-quotes-native-erc20-eth'; +import { validateFeatureFlagsResponse } from './feature-flags'; import { - DiscountType, - FeeDataSchema, - validateFeatureFlagsResponse, + IntentSchema, validateQuoteResponseV1, + FeeDataSchema, + DiscountType, +} from './quote-response'; +import { validateQuoteStreamComplete, QuoteStreamCompleteReason, - IntentSchema, -} from './validators'; +} from './quote-stream-complete'; describe('validators', () => { describe('validateFeatureFlagsResponse', () => { diff --git a/packages/bridge-controller/tests/mock-quotes-erc20-erc20.ts b/packages/bridge-controller/tests/mock-quotes-erc20-erc20.ts index 362963434d..1bca02382c 100644 --- a/packages/bridge-controller/tests/mock-quotes-erc20-erc20.ts +++ b/packages/bridge-controller/tests/mock-quotes-erc20-erc20.ts @@ -1,7 +1,10 @@ import { merge } from 'lodash'; import type { QuoteResponseV1, DeepPartial } from '../src/types'; -import { ActionTypes, validateQuoteResponseV1 } from '../src/utils/validators'; +import { + ActionTypes, + validateQuoteResponseV1, +} from '../src/validators/quote-response'; export const mockBridgeQuotesErc20Erc20V1: QuoteResponseV1[] = [ { diff --git a/packages/bridge-controller/tests/mock-quotes-erc20-native.ts b/packages/bridge-controller/tests/mock-quotes-erc20-native.ts index d5c1be1c9d..1ee086ea9a 100644 --- a/packages/bridge-controller/tests/mock-quotes-erc20-native.ts +++ b/packages/bridge-controller/tests/mock-quotes-erc20-native.ts @@ -1,7 +1,10 @@ import { merge } from 'lodash'; import type { QuoteResponseV1, DeepPartial } from '../src/types'; -import { ActionTypes, validateQuoteResponseV1 } from '../src/utils/validators'; +import { + ActionTypes, + validateQuoteResponseV1, +} from '../src/validators/quote-response'; export const mockBridgeQuotesErc20NativeV1: QuoteResponseV1[] = [ { diff --git a/packages/bridge-controller/tests/mock-quotes-native-erc20-eth.ts b/packages/bridge-controller/tests/mock-quotes-native-erc20-eth.ts index 65359833e8..4f4f864349 100644 --- a/packages/bridge-controller/tests/mock-quotes-native-erc20-eth.ts +++ b/packages/bridge-controller/tests/mock-quotes-native-erc20-eth.ts @@ -1,7 +1,10 @@ import { merge } from 'lodash'; import type { QuoteResponseV1, DeepPartial } from '../src/types'; -import { ActionTypes, validateQuoteResponseV1 } from '../src/utils/validators'; +import { + ActionTypes, + validateQuoteResponseV1, +} from '../src/validators/quote-response'; export const mockBridgeQuotesNativeErc20EthV1: QuoteResponseV1[] = [ { diff --git a/packages/bridge-controller/tests/mock-quotes-native-erc20.ts b/packages/bridge-controller/tests/mock-quotes-native-erc20.ts index 76788fc52d..f1289ec65a 100644 --- a/packages/bridge-controller/tests/mock-quotes-native-erc20.ts +++ b/packages/bridge-controller/tests/mock-quotes-native-erc20.ts @@ -1,7 +1,10 @@ import { merge } from 'lodash'; import type { QuoteResponseV1, DeepPartial } from '../src/types'; -import { ActionTypes, validateQuoteResponseV1 } from '../src/utils/validators'; +import { + ActionTypes, + validateQuoteResponseV1, +} from '../src/validators/quote-response'; export const mockBridgeQuotesNativeErc20V1: QuoteResponseV1[] = [ { diff --git a/packages/bridge-controller/tests/mock-quotes-sol-erc20.ts b/packages/bridge-controller/tests/mock-quotes-sol-erc20.ts index 14b925e7e6..6adbb08cae 100644 --- a/packages/bridge-controller/tests/mock-quotes-sol-erc20.ts +++ b/packages/bridge-controller/tests/mock-quotes-sol-erc20.ts @@ -1,7 +1,10 @@ import { merge } from 'lodash'; import type { QuoteResponseV1, DeepPartial } from '../src/types'; -import { ActionTypes, validateQuoteResponseV1 } from '../src/utils/validators'; +import { + ActionTypes, + validateQuoteResponseV1, +} from '../src/validators/quote-response'; export const mockBridgeQuotesSolErc20V1: QuoteResponseV1[] = [ { From bd1c1eebc5bc7292c6c37e19cf11532984bf2e6f Mon Sep 17 00:00:00 2001 From: micaelae Date: Wed, 3 Jun 2026 18:44:45 -0700 Subject: [PATCH 2/9] refactor: extract trade validators --- .../src/bridge-controller.sse.test.ts | 5 +- packages/bridge-controller/src/index.ts | 28 ++-- .../bridge-controller/src/selectors.test.ts | 2 +- packages/bridge-controller/src/types.ts | 17 +-- .../bridge-controller/src/utils/bridge.ts | 2 +- packages/bridge-controller/src/utils/fetch.ts | 2 +- .../src/utils/metrics/properties.ts | 4 +- .../src/utils/metrics/types.ts | 2 +- .../bridge-controller/src/utils/quote-fees.ts | 5 +- .../bridge-controller/src/utils/quote.test.ts | 2 +- packages/bridge-controller/src/utils/quote.ts | 2 +- .../src/utils/trade-utils.test.ts | 12 +- .../src/utils/trade-utils.ts | 94 +----------- .../src/validators/batch-sell.ts | 8 +- .../src/validators/quote-response.ts | 62 ++------ .../bridge-controller/src/validators/trade.ts | 140 ++++++++++++++++++ 16 files changed, 203 insertions(+), 184 deletions(-) create mode 100644 packages/bridge-controller/src/validators/trade.ts diff --git a/packages/bridge-controller/src/bridge-controller.sse.test.ts b/packages/bridge-controller/src/bridge-controller.sse.test.ts index 9f4db46703..269050ec00 100644 --- a/packages/bridge-controller/src/bridge-controller.sse.test.ts +++ b/packages/bridge-controller/src/bridge-controller.sse.test.ts @@ -30,14 +30,15 @@ import { ETH_USDT_ADDRESS, } from './constants/bridge'; import { ChainId, RequestStatus } from './types'; -import type { BridgeControllerMessenger, TxData } from './types'; +import type { BridgeControllerMessenger } from './types'; import * as balanceUtils from './utils/balance'; import { formatChainIdToDec } from './utils/caip-formatters'; import * as featureFlagUtils from './utils/feature-flags'; import * as fetchUtils from './utils/fetch'; +import { FeatureId } from './validators/feature-flags'; import { QuoteStreamCompleteReason } from './validators/quote-stream-complete'; import { TokenFeatureType } from './validators/token-feature'; -import { FeatureId } from './validators/feature-flags'; +import type { TxData } from './validators/trade'; type RootMessenger = Messenger< MockAnyNamespace, diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index 0b166aff4a..e2dca219fb 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -1,3 +1,5 @@ +import { DiscountType } from './validators/quote-response'; + export { BridgeController } from './bridge-controller'; export { @@ -56,12 +58,8 @@ export type { Quote, QuoteResponseV1 as QuoteResponse, FeeData, - TxData, Intent, IntentOrderLike, - BitcoinTradeData, - StellarTradeData, - TronTradeData, BridgeControllerState, InputPrimaryDenomination, BridgeControllerAction, @@ -100,6 +98,19 @@ export { type BridgeControllerStateChangeEvent, } from './types'; +export type { + TxData, + BitcoinTradeData, + TronTradeData, + StellarTradeData, + Trade, +} from './validators/trade'; +export { + isBitcoinTrade, + isTronTrade, + isEvmTxData, + isStellarTrade, +} from './validators/trade'; export { DiscountType, FeeType, @@ -189,14 +200,7 @@ export { formatAddressToAssetId, } from './utils/caip-formatters'; -export { - extractTradeData, - isBitcoinTrade, - isStellarTrade, - isTronTrade, - isEvmTxData, - type Trade, -} from './utils/trade-utils'; +export { extractTradeData } from './utils/trade-utils'; export { selectBridgeQuotes, diff --git a/packages/bridge-controller/src/selectors.test.ts b/packages/bridge-controller/src/selectors.test.ts index 6bd770fd16..74eeeee151 100644 --- a/packages/bridge-controller/src/selectors.test.ts +++ b/packages/bridge-controller/src/selectors.test.ts @@ -38,8 +38,8 @@ import { formatChainIdToDec, formatChainIdToHex, } from './utils/caip-formatters'; -import { validateQuoteResponseV1 } from './validators/quote-response'; import { BatchSellTransactionType } from './validators/batch-sell'; +import { validateQuoteResponseV1 } from './validators/quote-response'; const MOCK_USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; const MOCK_MUSD_ADDRESS = '0x12345A7890123456789012345678901234567890'; diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index 5aa2d54c5c..fea32471d0 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -40,21 +40,23 @@ import type { PlatformConfigSchema, } from './validators/feature-flags'; import type { - BitcoinTradeDataSchema, FeeDataSchema, IntentSchema, ProtocolSchema, QuoteResponseSchema, QuoteSchema, StepSchema, - StellarTradeDataSchema, - TronTradeDataSchema, - TxDataSchema, GaslessPropertiesSchema, TxFeeGasLimitsSchema, } from './validators/quote-response'; import type { QuoteStreamCompleteSchema } from './validators/quote-stream-complete'; import type { TokenFeatureSchema } from './validators/token-feature'; +import type { + BitcoinTradeData, + StellarTradeData, + TronTradeData, + TxData, +} from './validators/trade'; export type FetchFunction = ( input: RequestInfo | URL | string, @@ -284,16 +286,9 @@ export type FeeData = Infer; export type Quote = Infer; -export type TxData = Infer; - export type Intent = Infer; export type IntentOrderLike = Intent['order']; -export type BitcoinTradeData = Infer; - -export type TronTradeData = Infer; - -export type StellarTradeData = Infer; /** * This is the type for the quote response from the bridge-api * TxDataType can be overriden to be a string when the quote is non-evm diff --git a/packages/bridge-controller/src/utils/bridge.ts b/packages/bridge-controller/src/utils/bridge.ts index 3dd103dda1..763c58361e 100644 --- a/packages/bridge-controller/src/utils/bridge.ts +++ b/packages/bridge-controller/src/utils/bridge.ts @@ -22,9 +22,9 @@ import type { BridgeControllerState, GenericQuoteRequest, QuoteResponseV1, - TxData, } from '../types'; import { ChainId } from '../types'; +import type { TxData } from '../validators/trade'; import { formatChainIdToCaip, formatChainIdToDec, diff --git a/packages/bridge-controller/src/utils/fetch.ts b/packages/bridge-controller/src/utils/fetch.ts index 3672468ec4..59a0afee6e 100644 --- a/packages/bridge-controller/src/utils/fetch.ts +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -19,6 +19,7 @@ import type { FeatureId } from '../validators/feature-flags'; import { validateQuoteResponseV1 } from '../validators/quote-response'; import { validateQuoteStreamComplete } from '../validators/quote-stream-complete'; import { validateTokenFeature } from '../validators/token-feature'; +import { isEvmTxData } from '../validators/trade'; import { getEthUsdtResetData } from './bridge'; import { formatAddressToAssetId, @@ -26,7 +27,6 @@ import { formatChainIdToDec, } from './caip-formatters'; import { fetchServerEvents } from './fetch-server-events'; -import { isEvmTxData } from './trade-utils'; export const getClientHeaders = ({ clientId, diff --git a/packages/bridge-controller/src/utils/metrics/properties.ts b/packages/bridge-controller/src/utils/metrics/properties.ts index af3b18b6ac..bbcd641623 100644 --- a/packages/bridge-controller/src/utils/metrics/properties.ts +++ b/packages/bridge-controller/src/utils/metrics/properties.ts @@ -7,8 +7,9 @@ import type { QuoteMetadata, QuoteRequest, QuoteResponseV1, - TxData, } from '../../types'; +import { FeatureId } from '../../validators/feature-flags'; +import type { TxData } from '../../validators/trade'; import { getNativeAssetForChainId, isCrossChain } from '../bridge'; import { formatAddressToAssetId, @@ -22,7 +23,6 @@ import type { QuoteWarning, RequestParams, } from './types'; -import { FeatureId } from '../../validators/feature-flags'; export const toInputChangedPropertyKey: Partial< Record diff --git a/packages/bridge-controller/src/utils/metrics/types.ts b/packages/bridge-controller/src/utils/metrics/types.ts index 82bf1b54e8..ec4f165806 100644 --- a/packages/bridge-controller/src/utils/metrics/types.ts +++ b/packages/bridge-controller/src/utils/metrics/types.ts @@ -2,11 +2,11 @@ import type { CaipAssetType, CaipChainId } from '@metamask/utils'; import type { - FeatureId, InputPrimaryDenomination, SortOrder, StatusTypes, } from '../../types'; +import type { FeatureId } from '../../validators/feature-flags'; import type { UnifiedSwapBridgeEventName, BatchSellMetricsEventName, diff --git a/packages/bridge-controller/src/utils/quote-fees.ts b/packages/bridge-controller/src/utils/quote-fees.ts index ccd1cad5b0..0ff8bd4a77 100644 --- a/packages/bridge-controller/src/utils/quote-fees.ts +++ b/packages/bridge-controller/src/utils/quote-fees.ts @@ -7,13 +7,14 @@ import type { QuoteResponseV1, L1GasFees, NonEvmFees, - TxData, BridgeControllerMessenger, } from '../types'; +import { isTronTrade } from '../validators/trade'; +import type { TxData } from '../validators/trade'; import { isNonEvmChainId, sumHexes } from './bridge'; import { formatChainIdToCaip } from './caip-formatters'; import { computeFeeRequest } from './snaps'; -import { extractTradeData, isTronTrade } from './trade-utils'; +import { extractTradeData } from './trade-utils'; /** * Appends transaction fees for EVM chains to quotes diff --git a/packages/bridge-controller/src/utils/quote.test.ts b/packages/bridge-controller/src/utils/quote.test.ts index 5ad6c52afd..f75f838503 100644 --- a/packages/bridge-controller/src/utils/quote.test.ts +++ b/packages/bridge-controller/src/utils/quote.test.ts @@ -8,8 +8,8 @@ import type { Quote, NonEvmFees, L1GasFees, - TxData, } from '../types'; +import type { TxData } from '../validators/trade'; import { isValidQuoteRequest, getQuoteIdentifier, diff --git a/packages/bridge-controller/src/utils/quote.ts b/packages/bridge-controller/src/utils/quote.ts index f6112dce9e..2328fe0696 100644 --- a/packages/bridge-controller/src/utils/quote.ts +++ b/packages/bridge-controller/src/utils/quote.ts @@ -15,9 +15,9 @@ import type { QuoteMetadata, QuoteResponseV1, NonEvmFees, - TxData, } from '../types'; import { FeatureId } from '../validators/feature-flags'; +import type { TxData } from '../validators/trade'; import { isNativeAddress, isNonEvmChainId } from './bridge'; export const isValidQuoteRequest = ( diff --git a/packages/bridge-controller/src/utils/trade-utils.test.ts b/packages/bridge-controller/src/utils/trade-utils.test.ts index aae0def48f..f847daa13b 100644 --- a/packages/bridge-controller/src/utils/trade-utils.test.ts +++ b/packages/bridge-controller/src/utils/trade-utils.test.ts @@ -1,12 +1,16 @@ -import type { BitcoinTradeData, TronTradeData, TxData } from '../types'; +import type { + TxData, + BitcoinTradeData, + TronTradeData, + Trade, +} from '../validators/trade'; import { - extractTradeData, isEvmTxData, isBitcoinTrade, isStellarTrade, isTronTrade, -} from './trade-utils'; -import type { Trade } from './trade-utils'; +} from '../validators/trade'; +import { extractTradeData } from './trade-utils'; describe('Trade utils', () => { describe('isEvmTxData', () => { diff --git a/packages/bridge-controller/src/utils/trade-utils.ts b/packages/bridge-controller/src/utils/trade-utils.ts index e54c3c6b63..cf1aa645ef 100644 --- a/packages/bridge-controller/src/utils/trade-utils.ts +++ b/packages/bridge-controller/src/utils/trade-utils.ts @@ -1,89 +1,11 @@ -import type { - BitcoinTradeData, - StellarTradeData, - TronTradeData, - TxData, -} from '../types'; - -// Union type representing all possible trade formats (EVM, Solana, Bitcoin, Tron, Stellar) -export type Trade = - | TxData - | string - | BitcoinTradeData - | TronTradeData - | StellarTradeData; - -const hasOwnProp = (obj: object, key: PropertyKey): boolean => - Object.prototype.hasOwnProperty.call(obj, key); - -/** - * Type guard to check if a trade is an EVM TxData object - * - * @param trade - The trade object to check - * @returns True if the trade is a TxData object with data property - */ -export const isEvmTxData = (trade: Trade): trade is TxData => { - return ( - typeof trade === 'object' && - trade !== null && - hasOwnProp(trade, 'data') && - hasOwnProp(trade, 'chainId') && - hasOwnProp(trade, 'to') - ); -}; - -/** - * Type guard to check if a trade is a Bitcoin trade with unsignedPsbtBase64 - * - * @param trade - The trade object to check - * @returns True if the trade is a Bitcoin trade with unsignedPsbtBase64 property - */ -export const isBitcoinTrade = (trade: Trade): trade is BitcoinTradeData => { - return ( - typeof trade === 'object' && - trade !== null && - hasOwnProp(trade, 'unsignedPsbtBase64') - ); -}; - -/** - * Type guard to check if a trade is a Tron trade with raw_data_hex - * - * @param trade - The trade object to check - * @returns True if the trade is a Tron trade with raw_data_hex property - */ -export const isTronTrade = (trade: Trade): trade is TronTradeData => { - return ( - typeof trade === 'object' && - trade !== null && - hasOwnProp(trade, 'raw_data_hex') - ); -}; - -/** - * Type guard to check if a trade is a Stellar trade with XDR (base64) payload - * - * @param trade - The trade object to check - * @returns True if the trade is a Stellar trade with xdrBase64 or xdr property - */ -export const isStellarTrade = (trade: Trade): trade is StellarTradeData => { - if (typeof trade !== 'object' || trade === null) { - return false; - } - if ( - hasOwnProp(trade, 'xdrBase64') && - typeof (trade as { xdrBase64: unknown }).xdrBase64 === 'string' - ) { - return true; - } - if ( - hasOwnProp(trade, 'xdr') && - typeof (trade as { xdr: unknown }).xdr === 'string' - ) { - return true; - } - return false; -}; +import { + Trade, + isBitcoinTrade, + isTronTrade, + isEvmTxData, + isStellarTrade, + hasOwnProp, +} from '../validators/trade'; /** * Extracts the transaction data from different trade formats diff --git a/packages/bridge-controller/src/validators/batch-sell.ts b/packages/bridge-controller/src/validators/batch-sell.ts index 35acf98d1d..753078f8a6 100644 --- a/packages/bridge-controller/src/validators/batch-sell.ts +++ b/packages/bridge-controller/src/validators/batch-sell.ts @@ -5,17 +5,13 @@ import { enums, optional, assert, - union, } from '@metamask/superstruct'; import type { Infer } from '@metamask/superstruct'; import { StrictHexStruct } from '@metamask/utils'; import { BridgeAssetSchema } from './bridge-asset'; -import { - TxDataSchema, - NumberStringSchema, - GaslessPropertiesSchema, -} from './quote-response'; +import { NumberStringSchema, GaslessPropertiesSchema } from './quote-response'; +import { TxDataSchema } from './trade'; export enum BatchSellTransactionType { TRADE = 'trade', diff --git a/packages/bridge-controller/src/validators/quote-response.ts b/packages/bridge-controller/src/validators/quote-response.ts index 0111e2ab84..35522b232c 100644 --- a/packages/bridge-controller/src/validators/quote-response.ts +++ b/packages/bridge-controller/src/validators/quote-response.ts @@ -8,7 +8,6 @@ import { type, record, array, - nullable, optional, enums, define, @@ -16,14 +15,18 @@ import { assert, pattern, intersection, + nullable, } from '@metamask/superstruct'; -import { - HexAddressStruct, - HexChecksumAddressStruct, - StrictHexStruct, -} from '@metamask/utils'; +import { StrictHexStruct } from '@metamask/utils'; import { BridgeAssetSchema, ChainIdSchema } from './bridge-asset'; +import { + TxDataSchema, + TronTradeDataSchema, + BitcoinTradeDataSchema, + HexAddressOrChecksumAddressSchema, + StellarTradeDataSchema, +} from './trade'; export enum FeeType { METABRIDGE = 'metabridge', @@ -43,11 +46,6 @@ export enum ActionTypes { REFUEL = 'refuel', } -const HexAddressOrChecksumAddressSchema = union([ - HexAddressStruct, - HexChecksumAddressStruct, -]); - export const NumberStringSchema = define( 'NumberString', (value: unknown) => typeof value === 'string' && /^\d+$/u.test(value), @@ -283,48 +281,6 @@ export const QuoteSchema = intersection([ }), ]); -export const TxDataSchema = type({ - chainId: number(), - to: HexAddressOrChecksumAddressSchema, - from: HexAddressOrChecksumAddressSchema, - value: StrictHexStruct, - data: StrictHexStruct, - gasLimit: nullable(number()), - effectiveGas: optional(number()), -}); - -export const BitcoinTradeDataSchema = type({ - unsignedPsbtBase64: string(), - inputsToSign: nullable(array(type({}))), -}); - -export const TronTradeDataSchema = type({ - raw_data_hex: string(), - visible: optional(boolean()), - raw_data: optional( - nullable( - type({ - contract: optional( - array( - type({ - type: optional(string()), - }), - ), - ), - fee_limit: optional(number()), - }), - ), - ), -}); - -/** - * Stellar bridge quote: unsigned transaction envelope as XDR (base64). - */ -export const StellarTradeDataSchema = union([ - type({ xdrBase64: string() }), - type({ xdr: string() }), -]); - export const QuoteResponseSchema = type({ quoteId: optional(string()), quote: QuoteSchema, diff --git a/packages/bridge-controller/src/validators/trade.ts b/packages/bridge-controller/src/validators/trade.ts new file mode 100644 index 0000000000..dde71c9f17 --- /dev/null +++ b/packages/bridge-controller/src/validators/trade.ts @@ -0,0 +1,140 @@ +/* eslint-disable no-restricted-syntax */ +import { + type, + number, + nullable, + optional, + string, + array, + boolean, + define, + union, +} from '@metamask/superstruct'; +import type { Infer } from '@metamask/superstruct'; + +export const HexString = define<`0x${string}`>( + 'HexString', + (value: unknown): value is `0x${string}` => + typeof value === 'string' && /^0x[a-zA-Z0-9]*$/u.test(value), +); + +export const TxDataSchema = type({ + chainId: number(), + to: HexString, + from: HexString, + value: HexString, + data: HexString, + gasLimit: nullable(number()), + effectiveGas: optional(number()), +}); + +export const BitcoinTradeDataSchema = type({ + unsignedPsbtBase64: string(), + inputsToSign: nullable(array(type({}))), +}); + +export const TronTradeDataSchema = type({ + raw_data_hex: string(), + visible: optional(boolean()), + raw_data: optional( + nullable( + type({ + contract: optional( + array( + type({ + type: optional(string()), + }), + ), + ), + fee_limit: optional(number()), + }), + ), + ), +}); // Union type representing all possible trade formats (EVM, Solana, Bitcoin, Tron) + +/** + * Stellar bridge quote: unsigned transaction envelope as XDR (base64). + */ +export const StellarTradeDataSchema = union([ + type({ xdrBase64: string() }), + type({ xdr: string() }), +]); + +export type Trade = + | TxData + | string + | BitcoinTradeData + | TronTradeData + | StellarTradeData; +/** + * Type guard to check if a trade is an EVM TxData object + * + * @param trade - The trade object to check + * @returns True if the trade is a TxData object with data property + */ + +export const isEvmTxData = (trade: Trade): trade is TxData => { + return ( + typeof trade === 'object' && + trade !== null && + 'data' in trade && + 'chainId' in trade && + 'to' in trade + ); +}; +/** + * Type guard to check if a trade is a Bitcoin trade with unsignedPsbtBase64 + * + * @param trade - The trade object to check + * @returns True if the trade is a Bitcoin trade with unsignedPsbtBase64 property + */ + +export const isBitcoinTrade = (trade: Trade): trade is BitcoinTradeData => { + return ( + typeof trade === 'object' && trade !== null && 'unsignedPsbtBase64' in trade + ); +}; +/** + * Type guard to check if a trade is a Tron trade with raw_data_hex + * + * @param trade - The trade object to check + * @returns True if the trade is a Tron trade with raw_data_hex property + */ + +export const isTronTrade = (trade: Trade): trade is TronTradeData => { + return typeof trade === 'object' && trade !== null && 'raw_data_hex' in trade; +}; + +export const hasOwnProp = (obj: object, key: PropertyKey): boolean => + Object.prototype.hasOwnProperty.call(obj, key); + +/** + * Type guard to check if a trade is a Stellar trade with XDR (base64) payload + * + * @param trade - The trade object to check + * @returns True if the trade is a Stellar trade with xdrBase64 or xdr property + */ +export const isStellarTrade = (trade: Trade): trade is StellarTradeData => { + if (typeof trade !== 'object' || trade === null) { + return false; + } + if ( + hasOwnProp(trade, 'xdrBase64') && + typeof (trade as { xdrBase64: unknown }).xdrBase64 === 'string' + ) { + return true; + } + if ( + hasOwnProp(trade, 'xdr') && + typeof (trade as { xdr: unknown }).xdr === 'string' + ) { + return true; + } + return false; +}; + +export type BitcoinTradeData = Infer; + +export type TronTradeData = Infer; +export type TxData = Infer; +export type StellarTradeData = Infer; From 86125be3b0b5141c74fa6999523992b7dab69390 Mon Sep 17 00:00:00 2001 From: micaelae Date: Tue, 7 Jul 2026 08:56:32 -0700 Subject: [PATCH 3/9] chore: rename files etc --- .../src/bridge-controller.sse.test.ts | 1 + .../src/bridge-controller.test.ts | 7 +- .../src/bridge-controller.ts | 2 +- packages/bridge-controller/src/index.ts | 14 +- .../bridge-controller/src/selectors.test.ts | 4 +- packages/bridge-controller/src/selectors.ts | 2 +- packages/bridge-controller/src/types.ts | 69 +--- .../bridge-controller/src/utils/bridge.ts | 3 +- packages/bridge-controller/src/utils/fetch.ts | 4 +- .../src/utils/metrics/properties.test.ts | 3 +- .../src/utils/metrics/properties.ts | 3 +- .../bridge-controller/src/utils/quote-fees.ts | 2 +- .../bridge-controller/src/utils/quote.test.ts | 22 +- packages/bridge-controller/src/utils/quote.ts | 6 +- .../src/utils/struct-error.ts | 21 ++ .../src/validators/batch-sell.ts | 3 +- .../src/validators/feature-flags.ts | 8 +- .../src/validators/intent.ts | 152 +++++++++ .../src/validators/number.ts | 13 + .../src/validators/quote-response-v1.ts | 96 ++++++ .../src/validators/quote-response.ts | 303 ------------------ .../bridge-controller/src/validators/quote.ts | 113 +++++++ .../bridge-controller/src/validators/step.ts | 17 + .../src/validators/validators.test.ts | 10 +- .../tests/mock-quotes-erc20-erc20.ts | 25 +- .../tests/mock-quotes-erc20-native.ts | 9 +- .../tests/mock-quotes-native-erc20-eth.ts | 45 +-- .../tests/mock-quotes-native-erc20.ts | 63 +--- .../tests/mock-quotes-sol-erc20.ts | 27 +- packages/bridge-controller/tests/mock-sse.ts | 26 +- 30 files changed, 499 insertions(+), 574 deletions(-) create mode 100644 packages/bridge-controller/src/utils/struct-error.ts create mode 100644 packages/bridge-controller/src/validators/intent.ts create mode 100644 packages/bridge-controller/src/validators/number.ts create mode 100644 packages/bridge-controller/src/validators/quote-response-v1.ts delete mode 100644 packages/bridge-controller/src/validators/quote-response.ts create mode 100644 packages/bridge-controller/src/validators/quote.ts create mode 100644 packages/bridge-controller/src/validators/step.ts diff --git a/packages/bridge-controller/src/bridge-controller.sse.test.ts b/packages/bridge-controller/src/bridge-controller.sse.test.ts index 269050ec00..5267223fc7 100644 --- a/packages/bridge-controller/src/bridge-controller.sse.test.ts +++ b/packages/bridge-controller/src/bridge-controller.sse.test.ts @@ -36,6 +36,7 @@ import { formatChainIdToDec } from './utils/caip-formatters'; import * as featureFlagUtils from './utils/feature-flags'; import * as fetchUtils from './utils/fetch'; import { FeatureId } from './validators/feature-flags'; +import { validateQuoteResponseV1 } from './validators/quote-response-v1'; import { QuoteStreamCompleteReason } from './validators/quote-stream-complete'; import { TokenFeatureType } from './validators/token-feature'; import type { TxData } from './validators/trade'; diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index dbecaf1f8f..04eda0c7b8 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -33,11 +33,7 @@ import { import { SWAPS_API_V2_BASE_URL } from './constants/swaps'; import * as selectors from './selectors'; import { ChainId, RequestStatus, SortOrder, StatusTypes } from './types'; -import type { - BridgeControllerMessenger, - QuoteResponseV1, - GenericQuoteRequest, -} from './types'; +import type { BridgeControllerMessenger, GenericQuoteRequest } from './types'; import * as balanceUtils from './utils/balance'; import { getNativeAssetForChainId, isSolanaChainId } from './utils/bridge'; import { @@ -56,6 +52,7 @@ import { UnifiedSwapBridgeEventName, } from './utils/metrics/constants'; import { FeatureId } from './validators/feature-flags'; +import type { QuoteResponseV1 } from './validators/quote-response-v1'; const EMPTY_INIT_STATE = DEFAULT_BRIDGE_CONTROLLER_STATE; diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index f0e3805ca1..8e81ad7681 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -31,7 +31,6 @@ import type { GenericQuoteRequest, NonEvmFees, QuoteRequest, - QuoteResponseV1, BridgeControllerState, BridgeControllerMessenger, FetchFunction, @@ -95,6 +94,7 @@ import { import { appendFeesToQuotes } from './utils/quote-fees'; import { getMinimumBalanceForRentExemptionInLamports } from './utils/snaps'; import type { FeatureId } from './validators/feature-flags'; +import type { QuoteResponseV1 } from './validators/quote-response-v1'; const metadata: StateMetadata = { quoteRequest: { diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index e2dca219fb..92ef13c990 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -1,5 +1,3 @@ -import { DiscountType } from './validators/quote-response'; - export { BridgeController } from './bridge-controller'; export { @@ -48,15 +46,12 @@ export type { FeatureFlagResponse, BridgeAsset, GenericQuoteRequest, - Protocol, BatchSellTradesResponse, GaslessProperties, SimulatedGasFeeLimits, TokenAmountValues, Step, RefuelData, - Quote, - QuoteResponseV1 as QuoteResponse, FeeData, Intent, IntentOrderLike, @@ -111,11 +106,10 @@ export { isEvmTxData, isStellarTrade, } from './validators/trade'; -export { - DiscountType, - FeeType, - ActionTypes, -} from './validators/quote-response'; +export type { QuoteResponseV1 as QuoteResponse } from './validators/quote-response-v1'; +export type { Quote } from './validators/quote'; +export type { DiscountType, FeeType } from './validators/quote'; +export type { ActionTypes } from './validators/step'; export { validateQuoteStreamComplete, QuoteStreamCompleteReason, diff --git a/packages/bridge-controller/src/selectors.test.ts b/packages/bridge-controller/src/selectors.test.ts index 74eeeee151..03f12996eb 100644 --- a/packages/bridge-controller/src/selectors.test.ts +++ b/packages/bridge-controller/src/selectors.test.ts @@ -28,7 +28,6 @@ import { ChainId, BridgeAsset, NonEvmFees, - QuoteResponseV1, } from './types'; import { getNativeAssetForChainId, isNativeAddress } from './utils/bridge'; import { @@ -39,7 +38,8 @@ import { formatChainIdToHex, } from './utils/caip-formatters'; import { BatchSellTransactionType } from './validators/batch-sell'; -import { validateQuoteResponseV1 } from './validators/quote-response'; +import type { QuoteResponseV1 } from './validators/quote-response-v1'; +import { validateQuoteResponseV1 } from './validators/quote-response-v1'; const MOCK_USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; const MOCK_MUSD_ADDRESS = '0x12345A7890123456789012345678901234567890'; diff --git a/packages/bridge-controller/src/selectors.ts b/packages/bridge-controller/src/selectors.ts index ad04ff3796..a9246b7bee 100644 --- a/packages/bridge-controller/src/selectors.ts +++ b/packages/bridge-controller/src/selectors.ts @@ -23,7 +23,6 @@ import type { BridgeControllerState, ExchangeRate, QuoteMetadata, - QuoteResponseV1, TokenAmountValues, } from './types'; import { RequestStatus, SortOrder } from './types'; @@ -55,6 +54,7 @@ import { calcBatchFees, } from './utils/quote'; import { getDefaultSlippagePercentage } from './utils/slippage'; +import type { QuoteResponseV1 } from './validators/quote-response-v1'; /** * The controller states that provide exchange rates diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index fea32471d0..2951078a45 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -33,30 +33,19 @@ import type { BRIDGE_CONTROLLER_NAME } from './constants/bridge'; import type { SimulatedGasFeeLimitsSchema } from './validators/batch-sell'; import type { BatchSellTradesResponseSchema } from './validators/batch-sell'; import type { BridgeAssetSchema } from './validators/bridge-asset'; -import type { FeatureId } from './validators/feature-flags'; import type { ChainConfigurationSchema, ChainRankingSchema, PlatformConfigSchema, } from './validators/feature-flags'; -import type { - FeeDataSchema, - IntentSchema, - ProtocolSchema, - QuoteResponseSchema, - QuoteSchema, - StepSchema, - GaslessPropertiesSchema, - TxFeeGasLimitsSchema, -} from './validators/quote-response'; +import type { IntentSchema } from './validators/intent'; +import type { TxFeeGasLimitsSchema } from './validators/quote'; +import type { FeeDataSchema } from './validators/quote'; +import type { GaslessPropertiesSchema } from './validators/quote'; +import type { QuoteResponseV1 } from './validators/quote-response-v1'; import type { QuoteStreamCompleteSchema } from './validators/quote-stream-complete'; +import type { StepSchema } from './validators/step'; import type { TokenFeatureSchema } from './validators/token-feature'; -import type { - BitcoinTradeData, - StellarTradeData, - TronTradeData, - TxData, -} from './validators/trade'; export type FetchFunction = ( input: RequestInfo | URL | string, @@ -85,10 +74,16 @@ export type ChainConfiguration = Infer; export type ChainRanking = Infer; +/** + * @deprecated Avoid introducing new usages and use the QuoteResponseV2 feeData.network value instead + */ export type L1GasFees = { - l1GasFeesInHexWei?: string; // l1 fees for approval and trade in hex wei, appended by BridgeController.#appendL1GasFees + l1GasFeesInHexWei?: Hex; // l1 fees for approval and trade in hex wei, appended by BridgeController.#appendL1GasFees }; +/** + * @deprecated Avoid introducing new usages and use the QuoteResponseV2 feeData.network value instead + */ export type NonEvmFees = { nonEvmFeesInNative?: string; // Non-EVM chain fees in native units (SOL for Solana, BTC for Bitcoin) }; @@ -126,6 +121,8 @@ export type ExchangeRate = { exchangeRate?: string; usdExchangeRate?: string }; /** * Values derived from the quote response + * + * @deprecated Avoid introducing new usages and use the QuoteResponse V2 type instead */ export type QuoteMetadata = { /** @@ -276,51 +273,15 @@ export type GenericQuoteRequest = QuoteRequest< Hex | CaipAccountId | string // accountIds/addresses >; -export type Protocol = Infer; - export type Step = Infer; export type RefuelData = Step; export type FeeData = Infer; -export type Quote = Infer; - export type Intent = Infer; export type IntentOrderLike = Intent['order']; -/** - * This is the type for the quote response from the bridge-api - * TxDataType can be overriden to be a string when the quote is non-evm - * ApprovalType can be overriden when you know the specific approval type (e.g., TxData for EVM-only contexts) - */ -export type QuoteResponseV1< - TxDataType = - | TxData - | string - | BitcoinTradeData - | TronTradeData - | StellarTradeData, - ApprovalType = TxData | TronTradeData, -> = Infer & { - trade: TxDataType; - approval?: ApprovalType; - /** - * Appended to the quote response based on the quote request - */ - featureId?: FeatureId; - /** - * Appended to the quote response based on the quote request resetApproval flag - * If defined, the quote's total network fee will include the reset approval's gas limit. - */ - resetApproval?: TxData; - /** - * Appended to the quote if there are multiple quote requests in a batch. This - * indicates which quoteRequest the quote is for - */ - quoteRequestIndex?: number; -}; - export type BatchSellTradesRequest = { quotes: QuoteResponseV1[]; stxEnabled: boolean; diff --git a/packages/bridge-controller/src/utils/bridge.ts b/packages/bridge-controller/src/utils/bridge.ts index 763c58361e..a51bf2d4ef 100644 --- a/packages/bridge-controller/src/utils/bridge.ts +++ b/packages/bridge-controller/src/utils/bridge.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ import { AddressZero } from '@ethersproject/constants'; import { Contract } from '@ethersproject/contracts'; import { BtcScope, SolScope, TrxScope, XlmScope } from '@metamask/keyring-api'; @@ -21,9 +22,9 @@ import type { BridgeAsset, BridgeControllerState, GenericQuoteRequest, - QuoteResponseV1, } from '../types'; import { ChainId } from '../types'; +import type { QuoteResponseV1 } from '../validators/quote-response-v1'; import type { TxData } from '../validators/trade'; import { formatChainIdToCaip, diff --git a/packages/bridge-controller/src/utils/fetch.ts b/packages/bridge-controller/src/utils/fetch.ts index 59a0afee6e..3453d84112 100644 --- a/packages/bridge-controller/src/utils/fetch.ts +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -3,7 +3,6 @@ import { StructError } from '@metamask/superstruct'; import type { CaipAssetType, CaipChainId, Hex } from '@metamask/utils'; import type { - QuoteResponseV1, FetchFunction, GenericQuoteRequest, QuoteRequest, @@ -16,7 +15,8 @@ import type { import { validateBatchSellTradesResponse } from '../validators/batch-sell'; import { validateBridgeAsset } from '../validators/bridge-asset'; import type { FeatureId } from '../validators/feature-flags'; -import { validateQuoteResponseV1 } from '../validators/quote-response'; +import type { QuoteResponseV1 } from '../validators/quote-response-v1'; +import { validateQuoteResponseV1 } from '../validators/quote-response-v1'; import { validateQuoteStreamComplete } from '../validators/quote-stream-complete'; import { validateTokenFeature } from '../validators/token-feature'; import { isEvmTxData } from '../validators/trade'; diff --git a/packages/bridge-controller/src/utils/metrics/properties.test.ts b/packages/bridge-controller/src/utils/metrics/properties.test.ts index fbbf6e5007..7c46242b87 100644 --- a/packages/bridge-controller/src/utils/metrics/properties.test.ts +++ b/packages/bridge-controller/src/utils/metrics/properties.test.ts @@ -1,7 +1,8 @@ import { SolScope } from '@metamask/keyring-api'; import type { CaipChainId } from '@metamask/utils'; -import type { QuoteMetadata, QuoteResponseV1 } from '../../types'; +import type { QuoteMetadata } from '../../types'; +import type { QuoteResponseV1 } from '../../validators/quote-response-v1'; import { getNativeAssetForChainId } from '../bridge'; import { formatChainIdToCaip } from '../caip-formatters'; import { MetricsSwapType } from './constants'; diff --git a/packages/bridge-controller/src/utils/metrics/properties.ts b/packages/bridge-controller/src/utils/metrics/properties.ts index bbcd641623..9c0b45989a 100644 --- a/packages/bridge-controller/src/utils/metrics/properties.ts +++ b/packages/bridge-controller/src/utils/metrics/properties.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ import type { AccountsControllerState } from '@metamask/accounts-controller'; import { DEFAULT_BRIDGE_CONTROLLER_STATE } from '../../constants/bridge'; @@ -6,9 +7,9 @@ import type { GenericQuoteRequest, QuoteMetadata, QuoteRequest, - QuoteResponseV1, } from '../../types'; import { FeatureId } from '../../validators/feature-flags'; +import type { QuoteResponseV1 } from '../../validators/quote-response-v1'; import type { TxData } from '../../validators/trade'; import { getNativeAssetForChainId, isCrossChain } from '../bridge'; import { diff --git a/packages/bridge-controller/src/utils/quote-fees.ts b/packages/bridge-controller/src/utils/quote-fees.ts index 0ff8bd4a77..a6f639655c 100644 --- a/packages/bridge-controller/src/utils/quote-fees.ts +++ b/packages/bridge-controller/src/utils/quote-fees.ts @@ -4,11 +4,11 @@ import { numberToHex } from '@metamask/utils'; import { CHAIN_IDS } from '../constants/chains'; import type { - QuoteResponseV1, L1GasFees, NonEvmFees, BridgeControllerMessenger, } from '../types'; +import type { QuoteResponseV1 } from '../validators/quote-response-v1'; import { isTronTrade } from '../validators/trade'; import type { TxData } from '../validators/trade'; import { isNonEvmChainId, sumHexes } from './bridge'; diff --git a/packages/bridge-controller/src/utils/quote.test.ts b/packages/bridge-controller/src/utils/quote.test.ts index f75f838503..4113818c89 100644 --- a/packages/bridge-controller/src/utils/quote.test.ts +++ b/packages/bridge-controller/src/utils/quote.test.ts @@ -2,13 +2,9 @@ import { AddressZero } from '@ethersproject/constants'; import { convertHexToDecimal } from '@metamask/controller-utils'; import { BigNumber } from 'bignumber.js'; -import type { - GenericQuoteRequest, - QuoteResponseV1, - Quote, - NonEvmFees, - L1GasFees, -} from '../types'; +import type { GenericQuoteRequest, NonEvmFees, L1GasFees } from '../types'; +import type { Quote } from '../validators/quote'; +import type { QuoteResponseV1 } from '../validators/quote-response-v1'; import type { TxData } from '../validators/trade'; import { isValidQuoteRequest, @@ -389,7 +385,7 @@ describe('Quote Metadata Utils', () => { feeData: { metabridge: { amount: '100000000000000000' } }, }, trade: { value: '0x10A741A462780000' }, - } as QuoteResponseV1; + } as unknown as QuoteResponseV1; it('should calculate relayer fee correctly with exchange rates', () => { const result = calcRelayerFee(mockBridgeQuote, { @@ -464,7 +460,7 @@ describe('Quote Metadata Utils', () => { trade: { gasLimit: 21000 }, approval: { gasLimit: 46000 }, l1GasFeesInHexWei: '0x5AF3107A4000', - } as QuoteResponseV1 & L1GasFees; + } as unknown as QuoteResponseV1 & L1GasFees; it('should calculate estimated and max gas fees correctly', () => { const result = calcEstimatedAndMaxTotalGasFee({ @@ -588,7 +584,7 @@ describe('Quote Metadata Utils', () => { approval: { gasLimit: 0 }, l1GasFeesInHexWei: '0x0', estimatedProcessingTimeInSeconds: 60, - } as QuoteResponseV1 & L1GasFees; + } as unknown as QuoteResponseV1 & L1GasFees; const result = calcEstimatedAndMaxTotalGasFee({ bridgeQuote: zeroGasQuote, @@ -607,7 +603,7 @@ describe('Quote Metadata Utils', () => { it('should handle missing approval', () => { const noApprovalQuote = { quote: {} as Quote, - trade: { gasLimit: 21000 }, + trade: { gasLimit: 21000 } as TxData, approval: undefined, l1GasFeesInHexWei: '0x5AF3107A4000', estimatedProcessingTimeInSeconds: 60, @@ -652,8 +648,8 @@ describe('Quote Metadata Utils', () => { it('should handle large gas limits and fees', () => { const largeGasQuote = { quote: {} as Quote, - trade: { gasLimit: 1000000 }, - approval: { gasLimit: 500000 }, + trade: { gasLimit: 1000000 } as TxData, + approval: { gasLimit: 500000 } as TxData, l1GasFeesInHexWei: '0x1BC16D674EC80000', // 2 ETH in wei estimatedProcessingTimeInSeconds: 60, } as QuoteResponseV1 & L1GasFees; diff --git a/packages/bridge-controller/src/utils/quote.ts b/packages/bridge-controller/src/utils/quote.ts index 2328fe0696..fec1a84538 100644 --- a/packages/bridge-controller/src/utils/quote.ts +++ b/packages/bridge-controller/src/utils/quote.ts @@ -11,13 +11,13 @@ import type { ExchangeRate, GenericQuoteRequest, L1GasFees, - Quote, QuoteMetadata, - QuoteResponseV1, NonEvmFees, } from '../types'; import { FeatureId } from '../validators/feature-flags'; -import type { TxData } from '../validators/trade'; +import type { Quote } from '../validators/quote'; +import type { QuoteResponseV1 } from '../validators/quote-response-v1'; +import { TxData } from '../validators/trade'; import { isNativeAddress, isNonEvmChainId } from './bridge'; export const isValidQuoteRequest = ( diff --git a/packages/bridge-controller/src/utils/struct-error.ts b/packages/bridge-controller/src/utils/struct-error.ts new file mode 100644 index 0000000000..420de17e35 --- /dev/null +++ b/packages/bridge-controller/src/utils/struct-error.ts @@ -0,0 +1,21 @@ +import type { StructError } from '@metamask/superstruct'; + +/** + * Formats validation errors (StructError) into an array of messages + * that match the format used for metrics + * + * @param error - The validation errors (StructError) to format + * + * @returns An array of error messages + */ +export const formatStructErrors = (error: StructError): string[] => + Array.from( + new Set( + error + .failures() + .map( + ({ message, path }) => + `At path: ${path.join('.') || ''}${error.type ? ` (${error.type})` : ''} -- ${message}`, + ), + ), + ); diff --git a/packages/bridge-controller/src/validators/batch-sell.ts b/packages/bridge-controller/src/validators/batch-sell.ts index 753078f8a6..79246d08b8 100644 --- a/packages/bridge-controller/src/validators/batch-sell.ts +++ b/packages/bridge-controller/src/validators/batch-sell.ts @@ -10,7 +10,8 @@ import type { Infer } from '@metamask/superstruct'; import { StrictHexStruct } from '@metamask/utils'; import { BridgeAssetSchema } from './bridge-asset'; -import { NumberStringSchema, GaslessPropertiesSchema } from './quote-response'; +import { NumberStringSchema } from './number'; +import { GaslessPropertiesSchema } from './quote'; import { TxDataSchema } from './trade'; export enum BatchSellTransactionType { diff --git a/packages/bridge-controller/src/validators/feature-flags.ts b/packages/bridge-controller/src/validators/feature-flags.ts index 1aeb84965c..766ecb00d7 100644 --- a/packages/bridge-controller/src/validators/feature-flags.ts +++ b/packages/bridge-controller/src/validators/feature-flags.ts @@ -7,7 +7,6 @@ import { array, boolean, number, - enums, is, define, } from '@metamask/superstruct'; @@ -74,7 +73,6 @@ export const ChainConfigurationSchema = type({ }); export const PriceImpactThresholdSchema = type({ - // TODO: // We are moving into a unified approach where // price impact thresholds will be segmented by // importance rather than transaction type. @@ -89,20 +87,20 @@ export const PriceImpactThresholdSchema = type({ warning: optional(number()), // Percentage value in decimal format error: optional(number()), // Percentage value in decimal format }); + const GenericQuoteRequestSchema = type({ aggIds: optional(array(string())), bridgeIds: optional(array(string())), fee: optional(number()), }); -const FeatureIdSchema = enums(Object.values(FeatureId)); + /** * This is the schema for the feature flags response from the RemoteFeatureFlagController */ - export const PlatformConfigSchema = type({ priceImpactThreshold: optional(PriceImpactThresholdSchema), quoteRequestOverrides: optional( - record(FeatureIdSchema, optional(GenericQuoteRequestSchema)), + record(string(), optional(GenericQuoteRequestSchema)), ), minimumVersion: string(), refreshRate: number(), diff --git a/packages/bridge-controller/src/validators/intent.ts b/packages/bridge-controller/src/validators/intent.ts new file mode 100644 index 0000000000..ada780dcb8 --- /dev/null +++ b/packages/bridge-controller/src/validators/intent.ts @@ -0,0 +1,152 @@ +import { + union, + number, + string, + type, + optional, + enums, + boolean, + record, + array, + any, +} from '@metamask/superstruct'; + +import { TruthyDigitStringSchema } from './number'; +import { HexString } from './trade'; + +// Allow digit strings for amounts/validTo for flexibility across providers +const DigitStringOrNumberSchema = union([TruthyDigitStringSchema, number()]); +/** + * Identifier of the intent protocol used for order creation and submission. + * + * Examples: + * - CoW Swap + * - Other EIP-712–based intent protocols + */ +const IntentProtocolSchema = string(); +/** + * Schema for an intent-based order used for EIP-712 signing and submission. + * + * This represents the minimal subset of fields required by intent-based + * protocols (e.g. CoW Swap) to build, sign, and submit an order. + */ + +export const IntentOrderSchema = type({ + /** + * Address of the token being sold. + */ + sellToken: HexString, + + /** + * Address of the token being bought. + */ + buyToken: HexString, + + /** + * Optional receiver of the bought tokens. + * If omitted, defaults to the signer / order owner. + */ + receiver: optional(HexString), + + /** + * Order expiration time. + * + * Can be provided as a UNIX timestamp in seconds, either as a number + * or as a digit string, depending on provider requirements. + */ + validTo: DigitStringOrNumberSchema, + + /** + * Arbitrary application-specific data attached to the order. + */ + appData: string(), + + /** + * Hash of the `appData` field, used for EIP-712 signing. + */ + appDataHash: HexString, + + /** + * Fee amount paid for order execution, expressed as a digit string. + */ + feeAmount: TruthyDigitStringSchema, + + /** + * Order kind. + * + * - `sell`: exact sell amount, variable buy amount + * - `buy`: exact buy amount, variable sell amount + */ + kind: enums(['sell', 'buy']), + + /** + * Whether the order can be partially filled. + */ + partiallyFillable: boolean(), + + /** + * Exact amount of the sell token. + * + * Required for `sell` orders. + */ + sellAmount: optional(TruthyDigitStringSchema), + + /** + * Exact amount of the buy token. + * + * Required for `buy` orders. + */ + buyAmount: optional(TruthyDigitStringSchema), + + /** + * Optional order owner / sender address. + * + * Provided for convenience when building the EIP-712 domain and message. + */ + from: optional(HexString), +}); +/** + * Schema representing an intent submission payload. + * + * Wraps the intent order along with protocol and optional routing metadata + * required by the backend or relayer infrastructure. + */ + +export const IntentSchema = type({ + /** + * Identifier of the intent protocol used to interpret the order. + */ + protocol: IntentProtocolSchema, + + /** + * The intent order to be signed and submitted. + */ + order: IntentOrderSchema, + + /** + * Optional settlement contract address used for execution. + */ + settlementContract: optional(HexString), + + /** + * Optional EIP-712 typed data payload for signing. + * Must be JSON-serializable and include required EIP-712 fields. + */ + typedData: type({ + // Keep values as `any()` here. Using `unknown()` in this record causes + // TS2321/TS2589 (excessive type instantiation depth) in bridge state + // inference during build. + types: record( + string(), + array( + type({ + name: string(), + type: string(), + }), + ), + ), + primaryType: string(), + domain: record(string(), any()), + message: record(string(), any()), + }), +}); diff --git a/packages/bridge-controller/src/validators/number.ts b/packages/bridge-controller/src/validators/number.ts new file mode 100644 index 0000000000..db9f45b059 --- /dev/null +++ b/packages/bridge-controller/src/validators/number.ts @@ -0,0 +1,13 @@ +import { define, pattern, string } from '@metamask/superstruct'; + +export const NumberStringSchema = define( + 'NumberString', + (value: unknown) => typeof value === 'string' && /^\d+$/u.test(value), +); + +export const TruthyDigitStringSchema = pattern(string(), /^\d+$/u); + +export const FloatStringSchema = define( + 'FloatString', + (value: unknown) => typeof value === 'string' && /^-*\d*\.*\d+$/u.test(value), +); diff --git a/packages/bridge-controller/src/validators/quote-response-v1.ts b/packages/bridge-controller/src/validators/quote-response-v1.ts new file mode 100644 index 0000000000..ea1b328dcf --- /dev/null +++ b/packages/bridge-controller/src/validators/quote-response-v1.ts @@ -0,0 +1,96 @@ +import type { Infer } from '@metamask/superstruct'; +import { + string, + number, + type, + optional, + enums, + union, + assert, + StructError, +} from '@metamask/superstruct'; +import { StrictHexStruct } from '@metamask/utils'; + +import { formatStructErrors } from '../utils/struct-error'; +import { FeatureId } from './feature-flags'; +import { FloatStringSchema } from './number'; +import { QuoteSchema } from './quote'; +import { + TxDataSchema, + TronTradeDataSchema, + BitcoinTradeDataSchema, + StellarTradeDataSchema, +} from './trade'; +import type { + BitcoinTradeData, + StellarTradeData, + TronTradeData, + TxData, +} from './trade'; + +export const QuoteResponseSchemaV1 = type({ + featureId: optional(enums(Object.values(FeatureId))), + quoteId: optional(string()), + quote: QuoteSchema, + estimatedProcessingTimeInSeconds: number(), + approval: optional(union([TxDataSchema, TronTradeDataSchema])), + trade: union([ + TxDataSchema, + BitcoinTradeDataSchema, + TronTradeDataSchema, + StellarTradeDataSchema, + string(), + ]), + l1GasFeesInHexWei: optional(StrictHexStruct), + nonEvmFeesInNative: optional(FloatStringSchema), +}); + +export const validateQuoteResponseV1 = ( + data: unknown, +): data is Infer => { + try { + assert(data, QuoteResponseSchemaV1); + return true; + } catch (error) { + if (error instanceof StructError) { + console.warn('Quote validation failed', formatStructErrors(error)); + } else { + console.warn(JSON.stringify(error, null, 2)); + } + throw error; + } +}; + +/** + * This is the type for the quote response from the bridge-api + * TxDataType can be overriden to be a string when the quote is non-evm + * ApprovalType can be overriden when you know the specific approval type (e.g., TxData for EVM-only contexts) + * + * @deprecated Use `QuoteResponseV2` instead + */ +export type QuoteResponseV1< + TxDataType = + | TxData + | string + | BitcoinTradeData + | TronTradeData + | StellarTradeData, + ApprovalType = TxData | TronTradeData, +> = Infer & { + trade: TxDataType; + approval?: ApprovalType; + /** + * Appended to the quote response based on the quote request + */ + featureId?: FeatureId; + /** + * Appended to the quote response based on the quote request resetApproval flag + * If defined, the quote's total network fee will include the reset approval's gas limit. + */ + resetApproval?: TxData; + /** + * Appended to the quote if there are multiple quote requests in a batch. This + * indicates which quoteRequest the quote is for + */ + quoteRequestIndex?: number; +}; diff --git a/packages/bridge-controller/src/validators/quote-response.ts b/packages/bridge-controller/src/validators/quote-response.ts deleted file mode 100644 index 35522b232c..0000000000 --- a/packages/bridge-controller/src/validators/quote-response.ts +++ /dev/null @@ -1,303 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import type { Infer } from '@metamask/superstruct'; -import { - any, - string, - boolean, - number, - type, - record, - array, - optional, - enums, - define, - union, - assert, - pattern, - intersection, - nullable, -} from '@metamask/superstruct'; -import { StrictHexStruct } from '@metamask/utils'; - -import { BridgeAssetSchema, ChainIdSchema } from './bridge-asset'; -import { - TxDataSchema, - TronTradeDataSchema, - BitcoinTradeDataSchema, - HexAddressOrChecksumAddressSchema, - StellarTradeDataSchema, -} from './trade'; - -export enum FeeType { - METABRIDGE = 'metabridge', - REFUEL = 'refuel', - TX_FEE = 'txFee', -} - -export enum DiscountType { - VIP = 'vip', - PROMO = 'promo', - DAO = 'dao', -} - -export enum ActionTypes { - BRIDGE = 'bridge', - SWAP = 'swap', - REFUEL = 'refuel', -} - -export const NumberStringSchema = define( - 'NumberString', - (value: unknown) => typeof value === 'string' && /^\d+$/u.test(value), -); - -export const truthyString = (value: string): boolean => Boolean(value?.length); -const TruthyDigitStringSchema = pattern(string(), /^\d+$/u); - -export const FeeDataSchema = type({ - amount: TruthyDigitStringSchema, - asset: BridgeAssetSchema, - discountType: optional(nullable(string())), -}); - -export const ProtocolSchema = type({ - name: string(), - displayName: optional(string()), - icon: optional(string()), -}); - -export const StepSchema = type({ - action: enums(Object.values(ActionTypes)), - srcChainId: ChainIdSchema, - destChainId: optional(ChainIdSchema), - srcAsset: BridgeAssetSchema, - destAsset: BridgeAssetSchema, - srcAmount: string(), - destAmount: string(), - protocol: ProtocolSchema, -}); - -const RefuelDataSchema = StepSchema; - -// Allow digit strings for amounts/validTo for flexibility across providers -const DigitStringOrNumberSchema = union([TruthyDigitStringSchema, number()]); - -/** - * Identifier of the intent protocol used for order creation and submission. - * - * Examples: - * - CoW Swap - * - Other EIP-712–based intent protocols - */ -const IntentProtocolSchema = string(); - -/** - * Schema for an intent-based order used for EIP-712 signing and submission. - * - * This represents the minimal subset of fields required by intent-based - * protocols (e.g. CoW Swap) to build, sign, and submit an order. - */ -export const IntentOrderSchema = type({ - /** - * Address of the token being sold. - */ - sellToken: HexAddressOrChecksumAddressSchema, - - /** - * Address of the token being bought. - */ - buyToken: HexAddressOrChecksumAddressSchema, - - /** - * Optional receiver of the bought tokens. - * If omitted, defaults to the signer / order owner. - */ - receiver: optional(HexAddressOrChecksumAddressSchema), - - /** - * Order expiration time. - * - * Can be provided as a UNIX timestamp in seconds, either as a number - * or as a digit string, depending on provider requirements. - */ - validTo: DigitStringOrNumberSchema, - - /** - * Arbitrary application-specific data attached to the order. - */ - appData: string(), - - /** - * Hash of the `appData` field, used for EIP-712 signing. - */ - appDataHash: StrictHexStruct, - - /** - * Fee amount paid for order execution, expressed as a digit string. - */ - feeAmount: TruthyDigitStringSchema, - - /** - * Order kind. - * - * - `sell`: exact sell amount, variable buy amount - * - `buy`: exact buy amount, variable sell amount - */ - kind: enums(['sell', 'buy']), - - /** - * Whether the order can be partially filled. - */ - partiallyFillable: boolean(), - - /** - * Exact amount of the sell token. - * - * Required for `sell` orders. - */ - sellAmount: optional(TruthyDigitStringSchema), - - /** - * Exact amount of the buy token. - * - * Required for `buy` orders. - */ - buyAmount: optional(TruthyDigitStringSchema), - - /** - * Optional order owner / sender address. - * - * Provided for convenience when building the EIP-712 domain and message. - */ - from: optional(HexAddressOrChecksumAddressSchema), -}); - -/** - * Schema representing an intent submission payload. - * - * Wraps the intent order along with protocol and optional routing metadata - * required by the backend or relayer infrastructure. - */ -export const IntentSchema = type({ - /** - * Identifier of the intent protocol used to interpret the order. - */ - protocol: IntentProtocolSchema, - - /** - * The intent order to be signed and submitted. - */ - order: IntentOrderSchema, - - /** - * Optional settlement contract address used for execution. - */ - settlementContract: optional(HexAddressOrChecksumAddressSchema), - - /** - * Optional EIP-712 typed data payload for signing. - * Must be JSON-serializable and include required EIP-712 fields. - */ - typedData: type({ - // Keep values as `any()` here. Using `unknown()` in this record causes - // TS2321/TS2589 (excessive type instantiation depth) in bridge state - // inference during build. - types: record( - string(), - array( - type({ - name: string(), - type: string(), - }), - ), - ), - primaryType: string(), - domain: record(string(), any()), - message: record(string(), any()), - }), -}); - -export const TxFeeGasLimitsSchema = type({ - maxFeePerGas: NumberStringSchema, - maxPriorityFeePerGas: NumberStringSchema, -}); - -export const GaslessPropertiesSchema = type({ - gasIncluded: optional(boolean()), - /** - * Whether the quote can use EIP-7702 delegated gasless execution - */ - gasIncluded7702: optional(boolean()), - /** - * A third party sponsors the gas. If true, then gasIncluded7702 is also true. - */ - gasSponsored: optional(boolean()), -}); - -export const QuoteSchema = intersection([ - GaslessPropertiesSchema, - type({ - requestId: string(), - srcChainId: ChainIdSchema, - srcAsset: BridgeAssetSchema, - /** - * The amount sent, in atomic amount: amount sent - fees - * Some tokens have a fee of 0, so sometimes it's equal to amount sent - */ - srcTokenAmount: string(), - destChainId: ChainIdSchema, - destAsset: BridgeAssetSchema, - /** - * The amount received, in atomic amount - */ - destTokenAmount: string(), - /** - * The minimum amount that will be received, in atomic amount - */ - minDestTokenAmount: string(), - feeData: type({ - [FeeType.METABRIDGE]: FeeDataSchema, - /** - * This is the fee for the swap transaction taken from either the - * src or dest token if the quote has gas fees included or "gasless" - */ - [FeeType.TX_FEE]: optional( - intersection([FeeDataSchema, TxFeeGasLimitsSchema]), - ), - }), - bridgeId: string(), - bridges: array(string()), - steps: array(StepSchema), - refuel: optional(RefuelDataSchema), - priceData: optional( - type({ - totalFromAmountUsd: optional(string()), - totalToAmountUsd: optional(string()), - priceImpact: optional(string()), - totalFeeAmountUsd: optional(string()), - }), - ), - intent: optional(IntentSchema), - }), -]); - -export const QuoteResponseSchema = type({ - quoteId: optional(string()), - quote: QuoteSchema, - estimatedProcessingTimeInSeconds: number(), - approval: optional(union([TxDataSchema, TronTradeDataSchema])), - trade: union([ - TxDataSchema, - BitcoinTradeDataSchema, - TronTradeDataSchema, - StellarTradeDataSchema, - string(), - ]), -}); - -export const validateQuoteResponseV1 = ( - data: unknown, -): data is Infer => { - assert(data, QuoteResponseSchema); - return true; -}; diff --git a/packages/bridge-controller/src/validators/quote.ts b/packages/bridge-controller/src/validators/quote.ts new file mode 100644 index 0000000000..f27dc1926d --- /dev/null +++ b/packages/bridge-controller/src/validators/quote.ts @@ -0,0 +1,113 @@ +import { + type, + optional, + boolean, + intersection, + string, + number, + array, + nullable, +} from '@metamask/superstruct'; +import type { Infer } from '@metamask/superstruct'; + +import { ChainIdSchema, BridgeAssetSchema } from './bridge-asset'; +import { IntentSchema } from './intent'; +import { TruthyDigitStringSchema, NumberStringSchema } from './number'; +import { RefuelDataSchema, StepSchema } from './step'; + +export enum FeeType { + METABRIDGE = 'metabridge', + REFUEL = 'refuel', + // eslint-disable-next-line @typescript-eslint/naming-convention + TX_FEE = 'txFee', + NETWORK = 'network', + RELAYER = 'relayer', +} + +export enum DiscountType { + VIP = 'vip', + PROMO = 'promo', + DAO = 'dao', +} + +export const FeeDataSchema = type({ + amount: TruthyDigitStringSchema, + asset: BridgeAssetSchema, + discountType: optional(nullable(string())), +}); + +export const TxFeeGasLimitsSchema = type({ + maxFeePerGas: NumberStringSchema, + maxPriorityFeePerGas: NumberStringSchema, +}); + +export const GaslessPropertiesSchema = type({ + gasIncluded: optional(boolean()), + /** + * Whether the quote can use EIP-7702 delegated gasless execution + */ + gasIncluded7702: optional(boolean()), + /** + * A third party sponsors the gas. If true, then gasIncluded7702 is also true. + */ + gasSponsored: optional(boolean()), +}); + +export const QuoteSchema = intersection([ + GaslessPropertiesSchema, + type({ + requestId: string(), + srcChainId: ChainIdSchema, + srcAsset: BridgeAssetSchema, + /** + * The amount sent, in atomic amount: amount sent - fees + * Some tokens have a fee of 0, so sometimes it's equal to amount sent + */ + srcTokenAmount: string(), + destChainId: ChainIdSchema, + destAsset: BridgeAssetSchema, + /** + * The amount received, in atomic amount + */ + destTokenAmount: string(), + /** + * The minimum amount that will be received, in atomic amount + */ + minDestTokenAmount: string(), + feeData: type({ + [FeeType.METABRIDGE]: intersection([ + FeeDataSchema, + type({ + quoteBpsFee: optional(number()), + baseBpsFee: optional(number()), + }), + ]), + /** + * This is the fee for the swap transaction taken from either the + * src or dest token if the quote has gas fees included or "gasless" + */ + [FeeType.TX_FEE]: optional( + intersection([FeeDataSchema, TxFeeGasLimitsSchema]), + ), + }), + bridgeId: string(), + bridges: array(string()), + steps: array(StepSchema), + refuel: optional(RefuelDataSchema), + priceData: optional( + type({ + totalFromAmountUsd: optional(string()), + totalToAmountUsd: optional(string()), + priceImpact: optional(string()), + totalFeeAmountUsd: optional(string()), + }), + ), + intent: optional(IntentSchema), + walletAddress: optional(string()), + destWalletAddress: optional(string()), + slippage: optional(number()), + protocols: optional(array(string())), + }), +]); + +export type Quote = Infer; diff --git a/packages/bridge-controller/src/validators/step.ts b/packages/bridge-controller/src/validators/step.ts new file mode 100644 index 0000000000..33e66e0a53 --- /dev/null +++ b/packages/bridge-controller/src/validators/step.ts @@ -0,0 +1,17 @@ +import { type, enums, optional } from '@metamask/superstruct'; + +import { BridgeAssetSchema } from './bridge-asset'; + +export enum ActionTypes { + BRIDGE = 'bridge', + SWAP = 'swap', + REFUEL = 'refuel', +} + +export const StepSchema = type({ + action: enums(Object.values(ActionTypes)), + srcAsset: optional(BridgeAssetSchema), + destAsset: optional(BridgeAssetSchema), +}); + +export const RefuelDataSchema = StepSchema; diff --git a/packages/bridge-controller/src/validators/validators.test.ts b/packages/bridge-controller/src/validators/validators.test.ts index 20193d63e9..3b809f31e9 100644 --- a/packages/bridge-controller/src/validators/validators.test.ts +++ b/packages/bridge-controller/src/validators/validators.test.ts @@ -2,12 +2,10 @@ import { is } from '@metamask/superstruct'; import { mockBridgeQuotesNativeErc20EthV1 } from '../../tests/mock-quotes-native-erc20-eth'; import { validateFeatureFlagsResponse } from './feature-flags'; -import { - IntentSchema, - validateQuoteResponseV1, - FeeDataSchema, - DiscountType, -} from './quote-response'; +import { IntentSchema } from './intent'; +import { DiscountType } from './quote'; +import { FeeDataSchema } from './quote'; +import { validateQuoteResponseV1 } from './quote-response-v1'; import { validateQuoteStreamComplete, QuoteStreamCompleteReason, diff --git a/packages/bridge-controller/tests/mock-quotes-erc20-erc20.ts b/packages/bridge-controller/tests/mock-quotes-erc20-erc20.ts index 1bca02382c..d203b94103 100644 --- a/packages/bridge-controller/tests/mock-quotes-erc20-erc20.ts +++ b/packages/bridge-controller/tests/mock-quotes-erc20-erc20.ts @@ -1,10 +1,11 @@ import { merge } from 'lodash'; -import type { QuoteResponseV1, DeepPartial } from '../src/types'; +import type { DeepPartial } from '../src/types'; import { - ActionTypes, validateQuoteResponseV1, -} from '../src/validators/quote-response'; + QuoteResponseV1, +} from '../src/validators/quote-response-v1'; +import { ActionTypes } from '../src/validators/step'; export const mockBridgeQuotesErc20Erc20V1: QuoteResponseV1[] = [ { @@ -58,13 +59,6 @@ export const mockBridgeQuotesErc20Erc20V1: QuoteResponseV1[] = [ steps: [ { action: ActionTypes.BRIDGE, - srcChainId: 10, - destChainId: 137, - protocol: { - name: 'across', - displayName: 'Across', - icon: 'https://miro.medium.com/max/800/1*PN_F5yW4VMBgs_xX-fsyzQ.png', - }, srcAsset: { chainId: 10, address: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', @@ -85,8 +79,6 @@ export const mockBridgeQuotesErc20Erc20V1: QuoteResponseV1[] = [ decimals: 6, icon: 'https://assets.polygon.technology/tokenAssets/usdc.svg', }, - srcAmount: '14000000', - destAmount: '13984280', }, ], }, @@ -154,13 +146,6 @@ export const mockBridgeQuotesErc20Erc20V1: QuoteResponseV1[] = [ steps: [ { action: ActionTypes.BRIDGE, - srcChainId: 10, - destChainId: 137, - protocol: { - name: 'cctp', - displayName: 'Circle CCTP', - icon: 'https://movricons.s3.ap-south-1.amazonaws.com/CCTP.svg', - }, srcAsset: { chainId: 10, address: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', @@ -181,8 +166,6 @@ export const mockBridgeQuotesErc20Erc20V1: QuoteResponseV1[] = [ decimals: 6, icon: 'https://assets.polygon.technology/tokenAssets/usdc.svg', }, - srcAmount: '14000000', - destAmount: '13800000', }, ], }, diff --git a/packages/bridge-controller/tests/mock-quotes-erc20-native.ts b/packages/bridge-controller/tests/mock-quotes-erc20-native.ts index 1ee086ea9a..07c33bfb41 100644 --- a/packages/bridge-controller/tests/mock-quotes-erc20-native.ts +++ b/packages/bridge-controller/tests/mock-quotes-erc20-native.ts @@ -1,10 +1,9 @@ import { merge } from 'lodash'; -import type { QuoteResponseV1, DeepPartial } from '../src/types'; -import { - ActionTypes, - validateQuoteResponseV1, -} from '../src/validators/quote-response'; +import type { DeepPartial } from '../src/types'; +import type { QuoteResponseV1 } from '../src/validators/quote-response-v1'; +import { validateQuoteResponseV1 } from '../src/validators/quote-response-v1'; +import { ActionTypes } from '../src/validators/step'; export const mockBridgeQuotesErc20NativeV1: QuoteResponseV1[] = [ { diff --git a/packages/bridge-controller/tests/mock-quotes-native-erc20-eth.ts b/packages/bridge-controller/tests/mock-quotes-native-erc20-eth.ts index 4f4f864349..259bbf92ad 100644 --- a/packages/bridge-controller/tests/mock-quotes-native-erc20-eth.ts +++ b/packages/bridge-controller/tests/mock-quotes-native-erc20-eth.ts @@ -1,10 +1,9 @@ import { merge } from 'lodash'; -import type { QuoteResponseV1, DeepPartial } from '../src/types'; -import { - ActionTypes, - validateQuoteResponseV1, -} from '../src/validators/quote-response'; +import type { DeepPartial } from '../src/types'; +import type { QuoteResponseV1 } from '../src/validators/quote-response-v1'; +import { validateQuoteResponseV1 } from '../src/validators/quote-response-v1'; +import { ActionTypes } from '../src/validators/step'; export const mockBridgeQuotesNativeErc20EthV1: QuoteResponseV1[] = [ { @@ -52,13 +51,6 @@ export const mockBridgeQuotesNativeErc20EthV1: QuoteResponseV1[] = [ steps: [ { action: ActionTypes.SWAP, - srcChainId: 1, - destChainId: 1, - protocol: { - name: '0x', - displayName: '0x', - icon: 'https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/exchanges/zerox.png', - }, srcAsset: { address: '0x0000000000000000000000000000000000000000', assetId: 'eip155:1/slip44:60', @@ -76,18 +68,9 @@ export const mockBridgeQuotesNativeErc20EthV1: QuoteResponseV1[] = [ decimals: 6, name: 'USD Coin', }, - srcAmount: '991250000000000000', - destAmount: '3104701473', }, { action: ActionTypes.BRIDGE, - srcChainId: 1, - destChainId: 42161, - protocol: { - name: 'across', - displayName: 'Across', - icon: 'https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/acrossv2.png', - }, srcAsset: { address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', assetId: @@ -106,8 +89,6 @@ export const mockBridgeQuotesNativeErc20EthV1: QuoteResponseV1[] = [ decimals: 6, name: 'USD Coin', }, - srcAmount: '3104701473', - destAmount: '3104367033', }, ], }, @@ -167,13 +148,6 @@ export const mockBridgeQuotesNativeErc20EthV1: QuoteResponseV1[] = [ steps: [ { action: ActionTypes.SWAP, - srcChainId: 1, - destChainId: 1, - protocol: { - name: '0x', - displayName: '0x', - icon: 'https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/exchanges/zerox.png', - }, srcAsset: { address: '0x0000000000000000000000000000000000000000', assetId: 'eip155:1/slip44:60', @@ -191,18 +165,9 @@ export const mockBridgeQuotesNativeErc20EthV1: QuoteResponseV1[] = [ decimals: 6, name: 'USD Coin', }, - srcAmount: '991250000000000000', - destAmount: '3104701473', }, { action: ActionTypes.BRIDGE, - srcChainId: 1, - destChainId: 42161, - protocol: { - name: 'celercircle', - displayName: 'Circle CCTP', - icon: 'https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/circle.png', - }, srcAsset: { address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', assetId: @@ -221,8 +186,6 @@ export const mockBridgeQuotesNativeErc20EthV1: QuoteResponseV1[] = [ decimals: 6, name: 'USD Coin', }, - srcAmount: '3104701473', - destAmount: '3104601473', }, ], }, diff --git a/packages/bridge-controller/tests/mock-quotes-native-erc20.ts b/packages/bridge-controller/tests/mock-quotes-native-erc20.ts index f1289ec65a..4e9c70fd96 100644 --- a/packages/bridge-controller/tests/mock-quotes-native-erc20.ts +++ b/packages/bridge-controller/tests/mock-quotes-native-erc20.ts @@ -1,10 +1,9 @@ import { merge } from 'lodash'; -import type { QuoteResponseV1, DeepPartial } from '../src/types'; -import { - ActionTypes, - validateQuoteResponseV1, -} from '../src/validators/quote-response'; +import type { DeepPartial } from '../src/types'; +import type { QuoteResponseV1 } from '../src/validators/quote-response-v1'; +import { validateQuoteResponseV1 } from '../src/validators/quote-response-v1'; +import { ActionTypes } from '../src/validators/step'; export const mockBridgeQuotesNativeErc20V1: QuoteResponseV1[] = [ { @@ -52,13 +51,6 @@ export const mockBridgeQuotesNativeErc20V1: QuoteResponseV1[] = [ steps: [ { action: ActionTypes.SWAP, - srcChainId: 10, - destChainId: 10, - protocol: { - name: 'zerox', - displayName: '0x', - icon: 'https://media.socket.tech/dexes/0x.svg', - }, srcAsset: { chainId: 10, address: '0x0000000000000000000000000000000000000000', @@ -78,18 +70,9 @@ export const mockBridgeQuotesNativeErc20V1: QuoteResponseV1[] = [ decimals: 6, icon: 'https://assets.polygon.technology/tokenAssets/usdc.svg', }, - srcAmount: '9912500000000000', - destAmount: '24456223', }, { action: ActionTypes.BRIDGE, - srcChainId: 10, - destChainId: 137, - protocol: { - name: 'across', - displayName: 'Across', - icon: 'https://miro.medium.com/max/800/1*PN_F5yW4VMBgs_xX-fsyzQ.png', - }, srcAsset: { chainId: 10, address: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', @@ -110,19 +93,10 @@ export const mockBridgeQuotesNativeErc20V1: QuoteResponseV1[] = [ decimals: 6, icon: 'https://assets.polygon.technology/tokenAssets/usdc.svg', }, - srcAmount: '24456223', - destAmount: '24438902', }, ], refuel: { action: ActionTypes.REFUEL, - srcChainId: 10, - destChainId: 137, - protocol: { - name: 'refuel', - displayName: 'Refuel', - icon: '', - }, srcAsset: { chainId: 10, address: '0x0000000000000000000000000000000000000000', @@ -139,8 +113,6 @@ export const mockBridgeQuotesNativeErc20V1: QuoteResponseV1[] = [ name: 'Matic', decimals: 18, }, - srcAmount: '1000000000000000', - destAmount: '4405865573929566208', }, }, trade: { @@ -199,13 +171,6 @@ export const mockBridgeQuotesNativeErc20V1: QuoteResponseV1[] = [ steps: [ { action: ActionTypes.SWAP, - srcChainId: 10, - destChainId: 137, - protocol: { - name: 'zerox', - displayName: '0x', - icon: 'https://media.socket.tech/dexes/0x.svg', - }, srcAsset: { chainId: 10, address: '0x0000000000000000000000000000000000000000', @@ -225,18 +190,9 @@ export const mockBridgeQuotesNativeErc20V1: QuoteResponseV1[] = [ decimals: 6, icon: 'https://assets.polygon.technology/tokenAssets/usdc.svg', }, - srcAmount: '9912500000000000', - destAmount: '24456223', }, { action: ActionTypes.BRIDGE, - srcChainId: 10, - destChainId: 137, - protocol: { - name: 'cctp', - displayName: 'Circle CCTP', - icon: 'https://movricons.s3.ap-south-1.amazonaws.com/CCTP.svg', - }, srcAsset: { chainId: 10, address: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', @@ -257,19 +213,10 @@ export const mockBridgeQuotesNativeErc20V1: QuoteResponseV1[] = [ decimals: 6, icon: 'https://assets.polygon.technology/tokenAssets/usdc.svg', }, - srcAmount: '24456223', - destAmount: '24256223', }, ], refuel: { action: ActionTypes.REFUEL, - srcChainId: 10, - destChainId: 137, - protocol: { - name: 'refuel', - displayName: 'Refuel', - icon: '', - }, srcAsset: { chainId: 10, address: '0x0000000000000000000000000000000000000000', @@ -286,8 +233,6 @@ export const mockBridgeQuotesNativeErc20V1: QuoteResponseV1[] = [ name: 'Matic', decimals: 18, }, - srcAmount: '1000000000000000', - destAmount: '4405865573929566208', }, }, trade: { diff --git a/packages/bridge-controller/tests/mock-quotes-sol-erc20.ts b/packages/bridge-controller/tests/mock-quotes-sol-erc20.ts index 6adbb08cae..281bab02a0 100644 --- a/packages/bridge-controller/tests/mock-quotes-sol-erc20.ts +++ b/packages/bridge-controller/tests/mock-quotes-sol-erc20.ts @@ -1,10 +1,9 @@ import { merge } from 'lodash'; -import type { QuoteResponseV1, DeepPartial } from '../src/types'; -import { - ActionTypes, - validateQuoteResponseV1, -} from '../src/validators/quote-response'; +import type { DeepPartial } from '../src/types'; +import type { QuoteResponseV1 } from '../src/validators/quote-response-v1'; +import { validateQuoteResponseV1 } from '../src/validators/quote-response-v1'; +import { ActionTypes } from '../src/validators/step'; export const mockBridgeQuotesSolErc20V1: QuoteResponseV1[] = [ { @@ -57,13 +56,6 @@ export const mockBridgeQuotesSolErc20V1: QuoteResponseV1[] = [ steps: [ { action: ActionTypes.BRIDGE, - srcChainId: 1151111081099710, - destChainId: 10, - protocol: { - name: 'mayan', - displayName: 'Mayan (Swift)', - icon: 'https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/mayan.png', - }, srcAsset: { address: '0x0000000000000000000000000000000000000000', assetId: @@ -86,8 +78,6 @@ export const mockBridgeQuotesSolErc20V1: QuoteResponseV1[] = [ 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/10/erc20/0x4200000000000000000000000000000000000042.png', chainId: 10, }, - srcAmount: '991250000', - destAmount: '143291269234176100000', }, ], priceData: { @@ -150,13 +140,6 @@ export const mockBridgeQuotesSolErc20V1: QuoteResponseV1[] = [ steps: [ { action: ActionTypes.BRIDGE, - srcChainId: 1151111081099710, - destChainId: 10, - protocol: { - name: 'mayanMCTP', - displayName: 'Mayan (MCTP)', - icon: 'https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/mayan.png', - }, srcAsset: { address: '0x0000000000000000000000000000000000000000', assetId: @@ -179,8 +162,6 @@ export const mockBridgeQuotesSolErc20V1: QuoteResponseV1[] = [ 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/10/erc20/0x4200000000000000000000000000000000000042.png', chainId: 10, }, - srcAmount: '991250000', - destAmount: '141450025181571360000', }, ], priceData: { diff --git a/packages/bridge-controller/tests/mock-sse.ts b/packages/bridge-controller/tests/mock-sse.ts index e0fcab4206..c8846021ae 100644 --- a/packages/bridge-controller/tests/mock-sse.ts +++ b/packages/bridge-controller/tests/mock-sse.ts @@ -2,23 +2,19 @@ import { ReadableStream } from 'node:stream/web'; import { flushPromises } from '../../../tests/helpers'; -import type { - QuoteResponse, - QuoteStreamCompleteData, - TokenFeature, - Trade, -} from '../src'; +import type { QuoteStreamCompleteData, TokenFeature } from '../src'; +import { QuoteResponseV1 } from '../src/validators/quote-response-v1'; type MockSseResponse = { status: number; ok: boolean; body: ReadableStream }; -export const advanceToNthTimer = (n = 1): void => { - for (let i = 0; i < n; i++) { +export const advanceToNthTimer = (nth = 1): void => { + for (let i = 0; i < nth; i++) { jest.advanceTimersToNextTimer(); } }; -export const advanceToNthTimerThenFlush = async (n = 1): Promise => { - advanceToNthTimer(n); +export const advanceToNthTimerThenFlush = async (nth = 1): Promise => { + advanceToNthTimer(nth); await flushPromises(); }; @@ -49,7 +45,7 @@ const emitLine = ( * @returns a delayed stream of quotes */ export const mockSseEventSource = ( - mockQuotes: QuoteResponse[], + mockQuotes: QuoteResponseV1[], delay: number = 3000, ): MockSseResponse => { return { @@ -78,7 +74,7 @@ export const mockSseEventSource = ( * @returns a delayed stream of quotes */ export const mockSseBatchSellEventSource = ( - mockQuotes: QuoteResponse[][], + mockQuotes: QuoteResponseV1[][], delay: number = 3000, ): MockSseResponse => { return { @@ -108,7 +104,7 @@ export const mockSseBatchSellEventSource = ( * @returns a stream of quotes with multiple delays in between each quote */ export const mockSseEventSourceWithMultipleDelays = async ( - mockQuotes: QuoteResponse[], + mockQuotes: QuoteResponseV1[], delay: number = 4000, ): Promise => { return { @@ -143,7 +139,7 @@ export const mockSseEventSourceWithMultipleDelays = async ( * @returns a delayed stream of quotes and token warnings */ export const mockSseEventSourceWithWarnings = ( - mockQuotes: QuoteResponse[], + mockQuotes: QuoteResponseV1[], mockWarnings: TokenFeature[], delay: number = 3000, ): MockSseResponse => { @@ -183,7 +179,7 @@ export const mockSseEventSourceWithWarnings = ( * @returns a delayed stream of quotes, token warnings, and a complete event */ export const mockSseEventSourceWithComplete = ( - mockQuotes: QuoteResponse[], + mockQuotes: QuoteResponseV1[], mockWarnings: TokenFeature[], mockComplete: QuoteStreamCompleteData, delay: number = 3000, From aa2611f4433139bc804318ce88cef83f35084ab6 Mon Sep 17 00:00:00 2001 From: micaelae Date: Tue, 7 Jul 2026 10:34:40 -0700 Subject: [PATCH 4/9] fix: lint --- packages/bridge-controller/src/bridge-controller.sse.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/bridge-controller/src/bridge-controller.sse.test.ts b/packages/bridge-controller/src/bridge-controller.sse.test.ts index 5267223fc7..269050ec00 100644 --- a/packages/bridge-controller/src/bridge-controller.sse.test.ts +++ b/packages/bridge-controller/src/bridge-controller.sse.test.ts @@ -36,7 +36,6 @@ import { formatChainIdToDec } from './utils/caip-formatters'; import * as featureFlagUtils from './utils/feature-flags'; import * as fetchUtils from './utils/fetch'; import { FeatureId } from './validators/feature-flags'; -import { validateQuoteResponseV1 } from './validators/quote-response-v1'; import { QuoteStreamCompleteReason } from './validators/quote-stream-complete'; import { TokenFeatureType } from './validators/token-feature'; import type { TxData } from './validators/trade'; From 6e9d1cf1d6aadfedc5a20a309d0dc01cc92247ba Mon Sep 17 00:00:00 2001 From: micaelae Date: Tue, 7 Jul 2026 11:58:49 -0700 Subject: [PATCH 5/9] fix: exports --- eslint-suppressions.json | 20 +----------------- packages/bridge-controller/src/index.ts | 27 ++++++++++++------------- 2 files changed, 14 insertions(+), 33 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 0b9972ca5b..add8d634a9 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -637,9 +637,6 @@ } }, "packages/bridge-controller/src/utils/bridge.ts": { - "@typescript-eslint/explicit-function-return-type": { - "count": 9 - }, "id-denylist": { "count": 1 } @@ -666,11 +663,6 @@ "count": 1 } }, - "packages/bridge-controller/src/utils/metrics/properties.ts": { - "@typescript-eslint/explicit-function-return-type": { - "count": 5 - } - }, "packages/bridge-controller/src/utils/quote-fees.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 1 @@ -707,11 +699,6 @@ "count": 1 } }, - "packages/bridge-controller/tests/mock-sse.ts": { - "id-length": { - "count": 2 - } - }, "packages/bridge-status-controller/src/utils/snaps.ts": { "no-restricted-syntax": { "count": 3 @@ -2248,11 +2235,6 @@ "count": 2 } }, - "packages/transaction-pay-controller/src/strategy/bridge/bridge-submit.ts": { - "no-restricted-syntax": { - "count": 1 - } - }, "packages/transaction-pay-controller/src/strategy/relay/hyperliquid-withdraw.ts": { "no-restricted-syntax": { "count": 1 @@ -2382,4 +2364,4 @@ "count": 10 } } -} +} \ No newline at end of file diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index 92ef13c990..a3f3403ab3 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -63,6 +63,18 @@ export type { BridgeControllerMessenger, FeatureFlagsPlatformConfig, TxFeeGasLimits, + TokenFeature, + QuoteStreamCompleteData, + BridgeControllerGetStateAction, + BridgeControllerStateChangeEvent, +} from './types'; + +export { + AssetType, + SortOrder, + ChainId, + RequestStatus, + StatusTypes, } from './types'; export type { @@ -80,19 +92,6 @@ export type { export { AbortReason } from './utils/metrics/constants'; -export { StatusTypes } from './types'; - -export { - AssetType, - SortOrder, - ChainId, - RequestStatus, - type TokenFeature, - type QuoteStreamCompleteData, - type BridgeControllerGetStateAction, - type BridgeControllerStateChangeEvent, -} from './types'; - export type { TxData, BitcoinTradeData, @@ -108,7 +107,7 @@ export { } from './validators/trade'; export type { QuoteResponseV1 as QuoteResponse } from './validators/quote-response-v1'; export type { Quote } from './validators/quote'; -export type { DiscountType, FeeType } from './validators/quote'; +export { FeeType, DiscountType } from './validators/quote'; export type { ActionTypes } from './validators/step'; export { validateQuoteStreamComplete, From 4ae6355f6dd1f38c919ead7363a60b32763c71e9 Mon Sep 17 00:00:00 2001 From: micaelae Date: Tue, 7 Jul 2026 12:07:33 -0700 Subject: [PATCH 6/9] fix: unit tests --- .../src/bridge-controller.sse.test.ts | 54 ++++++++++--------- .../bridge-controller/src/utils/fetch.test.ts | 12 ++++- packages/bridge-controller/src/utils/fetch.ts | 3 +- .../src/validators/quote-response-v1.ts | 13 +---- 4 files changed, 43 insertions(+), 39 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.sse.test.ts b/packages/bridge-controller/src/bridge-controller.sse.test.ts index 269050ec00..e4a2708720 100644 --- a/packages/bridge-controller/src/bridge-controller.sse.test.ts +++ b/packages/bridge-controller/src/bridge-controller.sse.test.ts @@ -1239,18 +1239,20 @@ describe('BridgeController SSE', function () { [ "Quote validation failed", [ - "lifi|trade", - "lifi|trade.chainId", - "lifi|trade.to", - "lifi|trade.from", - "lifi|trade.value", - "lifi|trade.data", - "lifi|trade.gasLimit", - "lifi|trade.unsignedPsbtBase64", - "lifi|trade.inputsToSign", - "lifi|trade.raw_data_hex", - "lifi|trade.xdrBase64", - "lifi|trade.xdr", + "At path: trade (union) -- Expected the value to satisfy a union of \`type | type | type | union | string\`, but received: [object Object]", + "At path: trade.chainId (union) -- Expected a number, but received: undefined", + "At path: trade.to (union) -- Expected a value of type \`HexString\`, but received: \`undefined\`", + "At path: trade.from (union) -- Expected a value of type \`HexString\`, but received: \`undefined\`", + "At path: trade.value (union) -- Expected a value of type \`HexString\`, but received: \`undefined\`", + "At path: trade.data (union) -- Expected a value of type \`HexString\`, but received: \`undefined\`", + "At path: trade.gasLimit (union) -- Expected a number, but received: undefined", + "At path: trade.unsignedPsbtBase64 (union) -- Expected a string, but received: undefined", + "At path: trade.inputsToSign (union) -- Expected an array value, but received: undefined", + "At path: trade.raw_data_hex (union) -- Expected a string, but received: undefined", + "At path: trade (union) -- Expected the value to satisfy a union of \`type | type\`, but received: [object Object]", + "At path: trade.xdrBase64 (union) -- Expected a string, but received: undefined", + "At path: trade.xdr (union) -- Expected a string, but received: undefined", + "At path: trade (union) -- Expected a string, but received: [object Object]", ], ] `); @@ -1268,21 +1270,21 @@ describe('BridgeController SSE', function () { ); expect(consoleWarnSpy.mock.calls).toHaveLength(3); expect(consoleWarnSpy.mock.calls[1]).toMatchInlineSnapshot(` - [ - "Quote validation failed", - [ - "unknown|unknown", - ], - ] - `); + [ + "Quote validation failed", + [ + "At path: (type) -- Expected an object, but received: """, + ], + ] + `); expect(consoleWarnSpy.mock.calls[2]).toMatchInlineSnapshot(` - [ - "Quote validation failed", - [ - "unknown|quote", - ], - ] - `); + [ + "Quote validation failed", + [ + "At path: quote (type) -- Expected an object, but received: undefined", + ], + ] + `); expect(consoleLogSpy).toHaveBeenCalledTimes(1); expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(5); diff --git a/packages/bridge-controller/src/utils/fetch.test.ts b/packages/bridge-controller/src/utils/fetch.test.ts index 82fd3f3f51..62f058af5c 100644 --- a/packages/bridge-controller/src/utils/fetch.test.ts +++ b/packages/bridge-controller/src/utils/fetch.test.ts @@ -273,7 +273,17 @@ describe('fetch', () => { 'lifi|approval', 'socket|trade', ]); - expect(mockConsoleWarn).toHaveBeenCalledTimes(1); + expect(mockConsoleWarn.mock.calls).toMatchInlineSnapshot(` + [ + [ + "Quote validation failed", + [ + "lifi|approval", + "socket|trade", + ], + ], + ] + `); mockConsoleWarn.mockRestore(); }); diff --git a/packages/bridge-controller/src/utils/fetch.ts b/packages/bridge-controller/src/utils/fetch.ts index 3453d84112..b63b4d15dd 100644 --- a/packages/bridge-controller/src/utils/fetch.ts +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -27,6 +27,7 @@ import { formatChainIdToDec, } from './caip-formatters'; import { fetchServerEvents } from './fetch-server-events'; +import { formatStructErrors } from './struct-error'; export const getClientHeaders = ({ clientId, @@ -398,6 +399,7 @@ export async function fetchBridgeQuoteStream( } } catch (error) { if (error instanceof StructError) { + console.warn('Quote validation failed', formatStructErrors(error)); error.failures().forEach(({ branch, path }) => { const aggregatorId = branch?.[0]?.quote?.bridgeId ?? @@ -411,7 +413,6 @@ export async function fetchBridgeQuoteStream( } const validationFailures = Array.from(uniqueValidationFailures); if (uniqueValidationFailures.size > 0) { - console.warn('Quote validation failed', validationFailures); return serverEventHandlers.onQuoteValidationFailure(validationFailures); } // Rethrow any unexpected errors diff --git a/packages/bridge-controller/src/validators/quote-response-v1.ts b/packages/bridge-controller/src/validators/quote-response-v1.ts index ea1b328dcf..d8666c0858 100644 --- a/packages/bridge-controller/src/validators/quote-response-v1.ts +++ b/packages/bridge-controller/src/validators/quote-response-v1.ts @@ -48,17 +48,8 @@ export const QuoteResponseSchemaV1 = type({ export const validateQuoteResponseV1 = ( data: unknown, ): data is Infer => { - try { - assert(data, QuoteResponseSchemaV1); - return true; - } catch (error) { - if (error instanceof StructError) { - console.warn('Quote validation failed', formatStructErrors(error)); - } else { - console.warn(JSON.stringify(error, null, 2)); - } - throw error; - } + assert(data, QuoteResponseSchemaV1); + return true; }; /** From 4092316337fb507c25ded3e0f138456187774b4c Mon Sep 17 00:00:00 2001 From: micaelae Date: Tue, 7 Jul 2026 12:25:07 -0700 Subject: [PATCH 7/9] fix: exports --- eslint-suppressions.json | 2 +- packages/bridge-controller/src/index.ts | 2 +- packages/bridge-controller/src/validators/quote-response-v1.ts | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index add8d634a9..cc71abc5c2 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -2364,4 +2364,4 @@ "count": 10 } } -} \ No newline at end of file +} diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index a3f3403ab3..5bd78eb6e5 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -108,7 +108,7 @@ export { export type { QuoteResponseV1 as QuoteResponse } from './validators/quote-response-v1'; export type { Quote } from './validators/quote'; export { FeeType, DiscountType } from './validators/quote'; -export type { ActionTypes } from './validators/step'; +export { ActionTypes } from './validators/step'; export { validateQuoteStreamComplete, QuoteStreamCompleteReason, diff --git a/packages/bridge-controller/src/validators/quote-response-v1.ts b/packages/bridge-controller/src/validators/quote-response-v1.ts index d8666c0858..7695b30f85 100644 --- a/packages/bridge-controller/src/validators/quote-response-v1.ts +++ b/packages/bridge-controller/src/validators/quote-response-v1.ts @@ -7,11 +7,9 @@ import { enums, union, assert, - StructError, } from '@metamask/superstruct'; import { StrictHexStruct } from '@metamask/utils'; -import { formatStructErrors } from '../utils/struct-error'; import { FeatureId } from './feature-flags'; import { FloatStringSchema } from './number'; import { QuoteSchema } from './quote'; From 2cdd2f6e9218c197f14bfe28124c6a4333dc65ab Mon Sep 17 00:00:00 2001 From: micaelae Date: Tue, 7 Jul 2026 12:38:14 -0700 Subject: [PATCH 8/9] chore: changelog --- packages/bridge-controller/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index 78bde62db2..d6c68b0e95 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Split up validators into smaller files to prepare for QuoteResponse V2 migration ([#9413](https://github.com/MetaMask/core/pull/9413)) - Bump `@metamask/messenger` from `^1.2.0` to `^2.0.0` ([#9392](https://github.com/MetaMask/core/pull/9392)) ## [77.3.2] From f262909fa6a7d1a4e56e633e198a479aa0c6b5ac Mon Sep 17 00:00:00 2001 From: micaelae Date: Tue, 7 Jul 2026 12:53:42 -0700 Subject: [PATCH 9/9] fix: undo step chainId removal --- packages/bridge-controller/src/validators/step.ts | 4 +++- .../tests/mock-quotes-erc20-erc20.ts | 4 ++++ .../tests/mock-quotes-native-erc20-eth.ts | 8 ++++++++ .../tests/mock-quotes-native-erc20.ts | 12 ++++++++++++ .../bridge-controller/tests/mock-quotes-sol-erc20.ts | 4 ++++ 5 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/bridge-controller/src/validators/step.ts b/packages/bridge-controller/src/validators/step.ts index 33e66e0a53..59460be4c9 100644 --- a/packages/bridge-controller/src/validators/step.ts +++ b/packages/bridge-controller/src/validators/step.ts @@ -1,6 +1,6 @@ import { type, enums, optional } from '@metamask/superstruct'; -import { BridgeAssetSchema } from './bridge-asset'; +import { BridgeAssetSchema, ChainIdSchema } from './bridge-asset'; export enum ActionTypes { BRIDGE = 'bridge', @@ -10,6 +10,8 @@ export enum ActionTypes { export const StepSchema = type({ action: enums(Object.values(ActionTypes)), + srcChainId: ChainIdSchema, + destChainId: optional(ChainIdSchema), srcAsset: optional(BridgeAssetSchema), destAsset: optional(BridgeAssetSchema), }); diff --git a/packages/bridge-controller/tests/mock-quotes-erc20-erc20.ts b/packages/bridge-controller/tests/mock-quotes-erc20-erc20.ts index d203b94103..e43025d372 100644 --- a/packages/bridge-controller/tests/mock-quotes-erc20-erc20.ts +++ b/packages/bridge-controller/tests/mock-quotes-erc20-erc20.ts @@ -59,6 +59,8 @@ export const mockBridgeQuotesErc20Erc20V1: QuoteResponseV1[] = [ steps: [ { action: ActionTypes.BRIDGE, + srcChainId: 10, + destChainId: 137, srcAsset: { chainId: 10, address: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', @@ -146,6 +148,8 @@ export const mockBridgeQuotesErc20Erc20V1: QuoteResponseV1[] = [ steps: [ { action: ActionTypes.BRIDGE, + srcChainId: 10, + destChainId: 137, srcAsset: { chainId: 10, address: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', diff --git a/packages/bridge-controller/tests/mock-quotes-native-erc20-eth.ts b/packages/bridge-controller/tests/mock-quotes-native-erc20-eth.ts index 259bbf92ad..ad41fbed3e 100644 --- a/packages/bridge-controller/tests/mock-quotes-native-erc20-eth.ts +++ b/packages/bridge-controller/tests/mock-quotes-native-erc20-eth.ts @@ -51,6 +51,8 @@ export const mockBridgeQuotesNativeErc20EthV1: QuoteResponseV1[] = [ steps: [ { action: ActionTypes.SWAP, + srcChainId: 1, + destChainId: 1, srcAsset: { address: '0x0000000000000000000000000000000000000000', assetId: 'eip155:1/slip44:60', @@ -71,6 +73,8 @@ export const mockBridgeQuotesNativeErc20EthV1: QuoteResponseV1[] = [ }, { action: ActionTypes.BRIDGE, + srcChainId: 1, + destChainId: 42161, srcAsset: { address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', assetId: @@ -148,6 +152,8 @@ export const mockBridgeQuotesNativeErc20EthV1: QuoteResponseV1[] = [ steps: [ { action: ActionTypes.SWAP, + srcChainId: 1, + destChainId: 1, srcAsset: { address: '0x0000000000000000000000000000000000000000', assetId: 'eip155:1/slip44:60', @@ -168,6 +174,8 @@ export const mockBridgeQuotesNativeErc20EthV1: QuoteResponseV1[] = [ }, { action: ActionTypes.BRIDGE, + srcChainId: 1, + destChainId: 42161, srcAsset: { address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', assetId: diff --git a/packages/bridge-controller/tests/mock-quotes-native-erc20.ts b/packages/bridge-controller/tests/mock-quotes-native-erc20.ts index 4e9c70fd96..ed44c75a92 100644 --- a/packages/bridge-controller/tests/mock-quotes-native-erc20.ts +++ b/packages/bridge-controller/tests/mock-quotes-native-erc20.ts @@ -51,6 +51,8 @@ export const mockBridgeQuotesNativeErc20V1: QuoteResponseV1[] = [ steps: [ { action: ActionTypes.SWAP, + srcChainId: 10, + destChainId: 10, srcAsset: { chainId: 10, address: '0x0000000000000000000000000000000000000000', @@ -73,6 +75,8 @@ export const mockBridgeQuotesNativeErc20V1: QuoteResponseV1[] = [ }, { action: ActionTypes.BRIDGE, + srcChainId: 10, + destChainId: 137, srcAsset: { chainId: 10, address: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', @@ -97,6 +101,8 @@ export const mockBridgeQuotesNativeErc20V1: QuoteResponseV1[] = [ ], refuel: { action: ActionTypes.REFUEL, + srcChainId: 10, + destChainId: 137, srcAsset: { chainId: 10, address: '0x0000000000000000000000000000000000000000', @@ -171,6 +177,8 @@ export const mockBridgeQuotesNativeErc20V1: QuoteResponseV1[] = [ steps: [ { action: ActionTypes.SWAP, + srcChainId: 10, + destChainId: 137, srcAsset: { chainId: 10, address: '0x0000000000000000000000000000000000000000', @@ -193,6 +201,8 @@ export const mockBridgeQuotesNativeErc20V1: QuoteResponseV1[] = [ }, { action: ActionTypes.BRIDGE, + srcChainId: 10, + destChainId: 137, srcAsset: { chainId: 10, address: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', @@ -217,6 +227,8 @@ export const mockBridgeQuotesNativeErc20V1: QuoteResponseV1[] = [ ], refuel: { action: ActionTypes.REFUEL, + srcChainId: 10, + destChainId: 137, srcAsset: { chainId: 10, address: '0x0000000000000000000000000000000000000000', diff --git a/packages/bridge-controller/tests/mock-quotes-sol-erc20.ts b/packages/bridge-controller/tests/mock-quotes-sol-erc20.ts index 281bab02a0..7bc3ab0fcf 100644 --- a/packages/bridge-controller/tests/mock-quotes-sol-erc20.ts +++ b/packages/bridge-controller/tests/mock-quotes-sol-erc20.ts @@ -56,6 +56,8 @@ export const mockBridgeQuotesSolErc20V1: QuoteResponseV1[] = [ steps: [ { action: ActionTypes.BRIDGE, + srcChainId: 1151111081099710, + destChainId: 10, srcAsset: { address: '0x0000000000000000000000000000000000000000', assetId: @@ -140,6 +142,8 @@ export const mockBridgeQuotesSolErc20V1: QuoteResponseV1[] = [ steps: [ { action: ActionTypes.BRIDGE, + srcChainId: 1151111081099710, + destChainId: 10, srcAsset: { address: '0x0000000000000000000000000000000000000000', assetId: