diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index 6ff9d3d1650..854e4fc7f78 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -59,6 +59,7 @@ "@metamask/auto-changelog": "^3.4.4", "@metamask/eth-json-rpc-provider": "^4.1.8", "@metamask/network-controller": "^22.2.1", + "@metamask/superstruct": "^3.1.0", "@metamask/transaction-controller": "^46.0.0", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index 0abf61367cb..99ec1fa352d 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -86,19 +86,16 @@ export enum BridgeFlag { type DecimalChainId = string; export type GasMultiplierByChainId = Record; +type FeatureFlagResponsePlatformConfig = { + refreshRate: number; + maxRefreshCount: number; + support: boolean; + chains: Record; +}; + export type FeatureFlagResponse = { - [BridgeFlag.EXTENSION_CONFIG]: { - refreshRate: number; - maxRefreshCount: number; - support: boolean; - chains: Record; - }; - [BridgeFlag.MOBILE_CONFIG]: { - refreshRate: number; - maxRefreshCount: number; - support: boolean; - chains: Record; - }; + [BridgeFlag.EXTENSION_CONFIG]: FeatureFlagResponsePlatformConfig; + [BridgeFlag.MOBILE_CONFIG]: FeatureFlagResponsePlatformConfig; }; export type BridgeAsset = { @@ -211,19 +208,16 @@ export enum BridgeFeatureFlagsKey { MOBILE_CONFIG = 'mobileConfig', } +type FeatureFlagsPlatformConfig = { + refreshRate: number; + maxRefreshCount: number; + support: boolean; + chains: Record; +}; + export type BridgeFeatureFlags = { - [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: { - refreshRate: number; - maxRefreshCount: number; - support: boolean; - chains: Record; - }; - [BridgeFeatureFlagsKey.MOBILE_CONFIG]: { - refreshRate: number; - maxRefreshCount: number; - support: boolean; - chains: Record; - }; + [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: FeatureFlagsPlatformConfig; + [BridgeFeatureFlagsKey.MOBILE_CONFIG]: FeatureFlagsPlatformConfig; }; export enum RequestStatus { LOADING, diff --git a/packages/bridge-controller/src/utils/fetch.ts b/packages/bridge-controller/src/utils/fetch.ts index 6b0b64fefc0..3e9d993cdbb 100644 --- a/packages/bridge-controller/src/utils/fetch.ts +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -7,29 +7,21 @@ import { getBridgeApiBaseUrl, } from './bridge'; import { - FEATURE_FLAG_VALIDATORS, - QUOTE_VALIDATORS, - TX_DATA_VALIDATORS, - TOKEN_VALIDATORS, - validateResponse, - QUOTE_RESPONSE_VALIDATORS, - FEE_DATA_VALIDATORS, + validateFeatureFlagsResponse, + validateQuoteResponse, + validateSwapsTokenObject, } from './validators'; import { DEFAULT_FEATURE_FLAG_CONFIG } from '../constants/bridge'; import type { SwapsTokenObject } from '../constants/tokens'; import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../constants/tokens'; import type { - FeatureFlagResponse, - FeeData, - Quote, QuoteRequest, QuoteResponse, - TxData, BridgeFeatureFlags, FetchFunction, ChainConfiguration, } from '../types'; -import { BridgeFlag, FeeType, BridgeFeatureFlagsKey } from '../types'; +import { BridgeFlag, BridgeFeatureFlagsKey } from '../types'; // TODO put this back in once we have a fetchWithCache equivalent // const CACHE_REFRESH_TEN_MINUTES = 10 * Duration.Minute; @@ -50,17 +42,11 @@ export async function fetchBridgeFeatureFlags( fetchFn: FetchFunction, ): Promise { const url = `${getBridgeApiBaseUrl()}/getAllFeatureFlags`; - const rawFeatureFlags = await fetchFn(url, { + const rawFeatureFlags: unknown = await fetchFn(url, { headers: getClientIdHeader(clientId), }); - if ( - validateResponse( - FEATURE_FLAG_VALIDATORS, - rawFeatureFlags, - url, - ) - ) { + if (validateFeatureFlagsResponse(rawFeatureFlags)) { const getChainsObj = (chains: Record) => Object.entries(chains).reduce( (acc, [chainId, value]) => ({ @@ -127,7 +113,7 @@ export async function fetchBridgeTokens( tokens.forEach((token: unknown) => { if ( - validateResponse(TOKEN_VALIDATORS, token, url, false) && + validateSwapsTokenObject(token) && !( isSwapsDefaultTokenSymbol(token.symbol, chainId) || isSwapsDefaultTokenAddress(token.address, chainId) @@ -166,40 +152,13 @@ export async function fetchBridgeQuotes( resetApproval: request.resetApproval ? 'true' : 'false', }); const url = `${getBridgeApiBaseUrl()}/getQuote?${queryParams}`; - const quotes = await fetchFn(url, { + const quotes: unknown[] = await fetchFn(url, { headers: getClientIdHeader(clientId), signal, }); - const filteredQuotes = quotes.filter((quoteResponse: QuoteResponse) => { - const { quote, approval, trade } = quoteResponse; - return ( - validateResponse( - QUOTE_RESPONSE_VALIDATORS, - quoteResponse, - url, - ) && - validateResponse(QUOTE_VALIDATORS, quote, url) && - validateResponse( - TOKEN_VALIDATORS, - quote.srcAsset, - url, - ) && - validateResponse( - TOKEN_VALIDATORS, - quote.destAsset, - url, - ) && - validateResponse(TX_DATA_VALIDATORS, trade, url) && - validateResponse( - FEE_DATA_VALIDATORS, - quote.feeData[FeeType.METABRIDGE], - url, - ) && - (approval - ? validateResponse(TX_DATA_VALIDATORS, approval, url) - : true) - ); + const filteredQuotes = quotes.filter((quoteResponse: unknown) => { + return validateQuoteResponse(quoteResponse); }); - return filteredQuotes; + return filteredQuotes as QuoteResponse[]; } diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts index 56d8f93a47a..f2e5171c656 100644 --- a/packages/bridge-controller/src/utils/validators.ts +++ b/packages/bridge-controller/src/utils/validators.ts @@ -1,162 +1,142 @@ -import { isValidHexAddress as isValidHexAddress_ } from '@metamask/controller-utils'; +import { isValidHexAddress } from '@metamask/controller-utils'; +import { + string, + boolean, + number, + type, + is, + record, + array, + nullable, + optional, + enums, + define, + intersection, + size, +} from '@metamask/superstruct'; import { isStrictHexString } from '@metamask/utils'; import type { SwapsTokenObject } from '../constants/tokens'; -import type { - FeatureFlagResponse, - FeeData, - Quote, - QuoteResponse, - TxData, -} from '../types'; -import { BridgeFlag } from '../types'; - -export const truthyString = (string: string) => Boolean(string?.length); -export const truthyDigitString = (string: string) => - truthyString(string) && Boolean(string.match(/^\d+$/u)); - -export const isValidNumber = (v: unknown): v is number => typeof v === 'number'; -const isValidObject = (v: unknown): v is object => - typeof v === 'object' && v !== null; -const isValidString = (v: unknown): v is string => - typeof v === 'string' && v.length > 0; -const isValidHexAddress = (v: unknown) => - isValidString(v) && isValidHexAddress_(v, { allowNonPrefixed: false }); - -type Validator = { - property: keyof ExpectedResponse; - type: string; - validator?: (value: unknown) => boolean; -}; +import type { FeatureFlagResponse, QuoteResponse } from '../types'; +import { ActionTypes, BridgeFlag, FeeType } from '../types'; + +const HexAddressSchema = define('HexAddress', (v: unknown) => + isValidHexAddress(v as string, { allowNonPrefixed: false }), +); + +const HexStringSchema = define('HexString', (v: unknown) => + isStrictHexString(v as string), +); + +export const truthyString = (s: string) => Boolean(s?.length); +const TruthyDigitStringSchema = define( + 'TruthyDigitString', + (v: unknown) => + truthyString(v as string) && Boolean((v as string).match(/^\d+$/u)), +); + +const SwapsTokenObjectSchema = type({ + decimals: number(), + address: intersection([string(), HexAddressSchema]), + symbol: size(string(), 1, 12), +}); + +export const validateFeatureFlagsResponse = ( + data: unknown, +): data is FeatureFlagResponse => { + const ChainConfigurationSchema = type({ + isActiveSrc: boolean(), + isActiveDest: boolean(), + }); -export const validateData = ( - validators: Validator[], - object: unknown, - urlUsed: string, - logError = true, -): object is ExpectedResponse => { - return validators.every(({ property, type, validator }) => { - const types = type.split('|'); - const propertyString = String(property); - - const valid = - isValidObject(object) && - types.some( - (_type) => - typeof object[propertyString as keyof typeof object] === _type, - ) && - (!validator || validator(object[propertyString as keyof typeof object])); - - if (!valid && logError) { - const value = isValidObject(object) - ? object[propertyString as keyof typeof object] - : undefined; - const typeString = isValidObject(object) - ? typeof object[propertyString as keyof typeof object] - : 'undefined'; - - console.error( - `response to GET ${urlUsed} invalid for property ${String(property)}; value was:`, - value, - '| type was: ', - typeString, - ); - } - return valid; + const ConfigSchema = type({ + refreshRate: number(), + maxRefreshCount: number(), + support: boolean(), + chains: record(string(), ChainConfigurationSchema), }); + + // Create schema for FeatureFlagResponse + const FeatureFlagResponseSchema = type({ + [BridgeFlag.EXTENSION_CONFIG]: ConfigSchema, + [BridgeFlag.MOBILE_CONFIG]: ConfigSchema, + }); + + return is(data, FeatureFlagResponseSchema); }; -export const validateResponse = ( - validators: Validator[], +export const validateSwapsTokenObject = ( data: unknown, - urlUsed: string, - logError = true, -): data is ExpectedResponse => { - return validateData(validators, data, urlUsed, logError); +): data is SwapsTokenObject => { + return is(data, SwapsTokenObjectSchema); }; -export const FEATURE_FLAG_VALIDATORS = [ - { - property: BridgeFlag.EXTENSION_CONFIG, - type: 'object', - validator: ( - v: unknown, - ): v is Pick => - isValidObject(v) && - 'refreshRate' in v && - isValidNumber(v.refreshRate) && - 'maxRefreshCount' in v && - isValidNumber(v.maxRefreshCount) && - 'chains' in v && - isValidObject(v.chains) && - Object.values(v.chains).every((chain) => isValidObject(chain)) && - Object.values(v.chains).every( - (chain) => - 'isActiveSrc' in chain && - 'isActiveDest' in chain && - typeof chain.isActiveSrc === 'boolean' && - typeof chain.isActiveDest === 'boolean', - ), - }, -]; - -export const TOKEN_AGGREGATOR_VALIDATORS = [ - { - property: 'aggregators', - type: 'object', - validator: (v: unknown): v is number[] => - isValidObject(v) && Object.values(v).every(isValidString), - }, -]; - -export const TOKEN_VALIDATORS: Validator[] = [ - { property: 'decimals', type: 'number' }, - { property: 'address', type: 'string', validator: isValidHexAddress }, - { - property: 'symbol', - type: 'string', - validator: (v: unknown) => isValidString(v) && v.length <= 12, - }, -]; - -export const QUOTE_RESPONSE_VALIDATORS: Validator[] = [ - { property: 'quote', type: 'object', validator: isValidObject }, - { property: 'estimatedProcessingTimeInSeconds', type: 'number' }, - { - property: 'approval', - type: 'object|undefined', - validator: (v: unknown) => v === undefined || isValidObject(v), - }, - { property: 'trade', type: 'object', validator: isValidObject }, -]; - -export const QUOTE_VALIDATORS: Validator[] = [ - { property: 'requestId', type: 'string' }, - { property: 'srcTokenAmount', type: 'string' }, - { property: 'destTokenAmount', type: 'string' }, - { property: 'bridgeId', type: 'string' }, - { property: 'bridges', type: 'object', validator: isValidObject }, - { property: 'srcChainId', type: 'number' }, - { property: 'destChainId', type: 'number' }, - { property: 'srcAsset', type: 'object', validator: isValidObject }, - { property: 'destAsset', type: 'object', validator: isValidObject }, - { property: 'feeData', type: 'object', validator: isValidObject }, -]; - -export const FEE_DATA_VALIDATORS: Validator[] = [ - { - property: 'amount', - type: 'string', - validator: (v: unknown) => truthyDigitString(String(v)), - }, - { property: 'asset', type: 'object', validator: isValidObject }, -]; - -export const TX_DATA_VALIDATORS: Validator[] = [ - { property: 'chainId', type: 'number' }, - { property: 'value', type: 'string', validator: isStrictHexString }, - { property: 'gasLimit', type: 'number' }, - { property: 'to', type: 'string', validator: isValidHexAddress }, - { property: 'from', type: 'string', validator: isValidHexAddress }, - { property: 'data', type: 'string', validator: isStrictHexString }, -]; +export const validateQuoteResponse = (data: unknown): data is QuoteResponse => { + const ChainIdSchema = number(); + + const BridgeAssetSchema = type({ + chainId: ChainIdSchema, + address: string(), + symbol: string(), + name: string(), + decimals: number(), + icon: optional(string()), + }); + + const FeeDataSchema = type({ + amount: TruthyDigitStringSchema, + asset: BridgeAssetSchema, + }); + + const ProtocolSchema = type({ + name: string(), + displayName: optional(string()), + icon: optional(string()), + }); + + 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; + + const QuoteSchema = type({ + requestId: string(), + srcChainId: ChainIdSchema, + srcAsset: SwapsTokenObjectSchema, + srcTokenAmount: string(), + destChainId: ChainIdSchema, + destAsset: SwapsTokenObjectSchema, + destTokenAmount: string(), + feeData: record(enums(Object.values(FeeType)), FeeDataSchema), + bridgeId: string(), + bridges: array(string()), + steps: array(StepSchema), + refuel: optional(RefuelDataSchema), + }); + + const TxDataSchema = type({ + chainId: number(), + to: HexAddressSchema, + from: HexAddressSchema, + value: HexStringSchema, + data: HexStringSchema, + gasLimit: nullable(number()), + }); + + const QuoteResponseSchema = type({ + quote: QuoteSchema, + approval: optional(TxDataSchema), + trade: TxDataSchema, + estimatedProcessingTimeInSeconds: number(), + }); + + return is(data, QuoteResponseSchema); +}; diff --git a/packages/bridge-status-controller/package.json b/packages/bridge-status-controller/package.json index c3283716983..3b43f144de6 100644 --- a/packages/bridge-status-controller/package.json +++ b/packages/bridge-status-controller/package.json @@ -51,6 +51,7 @@ "@metamask/bridge-controller": "^2.0.0", "@metamask/controller-utils": "^11.5.0", "@metamask/polling-controller": "^12.0.3", + "@metamask/superstruct": "^3.1.0", "@metamask/utils": "^11.2.0" }, "devDependencies": { diff --git a/packages/bridge-status-controller/src/utils/bridge-status.test.ts b/packages/bridge-status-controller/src/utils/bridge-status.test.ts index bbf334a9f6b..2d4b37fb165 100644 --- a/packages/bridge-status-controller/src/utils/bridge-status.test.ts +++ b/packages/bridge-status-controller/src/utils/bridge-status.test.ts @@ -126,7 +126,8 @@ describe('utils', () => { await expect( fetchBridgeTxStatus(mockStatusRequest, mockClientId, mockFetch), - ).rejects.toThrow('Invalid response from bridge'); + // eslint-disable-next-line jest/require-to-throw-message + ).rejects.toThrow(); }); it('should handle fetch errors', async () => { diff --git a/packages/bridge-status-controller/src/utils/bridge-status.ts b/packages/bridge-status-controller/src/utils/bridge-status.ts index 8a8fa50936e..5c32f54e93d 100644 --- a/packages/bridge-status-controller/src/utils/bridge-status.ts +++ b/packages/bridge-status-controller/src/utils/bridge-status.ts @@ -1,7 +1,7 @@ import type { Quote } from '@metamask/bridge-controller'; import { getBridgeApiBaseUrl } from '@metamask/bridge-controller'; -import { validateResponse, validators } from './validators'; +import { validateBridgeStatusResponse } from './validators'; import type { StatusResponse, StatusRequestWithSrcTxHash, @@ -40,29 +40,22 @@ export const fetchBridgeTxStatus = async ( statusRequest: StatusRequestWithSrcTxHash, clientId: string, fetchFn: FetchFunction, -) => { +): Promise => { const statusRequestDto = getStatusRequestDto(statusRequest); const params = new URLSearchParams(statusRequestDto); // Fetch const url = `${BRIDGE_STATUS_BASE_URL}?${params.toString()}`; - const rawTxStatus = await fetchFn(url, { + const rawTxStatus: unknown = await fetchFn(url, { headers: getClientIdHeader(clientId), }); // Validate - const isValid = validateResponse( - validators, - rawTxStatus, - BRIDGE_STATUS_BASE_URL, - ); - if (!isValid) { - throw new Error('Invalid response from bridge'); - } + validateBridgeStatusResponse(rawTxStatus); // Return - return rawTxStatus; + return rawTxStatus as StatusResponse; }; export const getStatusRequestWithSrcTxHash = ( diff --git a/packages/bridge-status-controller/src/utils/validators.test.ts b/packages/bridge-status-controller/src/utils/validators.test.ts index 90128f6583e..b6cd2c97816 100644 --- a/packages/bridge-status-controller/src/utils/validators.test.ts +++ b/packages/bridge-status-controller/src/utils/validators.test.ts @@ -1,5 +1,4 @@ -import { validateResponse, validators } from './validators'; -import type { StatusResponse } from '../types'; +import { validateBridgeStatusResponse } from './validators'; const BridgeTxStatusResponses = { STATUS_PENDING_VALID: { @@ -221,73 +220,65 @@ describe('validators', () => { it.each([ { input: BridgeTxStatusResponses.STATUS_PENDING_VALID, - expected: true, description: 'valid pending bridge status', }, { input: BridgeTxStatusResponses.STATUS_PENDING_VALID_MISSING_FIELDS, - expected: true, description: 'valid pending bridge status missing fields', }, { input: BridgeTxStatusResponses.STATUS_PENDING_VALID_MISSING_FIELDS_2, - expected: true, description: 'valid pending bridge status missing fields 2', }, - { - input: BridgeTxStatusResponses.STATUS_PENDING_INVALID_MISSING_FIELDS, - expected: false, - description: 'pending bridge status with missing fields', - }, { input: BridgeTxStatusResponses.STATUS_COMPLETE_VALID, - expected: true, description: 'valid complete bridge status', }, - { - input: BridgeTxStatusResponses.STATUS_COMPLETE_INVALID_MISSING_FIELDS, - expected: false, - description: 'complete bridge status with missing fields', - }, { input: BridgeTxStatusResponses.STATUS_COMPLETE_VALID_MISSING_FIELDS_2, - expected: true, description: 'complete bridge status with missing fields 2', }, { input: BridgeTxStatusResponses.STATUS_COMPLETE_VALID_MISSING_FIELDS, - expected: true, description: 'complete bridge status with missing fields', }, { input: BridgeTxStatusResponses.STATUS_FAILED_VALID, - expected: true, description: 'valid failed bridge status', }, + ])( + 'should not throw for valid response for $description', + ({ input }: { input: unknown }) => { + expect(() => validateBridgeStatusResponse(input)).not.toThrow(); + }, + ); + + it.each([ + { + input: BridgeTxStatusResponses.STATUS_PENDING_INVALID_MISSING_FIELDS, + description: 'pending bridge status with missing fields', + }, + { + input: BridgeTxStatusResponses.STATUS_COMPLETE_INVALID_MISSING_FIELDS, + description: 'complete bridge status with missing fields', + }, { input: undefined, - expected: false, description: 'undefined', }, { input: null, - expected: false, description: 'null', }, { input: {}, - expected: false, description: 'empty object', }, ])( - 'should return $expected for $description', - ({ input, expected }: { input: unknown; expected: boolean }) => { - const res = validateResponse( - validators, - input, - 'dummyurl.com', - ); - expect(res).toBe(expected); + 'should throw for invalid response for $description', + ({ input }: { input: unknown }) => { + // eslint-disable-next-line jest/require-to-throw-message + expect(() => validateBridgeStatusResponse(input)).toThrow(); }, ); }); diff --git a/packages/bridge-status-controller/src/utils/validators.ts b/packages/bridge-status-controller/src/utils/validators.ts index cc32e0f031f..b94209d60f4 100644 --- a/packages/bridge-status-controller/src/utils/validators.ts +++ b/packages/bridge-status-controller/src/utils/validators.ts @@ -1,219 +1,57 @@ -import { isValidHexAddress } from '@metamask/controller-utils'; +import { + object, + string, + boolean, + number, + optional, + enums, + union, + type, + nullable, + assert, +} from '@metamask/superstruct'; -import { BRIDGE_STATUS_BASE_URL } from './bridge-status'; -import type { DestChainStatus, SrcChainStatus, Asset } from '../types'; import { BridgeId, StatusTypes } from '../types'; -type Validator = { - property: keyof ExpectedResponse | string; - type: string; - validator: (value: DataToValidate) => boolean; -}; - -export const validHex = (value: unknown) => - typeof value === 'string' && Boolean(value.match(/^0x[a-f0-9]+$/u)); -const isValidObject = (v: unknown): v is object => - typeof v === 'object' && v !== null; - -export const validateData = ( - validators: Validator[], - object: unknown, - urlUsed: string, - logError = true, -): object is ExpectedResponse => { - return validators.every(({ property, type, validator }) => { - const types = type.split('|'); - const propertyString = String(property); - - const valid = - isValidObject(object) && - types.some( - (_type) => - typeof object[propertyString as keyof typeof object] === _type, - ) && - (!validator || validator(object[propertyString as keyof typeof object])); +export const validateBridgeStatusResponse = (data: unknown) => { + const ChainIdSchema = union([number(), string()]); - if (!valid && logError) { - const value = isValidObject(object) - ? object[propertyString as keyof typeof object] - : undefined; - const typeString = isValidObject(object) - ? typeof object[propertyString as keyof typeof object] - : 'undefined'; - - console.error( - `response to GET ${urlUsed} invalid for property ${String(property)}; value was:`, - value, - '| type was: ', - typeString, - ); - } - return valid; + const AssetSchema = type({ + chainId: ChainIdSchema, + address: string(), + symbol: string(), + name: string(), + decimals: number(), + icon: optional(nullable(string())), }); -}; -export const validateResponse = ( - validators: Validator[], - data: unknown, - urlUsed: string, -): data is ExpectedResponse => { - if (data === null || data === undefined) { - return false; - } - return validateData(validators, data, urlUsed); -}; + const EmptyObjectSchema = object({}); -const assetValidators = [ - { - property: 'chainId', - type: 'number', - validator: (v: unknown): v is number => typeof v === 'number', - }, - { - property: 'address', - type: 'string', - validator: (v: unknown): v is string => isValidHexAddress(v as string), - }, - { - property: 'symbol', - type: 'string', - validator: (v: unknown): v is string => typeof v === 'string', - }, - { - property: 'name', - type: 'string', - validator: (v: unknown): v is string => typeof v === 'string', - }, - { - property: 'decimals', - type: 'number', - validator: (v: unknown): v is number => typeof v === 'number', - }, - { - property: 'icon', - // typeof null === 'object' - type: 'string|undefined|object', - validator: (v: unknown): v is string | undefined | object => - v === undefined || v === null || typeof v === 'string', - }, -]; - -const assetValidator = (v: unknown): v is Asset => - validateResponse(assetValidators, v, BRIDGE_STATUS_BASE_URL); - -const srcChainStatusValidators = [ - { - property: 'chainId', - // For some reason, API returns destChain.chainId as a string, it's a number everywhere else - type: 'number|string', - validator: (v: unknown): v is number | string => - typeof v === 'number' || typeof v === 'string', - }, - { - property: 'txHash', - type: 'string', - validator: validHex, - }, - { - property: 'amount', - type: 'string|undefined', - validator: (v: unknown): v is string | undefined => - v === undefined || typeof v === 'string', - }, - { - property: 'token', - type: 'object|undefined', - validator: (v: unknown): v is object | undefined => - v === undefined || - (v && typeof v === 'object' && Object.keys(v).length === 0) || - assetValidator(v), - }, -]; + const SrcChainStatusSchema = type({ + chainId: ChainIdSchema, + txHash: optional(string()), + amount: optional(string()), + token: optional(union([EmptyObjectSchema, AssetSchema])), + }); -const srcChainStatusValidator = (v: unknown): v is SrcChainStatus => - validateResponse( - srcChainStatusValidators, - v, - BRIDGE_STATUS_BASE_URL, - ); + const DestChainStatusSchema = type({ + chainId: ChainIdSchema, + txHash: optional(string()), + amount: optional(string()), + token: optional(union([EmptyObjectSchema, AssetSchema])), + }); -const destChainStatusValidators = [ - { - property: 'chainId', - // For some reason, API returns destChain.chainId as a string, it's a number everywhere else - type: 'number|string', - validator: (v: unknown): v is number | string => - typeof v === 'number' || typeof v === 'string', - }, - { - property: 'amount', - type: 'string|undefined', - validator: (v: unknown): v is string | undefined => - v === undefined || typeof v === 'string', - }, - { - property: 'txHash', - type: 'string|undefined', - validator: (v: unknown): v is string | undefined => - v === undefined || typeof v === 'string', - }, - { - property: 'token', - type: 'object|undefined', - validator: (v: unknown): v is Asset | undefined => - v === undefined || - (v && typeof v === 'object' && Object.keys(v).length === 0) || - assetValidator(v), - }, -]; + const RefuelStatusResponseSchema = object(); -const destChainStatusValidator = (v: unknown): v is DestChainStatus => - validateResponse( - destChainStatusValidators, - v, - BRIDGE_STATUS_BASE_URL, - ); + const StatusResponseSchema = type({ + status: enums(Object.values(StatusTypes)), + srcChain: SrcChainStatusSchema, + destChain: optional(DestChainStatusSchema), + bridge: optional(enums(Object.values(BridgeId))), + isExpectedToken: optional(boolean()), + isUnrecognizedRouterAddress: optional(boolean()), + refuel: optional(RefuelStatusResponseSchema), + }); -export const validators = [ - { - property: 'status', - type: 'string', - validator: (v: unknown): v is StatusTypes => - Object.values(StatusTypes).includes(v as StatusTypes), - }, - { - property: 'srcChain', - type: 'object', - validator: srcChainStatusValidator, - }, - { - property: 'destChain', - type: 'object|undefined', - validator: (v: unknown): v is object | unknown => - v === undefined || destChainStatusValidator(v), - }, - { - property: 'bridge', - type: 'string|undefined', - validator: (v: unknown): v is BridgeId | undefined => - v === undefined || Object.values(BridgeId).includes(v as BridgeId), - }, - { - property: 'isExpectedToken', - type: 'boolean|undefined', - validator: (v: unknown): v is boolean | undefined => - v === undefined || typeof v === 'boolean', - }, - { - property: 'isUnrecognizedRouterAddress', - type: 'boolean|undefined', - validator: (v: unknown): v is boolean | undefined => - v === undefined || typeof v === 'boolean', - }, - // TODO: add refuel validator - // { - // property: 'refuel', - // type: 'object', - // validator: (v: unknown) => Object.values(RefuelStatusResponse).includes(v), - // }, -]; + assert(data, StatusResponseSchema); +}; diff --git a/yarn.lock b/yarn.lock index 6965cd978cd..53454d133f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2595,6 +2595,7 @@ __metadata: "@metamask/metamask-eth-abis": "npm:^3.1.1" "@metamask/network-controller": "npm:^22.2.1" "@metamask/polling-controller": "npm:^12.0.3" + "@metamask/superstruct": "npm:^3.1.0" "@metamask/transaction-controller": "npm:^46.0.0" "@metamask/utils": "npm:^11.2.0" "@types/jest": "npm:^27.4.1" @@ -2626,6 +2627,7 @@ __metadata: "@metamask/controller-utils": "npm:^11.5.0" "@metamask/network-controller": "npm:^22.2.1" "@metamask/polling-controller": "npm:^12.0.3" + "@metamask/superstruct": "npm:^3.1.0" "@metamask/transaction-controller": "npm:^46.0.0" "@metamask/utils": "npm:^11.2.0" "@types/jest": "npm:^27.4.1"