Skip to content

Commit

Permalink
chore: use nested validators for readability
Browse files Browse the repository at this point in the history
  • Loading branch information
micaelae committed Oct 28, 2024
1 parent 304a039 commit 2961691
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 109 deletions.
41 changes: 32 additions & 9 deletions ui/pages/bridge/bridge.util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@ describe('Bridge utils', () => {

it('should use fallback bridge feature flags if response is unexpected', async () => {
const mockResponse = {
flag1: true,
flag2: false,
'extension-support': 25,
'src-network-allowlist': ['a', 'b', 1],
a: 'b',
'dest-network-allowlist': [1, 137, 59144, 11111],
};

(fetchWithCache as jest.Mock).mockResolvedValue(mockResponse);
Expand Down Expand Up @@ -183,9 +185,11 @@ describe('Bridge utils', () => {
});

it('should fetch bridge quotes successfully, with approvals', async () => {
(fetchWithCache as jest.Mock).mockResolvedValue(
mockBridgeQuotesErc20Erc20,
);
(fetchWithCache as jest.Mock).mockResolvedValue([
...mockBridgeQuotesErc20Erc20,
{ ...mockBridgeQuotesErc20Erc20[0], approval: null },
{ ...mockBridgeQuotesErc20Erc20[0], trade: null },
]);

const result = await fetchBridgeQuotes({
walletAddress: '0x123',
Expand All @@ -211,11 +215,30 @@ describe('Bridge utils', () => {
});

it('should filter out malformed bridge quotes', async () => {
(fetchWithCache as jest.Mock).mockResolvedValue(
mockBridgeQuotesErc20Erc20.map(
(fetchWithCache as jest.Mock).mockResolvedValue([
...mockBridgeQuotesErc20Erc20,
...mockBridgeQuotesErc20Erc20.map(
({ quote, ...restOfQuote }) => restOfQuote,
),
);
{
...mockBridgeQuotesErc20Erc20[0],
quote: {
srcAsset: {
...mockBridgeQuotesErc20Erc20[0].quote.srcAsset,
decimals: undefined,
},
},
},
{
...mockBridgeQuotesErc20Erc20[1],
quote: {
srcAsset: {
...mockBridgeQuotesErc20Erc20[1].quote.destAsset,
address: undefined,
},
},
},
]);

const result = await fetchBridgeQuotes({
walletAddress: '0x123',
Expand All @@ -237,7 +260,7 @@ describe('Bridge utils', () => {
functionName: 'fetchBridgeQuotes',
});

expect(result).toStrictEqual([]);
expect(result).toStrictEqual(mockBridgeQuotesErc20Erc20);
});
});
});
43 changes: 33 additions & 10 deletions ui/pages/bridge/bridge.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,29 @@ import {
SWAPS_CHAINID_DEFAULT_TOKEN_MAP,
SwapsTokenObject,
} from '../../../shared/constants/swaps';
import { TOKEN_VALIDATORS } from '../swaps/swaps.util';
import {
isSwapsDefaultTokenAddress,
isSwapsDefaultTokenSymbol,
} from '../../../shared/modules/swaps.utils';
import {
BridgeAsset,
BridgeFlag,
FeatureFlagResponse,
FeeData,
FeeType,
Quote,
QuoteRequest,
QuoteResponse,
TxData,
} from './types';
import {
FEATURE_FLAG_VALIDATORS,
QUOTE_VALIDATORS,
TX_DATA_VALIDATORS,
TOKEN_VALIDATORS,
validateResponse,
QUOTE_RESPONSE_VALIDATORS,
FEE_DATA_VALIDATORS,
} from './utils/validators';

const CLIENT_ID_HEADER = { 'X-Client-Id': BRIDGE_CLIENT_ID };
Expand Down Expand Up @@ -102,13 +110,9 @@ export async function fetchBridgeTokens(
transformedTokens[nativeToken.address] = nativeToken;
}

tokens.forEach((token: SwapsTokenObject) => {
tokens.forEach((token: unknown) => {
if (
validateResponse<SwapsTokenObject, string>(
TOKEN_VALIDATORS,
token,
url,
) &&
validateResponse<SwapsTokenObject>(TOKEN_VALIDATORS, token, url) &&
!(
isSwapsDefaultTokenSymbol(token.symbol, chainId) ||
isSwapsDefaultTokenAddress(token.address, chainId)
Expand Down Expand Up @@ -143,8 +147,27 @@ export async function fetchBridgeQuotes(
functionName: 'fetchBridgeQuotes',
});

const filteredQuotes = quotes.filter((quote: QuoteResponse) =>
validateResponse<QuoteResponse, object>(QUOTE_VALIDATORS, quote, url),
);
const filteredQuotes = quotes.filter((quoteResponse: QuoteResponse) => {
const { quote, approval, trade } = quoteResponse;
return (
validateResponse<QuoteResponse>(
QUOTE_RESPONSE_VALIDATORS,
quoteResponse,
url,
) &&
validateResponse<Quote>(QUOTE_VALIDATORS, quote, url) &&
validateResponse<BridgeAsset>(TOKEN_VALIDATORS, quote.srcAsset, url) &&
validateResponse<BridgeAsset>(TOKEN_VALIDATORS, quote.destAsset, url) &&
validateResponse<TxData>(TX_DATA_VALIDATORS, trade, url) &&
validateResponse<FeeData>(
FEE_DATA_VALIDATORS,
quote.feeData[FeeType.METABRIDGE],
url,
) &&
(approval
? validateResponse<TxData>(TX_DATA_VALIDATORS, approval, url)
: true)
);
});
return filteredQuotes;
}
4 changes: 2 additions & 2 deletions ui/pages/bridge/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,11 @@ enum ChainId {
LINEA = 59144,
}

enum FeeType {
export enum FeeType {
METABRIDGE = 'metabridge',
REFUEL = 'refuel',
}
type FeeData = {
export type FeeData = {
amount: string;
asset: BridgeAsset;
};
Expand Down
156 changes: 68 additions & 88 deletions ui/pages/bridge/utils/validators.ts
Original file line number Diff line number Diff line change
@@ -1,113 +1,93 @@
import { validateData } from '../../../../shared/lib/swaps-utils';
import { BridgeAsset, BridgeFlag, Quote, TxData } from '../types';
import { isStrictHexString } from '@metamask/utils';
import { isValidHexAddress as isValidHexAddress_ } from '@metamask/controller-utils';
import {
truthyDigitString,
validateData,
} from '../../../../shared/lib/swaps-utils';
import { BridgeFlag } from '../types';

type Validator<ExpectedResponse, ResponseDataType> = {
type Validator<ExpectedResponse> = {
property: keyof ExpectedResponse | string;
type: string;
validator: (value: ResponseDataType) => boolean;
validator?: (value: unknown) => boolean;
};

export const validateResponse = <ExpectedResponse, ResponseDataType = unknown>(
validators: Validator<ExpectedResponse, ResponseDataType>[],
export const validateResponse = <ExpectedResponse>(
validators: Validator<ExpectedResponse>[],
data: unknown,
urlUsed: string,
): data is ExpectedResponse => {
return validateData(validators, data, urlUsed);
};

export const QUOTE_VALIDATORS = [
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 });

export const FEATURE_FLAG_VALIDATORS = [
{ property: BridgeFlag.EXTENSION_SUPPORT, type: 'boolean' },
{
property: 'quote',
property: BridgeFlag.NETWORK_SRC_ALLOWLIST,
type: 'object',
validator: (v: unknown): v is Quote =>
typeof v === 'object' &&
v !== null &&
v !== undefined &&
['requestId', 'srcTokenAmount', 'destTokenAmount', 'bridgeId'].every(
(k) => k in v && typeof v[k as keyof typeof v] === 'string',
) &&
['srcTokenAmount', 'destTokenAmount'].every(
(k) =>
k in v &&
typeof v[k as keyof typeof v] === 'string' &&
/^\d+$/u.test(v[k as keyof typeof v] as string),
) &&
['srcAsset', 'destAsset'].every(
(k) =>
k in v &&
typeof v[k as keyof typeof v] === 'object' &&
'address' in v[k as keyof typeof v] &&
typeof (v[k as keyof typeof v] as BridgeAsset).address === 'string' &&
'decimals' in v[k as keyof typeof v] &&
typeof (v[k as keyof typeof v] as BridgeAsset).decimals === 'number',
),
},
{
property: 'approval',
type: 'object|undefined',
validator: (v: unknown): v is TxData | undefined =>
v === undefined ||
(v
? typeof v === 'object' &&
'gasLimit' in v &&
typeof v.gasLimit === 'number' &&
'to' in v &&
typeof v.to === 'string' &&
'from' in v &&
typeof v.from === 'string' &&
'data' in v &&
typeof v.data === 'string'
: false),
validator: (v: unknown): v is number[] =>
isValidObject(v) && Object.values(v).every(isValidNumber),
},
{
property: 'trade',
property: BridgeFlag.NETWORK_DEST_ALLOWLIST,
type: 'object',
validator: (v: unknown): v is TxData =>
v
? typeof v === 'object' &&
'gasLimit' in v &&
typeof v.gasLimit === 'number' &&
'to' in v &&
typeof v.to === 'string' &&
'from' in v &&
typeof v.from === 'string' &&
'data' in v &&
typeof v.data === 'string' &&
'value' in v &&
typeof v.value === 'string' &&
v.value.startsWith('0x')
: false,
},
{
property: 'estimatedProcessingTimeInSeconds',
type: 'number',
validator: (v: unknown): v is number[] =>
Object.values(v as { [s: string]: unknown }).every(
(i) => typeof i === 'number',
),
isValidObject(v) && Object.values(v).every(isValidNumber),
},
];

export const FEATURE_FLAG_VALIDATORS = [
{
property: BridgeFlag.EXTENSION_SUPPORT,
type: 'boolean',
validator: (v: unknown) => typeof v === 'boolean',
},
export const TOKEN_VALIDATORS = [
{ property: 'decimals', type: 'number' },
{ property: 'address', type: 'string', validator: isValidHexAddress },
{
property: BridgeFlag.NETWORK_SRC_ALLOWLIST,
type: 'object',
validator: (v: unknown): v is number[] =>
Object.values(v as { [s: string]: unknown }).every(
(i) => typeof i === 'number',
),
property: 'symbol',
type: 'string',
validator: (v: unknown) => isValidString(v) && v.length <= 12,
},
];

export const QUOTE_RESPONSE_VALIDATORS = [
{ property: 'quote', type: 'object', validator: isValidObject },
{ property: 'estimatedProcessingTimeInSeconds', type: 'number' },
{
property: BridgeFlag.NETWORK_DEST_ALLOWLIST,
type: 'object',
validator: (v: unknown): v is number[] =>
Object.values(v as { [s: string]: unknown }).every(
(i) => typeof i === 'number',
),
property: 'approval',
type: 'object|undefined',
validator: (v: unknown) => v === undefined || isValidObject(v),
},
{ property: 'trade', type: 'object', validator: isValidObject },
];

export const QUOTE_VALIDATORS = [
{ 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 = [
{ property: 'amount', type: 'string', validator: truthyDigitString },
{ property: 'asset', type: 'object', validator: isValidObject },
];

export const TX_DATA_VALIDATORS = [
{ 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 },
];

0 comments on commit 2961691

Please sign in to comment.