Skip to content
Merged
14 changes: 5 additions & 9 deletions src/hooks/useCowOrderToast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
isOrderFilled,
isOrderLoading,
} from 'src/components/transactions/Switch/cowprotocol/cowprotocol.helpers';
import { ActionFields, TransactionHistoryItemUnion } from 'src/modules/history/types';
import { isCowSwapTransaction } from 'src/modules/history/types';
import { useRootStore } from 'src/store/root';
import { findByChainId } from 'src/ui-config/marketsConfig';
import { queryKeysFactory } from 'src/ui-config/queries';
Expand Down Expand Up @@ -66,14 +66,10 @@ export const CowOrderToastProvider: React.FC<PropsWithChildren> = ({ children })
useEffect(() => {
if (transactions?.pages[0] && activeOrders.size === 0) {
transactions.pages[0]
.filter(
(tx: TransactionHistoryItemUnion) =>
tx.action === 'CowSwap' || tx.action === 'CowCollateralSwap'
)
.filter((tx: ActionFields['CowSwap']) => isOrderLoading(tx.status))
.map((tx: TransactionHistoryItemUnion) => tx as ActionFields['CowSwap'])
.filter((tx: ActionFields['CowSwap']) => !activeOrders.has(tx.orderId))
.forEach((tx: ActionFields['CowSwap']) => {
.filter(isCowSwapTransaction)
.filter((tx) => isOrderLoading(tx.status))
.filter((tx) => !activeOrders.has(tx.orderId))
.forEach((tx) => {
trackOrder(tx.orderId, tx.chainId);
});
}
Expand Down
254 changes: 153 additions & 101 deletions src/hooks/useTransactionHistory.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import {
chainId,
evmAddress,
OrderDirection,
PageSize,
useUserTransactionHistory,
} from '@aave/react';
import { Cursor } from '@aave/types';
import { OrderBookApi } from '@cowprotocol/cow-sdk';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import {
ADAPTER_APP_CODE,
HEADER_WIDGET_APP_CODE,
} from 'src/components/transactions/Switch/cowprotocol/cowprotocol.helpers';
import { isChainIdSupportedByCoWProtocol } from 'src/components/transactions/Switch/switch.constants';
import { getTransactionAction, getTransactionId } from 'src/modules/history/helpers';
import {
actionFilterMap,
hasCollateralReserve,
Expand All @@ -14,12 +23,8 @@ import {
hasSrcOrDestToken,
HistoryFilters,
TransactionHistoryItemUnion,
UserTransactionItem,
} from 'src/modules/history/types';
import {
USER_TRANSACTIONS_V2,
USER_TRANSACTIONS_V2_WITH_POOL,
} from 'src/modules/history/v2-user-history-query';
import { USER_TRANSACTIONS_V3 } from 'src/modules/history/v3-user-history-query';
import { ERC20Service } from 'src/services/Erc20Service';
import { useRootStore } from 'src/store/root';
import { queryKeysFactory } from 'src/ui-config/queries';
Expand All @@ -29,6 +34,13 @@ import { useShallow } from 'zustand/shallow';

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

const sortTransactionsByTimestampDesc = (
a: TransactionHistoryItemUnion,
b: TransactionHistoryItemUnion
) => {
return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();
};

export const applyTxHistoryFilters = ({
searchQuery,
filterQuery,
Expand All @@ -50,21 +62,23 @@ export const applyTxHistoryFilters = ({
let srcToken = '';
let destToken = '';

//SDK structure
if (hasCollateralReserve(txn)) {
collateralSymbol = txn.collateralReserve.symbol.toLowerCase();
collateralName = txn.collateralReserve.name.toLowerCase();
collateralSymbol = txn.collateral.reserve.underlyingToken.symbol.toLowerCase();
collateralName = txn.collateral.reserve.underlyingToken.name.toLowerCase();
}

if (hasPrincipalReserve(txn)) {
principalSymbol = txn.principalReserve.symbol.toLowerCase();
principalName = txn.principalReserve.name.toLowerCase();
principalSymbol = txn.debtRepaid.reserve.underlyingToken.symbol.toLowerCase();
principalName = txn.debtRepaid.reserve.underlyingToken.name.toLowerCase();
}

if (hasReserve(txn)) {
symbol = txn.reserve.symbol.toLowerCase();
name = txn.reserve.name.toLowerCase();
symbol = txn.reserve.underlyingToken.symbol.toLowerCase();
name = txn.reserve.underlyingToken.name.toLowerCase();
}

// CowSwap structure
if (hasSrcOrDestToken(txn)) {
srcToken = txn.underlyingSrcToken.symbol.toLowerCase();
destToken = txn.underlyingDestToken.symbol.toLowerCase();
Expand Down Expand Up @@ -92,7 +106,8 @@ export const applyTxHistoryFilters = ({
// apply txn type filter
if (filterQuery.length > 0) {
filteredTxns = filteredTxns.filter((txn: TransactionHistoryItemUnion) => {
if (filterQuery.includes(actionFilterMap(txn.action))) {
const action = getTransactionAction(txn);
if (filterQuery.includes(actionFilterMap(action))) {
return true;
} else {
return false;
Expand All @@ -108,89 +123,107 @@ export const useTransactionHistory = ({ isFilterActive }: { isFilterActive: bool
);

const { reserves, loading: reservesLoading } = useAppDataContext();

const [sdkCursor, setSdkCursor] = useState<Cursor | null>(null);
const [sdkTransactions, setSdkTransactions] = useState<UserTransactionItem[]>([]);
const sdkTransactionIds = useRef<Set<string>>(new Set());
const [isFetchingAllSdkPages, setIsFetchingAllSdkPages] = useState(true);
const [hasLoadedInitialSdkPage, setHasLoadedInitialSdkPage] = useState(false);
const [shouldKeepFetching, setShouldKeepFetching] = useState(false);

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

interface TransactionHistoryParams {
account: string;
subgraphUrl: string;
first: number;
skip: number;
v3: boolean;
pool?: string;
}
const fetchTransactionHistory = async ({
account,
subgraphUrl,
first,
skip,
v3,
pool,
}: TransactionHistoryParams) => {
let query = '';
if (v3) {
query = USER_TRANSACTIONS_V3;
} else if (pool) {
query = USER_TRANSACTIONS_V2_WITH_POOL;
} else {
query = USER_TRANSACTIONS_V2;
const {
data: sdkData,
loading: sdkLoading,
error: sdkError,
} = useUserTransactionHistory({
market: evmAddress(currentMarketData.addresses.LENDING_POOL),
user: isAccountValid
? evmAddress(account as string)
: evmAddress('0x0000000000000000000000000000000000000000'),
chainId: chainId(currentMarketData.chainId),
orderBy: { date: OrderDirection.Desc },
pageSize: PageSize.Fifty,
cursor: sdkCursor,
});

useEffect(() => {
setSdkCursor(null);
setSdkTransactions([]);
sdkTransactionIds.current.clear();
setIsFetchingAllSdkPages(true);
setHasLoadedInitialSdkPage(false);
}, [account, currentMarketData.addresses.LENDING_POOL, currentMarketData.chainId]);

useEffect(() => {
if (!sdkData?.items?.length) {
if (!sdkLoading && !sdkData?.pageInfo?.next) {
setIsFetchingAllSdkPages(false);
if (!hasLoadedInitialSdkPage) {
setHasLoadedInitialSdkPage(true);
}
}
return;
}

const requestBody = {
query,
variables: { userAddress: account, first, skip, pool },
};
try {
const response = await fetch(subgraphUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
});

if (!response.ok) {
throw new Error(`Network error: ${response.status} - ${response.statusText}`);
const newTransactions = sdkData.items.filter((transaction) => {
const transactionId = getTransactionId(transaction);
if (sdkTransactionIds.current.has(transactionId)) {
return false;
}
sdkTransactionIds.current.add(transactionId);
return true;
});

const data = await response.json();
return data.data?.userTransactions || [];
} catch (error) {
console.error('Error fetching transaction history:', error);
return [];
if (newTransactions.length > 0) {
setSdkTransactions((prev) => [...prev, ...newTransactions]);
if (!hasLoadedInitialSdkPage) {
setHasLoadedInitialSdkPage(true);
}
}
}, [sdkData, sdkLoading, hasLoadedInitialSdkPage]);

useEffect(() => {
if (sdkLoading) {
setIsFetchingAllSdkPages(true);
return;
}

const nextCursor = sdkData?.pageInfo?.next ?? null;
if (nextCursor && nextCursor !== sdkCursor) {
setIsFetchingAllSdkPages(true);
setSdkCursor(nextCursor);
return;
}

if (!nextCursor) {
setIsFetchingAllSdkPages(false);
}
}, [sdkData?.pageInfo?.next, sdkLoading, sdkCursor]);

useEffect(() => {
if (sdkError && !hasLoadedInitialSdkPage) {
setHasLoadedInitialSdkPage(true);
setIsFetchingAllSdkPages(false);
}
}, [sdkError, hasLoadedInitialSdkPage]);

const getSDKTransactions = (): UserTransactionItem[] => {
return sdkTransactions;
};

const fetchForDownload = async ({
searchQuery,
filterQuery,
}: HistoryFilters): Promise<TransactionHistoryItemUnion[]> => {
const allTransactions = [];
const batchSize = 100;
let skip = 0;
let currentBatchSize = batchSize;

// Pagination over multiple sources is not perfect but since this is not a user facing feature, it's not noticeable
while (currentBatchSize === batchSize) {
const currentBatch = await fetchTransactionHistory({
first: batchSize,
skip: skip,
account,
subgraphUrl: currentMarketData.subgraphUrl ?? '',
v3: !!currentMarketData.v3,
pool: selectedPool,
});
const cowSwapOrders = await fetchCowSwapsHistory(batchSize, skip * batchSize);
allTransactions.push(...currentBatch, ...cowSwapOrders);
currentBatchSize = currentBatch.length;
skip += batchSize;
}
const sdkTransactions = getSDKTransactions();
const skip = 0;
const allCowSwapOrders = await fetchCowSwapsHistory(PAGE_SIZE, skip);

const allTransactions: TransactionHistoryItemUnion[] = [
...sdkTransactions,
...allCowSwapOrders,
];

const filteredTxns = applyTxHistoryFilters({ searchQuery, filterQuery, txns: allTransactions });
return filteredTxns;
Expand Down Expand Up @@ -308,7 +341,7 @@ export const useTransactionHistory = ({ isFilterActive }: { isFilterActive: bool
return {
action: srcToken.isAToken ? 'CowCollateralSwap' : 'CowSwap',
id: order.uid,
timestamp: Math.floor(new Date(order.creationDate).getTime() / 1000),
timestamp: new Date(order.creationDate).toISOString(),
underlyingSrcToken: {
underlyingAsset: srcToken.address,
name: srcToken.name,
Expand Down Expand Up @@ -339,8 +372,8 @@ export const useTransactionHistory = ({ isFilterActive }: { isFilterActive: bool
).then((txns) => txns.filter((txn) => txn !== null));
};

const PAGE_SIZE = 100;
// Pagination over multiple sources is not perfect but since we are using an infinite query, won't be noticeable
const PAGE_SIZE = 50; //Limit SDK and CowSwap to same page size

const {
data,
fetchNextPage,
Expand All @@ -352,18 +385,10 @@ export const useTransactionHistory = ({ isFilterActive }: { isFilterActive: bool
} = useInfiniteQuery({
queryKey: queryKeysFactory.transactionHistory(account, currentMarketData),
queryFn: async ({ pageParam = 0 }) => {
const response = await fetchTransactionHistory({
account,
subgraphUrl: currentMarketData.subgraphUrl ?? '',
first: PAGE_SIZE,
skip: pageParam,
v3: !!currentMarketData.v3,
pool: selectedPool,
});
const cowSwapOrders = await fetchCowSwapsHistory(PAGE_SIZE, pageParam * PAGE_SIZE);
return [...response, ...cowSwapOrders].sort((a, b) => b.timestamp - a.timestamp);
const cowSwapOrders = await fetchCowSwapsHistory(PAGE_SIZE, pageParam);
return cowSwapOrders.sort(sortTransactionsByTimestampDesc);
},
enabled: !!account && !!currentMarketData.subgraphUrl && !reservesLoading && !!reserves,
enabled: !!account && !reservesLoading && !!reserves && !sdkLoading,
getNextPageParam: (
lastPage: TransactionHistoryItemUnion[],
allPages: TransactionHistoryItemUnion[][]
Expand All @@ -377,6 +402,32 @@ export const useTransactionHistory = ({ isFilterActive }: { isFilterActive: bool
initialPageParam: 0,
});

const mergedData = useMemo(() => {
if (!data) {
if (sdkTransactions.length === 0) {
return data;
}

return {
pageParams: [0],
pages: [sdkTransactions.slice().sort(sortTransactionsByTimestampDesc)],
};
}

const pagesWithSdk = data.pages.map((page, index) => {
if (index === 0) {
const combined = [...sdkTransactions, ...page];
return combined.sort(sortTransactionsByTimestampDesc);
}
return page;
});

return {
...data,
pages: pagesWithSdk,
};
}, [data, sdkTransactions]);

// If filter is active, keep fetching until all data is returned so that it's guaranteed all filter results will be returned
useEffect(() => {
if (isFilterActive && hasNextPage && !isFetchingNextPage) {
Expand All @@ -398,15 +449,16 @@ export const useTransactionHistory = ({ isFilterActive }: { isFilterActive: bool
}
}, [shouldKeepFetching, fetchNextPage, reservesLoading]);

const isInitialSdkLoading = !hasLoadedInitialSdkPage && (sdkLoading || isFetchingAllSdkPages);

return {
data,
data: mergedData,
fetchNextPage,
isFetchingNextPage,
hasNextPage,
isLoading: reservesLoading || isLoadingHistory,
isError,
error,
isLoading: reservesLoading || isLoadingHistory || isInitialSdkLoading,
isError: isError || !!sdkError,
error: error || sdkError,
fetchForDownload,
subgraphUrl: currentMarketData.subgraphUrl,
};
};
2 changes: 1 addition & 1 deletion src/locales/el/messages.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/locales/en/messages.js

Large diffs are not rendered by default.

Loading
Loading