Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
906f1a6
fix(perps): Don't see latest funding payments in Activity
abretonc7s Apr 10, 2026
1b112b3
debug(pr-28671): add reproduction marker
abretonc7s Apr 10, 2026
d842e61
cleanup(pr-28671): remove reproduction marker
abretonc7s Apr 10, 2026
f5c3495
fix(perps): use parallel window pagination in getFunding to prevent m…
abretonc7s Apr 10, 2026
dd34530
fix(perps): deduplicate funding records at chunk boundary timestamps
abretonc7s Apr 11, 2026
eac26df
style: fix ESLint and format issues in funding pagination
abretonc7s Apr 11, 2026
2da52f1
fix: address bugbot comments — partial window and dedup in loadMoreFu…
abretonc7s Apr 11, 2026
6d59712
fix: add catch block and refresh-race guard to loadMoreFunding
abretonc7s Apr 11, 2026
2e721d0
Merge branch 'main' into fix/tat-2057-dont-see-latest-funding-paymen
abretonc7s Apr 13, 2026
989dc5b
fix: use ref-based lock for loadMoreFunding to prevent double-fire
abretonc7s Apr 11, 2026
8d4602d
fix: update test to reflect hook no longer forwarding startTime/endTi…
abretonc7s Apr 13, 2026
5d42afc
fix(perps): auto-advance funding cursor when Funding tab is empty
abretonc7s Apr 13, 2026
5c4b874
fix(perps): fix funding dedup dropping all but one record
abretonc7s Apr 13, 2026
f2a346e
fix: stop funding pagination on error to prevent infinite retry loop
abretonc7s Apr 13, 2026
398ce42
test(perps): add loadMoreFunding unit tests for 93% new code coverage
abretonc7s Apr 13, 2026
353330b
fix(perps): stable funding sort order and preserve data on refresh error
abretonc7s Apr 13, 2026
53b3fb4
refactor(perps): extract shared deduplicateById helper
abretonc7s Apr 13, 2026
5d309b4
perf(perps): remove redundant intermediate sorts
abretonc7s Apr 13, 2026
a845e1b
cleanup(perps): use simple ASCII compare and extract MIN_SPLIT_WINDOW…
abretonc7s Apr 13, 2026
1e5875b
docs(perps): clarify funding history lookback comment accuracy
abretonc7s Apr 15, 2026
c7506b5
fix(perps): skip empty funding windows instead of stopping pagination
abretonc7s Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/components/UI/Perps/Perps.testIds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,7 @@ export const PerpsTransactionsViewSelectorsIDs = {
TAB_ORDERS: 'perps-transactions-tab-orders',
TAB_FUNDING: 'perps-transactions-tab-funding',
TAB_DEPOSITS: 'perps-transactions-tab-deposits',
FUNDING_LOAD_MORE_SPINNER: 'perps-transactions-funding-load-more-spinner',
} as const;

// ========================================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,9 @@ export const styleSheet = (params: { theme: Theme }) => {
paddingVertical: 12,
paddingHorizontal: 16,
},
loadMoreContainer: {
paddingVertical: 16,
alignItems: 'center' as const,
},
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ describe('PerpsTransactionsView', () => {
isLoading: false,
error: null,
refetch: mockRefetchTransactions,
loadMoreFunding: jest.fn().mockResolvedValue(undefined),
hasFundingMore: true,
isFetchingMoreFunding: false,
});

mockUsePerpsEventTracking.mockReturnValue({
Expand Down Expand Up @@ -299,6 +302,9 @@ describe('PerpsTransactionsView', () => {
isLoading: false,
error: null,
refetch: mockRefetch,
loadMoreFunding: jest.fn().mockResolvedValue(undefined),
hasFundingMore: true,
isFetchingMoreFunding: false,
});

renderWithProvider(<PerpsTransactionsView />, {
Expand All @@ -318,6 +324,9 @@ describe('PerpsTransactionsView', () => {
isLoading: false,
error: null,
refetch: jest.fn(),
loadMoreFunding: jest.fn().mockResolvedValue(undefined),
hasFundingMore: true,
isFetchingMoreFunding: false,
});

const component = renderWithProvider(<PerpsTransactionsView />, {
Expand Down Expand Up @@ -350,6 +359,9 @@ describe('PerpsTransactionsView', () => {
isLoading: false,
error: null,
refetch: mockRefetchTransactions,
loadMoreFunding: jest.fn().mockResolvedValue(undefined),
hasFundingMore: true,
isFetchingMoreFunding: false,
});

const component = renderWithProvider(<PerpsTransactionsView />, {
Expand All @@ -368,6 +380,9 @@ describe('PerpsTransactionsView', () => {
isLoading: false,
error: 'API Error',
refetch: jest.fn(),
loadMoreFunding: jest.fn().mockResolvedValue(undefined),
hasFundingMore: true,
isFetchingMoreFunding: false,
});

const component = renderWithProvider(<PerpsTransactionsView />, {
Expand Down Expand Up @@ -426,6 +441,9 @@ describe('PerpsTransactionsView', () => {
isLoading: false,
error: null,
refetch: jest.fn(),
loadMoreFunding: jest.fn().mockResolvedValue(undefined),
hasFundingMore: true,
isFetchingMoreFunding: false,
});

renderWithProvider(<PerpsTransactionsView />, {
Expand Down Expand Up @@ -459,6 +477,9 @@ describe('PerpsTransactionsView', () => {
isLoading: false,
error: null,
refetch: jest.fn(),
loadMoreFunding: jest.fn().mockResolvedValue(undefined),
hasFundingMore: true,
isFetchingMoreFunding: false,
});

renderWithProvider(<PerpsTransactionsView />, {
Expand All @@ -477,6 +498,9 @@ describe('PerpsTransactionsView', () => {
isLoading: false,
error: 'Network error',
refetch: jest.fn(),
loadMoreFunding: jest.fn().mockResolvedValue(undefined),
hasFundingMore: true,
isFetchingMoreFunding: false,
});

const component = renderWithProvider(<PerpsTransactionsView />, {
Expand All @@ -499,6 +523,9 @@ describe('PerpsTransactionsView', () => {
isLoading: false,
error: null,
refetch: jest.fn(),
loadMoreFunding: jest.fn().mockResolvedValue(undefined),
hasFundingMore: true,
isFetchingMoreFunding: false,
});

const component = renderWithProvider(<PerpsTransactionsView />, {
Expand Down Expand Up @@ -614,6 +641,9 @@ describe('PerpsTransactionsView', () => {
isLoading: false,
error: null,
refetch: jest.fn(),
loadMoreFunding: jest.fn().mockResolvedValue(undefined),
hasFundingMore: true,
isFetchingMoreFunding: false,
});

const component = renderWithProvider(<PerpsTransactionsView />, {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import { FlashList } from '@shopify/flash-list';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { RefreshControl, ScrollView, View } from 'react-native';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
ActivityIndicator,
RefreshControl,
ScrollView,
View,
} from 'react-native';
import { useSelector } from 'react-redux';
import { strings } from '../../../../../../locales/i18n';
import {
Expand Down Expand Up @@ -77,6 +88,9 @@ const PerpsTransactionsView: React.FC = () => {
transactions: allTransactions,
isLoading: transactionsLoading,
refetch: refreshTransactions,
loadMoreFunding,
hasFundingMore,
isFetchingMoreFunding,
} = usePerpsTransactionHistory({
skipInitialFetch: !isConnected,
accountId,
Expand Down Expand Up @@ -194,6 +208,28 @@ const PerpsTransactionsView: React.FC = () => {
}
}, [isConnected, refreshTransactions]);

// Auto-advance funding cursor when the Funding tab is empty but more data
// exists. FlashList does not reliably call onEndReached on empty lists, so
// we trigger loadMoreFunding directly when the tab shows no results.
useEffect(() => {
if (
activeFilter === 'Funding' &&
!transactionsLoading &&
!isFetchingMoreFunding &&
hasFundingMore &&
fundingTransactions.length === 0
) {
loadMoreFunding();
}
}, [
activeFilter,
transactionsLoading,
isFetchingMoreFunding,
hasFundingMore,
fundingTransactions,
loadMoreFunding,
]);
Comment thread
abretonc7s marked this conversation as resolved.

useFocusEffect(
useCallback(() => {
if (!isConnected) {
Expand Down Expand Up @@ -490,9 +526,26 @@ const PerpsTransactionsView: React.FC = () => {
item.type === 'header' ? 'header' : 'transaction'
}
ListEmptyComponent={shouldShowEmptyState ? renderEmptyState : null}
ListFooterComponent={
activeFilter === 'Funding' && isFetchingMoreFunding ? (
<View style={styles.loadMoreContainer}>
<ActivityIndicator
testID={
PerpsTransactionsViewSelectorsIDs.FUNDING_LOAD_MORE_SPINNER
}
/>
</View>
) : null
}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
onEndReached={
activeFilter === 'Funding' && hasFundingMore
? loadMoreFunding
: undefined
}
onEndReachedThreshold={0.5}
showsVerticalScrollIndicator={false}
drawDistance={
PERPS_TRANSACTIONS_HISTORY_CONSTANTS.FLASH_LIST_DRAW_DISTANCE
Expand Down
6 changes: 6 additions & 0 deletions app/components/UI/Perps/hooks/usePerpsHomeData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,9 @@ describe('usePerpsHomeData', () => {
isLoading: false,
error: null,
refetch: jest.fn().mockResolvedValue(undefined),
loadMoreFunding: jest.fn().mockResolvedValue(undefined),
hasFundingMore: true,
isFetchingMoreFunding: false,
});

// Mock sortMarkets to return markets as-is by default
Expand Down Expand Up @@ -897,6 +900,9 @@ describe('usePerpsHomeData', () => {
isLoading: false,
error: null,
refetch: jest.fn().mockResolvedValue(undefined),
loadMoreFunding: jest.fn().mockResolvedValue(undefined),
hasFundingMore: true,
isFetchingMoreFunding: false,
});

const { result } = renderHook(() => usePerpsHomeData());
Expand Down
136 changes: 132 additions & 4 deletions app/components/UI/Perps/hooks/usePerpsTransactionHistory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,8 +588,6 @@ describe('usePerpsTransactionHistory', () => {

it('uses provided parameters', async () => {
const params = {
startTime: 1640995200000,
endTime: 1640995300000,
accountId:
'eip155:1:0x1234567890123456789012345678901234567890' as CaipAccountId,
};
Expand All @@ -608,10 +606,9 @@ describe('usePerpsTransactionHistory', () => {
expect(mockProvider.getOrders).toHaveBeenCalledWith({
accountId: 'eip155:1:0x1234567890123456789012345678901234567890',
});
// startTime/endTime defaults are handled in HyperLiquidProvider via 30-day window
expect(mockProvider.getFunding).toHaveBeenCalledWith({
accountId: 'eip155:1:0x1234567890123456789012345678901234567890',
startTime: 1640995200000,
endTime: 1640995300000,
});
});

Expand Down Expand Up @@ -1369,6 +1366,137 @@ describe('usePerpsTransactionHistory', () => {
});
});

describe('loadMoreFunding', () => {
const olderFundingRaw = [
{
symbol: 'ETH',
amountUsd: '-1.00',
rate: '0.0001',
timestamp: 1638403200000,
},
];
const olderFundingTx = {
id: 'funding-1638403200000-ETH',
type: 'funding' as const,
category: 'funding_fee' as const,
title: 'Paid funding fee',
subtitle: 'ETH',
timestamp: 1638403200000,
asset: 'ETH',
fundingAmount: {
isPositive: false,
fee: '-$1.00',
feeNumber: -1,
rate: '0.01%',
},
};

async function renderAndWaitForInitialFetch() {
const hook = renderHook(() =>
usePerpsTransactionHistory({ skipInitialFetch: false }),
);
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
return hook;
}

it('fetches older funding and appends to transactions', async () => {
// Arrange
const { result } = await renderAndWaitForInitialFetch();
mockProvider.getFunding.mockResolvedValueOnce(olderFundingRaw);
mockTransformFundingToTransactions.mockReturnValueOnce([olderFundingTx]);

// Act
await act(async () => {
await result.current.loadMoreFunding();
});

// Assert
expect(mockProvider.getFunding).toHaveBeenLastCalledWith(
expect.objectContaining({
startTime: expect.any(Number),
endTime: expect.any(Number),
}),
);
expect(result.current.hasFundingMore).toBe(true);
expect(result.current.isFetchingMoreFunding).toBe(false);
});

it('skips empty windows and keeps hasFundingMore true', async () => {
// Arrange
const { result } = await renderAndWaitForInitialFetch();
mockProvider.getFunding.mockResolvedValueOnce([]);

// Act
await act(async () => {
await result.current.loadMoreFunding();
});

// Assert — cursor advances but pagination continues
expect(result.current.hasFundingMore).toBe(true);
});

it('sets hasFundingMore to false on fetch error', async () => {
// Arrange
const { result } = await renderAndWaitForInitialFetch();
mockProvider.getFunding.mockRejectedValueOnce(new Error('API error'));

// Act
await act(async () => {
await result.current.loadMoreFunding();
});

// Assert
expect(result.current.hasFundingMore).toBe(false);
expect(result.current.isFetchingMoreFunding).toBe(false);
});

it('does not fetch when hasFundingMore is false', async () => {
// Arrange
const { result } = await renderAndWaitForInitialFetch();
// Force hasFundingMore to false via error (errors still stop pagination)
mockProvider.getFunding.mockRejectedValueOnce(new Error('API error'));
await act(async () => {
await result.current.loadMoreFunding();
});
expect(result.current.hasFundingMore).toBe(false);
mockProvider.getFunding.mockClear();

// Act
await act(async () => {
await result.current.loadMoreFunding();
});

// Assert — no new call
expect(mockProvider.getFunding).not.toHaveBeenCalled();
});

it('deduplicates transactions when loadMore returns overlapping ids', async () => {
// Arrange
const { result } = await renderAndWaitForInitialFetch();
const dupFundingTx = {
...olderFundingTx,
id: 'funding-1638403200000-ETH',
};
mockProvider.getFunding.mockResolvedValueOnce(olderFundingRaw);
mockTransformFundingToTransactions.mockReturnValueOnce([
dupFundingTx,
dupFundingTx,
]);

// Act
await act(async () => {
await result.current.loadMoreFunding();
});

// Assert — no duplicates in result
const ids = result.current.transactions.map((tx) => tx.id);
const uniqueIds = new Set(ids);
expect(ids.length).toBe(uniqueIds.size);
});
});

describe('connection state transitions', () => {
it('triggers fetch when skipInitialFetch transitions from true to false', async () => {
// Reset mocks to track calls clearly
Expand Down
Loading
Loading