Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,22 @@ module.exports = {
message:
'Use ES private class fields (#field) instead of TypeScript private keyword.',
},
// Mirror @metamask/eslint-config base rule — prevents `'x' in obj`
// type-guards that would land in core as new `no-restricted-syntax`
// suppressions. Use `hasProperty()` from `@metamask/utils` instead.
{
selector: "BinaryExpression[operator='in']",
message:
'The "in" operator is not allowed. Use `hasProperty()` from `@metamask/utils` instead.',
},
{
selector: 'WithStatement',
message: 'With statements are not allowed',
},
{
selector: 'SequenceExpression',
message: 'Sequence expressions are not allowed',
},
],
'id-denylist': [
'error',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
PERPS_ERROR_CODES,
wait,
TradingReadinessCache,
withPerpsConnectionAttemptContext,
type ReconnectOptions,
} from '@metamask/perps-controller';
import { getStreamManagerInstance } from '../providers/PerpsStreamManager';
Expand All @@ -34,7 +35,6 @@ import {
} from '../selectors/perpsController';
import { selectHip3ConfigVersion } from '../selectors/featureFlags';
import { ensureError } from '../../../../util/errorUtils';
import { withPerpsConnectionAttemptContext } from '../../../../util/perpsConnectionAttemptContext';
import { PERPS_CONNECTION_SOURCE } from '../constants/perpsConfig';

interface ConnectOptions {
Expand Down
4 changes: 3 additions & 1 deletion app/controllers/perps/PerpsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1694,7 +1694,9 @@ export class PerpsController extends BaseController<
// IMPORTANT: Must use import() — NOT require() — for core/extension tree-shaking.
// require() is synchronous and bundlers include it in the main bundle.
// import() enables true code splitting so MYX is excluded when not enabled.
this.#myxRegistrationPromise = import('./providers/MYXProvider')
this.#myxRegistrationPromise = import(
/* webpackIgnore: true */ './providers/MYXProvider'
)
.then(({ MYXProvider }) => {
this.registerMYXProvider(MYXProvider);
return undefined;
Expand Down
13 changes: 4 additions & 9 deletions app/controllers/perps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,15 +464,10 @@ export {
} from './utils';
export type { HyperLiquidMarketData } from './utils';
export {
adaptMarketFromMYX,
adaptPriceFromMYX,
adaptMarketDataFromMYX,
filterMYXExclusiveMarkets,
isOverlappingMarket,
buildPoolSymbolMap,
buildSymbolPoolsMap,
extractSymbolFromPoolId,
} from './utils';
getPerpsConnectionAttemptContext,
withPerpsConnectionAttemptContext,
} from './utils/perpsConnectionAttemptContext';
export type { PerpsConnectionAttemptContext } from './utils/perpsConnectionAttemptContext';
export {
MAX_MARKET_PATTERN_LENGTH,
escapeRegex,
Expand Down
25 changes: 18 additions & 7 deletions app/controllers/perps/providers/HyperLiquidProvider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CaipAccountId } from '@metamask/utils';
import { CaipAccountId, hasProperty } from '@metamask/utils';
import type { Hex } from '@metamask/utils';
import { v4 as uuidv4 } from 'uuid';

Expand Down Expand Up @@ -1968,14 +1968,19 @@ export class HyperLiquidProvider implements PerpsProvider {

// Check order status
const status = result.response?.data?.statuses?.[0];
if (isStatusObject(status) && 'error' in status) {
if (isStatusObject(status) && hasProperty(status, 'error')) {
return { success: false, error: String(status.error) };
}

// Note: `in` narrows the HyperLiquid SDK discriminated union to the
// branch that has `filled`; `hasProperty` only narrows the key and
// types `status.filled` as `unknown`, which loses access to `.totalSz`.
/* eslint-disable no-restricted-syntax */
const filledSize =
isStatusObject(status) && 'filled' in status
? parseFloat(status.filled?.totalSz ?? '0')
: 0;
/* eslint-enable no-restricted-syntax */

this.#deps.debugLogger.log(
'HyperLiquidProvider: USDC→USDH swap completed',
Expand Down Expand Up @@ -3426,10 +3431,15 @@ export class HyperLiquidProvider implements PerpsProvider {
}

const status = result.response?.data?.statuses?.[0];
// Note: `in` narrows the HyperLiquid SDK discriminated union to the
// branch that has the property; `hasProperty` types the property as
// `unknown`, losing downstream access to `.oid`, `.totalSz`, `.avgPx`.
/* eslint-disable no-restricted-syntax */
const restingOrder =
isStatusObject(status) && 'resting' in status ? status.resting : null;
const filledOrder =
isStatusObject(status) && 'filled' in status ? status.filled : null;
/* eslint-enable no-restricted-syntax */

// Success - auto-rebalance excess funds
if (isHip3Order && transferInfo && dexName) {
Expand Down Expand Up @@ -4143,7 +4153,8 @@ export class HyperLiquidProvider implements PerpsProvider {
const { statuses } = result.response.data;
const successCount = statuses.filter(
(stat) =>
isStatusObject(stat) && ('filled' in stat || 'resting' in stat),
isStatusObject(stat) &&
(hasProperty(stat, 'filled') || hasProperty(stat, 'resting')),
).length;
const failureCount = statuses.length - successCount;

Expand All @@ -4153,7 +4164,7 @@ export class HyperLiquidProvider implements PerpsProvider {
const status = statuses[i];
const isSuccess =
isStatusObject(status) &&
('filled' in status || 'resting' in status);
(hasProperty(status, 'filled') || hasProperty(status, 'resting'));

if (isSuccess && hip3Transfers[i]) {
const { sourceDex, freedMargin } = hip3Transfers[i];
Expand All @@ -4179,9 +4190,9 @@ export class HyperLiquidProvider implements PerpsProvider {
symbol: positionsToClose[index].symbol,
success:
isStatusObject(status) &&
('filled' in status || 'resting' in status),
(hasProperty(status, 'filled') || hasProperty(status, 'resting')),
error:
isStatusObject(status) && 'error' in status
isStatusObject(status) && hasProperty(status, 'error')
? String(status.error)
: undefined,
})),
Expand Down Expand Up @@ -5998,7 +6009,7 @@ export class HyperLiquidProvider implements PerpsProvider {
// Extract HIP-3 DEX names (filter out null which is main DEX)
const hip3DexNames: string[] = [];
allDexs.forEach((dex) => {
if (dex !== null && 'name' in dex) {
if (dex !== null && hasProperty(dex, 'name')) {
hip3DexNames.push(dex.name);
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
WebSocketTransport,
} from '@nktkas/hyperliquid';

import { getPerpsConnectionAttemptContext } from '../../../util/perpsConnectionAttemptContext';
import { CandlePeriod, calculateCandleCount } from '../constants/chartConfig';
import { HYPERLIQUID_TRANSPORT_CONFIG } from '../constants/hyperLiquidConfig';
import { PERPS_CONSTANTS } from '../constants/perpsConfig';
Expand All @@ -20,6 +19,7 @@ import type {
import type { HyperLiquidNetwork } from '../types/config';
import type { CandleData } from '../types/perps-types';
import { ensureError } from '../utils/errorUtils';
import { getPerpsConnectionAttemptContext } from '../utils/perpsConnectionAttemptContext';

/**
* Maximum number of reconnection attempts before giving up.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { CaipAccountId } from '@metamask/utils';
import { hasProperty } from '@metamask/utils';
import type {
ISubscription,
AllMidsWsEvent,
Expand Down Expand Up @@ -2583,9 +2584,9 @@ export class HyperLiquidSubscriptionService {
const isPerpsContext = (
event: ActiveAssetCtxWsEvent | ActiveSpotAssetCtxWsEvent,
): event is ActiveAssetCtxWsEvent =>
'funding' in event.ctx &&
'openInterest' in event.ctx &&
'oraclePx' in event.ctx;
hasProperty(event.ctx, 'funding') &&
hasProperty(event.ctx, 'openInterest') &&
hasProperty(event.ctx, 'oraclePx');

const { ctx } = data;

Expand Down Expand Up @@ -2904,7 +2905,7 @@ export class HyperLiquidSubscriptionService {
// Use cached meta to map ctxs array indices to symbols (no REST API call!)
validatedMeta.universe.forEach((asset, index) => {
const ctx = data.ctxs[index];
if (ctx && 'funding' in ctx) {
if (ctx && hasProperty(ctx, 'funding')) {
// This is a perps context
const ctxPrice = ctx.midPx ?? ctx.markPx;
const openInterestUSD = calculateOpenInterestUSD(
Expand Down
3 changes: 2 additions & 1 deletion app/controllers/perps/services/TradingService.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { hasProperty } from '@metamask/utils';
import { v4 as uuidv4 } from 'uuid';

import type { RewardsIntegrationService } from './RewardsIntegrationService';
Expand Down Expand Up @@ -1516,7 +1517,7 @@ export class TradingService {

this.#deps.debugLogger.log('[closePositions] Batch method check', {
providerType: provider.protocolId,
hasBatchMethod: 'closePositions' in provider,
hasBatchMethod: hasProperty(provider, 'closePositions'),
providerKeys: Object.keys(provider).filter((key) =>
key.includes('close'),
),
Expand Down
5 changes: 3 additions & 2 deletions app/controllers/perps/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { hasProperty } from '@metamask/utils';
import type {
CaipAccountId,
CaipChainId,
Expand Down Expand Up @@ -1643,8 +1644,8 @@ export function isVersionGatedFeatureFlag(
return (
typeof value === 'object' &&
value !== null &&
'enabled' in value &&
'minimumVersion' in value &&
hasProperty(value, 'enabled') &&
hasProperty(value, 'minimumVersion') &&
typeof (value as { enabled: unknown }).enabled === 'boolean' &&
typeof (value as { minimumVersion: unknown }).minimumVersion === 'string'
);
Expand Down
5 changes: 3 additions & 2 deletions app/controllers/perps/types/transactionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Shared transaction types for Perps deposits and withdrawals
* Provides a unified structure while maintaining separate use cases
*/
import { hasProperty } from '@metamask/utils';

/**
* Base type with core properties shared between all transaction results
Expand Down Expand Up @@ -62,7 +63,7 @@ export type TransactionRecord = {
export function isTransactionRecord(
result: LastTransactionResult | TransactionRecord,
): result is TransactionRecord {
return 'id' in result && 'status' in result;
return hasProperty(result, 'id') && hasProperty(result, 'status');
}

/**
Expand All @@ -74,5 +75,5 @@ export function isTransactionRecord(
export function isLastTransactionResult(
result: LastTransactionResult | TransactionRecord,
): result is LastTransactionResult {
return !('id' in result) || !('status' in result);
return !hasProperty(result, 'id') || !hasProperty(result, 'status');
}
3 changes: 2 additions & 1 deletion app/controllers/perps/utils/errorUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Utility functions for error handling across the application.
* These are general-purpose utilities, not domain-specific.
*/
import { hasProperty } from '@metamask/utils';

/**
* Ensures we have a proper Error object for logging.
Expand All @@ -26,7 +27,7 @@ export function ensureError(error: unknown, context?: string): Error {
return new Error(error);
}
return new Error(
typeof error === 'object' && error !== null && 'message' in error
typeof error === 'object' && error !== null && hasProperty(error, 'message')
? String((error as { message: unknown }).message)
: 'Unknown error',
);
Expand Down
9 changes: 6 additions & 3 deletions app/controllers/perps/utils/hyperLiquidAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Hex, isHexString } from '@metamask/utils';
import { hasProperty, Hex, isHexString } from '@metamask/utils';

import {
countSignificantFigures,
Expand Down Expand Up @@ -438,10 +438,13 @@ export function adaptHyperLiquidLedgerUpdateToUserHistoryItem(
let amount = '0';
let asset = 'USDC';

if ('usdc' in update.delta && update.delta.usdc) {
if (hasProperty(update.delta, 'usdc') && update.delta.usdc) {
amount = Math.abs(parseFloat(update.delta.usdc)).toString();
}
if ('coin' in update.delta && typeof update.delta.coin === 'string') {
if (
hasProperty(update.delta, 'coin') &&
typeof update.delta.coin === 'string'
) {
asset = update.delta.coin;
}

Expand Down
4 changes: 4 additions & 0 deletions app/controllers/perps/utils/hyperLiquidValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,10 @@ export function applyPathFilters(
});
}

// Note: `in` is the idiomatic TypeScript way to narrow a string to
// `keyof typeof` for indexed access; `hasProperty` types the indexed
// result as `unknown` and loses the `{ testnet, mainnet }` shape.
/* eslint-disable-next-line no-restricted-syntax */
if (params.symbol && params.symbol in HYPERLIQUID_ASSET_CONFIGS) {
const config =
HYPERLIQUID_ASSET_CONFIGS[
Expand Down
1 change: 0 additions & 1 deletion app/controllers/perps/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ export * from './hyperLiquidOrderBookProcessor';
export * from './hyperLiquidValidation';
export * from './idUtils';
export * from './marketDataTransform';
export * from './myxAdapter';
export * from './marketUtils';
export * from './orderCalculations';
export * from './rewardsUtils';
Expand Down
4 changes: 3 additions & 1 deletion app/controllers/perps/utils/marketDataTransform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* Portable: no mobile-specific imports.
* Formatters are injected via MarketDataFormatters interface.
*/
import { hasProperty } from '@metamask/utils';

import { parseAssetName } from './hyperLiquidAdapter';
import { HYPERLIQUID_CONFIG } from '../constants/hyperLiquidConfig';
import { PERPS_CONSTANTS } from '../constants/perpsConfig';
Expand Down Expand Up @@ -221,7 +223,7 @@ export function transformMarketData(
// Get current funding rate from assetCtx - this is the actual current funding rate
let fundingRate: number | undefined;

if (assetCtx && 'funding' in assetCtx) {
if (assetCtx && hasProperty(assetCtx, 'funding')) {
fundingRate = parseFloat(assetCtx.funding);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export interface PerpsConnectionAttemptContext {
export type PerpsConnectionAttemptContext = {
source: string;
suppressError: boolean;
}
};

let currentAttemptContext: PerpsConnectionAttemptContext | null = null;

Expand Down
Loading
Loading