Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "recent" swaps section to token to buy list #5956

Merged
merged 6 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ interface SectionHeaderProp {
}

const SECTION_HEADER_INFO: { [id in AssetToBuySectionId]: SectionHeaderProp } = {
recent: {
title: i18n.t(i18n.l.token_search.section_header.recent),
symbol: '􀐫',
color: 'rgba(38, 143, 255, 1)',
},
favorites: {
title: i18n.t(i18n.l.token_search.section_header.favorites),
symbol: '􀋃',
Expand Down
20 changes: 19 additions & 1 deletion src/__swaps__/screens/Swap/hooks/useSearchCurrencyLists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import { runOnJS, useAnimatedReaction } from 'react-native-reanimated';
import { useDebouncedCallback } from 'use-debounce';
import { TokenToBuyListItem } from '../components/TokenList/TokenToBuyList';
import { useSwapContext } from '../providers/swap-provider';
import { RecentSwap } from '@/__swaps__/types/swap';

export type AssetToBuySectionId = 'bridge' | 'favorites' | 'verified' | 'unverified' | 'other_networks';
export type AssetToBuySectionId = 'bridge' | 'recent' | 'favorites' | 'verified' | 'unverified' | 'other_networks';

export interface AssetToBuySection {
data: SearchAsset[];
Expand Down Expand Up @@ -63,6 +64,7 @@ const buildListSectionsData = ({
verifiedAssets?: SearchAsset[];
unverifiedAssets?: SearchAsset[];
crosschainExactMatches?: SearchAsset[];
recentSwaps?: RecentSwap[];
};
favoritesList: SearchAsset[] | undefined;
filteredBridgeAssetAddress: string | undefined;
Expand All @@ -80,6 +82,15 @@ const buildListSectionsData = ({
addSection('bridge', [combinedData.bridgeAsset]);
}

if (combinedData.recentSwaps?.length) {
const filteredRecents = filterAssetsFromFavoritesAndBridge({
assets: combinedData.recentSwaps,
favoritesList,
filteredBridgeAssetAddress,
});
addSection('recent', filteredRecents);
}

if (favoritesList?.length) {
const filteredFavorites = filterAssetsFromBridge({
assets: favoritesList,
Expand Down Expand Up @@ -134,6 +145,7 @@ export function useSearchCurrencyLists() {
const { internalSelectedInputAsset: assetToSell, selectedOutputChainId } = useSwapContext();

const query = useSwapsStore(state => state.outputSearchQuery.trim().toLowerCase());
const getRecentSwapsByChain = useSwapsStore(state => state.getRecentSwapsByChain);

const [state, setState] = useState({
fromChainId: assetToSell.value ? assetToSell.value.chainId ?? ChainId.mainnet : undefined,
Expand Down Expand Up @@ -224,6 +236,10 @@ export function useSearchCurrencyLists() {
})) as SearchAsset[];
}, [favorites, state.toChainId]);

const recentsForChain = useMemo(() => {
return getRecentSwapsByChain(state.toChainId);
}, [getRecentSwapsByChain, state.toChainId]);

const memoizedData = useMemo(() => {
const queryIsAddress = isAddress(query);
const keys: TokenSearchAssetKey[] = queryIsAddress ? ['address'] : ['name', 'symbol'];
Expand Down Expand Up @@ -303,6 +319,7 @@ export function useSearchCurrencyLists() {
crosschainExactMatches: crosschainMatches,
unverifiedAssets: unverifiedResults,
verifiedAssets: verifiedResults,
recentSwaps: recentsForChain,
},
favoritesList,
filteredBridgeAssetAddress: memoizedData.filteredBridgeAsset?.address,
Expand All @@ -319,5 +336,6 @@ export function useSearchCurrencyLists() {
selectedOutputChainId.value,
unverifiedAssets,
verifiedAssets,
recentsForChain,
]);
}
1 change: 1 addition & 0 deletions src/__swaps__/screens/Swap/providers/swap-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => {
},
]);

swapsStore.getState().addRecentSwap(parameters.assetToBuy as ExtendedAnimatedAssetWithColors);
clearCustomGasSettings(chainId);
NotificationManager?.postNotification('rapCompleted');
Navigation.handleAction(Routes.PROFILE_SCREEN, {});
Expand Down
7 changes: 7 additions & 0 deletions src/__swaps__/types/swap.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { ExtendedAnimatedAssetWithColors, UniqueId } from './assets';
import { SearchAsset } from './search';

export type inputKeys = 'inputAmount' | 'inputNativeValue' | 'outputAmount' | 'outputNativeValue';
export type inputMethods = inputKeys | 'slider';
export type inputValuesType = { [key in inputKeys]: number | string };
Expand All @@ -21,3 +24,7 @@ export interface RequestNewQuoteParams {
lastTypedInput: inputKeys;
outputAmount: inputValuesType['outputAmount'];
}

export type RecentSwap = {
swappedAt: number;
} & ExtendedAnimatedAssetWithColors;
Comment on lines +28 to +30
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want more metadata here?

1 change: 1 addition & 0 deletions src/languages/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -2053,6 +2053,7 @@
},
"token_search": {
"section_header": {
"recent": "Recent",
"favorites": "Favorites",
"bridge": "Bridge",
"verified": "Verified",
Expand Down
99 changes: 97 additions & 2 deletions src/state/swaps/swapsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { getSelectedGasSpeed } from '@/__swaps__/screens/Swap/hooks/useSelectedG
import { ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '@/__swaps__/types/assets';
import { ChainId } from '@/__swaps__/types/chains';
import { GasSpeed } from '@/__swaps__/types/gas';
import { RecentSwap } from '@/__swaps__/types/swap';
import { getCachedGasSuggestions } from '@/__swaps__/utils/meteorology';
import { lessThan } from '@/__swaps__/utils/numbers';
import { getDefaultSlippage } from '@/__swaps__/utils/swaps';
import { RainbowError, logger } from '@/logger';
import { getRemoteConfig } from '@/model/remoteConfig';
import { createRainbowStore } from '@/state/internal/createRainbowStore';
import { CrosschainQuote, Quote, QuoteError, Source } from '@rainbow-me/swaps';
Expand Down Expand Up @@ -34,6 +36,74 @@ export interface SwapsState {
setSource: (source: Source | 'auto') => void;
degenMode: boolean;
setDegenMode: (degenMode: boolean) => void;

// recent swaps
latestSwapAt: Map<ChainId, number>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this prop used? do we need it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gotcha cool cool

recentSwaps: Map<ChainId, RecentSwap[]>;
getRecentSwapsByChain: (chainId: ChainId) => RecentSwap[];
addRecentSwap: (asset: ExtendedAnimatedAssetWithColors) => void;
}

type StateWithTransforms = Omit<Partial<SwapsState>, 'latestSwapAt' | 'recentSwaps'> & {
latestSwapAt: Array<[ChainId, number]>;
recentSwaps: Array<[ChainId, RecentSwap[]]>;
};

function serialize(state: Partial<SwapsState>, version?: number) {
try {
const transformedStateToPersist: StateWithTransforms = {
...state,
latestSwapAt: state.latestSwapAt ? Array.from(state.latestSwapAt) : [],
recentSwaps: state.recentSwaps ? Array.from(state.recentSwaps) : [],
};

return JSON.stringify({
state: transformedStateToPersist,
version,
});
} catch (error) {
logger.error(new RainbowError('Failed to serialize state for swaps storage'), { error });
throw error;
}
}

function deserialize(serializedState: string) {
let parsedState: { state: StateWithTransforms; version: number };
try {
parsedState = JSON.parse(serializedState);
} catch (error) {
logger.error(new RainbowError('Failed to parse serialized state from swaps storage'), { error });
throw error;
}

const { state, version } = parsedState;

let recentSwaps = new Map<ChainId, RecentSwap[]>();
try {
if (state.recentSwaps) {
recentSwaps = new Map(state.recentSwaps);
}
} catch (error) {
logger.error(new RainbowError('Failed to convert recentSwaps from swaps storage'), { error });
}

let latestSwapAt: Map<ChainId, number> = new Map();
try {
if (state.latestSwapAt) {
latestSwapAt = new Map(state.latestSwapAt);
}
} catch (error) {
logger.error(new RainbowError('Failed to convert latestSwapAt from swaps storage'), { error });
}

return {
state: {
...state,
latestSwapAt,
recentSwaps,
},
version,
};
}

const updateCustomGasSettingsForFlashbots = (flashbots: boolean, chainId: ChainId) => {
Expand Down Expand Up @@ -80,15 +150,40 @@ export const swapsStore = createRainbowStore<SwapsState>(

degenMode: false,
setDegenMode: (degenMode: boolean) => set({ degenMode }),

latestSwapAt: new Map(),
recentSwaps: new Map(),
getRecentSwapsByChain: (chainId: ChainId) => {
const { recentSwaps } = get();

const chainSwaps = recentSwaps.get(chainId) || [];
return chainSwaps.sort((a, b) => b.swappedAt - a.swappedAt);
},
addRecentSwap(asset) {
const { recentSwaps, latestSwapAt } = get();
const now = Date.now();
const chainId = asset.chainId;
const chainSwaps = recentSwaps.get(chainId) || [];

const updatedSwaps = [...chainSwaps, { ...asset, swappedAt: now }].slice(-3);
recentSwaps.set(chainId, updatedSwaps);
latestSwapAt.set(chainId, now);

set({ recentSwaps: new Map(recentSwaps) });
},
}),
{
storageKey: 'swapsStore',
version: 1,
// NOTE: Only persist the settings
version: 2,
deserializer: deserialize,
serializer: serialize,
// NOTE: Only persist the following
partialize(state) {
return {
flashbots: state.flashbots,
source: state.source,
latestSwapAt: state.latestSwapAt,
recentSwaps: state.recentSwaps,
};
},
}
Expand Down
Loading