Skip to content

Commit d4a21bd

Browse files
authored
Refactor/transaction history with sdk, using react hook (#2662)
1 parent a84df4e commit d4a21bd

16 files changed

+1031
-1462
lines changed

src/hooks/useCowOrderToast.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
isOrderFilled,
1717
isOrderLoading,
1818
} from 'src/components/transactions/Switch/cowprotocol/cowprotocol.helpers';
19-
import { ActionFields, TransactionHistoryItemUnion } from 'src/modules/history/types';
19+
import { isCowSwapTransaction } from 'src/modules/history/types';
2020
import { useRootStore } from 'src/store/root';
2121
import { findByChainId } from 'src/ui-config/marketsConfig';
2222
import { queryKeysFactory } from 'src/ui-config/queries';
@@ -66,14 +66,10 @@ export const CowOrderToastProvider: React.FC<PropsWithChildren> = ({ children })
6666
useEffect(() => {
6767
if (transactions?.pages[0] && activeOrders.size === 0) {
6868
transactions.pages[0]
69-
.filter(
70-
(tx: TransactionHistoryItemUnion) =>
71-
tx.action === 'CowSwap' || tx.action === 'CowCollateralSwap'
72-
)
73-
.filter((tx: ActionFields['CowSwap']) => isOrderLoading(tx.status))
74-
.map((tx: TransactionHistoryItemUnion) => tx as ActionFields['CowSwap'])
75-
.filter((tx: ActionFields['CowSwap']) => !activeOrders.has(tx.orderId))
76-
.forEach((tx: ActionFields['CowSwap']) => {
69+
.filter(isCowSwapTransaction)
70+
.filter((tx) => isOrderLoading(tx.status))
71+
.filter((tx) => !activeOrders.has(tx.orderId))
72+
.forEach((tx) => {
7773
trackOrder(tx.orderId, tx.chainId);
7874
});
7975
}

src/hooks/useTransactionHistory.tsx

Lines changed: 153 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1+
import {
2+
chainId,
3+
evmAddress,
4+
OrderDirection,
5+
PageSize,
6+
useUserTransactionHistory,
7+
} from '@aave/react';
8+
import { Cursor } from '@aave/types';
19
import { OrderBookApi } from '@cowprotocol/cow-sdk';
210
import { useInfiniteQuery } from '@tanstack/react-query';
3-
import { useEffect, useState } from 'react';
11+
import { useEffect, useMemo, useRef, useState } from 'react';
412
import {
513
ADAPTER_APP_CODE,
614
HEADER_WIDGET_APP_CODE,
715
} from 'src/components/transactions/Switch/cowprotocol/cowprotocol.helpers';
816
import { isChainIdSupportedByCoWProtocol } from 'src/components/transactions/Switch/switch.constants';
17+
import { getTransactionAction, getTransactionId } from 'src/modules/history/helpers';
918
import {
1019
actionFilterMap,
1120
hasCollateralReserve,
@@ -14,12 +23,8 @@ import {
1423
hasSrcOrDestToken,
1524
HistoryFilters,
1625
TransactionHistoryItemUnion,
26+
UserTransactionItem,
1727
} from 'src/modules/history/types';
18-
import {
19-
USER_TRANSACTIONS_V2,
20-
USER_TRANSACTIONS_V2_WITH_POOL,
21-
} from 'src/modules/history/v2-user-history-query';
22-
import { USER_TRANSACTIONS_V3 } from 'src/modules/history/v3-user-history-query';
2328
import { ERC20Service } from 'src/services/Erc20Service';
2429
import { useRootStore } from 'src/store/root';
2530
import { queryKeysFactory } from 'src/ui-config/queries';
@@ -29,6 +34,13 @@ import { useShallow } from 'zustand/shallow';
2934

3035
import { useAppDataContext } from './app-data-provider/useAppDataProvider';
3136

37+
const sortTransactionsByTimestampDesc = (
38+
a: TransactionHistoryItemUnion,
39+
b: TransactionHistoryItemUnion
40+
) => {
41+
return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();
42+
};
43+
3244
export const applyTxHistoryFilters = ({
3345
searchQuery,
3446
filterQuery,
@@ -50,21 +62,23 @@ export const applyTxHistoryFilters = ({
5062
let srcToken = '';
5163
let destToken = '';
5264

65+
//SDK structure
5366
if (hasCollateralReserve(txn)) {
54-
collateralSymbol = txn.collateralReserve.symbol.toLowerCase();
55-
collateralName = txn.collateralReserve.name.toLowerCase();
67+
collateralSymbol = txn.collateral.reserve.underlyingToken.symbol.toLowerCase();
68+
collateralName = txn.collateral.reserve.underlyingToken.name.toLowerCase();
5669
}
5770

5871
if (hasPrincipalReserve(txn)) {
59-
principalSymbol = txn.principalReserve.symbol.toLowerCase();
60-
principalName = txn.principalReserve.name.toLowerCase();
72+
principalSymbol = txn.debtRepaid.reserve.underlyingToken.symbol.toLowerCase();
73+
principalName = txn.debtRepaid.reserve.underlyingToken.name.toLowerCase();
6174
}
6275

6376
if (hasReserve(txn)) {
64-
symbol = txn.reserve.symbol.toLowerCase();
65-
name = txn.reserve.name.toLowerCase();
77+
symbol = txn.reserve.underlyingToken.symbol.toLowerCase();
78+
name = txn.reserve.underlyingToken.name.toLowerCase();
6679
}
6780

81+
// CowSwap structure
6882
if (hasSrcOrDestToken(txn)) {
6983
srcToken = txn.underlyingSrcToken.symbol.toLowerCase();
7084
destToken = txn.underlyingDestToken.symbol.toLowerCase();
@@ -92,7 +106,8 @@ export const applyTxHistoryFilters = ({
92106
// apply txn type filter
93107
if (filterQuery.length > 0) {
94108
filteredTxns = filteredTxns.filter((txn: TransactionHistoryItemUnion) => {
95-
if (filterQuery.includes(actionFilterMap(txn.action))) {
109+
const action = getTransactionAction(txn);
110+
if (filterQuery.includes(actionFilterMap(action))) {
96111
return true;
97112
} else {
98113
return false;
@@ -108,89 +123,107 @@ export const useTransactionHistory = ({ isFilterActive }: { isFilterActive: bool
108123
);
109124

110125
const { reserves, loading: reservesLoading } = useAppDataContext();
111-
126+
const [sdkCursor, setSdkCursor] = useState<Cursor | null>(null);
127+
const [sdkTransactions, setSdkTransactions] = useState<UserTransactionItem[]>([]);
128+
const sdkTransactionIds = useRef<Set<string>>(new Set());
129+
const [isFetchingAllSdkPages, setIsFetchingAllSdkPages] = useState(true);
130+
const [hasLoadedInitialSdkPage, setHasLoadedInitialSdkPage] = useState(false);
112131
const [shouldKeepFetching, setShouldKeepFetching] = useState(false);
113132

114-
// Handle subgraphs with multiple markets (currently only ETH V2 and ETH V2 AMM)
115-
let selectedPool: string | undefined = undefined;
116-
if (!currentMarketData.v3 && currentMarketData.marketTitle === 'Ethereum') {
117-
selectedPool = currentMarketData.addresses.LENDING_POOL_ADDRESS_PROVIDER.toLowerCase();
118-
}
133+
const isAccountValid = account && account.length > 0;
119134

120-
interface TransactionHistoryParams {
121-
account: string;
122-
subgraphUrl: string;
123-
first: number;
124-
skip: number;
125-
v3: boolean;
126-
pool?: string;
127-
}
128-
const fetchTransactionHistory = async ({
129-
account,
130-
subgraphUrl,
131-
first,
132-
skip,
133-
v3,
134-
pool,
135-
}: TransactionHistoryParams) => {
136-
let query = '';
137-
if (v3) {
138-
query = USER_TRANSACTIONS_V3;
139-
} else if (pool) {
140-
query = USER_TRANSACTIONS_V2_WITH_POOL;
141-
} else {
142-
query = USER_TRANSACTIONS_V2;
135+
const {
136+
data: sdkData,
137+
loading: sdkLoading,
138+
error: sdkError,
139+
} = useUserTransactionHistory({
140+
market: evmAddress(currentMarketData.addresses.LENDING_POOL),
141+
user: isAccountValid
142+
? evmAddress(account as string)
143+
: evmAddress('0x0000000000000000000000000000000000000000'),
144+
chainId: chainId(currentMarketData.chainId),
145+
orderBy: { date: OrderDirection.Desc },
146+
pageSize: PageSize.Fifty,
147+
cursor: sdkCursor,
148+
});
149+
150+
useEffect(() => {
151+
setSdkCursor(null);
152+
setSdkTransactions([]);
153+
sdkTransactionIds.current.clear();
154+
setIsFetchingAllSdkPages(true);
155+
setHasLoadedInitialSdkPage(false);
156+
}, [account, currentMarketData.addresses.LENDING_POOL, currentMarketData.chainId]);
157+
158+
useEffect(() => {
159+
if (!sdkData?.items?.length) {
160+
if (!sdkLoading && !sdkData?.pageInfo?.next) {
161+
setIsFetchingAllSdkPages(false);
162+
if (!hasLoadedInitialSdkPage) {
163+
setHasLoadedInitialSdkPage(true);
164+
}
165+
}
166+
return;
143167
}
144168

145-
const requestBody = {
146-
query,
147-
variables: { userAddress: account, first, skip, pool },
148-
};
149-
try {
150-
const response = await fetch(subgraphUrl, {
151-
method: 'POST',
152-
headers: {
153-
'Content-Type': 'application/json',
154-
},
155-
body: JSON.stringify(requestBody),
156-
});
157-
158-
if (!response.ok) {
159-
throw new Error(`Network error: ${response.status} - ${response.statusText}`);
169+
const newTransactions = sdkData.items.filter((transaction) => {
170+
const transactionId = getTransactionId(transaction);
171+
if (sdkTransactionIds.current.has(transactionId)) {
172+
return false;
160173
}
174+
sdkTransactionIds.current.add(transactionId);
175+
return true;
176+
});
161177

162-
const data = await response.json();
163-
return data.data?.userTransactions || [];
164-
} catch (error) {
165-
console.error('Error fetching transaction history:', error);
166-
return [];
178+
if (newTransactions.length > 0) {
179+
setSdkTransactions((prev) => [...prev, ...newTransactions]);
180+
if (!hasLoadedInitialSdkPage) {
181+
setHasLoadedInitialSdkPage(true);
182+
}
183+
}
184+
}, [sdkData, sdkLoading, hasLoadedInitialSdkPage]);
185+
186+
useEffect(() => {
187+
if (sdkLoading) {
188+
setIsFetchingAllSdkPages(true);
189+
return;
167190
}
191+
192+
const nextCursor = sdkData?.pageInfo?.next ?? null;
193+
if (nextCursor && nextCursor !== sdkCursor) {
194+
setIsFetchingAllSdkPages(true);
195+
setSdkCursor(nextCursor);
196+
return;
197+
}
198+
199+
if (!nextCursor) {
200+
setIsFetchingAllSdkPages(false);
201+
}
202+
}, [sdkData?.pageInfo?.next, sdkLoading, sdkCursor]);
203+
204+
useEffect(() => {
205+
if (sdkError && !hasLoadedInitialSdkPage) {
206+
setHasLoadedInitialSdkPage(true);
207+
setIsFetchingAllSdkPages(false);
208+
}
209+
}, [sdkError, hasLoadedInitialSdkPage]);
210+
211+
const getSDKTransactions = (): UserTransactionItem[] => {
212+
return sdkTransactions;
168213
};
169214

170215
const fetchForDownload = async ({
171216
searchQuery,
172217
filterQuery,
173218
}: HistoryFilters): Promise<TransactionHistoryItemUnion[]> => {
174-
const allTransactions = [];
175-
const batchSize = 100;
176-
let skip = 0;
177-
let currentBatchSize = batchSize;
178-
179-
// Pagination over multiple sources is not perfect but since this is not a user facing feature, it's not noticeable
180-
while (currentBatchSize === batchSize) {
181-
const currentBatch = await fetchTransactionHistory({
182-
first: batchSize,
183-
skip: skip,
184-
account,
185-
subgraphUrl: currentMarketData.subgraphUrl ?? '',
186-
v3: !!currentMarketData.v3,
187-
pool: selectedPool,
188-
});
189-
const cowSwapOrders = await fetchCowSwapsHistory(batchSize, skip * batchSize);
190-
allTransactions.push(...currentBatch, ...cowSwapOrders);
191-
currentBatchSize = currentBatch.length;
192-
skip += batchSize;
193-
}
219+
const sdkTransactions = getSDKTransactions();
220+
const skip = 0;
221+
const allCowSwapOrders = await fetchCowSwapsHistory(PAGE_SIZE, skip);
222+
223+
const allTransactions: TransactionHistoryItemUnion[] = [
224+
...sdkTransactions,
225+
...allCowSwapOrders,
226+
];
194227

195228
const filteredTxns = applyTxHistoryFilters({ searchQuery, filterQuery, txns: allTransactions });
196229
return filteredTxns;
@@ -308,7 +341,7 @@ export const useTransactionHistory = ({ isFilterActive }: { isFilterActive: bool
308341
return {
309342
action: srcToken.isAToken ? 'CowCollateralSwap' : 'CowSwap',
310343
id: order.uid,
311-
timestamp: Math.floor(new Date(order.creationDate).getTime() / 1000),
344+
timestamp: new Date(order.creationDate).toISOString(),
312345
underlyingSrcToken: {
313346
underlyingAsset: srcToken.address,
314347
name: srcToken.name,
@@ -339,8 +372,8 @@ export const useTransactionHistory = ({ isFilterActive }: { isFilterActive: bool
339372
).then((txns) => txns.filter((txn) => txn !== null));
340373
};
341374

342-
const PAGE_SIZE = 100;
343-
// Pagination over multiple sources is not perfect but since we are using an infinite query, won't be noticeable
375+
const PAGE_SIZE = 50; //Limit SDK and CowSwap to same page size
376+
344377
const {
345378
data,
346379
fetchNextPage,
@@ -352,18 +385,10 @@ export const useTransactionHistory = ({ isFilterActive }: { isFilterActive: bool
352385
} = useInfiniteQuery({
353386
queryKey: queryKeysFactory.transactionHistory(account, currentMarketData),
354387
queryFn: async ({ pageParam = 0 }) => {
355-
const response = await fetchTransactionHistory({
356-
account,
357-
subgraphUrl: currentMarketData.subgraphUrl ?? '',
358-
first: PAGE_SIZE,
359-
skip: pageParam,
360-
v3: !!currentMarketData.v3,
361-
pool: selectedPool,
362-
});
363-
const cowSwapOrders = await fetchCowSwapsHistory(PAGE_SIZE, pageParam * PAGE_SIZE);
364-
return [...response, ...cowSwapOrders].sort((a, b) => b.timestamp - a.timestamp);
388+
const cowSwapOrders = await fetchCowSwapsHistory(PAGE_SIZE, pageParam);
389+
return cowSwapOrders.sort(sortTransactionsByTimestampDesc);
365390
},
366-
enabled: !!account && !!currentMarketData.subgraphUrl && !reservesLoading && !!reserves,
391+
enabled: !!account && !reservesLoading && !!reserves && !sdkLoading,
367392
getNextPageParam: (
368393
lastPage: TransactionHistoryItemUnion[],
369394
allPages: TransactionHistoryItemUnion[][]
@@ -377,6 +402,32 @@ export const useTransactionHistory = ({ isFilterActive }: { isFilterActive: bool
377402
initialPageParam: 0,
378403
});
379404

405+
const mergedData = useMemo(() => {
406+
if (!data) {
407+
if (sdkTransactions.length === 0) {
408+
return data;
409+
}
410+
411+
return {
412+
pageParams: [0],
413+
pages: [sdkTransactions.slice().sort(sortTransactionsByTimestampDesc)],
414+
};
415+
}
416+
417+
const pagesWithSdk = data.pages.map((page, index) => {
418+
if (index === 0) {
419+
const combined = [...sdkTransactions, ...page];
420+
return combined.sort(sortTransactionsByTimestampDesc);
421+
}
422+
return page;
423+
});
424+
425+
return {
426+
...data,
427+
pages: pagesWithSdk,
428+
};
429+
}, [data, sdkTransactions]);
430+
380431
// If filter is active, keep fetching until all data is returned so that it's guaranteed all filter results will be returned
381432
useEffect(() => {
382433
if (isFilterActive && hasNextPage && !isFetchingNextPage) {
@@ -398,15 +449,16 @@ export const useTransactionHistory = ({ isFilterActive }: { isFilterActive: bool
398449
}
399450
}, [shouldKeepFetching, fetchNextPage, reservesLoading]);
400451

452+
const isInitialSdkLoading = !hasLoadedInitialSdkPage && (sdkLoading || isFetchingAllSdkPages);
453+
401454
return {
402-
data,
455+
data: mergedData,
403456
fetchNextPage,
404457
isFetchingNextPage,
405458
hasNextPage,
406-
isLoading: reservesLoading || isLoadingHistory,
407-
isError,
408-
error,
459+
isLoading: reservesLoading || isLoadingHistory || isInitialSdkLoading,
460+
isError: isError || !!sdkError,
461+
error: error || sdkError,
409462
fetchForDownload,
410-
subgraphUrl: currentMarketData.subgraphUrl,
411463
};
412464
};

src/locales/el/messages.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/locales/en/messages.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)