diff --git a/packages/bridge-controller/src/bridge-controller.sse.test.ts b/packages/bridge-controller/src/bridge-controller.sse.test.ts index 0c10142524..1f07767597 100644 --- a/packages/bridge-controller/src/bridge-controller.sse.test.ts +++ b/packages/bridge-controller/src/bridge-controller.sse.test.ts @@ -30,15 +30,14 @@ import { ETH_USDT_ADDRESS, } from './constants/bridge'; import { ChainId, RequestStatus } from './types'; -import type { BridgeControllerMessenger, QuoteResponse, TxData } from './types'; +import type { BridgeControllerMessenger, QuoteResponse } 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 type { TxData } from './validators/trade'; 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 07d58a7b44..d93ff0fa4a 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -51,7 +51,7 @@ import { MetricsSwapType, UnifiedSwapBridgeEventName, } from './utils/metrics/constants'; -import { FeatureId } from './utils/validators'; +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 d8eefb0a0a..25f016e191 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -88,7 +88,7 @@ import { } from './utils/quote'; import { appendFeesToQuotes } from './utils/quote-fees'; import { getMinimumBalanceForRentExemptionInLamports } from './utils/snaps'; -import type { FeatureId } from './utils/validators'; +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 e1b3b447e8..b8f4186bd4 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -49,11 +49,8 @@ export type { Quote, QuoteResponse, FeeData, - TxData, Intent, IntentOrderLike, - BitcoinTradeData, - TronTradeData, BridgeControllerState, BridgeControllerAction, BridgeControllerActions, @@ -89,16 +86,22 @@ export { type BridgeControllerStateChangeEvent, } from './types'; +export type { + TxData, + BitcoinTradeData, + TronTradeData, + Trade, +} from './validators/trade'; +export { isBitcoinTrade, isTronTrade, isEvmTxData } from './validators/trade'; +export { FeeType, ActionTypes } from './validators/quote-response'; export { - FeeType, - ActionTypes, - BridgeAssetSchema, - FeatureId, - TokenFeatureType, 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, @@ -168,13 +171,7 @@ export { formatAddressToAssetId, } from './utils/caip-formatters'; -export { - extractTradeData, - isBitcoinTrade, - 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 39c894f082..156acedc54 100644 --- a/packages/bridge-controller/src/selectors.test.ts +++ b/packages/bridge-controller/src/selectors.test.ts @@ -28,7 +28,7 @@ import { formatAddressToAssetId, formatChainIdToHex, } from './utils/caip-formatters'; -import { BatchSellTransactionType } from './utils/validators'; +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 cd5e52ad93..0d7d8baa28 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -30,28 +30,32 @@ 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, - FeatureId, + PlatformConfigSchema, +} from './validators/feature-flags'; +import type { FeeDataSchema, IntentSchema, - PlatformConfigSchema, ProtocolSchema, QuoteResponseSchema, QuoteSchema, StepSchema, - TokenFeatureSchema, - QuoteStreamCompleteSchema, - 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'; +import type { + BitcoinTradeData, + TronTradeData, + TxData, +} from './validators/trade'; export type FetchFunction = ( input: RequestInfo | URL | string, @@ -279,14 +283,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; /** * 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 980f88f3fc..1e6982c600 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, QuoteResponse, - TxData, } from '../types'; import { ChainId } from '../types'; +import type { TxData } from '../validators/trade'; import { formatChainIdToCaip, formatChainIdToDec, 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 85dd6daa74..b6bf219fb3 100644 --- a/packages/bridge-controller/src/utils/fetch.test.ts +++ b/packages/bridge-controller/src/utils/fetch.test.ts @@ -5,6 +5,8 @@ import mockBridgeQuotesErc20Erc20 from '../../tests/mock-quotes-erc20-erc20.json import mockBridgeQuotesNativeErc20 from '../../tests/mock-quotes-native-erc20.json'; import { BridgeClientId, BRIDGE_PROD_API_BASE_URL } from '../constants/bridge'; import { QuoteResponse } from '../types'; +import { BatchSellTransactionType } from '../validators/batch-sell'; +import { FeatureId } from '../validators/feature-flags'; import { fetchBridgeQuotes, fetchBridgeTokens, @@ -12,7 +14,6 @@ import { fetchBatchSellTrades, formatBatchSellTradesRequest, } from './fetch'; -import { BatchSellTransactionType, FeatureId } from './validators'; const mockFetchFn = jest.fn(); @@ -914,15 +915,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 59b7f1c7d8..44db811f31 100644 --- a/packages/bridge-controller/src/utils/fetch.ts +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -13,6 +13,13 @@ import type { BatchSellTradesRequest, BatchSellTradesResponse, } from '../types'; +import { validateBatchSellTradesResponse } from '../validators/batch-sell'; +import { validateBridgeAsset } from '../validators/bridge-asset'; +import type { FeatureId } from '../validators/feature-flags'; +import { validateQuoteResponse } 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, @@ -20,15 +27,6 @@ import { formatChainIdToDec, } from './caip-formatters'; import { fetchServerEvents } from './fetch-server-events'; -import { isEvmTxData } from './trade-utils'; -import type { FeatureId } from './validators'; -import { - validateQuoteResponse, - 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 a550e7190d..d2f9ca5fc8 100644 --- a/packages/bridge-controller/src/utils/metrics/properties.ts +++ b/packages/bridge-controller/src/utils/metrics/properties.ts @@ -7,8 +7,8 @@ import type { QuoteMetadata, QuoteRequest, QuoteResponse, - TxData, } from '../../types'; +import type { TxData } from '../../validators/trade'; import { getNativeAssetForChainId, isCrossChain } from '../bridge'; import { formatAddressToAssetId, diff --git a/packages/bridge-controller/src/utils/quote-fees.ts b/packages/bridge-controller/src/utils/quote-fees.ts index a40e1c9a46..e9d31fa7be 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 { QuoteResponse, 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 c6435f6cd6..4760afbdf1 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 7d89e7d22d..65d6aac6d4 100644 --- a/packages/bridge-controller/src/utils/quote.ts +++ b/packages/bridge-controller/src/utils/quote.ts @@ -15,10 +15,10 @@ import type { QuoteMetadata, QuoteResponse, NonEvmFees, - TxData, } from '../types'; +import { FeatureId } from '../validators/feature-flags'; +import type { TxData } from '../validators/trade'; import { isNativeAddress, isNonEvmChainId } from './bridge'; -import { FeatureId } from './validators'; export const isValidQuoteRequest = ( partialRequest: Partial, @@ -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/utils/trade-utils.test.ts b/packages/bridge-controller/src/utils/trade-utils.test.ts index 0d4a4cf74f..ba36f73800 100644 --- a/packages/bridge-controller/src/utils/trade-utils.test.ts +++ b/packages/bridge-controller/src/utils/trade-utils.test.ts @@ -1,11 +1,11 @@ -import type { BitcoinTradeData, TronTradeData, TxData } from '../types'; -import { - extractTradeData, - isEvmTxData, - isBitcoinTrade, - isTronTrade, -} from './trade-utils'; -import type { Trade } from './trade-utils'; +import type { + TxData, + BitcoinTradeData, + TronTradeData, + Trade, +} from '../validators/trade'; +import { isEvmTxData, isBitcoinTrade, isTronTrade } 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 0e78b063da..bed513ce8d 100644 --- a/packages/bridge-controller/src/utils/trade-utils.ts +++ b/packages/bridge-controller/src/utils/trade-utils.ts @@ -1,45 +1,9 @@ -import type { BitcoinTradeData, TronTradeData, TxData } from '../types'; - -// Union type representing all possible trade formats (EVM, Solana, Bitcoin, Tron) -export type Trade = TxData | string | BitcoinTradeData | TronTradeData; - -/** - * 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; -}; +import { + Trade, + isBitcoinTrade, + isTronTrade, + isEvmTxData, +} from '../validators/trade'; /** * Extracts the transaction data from different trade formats diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts deleted file mode 100644 index 06030fb11e..0000000000 --- a/packages/bridge-controller/src/utils/validators.ts +++ /dev/null @@ -1,574 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import { isValidHexAddress } from '@metamask/controller-utils'; -import type { Infer } from '@metamask/superstruct'; -import { - any, - string, - boolean, - number, - type, - is, - record, - array, - nullable, - optional, - enums, - define, - union, - assert, - pattern, - intersection, -} from '@metamask/superstruct'; -import { - CaipAssetTypeStruct, - CaipChainIdStruct, - isStrictHexString, -} from '@metamask/utils'; - -export enum FeeType { - METABRIDGE = 'metabridge', - REFUEL = 'refuel', - TX_FEE = 'txFee', -} - -export enum FeatureId { - PERPS = 'perps', - QUICK_BUY = 'quickBuy', - DAPP_SWAP = 'dappSwap', -} - -export enum ActionTypes { - BRIDGE = 'bridge', - SWAP = 'swap', - REFUEL = 'refuel', -} - -const HexAddressSchema = define<`0x${string}`>('HexAddress', (value: unknown) => - isValidHexAddress(value as string, { allowNonPrefixed: false }), -); - -const HexStringSchema = define<`0x${string}`>('HexString', isStrictHexString); - -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()), -}); - -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); -}; - -export const validateSwapsTokenObject = ( - data: unknown, -): data is Infer => { - return is(data, BridgeAssetSchema); -}; - -export const FeeDataSchema = type({ - amount: TruthyDigitStringSchema, - asset: BridgeAssetSchema, -}); - -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: HexAddressSchema, - - /** - * Address of the token being bought. - */ - buyToken: HexAddressSchema, - - /** - * Optional receiver of the bought tokens. - * If omitted, defaults to the signer / order owner. - */ - receiver: optional(HexAddressSchema), - - /** - * 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: HexStringSchema, - - /** - * 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(HexAddressSchema), -}); - -/** - * 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(HexAddressSchema), - - /** - * 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 TxDataSchema = type({ - chainId: number(), - to: HexAddressSchema, - from: HexAddressSchema, - value: HexStringSchema, - data: HexStringSchema, - 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()), - }), - ), - ), -}); - -export const QuoteResponseSchema = type({ - quoteId: optional(string()), - quote: QuoteSchema, - estimatedProcessingTimeInSeconds: number(), - approval: optional(union([TxDataSchema, TronTradeDataSchema])), - trade: union([ - TxDataSchema, - BitcoinTradeDataSchema, - TronTradeDataSchema, - string(), - ]), -}); - -export const validateQuoteResponse = ( - data: unknown, -): data is Infer => { - 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/batch-sell.ts b/packages/bridge-controller/src/validators/batch-sell.ts new file mode 100644 index 0000000000..753078f8a6 --- /dev/null +++ b/packages/bridge-controller/src/validators/batch-sell.ts @@ -0,0 +1,51 @@ +import { + intersection, + type, + array, + enums, + optional, + assert, +} from '@metamask/superstruct'; +import type { Infer } from '@metamask/superstruct'; +import { StrictHexStruct } from '@metamask/utils'; + +import { BridgeAssetSchema } from './bridge-asset'; +import { NumberStringSchema, GaslessPropertiesSchema } from './quote-response'; +import { TxDataSchema } from './trade'; + +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..b4e5000994 --- /dev/null +++ b/packages/bridge-controller/src/validators/feature-flags.ts @@ -0,0 +1,131 @@ +/* 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 { + PERPS = 'perps', + QUICK_BUY = 'quickBuy', + DAPP_SWAP = 'dappSwap', +} +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/validators/quote-response.ts b/packages/bridge-controller/src/validators/quote-response.ts new file mode 100644 index 0000000000..50370ad0a5 --- /dev/null +++ b/packages/bridge-controller/src/validators/quote-response.ts @@ -0,0 +1,293 @@ +/* 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, +} from '@metamask/superstruct'; +import { StrictHexStruct } from '@metamask/utils'; + +import { BridgeAssetSchema, ChainIdSchema } from './bridge-asset'; +import { + TxDataSchema, + TronTradeDataSchema, + BitcoinTradeDataSchema, + HexAddressOrChecksumAddressSchema, +} from './trade'; + +export enum FeeType { + METABRIDGE = 'metabridge', + REFUEL = 'refuel', + TX_FEE = 'txFee', +} + +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, +}); + +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, + string(), + ]), +}); + +export const validateQuoteResponse = ( + data: unknown, +): data is Infer => { + assert(data, QuoteResponseSchema); + 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/validators/trade.ts b/packages/bridge-controller/src/validators/trade.ts new file mode 100644 index 0000000000..de9611f610 --- /dev/null +++ b/packages/bridge-controller/src/validators/trade.ts @@ -0,0 +1,99 @@ +import { + type, + number, + nullable, + optional, + string, + array, + boolean, + union, +} from '@metamask/superstruct'; +import type { Infer } from '@metamask/superstruct'; +import { + HexAddressStruct, + HexChecksumAddressStruct, + StrictHexStruct, +} from '@metamask/utils'; + +export const HexAddressOrChecksumAddressSchema = union([ + HexAddressStruct, + HexChecksumAddressStruct, +]); + +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()), + }), + ), + ), +}); // Union type representing all possible trade formats (EVM, Solana, Bitcoin, Tron) + +export type Trade = TxData | string | BitcoinTradeData | TronTradeData; +/** + * 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 type BitcoinTradeData = Infer; + +export type TronTradeData = Infer; +export type TxData = Infer; 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 2d1a6c09a4..87b29163f2 100644 --- a/packages/bridge-controller/src/utils/validators.test.ts +++ b/packages/bridge-controller/src/validators/validators.test.ts @@ -1,11 +1,11 @@ import { is } from '@metamask/superstruct'; +import { validateFeatureFlagsResponse } from './feature-flags'; +import { IntentSchema } from './quote-response'; import { - validateFeatureFlagsResponse, validateQuoteStreamComplete, QuoteStreamCompleteReason, - IntentSchema, -} from './validators'; +} from './quote-stream-complete'; describe('validators', () => { describe('validateFeatureFlagsResponse', () => {