Skip to content

Commit

Permalink
Add "recent" swaps section to token to buy list (#5956)
Browse files Browse the repository at this point in the history
* finish recents section

* Update src/state/swaps/swapsStore.ts

* add sorting

* fix formatting
  • Loading branch information
walmat committed Jul 31, 2024
1 parent 2256ae4 commit ae43947
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 4 deletions.
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 @@ -288,6 +288,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;
1 change: 1 addition & 0 deletions src/languages/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -2058,6 +2058,7 @@
},
"token_search": {
"section_header": {
"recent": "Recent",
"favorites": "Favorites",
"bridge": "Bridge",
"verified": "Verified",
Expand Down
102 changes: 99 additions & 3 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,10 +36,80 @@ export interface SwapsState {
setSource: (source: Source | 'auto') => void;
degenMode: boolean;
setDegenMode: (degenMode: boolean) => void;

// recent swaps
latestSwapAt: Map<ChainId, number>;
recentSwaps: Map<ChainId, RecentSwap[]>;
getRecentSwapsByChain: (chainId: ChainId) => RecentSwap[];
addRecentSwap: (asset: ExtendedAnimatedAssetWithColors) => void;

// degen mode preferences
preferredNetwork: ChainId | undefined;
setPreferredNetwork: (preferredNetwork: ChainId | undefined) => 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) => {
const gasSpeed = getSelectedGasSpeed(chainId);
if (gasSpeed !== GasSpeed.CUSTOM) return;
Expand Down Expand Up @@ -82,20 +154,44 @@ export const swapsStore = createRainbowStore<SwapsState>(

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

preferredNetwork: undefined,
setPreferredNetwork: (preferredNetwork: ChainId | undefined) => set({ preferredNetwork }),

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 {
degenMode: state.degenMode,
flashbots: state.flashbots,
preferredNetwork: state.preferredNetwork,
source: state.source,
latestSwapAt: state.latestSwapAt,
recentSwaps: state.recentSwaps,
};
},
}
Expand Down

0 comments on commit ae43947

Please sign in to comment.