Skip to content

Commit

Permalink
Merge pull request #41 from multiversx/development
Browse files Browse the repository at this point in the history
[MEX-559] - Progressive Fetching
  • Loading branch information
EmanuelMiron authored Dec 4, 2024
2 parents ef86363 + 9d22939 commit 6720031
Show file tree
Hide file tree
Showing 10 changed files with 404 additions and 13 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [[2.1.3](https://github.com/multiversx/mx-sdk-dapp-swap/pull/41)] - 2024-12-04

- [Progressive Fetching for Tokens](https://github.com/multiversx/mx-sdk-dapp-swap/pull/40)

## [[2.1.2](https://github.com/multiversx/mx-sdk-dapp-swap/pull/39)] - 2024-11-12

- [Removed onlySafeTokens](https://github.com/multiversx/mx-sdk-dapp-swap/pull/39)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@multiversx/sdk-dapp-swap",
"version": "2.1.2",
"version": "2.1.3",
"description": "A library to hold the main logic for swapping between tokens on the MultiversX blockchain",
"author": "MultiversX",
"license": "GPL-3.0-or-later",
Expand Down
3 changes: 2 additions & 1 deletion src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ export * from './useSwapRoute';
export * from './useUnwrapEgld';
export * from './useQueryWrapper';
export * from './useIsPageVisible';
export * from './useFilteredTokens';
export * from './useRateCalculator';
export * from './useLazyQueryWrapper';
export * from './useSwapFormHandlers';
export * from './useInputAmountUsdValue';
export * from './useFetchMaintenanceFlag';

export * from './useIntersectionObserver';
197 changes: 197 additions & 0 deletions src/hooks/useFilteredTokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useAuthorizationContext } from 'components/SwapAuthorizationProvider';
import {
FilteredTokensQueryType,
GET_FILTERED_TOKENS,
GET_FILTERED_TOKENS_AND_BALANCE
} from 'queries';
import {
EsdtType,
FactoryType,
TokensPaginationType,
UserEsdtType
} from 'types';
import { getSortedTokensByUsdValue, mergeTokens } from 'utils';
import { useFetchTokenPrices } from './useFetchTokenPrices';
import { useIntersectionObserver } from './useIntersectionObserver';
import { useLazyQueryWrapper } from './useLazyQueryWrapper';

const DEFAULT_OFFSET = 0;
const DEFAULT_LIMIT = 500;
const DEFAULT_ENABLED_SWAPS = true;
const DEFAULT_PRICE_POLLING = false;
const DEFAULT_IDENTIFIERS: string[] = [];
const DEFAULT_PAGINATION: TokensPaginationType = { first: 20, after: '' };
const DEFAULT_SEARCH_INPUT = '';

interface GetTokensType {
limit?: number;
offset?: number;
identifiers?: string[];
enabledSwaps?: boolean;
pagination?: TokensPaginationType;
searchInput?: string;
}

interface UseTokensType {
pricePolling?: boolean;
observerId?: string;
searchInput?: string;
identifiers?: string[];
}

export const useFilteredTokens = (options?: UseTokensType) => {
const isInitialLoad = useRef(true);
const { client, isAuthenticated } = useAuthorizationContext();

if (!client) {
throw new Error('Swap GraphQL client not initialized');
}

const pricePolling = options?.pricePolling ?? DEFAULT_PRICE_POLLING;
const searchInput = options?.searchInput;

const [pagination, setPagination] = useState<TokensPaginationType>({
first: 20,
after: ''
});
const [hasMore, setHasMore] = useState(true);
const [currentCursor, setCurrentCursor] = useState<string>();
const [loadedCursors, setLoadedCursors] = useState<Set<string>>(new Set());

const [tokens, setTokens] = useState<UserEsdtType[]>([]);
const [wrappedEgld, setWrappedEgld] = useState<EsdtType>();
const [swapConfig, setSwapConfig] = useState<FactoryType>();
const [tokensCount, setTokensCount] = useState<number>();
let ignoreNextHasMore = false;

const { tokenPrices } = useFetchTokenPrices({
isPollingEnabled: pricePolling
});

const handleOnCompleted = (data?: FilteredTokensQueryType | null) => {
if (!data) return;

const { wrappingInfo, userTokens, factory, filteredTokens } = data;
const { edges, pageInfo, pageData } = filteredTokens;

setTokensCount(pageData?.count);
if (factory) setSwapConfig(factory);

const newWrappedEgld =
wrappingInfo && wrappingInfo.length
? wrappingInfo[0].wrappedToken
: undefined;
setWrappedEgld(newWrappedEgld);

if (!edges) return;

setCurrentCursor(edges[edges.length - 1]?.cursor);
const tokensWithBalance: UserEsdtType[] = edges.map((token) => ({
...token.node,
balance: '0',
valueUSD: '0'
}));

const mergedTokens = mergeTokens(tokensWithBalance, userTokens);
const sortedTokensWithBalance = getSortedTokensByUsdValue({
tokens: mergedTokens,
wrappedEgld: newWrappedEgld
});

setTokens((prevTokens) => mergeTokens(prevTokens, sortedTokensWithBalance));

if (!pageInfo?.hasNextPage && !ignoreNextHasMore) {
setHasMore(false);
} else {
ignoreNextHasMore = false;
}
};

const {
isError,
isLoading,
execute: getTokensTrigger
} = useLazyQueryWrapper<FilteredTokensQueryType>({
query: isAuthenticated
? GET_FILTERED_TOKENS_AND_BALANCE
: GET_FILTERED_TOKENS,
queryOptions: {
client,
onCompleted: handleOnCompleted
}
});

const getTokens = (options?: GetTokensType, continueFetching?: boolean) => {
const variables = {
limit: options?.limit ?? DEFAULT_LIMIT,
offset: options?.offset ?? DEFAULT_OFFSET,
identifiers: options?.identifiers ?? DEFAULT_IDENTIFIERS,
enabledSwaps: options?.enabledSwaps ?? DEFAULT_ENABLED_SWAPS,
pagination: options?.pagination ?? DEFAULT_PAGINATION,
searchInput: options?.searchInput ?? DEFAULT_SEARCH_INPUT
};

getTokensTrigger({
variables
});

if (continueFetching) {
ignoreNextHasMore = true;
}
};

const tokensWithUpdatedPrice = useMemo(
() =>
tokens.map((token) => {
const tokenPrice = tokenPrices?.find(
({ identifier }) => identifier === token.identifier
)?.price;

return {
...token,
price: tokenPrice ?? token.price
};
}),
[tokens, tokenPrices]
);

useEffect(() => {
if (isInitialLoad.current) {
isInitialLoad.current = false;
return;
}
setPagination({ first: 20, after: '' });
setLoadedCursors(new Set());
setHasMore(true);
getTokens({ pagination: { first: 20, after: '' }, searchInput });
}, [searchInput]);

useEffect(() => {
if (pagination.after) {
setLoadedCursors((prev) => new Set(prev).add(pagination.after as string));
getTokens({ pagination, searchInput });
}
}, [pagination]);

useIntersectionObserver({
tokens,
hasMore,
isLoading: isLoading ?? false,
observerId: options?.observerId ?? '',
loadedCursors,
currentCursor: currentCursor ?? '',
setPagination
});

return {
swapConfig,
wrappedEgld,
isTokensError: isError,
isTokensLoading: isLoading,
tokens: tokensWithUpdatedPrice,
totalTokensCount: tokensCount,
getTokens,
refetch: getTokensTrigger
};
};
51 changes: 51 additions & 0 deletions src/hooks/useIntersectionObserver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Dispatch, SetStateAction, useEffect } from 'react';
import { TokensPaginationType, UserEsdtType } from 'types';

interface UseIntersectionObserverType {
hasMore: boolean;
observerId: string;
isLoading: boolean;
currentCursor: string;
tokens: UserEsdtType[];
loadedCursors: Set<string>;
setPagination: Dispatch<SetStateAction<TokensPaginationType>>;
}

export const useIntersectionObserver = ({
tokens,
hasMore,
isLoading,
observerId,
loadedCursors,
currentCursor,
setPagination
}: UseIntersectionObserverType) => {
useEffect(() => {
if (!observerId) return;
const element = document.getElementById(observerId);
if (!element) return;

const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasMore && !isLoading) {
// Check if the current cursor is already loaded
if (currentCursor && !loadedCursors.has(currentCursor)) {
const newPagination = {
first: 20,
after: currentCursor
};

setPagination(newPagination);
}
}
},
{ threshold: 1.0 }
);

observer.observe(element);

return () => {
observer.disconnect();
};
}, [tokens, observerId]);
};
72 changes: 71 additions & 1 deletion src/queries/tokens/tokens.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { gql } from '@apollo/client';
import { EsdtType, FactoryType, UserEsdtType, WrappingInfoType } from 'types';
import {
EsdtType,
FactoryType,
FilteredTokensType,
UserEsdtType,
WrappingInfoType
} from 'types';
import {
esdtAttributes,
factoryAttributes,
Expand All @@ -13,6 +19,13 @@ export interface TokensType {
wrappingInfo: WrappingInfoType[];
}

export interface FilteredTokensQueryType {
filteredTokens: FilteredTokensType;
factory: FactoryType;
userTokens?: UserEsdtType[];
wrappingInfo: WrappingInfoType[];
}

export const GET_TOKENS = gql`
query swapPackageTokens ($identifiers: [String!], $enabledSwaps: Boolean) {
tokens(identifiers: $identifiers, enabledSwaps: $enabledSwaps) {
Expand All @@ -29,6 +42,33 @@ export const GET_TOKENS = gql`
}
`;

export const GET_FILTERED_TOKENS = gql`
query swapPackageFilteredTokens ($enabledSwaps: Boolean, $pagination: ConnectionArgs, $searchInput: String, $identifiers: [String!]) {
filteredTokens (pagination: $pagination, filters: {searchToken: $searchInput, enabledSwaps: $enabledSwaps, identifiers: $identifiers}) {
edges {
node {
${esdtAttributes}
}
cursor
}
pageInfo {
hasNextPage
}
pageData {
count
}
}
wrappingInfo {
wrappedToken {
${esdtAttributes}
}
}
factory {
${factoryAttributes}
}
}
`;

export const GET_TOKENS_AND_BALANCE = gql`
query swapPackageTokensWithBalance ($identifiers: [String!], $offset: Int, $limit: Int, $enabledSwaps: Boolean) {
tokens(identifiers: $identifiers, enabledSwaps: $enabledSwaps) {
Expand All @@ -47,3 +87,33 @@ export const GET_TOKENS_AND_BALANCE = gql`
}
}
`;

export const GET_FILTERED_TOKENS_AND_BALANCE = gql`
query swapPackageFilteredTokensWithBalance ($identifiers: [String!], $pagination: ConnectionArgs, $searchInput: String, $offset: Int, $limit: Int, $enabledSwaps: Boolean) {
filteredTokens (pagination: $pagination, filters: {searchToken: $searchInput, enabledSwaps: $enabledSwaps, identifiers: $identifiers}) {
edges {
node {
${esdtAttributes}
}
cursor
}
pageInfo {
hasNextPage
}
pageData {
count
}
}
userTokens (offset: $offset, limit: $limit) {
${userEsdtAttributes}
}
wrappingInfo {
wrappedToken {
${esdtAttributes}
}
}
factory {
${factoryAttributes}
}
}
`;
Loading

0 comments on commit 6720031

Please sign in to comment.