diff --git a/app/components/UI/Perps/PERPS_ARCH.md b/app/components/UI/Perps/PERPS_ARCH.md
index 45c8ffaf9668..0419bf8a0ef8 100644
--- a/app/components/UI/Perps/PERPS_ARCH.md
+++ b/app/components/UI/Perps/PERPS_ARCH.md
@@ -78,11 +78,32 @@ Before creating a new hook:
Single WebSocket subscriptions shared across all components with component-level debouncing. This prevents subscription interference and reduces WebSocket connections by 90%.
+### WebSocket Pre-warming (Persistent Connections)
+
+Pre-warming creates persistent subscriptions that stay alive throughout the Perps session:
+
+- **Problem**: WebSocket subscriptions start on-demand, causing ~10 second delays before data arrives
+- **Solution**: Create persistent subscriptions with no-op callbacks when entering Perps environment
+- **Implementation**:
+ - `prewarm()` creates actual subscriptions that keep connections alive
+ - `PerpsConnectionManager` stores cleanup functions and only calls them when leaving Perps
+- **Result**: Connections stay alive, cache continuously populated, instant data for all components
+
+### Single WebSocket Connection Architecture
+
+To minimize network overhead and ensure data consistency:
+
+- **Shared webData2**: Single subscription provides both positions (with TP/SL) and orders data
+- **Reference Counting**: Tracks subscriber count to maintain connection while needed
+- **Automatic Cleanup**: Disconnects when last subscriber unsubscribes
+- **Result**: One WebSocket connection per data type instead of per component
+
### Provider Setup
- `PerpsStreamProvider` wraps all routes in `/routes/index.tsx`
- Provides access to stream channels without holding state
- No re-renders propagated to parent components
+- `PerpsConnectionManager` pre-loads critical subscriptions on connection
### Stream Hooks
@@ -92,13 +113,13 @@ Located in `/hooks/stream/`:
// Each component sets its own update rate
const prices = useLivePrices({
symbols: ['BTC', 'ETH'],
- debounceMs: 10000, // 10s for order view
+ throttleMs: 10000, // 10s for order view
});
```
Available hooks:
-- `useLivePrices(options)` - Real-time prices with custom debounce
+- `useLivePrices(options)` - Real-time prices with custom throttle
- `useLiveOrders(options)` - Order updates (future)
- `useLivePositions(options)` - Position updates (future)
- `useLiveFills(options)` - Fill notifications (future)
@@ -110,11 +131,12 @@ Available hooks:
- **Component-level control** - Different rates for different views
- **Instant first render** - Cached data available immediately
- **Zero parent re-renders** - Updates go directly to subscribers
+- **No empty initial states** - Pre-warmed subscriptions provide data immediately
### Migration Path
1. Replace `usePerpsPrices` with `useLivePrices`
-2. Set appropriate debounce for each view:
+2. Set appropriate throttle for each view:
- Order entry: 10000ms (stable prices)
- Market list: 2000ms (responsive updates)
- Market details: 500ms (near real-time)
@@ -130,6 +152,8 @@ Available hooks:
├─────────────────────────────────────┤
│ Stream Manager (WebSocket) │ <- NEW LAYER
├─────────────────────────────────────┤
+│ Connection Manager (Pre-warming) │ <- NEW LAYER
+├─────────────────────────────────────┤
│ Controller (Business) │
├─────────────────────────────────────┤
│ Provider (Protocol) │
diff --git a/app/components/UI/Perps/PERPS_NAVIGATION_ARCHITECTURE.md b/app/components/UI/Perps/PERPS_NAVIGATION_ARCHITECTURE.md
new file mode 100644
index 000000000000..74194bc3a573
--- /dev/null
+++ b/app/components/UI/Perps/PERPS_NAVIGATION_ARCHITECTURE.md
@@ -0,0 +1,250 @@
+# Perps Navigation Architecture
+
+> Visual documentation of the MetaMask Mobile Perps feature navigation flow and screen relationships
+
+## 📊 Navigation Flow Diagram
+
+```mermaid
+graph TB
+ %% Entry Points
+ Start[App Start] --> MainTab[Main Tab Navigation]
+ MainTab --> PerpsRoot[PERPS.ROOT]
+
+ %% Main Hub - PerpsView (Trading View)
+ PerpsRoot --> TradingView[PERPS.TRADING_VIEW
PerpsView]
+
+ %% Primary Navigation from Trading View
+ TradingView --> Markets[PERPS.MARKETS
PerpsMarketListView]
+ TradingView --> Positions[PERPS.POSITIONS
PerpsPositionsView]
+ TradingView --> Withdraw[PERPS.WITHDRAW
PerpsWithdrawView]
+ TradingView --> Order[PERPS.ORDER
PerpsOrderView]
+
+ %% Market Flow
+ Markets --> MarketDetails[PERPS.MARKET_DETAILS
PerpsMarketDetailsView]
+ Markets --> Tutorial[PERPS.TUTORIAL
PerpsTutorialCarousel]
+
+ %% Market Details Actions
+ MarketDetails --> Order
+ MarketDetails --> DepositFlow[Deposit Flow
via Confirmations]
+
+ %% Order Flow
+ Order --> QuoteExpired[PERPS.MODALS.QUOTE_EXPIRED_MODAL
PerpsQuoteExpiredModal]
+ Order --> BackToMarketDetails[Back to Market Details]
+
+ %% Transaction History (Not directly linked in navigation)
+ Transactions[Transaction Views
Not in main flow] -.-> PositionTx[PERPS.POSITION_TRANSACTION]
+ Transactions -.-> OrderTx[PERPS.ORDER_TRANSACTION]
+ Transactions -.-> FundingTx[PERPS.FUNDING_TRANSACTION]
+
+ %% Styling
+ classDef mainView fill:#e1f5e1,stroke:#4caf50,stroke-width:3px
+ classDef secondaryView fill:#e3f2fd,stroke:#2196f3,stroke-width:2px
+ classDef modalView fill:#fff3e0,stroke:#ff9800,stroke-width:2px
+ classDef unusedView fill:#ffebee,stroke:#f44336,stroke-width:2px,stroke-dasharray: 5 5
+
+ class TradingView mainView
+ class Markets,Positions,MarketDetails secondaryView
+ class QuoteExpired,Tutorial modalView
+ class Transactions,PositionTx,OrderTx,FundingTx unusedView
+```
+
+## 🏗️ Screen Hierarchy
+
+### Main Stack
+
+```
+Routes.PERPS.ROOT
+├── Routes.PERPS.TRADING_VIEW (PerpsView) - Main Hub
+│ ├── → Routes.PERPS.MARKETS
+│ ├── → Routes.PERPS.POSITIONS
+│ ├── → Routes.PERPS.WITHDRAW
+│ └── → Routes.PERPS.ORDER
+│
+├── Routes.PERPS.MARKETS (PerpsMarketListView)
+│ ├── → Routes.PERPS.MARKET_DETAILS
+│ └── → Routes.PERPS.TUTORIAL
+│
+├── Routes.PERPS.MARKET_DETAILS (PerpsMarketDetailsView)
+│ ├── → Routes.PERPS.ORDER (Long/Short)
+│ └── → Deposit Flow (via Confirmations)
+│
+├── Routes.PERPS.ORDER (PerpsOrderView)
+│ └── → Routes.PERPS.MODALS.QUOTE_EXPIRED_MODAL
+│
+└── Routes.PERPS.WITHDRAW (PerpsWithdrawView)
+```
+
+### Modal Stack
+
+```
+Routes.PERPS.MODALS.ROOT
+└── Routes.PERPS.MODALS.QUOTE_EXPIRED_MODAL (PerpsQuoteExpiredModal)
+```
+
+## 📋 Route Usage Analysis
+
+| Route | Component | Used In | Status |
+| ---------------------------------- | ---------------------------- | ---------------- | ----------- |
+| `PERPS.ROOT` | Navigation Root | App entry | ✅ Active |
+| `PERPS.TRADING_VIEW` | PerpsView | Initial route | ✅ Active |
+| `PERPS.MARKETS` | PerpsMarketListView | PerpsView | ✅ Active |
+| `PERPS.MARKET_DETAILS` | PerpsMarketDetailsView | MarketListView | ✅ Active |
+| `PERPS.POSITIONS` | PerpsPositionsView | PerpsView | ✅ Active |
+| `PERPS.ORDER` | PerpsOrderView | Multiple screens | ✅ Active |
+| `PERPS.WITHDRAW` | PerpsWithdrawView | PerpsView | ✅ Active |
+| `PERPS.TUTORIAL` | PerpsTutorialCarousel | MarketListView | ✅ Active |
+| `PERPS.MODALS.QUOTE_EXPIRED_MODAL` | PerpsQuoteExpiredModal | OrderView | ✅ Active |
+| `PERPS.DEPOSIT` | - | Routes only | ⚠️ Unused |
+| `PERPS.POSITION_DETAILS` | - | Routes only | ⚠️ Unused |
+| `PERPS.ORDER_HISTORY` | - | Routes only | ⚠️ Unused |
+| `PERPS.ORDER_DETAILS` | - | Routes only | ⚠️ Unused |
+| `PERPS.POSITION_TRANSACTION` | PerpsPositionTransactionView | TransactionsView | ❓ Orphaned |
+| `PERPS.ORDER_TRANSACTION` | PerpsOrderTransactionView | TransactionsView | ❓ Orphaned |
+| `PERPS.FUNDING_TRANSACTION` | PerpsFundingTransactionView | TransactionsView | ❓ Orphaned |
+
+## 🔄 Navigation Patterns
+
+### 1. **Main Trading Hub Pattern**
+
+```
+PerpsView (Trading View)
+ ├── View Markets → PerpsMarketListView
+ ├── View Positions → PerpsPositionsView
+ ├── Withdraw → PerpsWithdrawView
+ └── Quick Trade → PerpsOrderView
+```
+
+### 2. **Market Discovery Pattern**
+
+```
+PerpsMarketListView
+ ├── Select Market → PerpsMarketDetailsView
+ └── Tutorial → PerpsTutorialCarousel
+```
+
+### 3. **Trading Execution Pattern**
+
+```
+PerpsMarketDetailsView
+ ├── Long → PerpsOrderView (direction: 'long')
+ ├── Short → PerpsOrderView (direction: 'short')
+ └── Add Funds → Confirmations Screen
+```
+
+## 🧩 Key Components Usage
+
+### Tab Components (PerpsTabView)
+
+- **Location**: Embedded in PerpsView
+- **Purpose**: Main navigation hub with tabs
+- **Tabs**: Portfolio, Markets, Orders, Transactions
+
+### Market Components
+
+- **PerpsMarketCard**: Used in MarketListView
+- **PerpsMarketHeader**: Used in MarketDetailsView
+- **PerpsMarketTabs**: Used in MarketDetailsView (Position/Orders/Stats)
+
+### Position Components
+
+- **PerpsPositionCard**: Used in PositionsView, MarketTabs
+- **PerpsPositionSummary**: Used in PerpsView
+
+### Order Components
+
+- **PerpsOpenOrderCard**: Used in MarketTabs, OrdersView
+- **PerpsOrderConfirmation**: Used in OrderView
+
+## 🔍 Potential Cleanup Opportunities
+
+### 1. **Unused Routes** (Can be removed from Routes.ts)
+
+- `PERPS.DEPOSIT` - No implementation found
+- `PERPS.POSITION_DETAILS` - No implementation found
+- `PERPS.ORDER_HISTORY` - No implementation found
+- `PERPS.ORDER_DETAILS` - No implementation found
+
+### 2. **Orphaned Transaction Views**
+
+- `PerpsTransactionsView` - Parent component exists but not navigated to
+- `PerpsPositionTransactionView` - Child view not accessible
+- `PerpsOrderTransactionView` - Child view not accessible
+- `PerpsFundingTransactionView` - Child view not accessible
+
+**Note**: These transaction views might be intended for future use or are accessed through a different flow not visible in the main navigation.
+
+### 3. **Refactoring Opportunities**
+
+- **PerpsTabView**: Consider if this needs to be a separate view or can be integrated
+- **Transaction Views**: Either implement navigation or remove if not needed
+
+## 📱 Screen Flow Examples
+
+### Example 1: Opening a Position
+
+```
+1. PerpsView (Trading View)
+2. → PerpsMarketListView (Browse Markets)
+3. → PerpsMarketDetailsView (Select SOL)
+4. → PerpsOrderView (Long/Short)
+5. → Confirm → Back to PerpsMarketDetailsView
+```
+
+### Example 2: Managing Positions
+
+```
+1. PerpsView (Trading View)
+2. → PerpsPositionsView (View All Positions)
+3. → Select Position → Actions (Close/Edit)
+```
+
+### Example 3: First Time User
+
+```
+1. PerpsView (Trading View)
+2. → PerpsMarketListView
+3. → PerpsTutorialCarousel (Tutorial)
+4. → Back to Markets
+```
+
+## 🎯 Recommendations
+
+1. **Remove unused routes** from `Routes.ts` to clean up the codebase
+2. **Investigate transaction views** - Either implement proper navigation or remove if deprecated
+3. **Consider consolidating** PerpsTabView functionality if it's only used in one place
+4. **Document intended use** for transaction views if they're for future features
+5. **Add navigation tests** to ensure all routes are accessible and working
+
+## 📊 Component Dependencies
+
+```mermaid
+graph LR
+ subgraph Providers
+ ConnectionProvider[PerpsConnectionProvider]
+ StreamProvider[PerpsStreamProvider]
+ end
+
+ subgraph Views
+ PerpsView
+ MarketListView
+ MarketDetailsView
+ PositionsView
+ OrderView
+ end
+
+ subgraph Components
+ MarketCard
+ PositionCard
+ OrderCard
+ MarketTabs
+ end
+
+ ConnectionProvider --> Views
+ StreamProvider --> Views
+ Views --> Components
+```
+
+---
+
+_Last Updated: January 2025_
+_Note: This documentation reflects the current state of the codebase. Some routes exist in Routes.ts but have no corresponding implementation._
diff --git a/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.test.tsx b/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.test.tsx
index 3483ababf230..1a6d112a3f0d 100644
--- a/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.test.tsx
+++ b/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.test.tsx
@@ -11,6 +11,9 @@ import {
import { PerpsConnectionProvider } from '../../providers/PerpsConnectionProvider';
import Routes from '../../../../../constants/navigation/Routes';
+// Mock PerpsStreamManager
+jest.mock('../../providers/PerpsStreamManager');
+
// Create mock functions that can be modified during tests
const mockUsePerpsAccount = jest.fn();
const mockUseHasExistingPosition = jest.fn();
@@ -691,9 +694,9 @@ describe('PerpsMarketDetailsView', () => {
// Trigger the refresh
await refreshControl.props.onRefresh();
- // Should refresh candle data and orders data
+ // Should refresh candle data
+ // Note: Orders refresh automatically via WebSocket, no manual refresh needed
expect(mockRefreshCandleData).toHaveBeenCalledTimes(1);
- expect(mockRefreshOrders).toHaveBeenCalledTimes(1);
// Should not refresh position data when orders tab is active
expect(mockRefreshPosition).not.toHaveBeenCalled();
});
diff --git a/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.tsx b/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.tsx
index e8cef238b6b8..6c477d81fdf0 100644
--- a/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.tsx
+++ b/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.tsx
@@ -59,10 +59,10 @@ import { capitalize } from '../../../../../util/general';
import {
usePerpsAccount,
usePerpsConnection,
- usePerpsOpenOrders,
usePerpsPerformance,
usePerpsTrading,
} from '../../hooks';
+import { usePerpsLiveOrders } from '../../hooks/stream';
import PerpsMarketTabs from '../../components/PerpsMarketTabs/PerpsMarketTabs';
import PerpsNotificationTooltip from '../../components/PerpsNotificationTooltip';
import { isNotificationsFeatureEnabled } from '../../../../../util/notifications';
@@ -108,15 +108,11 @@ const PerpsMarketDetailsView: React.FC = () => {
const account = usePerpsAccount();
- const { isConnected } = usePerpsConnection();
+ usePerpsConnection();
const { depositWithConfirmation } = usePerpsTrading();
- // Get currently open orders for this market
- const { orders: ordersData, refresh: refreshOrders } = usePerpsOpenOrders({
- skipInitialFetch: !isConnected,
- enablePolling: true,
- pollingInterval: 5000, // Poll every 5 seconds for real-time updates
- });
+ // Get real-time open orders via WebSocket
+ const ordersData = usePerpsLiveOrders(); // Instant updates (no debouncing)
// Filter orders for the current market
const openOrders = useMemo(() => {
@@ -133,7 +129,7 @@ const PerpsMarketDetailsView: React.FC = () => {
const marketStats = usePerpsMarketStats(market?.symbol || '');
// Get candlestick data
- const { candleData, isLoadingHistory, priceData, refreshCandleData } =
+ const { candleData, isLoadingHistory, refreshCandleData } =
usePerpsPositionData({
coin: market?.symbol || '',
selectedDuration, // Time duration (1hr, 1D, 1W, etc.)
@@ -228,8 +224,7 @@ const PerpsMarketDetailsView: React.FC = () => {
break;
case 'orders':
- // Refresh orders data
- await refreshOrders();
+ // Orders update automatically via WebSocket, no refresh needed
break;
case 'statistics':
@@ -251,7 +246,6 @@ const PerpsMarketDetailsView: React.FC = () => {
}, [
activeTabId,
refreshPosition,
- refreshOrders,
marketStats,
candleData,
refreshCandleData,
@@ -328,8 +322,6 @@ const PerpsMarketDetailsView: React.FC = () => {
@@ -383,7 +375,8 @@ const PerpsMarketDetailsView: React.FC = () => {
unfilledOrders={openOrders}
onPositionUpdate={refreshPosition}
onActiveTabChange={setActiveTabId}
- priceData={priceData}
+ nextFundingTime={market?.nextFundingTime}
+ fundingIntervalHours={market?.fundingIntervalHours}
/>
@@ -471,4 +464,18 @@ const PerpsMarketDetailsView: React.FC = () => {
);
};
+// Enable Why Did You Render in development
+// Uncomment to enable WDYR for debugging re-renders
+// if (__DEV__) {
+// // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
+// const { shouldEnableWhyDidYouRender } = require('../../../../../../wdyr');
+// if (shouldEnableWhyDidYouRender()) {
+// // @ts-expect-error - whyDidYouRender is added by the WDYR library
+// PerpsMarketDetailsView.whyDidYouRender = {
+// logOnDifferentValues: true,
+// customName: 'PerpsMarketDetailsView',
+// };
+// }
+// }
+
export default PerpsMarketDetailsView;
diff --git a/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx b/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx
index 6c868d41d12c..3bcc30789484 100644
--- a/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx
+++ b/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx
@@ -82,7 +82,7 @@ import {
usePerpsOrderValidation,
usePerpsPerformance,
} from '../../hooks';
-import { useLivePrices } from '../../hooks/stream';
+import { usePerpsLivePrices } from '../../hooks/stream';
import { usePerpsEventTracking } from '../../hooks/usePerpsEventTracking';
import { usePerpsScreenTracking } from '../../hooks/usePerpsScreenTracking';
import { formatPrice } from '../../utils/formatUtils';
@@ -314,9 +314,9 @@ const PerpsOrderViewContentBase: React.FC = () => {
// Get real-time price data using new stream architecture
// Uses single WebSocket subscription with component-level debouncing
- const prices = useLivePrices({
+ const prices = usePerpsLivePrices({
symbols: [orderForm.asset],
- debounceMs: 10000, // 10 seconds for testing the architecture
+ throttleMs: 10000, // 10 seconds for testing the architecture
});
const currentPrice = prices[orderForm.asset];
diff --git a/app/components/UI/Perps/Views/PerpsPositionsView/PerpsPositionsView.test.tsx b/app/components/UI/Perps/Views/PerpsPositionsView/PerpsPositionsView.test.tsx
index a840ee503001..3c53ce661017 100644
--- a/app/components/UI/Perps/Views/PerpsPositionsView/PerpsPositionsView.test.tsx
+++ b/app/components/UI/Perps/Views/PerpsPositionsView/PerpsPositionsView.test.tsx
@@ -10,9 +10,9 @@ import PerpsPositionsView from './PerpsPositionsView';
import {
usePerpsAccount,
usePerpsTrading,
- usePerpsPositions,
usePerpsTPSLUpdate,
usePerpsClosePosition,
+ usePerpsLivePositions,
} from '../../hooks';
import type { Position } from '../../controllers/types';
@@ -29,6 +29,9 @@ jest.mock('@react-navigation/native', () => ({
useFocusEffect: jest.fn(),
}));
+// Mock PerpsStreamManager
+jest.mock('../../providers/PerpsStreamManager');
+
jest.mock('../../hooks', () => ({
usePerpsAccount: jest.fn(),
usePerpsTrading: jest.fn(),
@@ -36,7 +39,6 @@ jest.mock('../../hooks', () => ({
handleUpdateTPSL: jest.fn(),
isUpdating: false,
})),
- usePerpsPositions: jest.fn(),
usePerpsClosePosition: jest.fn(() => ({
handleClosePosition: jest.fn(),
isClosing: false,
@@ -46,8 +48,11 @@ jest.mock('../../hooks', () => ({
{ name: 'ETH', symbol: 'ETH' },
{ name: 'BTC', symbol: 'BTC' },
],
- error: null,
- isLoading: false,
+ isInitialLoading: false,
+ })),
+ usePerpsLivePositions: jest.fn(() => ({
+ positions: [],
+ isInitialLoading: false,
})),
}));
@@ -156,12 +161,9 @@ describe('PerpsPositionsView', () => {
});
// Mock usePerpsPositions hook
- (usePerpsPositions as jest.Mock).mockReturnValue({
+ (usePerpsLivePositions as jest.Mock).mockReturnValue({
positions: mockPositions,
- isLoading: false,
- isRefreshing: false,
- error: null,
- loadPositions: jest.fn(),
+ isInitialLoading: false,
});
// Using real implementations of utility functions (calculateTotalPnL, formatPrice, formatPnl) to test actual behavior
@@ -212,12 +214,9 @@ describe('PerpsPositionsView', () => {
it('displays correct position count for single position', async () => {
// Arrange
- (usePerpsPositions as jest.Mock).mockReturnValue({
+ (usePerpsLivePositions as jest.Mock).mockReturnValue({
positions: [mockPositions[0]],
- isLoading: false,
- isRefreshing: false,
- error: null,
- loadPositions: jest.fn(),
+ isInitialLoading: false,
});
// Act
@@ -233,12 +232,9 @@ describe('PerpsPositionsView', () => {
describe('Loading States', () => {
it('displays loading state initially', () => {
// Arrange
- (usePerpsPositions as jest.Mock).mockReturnValue({
+ (usePerpsLivePositions as jest.Mock).mockReturnValue({
positions: [],
- isLoading: true,
- isRefreshing: false,
- error: null,
- loadPositions: jest.fn(),
+ isInitialLoading: true,
});
// Act
@@ -249,54 +245,20 @@ describe('PerpsPositionsView', () => {
});
});
- describe('Error States', () => {
- it('displays error message when positions fail to load', async () => {
- // Arrange
- const errorMessage = 'Network error';
- (usePerpsPositions as jest.Mock).mockReturnValue({
- positions: [],
- isLoading: false,
- isRefreshing: false,
- error: errorMessage,
- loadPositions: jest.fn(),
- });
-
- // Act
- render();
-
- // Assert
- expect(screen.getByText('Error Loading Positions')).toBeOnTheScreen();
- expect(screen.getByText(errorMessage)).toBeOnTheScreen();
- });
-
- it('displays generic error message for non-Error objects', async () => {
- // Arrange
- (usePerpsPositions as jest.Mock).mockReturnValue({
- positions: [],
- isLoading: false,
- isRefreshing: false,
- error: 'Failed to load positions',
- loadPositions: jest.fn(),
- });
-
- // Act
- render();
-
- // Assert
- expect(screen.getByText('Error Loading Positions')).toBeOnTheScreen();
- expect(screen.getByText('Failed to load positions')).toBeOnTheScreen();
- });
- });
+ // Error States tests are commented out as the new usePerpsLivePositions
+ // hook doesn't return error state - errors are handled internally
+ // describe('Error States', () => {
+ // it('displays error message when positions fail to load', async () => {
+ // // Test removed - new hook doesn't expose error state
+ // });
+ // });
describe('Empty State', () => {
it('displays empty state when no positions are available', async () => {
// Arrange
- (usePerpsPositions as jest.Mock).mockReturnValue({
+ (usePerpsLivePositions as jest.Mock).mockReturnValue({
positions: [],
- isLoading: false,
- isRefreshing: false,
- error: null,
- loadPositions: jest.fn(),
+ isInitialLoading: false,
});
// Act
@@ -316,12 +278,9 @@ describe('PerpsPositionsView', () => {
it('displays empty state when positions is null', async () => {
// Arrange
- (usePerpsPositions as jest.Mock).mockReturnValue({
+ (usePerpsLivePositions as jest.Mock).mockReturnValue({
positions: [],
- isLoading: false,
- isRefreshing: false,
- error: null,
- loadPositions: jest.fn(),
+ isInitialLoading: false,
});
// Act
@@ -430,12 +389,9 @@ describe('PerpsPositionsView', () => {
{ ...mockPositions[0], coin: 'ETH' },
{ ...mockPositions[0], coin: 'ETH' },
];
- (usePerpsPositions as jest.Mock).mockReturnValue({
+ (usePerpsLivePositions as jest.Mock).mockReturnValue({
positions: duplicatePositions,
- isLoading: false,
- isRefreshing: false,
- error: null,
- loadPositions: jest.fn(),
+ isInitialLoading: false,
});
// Act
@@ -465,12 +421,9 @@ describe('PerpsPositionsView', () => {
isClosing: false,
});
- (usePerpsPositions as jest.Mock).mockReturnValue({
+ (usePerpsLivePositions as jest.Mock).mockReturnValue({
positions: mockPositions,
- isLoading: false,
- isRefreshing: false,
- error: null,
- loadPositions: mockLoadPositions,
+ isInitialLoading: false,
});
});
@@ -547,12 +500,9 @@ describe('PerpsPositionsView', () => {
isUpdating: false,
});
- (usePerpsPositions as jest.Mock).mockReturnValue({
+ (usePerpsLivePositions as jest.Mock).mockReturnValue({
positions: mockPositions,
- isLoading: false,
- isRefreshing: false,
- error: null,
- loadPositions: mockLoadPositions,
+ isInitialLoading: false,
});
});
diff --git a/app/components/UI/Perps/Views/PerpsPositionsView/PerpsPositionsView.tsx b/app/components/UI/Perps/Views/PerpsPositionsView/PerpsPositionsView.tsx
index ec09bd1f0e1c..14db2db90506 100644
--- a/app/components/UI/Perps/Views/PerpsPositionsView/PerpsPositionsView.tsx
+++ b/app/components/UI/Perps/Views/PerpsPositionsView/PerpsPositionsView.tsx
@@ -4,7 +4,7 @@ import {
type ParamListBase,
} from '@react-navigation/native';
import React, { useMemo, useState } from 'react';
-import { RefreshControl, SafeAreaView, ScrollView, View } from 'react-native';
+import { SafeAreaView, ScrollView, View } from 'react-native';
import { strings } from '../../../../../../locales/i18n';
import ButtonIcon, {
ButtonIconSizes,
@@ -23,7 +23,7 @@ import PerpsTPSLBottomSheet from '../../components/PerpsTPSLBottomSheet';
import type { Position } from '../../controllers/types';
import {
usePerpsAccount,
- usePerpsPositions,
+ usePerpsLivePositions,
usePerpsTPSLUpdate,
} from '../../hooks';
import { formatPnl, formatPrice } from '../../utils/formatUtils';
@@ -32,7 +32,7 @@ import { createStyles } from './PerpsPositionsView.styles';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const PerpsPositionsView: React.FC = () => {
- const { styles, theme } = useStyles(createStyles, {});
+ const { styles } = useStyles(createStyles, {});
const navigation = useNavigation>();
const cachedAccountState = usePerpsAccount();
@@ -42,16 +42,18 @@ const PerpsPositionsView: React.FC = () => {
);
const [isTPSLVisible, setIsTPSLVisible] = useState(false);
- const { positions, isLoading, isRefreshing, error, loadPositions } =
- usePerpsPositions({
- loadOnMount: true,
- refreshOnFocus: true,
- });
+ // Get real-time positions via WebSocket
+ const { positions, isInitialLoading } = usePerpsLivePositions({
+ throttleMs: 1000, // Update every second
+ });
+
+ const error = null;
const { handleUpdateTPSL, isUpdating } = usePerpsTPSLUpdate({
onSuccess: () => {
- // Refresh positions to show updated data
- loadPositions({ isRefresh: true });
+ // Positions update automatically via WebSocket
+ setIsTPSLVisible(false);
+ setSelectedPosition(null);
},
});
@@ -74,12 +76,8 @@ const PerpsPositionsView: React.FC = () => {
navigation.goBack();
};
- const handleRefresh = () => {
- loadPositions({ isRefresh: true });
- };
-
const renderContent = () => {
- if (isLoading) {
+ if (isInitialLoading) {
return (
@@ -153,16 +151,7 @@ const PerpsPositionsView: React.FC = () => {
-
- }
- >
+
{/* Account Summary */}
diff --git a/app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.test.tsx b/app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.test.tsx
index fe8b8ee02ff0..2b841856c7ff 100644
--- a/app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.test.tsx
+++ b/app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.test.tsx
@@ -13,6 +13,9 @@ jest.mock('@react-navigation/native', () => ({
useNavigation: jest.fn(),
}));
+// Mock PerpsStreamManager
+jest.mock('../../providers/PerpsStreamManager');
+
// Mock PerpsConnectionProvider
jest.mock('../../providers/PerpsConnectionProvider', () => ({
PerpsConnectionProvider: ({ children }: { children: React.ReactNode }) =>
@@ -31,7 +34,6 @@ jest.mock('../../providers/PerpsConnectionProvider', () => ({
// Mock hooks
jest.mock('../../hooks', () => ({
usePerpsConnection: jest.fn(),
- usePerpsPositions: jest.fn(),
usePerpsTrading: jest.fn(),
usePerpsFirstTimeUser: jest.fn(),
usePerpsAccount: jest.fn(),
@@ -46,6 +48,14 @@ jest.mock('../../hooks', () => ({
})),
}));
+// Mock stream hooks
+jest.mock('../../hooks/stream', () => ({
+ usePerpsLivePositions: jest.fn(() => ({
+ positions: [],
+ isInitialLoading: false,
+ })),
+}));
+
// Mock components
jest.mock('../../components/PerpsTabControlBar', () => ({
PerpsTabControlBar: ({
@@ -150,8 +160,8 @@ describe('PerpsTabView', () => {
const mockUsePerpsConnection =
jest.requireMock('../../hooks').usePerpsConnection;
- const mockUsePerpsPositions =
- jest.requireMock('../../hooks').usePerpsPositions;
+ const mockUsePerpsLivePositions =
+ jest.requireMock('../../hooks/stream').usePerpsLivePositions;
const mockUsePerpsTrading = jest.requireMock('../../hooks').usePerpsTrading;
const mockUsePerpsFirstTimeUser =
jest.requireMock('../../hooks').usePerpsFirstTimeUser;
@@ -188,11 +198,9 @@ describe('PerpsTabView', () => {
isInitialized: true,
});
- mockUsePerpsPositions.mockReturnValue({
+ mockUsePerpsLivePositions.mockReturnValue({
positions: [],
- isLoading: false,
- isRefreshing: false,
- loadPositions: jest.fn(),
+ isInitialLoading: false,
});
mockUsePerpsTrading.mockReturnValue({
@@ -248,11 +256,9 @@ describe('PerpsTabView', () => {
});
it('should render loading state when positions are loading', () => {
- mockUsePerpsPositions.mockReturnValue({
+ mockUsePerpsLivePositions.mockReturnValue({
positions: [],
- isLoading: true,
- isRefreshing: false,
- loadPositions: jest.fn(),
+ isInitialLoading: true,
});
render();
@@ -263,11 +269,9 @@ describe('PerpsTabView', () => {
});
it('should render empty state when no positions exist', () => {
- mockUsePerpsPositions.mockReturnValue({
+ mockUsePerpsLivePositions.mockReturnValue({
positions: [],
- isLoading: false,
- isRefreshing: false,
- loadPositions: jest.fn(),
+ isInitialLoading: false,
});
render();
@@ -281,11 +285,9 @@ describe('PerpsTabView', () => {
});
it('should render positions when they exist', () => {
- mockUsePerpsPositions.mockReturnValue({
+ mockUsePerpsLivePositions.mockReturnValue({
positions: [mockPosition],
- isLoading: false,
- isRefreshing: false,
- loadPositions: jest.fn(),
+ isInitialLoading: false,
});
render();
@@ -305,11 +307,9 @@ describe('PerpsTabView', () => {
{ ...mockPosition, coin: 'SOL', size: '50.0' },
];
- mockUsePerpsPositions.mockReturnValue({
+ mockUsePerpsLivePositions.mockReturnValue({
positions,
- isLoading: false,
- isRefreshing: false,
- loadPositions: jest.fn(),
+ isInitialLoading: false,
});
render();
@@ -420,7 +420,7 @@ describe('PerpsTabView', () => {
it('should have pull-to-refresh functionality configured', async () => {
const mockLoadPositions = jest.fn();
- mockUsePerpsPositions.mockReturnValue({
+ mockUsePerpsLivePositions.mockReturnValue({
positions: [],
isLoading: false,
isRefreshing: false,
@@ -510,11 +510,9 @@ describe('PerpsTabView', () => {
describe('State Management', () => {
it('should handle refresh state correctly', () => {
- mockUsePerpsPositions.mockReturnValue({
+ mockUsePerpsLivePositions.mockReturnValue({
positions: [],
- isLoading: false,
- isRefreshing: true,
- loadPositions: jest.fn(),
+ isInitialLoading: false,
});
render();
@@ -561,11 +559,9 @@ describe('PerpsTabView', () => {
stopLossPrice: undefined,
};
- mockUsePerpsPositions.mockReturnValue({
+ mockUsePerpsLivePositions.mockReturnValue({
positions: [incompletePosition],
- isLoading: false,
- isRefreshing: false,
- loadPositions: jest.fn(),
+ isInitialLoading: false,
});
expect(() => render()).not.toThrow();
@@ -573,11 +569,9 @@ describe('PerpsTabView', () => {
});
it('should handle empty positions array correctly', () => {
- mockUsePerpsPositions.mockReturnValue({
+ mockUsePerpsLivePositions.mockReturnValue({
positions: [],
- isLoading: false,
- isRefreshing: false,
- loadPositions: jest.fn(),
+ isInitialLoading: false,
});
render();
@@ -625,7 +619,7 @@ describe('PerpsTabView', () => {
// Mock console.error to avoid noise in tests
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
- mockUsePerpsPositions.mockImplementation(() => {
+ mockUsePerpsLivePositions.mockImplementation(() => {
throw new Error('Hook error');
});
@@ -644,11 +638,9 @@ describe('PerpsTabView', () => {
});
it('should render text with proper variants and colors', () => {
- mockUsePerpsPositions.mockReturnValue({
+ mockUsePerpsLivePositions.mockReturnValue({
positions: [mockPosition],
- isLoading: false,
- isRefreshing: false,
- loadPositions: jest.fn(),
+ isInitialLoading: false,
});
render();
@@ -667,11 +659,9 @@ describe('PerpsTabView', () => {
size: `${i + 1}.0`,
}));
- mockUsePerpsPositions.mockReturnValue({
+ mockUsePerpsLivePositions.mockReturnValue({
positions: manyPositions,
- isLoading: false,
- isRefreshing: false,
- loadPositions: jest.fn(),
+ isInitialLoading: false,
});
const startTime = performance.now();
@@ -689,7 +679,7 @@ describe('PerpsTabView', () => {
// Simulate rapid state changes
for (let i = 0; i < 5; i++) {
- mockUsePerpsPositions.mockReturnValue({
+ mockUsePerpsLivePositions.mockReturnValue({
positions: i % 2 === 0 ? [] : [mockPosition],
isLoading: i % 3 === 0,
isRefreshing: i % 4 === 0,
@@ -710,8 +700,8 @@ describe('PerpsTabViewWithProvider', () => {
// Setup mocks for wrapped component tests
const mockUsePerpsConnection = jest.requireMock('../../hooks')
.usePerpsConnection as jest.Mock;
- const mockUsePerpsPositions = jest.requireMock('../../hooks')
- .usePerpsPositions as jest.Mock;
+ const mockUsePerpsLivePositions = jest.requireMock('../../hooks/stream')
+ .usePerpsLivePositions as jest.Mock;
const mockUsePerpsTrading = jest.requireMock('../../hooks')
.usePerpsTrading as jest.Mock;
const mockUsePerpsFirstTimeUser = jest.requireMock('../../hooks')
@@ -725,11 +715,9 @@ describe('PerpsTabViewWithProvider', () => {
isInitialized: true,
});
- mockUsePerpsPositions.mockReturnValue({
+ mockUsePerpsLivePositions.mockReturnValue({
positions: [],
- isLoading: false,
- isRefreshing: false,
- loadPositions: jest.fn(),
+ isInitialLoading: false,
});
mockUsePerpsTrading.mockReturnValue({
diff --git a/app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.tsx b/app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.tsx
index 42a166d1d070..dd189d9adb7e 100644
--- a/app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.tsx
+++ b/app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.tsx
@@ -1,6 +1,6 @@
import { useNavigation, type NavigationProp } from '@react-navigation/native';
import React, { useCallback, useEffect, useRef, useState } from 'react';
-import { Modal, RefreshControl, ScrollView, View } from 'react-native';
+import { Modal, ScrollView, View } from 'react-native';
import { strings } from '../../../../../../locales/i18n';
import BottomSheet, {
BottomSheetRef,
@@ -36,10 +36,10 @@ import {
usePerpsConnection,
usePerpsEventTracking,
usePerpsFirstTimeUser,
- usePerpsPositions,
usePerpsTrading,
usePerpsPerformance,
} from '../../hooks';
+import { usePerpsLivePositions } from '../../hooks/stream';
import styleSheet from './PerpsTabView.styles';
interface PerpsTabViewProps {}
@@ -58,15 +58,12 @@ const PerpsTabView: React.FC = () => {
const bottomSheetRef = useRef(null);
- const {
- positions,
- isLoading: isPositionsLoading,
- isRefreshing,
- loadPositions,
- } = usePerpsPositions();
+ // Get real-time positions via WebSocket
+ const { positions, isInitialLoading } = usePerpsLivePositions({
+ throttleMs: 1000, // Update positions every second
+ });
const { isFirstTimeUser } = usePerpsFirstTimeUser();
- const isLoading = isPositionsLoading;
const firstTimeUserIconSize = 48 as unknown as IconSize;
// Start measuring position data load time on mount
@@ -88,7 +85,7 @@ const PerpsTabView: React.FC = () => {
useEffect(() => {
if (
!hasTrackedHomescreen.current &&
- !isLoading &&
+ !isInitialLoading &&
positions &&
cachedAccountState?.totalBalance !== undefined
) {
@@ -113,17 +110,13 @@ const PerpsTabView: React.FC = () => {
hasTrackedHomescreen.current = true;
}
}, [
- isLoading,
+ isInitialLoading,
positions,
cachedAccountState?.totalBalance,
track,
endMeasure,
]);
- const handleRefresh = useCallback(() => {
- loadPositions();
- }, [loadPositions]);
-
const handleManageBalancePress = useCallback(() => {
setIsBottomSheetVisible(true);
}, []);
@@ -161,7 +154,7 @@ const PerpsTabView: React.FC = () => {
}, [navigation]);
const renderPositionsSection = () => {
- if (isLoading) {
+ if (isInitialLoading) {
return (
@@ -254,15 +247,7 @@ const PerpsTabView: React.FC = () => {
) : (
<>
-
- }
- >
+
{renderPositionsSection()}
>
@@ -308,12 +293,13 @@ const PerpsTabView: React.FC = () => {
};
// Enable WDYR tracking in development
-if (__DEV__) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (PerpsTabView as any).whyDidYouRender = {
- logOnDifferentValues: true,
- customName: 'PerpsTabView',
- };
-}
+// Uncomment to enable WDYR for debugging re-renders
+// if (__DEV__) {
+// // eslint-disable-next-line @typescript-eslint/no-explicit-any
+// (PerpsTabView as any).whyDidYouRender = {
+// logOnDifferentValues: true,
+// customName: 'PerpsTabView',
+// };
+// }
export default PerpsTabView;
diff --git a/app/components/UI/Perps/Views/PerpsView.test.tsx b/app/components/UI/Perps/Views/PerpsView.test.tsx
index 115a9a92d7fb..2e7e231d298c 100644
--- a/app/components/UI/Perps/Views/PerpsView.test.tsx
+++ b/app/components/UI/Perps/Views/PerpsView.test.tsx
@@ -7,9 +7,12 @@ jest.mock('@react-navigation/native', () => ({
useNavigation: jest.fn(() => ({ navigate: jest.fn() })),
}));
+// Mock PerpsStreamManager
+jest.mock('../providers/PerpsStreamManager');
+
// Mock stream hooks
jest.mock('../hooks/stream', () => ({
- useLivePrices: jest.fn(() => ({
+ usePerpsLivePrices: jest.fn(() => ({
'BTC-PERP': { price: '50000', percentChange24h: '2.5' },
'ETH-PERP': { price: '3000', percentChange24h: '-1.2' },
'SOL-PERP': { price: '100', percentChange24h: '5.0' },
diff --git a/app/components/UI/Perps/Views/PerpsView.tsx b/app/components/UI/Perps/Views/PerpsView.tsx
index 61c38c6db985..bd5abd929236 100644
--- a/app/components/UI/Perps/Views/PerpsView.tsx
+++ b/app/components/UI/Perps/Views/PerpsView.tsx
@@ -39,7 +39,7 @@ import {
usePerpsNetworkConfig,
usePerpsTrading,
} from '../hooks';
-import { useLivePrices } from '../hooks/stream';
+import { usePerpsLivePrices } from '../hooks/stream';
// Import connection components
import PerpsConnectionErrorView from '../components/PerpsConnectionErrorView';
@@ -173,10 +173,10 @@ const PerpsView: React.FC = () => {
resetError,
} = usePerpsConnection();
- // Get real-time prices for popular assets with 5s debounce for portfolio view
- const priceData = useLivePrices({
+ // Get real-time prices for popular assets with 5s throttle for portfolio view
+ const priceData = usePerpsLivePrices({
symbols: POPULAR_ASSETS,
- debounceMs: 5000,
+ throttleMs: 5000,
});
// Parse available balance to check if withdrawal should be enabled
diff --git a/app/components/UI/Perps/components/FundingCountdown/FundingCountdown.test.tsx b/app/components/UI/Perps/components/FundingCountdown/FundingCountdown.test.tsx
new file mode 100644
index 000000000000..3d554726e534
--- /dev/null
+++ b/app/components/UI/Perps/components/FundingCountdown/FundingCountdown.test.tsx
@@ -0,0 +1,98 @@
+import React from 'react';
+import { render } from '@testing-library/react-native';
+import FundingCountdown from './FundingCountdown';
+import { calculateFundingCountdown } from '../../utils/marketUtils';
+
+jest.mock('../../utils/marketUtils', () => ({
+ calculateFundingCountdown: jest.fn(),
+}));
+
+describe('FundingCountdown', () => {
+ const mockCalculateFundingCountdown = calculateFundingCountdown as jest.Mock;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should render with market-specific funding time', () => {
+ const nextFundingTime = Date.now() + 3600000; // 1 hour from now
+ const fundingIntervalHours = 4;
+
+ mockCalculateFundingCountdown.mockReturnValue('00:59:59');
+
+ const { getByText } = render(
+ ,
+ );
+
+ expect(mockCalculateFundingCountdown).toHaveBeenCalledWith({
+ nextFundingTime,
+ fundingIntervalHours,
+ });
+ expect(getByText('(00:59:59)')).toBeTruthy();
+ });
+
+ it('should render with default 8-hour intervals when no specific time provided', () => {
+ mockCalculateFundingCountdown.mockReturnValue('07:59:59');
+
+ const { getByText } = render();
+
+ expect(mockCalculateFundingCountdown).toHaveBeenCalledWith({
+ nextFundingTime: undefined,
+ fundingIntervalHours: undefined,
+ });
+ expect(getByText('(07:59:59)')).toBeTruthy();
+ });
+
+ it('should update countdown every second', () => {
+ jest.useFakeTimers();
+
+ // Initial render returns 00:59:59
+ mockCalculateFundingCountdown.mockReturnValue('00:59:59');
+
+ const nextFundingTime = Date.now() + 3600000;
+
+ const { getByText } = render(
+ ,
+ );
+
+ // Verify initial render
+ expect(getByText('(00:59:59)')).toBeTruthy();
+
+ // Update mock for next call
+ mockCalculateFundingCountdown.mockReturnValue('00:59:58');
+
+ // Fast-forward 1 second to trigger the interval
+ jest.advanceTimersByTime(1000);
+
+ // The component should have called calculateFundingCountdown multiple times (initial + interval)
+ expect(mockCalculateFundingCountdown).toHaveBeenCalled();
+
+ jest.useRealTimers();
+ });
+
+ it('should pass testID when provided', () => {
+ mockCalculateFundingCountdown.mockReturnValue('00:59:59');
+
+ const { getByTestId, getByText } = render(
+ ,
+ );
+
+ expect(getByTestId('funding-countdown-test')).toBeTruthy();
+ expect(getByText('(00:59:59)')).toBeTruthy();
+ });
+
+ it('should accept and apply style prop', () => {
+ mockCalculateFundingCountdown.mockReturnValue('00:59:59');
+
+ const customStyle = { marginLeft: 10, fontSize: 20 };
+ const { getByTestId } = render(
+ ,
+ );
+
+ const element = getByTestId('styled-countdown');
+ expect(element.props.style).toEqual(expect.objectContaining(customStyle));
+ });
+});
diff --git a/app/components/UI/Perps/components/FundingCountdown/FundingCountdown.tsx b/app/components/UI/Perps/components/FundingCountdown/FundingCountdown.tsx
new file mode 100644
index 000000000000..625658e91cb5
--- /dev/null
+++ b/app/components/UI/Perps/components/FundingCountdown/FundingCountdown.tsx
@@ -0,0 +1,65 @@
+import React, { useState, useEffect, memo } from 'react';
+import type { TextStyle } from 'react-native';
+import Text, {
+ TextVariant,
+ TextColor,
+} from '../../../../../component-library/components/Texts/Text';
+import { calculateFundingCountdown } from '../../utils/marketUtils';
+
+interface FundingCountdownProps {
+ variant?: TextVariant;
+ color?: TextColor;
+ style?: TextStyle;
+ testID?: string;
+ /**
+ * Next funding time in milliseconds since epoch (optional, market-specific)
+ */
+ nextFundingTime?: number;
+ /**
+ * Funding interval in hours (optional, market-specific)
+ */
+ fundingIntervalHours?: number;
+}
+
+/**
+ * Isolated countdown component that updates every second
+ * without causing parent re-renders.
+ * Supports market-specific funding times when provided.
+ */
+const FundingCountdown: React.FC = ({
+ variant = TextVariant.BodySM,
+ color = TextColor.Default,
+ style,
+ testID,
+ nextFundingTime,
+ fundingIntervalHours,
+}) => {
+ const [countdown, setCountdown] = useState(() =>
+ calculateFundingCountdown({ nextFundingTime, fundingIntervalHours }),
+ );
+
+ useEffect(() => {
+ const updateCountdown = () => {
+ setCountdown(
+ calculateFundingCountdown({ nextFundingTime, fundingIntervalHours }),
+ );
+ };
+
+ // Update immediately
+ updateCountdown();
+
+ // Then update every second
+ const interval = setInterval(updateCountdown, 1000);
+
+ return () => clearInterval(interval);
+ }, [nextFundingTime, fundingIntervalHours]);
+
+ return (
+
+ ({countdown})
+
+ );
+};
+
+// Memoize to prevent unnecessary re-renders when parent re-renders
+export default memo(FundingCountdown);
diff --git a/app/components/UI/Perps/components/FundingCountdown/index.ts b/app/components/UI/Perps/components/FundingCountdown/index.ts
new file mode 100644
index 000000000000..aefefadb5809
--- /dev/null
+++ b/app/components/UI/Perps/components/FundingCountdown/index.ts
@@ -0,0 +1 @@
+export { default } from './FundingCountdown';
diff --git a/app/components/UI/Perps/components/LivePriceDisplay/LivePriceDisplay.test.tsx b/app/components/UI/Perps/components/LivePriceDisplay/LivePriceDisplay.test.tsx
new file mode 100644
index 000000000000..5a3882192442
--- /dev/null
+++ b/app/components/UI/Perps/components/LivePriceDisplay/LivePriceDisplay.test.tsx
@@ -0,0 +1,198 @@
+import React from 'react';
+import { render } from '@testing-library/react-native';
+import LivePriceDisplay from './LivePriceDisplay';
+import { usePerpsLivePrices } from '../../hooks/stream';
+import { formatPrice, formatPercentage } from '../../utils/formatUtils';
+import {
+ TextVariant,
+ TextColor,
+} from '../../../../../component-library/components/Texts/Text';
+
+// Mock dependencies
+jest.mock('../../hooks/stream');
+jest.mock('../../utils/formatUtils');
+
+describe('LivePriceDisplay', () => {
+ const mockUsePerpsLivePrices = usePerpsLivePrices as jest.MockedFunction<
+ typeof usePerpsLivePrices
+ >;
+ const mockFormatPrice = formatPrice as jest.MockedFunction<
+ typeof formatPrice
+ >;
+ const mockFormatPercentage = formatPercentage as jest.MockedFunction<
+ typeof formatPercentage
+ >;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockFormatPrice.mockImplementation((price) => `$${price}`);
+ mockFormatPercentage.mockImplementation((pct) => `${pct}%`);
+ });
+
+ it('should render with live price data', () => {
+ mockUsePerpsLivePrices.mockReturnValue({
+ BTC: {
+ coin: 'BTC',
+ price: '50000',
+ percentChange24h: '5.5',
+ timestamp: Date.now(),
+ },
+ });
+
+ const { getByText } = render();
+
+ expect(getByText('$50000')).toBeTruthy();
+ expect(mockUsePerpsLivePrices).toHaveBeenCalledWith({
+ symbols: ['BTC'],
+ throttleMs: 1000,
+ });
+ });
+
+ it('should render placeholder when no price data available', () => {
+ mockUsePerpsLivePrices.mockReturnValue({});
+
+ const { getByText } = render();
+
+ expect(getByText('--')).toBeTruthy();
+ });
+
+ it('should render price with change when showChange is true', () => {
+ mockUsePerpsLivePrices.mockReturnValue({
+ ETH: {
+ coin: 'ETH',
+ price: '3000',
+ percentChange24h: '-2.5',
+ timestamp: Date.now(),
+ },
+ });
+
+ const { getByText } = render();
+
+ expect(getByText('$3000')).toBeTruthy();
+ expect(getByText('-2.5%')).toBeTruthy();
+ });
+
+ it('should render price without change when showChange is false', () => {
+ mockUsePerpsLivePrices.mockReturnValue({
+ SOL: {
+ coin: 'SOL',
+ price: '100',
+ percentChange24h: '10',
+ timestamp: Date.now(),
+ },
+ });
+
+ const { getByText, queryByText } = render(
+ ,
+ );
+
+ expect(getByText('$100')).toBeTruthy();
+ expect(queryByText('10%')).toBeNull();
+ });
+
+ it('should use custom throttle value', () => {
+ mockUsePerpsLivePrices.mockReturnValue({
+ DOGE: {
+ coin: 'DOGE',
+ price: '0.1',
+ percentChange24h: '0',
+ timestamp: Date.now(),
+ },
+ });
+
+ render();
+
+ expect(mockUsePerpsLivePrices).toHaveBeenCalledWith({
+ symbols: ['DOGE'],
+ throttleMs: 500,
+ });
+ });
+
+ it('should apply custom text styles', () => {
+ mockUsePerpsLivePrices.mockReturnValue({
+ AVAX: {
+ coin: 'AVAX',
+ price: '25',
+ percentChange24h: '0',
+ timestamp: Date.now(),
+ },
+ });
+
+ const { getByTestId } = render(
+ ,
+ );
+
+ const priceElement = getByTestId('custom-price');
+ expect(priceElement).toBeTruthy();
+ // Just verify the element exists with the custom testID
+ // Props are implementation details that shouldn't be tested
+ });
+
+ it('should handle positive price change color', () => {
+ mockUsePerpsLivePrices.mockReturnValue({
+ UNI: {
+ coin: 'UNI',
+ price: '10',
+ percentChange24h: '15',
+ timestamp: Date.now(),
+ },
+ });
+
+ const { getByText } = render();
+
+ expect(getByText('15%')).toBeTruthy();
+ });
+
+ it('should handle negative price change color', () => {
+ mockUsePerpsLivePrices.mockReturnValue({
+ LINK: {
+ coin: 'LINK',
+ price: '15',
+ percentChange24h: '-8',
+ timestamp: Date.now(),
+ },
+ });
+
+ const { getByText } = render();
+
+ expect(getByText('-8%')).toBeTruthy();
+ });
+
+ it('should handle zero price change', () => {
+ mockUsePerpsLivePrices.mockReturnValue({
+ MATIC: {
+ coin: 'MATIC',
+ price: '1',
+ percentChange24h: '0',
+ timestamp: Date.now(),
+ },
+ });
+
+ const { getByText } = render(
+ ,
+ );
+
+ expect(getByText('0%')).toBeTruthy();
+ });
+
+ it('should handle missing percentChange24h', () => {
+ mockUsePerpsLivePrices.mockReturnValue({
+ DOT: {
+ coin: 'DOT',
+ price: '5',
+ timestamp: Date.now(),
+ // percentChange24h is missing
+ },
+ });
+
+ const { getByText } = render();
+
+ expect(getByText('$5')).toBeTruthy();
+ expect(getByText('0%')).toBeTruthy(); // Defaults to 0
+ });
+});
diff --git a/app/components/UI/Perps/components/LivePriceDisplay/LivePriceDisplay.tsx b/app/components/UI/Perps/components/LivePriceDisplay/LivePriceDisplay.tsx
new file mode 100644
index 000000000000..a15f2b788641
--- /dev/null
+++ b/app/components/UI/Perps/components/LivePriceDisplay/LivePriceDisplay.tsx
@@ -0,0 +1,71 @@
+import React, { memo } from 'react';
+import { View } from 'react-native';
+import Text, {
+ TextVariant,
+ TextColor,
+} from '../../../../../component-library/components/Texts/Text';
+import { usePerpsLivePrices } from '../../hooks/stream';
+import { formatPrice, formatPercentage } from '../../utils/formatUtils';
+
+interface LivePriceDisplayProps {
+ symbol: string;
+ variant?: TextVariant;
+ color?: TextColor;
+ showChange?: boolean;
+ testID?: string;
+ throttleMs?: number;
+}
+
+/**
+ * Component that displays live price updates
+ * Subscribes to price stream independently to avoid parent re-renders
+ */
+const LivePriceDisplay: React.FC = ({
+ symbol,
+ variant = TextVariant.BodyMD,
+ color = TextColor.Default,
+ showChange = false,
+ testID,
+ throttleMs = 1000, // Default to 1 second updates for price displays
+}) => {
+ const prices = usePerpsLivePrices({
+ symbols: [symbol],
+ throttleMs,
+ });
+
+ const priceData = prices[symbol];
+
+ if (!priceData) {
+ return (
+
+ --
+
+ );
+ }
+
+ const price = parseFloat(priceData.price);
+ const change = parseFloat(priceData.percentChange24h || '0');
+
+ if (showChange) {
+ const changeColor = change >= 0 ? TextColor.Success : TextColor.Error;
+ return (
+
+
+ {formatPrice(price)}
+
+
+ {formatPercentage(change)}
+
+
+ );
+ }
+
+ return (
+
+ {formatPrice(price)}
+
+ );
+};
+
+// Memoize to prevent unnecessary re-renders when parent re-renders
+export default memo(LivePriceDisplay);
diff --git a/app/components/UI/Perps/components/LivePriceDisplay/LivePriceHeader.test.tsx b/app/components/UI/Perps/components/LivePriceDisplay/LivePriceHeader.test.tsx
new file mode 100644
index 000000000000..1bb3de0e06de
--- /dev/null
+++ b/app/components/UI/Perps/components/LivePriceDisplay/LivePriceHeader.test.tsx
@@ -0,0 +1,257 @@
+import React from 'react';
+import { render } from '@testing-library/react-native';
+import LivePriceHeader from './LivePriceHeader';
+import { usePerpsLivePrices } from '../../hooks/stream';
+import {
+ formatPrice,
+ formatPercentage,
+ formatPnl,
+} from '../../utils/formatUtils';
+import { useStyles } from '../../../../../component-library/hooks';
+
+// Mock dependencies
+jest.mock('../../hooks/stream');
+jest.mock('../../utils/formatUtils');
+jest.mock('../../../../../component-library/hooks');
+
+describe('LivePriceHeader', () => {
+ const mockUsePerpsLivePrices = usePerpsLivePrices as jest.MockedFunction<
+ typeof usePerpsLivePrices
+ >;
+ const mockFormatPrice = formatPrice as jest.MockedFunction<
+ typeof formatPrice
+ >;
+ const mockFormatPercentage = formatPercentage as jest.MockedFunction<
+ typeof formatPercentage
+ >;
+ const mockFormatPnl = formatPnl as jest.MockedFunction;
+ const mockUseStyles = useStyles as jest.MockedFunction;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockFormatPrice.mockImplementation((price) => {
+ const num = typeof price === 'string' ? parseFloat(price) : price;
+ return `$${num.toFixed(2)}`;
+ });
+ mockFormatPercentage.mockImplementation((pct) => `${pct}%`);
+ mockFormatPnl.mockImplementation((amount) => {
+ const num = typeof amount === 'string' ? parseFloat(amount) : amount;
+ return num >= 0
+ ? `+$${Math.abs(num).toFixed(2)}`
+ : `-$${Math.abs(num).toFixed(2)}`;
+ });
+ mockUseStyles.mockReturnValue({
+ styles: {
+ container: { flexDirection: 'row', alignItems: 'baseline', gap: 6 },
+ positionValue: { fontWeight: '700' },
+ priceChange24h: { fontSize: 12 },
+ },
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ theme: {} as any,
+ });
+ });
+
+ it('should render with live price data', () => {
+ mockUsePerpsLivePrices.mockReturnValue({
+ BTC: {
+ coin: 'BTC',
+ price: '50000',
+ percentChange24h: '5.5',
+ timestamp: Date.now(),
+ },
+ });
+
+ const { getByText } = render();
+
+ expect(getByText('$50000.00')).toBeTruthy();
+ // 5.5% of 50000 = 2750
+ expect(getByText('+$2750.00 (5.5%)')).toBeTruthy();
+ });
+
+ it('should use fallback values when no live data', () => {
+ mockUsePerpsLivePrices.mockReturnValue({});
+
+ const { getByText } = render(
+ ,
+ );
+
+ expect(getByText('$3000.00')).toBeTruthy();
+ // 2.5% of 3000 = 75
+ expect(getByText('+$75.00 (2.5%)')).toBeTruthy();
+ });
+
+ it('should handle negative price change', () => {
+ mockUsePerpsLivePrices.mockReturnValue({
+ SOL: {
+ coin: 'SOL',
+ price: '100',
+ percentChange24h: '-10',
+ timestamp: Date.now(),
+ },
+ });
+
+ const { getByText } = render();
+
+ expect(getByText('$100.00')).toBeTruthy();
+ // -10% of 100 = -10
+ expect(getByText('-$10.00 (-10%)')).toBeTruthy();
+ });
+
+ it('should handle positive price change color', () => {
+ mockUsePerpsLivePrices.mockReturnValue({
+ AVAX: {
+ coin: 'AVAX',
+ price: '25',
+ percentChange24h: '8',
+ timestamp: Date.now(),
+ },
+ });
+
+ const { getByText } = render();
+
+ expect(getByText('+$2.00 (8%)')).toBeTruthy();
+ });
+
+ it('should handle zero price change', () => {
+ mockUsePerpsLivePrices.mockReturnValue({
+ MATIC: {
+ coin: 'MATIC',
+ price: '1',
+ percentChange24h: '0',
+ timestamp: Date.now(),
+ },
+ });
+
+ const { getByText } = render();
+
+ expect(getByText('$1.00')).toBeTruthy();
+ expect(getByText('+$0.00 (0%)')).toBeTruthy();
+ });
+
+ it('should use custom throttle value', () => {
+ mockUsePerpsLivePrices.mockReturnValue({
+ DOGE: {
+ coin: 'DOGE',
+ price: '0.1',
+ percentChange24h: '0',
+ timestamp: Date.now(),
+ },
+ });
+
+ render();
+
+ expect(mockUsePerpsLivePrices).toHaveBeenCalledWith({
+ symbols: ['DOGE'],
+ throttleMs: 2000,
+ });
+ });
+
+ it('should use default throttle value of 1000ms', () => {
+ mockUsePerpsLivePrices.mockReturnValue({
+ UNI: {
+ coin: 'UNI',
+ price: '10',
+ percentChange24h: '0',
+ timestamp: Date.now(),
+ },
+ });
+
+ render();
+
+ expect(mockUsePerpsLivePrices).toHaveBeenCalledWith({
+ symbols: ['UNI'],
+ throttleMs: 1000,
+ });
+ });
+
+ it('should apply test IDs correctly', () => {
+ mockUsePerpsLivePrices.mockReturnValue({
+ LINK: {
+ coin: 'LINK',
+ price: '15',
+ percentChange24h: '3',
+ timestamp: Date.now(),
+ },
+ });
+
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId('price-test')).toBeTruthy();
+ expect(getByTestId('change-test')).toBeTruthy();
+ });
+
+ it('should handle missing percentChange24h', () => {
+ mockUsePerpsLivePrices.mockReturnValue({
+ DOT: {
+ coin: 'DOT',
+ price: '5',
+ timestamp: Date.now(),
+ // percentChange24h is missing
+ },
+ });
+
+ const { getByText } = render();
+
+ expect(getByText('$5.00')).toBeTruthy();
+ expect(getByText('+$0.00 (0%)')).toBeTruthy(); // Defaults to 0
+ });
+
+ it('should calculate change amount correctly', () => {
+ mockUsePerpsLivePrices.mockReturnValue({
+ ADA: {
+ coin: 'ADA',
+ price: '0.5',
+ percentChange24h: '20',
+ timestamp: Date.now(),
+ },
+ });
+
+ const { getByText } = render();
+
+ expect(getByText('$0.50')).toBeTruthy();
+ // 20% of 0.5 = 0.1
+ expect(getByText('+$0.10 (20%)')).toBeTruthy();
+ });
+
+ it('should use fallback values as defaults', () => {
+ mockUsePerpsLivePrices.mockReturnValue({});
+
+ const { getByText } = render(
+ ,
+ );
+
+ expect(getByText('$0.60')).toBeTruthy();
+ // -5% of 0.6 = -0.03
+ expect(getByText('-$0.03 (-5%)')).toBeTruthy();
+ });
+
+ it('should prefer live data over fallback', () => {
+ mockUsePerpsLivePrices.mockReturnValue({
+ ALGO: {
+ coin: 'ALGO',
+ price: '0.2',
+ percentChange24h: '15',
+ timestamp: Date.now(),
+ },
+ });
+
+ const { getByText } = render(
+ ,
+ );
+
+ // Should use live data, not fallback
+ expect(getByText('$0.20')).toBeTruthy();
+ // 15% of 0.2 = 0.03
+ expect(getByText('+$0.03 (15%)')).toBeTruthy();
+ });
+});
diff --git a/app/components/UI/Perps/components/LivePriceDisplay/LivePriceHeader.tsx b/app/components/UI/Perps/components/LivePriceDisplay/LivePriceHeader.tsx
new file mode 100644
index 000000000000..630f99fc17e2
--- /dev/null
+++ b/app/components/UI/Perps/components/LivePriceDisplay/LivePriceHeader.tsx
@@ -0,0 +1,96 @@
+import React, { memo } from 'react';
+import { View, StyleSheet } from 'react-native';
+import Text, {
+ TextVariant,
+ TextColor,
+} from '../../../../../component-library/components/Texts/Text';
+import { usePerpsLivePrices } from '../../hooks/stream';
+import {
+ formatPrice,
+ formatPercentage,
+ formatPnl,
+} from '../../utils/formatUtils';
+import { useStyles } from '../../../../../component-library/hooks';
+
+interface LivePriceHeaderProps {
+ symbol: string;
+ fallbackPrice?: string;
+ fallbackChange?: string;
+ testIDPrice?: string;
+ testIDChange?: string;
+ throttleMs?: number;
+}
+
+const styleSheet = () =>
+ StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ alignItems: 'baseline',
+ gap: 6,
+ },
+ positionValue: {
+ fontWeight: '700',
+ },
+ priceChange24h: {
+ fontSize: 12,
+ },
+ });
+
+/**
+ * Component that displays live price and change for header
+ * Subscribes to price stream independently to avoid parent re-renders
+ */
+const LivePriceHeader: React.FC = ({
+ symbol,
+ fallbackPrice = '0',
+ fallbackChange = '0',
+ testIDPrice,
+ testIDChange,
+ throttleMs = 1000, // Balanced updates for header (1 update per second)
+}) => {
+ const { styles } = useStyles(styleSheet, {});
+ const prices = usePerpsLivePrices({
+ symbols: [symbol],
+ throttleMs,
+ });
+
+ const priceData = prices[symbol];
+
+ // Use fallback data if no live data yet
+ const displayPrice = priceData
+ ? parseFloat(priceData.price)
+ : parseFloat(fallbackPrice);
+ const displayChange = priceData
+ ? parseFloat(priceData.percentChange24h || '0')
+ : parseFloat(fallbackChange);
+
+ const isPositiveChange = displayChange >= 0;
+ const changeColor = isPositiveChange ? TextColor.Success : TextColor.Error;
+
+ // Calculate fiat change amount (exactly as original)
+ const changeAmount = (displayChange / 100) * displayPrice;
+
+ return (
+
+
+ {formatPrice(displayPrice)}
+
+
+ {formatPnl(changeAmount)} ({formatPercentage(displayChange.toString())})
+
+
+ );
+};
+
+// Memoize to prevent unnecessary re-renders when parent re-renders
+export default memo(LivePriceHeader);
diff --git a/app/components/UI/Perps/components/LivePriceDisplay/index.ts b/app/components/UI/Perps/components/LivePriceDisplay/index.ts
new file mode 100644
index 000000000000..87012a2c5cd5
--- /dev/null
+++ b/app/components/UI/Perps/components/LivePriceDisplay/index.ts
@@ -0,0 +1 @@
+export { default } from './LivePriceDisplay';
diff --git a/app/components/UI/Perps/components/PerpsClosePositionBottomSheet/PerpsClosePositionBottomSheet.test.tsx b/app/components/UI/Perps/components/PerpsClosePositionBottomSheet/PerpsClosePositionBottomSheet.test.tsx
index 43b56eba93e6..c1192f399d3c 100644
--- a/app/components/UI/Perps/components/PerpsClosePositionBottomSheet/PerpsClosePositionBottomSheet.test.tsx
+++ b/app/components/UI/Perps/components/PerpsClosePositionBottomSheet/PerpsClosePositionBottomSheet.test.tsx
@@ -66,7 +66,7 @@ jest.mock('../PerpsSlider/PerpsSlider', () => {
// Mock stream hooks
jest.mock('../../hooks/stream', () => ({
- useLivePrices: jest.fn(() => ({
+ usePerpsLivePrices: jest.fn(() => ({
BTC: { price: '45000' },
ETH: { price: '2500' },
})),
diff --git a/app/components/UI/Perps/components/PerpsClosePositionBottomSheet/PerpsClosePositionBottomSheet.tsx b/app/components/UI/Perps/components/PerpsClosePositionBottomSheet/PerpsClosePositionBottomSheet.tsx
index 63e04003ae2d..3af0d073ec5a 100644
--- a/app/components/UI/Perps/components/PerpsClosePositionBottomSheet/PerpsClosePositionBottomSheet.tsx
+++ b/app/components/UI/Perps/components/PerpsClosePositionBottomSheet/PerpsClosePositionBottomSheet.tsx
@@ -26,7 +26,7 @@ import {
usePerpsOrderFees,
usePerpsClosePositionValidation,
} from '../../hooks';
-import { useLivePrices } from '../../hooks/stream';
+import { usePerpsLivePrices } from '../../hooks/stream';
import { formatPositionSize, formatPrice } from '../../utils/formatUtils';
import PerpsSlider from '../PerpsSlider/PerpsSlider';
import { createStyles } from './PerpsClosePositionBottomSheet.styles';
@@ -80,9 +80,9 @@ const PerpsClosePositionBottomSheet: React.FC<
const [limitPriceInputFocused, setLimitPriceInputFocused] = useState(false);
// Subscribe to real-time price with 1s debounce for position closing
- const priceData = useLivePrices({
+ const priceData = usePerpsLivePrices({
symbols: isVisible ? [position.coin] : [],
- debounceMs: 1000,
+ throttleMs: 1000,
});
const currentPrice = priceData[position.coin]?.price
? parseFloat(priceData[position.coin].price)
diff --git a/app/components/UI/Perps/components/PerpsLimitPriceBottomSheet/PerpsLimitPriceBottomSheet.test.tsx b/app/components/UI/Perps/components/PerpsLimitPriceBottomSheet/PerpsLimitPriceBottomSheet.test.tsx
index adbacfa0be98..ac4ca5110e42 100644
--- a/app/components/UI/Perps/components/PerpsLimitPriceBottomSheet/PerpsLimitPriceBottomSheet.test.tsx
+++ b/app/components/UI/Perps/components/PerpsLimitPriceBottomSheet/PerpsLimitPriceBottomSheet.test.tsx
@@ -59,7 +59,7 @@ jest.mock('../../../../../../locales/i18n', () => ({
// Mock stream hooks
jest.mock('../../hooks/stream', () => ({
- useLivePrices: jest.fn(() => ({})),
+ usePerpsLivePrices: jest.fn(() => ({})),
}));
// Mock usePerpsConnection hook
@@ -266,9 +266,9 @@ describe('PerpsLimitPriceBottomSheet', () => {
jest.clearAllMocks();
mockUseTheme.mockReturnValue(mockTheme);
- // Mock useLivePrices hook to return empty by default
- const { useLivePrices } = jest.requireMock('../../hooks/stream');
- useLivePrices.mockReturnValue({});
+ // Mock usePerpsLivePrices hook to return empty by default
+ const { usePerpsLivePrices } = jest.requireMock('../../hooks/stream');
+ usePerpsLivePrices.mockReturnValue({});
// Mock usePerpsConnection hook
const { usePerpsConnection } = jest.requireMock('../../hooks/index');
@@ -361,8 +361,8 @@ describe('PerpsLimitPriceBottomSheet', () => {
describe('Price Data Integration', () => {
it('uses real-time price data when available', () => {
// Arrange - Mock returns real-time data
- const { useLivePrices } = jest.requireMock('../../hooks/stream');
- useLivePrices.mockReturnValue({
+ const { usePerpsLivePrices } = jest.requireMock('../../hooks/stream');
+ usePerpsLivePrices.mockReturnValue({
ETH: {
price: '3200.00',
markPrice: '3201.00',
@@ -382,8 +382,8 @@ describe('PerpsLimitPriceBottomSheet', () => {
it('falls back to passed current price when real-time data unavailable', () => {
// Arrange - Mock returns no real-time data
- const { useLivePrices } = jest.requireMock('../../hooks/stream');
- useLivePrices.mockReturnValue({});
+ const { usePerpsLivePrices } = jest.requireMock('../../hooks/stream');
+ usePerpsLivePrices.mockReturnValue({});
// Act
render();
@@ -395,8 +395,8 @@ describe('PerpsLimitPriceBottomSheet', () => {
it('displays unavailable prices when no data', () => {
// Arrange
const propsWithoutPrice = { ...defaultProps, currentPrice: 0 };
- const { useLivePrices } = jest.requireMock('../../hooks/stream');
- useLivePrices.mockReturnValue({});
+ const { usePerpsLivePrices } = jest.requireMock('../../hooks/stream');
+ usePerpsLivePrices.mockReturnValue({});
// Act
render();
@@ -407,8 +407,8 @@ describe('PerpsLimitPriceBottomSheet', () => {
it('calculates default bid/ask spreads when order book data unavailable', () => {
// Arrange - Mock returns only basic price data
- const { useLivePrices } = jest.requireMock('../../hooks/stream');
- useLivePrices.mockReturnValue({
+ const { usePerpsLivePrices } = jest.requireMock('../../hooks/stream');
+ usePerpsLivePrices.mockReturnValue({
ETH: {
price: '3000.00',
markPrice: '3001.00',
diff --git a/app/components/UI/Perps/components/PerpsLimitPriceBottomSheet/PerpsLimitPriceBottomSheet.tsx b/app/components/UI/Perps/components/PerpsLimitPriceBottomSheet/PerpsLimitPriceBottomSheet.tsx
index 64df6e013aee..5d701f6f0c39 100644
--- a/app/components/UI/Perps/components/PerpsLimitPriceBottomSheet/PerpsLimitPriceBottomSheet.tsx
+++ b/app/components/UI/Perps/components/PerpsLimitPriceBottomSheet/PerpsLimitPriceBottomSheet.tsx
@@ -17,7 +17,7 @@ import { useTheme } from '../../../../../util/theme';
import Keypad from '../../../../Base/Keypad';
import { formatPrice } from '../../utils/formatUtils';
import { createStyles } from './PerpsLimitPriceBottomSheet.styles';
-import { useLivePrices } from '../../hooks/stream';
+import { usePerpsLivePrices } from '../../hooks/stream';
import { ORDER_BOOK_SPREAD } from '../../constants/hyperLiquidConfig';
interface PerpsLimitPriceBottomSheetProps {
@@ -44,11 +44,11 @@ const PerpsLimitPriceBottomSheet: React.FC = ({
// Initialize with initial limit price or empty to show placeholder
const [limitPrice, setLimitPrice] = useState(initialLimitPrice || '');
- // Get real-time price data with 500ms debounce for limit price bottom sheet
+ // Get real-time price data with 1000ms throttle for limit price bottom sheet
// Only subscribe when visible
- const priceData = useLivePrices({
+ const priceData = usePerpsLivePrices({
symbols: isVisible ? [asset] : [],
- debounceMs: 500,
+ throttleMs: 1000,
});
const currentPriceData = priceData[asset];
diff --git a/app/components/UI/Perps/components/PerpsMarketHeader/PerpsMarketHeader.test.tsx b/app/components/UI/Perps/components/PerpsMarketHeader/PerpsMarketHeader.test.tsx
index 9a6f7d2d6b59..1be725d252f4 100644
--- a/app/components/UI/Perps/components/PerpsMarketHeader/PerpsMarketHeader.test.tsx
+++ b/app/components/UI/Perps/components/PerpsMarketHeader/PerpsMarketHeader.test.tsx
@@ -8,6 +8,9 @@ import { PerpsMarketHeaderSelectorsIDs } from '../../../../../../e2e/selectors/P
import { PerpsMarketData } from '../../controllers/types';
import ButtonIcon from '../../../../../component-library/components/Buttons/ButtonIcon';
+// Mock PerpsStreamManager
+jest.mock('../../providers/PerpsStreamManager');
+
const mockMarket: PerpsMarketData = {
symbol: 'BTC',
name: 'Bitcoin',
@@ -29,8 +32,6 @@ describe('PerpsMarketHeader', () => {
const { getByTestId } = renderWithProvider(
,
{ state: initialState },
@@ -44,8 +45,6 @@ describe('PerpsMarketHeader', () => {
const { UNSAFE_getByType } = renderWithProvider(
,
@@ -63,8 +62,6 @@ describe('PerpsMarketHeader', () => {
const { UNSAFE_getByType } = renderWithProvider(
,
diff --git a/app/components/UI/Perps/components/PerpsMarketHeader/PerpsMarketHeader.tsx b/app/components/UI/Perps/components/PerpsMarketHeader/PerpsMarketHeader.tsx
index c458525614ed..9488e9253749 100644
--- a/app/components/UI/Perps/components/PerpsMarketHeader/PerpsMarketHeader.tsx
+++ b/app/components/UI/Perps/components/PerpsMarketHeader/PerpsMarketHeader.tsx
@@ -16,20 +16,12 @@ import { useStyles } from '../../../../../component-library/hooks';
import RemoteImage from '../../../../Base/RemoteImage';
import type { PerpsMarketData } from '../../controllers/types';
import { usePerpsAssetMetadata } from '../../hooks/usePerpsAssetsMetadata';
-import {
- formatPercentage,
- formatPnl,
- formatPrice,
- parseCurrencyString,
- parsePercentageString,
-} from '../../utils/formatUtils';
import { styleSheet } from './PerpsMarketHeader.styles';
import { PerpsMarketHeaderSelectorsIDs } from '../../../../../../e2e/selectors/Perps/Perps.selectors';
+import LivePriceHeader from '../LivePriceDisplay/LivePriceHeader';
interface PerpsMarketHeaderProps {
market: PerpsMarketData;
- currentPrice?: number;
- priceChange24h?: number;
onBackPress?: () => void;
onMorePress?: () => void;
testID?: string;
@@ -37,8 +29,6 @@ interface PerpsMarketHeaderProps {
const PerpsMarketHeader: React.FC = ({
market,
- currentPrice,
- priceChange24h,
onBackPress,
onMorePress,
testID,
@@ -46,14 +36,6 @@ const PerpsMarketHeader: React.FC = ({
const { styles } = useStyles(styleSheet, {});
const { assetUrl } = usePerpsAssetMetadata(market.symbol);
- const displayPrice = currentPrice || parseCurrencyString(market.price || '0');
- const displayChange =
- priceChange24h ?? parsePercentageString(market.change24hPercent);
- const isPositiveChange = displayChange >= 0;
-
- // Calculate fiat change amount
- const changeAmount = (displayChange / 100) * displayPrice;
-
return (
{/* Back Button */}
@@ -93,21 +75,14 @@ const PerpsMarketHeader: React.FC = ({
-
- {formatPrice(displayPrice)}
-
-
- {formatPnl(changeAmount)} (
- {formatPercentage(displayChange.toString())})
-
+
diff --git a/app/components/UI/Perps/components/PerpsMarketStatisticsCard/PerpsMarketStatisticsCard.test.tsx b/app/components/UI/Perps/components/PerpsMarketStatisticsCard/PerpsMarketStatisticsCard.test.tsx
index b915eb988651..7a4bfd95e111 100644
--- a/app/components/UI/Perps/components/PerpsMarketStatisticsCard/PerpsMarketStatisticsCard.test.tsx
+++ b/app/components/UI/Perps/components/PerpsMarketStatisticsCard/PerpsMarketStatisticsCard.test.tsx
@@ -48,7 +48,6 @@ describe('PerpsMarketStatisticsCard', () => {
volume24h: '$1,234,567.89',
openInterest: '$987,654.32',
fundingRate: '0.0125%',
- fundingCountdown: '02:15:30',
currentPrice: 47500,
priceChange24h: 0.05,
isLoading: false,
@@ -86,7 +85,6 @@ describe('PerpsMarketStatisticsCard', () => {
// Check funding rate row
expect(getByText('perps.market.funding_rate')).toBeOnTheScreen();
expect(getByText('0.0125%')).toBeOnTheScreen();
- expect(getByText('(02:15:30)')).toBeOnTheScreen();
});
it('displays positive funding rate in success color', () => {
@@ -177,7 +175,6 @@ describe('PerpsMarketStatisticsCard', () => {
expect(getByTestId('perps-statistics-volume-24h')).toBeOnTheScreen();
expect(getByTestId('perps-statistics-open-interest')).toBeOnTheScreen();
expect(getByTestId('perps-statistics-funding-rate')).toBeOnTheScreen();
- expect(getByTestId('perps-statistics-funding-countdown')).toBeOnTheScreen();
});
it('handles edge case with very small funding rate values', () => {
@@ -212,15 +209,6 @@ describe('PerpsMarketStatisticsCard', () => {
expect(getByText('15.7500%')).toBeOnTheScreen();
});
- it('renders funding countdown in parentheses', () => {
- const { getByText } = render(
- ,
- );
-
- const countdownText = getByText('(02:15:30)');
- expect(countdownText).toBeOnTheScreen();
- });
-
it('displays all market statistics with proper formatting', () => {
const { getByText } = render(
,
@@ -232,7 +220,6 @@ describe('PerpsMarketStatisticsCard', () => {
expect(getByText('$1,234,567.89')).toBeOnTheScreen(); // volume24h
expect(getByText('$987,654.32')).toBeOnTheScreen(); // openInterest
expect(getByText('0.0125%')).toBeOnTheScreen(); // fundingRate
- expect(getByText('(02:15:30)')).toBeOnTheScreen(); // fundingCountdown
});
it('calls onTooltipPress only when info icons are pressed', () => {
diff --git a/app/components/UI/Perps/components/PerpsMarketStatisticsCard/PerpsMarketStatisticsCard.tsx b/app/components/UI/Perps/components/PerpsMarketStatisticsCard/PerpsMarketStatisticsCard.tsx
index 89adcdc5aa4c..4f182cf766dc 100644
--- a/app/components/UI/Perps/components/PerpsMarketStatisticsCard/PerpsMarketStatisticsCard.tsx
+++ b/app/components/UI/Perps/components/PerpsMarketStatisticsCard/PerpsMarketStatisticsCard.tsx
@@ -13,12 +13,14 @@ import Text, {
import { useStyles } from '../../../../hooks/useStyles';
import styleSheet from './PerpsMarketStatisticsCard.styles';
import type { PerpsMarketStatisticsCardProps } from './PerpsMarketStatisticsCard.types';
-// TODO: Consider renaming to PerpsMarketStatisticsCard since it isn't tied to a specific view anymore
import { PerpsMarketDetailsViewSelectorsIDs } from '../../../../../../e2e/selectors/Perps/Perps.selectors';
+import FundingCountdown from '../FundingCountdown';
const PerpsMarketStatisticsCard: React.FC = ({
marketStats,
onTooltipPress,
+ nextFundingTime,
+ fundingIntervalHours,
}) => {
const { styles } = useStyles(styleSheet, {});
@@ -120,16 +122,16 @@ const PerpsMarketStatisticsCard: React.FC = ({
>
{marketStats.fundingRate}
-
- ({marketStats.fundingCountdown})
-
+ />
diff --git a/app/components/UI/Perps/components/PerpsMarketStatisticsCard/PerpsMarketStatisticsCard.types.ts b/app/components/UI/Perps/components/PerpsMarketStatisticsCard/PerpsMarketStatisticsCard.types.ts
index 3a9facbfe743..ab2c38acd1ea 100644
--- a/app/components/UI/Perps/components/PerpsMarketStatisticsCard/PerpsMarketStatisticsCard.types.ts
+++ b/app/components/UI/Perps/components/PerpsMarketStatisticsCard/PerpsMarketStatisticsCard.types.ts
@@ -4,4 +4,12 @@ import type { PerpsTooltipContentKey } from '../PerpsBottomSheetTooltip';
export interface PerpsMarketStatisticsCardProps {
marketStats: ReturnType;
onTooltipPress: (contentKey: PerpsTooltipContentKey) => void;
+ /**
+ * Next funding time in milliseconds since epoch (optional, market-specific)
+ */
+ nextFundingTime?: number;
+ /**
+ * Funding interval in hours (optional, market-specific)
+ */
+ fundingIntervalHours?: number;
}
diff --git a/app/components/UI/Perps/components/PerpsMarketTabs/PerpsMarketTabs.test.tsx b/app/components/UI/Perps/components/PerpsMarketTabs/PerpsMarketTabs.test.tsx
index 7cb0352568c5..22d237f11fb0 100644
--- a/app/components/UI/Perps/components/PerpsMarketTabs/PerpsMarketTabs.test.tsx
+++ b/app/components/UI/Perps/components/PerpsMarketTabs/PerpsMarketTabs.test.tsx
@@ -107,13 +107,11 @@ jest.mock('../../../../../core/Engine', () => ({
const mockMarketStats: PerpsMarketTabsProps['marketStats'] = {
currentPrice: 45000,
- priceChange24h: 1125,
high24h: '$46,000.00',
low24h: '$44,000.00',
volume24h: '$1.23B',
openInterest: '$500M',
fundingRate: '+0.01%',
- fundingCountdown: '5h 30m',
isLoading: false,
refresh: jest.fn(),
};
diff --git a/app/components/UI/Perps/components/PerpsMarketTabs/PerpsMarketTabs.tsx b/app/components/UI/Perps/components/PerpsMarketTabs/PerpsMarketTabs.tsx
index e6210247e55a..00e7d166c8ee 100644
--- a/app/components/UI/Perps/components/PerpsMarketTabs/PerpsMarketTabs.tsx
+++ b/app/components/UI/Perps/components/PerpsMarketTabs/PerpsMarketTabs.tsx
@@ -33,7 +33,8 @@ const PerpsMarketTabs: React.FC = ({
unfilledOrders = [],
onPositionUpdate,
onActiveTabChange,
- priceData,
+ nextFundingTime,
+ fundingIntervalHours,
}) => {
const { styles } = useStyles(styleSheet, {});
const fadeAnim = useRef(new Animated.Value(0)).current;
@@ -167,12 +168,20 @@ const PerpsMarketTabs: React.FC = ({
{renderTooltipModal()}
);
}
+ const getTabTestId = (tabId: string) => {
+ if (tabId === 'position') return PerpsMarketTabsSelectorsIDs.POSITION_TAB;
+ if (tabId === 'orders') return PerpsMarketTabsSelectorsIDs.ORDERS_TAB;
+ return PerpsMarketTabsSelectorsIDs.STATISTICS_TAB;
+ };
+
const renderTabBar = () => (
{tabs.map((tab) => {
@@ -183,13 +192,7 @@ const PerpsMarketTabs: React.FC = ({
style={[styles.tab]}
onPress={() => handleTabChange(tab.id)}
activeOpacity={0.7}
- testID={
- tab.id === 'position'
- ? PerpsMarketTabsSelectorsIDs.POSITION_TAB
- : tab.id === 'orders'
- ? PerpsMarketTabsSelectorsIDs.ORDERS_TAB
- : PerpsMarketTabsSelectorsIDs.STATISTICS_TAB
- }
+ testID={getTabTestId(tab.id)}
>
= ({
expanded
showIcon
onPositionUpdate={onPositionUpdate}
- priceData={priceData}
/>
);
@@ -233,6 +235,8 @@ const PerpsMarketTabs: React.FC = ({
);
diff --git a/app/components/UI/Perps/components/PerpsMarketTabs/PerpsMarketTabs.types.ts b/app/components/UI/Perps/components/PerpsMarketTabs/PerpsMarketTabs.types.ts
index fbf18134ffaa..9165823b318b 100644
--- a/app/components/UI/Perps/components/PerpsMarketTabs/PerpsMarketTabs.types.ts
+++ b/app/components/UI/Perps/components/PerpsMarketTabs/PerpsMarketTabs.types.ts
@@ -1,4 +1,4 @@
-import type { Position, Order, PriceUpdate } from '../../controllers/types';
+import type { Position, Order } from '../../controllers/types';
import { usePerpsMarketStats } from '../../hooks';
export interface TabViewProps {
@@ -12,5 +12,12 @@ export interface PerpsMarketTabsProps {
unfilledOrders: Order[];
onPositionUpdate?: () => Promise;
onActiveTabChange?: (tabId: string) => void;
- priceData?: PriceUpdate | null;
+ /**
+ * Next funding time in milliseconds since epoch (optional, market-specific)
+ */
+ nextFundingTime?: number;
+ /**
+ * Funding interval in hours (optional, market-specific)
+ */
+ fundingIntervalHours?: number;
}
diff --git a/app/components/UI/Perps/components/PerpsOpenOrderCard/PerpsOpenOrderCard.tsx b/app/components/UI/Perps/components/PerpsOpenOrderCard/PerpsOpenOrderCard.tsx
index e3541a1776a6..9d7144a2cd5d 100644
--- a/app/components/UI/Perps/components/PerpsOpenOrderCard/PerpsOpenOrderCard.tsx
+++ b/app/components/UI/Perps/components/PerpsOpenOrderCard/PerpsOpenOrderCard.tsx
@@ -70,7 +70,17 @@ const PerpsOpenOrderCard: React.FC = ({
// Derive order data for display
const derivedData = useMemo(() => {
- const direction = order.side === 'buy' ? 'long' : 'short';
+ // For reduce-only orders (TP/SL), show them as closing positions
+ let direction: OpenOrderCardDerivedData['direction'];
+ if (order.reduceOnly || order.isTrigger) {
+ // This is a TP/SL order closing a position
+ // If side is 'sell', it's closing a long position
+ // If side is 'buy', it's closing a short position
+ direction = order.side === 'sell' ? 'Close Long' : 'Close Short';
+ } else {
+ // Regular order
+ direction = order.side === 'buy' ? 'long' : 'short';
+ }
// Calculate size in USD
const sizeInUSD = BigNumber(order.originalSize)
diff --git a/app/components/UI/Perps/components/PerpsOpenOrderCard/PerpsOpenOrderCard.types.ts b/app/components/UI/Perps/components/PerpsOpenOrderCard/PerpsOpenOrderCard.types.ts
index 16e36310e87e..317518716ddb 100644
--- a/app/components/UI/Perps/components/PerpsOpenOrderCard/PerpsOpenOrderCard.types.ts
+++ b/app/components/UI/Perps/components/PerpsOpenOrderCard/PerpsOpenOrderCard.types.ts
@@ -10,7 +10,7 @@ export interface PerpsOpenOrderCardProps {
}
export interface OpenOrderCardDerivedData {
- direction: 'long' | 'short';
+ direction: 'long' | 'short' | 'Close Long' | 'Close Short';
sizeInUSD: string;
fillPercentage: number;
}
diff --git a/app/components/UI/Perps/components/PerpsPositionCard/PerpsPositionCard.test.tsx b/app/components/UI/Perps/components/PerpsPositionCard/PerpsPositionCard.test.tsx
index eb7211091ad8..4cf969939d17 100644
--- a/app/components/UI/Perps/components/PerpsPositionCard/PerpsPositionCard.test.tsx
+++ b/app/components/UI/Perps/components/PerpsPositionCard/PerpsPositionCard.test.tsx
@@ -428,91 +428,8 @@ describe('PerpsPositionCard', () => {
});
describe('Hook Integration', () => {
- it('calls loadPositions when usePerpsTPSLUpdate onSuccess is triggered', async () => {
- // Arrange
- const mockLoadPositions = jest.fn().mockResolvedValue(undefined);
- const mockOnPositionUpdate = jest.fn().mockResolvedValue(undefined);
- const mockHandleUpdateTPSL = jest.fn().mockResolvedValue(undefined);
-
- const { usePerpsPositions, usePerpsTPSLUpdate } =
- jest.requireMock('../../hooks');
-
- usePerpsPositions.mockReturnValue({
- loadPositions: mockLoadPositions,
- });
-
- // Mock implementation to capture and immediately call the onSuccess callback
- usePerpsTPSLUpdate.mockImplementation(
- ({ onSuccess }: { onSuccess: () => void }) => {
- // Simulate the onSuccess callback being called
- setTimeout(() => {
- onSuccess();
- }, 0);
- return {
- handleUpdateTPSL: mockHandleUpdateTPSL,
- isUpdating: false,
- };
- },
- );
-
- // Act
- render(
- ,
- );
-
- // Wait for async operations to complete
- await new Promise((resolve) => setTimeout(resolve, 10));
-
- // Assert
- expect(mockLoadPositions).toHaveBeenCalledWith({ isRefresh: true });
- expect(mockOnPositionUpdate).toHaveBeenCalled();
- });
-
- it('calls loadPositions when usePerpsClosePosition onSuccess is triggered', async () => {
- // Arrange
- const mockLoadPositions = jest.fn().mockResolvedValue(undefined);
- const mockOnPositionUpdate = jest.fn().mockResolvedValue(undefined);
- const mockHandleClosePosition = jest.fn().mockResolvedValue(undefined);
-
- const { usePerpsPositions, usePerpsClosePosition } =
- jest.requireMock('../../hooks');
-
- usePerpsPositions.mockReturnValue({
- loadPositions: mockLoadPositions,
- });
-
- // Mock implementation to capture and immediately call the onSuccess callback
- usePerpsClosePosition.mockImplementation(
- ({ onSuccess }: { onSuccess: () => void }) => {
- // Simulate the onSuccess callback being called
- setTimeout(() => {
- onSuccess();
- }, 0);
- return {
- handleClosePosition: mockHandleClosePosition,
- isClosing: false,
- };
- },
- );
-
- // Act
- render(
- ,
- );
-
- // Wait for async operations to complete
- await new Promise((resolve) => setTimeout(resolve, 10));
-
- // Assert
- expect(mockLoadPositions).toHaveBeenCalledWith({ isRefresh: true });
- expect(mockOnPositionUpdate).toHaveBeenCalled();
- });
+ // Tests removed - loadPositions no longer exists with WebSocket streaming
+ // Positions update automatically via WebSocket subscriptions
it('returns early from handleCardPress when isLoading is true', () => {
// Arrange
diff --git a/app/components/UI/Perps/components/PerpsPositionCard/PerpsPositionCard.tsx b/app/components/UI/Perps/components/PerpsPositionCard/PerpsPositionCard.tsx
index a4daaa336441..2b9966c913cc 100644
--- a/app/components/UI/Perps/components/PerpsPositionCard/PerpsPositionCard.tsx
+++ b/app/components/UI/Perps/components/PerpsPositionCard/PerpsPositionCard.tsx
@@ -36,7 +36,6 @@ import { usePerpsAssetMetadata } from '../../hooks/usePerpsAssetsMetadata';
import RemoteImage from '../../../../Base/RemoteImage';
import {
usePerpsMarkets,
- usePerpsPositions,
usePerpsTPSLUpdate,
usePerpsClosePosition,
} from '../../hooks';
@@ -70,32 +69,23 @@ const PerpsPositionCard: React.FC = ({
null,
);
- const { loadPositions } = usePerpsPositions({
- loadOnMount: true,
- refreshOnFocus: true,
- });
-
const { handleUpdateTPSL, isUpdating } = usePerpsTPSLUpdate({
onSuccess: () => {
- // Refresh positions to show updated data
- loadPositions({ isRefresh: true }).then(() => {
- // Also call parent's position update callback if provided
- if (onPositionUpdate) {
- onPositionUpdate();
- }
- });
+ // Positions update automatically via WebSocket
+ // Call parent's position update callback if provided
+ if (onPositionUpdate) {
+ onPositionUpdate();
+ }
},
});
const { handleClosePosition, isClosing } = usePerpsClosePosition({
onSuccess: () => {
- // Refresh positions after successful close
- loadPositions({ isRefresh: true }).then(() => {
- // Also call parent's position update callback if provided
- if (onPositionUpdate) {
- onPositionUpdate();
- }
- });
+ // Positions update automatically via WebSocket
+ // Call parent's position update callback if provided
+ if (onPositionUpdate) {
+ onPositionUpdate();
+ }
setIsClosePositionVisible(false);
setSelectedPosition(null);
},
diff --git a/app/components/UI/Perps/components/PerpsTPSLBottomSheet/PerpsTPSLBottomSheet.test.tsx b/app/components/UI/Perps/components/PerpsTPSLBottomSheet/PerpsTPSLBottomSheet.test.tsx
index d05ef4508f58..653ca3ec379d 100644
--- a/app/components/UI/Perps/components/PerpsTPSLBottomSheet/PerpsTPSLBottomSheet.test.tsx
+++ b/app/components/UI/Perps/components/PerpsTPSLBottomSheet/PerpsTPSLBottomSheet.test.tsx
@@ -57,7 +57,7 @@ jest.mock('../../hooks', () => ({
// Mock stream hooks
jest.mock('../../hooks/stream', () => ({
- useLivePrices: jest.fn(() => ({})), // Return empty object for prices
+ usePerpsLivePrices: jest.fn(() => ({})), // Return empty object for prices
}));
// Mock format utilities
diff --git a/app/components/UI/Perps/components/PerpsTPSLBottomSheet/PerpsTPSLBottomSheet.tsx b/app/components/UI/Perps/components/PerpsTPSLBottomSheet/PerpsTPSLBottomSheet.tsx
index 5dced08d4d5a..cdbcc1ad70b7 100644
--- a/app/components/UI/Perps/components/PerpsTPSLBottomSheet/PerpsTPSLBottomSheet.tsx
+++ b/app/components/UI/Perps/components/PerpsTPSLBottomSheet/PerpsTPSLBottomSheet.tsx
@@ -25,7 +25,7 @@ import { strings } from '../../../../../../locales/i18n';
import type { Position } from '../../controllers/types';
import { createStyles } from './PerpsTPSLBottomSheet.styles';
import { usePerpsPerformance } from '../../hooks';
-import { useLivePrices } from '../../hooks/stream';
+import { usePerpsLivePrices } from '../../hooks/stream';
import { PerpsMeasurementName } from '../../constants/performanceMetrics';
import {
PerpsEventProperties,
@@ -106,9 +106,9 @@ const PerpsTPSLBottomSheet: React.FC = ({
// Subscribe to real-time price only when visible and we have an asset
// Use 1s debounce for TP/SL bottom sheet
- const priceData = useLivePrices({
+ const priceData = usePerpsLivePrices({
symbols: isVisible && asset ? [asset] : [],
- debounceMs: 1000,
+ throttleMs: 1000,
});
const livePrice = priceData[asset]?.price
? parseFloat(priceData[asset].price)
diff --git a/app/components/UI/Perps/components/PerpsTabControlBar/PerpsTabControlBar.test.tsx b/app/components/UI/Perps/components/PerpsTabControlBar/PerpsTabControlBar.test.tsx
index 056ee2f186ae..cfc1af2b8b83 100644
--- a/app/components/UI/Perps/components/PerpsTabControlBar/PerpsTabControlBar.test.tsx
+++ b/app/components/UI/Perps/components/PerpsTabControlBar/PerpsTabControlBar.test.tsx
@@ -1,20 +1,19 @@
/* eslint-disable import/no-namespace */
-import React from 'react';
import {
+ fireEvent,
render,
screen,
- fireEvent,
waitFor,
- act,
} from '@testing-library/react-native';
+import React from 'react';
import { Animated } from 'react-native';
-import PerpsTabControlBar from './PerpsTabControlBar';
-import * as PerpsHooks from '../../hooks';
import * as ComponentLibraryHooks from '../../../../../component-library/hooks';
import DevLogger from '../../../../../core/SDKConnect/utils/DevLogger';
-import { Position } from '../../controllers';
+import * as PerpsHooks from '../../hooks';
+import PerpsTabControlBar from './PerpsTabControlBar';
// Mock dependencies
+jest.mock('../../providers/PerpsStreamManager');
jest.mock('../../../../../component-library/hooks', () => ({
useStyles: jest.fn(() => ({
styles: {
@@ -33,6 +32,13 @@ jest.mock('../../hooks', () => ({
useBalanceComparison: jest.fn(),
}));
+jest.mock('../../hooks/stream', () => ({
+ usePerpsLivePositions: jest.fn(() => ({
+ positions: [],
+ isInitialLoading: false,
+ })),
+}));
+
jest.mock('../../utils/formatUtils', () => ({
formatPerpsFiat: jest.fn(
(balance: string) => `$${parseFloat(balance || '0').toFixed(2)}`,
@@ -87,7 +93,6 @@ describe('PerpsTabControlBar', () => {
// Mock implementations
const mockGetAccountState = jest.fn();
- const mockSubscribeToPositions = jest.fn();
const mockStartPulseAnimation = jest.fn();
const mockStopAnimation = jest.fn();
const mockCompareAndUpdateBalance = jest.fn();
@@ -125,7 +130,6 @@ describe('PerpsTabControlBar', () => {
getPositions: jest.fn(),
getAccountState: mockGetAccountState,
subscribeToPrices: jest.fn(),
- subscribeToPositions: mockSubscribeToPositions,
subscribeToOrderFills: jest.fn(),
deposit: jest.fn(),
getDepositRoutes: jest.fn(),
@@ -150,9 +154,6 @@ describe('PerpsTabControlBar', () => {
// Default successful responses
mockGetAccountState.mockResolvedValue(defaultAccountState);
- mockSubscribeToPositions.mockReturnValue(() => {
- /* empty unsubscribe function */
- });
mockCompareAndUpdateBalance.mockReturnValue('increase');
});
@@ -242,55 +243,7 @@ describe('PerpsTabControlBar', () => {
});
});
- describe('WebSocket Subscription and Polling', () => {
- it('subscribes to position updates on mount', async () => {
- render();
-
- expect(mockSubscribeToPositions).toHaveBeenCalledWith({
- callback: expect.any(Function),
- });
- });
-
- it('refreshes balance when position updates are received', async () => {
- let positionCallback: ((positions: Position[]) => void) | null = null;
- mockSubscribeToPositions.mockImplementation(({ callback }) => {
- positionCallback = callback;
- return () => {
- /* empty unsubscribe function */
- };
- });
-
- render();
-
- await waitFor(() => {
- expect(mockSubscribeToPositions).toHaveBeenCalled();
- });
-
- // Simulate position update
- await act(async () => {
- positionCallback?.([
- { id: '1', symbol: 'BTC' },
- ] as unknown as Position[]);
- });
-
- await waitFor(() => {
- expect(mockGetAccountState).toHaveBeenCalledTimes(2); // Initial + position update
- });
- });
-
- it('only calls getAccountState once without position updates', async () => {
- render();
-
- // Fast forward time to ensure no polling occurs
- act(() => {
- jest.advanceTimersByTime(60000);
- });
-
- await waitFor(() => {
- expect(mockGetAccountState).toHaveBeenCalledTimes(1); // Initial load only
- });
- });
- });
+ // WebSocket subscription tests removed - usePerpsLivePositions handles subscriptions internally
describe('Press Handler', () => {
it('calls onManageBalancePress when pressed', async () => {
@@ -371,16 +324,7 @@ describe('PerpsTabControlBar', () => {
});
describe('Cleanup and Memory Management', () => {
- it('cleans up subscription on unmount', async () => {
- const mockUnsubscribe = jest.fn();
- mockSubscribeToPositions.mockReturnValue(mockUnsubscribe);
-
- const { unmount } = render();
-
- unmount();
-
- expect(mockUnsubscribe).toHaveBeenCalled();
- });
+ // Subscription cleanup test removed - handled by usePerpsLivePositions internally
it('stops animation on unmount', async () => {
const { unmount } = render();
@@ -390,13 +334,7 @@ describe('PerpsTabControlBar', () => {
expect(mockStopAnimation).toHaveBeenCalled();
});
- it('handles cleanup when subscription returns null', async () => {
- mockSubscribeToPositions.mockReturnValue(null);
-
- const { unmount } = render();
-
- expect(() => unmount()).not.toThrow();
- });
+ // Null subscription test removed - no longer applicable
});
describe('Edge Cases', () => {
@@ -426,42 +364,7 @@ describe('PerpsTabControlBar', () => {
});
});
- it('handles multiple rapid balance updates via position changes', async () => {
- let positionCallback: ((positions: Position[]) => void) | undefined;
- mockSubscribeToPositions.mockImplementation(({ callback }) => {
- positionCallback = callback;
- return jest.fn();
- });
-
- render();
-
- // Simulate multiple rapid position updates with different values
- await act(async () => {
- positionCallback?.([
- {
- coin: 'BTC',
- size: '1.0',
- entryPrice: '50000',
- unrealizedPnl: '100',
- },
- ] as unknown as Position[]);
- positionCallback?.([
- {
- coin: 'ETH',
- size: '2.0',
- entryPrice: '3000',
- unrealizedPnl: '200',
- },
- ] as unknown as Position[]);
- positionCallback?.([
- { coin: 'SOL', size: '3.0', entryPrice: '100', unrealizedPnl: '300' },
- ] as unknown as Position[]);
- });
-
- await waitFor(() => {
- expect(mockGetAccountState).toHaveBeenCalledTimes(4); // Initial + 3 position updates
- });
- });
+ // Test removed - position updates handled by usePerpsLivePositions internally
});
describe('Integration', () => {
diff --git a/app/components/UI/Perps/components/PerpsTabControlBar/PerpsTabControlBar.tsx b/app/components/UI/Perps/components/PerpsTabControlBar/PerpsTabControlBar.tsx
index 02438ffff717..21f8a7debf7b 100644
--- a/app/components/UI/Perps/components/PerpsTabControlBar/PerpsTabControlBar.tsx
+++ b/app/components/UI/Perps/components/PerpsTabControlBar/PerpsTabControlBar.tsx
@@ -18,6 +18,7 @@ import {
useColorPulseAnimation,
useBalanceComparison,
} from '../../hooks';
+import { usePerpsLivePositions } from '../../hooks/stream';
import { AccountState } from '../../controllers';
import DevLogger from '../../../../../core/SDKConnect/utils/DevLogger';
import { formatPerpsFiat } from '../../utils/formatUtils';
@@ -37,7 +38,7 @@ export const PerpsTabControlBar: React.FC = ({
unrealizedPnl: '',
});
- const { getAccountState, subscribeToPositions } = usePerpsTrading();
+ const { getAccountState } = usePerpsTrading();
// Use the reusable hooks
const { startPulseAnimation, getAnimatedStyle, stopAnimation } =
@@ -78,54 +79,47 @@ export const PerpsTabControlBar: React.FC = ({
// Track last positions hash to detect actual changes
const lastPositionsHashRef = useRef('');
- // Auto-refresh setup with WebSocket subscription + polling fallback
+ // Use StreamManager for real-time position updates
+ const { positions } = usePerpsLivePositions({
+ throttleMs: 2000, // Check every 2 seconds for balance updates
+ });
+
+ // Auto-refresh balance when positions change
useEffect(() => {
// Initial load
getAccountBalance();
+ }, [getAccountBalance]);
- // Set up WebSocket subscription for real-time position updates
- let unsubscribePositions: (() => void) | null = null;
-
- try {
- unsubscribePositions = subscribeToPositions({
- callback: (positions) => {
- // Create a simple hash of positions to detect actual changes
- const positionsHash = JSON.stringify(
- positions.map((p) => ({
- coin: p.coin,
- size: p.size,
- entryPrice: p.entryPrice,
- unrealizedPnl: p.unrealizedPnl,
- })),
- );
-
- // Only refresh if positions actually changed
- if (positionsHash !== lastPositionsHashRef.current) {
- DevLogger.log(
- 'PerpsTabControlBar: Position change detected, refreshing balance',
- );
- lastPositionsHashRef.current = positionsHash;
- getAccountBalance();
- }
- },
- });
- } catch (error) {
+ // Monitor position changes and refresh balance
+ useEffect(() => {
+ // Create a simple hash of positions to detect actual changes
+ const positionsHash = JSON.stringify(
+ positions.map((p) => ({
+ coin: p.coin,
+ size: p.size,
+ entryPrice: p.entryPrice,
+ unrealizedPnl: p.unrealizedPnl,
+ })),
+ );
+
+ // Only refresh if positions actually changed
+ if (
+ positionsHash !== lastPositionsHashRef.current &&
+ lastPositionsHashRef.current !== ''
+ ) {
DevLogger.log(
- 'PerpsTabControlBar: Failed to subscribe to positions, using polling only',
- error,
+ 'PerpsTabControlBar: Position change detected, refreshing balance',
);
+ lastPositionsHashRef.current = positionsHash;
+ getAccountBalance();
+ } else if (lastPositionsHashRef.current === '') {
+ // First time, just store the hash
+ lastPositionsHashRef.current = positionsHash;
}
+ }, [positions, getAccountBalance]);
- return () => {
- // Cleanup WebSocket subscription
- if (unsubscribePositions) {
- unsubscribePositions();
- }
-
- // Cleanup animations
- stopAnimation();
- };
- }, [getAccountBalance, subscribeToPositions, stopAnimation]);
+ // Cleanup animations on unmount
+ useEffect(() => () => stopAnimation(), [stopAnimation]);
const handlePress = () => {
onManageBalancePress?.();
diff --git a/app/components/UI/Perps/controllers/PerpsController.ts b/app/components/UI/Perps/controllers/PerpsController.ts
index f426b9aab0ba..762f0be52532 100644
--- a/app/components/UI/Perps/controllers/PerpsController.ts
+++ b/app/components/UI/Perps/controllers/PerpsController.ts
@@ -53,6 +53,7 @@ import type {
OrderResult,
Position,
SubscribeOrderFillsParams,
+ SubscribeOrdersParams,
SubscribePositionsParams,
SubscribePricesParams,
SwitchProviderResult,
@@ -1711,6 +1712,14 @@ export class PerpsController extends BaseController<
return provider.subscribeToOrderFills(params);
}
+ /**
+ * Subscribe to live order updates
+ */
+ subscribeToOrders(params: SubscribeOrdersParams): () => void {
+ const provider = this.getActiveProvider();
+ return provider.subscribeToOrders(params);
+ }
+
/**
* Configure live data throttling
*/
diff --git a/app/components/UI/Perps/controllers/providers/HyperLiquidProvider.test.ts b/app/components/UI/Perps/controllers/providers/HyperLiquidProvider.test.ts
index 3020ca792fb4..f383b30b3ed5 100644
--- a/app/components/UI/Perps/controllers/providers/HyperLiquidProvider.test.ts
+++ b/app/components/UI/Perps/controllers/providers/HyperLiquidProvider.test.ts
@@ -2475,12 +2475,13 @@ describe('HyperLiquidProvider', () => {
expect(result).toEqual([]);
});
- it('should handle metaAndAssetCtxs call successfully', async () => {
+ it('should handle meta and predictedFundings calls successfully', async () => {
mockClientService.getInfoClient = jest.fn().mockReturnValue({
meta: jest.fn().mockResolvedValue({
universe: [{ name: 'BTC', szDecimals: 3, maxLeverage: 50 }],
}),
allMids: jest.fn().mockResolvedValue({ BTC: '50000' }),
+ predictedFundings: jest.fn().mockResolvedValue([]),
metaAndAssetCtxs: jest.fn().mockResolvedValue([
{ universe: [{ name: 'BTC', szDecimals: 3, maxLeverage: 50 }] },
[
@@ -2496,8 +2497,9 @@ describe('HyperLiquidProvider', () => {
const result = await provider.getMarketDataWithPrices();
expect(Array.isArray(result)).toBe(true);
+ expect(mockClientService.getInfoClient().meta).toHaveBeenCalled();
expect(
- mockClientService.getInfoClient().metaAndAssetCtxs,
+ mockClientService.getInfoClient().predictedFundings,
).toHaveBeenCalled();
});
});
diff --git a/app/components/UI/Perps/controllers/providers/HyperLiquidProvider.ts b/app/components/UI/Perps/controllers/providers/HyperLiquidProvider.ts
index 8a1af42a754a..d0165e15aa79 100644
--- a/app/components/UI/Perps/controllers/providers/HyperLiquidProvider.ts
+++ b/app/components/UI/Perps/controllers/providers/HyperLiquidProvider.ts
@@ -22,6 +22,7 @@ import { HyperLiquidWalletService } from '../../services/HyperLiquidWalletServic
import {
adaptAccountStateFromSDK,
adaptMarketFromSDK,
+ adaptOrderFromSDK,
adaptPositionFromSDK,
buildAssetMapping,
formatHyperLiquidPrice,
@@ -70,6 +71,7 @@ import type {
Position,
ReadyToTradeResult,
SubscribeOrderFillsParams,
+ SubscribeOrdersParams,
SubscribePositionsParams,
SubscribePricesParams,
ToggleTestnetResult,
@@ -1177,62 +1179,8 @@ export class HyperLiquidProvider implements IPerpsProvider {
DevLogger.log('Currently open orders received:', rawOrders);
- // Transform HyperLiquid open orders to abstract Order type
- const orders: Order[] = (rawOrders || []).map((rawOrder) => {
- const orderId = rawOrder.oid?.toString() || '';
- const symbol = rawOrder.coin;
- const side = rawOrder.side === 'B' ? 'buy' : 'sell';
- const detailedOrderType = rawOrder.orderType || '';
- const orderType = detailedOrderType.toLowerCase().includes('limit')
- ? 'limit'
- : 'market';
- const size = rawOrder.sz;
- const originalSize = rawOrder.origSz || size;
- const price = rawOrder.limitPx || rawOrder.triggerPx || '0';
- const isTrigger = rawOrder.isTrigger || false;
- const reduceOnly = rawOrder.reduceOnly || false;
-
- // Calculate filled and remaining size
- const currentSize = parseFloat(size);
- const origSize = parseFloat(originalSize);
- const filledSize = origSize - currentSize;
-
- // Check for TP/SL in child orders
- let takeProfitPrice: string | undefined;
- let stopLossPrice: string | undefined;
-
- if (rawOrder.children && rawOrder.children.length > 0) {
- rawOrder.children.forEach((child: typeof rawOrder) => {
- if (child.isTrigger && child.orderType) {
- if (child.orderType.includes('Take Profit')) {
- takeProfitPrice = child.triggerPx || child.limitPx;
- } else if (child.orderType.includes('Stop')) {
- stopLossPrice = child.triggerPx || child.limitPx;
- }
- }
- });
- }
-
- return {
- orderId,
- symbol,
- side,
- orderType,
- size,
- originalSize,
- price,
- filledSize: filledSize.toString(),
- remainingSize: size,
- status: 'open' as const,
- timestamp: rawOrder.timestamp,
- lastUpdated: rawOrder.timestamp,
- takeProfitPrice,
- stopLossPrice,
- detailedOrderType,
- isTrigger,
- reduceOnly,
- };
- });
+ // Transform HyperLiquid open orders to abstract Order type using adapter
+ const orders: Order[] = (rawOrders || []).map(adaptOrderFromSDK);
return orders;
} catch (error) {
@@ -1355,9 +1303,10 @@ export class HyperLiquidProvider implements IPerpsProvider {
const infoClient = this.clientService.getInfoClient();
// Fetch all required data in parallel for better performance
- const [perpsMeta, allMids] = await Promise.all([
+ const [perpsMeta, allMids, predictedFundings] = await Promise.all([
infoClient.meta(),
infoClient.allMids(),
+ infoClient.predictedFundings(),
]);
if (!perpsMeta?.universe || !allMids) {
@@ -1373,6 +1322,7 @@ export class HyperLiquidProvider implements IPerpsProvider {
universe: perpsMeta.universe,
assetCtxs,
allMids,
+ predictedFundings,
});
} catch (error) {
DevLogger.log('Error getting market data with prices:', error);
@@ -1453,6 +1403,8 @@ export class HyperLiquidProvider implements IPerpsProvider {
};
}
} catch (error) {
+ // Log the error before falling back
+ DevLogger.log('Failed to get max leverage for symbol', error);
// If we can't get max leverage, use the default as fallback
const defaultMaxLeverage = PERPS_CONSTANTS.DEFAULT_MAX_LEVERAGE;
if (params.leverage < 1 || params.leverage > defaultMaxLeverage) {
@@ -1810,6 +1762,13 @@ export class HyperLiquidProvider implements IPerpsProvider {
return this.subscriptionService.subscribeToOrderFills(params);
}
+ /**
+ * Subscribe to live order updates
+ */
+ subscribeToOrders(params: SubscribeOrdersParams): () => void {
+ return this.subscriptionService.subscribeToOrders(params);
+ }
+
/**
* Configure live data settings
*/
diff --git a/app/components/UI/Perps/controllers/types/index.ts b/app/components/UI/Perps/controllers/types/index.ts
index eb1e62108e5b..3fd910028340 100644
--- a/app/components/UI/Perps/controllers/types/index.ts
+++ b/app/components/UI/Perps/controllers/types/index.ts
@@ -143,6 +143,14 @@ export interface PerpsMarketData {
* Trading volume as formatted string (e.g., '$1.2B', '$850M')
*/
volume: string;
+ /**
+ * Next funding time in milliseconds since epoch (optional, market-specific)
+ */
+ nextFundingTime?: number;
+ /**
+ * Funding interval in hours (optional, market-specific)
+ */
+ fundingIntervalHours?: number;
}
export interface ToggleTestnetResult {
@@ -340,6 +348,12 @@ export interface SubscribeOrderFillsParams {
since?: number; // Future: only fills after timestamp
}
+export interface SubscribeOrdersParams {
+ callback: (orders: Order[]) => void;
+ accountId?: CaipAccountId; // Optional: defaults to selected account
+ includeHistory?: boolean; // Optional: include filled/canceled orders
+}
+
export interface LiquidationPriceParams {
entryPrice: number;
leverage: number;
@@ -390,7 +404,7 @@ export interface Order {
remainingSize: string; // Amount remaining
status: 'open' | 'filled' | 'canceled' | 'rejected' | 'triggered' | 'queued'; // Normalized status
timestamp: number; // Order timestamp
- lastUpdated: number; // Last status update timestamp
+ lastUpdated?: number; // Last status update timestamp (optional - not provided by all APIs)
// TODO: Consider creating separate type for OpenOrders (UI Orders) potentially if optional properties muddy up the original Order type
takeProfitPrice?: string; // Take profit price (if set)
stopLossPrice?: string; // Stop loss price (if set)
@@ -479,6 +493,7 @@ export interface IPerpsProvider {
subscribeToPrices(params: SubscribePricesParams): () => void;
subscribeToPositions(params: SubscribePositionsParams): () => void;
subscribeToOrderFills(params: SubscribeOrderFillsParams): () => void;
+ subscribeToOrders(params: SubscribeOrdersParams): () => void;
// Live data configuration
setLiveDataConfig(config: Partial): void;
diff --git a/app/components/UI/Perps/hooks/index.ts b/app/components/UI/Perps/hooks/index.ts
index 8b3113ce86f9..5ef361a145d6 100644
--- a/app/components/UI/Perps/hooks/index.ts
+++ b/app/components/UI/Perps/hooks/index.ts
@@ -34,7 +34,6 @@ export { usePerpsPaymentTokens } from './usePerpsPaymentTokens';
// UI utility hooks
export { useBalanceComparison } from './useBalanceComparison';
export { useColorPulseAnimation } from './useColorPulseAnimation';
-export { usePerpsPositions } from './usePerpsPositions';
export { usePerpsTPSLUpdate } from './usePerpsTPSLUpdate';
export { usePerpsClosePosition } from './usePerpsClosePosition';
export { usePerpsOrderFees, formatFeeRate } from './usePerpsOrderFees';
@@ -49,7 +48,6 @@ export { usePerpsFirstTimeUser } from './usePerpsFirstTimeUser';
// Transaction data hooks
export { usePerpsOrderFills } from './usePerpsOrderFills';
export { usePerpsOrders } from './usePerpsOrders';
-export { usePerpsOpenOrders } from './usePerpsOpenOrders';
export { usePerpsFunding } from './usePerpsFunding';
// Event tracking hook
diff --git a/app/components/UI/Perps/hooks/stream/index.ts b/app/components/UI/Perps/hooks/stream/index.ts
index b7ca3f7b3a34..5241e8f85307 100644
--- a/app/components/UI/Perps/hooks/stream/index.ts
+++ b/app/components/UI/Perps/hooks/stream/index.ts
@@ -1,177 +1,19 @@
-import { useEffect, useState } from 'react';
-import { usePerpsStream } from '../../providers/PerpsStreamManager';
-import { DevLogger } from '../../../../../core/SDKConnect/utils/DevLogger';
-import type {
+// Export individual hooks with proper naming convention
+export { usePerpsLivePrices } from './usePerpsLivePrices';
+export { usePerpsLiveOrders } from './usePerpsLiveOrders';
+export { usePerpsLivePositions } from './usePerpsLivePositions';
+export { usePerpsLiveFills } from './usePerpsLiveFills';
+
+// Export types for convenience
+export type { UsePerpsLivePricesOptions } from './usePerpsLivePrices';
+export type { UsePerpsLiveOrdersOptions } from './usePerpsLiveOrders';
+export type { UsePerpsLivePositionsOptions } from './usePerpsLivePositions';
+export type { UsePerpsLiveFillsOptions } from './usePerpsLiveFills';
+
+// Re-export types from controllers
+export type {
+ PriceUpdate,
Order,
Position,
OrderFill,
- PriceUpdate,
} from '../../controllers/types';
-
-export interface UseLivePricesOptions {
- /** Array of symbols to subscribe to */
- symbols: string[];
- /** Debounce delay in milliseconds (default: 100ms) */
- debounceMs?: number;
-}
-
-/**
- * Hook for live price updates with component-specific debouncing
- * @param options - Configuration options for the hook
- * @returns Record of symbol to price data
- */
-export function useLivePrices(
- options: UseLivePricesOptions,
-): Record {
- const { symbols, debounceMs = 100 } = options;
- const stream = usePerpsStream();
- const [prices, setPrices] = useState>({});
-
- useEffect(() => {
- if (symbols.length === 0) return;
-
- DevLogger.log(
- `useLivePrices: Subscribing to symbols with ${debounceMs}ms debounce`,
- { symbols },
- );
-
- const unsubscribe = stream.prices.subscribeToSymbols({
- symbols,
- callback: (newPrices) => {
- if (!newPrices) {
- return;
- }
- DevLogger.log(
- `useLivePrices: Received price update (${debounceMs}ms debounce)`,
- {
- symbols: Object.keys(newPrices),
- prices: Object.entries(newPrices).map(([coin, data]) => ({
- coin,
- price: data.price,
- })),
- },
- );
- setPrices(newPrices);
- },
- debounceMs,
- });
-
- return () => {
- DevLogger.log(
- `useLivePrices: Unsubscribing from symbols (${debounceMs}ms debounce)`,
- { symbols },
- );
- unsubscribe();
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [stream, symbols.join(','), debounceMs]);
-
- return prices;
-}
-
-export interface UseLiveOrdersOptions {
- /** Debounce delay in milliseconds (default: 500ms) */
- debounceMs?: number;
-}
-
-/**
- * Hook for live order updates with component-specific debouncing
- * @param options - Configuration options for the hook
- * @returns Array of orders
- */
-export function useLiveOrders(options: UseLiveOrdersOptions = {}): Order[] {
- const { debounceMs = 500 } = options;
- const stream = usePerpsStream();
- const [orders, setOrders] = useState([]);
-
- useEffect(() => {
- const unsubscribe = stream.orders.subscribe({
- callback: (newOrders) => {
- if (!newOrders) {
- return;
- }
- setOrders(newOrders);
- },
- debounceMs,
- });
-
- return unsubscribe;
- }, [stream, debounceMs]);
-
- return orders;
-}
-
-export interface UseLivePositionsOptions {
- /** Debounce delay in milliseconds (default: 1000ms) */
- debounceMs?: number;
-}
-
-/**
- * Hook for live position updates with component-specific debouncing
- * @param options - Configuration options for the hook
- * @returns Array of positions
- */
-export function useLivePositions(
- options: UseLivePositionsOptions = {},
-): Position[] {
- const { debounceMs = 1000 } = options;
- const stream = usePerpsStream();
- const [positions, setPositions] = useState([]);
-
- useEffect(() => {
- const unsubscribe = stream.positions.subscribe({
- callback: (newPositions) => {
- if (!newPositions) {
- return;
- }
- setPositions(newPositions);
- },
- debounceMs,
- });
-
- return unsubscribe;
- }, [stream, debounceMs]);
-
- return positions;
-}
-
-export interface UseLiveFillsOptions {
- /** Debounce delay in milliseconds (default: 0ms for immediate updates) */
- debounceMs?: number;
-}
-
-/**
- * Hook for live order fill updates with component-specific debouncing
- * @param options - Configuration options for the hook
- * @returns Array of order fills
- */
-export function useLiveFills(options: UseLiveFillsOptions = {}): OrderFill[] {
- const { debounceMs = 0 } = options;
- const stream = usePerpsStream();
- const [fills, setFills] = useState([]);
-
- useEffect(() => {
- const unsubscribe = stream.fills.subscribe({
- callback: (newFills) => {
- if (!newFills) {
- return;
- }
- setFills(newFills);
- },
- debounceMs,
- });
-
- return unsubscribe;
- }, [stream, debounceMs]);
-
- return fills;
-}
-
-// Export types for components to use
-export type { PriceUpdate } from '../../controllers/types';
-
-// Future hooks to be added:
-// export function useLiveFunding(debounceMs: number = 5000): Funding[] { ... }
-// export function useLiveAccountState(debounceMs: number = 2000): AccountState { ... }
-// export function useLiveOrderBook(symbol: string, debounceMs: number = 100): OrderBook { ... }
-// export function useLiveTrades(symbol: string, debounceMs: number = 0): Trade[] { ... }
diff --git a/app/components/UI/Perps/hooks/stream/useLiveFills.test.ts b/app/components/UI/Perps/hooks/stream/useLiveFills.test.ts
index e24223a0cd07..cc4f147302a2 100644
--- a/app/components/UI/Perps/hooks/stream/useLiveFills.test.ts
+++ b/app/components/UI/Perps/hooks/stream/useLiveFills.test.ts
@@ -1,6 +1,6 @@
import { renderHook, act, waitFor } from '@testing-library/react-native';
import React from 'react';
-import { useLiveFills } from './index';
+import { usePerpsLiveFills } from './index';
import type { OrderFill } from '../../controllers/types';
// Mock the stream provider
@@ -16,7 +16,7 @@ jest.mock('../../providers/PerpsStreamManager', () => ({
children,
}));
-describe('useLiveFills', () => {
+describe('usePerpsLiveFills', () => {
const mockFill: OrderFill = {
orderId: 'order-1',
symbol: 'BTC-PERP',
@@ -40,14 +40,14 @@ describe('useLiveFills', () => {
});
it('should subscribe to fills on mount', () => {
- const debounceMs = 2000;
+ const throttleMs = 2000;
mockSubscribe.mockReturnValue(jest.fn());
- renderHook(() => useLiveFills({ debounceMs }));
+ renderHook(() => usePerpsLiveFills({ throttleMs }));
expect(mockSubscribe).toHaveBeenCalledWith({
callback: expect.any(Function),
- debounceMs,
+ throttleMs,
});
});
@@ -55,7 +55,7 @@ describe('useLiveFills', () => {
const mockUnsubscribe = jest.fn();
mockSubscribe.mockReturnValue(mockUnsubscribe);
- const { unmount } = renderHook(() => useLiveFills());
+ const { unmount } = renderHook(() => usePerpsLiveFills());
unmount();
@@ -69,7 +69,7 @@ describe('useLiveFills', () => {
return jest.fn();
});
- const { result } = renderHook(() => useLiveFills());
+ const { result } = renderHook(() => usePerpsLiveFills());
// Initially empty
expect(result.current).toEqual([]);
@@ -89,18 +89,18 @@ describe('useLiveFills', () => {
});
});
- it('should use default debounce value when not provided', () => {
+ it('should use default throttle value when not provided', () => {
mockSubscribe.mockReturnValue(jest.fn());
- renderHook(() => useLiveFills());
+ renderHook(() => usePerpsLiveFills());
expect(mockSubscribe).toHaveBeenCalledWith({
callback: expect.any(Function),
- debounceMs: 0, // Default value for fills (immediate)
+ throttleMs: 0, // Default value for fills (immediate)
});
});
- it('should handle debounce changes', () => {
+ it('should handle throttle changes', () => {
const mockUnsubscribe1 = jest.fn();
const mockUnsubscribe2 = jest.fn();
@@ -109,25 +109,25 @@ describe('useLiveFills', () => {
.mockReturnValueOnce(mockUnsubscribe2);
const { rerender } = renderHook(
- ({ debounceMs }) => useLiveFills({ debounceMs }),
+ ({ throttleMs }) => usePerpsLiveFills({ throttleMs }),
{
- initialProps: { debounceMs: 2000 },
+ initialProps: { throttleMs: 2000 },
},
);
expect(mockSubscribe).toHaveBeenCalledWith({
callback: expect.any(Function),
- debounceMs: 2000,
+ throttleMs: 2000,
});
- // Change debounce
- rerender({ debounceMs: 3000 });
+ // Change throttle
+ rerender({ throttleMs: 3000 });
- // Should resubscribe with new debounce
+ // Should resubscribe with new throttle
expect(mockUnsubscribe1).toHaveBeenCalled();
expect(mockSubscribe).toHaveBeenCalledWith({
callback: expect.any(Function),
- debounceMs: 3000,
+ throttleMs: 3000,
});
});
@@ -138,7 +138,7 @@ describe('useLiveFills', () => {
return jest.fn();
});
- const { result } = renderHook(() => useLiveFills());
+ const { result } = renderHook(() => usePerpsLiveFills());
act(() => {
capturedCallback([]);
@@ -156,7 +156,7 @@ describe('useLiveFills', () => {
return jest.fn();
});
- const { result } = renderHook(() => useLiveFills());
+ const { result } = renderHook(() => usePerpsLiveFills());
// Send null update (should be handled gracefully)
act(() => {
@@ -193,7 +193,7 @@ describe('useLiveFills', () => {
return jest.fn();
});
- const { result } = renderHook(() => useLiveFills());
+ const { result } = renderHook(() => usePerpsLiveFills());
// First update
const firstFills: OrderFill[] = [mockFill];
@@ -228,7 +228,7 @@ describe('useLiveFills', () => {
return jest.fn();
});
- const { result } = renderHook(() => useLiveFills());
+ const { result } = renderHook(() => usePerpsLiveFills());
const now = Date.now();
const fills: OrderFill[] = [
@@ -254,7 +254,7 @@ describe('useLiveFills', () => {
return jest.fn();
});
- const { result } = renderHook(() => useLiveFills());
+ const { result } = renderHook(() => usePerpsLiveFills());
const fills: OrderFill[] = [
{ ...mockFill, orderId: 'order-fill-1', symbol: 'BTC-PERP' },
diff --git a/app/components/UI/Perps/hooks/stream/useLiveOrders.test.ts b/app/components/UI/Perps/hooks/stream/useLiveOrders.test.ts
index 467fb8631f90..d1e38d905f94 100644
--- a/app/components/UI/Perps/hooks/stream/useLiveOrders.test.ts
+++ b/app/components/UI/Perps/hooks/stream/useLiveOrders.test.ts
@@ -1,6 +1,6 @@
import { renderHook, act, waitFor } from '@testing-library/react-native';
import React from 'react';
-import { useLiveOrders } from './index';
+import { usePerpsLiveOrders } from './index';
import type { Order } from '../../controllers/types';
// Mock the stream provider
@@ -16,7 +16,7 @@ jest.mock('../../providers/PerpsStreamManager', () => ({
children,
}));
-describe('useLiveOrders', () => {
+describe('usePerpsLiveOrders', () => {
const mockOrder: Order = {
orderId: 'order-1',
symbol: 'BTC-PERP',
@@ -40,14 +40,14 @@ describe('useLiveOrders', () => {
});
it('should subscribe to orders on mount', () => {
- const debounceMs = 2000;
+ const throttleMs = 2000;
mockSubscribe.mockReturnValue(jest.fn());
- renderHook(() => useLiveOrders({ debounceMs }));
+ renderHook(() => usePerpsLiveOrders({ throttleMs }));
expect(mockSubscribe).toHaveBeenCalledWith({
callback: expect.any(Function),
- debounceMs,
+ throttleMs,
});
});
@@ -55,7 +55,7 @@ describe('useLiveOrders', () => {
const mockUnsubscribe = jest.fn();
mockSubscribe.mockReturnValue(mockUnsubscribe);
- const { unmount } = renderHook(() => useLiveOrders());
+ const { unmount } = renderHook(() => usePerpsLiveOrders());
unmount();
@@ -69,7 +69,7 @@ describe('useLiveOrders', () => {
return jest.fn();
});
- const { result } = renderHook(() => useLiveOrders());
+ const { result } = renderHook(() => usePerpsLiveOrders());
// Initially empty
expect(result.current).toEqual([]);
@@ -89,18 +89,18 @@ describe('useLiveOrders', () => {
});
});
- it('should use default debounce value when not provided', () => {
+ it('should use default throttle value when not provided', () => {
mockSubscribe.mockReturnValue(jest.fn());
- renderHook(() => useLiveOrders());
+ renderHook(() => usePerpsLiveOrders());
expect(mockSubscribe).toHaveBeenCalledWith({
callback: expect.any(Function),
- debounceMs: 500, // Default value for orders
+ throttleMs: 0, // Default value for orders (no throttling for instant updates)
});
});
- it('should handle debounce changes', () => {
+ it('should handle throttle changes', () => {
const mockUnsubscribe1 = jest.fn();
const mockUnsubscribe2 = jest.fn();
@@ -109,25 +109,25 @@ describe('useLiveOrders', () => {
.mockReturnValueOnce(mockUnsubscribe2);
const { rerender } = renderHook(
- ({ debounceMs }) => useLiveOrders({ debounceMs }),
+ ({ throttleMs }) => usePerpsLiveOrders({ throttleMs }),
{
- initialProps: { debounceMs: 500 },
+ initialProps: { throttleMs: 500 },
},
);
expect(mockSubscribe).toHaveBeenCalledWith({
callback: expect.any(Function),
- debounceMs: 500,
+ throttleMs: 500,
});
- // Change debounce
- rerender({ debounceMs: 1000 });
+ // Change throttle
+ rerender({ throttleMs: 1000 });
- // Should resubscribe with new debounce
+ // Should resubscribe with new throttle
expect(mockUnsubscribe1).toHaveBeenCalled();
expect(mockSubscribe).toHaveBeenCalledWith({
callback: expect.any(Function),
- debounceMs: 1000,
+ throttleMs: 1000,
});
});
@@ -138,7 +138,7 @@ describe('useLiveOrders', () => {
return jest.fn();
});
- const { result } = renderHook(() => useLiveOrders());
+ const { result } = renderHook(() => usePerpsLiveOrders());
act(() => {
capturedCallback([]);
@@ -156,7 +156,7 @@ describe('useLiveOrders', () => {
return jest.fn();
});
- const { result } = renderHook(() => useLiveOrders());
+ const { result } = renderHook(() => usePerpsLiveOrders());
// Send null update (should be handled gracefully)
act(() => {
@@ -193,7 +193,7 @@ describe('useLiveOrders', () => {
return jest.fn();
});
- const { result } = renderHook(() => useLiveOrders());
+ const { result } = renderHook(() => usePerpsLiveOrders());
// First update
const firstOrders: Order[] = [mockOrder];
diff --git a/app/components/UI/Perps/hooks/stream/useLivePositions.test.ts b/app/components/UI/Perps/hooks/stream/useLivePositions.test.ts
index fde1017b0045..28835539ce9c 100644
--- a/app/components/UI/Perps/hooks/stream/useLivePositions.test.ts
+++ b/app/components/UI/Perps/hooks/stream/useLivePositions.test.ts
@@ -1,6 +1,6 @@
import { renderHook, act, waitFor } from '@testing-library/react-native';
import React from 'react';
-import { useLivePositions } from './index';
+import { usePerpsLivePositions } from './index';
import type { Position } from '../../controllers/types';
// Mock the stream provider
@@ -16,7 +16,7 @@ jest.mock('../../providers/PerpsStreamManager', () => ({
children,
}));
-describe('useLivePositions', () => {
+describe('usePerpsLivePositions', () => {
const mockPosition: Position = {
coin: 'BTC-PERP',
size: '1.0',
@@ -48,14 +48,14 @@ describe('useLivePositions', () => {
});
it('should subscribe to positions on mount', () => {
- const debounceMs = 3000;
+ const throttleMs = 3000;
mockSubscribe.mockReturnValue(jest.fn());
- renderHook(() => useLivePositions({ debounceMs }));
+ renderHook(() => usePerpsLivePositions({ throttleMs }));
expect(mockSubscribe).toHaveBeenCalledWith({
callback: expect.any(Function),
- debounceMs,
+ throttleMs,
});
});
@@ -63,7 +63,7 @@ describe('useLivePositions', () => {
const mockUnsubscribe = jest.fn();
mockSubscribe.mockReturnValue(mockUnsubscribe);
- const { unmount } = renderHook(() => useLivePositions());
+ const { unmount } = renderHook(() => usePerpsLivePositions());
unmount();
@@ -77,10 +77,13 @@ describe('useLivePositions', () => {
return jest.fn();
});
- const { result } = renderHook(() => useLivePositions());
+ const { result } = renderHook(() => usePerpsLivePositions());
- // Initially empty
- expect(result.current).toEqual([]);
+ // Initially empty with loading state
+ expect(result.current).toEqual({
+ positions: [],
+ isInitialLoading: true,
+ });
// Simulate positions update
const positions: Position[] = [
@@ -93,22 +96,25 @@ describe('useLivePositions', () => {
});
await waitFor(() => {
- expect(result.current).toEqual(positions);
+ expect(result.current).toEqual({
+ positions,
+ isInitialLoading: false,
+ });
});
});
- it('should use default debounce value when not provided', () => {
+ it('should use default throttle value when not provided', () => {
mockSubscribe.mockReturnValue(jest.fn());
- renderHook(() => useLivePositions());
+ renderHook(() => usePerpsLivePositions());
expect(mockSubscribe).toHaveBeenCalledWith({
callback: expect.any(Function),
- debounceMs: 1000, // Default value for positions
+ throttleMs: 0, // Default value for positions (no throttling for instant updates)
});
});
- it('should handle debounce changes', () => {
+ it('should handle throttle changes', () => {
const mockUnsubscribe1 = jest.fn();
const mockUnsubscribe2 = jest.fn();
@@ -117,25 +123,25 @@ describe('useLivePositions', () => {
.mockReturnValueOnce(mockUnsubscribe2);
const { rerender } = renderHook(
- ({ debounceMs }) => useLivePositions({ debounceMs }),
+ ({ throttleMs }) => usePerpsLivePositions({ throttleMs }),
{
- initialProps: { debounceMs: 1000 },
+ initialProps: { throttleMs: 0 },
},
);
expect(mockSubscribe).toHaveBeenCalledWith({
callback: expect.any(Function),
- debounceMs: 1000,
+ throttleMs: 0,
});
- // Change debounce
- rerender({ debounceMs: 2000 });
+ // Change throttle
+ rerender({ throttleMs: 2000 });
- // Should resubscribe with new debounce
+ // Should resubscribe with new throttle
expect(mockUnsubscribe1).toHaveBeenCalled();
expect(mockSubscribe).toHaveBeenCalledWith({
callback: expect.any(Function),
- debounceMs: 2000,
+ throttleMs: 2000,
});
});
@@ -146,14 +152,17 @@ describe('useLivePositions', () => {
return jest.fn();
});
- const { result } = renderHook(() => useLivePositions());
+ const { result } = renderHook(() => usePerpsLivePositions());
act(() => {
capturedCallback([]);
});
await waitFor(() => {
- expect(result.current).toEqual([]);
+ expect(result.current).toEqual({
+ positions: [],
+ isInitialLoading: false,
+ });
});
});
@@ -164,7 +173,7 @@ describe('useLivePositions', () => {
return jest.fn();
});
- const { result } = renderHook(() => useLivePositions());
+ const { result } = renderHook(() => usePerpsLivePositions());
// Send null update (should be handled gracefully)
act(() => {
@@ -172,7 +181,10 @@ describe('useLivePositions', () => {
});
// Should not crash and positions should remain empty
- expect(result.current).toEqual([]);
+ expect(result.current).toEqual({
+ positions: [],
+ isInitialLoading: true,
+ });
// Send undefined update
act(() => {
@@ -180,7 +192,10 @@ describe('useLivePositions', () => {
});
// Should still not crash
- expect(result.current).toEqual([]);
+ expect(result.current).toEqual({
+ positions: [],
+ isInitialLoading: true,
+ });
// Send valid update to ensure it still works
const validPositions: Position[] = [mockPosition];
@@ -190,7 +205,10 @@ describe('useLivePositions', () => {
});
await waitFor(() => {
- expect(result.current).toEqual(validPositions);
+ expect(result.current).toEqual({
+ positions: validPositions,
+ isInitialLoading: false,
+ });
});
});
@@ -201,7 +219,7 @@ describe('useLivePositions', () => {
return jest.fn();
});
- const { result } = renderHook(() => useLivePositions());
+ const { result } = renderHook(() => usePerpsLivePositions());
// First update
const firstPositions: Position[] = [mockPosition];
@@ -210,7 +228,10 @@ describe('useLivePositions', () => {
});
await waitFor(() => {
- expect(result.current).toEqual(firstPositions);
+ expect(result.current).toEqual({
+ positions: firstPositions,
+ isInitialLoading: false,
+ });
});
// Second update with different positions
@@ -224,8 +245,11 @@ describe('useLivePositions', () => {
});
await waitFor(() => {
- expect(result.current).toEqual(secondPositions);
- expect(result.current).not.toContain(mockPosition);
+ expect(result.current).toEqual({
+ positions: secondPositions,
+ isInitialLoading: false,
+ });
+ expect(result.current.positions).not.toContain(mockPosition);
});
});
@@ -236,7 +260,7 @@ describe('useLivePositions', () => {
return jest.fn();
});
- const { result } = renderHook(() => useLivePositions());
+ const { result } = renderHook(() => usePerpsLivePositions());
// Initial position
const initialPosition: Position = { ...mockPosition };
@@ -245,7 +269,7 @@ describe('useLivePositions', () => {
});
await waitFor(() => {
- expect(result.current[0].unrealizedPnl).toBe('1000');
+ expect(result.current.positions[0].unrealizedPnl).toBe('1000');
});
// Update with changed PnL
@@ -260,8 +284,8 @@ describe('useLivePositions', () => {
});
await waitFor(() => {
- expect(result.current[0].unrealizedPnl).toBe('2000');
- expect(result.current[0].positionValue).toBe('52000');
+ expect(result.current.positions[0].unrealizedPnl).toBe('2000');
+ expect(result.current.positions[0].positionValue).toBe('52000');
});
});
});
diff --git a/app/components/UI/Perps/hooks/stream/useLivePrices.test.ts b/app/components/UI/Perps/hooks/stream/useLivePrices.test.ts
index 5c2a8bca7438..fed03edeee03 100644
--- a/app/components/UI/Perps/hooks/stream/useLivePrices.test.ts
+++ b/app/components/UI/Perps/hooks/stream/useLivePrices.test.ts
@@ -1,6 +1,6 @@
import { renderHook, act, waitFor } from '@testing-library/react-native';
import React from 'react';
-import { useLivePrices } from './index';
+import { usePerpsLivePrices } from './index';
import type { PriceUpdate } from '../../controllers/types';
// Mock the stream provider
@@ -16,7 +16,7 @@ jest.mock('../../providers/PerpsStreamManager', () => ({
children,
}));
-describe('useLivePrices', () => {
+describe('usePerpsLivePrices', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.useFakeTimers();
@@ -28,16 +28,16 @@ describe('useLivePrices', () => {
it('should subscribe to prices on mount', () => {
const symbols = ['BTC-PERP', 'ETH-PERP'];
- const debounceMs = 1000;
+ const throttleMs = 1000;
mockSubscribeToSymbols.mockReturnValue(jest.fn());
- renderHook(() => useLivePrices({ symbols, debounceMs }));
+ renderHook(() => usePerpsLivePrices({ symbols, throttleMs }));
expect(mockSubscribeToSymbols).toHaveBeenCalledWith({
symbols,
callback: expect.any(Function),
- debounceMs,
+ throttleMs,
});
});
@@ -46,7 +46,7 @@ describe('useLivePrices', () => {
mockSubscribeToSymbols.mockReturnValue(mockUnsubscribe);
const { unmount } = renderHook(() =>
- useLivePrices({ symbols: ['BTC-PERP'] }),
+ usePerpsLivePrices({ symbols: ['BTC-PERP'] }),
);
unmount();
@@ -63,7 +63,7 @@ describe('useLivePrices', () => {
});
const { result } = renderHook(() =>
- useLivePrices({ symbols: ['BTC-PERP', 'ETH-PERP'] }),
+ usePerpsLivePrices({ symbols: ['BTC-PERP', 'ETH-PERP'] }),
);
// Initially empty
@@ -104,15 +104,15 @@ describe('useLivePrices', () => {
});
});
- it('should use default debounce value when not provided', () => {
+ it('should use default throttle value when not provided', () => {
mockSubscribeToSymbols.mockReturnValue(jest.fn());
- renderHook(() => useLivePrices({ symbols: ['BTC-PERP'] }));
+ renderHook(() => usePerpsLivePrices({ symbols: ['BTC-PERP'] }));
expect(mockSubscribeToSymbols).toHaveBeenCalledWith({
symbols: ['BTC-PERP'],
callback: expect.any(Function),
- debounceMs: 100, // Default value
+ throttleMs: 1000, // Default value (1 second for balanced performance)
});
});
@@ -125,7 +125,7 @@ describe('useLivePrices', () => {
.mockReturnValueOnce(mockUnsubscribe2);
const { rerender } = renderHook(
- ({ symbols }) => useLivePrices({ symbols }),
+ ({ symbols }) => usePerpsLivePrices({ symbols }),
{
initialProps: { symbols: ['BTC-PERP'] },
},
@@ -134,7 +134,7 @@ describe('useLivePrices', () => {
expect(mockSubscribeToSymbols).toHaveBeenCalledWith({
symbols: ['BTC-PERP'],
callback: expect.any(Function),
- debounceMs: 100,
+ throttleMs: 1000,
});
// Change symbols
@@ -145,11 +145,11 @@ describe('useLivePrices', () => {
expect(mockSubscribeToSymbols).toHaveBeenCalledWith({
symbols: ['ETH-PERP', 'SOL-PERP'],
callback: expect.any(Function),
- debounceMs: 100,
+ throttleMs: 1000,
});
});
- it('should handle debounce changes', () => {
+ it('should handle throttle changes', () => {
const mockUnsubscribe1 = jest.fn();
const mockUnsubscribe2 = jest.fn();
@@ -158,34 +158,35 @@ describe('useLivePrices', () => {
.mockReturnValueOnce(mockUnsubscribe2);
const { rerender } = renderHook(
- ({ debounceMs }) => useLivePrices({ symbols: ['BTC-PERP'], debounceMs }),
+ ({ throttleMs }) =>
+ usePerpsLivePrices({ symbols: ['BTC-PERP'], throttleMs }),
{
- initialProps: { debounceMs: 100 },
+ initialProps: { throttleMs: 1000 },
},
);
expect(mockSubscribeToSymbols).toHaveBeenCalledWith({
symbols: ['BTC-PERP'],
callback: expect.any(Function),
- debounceMs: 100,
+ throttleMs: 1000,
});
- // Change debounce
- rerender({ debounceMs: 500 });
+ // Change throttle
+ rerender({ throttleMs: 500 });
- // Should resubscribe with new debounce
+ // Should resubscribe with new throttle
expect(mockUnsubscribe1).toHaveBeenCalled();
expect(mockSubscribeToSymbols).toHaveBeenCalledWith({
symbols: ['BTC-PERP'],
callback: expect.any(Function),
- debounceMs: 500,
+ throttleMs: 500,
});
});
it('should handle empty symbols array', () => {
mockSubscribeToSymbols.mockReturnValue(jest.fn());
- const { result } = renderHook(() => useLivePrices({ symbols: [] }));
+ const { result } = renderHook(() => usePerpsLivePrices({ symbols: [] }));
expect(result.current).toEqual({});
// Should not subscribe with empty array
@@ -201,7 +202,7 @@ describe('useLivePrices', () => {
});
const { result } = renderHook(() =>
- useLivePrices({ symbols: ['BTC-PERP', 'ETH-PERP', 'SOL-PERP'] }),
+ usePerpsLivePrices({ symbols: ['BTC-PERP', 'ETH-PERP', 'SOL-PERP'] }),
);
// Add prices one by one
@@ -266,7 +267,7 @@ describe('useLivePrices', () => {
mockSubscribeToSymbols.mockImplementation(() => unsubscribes[index++]);
const { rerender } = renderHook(
- ({ symbols }) => useLivePrices({ symbols }),
+ ({ symbols }) => usePerpsLivePrices({ symbols }),
{
initialProps: { symbols: ['BTC-PERP'] },
},
@@ -295,7 +296,7 @@ describe('useLivePrices', () => {
});
const { result } = renderHook(() =>
- useLivePrices({ symbols: ['BTC-PERP'] }),
+ usePerpsLivePrices({ symbols: ['BTC-PERP'] }),
);
// Send null update (should be handled gracefully)
@@ -339,7 +340,7 @@ describe('useLivePrices', () => {
});
const { result, rerender } = renderHook(
- ({ symbols }) => useLivePrices({ symbols }),
+ ({ symbols }) => usePerpsLivePrices({ symbols }),
{
initialProps: { symbols: ['BTC-PERP', 'ETH-PERP'] },
},
diff --git a/app/components/UI/Perps/hooks/stream/usePerpsLiveFills.ts b/app/components/UI/Perps/hooks/stream/usePerpsLiveFills.ts
new file mode 100644
index 000000000000..6c248255541d
--- /dev/null
+++ b/app/components/UI/Perps/hooks/stream/usePerpsLiveFills.ts
@@ -0,0 +1,51 @@
+import { useEffect, useState } from 'react';
+import { usePerpsStream } from '../../providers/PerpsStreamManager';
+import { DevLogger } from '../../../../../core/SDKConnect/utils/DevLogger';
+import type { OrderFill } from '../../controllers/types';
+
+export interface UsePerpsLiveFillsOptions {
+ /** Throttle delay in milliseconds (default: 0ms for immediate updates) */
+ throttleMs?: number;
+}
+
+/**
+ * Hook for real-time order fill updates via WebSocket subscription
+ * Provides immediate notification of trade executions
+ *
+ * @param options - Configuration options for the hook
+ * @returns Array of order fills with real-time updates
+ */
+export function usePerpsLiveFills(
+ options: UsePerpsLiveFillsOptions = {},
+): OrderFill[] {
+ const { throttleMs = 0 } = options;
+ const stream = usePerpsStream();
+ const [fills, setFills] = useState([]);
+
+ useEffect(() => {
+ const logMessage = throttleMs
+ ? `usePerpsLiveFills: Subscribing with ${throttleMs}ms throttle`
+ : `usePerpsLiveFills: Subscribing with no throttle (instant updates)`;
+ DevLogger.log(logMessage);
+
+ const unsubscribe = stream.fills.subscribe({
+ callback: (newFills) => {
+ if (!newFills) {
+ return;
+ }
+ DevLogger.log('usePerpsLiveFills: Received fill update', {
+ count: newFills.length,
+ });
+ setFills(newFills);
+ },
+ throttleMs,
+ });
+
+ return () => {
+ DevLogger.log('usePerpsLiveFills: Unsubscribing');
+ unsubscribe();
+ };
+ }, [stream, throttleMs]);
+
+ return fills;
+}
diff --git a/app/components/UI/Perps/hooks/stream/usePerpsLiveOrders.ts b/app/components/UI/Perps/hooks/stream/usePerpsLiveOrders.ts
new file mode 100644
index 000000000000..d4b42a0d367e
--- /dev/null
+++ b/app/components/UI/Perps/hooks/stream/usePerpsLiveOrders.ts
@@ -0,0 +1,44 @@
+import { useEffect, useState } from 'react';
+import { usePerpsStream } from '../../providers/PerpsStreamManager';
+import type { Order } from '../../controllers/types';
+
+export interface UsePerpsLiveOrdersOptions {
+ /** Throttle delay in milliseconds (default: 0 - no throttling for instant updates) */
+ throttleMs?: number;
+}
+
+/**
+ * Hook for real-time order updates via WebSocket subscription
+ * Replaces the old polling-based usePerpsOpenOrders hook
+ *
+ * Orders update instantly by default since they don't change frequently
+ * and users expect immediate feedback when placing/cancelling orders.
+ *
+ * @param options - Configuration options for the hook
+ * @returns Array of current orders with real-time updates
+ */
+export function usePerpsLiveOrders(
+ options: UsePerpsLiveOrdersOptions = {},
+): Order[] {
+ const { throttleMs = 0 } = options; // No throttling by default for instant updates
+ const stream = usePerpsStream();
+ const [orders, setOrders] = useState([]);
+
+ useEffect(() => {
+ const unsubscribe = stream.orders.subscribe({
+ callback: (newOrders) => {
+ if (!newOrders) {
+ return;
+ }
+ setOrders(newOrders);
+ },
+ throttleMs,
+ });
+
+ return () => {
+ unsubscribe();
+ };
+ }, [stream, throttleMs]);
+
+ return orders;
+}
diff --git a/app/components/UI/Perps/hooks/stream/usePerpsLivePositions.ts b/app/components/UI/Perps/hooks/stream/usePerpsLivePositions.ts
new file mode 100644
index 000000000000..c850e3fdccc5
--- /dev/null
+++ b/app/components/UI/Perps/hooks/stream/usePerpsLivePositions.ts
@@ -0,0 +1,82 @@
+import { useEffect, useState, useRef } from 'react';
+import { usePerpsStream } from '../../providers/PerpsStreamManager';
+import { DevLogger } from '../../../../../core/SDKConnect/utils/DevLogger';
+import type { Position } from '../../controllers/types';
+
+// Stable empty array reference to prevent re-renders
+const EMPTY_POSITIONS: Position[] = [];
+
+export interface UsePerpsLivePositionsOptions {
+ /** Throttle delay in milliseconds (default: 0 - no throttling for instant updates) */
+ throttleMs?: number;
+}
+
+export interface UsePerpsLivePositionsReturn {
+ /** Array of current positions */
+ positions: Position[];
+ /** Whether we're waiting for the first real WebSocket data (not cached) */
+ isInitialLoading: boolean;
+}
+
+/**
+ * Hook for real-time position updates via WebSocket subscription
+ * Replaces the old polling-based usePerpsPositions hook
+ *
+ * Positions update instantly by default since changes are important
+ * (TP/SL modifications, liquidations, etc.) and users need immediate feedback.
+ *
+ * @param options - Configuration options for the hook
+ * @returns Object containing positions array and loading state
+ */
+export function usePerpsLivePositions(
+ options: UsePerpsLivePositionsOptions = {},
+): UsePerpsLivePositionsReturn {
+ const { throttleMs = 0 } = options; // No throttling by default for instant updates
+ const stream = usePerpsStream();
+ const [positions, setPositions] = useState(EMPTY_POSITIONS);
+ const [isInitialLoading, setIsInitialLoading] = useState(true);
+ const lastPositionsRef = useRef(EMPTY_POSITIONS);
+ const hasReceivedFirstUpdate = useRef(false);
+
+ useEffect(() => {
+ const unsubscribe = stream.positions.subscribe({
+ callback: (newPositions) => {
+ if (!newPositions) {
+ return;
+ }
+
+ // Mark that we've received the first real WebSocket update
+ if (!hasReceivedFirstUpdate.current) {
+ DevLogger.log(
+ 'usePerpsLivePositions: Received first WebSocket update',
+ );
+ hasReceivedFirstUpdate.current = true;
+ setIsInitialLoading(false);
+ }
+ // Only update if positions actually changed
+ // For empty arrays, use stable reference
+ if (newPositions.length === 0) {
+ if (lastPositionsRef.current.length === 0) {
+ // Already empty, don't update
+ return;
+ }
+ lastPositionsRef.current = EMPTY_POSITIONS;
+ setPositions(EMPTY_POSITIONS);
+ } else {
+ lastPositionsRef.current = newPositions;
+ setPositions(newPositions);
+ }
+ },
+ throttleMs,
+ });
+
+ return () => {
+ unsubscribe();
+ };
+ }, [stream, throttleMs]);
+
+ return {
+ positions,
+ isInitialLoading,
+ };
+}
diff --git a/app/components/UI/Perps/hooks/stream/usePerpsLivePrices.ts b/app/components/UI/Perps/hooks/stream/usePerpsLivePrices.ts
new file mode 100644
index 000000000000..1907f0128e26
--- /dev/null
+++ b/app/components/UI/Perps/hooks/stream/usePerpsLivePrices.ts
@@ -0,0 +1,52 @@
+import { useEffect, useState } from 'react';
+import { usePerpsStream } from '../../providers/PerpsStreamManager';
+import type { PriceUpdate } from '../../controllers/types';
+
+export interface UsePerpsLivePricesOptions {
+ /** Array of symbols to subscribe to */
+ symbols: string[];
+ /** Throttle delay in milliseconds (default: 1000ms to prevent excessive re-renders) */
+ throttleMs?: number;
+}
+
+/**
+ * Hook for real-time price updates via WebSocket subscription
+ * Supports component-specific throttling for performance optimization
+ *
+ * Prices update every second by default to balance between real-time feel
+ * and performance. Components can override this based on their needs:
+ * - Charts might want slower updates (2000ms)
+ * - Price displays might want faster updates (500ms)
+ *
+ * @param options - Configuration options for the hook
+ * @returns Record of symbol to price data with real-time updates
+ */
+export function usePerpsLivePrices(
+ options: UsePerpsLivePricesOptions,
+): Record {
+ const { symbols, throttleMs = 1000 } = options; // 1 second default for balanced performance
+ const stream = usePerpsStream();
+ const [prices, setPrices] = useState>({});
+
+ useEffect(() => {
+ if (symbols.length === 0) return;
+
+ const unsubscribe = stream.prices.subscribeToSymbols({
+ symbols,
+ callback: (newPrices) => {
+ if (!newPrices) {
+ return;
+ }
+ setPrices(newPrices);
+ },
+ throttleMs,
+ });
+
+ return () => {
+ unsubscribe();
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [stream, symbols.join(','), throttleMs]);
+
+ return prices;
+}
diff --git a/app/components/UI/Perps/hooks/useHasExistingPosition.test.ts b/app/components/UI/Perps/hooks/useHasExistingPosition.test.ts
index 96ce93acd702..dea911eea837 100644
--- a/app/components/UI/Perps/hooks/useHasExistingPosition.test.ts
+++ b/app/components/UI/Perps/hooks/useHasExistingPosition.test.ts
@@ -1,15 +1,16 @@
import { renderHook } from '@testing-library/react-hooks';
import { useHasExistingPosition } from './useHasExistingPosition';
-import { usePerpsPositions } from './usePerpsPositions';
+import { usePerpsLivePositions } from './stream';
import type { Position } from '../controllers/types';
-// Mock the usePerpsPositions hook
-jest.mock('./usePerpsPositions');
+// Mock the usePerpsLivePositions hook
+jest.mock('./stream', () => ({
+ usePerpsLivePositions: jest.fn(),
+}));
describe('useHasExistingPosition', () => {
- const mockUsePerpsPositions = usePerpsPositions as jest.MockedFunction<
- typeof usePerpsPositions
- >;
+ const mockUsePerpsLivePositions =
+ usePerpsLivePositions as jest.MockedFunction;
const mockPositions: Position[] = [
{
@@ -59,12 +60,9 @@ describe('useHasExistingPosition', () => {
});
it('should return hasPosition as true when position exists for asset', () => {
- mockUsePerpsPositions.mockReturnValue({
+ mockUsePerpsLivePositions.mockReturnValue({
positions: mockPositions,
- isLoading: false,
- isRefreshing: false,
- error: null,
- loadPositions: jest.fn(),
+ isInitialLoading: false,
});
const { result } = renderHook(() =>
@@ -78,12 +76,9 @@ describe('useHasExistingPosition', () => {
});
it('should return hasPosition as false when no position exists for asset', () => {
- mockUsePerpsPositions.mockReturnValue({
+ mockUsePerpsLivePositions.mockReturnValue({
positions: mockPositions,
- isLoading: false,
- isRefreshing: false,
- error: null,
- loadPositions: jest.fn(),
+ isInitialLoading: false,
});
const { result } = renderHook(() =>
@@ -96,13 +91,10 @@ describe('useHasExistingPosition', () => {
expect(result.current.error).toBe(null);
});
- it('should return loading state correctly', () => {
- mockUsePerpsPositions.mockReturnValue({
+ it('should return loading state as false (WebSocket loads from cache)', () => {
+ mockUsePerpsLivePositions.mockReturnValue({
positions: [],
- isLoading: true,
- isRefreshing: false,
- error: null,
- loadPositions: jest.fn(),
+ isInitialLoading: false,
});
const { result } = renderHook(() =>
@@ -111,18 +103,14 @@ describe('useHasExistingPosition', () => {
expect(result.current.hasPosition).toBe(false);
expect(result.current.existingPosition).toBe(null);
- expect(result.current.isLoading).toBe(true);
+ expect(result.current.isLoading).toBe(false); // Always false with WebSocket
expect(result.current.error).toBe(null);
});
- it('should return error state correctly', () => {
- const errorMessage = 'Failed to load positions';
- mockUsePerpsPositions.mockReturnValue({
+ it('should return error as null (WebSocket handles errors internally)', () => {
+ mockUsePerpsLivePositions.mockReturnValue({
positions: [],
- isLoading: false,
- isRefreshing: false,
- error: errorMessage,
- loadPositions: jest.fn(),
+ isInitialLoading: false,
});
const { result } = renderHook(() =>
@@ -132,35 +120,27 @@ describe('useHasExistingPosition', () => {
expect(result.current.hasPosition).toBe(false);
expect(result.current.existingPosition).toBe(null);
expect(result.current.isLoading).toBe(false);
- expect(result.current.error).toBe(errorMessage);
+ expect(result.current.error).toBe(null); // Always null with WebSocket
});
- it('should pass loadOnMount parameter correctly', () => {
- mockUsePerpsPositions.mockReturnValue({
+ it('should ignore loadOnMount parameter (WebSocket loads from cache)', () => {
+ mockUsePerpsLivePositions.mockReturnValue({
positions: [],
- isLoading: false,
- isRefreshing: false,
- error: null,
- loadPositions: jest.fn(),
+ isInitialLoading: false,
});
- renderHook(() =>
+ const { result } = renderHook(() =>
useHasExistingPosition({ asset: 'BTC', loadOnMount: false }),
);
- expect(mockUsePerpsPositions).toHaveBeenCalledWith({
- loadOnMount: false,
- refreshOnFocus: true,
- });
+ // loadOnMount is ignored in WebSocket implementation
+ expect(result.current.hasPosition).toBe(false);
});
it('should handle empty positions array', () => {
- mockUsePerpsPositions.mockReturnValue({
+ mockUsePerpsLivePositions.mockReturnValue({
positions: [],
- isLoading: false,
- isRefreshing: false,
- error: null,
- loadPositions: jest.fn(),
+ isInitialLoading: false,
});
const { result } = renderHook(() =>
@@ -172,33 +152,40 @@ describe('useHasExistingPosition', () => {
});
it('should update when positions change', () => {
- const { result, rerender } = renderHook(() =>
- useHasExistingPosition({ asset: 'BTC' }),
- );
-
// Initially no positions
- mockUsePerpsPositions.mockReturnValue({
+ mockUsePerpsLivePositions.mockReturnValue({
positions: [],
- isLoading: false,
- isRefreshing: false,
- error: null,
- loadPositions: jest.fn(),
+ isInitialLoading: false,
});
- rerender();
+ const { result, rerender } = renderHook(() =>
+ useHasExistingPosition({ asset: 'BTC' }),
+ );
+
expect(result.current.hasPosition).toBe(false);
// Update with positions
- mockUsePerpsPositions.mockReturnValue({
+ mockUsePerpsLivePositions.mockReturnValue({
positions: mockPositions,
- isLoading: false,
- isRefreshing: false,
- error: null,
- loadPositions: jest.fn(),
+ isInitialLoading: false,
});
rerender();
expect(result.current.hasPosition).toBe(true);
expect(result.current.existingPosition).toEqual(mockPositions[0]);
});
+
+ it('should return a no-op refreshPosition function', async () => {
+ mockUsePerpsLivePositions.mockReturnValue({
+ positions: mockPositions,
+ isInitialLoading: false,
+ });
+
+ const { result } = renderHook(() =>
+ useHasExistingPosition({ asset: 'BTC' }),
+ );
+
+ // refreshPosition should be a no-op that returns a resolved promise
+ await expect(result.current.refreshPosition()).resolves.toBeUndefined();
+ });
});
diff --git a/app/components/UI/Perps/hooks/useHasExistingPosition.ts b/app/components/UI/Perps/hooks/useHasExistingPosition.ts
index e8aaa4c38a4b..057b19208cac 100644
--- a/app/components/UI/Perps/hooks/useHasExistingPosition.ts
+++ b/app/components/UI/Perps/hooks/useHasExistingPosition.ts
@@ -1,5 +1,5 @@
import { useMemo, useCallback } from 'react';
-import { usePerpsPositions } from './usePerpsPositions';
+import { usePerpsLivePositions } from './stream';
import type { Position } from '../controllers/types';
interface UseHasExistingPositionParams {
@@ -12,35 +12,33 @@ interface UseHasExistingPositionParams {
interface UseHasExistingPositionReturn {
/** Whether user has an existing position for the asset */
hasPosition: boolean;
- /** Loading state */
+ /** Loading state - always false since WebSocket data loads from cache */
isLoading: boolean;
- /** Error state */
+ /** Error state - always null for WebSocket subscriptions */
error: string | null;
/** The existing position if found */
existingPosition: Position | null;
- /** Function to refresh positions data */
+ /** Function to refresh positions data - no-op for WebSocket */
refreshPosition: () => Promise;
}
/**
* Hook to check if user has an existing position for a specific asset
+ * Uses WebSocket subscription for real-time position updates
* @param params Parameters for position checking
* @returns Object containing position existence info and related states
*/
export function useHasExistingPosition(
params: UseHasExistingPositionParams,
): UseHasExistingPositionReturn {
- const { asset, loadOnMount = true } = params;
-
- // Use the existing positions hook to get all positions
- const { positions, isLoading, error, loadPositions } = usePerpsPositions({
- loadOnMount,
- refreshOnFocus: true,
- });
+ const { asset } = params;
+ // loadOnMount is ignored since WebSocket subscriptions load from cache immediately
+ // Get real-time positions via WebSocket
+ const { positions } = usePerpsLivePositions();
// Check if user has an existing position for this asset
const existingPosition = useMemo(
- () => positions.find((position) => position.coin === asset) || null,
+ () => (positions || []).find((position) => position.coin === asset) || null,
[positions, asset],
);
@@ -49,15 +47,19 @@ export function useHasExistingPosition(
[existingPosition],
);
- // Wrapper function to refresh positions
- const refreshPosition = useCallback(async () => {
- await loadPositions({ isRefresh: true });
- }, [loadPositions]);
+ // No-op refresh function for compatibility
+ // Positions update automatically via WebSocket
+ const refreshPosition = useCallback(
+ async () =>
+ // WebSocket positions update automatically, no manual refresh needed
+ Promise.resolve(),
+ [],
+ );
return {
hasPosition,
- isLoading,
- error,
+ isLoading: false, // WebSocket data loads immediately from cache
+ error: null, // WebSocket subscriptions handle errors internally
existingPosition,
refreshPosition,
};
diff --git a/app/components/UI/Perps/hooks/usePerpsMarketStats.test.ts b/app/components/UI/Perps/hooks/usePerpsMarketStats.test.ts
index 21654e97dc91..3bd01656ddba 100644
--- a/app/components/UI/Perps/hooks/usePerpsMarketStats.test.ts
+++ b/app/components/UI/Perps/hooks/usePerpsMarketStats.test.ts
@@ -1,6 +1,6 @@
-import { renderHook, act } from '@testing-library/react-hooks';
-import { usePerpsMarketStats } from './usePerpsMarketStats';
+import { renderHook } from '@testing-library/react-hooks';
import { CandlePeriod } from '../constants/chartConfig';
+import { usePerpsMarketStats } from './usePerpsMarketStats';
// Mock Engine
jest.mock('../../../../core/Engine', () => ({
@@ -29,8 +29,8 @@ jest.mock('../utils/formatUtils', () => ({
},
}));
-import { usePerpsPositionData } from './usePerpsPositionData';
import Engine from '../../../../core/Engine';
+import { usePerpsPositionData } from './usePerpsPositionData';
const mockedUsePerpsPositionData = jest.mocked(usePerpsPositionData);
const mockSubscribeToPrices = Engine.context.PerpsController
@@ -91,7 +91,6 @@ describe('usePerpsMarketStats', () => {
mockedUsePerpsPositionData.mockReturnValue({
candleData: mockCandleData,
- priceData: mockPriceData.BTC,
isLoadingHistory: false,
refreshCandleData: jest.fn(),
});
@@ -99,13 +98,11 @@ describe('usePerpsMarketStats', () => {
const { result } = renderHook(() => usePerpsMarketStats('BTC'));
expect(result.current.currentPrice).toBe(45000);
- expect(result.current.priceChange24h).toBe(2.5);
expect(result.current.high24h).toBe('$46,000.00');
expect(result.current.low24h).toBe('$43,500.00');
expect(result.current.volume24h).toBe('$1.23B');
expect(result.current.openInterest).toBe('$987.65M');
expect(result.current.fundingRate).toBe('1.0000%');
- expect(result.current.fundingCountdown).toMatch(/^\d{2}:\d{2}:\d{2}$/);
expect(result.current.isLoading).toBe(false);
});
@@ -115,7 +112,6 @@ describe('usePerpsMarketStats', () => {
mockedUsePerpsPositionData.mockReturnValue({
candleData: null,
- priceData: null,
isLoadingHistory: true,
refreshCandleData: jest.fn(),
});
@@ -126,64 +122,13 @@ describe('usePerpsMarketStats', () => {
expect(result.current.currentPrice).toBe(0);
});
- it('should calculate funding countdown correctly', () => {
- // Set current time to 7:30:00 UTC (30 minutes before funding)
- const mockDate = new Date('2024-01-01T07:30:00Z');
- jest.setSystemTime(mockDate);
-
- mockSubscribeToPrices.mockImplementation(({ callback }) => {
- callback([mockPriceData.BTC]);
- return jest.fn();
- });
-
- mockedUsePerpsPositionData.mockReturnValue({
- candleData: mockCandleData,
- priceData: mockPriceData.BTC,
- isLoadingHistory: false,
- refreshCandleData: jest.fn(),
- });
-
- const { result } = renderHook(() => usePerpsMarketStats('BTC'));
-
- expect(result.current.fundingCountdown).toBe('00:30:00');
- });
-
- it('should update funding countdown every second', () => {
- // Set initial time
- const mockDate = new Date('2024-01-01T07:30:00Z');
- jest.setSystemTime(mockDate);
-
- mockSubscribeToPrices.mockImplementation(({ callback }) => {
- callback([mockPriceData.BTC]);
- return jest.fn();
- });
-
- mockedUsePerpsPositionData.mockReturnValue({
- candleData: mockCandleData,
- priceData: mockPriceData.BTC,
- isLoadingHistory: false,
- refreshCandleData: jest.fn(),
- });
-
- const { result } = renderHook(() => usePerpsMarketStats('BTC'));
-
- expect(result.current.fundingCountdown).toBe('00:30:00');
-
- // Advance time by 1 second
- act(() => {
- jest.advanceTimersByTime(1000);
- jest.setSystemTime(new Date('2024-01-01T07:30:01Z'));
- });
-
- expect(result.current.fundingCountdown).toBe('00:29:59');
- });
+ // Funding countdown tests removed - handled by separate component
it('should handle no market data gracefully', () => {
mockSubscribeToPrices.mockImplementation(() => jest.fn());
mockedUsePerpsPositionData.mockReturnValue({
candleData: null,
- priceData: null,
isLoadingHistory: false,
refreshCandleData: jest.fn(),
});
@@ -191,13 +136,11 @@ describe('usePerpsMarketStats', () => {
const { result } = renderHook(() => usePerpsMarketStats('BTC'));
expect(result.current.currentPrice).toBe(0);
- expect(result.current.priceChange24h).toBe(0);
expect(result.current.high24h).toBe('$0.00');
expect(result.current.low24h).toBe('$0.00');
expect(result.current.volume24h).toBe('$0.00');
expect(result.current.openInterest).toBe('$0.00');
expect(result.current.fundingRate).toBe('0.0000%');
- expect(result.current.fundingCountdown).toMatch(/^\d{2}:\d{2}:\d{2}$/);
});
it('should format large numbers correctly', () => {
@@ -216,7 +159,6 @@ describe('usePerpsMarketStats', () => {
mockedUsePerpsPositionData.mockReturnValue({
candleData: mockCandleData,
- priceData: largeNumberPriceData.BTC,
isLoadingHistory: false,
refreshCandleData: jest.fn(),
});
@@ -242,7 +184,6 @@ describe('usePerpsMarketStats', () => {
mockedUsePerpsPositionData.mockReturnValue({
candleData: mockCandleData,
- priceData: negativeFundingData.BTC,
isLoadingHistory: false,
refreshCandleData: jest.fn(),
});
diff --git a/app/components/UI/Perps/hooks/usePerpsMarketStats.ts b/app/components/UI/Perps/hooks/usePerpsMarketStats.ts
index 43e2d2bef845..b36348589d48 100644
--- a/app/components/UI/Perps/hooks/usePerpsMarketStats.ts
+++ b/app/components/UI/Perps/hooks/usePerpsMarketStats.ts
@@ -3,10 +3,7 @@ import Engine from '../../../../core/Engine';
import { usePerpsPositionData } from './usePerpsPositionData';
import type { PriceUpdate } from '../controllers/types';
import { formatPrice, formatLargeNumber } from '../utils/formatUtils';
-import {
- calculateFundingCountdown,
- calculate24hHighLow,
-} from '../utils/marketUtils';
+import { calculate24hHighLow } from '../utils/marketUtils';
import { CandlePeriod, TimeDuration } from '../constants/chartConfig';
interface MarketStats {
@@ -15,9 +12,7 @@ interface MarketStats {
volume24h: string;
openInterest: string;
fundingRate: string;
- fundingCountdown: string;
- currentPrice: number;
- priceChange24h: number;
+ currentPrice?: number;
isLoading: boolean;
}
@@ -38,10 +33,7 @@ export const usePerpsMarketStats = (
symbol: string,
): UsePerpsMarketStatsReturn => {
const [marketData, setMarketData] = useState({});
- const [fundingCountdown, setFundingCountdown] = useState('00:00:00');
- const [currentPriceData, setCurrentPriceData] = useState<
- PriceUpdate | undefined
- >();
+ const [initialPrice, setInitialPrice] = useState();
// Get candlestick data for 24h high/low calculation
const { candleData, refreshCandleData } = usePerpsPositionData({
@@ -51,6 +43,7 @@ export const usePerpsMarketStats = (
});
// Subscribe to market data updates (funding, open interest, volume)
+ // Note: We still subscribe to prices but only extract market metadata, not price itself
useEffect(() => {
if (!symbol) return;
@@ -65,13 +58,27 @@ export const usePerpsMarketStats = (
callback: (updates: PriceUpdate[]) => {
const update = updates.find((u) => u.coin === symbol);
if (update) {
- // Set both price data and market data from the same update
- setCurrentPriceData(update);
- setMarketData({
- funding: update.funding,
- openInterest: update.openInterest,
- volume24h: update.volume24h,
+ // Only extract market data, ignore price changes to prevent re-renders
+ setMarketData((prev) => {
+ // Check if market data actually changed
+ if (
+ prev.funding === update.funding &&
+ prev.openInterest === update.openInterest &&
+ prev.volume24h === update.volume24h
+ ) {
+ return prev; // Return same reference if no change
+ }
+ return {
+ funding: update.funding,
+ openInterest: update.openInterest,
+ volume24h: update.volume24h,
+ };
});
+
+ // Store initial price only once for high/low calculation fallback
+ if (!initialPrice && update.price) {
+ setInitialPrice(parseFloat(update.price));
+ }
}
},
});
@@ -87,32 +94,17 @@ export const usePerpsMarketStats = (
unsubscribe();
}
};
- }, [symbol]);
-
- // Update funding countdown every second
- useEffect(() => {
- const updateCountdown = () => {
- setFundingCountdown(calculateFundingCountdown());
- };
-
- updateCountdown(); // Initial update
- const interval = setInterval(updateCountdown, 1000);
-
- return () => clearInterval(interval);
- }, []);
+ }, [symbol, initialPrice]);
// Calculate all statistics
const stats = useMemo(() => {
- const currentPrice = parseFloat(currentPriceData?.price || '0');
- const priceChange24h = parseFloat(
- currentPriceData?.percentChange24h || '0',
- );
const { high, low } = calculate24hHighLow(candleData);
+ const fallbackPrice = initialPrice || 0;
return {
// 24h high/low from candlestick data, with fallback estimates
- high24h: high > 0 ? formatPrice(high) : formatPrice(currentPrice),
- low24h: low > 0 ? formatPrice(low) : formatPrice(currentPrice),
+ high24h: high > 0 ? formatPrice(high) : formatPrice(fallbackPrice),
+ low24h: low > 0 ? formatPrice(low) : formatPrice(fallbackPrice),
volume24h: marketData.volume24h
? formatLargeNumber(marketData.volume24h)
: '$0.00',
@@ -122,12 +114,10 @@ export const usePerpsMarketStats = (
fundingRate: marketData.funding
? `${(marketData.funding * 100).toFixed(4)}%`
: '0.0000%',
- fundingCountdown,
- currentPrice,
- priceChange24h,
- isLoading: !currentPriceData || !candleData,
+ currentPrice: fallbackPrice,
+ isLoading: !candleData,
};
- }, [currentPriceData, candleData, marketData, fundingCountdown]);
+ }, [candleData, marketData, initialPrice]);
// Refresh function to reload market data
const refresh = useCallback(async () => {
diff --git a/app/components/UI/Perps/hooks/usePerpsMarkets.test.ts b/app/components/UI/Perps/hooks/usePerpsMarkets.test.ts
index 7ba878bae788..7b3ebfccaaa5 100644
--- a/app/components/UI/Perps/hooks/usePerpsMarkets.test.ts
+++ b/app/components/UI/Perps/hooks/usePerpsMarkets.test.ts
@@ -3,7 +3,7 @@ import { waitFor } from '@testing-library/react-native';
import DevLogger from '../../../../core/SDKConnect/utils/DevLogger';
import Engine from '../../../../core/Engine';
import { usePerpsMarkets } from './usePerpsMarkets';
-import type { PerpsMarketData } from '../controllers/types';
+import type { PerpsMarketData, IPerpsProvider } from '../controllers/types';
// Mock dependencies
jest.mock('../../../../core/SDKConnect/utils/DevLogger');
@@ -30,7 +30,7 @@ jest.mock('../providers/PerpsConnectionProvider', () => ({
// Mock stream hooks
jest.mock('./stream', () => ({
- useLivePrices: jest.fn(() => ({})),
+ usePerpsLivePrices: jest.fn(() => ({})),
}));
// Mock data
@@ -56,7 +56,7 @@ const mockMarketData: PerpsMarketData[] = [
];
const mockProvider = {
- protocolId: 'hyperliquid',
+ protocolId: 'hyperliquid' as const,
getMarketDataWithPrices: jest.fn(),
getDepositRoutes: jest.fn(),
getWithdrawalRoutes: jest.fn(),
@@ -102,7 +102,12 @@ const mockProvider = {
getOpenOrders: jest.fn(),
getFunding: jest.fn(),
getIsFirstTimeUser: jest.fn(),
-} as const;
+ subscribeToOrders: jest.fn(),
+ unsubscribeFromOrders: jest.fn(),
+ unsubscribeFromPrices: jest.fn(),
+ unsubscribeFromPositions: jest.fn(),
+ unsubscribeFromOrderFills: jest.fn(),
+};
const mockPerpsController = Engine.context.PerpsController as jest.Mocked<
typeof Engine.context.PerpsController
@@ -115,7 +120,9 @@ describe('usePerpsMarkets', () => {
jest.useFakeTimers();
// Set up default mocks
- mockPerpsController.getActiveProvider.mockReturnValue(mockProvider);
+ mockPerpsController.getActiveProvider.mockReturnValue(
+ mockProvider as IPerpsProvider,
+ );
mockProvider.getMarketDataWithPrices.mockResolvedValue(mockMarketData);
});
diff --git a/app/components/UI/Perps/hooks/usePerpsMarkets.ts b/app/components/UI/Perps/hooks/usePerpsMarkets.ts
index 78f7eef34948..0851dfc260cc 100644
--- a/app/components/UI/Perps/hooks/usePerpsMarkets.ts
+++ b/app/components/UI/Perps/hooks/usePerpsMarkets.ts
@@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react';
import DevLogger from '../../../../core/SDKConnect/utils/DevLogger';
import Engine from '../../../../core/Engine';
import type { PerpsMarketData } from '../controllers/types';
-import { useLivePrices } from './stream';
+import { usePerpsLivePrices } from './stream';
export interface UsePerpsMarketsResult {
/**
@@ -83,9 +83,9 @@ export const usePerpsMarkets = (
);
// Conditionally subscribe to live prices if enabled
- const livePrices = useLivePrices({
+ const livePrices = usePerpsLivePrices({
symbols: enableLivePrices ? marketSymbols : [],
- debounceMs: livePriceDebounceMs,
+ throttleMs: livePriceDebounceMs,
});
const fetchMarketData = useCallback(
diff --git a/app/components/UI/Perps/hooks/usePerpsOpenOrders.test.ts b/app/components/UI/Perps/hooks/usePerpsOpenOrders.test.ts
deleted file mode 100644
index 910d7f015dc0..000000000000
--- a/app/components/UI/Perps/hooks/usePerpsOpenOrders.test.ts
+++ /dev/null
@@ -1,388 +0,0 @@
-import { renderHook, act, waitFor } from '@testing-library/react-native';
-import { usePerpsOpenOrders } from './usePerpsOpenOrders';
-import Engine from '../../../../core/Engine';
-import DevLogger from '../../../../core/SDKConnect/utils/DevLogger';
-import type { Order, GetOrdersParams } from '../controllers/types';
-
-// Mock Engine and DevLogger
-jest.mock('../../../../core/Engine');
-jest.mock('../../../../core/SDKConnect/utils/DevLogger');
-jest.mock('../providers/PerpsConnectionProvider', () => ({
- usePerpsConnection: jest.fn(() => ({
- isInitialized: true,
- isConnected: true,
- })),
-}));
-
-const mockEngine = Engine as jest.Mocked;
-const mockDevLogger = DevLogger as jest.Mocked;
-
-describe('usePerpsOpenOrders', () => {
- const mockOrders: Order[] = [
- {
- orderId: 'order-1',
- symbol: 'BTC-PERP',
- side: 'buy',
- originalSize: '1.0',
- filledSize: '0.0',
- price: '50000',
- orderType: 'limit',
- status: 'open',
- timestamp: Date.now(),
- reduceOnly: false,
- } as Order,
- {
- orderId: 'order-2',
- symbol: 'ETH-PERP',
- side: 'sell',
- originalSize: '10.0',
- filledSize: '0.0',
- price: '3000',
- orderType: 'limit',
- status: 'open',
- timestamp: Date.now(),
- reduceOnly: false,
- } as Order,
- ];
-
- const mockParams = {
- symbol: 'BTC-PERP',
- limit: 100,
- };
-
- let mockGetOpenOrders: jest.MockedFunction<
- (params?: GetOrdersParams) => Promise
- >;
-
- beforeEach(() => {
- jest.clearAllMocks();
- jest.useFakeTimers();
-
- // Setup default mock implementations
- mockGetOpenOrders = jest.fn().mockResolvedValue(mockOrders);
- mockEngine.context.PerpsController = {
- getOpenOrders: mockGetOpenOrders,
- } as unknown as typeof mockEngine.context.PerpsController;
-
- mockDevLogger.log = jest.fn();
- });
-
- afterEach(() => {
- jest.useRealTimers();
- });
-
- it('fetches open orders on mount by default', async () => {
- const { result } = renderHook(() => usePerpsOpenOrders());
-
- // Initially loading
- expect(result.current.isLoading).toBe(true);
- expect(result.current.orders).toEqual([]);
- expect(result.current.error).toBeNull();
-
- await waitFor(
- () => {
- expect(result.current.isLoading).toBe(false);
- },
- { timeout: 3000 },
- );
-
- expect(result.current.orders).toEqual(mockOrders);
- expect(
- mockEngine.context.PerpsController.getOpenOrders,
- ).toHaveBeenCalledWith(undefined);
- expect(mockDevLogger.log).toHaveBeenCalledWith(
- 'Perps: Fetching open orders from controller...',
- );
- expect(mockDevLogger.log).toHaveBeenCalledWith(
- 'Perps: Successfully fetched open orders',
- {
- orderCount: 2,
- },
- );
- });
-
- it('skips initial fetch when skipInitialFetch is true', () => {
- const { result } = renderHook(() =>
- usePerpsOpenOrders({ skipInitialFetch: true }),
- );
-
- expect(result.current.isLoading).toBe(false);
- expect(result.current.orders).toEqual([]);
- expect(
- mockEngine.context.PerpsController.getOpenOrders,
- ).not.toHaveBeenCalled();
- });
-
- it('passes params to getOpenOrders when provided', async () => {
- const { result } = renderHook(() =>
- usePerpsOpenOrders({ params: mockParams }),
- );
-
- await waitFor(() => {
- expect(result.current.isLoading).toBe(false);
- });
-
- expect(
- mockEngine.context.PerpsController.getOpenOrders,
- ).toHaveBeenCalledWith(mockParams);
- });
-
- it('handles successful data refresh', async () => {
- const { result } = renderHook(() => usePerpsOpenOrders());
-
- // Wait for initial load
- await waitFor(() => {
- expect(result.current.isLoading).toBe(false);
- });
-
- // Mock new data for refresh
- const newOrders = [{ ...mockOrders[0], price: '51000' }];
- mockGetOpenOrders.mockResolvedValueOnce(newOrders);
-
- // Trigger refresh
- await act(async () => {
- await result.current.refresh();
- });
-
- expect(result.current.isRefreshing).toBe(false);
- expect(result.current.orders).toEqual(newOrders);
- expect(mockDevLogger.log).toHaveBeenCalledWith(
- 'Perps: Fetching open orders from controller...',
- );
- });
-
- it('handles refresh errors without clearing existing data', async () => {
- const { result } = renderHook(() => usePerpsOpenOrders());
-
- // Wait for initial load
- await waitFor(() => {
- expect(result.current.isLoading).toBe(false);
- });
-
- // Mock error on refresh
- const errorMessage = 'Network error';
- mockGetOpenOrders.mockRejectedValueOnce(new Error(errorMessage));
-
- // Trigger refresh
- await act(async () => {
- await result.current.refresh();
- });
-
- expect(result.current.isRefreshing).toBe(false);
- expect(result.current.error).toBe(errorMessage);
- // Existing data should be preserved on refresh error
- expect(result.current.orders).toEqual(mockOrders);
- expect(mockDevLogger.log).toHaveBeenCalledWith(
- 'Perps: Failed to fetch open orders',
- expect.any(Error),
- );
- });
-
- it('clears existing data on initial fetch error', async () => {
- const errorMessage = 'Initial fetch failed';
- mockGetOpenOrders.mockRejectedValueOnce(new Error(errorMessage));
-
- const { result } = renderHook(() => usePerpsOpenOrders());
-
- await waitFor(() => {
- expect(result.current.isLoading).toBe(false);
- });
-
- expect(result.current.error).toBe(errorMessage);
- expect(result.current.orders).toEqual([]);
- expect(mockDevLogger.log).toHaveBeenCalledWith(
- 'Perps: Failed to fetch open orders',
- expect.any(Error),
- );
- });
-
- it('handles non-Error exceptions gracefully', async () => {
- mockGetOpenOrders.mockRejectedValueOnce('String error');
-
- const { result } = renderHook(() => usePerpsOpenOrders());
-
- await waitFor(() => {
- expect(result.current.isLoading).toBe(false);
- });
-
- expect(result.current.error).toBe('Unknown error occurred');
- expect(result.current.orders).toEqual([]);
- });
-
- it('enables polling when enablePolling is true', async () => {
- const pollingInterval = 1000;
- const { result } = renderHook(() =>
- usePerpsOpenOrders({ enablePolling: true, pollingInterval }),
- );
-
- // Wait for initial load
- await waitFor(() => {
- expect(result.current.isLoading).toBe(false);
- });
-
- // Fast-forward time to trigger polling
- act(() => {
- jest.advanceTimersByTime(pollingInterval);
- });
-
- await waitFor(() => {
- expect(
- mockEngine.context.PerpsController.getOpenOrders,
- ).toHaveBeenCalledTimes(2);
- });
- });
-
- it('uses default polling interval when not specified', async () => {
- const { result } = renderHook(() =>
- usePerpsOpenOrders({ enablePolling: true }),
- );
-
- // Wait for initial load
- await waitFor(() => {
- expect(result.current.isLoading).toBe(false);
- });
-
- // Fast-forward to default 30 second interval
- act(() => {
- jest.advanceTimersByTime(30000);
- });
-
- await waitFor(() => {
- expect(
- mockEngine.context.PerpsController.getOpenOrders,
- ).toHaveBeenCalledTimes(2);
- });
- });
-
- it('disables polling when enablePolling is false', async () => {
- const { result } = renderHook(() =>
- usePerpsOpenOrders({ enablePolling: false, pollingInterval: 1000 }),
- );
-
- // Wait for initial load
- await waitFor(() => {
- expect(result.current.isLoading).toBe(false);
- });
-
- // Fast-forward time
- act(() => {
- jest.advanceTimersByTime(5000);
- });
-
- // Should only be called once (initial fetch)
- expect(
- mockEngine.context.PerpsController.getOpenOrders,
- ).toHaveBeenCalledTimes(1);
- });
-
- it('cleans up polling interval on unmount', () => {
- const { unmount } = renderHook(() =>
- usePerpsOpenOrders({ enablePolling: true, pollingInterval: 1000 }),
- );
-
- // Unmount the hook
- unmount();
-
- // Fast-forward time - should not trigger additional calls
- act(() => {
- jest.advanceTimersByTime(5000);
- });
-
- // Should only be called once (initial fetch)
- expect(
- mockEngine.context.PerpsController.getOpenOrders,
- ).toHaveBeenCalledTimes(1);
- });
-
- it('handles empty orders array from controller', async () => {
- mockGetOpenOrders.mockResolvedValueOnce([]);
-
- const { result } = renderHook(() => usePerpsOpenOrders());
-
- await waitFor(() => {
- expect(result.current.isLoading).toBe(false);
- });
-
- expect(result.current.orders).toEqual([]);
- expect(mockDevLogger.log).toHaveBeenCalledWith(
- 'Perps: Successfully fetched open orders',
- {
- orderCount: 0,
- },
- );
- });
-
- it('handles null response from controller', async () => {
- mockGetOpenOrders.mockResolvedValueOnce(null as unknown as Order[]);
-
- const { result } = renderHook(() => usePerpsOpenOrders());
-
- await waitFor(() => {
- expect(result.current.isLoading).toBe(false);
- });
-
- expect(result.current.orders).toEqual([]);
- expect(mockDevLogger.log).toHaveBeenCalledWith(
- 'Perps: Successfully fetched open orders',
- {
- orderCount: 0,
- },
- );
- });
-
- it('maintains separate loading states for initial fetch and refresh', async () => {
- const { result } = renderHook(() => usePerpsOpenOrders());
-
- // Initial loading state
- expect(result.current.isLoading).toBe(true);
- expect(result.current.isRefreshing).toBe(false);
-
- // Wait for initial load to complete
- await waitFor(() => {
- expect(result.current.isLoading).toBe(false);
- });
-
- // Mock delay for refresh
- mockGetOpenOrders.mockImplementationOnce(
- () =>
- new Promise((resolve) => setTimeout(() => resolve(mockOrders), 100)),
- );
-
- // Trigger refresh
- act(() => {
- result.current.refresh();
- });
-
- // Should show refreshing state
- expect(result.current.isRefreshing).toBe(true);
- expect(result.current.isLoading).toBe(false);
-
- // Wait for refresh to complete
- await waitFor(() => {
- expect(result.current.isRefreshing).toBe(false);
- });
- });
-
- it('updates params dependency when params change', async () => {
- const { result, rerender } = renderHook(
- ({ params }) => usePerpsOpenOrders({ params }),
- { initialProps: { params: mockParams } },
- );
-
- // Wait for initial load
- await waitFor(() => {
- expect(result.current.isLoading).toBe(false);
- });
-
- // Change params
- const newParams = { symbol: 'ETH-PERP', limit: 50 };
- rerender({ params: newParams });
-
- // Should refetch with new params
- await waitFor(() => {
- expect(
- mockEngine.context.PerpsController.getOpenOrders,
- ).toHaveBeenCalledWith(newParams);
- });
- });
-});
diff --git a/app/components/UI/Perps/hooks/usePerpsOrders.test.ts b/app/components/UI/Perps/hooks/usePerpsOrders.test.ts
index 435b446835a2..7bd3ba557dc1 100644
--- a/app/components/UI/Perps/hooks/usePerpsOrders.test.ts
+++ b/app/components/UI/Perps/hooks/usePerpsOrders.test.ts
@@ -419,120 +419,8 @@ describe('usePerpsOrders', () => {
});
});
- describe('Polling functionality', () => {
- it('does not poll by default', async () => {
- // Arrange
- const { result } = renderHook(() => usePerpsOrders());
-
- await waitFor(() => {
- expect(result.current.isLoading).toBe(false);
- });
-
- // Reset call count
- jest.clearAllMocks();
-
- // Act - advance time
- act(() => {
- jest.advanceTimersByTime(60000); // 1 minute
- });
-
- // Assert - should not have polled
- expect(mockPerpsController.getOrders).not.toHaveBeenCalled();
- });
-
- it('polls when enablePolling is true', async () => {
- // Arrange
- const pollingInterval = 30000; // 30 seconds
- const { result } = renderHook(() =>
- usePerpsOrders({ enablePolling: true, pollingInterval }),
- );
-
- await waitFor(() => {
- expect(result.current.isLoading).toBe(false);
- });
-
- // Reset call count
- jest.clearAllMocks();
-
- // Act - advance time by polling interval
- act(() => {
- jest.advanceTimersByTime(pollingInterval);
- });
-
- // Wait for the polling request to complete
- await waitFor(() => {
- expect(mockPerpsController.getOrders).toHaveBeenCalledTimes(1);
- });
-
- // Act - advance time again
- act(() => {
- jest.advanceTimersByTime(pollingInterval);
- });
-
- await waitFor(() => {
- expect(mockPerpsController.getOrders).toHaveBeenCalledTimes(2);
- });
- });
-
- it('uses custom polling interval correctly', async () => {
- // Arrange
- const customInterval = 15000; // 15 seconds
- const { result } = renderHook(() =>
- usePerpsOrders({
- enablePolling: true,
- pollingInterval: customInterval,
- }),
- );
-
- await waitFor(() => {
- expect(result.current.isLoading).toBe(false);
- });
-
- jest.clearAllMocks();
-
- // Act - advance time less than interval
- act(() => {
- jest.advanceTimersByTime(customInterval - 1000);
- });
-
- // Assert - should not poll yet
- expect(mockPerpsController.getOrders).not.toHaveBeenCalled();
-
- // Act - advance time to complete interval
- act(() => {
- jest.advanceTimersByTime(1000);
- });
-
- await waitFor(() => {
- expect(mockPerpsController.getOrders).toHaveBeenCalledTimes(1);
- });
- });
-
- it('stops polling when component unmounts', async () => {
- // Arrange
- const pollingInterval = 10000;
- const { result, unmount } = renderHook(() =>
- usePerpsOrders({ enablePolling: true, pollingInterval }),
- );
-
- await waitFor(() => {
- expect(result.current.isLoading).toBe(false);
- });
-
- jest.clearAllMocks();
-
- // Act - unmount component
- unmount();
-
- // Advance time
- act(() => {
- jest.advanceTimersByTime(pollingInterval * 2);
- });
-
- // Assert - should not poll after unmount
- expect(mockPerpsController.getOrders).not.toHaveBeenCalled();
- });
- });
+ // Note: Polling functionality has been removed in favor of WebSocket streaming
+ // For real-time order updates, use usePerpsLiveOrders from stream hooks
describe('Parameter changes', () => {
it('refetches data when params change', async () => {
diff --git a/app/components/UI/Perps/hooks/usePerpsOrders.ts b/app/components/UI/Perps/hooks/usePerpsOrders.ts
index c4717398cc71..bebd0f0bd8d2 100644
--- a/app/components/UI/Perps/hooks/usePerpsOrders.ts
+++ b/app/components/UI/Perps/hooks/usePerpsOrders.ts
@@ -31,16 +31,6 @@ export interface UsePerpsOrdersOptions {
* Parameters to pass to getOrders
*/
params?: GetOrdersParams;
- /**
- * Enable automatic polling for live updates
- * @default false
- */
- enablePolling?: boolean;
- /**
- * Polling interval in milliseconds
- * @default 30000 (30 seconds)
- */
- pollingInterval?: number;
/**
* Skip initial data fetch on mount
* @default false
@@ -51,16 +41,13 @@ export interface UsePerpsOrdersOptions {
/**
* Custom hook to fetch and manage Perps orders from the controller
* Provides loading states, error handling, and refresh functionality
+ * Note: This hook fetches historical order data. For real-time open orders,
+ * use usePerpsLiveOrders from the stream hooks.
*/
export const usePerpsOrders = (
options: UsePerpsOrdersOptions = {},
): UsePerpsOrdersResult => {
- const {
- params,
- enablePolling = false,
- pollingInterval = 30000, // 30 seconds default
- skipInitialFetch = false,
- } = options;
+ const { params, skipInitialFetch = false } = options;
const [orders, setOrders] = useState([]);
const [isLoading, setIsLoading] = useState(!skipInitialFetch);
@@ -117,17 +104,6 @@ export const usePerpsOrders = (
}
}, [fetchOrders, skipInitialFetch]);
- // Polling effect
- useEffect(() => {
- if (!enablePolling) return;
-
- const intervalId = setInterval(() => {
- fetchOrders(true);
- }, pollingInterval);
-
- return () => clearInterval(intervalId);
- }, [enablePolling, pollingInterval, fetchOrders]);
-
return {
orders,
isLoading,
diff --git a/app/components/UI/Perps/hooks/usePerpsPositionData.test.ts b/app/components/UI/Perps/hooks/usePerpsPositionData.test.ts
index 02bc43d2c5df..a9545f4d7c98 100644
--- a/app/components/UI/Perps/hooks/usePerpsPositionData.test.ts
+++ b/app/components/UI/Perps/hooks/usePerpsPositionData.test.ts
@@ -8,7 +8,6 @@ jest.mock('../../../../core/Engine', () => ({
context: {
PerpsController: {
fetchHistoricalCandles: jest.fn(),
- subscribeToPrices: jest.fn(),
},
},
}));
@@ -16,9 +15,6 @@ jest.mock('../../../../core/Engine', () => ({
describe('usePerpsPositionData', () => {
const mockFetchHistoricalCandles = Engine.context.PerpsController
.fetchHistoricalCandles as jest.Mock;
- const mockSubscribeToPrices = Engine.context.PerpsController
- .subscribeToPrices as jest.Mock;
- const mockUnsubscribe = jest.fn();
const mockCandleData = {
candles: [
@@ -26,17 +22,9 @@ describe('usePerpsPositionData', () => {
],
};
- const mockPriceUpdate = {
- coin: 'ETH',
- price: '3000.00',
- change24h: 2.5,
- markPrice: '3001.00',
- };
-
beforeEach(() => {
jest.clearAllMocks();
mockFetchHistoricalCandles.mockResolvedValue(mockCandleData);
- mockSubscribeToPrices.mockReturnValue(mockUnsubscribe);
});
it('should fetch historical candles on mount', async () => {
@@ -53,40 +41,9 @@ describe('usePerpsPositionData', () => {
expect(mockFetchHistoricalCandles).toHaveBeenCalledWith('ETH', '1h', 24);
});
- it('should subscribe to price updates on mount', () => {
- renderHook(() =>
- usePerpsPositionData({
- coin: 'ETH',
- selectedInterval: CandlePeriod.ONE_HOUR,
- selectedDuration: TimeDuration.ONE_DAY,
- }),
- );
+ // Price subscriptions have been removed - use usePerpsLivePrices for real-time prices
- expect(mockSubscribeToPrices).toHaveBeenCalledWith({
- symbols: ['ETH'],
- callback: expect.any(Function),
- });
- });
-
- it('should update price data when receiving updates', async () => {
- const { result } = renderHook(() =>
- usePerpsPositionData({
- coin: 'ETH',
- selectedInterval: CandlePeriod.ONE_HOUR,
- selectedDuration: TimeDuration.ONE_DAY,
- }),
- );
-
- // Get the callback that was passed to subscribeToPrices
- const callback = mockSubscribeToPrices.mock.calls[0][0].callback;
-
- // Trigger price update
- act(() => {
- callback([mockPriceUpdate]);
- });
-
- expect(result.current.priceData).toEqual(mockPriceUpdate);
- });
+ // Price data updates have been moved to usePerpsLivePrices hook
it('should handle loading state correctly', async () => {
const { result, waitForNextUpdate } = renderHook(() =>
@@ -107,19 +64,7 @@ describe('usePerpsPositionData', () => {
expect(result.current.candleData).toEqual(mockCandleData);
});
- it('should unsubscribe on unmount', () => {
- const { unmount } = renderHook(() =>
- usePerpsPositionData({
- coin: 'ETH',
- selectedInterval: CandlePeriod.ONE_HOUR,
- selectedDuration: TimeDuration.ONE_DAY,
- }),
- );
-
- unmount();
-
- expect(mockUnsubscribe).toHaveBeenCalled();
- });
+ // Unsubscribe test removed - no subscriptions to clean up anymore
it('should handle errors in fetching historical data', async () => {
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
diff --git a/app/components/UI/Perps/hooks/usePerpsPositionData.ts b/app/components/UI/Perps/hooks/usePerpsPositionData.ts
index 8a7cba4f71e9..9e1beee800e8 100644
--- a/app/components/UI/Perps/hooks/usePerpsPositionData.ts
+++ b/app/components/UI/Perps/hooks/usePerpsPositionData.ts
@@ -1,6 +1,5 @@
import { useCallback, useEffect, useState } from 'react';
import Engine from '../../../../core/Engine';
-import type { PriceUpdate } from '../controllers/types';
import type { CandleData } from '../types';
import {
calculateCandleCount,
@@ -21,7 +20,6 @@ export const usePerpsPositionData = ({
selectedInterval,
}: UsePerpsPositionDataProps) => {
const [candleData, setCandleData] = useState(null);
- const [priceData, setPriceData] = useState(null);
const [isLoadingHistory, setIsLoadingHistory] = useState(false);
const fetchHistoricalCandles = useCallback(async () => {
@@ -41,26 +39,6 @@ export const usePerpsPositionData = ({
return historicalData;
}, [coin, selectedDuration, selectedInterval]);
- const subscribeToPriceUpdates = useCallback(() => {
- try {
- const unsubscribe = Engine.context.PerpsController.subscribeToPrices({
- symbols: [coin],
- callback: (priceUpdates) => {
- const update = priceUpdates.find((p) => p.coin === coin);
- if (update) {
- setPriceData(update);
- }
- },
- });
- return unsubscribe;
- } catch (err) {
- console.error('Error subscribing to price updates:', err);
- return () => {
- // Empty cleanup function on error
- };
- }
- }, [coin]);
-
// Load historical candles
useEffect(() => {
setIsLoadingHistory(true);
@@ -78,15 +56,6 @@ export const usePerpsPositionData = ({
loadHistoricalData();
}, [fetchHistoricalCandles]);
- // Subscribe to price updates for 24-hour data
- useEffect(() => {
- const unsubscribe = subscribeToPriceUpdates();
-
- return () => {
- unsubscribe();
- };
- }, [subscribeToPriceUpdates]);
-
// Refresh function to reload candle data
const refreshCandleData = useCallback(async () => {
setIsLoadingHistory(true);
@@ -102,7 +71,6 @@ export const usePerpsPositionData = ({
return {
candleData,
- priceData,
isLoadingHistory,
refreshCandleData,
};
diff --git a/app/components/UI/Perps/hooks/usePerpsPositions.test.ts b/app/components/UI/Perps/hooks/usePerpsPositions.test.ts
deleted file mode 100644
index b15829edda8b..000000000000
--- a/app/components/UI/Perps/hooks/usePerpsPositions.test.ts
+++ /dev/null
@@ -1,128 +0,0 @@
-import { renderHook, act, waitFor } from '@testing-library/react-native';
-import { usePerpsPositions } from './usePerpsPositions';
-import { usePerpsTrading } from './usePerpsTrading';
-import { useFocusEffect } from '@react-navigation/native';
-
-// Mock dependencies
-jest.mock('./usePerpsTrading');
-jest.mock('@react-navigation/native', () => ({
- useFocusEffect: jest.fn(),
-}));
-jest.mock('../../../../core/SDKConnect/utils/DevLogger', () => ({
- DevLogger: {
- log: jest.fn(),
- },
-}));
-jest.mock('../providers/PerpsConnectionProvider', () => ({
- usePerpsConnection: jest.fn(() => ({
- isInitialized: true,
- isConnected: true,
- })),
-}));
-
-describe('usePerpsPositions', () => {
- const mockGetPositions = jest.fn();
- const mockUseFocusEffect = useFocusEffect as jest.Mock;
-
- beforeEach(() => {
- jest.clearAllMocks();
- (usePerpsTrading as jest.Mock).mockReturnValue({
- getPositions: mockGetPositions,
- });
- });
-
- it('should load positions on mount by default', async () => {
- mockGetPositions.mockResolvedValue([
- { coin: 'ETH', size: '1.5', unrealizedPnl: '100' },
- { coin: 'BTC', size: '0.1', unrealizedPnl: '50' },
- ]);
-
- const { result } = renderHook(() => usePerpsPositions());
-
- expect(result.current.isLoading).toBe(true);
- expect(result.current.positions).toEqual([]);
-
- await waitFor(() => {
- expect(result.current.isLoading).toBe(false);
- });
-
- expect(mockGetPositions).toHaveBeenCalled();
- expect(result.current.positions).toHaveLength(2);
- expect(result.current.error).toBe(null);
- });
-
- it('should not load on mount when loadOnMount is false', () => {
- mockGetPositions.mockResolvedValue([]);
-
- renderHook(() => usePerpsPositions({ loadOnMount: false }));
-
- expect(mockGetPositions).not.toHaveBeenCalled();
- });
-
- it('should handle errors correctly', async () => {
- const testError = new Error('Failed to fetch positions');
- mockGetPositions.mockRejectedValue(testError);
- const onError = jest.fn();
-
- const { result } = renderHook(() => usePerpsPositions({ onError }));
-
- await waitFor(() => {
- expect(result.current.isLoading).toBe(false);
- });
-
- expect(result.current.error).toBe('Failed to fetch positions');
- expect(result.current.positions).toEqual([]);
- expect(onError).toHaveBeenCalledWith('Failed to fetch positions');
- });
-
- it('should refresh positions with isRefresh flag', async () => {
- mockGetPositions.mockResolvedValue([]);
-
- const { result } = renderHook(() => usePerpsPositions());
-
- await waitFor(() => {
- expect(result.current.isLoading).toBe(false);
- });
-
- // Reset mock to track refresh call
- mockGetPositions.mockClear();
- mockGetPositions.mockResolvedValue([
- { coin: 'ETH', size: '2.0', unrealizedPnl: '200' },
- ]);
-
- await act(async () => {
- await result.current.loadPositions({ isRefresh: true });
- });
-
- expect(mockGetPositions).toHaveBeenCalled();
- expect(result.current.isRefreshing).toBe(false);
- expect(result.current.positions).toHaveLength(1);
- });
-
- it('should call onSuccess callback when positions load successfully', async () => {
- const onSuccess = jest.fn();
- const positions = [{ coin: 'ETH', size: '1.5', unrealizedPnl: '100' }];
- mockGetPositions.mockResolvedValue(positions);
-
- const { result } = renderHook(() => usePerpsPositions({ onSuccess }));
-
- await waitFor(() => {
- expect(result.current.isLoading).toBe(false);
- });
-
- expect(onSuccess).toHaveBeenCalledWith(positions);
- });
-
- it('should setup focus effect when refreshOnFocus is true', () => {
- renderHook(() => usePerpsPositions({ refreshOnFocus: true }));
-
- expect(mockUseFocusEffect).toHaveBeenCalled();
- });
-
- it('should not setup focus effect when refreshOnFocus is false', () => {
- renderHook(() => usePerpsPositions({ refreshOnFocus: false }));
-
- // The hook is still called but the callback won't trigger loadPositions
- expect(mockUseFocusEffect).toHaveBeenCalled();
- });
-});
diff --git a/app/components/UI/Perps/hooks/usePerpsPrices.ts b/app/components/UI/Perps/hooks/usePerpsPrices.ts
index 27d7d39c2cc5..5c4552c4bb0d 100644
--- a/app/components/UI/Perps/hooks/usePerpsPrices.ts
+++ b/app/components/UI/Perps/hooks/usePerpsPrices.ts
@@ -12,7 +12,7 @@ export interface UsePerpsPricesOptions {
/** Whether to include order book data (bid/ask) */
includeOrderBook?: boolean;
/** Debounce delay in milliseconds (default: 50ms) */
- debounceMs?: number;
+ throttleMs?: number;
/** Whether to include market data (funding, OI, volume) */
includeMarketData?: boolean;
}
@@ -29,7 +29,7 @@ export function usePerpsPrices(
): Record {
const {
includeOrderBook = false,
- debounceMs,
+ throttleMs,
includeMarketData = false,
} = options;
@@ -67,7 +67,7 @@ export function usePerpsPrices(
// Use provided debounce or fall back to default
const debounceDelay =
- debounceMs ?? PERFORMANCE_CONFIG.PRICE_UPDATE_DEBOUNCE_MS;
+ throttleMs ?? PERFORMANCE_CONFIG.PRICE_UPDATE_DEBOUNCE_MS;
// Track if we've received the first update for each symbol
// This only resets when symbols change, not debounce settings
diff --git a/app/components/UI/Perps/providers/PerpsStreamManager.test.tsx b/app/components/UI/Perps/providers/PerpsStreamManager.test.tsx
index 712a4ef74d15..c8cd61b9d919 100644
--- a/app/components/UI/Perps/providers/PerpsStreamManager.test.tsx
+++ b/app/components/UI/Perps/providers/PerpsStreamManager.test.tsx
@@ -1,7 +1,11 @@
import React from 'react';
import { render, waitFor, act } from '@testing-library/react-native';
import { Text } from 'react-native';
-import { PerpsStreamProvider, usePerpsStream } from './PerpsStreamManager';
+import {
+ PerpsStreamProvider,
+ usePerpsStream,
+ PerpsStreamManager,
+} from './PerpsStreamManager';
import Engine from '../../../../core/Engine';
import DevLogger from '../../../../core/SDKConnect/utils/DevLogger';
import type { PriceUpdate } from '../controllers/types';
@@ -27,7 +31,7 @@ const TestPriceComponent = ({
callback: (prices: Record) => {
onUpdate?.(prices);
},
- debounceMs: 100,
+ throttleMs: 100,
});
return () => {
@@ -41,12 +45,16 @@ const TestPriceComponent = ({
describe('PerpsStreamManager', () => {
let mockSubscribeToPrices: jest.Mock;
let mockUnsubscribeFromPrices: jest.Mock;
+ let testStreamManager: PerpsStreamManager;
beforeEach(() => {
jest.clearAllMocks();
jest.clearAllTimers();
jest.useFakeTimers();
+ // Create a fresh stream manager for each test
+ testStreamManager = new PerpsStreamManager();
+
// Setup default mocks
mockSubscribeToPrices = jest.fn();
mockUnsubscribeFromPrices = jest.fn();
@@ -60,12 +68,14 @@ describe('PerpsStreamManager', () => {
});
afterEach(() => {
+ jest.clearAllTimers();
jest.useRealTimers();
+ jest.clearAllMocks();
});
it('should render children correctly', () => {
const { getByText } = render(
-
+
Child Component
,
);
@@ -91,13 +101,395 @@ describe('PerpsStreamManager', () => {
console.error = originalError;
});
+ it('should provide immediate cached data on subscription', async () => {
+ // Setup mock subscription that will trigger updates
+ mockSubscribeToPrices.mockImplementation(
+ (params: { callback: (updates: PriceUpdate[]) => void }) => {
+ // Simulate immediate cached data with all required fields
+ const cachedData: PriceUpdate[] = [
+ {
+ coin: 'BTC-PERP',
+ price: '50000',
+ percentChange24h: '5',
+ timestamp: Date.now(),
+ bestBid: '49900',
+ bestAsk: '50100',
+ spread: '200',
+ markPrice: '50050',
+ },
+ ];
+ params.callback(cachedData);
+ return jest.fn();
+ },
+ );
+
+ const onUpdate = jest.fn();
+
+ render(
+
+
+ ,
+ );
+
+ // Should receive cached data immediately
+ await waitFor(() => {
+ expect(onUpdate).toHaveBeenCalledWith({
+ 'BTC-PERP': {
+ coin: 'BTC-PERP',
+ price: '50000',
+ timestamp: expect.any(Number),
+ percentChange24h: '5',
+ bestBid: '49900',
+ bestAsk: '50100',
+ spread: '200',
+ markPrice: '50050',
+ funding: undefined,
+ openInterest: undefined,
+ volume24h: undefined,
+ },
+ });
+ });
+ });
+
+ it('should throttle updates after first immediate update', async () => {
+ let controllerCallback: ((updates: PriceUpdate[]) => void) | null = null;
+ mockSubscribeToPrices.mockImplementation(
+ (params: { callback: (updates: PriceUpdate[]) => void }) => {
+ controllerCallback = params.callback;
+ return jest.fn();
+ },
+ );
+
+ const onUpdate = jest.fn();
+
+ render(
+
+
+ ,
+ );
+
+ // Wait for subscription setup
+ await waitFor(() => {
+ expect(mockSubscribeToPrices).toHaveBeenCalled();
+ });
+
+ // First update should be immediate
+ act(() => {
+ controllerCallback?.([
+ {
+ coin: 'BTC-PERP',
+ price: '50000',
+ percentChange24h: '5',
+ timestamp: Date.now(),
+ },
+ ]);
+ });
+
+ await waitFor(() => {
+ expect(onUpdate).toHaveBeenCalledTimes(1);
+ });
+
+ // Subsequent updates should be throttled
+ act(() => {
+ controllerCallback?.([
+ {
+ coin: 'BTC-PERP',
+ price: '50100',
+ percentChange24h: '5.1',
+ timestamp: Date.now(),
+ },
+ ]);
+ });
+
+ // Should not be called immediately
+ expect(onUpdate).toHaveBeenCalledTimes(1);
+
+ // Fast-forward time to trigger throttled update
+ act(() => {
+ jest.advanceTimersByTime(100);
+ });
+
+ await waitFor(() => {
+ expect(onUpdate).toHaveBeenCalledTimes(2);
+ expect(onUpdate).toHaveBeenLastCalledWith({
+ 'BTC-PERP': {
+ coin: 'BTC-PERP',
+ price: '50100',
+ timestamp: expect.any(Number),
+ percentChange24h: '5.1',
+ bestBid: undefined,
+ bestAsk: undefined,
+ spread: undefined,
+ markPrice: undefined,
+ funding: undefined,
+ openInterest: undefined,
+ volume24h: undefined,
+ },
+ });
+ });
+ });
+
+ it('should handle multiple rapid updates with throttling', async () => {
+ let controllerCallback: ((updates: PriceUpdate[]) => void) | null = null;
+ mockSubscribeToPrices.mockImplementation(
+ (params: { callback: (updates: PriceUpdate[]) => void }) => {
+ controllerCallback = params.callback;
+ return jest.fn();
+ },
+ );
+
+ const onUpdate = jest.fn();
+
+ render(
+
+
+ ,
+ );
+
+ // Wait for subscription setup
+ await waitFor(() => {
+ expect(mockSubscribeToPrices).toHaveBeenCalled();
+ });
+
+ // First update (immediate)
+ act(() => {
+ controllerCallback?.([
+ {
+ coin: 'BTC-PERP',
+ price: '50000',
+ percentChange24h: '5',
+ timestamp: Date.now(),
+ },
+ ]);
+ });
+
+ await waitFor(() => {
+ expect(onUpdate).toHaveBeenCalledTimes(1);
+ });
+
+ // Multiple rapid updates during throttle period
+ act(() => {
+ controllerCallback?.([
+ {
+ coin: 'BTC-PERP',
+ price: '50100',
+ percentChange24h: '5.1',
+ timestamp: Date.now(),
+ },
+ ]);
+ controllerCallback?.([
+ {
+ coin: 'BTC-PERP',
+ price: '50200',
+ percentChange24h: '5.2',
+ timestamp: Date.now(),
+ },
+ ]);
+ controllerCallback?.([
+ {
+ coin: 'BTC-PERP',
+ price: '50300',
+ percentChange24h: '5.3',
+ timestamp: Date.now(),
+ },
+ ]);
+ });
+
+ // Still only 1 call (first immediate)
+ expect(onUpdate).toHaveBeenCalledTimes(1);
+
+ // Advance timer to trigger throttled update
+ act(() => {
+ jest.advanceTimersByTime(100);
+ });
+
+ // Should receive the latest update
+ await waitFor(() => {
+ expect(onUpdate).toHaveBeenCalledTimes(2);
+ expect(onUpdate).toHaveBeenLastCalledWith({
+ 'BTC-PERP': {
+ coin: 'BTC-PERP',
+ price: '50300',
+ timestamp: expect.any(Number),
+ percentChange24h: '5.3',
+ bestBid: undefined,
+ bestAsk: undefined,
+ spread: undefined,
+ markPrice: undefined,
+ funding: undefined,
+ openInterest: undefined,
+ volume24h: undefined,
+ },
+ });
+ });
+ });
+
+ it('should handle subscription without throttling', async () => {
+ let controllerCallback: ((updates: PriceUpdate[]) => void) | null = null;
+ mockSubscribeToPrices.mockImplementation(
+ (params: { callback: (updates: PriceUpdate[]) => void }) => {
+ controllerCallback = params.callback;
+ return jest.fn();
+ },
+ );
+
+ const TestNoThrottleComponent = ({
+ onUpdate,
+ }: {
+ onUpdate?: (prices: Record) => void;
+ }) => {
+ const stream = usePerpsStream();
+
+ React.useEffect(() => {
+ const unsubscribe = stream.prices.subscribeToSymbols({
+ symbols: ['ETH-PERP'],
+ callback: (prices: Record) => {
+ onUpdate?.(prices);
+ },
+ throttleMs: 0, // No throttling
+ });
+
+ return () => {
+ unsubscribe();
+ };
+ }, [stream, onUpdate]);
+
+ return No Throttle;
+ };
+
+ const onUpdate = jest.fn();
+
+ render(
+
+
+ ,
+ );
+
+ // Wait for subscription setup
+ await waitFor(() => {
+ expect(mockSubscribeToPrices).toHaveBeenCalled();
+ });
+
+ // Reset the mock call count after initial setup
+ onUpdate.mockClear();
+
+ // All updates should be immediate when throttleMs is 0
+ act(() => {
+ controllerCallback?.([
+ {
+ coin: 'ETH-PERP',
+ price: '3000',
+ timestamp: Date.now(),
+ percentChange24h: '2',
+ },
+ ]);
+ });
+
+ await waitFor(() => {
+ expect(onUpdate).toHaveBeenCalledTimes(1);
+ });
+
+ act(() => {
+ controllerCallback?.([
+ {
+ coin: 'ETH-PERP',
+ price: '3010',
+ timestamp: Date.now(),
+ percentChange24h: '2.1',
+ },
+ ]);
+ });
+
+ await waitFor(() => {
+ expect(onUpdate).toHaveBeenCalledTimes(2);
+ });
+
+ act(() => {
+ controllerCallback?.([
+ {
+ coin: 'ETH-PERP',
+ price: '3020',
+ timestamp: Date.now(),
+ percentChange24h: '2.2',
+ },
+ ]);
+ });
+
+ await waitFor(() => {
+ expect(onUpdate).toHaveBeenCalledTimes(3);
+ });
+ });
+
+ it('should clean up timers on unsubscribe', async () => {
+ let controllerCallback: ((updates: PriceUpdate[]) => void) | null = null;
+ const unsubscribeMock = jest.fn();
+ mockSubscribeToPrices.mockImplementation(
+ (params: { callback: (updates: PriceUpdate[]) => void }) => {
+ controllerCallback = params.callback;
+ return unsubscribeMock;
+ },
+ );
+
+ const onUpdate = jest.fn();
+
+ const { unmount } = render(
+
+
+ ,
+ );
+
+ // Wait for subscription setup
+ await waitFor(() => {
+ expect(mockSubscribeToPrices).toHaveBeenCalled();
+ });
+
+ // First update (immediate)
+ act(() => {
+ controllerCallback?.([
+ {
+ coin: 'BTC-PERP',
+ price: '50000',
+ timestamp: Date.now(),
+ percentChange24h: '5',
+ },
+ ]);
+ });
+
+ await waitFor(() => {
+ expect(onUpdate).toHaveBeenCalledTimes(1);
+ });
+
+ // Queue a throttled update
+ act(() => {
+ controllerCallback?.([
+ {
+ coin: 'BTC-PERP',
+ price: '50100',
+ timestamp: Date.now(),
+ percentChange24h: '5.1',
+ },
+ ]);
+ });
+
+ // Unmount before timer fires
+ unmount();
+
+ // Advance timer
+ act(() => {
+ jest.advanceTimersByTime(100);
+ });
+
+ // Should not receive update after unmount
+ expect(onUpdate).toHaveBeenCalledTimes(1);
+ });
+
it('should subscribe to prices when component mounts', async () => {
mockSubscribeToPrices.mockImplementation(() => jest.fn());
const onUpdate = jest.fn();
render(
-
+
,
);
@@ -116,7 +508,7 @@ describe('PerpsStreamManager', () => {
mockSubscribeToPrices.mockReturnValue(mockUnsubscribe);
const { unmount } = render(
-
+
,
);
@@ -143,7 +535,7 @@ describe('PerpsStreamManager', () => {
});
render(
-
+
,
);
@@ -175,7 +567,7 @@ describe('PerpsStreamManager', () => {
});
});
- it('should debounce subsequent updates', async () => {
+ it('should throttle subsequent updates', async () => {
const onUpdate = jest.fn();
let priceCallback: (data: PriceUpdate[]) => void = jest.fn();
@@ -185,7 +577,7 @@ describe('PerpsStreamManager', () => {
});
render(
-
+
,
);
@@ -233,12 +625,12 @@ describe('PerpsStreamManager', () => {
// Should not be called immediately
expect(onUpdate).toHaveBeenCalledTimes(1);
- // Advance timers to trigger debounce
+ // Advance timers to trigger throttle
act(() => {
jest.advanceTimersByTime(100);
});
- // Should receive the last update after debounce
+ // Should receive the last update after throttle
await waitFor(() => {
expect(onUpdate).toHaveBeenCalledTimes(2);
const lastCall = onUpdate.mock.calls[1][0];
@@ -249,7 +641,7 @@ describe('PerpsStreamManager', () => {
});
});
- it('should handle multiple subscribers with different debounce times', async () => {
+ it('should handle multiple subscribers with different throttle times', async () => {
const onUpdate1 = jest.fn();
const onUpdate2 = jest.fn();
let priceCallback: (data: PriceUpdate[]) => void = jest.fn();
@@ -268,7 +660,7 @@ describe('PerpsStreamManager', () => {
callback: (prices: Record) => {
onUpdate1(prices);
},
- debounceMs: 100,
+ throttleMs: 100,
});
const sub2 = stream.prices.subscribeToSymbols({
@@ -276,7 +668,7 @@ describe('PerpsStreamManager', () => {
callback: (prices: Record) => {
onUpdate2(prices);
},
- debounceMs: 200,
+ throttleMs: 200,
});
return () => {
@@ -289,7 +681,7 @@ describe('PerpsStreamManager', () => {
};
render(
-
+
,
);
@@ -365,7 +757,7 @@ describe('PerpsStreamManager', () => {
callback: (prices: Record) => {
onUpdate(prices);
},
- debounceMs: 100,
+ throttleMs: 100,
});
return () => {
@@ -379,7 +771,7 @@ describe('PerpsStreamManager', () => {
const onUpdate = jest.fn();
render(
-
+
,
);
@@ -397,7 +789,7 @@ describe('PerpsStreamManager', () => {
mockSubscribeToPrices.mockReturnValue(mockUnsubscribe);
const { unmount } = render(
-
+
,
);
diff --git a/app/components/UI/Perps/providers/PerpsStreamManager.tsx b/app/components/UI/Perps/providers/PerpsStreamManager.tsx
index 90633a45f34e..ba73eca011ea 100644
--- a/app/components/UI/Perps/providers/PerpsStreamManager.tsx
+++ b/app/components/UI/Perps/providers/PerpsStreamManager.tsx
@@ -1,6 +1,5 @@
import React, { createContext, useContext } from 'react';
import Engine from '../../../../core/Engine';
-import { DevLogger } from '../../../../core/SDKConnect/utils/DevLogger';
import type {
PriceUpdate,
Position,
@@ -12,7 +11,7 @@ import type {
interface StreamSubscription {
id: string;
callback: (data: T) => void;
- debounceMs: number;
+ throttleMs?: number;
timer?: NodeJS.Timeout;
pendingUpdate?: T;
hasReceivedFirstUpdate?: boolean; // Track if subscriber has received first update
@@ -28,46 +27,40 @@ class StreamChannel {
this.subscribers.forEach((subscriber) => {
// Check if this is the first update for this subscriber
if (!subscriber.hasReceivedFirstUpdate) {
- DevLogger.log(
- `StreamChannel: First update for subscriber ${subscriber.id}, executing immediately`,
- );
subscriber.callback(updates);
subscriber.hasReceivedFirstUpdate = true;
- return; // Don't set up debounce for the first update
+ return; // Don't set up throttle for the first update
}
- // For subsequent updates, use debounce logic
+ // If no throttling (throttleMs is 0 or undefined), notify immediately
+ if (!subscriber.throttleMs) {
+ subscriber.callback(updates);
+ return;
+ }
+
+ // For subsequent updates with throttling, use throttle logic
// Store pending update
subscriber.pendingUpdate = updates;
// Only set timer if one isn't already running
if (!subscriber.timer) {
- DevLogger.log(
- `StreamChannel: Setting ${subscriber.debounceMs}ms timer for subscriber ${subscriber.id}`,
- );
subscriber.timer = setTimeout(() => {
if (subscriber.pendingUpdate) {
- DevLogger.log(
- `StreamChannel: Executing callback for subscriber ${subscriber.id} after ${subscriber.debounceMs}ms`,
- );
subscriber.callback(subscriber.pendingUpdate);
subscriber.pendingUpdate = undefined;
subscriber.timer = undefined;
}
- }, subscriber.debounceMs);
+ }, subscriber.throttleMs);
}
});
}
subscribe(params: {
callback: (data: T) => void;
- debounceMs: number;
+ throttleMs?: number;
}): () => void {
const id = Math.random().toString(36);
- DevLogger.log(
- `StreamChannel: New subscriber ${id} with ${params.debounceMs}ms debounce`,
- );
const subscription: StreamSubscription = {
id,
...params,
@@ -78,7 +71,6 @@ class StreamChannel {
// Give immediate cached data if available
const cached = this.getCachedData();
if (cached) {
- DevLogger.log(`StreamChannel: Providing cached data to subscriber ${id}`);
params.callback(cached);
// Mark as having received first update since we provided cached data
subscription.hasReceivedFirstUpdate = true;
@@ -88,7 +80,6 @@ class StreamChannel {
this.connect();
return () => {
- DevLogger.log(`StreamChannel: Removing subscriber ${id}`);
const sub = this.subscribers.get(id);
if (sub?.timer) {
clearTimeout(sub.timer);
@@ -97,15 +88,13 @@ class StreamChannel {
// Disconnect if no subscribers
if (this.subscribers.size === 0) {
- DevLogger.log('StreamChannel: No subscribers left, disconnecting');
this.disconnect();
}
};
}
protected connect() {
- // Template method for establishing WebSocket connections
- // Each stream type overrides this with specific subscription logic
+ // Override in subclasses
}
protected disconnect() {
@@ -116,8 +105,7 @@ class StreamChannel {
}
protected getCachedData(): T | null {
- // Template method for retrieving cached data for new subscribers
- // Each stream type overrides this to return their specific cached format
+ // Override in subclasses
return null;
}
}
@@ -130,7 +118,6 @@ class PriceStreamChannel extends StreamChannel> {
protected connect() {
if (this.wsSubscription) {
- DevLogger.log('PriceStream: Already connected');
return;
}
@@ -138,14 +125,9 @@ class PriceStreamChannel extends StreamChannel> {
const allSymbols = Array.from(this.symbols);
if (allSymbols.length === 0) {
- DevLogger.log('PriceStream: No symbols to subscribe to yet');
return;
}
- DevLogger.log('PriceStream: Establishing WebSocket subscription', {
- symbols: allSymbols,
- });
-
this.wsSubscription = Engine.context.PerpsController.subscribeToPrices({
symbols: allSymbols, // Subscribe to specific symbols
callback: (updates: PriceUpdate[]) => {
@@ -173,7 +155,6 @@ class PriceStreamChannel extends StreamChannel> {
this.notifySubscribers(priceMap);
},
});
- DevLogger.log('PriceStream: WebSocket subscription established');
}
protected getCachedData(): Record | null {
@@ -188,7 +169,7 @@ class PriceStreamChannel extends StreamChannel> {
subscribeToSymbols(params: {
symbols: string[];
callback: (prices: Record) => void;
- debounceMs: number;
+ throttleMs?: number;
}): () => void {
// Track new symbols
const newSymbols: string[] = [];
@@ -199,16 +180,8 @@ class PriceStreamChannel extends StreamChannel> {
this.symbols.add(s);
});
- DevLogger.log(
- `PriceStream: Component subscribing to symbols with ${params.debounceMs}ms debounce`,
- { symbols: params.symbols, newSymbols },
- );
-
// If we have new symbols and WebSocket is already connected, we need to reconnect
if (newSymbols.length > 0 && this.wsSubscription) {
- DevLogger.log(
- 'PriceStream: New symbols detected, reconnecting WebSocket',
- );
this.disconnect();
this.connect();
}
@@ -222,51 +195,73 @@ class PriceStreamChannel extends StreamChannel> {
filtered[symbol] = allPrices[symbol];
}
});
- DevLogger.log(
- `PriceStream: Sending filtered prices to component (${params.debounceMs}ms debounce)`,
- {
- symbols: Object.keys(filtered),
- count: Object.keys(filtered).length,
- },
- );
params.callback(filtered);
},
- debounceMs: params.debounceMs,
+ throttleMs: params.throttleMs,
});
}
}
// Specific channel for orders
class OrderStreamChannel extends StreamChannel {
+ private prewarmUnsubscribe?: () => void;
+
protected connect() {
if (this.wsSubscription) return;
- // For now, we'll use polling until WebSocket is available
- // This will be replaced with actual WebSocket subscription
- const controller = Engine.context.PerpsController;
- const pollInterval = setInterval(async () => {
- try {
- const orders = await controller.getOrders();
+ // This calls HyperLiquidSubscriptionService.subscribeToOrders which uses shared webData2
+ this.wsSubscription = Engine.context.PerpsController.subscribeToOrders({
+ callback: (orders: Order[]) => {
this.cache.set('orders', orders);
this.notifySubscribers(orders);
- } catch (error) {
- // Handle error silently
- }
- }, 5000);
-
- this.wsSubscription = () => clearInterval(pollInterval);
+ },
+ });
}
protected getCachedData() {
return this.cache.get('orders') || [];
}
+
+ /**
+ * Pre-warm the channel by creating a persistent subscription
+ * This keeps the WebSocket connection alive and caches data continuously
+ * @returns Cleanup function to call when leaving Perps environment
+ */
+ public prewarm(): () => void {
+ if (this.prewarmUnsubscribe) {
+ return this.prewarmUnsubscribe;
+ }
+
+ // Create a real subscription with no-op callback to keep connection alive
+ this.prewarmUnsubscribe = this.subscribe({
+ callback: () => {
+ // No-op callback - just keeps the connection alive for caching
+ },
+ throttleMs: 0, // No throttle for pre-warm
+ });
+
+ return this.prewarmUnsubscribe;
+ }
+
+ /**
+ * Cleanup pre-warm subscription
+ */
+ public cleanupPrewarm(): void {
+ if (this.prewarmUnsubscribe) {
+ this.prewarmUnsubscribe();
+ this.prewarmUnsubscribe = undefined;
+ }
+ }
}
// Specific channel for positions
class PositionStreamChannel extends StreamChannel {
+ private prewarmUnsubscribe?: () => void;
+
protected connect() {
if (this.wsSubscription) return;
+ // This calls HyperLiquidSubscriptionService.subscribeToPositions which uses shared webData2
this.wsSubscription = Engine.context.PerpsController.subscribeToPositions({
callback: (positions: Position[]) => {
this.cache.set('positions', positions);
@@ -278,6 +273,37 @@ class PositionStreamChannel extends StreamChannel {
protected getCachedData() {
return this.cache.get('positions') || [];
}
+
+ /**
+ * Pre-warm the channel by creating a persistent subscription
+ * This keeps the WebSocket connection alive and caches data continuously
+ * @returns Cleanup function to call when leaving Perps environment
+ */
+ public prewarm(): () => void {
+ if (this.prewarmUnsubscribe) {
+ return this.prewarmUnsubscribe;
+ }
+
+ // Create a real subscription with no-op callback to keep connection alive
+ this.prewarmUnsubscribe = this.subscribe({
+ callback: () => {
+ // No-op callback - just keeps the connection alive for caching
+ },
+ throttleMs: 0, // No throttle for pre-warm
+ });
+
+ return this.prewarmUnsubscribe;
+ }
+
+ /**
+ * Cleanup pre-warm subscription
+ */
+ public cleanupPrewarm(): void {
+ if (this.prewarmUnsubscribe) {
+ this.prewarmUnsubscribe();
+ this.prewarmUnsubscribe = undefined;
+ }
+ }
}
// Specific channel for fills
@@ -302,7 +328,7 @@ class FillStreamChannel extends StreamChannel {
}
// Main manager class
-class PerpsStreamManager {
+export class PerpsStreamManager {
public readonly prices = new PriceStreamChannel();
public readonly orders = new OrderStreamChannel();
public readonly positions = new PositionStreamChannel();
@@ -318,13 +344,17 @@ class PerpsStreamManager {
// Singleton instance
const streamManager = new PerpsStreamManager();
+// Export singleton for pre-warming in PerpsConnectionManager
+export const getStreamManagerInstance = () => streamManager;
+
// Context
const PerpsStreamContext = createContext(null);
export const PerpsStreamProvider: React.FC<{
children: React.ReactNode;
-}> = ({ children }) => (
-
+ testStreamManager?: PerpsStreamManager; // Only for testing
+}> = ({ children, testStreamManager }) => (
+
{children}
);
diff --git a/app/components/UI/Perps/providers/__mocks__/PerpsStreamManager.tsx b/app/components/UI/Perps/providers/__mocks__/PerpsStreamManager.tsx
new file mode 100644
index 000000000000..ec41d6e94805
--- /dev/null
+++ b/app/components/UI/Perps/providers/__mocks__/PerpsStreamManager.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+
+// Mock stream manager
+const mockStreamManager = {
+ prices: {
+ subscribeToSymbols: jest.fn(() => jest.fn()),
+ subscribe: jest.fn(() => jest.fn()),
+ },
+ orders: {
+ subscribe: jest.fn(() => jest.fn()),
+ },
+ positions: {
+ subscribe: jest.fn(() => jest.fn()),
+ },
+ fills: {
+ subscribe: jest.fn(() => jest.fn()),
+ },
+};
+
+// Mock provider component
+export const PerpsStreamProvider = ({
+ children,
+}: {
+ children: React.ReactNode;
+}) => <>{children}>;
+
+// Mock hook
+export const usePerpsStream = jest.fn(() => mockStreamManager);
+
+// Export the mock stream manager for test access
+export const getStreamManagerInstance = () => mockStreamManager;
diff --git a/app/components/UI/Perps/services/HyperLiquidSubscriptionService.test.ts b/app/components/UI/Perps/services/HyperLiquidSubscriptionService.test.ts
index 336e486cdb44..768a9b6b80d6 100644
--- a/app/components/UI/Perps/services/HyperLiquidSubscriptionService.test.ts
+++ b/app/components/UI/Perps/services/HyperLiquidSubscriptionService.test.ts
@@ -30,6 +30,22 @@ jest.mock('../utils/hyperLiquidAdapter', () => ({
averagePrice: '50000',
markPrice: '52000',
})),
+ adaptOrderFromSDK: jest.fn((order: any) => ({
+ orderId: order.oid.toString(),
+ symbol: order.coin,
+ side: order.side === 'B' ? 'buy' : 'sell',
+ orderType: 'limit',
+ size: order.sz,
+ originalSize: order.sz,
+ price: order.limitPx || '0',
+ filledSize: '0',
+ remainingSize: order.sz,
+ status: 'open',
+ timestamp: Date.now(),
+ detailedOrderType: order.orderType || 'Limit',
+ isTrigger: false,
+ reduceOnly: false,
+ })),
}));
// Mock DevLogger
@@ -103,7 +119,8 @@ describe('HyperLiquidSubscriptionService', () => {
return Promise.resolve(mockSubscription);
}),
webData2: jest.fn((_params: any, callback: any) => {
- // Simulate position data
+ // Simulate position and order data
+ // First callback immediately
setTimeout(() => {
callback({
clearinghouseState: {
@@ -114,8 +131,51 @@ describe('HyperLiquidSubscriptionService', () => {
},
],
},
+ openOrders: [
+ {
+ oid: 12345,
+ coin: 'BTC',
+ side: 'B',
+ sz: '0.5',
+ origSz: '1.0',
+ limitPx: '50000',
+ orderType: 'Limit',
+ timestamp: 1234567890000,
+ isTrigger: false,
+ reduceOnly: false,
+ },
+ ],
});
}, 0);
+
+ // Second callback with changed data to ensure updates are triggered
+ setTimeout(() => {
+ callback({
+ clearinghouseState: {
+ assetPositions: [
+ {
+ position: { szi: '0.2' }, // Changed position size
+ coin: 'BTC',
+ },
+ ],
+ },
+ openOrders: [
+ {
+ oid: 12346, // Changed order ID
+ coin: 'BTC',
+ side: 'S',
+ sz: '0.3',
+ origSz: '0.5',
+ limitPx: '51000',
+ orderType: 'Limit',
+ timestamp: 1234567890001,
+ isTrigger: false,
+ reduceOnly: false,
+ },
+ ],
+ });
+ }, 10);
+
return Promise.resolve(mockSubscription);
}),
userFills: jest.fn((_params: any, callback: any) => {
@@ -438,6 +498,145 @@ describe('HyperLiquidSubscriptionService', () => {
});
});
+ describe('Shared WebData2 Subscription', () => {
+ it('should share webData2 subscription between positions and orders', async () => {
+ const positionCallback = jest.fn();
+ const orderCallback = jest.fn();
+
+ // Mock getUserAddressWithDefault to return immediately
+ mockWalletService.getUserAddressWithDefault.mockResolvedValue(
+ '0x123' as Hex,
+ );
+
+ // Subscribe to positions first
+ const unsubscribePositions = service.subscribeToPositions({
+ callback: positionCallback,
+ });
+
+ // Wait for subscription to be established and initial callback
+ // This will trigger the first webData2 callback which caches both positions and orders
+ await new Promise((resolve) => setTimeout(resolve, 20));
+
+ // Verify position callback was called
+ expect(positionCallback).toHaveBeenCalled();
+
+ // Subscribe to orders - should reuse same webData2 subscription
+ // and immediately get cached data
+ const unsubscribeOrders = service.subscribeToOrders({
+ callback: orderCallback,
+ });
+
+ // Orders should get cached data immediately (synchronously)
+ // or after the second webData2 update with changed data
+ await new Promise((resolve) => setTimeout(resolve, 20));
+
+ // Should only call webData2 once for shared subscription
+ expect(mockSubscriptionClient.webData2).toHaveBeenCalledTimes(1);
+
+ // Both callbacks should be called with their respective data
+ expect(positionCallback).toHaveBeenCalled();
+ expect(orderCallback).toHaveBeenCalled();
+
+ // Cleanup
+ unsubscribePositions();
+ unsubscribeOrders();
+ });
+
+ it('should maintain subscription when one subscriber unsubscribes', async () => {
+ const positionCallback1 = jest.fn();
+ const positionCallback2 = jest.fn();
+
+ // Subscribe two position callbacks
+ const unsubscribe1 = service.subscribeToPositions({
+ callback: positionCallback1,
+ });
+
+ const unsubscribe2 = service.subscribeToPositions({
+ callback: positionCallback2,
+ });
+
+ await new Promise((resolve) => setTimeout(resolve, 10));
+
+ // Unsubscribe first callback
+ unsubscribe1();
+
+ // Second callback should still receive updates
+ mockSubscriptionClient.webData2.mock.calls[0][1]({
+ clearinghouseState: {
+ assetPositions: [
+ {
+ position: { coin: 'BTC', szi: '1.0' },
+ },
+ ],
+ },
+ openOrders: [],
+ });
+
+ expect(positionCallback2).toHaveBeenCalled();
+
+ unsubscribe2();
+ });
+
+ it('should cache positions and orders data', async () => {
+ const positionCallback = jest.fn();
+
+ // Setup webData2 mock to call callback with data
+ mockSubscriptionClient.webData2.mockImplementation(
+ (_addr: any, callback: any) => {
+ setTimeout(() => {
+ callback({
+ clearinghouseState: {
+ assetPositions: [
+ {
+ position: { szi: '1.0' },
+ coin: 'BTC',
+ },
+ ],
+ },
+ openOrders: [
+ {
+ oid: 123,
+ coin: 'BTC',
+ side: 'B',
+ sz: '0.5',
+ origSz: '0.5',
+ limitPx: '50000',
+ orderType: 'Limit',
+ timestamp: Date.now(),
+ isTrigger: false,
+ reduceOnly: false,
+ },
+ ],
+ });
+ }, 0);
+ return Promise.resolve({
+ unsubscribe: jest.fn().mockResolvedValue(undefined),
+ });
+ },
+ );
+
+ const unsubscribe = service.subscribeToPositions({
+ callback: positionCallback,
+ });
+
+ await new Promise((resolve) => setTimeout(resolve, 10));
+
+ // Should receive cached data on new subscription
+ const newCallback = jest.fn();
+ const unsubscribe2 = service.subscribeToPositions({
+ callback: newCallback,
+ });
+
+ // New subscriber should get cached data immediately
+ expect(newCallback).toHaveBeenCalledWith(
+ expect.arrayContaining([expect.objectContaining({ coin: 'BTC' })]),
+ );
+
+ unsubscribe();
+ unsubscribe2();
+ });
+ });
+
describe('Subscription Lifecycle', () => {
it('should unsubscribe from position updates successfully', async () => {
const mockCallback = jest.fn();
@@ -741,8 +940,9 @@ describe('HyperLiquidSubscriptionService', () => {
setTimeout(() => {
callback({
clearinghouseState: {
- // No assetPositions
+ assetPositions: [], // Empty array instead of undefined
},
+ openOrders: [], // Also need openOrders array
});
}, 0);
return Promise.resolve({
diff --git a/app/components/UI/Perps/services/HyperLiquidSubscriptionService.ts b/app/components/UI/Perps/services/HyperLiquidSubscriptionService.ts
index b2393615b4f9..cec3f6785963 100644
--- a/app/components/UI/Perps/services/HyperLiquidSubscriptionService.ts
+++ b/app/components/UI/Perps/services/HyperLiquidSubscriptionService.ts
@@ -20,13 +20,19 @@ import type {
PriceUpdate,
Position,
OrderFill,
+ Order,
SubscribePricesParams,
SubscribePositionsParams,
SubscribeOrderFillsParams,
+ SubscribeOrdersParams,
} from '../controllers/types';
-import { adaptPositionFromSDK } from '../utils/hyperLiquidAdapter';
+import {
+ adaptPositionFromSDK,
+ adaptOrderFromSDK,
+} from '../utils/hyperLiquidAdapter';
import type { HyperLiquidClientService } from './HyperLiquidClientService';
import type { HyperLiquidWalletService } from './HyperLiquidWalletService';
+import type { CaipAccountId } from '@metamask/utils';
import { strings } from '../../../../../locales/i18n';
/**
@@ -45,6 +51,7 @@ export class HyperLiquidSubscriptionService {
>();
private positionSubscribers = new Set<(positions: Position[]) => void>();
private orderFillSubscribers = new Set<(fills: OrderFill[]) => void>();
+ private orderSubscribers = new Set<(orders: Order[]) => void>();
// Track which subscribers want market data
private marketDataSubscribers = new Map<
@@ -58,6 +65,14 @@ export class HyperLiquidSubscriptionService {
private globalL2BookSubscriptions = new Map();
private symbolSubscriberCounts = new Map();
+ // Shared webData2 subscription for positions and orders
+ private sharedWebData2Subscription?: Subscription;
+ private webData2SubscriptionPromise?: Promise;
+ private positionSubscriberCount = 0;
+ private orderSubscriberCount = 0;
+ private cachedPositions: Position[] = [];
+ private cachedOrders: Order[] = [];
+
// Global price data cache
private cachedPriceData = new Map();
@@ -187,68 +202,214 @@ export class HyperLiquidSubscriptionService {
}
/**
- * Subscribe to live position updates
+ * Ensure shared webData2 subscription is active (singleton pattern)
+ * This subscription provides both positions and orders data
*/
- public subscribeToPositions(params: SubscribePositionsParams): () => void {
- const { callback, accountId } = params;
- const unsubscribe = this.createSubscription(
- this.positionSubscribers,
- callback,
- );
+ private async ensureSharedWebData2Subscription(
+ accountId?: CaipAccountId,
+ ): Promise {
+ // Return existing subscription if active
+ if (this.sharedWebData2Subscription) {
+ return;
+ }
- let subscription: Subscription | undefined;
+ // Return existing promise if subscription is being established
+ if (this.webData2SubscriptionPromise) {
+ return this.webData2SubscriptionPromise;
+ }
+ // Create new subscription promise to prevent race conditions
+ this.webData2SubscriptionPromise =
+ this.createWebData2Subscription(accountId);
+
+ try {
+ await this.webData2SubscriptionPromise;
+ } catch (error) {
+ // Clear promise on error so it can be retried
+ this.webData2SubscriptionPromise = undefined;
+ throw error;
+ }
+ }
+
+ /**
+ * Create the actual webData2 subscription
+ */
+ private async createWebData2Subscription(
+ accountId?: CaipAccountId,
+ ): Promise {
this.clientService.ensureSubscriptionClient(
this.walletService.createWalletAdapter(),
);
const subscriptionClient = this.clientService.getSubscriptionClient();
- if (subscriptionClient) {
- this.walletService
- .getUserAddressWithDefault(accountId)
- .then((userAddress) => {
- if (!subscriptionClient) {
- throw new Error(
- strings('perps.errors.subscriptionClientNotInitialized'),
- );
- }
+ if (!subscriptionClient) {
+ throw new Error(strings('perps.errors.subscriptionClientNotInitialized'));
+ }
- return subscriptionClient.webData2(
- { user: userAddress },
- (data: WsWebData2) => {
- if (data.clearinghouseState.assetPositions) {
- const positions: Position[] =
- data.clearinghouseState.assetPositions
- .filter((assetPos) => assetPos.position.szi !== '0')
- .map((assetPos) => adaptPositionFromSDK(assetPos));
-
- callback(positions);
+ const userAddress = await this.walletService.getUserAddressWithDefault(
+ accountId,
+ );
+
+ return new Promise((resolve, reject) => {
+ subscriptionClient
+ .webData2({ user: userAddress }, (data: WsWebData2) => {
+ // Extract and process positions with TP/SL data
+ const positions = data.clearinghouseState.assetPositions
+ .filter((assetPos) => assetPos.position.szi !== '0')
+ .map((assetPos) => adaptPositionFromSDK(assetPos));
+
+ // Extract TP/SL from openOrders for positions
+ const tpslMap = new Map<
+ string,
+ { takeProfitPrice?: string; stopLossPrice?: string }
+ >();
+
+ // Also extract regular orders for order subscribers
+ const orders: Order[] = [];
+
+ (data.openOrders || []).forEach((order) => {
+ // Process trigger orders for TP/SL
+ if (order.triggerPx) {
+ const coin = order.coin;
+ const position = positions.find((p) => p.coin === coin);
+
+ if (position) {
+ const existing = tpslMap.get(coin) || {};
+ const isLong = parseFloat(position.size) > 0;
+
+ // Determine if it's TP or SL based on order type
+ if (order.orderType?.includes('Take Profit')) {
+ existing.takeProfitPrice = order.triggerPx;
+ } else if (order.orderType?.includes('Stop')) {
+ existing.stopLossPrice = order.triggerPx;
+ } else {
+ // Fallback: determine based on trigger price vs entry price
+ const triggerPrice = parseFloat(order.triggerPx);
+ const entryPrice = parseFloat(position.entryPrice || '0');
+
+ if (isLong) {
+ if (triggerPrice > entryPrice) {
+ existing.takeProfitPrice = order.triggerPx;
+ } else {
+ existing.stopLossPrice = order.triggerPx;
+ }
+ } else if (triggerPrice < entryPrice) {
+ existing.takeProfitPrice = order.triggerPx;
+ } else {
+ existing.stopLossPrice = order.triggerPx;
+ }
+ }
+
+ tpslMap.set(coin, existing);
}
- },
- );
+ }
+
+ // Convert ALL open orders to Order format using adapter
+ // We NO LONGER skip TP/SL orders - they should appear in the orders list too
+ // TP/SL orders are both:
+ // 1. Used to populate position TP/SL fields (done above)
+ // 2. Shown as separate orders in the orders list (done here)
+ const convertedOrder = adaptOrderFromSDK(order);
+ orders.push(convertedOrder);
+ });
+
+ // Merge positions with TP/SL data, ensuring fields are always present
+ const positionsWithTPSL = positions.map((position) => {
+ const tpsl = tpslMap.get(position.coin) || {};
+ return {
+ ...position,
+ takeProfitPrice: tpsl.takeProfitPrice || undefined,
+ stopLossPrice: tpsl.stopLossPrice || undefined,
+ };
+ });
+
+ // Check if positions actually changed
+ const positionsChanged =
+ JSON.stringify(positionsWithTPSL) !==
+ JSON.stringify(this.cachedPositions);
+ const ordersChanged =
+ JSON.stringify(orders) !== JSON.stringify(this.cachedOrders);
+
+ // Only update and notify if data actually changed
+ if (positionsChanged) {
+ this.cachedPositions = positionsWithTPSL;
+ this.positionSubscribers.forEach((callback) => {
+ callback(positionsWithTPSL);
+ });
+ }
+
+ if (ordersChanged) {
+ this.cachedOrders = orders;
+ this.orderSubscribers.forEach((callback) => {
+ callback(orders);
+ });
+ }
})
.then((sub) => {
- subscription = sub;
+ this.sharedWebData2Subscription = sub;
+ DevLogger.log(
+ 'Shared webData2 subscription established (single connection for positions + orders)',
+ );
+ resolve();
})
.catch((error) => {
DevLogger.log(
- strings('perps.errors.failedToSubscribePosition'),
+ 'Failed to establish shared webData2 subscription',
error,
);
+ reject(error instanceof Error ? error : new Error(String(error)));
});
+ });
+ }
+
+ /**
+ * Clean up shared webData2 subscription when no longer needed
+ */
+ private cleanupSharedWebData2Subscription(): void {
+ const totalSubscribers =
+ this.positionSubscriberCount + this.orderSubscriberCount;
+
+ if (totalSubscribers <= 0 && this.sharedWebData2Subscription) {
+ this.sharedWebData2Subscription.unsubscribe().catch((error: Error) => {
+ DevLogger.log('Failed to unsubscribe shared webData2', error);
+ });
+ this.sharedWebData2Subscription = undefined;
+ this.webData2SubscriptionPromise = undefined;
+ this.positionSubscriberCount = 0;
+ this.orderSubscriberCount = 0;
+ this.cachedPositions = [];
+ this.cachedOrders = [];
+ DevLogger.log('Shared webData2 subscription cleaned up');
}
+ }
+
+ /**
+ * Subscribe to live position updates with TP/SL data
+ */
+ public subscribeToPositions(params: SubscribePositionsParams): () => void {
+ const { callback, accountId } = params;
+ const unsubscribe = this.createSubscription(
+ this.positionSubscribers,
+ callback,
+ );
+
+ // Increment position subscriber count
+ this.positionSubscriberCount++;
+
+ // Immediately provide cached data if available
+ if (this.cachedPositions.length > 0) {
+ callback(this.cachedPositions);
+ }
+
+ // Ensure shared subscription is active
+ this.ensureSharedWebData2Subscription(accountId).catch((error) => {
+ DevLogger.log(strings('perps.errors.failedToSubscribePosition'), error);
+ });
return () => {
unsubscribe();
-
- if (subscription) {
- subscription.unsubscribe().catch((error: Error) => {
- DevLogger.log(
- strings('perps.errors.failedToUnsubscribePosition'),
- error,
- );
- });
- }
+ this.positionSubscriberCount--;
+ this.cleanupSharedWebData2Subscription();
};
}
@@ -325,6 +486,37 @@ export class HyperLiquidSubscriptionService {
};
}
+ /**
+ * Subscribe to live order updates
+ * Uses the shared webData2 subscription to avoid duplicate connections
+ */
+ public subscribeToOrders(params: SubscribeOrdersParams): () => void {
+ const { callback, accountId } = params;
+ const unsubscribe = this.createSubscription(
+ this.orderSubscribers,
+ callback,
+ );
+
+ // Increment order subscriber count
+ this.orderSubscriberCount++;
+
+ // Immediately provide cached data if available
+ if (this.cachedOrders.length > 0) {
+ callback(this.cachedOrders);
+ }
+
+ // Ensure shared subscription is active
+ this.ensureSharedWebData2Subscription(accountId).catch((error) => {
+ DevLogger.log(strings('perps.errors.failedToSubscribeOrders'), error);
+ });
+
+ return () => {
+ unsubscribe();
+ this.orderSubscriberCount--;
+ this.cleanupSharedWebData2Subscription();
+ };
+ }
+
/**
* Create subscription with common error handling
*/
@@ -720,6 +912,8 @@ export class HyperLiquidSubscriptionService {
this.globalAllMidsSubscription = undefined;
this.globalActiveAssetSubscriptions.clear();
this.globalL2BookSubscriptions.clear();
+ this.sharedWebData2Subscription = undefined;
+ this.webData2SubscriptionPromise = undefined;
DevLogger.log('HyperLiquid: Subscription service cleared', {
timestamp: new Date().toISOString(),
diff --git a/app/components/UI/Perps/services/PerpsConnectionManager.ts b/app/components/UI/Perps/services/PerpsConnectionManager.ts
index f8fe9d331f8a..fd864f2c71e4 100644
--- a/app/components/UI/Perps/services/PerpsConnectionManager.ts
+++ b/app/components/UI/Perps/services/PerpsConnectionManager.ts
@@ -1,5 +1,6 @@
import { DevLogger } from '../../../../core/SDKConnect/utils/DevLogger';
import Engine from '../../../../core/Engine';
+import { getStreamManagerInstance } from '../providers/PerpsStreamManager';
/**
* Singleton manager for Perps connection state
@@ -13,6 +14,8 @@ class PerpsConnectionManagerClass {
private isInitialized = false;
private connectionRefCount = 0;
private initPromise: Promise | null = null;
+ private hasPreloaded = false;
+ private prewarmCleanups: (() => void)[] = [];
private constructor() {
// Private constructor to enforce singleton pattern
@@ -69,6 +72,9 @@ class PerpsConnectionManagerClass {
this.isConnected = true;
this.isConnecting = false;
DevLogger.log('PerpsConnectionManager: Successfully connected');
+
+ // Pre-load positions and orders subscriptions to populate cache
+ await this.preloadSubscriptions();
} catch (error) {
this.isConnecting = false;
this.isConnected = false;
@@ -98,10 +104,15 @@ class PerpsConnectionManagerClass {
DevLogger.log(
'PerpsConnectionManager: Disconnecting (no more references)',
);
+
+ // Clean up preloaded subscriptions
+ this.cleanupPreloadedSubscriptions();
+
// Reset state before disconnecting to prevent race conditions
this.isConnected = false;
this.isInitialized = false;
this.isConnecting = false;
+ this.hasPreloaded = false; // Reset pre-load flag on disconnect
await Engine.context.PerpsController.disconnect();
} catch (error) {
@@ -111,6 +122,83 @@ class PerpsConnectionManagerClass {
}
}
+ /**
+ * Pre-load critical WebSocket subscriptions to populate cache
+ * This ensures positions and orders are available immediately when components mount
+ * Uses the StreamManager singleton to ensure single WebSocket connections
+ */
+ private async preloadSubscriptions(): Promise {
+ // Only pre-load once per session
+ if (this.hasPreloaded) {
+ DevLogger.log('PerpsConnectionManager: Already pre-loaded, skipping');
+ return;
+ }
+
+ try {
+ DevLogger.log(
+ 'PerpsConnectionManager: Pre-loading WebSocket subscriptions via StreamManager',
+ );
+ this.hasPreloaded = true;
+
+ // Get the singleton StreamManager instance
+ const streamManager = getStreamManagerInstance();
+
+ // Pre-warm the positions and orders channels
+ // This creates persistent subscriptions that keep connections alive
+ // Store cleanup functions to call when leaving Perps
+ const positionCleanup = streamManager.positions.prewarm();
+ const orderCleanup = streamManager.orders.prewarm();
+
+ this.prewarmCleanups.push(positionCleanup, orderCleanup);
+
+ // Give subscriptions a moment to receive initial data
+ await new Promise((resolve) => setTimeout(resolve, 100));
+
+ DevLogger.log(
+ 'PerpsConnectionManager: Pre-loading complete with persistent subscriptions',
+ );
+ } catch (error) {
+ DevLogger.log(
+ 'PerpsConnectionManager: Failed to pre-load subscriptions',
+ error,
+ );
+ // Non-critical error - components will still work with on-demand subscriptions
+ }
+ }
+
+ /**
+ * Clean up pre-loaded subscriptions
+ * Called when leaving the Perps environment
+ */
+ private cleanupPreloadedSubscriptions(): void {
+ if (this.prewarmCleanups.length === 0) {
+ DevLogger.log(
+ 'PerpsConnectionManager: No pre-warm subscriptions to cleanup',
+ );
+ return;
+ }
+
+ DevLogger.log(
+ `PerpsConnectionManager: Cleaning up ${this.prewarmCleanups.length} pre-warm subscriptions`,
+ );
+
+ // Call all cleanup functions
+ this.prewarmCleanups.forEach((cleanup) => {
+ try {
+ cleanup();
+ } catch (error) {
+ DevLogger.log(
+ 'PerpsConnectionManager: Error during pre-warm cleanup',
+ error,
+ );
+ }
+ });
+
+ // Clear the array
+ this.prewarmCleanups = [];
+ DevLogger.log('PerpsConnectionManager: Pre-warm cleanup complete');
+ }
+
getConnectionState() {
return {
isConnected: this.isConnected,
diff --git a/app/components/UI/Perps/utils/hyperLiquidAdapter.test.ts b/app/components/UI/Perps/utils/hyperLiquidAdapter.test.ts
index eca662336516..77183abc8c68 100644
--- a/app/components/UI/Perps/utils/hyperLiquidAdapter.test.ts
+++ b/app/components/UI/Perps/utils/hyperLiquidAdapter.test.ts
@@ -4,6 +4,7 @@
import {
adaptOrderToSDK,
+ adaptOrderFromSDK,
adaptPositionFromSDK,
adaptMarketFromSDK,
adaptAccountStateFromSDK,
@@ -19,6 +20,7 @@ import type {
SpotClearinghouseState,
} from '@deeeed/hyperliquid-node20/esm/src/types/info/accounts';
import type { PerpsUniverse } from '@deeeed/hyperliquid-node20/esm/src/types/info/assets';
+import type { FrontendOrder } from '@deeeed/hyperliquid-node20/esm/src/types/info/orders';
import { SpotBalance } from '@deeeed/hyperliquid-node20';
// Mock the isHexString utility
@@ -125,6 +127,398 @@ describe('hyperLiquidAdapter', () => {
});
});
+ describe('adaptOrderFromSDK', () => {
+ it('should convert basic buy order from SDK', () => {
+ const frontendOrder: FrontendOrder = {
+ oid: 12345,
+ coin: 'BTC',
+ side: 'B',
+ sz: '0.5',
+ origSz: '1.0',
+ limitPx: '50000',
+ orderType: 'Limit',
+ timestamp: 1234567890000,
+ isTrigger: false,
+ reduceOnly: false,
+ triggerCondition: '',
+ triggerPx: '',
+ children: [],
+ isPositionTpsl: false,
+ tif: null,
+ cloid: null,
+ };
+
+ const result = adaptOrderFromSDK(frontendOrder);
+
+ expect(result).toEqual({
+ orderId: '12345',
+ symbol: 'BTC',
+ side: 'buy',
+ orderType: 'limit',
+ size: '0.5',
+ originalSize: '1.0',
+ price: '50000',
+ filledSize: '0.5',
+ remainingSize: '0.5',
+ status: 'open',
+ timestamp: 1234567890000,
+ detailedOrderType: 'Limit',
+ isTrigger: false,
+ reduceOnly: false,
+ });
+ });
+
+ it('should convert sell order from SDK', () => {
+ const frontendOrder: FrontendOrder = {
+ oid: 54321,
+ coin: 'ETH',
+ side: 'A',
+ sz: '2.0',
+ origSz: '2.0',
+ limitPx: '3000',
+ orderType: 'Limit',
+ timestamp: 1234567890000,
+ isTrigger: false,
+ reduceOnly: true,
+ triggerCondition: '',
+ triggerPx: '',
+ children: [],
+ isPositionTpsl: false,
+ tif: null,
+ cloid: null,
+ };
+
+ const result = adaptOrderFromSDK(frontendOrder);
+
+ expect(result).toEqual({
+ orderId: '54321',
+ symbol: 'ETH',
+ side: 'sell',
+ orderType: 'limit',
+ size: '2.0',
+ originalSize: '2.0',
+ price: '3000',
+ filledSize: '0',
+ remainingSize: '2.0',
+ status: 'open',
+ timestamp: 1234567890000,
+ detailedOrderType: 'Limit',
+ isTrigger: false,
+ reduceOnly: true,
+ });
+ });
+
+ it('should handle market order', () => {
+ const frontendOrder: FrontendOrder = {
+ oid: 99999,
+ coin: 'SOL',
+ side: 'B',
+ sz: '10',
+ origSz: '10',
+ orderType: 'Market',
+ timestamp: 1234567890000,
+ isTrigger: false,
+ reduceOnly: false,
+ triggerCondition: '',
+ triggerPx: '',
+ limitPx: '',
+ children: [],
+ isPositionTpsl: false,
+ tif: null,
+ cloid: null,
+ };
+
+ const result = adaptOrderFromSDK(frontendOrder);
+
+ expect(result).toEqual({
+ orderId: '99999',
+ symbol: 'SOL',
+ side: 'buy',
+ orderType: 'market',
+ size: '10',
+ originalSize: '10',
+ price: '0',
+ filledSize: '0',
+ remainingSize: '10',
+ status: 'open',
+ timestamp: 1234567890000,
+ detailedOrderType: 'Market',
+ isTrigger: false,
+ reduceOnly: false,
+ });
+ });
+
+ it('should handle trigger order with triggerPx', () => {
+ const frontendOrder: FrontendOrder = {
+ oid: 11111,
+ coin: 'AVAX',
+ side: 'A',
+ sz: '5',
+ origSz: '5',
+ triggerPx: '25.50',
+ orderType: 'Stop Market',
+ timestamp: 1234567890000,
+ isTrigger: true,
+ reduceOnly: true,
+ triggerCondition: '',
+ limitPx: '',
+ children: [],
+ isPositionTpsl: false,
+ tif: null,
+ cloid: null,
+ };
+
+ const result = adaptOrderFromSDK(frontendOrder);
+
+ expect(result).toEqual({
+ orderId: '11111',
+ symbol: 'AVAX',
+ side: 'sell',
+ orderType: 'market',
+ size: '5',
+ originalSize: '5',
+ price: '25.50',
+ filledSize: '0',
+ remainingSize: '5',
+ status: 'open',
+ timestamp: 1234567890000,
+ detailedOrderType: 'Stop Market',
+ isTrigger: true,
+ reduceOnly: true,
+ });
+ });
+
+ it('should handle order with child orders (TP/SL)', () => {
+ const frontendOrder: FrontendOrder = {
+ oid: 22222,
+ coin: 'UNI',
+ side: 'B',
+ sz: '100',
+ origSz: '100',
+ limitPx: '10',
+ orderType: 'Limit',
+ timestamp: 1234567890000,
+ isTrigger: false,
+ reduceOnly: false,
+ triggerCondition: '',
+ triggerPx: '',
+ isPositionTpsl: false,
+ tif: null,
+ cloid: null,
+ children: [
+ {
+ oid: 22223,
+ coin: 'UNI',
+ side: 'A',
+ sz: '100',
+ origSz: '100',
+ triggerPx: '12',
+ orderType: 'Take Profit Market',
+ timestamp: 1234567890001,
+ isTrigger: true,
+ reduceOnly: true,
+ triggerCondition: '',
+ limitPx: '',
+ children: [],
+ isPositionTpsl: true,
+ tif: null,
+ cloid: null,
+ },
+ {
+ oid: 22224,
+ coin: 'UNI',
+ side: 'A',
+ sz: '100',
+ origSz: '100',
+ triggerPx: '8',
+ orderType: 'Stop Market', // 'Stop Loss' is not a valid OrderType
+ timestamp: 1234567890002,
+ isTrigger: true,
+ reduceOnly: true,
+ triggerCondition: '',
+ limitPx: '',
+ children: [],
+ isPositionTpsl: true,
+ tif: null,
+ cloid: null,
+ },
+ ],
+ };
+
+ const result = adaptOrderFromSDK(frontendOrder);
+
+ expect(result).toEqual({
+ orderId: '22222',
+ symbol: 'UNI',
+ side: 'buy',
+ orderType: 'limit',
+ size: '100',
+ originalSize: '100',
+ price: '10',
+ filledSize: '0',
+ remainingSize: '100',
+ status: 'open',
+ timestamp: 1234567890000,
+ detailedOrderType: 'Limit',
+ isTrigger: false,
+ reduceOnly: false,
+ takeProfitPrice: '12',
+ stopLossPrice: '8',
+ });
+ });
+
+ it('should handle partially filled order', () => {
+ const frontendOrder: FrontendOrder = {
+ oid: 33333,
+ coin: 'LINK',
+ side: 'B',
+ sz: '30',
+ origSz: '100',
+ limitPx: '15',
+ orderType: 'Limit',
+ timestamp: 1234567890000,
+ isTrigger: false,
+ reduceOnly: false,
+ triggerCondition: '',
+ triggerPx: '',
+ children: [],
+ isPositionTpsl: false,
+ tif: null,
+ cloid: null,
+ };
+
+ const result = adaptOrderFromSDK(frontendOrder);
+
+ expect(result).toEqual({
+ orderId: '33333',
+ symbol: 'LINK',
+ side: 'buy',
+ orderType: 'limit',
+ size: '30',
+ originalSize: '100',
+ price: '15',
+ filledSize: '70', // 100 - 30
+ remainingSize: '30',
+ status: 'open',
+ timestamp: 1234567890000,
+ detailedOrderType: 'Limit',
+ isTrigger: false,
+ reduceOnly: false,
+ });
+ });
+
+ it('should handle order without origSz', () => {
+ const frontendOrder: FrontendOrder = {
+ oid: 44444,
+ coin: 'MATIC',
+ side: 'A',
+ sz: '500',
+ limitPx: '1.2',
+ orderType: 'Limit',
+ timestamp: 1234567890000,
+ isTrigger: false,
+ reduceOnly: false,
+ origSz: '500',
+ triggerCondition: '',
+ triggerPx: '',
+ children: [],
+ isPositionTpsl: false,
+ tif: null,
+ cloid: null,
+ };
+
+ const result = adaptOrderFromSDK(frontendOrder);
+
+ expect(result).toEqual({
+ orderId: '44444',
+ symbol: 'MATIC',
+ side: 'sell',
+ orderType: 'limit',
+ size: '500',
+ originalSize: '500', // Uses sz as default
+ price: '1.2',
+ filledSize: '0',
+ remainingSize: '500',
+ status: 'open',
+ timestamp: 1234567890000,
+ detailedOrderType: 'Limit',
+ isTrigger: false,
+ reduceOnly: false,
+ });
+ });
+
+ it('should determine order type from orderType string', () => {
+ const frontendOrder: FrontendOrder = {
+ oid: 55555,
+ coin: 'DOT',
+ side: 'B',
+ sz: '20',
+ origSz: '20',
+ orderType: 'Limit', // 'Limit at 5.5' is not a valid OrderType
+ limitPx: '5.5',
+ timestamp: 1234567890000,
+ isTrigger: false,
+ reduceOnly: false,
+ triggerCondition: '',
+ triggerPx: '',
+ children: [],
+ isPositionTpsl: false,
+ tif: null,
+ cloid: null,
+ };
+
+ const result = adaptOrderFromSDK(frontendOrder);
+
+ expect(result.orderType).toBe('limit');
+ expect(result.price).toBe('5.5');
+ });
+
+ it('should handle child order with limitPx instead of triggerPx', () => {
+ const frontendOrder: FrontendOrder = {
+ oid: 66666,
+ coin: 'ADA',
+ side: 'B',
+ sz: '1000',
+ origSz: '1000',
+ limitPx: '0.5',
+ orderType: 'Limit',
+ timestamp: 1234567890000,
+ isTrigger: false,
+ reduceOnly: false,
+ triggerCondition: '',
+ triggerPx: '',
+ isPositionTpsl: false,
+ tif: null,
+ cloid: null,
+ children: [
+ {
+ oid: 66667,
+ coin: 'ADA',
+ side: 'A',
+ sz: '1000',
+ origSz: '1000',
+ limitPx: '0.6', // Using limitPx instead of triggerPx
+ orderType: 'Take Profit Limit',
+ timestamp: 1234567890001,
+ isTrigger: true,
+ reduceOnly: true,
+ triggerCondition: '',
+ triggerPx: '',
+ children: [],
+ isPositionTpsl: true,
+ tif: null,
+ cloid: null,
+ },
+ ],
+ };
+
+ const result = adaptOrderFromSDK(frontendOrder);
+
+ expect(result.takeProfitPrice).toBe('0.6');
+ expect(result.stopLossPrice).toBeUndefined();
+ });
+ });
+
describe('adaptPositionFromSDK', () => {
it('should convert asset position correctly', () => {
const assetPosition: AssetPosition = {
diff --git a/app/components/UI/Perps/utils/hyperLiquidAdapter.ts b/app/components/UI/Perps/utils/hyperLiquidAdapter.ts
index 7dfca72f8045..01272dc3066d 100644
--- a/app/components/UI/Perps/utils/hyperLiquidAdapter.ts
+++ b/app/components/UI/Perps/utils/hyperLiquidAdapter.ts
@@ -5,10 +5,12 @@ import type {
SpotClearinghouseState,
} from '@deeeed/hyperliquid-node20/esm/src/types/info/accounts';
import type { PerpsUniverse } from '@deeeed/hyperliquid-node20/esm/src/types/info/assets';
+import type { FrontendOrder } from '@deeeed/hyperliquid-node20/esm/src/types/info/orders';
import { isHexString } from '@metamask/utils';
import type {
AccountState,
MarketInfo,
+ Order,
OrderParams as PerpsOrderParams,
Position,
} from '../controllers/types';
@@ -84,6 +86,90 @@ export function adaptPositionFromSDK(assetPosition: AssetPosition): Position {
};
}
+/**
+ * Transform HyperLiquid SDK order to MetaMask Perps API format
+ * Handles both REST API responses (FrontendOrder) and WebSocket data formats
+ * @param rawOrder - Raw order data from HyperLiquid SDK (frontendOpenOrders or webData2)
+ * @returns MetaMask Perps API order object
+ */
+export function adaptOrderFromSDK(rawOrder: FrontendOrder): Order {
+ // Extract basic fields with appropriate conversions
+ const orderId = rawOrder.oid.toString();
+ const symbol = rawOrder.coin;
+
+ // Convert side: HyperLiquid uses 'B' for Buy and 'A' for Ask (Sell)
+ const side: 'buy' | 'sell' = rawOrder.side === 'B' ? 'buy' : 'sell';
+
+ // Get detailed order type from API
+ const detailedOrderType = rawOrder.orderType;
+
+ // Determine if this is a trigger order (TP/SL)
+ const isTrigger = rawOrder.isTrigger;
+ const reduceOnly = rawOrder.reduceOnly;
+
+ // Determine basic order type
+ let orderType: 'limit' | 'market' = 'market';
+ if (detailedOrderType.toLowerCase().includes('limit') || rawOrder.limitPx) {
+ orderType = 'limit';
+ }
+
+ // For trigger orders (TP/SL), use triggerPx as the price
+ const price = rawOrder.limitPx || rawOrder.triggerPx || '0';
+
+ // Sizes
+ const size = rawOrder.sz;
+ const originalSize = rawOrder.origSz || size;
+
+ // Calculate filled and remaining size
+ const currentSize = parseFloat(size);
+ const origSize = parseFloat(originalSize);
+ const filledSize = origSize - currentSize;
+
+ // Check for TP/SL in child orders (REST API feature)
+ let takeProfitPrice: string | undefined;
+ let stopLossPrice: string | undefined;
+
+ if (rawOrder.children && rawOrder.children.length > 0) {
+ rawOrder.children.forEach((child) => {
+ if (child.isTrigger && child.orderType) {
+ if (child.orderType.includes('Take Profit')) {
+ takeProfitPrice = child.triggerPx || child.limitPx;
+ } else if (child.orderType.includes('Stop')) {
+ stopLossPrice = child.triggerPx || child.limitPx;
+ }
+ }
+ });
+ }
+
+ // Build the order object
+ const order: Order = {
+ orderId,
+ symbol,
+ side,
+ orderType,
+ size,
+ originalSize,
+ price,
+ filledSize: filledSize.toString(),
+ remainingSize: size,
+ status: 'open' as const, // All orders from frontendOpenOrders/webData2 are open
+ timestamp: rawOrder.timestamp,
+ detailedOrderType,
+ isTrigger,
+ reduceOnly,
+ };
+
+ // Add optional fields if they exist
+ if (takeProfitPrice) {
+ order.takeProfitPrice = takeProfitPrice;
+ }
+ if (stopLossPrice) {
+ order.stopLossPrice = stopLossPrice;
+ }
+
+ return order;
+}
+
/**
* Transform SDK market info to MetaMask Perps API format
* @param sdkMarket - Market metadata from HyperLiquid SDK
diff --git a/app/components/UI/Perps/utils/marketDataTransform.test.ts b/app/components/UI/Perps/utils/marketDataTransform.test.ts
index 2c1d04c31a70..c93c82a8f10d 100644
--- a/app/components/UI/Perps/utils/marketDataTransform.test.ts
+++ b/app/components/UI/Perps/utils/marketDataTransform.test.ts
@@ -10,7 +10,11 @@ import {
formatVolume,
HyperLiquidMarketData,
} from './marketDataTransform';
-import { AllMids, PerpsAssetCtx } from '@deeeed/hyperliquid-node20';
+import {
+ AllMids,
+ PerpsAssetCtx,
+ PredictedFunding,
+} from '@deeeed/hyperliquid-node20';
// Helper function to create mock asset context with all required properties
const createMockAssetCtx = (overrides: Record = {}) => ({
@@ -200,6 +204,97 @@ describe('marketDataTransform', () => {
expect(result[0].volume).toBe('$1B'); // Has context
expect(result[1].volume).toBe('$0'); // No context
});
+
+ it('handles predicted funding data correctly', () => {
+ // Arrange
+ const hyperLiquidData: HyperLiquidMarketData = {
+ universe: [mockUniverseAsset],
+ assetCtxs: [mockAssetCtx],
+ allMids: mockAllMids,
+ predictedFundings: [
+ [
+ 'BTC',
+ [
+ [
+ 'HyperLiquid',
+ {
+ fundingRate: '0.001',
+ nextFundingTime: 1234567890000,
+ fundingIntervalHours: 8,
+ },
+ ],
+ ],
+ ],
+ ],
+ };
+
+ // Act
+ const result = transformMarketData(hyperLiquidData);
+
+ // Assert
+ expect(result[0].nextFundingTime).toBe(1234567890000);
+ expect(result[0].fundingIntervalHours).toBe(8);
+ });
+
+ it('handles malformed funding data without crashing', () => {
+ // Arrange - Test various edge cases that could cause destructuring errors
+ const testCases = [
+ // Case 1: fundingData[1][0] is not an array
+ {
+ predictedFundings: [['BTC', ['not-an-array']]],
+ },
+ // Case 2: fundingData[1][0] is an array with less than 2 elements
+ {
+ predictedFundings: [['BTC', [['HyperLiquid']]]],
+ },
+ // Case 3: fundingData[1][0] is null
+ {
+ predictedFundings: [['BTC', [null]]],
+ },
+ // Case 4: fundingData[1] is empty array
+ {
+ predictedFundings: [['BTC', []]],
+ },
+ // Case 5: fundingData[1] is undefined
+ {
+ predictedFundings: [['BTC', undefined]],
+ },
+ ];
+
+ testCases.forEach((testCase) => {
+ const hyperLiquidData: HyperLiquidMarketData = {
+ universe: [mockUniverseAsset],
+ assetCtxs: [mockAssetCtx],
+ allMids: mockAllMids,
+ predictedFundings: testCase.predictedFundings as PredictedFunding[],
+ };
+
+ // Act & Assert - should not throw
+ expect(() => {
+ const result = transformMarketData(hyperLiquidData);
+ // Should return result without funding data
+ expect(result[0].nextFundingTime).toBeUndefined();
+ expect(result[0].fundingIntervalHours).toBeUndefined();
+ }).not.toThrow();
+ });
+ });
+
+ it('handles missing predicted funding gracefully', () => {
+ // Arrange
+ const hyperLiquidData: HyperLiquidMarketData = {
+ universe: [mockUniverseAsset],
+ assetCtxs: [mockAssetCtx],
+ allMids: mockAllMids,
+ // No predictedFundings field
+ };
+
+ // Act
+ const result = transformMarketData(hyperLiquidData);
+
+ // Assert
+ expect(result[0].nextFundingTime).toBeUndefined();
+ expect(result[0].fundingIntervalHours).toBeUndefined();
+ });
});
describe('formatPrice', () => {
diff --git a/app/components/UI/Perps/utils/marketDataTransform.ts b/app/components/UI/Perps/utils/marketDataTransform.ts
index d8d90d2a77be..9a7585d13f15 100644
--- a/app/components/UI/Perps/utils/marketDataTransform.ts
+++ b/app/components/UI/Perps/utils/marketDataTransform.ts
@@ -3,6 +3,7 @@ import type {
PerpsUniverse,
PerpsAssetCtx,
AllMids,
+ PredictedFunding,
} from '@deeeed/hyperliquid-node20';
/**
@@ -12,6 +13,7 @@ export interface HyperLiquidMarketData {
universe: PerpsUniverse[];
assetCtxs: PerpsAssetCtx[];
allMids: AllMids;
+ predictedFundings?: PredictedFunding[];
}
/**
@@ -22,7 +24,7 @@ export interface HyperLiquidMarketData {
export function transformMarketData(
hyperLiquidData: HyperLiquidMarketData,
): PerpsMarketData[] {
- const { universe, assetCtxs, allMids } = hyperLiquidData;
+ const { universe, assetCtxs, allMids, predictedFundings } = hyperLiquidData;
return universe.map((asset) => {
const symbol = asset.name;
@@ -57,6 +59,30 @@ export function transformMarketData(
// Format volume (dayNtlVlm is daily notional volume)
const volume = assetCtx ? parseFloat(assetCtx.dayNtlVlm) : 0;
+ // Extract funding time data if available
+ let nextFundingTime: number | undefined;
+ let fundingIntervalHours: number | undefined;
+
+ if (predictedFundings) {
+ // Find the funding data for this specific symbol
+ const fundingData = predictedFundings.find(
+ ([assetSymbol]) => assetSymbol === symbol,
+ );
+ // eslint-disable-next-line @typescript-eslint/prefer-optional-chain
+ if (fundingData && fundingData[1] && fundingData[1].length > 0) {
+ // Get the first exchange's funding data (usually HyperLiquid itself)
+ // Safely check if the first element is an array with at least 2 elements
+ const firstExchange = fundingData[1][0];
+ if (Array.isArray(firstExchange) && firstExchange.length >= 2) {
+ const exchangeData = firstExchange[1];
+ if (exchangeData) {
+ nextFundingTime = exchangeData.nextFundingTime;
+ fundingIntervalHours = exchangeData.fundingIntervalHours;
+ }
+ }
+ }
+ }
+
return {
symbol,
name: symbol, // HyperLiquid uses symbol as name
@@ -67,6 +93,8 @@ export function transformMarketData(
? '0.00%'
: formatPercentage(change24hPercent),
volume: isNaN(volume) ? '$0' : formatVolume(volume),
+ nextFundingTime,
+ fundingIntervalHours,
};
});
}
diff --git a/app/components/UI/Perps/utils/marketUtils.test.ts b/app/components/UI/Perps/utils/marketUtils.test.ts
index 89dd11cf2ea7..b4c39eefd9e2 100644
--- a/app/components/UI/Perps/utils/marketUtils.test.ts
+++ b/app/components/UI/Perps/utils/marketUtils.test.ts
@@ -65,6 +65,91 @@ describe('marketUtils', () => {
const result = calculateFundingCountdown();
expect(result).toBe('00:01:05'); // 1 minute 5 seconds until 8:00
});
+
+ it('should use specific next funding time when provided', () => {
+ const mockDate = new Date('2024-01-01T12:00:00.000Z');
+ jest.setSystemTime(mockDate);
+
+ // Next funding time is in 2 hours 30 minutes
+ const nextFundingTime = mockDate.getTime() + (2 * 60 + 30) * 60 * 1000;
+
+ const result = calculateFundingCountdown({ nextFundingTime });
+ expect(result).toBe('02:30:00');
+ });
+
+ it('should use specific next funding time with seconds', () => {
+ const mockDate = new Date('2024-01-01T12:00:00.000Z');
+ jest.setSystemTime(mockDate);
+
+ // Next funding time is in 1 hour 15 minutes 45 seconds
+ const nextFundingTime =
+ mockDate.getTime() + (1 * 60 * 60 + 15 * 60 + 45) * 1000;
+
+ const result = calculateFundingCountdown({ nextFundingTime });
+ expect(result).toBe('01:15:45');
+ });
+
+ it('should handle expired specific funding time', () => {
+ const mockDate = new Date('2024-01-01T12:00:00.000Z');
+ jest.setSystemTime(mockDate);
+
+ // Next funding time is in the past
+ const nextFundingTime = mockDate.getTime() - 1000;
+
+ const result = calculateFundingCountdown({ nextFundingTime });
+ // Falls back to default calculation when specific time is expired
+ expect(result).toBe('04:00:00'); // 4 hours until 16:00
+ });
+
+ it('should handle edge case at 59 seconds', () => {
+ // Set time to 07:59:01 UTC (59 seconds before funding)
+ const mockDate = new Date('2024-01-01T07:59:01.000Z');
+ jest.setSystemTime(mockDate);
+
+ const result = calculateFundingCountdown();
+ expect(result).toBe('00:00:59');
+ });
+
+ it('should handle edge case with 60 seconds exactly', () => {
+ // Set time to 07:59:00 UTC (60 seconds before funding)
+ const mockDate = new Date('2024-01-01T07:59:00.000Z');
+ jest.setSystemTime(mockDate);
+
+ const result = calculateFundingCountdown();
+ expect(result).toBe('00:01:00');
+ });
+
+ it('should handle different UTC hours correctly', () => {
+ // Test each hour of the day
+ const testCases = [
+ { hour: 0, expected: '08:00:00' }, // 00:00 -> 08:00
+ { hour: 4, expected: '04:00:00' }, // 04:00 -> 08:00
+ { hour: 8, expected: '08:00:00' }, // 08:00 -> 16:00
+ { hour: 12, expected: '04:00:00' }, // 12:00 -> 16:00
+ { hour: 16, expected: '08:00:00' }, // 16:00 -> 00:00 (next day)
+ { hour: 20, expected: '04:00:00' }, // 20:00 -> 00:00 (next day)
+ ];
+
+ testCases.forEach(({ hour, expected }) => {
+ const mockDate = new Date(
+ `2024-01-01T${hour.toString().padStart(2, '0')}:00:00.000Z`,
+ );
+ jest.setSystemTime(mockDate);
+
+ const result = calculateFundingCountdown();
+ expect(result).toBe(expected);
+ });
+ });
+
+ it('should handle time with custom funding interval', () => {
+ const mockDate = new Date('2024-01-01T10:00:00.000Z');
+ jest.setSystemTime(mockDate);
+
+ // Custom 4 hour funding interval (not used in default calculation)
+ const result = calculateFundingCountdown({ fundingIntervalHours: 4 });
+ // Still uses default calculation when no nextFundingTime provided
+ expect(result).toBe('06:00:00'); // 6 hours until 16:00
+ });
});
describe('calculate24hHighLow', () => {
diff --git a/app/components/UI/Perps/utils/marketUtils.ts b/app/components/UI/Perps/utils/marketUtils.ts
index 4dbaa08a49df..f78762016b03 100644
--- a/app/components/UI/Perps/utils/marketUtils.ts
+++ b/app/components/UI/Perps/utils/marketUtils.ts
@@ -1,11 +1,46 @@
import type { CandleData, CandleStick } from '../types';
+interface FundingCountdownParams {
+ /**
+ * Next funding time in milliseconds since epoch (optional, market-specific)
+ */
+ nextFundingTime?: number;
+ /**
+ * Funding interval in hours (optional, market-specific)
+ * Default is 8 hours for HyperLiquid
+ */
+ fundingIntervalHours?: number;
+}
+
/**
* Calculate the time until the next funding period
- * HyperLiquid has 8-hour funding periods at 00:00, 08:00, and 16:00 UTC
+ * Supports market-specific funding times when provided
+ * Falls back to default HyperLiquid 8-hour periods at 00:00, 08:00, and 16:00 UTC
*/
-export const calculateFundingCountdown = (): string => {
+export const calculateFundingCountdown = (
+ params?: FundingCountdownParams,
+): string => {
const now = new Date();
+ const nowMs = now.getTime();
+
+ // If we have a specific next funding time, use it
+ if (params?.nextFundingTime && params.nextFundingTime > nowMs) {
+ const msUntilFunding = params.nextFundingTime - nowMs;
+ const totalSeconds = Math.floor(msUntilFunding / 1000);
+
+ const hours = Math.floor(totalSeconds / 3600);
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
+ const seconds = totalSeconds % 60;
+
+ // Format as HH:MM:SS
+ const formattedHours = String(hours).padStart(2, '0');
+ const formattedMinutes = String(minutes).padStart(2, '0');
+ const formattedSeconds = String(seconds).padStart(2, '0');
+
+ return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`;
+ }
+
+ // Fall back to default calculation for HyperLiquid (8-hour periods)
const utcHour = now.getUTCHours();
const utcMinutes = now.getUTCMinutes();
const utcSeconds = now.getUTCSeconds();
diff --git a/e2e/api-mocking/default-mocks.js b/e2e/api-mocking/default-mocks.js
new file mode 100644
index 000000000000..6a58515d0c59
--- /dev/null
+++ b/e2e/api-mocking/default-mocks.js
@@ -0,0 +1,380 @@
+/**
+ * Default mock responses for common MetaMask mobile endpoints
+ * These are used as fallbacks when no specific mock is provided
+ */
+
+import { getAuthMocks } from './mock-responses/auth-mocks';
+import { SWAPS_FEATURE_FLAG_RESPONSE } from './mock-responses/feature-flags-mocks';
+import {
+ ACCOUNTS_API_ACTIVE_NETWORKS_RESPONSE,
+ ACCOUNTS_API_TRANSACTIONS_RESPONSE,
+ ACTIVE_NETWORKS_RESPONSE,
+} from './mock-responses/accounts-api-responses';
+import {
+ POOLED_STAKING_VAULT_RESPONSE,
+ STAKING_API_LENDING_RESPONSE,
+} from './mock-responses/staking-api-responses-mocks';
+import { TOKEN_API_TOKENS_RESPONSE } from './mock-responses/token-api-responses';
+
+// Get auth mocks
+const authMocks = getAuthMocks();
+
+export const DEFAULT_MOCKS = {
+ GET: [
+ // Auth mocks
+ ...authMocks.GET,
+ {
+ urlEndpoint:
+ 'https://dapp-scanning.api.cx.metamask.io/v2/scan?url=www.google.com',
+ responseCode: 200,
+ response: {
+ domainName: 'google.com',
+ recommendedAction: 'NONE',
+ },
+ },
+ {
+ urlEndpoint:
+ 'https://dapp-scanning.api.cx.metamask.io/v2/scan?url=google.com',
+ responseCode: 200,
+ response: {
+ domainName: 'google.com',
+ recommendedAction: 'NONE',
+ },
+ },
+ {
+ urlEndpoint:
+ 'https://dapp-scanning.api.cx.metamask.io/v2/scan?url=localhost',
+ responseCode: 200,
+ response: {
+ domainName: 'localhost',
+ recommendedAction: 'NONE',
+ },
+ },
+ {
+ urlEndpoint:
+ 'https://dapp-scanning.api.cx.metamask.io/v2/scan?url=verify.walletconnect.com',
+ responseCode: 200,
+ response: {
+ domainName: 'verify.walletconnect.com',
+ recommendedAction: 'NONE',
+ },
+ },
+ {
+ urlEndpoint:
+ 'https://dapp-scanning.api.cx.metamask.io/v2/scan?url=portfolio.metamask.io',
+ responseCode: 200,
+ response: {
+ domainName: 'portfolio.metamask.io',
+ recommendedAction: 'NONE',
+ },
+ },
+ {
+ urlEndpoint:
+ 'https://min-api.cryptocompare.com/data/pricemulti?fsyms=usd&tsyms=usd',
+ responseCode: 200,
+ response: {
+ USD: {
+ USD: 1,
+ },
+ },
+ },
+ {
+ urlEndpoint:
+ 'https://min-api.cryptocompare.com/data/pricemulti?fsyms=ETH&tsyms=usd',
+ responseCode: 200,
+ response: {
+ ETH: {
+ USD: 3807.92,
+ },
+ },
+ },
+ {
+ urlEndpoint: 'https://security-alerts.api.cx.metamask.io/validate/0x539',
+ responseCode: 200,
+ response: {
+ block: null,
+ result_type: 'Benign',
+ reason: '',
+ description: '',
+ features: [],
+ },
+ },
+ {
+ urlEndpoint:
+ 'https://user-storage.api.cx.metamask.io/api/v1/userstorage/addressBook',
+ responseCode: 200,
+ response: [],
+ },
+ {
+ urlEndpoint:
+ 'https://authentication.api.cx.metamask.io/api/v2/nonce?identifier=0x030b4cfd21a0a0aca69b038e6d268f8eb83a8ea43610aabcd4ff6a19d13e0d10ba',
+ responseCode: 200,
+ response: {
+ nonce: 'gxTzW7WWhXSLlbCg',
+ identifier:
+ '0x030b4cfd21a0a0aca69b038e6d268f8eb83a8ea43610aabcd4ff6a19d13e0d10ba',
+ expires_in: 300,
+ },
+ },
+ {
+ urlEndpoint:
+ 'https://authentication.api.cx.metamask.io/api/v2/nonce?identifier=0x0306d1490b98c04a4265247ef4a4337e84be3430221a2156804bab2387bb1169b8',
+ responseCode: 200,
+ response: {
+ nonce: '9kFZyPPFkud2z4Ug',
+ identifier:
+ '0x0306d1490b98c04a4265247ef4a4337e84be3430221a2156804bab2387bb1169b8',
+ expires_in: 300,
+ },
+ },
+ {
+ urlEndpoint:
+ 'https://api.web3auth.io/fnd-service/node-details?network=sapphire_mainnet&verifier=auth-connection-id&verifierId=user-id&keyType=secp256k1&sigType=ecdsa-secp256k1',
+ responseCode: 200,
+ response: {
+ nodeDetails: {
+ currentEpoch: '1',
+ torusNodeEndpoints: [
+ 'https://node-1.node.web3auth.io/sss/jrpc',
+ 'https://node-2.node.web3auth.io/sss/jrpc',
+ 'https://node-3.node.web3auth.io/sss/jrpc',
+ 'https://node-4.node.web3auth.io/sss/jrpc',
+ 'https://node-5.node.web3auth.io/sss/jrpc',
+ ],
+ torusNodeSSSEndpoints: [
+ 'https://node-1.node.web3auth.io/sss/jrpc',
+ 'https://node-2.node.web3auth.io/sss/jrpc',
+ 'https://node-3.node.web3auth.io/sss/jrpc',
+ 'https://node-4.node.web3auth.io/sss/jrpc',
+ 'https://node-5.node.web3auth.io/sss/jrpc',
+ ],
+ torusNodeRSSEndpoints: [
+ 'https://node-1.node.web3auth.io/rss',
+ 'https://node-2.node.web3auth.io/rss',
+ 'https://node-3.node.web3auth.io/rss',
+ 'https://node-4.node.web3auth.io/rss',
+ 'https://node-5.node.web3auth.io/rss',
+ ],
+ torusNodeTSSEndpoints: [
+ 'https://node-1.node.web3auth.io/tss',
+ 'https://node-2.node.web3auth.io/tss',
+ 'https://node-3.node.web3auth.io/tss',
+ 'https://node-4.node.web3auth.io/tss',
+ 'https://node-5.node.web3auth.io/tss',
+ ],
+ torusIndexes: [1, 2, 3, 4, 5],
+ torusNodePub: [
+ {
+ X: 'e0925898fee0e9e941fdca7ee88deec99939ae9407e923535c4d4a3a3ff8b052',
+ Y: '54b9fea924e3f3e40791f9987f4234ae4222412d65b74068032fa5d8b63375c1',
+ },
+ {
+ X: '9124cf1e280aab32ba50dffd2de81cecabc13d82d2c1fe9de82f3b3523f9b637',
+ Y: 'fca939a1ceb42ce745c55b21ef094f543b457630cb63a94ef4f1afeee2b1f107',
+ },
+ {
+ X: '555f681a63d469cc6c3a58a97e29ebd277425f0e6159708e7c7bf05f18f89476',
+ Y: '606f2bcc0884fa5b64366fc3e8362e4939841b56acd60d5f4553cf36b891ac4e',
+ },
+ {
+ X: '2b5f58d8e340f1ab922e89b3a69a68930edfe51364644a456335e179bc130128',
+ Y: '4b4daa05939426e3cbe7d08f0e773d2bf36f64c00d04620ee6df2a7af4d2247',
+ },
+ {
+ X: '3ecbb6a68afe72cf34ec6c0a12b5cb78a0d2e83ba402983b6adbc5f36219861a',
+ Y: 'dc1031c5cc8f0472bd521a62a64ebca9e163902c247bf05937daf4ae835091e4',
+ },
+ ],
+ },
+ success: true,
+ },
+ },
+ {
+ urlEndpoint: 'https://accounts.api.cx.metamask.io/v1/supportedNetworks',
+ responseCode: 200,
+ response: {
+ fullSupport: [1, 137, 56, 59144, 8453, 10, 42161, 534352, 1329],
+ partialSupport: { balances: [42220, 43114] },
+ },
+ },
+ {
+ urlEndpoint: 'https://swap.dev-api.cx.metamask.io/featureFlags',
+ responseCode: 200,
+ response: SWAPS_FEATURE_FLAG_RESPONSE,
+ },
+ {
+ urlEndpoint: 'https://swap.api.cx.metamask.io/featureFlags',
+ responseCode: 200,
+ response: SWAPS_FEATURE_FLAG_RESPONSE,
+ },
+ {
+ urlEndpoint:
+ 'https://accounts.api.cx.metamask.io/v2/activeNetworks?accountIds=eip155%3A0%3A0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ responseCode: 200,
+ response: ACCOUNTS_API_ACTIVE_NETWORKS_RESPONSE,
+ },
+ {
+ urlEndpoint:
+ 'https://accounts.api.cx.metamask.io/v2/accounts/0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3/balances?networks=1',
+ responseCode: 200,
+ response: {
+ count: 1,
+ balances: [
+ {
+ object: 'token',
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ type: 'native',
+ timestamp: '2015-07-30T15:26:13.000Z',
+ decimals: 18,
+ chainId: 1,
+ balance: '0.000000000000000000',
+ },
+ ],
+ unprocessedNetworks: [],
+ },
+ },
+ {
+ urlEndpoint:
+ 'https://accounts.api.cx.metamask.io/v2/accounts/0xaa4179e7f103701e904d27df223a39aa9c27405a/balances?networks=1%2C59144%2C8453%2C42161%2C56%2C10%2C137',
+ responseCode: 200,
+ response: { count: 0, balances: [], unprocessedNetworks: [] },
+ },
+ {
+ urlEndpoint:
+ 'https://accounts.api.cx.metamask.io/v1/accounts/0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3/transactions?networks=0x1,0x89,0x38,0xe708,0x2105,0xa,0xa4b1,0x82750,0x531&sortDirection=DESC',
+ responseCode: 200,
+ response: ACCOUNTS_API_TRANSACTIONS_RESPONSE,
+ },
+ {
+ urlEndpoint:
+ 'https://staking.api.cx.metamask.io/v1/pooled-staking/vault/1/apys?days=365&order=desc',
+ responseCode: 200,
+ response: POOLED_STAKING_VAULT_RESPONSE,
+ },
+ {
+ urlEndpoint: 'https://staking.api.cx.metamask.io/v1/lending/markets',
+ responseCode: 200,
+ response: STAKING_API_LENDING_RESPONSE,
+ },
+ {
+ urlEndpoint:
+ 'https://staking.api.cx.metamask.io/v1/pooled-staking/eligibility?addresses=0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ responseCode: 200,
+ response: { eligible: true },
+ },
+ {
+ urlEndpoint:
+ 'https://staking.api.cx.metamask.io/v1/lending/positions/0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ responseCode: 200,
+ response: { positions: [] },
+ },
+ {
+ urlEndpoint:
+ 'https://staking.api.cx.metamask.io/v1/pooled-staking/vault/1',
+ responseCode: 200,
+ response: {
+ apy: '2.423922825407589424778761061946903',
+ capacity:
+ '115792089237316195423570985008687907853269984665640564039457584007913129639935',
+ feePercent: 1500,
+ totalAssets: '34582364608391084442226',
+ vaultAddress: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ },
+ },
+ {
+ urlEndpoint:
+ 'https://staking.api.cx.metamask.io/v1/pooled-staking/vault/1/apys/averages',
+ responseCode: 200,
+ response: {
+ oneDay: '2.160630689308144746',
+ oneWeek: '2.42203859587349324429',
+ oneMonth: '2.49056583176788989407',
+ threeMonths: '2.52669759044094515534',
+ sixMonths: '2.65550671135719381941',
+ oneYear: '2.6612714152041561994',
+ },
+ },
+ {
+ urlEndpoint:
+ 'https://staking.api.cx.metamask.io/v1/pooled-staking/stakes/1?accounts=0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ responseCode: 200,
+ response: {
+ accounts: [
+ {
+ account: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ lifetimeRewards: '0',
+ assets: '0',
+ exitRequests: [],
+ },
+ ],
+ exchangeRate: '1.034162108591709262',
+ },
+ },
+ {
+ urlEndpoint: 'https://on-ramp.api.cx.metamask.io/geolocation',
+ responseCode: 200,
+ response: 'US',
+ },
+ {
+ urlEndpoint:
+ 'https://token.api.cx.metamask.io/tokens/1337?occurrenceFloor=3&includeNativeAssets=false&includeTokenFees=false&includeAssetType=false&includeERC20Permit=false&includeStorage=false',
+ responseCode: 200,
+ response: TOKEN_API_TOKENS_RESPONSE,
+ },
+ {
+ urlEndpoint:
+ 'https://defiadapters.api.cx.metamask.io/positions/0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ responseCode: 200,
+ response: {
+ data: [],
+ },
+ },
+ {
+ urlEndpoint:
+ 'https://dweb.link/ipfs/Qmaisz6NMhDB51cCvNWa1GMS7LU1pAxdF4Ld6Ft9kZEP2a#x-ipfs-companion-no-redirect',
+ responseCode: 200,
+ response: 'Hello from IPFS Gateway Checker',
+ },
+ ],
+ POST: [
+ // Auth mocks
+ ...authMocks.POST,
+ {
+ urlEndpoint: 'https://api.segment.io/v1/track',
+ responseCode: 200,
+ response: {
+ success: true,
+ },
+ },
+ {
+ urlEndpoint: 'https://api.mixpanel.com/track',
+ responseCode: 200,
+ response: {
+ status: 1,
+ },
+ },
+ {
+ urlEndpoint: 'https://token-api.metaswap.codefi.network/tokens',
+ responseCode: 200,
+ response: [],
+ },
+ {
+ urlEndpoint: 'https://security-alerts.api.cx.metamask.io/validate',
+ responseCode: 200,
+ response: {
+ flagAsDangerous: 0,
+ },
+ },
+ {
+ urlEndpoint:
+ 'https://pulse.walletconnect.org/batch?projectId=017a80231854c3b1c56df7bb46bba859&st=events_sdk&sv=js-2.19.2&sp=desktop',
+ responseCode: 200,
+ response: {},
+ },
+ ],
+ PUT: [],
+ DELETE: [],
+ PATCH: [],
+};
diff --git a/e2e/api-mocking/mock-e2e-allowlist.js b/e2e/api-mocking/mock-e2e-allowlist.js
index b67cb2a04995..165ecad4d9c6 100644
--- a/e2e/api-mocking/mock-e2e-allowlist.js
+++ b/e2e/api-mocking/mock-e2e-allowlist.js
@@ -2,27 +2,28 @@
// This list is temporary and the goal is to reduce it to 0, meaning all requests are mocked in our e2e tests.
export const ALLOWLISTED_HOSTS = [
- 'localhost',
+ '0.0.0.0',
'127.0.0.1',
+ 'localhost',
'10.0.2.2', // Android emulator host
'api.tenderly.co',
'rpc.tenderly.co',
+ 'virtual.mainnet.rpc.tenderly.co',
];
export const ALLOWLISTED_URLS = [
// Temporarily allow existing live requests during migration
'https://client-config.api.cx.metamask.io/v1/flags?client=mobile&distribution=main&environment=dev',
'https://staking.api.cx.metamask.io/v1/lending/1/markets',
- 'https://staking.api.cx.metamask.io/v1/pooled-staking/stakes/1?accounts=0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3',
- 'https://staking.api.cx.metamask.io/v1/pooled-staking/eligibility?addresses=0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3',
- 'https://staking.api.cx.metamask.io/v1/pooled-staking/vault/1',
- 'https://staking.api.cx.metamask.io/v1/lending/markets',
- 'https://staking.api.cx.metamask.io/v1/pooled-staking/vault/1/apys?days=365&order=desc',
- 'https://staking.api.cx.metamask.io/v1/pooled-staking/vault/1/apys/averages',
- 'https://staking.api.cx.metamask.io/v1/lending/positions/0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3',
- 'https://mainnet.infura.io/v3/8f4bc0ed77aa4a2c886a4d929754f414',
+ 'https://staking.api.cx.metamask.io/v1/lending/positions/CEQ87PmqFPA8cajAXYVrFT2FQobRrAT4Wd53FvfgYrrd',
'https://min-api.cryptocompare.com/data/pricemulti?fsyms=btc%2Csol&tsyms=usd',
'https://pulse.walletconnect.org/batch?projectId=e698cc28a9e75eb175ae3c991ac7eb2a&st=events_sdk&sv=js-2.19.2&sp=desktop',
'https://clients3.google.com/generate_204',
+ 'https://api.avax.network/ext/bc/C/rpc',
'https://security-alerts.api.cx.metamask.io/validate/0x539',
+ 'https://token.api.cx.metamask.io/tokens/1?occurrenceFloor=3&includeNativeAssets=false&includeTokenFees=false&includeAssetType=false&includeERC20Permit=false&includeStorage=false',
+ // this should be fixed in code to remove the double slash before transactions, mock without double slash already in the defaults
+ 'https://accounts.api.cx.metamask.io/v1/accounts/0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3//transactions?networks=0x1,0x89,0x38,0xe708,0x2105,0xa,0xa4b1,0x82750,0x531&sortDirection=DESC',
+ // this should be fixed in code to remove the double slash before balances, mock without double slash already in the default mocks
+ 'https://accounts.api.cx.metamask.io/v2/accounts//balances?networks=1',
];
diff --git a/e2e/api-mocking/mock-responses/accounts-api-responses.ts b/e2e/api-mocking/mock-responses/accounts-api-responses.ts
new file mode 100644
index 000000000000..9f32c9962ed0
--- /dev/null
+++ b/e2e/api-mocking/mock-responses/accounts-api-responses.ts
@@ -0,0 +1,1208 @@
+export const ACCOUNTS_API_TRANSACTIONS_RESPONSE = {
+ data: [
+ {
+ hash: '0x027c29d4b64ae91d6c2cc6f72a5b1885e5ab17ef1b6602452448043e13f6770d',
+ timestamp: '2025-07-26T19:31:31.000Z',
+ chainId: 10,
+ accountId: 'eip155:10:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 138979757,
+ blockHash:
+ '0x2c8759771b01cb297c89dfa7b3cdd816848a9a88fb3bdbc810c7eaf9b3275609',
+ gas: 5000000,
+ gasUsed: 2131592,
+ gasPrice: '1000933',
+ effectiveGasPrice: '1000933',
+ nonce: 3130,
+ cumulativeGasUsed: 7434815,
+ methodId: '0x441ff998',
+ value: '0',
+ to: '0xfd59cd78fa951c750ef5d129390e16807304264a',
+ from: '0x085cc749b6f9334fd94abdc473abefdbc790f057',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x085cc749b6f9334fd94abdc473abefdbc790f057',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: 1,
+ tokenId: '0',
+ contractAddress: '0xfd59cd78fa951c750ef5d129390e16807304264a',
+ transferType: 'erc1155',
+ },
+ ],
+ logs: [],
+ transactionType: 'SPAM_TOKEN_TRANSFER',
+ transactionCategory: 'TRANSFER',
+ readable: 'Spam Token: Transfer',
+ transactionProtocol: 'SPAM_TOKEN',
+ },
+ {
+ hash: '0x33e808a4107cf537b858c67a4b267ddf4e1e08fc6ca0476ee585a7855587b1f5',
+ timestamp: '2025-07-26T19:31:30.000Z',
+ chainId: 137,
+ accountId: 'eip155:137:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 74444312,
+ blockHash:
+ '0x1ce60ca3603baae547a5f47beaf0d67117d462e978e55ef9e28f5aba01927491',
+ gas: 816820,
+ gasUsed: 809230,
+ gasPrice: '28600000085',
+ effectiveGasPrice: '28600000085',
+ nonce: 4557,
+ cumulativeGasUsed: 14287546,
+ methodId: '0xa06c1a33',
+ value: '0',
+ to: '0x5ca2ad27e80a4d1fc814b12afc0478be2b111ae0',
+ from: '0xf9caf760edc7870f0e6dc1fa87b560e447aabc97',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0xe7804c37c13166ff0b37f5ae0bb07a3aebb6e245',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: '1',
+ contractAddress: '0x5ca2ad27e80a4d1fc814b12afc0478be2b111ae0',
+ symbol: 'Swap your Voucher on ethers.bio.link',
+ name: '✅ ETHERS VOUCHER',
+ transferType: 'erc20',
+ },
+ ],
+ logs: [],
+ transactionType: 'SPAM_TOKEN_TRANSFER',
+ transactionCategory: 'TRANSFER',
+ readable: 'Spam Token: Transfer',
+ transactionProtocol: 'SPAM_TOKEN',
+ },
+ {
+ hash: '0x1659dd108b18824828631d4dcb501fdd961d45a0b479b9b4c71b88746c775a3e',
+ timestamp: '2025-07-26T19:31:27.000Z',
+ chainId: 10,
+ accountId: 'eip155:10:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 138979755,
+ blockHash:
+ '0x287f0caa3a603f67b08e2b0fd6cb26b014d209b7fe93a51e9333a43f0efca8d9',
+ gas: 1019124,
+ gasUsed: 1009941,
+ gasPrice: '1211023',
+ effectiveGasPrice: '1211023',
+ nonce: 9041,
+ cumulativeGasUsed: 2275038,
+ methodId: '0xa06c1a33',
+ value: '0',
+ to: '0xd9d2cb4ee2388c1b453f572f9ccbd4419d73c8c8',
+ from: '0x3e26c186419b52d70f77d8ffb8eb2b1615adcd2d',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x3e26c186419b52d70f77d8ffb8eb2b1615adcd2d',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: 1,
+ tokenId: '0',
+ contractAddress: '0xd9d2cb4ee2388c1b453f572f9ccbd4419d73c8c8',
+ transferType: 'erc1155',
+ },
+ ],
+ logs: [],
+ transactionType: 'SPAM_TOKEN_TRANSFER',
+ transactionCategory: 'TRANSFER',
+ readable: 'Spam Token: Transfer',
+ transactionProtocol: 'SPAM_TOKEN',
+ },
+ {
+ hash: '0x5dec791534a92a4a170d193f5d9e77e209c574f36e4de36b47fac8f276a5ac57',
+ timestamp: '2025-07-26T19:31:25.000Z',
+ chainId: 10,
+ accountId: 'eip155:10:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 138979754,
+ blockHash:
+ '0xf2f146ecebe791b7ad372291b0e97df63e10428ff155fd91480f3f593b05622c',
+ gas: 5000000,
+ gasUsed: 2131592,
+ gasPrice: '1000932',
+ effectiveGasPrice: '1000932',
+ nonce: 3129,
+ cumulativeGasUsed: 9392520,
+ methodId: '0x441ff998',
+ value: '0',
+ to: '0xfd59cd78fa951c750ef5d129390e16807304264a',
+ from: '0x085cc749b6f9334fd94abdc473abefdbc790f057',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x085cc749b6f9334fd94abdc473abefdbc790f057',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: 1,
+ tokenId: '0',
+ contractAddress: '0xfd59cd78fa951c750ef5d129390e16807304264a',
+ transferType: 'erc1155',
+ },
+ ],
+ logs: [],
+ transactionType: 'SPAM_TOKEN_TRANSFER',
+ transactionCategory: 'TRANSFER',
+ readable: 'Spam Token: Transfer',
+ transactionProtocol: 'SPAM_TOKEN',
+ },
+ {
+ hash: '0x155b33f5a84c27b1af5d3a712d67f1bec2561d3bd792a1928cc0086e0561cec1',
+ timestamp: '2025-07-26T19:31:21.000Z',
+ chainId: 10,
+ accountId: 'eip155:10:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 138979752,
+ blockHash:
+ '0xb0f384961a8d309c2f376138038dfc759a569d315b5465a7d92fc462aa14fcc0',
+ gas: 5000000,
+ gasUsed: 2131592,
+ gasPrice: '1000929',
+ effectiveGasPrice: '1000929',
+ nonce: 3128,
+ cumulativeGasUsed: 7584613,
+ methodId: '0x441ff998',
+ value: '0',
+ to: '0xfd59cd78fa951c750ef5d129390e16807304264a',
+ from: '0x085cc749b6f9334fd94abdc473abefdbc790f057',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x085cc749b6f9334fd94abdc473abefdbc790f057',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: 1,
+ tokenId: '0',
+ contractAddress: '0xfd59cd78fa951c750ef5d129390e16807304264a',
+ transferType: 'erc1155',
+ },
+ ],
+ logs: [],
+ transactionType: 'SPAM_TOKEN_TRANSFER',
+ transactionCategory: 'TRANSFER',
+ readable: 'Spam Token: Transfer',
+ transactionProtocol: 'SPAM_TOKEN',
+ },
+ {
+ hash: '0x527290a4001ab0dc13338af016c83dbf61c3e200b29d984187aff5149aa1f0fc',
+ timestamp: '2025-07-26T19:31:17.000Z',
+ chainId: 10,
+ accountId: 'eip155:10:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 138979750,
+ blockHash:
+ '0x73d83bb2e7ec4b1da7a6acf13d768aafe75a648fa2149fed204a2e0b87936728',
+ gas: 4699788,
+ gasUsed: 4439587,
+ gasPrice: '1172',
+ effectiveGasPrice: '1172',
+ nonce: 46241,
+ cumulativeGasUsed: 19957046,
+ methodId: '0x729ad39e',
+ value: '0',
+ to: '0xef1cee46176605fe90519981db858d01d1c4745f',
+ from: '0xc070ccd5c2c4d824f0c1cf50675b8ac4feb4e0e2',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0xef1cee46176605fe90519981db858d01d1c4745f',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: 1,
+ tokenId: '0',
+ contractAddress: '0xef1cee46176605fe90519981db858d01d1c4745f',
+ transferType: 'erc1155',
+ },
+ ],
+ logs: [],
+ transactionType: 'SPAM_TOKEN_TRANSFER',
+ transactionCategory: 'TRANSFER',
+ readable: 'Spam Token: Transfer',
+ transactionProtocol: 'SPAM_TOKEN',
+ },
+ {
+ hash: '0xcdd24e1414c8fa3fb6e8148497b96cd476020bd0ee90dfc31aca5126820e8fa5',
+ timestamp: '2025-07-26T19:30:47.000Z',
+ chainId: 1,
+ accountId: 'eip155:1:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 23005418,
+ blockHash:
+ '0x47fe85c31e198367d060512b5bf85b446f46c58a04fb9b7760f6fb97716dc4ca',
+ gas: 200000,
+ gasUsed: 61418,
+ gasPrice: '248473479',
+ effectiveGasPrice: '248473479',
+ nonce: 597,
+ cumulativeGasUsed: 18106189,
+ methodId: '0xa9059cbb',
+ value: '0',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ from: '0x8b4ea4f8a25ed1b7b18044bb1a2bec9425603572',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ to: '0x8b4ea4f8a25ed1b7b18044bb1a2bec9425603572',
+ amount: '120000',
+ decimal: 6,
+ contractAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
+ symbol: 'USDC',
+ name: 'USD Coin',
+ transferType: 'erc20',
+ },
+ ],
+ logs: [],
+ transactionProtocol: 'ERC_20',
+ transactionCategory: 'TRANSFER',
+ transactionType: 'ERC_20_TRANSFER',
+ readable: 'Token: Transfer',
+ },
+ {
+ hash: '0x942a03ff7599f739e00a19c991130a0b4bebfebcfd24b31b5a007e6047272166',
+ timestamp: '2025-07-23T23:54:55.000Z',
+ chainId: 10,
+ accountId: 'eip155:10:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 138858059,
+ blockHash:
+ '0xcb47e8545ec8432a30ef70b659d6de0ac4d48471356a58b3e35d433a7aa09702',
+ gas: 5000000,
+ gasUsed: 2368988,
+ gasPrice: '1028339',
+ effectiveGasPrice: '1028339',
+ nonce: 288,
+ cumulativeGasUsed: 15167965,
+ methodId: '0x441ff998',
+ value: '0',
+ to: '0x74f59dfa8d9e8ef9dfca86c6a5e7e617c26f52ba',
+ from: '0xe2def864f6def93144144b8f7a1d29de46ba7e56',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0xe2def864f6def93144144b8f7a1d29de46ba7e56',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: 1,
+ tokenId: '0',
+ contractAddress: '0x74f59dfa8d9e8ef9dfca86c6a5e7e617c26f52ba',
+ transferType: 'erc1155',
+ },
+ ],
+ logs: [],
+ transactionType: 'SPAM_TOKEN_TRANSFER',
+ transactionCategory: 'TRANSFER',
+ readable: 'Spam Token: Transfer',
+ transactionProtocol: 'SPAM_TOKEN',
+ },
+ {
+ hash: '0x18e6db5a5f2ce5af3f37dbe9d6fb055e03d5e1188f3ad9df8fd934d80e9f7cdd',
+ timestamp: '2025-07-23T23:54:51.000Z',
+ chainId: 10,
+ accountId: 'eip155:10:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 138858057,
+ blockHash:
+ '0xd7959aad3a786095de7ede87a951214281e2f62bc62081114938cba5ce1567d7',
+ gas: 5000000,
+ gasUsed: 2368988,
+ gasPrice: '1028409',
+ effectiveGasPrice: '1028409',
+ nonce: 287,
+ cumulativeGasUsed: 3267144,
+ methodId: '0x441ff998',
+ value: '0',
+ to: '0x74f59dfa8d9e8ef9dfca86c6a5e7e617c26f52ba',
+ from: '0xe2def864f6def93144144b8f7a1d29de46ba7e56',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0xe2def864f6def93144144b8f7a1d29de46ba7e56',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: 1,
+ tokenId: '0',
+ contractAddress: '0x74f59dfa8d9e8ef9dfca86c6a5e7e617c26f52ba',
+ transferType: 'erc1155',
+ },
+ ],
+ logs: [],
+ transactionType: 'SPAM_TOKEN_TRANSFER',
+ transactionCategory: 'TRANSFER',
+ readable: 'Spam Token: Transfer',
+ transactionProtocol: 'SPAM_TOKEN',
+ },
+ {
+ hash: '0xa3d438d71085b354cf2dfd65beef373f616b688f3110d3275201a2e97ceb313d',
+ timestamp: '2025-07-23T23:54:47.000Z',
+ chainId: 10,
+ accountId: 'eip155:10:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 138858055,
+ blockHash:
+ '0x03defe95d5d4b8a57062dd2436e1f23bb2fb9a23fccbf31c0bd23f898d880dc8',
+ gas: 5000000,
+ gasUsed: 2368988,
+ gasPrice: '1028429',
+ effectiveGasPrice: '1028429',
+ nonce: 286,
+ cumulativeGasUsed: 3130984,
+ methodId: '0x441ff998',
+ value: '0',
+ to: '0x74f59dfa8d9e8ef9dfca86c6a5e7e617c26f52ba',
+ from: '0xe2def864f6def93144144b8f7a1d29de46ba7e56',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0xe2def864f6def93144144b8f7a1d29de46ba7e56',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: 1,
+ tokenId: '0',
+ contractAddress: '0x74f59dfa8d9e8ef9dfca86c6a5e7e617c26f52ba',
+ transferType: 'erc1155',
+ },
+ ],
+ logs: [],
+ transactionType: 'SPAM_TOKEN_TRANSFER',
+ transactionCategory: 'TRANSFER',
+ readable: 'Spam Token: Transfer',
+ transactionProtocol: 'SPAM_TOKEN',
+ },
+ {
+ hash: '0x7e337b9135cc9408acdd045b743e34f7d4d13ed53d21728b1ceaf2f4de6d83e1',
+ timestamp: '2025-07-23T23:33:38.000Z',
+ chainId: 137,
+ accountId: 'eip155:137:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 74329442,
+ blockHash:
+ '0x05d4002c3d302306ba0f3b7d5f414fbf950d9615d867f2a405f83ac92aab4949',
+ gas: 499111,
+ gasUsed: 415926,
+ gasPrice: '30000000000',
+ effectiveGasPrice: '30000000000',
+ nonce: 3945,
+ cumulativeGasUsed: 15454145,
+ methodId: '0x729ad39e',
+ value: '0',
+ to: '0xa264885c44a58f20bd0cc27ccec57660d7b98ca1',
+ from: '0x081b6355a6fd09367b68c54be1848bd30e3d8dff',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0xe7804c37c13166ff0b37f5ae0bb07a3aebb6e245',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: '1000000000000000000',
+ decimal: 18,
+ contractAddress: '0xa264885c44a58f20bd0cc27ccec57660d7b98ca1',
+ symbol: 'WWW.ARBQUEST.LIVE VISIT TO SWAP',
+ name: '✅ ARB Voucher',
+ transferType: 'erc20',
+ },
+ ],
+ logs: [],
+ transactionType: 'SPAM_TOKEN_TRANSFER',
+ transactionCategory: 'TRANSFER',
+ readable: 'Spam Token: Transfer',
+ transactionProtocol: 'SPAM_TOKEN',
+ },
+ {
+ hash: '0x767f0dc01b2d7458f849bd82c680b345daf428c3d2e895902aa9021a65ff3e32',
+ timestamp: '2025-07-23T23:30:01.000Z',
+ chainId: 10,
+ accountId: 'eip155:10:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 138857312,
+ blockHash:
+ '0x3ee254f749296105bfb74c794a6949d875000dc07cebe2b801d0cd4738546d15',
+ gas: 8051574,
+ gasUsed: 6655655,
+ gasPrice: '57741',
+ effectiveGasPrice: '57741',
+ nonce: 52069,
+ cumulativeGasUsed: 17895936,
+ methodId: '0x729ad39e',
+ value: '0',
+ to: '0xe07f9d9bbb7e84da658cfa42cbd71f087c5e87f4',
+ from: '0x1ce7f95ec52e748e7afa4bca38edc6fef413ab2f',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0xe07f9d9bbb7e84da658cfa42cbd71f087c5e87f4',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: 1,
+ tokenId: '0',
+ contractAddress: '0xe07f9d9bbb7e84da658cfa42cbd71f087c5e87f4',
+ transferType: 'erc1155',
+ },
+ ],
+ logs: [],
+ transactionType: 'SPAM_TOKEN_TRANSFER',
+ transactionCategory: 'TRANSFER',
+ readable: 'Spam Token: Transfer',
+ transactionProtocol: 'SPAM_TOKEN',
+ },
+ {
+ hash: '0x61b94f12e36e27c0da740073413c855c08e1d64f92e1fe9f111a373bda0e3187',
+ timestamp: '2025-07-23T23:30:01.000Z',
+ chainId: 10,
+ accountId: 'eip155:10:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 138857312,
+ blockHash:
+ '0x3ee254f749296105bfb74c794a6949d875000dc07cebe2b801d0cd4738546d15',
+ gas: 7953562,
+ gasUsed: 6574622,
+ gasPrice: '57553',
+ effectiveGasPrice: '57553',
+ nonce: 42649,
+ cumulativeGasUsed: 24470558,
+ methodId: '0x729ad39e',
+ value: '0',
+ to: '0x2b54f5293b1960de7faabfef6bcf2499a4903861',
+ from: '0x63b1413fe071cc71bdcdfe0cdaa81c40401088f3',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x2b54f5293b1960de7faabfef6bcf2499a4903861',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: 1,
+ tokenId: '0',
+ contractAddress: '0x2b54f5293b1960de7faabfef6bcf2499a4903861',
+ transferType: 'erc1155',
+ },
+ ],
+ logs: [],
+ transactionType: 'SPAM_TOKEN_TRANSFER',
+ transactionCategory: 'TRANSFER',
+ readable: 'Spam Token: Transfer',
+ transactionProtocol: 'SPAM_TOKEN',
+ },
+ {
+ hash: '0x5b6ddb41ede3697c2cfc3b14239ffc443346ec312199e01572acf15e7df00cca',
+ timestamp: '2025-07-23T23:29:53.000Z',
+ chainId: 10,
+ accountId: 'eip155:10:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 138857308,
+ blockHash:
+ '0x8770643b7c0d5067e9e9c5578076fed80f312bf901b4c95016774ef34bfeb48a',
+ gas: 8246529,
+ gasUsed: 7790839,
+ gasPrice: '38535',
+ effectiveGasPrice: '38535',
+ nonce: 50879,
+ cumulativeGasUsed: 13002280,
+ methodId: '0x729ad39e',
+ value: '0',
+ to: '0xe1bf0fda900ed301f370e50a6edcb43580bff2ad',
+ from: '0x059594bb49c2ed5d99f91ada331803982889fdb7',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0xe1bf0fda900ed301f370e50a6edcb43580bff2ad',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: 1,
+ tokenId: '0',
+ contractAddress: '0xe1bf0fda900ed301f370e50a6edcb43580bff2ad',
+ transferType: 'erc1155',
+ },
+ ],
+ logs: [],
+ transactionType: 'SPAM_TOKEN_TRANSFER',
+ transactionCategory: 'TRANSFER',
+ readable: 'Spam Token: Transfer',
+ transactionProtocol: 'SPAM_TOKEN',
+ },
+ {
+ hash: '0x2cdfb79de64a220a717c4e3b4f8f6842afa59d7dafeb62ae050e9be0395fe970',
+ timestamp: '2025-07-23T23:29:47.000Z',
+ chainId: 10,
+ accountId: 'eip155:10:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 138857305,
+ blockHash:
+ '0x8bfad84ae6851d3b96cc9870e76f5df3fdb68870baa234a9998a488d8b3b7e2e',
+ gas: 5329084,
+ gasUsed: 5034198,
+ gasPrice: '38650',
+ effectiveGasPrice: '38650',
+ nonce: 36595,
+ cumulativeGasUsed: 14626722,
+ methodId: '0x729ad39e',
+ value: '0',
+ to: '0x5da41596b45526c15ca2b68375437da5ab2c0243',
+ from: '0xc070ccd5c2c4d824f0c1cf50675b8ac4feb4e0e2',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x5da41596b45526c15ca2b68375437da5ab2c0243',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: 1,
+ tokenId: '0',
+ contractAddress: '0x5da41596b45526c15ca2b68375437da5ab2c0243',
+ transferType: 'erc1155',
+ },
+ ],
+ logs: [],
+ transactionType: 'SPAM_TOKEN_TRANSFER',
+ transactionCategory: 'TRANSFER',
+ readable: 'Spam Token: Transfer',
+ transactionProtocol: 'SPAM_TOKEN',
+ },
+ {
+ hash: '0xdb2c3455378d278bc7ff19ecc00ddbed95940718e4ab1417720444148d7c9796',
+ timestamp: '2025-07-23T23:29:43.000Z',
+ chainId: 10,
+ accountId: 'eip155:10:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 138857303,
+ blockHash:
+ '0x271a1d6ac1425c4bc4e71d8f6e50741070cb8b1e5934267a3fd1c0dcb07f4c25',
+ gas: 7397881,
+ gasUsed: 6115200,
+ gasPrice: '54928',
+ effectiveGasPrice: '54928',
+ nonce: 45772,
+ cumulativeGasUsed: 16006228,
+ methodId: '0x729ad39e',
+ value: '0',
+ to: '0x799f442248fa09b838ea84b5044751ee1d2a4ee5',
+ from: '0xc53b40ef36693b0d6fe4a17a4b9ce8c40402a8fd',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x799f442248fa09b838ea84b5044751ee1d2a4ee5',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: 1,
+ tokenId: '0',
+ contractAddress: '0x799f442248fa09b838ea84b5044751ee1d2a4ee5',
+ transferType: 'erc1155',
+ },
+ ],
+ logs: [],
+ transactionType: 'SPAM_TOKEN_TRANSFER',
+ transactionCategory: 'TRANSFER',
+ readable: 'Spam Token: Transfer',
+ transactionProtocol: 'SPAM_TOKEN',
+ },
+ {
+ hash: '0x5ea58c6b6c7fb83dd6bd1bbb56c063dc98e5dc16e32b4005fdf7ec4e8eb52984',
+ timestamp: '2025-07-23T23:29:41.000Z',
+ chainId: 10,
+ accountId: 'eip155:10:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 138857302,
+ blockHash:
+ '0x91794267a2efd11c974c7abafe9729133c4872a2d3de18e07fad2754bad81b30',
+ gas: 9189574,
+ gasUsed: 7596522,
+ gasPrice: '52521',
+ effectiveGasPrice: '52521',
+ nonce: 43312,
+ cumulativeGasUsed: 35426429,
+ methodId: '0x729ad39e',
+ value: '0',
+ to: '0x0c20980c94ad027e2340d65edb22445b40288fea',
+ from: '0x7648d50120a436045dcac8d54c2e7acd848e48bb',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x0c20980c94ad027e2340d65edb22445b40288fea',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: 1,
+ tokenId: '0',
+ contractAddress: '0x0c20980c94ad027e2340d65edb22445b40288fea',
+ transferType: 'erc1155',
+ },
+ ],
+ logs: [],
+ transactionType: 'SPAM_TOKEN_TRANSFER',
+ transactionCategory: 'TRANSFER',
+ readable: 'Spam Token: Transfer',
+ transactionProtocol: 'SPAM_TOKEN',
+ },
+ {
+ hash: '0x3ebd870467f68fd21877893eb67e3c6ccb25464d01b938b441732ff415547f5c',
+ timestamp: '2025-04-19T01:41:11.000Z',
+ chainId: 1,
+ accountId: 'eip155:1:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 22299831,
+ blockHash:
+ '0xb8d2f604ca30aeb55c6a765518d3221b42b14ecd9c73936aba872b5880d8cf96',
+ gas: 21000,
+ gasUsed: 21000,
+ gasPrice: '279367455',
+ effectiveGasPrice: '279367455',
+ nonce: 2,
+ cumulativeGasUsed: 24447918,
+ methodId: null,
+ value: '53829416400',
+ to: '0x7052f331db3d924686f08cf47d36d912160d127a',
+ from: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ to: '0x7052f331db3d924686f08cf47d36d912160d127a',
+ amount: '53829416400',
+ decimal: 18,
+ transferType: 'normal',
+ },
+ ],
+ logs: [],
+ transactionType: 'STANDARD',
+ transactionCategory: 'STANDARD',
+ readable: 'Native Transfer',
+ },
+ {
+ hash: '0xfc887fc95845e3aa8ae52fc03217dc57c962047846474dd605e006bdd1aa4ff9',
+ timestamp: '2025-04-11T07:54:11.000Z',
+ chainId: 1,
+ accountId: 'eip155:1:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 22244333,
+ blockHash:
+ '0xfe3c1a87497e1019ca26eaced5fa62f91f0ec4edd3c67092c6e1e2d04fb431a9',
+ gas: 70018,
+ gasUsed: 64868,
+ gasPrice: '661810236',
+ effectiveGasPrice: '661810236',
+ nonce: 1,
+ cumulativeGasUsed: 4683910,
+ methodId: '0xb88d4fde',
+ value: '0',
+ to: '0x6cb26df0c825fece867a84658f87b0ecbcea72f6',
+ from: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ to: '0x5f4a2f10181852b5e46244168f4a8aed00fc8850',
+ tokenId: '2875',
+ contractAddress: '0x6cb26df0c825fece867a84658f87b0ecbcea72f6',
+ transferType: 'erc721',
+ },
+ ],
+ logs: [],
+ transactionProtocol: 'ERC_721',
+ transactionCategory: 'TRANSFER',
+ transactionType: 'ERC_721_TRANSFER',
+ readable: 'Nft: Transfer',
+ },
+ {
+ hash: '0xf72c330a2fdd3354b6b11f9230fd4b245d755b1a96f28ddd8fb46e1b47e91012',
+ timestamp: '2025-04-11T07:54:11.000Z',
+ chainId: 1,
+ accountId: 'eip155:1:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 22244333,
+ blockHash:
+ '0xfe3c1a87497e1019ca26eaced5fa62f91f0ec4edd3c67092c6e1e2d04fb431a9',
+ gas: 21000,
+ gasUsed: 21000,
+ gasPrice: '3409951183',
+ effectiveGasPrice: '3409951183',
+ nonce: 7106,
+ cumulativeGasUsed: 4619042,
+ methodId: null,
+ value: '46338629104248',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ from: '0x9146bc6960a81ce3e60d3a0b51d8a503747c56ef',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x9146bc6960a81ce3e60d3a0b51d8a503747c56ef',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: '46338629104248',
+ decimal: 18,
+ transferType: 'normal',
+ },
+ ],
+ logs: [],
+ transactionType: 'STANDARD',
+ transactionCategory: 'STANDARD',
+ readable: 'Native Transfer',
+ },
+ {
+ hash: '0xb86195d482105457978fc9a02de391383f7724d9d219e70e641ccd2c0e6b9864',
+ timestamp: '2025-03-23T07:59:25.000Z',
+ chainId: 8453,
+ accountId: 'eip155:8453:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 27963709,
+ blockHash:
+ '0xabb3d7bc9620ea7a73a9e946f2e9e7cec702b40d7d88b14325a2c6e3d471f663',
+ gas: 173477,
+ gasUsed: 170953,
+ gasPrice: '3033731',
+ effectiveGasPrice: '3033731',
+ nonce: 38994,
+ cumulativeGasUsed: 48495543,
+ methodId: '0x729ad39e',
+ value: '0',
+ to: '0x67c9f4737f36a7dec28b4d3f4220c31f3b9aeae6',
+ from: '0x18a0d23d9bccfb8ef993a1bf226f44e7713eddc8',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x698dc45e4f10966f6d1d98e3bfd7071d8144c233',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ tokenId: '0',
+ contractAddress: '0x67c9f4737f36a7dec28b4d3f4220c31f3b9aeae6',
+ transferType: 'erc721',
+ },
+ ],
+ logs: [],
+ transactionProtocol: 'ERC_721',
+ transactionCategory: 'TRANSFER',
+ transactionType: 'ERC_721_TRANSFER',
+ readable: 'Nft: Transfer',
+ },
+ {
+ hash: '0xf3cc5507af7c8adfbe0483af86f1bffd0c21b44b0d9dea708289c76572bea1af',
+ timestamp: '2025-03-23T07:57:59.000Z',
+ chainId: 1,
+ accountId: 'eip155:1:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 22108244,
+ blockHash:
+ '0x1168f453d13486ea7a91f42fa2cb077b8f455b5a7f5d4e75a486085ada0c2c02',
+ gas: 34706,
+ gasUsed: 29906,
+ gasPrice: '523379845',
+ effectiveGasPrice: '523379845',
+ nonce: 0,
+ cumulativeGasUsed: 4535331,
+ methodId: '0xa9059cbb',
+ value: '0',
+ to: '0x6b175474e89094c44da98b954eedeac495271d0f',
+ from: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ to: '0x792a83e74d76fcdab681249c82bc8ec6e1aa1111',
+ amount: '100000000000000000',
+ decimal: 18,
+ contractAddress: '0x6b175474e89094c44da98b954eedeac495271d0f',
+ symbol: 'DAI',
+ name: 'Dai Stablecoin',
+ transferType: 'erc20',
+ },
+ ],
+ logs: [],
+ transactionProtocol: 'ERC_20',
+ transactionCategory: 'TRANSFER',
+ transactionType: 'ERC_20_TRANSFER',
+ readable: 'Token: Transfer',
+ },
+ {
+ hash: '0x70b9b7fe330d882cdb49a84a159a167d68a6c4e0c6a38f321c38dfb3c4c792ab',
+ timestamp: '2025-03-23T07:57:59.000Z',
+ chainId: 1,
+ accountId: 'eip155:1:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 22108244,
+ blockHash:
+ '0x1168f453d13486ea7a91f42fa2cb077b8f455b5a7f5d4e75a486085ada0c2c02',
+ gas: 21000,
+ gasUsed: 21000,
+ gasPrice: '523379845',
+ effectiveGasPrice: '523379845',
+ nonce: 2554,
+ cumulativeGasUsed: 4505425,
+ methodId: null,
+ value: '18164420900570',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ from: '0x0004405239ff2d9ce60acc56e0b302a3299c4840',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x0004405239ff2d9ce60acc56e0b302a3299c4840',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: '18164420900570',
+ decimal: 18,
+ transferType: 'normal',
+ },
+ ],
+ logs: [],
+ transactionType: 'STANDARD',
+ transactionCategory: 'STANDARD',
+ readable: 'Native Transfer',
+ },
+ {
+ hash: '0x449f6329b17246be05a611ede3761fad67c1f9637357f137b5416bec52389dd7',
+ timestamp: '2024-06-29T22:34:08.000Z',
+ chainId: 137,
+ accountId: 'eip155:137:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 58765539,
+ blockHash:
+ '0x4a862c753dd066318a69c6b92b0955cee0b30733a95d82a237d9e2293b915a91',
+ gas: 11000000,
+ gasUsed: 8919284,
+ gasPrice: '31500000028',
+ effectiveGasPrice: '31500000028',
+ nonce: 9529,
+ cumulativeGasUsed: 11952907,
+ methodId: '0xfaf67b43',
+ value: '0',
+ to: '0x76f8e90320b64590c1a11f5c63ffbbdc83371279',
+ from: '0xc078e264196e2233039aaa422d4861090dbfb86a',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x0000000000000000000000000000000000000000',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: 1,
+ tokenId: '1',
+ contractAddress: '0x76f8e90320b64590c1a11f5c63ffbbdc83371279',
+ transferType: 'erc1155',
+ },
+ ],
+ logs: [],
+ transactionType: 'SPAM_TOKEN_TRANSFER',
+ transactionCategory: 'TRANSFER',
+ readable: 'Spam Token: Transfer',
+ transactionProtocol: 'SPAM_TOKEN',
+ },
+ {
+ hash: '0x6dd52a01691b54d25b6098df1f80e1b181449ec237560054ad1351e176c21406',
+ timestamp: '2024-06-29T18:27:46.000Z',
+ chainId: 137,
+ accountId: 'eip155:137:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 58758592,
+ blockHash:
+ '0x71b52fd770b65d6ad71a16e729634f1dcac141413e20dd45ebe92ecc313330fd',
+ gas: 11000000,
+ gasUsed: 8919488,
+ gasPrice: '31500000089',
+ effectiveGasPrice: '31500000089',
+ nonce: 3456,
+ cumulativeGasUsed: 14031888,
+ methodId: '0xfaf67b43',
+ value: '0',
+ to: '0xc60ba1c956331d76c8e781a7147519ed0427661a',
+ from: '0xda3e3f3a946e759d84408462c538e74d4f44ddd0',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x0000000000000000000000000000000000000000',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: 1,
+ tokenId: '1',
+ contractAddress: '0xc60ba1c956331d76c8e781a7147519ed0427661a',
+ transferType: 'erc1155',
+ },
+ ],
+ logs: [],
+ transactionType: 'SPAM_TOKEN_TRANSFER',
+ transactionCategory: 'TRANSFER',
+ readable: 'Spam Token: Transfer',
+ transactionProtocol: 'SPAM_TOKEN',
+ },
+ {
+ hash: '0x01d737867887caafe0502a329736b543e6828cc30a13853bb73c63cc72bfc6c4',
+ timestamp: '2024-06-20T18:15:41.000Z',
+ chainId: 137,
+ accountId: 'eip155:137:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 58399154,
+ blockHash:
+ '0x7e2a1cda6cfa5491861f419f067b962b2074c541f51d3caf78ade8d390a4eabc',
+ gas: 11000000,
+ gasUsed: 6745986,
+ gasPrice: '31500000033',
+ effectiveGasPrice: '31500000033',
+ nonce: 438,
+ cumulativeGasUsed: 11201316,
+ methodId: '0xfaf67b43',
+ value: '0',
+ to: '0x33d67d15214129e47e357510a2d0f1a25d66ae9b',
+ from: '0x02de77e62c6b11c64a76fb7341cd678787f8addd',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x0000000000000000000000000000000000000000',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: 1,
+ tokenId: '1',
+ contractAddress: '0x33d67d15214129e47e357510a2d0f1a25d66ae9b',
+ transferType: 'erc1155',
+ },
+ ],
+ logs: [],
+ transactionType: 'SPAM_TOKEN_TRANSFER',
+ transactionCategory: 'TRANSFER',
+ readable: 'Spam Token: Transfer',
+ transactionProtocol: 'SPAM_TOKEN',
+ },
+ {
+ hash: '0xc143c1535a4036180db08cc15d526a5861a4b190ff172796edb8bf762f2ab72d',
+ timestamp: '2024-06-20T18:14:59.000Z',
+ chainId: 1,
+ accountId: 'eip155:1:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 20134660,
+ blockHash:
+ '0x0432270a0889ab380c04738cd00fdc125c6b8b602fe959c029f0224ce0cdb910',
+ gas: 94617,
+ gasUsed: 62248,
+ gasPrice: '8572767507',
+ effectiveGasPrice: '8572767507',
+ nonce: 1688,
+ cumulativeGasUsed: 6762162,
+ methodId: '0xa9059cbb',
+ value: '0',
+ to: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
+ from: '0x2990079bcdee240329a520d2444386fc119da21a',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x2990079bcdee240329a520d2444386fc119da21a',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: '120000',
+ decimal: 6,
+ contractAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
+ symbol: 'USDC',
+ name: 'USD Coin',
+ transferType: 'erc20',
+ },
+ ],
+ logs: [],
+ transactionProtocol: 'ERC_20',
+ transactionCategory: 'TRANSFER',
+ transactionType: 'ERC_20_TRANSFER',
+ readable: 'Token: Transfer',
+ },
+ {
+ hash: '0xbc67f71241b92b5c902d4fd0257e8d7137ebec0f608829bba964ce3dfdcf51b7',
+ timestamp: '2024-06-20T18:14:57.000Z',
+ chainId: 137,
+ accountId: 'eip155:137:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 58399134,
+ blockHash:
+ '0x83eaf35919c5c8ba46779533b38589f0e33739320c52e6a588b615d87df50cbc',
+ gas: 11000000,
+ gasUsed: 5183636,
+ gasPrice: '31500000029',
+ effectiveGasPrice: '31500000029',
+ nonce: 6037,
+ cumulativeGasUsed: 7954869,
+ methodId: '0xfaf67b43',
+ value: '0',
+ to: '0xfe5d94a2b1b3066c814ee5fd024f8c3f99ef51f4',
+ from: '0x4c4404d760b18f824735f4ad746f7d809baf3432',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x0000000000000000000000000000000000000000',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: 1,
+ tokenId: '1',
+ contractAddress: '0xfe5d94a2b1b3066c814ee5fd024f8c3f99ef51f4',
+ transferType: 'erc1155',
+ },
+ ],
+ logs: [],
+ transactionType: 'SPAM_TOKEN_TRANSFER',
+ transactionCategory: 'TRANSFER',
+ readable: 'Spam Token: Transfer',
+ transactionProtocol: 'SPAM_TOKEN',
+ },
+ {
+ hash: '0xa73254e3cc0c03c2af521b09ed848ec1f37c6e6f0b9ca92781c39f52fa3796cc',
+ timestamp: '2024-06-20T18:14:45.000Z',
+ chainId: 137,
+ accountId: 'eip155:137:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 58399128,
+ blockHash:
+ '0x823df9d476c3e9963b6ae37be3392ba88d5c95edaaeeed07136c290b0fb2056e',
+ gas: 53812,
+ gasUsed: 48920,
+ gasPrice: '30000000028',
+ effectiveGasPrice: '30000000028',
+ nonce: 1531,
+ cumulativeGasUsed: 10254933,
+ methodId: '0x9c96eec5',
+ value: '0',
+ to: '0x870b5c4e16adf0ac629cd6053438a71908fc6132',
+ from: '0x3d6f605526f69d0376c6f4aceee5f2a47e255f89',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x9d1b1669c73b033dfe47ae5a0164ab96df25b944',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: '999098777100000000000',
+ decimal: 18,
+ contractAddress: '0x870b5c4e16adf0ac629cd6053438a71908fc6132',
+ symbol: 'Invitation Link : https://zero-bridge.xyz/',
+ name: 'LayerZero : Bridge Rewards',
+ transferType: 'erc20',
+ },
+ ],
+ logs: [],
+ transactionType: 'GENERIC_CONTRACT_CALL',
+ transactionCategory: 'CONTRACT_CALL',
+ readable: 'Unidentified Transaction',
+ },
+ {
+ hash: '0xa87e7bc4ce49ff43541ea5d6e2af5a0281caacdd356418be1c1d85113acf7e74',
+ timestamp: '2024-06-20T18:14:35.000Z',
+ chainId: 1,
+ accountId: 'eip155:1:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 20134658,
+ blockHash:
+ '0xc5aeba1b9a31ddb7f39d071a8d2b1d68eedbd3ab403e2cf8a351d24a20fbada8',
+ gas: 78298,
+ gasUsed: 51806,
+ gasPrice: '7453274534',
+ effectiveGasPrice: '7453274534',
+ nonce: 1687,
+ cumulativeGasUsed: 20241794,
+ methodId: '0xa9059cbb',
+ value: '0',
+ to: '0x6b175474e89094c44da98b954eedeac495271d0f',
+ from: '0x2990079bcdee240329a520d2444386fc119da21a',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x2990079bcdee240329a520d2444386fc119da21a',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: '100000000000000000',
+ decimal: 18,
+ contractAddress: '0x6b175474e89094c44da98b954eedeac495271d0f',
+ symbol: 'DAI',
+ name: 'Dai Stablecoin',
+ transferType: 'erc20',
+ },
+ ],
+ logs: [],
+ transactionProtocol: 'ERC_20',
+ transactionCategory: 'TRANSFER',
+ transactionType: 'ERC_20_TRANSFER',
+ readable: 'Token: Transfer',
+ },
+ {
+ hash: '0x4d6d83631f1742144e95397b5910bf4807192be38ad3a79d390cf4744c0c6550',
+ timestamp: '2024-06-19T16:28:23.000Z',
+ chainId: 1,
+ accountId: 'eip155:1:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 20126979,
+ blockHash:
+ '0x4a8789c29ef4bc8ced1ff59735181d76b9548d8996e50b9175f5f84a6fc169c4',
+ gas: 71969,
+ gasUsed: 59816,
+ gasPrice: '13332526504',
+ effectiveGasPrice: '13332526504',
+ nonce: 1672,
+ cumulativeGasUsed: 13365022,
+ methodId: '0xf242432a',
+ value: '0',
+ to: '0xb66a603f4cfe17e3d27b87a8bfcad319856518b8',
+ from: '0x2990079bcdee240329a520d2444386fc119da21a',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x2990079bcdee240329a520d2444386fc119da21a',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: 1,
+ tokenId:
+ '100059584189867566852894599154934721552567499366058460268305346163854916190212',
+ contractAddress: '0xb66a603f4cfe17e3d27b87a8bfcad319856518b8',
+ transferType: 'erc1155',
+ },
+ ],
+ logs: [],
+ transactionType: 'GENERIC_CONTRACT_CALL',
+ transactionCategory: 'CONTRACT_CALL',
+ readable: 'Unidentified Transaction',
+ },
+ {
+ hash: '0x064a773c959cfae4c5911fc730cb7920351e55f81e7aa661246b90c382f0054e',
+ timestamp: '2024-06-19T14:59:47.000Z',
+ chainId: 1,
+ accountId: 'eip155:1:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 20126537,
+ blockHash:
+ '0x99b36ebd93ea840414cc09f6c441afc3cddada3ece4b7f95e28e66e2545d7d08',
+ gas: 98368,
+ gasUsed: 42056,
+ gasPrice: '7691903129',
+ effectiveGasPrice: '7691903129',
+ nonce: 1669,
+ cumulativeGasUsed: 24774395,
+ methodId: '0xf242432a',
+ value: '0',
+ to: '0xb66a603f4cfe17e3d27b87a8bfcad319856518b8',
+ from: '0x2990079bcdee240329a520d2444386fc119da21a',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x2990079bcdee240329a520d2444386fc119da21a',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: null,
+ tokenId:
+ '100059584189867566852894599154934721552567499366058460268305346163854916190212',
+ contractAddress: '0xb66a603f4cfe17e3d27b87a8bfcad319856518b8',
+ transferType: 'erc1155',
+ },
+ ],
+ logs: [],
+ transactionType: 'GENERIC_CONTRACT_CALL',
+ transactionCategory: 'CONTRACT_CALL',
+ readable: 'Unidentified Transaction',
+ },
+ {
+ hash: '0x833d5e703c269f691ae609805be789aac2a605662bb15099ce5361228ebed9e2',
+ timestamp: '2024-06-17T20:46:23.000Z',
+ chainId: 1,
+ accountId: 'eip155:1:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 20113979,
+ blockHash:
+ '0x3ec9c170e8f059142f106daa5c54ebc2a0bb7c552d5e43ad8696d97bfee2de31',
+ gas: 98368,
+ gasUsed: 42056,
+ gasPrice: '7822363494',
+ effectiveGasPrice: '7822363494',
+ nonce: 1636,
+ cumulativeGasUsed: 10428073,
+ methodId: '0xf242432a',
+ value: '0',
+ to: '0xb66a603f4cfe17e3d27b87a8bfcad319856518b8',
+ from: '0x2990079bcdee240329a520d2444386fc119da21a',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x2990079bcdee240329a520d2444386fc119da21a',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ amount: null,
+ tokenId:
+ '100059584189867566852894599154934721552567499366058460268305346163854916190212',
+ contractAddress: '0xb66a603f4cfe17e3d27b87a8bfcad319856518b8',
+ transferType: 'erc1155',
+ },
+ ],
+ logs: [],
+ transactionType: 'GENERIC_CONTRACT_CALL',
+ transactionCategory: 'CONTRACT_CALL',
+ readable: 'Unidentified Transaction',
+ },
+ {
+ hash: '0x1686ff4f3ca5aec7cdc13dbd786ef90741b09fe1150024a920fa72b8939238ae',
+ timestamp: '2024-06-17T20:35:47.000Z',
+ chainId: 1,
+ accountId: 'eip155:1:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ blockNumber: 20113926,
+ blockHash:
+ '0x380b18f09a627e24741e84cd47f00f57eea1ba67931e7ce010f7e85894af6d0b',
+ gas: 99802,
+ gasUsed: 61419,
+ gasPrice: '8368575474',
+ effectiveGasPrice: '8368575474',
+ nonce: 1632,
+ cumulativeGasUsed: 13180858,
+ methodId: '0x23b872dd',
+ value: '0',
+ to: '0x6cb26df0c825fece867a84658f87b0ecbcea72f6',
+ from: '0x2990079bcdee240329a520d2444386fc119da21a',
+ isError: false,
+ valueTransfers: [
+ {
+ from: '0x2990079bcdee240329a520d2444386fc119da21a',
+ to: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ tokenId: '2875',
+ contractAddress: '0x6cb26df0c825fece867a84658f87b0ecbcea72f6',
+ transferType: 'erc721',
+ },
+ ],
+ logs: [],
+ transactionCategory: 'TRANSFER',
+ transactionProtocol: 'ERC_721',
+ transactionType: 'ERC_721_TRANSFER',
+ readable: 'Nft: Transfer',
+ },
+ ],
+ unprocessedNetworks: [],
+ pageInfo: { hasNextPage: false, cursor: null, count: 34 },
+};
+
+export const ACCOUNTS_API_ACTIVE_NETWORKS_RESPONSE = {
+ activeNetworks: [
+ 'eip155:1:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ 'eip155:10:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ 'eip155:137:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ 'eip155:8453:0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
+ ],
+};
diff --git a/e2e/api-mocking/mock-responses/feature-flags-mocks.ts b/e2e/api-mocking/mock-responses/feature-flags-mocks.ts
new file mode 100644
index 000000000000..9cd9d334615e
--- /dev/null
+++ b/e2e/api-mocking/mock-responses/feature-flags-mocks.ts
@@ -0,0 +1,164 @@
+export const SWAPS_FEATURE_FLAG_RESPONSE = {
+ ethereum: {
+ mobile_active: true,
+ extension_active: true,
+ fallback_to_v1: false,
+ fallbackToV1: false,
+ mobileActive: true,
+ extensionActive: true,
+ mobileActiveIOS: true,
+ mobileActiveAndroid: true,
+ v2: { swapAndSend: { enabled: false } },
+ smartTransactions: {
+ expectedDeadline: 45,
+ maxDeadline: 160,
+ returnTxHashAsap: false,
+ mobileActive: true,
+ extensionActive: true,
+ mobileActiveIos: true,
+ mobileActiveAndroid: true,
+ },
+ },
+ bsc: {
+ mobile_active: true,
+ extension_active: true,
+ fallback_to_v1: false,
+ fallbackToV1: false,
+ mobileActive: true,
+ extensionActive: true,
+ mobileActiveIOS: true,
+ mobileActiveAndroid: true,
+ v2: { swapAndSend: { enabled: false } },
+ smartTransactions: {
+ mobileActive: true,
+ extensionActive: true,
+ mobileActiveIos: true,
+ mobileActiveAndroid: true,
+ },
+ },
+ polygon: {
+ mobile_active: true,
+ extension_active: true,
+ fallback_to_v1: false,
+ fallbackToV1: false,
+ mobileActive: true,
+ extensionActive: true,
+ mobileActiveIOS: true,
+ mobileActiveAndroid: true,
+ v2: { swapAndSend: { enabled: false } },
+ smartTransactions: {},
+ },
+ avalanche: {
+ mobile_active: true,
+ extension_active: true,
+ fallback_to_v1: false,
+ fallbackToV1: false,
+ mobileActive: true,
+ extensionActive: true,
+ mobileActiveIOS: true,
+ mobileActiveAndroid: true,
+ v2: { swapAndSend: { enabled: false } },
+ smartTransactions: {},
+ },
+ arbitrum: {
+ mobile_active: true,
+ extension_active: true,
+ fallback_to_v1: false,
+ fallbackToV1: false,
+ mobileActive: true,
+ extensionActive: true,
+ mobileActiveIOS: true,
+ mobileActiveAndroid: true,
+ v2: { swapAndSend: { enabled: false } },
+ smartTransactions: {
+ mobileActive: true,
+ extensionActive: true,
+ mobileActiveIos: true,
+ mobileActiveAndroid: true,
+ },
+ },
+ optimism: {
+ mobile_active: true,
+ extension_active: true,
+ fallback_to_v1: false,
+ fallbackToV1: false,
+ mobileActive: true,
+ extensionActive: true,
+ mobileActiveIOS: true,
+ mobileActiveAndroid: true,
+ v2: { swapAndSend: { enabled: false } },
+ smartTransactions: {},
+ },
+ zksync: {
+ mobile_active: true,
+ extension_active: true,
+ fallback_to_v1: false,
+ fallbackToV1: false,
+ mobileActive: true,
+ extensionActive: true,
+ mobileActiveIOS: true,
+ mobileActiveAndroid: true,
+ v2: { swapAndSend: { enabled: false } },
+ smartTransactions: {},
+ },
+ linea: {
+ mobile_active: true,
+ extension_active: true,
+ fallback_to_v1: false,
+ fallbackToV1: false,
+ mobileActive: true,
+ extensionActive: true,
+ mobileActiveIOS: true,
+ mobileActiveAndroid: true,
+ v2: { swapAndSend: { enabled: false } },
+ smartTransactions: {},
+ },
+ base: {
+ mobile_active: true,
+ extension_active: true,
+ fallback_to_v1: false,
+ fallbackToV1: false,
+ mobileActive: true,
+ extensionActive: true,
+ mobileActiveIOS: true,
+ mobileActiveAndroid: true,
+ v2: { swapAndSend: { enabled: false } },
+ smartTransactions: {
+ mobileActive: true,
+ extensionActive: true,
+ mobileActiveIos: true,
+ mobileActiveAndroid: true,
+ },
+ },
+ sei: {
+ mobile_active: true,
+ extension_active: false,
+ fallback_to_v1: false,
+ fallbackToV1: false,
+ mobileActive: true,
+ extensionActive: false,
+ mobileActiveIOS: true,
+ mobileActiveAndroid: true,
+ v2: { swapAndSend: { enabled: true } },
+ smartTransactions: {},
+ },
+ smart_transactions: { mobile_active: true, extension_active: true },
+ smartTransactions: {
+ mobileActive: true,
+ extensionActive: true,
+ mobileActiveIOS: false,
+ mobileActiveAndroid: false,
+ mobileReturnTxHashAsap: true,
+ extensionReturnTxHashAsap: true,
+ batchStatusPollingInterval: 5000,
+ },
+ transactions: {
+ acceleratedPollingEnabled: false,
+ acceleratedPollingInterval: 5,
+ maxAcceleratedPolls: 0,
+ },
+ swapRedesign: { mobileActive: false, extensionActive: true },
+ migrateToV2: { extensionActive: false, mobileActive: false },
+ compliance: { merkleScienceMinThreshold: 25000 },
+ multiChainAssets: { pollingSeconds: 0 },
+};
diff --git a/e2e/api-mocking/mock-responses/staking-api-responses-mocks.ts b/e2e/api-mocking/mock-responses/staking-api-responses-mocks.ts
new file mode 100644
index 000000000000..a73cf5914020
--- /dev/null
+++ b/e2e/api-mocking/mock-responses/staking-api-responses-mocks.ts
@@ -0,0 +1,3463 @@
+export const POOLED_STAKING_VAULT_RESPONSE = [
+ {
+ id: 2148691,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-08-05T00:00:00.000Z',
+ daily_apy: '2.160630689308144746',
+ created_at: '2025-08-06T01:00:01.209Z',
+ updated_at: '2025-08-06T01:00:01.209Z',
+ },
+ {
+ id: 2137242,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-08-04T00:00:00.000Z',
+ daily_apy: '2.594766097760912223',
+ created_at: '2025-08-05T01:00:00.675Z',
+ updated_at: '2025-08-05T01:00:00.675Z',
+ },
+ {
+ id: 2125229,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-08-03T00:00:00.000Z',
+ daily_apy: '2.230985359762322456',
+ created_at: '2025-08-04T01:00:03.147Z',
+ updated_at: '2025-08-04T01:00:03.147Z',
+ },
+ {
+ id: 2113939,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-08-02T00:00:00.000Z',
+ daily_apy: '2.822078388979096626',
+ created_at: '2025-08-03T01:00:02.344Z',
+ updated_at: '2025-08-03T01:00:02.344Z',
+ },
+ {
+ id: 2102453,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-08-01T00:00:00.000Z',
+ daily_apy: '2.354552962265530254',
+ created_at: '2025-08-02T01:00:00.479Z',
+ updated_at: '2025-08-02T01:00:00.479Z',
+ },
+ {
+ id: 2090629,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-31T00:00:00.000Z',
+ daily_apy: '2.430527730769124502',
+ created_at: '2025-08-01T01:00:00.630Z',
+ updated_at: '2025-08-01T01:00:00.630Z',
+ },
+ {
+ id: 2078463,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-30T00:00:00.000Z',
+ daily_apy: '2.360728942269321903',
+ created_at: '2025-07-31T01:00:00.432Z',
+ updated_at: '2025-07-31T01:00:00.432Z',
+ },
+ {
+ id: 2067053,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-29T00:00:00.000Z',
+ daily_apy: '2.550855288009415321',
+ created_at: '2025-07-30T01:00:00.453Z',
+ updated_at: '2025-07-30T01:00:00.453Z',
+ },
+ {
+ id: 2055302,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-28T00:00:00.000Z',
+ daily_apy: '2.699143982584143850',
+ created_at: '2025-07-29T01:00:00.728Z',
+ updated_at: '2025-07-29T01:00:00.728Z',
+ },
+ {
+ id: 2044149,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-27T00:00:00.000Z',
+ daily_apy: '2.260023781340267047',
+ created_at: '2025-07-28T01:00:00.468Z',
+ updated_at: '2025-07-28T01:00:00.468Z',
+ },
+ {
+ id: 2032916,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-26T00:00:00.000Z',
+ daily_apy: '2.367243569636514657',
+ created_at: '2025-07-27T01:00:00.570Z',
+ updated_at: '2025-07-27T01:00:00.570Z',
+ },
+ {
+ id: 2019474,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-25T00:00:00.000Z',
+ daily_apy: '2.425507062525974170',
+ created_at: '2025-07-26T01:00:00.376Z',
+ updated_at: '2025-07-26T01:00:00.376Z',
+ },
+ {
+ id: 2007822,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-24T00:00:00.000Z',
+ daily_apy: '2.416965602179156029',
+ created_at: '2025-07-25T01:00:00.220Z',
+ updated_at: '2025-07-25T01:00:00.220Z',
+ },
+ {
+ id: 1996570,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-23T00:00:00.000Z',
+ daily_apy: '2.758675024861472788',
+ created_at: '2025-07-24T01:00:00.525Z',
+ updated_at: '2025-07-24T01:00:00.525Z',
+ },
+ {
+ id: 1985426,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-22T00:00:00.000Z',
+ daily_apy: '2.666457904467836947',
+ created_at: '2025-07-23T01:00:00.300Z',
+ updated_at: '2025-07-23T01:00:00.300Z',
+ },
+ {
+ id: 1971170,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-21T00:00:00.000Z',
+ daily_apy: '2.231467458835929591',
+ created_at: '2025-07-22T01:00:00.584Z',
+ updated_at: '2025-07-22T01:00:00.584Z',
+ },
+ {
+ id: 1959984,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-20T00:00:00.000Z',
+ daily_apy: '2.301110730349541980',
+ created_at: '2025-07-21T01:00:00.883Z',
+ updated_at: '2025-07-21T01:00:00.883Z',
+ },
+ {
+ id: 1948555,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-19T00:00:00.000Z',
+ daily_apy: '2.352087941423962002',
+ created_at: '2025-07-20T00:01:31.192Z',
+ updated_at: '2025-07-20T00:01:31.192Z',
+ },
+ {
+ id: 1937514,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-18T00:00:00.000Z',
+ daily_apy: '2.709889646777819303',
+ created_at: '2025-07-19T01:00:00.307Z',
+ updated_at: '2025-07-19T01:00:00.307Z',
+ },
+ {
+ id: 1926404,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-17T00:00:00.000Z',
+ daily_apy: '2.578081601755443584',
+ created_at: '2025-07-18T01:00:00.230Z',
+ updated_at: '2025-07-18T01:00:00.230Z',
+ },
+ {
+ id: 1915046,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-16T00:00:00.000Z',
+ daily_apy: '2.511785450400075201',
+ created_at: '2025-07-17T00:01:31.166Z',
+ updated_at: '2025-07-17T00:01:31.166Z',
+ },
+ {
+ id: 1904077,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-15T00:00:00.000Z',
+ daily_apy: '2.242968214074173364',
+ created_at: '2025-07-16T01:00:00.280Z',
+ updated_at: '2025-07-16T01:00:00.280Z',
+ },
+ {
+ id: 1893132,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-14T00:00:00.000Z',
+ daily_apy: '2.572947490988122566',
+ created_at: '2025-07-15T01:00:00.622Z',
+ updated_at: '2025-07-15T01:00:00.622Z',
+ },
+ {
+ id: 1881756,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-13T00:00:00.000Z',
+ daily_apy: '2.689218566706491482',
+ created_at: '2025-07-14T00:01:31.157Z',
+ updated_at: '2025-07-14T00:01:31.157Z',
+ },
+ {
+ id: 1870771,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-12T00:00:00.000Z',
+ daily_apy: '2.365846320573506748',
+ created_at: '2025-07-13T01:00:00.303Z',
+ updated_at: '2025-07-13T01:00:00.303Z',
+ },
+ {
+ id: 1859898,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-11T00:00:00.000Z',
+ daily_apy: '2.611392743830641121',
+ created_at: '2025-07-12T01:00:00.552Z',
+ updated_at: '2025-07-12T01:00:00.552Z',
+ },
+ {
+ id: 1849051,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-10T00:00:00.000Z',
+ daily_apy: '2.474061921678370022',
+ created_at: '2025-07-11T01:00:00.318Z',
+ updated_at: '2025-07-11T01:00:00.318Z',
+ },
+ {
+ id: 1837776,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-09T00:00:00.000Z',
+ daily_apy: '2.414119937632374226',
+ created_at: '2025-07-10T01:00:00.390Z',
+ updated_at: '2025-07-10T01:00:00.390Z',
+ },
+ {
+ id: 1829760,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-08T00:00:00.000Z',
+ daily_apy: '3.010997754405619248',
+ created_at: '2025-07-09T07:00:00.507Z',
+ updated_at: '2025-07-09T07:00:00.507Z',
+ },
+ {
+ id: 1819432,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-07T00:00:00.000Z',
+ daily_apy: '2.551856786885392865',
+ created_at: '2025-07-08T01:00:00.298Z',
+ updated_at: '2025-07-08T01:00:00.298Z',
+ },
+ {
+ id: 1808679,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-06T00:00:00.000Z',
+ daily_apy: '2.322264721475653208',
+ created_at: '2025-07-07T01:00:00.482Z',
+ updated_at: '2025-07-07T01:00:00.482Z',
+ },
+ {
+ id: 1797950,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-05T00:00:00.000Z',
+ daily_apy: '2.545299229175557965',
+ created_at: '2025-07-06T01:00:00.323Z',
+ updated_at: '2025-07-06T01:00:00.323Z',
+ },
+ {
+ id: 1786799,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-04T00:00:00.000Z',
+ daily_apy: '2.238388442775579425',
+ created_at: '2025-07-05T01:00:00.445Z',
+ updated_at: '2025-07-05T01:00:00.445Z',
+ },
+ {
+ id: 1776118,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-03T00:00:00.000Z',
+ daily_apy: '2.483210588259805088',
+ created_at: '2025-07-04T01:00:00.649Z',
+ updated_at: '2025-07-04T01:00:00.649Z',
+ },
+ {
+ id: 1764050,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-02T00:00:00.000Z',
+ daily_apy: '2.361765502010076272',
+ created_at: '2025-07-03T00:44:17.079Z',
+ updated_at: '2025-07-03T00:44:17.079Z',
+ },
+ {
+ id: 1752167,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-07-01T00:00:00.000Z',
+ daily_apy: '2.560299460784560951',
+ created_at: '2025-07-02T01:00:00.254Z',
+ updated_at: '2025-07-02T01:00:00.254Z',
+ },
+ {
+ id: 1743842,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-30T00:00:00.000Z',
+ daily_apy: '3.092940485915711615',
+ created_at: '2025-07-01T07:51:52.275Z',
+ updated_at: '2025-07-01T07:51:52.275Z',
+ },
+ {
+ id: 1729618,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-29T00:00:00.000Z',
+ daily_apy: '2.473858935145985675',
+ created_at: '2025-06-30T01:00:00.398Z',
+ updated_at: '2025-06-30T01:00:00.398Z',
+ },
+ {
+ id: 1718983,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-28T00:00:00.000Z',
+ daily_apy: '2.636138892886318473',
+ created_at: '2025-06-29T01:00:00.277Z',
+ updated_at: '2025-06-29T01:00:00.277Z',
+ },
+ {
+ id: 1708081,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-27T00:00:00.000Z',
+ daily_apy: '2.364594758530096958',
+ created_at: '2025-06-28T00:01:30.698Z',
+ updated_at: '2025-06-28T00:01:30.698Z',
+ },
+ {
+ id: 1697568,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-26T00:00:00.000Z',
+ daily_apy: '2.599179838082240376',
+ created_at: '2025-06-27T01:00:00.401Z',
+ updated_at: '2025-06-27T01:00:00.401Z',
+ },
+ {
+ id: 1687007,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-25T00:00:00.000Z',
+ daily_apy: '2.781241096874648894',
+ created_at: '2025-06-26T01:00:00.495Z',
+ updated_at: '2025-06-26T01:00:00.495Z',
+ },
+ {
+ id: 1676614,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-24T00:00:00.000Z',
+ daily_apy: '3.313982129947740597',
+ created_at: '2025-06-25T01:00:00.300Z',
+ updated_at: '2025-06-25T01:00:00.300Z',
+ },
+ {
+ id: 1665738,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-23T00:00:00.000Z',
+ daily_apy: '2.920984915416314934',
+ created_at: '2025-06-24T01:00:00.264Z',
+ updated_at: '2025-06-24T01:00:00.264Z',
+ },
+ {
+ id: 1655321,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-22T00:00:00.000Z',
+ daily_apy: '2.508811228066068916',
+ created_at: '2025-06-23T01:00:00.510Z',
+ updated_at: '2025-06-23T01:00:00.510Z',
+ },
+ {
+ id: 1644494,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-21T00:00:00.000Z',
+ daily_apy: '2.417145276593338606',
+ created_at: '2025-06-22T01:00:00.462Z',
+ updated_at: '2025-06-22T01:00:00.462Z',
+ },
+ {
+ id: 1634125,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-20T00:00:00.000Z',
+ daily_apy: '3.132806762001707522',
+ created_at: '2025-06-21T01:00:00.369Z',
+ updated_at: '2025-06-21T01:00:00.369Z',
+ },
+ {
+ id: 1623780,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-19T00:00:00.000Z',
+ daily_apy: '2.326934563020984126',
+ created_at: '2025-06-20T01:00:00.390Z',
+ updated_at: '2025-06-20T01:00:00.390Z',
+ },
+ {
+ id: 1613458,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-18T00:00:00.000Z',
+ daily_apy: '2.528577333658723341',
+ created_at: '2025-06-19T01:00:01.798Z',
+ updated_at: '2025-06-19T01:00:01.798Z',
+ },
+ {
+ id: 1603161,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-17T00:00:00.000Z',
+ daily_apy: '2.420846308287214689',
+ created_at: '2025-06-18T01:00:00.215Z',
+ updated_at: '2025-06-18T01:00:00.215Z',
+ },
+ {
+ id: 1592902,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-16T00:00:00.000Z',
+ daily_apy: '2.317700676927453592',
+ created_at: '2025-06-17T01:00:01.107Z',
+ updated_at: '2025-06-17T01:00:01.107Z',
+ },
+ {
+ id: 1582653,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-15T00:00:00.000Z',
+ daily_apy: '2.847038769765440542',
+ created_at: '2025-06-16T01:00:00.234Z',
+ updated_at: '2025-06-16T01:00:00.234Z',
+ },
+ {
+ id: 1572428,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-14T00:00:00.000Z',
+ daily_apy: '2.661913891382594137',
+ created_at: '2025-06-15T01:00:00.311Z',
+ updated_at: '2025-06-15T01:00:00.311Z',
+ },
+ {
+ id: 1562227,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-13T00:00:00.000Z',
+ daily_apy: '2.395936713018122124',
+ created_at: '2025-06-14T01:00:00.477Z',
+ updated_at: '2025-06-14T01:00:00.477Z',
+ },
+ {
+ id: 1552056,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-12T00:00:00.000Z',
+ daily_apy: '2.308595718513410841',
+ created_at: '2025-06-13T01:00:00.357Z',
+ updated_at: '2025-06-13T01:00:00.357Z',
+ },
+ {
+ id: 1541904,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-11T00:00:00.000Z',
+ daily_apy: '2.714989932921688108',
+ created_at: '2025-06-12T01:00:00.377Z',
+ updated_at: '2025-06-12T01:00:00.377Z',
+ },
+ {
+ id: 1531775,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-10T00:00:00.000Z',
+ daily_apy: '2.730241025946509735',
+ created_at: '2025-06-11T01:00:00.451Z',
+ updated_at: '2025-06-11T01:00:00.451Z',
+ },
+ {
+ id: 1504410,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-09T00:00:00.000Z',
+ daily_apy: '2.438940357185122676',
+ created_at: '2025-06-10T01:00:01.847Z',
+ updated_at: '2025-06-10T01:00:01.847Z',
+ },
+ {
+ id: 1494384,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-08T00:00:00.000Z',
+ daily_apy: '2.394719753798906958',
+ created_at: '2025-06-09T01:00:01.247Z',
+ updated_at: '2025-06-09T01:00:01.247Z',
+ },
+ {
+ id: 1484327,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-07T00:00:00.000Z',
+ daily_apy: '2.646813409672606029',
+ created_at: '2025-06-08T01:00:00.257Z',
+ updated_at: '2025-06-08T01:00:00.257Z',
+ },
+ {
+ id: 1474294,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-06T00:00:00.000Z',
+ daily_apy: '3.610860546185630642',
+ created_at: '2025-06-07T01:00:00.227Z',
+ updated_at: '2025-06-07T01:00:00.227Z',
+ },
+ {
+ id: 1464233,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-05T00:00:00.000Z',
+ daily_apy: '2.866066000911694912',
+ created_at: '2025-06-06T01:00:00.204Z',
+ updated_at: '2025-06-06T01:00:00.204Z',
+ },
+ {
+ id: 1454249,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-04T00:00:00.000Z',
+ daily_apy: '2.548511908606253429',
+ created_at: '2025-06-05T01:00:00.236Z',
+ updated_at: '2025-06-05T01:00:00.236Z',
+ },
+ {
+ id: 1444704,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-03T00:00:00.000Z',
+ daily_apy: '2.765209089148432467',
+ created_at: '2025-06-04T01:00:00.229Z',
+ updated_at: '2025-06-04T01:00:00.229Z',
+ },
+ {
+ id: 1434767,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-02T00:00:00.000Z',
+ daily_apy: '2.718609123877091482',
+ created_at: '2025-06-03T01:00:00.287Z',
+ updated_at: '2025-06-03T01:00:00.287Z',
+ },
+ {
+ id: 1424854,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-06-01T00:00:00.000Z',
+ daily_apy: '2.674633072827114657',
+ created_at: '2025-06-02T01:00:00.242Z',
+ updated_at: '2025-06-02T01:00:00.242Z',
+ },
+ {
+ id: 1414965,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-31T00:00:00.000Z',
+ daily_apy: '2.503740742292486449',
+ created_at: '2025-06-01T01:00:00.252Z',
+ updated_at: '2025-06-01T01:00:00.252Z',
+ },
+ {
+ id: 1405100,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-30T00:00:00.000Z',
+ daily_apy: '2.377321111112683739',
+ created_at: '2025-05-31T01:00:00.247Z',
+ updated_at: '2025-05-31T01:00:00.247Z',
+ },
+ {
+ id: 1394849,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-29T00:00:00.000Z',
+ daily_apy: '2.266832495716882467',
+ created_at: '2025-05-30T00:01:30.569Z',
+ updated_at: '2025-05-30T00:01:30.569Z',
+ },
+ {
+ id: 1385032,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-28T00:00:00.000Z',
+ daily_apy: '2.761684766061966040',
+ created_at: '2025-05-29T01:00:01.647Z',
+ updated_at: '2025-05-29T01:00:01.647Z',
+ },
+ {
+ id: 1375197,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-27T00:00:00.000Z',
+ daily_apy: '2.254981325304014602',
+ created_at: '2025-05-28T01:00:00.227Z',
+ updated_at: '2025-05-28T01:00:00.227Z',
+ },
+ {
+ id: 1365429,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-26T00:00:00.000Z',
+ daily_apy: '2.455358672817514712',
+ created_at: '2025-05-27T01:00:00.247Z',
+ updated_at: '2025-05-27T01:00:00.247Z',
+ },
+ {
+ id: 1355725,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-25T00:00:00.000Z',
+ daily_apy: '2.171054355556651327',
+ created_at: '2025-05-26T01:00:00.204Z',
+ updated_at: '2025-05-26T01:00:00.204Z',
+ },
+ {
+ id: 1346004,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-24T00:00:00.000Z',
+ daily_apy: '1.923545158765472788',
+ created_at: '2025-05-25T01:00:00.223Z',
+ updated_at: '2025-05-25T01:00:00.223Z',
+ },
+ {
+ id: 1336268,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-23T00:00:00.000Z',
+ daily_apy: '2.616579553526408131',
+ created_at: '2025-05-24T01:00:00.242Z',
+ updated_at: '2025-05-24T01:00:00.242Z',
+ },
+ {
+ id: 1326634,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-22T00:00:00.000Z',
+ daily_apy: '2.208794804288317035',
+ created_at: '2025-05-23T01:00:00.363Z',
+ updated_at: '2025-05-23T01:00:00.363Z',
+ },
+ {
+ id: 1316985,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-21T00:00:00.000Z',
+ daily_apy: '1.975900247716129148',
+ created_at: '2025-05-22T01:00:00.244Z',
+ updated_at: '2025-05-22T01:00:00.244Z',
+ },
+ {
+ id: 1307360,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-20T00:00:00.000Z',
+ daily_apy: '1.823334231538022954',
+ created_at: '2025-05-21T01:00:00.224Z',
+ updated_at: '2025-05-21T01:00:00.224Z',
+ },
+ {
+ id: 1297969,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-19T00:00:00.000Z',
+ daily_apy: '1.931945364844438938',
+ created_at: '2025-05-20T01:00:00.198Z',
+ updated_at: '2025-05-20T01:00:00.198Z',
+ },
+ {
+ id: 1288028,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-18T00:00:00.000Z',
+ daily_apy: '1.946455943490720299',
+ created_at: '2025-05-19T01:00:00.281Z',
+ updated_at: '2025-05-19T01:00:00.281Z',
+ },
+ {
+ id: 1278475,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-17T00:00:00.000Z',
+ daily_apy: '1.857306102146380254',
+ created_at: '2025-05-18T01:00:00.228Z',
+ updated_at: '2025-05-18T01:00:00.228Z',
+ },
+ {
+ id: 1268946,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-16T00:00:00.000Z',
+ daily_apy: '2.636579843247607965',
+ created_at: '2025-05-17T01:00:00.196Z',
+ updated_at: '2025-05-17T01:00:00.196Z',
+ },
+ {
+ id: 1259479,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-15T00:00:00.000Z',
+ daily_apy: '2.730951603838317644',
+ created_at: '2025-05-16T01:00:00.427Z',
+ updated_at: '2025-05-16T01:00:00.427Z',
+ },
+ {
+ id: 1250719,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-14T00:00:00.000Z',
+ daily_apy: '2.892905192811717752',
+ created_at: '2025-05-15T01:00:00.216Z',
+ updated_at: '2025-05-15T01:00:00.216Z',
+ },
+ {
+ id: 1241959,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-13T00:00:00.000Z',
+ daily_apy: '2.963932043397683281',
+ created_at: '2025-05-14T01:00:00.239Z',
+ updated_at: '2025-05-14T01:00:00.239Z',
+ },
+ {
+ id: 1233199,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-12T00:00:00.000Z',
+ daily_apy: '2.888689132021701945',
+ created_at: '2025-05-13T01:00:00.409Z',
+ updated_at: '2025-05-13T01:00:00.409Z',
+ },
+ {
+ id: 1224439,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-11T00:00:00.000Z',
+ daily_apy: '2.734279654635252893',
+ created_at: '2025-05-12T01:00:00.255Z',
+ updated_at: '2025-05-12T01:00:00.255Z',
+ },
+ {
+ id: 1215679,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-10T00:00:00.000Z',
+ daily_apy: '2.319366804802240503',
+ created_at: '2025-05-11T01:00:00.371Z',
+ updated_at: '2025-05-11T01:00:00.371Z',
+ },
+ {
+ id: 1206919,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-09T00:00:00.000Z',
+ daily_apy: '2.919990925078954060',
+ created_at: '2025-05-10T01:00:00.494Z',
+ updated_at: '2025-05-10T01:00:00.494Z',
+ },
+ {
+ id: 1198159,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-08T00:00:00.000Z',
+ daily_apy: '2.854227652040402271',
+ created_at: '2025-05-09T01:00:00.241Z',
+ updated_at: '2025-05-09T01:00:00.241Z',
+ },
+ {
+ id: 1188669,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-07T00:00:00.000Z',
+ daily_apy: '2.655421738777143586',
+ created_at: '2025-05-08T00:35:03.897Z',
+ updated_at: '2025-05-08T00:35:03.897Z',
+ },
+ {
+ id: 1179544,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-06T00:00:00.000Z',
+ daily_apy: '2.295015278636215653',
+ created_at: '2025-05-07T01:00:00.282Z',
+ updated_at: '2025-05-07T01:00:00.282Z',
+ },
+ {
+ id: 1171149,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-05T00:00:00.000Z',
+ daily_apy: '2.463374048331001162',
+ created_at: '2025-05-06T01:00:00.221Z',
+ updated_at: '2025-05-06T01:00:00.221Z',
+ },
+ {
+ id: 1162389,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-04T00:00:00.000Z',
+ daily_apy: '2.521618298931638330',
+ created_at: '2025-05-05T01:00:00.287Z',
+ updated_at: '2025-05-05T01:00:00.287Z',
+ },
+ {
+ id: 1153629,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-03T00:00:00.000Z',
+ daily_apy: '2.595101808726805420',
+ created_at: '2025-05-04T01:00:00.371Z',
+ updated_at: '2025-05-04T01:00:00.371Z',
+ },
+ {
+ id: 1144869,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-02T00:00:00.000Z',
+ daily_apy: '2.787938980201913496',
+ created_at: '2025-05-03T01:09:28.224Z',
+ updated_at: '2025-05-03T01:09:28.224Z',
+ },
+ {
+ id: 1136474,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-05-01T00:00:00.000Z',
+ daily_apy: '2.431794215797376217',
+ created_at: '2025-05-02T01:00:00.264Z',
+ updated_at: '2025-05-02T01:00:00.264Z',
+ },
+ {
+ id: 1127714,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-30T00:00:00.000Z',
+ daily_apy: '2.552364609574939420',
+ created_at: '2025-05-01T01:00:00.212Z',
+ updated_at: '2025-05-01T01:00:00.212Z',
+ },
+ {
+ id: 1118954,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-29T00:00:00.000Z',
+ daily_apy: '2.597304459121422799',
+ created_at: '2025-04-30T01:00:00.402Z',
+ updated_at: '2025-04-30T01:00:00.402Z',
+ },
+ {
+ id: 1109829,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-28T00:00:00.000Z',
+ daily_apy: '2.639328108417040417',
+ created_at: '2025-04-29T01:00:00.276Z',
+ updated_at: '2025-04-29T01:00:00.276Z',
+ },
+ {
+ id: 1100704,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-27T00:00:00.000Z',
+ daily_apy: '2.731128574803426936',
+ created_at: '2025-04-28T00:54:02.772Z',
+ updated_at: '2025-04-28T00:54:02.772Z',
+ },
+ {
+ id: 1091944,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-26T00:00:00.000Z',
+ daily_apy: '2.513730750183385288',
+ created_at: '2025-04-27T01:00:00.388Z',
+ updated_at: '2025-04-27T01:00:00.388Z',
+ },
+ {
+ id: 1083184,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-25T00:00:00.000Z',
+ daily_apy: '3.657783187026349447',
+ created_at: '2025-04-26T01:00:00.476Z',
+ updated_at: '2025-04-26T01:00:00.476Z',
+ },
+ {
+ id: 1074424,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-24T00:00:00.000Z',
+ daily_apy: '2.497561674999690265',
+ created_at: '2025-04-25T01:00:00.425Z',
+ updated_at: '2025-04-25T01:00:00.425Z',
+ },
+ {
+ id: 1065664,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-23T00:00:00.000Z',
+ daily_apy: '2.434718736272556139',
+ created_at: '2025-04-24T00:41:56.479Z',
+ updated_at: '2025-04-24T00:41:56.479Z',
+ },
+ {
+ id: 1056904,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-22T00:00:00.000Z',
+ daily_apy: '3.089233192365398230',
+ created_at: '2025-04-23T01:00:00.592Z',
+ updated_at: '2025-04-23T01:00:00.592Z',
+ },
+ {
+ id: 1048168,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-21T00:00:00.000Z',
+ daily_apy: '3.073296504073085343',
+ created_at: '2025-04-22T01:00:00.247Z',
+ updated_at: '2025-04-22T01:00:00.247Z',
+ },
+ {
+ id: 1039093,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-20T00:00:00.000Z',
+ daily_apy: '2.351103366533415929',
+ created_at: '2025-04-21T00:37:31.958Z',
+ updated_at: '2025-04-21T00:37:31.958Z',
+ },
+ {
+ id: 1030405,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-19T00:00:00.000Z',
+ daily_apy: '2.225352639174754480',
+ created_at: '2025-04-20T01:00:00.251Z',
+ updated_at: '2025-04-20T01:00:00.251Z',
+ },
+ {
+ id: 1021741,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-18T00:00:00.000Z',
+ daily_apy: '2.644886616949982688',
+ created_at: '2025-04-19T01:00:00.357Z',
+ updated_at: '2025-04-19T01:00:00.357Z',
+ },
+ {
+ id: 1012741,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-17T00:00:00.000Z',
+ daily_apy: '3.042329145189525000',
+ created_at: '2025-04-18T00:57:21.407Z',
+ updated_at: '2025-04-18T00:57:21.407Z',
+ },
+ {
+ id: 1004125,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-16T00:00:00.000Z',
+ daily_apy: '3.643247075892654388',
+ created_at: '2025-04-17T01:00:00.386Z',
+ updated_at: '2025-04-17T01:00:00.386Z',
+ },
+ {
+ id: 995533,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-15T00:00:00.000Z',
+ daily_apy: '2.520282215494650327',
+ created_at: '2025-04-16T01:00:00.359Z',
+ updated_at: '2025-04-16T01:00:00.359Z',
+ },
+ {
+ id: 986965,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-14T00:00:00.000Z',
+ daily_apy: '3.208098989378294303',
+ created_at: '2025-04-15T01:00:00.244Z',
+ updated_at: '2025-04-15T01:00:00.244Z',
+ },
+ {
+ id: 978421,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-13T00:00:00.000Z',
+ daily_apy: '2.721165617974596073',
+ created_at: '2025-04-14T01:24:47.357Z',
+ updated_at: '2025-04-14T01:24:47.357Z',
+ },
+ {
+ id: 969901,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-12T00:00:00.000Z',
+ daily_apy: '2.250107844448287445',
+ created_at: '2025-04-13T01:00:00.586Z',
+ updated_at: '2025-04-13T01:00:00.586Z',
+ },
+ {
+ id: 961405,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-11T00:00:00.000Z',
+ daily_apy: '2.344017241063298728',
+ created_at: '2025-04-12T01:00:00.643Z',
+ updated_at: '2025-04-12T01:00:00.643Z',
+ },
+ {
+ id: 952933,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-10T00:00:00.000Z',
+ daily_apy: '2.569225132702528208',
+ created_at: '2025-04-11T01:00:00.279Z',
+ updated_at: '2025-04-11T01:00:00.279Z',
+ },
+ {
+ id: 944485,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-09T00:00:00.000Z',
+ daily_apy: '3.234066352579156858',
+ created_at: '2025-04-10T01:00:00.450Z',
+ updated_at: '2025-04-10T01:00:00.450Z',
+ },
+ {
+ id: 936412,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-08T00:00:00.000Z',
+ daily_apy: '2.404572998161749392',
+ created_at: '2025-04-09T01:00:00.472Z',
+ updated_at: '2025-04-09T01:00:00.472Z',
+ },
+ {
+ id: 930462,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-07T00:00:00.000Z',
+ daily_apy: '16.274294029981817920',
+ created_at: '2025-04-08T08:00:00.412Z',
+ updated_at: '2025-04-08T08:00:00.412Z',
+ },
+ {
+ id: 922784,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-06T00:00:00.000Z',
+ daily_apy: '2.327823284439173894',
+ created_at: '2025-04-07T01:00:00.387Z',
+ updated_at: '2025-04-07T01:00:00.387Z',
+ },
+ {
+ id: 914432,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-05T00:00:00.000Z',
+ daily_apy: '2.713790128923583407',
+ created_at: '2025-04-06T01:00:00.337Z',
+ updated_at: '2025-04-06T01:00:00.337Z',
+ },
+ {
+ id: 906104,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-04T00:00:00.000Z',
+ daily_apy: '3.137879600264120575',
+ created_at: '2025-04-05T01:40:12.734Z',
+ updated_at: '2025-04-05T01:40:12.734Z',
+ },
+ {
+ id: 897800,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-03T00:00:00.000Z',
+ daily_apy: '2.749937989528119524',
+ created_at: '2025-04-04T01:00:00.477Z',
+ updated_at: '2025-04-04T01:00:00.477Z',
+ },
+ {
+ id: 889520,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-02T00:00:00.000Z',
+ daily_apy: '2.808586782003876191',
+ created_at: '2025-04-03T01:00:00.288Z',
+ updated_at: '2025-04-03T01:00:00.288Z',
+ },
+ {
+ id: 881264,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-04-01T00:00:00.000Z',
+ daily_apy: '2.641420673236496893',
+ created_at: '2025-04-02T01:00:00.522Z',
+ updated_at: '2025-04-02T01:00:00.522Z',
+ },
+ {
+ id: 873032,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-31T00:00:00.000Z',
+ daily_apy: '2.782427831689054822',
+ created_at: '2025-04-01T01:00:00.645Z',
+ updated_at: '2025-04-01T01:00:00.645Z',
+ },
+ {
+ id: 864140,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-30T00:00:00.000Z',
+ daily_apy: '2.435077125451363938',
+ created_at: '2025-03-31T01:00:00.403Z',
+ updated_at: '2025-03-31T01:00:00.403Z',
+ },
+ {
+ id: 855956,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-29T00:00:00.000Z',
+ daily_apy: '2.862289970264342588',
+ created_at: '2025-03-30T01:00:00.743Z',
+ updated_at: '2025-03-30T01:00:00.743Z',
+ },
+ {
+ id: 847796,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-28T00:00:00.000Z',
+ daily_apy: '2.777681416203091593',
+ created_at: '2025-03-29T01:00:00.472Z',
+ updated_at: '2025-03-29T01:00:00.472Z',
+ },
+ {
+ id: 835532,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-27T00:00:00.000Z',
+ daily_apy: '2.407050370521265321',
+ created_at: '2025-03-28T01:00:00.320Z',
+ updated_at: '2025-03-28T01:00:00.320Z',
+ },
+ {
+ id: 820184,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-26T00:00:00.000Z',
+ daily_apy: '2.334231120347892478',
+ created_at: '2025-03-27T01:00:00.594Z',
+ updated_at: '2025-03-27T01:00:00.594Z',
+ },
+ {
+ id: 803520,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-25T00:00:00.000Z',
+ daily_apy: '2.288783268450069690',
+ created_at: '2025-03-26T01:00:00.327Z',
+ updated_at: '2025-03-26T01:00:00.327Z',
+ },
+ {
+ id: 787248,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-24T00:00:00.000Z',
+ daily_apy: '3.207491419307942588',
+ created_at: '2025-03-25T01:00:00.352Z',
+ updated_at: '2025-03-25T01:00:00.352Z',
+ },
+ {
+ id: 771365,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-23T00:00:00.000Z',
+ daily_apy: '2.427588569162684071',
+ created_at: '2025-03-24T01:00:00.532Z',
+ updated_at: '2025-03-24T01:00:00.532Z',
+ },
+ {
+ id: 754825,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-22T00:00:00.000Z',
+ daily_apy: '2.427961598326381858',
+ created_at: '2025-03-23T01:00:00.458Z',
+ updated_at: '2025-03-23T01:00:00.458Z',
+ },
+ {
+ id: 738673,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-21T00:00:00.000Z',
+ daily_apy: '2.221222106732271571',
+ created_at: '2025-03-22T01:00:00.581Z',
+ updated_at: '2025-03-22T01:00:00.581Z',
+ },
+ {
+ id: 722908,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-20T00:00:00.000Z',
+ daily_apy: '3.109400223632458407',
+ created_at: '2025-03-21T01:00:00.403Z',
+ updated_at: '2025-03-21T01:00:00.403Z',
+ },
+ {
+ id: 706182,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-19T00:00:00.000Z',
+ daily_apy: '2.114127349815966869',
+ created_at: '2025-03-20T01:00:00.412Z',
+ updated_at: '2025-03-20T01:00:00.412Z',
+ },
+ {
+ id: 690174,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-18T00:00:00.000Z',
+ daily_apy: '1.995508052653456692',
+ created_at: '2025-03-19T01:00:00.254Z',
+ updated_at: '2025-03-19T01:00:00.254Z',
+ },
+ {
+ id: 674213,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-17T00:00:00.000Z',
+ daily_apy: '2.808134980278030199',
+ created_at: '2025-03-18T01:00:00.236Z',
+ updated_at: '2025-03-18T01:00:00.236Z',
+ },
+ {
+ id: 658682,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-16T00:00:00.000Z',
+ daily_apy: '2.046101340514063938',
+ created_at: '2025-03-17T01:00:00.504Z',
+ updated_at: '2025-03-17T01:00:00.504Z',
+ },
+ {
+ id: 642842,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-15T00:00:00.000Z',
+ daily_apy: '2.412183366907788883',
+ created_at: '2025-03-16T01:00:00.448Z',
+ updated_at: '2025-03-16T01:00:00.448Z',
+ },
+ {
+ id: 627026,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-14T00:00:00.000Z',
+ daily_apy: '2.416168547971019726',
+ created_at: '2025-03-15T01:00:01.011Z',
+ updated_at: '2025-03-15T01:00:01.011Z',
+ },
+ {
+ id: 610915,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-13T00:00:00.000Z',
+ daily_apy: '3.320387982775072235',
+ created_at: '2025-03-14T01:00:00.516Z',
+ updated_at: '2025-03-14T01:00:00.516Z',
+ },
+ {
+ id: 595171,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-12T00:00:00.000Z',
+ daily_apy: '2.398300210461381471',
+ created_at: '2025-03-13T01:00:01.132Z',
+ updated_at: '2025-03-13T01:00:01.132Z',
+ },
+ {
+ id: 579789,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-11T00:00:00.000Z',
+ daily_apy: '2.509179581886257688',
+ created_at: '2025-03-12T01:00:00.790Z',
+ updated_at: '2025-03-12T01:00:00.790Z',
+ },
+ {
+ id: 563878,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-10T00:00:00.000Z',
+ daily_apy: '3.040062426245623838',
+ created_at: '2025-03-11T01:00:00.498Z',
+ updated_at: '2025-03-11T01:00:00.498Z',
+ },
+ {
+ id: 548653,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-09T00:00:00.000Z',
+ daily_apy: '3.129336844068671681',
+ created_at: '2025-03-10T01:00:00.818Z',
+ updated_at: '2025-03-10T01:00:00.818Z',
+ },
+ {
+ id: 532798,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-08T00:00:00.000Z',
+ daily_apy: '2.863111335359804701',
+ created_at: '2025-03-09T01:00:00.390Z',
+ updated_at: '2025-03-09T01:00:00.390Z',
+ },
+ {
+ id: 517621,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-07T00:00:00.000Z',
+ daily_apy: '2.264822137341302157',
+ created_at: '2025-03-08T01:00:00.482Z',
+ updated_at: '2025-03-08T01:00:00.482Z',
+ },
+ {
+ id: 501814,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-06T00:00:00.000Z',
+ daily_apy: '2.532227088223784071',
+ created_at: '2025-03-07T01:00:00.255Z',
+ updated_at: '2025-03-07T01:00:00.255Z',
+ },
+ {
+ id: 486358,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-05T00:00:00.000Z',
+ daily_apy: '2.732211047232251383',
+ created_at: '2025-03-06T01:00:00.523Z',
+ updated_at: '2025-03-06T01:00:00.523Z',
+ },
+ {
+ id: 470926,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-04T00:00:00.000Z',
+ daily_apy: '2.512702783538479314',
+ created_at: '2025-03-05T01:00:00.556Z',
+ updated_at: '2025-03-05T01:00:00.556Z',
+ },
+ {
+ id: 455873,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-03T00:00:00.000Z',
+ daily_apy: '2.763614942213400498',
+ created_at: '2025-03-04T01:00:00.316Z',
+ updated_at: '2025-03-04T01:00:00.316Z',
+ },
+ {
+ id: 440322,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-02T00:00:00.000Z',
+ daily_apy: '3.428494642246296405',
+ created_at: '2025-03-03T01:00:00.700Z',
+ updated_at: '2025-03-03T01:00:00.700Z',
+ },
+ {
+ id: 425246,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-03-01T00:00:00.000Z',
+ daily_apy: '2.154023267416044690',
+ created_at: '2025-03-02T01:00:00.870Z',
+ updated_at: '2025-03-02T01:00:00.870Z',
+ },
+ {
+ id: 409659,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-28T00:00:00.000Z',
+ daily_apy: '2.598153256102263053',
+ created_at: '2025-03-01T01:00:00.566Z',
+ updated_at: '2025-03-01T01:00:00.566Z',
+ },
+ {
+ id: 394443,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-27T00:00:00.000Z',
+ daily_apy: '3.762723089359417810',
+ created_at: '2025-02-28T01:00:00.524Z',
+ updated_at: '2025-02-28T01:00:00.524Z',
+ },
+ {
+ id: 379274,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-26T00:00:00.000Z',
+ daily_apy: '2.229242686432259735',
+ created_at: '2025-02-27T01:00:00.610Z',
+ updated_at: '2025-02-27T01:00:00.610Z',
+ },
+ {
+ id: 364311,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-25T00:00:00.000Z',
+ daily_apy: '3.084851610358903374',
+ created_at: '2025-02-26T01:00:00.611Z',
+ updated_at: '2025-02-26T01:00:00.611Z',
+ },
+ {
+ id: 348804,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-24T00:00:00.000Z',
+ daily_apy: '2.178221453819123396',
+ created_at: '2025-02-25T01:00:00.695Z',
+ updated_at: '2025-02-25T01:00:00.695Z',
+ },
+ {
+ id: 333461,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-23T00:00:00.000Z',
+ daily_apy: '2.279118367895585177',
+ created_at: '2025-02-24T01:00:00.520Z',
+ updated_at: '2025-02-24T01:00:00.520Z',
+ },
+ {
+ id: 318485,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-22T00:00:00.000Z',
+ daily_apy: '2.162399050384939712',
+ created_at: '2025-02-23T01:00:00.438Z',
+ updated_at: '2025-02-23T01:00:00.438Z',
+ },
+ {
+ id: 303874,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-21T00:00:00.000Z',
+ daily_apy: '2.456315229864533296',
+ created_at: '2025-02-22T01:00:01.098Z',
+ updated_at: '2025-02-22T01:00:01.098Z',
+ },
+ {
+ id: 288678,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-20T00:00:00.000Z',
+ daily_apy: '3.114867534342414989',
+ created_at: '2025-02-21T01:00:00.390Z',
+ updated_at: '2025-02-21T01:00:00.390Z',
+ },
+ {
+ id: 274160,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-19T00:00:00.000Z',
+ daily_apy: '2.273150114369428540',
+ created_at: '2025-02-20T01:00:00.686Z',
+ updated_at: '2025-02-20T01:00:00.686Z',
+ },
+ {
+ id: 259375,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-18T00:00:00.000Z',
+ daily_apy: '2.601753752988867146',
+ created_at: '2025-02-19T01:00:00.460Z',
+ updated_at: '2025-02-19T01:00:00.460Z',
+ },
+ {
+ id: 244325,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-17T00:00:00.000Z',
+ daily_apy: '2.371788704658418308',
+ created_at: '2025-02-18T01:00:00.579Z',
+ updated_at: '2025-02-18T01:00:00.579Z',
+ },
+ {
+ id: 229949,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-16T00:00:00.000Z',
+ daily_apy: '2.037130166329167644',
+ created_at: '2025-02-17T01:00:00.368Z',
+ updated_at: '2025-02-17T01:00:00.368Z',
+ },
+ {
+ id: 214997,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-15T00:00:00.000Z',
+ daily_apy: '2.495509141072538330',
+ created_at: '2025-02-16T01:00:00.737Z',
+ updated_at: '2025-02-16T01:00:00.737Z',
+ },
+ {
+ id: 200715,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-14T00:00:00.000Z',
+ daily_apy: '2.760147959320520741',
+ created_at: '2025-02-15T01:00:00.521Z',
+ updated_at: '2025-02-15T01:00:00.521Z',
+ },
+ {
+ id: 185862,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-13T00:00:00.000Z',
+ daily_apy: '2.620957696005122124',
+ created_at: '2025-02-14T01:00:00.438Z',
+ updated_at: '2025-02-14T01:00:00.438Z',
+ },
+ {
+ id: 171673,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-12T00:00:00.000Z',
+ daily_apy: '3.520427702298995520',
+ created_at: '2025-02-13T01:00:00.384Z',
+ updated_at: '2025-02-13T01:00:00.384Z',
+ },
+ {
+ id: 156917,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-11T00:00:00.000Z',
+ daily_apy: '2.314556111097796460',
+ created_at: '2025-02-12T01:00:00.293Z',
+ updated_at: '2025-02-12T01:00:00.293Z',
+ },
+ {
+ id: 142517,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-10T00:00:00.000Z',
+ daily_apy: '2.171292704662784237',
+ created_at: '2025-02-11T01:00:00.521Z',
+ updated_at: '2025-02-11T01:00:00.521Z',
+ },
+ {
+ id: 128165,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-09T00:00:00.000Z',
+ daily_apy: '2.676286927153115044',
+ created_at: '2025-02-10T01:00:00.790Z',
+ updated_at: '2025-02-10T01:00:00.790Z',
+ },
+ {
+ id: 113861,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-08T00:00:00.000Z',
+ daily_apy: '2.386368022241520299',
+ created_at: '2025-02-09T01:00:00.401Z',
+ updated_at: '2025-02-09T01:00:00.401Z',
+ },
+ {
+ id: 99624,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-07T00:00:00.000Z',
+ daily_apy: '2.422358786455122843',
+ created_at: '2025-02-08T01:00:00.409Z',
+ updated_at: '2025-02-08T01:00:00.409Z',
+ },
+ {
+ id: 85398,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-06T00:00:00.000Z',
+ daily_apy: '2.265223347079687334',
+ created_at: '2025-02-07T01:00:00.464Z',
+ updated_at: '2025-02-07T01:00:00.464Z',
+ },
+ {
+ id: 71538,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-05T00:00:00.000Z',
+ daily_apy: '2.718738720546652931',
+ created_at: '2025-02-06T01:00:00.630Z',
+ updated_at: '2025-02-06T01:00:00.630Z',
+ },
+ {
+ id: 57125,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-04T00:00:00.000Z',
+ daily_apy: '2.305794112369113883',
+ created_at: '2025-02-05T01:00:00.369Z',
+ updated_at: '2025-02-05T01:00:00.369Z',
+ },
+ {
+ id: 43061,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-03T00:00:00.000Z',
+ daily_apy: '2.861341474969266577',
+ created_at: '2025-02-04T01:00:00.488Z',
+ updated_at: '2025-02-04T01:00:00.488Z',
+ },
+ {
+ id: 29343,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-02T00:00:00.000Z',
+ daily_apy: '2.214925435010173341',
+ created_at: '2025-02-03T01:00:00.681Z',
+ updated_at: '2025-02-03T01:00:00.681Z',
+ },
+ {
+ id: 15374,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-02-01T00:00:00.000Z',
+ daily_apy: '2.490612483336790321',
+ created_at: '2025-02-02T01:00:00.369Z',
+ updated_at: '2025-02-02T01:00:00.369Z',
+ },
+ {
+ id: 1453,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-31T00:00:00.000Z',
+ daily_apy: '2.384122251339697898',
+ created_at: '2025-02-01T01:00:00.892Z',
+ updated_at: '2025-02-01T01:00:00.892Z',
+ },
+ {
+ id: 1,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-30T00:00:00.000Z',
+ daily_apy: '2.588418773657355365',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 2,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-29T00:00:00.000Z',
+ daily_apy: '2.369512244109374004',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 3,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-28T00:00:00.000Z',
+ daily_apy: '2.562467575560847069',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 4,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-27T00:00:00.000Z',
+ daily_apy: '2.911412020118756416',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 5,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-26T00:00:00.000Z',
+ daily_apy: '2.438629958970523673',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 6,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-25T00:00:00.000Z',
+ daily_apy: '2.153496352059892035',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 7,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-24T00:00:00.000Z',
+ daily_apy: '2.114212655218078374',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 8,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-23T00:00:00.000Z',
+ daily_apy: '2.003296807378464491',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 9,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-22T00:00:00.000Z',
+ daily_apy: '2.687482649277195133',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 10,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-21T00:00:00.000Z',
+ daily_apy: '2.861342120295913440',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 11,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-20T00:00:00.000Z',
+ daily_apy: '3.774434617124937777',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 12,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-19T00:00:00.000Z',
+ daily_apy: '2.663583581696357190',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 13,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-18T00:00:00.000Z',
+ daily_apy: '2.502571414414859403',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 14,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-17T00:00:00.000Z',
+ daily_apy: '2.515897726411376051',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 15,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-16T00:00:00.000Z',
+ daily_apy: '3.200407596144865155',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 16,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-15T00:00:00.000Z',
+ daily_apy: '3.120664973229844248',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 17,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-14T00:00:00.000Z',
+ daily_apy: '3.077150313762752157',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 18,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-13T00:00:00.000Z',
+ daily_apy: '2.707778270294490265',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 19,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-12T00:00:00.000Z',
+ daily_apy: '2.433065699826669027',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 20,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-11T00:00:00.000Z',
+ daily_apy: '2.527220371813221903',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 21,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-10T00:00:00.000Z',
+ daily_apy: '2.519988313771537168',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 22,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-09T00:00:00.000Z',
+ daily_apy: '2.398646948217626051',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 23,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-08T00:00:00.000Z',
+ daily_apy: '2.733806455540747566',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 24,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-07T00:00:00.000Z',
+ daily_apy: '2.328383448288102046',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 25,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-06T00:00:00.000Z',
+ daily_apy: '2.308889054919307301',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 26,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-05T00:00:00.000Z',
+ daily_apy: '2.667287457568427655',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 27,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-04T00:00:00.000Z',
+ daily_apy: '2.769141076722838274',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 28,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-03T00:00:00.000Z',
+ daily_apy: '2.172711392137953816',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 29,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-02T00:00:00.000Z',
+ daily_apy: '2.380809034996447898',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 30,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2025-01-01T00:00:00.000Z',
+ daily_apy: '2.316257371201709071',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 31,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-31T00:00:00.000Z',
+ daily_apy: '2.381666501520736228',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 32,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-30T00:00:00.000Z',
+ daily_apy: '3.290020080906444690',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 33,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-29T00:00:00.000Z',
+ daily_apy: '2.615126145462823341',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 34,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-28T00:00:00.000Z',
+ daily_apy: '2.557189631387469027',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 35,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-27T00:00:00.000Z',
+ daily_apy: '2.158458164262885066',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 36,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-26T00:00:00.000Z',
+ daily_apy: '2.206510584899516980',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 37,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-25T00:00:00.000Z',
+ daily_apy: '2.158614798202507662',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 38,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-24T00:00:00.000Z',
+ daily_apy: '2.295841281173372788',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 39,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-23T00:00:00.000Z',
+ daily_apy: '2.537213663964201538',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 40,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-22T00:00:00.000Z',
+ daily_apy: '2.526363960719146691',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 41,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-21T00:00:00.000Z',
+ daily_apy: '2.827957235815345022',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 42,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-20T00:00:00.000Z',
+ daily_apy: '1.923858579064909181',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 43,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-19T00:00:00.000Z',
+ daily_apy: '2.312101511338184403',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 44,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-18T00:00:00.000Z',
+ daily_apy: '2.379774509103764028',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 45,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-17T00:00:00.000Z',
+ daily_apy: '2.139060810693860797',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 46,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-16T00:00:00.000Z',
+ daily_apy: '2.961584998909087010',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 47,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-15T00:00:00.000Z',
+ daily_apy: '2.379826551660605144',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 48,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-14T00:00:00.000Z',
+ daily_apy: '2.510051909115494469',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 49,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-13T00:00:00.000Z',
+ daily_apy: '2.306349129890238053',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 50,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-12T00:00:00.000Z',
+ daily_apy: '2.769878005945986504',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 51,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-11T00:00:00.000Z',
+ daily_apy: '2.114872475542218916',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 52,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-10T00:00:00.000Z',
+ daily_apy: '2.155594623401686062',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 53,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-09T00:00:00.000Z',
+ daily_apy: '2.710501371113315376',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 54,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-08T00:00:00.000Z',
+ daily_apy: '2.399345980776163772',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 55,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-07T00:00:00.000Z',
+ daily_apy: '2.908041198804088108',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 56,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-06T00:00:00.000Z',
+ daily_apy: '2.145060119823427799',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 57,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-05T00:00:00.000Z',
+ daily_apy: '2.841421083128894904',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 58,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-04T00:00:00.000Z',
+ daily_apy: '3.308055911325480863',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 59,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-03T00:00:00.000Z',
+ daily_apy: '2.686695914242465032',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 60,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-02T00:00:00.000Z',
+ daily_apy: '2.536785079364388570',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 61,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-12-01T00:00:00.000Z',
+ daily_apy: '2.648720732497768971',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 62,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-30T00:00:00.000Z',
+ daily_apy: '2.082197028966412998',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 63,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-29T00:00:00.000Z',
+ daily_apy: '2.340216070995490985',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 64,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-28T00:00:00.000Z',
+ daily_apy: '3.319977937666746903',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 65,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-27T00:00:00.000Z',
+ daily_apy: '3.703776281413848230',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 66,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-26T00:00:00.000Z',
+ daily_apy: '3.013172805913127821',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 67,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-25T00:00:00.000Z',
+ daily_apy: '2.365903942695459458',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 68,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-24T00:00:00.000Z',
+ daily_apy: '3.071031581384699336',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 69,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-23T00:00:00.000Z',
+ daily_apy: '3.129977735007652931',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 70,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-22T00:00:00.000Z',
+ daily_apy: '2.244804237084751327',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 71,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-21T00:00:00.000Z',
+ daily_apy: '1.950710572454624004',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 72,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-20T00:00:00.000Z',
+ daily_apy: '3.239912566365526493',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 73,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-19T00:00:00.000Z',
+ daily_apy: '2.186565614119543639',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 74,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-18T00:00:00.000Z',
+ daily_apy: '2.500262242085932965',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 75,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-17T00:00:00.000Z',
+ daily_apy: '2.750629904747951715',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 76,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-16T00:00:00.000Z',
+ daily_apy: '2.276368493096229591',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 77,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-15T00:00:00.000Z',
+ daily_apy: '4.185305614351850885',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 78,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-14T00:00:00.000Z',
+ daily_apy: '4.054927851639739823',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 79,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-13T00:00:00.000Z',
+ daily_apy: '2.909077278596095022',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 80,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-12T00:00:00.000Z',
+ daily_apy: '2.307813822034531527',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 81,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-11T00:00:00.000Z',
+ daily_apy: '1.998366010898116261',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 82,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-10T00:00:00.000Z',
+ daily_apy: '2.785830933199919137',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 83,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-09T00:00:00.000Z',
+ daily_apy: '2.285879836319064159',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 84,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-08T00:00:00.000Z',
+ daily_apy: '2.473256195837844616',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 85,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-07T00:00:00.000Z',
+ daily_apy: '3.098427253806472069',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 86,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-06T00:00:00.000Z',
+ daily_apy: '2.698985513984858462',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 87,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-05T00:00:00.000Z',
+ daily_apy: '2.600165108949929204',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 88,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-04T00:00:00.000Z',
+ daily_apy: '3.047092823478128359',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 89,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-03T00:00:00.000Z',
+ daily_apy: '2.262961094949021405',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 90,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-02T00:00:00.000Z',
+ daily_apy: '2.472755536754316040',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 91,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-11-01T00:00:00.000Z',
+ daily_apy: '2.213646791454453485',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 92,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-31T00:00:00.000Z',
+ daily_apy: '1.995347106718121350',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 93,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-30T00:00:00.000Z',
+ daily_apy: '3.662338703533022788',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 94,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-29T00:00:00.000Z',
+ daily_apy: '3.998958366621137168',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 95,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-28T00:00:00.000Z',
+ daily_apy: '1.986877342949697677',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 96,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-27T00:00:00.000Z',
+ daily_apy: '2.759116101988913662',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 97,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-26T00:00:00.000Z',
+ daily_apy: '2.190927465124786394',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 98,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-25T00:00:00.000Z',
+ daily_apy: '2.536275873721945520',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 99,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-24T00:00:00.000Z',
+ daily_apy: '3.024505178229807577',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 100,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-23T00:00:00.000Z',
+ daily_apy: '4.579748020726853208',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 101,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-22T00:00:00.000Z',
+ daily_apy: '2.081328937349204369',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 102,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-21T00:00:00.000Z',
+ daily_apy: '2.184895085506485232',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 103,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-20T00:00:00.000Z',
+ daily_apy: '2.398880991852390265',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 104,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-19T00:00:00.000Z',
+ daily_apy: '2.858904340345552600',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 105,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-18T00:00:00.000Z',
+ daily_apy: '2.657885568442611670',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 106,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-17T00:00:00.000Z',
+ daily_apy: '3.193858105866704867',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 107,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-16T00:00:00.000Z',
+ daily_apy: '2.620667694611860785',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 108,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-15T00:00:00.000Z',
+ daily_apy: '2.257808905335986283',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 109,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-14T00:00:00.000Z',
+ daily_apy: '3.048205889855458503',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 110,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-13T00:00:00.000Z',
+ daily_apy: '3.491271068634706969',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 111,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-12T00:00:00.000Z',
+ daily_apy: '2.271615154691328816',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 112,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-11T00:00:00.000Z',
+ daily_apy: '4.405412493954814878',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 113,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-10T00:00:00.000Z',
+ daily_apy: '2.503050109321094303',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 114,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-09T00:00:00.000Z',
+ daily_apy: '2.548443687968629314',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 115,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-08T00:00:00.000Z',
+ daily_apy: '4.136252852174733518',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 116,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-07T00:00:00.000Z',
+ daily_apy: '2.485272772198996128',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 117,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-06T00:00:00.000Z',
+ daily_apy: '2.479277102403876272',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 118,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-05T00:00:00.000Z',
+ daily_apy: '2.837898081632334126',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 119,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-04T00:00:00.000Z',
+ daily_apy: '2.934606383096206361',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 120,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-03T00:00:00.000Z',
+ daily_apy: '2.321912203129618805',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 121,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-02T00:00:00.000Z',
+ daily_apy: '4.075864607847539878',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 122,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-10-01T00:00:00.000Z',
+ daily_apy: '3.492224428286716040',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 123,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-30T00:00:00.000Z',
+ daily_apy: '3.043821641472397732',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 124,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-29T00:00:00.000Z',
+ daily_apy: '2.613509495777192257',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 125,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-28T00:00:00.000Z',
+ daily_apy: '3.063007925443190708',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 126,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-27T00:00:00.000Z',
+ daily_apy: '2.378093521658078927',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 127,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-26T00:00:00.000Z',
+ daily_apy: '2.994244265878227378',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 128,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-25T00:00:00.000Z',
+ daily_apy: '2.745946218296348711',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 129,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-24T00:00:00.000Z',
+ daily_apy: '2.405584015735161504',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 130,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-23T00:00:00.000Z',
+ daily_apy: '2.989605996702352434',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 131,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-22T00:00:00.000Z',
+ daily_apy: '3.133819559469009126',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 132,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-21T00:00:00.000Z',
+ daily_apy: '3.281163916757090874',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 133,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-20T00:00:00.000Z',
+ daily_apy: '2.421092014783149226',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 134,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-19T00:00:00.000Z',
+ daily_apy: '2.022003308779013827',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 135,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-18T00:00:00.000Z',
+ daily_apy: '4.529200757864845022',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 136,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-17T00:00:00.000Z',
+ daily_apy: '3.814667283365271847',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 137,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-16T00:00:00.000Z',
+ daily_apy: '2.476474847261134347',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 138,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-15T00:00:00.000Z',
+ daily_apy: '2.220164627763467091',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 139,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-14T00:00:00.000Z',
+ daily_apy: '2.169901379130526991',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 140,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-13T00:00:00.000Z',
+ daily_apy: '2.541326517193025111',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 141,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-12T00:00:00.000Z',
+ daily_apy: '1.954328493653199558',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 142,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-11T00:00:00.000Z',
+ daily_apy: '4.072906334326804757',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 143,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-10T00:00:00.000Z',
+ daily_apy: '2.739440400339635841',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 144,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-09T00:00:00.000Z',
+ daily_apy: '2.273344513516595409',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 145,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-08T00:00:00.000Z',
+ daily_apy: '3.217442261940307633',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 146,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-07T00:00:00.000Z',
+ daily_apy: '4.105387731991787334',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 147,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-06T00:00:00.000Z',
+ daily_apy: '2.563906839247964270',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 148,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-05T00:00:00.000Z',
+ daily_apy: '2.380780361973878761',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 149,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-04T00:00:00.000Z',
+ daily_apy: '2.010176278212447677',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 150,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-03T00:00:00.000Z',
+ daily_apy: '2.014879575261188330',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 151,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-02T00:00:00.000Z',
+ daily_apy: '2.391696310813180586',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 152,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-09-01T00:00:00.000Z',
+ daily_apy: '2.691837002881187721',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 153,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-31T00:00:00.000Z',
+ daily_apy: '2.848137731334544082',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 154,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-30T00:00:00.000Z',
+ daily_apy: '2.625672748211783960',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 155,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-29T00:00:00.000Z',
+ daily_apy: '3.172130768572289602',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 156,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-28T00:00:00.000Z',
+ daily_apy: '2.224165802625017824',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 157,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-27T00:00:00.000Z',
+ daily_apy: '2.562155658467889340',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 158,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-26T00:00:00.000Z',
+ daily_apy: '2.242707816052420686',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 159,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-25T00:00:00.000Z',
+ daily_apy: '2.890637338773570631',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 160,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-24T00:00:00.000Z',
+ daily_apy: '1.953735894063520686',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 161,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-23T00:00:00.000Z',
+ daily_apy: '2.341905215163377655',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 162,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-22T00:00:00.000Z',
+ daily_apy: '2.536577572533897118',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 163,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-21T00:00:00.000Z',
+ daily_apy: '2.936813438123844461',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 164,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-20T00:00:00.000Z',
+ daily_apy: '2.331715949254856433',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 165,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-19T00:00:00.000Z',
+ daily_apy: '2.567146482818828263',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 166,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-18T00:00:00.000Z',
+ daily_apy: '4.105202614977249668',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 167,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-17T00:00:00.000Z',
+ daily_apy: '2.997171370184038606',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 168,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-16T00:00:00.000Z',
+ daily_apy: '2.407456734779822954',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 169,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-15T00:00:00.000Z',
+ daily_apy: '2.454080118573422788',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 170,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-14T00:00:00.000Z',
+ daily_apy: '2.002963922567441482',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 171,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-13T00:00:00.000Z',
+ daily_apy: '2.012008626631090653',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 172,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-12T00:00:00.000Z',
+ daily_apy: '2.028749136065413606',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 173,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-11T00:00:00.000Z',
+ daily_apy: '1.976160574262442644',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 174,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-10T00:00:00.000Z',
+ daily_apy: '1.948307879992547788',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 175,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-09T00:00:00.000Z',
+ daily_apy: '2.586055369837104757',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 176,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-08T00:00:00.000Z',
+ daily_apy: '3.923370517729219746',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 177,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-07T00:00:00.000Z',
+ daily_apy: '1.862965024176516372',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+ {
+ id: 178,
+ chain_id: 1,
+ vault_address: '0x4fef9d741011476750a243ac70b9789a63dd47df',
+ timestamp: '2024-08-06T00:00:00.000Z',
+ daily_apy: '2.243214854150509015',
+ created_at: '2025-01-31T23:05:14.390Z',
+ updated_at: '2025-01-31T23:05:14.390Z',
+ },
+];
+
+export const STAKING_API_LENDING_RESPONSE = {
+ markets: [
+ {
+ id: '0x6ab707aca953edaefbc4fd23ba73294241490620',
+ chainId: 42161,
+ protocol: 'aave',
+ name: '0x6ab707aca953edaefbc4fd23ba73294241490620',
+ address: '0x6ab707aca953edaefbc4fd23ba73294241490620',
+ netSupplyRate: 3.8161574310387487,
+ totalSupplyRate: 3.8161574310387487,
+ rewards: [],
+ tvlUnderlying: '94482616843290',
+ underlying: {
+ address: '0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9',
+ chainId: 42161,
+ },
+ outputToken: {
+ address: '0x6ab707aca953edaefbc4fd23ba73294241490620',
+ chainId: 42161,
+ },
+ },
+ {
+ id: '0x724dc807b04555b71ed48a6896b6f41593b8c637',
+ chainId: 42161,
+ protocol: 'aave',
+ name: '0x724dc807b04555b71ed48a6896b6f41593b8c637',
+ address: '0x724dc807b04555b71ed48a6896b6f41593b8c637',
+ netSupplyRate: 3.5114225639239254,
+ totalSupplyRate: 3.5114225639239254,
+ rewards: [],
+ tvlUnderlying: '280326808784312',
+ underlying: {
+ address: '0xaf88d065e77c8cc2239327c5edb3a432268e5831',
+ chainId: 42161,
+ },
+ outputToken: {
+ address: '0x724dc807b04555b71ed48a6896b6f41593b8c637',
+ chainId: 42161,
+ },
+ },
+ {
+ id: '0x82e64f49ed5ec1bc6e43dad4fc8af9bb3a2312ee',
+ chainId: 42161,
+ protocol: 'aave',
+ name: '0x82e64f49ed5ec1bc6e43dad4fc8af9bb3a2312ee',
+ address: '0x82e64f49ed5ec1bc6e43dad4fc8af9bb3a2312ee',
+ netSupplyRate: 3.3825609249561017,
+ totalSupplyRate: 3.3825609249561017,
+ rewards: [],
+ tvlUnderlying: '7029707533530581450561709',
+ underlying: {
+ address: '0xda10009cbd5d07dd0cecc66161fc93d7c9000da1',
+ chainId: 42161,
+ },
+ outputToken: {
+ address: '0x82e64f49ed5ec1bc6e43dad4fc8af9bb3a2312ee',
+ chainId: 42161,
+ },
+ },
+ {
+ id: '0x018008bfb33d285247a21d44e50697654f754e63',
+ chainId: 1,
+ protocol: 'aave',
+ name: '0x018008bfb33d285247a21d44e50697654f754e63',
+ address: '0x018008bfb33d285247a21d44e50697654f754e63',
+ netSupplyRate: 3.246198558489036,
+ totalSupplyRate: 3.246198558489036,
+ rewards: [],
+ tvlUnderlying: '176868291129310390562110292',
+ underlying: {
+ address: '0x6b175474e89094c44da98b954eedeac495271d0f',
+ chainId: 1,
+ },
+ outputToken: {
+ address: '0x018008bfb33d285247a21d44e50697654f754e63',
+ chainId: 1,
+ },
+ },
+ {
+ id: '0x23878914efe38d27c4d67ab83ed1b93a74d4086a',
+ chainId: 1,
+ protocol: 'aave',
+ name: '0x23878914efe38d27c4d67ab83ed1b93a74d4086a',
+ address: '0x23878914efe38d27c4d67ab83ed1b93a74d4086a',
+ netSupplyRate: 3.5842156590894767,
+ totalSupplyRate: 3.5842156590894767,
+ rewards: [],
+ tvlUnderlying: '8075909063710562',
+ underlying: {
+ address: '0xdac17f958d2ee523a2206206994597c13d831ec7',
+ chainId: 1,
+ },
+ outputToken: {
+ address: '0x23878914efe38d27c4d67ab83ed1b93a74d4086a',
+ chainId: 1,
+ },
+ },
+ {
+ id: '0x98c23e9d8f34fefb1b7bd6a91b7ff122f4e16f5c',
+ chainId: 1,
+ protocol: 'aave',
+ name: '0x98c23e9d8f34fefb1b7bd6a91b7ff122f4e16f5c',
+ address: '0x98c23e9d8f34fefb1b7bd6a91b7ff122f4e16f5c',
+ netSupplyRate: 4.127418405053469,
+ totalSupplyRate: 4.127418405053469,
+ rewards: [],
+ tvlUnderlying: '3918003902274145',
+ underlying: {
+ address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
+ chainId: 1,
+ },
+ outputToken: {
+ address: '0x98c23e9d8f34fefb1b7bd6a91b7ff122f4e16f5c',
+ chainId: 1,
+ },
+ },
+ {
+ id: '0x4e65fe4dba92790696d040ac24aa414708f5c0ab',
+ chainId: 8453,
+ protocol: 'aave',
+ name: '0x4e65fe4dba92790696d040ac24aa414708f5c0ab',
+ address: '0x4e65fe4dba92790696d040ac24aa414708f5c0ab',
+ netSupplyRate: 5.1724226654358745,
+ totalSupplyRate: 5.1724226654358745,
+ rewards: [],
+ tvlUnderlying: '200378429920868',
+ underlying: {
+ address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
+ chainId: 8453,
+ },
+ outputToken: {
+ address: '0x4e65fe4dba92790696d040ac24aa414708f5c0ab',
+ chainId: 8453,
+ },
+ },
+ {
+ id: '0x374d7860c4f2f604de0191298dd393703cce84f3',
+ chainId: 59144,
+ protocol: 'aave',
+ name: '0x374d7860c4f2f604de0191298dd393703cce84f3',
+ address: '0x374d7860c4f2f604de0191298dd393703cce84f3',
+ netSupplyRate: 3.6878492921828356,
+ totalSupplyRate: 3.6878492921828356,
+ rewards: [],
+ tvlUnderlying: '1875214972417',
+ underlying: {
+ address: '0x176211869ca2b568f2a7d4ee941e073a821ee1ff',
+ chainId: 59144,
+ },
+ outputToken: {
+ address: '0x374d7860c4f2f604de0191298dd393703cce84f3',
+ chainId: 59144,
+ },
+ },
+ {
+ id: '0x88231dfec71d4ff5c1e466d08c321944a7adc673',
+ chainId: 59144,
+ protocol: 'aave',
+ name: '0x88231dfec71d4ff5c1e466d08c321944a7adc673',
+ address: '0x88231dfec71d4ff5c1e466d08c321944a7adc673',
+ netSupplyRate: 3.890851460575282,
+ totalSupplyRate: 3.890851460575282,
+ rewards: [],
+ tvlUnderlying: '550394667084',
+ underlying: {
+ address: '0xa219439258ca9da29e9cc4ce5596924745e12b93',
+ chainId: 59144,
+ },
+ outputToken: {
+ address: '0x88231dfec71d4ff5c1e466d08c321944a7adc673',
+ chainId: 59144,
+ },
+ },
+ ],
+};
diff --git a/e2e/api-mocking/mock-responses/token-api-responses.ts b/e2e/api-mocking/mock-responses/token-api-responses.ts
new file mode 100644
index 000000000000..7c31a81fc523
--- /dev/null
+++ b/e2e/api-mocking/mock-responses/token-api-responses.ts
@@ -0,0 +1,293 @@
+export const TOKEN_API_TOKENS_RESPONSE = [
+ {
+ address: '0xdac17f958d2ee523a2206206994597c13d831ec7',
+ symbol: 'USDT',
+ decimals: 6,
+ name: 'Tether USD',
+ iconUrl:
+ 'https://static.cx.metamask.io/api/v1/tokenIcons/1337/0xdac17f958d2ee523a2206206994597c13d831ec7.png',
+ aggregators: [
+ 'uniswapLabs',
+ 'metamask',
+ 'aave',
+ 'coinGecko',
+ 'openSwap',
+ 'zerion',
+ 'oneInch',
+ 'liFi',
+ 'xSwap',
+ 'socket',
+ 'rubic',
+ 'squid',
+ 'rango',
+ 'sonarwatch',
+ 'sushiSwap',
+ 'pmm',
+ 'bancor',
+ ],
+ occurrences: 17,
+ },
+ {
+ address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
+ symbol: 'USDC',
+ decimals: 6,
+ name: 'USDCoin',
+ iconUrl:
+ 'https://static.cx.metamask.io/api/v1/tokenIcons/1337/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.png',
+ aggregators: [
+ 'uniswapLabs',
+ 'metamask',
+ 'aave',
+ 'coinGecko',
+ 'openSwap',
+ 'zerion',
+ 'oneInch',
+ 'liFi',
+ 'xSwap',
+ 'socket',
+ 'rubic',
+ 'squid',
+ 'rango',
+ 'sonarwatch',
+ 'sushiSwap',
+ 'pmm',
+ 'bancor',
+ ],
+ occurrences: 17,
+ },
+ {
+ address: '0x6b175474e89094c44da98b954eedeac495271d0f',
+ symbol: 'DAI',
+ decimals: 18,
+ name: 'Dai Stablecoin',
+ iconUrl:
+ 'https://static.cx.metamask.io/api/v1/tokenIcons/1337/0x6b175474e89094c44da98b954eedeac495271d0f.png',
+ aggregators: [
+ 'uniswapLabs',
+ 'metamask',
+ 'aave',
+ 'cmc',
+ 'coinGecko',
+ 'coinMarketCap',
+ 'openSwap',
+ 'zerion',
+ 'oneInch',
+ 'liFi',
+ 'xSwap',
+ 'socket',
+ 'rubic',
+ 'squid',
+ 'rango',
+ 'sonarwatch',
+ 'sushiSwap',
+ 'pmm',
+ 'bancor',
+ ],
+ occurrences: 19,
+ },
+ {
+ address: '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2',
+ symbol: 'SUSHI',
+ decimals: 18,
+ name: 'Sushi',
+ iconUrl:
+ 'https://static.cx.metamask.io/api/v1/tokenIcons/1337/0x6b3595068778dd592e39a122f4f5a5cf09c90fe2.png',
+ aggregators: [
+ 'uniswapLabs',
+ 'metamask',
+ 'cmc',
+ 'coinGecko',
+ 'coinMarketCap',
+ 'openSwap',
+ 'zerion',
+ 'oneInch',
+ 'liFi',
+ 'xSwap',
+ 'socket',
+ 'rubic',
+ 'squid',
+ 'rango',
+ 'sonarwatch',
+ 'sushiSwap',
+ 'pmm',
+ 'bancor',
+ ],
+ occurrences: 18,
+ },
+ {
+ address: '0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9',
+ symbol: 'AAVE',
+ decimals: 18,
+ name: 'Aave',
+ iconUrl:
+ 'https://static.cx.metamask.io/api/v1/tokenIcons/1337/0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9.png',
+ aggregators: [
+ 'uniswapLabs',
+ 'metamask',
+ 'aave',
+ 'cmc',
+ 'coinGecko',
+ 'coinMarketCap',
+ 'openSwap',
+ 'zerion',
+ 'oneInch',
+ 'liFi',
+ 'xSwap',
+ 'socket',
+ 'rubic',
+ 'squid',
+ 'rango',
+ 'sonarwatch',
+ 'sushiSwap',
+ 'pmm',
+ 'bancor',
+ ],
+ occurrences: 19,
+ },
+ {
+ address: '0x111111111117dc0aa78b770fa6a738034120c302',
+ symbol: '1INCH',
+ decimals: 18,
+ name: '1inch',
+ iconUrl:
+ 'https://static.cx.metamask.io/api/v1/tokenIcons/1337/0x111111111117dc0aa78b770fa6a738034120c302.png',
+ aggregators: [
+ 'uniswapLabs',
+ 'metamask',
+ 'coinGecko',
+ 'coinMarketCap',
+ 'openSwap',
+ 'zerion',
+ 'oneInch',
+ 'liFi',
+ 'xSwap',
+ 'socket',
+ 'rubic',
+ 'squid',
+ 'rango',
+ 'sonarwatch',
+ 'sushiSwap',
+ 'pmm',
+ 'bancor',
+ ],
+ occurrences: 17,
+ },
+ {
+ address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
+ symbol: 'WETH',
+ decimals: 18,
+ name: 'Wrapped Ether',
+ iconUrl:
+ 'https://static.cx.metamask.io/api/v1/tokenIcons/1337/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2.png',
+ aggregators: [
+ 'uniswapLabs',
+ 'metamask',
+ 'aave',
+ 'coinGecko',
+ 'coinMarketCap',
+ 'openSwap',
+ 'zerion',
+ 'oneInch',
+ 'liFi',
+ 'xSwap',
+ 'socket',
+ 'rubic',
+ 'squid',
+ 'rango',
+ 'sonarwatch',
+ 'sushiSwap',
+ 'pmm',
+ ],
+ occurrences: 17,
+ },
+ {
+ address: '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599',
+ symbol: 'WBTC',
+ decimals: 8,
+ name: 'Wrapped BTC',
+ iconUrl:
+ 'https://static.cx.metamask.io/api/v1/tokenIcons/1337/0x2260fac5e5542a773aa44fbcfedf7c193bc2c599.png',
+ aggregators: [
+ 'uniswapLabs',
+ 'metamask',
+ 'aave',
+ 'cmc',
+ 'coinGecko',
+ 'coinMarketCap',
+ 'openSwap',
+ 'zerion',
+ 'oneInch',
+ 'liFi',
+ 'xSwap',
+ 'socket',
+ 'rubic',
+ 'squid',
+ 'rango',
+ 'sonarwatch',
+ 'sushiSwap',
+ 'pmm',
+ 'bancor',
+ ],
+ occurrences: 19,
+ },
+ {
+ address: '0x514910771af9ca656af840dff83e8264ecf986ca',
+ symbol: 'LINK',
+ decimals: 18,
+ name: 'ChainLink Token',
+ iconUrl:
+ 'https://static.cx.metamask.io/api/v1/tokenIcons/1337/0x514910771af9ca656af840dff83e8264ecf986ca.png',
+ aggregators: [
+ 'uniswapLabs',
+ 'metamask',
+ 'aave',
+ 'cmc',
+ 'coinGecko',
+ 'coinMarketCap',
+ 'openSwap',
+ 'zerion',
+ 'oneInch',
+ 'liFi',
+ 'xSwap',
+ 'socket',
+ 'rubic',
+ 'squid',
+ 'rango',
+ 'sonarwatch',
+ 'sushiSwap',
+ 'pmm',
+ 'bancor',
+ ],
+ occurrences: 19,
+ },
+ {
+ address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
+ symbol: 'UNI',
+ decimals: 18,
+ name: 'Uniswap',
+ iconUrl:
+ 'https://static.cx.metamask.io/api/v1/tokenIcons/1337/0x1f9840a85d5af5bf1d1762f925bdaddc4201f984.png',
+ aggregators: [
+ 'uniswapLabs',
+ 'metamask',
+ 'aave',
+ 'cmc',
+ 'coinGecko',
+ 'coinMarketCap',
+ 'openSwap',
+ 'zerion',
+ 'oneInch',
+ 'liFi',
+ 'xSwap',
+ 'socket',
+ 'rubic',
+ 'squid',
+ 'rango',
+ 'sonarwatch',
+ 'sushiSwap',
+ 'pmm',
+ 'bancor',
+ ],
+ occurrences: 19,
+ },
+];
diff --git a/e2e/framework/fixtures/FixtureHelper.ts b/e2e/framework/fixtures/FixtureHelper.ts
index 129da74987ac..c5bb7db58c79 100644
--- a/e2e/framework/fixtures/FixtureHelper.ts
+++ b/e2e/framework/fixtures/FixtureHelper.ts
@@ -41,6 +41,7 @@ import { mockNotificationServices } from '../../specs/notifications/utils/mocks'
import { type Mockttp } from 'mockttp';
import { Buffer } from 'buffer';
import crypto from 'crypto';
+import { DEFAULT_MOCKS } from '../../api-mocking/default-mocks';
const logger = createLogger({
name: 'FixtureHelper',
@@ -320,6 +321,48 @@ export const stopFixtureServer = async (fixtureServer: FixtureServer) => {
logger.debug('The fixture server is stopped');
};
+/**
+ * Merges test-specific mocks with default mocks, prioritizing test-specific mocks
+ * @param testSpecificMocks - Test-specific mock events organized by method
+ * @returns Merged mock events with test-specific mocks taking priority
+ */
+const mergeWithDefaultMocks = (
+ testSpecificMocks: TestSpecificMock | undefined,
+) => {
+ if (!testSpecificMocks) {
+ return DEFAULT_MOCKS;
+ }
+
+ const mergedMocks: TestSpecificMock = {};
+
+ // Get all HTTP methods from both test-specific and default mocks
+ const allMethods = new Set([
+ ...Object.keys(testSpecificMocks),
+ ...Object.keys(DEFAULT_MOCKS),
+ ]);
+
+ allMethods.forEach((method) => {
+ const testMocks = testSpecificMocks[method as keyof TestSpecificMock] || [];
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const defaultMocks = (DEFAULT_MOCKS as any)[method] || [];
+
+ // Create a set of URLs that already exist in test-specific mocks
+ const testMockUrls = new Set(testMocks.map((mock) => mock.urlEndpoint));
+
+ // Filter out default mocks that have the same URL as test-specific mocks
+ const filteredDefaultMocks = defaultMocks.filter(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (defaultMock: any) => !testMockUrls.has(defaultMock.urlEndpoint),
+ );
+
+ // Merge test-specific mocks first, then append non-duplicate default mocks
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (mergedMocks as any)[method] = [...testMocks, ...filteredDefaultMocks];
+ });
+
+ return mergedMocks;
+};
+
export const createMockAPIServer = async (
mockServerInstance?: Mockttp,
testSpecificMock?: TestSpecificMock,
@@ -351,7 +394,8 @@ export const createMockAPIServer = async (
// testSpecificMock only
if (!mockServerInstance && testSpecificMock) {
mockServerPort = getMockServerPort();
- mockServer = await startMockServer(testSpecificMock, mockServerPort);
+ const mergedMocks = mergeWithDefaultMocks(testSpecificMock);
+ mockServer = await startMockServer(mergedMocks, mockServerPort);
logger.debug(
`Mock server started from testSpecificMock on port ${mockServerPort}`,
@@ -361,7 +405,8 @@ export const createMockAPIServer = async (
// neither
if (!mockServerInstance && !testSpecificMock) {
mockServerPort = getMockServerPort();
- mockServer = await startMockServer({}, mockServerPort);
+ const mergedMocks = mergeWithDefaultMocks(testSpecificMock);
+ mockServer = await startMockServer(mergedMocks, mockServerPort);
logger.debug(
`Mock server started from testSpecificMock on port ${mockServerPort}`,
diff --git a/e2e/specs/assets/multichain/asset-list.spec.ts b/e2e/specs/assets/multichain/asset-list.spec.ts
index c80a15b082c4..863719bc57df 100644
--- a/e2e/specs/assets/multichain/asset-list.spec.ts
+++ b/e2e/specs/assets/multichain/asset-list.spec.ts
@@ -68,6 +68,7 @@ describe(SmokeNetworkAbstractions('Import Tokens'), () => {
await WalletView.tapTokenNetworkFilter();
await WalletView.tapTokenNetworkFilterAll();
const avax = WalletView.tokenInWallet('AVAX');
+ await WalletView.scrollToToken('AVAX');
await Assertions.expectElementToBeVisible(avax);
await WalletView.tapOnToken('AVAX');
await Assertions.expectElementToBeVisible(TokenOverview.sendButton);
@@ -120,9 +121,7 @@ describe(SmokeNetworkAbstractions('Import Tokens'), () => {
await WalletView.tapTokenNetworkFilter();
await WalletView.tapTokenNetworkFilterAll();
- if (device.getPlatform() === 'ios') {
- await WalletView.scrollToToken('AVAX', 'up');
- }
+ await WalletView.scrollToToken('AVAX');
await WalletView.tapOnToken('AVAX');
await Assertions.expectElementToBeVisible(TokenOverview.container);
diff --git a/e2e/specs/browser/browser-tests.spec.ts b/e2e/specs/quarantine/browser/browser-tests.failing.ts
similarity index 85%
rename from e2e/specs/browser/browser-tests.spec.ts
rename to e2e/specs/quarantine/browser/browser-tests.failing.ts
index bf821cf9e446..680ce661e37d 100644
--- a/e2e/specs/browser/browser-tests.spec.ts
+++ b/e2e/specs/quarantine/browser/browser-tests.failing.ts
@@ -1,22 +1,22 @@
-import { SmokeWalletPlatform } from '../../tags';
-import { loginToApp } from '../../viewHelper';
-import FixtureBuilder from '../../framework/fixtures/FixtureBuilder';
-import { withFixtures } from '../../framework/fixtures/FixtureHelper';
-import ExternalSites from '../../resources/externalsites.json';
-import Browser from '../../pages/Browser/BrowserView';
-import EnsWebsite from '../../pages/Browser/ExternalWebsites/EnsWebsite.ts';
-import TabBarComponent from '../../pages/wallet/TabBarComponent';
-import Assertions from '../../framework/Assertions';
-import ConnectBottomSheet from '../../pages/Browser/ConnectBottomSheet.ts';
-import RedirectWebsite from '../../pages/Browser/ExternalWebsites/RedirectWebsite.ts';
-import UniswapWebsite from '../../pages/Browser/ExternalWebsites/UniswapWebsite.ts';
-import OpenseaWebsite from '../../pages/Browser/ExternalWebsites/OpenseaWebsite.ts';
-import PancakeSwapWebsite from '../../pages/Browser/ExternalWebsites/PancakeSwapWebsite.ts';
-import DownloadFile from '../../pages/Browser/DownloadFile.ts';
-import DownloadFileWebsite from '../../pages/Browser/ExternalWebsites/DownloadFileWebsite.ts';
-import TestHelpers from '../../helpers';
-import CameraWebsite from '../../pages/Browser/ExternalWebsites/Security/CameraWebsite.ts';
-import HistoryDisclosureWebsite from '../../pages/Browser/ExternalWebsites/Security/HistoryDisclosureWebsite.ts';
+import { SmokeWalletPlatform } from '../../../tags.js';
+import { loginToApp } from '../../../viewHelper.ts';
+import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder.ts';
+import { withFixtures } from '../../../framework/fixtures/FixtureHelper.ts';
+import ExternalSites from '../../../resources/externalsites.json';
+import Browser from '../../../pages/Browser/BrowserView.ts';
+import EnsWebsite from '../../../pages/Browser/ExternalWebsites/EnsWebsite.ts';
+import TabBarComponent from '../../../pages/wallet/TabBarComponent.ts';
+import Assertions from '../../../framework/Assertions.ts';
+import ConnectBottomSheet from '../../../pages/Browser/ConnectBottomSheet.ts';
+import RedirectWebsite from '../../../pages/Browser/ExternalWebsites/RedirectWebsite.ts';
+import UniswapWebsite from '../../../pages/Browser/ExternalWebsites/UniswapWebsite.ts';
+import OpenseaWebsite from '../../../pages/Browser/ExternalWebsites/OpenseaWebsite.ts';
+import PancakeSwapWebsite from '../../../pages/Browser/ExternalWebsites/PancakeSwapWebsite.ts';
+import DownloadFile from '../../../pages/Browser/DownloadFile.ts';
+import DownloadFileWebsite from '../../../pages/Browser/ExternalWebsites/DownloadFileWebsite.ts';
+import TestHelpers from '../../../helpers.js';
+import CameraWebsite from '../../../pages/Browser/ExternalWebsites/Security/CameraWebsite.ts';
+import HistoryDisclosureWebsite from '../../../pages/Browser/ExternalWebsites/Security/HistoryDisclosureWebsite.ts';
const getHostFromURL = (url: string): string => {
try {
diff --git a/locales/languages/en.json b/locales/languages/en.json
index bd02864c8967..46de305af263 100644
--- a/locales/languages/en.json
+++ b/locales/languages/en.json
@@ -1150,6 +1150,9 @@
"available_balance": "Available Balance",
"margin_used": "Margin Used",
"total_unrealized_pnl": "Total Unrealized P&L"
+ },
+ "tpsl": {
+ "update_success": "TP/SL updated successfully"
}
},
"markets": {