diff --git a/src/__swaps__/utils/swaps.ts b/src/__swaps__/utils/swaps.ts index 924e6a1b290..4d9ec3982ea 100644 --- a/src/__swaps__/utils/swaps.ts +++ b/src/__swaps__/utils/swaps.ts @@ -1,177 +1,618 @@ -import { INITIAL_SLIDER_POSITION } from '@/__swaps__/screens/Swap/constants'; -import { ExtendedAnimatedAssetWithColors, ParsedSearchAsset, UniqueId } from '@/__swaps__/types/assets'; +import c from 'chroma-js'; +import { SharedValue, convertToRGBA, isColor } from 'react-native-reanimated'; + +import { + ETH_COLOR, + ETH_COLOR_DARK, + MAXIMUM_SIGNIFICANT_DECIMALS, + SCRUBBER_WIDTH, + SLIDER_WIDTH, + STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS, +} from '@/__swaps__/screens/Swap/constants'; +import { globalColors } from '@/design-system'; +import { ForegroundColor, palettes } from '@/design-system/color/palettes'; +import { TokenColors } from '@/graphql/__generated__/metadata'; +import * as i18n from '@/languages'; +import { DEFAULT_CONFIG, RainbowConfig } from '@/model/remoteConfig'; +import store from '@/redux/store'; +import { supportedNativeCurrencies } from '@/references'; +import { userAssetsStore } from '@/state/assets/userAssets'; +import { colors } from '@/styles'; +import { BigNumberish } from '@ethersproject/bignumber'; +import { CrosschainQuote, ETH_ADDRESS as ETH_ADDRESS_AGGREGATOR, Quote, QuoteParams } from '@rainbow-me/swaps'; +import { swapsStore } from '../../state/swaps/swapsStore'; +import { + divWorklet, + equalWorklet, + greaterThanOrEqualToWorklet, + isNumberStringWorklet, + lessThanOrEqualToWorklet, + mulWorklet, + orderOfMagnitudeWorklet, + powWorklet, + roundWorklet, + toFixedWorklet, +} from '@/safe-math/SafeMath'; +import { ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '../types/assets'; +import { inputKeys } from '../types/swap'; +import { valueBasedDecimalFormatter } from './decimalFormatter'; +import { convertAmountToRawAmount } from '@/helpers/utilities'; import { ChainId } from '@/state/backendNetworks/types'; -import { RecentSwap } from '@/__swaps__/types/swap'; -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'; - -export interface SwapsState { - isSwapsOpen: boolean; - setIsSwapsOpen: (isSwapsOpen: boolean) => void; - - // assets - inputAsset: ParsedSearchAsset | ExtendedAnimatedAssetWithColors | null; - outputAsset: ParsedSearchAsset | ExtendedAnimatedAssetWithColors | null; - - // quote - quote: Quote | CrosschainQuote | QuoteError | null; - - selectedOutputChainId: ChainId; - - percentageToSell: number; // Value between 0 and 1, e.g., 0.5, 0.1, 0.25 - setPercentageToSell: (percentageToSell: number) => void; // Accepts values from 0 to 1 - - // settings - slippage: string; - setSlippage: (slippage: string) => void; - source: Source | 'auto'; - setSource: (source: Source | 'auto') => void; - degenMode: boolean; - setDegenMode: (degenMode: boolean) => void; - - // recent swaps - latestSwapAt: Map; - recentSwaps: Map; - getRecentSwapsByChain: (chainId?: ChainId) => RecentSwap[]; - addRecentSwap: (asset: ExtendedAnimatedAssetWithColors) => void; - - // degen mode preferences - preferredNetwork: ChainId | undefined; - setPreferredNetwork: (preferredNetwork: ChainId | undefined) => void; - - lastNavigatedTrendingToken: UniqueId | undefined; -} +import { getUniqueId } from '@/utils/ethereumUtils'; + +// DO NOT REMOVE THESE COMMENTED ENV VARS +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { IS_APK_BUILD } from 'react-native-dotenv'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import isTestFlight from '@/helpers/isTestFlight'; + +// /---- 🎨 Color functions 🎨 ----/ // +// +export const opacity = (color: string, opacity: number): string => { + return c(color).alpha(opacity).css(); +}; + +export type ResponseByTheme = { + light: T; + dark: T; +}; + +export const getColorValueForTheme = (values: ResponseByTheme | undefined, isDarkMode: boolean) => { + if (!values) { + return isDarkMode ? ETH_COLOR_DARK : ETH_COLOR; + } + return isDarkMode ? values.dark : values.light; +}; + +export const getColorValueForThemeWorklet = (values: ResponseByTheme | undefined, isDarkMode: boolean) => { + 'worklet'; -type StateWithTransforms = Omit, 'latestSwapAt' | 'recentSwaps'> & { - latestSwapAt: Array<[ChainId, number]>; - recentSwaps: Array<[ChainId, RecentSwap[]]>; + if (!values) { + return isDarkMode ? ETH_COLOR_DARK : ETH_COLOR; + } + return isDarkMode ? values.dark : values.light; }; -function serialize(state: Partial, version?: number) { - try { - const transformedStateToPersist: StateWithTransforms = { - ...state, - latestSwapAt: state.latestSwapAt ? Array.from(state.latestSwapAt) : [], - recentSwaps: state.recentSwaps ? Array.from(state.recentSwaps) : [], +export const getHighContrastColor = (color: string): ResponseByTheme => { + if (color === ETH_COLOR) { + return { + light: ETH_COLOR, + dark: ETH_COLOR_DARK, }; + } - return JSON.stringify({ - state: transformedStateToPersist, - version, - }); - } catch (error) { - logger.error(new RainbowError(`[swapsStore]: Failed to serialize state for swaps storage`), { error }); - throw error; + let lightColor = color; + let darkColor = color; + + const lightModeContrast = c.contrast(lightColor, globalColors.white100); + const darkModeContrast = c.contrast(darkColor, globalColors.grey100); + + if (lightModeContrast < 2.5) { + lightColor = c(lightColor) + .set('hsl.s', `*${lightModeContrast < 1.5 ? 2 : 1.2}`) + .darken(2.5 - (lightModeContrast - (lightModeContrast < 1.5 ? 0.5 : 0))) + .hex(); } -} -function deserialize(serializedState: string) { - let parsedState: { state: StateWithTransforms; version: number }; - try { - parsedState = JSON.parse(serializedState); - } catch (error) { - logger.error(new RainbowError(`[swapsStore]: Failed to parse serialized state from swaps storage`), { error }); - throw error; + if (darkModeContrast < 3) { + darkColor = c(darkColor) + .set('hsl.l', darkModeContrast < 1.5 ? 0.88 : 0.8) + .set('hsl.s', `*${darkModeContrast < 1.5 ? 0.75 : 0.85}`) + .hex(); } - const { state, version } = parsedState; + return { + light: lightColor, + dark: darkColor, + }; +}; + +export const getMixedColor = (color1: string, color2: string, ratio: number) => { + return c.mix(color1, color2, ratio).hex(); +}; - let recentSwaps = new Map(); - try { - if (state.recentSwaps) { - recentSwaps = new Map(state.recentSwaps); - } - } catch (error) { - logger.error(new RainbowError(`[swapsStore]: Failed to convert recentSwaps from swaps storage`), { error }); +export const getTextColor = (colors: ResponseByTheme): ResponseByTheme => { + const lightContrast = c.contrast(colors.light, globalColors.white100); + const darkContrast = c.contrast(colors.dark === ETH_COLOR ? ETH_COLOR_DARK : colors.dark, globalColors.white100); + + return { + light: lightContrast < 2 ? globalColors.grey100 : globalColors.white100, + dark: darkContrast < 2.6 ? globalColors.grey100 : globalColors.white100, + }; +}; + +export const getMixedShadowColor = (color: string): ResponseByTheme => { + return { + light: getMixedColor(color, colors.dark, 0.84), + dark: globalColors.grey100, + }; +}; + +export const getTintedBackgroundColor = (colors: ResponseByTheme): ResponseByTheme => { + const lightModeColorToMix = globalColors.white100; + const darkModeColorToMix = globalColors.grey100; + + return { + light: c.mix(colors.light, lightModeColorToMix, 0.94).saturate(-0.06).hex(), + dark: c.mix(colors.dark === ETH_COLOR ? ETH_COLOR_DARK : colors.dark, darkModeColorToMix, 0.9875).hex(), + }; +}; + +// +// /---- END color functions ----/ // + +// /---- 🟢 JS utils 🟢 ----/ // +// +export const clampJS = (value: number, lowerBound: number, upperBound: number) => { + return Math.min(Math.max(lowerBound, value), upperBound); +}; + +export const countDecimalPlaces = (number: number | string): number => { + 'worklet'; + + const numAsString = typeof number === 'string' ? number : number.toString(); + + if (numAsString.includes('.')) { + // Return the number of digits after the decimal point, excluding trailing zeros + return numAsString.split('.')[1].replace(/0+$/, '').length; + } + + // If no decimal point + return 0; +}; + +export const findNiceIncrement = (availableBalance: string | number | undefined) => { + 'worklet'; + if (Number(availableBalance) === 0) { + return 0; + } + + if (!availableBalance || !isNumberStringWorklet(availableBalance.toString()) || equalWorklet(availableBalance, 0)) { + return 0; } - let latestSwapAt: Map = new Map(); - try { - if (state.latestSwapAt) { - latestSwapAt = new Map(state.latestSwapAt); + // We'll use one of these factors to adjust the base increment + // These factors are chosen to: + // a) Produce user-friendly amounts to swap (e.g., 0.1, 0.2, 0.3, 0.4…) + // b) Limit shifts in the number of decimal places between increments + const niceFactors = [1, 2, 10]; + + // Calculate the exact increment for 100 steps + const exactIncrement = divWorklet(availableBalance, 100); + + // Calculate the order of magnitude of the exact increment + const orderOfMagnitude = orderOfMagnitudeWorklet(exactIncrement); + + const baseIncrement = powWorklet(10, orderOfMagnitude); + + let adjustedIncrement = baseIncrement; + + // Find the first nice increment that ensures at least 100 steps + for (let i = niceFactors.length - 1; i >= 0; i--) { + const potentialIncrement = mulWorklet(baseIncrement, niceFactors[i]); + if (lessThanOrEqualToWorklet(potentialIncrement, exactIncrement)) { + adjustedIncrement = potentialIncrement; + break; } - } catch (error) { - logger.error(new RainbowError(`[swapsStore]: Failed to convert latestSwapAt from swaps storage`), { error }); } + return adjustedIncrement; +}; +// +// /---- END JS utils ----/ // + +// /---- 🔵 Worklet utils 🔵 ----/ // +// +type nativeCurrencyType = typeof supportedNativeCurrencies; + +export function addCommasToNumber(number: string | number, fallbackValue: T = 0 as T): T | string { + 'worklet'; + if (isNaN(Number(number))) { + return fallbackValue; + } + const numberString = number.toString(); + + if (numberString.includes(',')) { + return numberString; + } + + if (greaterThanOrEqualToWorklet(number, 1000)) { + const parts = numberString.split('.'); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); + return parts.join('.'); + } else { + return numberString; + } +} + +export const addSymbolToNativeDisplayWorklet = (value: number | string, nativeCurrency: keyof nativeCurrencyType): string => { + 'worklet'; + + const nativeSelected = supportedNativeCurrencies?.[nativeCurrency]; + const { symbol } = nativeSelected; + + const nativeValueWithCommas = addCommasToNumber(value, '0'); + + return `${symbol}${nativeValueWithCommas}`; +}; + +export function clamp(value: number, lowerBound: number, upperBound: number) { + 'worklet'; + return Math.min(Math.max(lowerBound, value), upperBound); +} + +export function stripNonDecimalNumbers(value: string) { + 'worklet'; + return value.replace(/[^0-9.]/g, ''); +} + +export function trimTrailingZeros(value: string) { + 'worklet'; + const withTrimmedZeros = value.replace(/0+$/, ''); + return withTrimmedZeros.endsWith('.') ? withTrimmedZeros.slice(0, -1) : withTrimmedZeros; +} + +export function niceIncrementFormatter({ + inputAssetBalance, + inputAssetNativePrice, + percentageToSwap, + sliderXPosition, + stripSeparators, + isStablecoin = false, +}: { + inputAssetBalance: number | string; + inputAssetNativePrice: number; + percentageToSwap: number; + sliderXPosition: number; + stripSeparators?: boolean; + isStablecoin?: boolean; +}) { + 'worklet'; + const niceIncrement = findNiceIncrement(inputAssetBalance); + const incrementDecimalPlaces = countDecimalPlaces(niceIncrement); + + if (percentageToSwap === 0 || equalWorklet(niceIncrement, 0)) return 0; + if (percentageToSwap === 0.25) { + const amount = mulWorklet(inputAssetBalance, 0.25); + return valueBasedDecimalFormatter({ + nativePrice: inputAssetNativePrice, + niceIncrementMinimumDecimals: incrementDecimalPlaces, + amount, + roundingMode: 'up', + isStablecoin, + }); + } + if (percentageToSwap === 0.5) { + const amount = mulWorklet(inputAssetBalance, 0.5); + return valueBasedDecimalFormatter({ + nativePrice: inputAssetNativePrice, + niceIncrementMinimumDecimals: incrementDecimalPlaces, + amount, + roundingMode: 'up', + isStablecoin, + }); + } + if (percentageToSwap === 0.75) { + const amount = mulWorklet(inputAssetBalance, 0.75); + return valueBasedDecimalFormatter({ + nativePrice: inputAssetNativePrice, + niceIncrementMinimumDecimals: incrementDecimalPlaces, + amount, + roundingMode: 'up', + isStablecoin, + }); + } + if (percentageToSwap === 1) { + return inputAssetBalance; + } + + const decimals = isStablecoin ? STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS : incrementDecimalPlaces; + const exactIncrement = divWorklet(inputAssetBalance, 100); + const isIncrementExact = equalWorklet(niceIncrement, exactIncrement); + const numberOfIncrements = divWorklet(inputAssetBalance, niceIncrement); + const incrementStep = divWorklet(1, numberOfIncrements); + const percentage = isIncrementExact + ? percentageToSwap + : divWorklet( + roundWorklet( + mulWorklet(clamp((sliderXPosition - SCRUBBER_WIDTH / SLIDER_WIDTH) / SLIDER_WIDTH, 0, 1), divWorklet(1, incrementStep)) + ), + divWorklet(1, incrementStep) + ); + + const rawAmount = mulWorklet(roundWorklet(divWorklet(mulWorklet(percentage, inputAssetBalance), niceIncrement)), niceIncrement); + + const amountToFixedDecimals = toFixedWorklet(rawAmount, decimals); + + const numberFormatter = new Intl.NumberFormat('en-US', { + minimumFractionDigits: 0, + maximumFractionDigits: MAXIMUM_SIGNIFICANT_DECIMALS, + useGrouping: !stripSeparators, + }); + + return numberFormatter.format(Number(amountToFixedDecimals)); +} + +export const opacityWorklet = (color: string, opacity: number) => { + 'worklet'; + + if (isColor(color)) { + const rgbaColor = convertToRGBA(color); + return `rgba(${rgbaColor[0] * 255}, ${rgbaColor[1] * 255}, ${rgbaColor[2] * 255}, ${opacity})`; + } else { + return color; + } +}; +// +// /---- END worklet utils ----/ // + +export const slippageInBipsToString = (slippageInBips: number) => (slippageInBips / 100).toFixed(1); + +export const slippageInBipsToStringWorklet = (slippageInBips: number) => { + 'worklet'; + return (slippageInBips / 100).toFixed(1); +}; + +export const getDefaultSlippage = (chainId: ChainId, config: RainbowConfig) => { + const amount = +( + (config.default_slippage_bips_chainId as unknown as { [key: number]: number })[chainId] || + DEFAULT_CONFIG.default_slippage_bips_chainId[chainId] || + 200 + ); + + return slippageInBipsToString(amount); +}; + +export const getDefaultSlippageWorklet = (chainId: ChainId, config: RainbowConfig) => { + 'worklet'; + const amount = +( + (config.default_slippage_bips_chainId as unknown as { [key: number]: number })[chainId] || + DEFAULT_CONFIG.default_slippage_bips_chainId[chainId] || + 200 + ); + + return slippageInBipsToStringWorklet(amount); +}; + +export type Colors = { + primary?: string; + fallback?: string; + shadow?: string; +}; + +type ExtractColorValueForColorsProps = { + colors: TokenColors; +}; + +export const extractColorValueForColors = ({ + colors, +}: ExtractColorValueForColorsProps): Omit => { + const color = colors.primary || colors.fallback; + const darkColor = color || ETH_COLOR_DARK; + const lightColor = color || ETH_COLOR; + + const highContrastColor = getHighContrastColor(lightColor); return { - state: { - ...state, - latestSwapAt, - recentSwaps, + color: { + light: lightColor, + dark: darkColor, + }, + shadowColor: { + light: lightColor, + dark: darkColor, }, - version, + mixedShadowColor: getMixedShadowColor(lightColor), + highContrastColor: highContrastColor, + tintedBackgroundColor: getTintedBackgroundColor(highContrastColor), + textColor: getTextColor(highContrastColor), + nativePrice: undefined, }; -} +}; -export const swapsStore = createRainbowStore( - (set, get) => ({ - isSwapsOpen: false, - setIsSwapsOpen: (isSwapsOpen: boolean) => set({ isSwapsOpen }), +export const getColorWorklet = (color: ForegroundColor, isDarkMode: boolean) => { + 'worklet'; + return palettes[isDarkMode ? 'dark' : 'light'].foregroundColors[color]; +}; - inputAsset: null, - outputAsset: null, +export const getChainColorWorklet = (chainId: ChainId, isDarkMode: boolean): string => { + 'worklet'; + switch (chainId) { + case ChainId.mainnet: + return getColorWorklet('mainnet', isDarkMode); + case ChainId.arbitrum: + return getColorWorklet('arbitrum', isDarkMode); + case ChainId.optimism: + return getColorWorklet('optimism', isDarkMode); + case ChainId.polygon: + return getColorWorklet('polygon', isDarkMode); + case ChainId.base: + return getColorWorklet('base', isDarkMode); + case ChainId.zora: + return getColorWorklet('zora', isDarkMode); + case ChainId.bsc: + return getColorWorklet('bsc', isDarkMode); + case ChainId.avalanche: + return getColorWorklet('avalanche', isDarkMode); + case ChainId.blast: + return getColorWorklet('blast', isDarkMode); + case ChainId.degen: + return getColorWorklet('degen', isDarkMode); + default: + return getColorWorklet('mainnet', isDarkMode); + } +}; + +export const getQuoteServiceTimeWorklet = ({ quote }: { quote: Quote | CrosschainQuote }) => { + 'worklet'; + return (quote as CrosschainQuote)?.routes?.[0]?.serviceTime || 0; +}; + +const I18N_TIME = { + singular: { + hours_long: i18n.t(i18n.l.time.hours.long.singular), + minutes_short: i18n.t(i18n.l.time.minutes.short.singular), + seconds_short: i18n.t(i18n.l.time.seconds.short.singular), + }, + plural: { + hours_long: i18n.t(i18n.l.time.hours.long.plural), + minutes_short: i18n.t(i18n.l.time.minutes.short.plural), + seconds_short: i18n.t(i18n.l.time.seconds.short.plural), + }, +}; - quote: null, +export const getCrossChainTimeEstimateWorklet = ({ + serviceTime, +}: { + serviceTime?: number; +}): { + isLongWait: boolean; + timeEstimate?: number; + timeEstimateDisplay: string; +} => { + 'worklet'; - selectedOutputChainId: ChainId.mainnet, + let isLongWait = false; + let timeEstimateDisplay; + const timeEstimate = serviceTime; - percentageToSell: INITIAL_SLIDER_POSITION, - setPercentageToSell: (percentageToSell: number) => set({ percentageToSell }), - slippage: getDefaultSlippage(ChainId.mainnet, getRemoteConfig()), - setSlippage: (slippage: string) => set({ slippage }), - source: 'auto', - setSource: (source: Source | 'auto') => set({ source }), + const minutes = Math.floor((timeEstimate || 0) / 60); + const hours = Math.floor(minutes / 60); - degenMode: false, - setDegenMode: (degenMode: boolean) => set({ degenMode }), - preferredNetwork: undefined, - setPreferredNetwork: (preferredNetwork: ChainId | undefined) => set({ preferredNetwork }), + if (hours >= 1) { + isLongWait = true; + timeEstimateDisplay = `>${hours} ${hours === 1 ? I18N_TIME.singular.hours_long : I18N_TIME.plural.hours_long}`; + } else if (minutes >= 1) { + timeEstimateDisplay = `~${minutes} ${minutes === 1 ? I18N_TIME.singular.minutes_short : I18N_TIME.plural.minutes_short}`; + } else { + timeEstimateDisplay = `~${timeEstimate} ${timeEstimate === 1 ? I18N_TIME.singular.seconds_short : I18N_TIME.plural.seconds_short}`; + } - latestSwapAt: new Map(), - recentSwaps: new Map(), - getRecentSwapsByChain: (chainId?: ChainId) => get().recentSwaps.get(chainId ?? get().selectedOutputChainId) || [], + return { + isLongWait, + timeEstimate, + timeEstimateDisplay, + }; +}; - addRecentSwap(asset) { - const { recentSwaps, latestSwapAt } = get(); - const now = Date.now(); - const chainId = asset.chainId; - const chainSwaps = recentSwaps.get(chainId) || []; +export const priceForAsset = ({ + asset, + assetType, + assetToSellPrice, + assetToBuyPrice, +}: { + asset: ParsedSearchAsset | null; + assetType: 'assetToSell' | 'assetToBuy'; + assetToSellPrice: SharedValue; + assetToBuyPrice: SharedValue; +}) => { + 'worklet'; - // Remove any existing entries of the same asset - const filteredSwaps = chainSwaps.filter(swap => swap.uniqueId !== asset.uniqueId); + if (!asset) return 0; - const updatedSwaps = [{ ...asset, swappedAt: now }, ...filteredSwaps].slice(0, 3); - recentSwaps.set(chainId, updatedSwaps); - latestSwapAt.set(chainId, now); + if (assetType === 'assetToSell' && assetToSellPrice.value) { + return assetToSellPrice.value; + } else if (assetType === 'assetToBuy' && assetToBuyPrice.value) { + return assetToBuyPrice.value; + } else if (asset.price?.value) { + return asset.price.value; + } else if (asset.native.price?.amount) { + return asset.native.price.amount; + } + return 0; +}; - set({ - recentSwaps: new Map(recentSwaps), - latestSwapAt: new Map(latestSwapAt), - }); - }, +type ParseAssetAndExtendProps = { + asset: ParsedSearchAsset | null; + insertUserAssetBalance?: boolean; +}; - lastNavigatedTrendingToken: undefined, - }), - { - storageKey: 'swapsStore', - version: 2, - deserializer: deserialize, - serializer: serialize, - partialize(state) { - return { - degenMode: state.degenMode, - preferredNetwork: state.preferredNetwork, - selectedOutputChainId: state.selectedOutputChainId, - source: state.source, - latestSwapAt: state.latestSwapAt, - recentSwaps: state.recentSwaps, - }; - }, +const ETH_COLORS: Colors = { + primary: undefined, + fallback: undefined, + shadow: undefined, +}; + +export const parseAssetAndExtend = ({ + asset, + insertUserAssetBalance, +}: ParseAssetAndExtendProps): ExtendedAnimatedAssetWithColors | null => { + if (!asset) { + return null; + } + + const isAssetEth = asset.isNativeAsset && asset.symbol === 'ETH'; + const colors = extractColorValueForColors({ + colors: (isAssetEth ? ETH_COLORS : asset.colors) as TokenColors, + }); + + const uniqueId = getUniqueId(asset.address, asset.chainId); + const balance = insertUserAssetBalance ? userAssetsStore.getState().getUserAsset(uniqueId)?.balance || asset.balance : asset.balance; + + return { + ...asset, + ...colors, + maxSwappableAmount: balance.amount, + nativePrice: asset.price?.value, + balance, + + // For some reason certain assets have a unique ID in the format of `${address}_mainnet` rather than + // `${address}_${chainId}`, so at least for now we ensure consistency by reconstructing the unique ID here. + uniqueId, + }; +}; + +type BuildQuoteParamsProps = { + currentAddress: string; + inputAmount: BigNumberish; + outputAmount: BigNumberish; + inputAsset: ExtendedAnimatedAssetWithColors | null; + outputAsset: ExtendedAnimatedAssetWithColors | null; + lastTypedInput: inputKeys; +}; + +/** + * Builds the quote params for the swap based on the current state of the store. + * + * NOTE: Will return null if either asset isn't set. + * @returns data needed to execute a swap or cross-chain swap + */ +export const buildQuoteParams = ({ + currentAddress, + inputAmount, + outputAmount, + inputAsset, + outputAsset, + lastTypedInput, +}: BuildQuoteParamsProps): QuoteParams | null => { + const { source, slippage } = swapsStore.getState(); + if (!inputAsset || !outputAsset) { + return null; } -); -export const useSwapsStore = swapsStore; + const isCrosschainSwap = inputAsset.chainId !== outputAsset.chainId; + + const quoteParams: QuoteParams = { + source: source === 'auto' ? undefined : source, + chainId: inputAsset.chainId, + fromAddress: currentAddress, + sellTokenAddress: inputAsset.isNativeAsset ? ETH_ADDRESS_AGGREGATOR : inputAsset.address, + buyTokenAddress: outputAsset.isNativeAsset ? ETH_ADDRESS_AGGREGATOR : outputAsset.address, + sellAmount: + lastTypedInput === 'inputAmount' || lastTypedInput === 'inputNativeValue' + ? convertAmountToRawAmount(inputAmount.toString(), inputAsset.decimals) + : undefined, + buyAmount: + lastTypedInput === 'outputAmount' || lastTypedInput === 'outputNativeValue' + ? convertAmountToRawAmount(outputAmount.toString(), outputAsset.decimals) + : undefined, + slippage: Number(slippage), + refuel: false, + toChainId: isCrosschainSwap ? outputAsset.chainId : inputAsset.chainId, + currency: store.getState().settings.nativeCurrency, + }; + + // Do not delete the comment below 😤 + // @ts-ignore About to get quote + + return quoteParams; +};